This adds a sync mechanism in Ndb.swift to coordinate certain usage of nostrdb.c calls and the need to close nostrdb due to app lifecycle requirements. Furthermore, it fixes the order of operations when re-opening NostrDB, to avoid race conditions where a query uses an older Ndb generation. This sync mechanism allows multiple queries to happen simultaneously (from the Swift-side), while preventing ndb from simultaneously closing during such usages. It also does that while keeping the Ndb interface sync and nonisolated, which keeps the API easy to use from Swift/SwiftUI and allows for parallel operations to occur. If Swift Actors were to be used (e.g. creating an NdbActor), the Ndb.swift interface would change in such a way that it would propagate the need for several changes throughout the codebase, including loading logic in some ViewModels. Furthermore, it would likely decrease performance by forcing Ndb.swift operations to run sequentially when they could run in parallel. Changelog-Fixed: Fixed crashes that happened when the app went into background mode Closes: https://github.com/damus-io/damus/issues/3245 Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
143 lines
7.2 KiB
Swift
143 lines
7.2 KiB
Swift
//
|
||
// NotificationFormatter.swift
|
||
// DamusNotificationService
|
||
//
|
||
// Created by Daniel D’Aquino on 2023-11-13.
|
||
//
|
||
|
||
import Foundation
|
||
import UserNotifications
|
||
|
||
struct NotificationFormatter {
|
||
static var shared = NotificationFormatter()
|
||
|
||
// MARK: - Formatting with NdbNote
|
||
|
||
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 = [
|
||
NDB_NOTE_JSON_USER_INFO_KEY: event_json_string
|
||
]
|
||
}
|
||
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
|
||
break
|
||
case .dm:
|
||
content.title = NSLocalizedString("New message", comment: "Title label for push notifications where a direct message was sent to the user")
|
||
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 = 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
|
||
}
|
||
content.title = NSLocalizedString("New note reaction", comment: "Title label for push notifications where someone reacted to the user's post with a specific emoji")
|
||
content.body = String(format: NSLocalizedString("Someone reacted to your note with %@", comment: "Body label for push notifications where someone reacted to the user's post with a specific emoji"), reactionEmoji)
|
||
break
|
||
case .zap:
|
||
content.title = NSLocalizedString("Someone zapped you ⚡️", comment: "Title label for a push notification where someone zapped the user")
|
||
break
|
||
default:
|
||
return nil
|
||
}
|
||
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 .tagged:
|
||
title = String(format: NSLocalizedString("Tagged by %@", comment: "Tagged by heading in local notification"), displayName)
|
||
identifier = "myMentionNotification"
|
||
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. Try `format_message(displayName: String, notify: LocalNotification, state: HeadlessDamusState) async -> (content: UNMutableNotificationContent, identifier: String)?`
|
||
return nil
|
||
case .reply:
|
||
title = String(format: NSLocalizedString("%@ replied to your note", comment: "Heading for local notification indicating a new reply"), displayName)
|
||
identifier = "myReplyNotification"
|
||
}
|
||
content.title = title
|
||
content.body = notify.content
|
||
content.sound = UNNotificationSound.default
|
||
content.userInfo = notify.to_lossy().to_user_info()
|
||
|
||
return (content, identifier)
|
||
}
|
||
|
||
func format_message(displayName: String, notify: LocalNotification, state: HeadlessDamusState) async -> (content: UNMutableNotificationContent, identifier: String)? {
|
||
// Try sync method first and return if it works
|
||
if let sync_formatted_message = self.format_message(displayName: displayName, notify: notify) {
|
||
return sync_formatted_message
|
||
}
|
||
|
||
// If it does not work, try async formatting methods
|
||
let content = UNMutableNotificationContent()
|
||
|
||
switch notify.type {
|
||
case .zap, .profile_zap:
|
||
guard let zap = await get_zap(from: notify.event, state: state) else {
|
||
Log.debug("format_message: async get_zap failed", for: .push_notifications)
|
||
return nil
|
||
}
|
||
content.title = Self.zap_notification_title(zap)
|
||
content.body = Self.zap_notification_body(profiles: state.profiles, zap: zap)
|
||
content.sound = UNNotificationSound.default
|
||
content.userInfo = LossyLocalNotification(type: .zap, mention: .init(nip19: .note(notify.event.id))).to_user_info()
|
||
return (content, "myZapNotification")
|
||
default:
|
||
// The sync method should have taken care of this.
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// MARK: - Formatting zap utility notifications
|
||
|
||
static func zap_notification_title(_ zap: Zap) -> String {
|
||
if zap.private_request != nil {
|
||
return NSLocalizedString("Private Zap", comment: "Title of notification when a private zap is received.")
|
||
} else {
|
||
return NSLocalizedString("Zap", comment: "Title of notification when a non-private zap is received.")
|
||
}
|
||
}
|
||
|
||
static func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
|
||
let src = zap.request.ev
|
||
let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey
|
||
|
||
let profile = try? profiles.lookup(id: pk)
|
||
let name = Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50)
|
||
|
||
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
|
||
let formattedSats = format_msats_abbrev(zap.invoice.amount)
|
||
|
||
if src.content.isEmpty {
|
||
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
|
||
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name)
|
||
} else {
|
||
let format = localizedStringFormat(key: "zap_notification_with_message", locale: locale)
|
||
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name, src.content)
|
||
}
|
||
}
|
||
}
|