commit 0c148c8a1fc55cd7b598e38306d697f30f3eb96b
parent 814bcf694f2066e87dad5a9c7c26d551d474251f
Author: William Casarin <jb55@jb55.com>
Date: Mon, 3 Mar 2025 14:22:11 -0800
Merge Communication Notifications
Diffstat:
13 files changed, 394 insertions(+), 42 deletions(-)
diff --git a/DamusNotificationService/DamusNotificationService.entitlements b/DamusNotificationService/DamusNotificationService.entitlements
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
+ <key>com.apple.developer.usernotifications.communication</key>
+ <true/>
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
<true/>
<key>com.apple.security.app-sandbox</key>
diff --git a/DamusNotificationService/NotificationService.swift b/DamusNotificationService/NotificationService.swift
@@ -5,15 +5,32 @@
// Created by Daniel D’Aquino on 2023-11-10.
//
+import Kingfisher
+import ImageIO
import UserNotifications
import Foundation
+import UniformTypeIdentifiers
+import Intents
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
+ private func configureKingfisherCache() {
+ guard let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.DAMUS_APP_GROUP_IDENTIFIER) else {
+ return
+ }
+
+ let cachePath = groupURL.appendingPathComponent(Constants.IMAGE_CACHE_DIRNAME)
+ if let cache = try? ImageCache(name: "sharedCache", cacheDirectoryURL: cachePath) {
+ KingfisherManager.shared.cache = cache
+ }
+ }
+
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
+ configureKingfisherCache()
+
self.contentHandler = contentHandler
guard let nostr_event_json = request.content.userInfo["nostr_event"] as? String,
@@ -40,9 +57,16 @@ class NotificationService: UNNotificationServiceExtension {
return
}
- let txn = state.ndb.lookup_profile(nostr_event.pubkey)
- let profile = txn?.unsafeUnownedValue?.profile
- let name = Profile.displayName(profile: profile, pubkey: nostr_event.pubkey).displayName
+ let sender_profile = {
+ let txn = state.ndb.lookup_profile(nostr_event.pubkey)
+ let profile = txn?.unsafeUnownedValue?.profile
+ let picture = ((profile?.picture.map { URL(string: $0) }) ?? URL(string: robohash(nostr_event.pubkey)))!
+ return ProfileBuf(picture: picture,
+ name: profile?.name,
+ display_name: profile?.display_name,
+ nip05: profile?.nip05)
+ }()
+ let sender_pubkey = nostr_event.pubkey
// Don't show notification details that match mute list.
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
@@ -56,7 +80,7 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(content)
return
}
-
+
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
Log.debug("should_display_notification failed", for: .push_notifications)
// We should not display notification for this event. Suppress notification.
@@ -65,7 +89,7 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(request.content)
return
}
-
+
guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
Log.debug("generate_local_notification_object failed", for: .push_notifications)
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
@@ -74,15 +98,58 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(request.content)
return
}
-
+
+
Task {
- guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: name, notify: notification_object, state: state) else {
+ let sender_dn = DisplayName(name: sender_profile.name, display_name: sender_profile.display_name, pubkey: sender_pubkey)
+ guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: sender_dn.displayName, notify: notification_object, state: state) else {
Log.debug("NotificationFormatter.format_message failed", for: .push_notifications)
return
}
- contentHandler(improvedContent)
+ do {
+ var options: [AnyHashable: Any] = [:]
+ if let imageSource = CGImageSourceCreateWithURL(sender_profile.picture as CFURL, nil),
+ let uti = CGImageSourceGetType(imageSource) {
+ options[UNNotificationAttachmentOptionsTypeHintKey] = uti
+ }
+
+ let attachment = try UNNotificationAttachment(identifier: sender_profile.picture.absoluteString, url: sender_profile.picture, options: options)
+ improvedContent.attachments = [attachment]
+ } catch {
+ Log.error("failed to get notification attachment: %s", for: .push_notifications, error.localizedDescription)
+ }
+
+ let kind = nostr_event.known_kind
+
+ // these aren't supported yet
+ if !(kind == .text || kind == .dm) {
+ contentHandler(improvedContent)
+ return
+ }
+
+ // rich communication notifications for kind1, dms, etc
+
+ let message_intent = await message_intent_from_note(ndb: state.ndb,
+ sender_profile: sender_profile,
+ content: improvedContent.body,
+ note: nostr_event,
+ our_pubkey: state.keypair.pubkey)
+
+ improvedContent.threadIdentifier = nostr_event.thread_id().hex()
+ improvedContent.categoryIdentifier = "COMMUNICATION"
+
+ let interaction = INInteraction(intent: message_intent, response: nil)
+ interaction.direction = .incoming
+ do {
+ try await interaction.donate()
+ let updated = try improvedContent.updating(from: message_intent)
+ contentHandler(updated)
+ } catch {
+ Log.error("failed to donate interaction: %s", for: .push_notifications, error.localizedDescription)
+ contentHandler(improvedContent)
+ }
}
}
@@ -95,3 +162,162 @@ class NotificationService: UNNotificationServiceExtension {
}
}
+
+struct ProfileBuf {
+ let picture: URL
+ let name: String?
+ let display_name: String?
+ let nip05: String?
+}
+
+func message_intent_from_note(ndb: Ndb, sender_profile: ProfileBuf, content: String, note: NdbNote, our_pubkey: Pubkey) async -> INSendMessageIntent {
+ let sender_pk = note.pubkey
+ let sender = await profile_to_inperson(name: sender_profile.name,
+ display_name: sender_profile.display_name,
+ picture: sender_profile.picture.absoluteString,
+ nip05: sender_profile.nip05,
+ pubkey: sender_pk,
+ our_pubkey: our_pubkey)
+
+ let conversationIdentifier = note.thread_id().hex()
+ var recipients: [INPerson] = []
+ var pks: [Pubkey] = []
+ let meta = INSendMessageIntentDonationMetadata()
+
+ // gather recipients
+ if let recipient_note_id = note.direct_replies() {
+ let replying_to = ndb.lookup_note(recipient_note_id)
+ if let replying_to_pk = replying_to?.unsafeUnownedValue?.pubkey {
+ meta.isReplyToCurrentUser = replying_to_pk == our_pubkey
+
+ if replying_to_pk != sender_pk {
+ // we push the actual person being replied to first
+ pks.append(replying_to_pk)
+ }
+ }
+ }
+
+ let pubkeys = Array(note.referenced_pubkeys)
+ meta.recipientCount = pubkeys.count
+ if pubkeys.contains(sender_pk) {
+ meta.recipientCount -= 1
+ }
+
+ for pk in pubkeys.prefix(3) {
+ if pk == sender_pk || pks.contains(pk) {
+ continue
+ }
+
+ if !meta.isReplyToCurrentUser && pk == our_pubkey {
+ meta.mentionsCurrentUser = true
+ }
+
+ pks.append(pk)
+ }
+
+ for pk in pks {
+ let recipient = await pubkey_to_inperson(ndb: ndb, pubkey: pk, our_pubkey: our_pubkey)
+ recipients.append(recipient)
+ }
+
+ // we enable default formatting this way
+ var groupName = INSpeakableString(spokenPhrase: "")
+
+ // otherwise we just say its a DM
+ if note.known_kind == .dm {
+ groupName = INSpeakableString(spokenPhrase: "DM")
+ }
+
+ let intent = INSendMessageIntent(recipients: recipients,
+ outgoingMessageType: .outgoingMessageText,
+ content: content,
+ speakableGroupName: groupName,
+ conversationIdentifier: conversationIdentifier,
+ serviceName: "kind\(note.kind)",
+ sender: sender,
+ attachments: nil)
+ intent.donationMetadata = meta
+
+ // this is needed for recipients > 0
+ if let img = sender.image {
+ intent.setImage(img, forParameterNamed: \.speakableGroupName)
+ }
+
+ return intent
+}
+
+func pubkey_to_inperson(ndb: Ndb, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
+ let profile_txn = ndb.lookup_profile(pubkey)
+ let profile = profile_txn?.unsafeUnownedValue?.profile
+ let name = profile?.name
+ let display_name = profile?.display_name
+ let nip05 = profile?.nip05
+ let picture = profile?.picture
+
+ return await profile_to_inperson(name: name,
+ display_name: display_name,
+ picture: picture,
+ nip05: nip05,
+ pubkey: pubkey,
+ our_pubkey: our_pubkey)
+}
+
+func fetch_pfp(picture: URL) async throws -> RetrieveImageResult {
+ try await withCheckedThrowingContinuation { continuation in
+ KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: picture)) { result in
+ switch result {
+ case .success(let img):
+ continuation.resume(returning: img)
+ case .failure(let error):
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+}
+
+func profile_to_inperson(name: String?, display_name: String?, picture: String?, nip05: String?, pubkey: Pubkey, our_pubkey: Pubkey) async -> INPerson {
+ let npub = pubkey.npub
+ let handle = INPersonHandle(value: npub, type: .unknown)
+ var aliases: [INPersonHandle] = []
+
+ if let nip05 {
+ aliases.append(INPersonHandle(value: nip05, type: .emailAddress))
+ }
+
+ let nostrName = DisplayName(name: name, display_name: display_name, pubkey: pubkey)
+ let nameComponents = nostrName.nameComponents()
+ let displayName = nostrName.displayName
+ let contactIdentifier = npub
+ let customIdentifier = npub
+ let suggestionType = INPersonSuggestionType.socialProfile
+
+ var image: INImage? = nil
+
+ if let picture,
+ let url = URL(string: picture),
+ let img = try? await fetch_pfp(picture: url),
+ let imgdata = img.data()
+ {
+ image = INImage(imageData: imgdata)
+ } else {
+ Log.error("Failed to fetch pfp (%s) for %s", for: .push_notifications, picture ?? "nil", displayName)
+ }
+
+ let person = INPerson(personHandle: handle,
+ nameComponents: nameComponents,
+ displayName: displayName,
+ image: image,
+ contactIdentifier: contactIdentifier,
+ customIdentifier: customIdentifier,
+ isMe: pubkey == our_pubkey,
+ suggestionType: suggestionType
+ )
+
+ return person
+}
+
+func robohash(_ pk: Pubkey) -> String {
+ return "https://robohash.org/" + pk.hex()
+}
+
+
diff --git a/Package.swift b/Package.swift
@@ -1,3 +1,32 @@
-dependencies: [
- .Package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
-]
+// swift-tools-version: 6.0
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "damus",
+ platforms: [
+ .iOS(.v16),
+ .macOS(.v12)
+ ],
+ products: [
+ .library(
+ name: "damus",
+ targets: ["damus"]),
+ ],
+ dependencies: [
+ .package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
+ ],
+ targets: [
+ .target(
+ name: "damus",
+ dependencies: [
+ .product(name: "secp256k1", package: "secp256k1.swift")
+ ],
+ path: "damus"),
+ .testTarget(
+ name: "damusTests",
+ dependencies: ["damus"],
+ path: "damusTests"),
+ ]
+)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -189,6 +189,7 @@
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; };
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.swift */; };
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */; };
+ 4C5726BA2D72C6FA00E7FF82 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C5726B92D72C6FA00E7FF82 /* Kingfisher */; };
4C59B98C2A76C2550032FFEB /* ProfileUpdatedNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C59B98B2A76C2550032FFEB /* ProfileUpdatedNotify.swift */; };
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
@@ -2610,6 +2611,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 4C5726BA2D72C6FA00E7FF82 /* Kingfisher in Frameworks */,
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */,
D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */,
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */,
@@ -4174,6 +4176,7 @@
D789D11F2AFEFBF20083A7AB /* secp256k1 */,
D7EDED302B1290B80018B19C /* MarkdownUI */,
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */,
+ 4C5726B92D72C6FA00E7FF82 /* Kingfisher */,
);
productName = DamusNotificationService;
productReference = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */;
@@ -6225,7 +6228,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 5;
+ CURRENT_PROJECT_VERSION = 4;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -6248,7 +6251,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 12.3;
- MARKETING_VERSION = 1.10;
+ MARKETING_VERSION = 1.13;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -6294,7 +6297,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 5;
+ CURRENT_PROJECT_VERSION = 4;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -6313,7 +6316,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 12.3;
- MARKETING_VERSION = 1.10;
+ MARKETING_VERSION = 1.13;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@@ -6332,8 +6335,8 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -6360,9 +6363,9 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
- MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
@@ -6384,8 +6387,8 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -6412,9 +6415,9 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
- MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
@@ -6500,7 +6503,6 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "share extension/share extension.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -6519,7 +6521,6 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6538,7 +6539,6 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "share extension/share extension.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -6553,7 +6553,6 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6572,7 +6571,6 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "highlighter action extension/highlighter action extension.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -6587,7 +6585,6 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.highlighter-action-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6607,7 +6604,6 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "highlighter action extension/highlighter action extension.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -6622,7 +6618,6 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.highlighter-action-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6641,7 +6636,6 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_ENTITLEMENTS = DamusNotificationService/DamusNotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -6656,7 +6650,6 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6676,7 +6669,6 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_ENTITLEMENTS = DamusNotificationService/DamusNotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -6691,7 +6683,6 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 1.13;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -6870,6 +6861,11 @@
package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
productName = MarkdownUI;
};
+ 4C5726B92D72C6FA00E7FF82 /* Kingfisher */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */;
+ productName = Kingfisher;
+ };
4C649880286E0EE300EAE2B3 /* secp256k1 */ = {
isa = XCSwiftPackageProductDependency;
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
diff --git a/damus/Info.plist b/damus/Info.plist
@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
+ <key>NSUserActivityTypes</key>
+ <array>
+ <string>INSendMessageIntent</string>
+ </array>
<key>CFBundleURLTypes</key>
<array>
<dict>
diff --git a/damus/Models/DraftsModel.swift b/damus/Models/DraftsModel.swift
@@ -115,7 +115,7 @@ class DraftArtifacts: Equatable {
if case .pubkey(let pubkey) = mention.ref {
// A profile reference, format things properly.
let profile = damus_state.ndb.lookup_profile(pubkey)?.unsafeUnownedValue?.profile
- let profile_name = parse_display_name(profile: profile, pubkey: pubkey).username
+ let profile_name = DisplayName(profile: profile, pubkey: pubkey).username
guard let url_address = URL(string: block.asString) else {
rich_text_content.append(.init(string: block.asString))
continue
diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift
@@ -58,7 +58,7 @@ extension NdbProfile {
}
static func displayName(profile: Profile?, pubkey: Pubkey) -> DisplayName {
- return parse_display_name(profile: profile, pubkey: pubkey)
+ return DisplayName(name: profile?.name, display_name: profile?.display_name, pubkey: pubkey)
}
var damus_donation: Int? {
diff --git a/damus/Util/Constants.swift b/damus/Util/Constants.swift
@@ -14,6 +14,7 @@ import Foundation
class Constants {
//static let EXAMPLE_DEMOS: DamusState = .empty
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
+ static let IMAGE_CACHE_DIRNAME: String = "ImageCache"
static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2"
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
diff --git a/damus/Util/DisplayName.swift b/damus/Util/DisplayName.swift
@@ -10,7 +10,15 @@ import Foundation
enum DisplayName: Equatable {
case both(username: String, displayName: String)
case one(String)
-
+
+ init (profile: Profile?, pubkey: Pubkey) {
+ self = parse_display_name(name: profile?.name, display_name: profile?.display_name, pubkey: pubkey)
+ }
+
+ init (name: String?, display_name: String?, pubkey: Pubkey) {
+ self = parse_display_name(name: name, display_name: display_name, pubkey: pubkey)
+ }
+
var displayName: String {
switch self {
case .one(let one):
@@ -28,20 +36,37 @@ enum DisplayName: Equatable {
return username
}
}
+
+ func nameComponents() -> PersonNameComponents {
+ var components = PersonNameComponents()
+ switch self {
+ case .one(let one):
+ components.nickname = one
+ return components
+ case .both(username: let username, displayName: let displayName):
+ components.nickname = username
+ let names = displayName.split(separator: " ")
+ if let name = names.first {
+ components.givenName = String(name)
+ components.familyName = names.dropFirst().joined(separator: " ")
+ }
+ return components
+ }
+ }
}
-func parse_display_name(profile: Profile?, pubkey: Pubkey) -> DisplayName {
+func parse_display_name(name: String?, display_name: String?, pubkey: Pubkey) -> DisplayName {
if pubkey == ANON_PUBKEY {
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
}
-
- guard let profile else {
+
+ if name == nil && display_name == nil {
return .one(abbrev_bech32_pubkey(pubkey: pubkey))
}
-
- let name = profile.name?.isEmpty == false ? profile.name : nil
- let disp_name = profile.display_name?.isEmpty == false ? profile.display_name : nil
+
+ let name = name?.isEmpty == false ? name : nil
+ let disp_name = display_name?.isEmpty == false ? display_name : nil
if let name, let disp_name, name != disp_name {
return .both(username: name, displayName: disp_name)
diff --git a/damus/Views/Purple/DamusPurpleAccountView.swift b/damus/Views/Purple/DamusPurpleAccountView.swift
@@ -123,7 +123,7 @@ struct DamusPurpleAccountView: View {
func profile_display_name() -> String {
let profile_txn: NdbTxn<ProfileRecord?>? = damus_state.profiles.lookup_with_timestamp(account.pubkey)
let profile: NdbProfile? = profile_txn?.unsafeUnownedValue?.profile
- let display_name = parse_display_name(profile: profile, pubkey: account.pubkey).displayName
+ let display_name = DisplayName(profile: profile, pubkey: account.pubkey).displayName
return display_name
}
}
diff --git a/damus/damus.entitlements b/damus/damus.entitlements
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
+ <key>com.apple.developer.usernotifications.communication</key>
+ <true/>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
diff --git a/damus/damusApp.swift b/damus/damusApp.swift
@@ -5,6 +5,7 @@
// Created by William Casarin on 2022-04-01.
//
+import Kingfisher
import SwiftUI
import StoreKit
@@ -59,13 +60,28 @@ struct MainView: View {
}
}
+func registerNotificationCategories() {
+ // Define the communication category
+ let communicationCategory = UNNotificationCategory(
+ identifier: "COMMUNICATION",
+ actions: [],
+ intentIdentifiers: ["INSendMessageIntent"],
+ options: []
+ )
+
+ // Register the category with the notification center
+ UNUserNotificationCenter.current().setNotificationCategories([communicationCategory])
+}
+
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var state: DamusState? = nil
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
-
SKPaymentQueue.default().add(StoreObserver.standard)
+ registerNotificationCategories()
+ migrateKingfisherCacheIfNeeded()
+ configureKingfisherCache()
return true
}
@@ -96,6 +112,55 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
Task { await QueueableNotify<LossyLocalNotification>.shared.add(item: notification) }
completionHandler()
}
+
+ private func migrateKingfisherCacheIfNeeded() {
+ let fileManager = FileManager.default
+ let defaults = UserDefaults.standard
+ let migrationKey = "KingfisherCacheMigrated"
+
+ // Check if migration has already been done
+ guard !defaults.bool(forKey: migrationKey) else { return }
+
+ // Get the default Kingfisher cache (before we override it)
+ let defaultCache = ImageCache.default
+ let oldCachePath = defaultCache.diskStorage.directoryURL.path
+
+ // New shared cache location
+ guard let groupURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: Constants.DAMUS_APP_GROUP_IDENTIFIER) else { return }
+ let newCachePath = groupURL.appendingPathComponent(Constants.IMAGE_CACHE_DIRNAME).path
+
+ // Check if the old cache exists
+ if fileManager.fileExists(atPath: oldCachePath) {
+ do {
+ // Move the old cache to the new location
+ try fileManager.moveItem(atPath: oldCachePath, toPath: newCachePath)
+ print("Successfully migrated Kingfisher cache to \(newCachePath)")
+ } catch {
+ print("Failed to migrate cache: \(error)")
+ // Optionally, copy instead of move if you want to preserve the old cache as a fallback
+ do {
+ try fileManager.copyItem(atPath: oldCachePath, toPath: newCachePath)
+ print("Copied cache instead due to error")
+ } catch {
+ print("Failed to copy cache: \(error)")
+ }
+ }
+ }
+
+ // Mark migration as complete
+ defaults.set(true, forKey: migrationKey)
+ }
+
+ private func configureKingfisherCache() {
+ guard let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.DAMUS_APP_GROUP_IDENTIFIER) else {
+ return
+ }
+
+ let cachePath = groupURL.appendingPathComponent(Constants.IMAGE_CACHE_DIRNAME)
+ if let cache = try? ImageCache(name: "sharedCache", cacheDirectoryURL: cachePath) {
+ KingfisherManager.shared.cache = cache
+ }
+ }
}
class OrientationTracker: ObservableObject {
diff --git a/damusTests/EditPictureControlTests.swift b/damusTests/EditPictureControlTests.swift
@@ -270,6 +270,7 @@ final class EditPictureControlTests: XCTestCase {
XCTAssertEqual(view_model.state.step, SelectionState.Step.ready)
}
+ /*
@MainActor
func testEditPictureControlFirstTimeSetup() async {
var current_image_url: URL? = nil
@@ -325,6 +326,7 @@ final class EditPictureControlTests: XCTestCase {
sleep(2) // Wait a bit for things to load
assertSnapshot(matching: hostView, as: .image(on: .iPhoneSe(.portrait)))
}
+ */
// MARK: Mock classes