diff --git a/DamusNotificationService/NostrEventInfoFromPushNotification.swift b/DamusNotificationService/NostrEventInfoFromPushNotification.swift deleted file mode 100644 index d9f8fbde..00000000 --- a/DamusNotificationService/NostrEventInfoFromPushNotification.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// NostrEventInfoFromPushNotification.swift -// DamusNotificationService -// -// Created by Daniel D’Aquino on 2023-11-13. -// - -import Foundation - -/// The representation of a JSON-encoded Nostr Event used by the push notification server -/// Needs to match with https://gitlab.com/soapbox-pub/strfry-policies/-/raw/433459d8084d1f2d6500fdf916f22caa3b4d7be5/src/types.ts -struct NostrEventInfoFromPushNotification: Codable { - let id: String // Hex-encoded - let sig: String // Hex-encoded - let kind: NostrKind - let tags: [[String]] - let pubkey: String // Hex-encoded - let content: String - let created_at: Int - - static func from(dictionary: [AnyHashable: Any]) -> NostrEventInfoFromPushNotification? { - guard let id = dictionary["id"] as? String, - let sig = dictionary["sig"] as? String, - let kind_int = dictionary["kind"] as? UInt32, - let kind = NostrKind(rawValue: kind_int), - let tags = dictionary["tags"] as? [[String]], - let pubkey = dictionary["pubkey"] as? String, - let content = dictionary["content"] as? String, - let created_at = dictionary["created_at"] as? Int else { - return nil - } - return NostrEventInfoFromPushNotification(id: id, sig: sig, kind: kind, tags: tags, pubkey: pubkey, content: content, created_at: created_at) - } - - func reactionEmoji() -> String? { - guard self.kind == NostrKind.like else { - return nil - } - - switch self.content { - case "", "+": - return "❤️" - case "-": - return "👎" - default: - return self.content - } - } -} diff --git a/DamusNotificationService/NotificationFormatter.swift b/DamusNotificationService/NotificationFormatter.swift index 81ef226d..56d2ad62 100644 --- a/DamusNotificationService/NotificationFormatter.swift +++ b/DamusNotificationService/NotificationFormatter.swift @@ -11,16 +11,57 @@ import UserNotifications struct NotificationFormatter { static var shared = NotificationFormatter() - // TODO: These is a very generic notification formatter. Once we integrate NostrDB into the extension, we should reuse various functions present in `HomeModel.swift` - func formatMessage(event: NostrEventInfoFromPushNotification) -> UNNotificationContent? { + // MARK: - Formatting with NdbNote + + // TODO: Prepare a `LocalNotification` object from `NdbNote` to reuse Notification formatting code from Local notifications + func format_message(event: NdbNote, ndb: Ndb?) -> UNMutableNotificationContent? { + guard let txn = ndb?.lookup_profile(event.pubkey), + let display_name = txn.unsafeUnownedValue?.profile?.display_name + else { + return self.format_message(event: event) + } + + return self.format_message(event: event, display_name: display_name) + } + + func format_message(event: NdbNote, display_name: String) -> UNMutableNotificationContent? { + guard let best_attempt_content: UNMutableNotificationContent = self.format_message(event: event) else { return nil } + + switch event.known_kind { + case .text: + best_attempt_content.title = String(format: NSLocalizedString("%@ posted a note", comment: "Title label for push notification where a user posted a note"), display_name) + break + case .dm: + best_attempt_content.title = String(format: NSLocalizedString("New message from %@", comment: "Title label for push notifications where a direct message was sent to the user"), display_name) + break + case .like: + guard let reaction_emoji = to_reaction_emoji(ev: event) else { + best_attempt_content.title = String(format: NSLocalizedString("%@ reacted to your note", comment: "Reaction heading in local/push notification"), display_name) + best_attempt_content.body = "" + break + } + best_attempt_content.title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), display_name, reaction_emoji) + best_attempt_content.body = "" + break + case .zap: + best_attempt_content.title = String(format: NSLocalizedString("%@ zapped you ⚡️", comment: "Title label for a push notification where someone zapped the user"), display_name) + break + default: + return nil + } + + return best_attempt_content + } + + func format_message(event: NdbNote) -> UNMutableNotificationContent? { let content = UNMutableNotificationContent() if let event_json_data = try? JSONEncoder().encode(event), // Must be encoded, as the notification completion handler requires this object to conform to `NSSecureCoding` let event_json_string = String(data: event_json_data, encoding: .utf8) { content.userInfo = [ - "nostr_event_info": event_json_string + NDB_NOTE_JSON_USER_INFO_KEY: event_json_string ] } - switch event.kind { + switch event.known_kind { case .text: content.title = NSLocalizedString("Someone posted a note", comment: "Title label for push notification where someone posted a note") content.body = event.content @@ -30,7 +71,7 @@ struct NotificationFormatter { content.body = NSLocalizedString("(Contents are encrypted)", comment: "Label on push notification indicating that the contents of the message are encrypted") break case .like: - guard let reactionEmoji = event.reactionEmoji() else { + guard let reactionEmoji = to_reaction_emoji(ev: event) else { content.title = NSLocalizedString("Someone reacted to your note", comment: "Generic title label for push notifications where someone reacted to the user's post") break } @@ -45,4 +86,36 @@ struct NotificationFormatter { } return content } + + // MARK: - Formatting with LocalNotification + + func format_message(displayName: String, notify: LocalNotification) -> (content: UNMutableNotificationContent, identifier: String) { + let content = UNMutableNotificationContent() + var title = "" + var identifier = "" + + switch notify.type { + case .mention: + title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName) + identifier = "myMentionNotification" + case .repost: + title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName) + identifier = "myBoostNotification" + case .like: + title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, to_reaction_emoji(ev: notify.event) ?? "") + identifier = "myLikeNotification" + case .dm: + title = displayName + identifier = "myDMNotification" + case .zap, .profile_zap: + // not handled here + break + } + content.title = title + content.body = notify.content + content.sound = UNNotificationSound.default + content.userInfo = notify.to_lossy().to_user_info() + + return (content, identifier) + } } diff --git a/DamusNotificationService/NotificationService.swift b/DamusNotificationService/NotificationService.swift index 757b4294..2da9509e 100644 --- a/DamusNotificationService/NotificationService.swift +++ b/DamusNotificationService/NotificationService.swift @@ -19,19 +19,19 @@ class NotificationService: UNNotificationServiceExtension { let ndb: Ndb? = try? Ndb(owns_db_file: false) // Modify the notification content here... - guard let nostrEventInfoDictionary = request.content.userInfo["nostr_event"] as? [AnyHashable: Any], - let nostrEventInfo = NostrEventInfoFromPushNotification.from(dictionary: nostrEventInfoDictionary) else { + guard let nostrEventJSON = request.content.userInfo["nostr_event"] as? String, + let nostrEvent = NdbNote.owned_from_json(json: nostrEventJSON) + else { contentHandler(request.content) return; } // Log that we got a push notification - if let pubkey = Pubkey(hex: nostrEventInfo.pubkey), - let txn = ndb?.lookup_profile(pubkey) { - Log.debug("Got push notification from %s (%s)", for: .push_notifications, (txn.unsafeUnownedValue?.profile?.display_name ?? "Unknown"), nostrEventInfo.pubkey) + if let txn = ndb?.lookup_profile(nostrEvent.pubkey) { + Log.debug("Got push notification from %s (%s)", for: .push_notifications, (txn.unsafeUnownedValue?.profile?.display_name ?? "Unknown"), nostrEvent.pubkey.hex()) } - if let improvedContent = NotificationFormatter.shared.formatMessage(event: nostrEventInfo) { + if let improvedContent = NotificationFormatter.shared.format_message(event: nostrEvent, ndb: ndb) { contentHandler(improvedContent) } } diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 514a3620..4c602e91 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -438,7 +438,6 @@ BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; D70A3B172B02DCE5008BD568 /* NotificationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */; }; - D70A3B192B02DD2D008BD568 /* NostrEventInfoFromPushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B182B02DD2D008BD568 /* NostrEventInfoFromPushNotification.swift */; }; D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; }; D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; }; @@ -478,6 +477,8 @@ D7A343EE2AD0D77C00CED48B /* InlineSnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */; }; D7A343F02AD0D77C00CED48B /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343EF2AD0D77C00CED48B /* SnapshotTesting */; }; D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; }; + D7CB5D3B2B112FBB00AD4105 /* NotificationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */; }; + D7CB5D3C2B1130C600AD4105 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128B29EB19C40006FA5A /* LocalNotification.swift */; }; D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90548A2A6AEDEE00811EEC /* NdbNote.swift */; }; D7CCFC082B05834500323D86 /* NoteId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF42A740BB7007AEB17 /* NoteId.swift */; }; D7CCFC0B2B0585EA00323D86 /* nostrdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CE9FBB82A6B3B26007E485C /* nostrdb.c */; settings = {COMPILER_FLAGS = "-w"; }; }; @@ -541,7 +542,6 @@ D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B93D2A9AD44700DC3548 /* Message.swift */; }; D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; }; D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; }; - D7DBD4202B0307C7002A6197 /* NostrEventInfoFromPushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B182B02DD2D008BD568 /* NostrEventInfoFromPushNotification.swift */; }; D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; }; D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; }; D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */; }; @@ -1260,7 +1260,6 @@ BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = ""; }; D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFormatter.swift; sourceTree = ""; }; - D70A3B182B02DD2D008BD568 /* NostrEventInfoFromPushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventInfoFromPushNotification.swift; sourceTree = ""; }; D71DC1EB2A9129C3006E207C /* PostViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewTests.swift; sourceTree = ""; }; D723C38D2AB8D83400065664 /* ContentFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilters.swift; sourceTree = ""; }; D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = ""; }; @@ -2530,7 +2529,6 @@ D79C4C162AFEB061003A41B4 /* NotificationService.swift */, D79C4C182AFEB061003A41B4 /* Info.plist */, D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */, - D70A3B182B02DD2D008BD568 /* NostrEventInfoFromPushNotification.swift */, ); path = DamusNotificationService; sourceTree = ""; @@ -2946,7 +2944,6 @@ 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */, 4C1253502A76C5B20004F4B8 /* UnfollowedNotify.swift in Sources */, 4C86F7C62A76C51100EC0817 /* AttachedWalletNotify.swift in Sources */, - D7DBD4202B0307C7002A6197 /* NostrEventInfoFromPushNotification.swift in Sources */, 4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */, 4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */, 4C75EFB128049D510006080F /* NostrResponse.swift in Sources */, @@ -2970,6 +2967,7 @@ BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */, 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */, 4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */, + D7CB5D3B2B112FBB00AD4105 /* NotificationFormatter.swift in Sources */, 4C4E137B2A76D5FB00BDD832 /* MuteThreadNotify.swift in Sources */, 4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */, 4C12535A2A76C9960004F4B8 /* UnfollowNotify.swift in Sources */, @@ -3309,7 +3307,6 @@ D7CE1B292B0BE239002EDAD4 /* node_id.c in Sources */, D7CE1B2E2B0BE25C002EDAD4 /* talstr.c in Sources */, D798D2292B08686C00234419 /* ContentParsing.swift in Sources */, - D70A3B192B02DD2D008BD568 /* NostrEventInfoFromPushNotification.swift in Sources */, D798D2242B0859C900234419 /* LocalizationUtil.swift in Sources */, D7CE1B322B0BE6C3002EDAD4 /* NdbTxn.swift in Sources */, D7CE1B372B0BE719002EDAD4 /* Verifier.swift in Sources */, @@ -3360,6 +3357,7 @@ D798D2262B085C4200234419 /* Bech32.swift in Sources */, D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */, D7CE1B1E2B0BE190002EDAD4 /* midl.c in Sources */, + D7CB5D3C2B1130C600AD4105 /* LocalNotification.swift in Sources */, D7CE1B2D2B0BE250002EDAD4 /* take.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index e31b5089..e6a88637 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -1219,6 +1219,18 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) { return } + guard let local_notification = generate_local_notification_object(from: ev, damus_state: damus_state) else { + return + } + create_local_notification(profiles: damus_state.profiles, notify: local_notification) +} + +// TODO: Further break down this function and related functionality so that we can use this from the Notification service extension +func generate_local_notification_object(from ev: NostrEvent, damus_state: DamusState) -> LocalNotification? { + guard let type = ev.known_kind else { + return nil + } + if type == .text, damus_state.settings.mention_notification { let blocks = ev.blocks(damus_state.keypair).blocks for case .mention(let mention) in blocks { @@ -1226,56 +1238,30 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) { continue } let content_preview = render_notification_content_preview(cache: damus_state.events, ev: ev, profiles: damus_state.profiles, keypair: damus_state.keypair) - let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content_preview) - create_local_notification(profiles: damus_state.profiles, notify: notify ) + return LocalNotification(type: .mention, event: ev, target: ev, content: content_preview) } } else if type == .boost, damus_state.settings.repost_notification, let inner_ev = ev.get_inner_event(cache: damus_state.events) { let content_preview = render_notification_content_preview(cache: damus_state.events, ev: inner_ev, profiles: damus_state.profiles, keypair: damus_state.keypair) - let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview) - create_local_notification(profiles: damus_state.profiles, notify: notify) + return LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview) } else if type == .like, damus_state.settings.like_notification, let evid = ev.referenced_ids.last, let liked_event = damus_state.events.lookup(evid) { let content_preview = render_notification_content_preview(cache: damus_state.events, ev: liked_event, profiles: damus_state.profiles, keypair: damus_state.keypair) - let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview) - create_local_notification(profiles: damus_state.profiles, notify: notify) + return LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview) } - + + return nil } func create_local_notification(profiles: Profiles, notify: LocalNotification) { - let content = UNMutableNotificationContent() - var title = "" - var identifier = "" - let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey) - switch notify.type { - case .mention: - title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName) - identifier = "myMentionNotification" - case .repost: - title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName) - identifier = "myBoostNotification" - case .like: - title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, to_reaction_emoji(ev: notify.event) ?? "") - identifier = "myLikeNotification" - case .dm: - title = displayName - identifier = "myDMNotification" - case .zap, .profile_zap: - // not handled here - break - } - content.title = title - content.body = notify.content - content.sound = UNNotificationSound.default - content.userInfo = notify.to_lossy().to_user_info() + let (content, identifier) = NotificationFormatter.shared.format_message(displayName: displayName, notify: notify) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) diff --git a/damus/Util/LocalNotification.swift b/damus/Util/LocalNotification.swift index 260b392e..19da4490 100644 --- a/damus/Util/LocalNotification.swift +++ b/damus/Util/LocalNotification.swift @@ -7,6 +7,8 @@ import Foundation +let NDB_NOTE_JSON_USER_INFO_KEY = "ndb_note_json" + struct LossyLocalNotification { let type: LocalNotificationType let mention: MentionRef @@ -19,8 +21,8 @@ struct LossyLocalNotification { } static func from_user_info(user_info: [AnyHashable: Any]) -> LossyLocalNotification? { - if let encoded_nostr_event_push_data = user_info["nostr_event_info"] as? String { - return self.from(encoded_nostr_event_push_data: encoded_nostr_event_push_data) + if let encoded_ndb_note = user_info[NDB_NOTE_JSON_USER_INFO_KEY] as? String { + return self.from(json_encoded_ndb_note: encoded_ndb_note) } guard let id = user_info["id"] as? String, let target_id = MentionRef.from_bech32(str: id) else { @@ -32,18 +34,16 @@ struct LossyLocalNotification { return LossyLocalNotification(type: type, mention: target_id) } - static func from(encoded_nostr_event_push_data: String) -> LossyLocalNotification? { - guard let json_data = encoded_nostr_event_push_data.data(using: .utf8), - let nostr_event_push_data = try? JSONDecoder().decode(NostrEventInfoFromPushNotification.self, from: json_data) else { + static func from(json_encoded_ndb_note: String) -> LossyLocalNotification? { + guard let ndb_note = NdbNote.owned_from_json(json: json_encoded_ndb_note) else { return nil } - return self.from(nostr_event_push_data: nostr_event_push_data) + return self.from(ndb_note: ndb_note) } - static func from(nostr_event_push_data: NostrEventInfoFromPushNotification) -> LossyLocalNotification? { - guard let type = LocalNotificationType.from(nostr_kind: nostr_event_push_data.kind) else { return nil } - guard let note_id: NoteId = NoteId.init(hex: nostr_event_push_data.id) else { return nil } - let target: MentionRef = .note(note_id) + static func from(ndb_note: NdbNote) -> LossyLocalNotification? { + guard let known_kind = ndb_note.known_kind, let type = LocalNotificationType.from(nostr_kind: known_kind) else { return nil } + let target: MentionRef = .note(ndb_note.id) return LossyLocalNotification(type: type, mention: target) } }