diff --git a/damus/Assets.xcassets/tor.imageset/Contents.json b/damus/Assets.xcassets/tor.imageset/Contents.json new file mode 100644 index 00000000..62062a1b --- /dev/null +++ b/damus/Assets.xcassets/tor.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tor.svg.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Assets.xcassets/tor.imageset/tor.svg.png b/damus/Assets.xcassets/tor.imageset/tor.svg.png new file mode 100644 index 00000000..0f4afdb1 Binary files /dev/null and b/damus/Assets.xcassets/tor.imageset/tor.svg.png differ diff --git a/damus/Models/PushNotificationClient.swift b/damus/Models/PushNotificationClient.swift index 13e75758..70775783 100644 --- a/damus/Models/PushNotificationClient.swift +++ b/damus/Models/PushNotificationClient.swift @@ -11,6 +11,10 @@ struct PushNotificationClient { let keypair: Keypair let settings: UserSettingsStore private(set) var device_token: Data? = nil + var device_token_hex: String? { + guard let device_token else { return nil } + return device_token.map { String(format: "%02.2hhx", $0) }.joined() + } mutating func set_device_token(new_device_token: Data) async throws { self.device_token = new_device_token @@ -20,26 +24,21 @@ struct PushNotificationClient { } func send_token() async throws { - guard let device_token else { return } // Send the device token and pubkey to the server - let token = device_token.map { String(format: "%02.2hhx", $0) }.joined() + guard let token = device_token_hex else { return } Log.info("Sending device token to server: %s", for: .push_notifications, token) - let pubkey = self.keypair.pubkey - - // Send those as JSON to the server - let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()] - // create post request - let url = self.settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_RECEIVER_TEST_URL : Constants.DEVICE_TOKEN_RECEIVER_PRODUCTION_URL - let json_data = try JSONSerialization.data(withJSONObject: json) - + let url = self.current_push_notification_environment().api_base_url() + .appendingPathComponent("user-info") + .appendingPathComponent(self.keypair.pubkey.hex()) + .appendingPathComponent(token) let (data, response) = try await make_nip98_authenticated_request( - method: .post, + method: .put, url: url, - payload: json_data, + payload: nil, payload_type: .json, auth_keypair: self.keypair ) @@ -58,26 +57,23 @@ struct PushNotificationClient { } func revoke_token() async throws { - guard let device_token else { return } - // Send the device token and pubkey to the server - let token = device_token.map { String(format: "%02.2hhx", $0) }.joined() + guard let token = device_token_hex else { return } Log.info("Revoking device token from server: %s", for: .push_notifications, token) let pubkey = self.keypair.pubkey - // Send those as JSON to the server - let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()] - // create post request - let url = self.settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_REVOKER_TEST_URL : Constants.DEVICE_TOKEN_REVOKER_PRODUCTION_URL - let json_data = try JSONSerialization.data(withJSONObject: json) + let url = self.current_push_notification_environment().api_base_url() + .appendingPathComponent("user-info") + .appendingPathComponent(pubkey.hex()) + .appendingPathComponent(token) let (data, response) = try await make_nip98_authenticated_request( - method: .post, + method: .delete, url: url, - payload: json_data, + payload: nil, payload_type: .json, auth_keypair: self.keypair ) @@ -94,6 +90,78 @@ struct PushNotificationClient { return } + + func set_settings(_ new_settings: NotificationSettings? = nil) async throws { + // Send the device token and pubkey to the server + guard let token = device_token_hex else { return } + + Log.info("Sending notification preferences to the server", for: .push_notifications) + + let url = self.current_push_notification_environment().api_base_url() + .appendingPathComponent("user-info") + .appendingPathComponent(self.keypair.pubkey.hex()) + .appendingPathComponent(token) + .appendingPathComponent("preferences") + + let json_payload = try JSONEncoder().encode(new_settings ?? NotificationSettings.from(settings: settings)) + + let (data, response) = try await make_nip98_authenticated_request( + method: .put, + url: url, + payload: json_payload, + payload_type: .json, + auth_keypair: self.keypair + ) + + if let httpResponse = response as? HTTPURLResponse { + switch httpResponse.statusCode { + case 200: + Log.info("Sent notification settings to Damus push notification server successfully", for: .push_notifications) + default: + Log.error("Error in sending notification settings to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown") + throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data) + } + } + + return + } + + func get_settings() async throws -> NotificationSettings { + // Send the device token and pubkey to the server + guard let token = device_token_hex else { + throw ClientError.no_device_token + } + + let url = self.current_push_notification_environment().api_base_url() + .appendingPathComponent("user-info") + .appendingPathComponent(self.keypair.pubkey.hex()) + .appendingPathComponent(token) + .appendingPathComponent("preferences") + + let (data, response) = try await make_nip98_authenticated_request( + method: .get, + url: url, + payload: nil, + payload_type: .json, + auth_keypair: self.keypair + ) + + if let httpResponse = response as? HTTPURLResponse { + switch httpResponse.statusCode { + case 200: + guard let notification_settings = NotificationSettings.from(json_data: data) else { throw ClientError.json_decoding_error } + return notification_settings + default: + Log.error("Error in getting notification settings to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown") + throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data) + } + } + throw ClientError.could_not_process_response + } + + func current_push_notification_environment() -> Environment { + return self.settings.send_device_token_to_localhost ? .local_test(host: nil) : .production + } } // MARK: Helper structures @@ -101,5 +169,111 @@ struct PushNotificationClient { extension PushNotificationClient { enum ClientError: Error { case http_response_error(status_code: Int, response: Data) + case could_not_process_response + case no_device_token + case json_decoding_error + } + + struct NotificationSettings: Codable, Equatable { + let zap_notifications_enabled: Bool + let mention_notifications_enabled: Bool + let repost_notifications_enabled: Bool + let reaction_notifications_enabled: Bool + let dm_notifications_enabled: Bool + let only_notifications_from_following_enabled: Bool + + static func from(json_data: Data) -> Self? { + guard let decoded = try? JSONDecoder().decode(Self.self, from: json_data) else { return nil } + return decoded + } + + static func from(settings: UserSettingsStore) -> Self { + return NotificationSettings( + zap_notifications_enabled: settings.zap_notification, + mention_notifications_enabled: settings.mention_notification, + repost_notifications_enabled: settings.repost_notification, + reaction_notifications_enabled: settings.like_notification, + dm_notifications_enabled: settings.dm_notification, + only_notifications_from_following_enabled: settings.notification_only_from_following + ) + } + + } + + enum Environment: CaseIterable, Codable, Identifiable, StringCodable, Equatable, Hashable { + static var allCases: [Environment] = [.local_test(host: nil), .production] + + case local_test(host: String?) + case production + + func text_description() -> String { + switch self { + case .local_test: + return NSLocalizedString("Test (local)", comment: "Label indicating a local test environment for Push notification functionality (Developer feature)") + case .production: + return NSLocalizedString("Production", comment: "Label indicating the production environment for Push notification functionality") + } + } + + func api_base_url() -> URL { + switch self { + case .local_test(let host): + URL(string: "http://\(host ?? "localhost:8000")") ?? Constants.PUSH_NOTIFICATION_SERVER_TEST_BASE_URL + case .production: + Constants.PURPLE_API_PRODUCTION_BASE_URL + + } + } + + func custom_host() -> String? { + switch self { + case .local_test(let host): + return host + default: + return nil + } + } + + init?(from string: String) { + switch string { + case "local_test": + self = .local_test(host: nil) + case "production": + self = .production + default: + let components = string.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) + if components.count == 2 && components[0] == "local_test" { + self = .local_test(host: String(components[1])) + } else { + return nil + } + } + } + + func to_string() -> String { + switch self { + case .local_test(let host): + if let host { + return "local_test:\(host)" + } + return "local_test" + case .production: + return "production" + } + } + + var id: String { + switch self { + case .local_test(let host): + if let host { + return "local_test:\(host)" + } + else { + return "local_test" + } + case .production: + return "production" + } + } } } diff --git a/damus/Util/Constants.swift b/damus/Util/Constants.swift index f3cbf43a..279b92d7 100644 --- a/damus/Util/Constants.swift +++ b/damus/Util/Constants.swift @@ -10,13 +10,13 @@ import Foundation class Constants { //static let EXAMPLE_DEMOS: DamusState = .empty static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus" - static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "http://45.33.32.5:8000/user-info")! - static let DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")! - static let DEVICE_TOKEN_REVOKER_PRODUCTION_URL: URL = URL(string: "http://45.33.32.5:8000/user-info/remove")! - static let DEVICE_TOKEN_REVOKER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info/remove")! static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2" static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService" + // MARK: Push notification server + static let PUSH_NOTIFICATION_SERVER_PRODUCTION_BASE_URL: URL = URL(string: "http://45.33.32.5:8000")! + static let PUSH_NOTIFICATION_SERVER_TEST_BASE_URL: URL = URL(string: "http://localhost:8000")! + // MARK: Purple // API static let PURPLE_API_LOCAL_TEST_BASE_URL: URL = URL(string: "http://localhost:8989")! diff --git a/damus/Views/Chat/ChatBubbleView.swift b/damus/Views/Chat/ChatBubbleView.swift index bd79a746..6fe6fbd8 100644 --- a/damus/Views/Chat/ChatBubbleView.swift +++ b/damus/Views/Chat/ChatBubbleView.swift @@ -165,7 +165,7 @@ struct ChatBubble: View { stroke_style: .init(lineWidth: 4), background_style: Color.accentColor ) { - Text("Hello there") + Text(verbatim: "Hello there") .padding() } .foregroundColor(.white) @@ -176,7 +176,7 @@ struct ChatBubble: View { stroke_style: .init(lineWidth: 4), background_style: Color.accentColor ) { - Text("Hello there") + Text(verbatim: "Hello there") .padding() } .foregroundColor(.white) diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index 235720e0..a17df5f0 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -217,16 +217,6 @@ struct ProfileView: View { } } - var customNavbar: some View { - HStack { - navBackButton - Spacer() - navActionSheetButton - } - .padding(.top, 5) - .accentColor(DamusColors.white) - } - func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View { return ProfileZapLinkView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in Image(reactions_enabled ? "zap.fill" : "zap") @@ -458,8 +448,15 @@ struct ProfileView: View { .navigationTitle("") .navigationBarBackButtonHidden() .toolbar { - ToolbarItem(placement: .principal) { - customNavbar + ToolbarItem(placement: .topBarLeading) { + navBackButton + .padding(.top, 5) + .accentColor(DamusColors.white) + } + ToolbarItem(placement: .topBarTrailing) { + navActionSheetButton + .padding(.top, 5) + .accentColor(DamusColors.white) } } .toolbarBackground(.hidden) diff --git a/damus/Views/Relays/RelayView.swift b/damus/Views/Relays/RelayView.swift index a50a35b5..428e8b73 100644 --- a/damus/Views/Relays/RelayView.swift +++ b/damus/Views/Relays/RelayView.swift @@ -50,6 +50,13 @@ struct RelayView: View { .padding(.bottom, 2) .lineLimit(1) RelayType(is_paid: state.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false) + + if relay.absoluteString.hasSuffix(".onion") { + Image("tor") + .resizable() + .interpolation(.none) + .frame(width: 20, height: 20) + } } Text(relay.absoluteString) .font(.subheadline) diff --git a/damus/Views/Settings/NotificationSettingsView.swift b/damus/Views/Settings/NotificationSettingsView.swift index f2711156..4ce30ef4 100644 --- a/damus/Views/Settings/NotificationSettingsView.swift +++ b/damus/Views/Settings/NotificationSettingsView.swift @@ -7,10 +7,13 @@ import SwiftUI +let MINIMUM_PUSH_NOTIFICATION_SYNC_DELAY_IN_SECONDS = 0.25 + struct NotificationSettingsView: View { let damus_state: DamusState @ObservedObject var settings: UserSettingsStore @State var notification_mode_setting_error: String? = nil + @State var notification_preferences_sync_state: PreferencesSyncState = .undefined @Environment(\.dismiss) var dismiss @@ -32,6 +35,7 @@ struct NotificationSettingsView: View { Task { do { try await damus_state.push_notification_client.send_token() + await self.sync_up_remote_notification_settings() settings.notifications_mode = new_value } catch { @@ -44,6 +48,7 @@ struct NotificationSettingsView: View { do { try await damus_state.push_notification_client.revoke_token() settings.notifications_mode = new_value + notification_preferences_sync_state = .not_applicable } catch { notification_mode_setting_error = String(format: NSLocalizedString("Error disabling push notifications with the server: %@", comment: "Error label shown when user tries to disable push notifications but something fails"), error.localizedDescription) @@ -52,6 +57,61 @@ struct NotificationSettingsView: View { } } + // MARK: - Push notification preference sync management + + func notification_preference_binding(_ raw_binding: Binding) -> Binding { + return Binding( + get: { + return raw_binding.wrappedValue + }, + set: { new_value in + let old_value = raw_binding.wrappedValue + raw_binding.wrappedValue = new_value + if self.settings.notifications_mode == .push { + Task { + await self.send_push_notification_preferences(on_failure: { + raw_binding.wrappedValue = old_value + }) + } + } + } + ) + } + + func sync_up_remote_notification_settings() async { + do { + notification_preferences_sync_state = .syncing + let remote_settings = try await damus_state.push_notification_client.get_settings() + let local_settings = PushNotificationClient.NotificationSettings.from(settings: settings) + if remote_settings != local_settings { + await self.send_push_notification_preferences(local_settings) + } + else { + notification_preferences_sync_state = .success + } + } + catch { + notification_preferences_sync_state = .failure(error: String(format: NSLocalizedString("Failed to get push notification preferences from the server", comment: "Error label indicating about a failure in fetching notification preferences"), error.localizedDescription)) + } + } + + func send_push_notification_preferences(_ new_settings: PushNotificationClient.NotificationSettings? = nil, on_failure: (() -> Void)? = nil) async { + do { + notification_preferences_sync_state = .syncing + try await damus_state.push_notification_client.set_settings(new_settings) + // Make sync appear to take at least a few milliseconds or so to avoid issues with labor perception bias (https://growth.design/case-studies/labor-perception-bias) + DispatchQueue.main.asyncAfter(deadline: .now() + MINIMUM_PUSH_NOTIFICATION_SYNC_DELAY_IN_SECONDS) { + notification_preferences_sync_state = .success + } + } + catch { + notification_preferences_sync_state = .failure(error: String(format: NSLocalizedString("Error syncing up push notifications preferences with the server: %@", comment: "Error label shown when system tries to sync up notification preferences to the push notification server but something fails"), error.localizedDescription)) + on_failure?() + } + } + + // MARK: - View layout + var body: some View { Form { if settings.enable_experimental_push_notifications { @@ -80,21 +140,40 @@ struct NotificationSettingsView: View { } } - Section(header: Text("Local Notifications", comment: "Section header for damus local notifications user configuration")) { - Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: $settings.zap_notification) + Section( + header: Text("Notification Preferences", comment: "Section header for Notification Preferences"), + footer: VStack { + switch notification_preferences_sync_state { + case .undefined, .not_applicable: + EmptyView() + case .success: + HStack { + Image("check-circle.fill") + .foregroundStyle(.damusGreen) + Text("Successfully synced", comment: "Label indicating success in syncing notification preferences") + } + case .syncing: + HStack(spacing: 10) { + ProgressView() + Text("Syncing", comment: "Label indicating success in syncing notification preferences") + } + case .failure(let error): + Text(error) + .foregroundStyle(.damusDangerPrimary) + } + } + ) { + Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: self.notification_preference_binding($settings.zap_notification)) .toggleStyle(.switch) - Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: $settings.mention_notification) + Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: self.notification_preference_binding($settings.mention_notification)) .toggleStyle(.switch) - Toggle(NSLocalizedString("Reposts", comment: "Setting to enable Repost Local Notification"), isOn: $settings.repost_notification) + Toggle(NSLocalizedString("Reposts", comment: "Setting to enable Repost Local Notification"), isOn: self.notification_preference_binding($settings.repost_notification)) .toggleStyle(.switch) - Toggle(NSLocalizedString("Likes", comment: "Setting to enable Like Local Notification"), isOn: $settings.like_notification) + Toggle(NSLocalizedString("Likes", comment: "Setting to enable Like Local Notification"), isOn: self.notification_preference_binding($settings.like_notification)) .toggleStyle(.switch) - Toggle(NSLocalizedString("DMs", comment: "Setting to enable DM Local Notification"), isOn: $settings.dm_notification) + Toggle(NSLocalizedString("DMs", comment: "Setting to enable DM Local Notification"), isOn: self.notification_preference_binding($settings.dm_notification)) .toggleStyle(.switch) - } - - Section(header: Text("Notification Preference", comment: "Section header for Notification Preferences")) { - Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: $settings.notification_only_from_following) + Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: self.notification_preference_binding($settings.notification_only_from_following)) .toggleStyle(.switch) } @@ -113,6 +192,28 @@ struct NotificationSettingsView: View { .onReceive(handle_notify(.switched_timeline)) { _ in dismiss() } + .onAppear(perform: { + Task { + if self.settings.notifications_mode == .push { + await self.sync_up_remote_notification_settings() + } + } + }) + } +} + +extension NotificationSettingsView { + enum PreferencesSyncState { + /// State is unknown + case undefined + /// State is not applicable (e.g. Notifications are set to local) + case not_applicable + /// Preferences are successfully synced + case success + /// Preferences are being synced + case syncing + /// There was a failure during syncing + case failure(error: String) } } diff --git a/damus/de.lproj/Localizable.strings b/damus/de.lproj/Localizable.strings index 12b6c466..6df686d9 100644 Binary files a/damus/de.lproj/Localizable.strings and b/damus/de.lproj/Localizable.strings differ diff --git a/damus/en-US.xcloc/Localized Contents/en-US.xliff b/damus/en-US.xcloc/Localized Contents/en-US.xliff index 855f8aee..ae5412ec 100644 --- a/damus/en-US.xcloc/Localized Contents/en-US.xliff +++ b/damus/en-US.xcloc/Localized Contents/en-US.xliff @@ -2,7 +2,7 @@
- +
@@ -44,7 +44,7 @@
- +
@@ -144,11 +144,6 @@ Sentence composed of 2 variables to describe how many people are following a use API Key (required) Prompt for required entry of API Key to use translation server. - - About - About - Label to prompt for about text entry for user to describe about themself. - About Me About Me @@ -159,6 +154,11 @@ Sentence composed of 2 variables to describe how many people are following a use Absolute Boss Placeholder text for About Me description. + + Absolute legend. + Absolute legend. + Example Bio + Accessibility Accessibility @@ -189,6 +189,11 @@ Sentence composed of 2 variables to describe how many people are following a use Add Bookmark Button text to add bookmark to a note. + + Add Photo + Add Photo + Label to indicate user can add a photo. + Add all Add all @@ -255,6 +260,11 @@ Button text to add a relay An additional percentage of each zap will be sent to support Damus development Text indicating that they can contribute zaps to support Damus development. + + An unexpected error happened while trying to create the new contact list. Please contact support. + An unexpected error happened while trying to create the new contact list. Please contact support. + Error message for a failed contact list reset operation + Animations Animations @@ -351,10 +361,10 @@ Tip: You can always change this later in Settings → Translations Be the first to access upcoming premium features: Automatic translations, longer note storage, and more Description of new features to be expected - - Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus. - Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus. - Reminder to user that they should save their account information. + + Bio + Bio + Label to prompt bio entry for user to describe themself. Bitcoin Lightning Tips @@ -383,10 +393,10 @@ Tip: You can always change this later in Settings → Translations Broadcast music playing on Apple Music Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status. - - By signing up, you agree to our - By signing up, you agree to our - Ask the user if they already have an account on Nostr + + By continuing you agree to our + By continuing you agree to our + No comment provided by engineer. By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/) @@ -413,7 +423,8 @@ Tip: You can always change this later in Settings → Translations Button to cancel the upload. Cancel deleting bookmarks. Cancel deleting the user. - Cancel out of logging out the user. + Cancel out of logging out the user. + Cancel resetting the contact list. Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed? @@ -486,6 +497,16 @@ Button to connect to the relay. Connecting Relay status label that indicates a relay is connecting. + + Contact list (Follows + Relay list) + Contact list (Follows + Relay list) + Section title for Contact list first aid tools + + + Contact list has been reset + Contact list has been reset + Message indicating that the contact list was successfully reset. + Content filters Content filters @@ -495,7 +516,8 @@ Button to connect to the relay. Continue Continue Continue with bookmarks. - Continue with deleting the user. + Continue with deleting the user. + Continue with resetting the contact list. Copied @@ -569,6 +591,11 @@ Button to connect to the relay. Copy user public key Context menu option for copying the ID of the user who created the note. + + Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help. + Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help. + Error message to the user indicating that the initial contact list failed to be created. + Could not find the user you're looking for Could not find the user you're looking for @@ -579,26 +606,21 @@ Button to connect to the relay. Could not find user to mute... Alert message to indicate that the muted user could not be found. + + Create Account + Create Account + Button to continue to the create account page. + Create account Create account Button to navigate to create account view. - - Create account now - Create account now - Button to create account. - Create new mutelist Create new mutelist Title of alert prompting the user to create a new mutelist. - - Creator(s) of Bitcoin. Absolute legend. - Creator(s) of Bitcoin. Absolute legend. - Example description about Bitcoin creator(s), Satoshi Nakamoto. - Custom Custom @@ -609,6 +631,7 @@ Button to connect to the relay. DMs Navigation title for DMs view, where DM is the English abbreviation for Direct Message. Navigation title for view of DMs, where DM is an English abbreviation for Direct Message. + Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message. Setting to enable DM Local Notification Toolbar label for DMs view, where DM is the English abbreviation for Direct Message. @@ -695,11 +718,6 @@ Button to disconnect from a relay server. Dismiss Button to dismiss alert - - Display name - Display name - Label to prompt display name entry. - Done Done @@ -766,6 +784,16 @@ Button to disconnect from a relay server. Label to display that authentication to a server has failed. Relay status label that indicates a relay had an error when connecting + + Error configuring push notifications with the server: %@ + Error configuring push notifications with the server: %@ + Error label shown when user tries to enable push notifications but something fails + + + Error disabling push notifications with the server: %@ + Error disabling push notifications with the server: %@ + Error label shown when user tries to disable push notifications but something fails + Error fetching lightning invoice Error fetching lightning invoice @@ -816,6 +844,12 @@ Relay status label that indicates a relay had an error when connecting Failed to parse NostrScript error message when it fails to parse a script. + + First Aid + First Aid + Navigation title for first aid settings and tools + Section header for first aid tools and settings + Follow Follow @@ -907,6 +941,11 @@ My side interests include languages and I am striving to be a #polyglot - I am a Free Dropdown option for selecting Free plan for DeepL translation service. + + General + General + Section header for general damus notifications user configuration + Get API Key Get API Key @@ -926,7 +965,8 @@ My side interests include languages and I am striving to be a #polyglot - I am a Hashtags Hashtags - Section header title for a list of hashtags that are muted. + Label for filter for seeing only hashtag follows. + Section header title for a list of hashtags that are muted. Hello everybody! @@ -971,6 +1011,16 @@ This is my first post on Damus, I am happy to meet you all 🤙. What’s up? Hide notes with #nsfw tags Setting to hide notes with the #nsfw (not safe for work) tags + + Highlighted + Highlighted + Label to indicate that the user is highlighting their own post. + + + Highlighted %@ + Highlighted %@ + Label to indicate that the user is highlighting 1 user. + Home Home @@ -1005,6 +1055,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!
Impersonation Description of report type for impersonation. + + In progress… + In progress… + Loading message indicating that a contact list reset operation is in progress. + Indefinite Indefinite @@ -1051,11 +1106,6 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!
LIVE Text indicator that the video is a livestream. - - Learn more about Nostr - Learn more about Nostr - Button that opens up a webpage where the user can learn more about Nostr. - Learn more about the features Learn more about the features @@ -1066,16 +1116,6 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!
Left Handed Moves the post button to the left side of the screen - - Let's get started! - Let's get started! - Button to continue to login page. - - - Let's go! - Let's go! - Button to complete account creation and start using the app. - LibreTranslate (Open Source) LibreTranslate (Open Source) @@ -1106,6 +1146,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!Load media Button to show media in note. + + Local + Local + Option for notification mode setting: Local notification mode + Local Notifications Local Notifications @@ -1175,7 +1220,8 @@ Hope to meet folks who are on their own journeys to a peaceful and free life! Mentions Mentions - Setting to enable Mention Local Notification + Label for filter for seeing mention notifications (replies, etc). + Setting to enable Mention Local Notification Merch @@ -1195,7 +1241,9 @@ Hope to meet folks who are on their own journeys to a peaceful and free life! Mute Mute - Alert button to mute a user. + Alert button to mute a user. + Button to mute a profile + Title for confirmation dialog to mute a profile. Mute %@? @@ -1235,6 +1283,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life! + + Name + Name + Label to prompt name entry. + Never Never @@ -1260,11 +1313,21 @@ Text label indicating that there is no NIP-11 relay software information found. New to Nostr? Ask the user if they are new to Nostr + + Next + Next + Button to continue with account creation. + No No User confirm No + + No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it + No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it + Section footer for Contact list first aid tools + No logs to display No logs to display @@ -1310,11 +1373,6 @@ Text label indicating that there is no NIP-11 relay software information found. Nostr Address Label for the Nostr Address section of user profile form. - - Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network - Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network - Description about what is Nostr. - NostrScript NostrScript @@ -1325,6 +1383,11 @@ Text label indicating that there is no NIP-11 relay software information found. NostrScript Error Text indicating that there was an error with loading NostrScript. There is a more descriptive error message shown separately underneath. + + Not now + Not now + Button to not save key, complete account creation, and start using the app. + Note from a %@ you've muted Note from a %@ you've muted @@ -1333,20 +1396,18 @@ Text label indicating that there is no NIP-11 relay software information found. Note you've muted Note you've muted - Text to indicate that what is being shown is a note which has been muted. + Text to indicate that what is being shown is a note which has been muted. +Label indicating note has been muted Notes Notes - Label for filter for seeing only notes (instead of notes and replies). -Label for filter for seeing only your notes (instead of notes and replies). -A label indicating that the notes being displayed below it are from a timeline, not search results + Label for filter for seeing only notes (instead of notes and replies). Notes & Replies Notes & Replies - Label for filter for seeing notes and replies (instead of only notes). -Label for filter for seeing your notes and replies (instead of only your notes). + Label for filter for seeing notes and replies (instead of only notes). Notes with the #nsfw tag usually contains adult content or other "Not safe for work" content @@ -1374,6 +1435,11 @@ Label for filter for seeing your notes and replies (instead of only your notes). Section header for Damus notifications Toolbar label for Notifications view. + + Notifications mode + Notifications mode + Prompt selection of the notification mode (Feature to switch between local notifications (generated from user's own phone) or push notifications (generated by Damus server). + Nudity Nudity @@ -1430,6 +1496,11 @@ Button label to dismiss an error dialog Optional Prompt to enter optional additional information when reporting an account or content. + + Orange-pill + Orange-pill + Button label that allows the user to start a direct message conversation with the user shown on-screen, to orange-pill them (i.e. help them to setup zaps) +