diff --git a/DamusNotificationService/DamusNotificationService.entitlements b/DamusNotificationService/DamusNotificationService.entitlements
new file mode 100644
index 00000000..b5d14b53
--- /dev/null
+++ b/DamusNotificationService/DamusNotificationService.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.application-groups
+
+ group.com.damus
+
+ com.apple.security.network.client
+
+
+
diff --git a/DamusNotificationService/Info.plist b/DamusNotificationService/Info.plist
new file mode 100644
index 00000000..57421ebf
--- /dev/null
+++ b/DamusNotificationService/Info.plist
@@ -0,0 +1,13 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.usernotifications.service
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).NotificationService
+
+
+
diff --git a/DamusNotificationService/NostrEventInfoFromPushNotification.swift b/DamusNotificationService/NostrEventInfoFromPushNotification.swift
new file mode 100644
index 00000000..d9f8fbde
--- /dev/null
+++ b/DamusNotificationService/NostrEventInfoFromPushNotification.swift
@@ -0,0 +1,49 @@
+//
+// 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
new file mode 100644
index 00000000..81ef226d
--- /dev/null
+++ b/DamusNotificationService/NotificationFormatter.swift
@@ -0,0 +1,48 @@
+//
+// NotificationFormatter.swift
+// DamusNotificationService
+//
+// Created by Daniel D’Aquino on 2023-11-13.
+//
+
+import Foundation
+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? {
+ 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
+ ]
+ }
+ switch event.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 = event.reactionEmoji() 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
+ }
+}
diff --git a/DamusNotificationService/NotificationService.swift b/DamusNotificationService/NotificationService.swift
new file mode 100644
index 00000000..4679c6b9
--- /dev/null
+++ b/DamusNotificationService/NotificationService.swift
@@ -0,0 +1,39 @@
+//
+// NotificationService.swift
+// DamusNotificationService
+//
+// Created by Daniel D’Aquino on 2023-11-10.
+//
+
+import UserNotifications
+import Foundation
+
+class NotificationService: UNNotificationServiceExtension {
+
+ var contentHandler: ((UNNotificationContent) -> Void)?
+ var bestAttemptContent: UNMutableNotificationContent?
+
+ override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
+ self.contentHandler = contentHandler
+
+ // Modify the notification content here...
+ guard let nostrEventInfoDictionary = request.content.userInfo["nostr_event"] as? [AnyHashable: Any],
+ let nostrEventInfo = NostrEventInfoFromPushNotification.from(dictionary: nostrEventInfoDictionary) else {
+ contentHandler(request.content)
+ return;
+ }
+
+ if let improvedContent = NotificationFormatter.shared.formatMessage(event: nostrEventInfo) {
+ contentHandler(improvedContent)
+ }
+ }
+
+ override func serviceExtensionTimeWillExpire() {
+ // Called just before the extension will be terminated by the system.
+ // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
+ if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
+ contentHandler(bestAttemptContent)
+ }
+ }
+
+}
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
index a1edd191..db2f15ed 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -429,6 +429,8 @@
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
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 */; };
@@ -442,8 +444,13 @@
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; };
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; };
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; };
+ D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = D789D11F2AFEFBF20083A7AB /* secp256k1 */; };
+ D79C4C172AFEB061003A41B4 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C4C162AFEB061003A41B4 /* NotificationService.swift */; };
+ D79C4C1B2AFEB061003A41B4 /* DamusNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D7A343EE2AD0D77C00CED48B /* InlineSnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */; };
D7A343F02AD0D77C00CED48B /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343EF2AD0D77C00CED48B /* SnapshotTesting */; };
+ 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 */; };
D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */; };
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; };
@@ -481,8 +488,29 @@
remoteGlobalIDString = 4CE6DEE227F7A08100C66700;
remoteInfo = damus;
};
+ D79C4C192AFEB061003A41B4 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 4CE6DEDB27F7A08100C66700 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = D79C4C132AFEB061003A41B4;
+ remoteInfo = DamusNotificationService;
+ };
/* End PBXContainerItemProxy section */
+/* Begin PBXCopyFilesBuildPhase section */
+ D79C4C1D2AFEB061003A41B4 /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ D79C4C1B2AFEB061003A41B4 /* DamusNotificationService.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
/* Begin PBXFileReference section */
0E8A4BB62AE4359200065E81 /* NostrFilter+Hashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrFilter+Hashable.swift"; sourceTree = ""; };
3165648A295B70D500C64604 /* LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = ""; };
@@ -1130,6 +1158,8 @@
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = ""; };
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 = ""; };
@@ -1143,6 +1173,10 @@
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = ""; };
D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = ""; };
D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = ""; };
+ D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DamusNotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ D79C4C162AFEB061003A41B4 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; };
+ D79C4C182AFEB061003A41B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ D79C4C1C2AFEB061003A41B4 /* DamusNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DamusNotificationService.entitlements; sourceTree = ""; };
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = ""; };
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = ""; };
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = ""; };
@@ -1192,6 +1226,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ D79C4C112AFEB061003A41B4 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -2114,6 +2156,7 @@
4CE6DEE527F7A08100C66700 /* damus */,
4CE6DEF627F7A08200C66700 /* damusTests */,
4CE6DF0027F7A08200C66700 /* damusUITests */,
+ D79C4C152AFEB061003A41B4 /* DamusNotificationService */,
4CE6DEE427F7A08100C66700 /* Products */,
4CEE2AE62804F57B00AB5EEF /* Frameworks */,
);
@@ -2127,6 +2170,7 @@
4CE6DEE327F7A08100C66700 /* damus.app */,
4CE6DEF327F7A08200C66700 /* damusTests.xctest */,
4CE6DEFD27F7A08200C66700 /* damusUITests.xctest */,
+ D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */,
);
name = Products;
sourceTree = "";
@@ -2339,6 +2383,18 @@
path = Mocking;
sourceTree = "";
};
+ D79C4C152AFEB061003A41B4 /* DamusNotificationService */ = {
+ isa = PBXGroup;
+ children = (
+ D79C4C1C2AFEB061003A41B4 /* DamusNotificationService.entitlements */,
+ D79C4C162AFEB061003A41B4 /* NotificationService.swift */,
+ D79C4C182AFEB061003A41B4 /* Info.plist */,
+ D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */,
+ D70A3B182B02DD2D008BD568 /* NostrEventInfoFromPushNotification.swift */,
+ );
+ path = DamusNotificationService;
+ sourceTree = "";
+ };
F71694E82A66221E001F4053 /* Onboarding */ = {
isa = PBXGroup;
children = (
@@ -2378,10 +2434,12 @@
4CE6DEE027F7A08100C66700 /* Frameworks */,
4C1D4FB22A7965230024F453 /* ShellScript */,
4CE6DEE127F7A08100C66700 /* Resources */,
+ D79C4C1D2AFEB061003A41B4 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
+ D79C4C1A2AFEB061003A41B4 /* PBXTargetDependency */,
);
name = damus;
packageProductDependencies = (
@@ -2433,6 +2491,26 @@
productReference = 4CE6DEFD27F7A08200C66700 /* damusUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
+ D79C4C132AFEB061003A41B4 /* DamusNotificationService */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = D79C4C202AFEB061003A41B4 /* Build configuration list for PBXNativeTarget "DamusNotificationService" */;
+ buildPhases = (
+ D79C4C102AFEB061003A41B4 /* Sources */,
+ D79C4C112AFEB061003A41B4 /* Frameworks */,
+ D79C4C122AFEB061003A41B4 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = DamusNotificationService;
+ packageProductDependencies = (
+ D789D11F2AFEFBF20083A7AB /* secp256k1 */,
+ );
+ productName = DamusNotificationService;
+ productReference = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -2440,7 +2518,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1330;
+ LastSwiftUpdateCheck = 1500;
LastUpgradeCheck = 1500;
TargetAttributes = {
4CE6DEE227F7A08100C66700 = {
@@ -2455,6 +2533,9 @@
CreatedOnToolsVersion = 13.3;
TestTargetID = 4CE6DEE227F7A08100C66700;
};
+ D79C4C132AFEB061003A41B4 = {
+ CreatedOnToolsVersion = 15.0.1;
+ };
};
};
buildConfigurationList = 4CE6DEDE27F7A08100C66700 /* Build configuration list for PBXProject "damus" */;
@@ -2508,6 +2589,7 @@
4CE6DEE227F7A08100C66700 /* damus */,
4CE6DEF227F7A08200C66700 /* damusTests */,
4CE6DEFC27F7A08200C66700 /* damusUITests */,
+ D79C4C132AFEB061003A41B4 /* DamusNotificationService */,
);
};
/* End PBXProject section */
@@ -2546,6 +2628,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ D79C4C122AFEB061003A41B4 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -2713,6 +2802,7 @@
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 */,
@@ -3032,6 +3122,17 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ D79C4C102AFEB061003A41B4 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ D79C4C172AFEB061003A41B4 /* NotificationService.swift in Sources */,
+ D70A3B192B02DD2D008BD568 /* NostrEventInfoFromPushNotification.swift in Sources */,
+ D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */,
+ D70A3B172B02DCE5008BD568 /* NotificationFormatter.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -3045,6 +3146,11 @@
target = 4CE6DEE227F7A08100C66700 /* damus */;
targetProxy = 4CE6DEFE27F7A08200C66700 /* PBXContainerItemProxy */;
};
+ D79C4C1A2AFEB061003A41B4 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = D79C4C132AFEB061003A41B4 /* DamusNotificationService */;
+ targetProxy = D79C4C192AFEB061003A41B4 /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -3284,6 +3390,7 @@
4CE6DF0827F7A08200C66700 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
@@ -3333,6 +3440,7 @@
4CE6DF0927F7A08200C66700 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
@@ -3454,6 +3562,73 @@
};
name = Release;
};
+ D79C4C1E2AFEB061003A41B4 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ 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 = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = DamusNotificationService/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = DamusNotificationService;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SUPPORTS_MACCATALYST = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ D79C4C1F2AFEB061003A41B4 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ 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 = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = DamusNotificationService/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = DamusNotificationService;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SUPPORTS_MACCATALYST = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -3493,6 +3668,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ D79C4C202AFEB061003A41B4 /* Build configuration list for PBXNativeTarget "DamusNotificationService" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ D79C4C1E2AFEB061003A41B4 /* Debug */,
+ D79C4C1F2AFEB061003A41B4 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
@@ -3554,6 +3738,11 @@
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
productName = secp256k1;
};
+ D789D11F2AFEFBF20083A7AB /* secp256k1 */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
+ productName = secp256k1;
+ };
D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */ = {
isa = XCSwiftPackageProductDependency;
package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */;
diff --git a/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme b/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme
new file mode 100644
index 00000000..c7fded09
--- /dev/null
+++ b/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
index 4d3e5b83..6b202215 100644
--- a/damus/ContentView.swift
+++ b/damus/ContentView.swift
@@ -54,6 +54,7 @@ enum Sheets: Identifiable {
struct ContentView: View {
let keypair: Keypair
+ let appDelegate: AppDelegate?
var pubkey: Pubkey {
return keypair.pubkey
@@ -303,6 +304,7 @@ struct ContentView: View {
active_sheet = .onboardingSuggestions
hasSeenOnboardingSuggestions = true
}
+ self.appDelegate?.settings = damus_state?.settings
}
.sheet(item: $active_sheet) { item in
switch item {
@@ -694,7 +696,7 @@ struct ContentView: View {
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
- ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil))
+ ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil), appDelegate: nil)
}
}
diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift
index b95a9031..684f2b49 100644
--- a/damus/Models/UserSettingsStore.swift
+++ b/damus/Models/UserSettingsStore.swift
@@ -191,6 +191,12 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "always_show_onboarding_suggestions", default_value: false)
var always_show_onboarding_suggestions: Bool
+
+ @Setting(key: "enable_experimental_push_notifications", default_value: false)
+ var enable_experimental_push_notifications: Bool
+
+ @Setting(key: "send_device_token_to_localhost", default_value: false)
+ var send_device_token_to_localhost: Bool
@Setting(key: "emoji_reactions", default_value: default_emoji_reactions)
var emoji_reactions: [String]
diff --git a/damus/Util/LocalNotification.swift b/damus/Util/LocalNotification.swift
index 51827734..260b392e 100644
--- a/damus/Util/LocalNotification.swift
+++ b/damus/Util/LocalNotification.swift
@@ -19,6 +19,9 @@ 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)
+ }
guard let id = user_info["id"] as? String,
let target_id = MentionRef.from_bech32(str: id) else {
return nil
@@ -28,6 +31,21 @@ 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 {
+ return nil
+ }
+ return self.from(nostr_event_push_data: nostr_event_push_data)
+ }
+
+ 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)
+ return LossyLocalNotification(type: type, mention: target)
+ }
}
struct LocalNotification {
@@ -48,4 +66,21 @@ enum LocalNotificationType: String {
case repost
case zap
case profile_zap
+
+ static func from(nostr_kind: NostrKind) -> Self? {
+ switch nostr_kind {
+ case .text:
+ return .mention
+ case .dm:
+ return .dm
+ case .like:
+ return .like
+ case .longform:
+ return .mention
+ case .zap:
+ return .zap
+ default:
+ return nil
+ }
+ }
}
diff --git a/damus/Views/Settings/DeveloperSettingsView.swift b/damus/Views/Settings/DeveloperSettingsView.swift
index b65a29c7..8ed6c119 100644
--- a/damus/Views/Settings/DeveloperSettingsView.swift
+++ b/damus/Views/Settings/DeveloperSettingsView.swift
@@ -17,7 +17,12 @@ struct DeveloperSettingsView: View {
Toggle(NSLocalizedString("Developer Mode", comment: "Setting to enable developer mode"), isOn: $settings.developer_mode)
.toggleStyle(.switch)
if settings.developer_mode {
- Toggle(NSLocalizedString("Always show onboarding", comment: "Setting to always show onboarding suggestions, for developers who need to test onboarding"), isOn: $settings.always_show_onboarding_suggestions)
+ Toggle("Always show onboarding", isOn: $settings.always_show_onboarding_suggestions)
+
+ Toggle("Enable experimental push notifications", isOn: $settings.enable_experimental_push_notifications)
+ .toggleStyle(.switch)
+
+ Toggle("Send device token to localhost", isOn: $settings.send_device_token_to_localhost)
.toggleStyle(.switch)
}
}
diff --git a/damus/damus.entitlements b/damus/damus.entitlements
index 4bb26140..0da9f032 100644
--- a/damus/damus.entitlements
+++ b/damus/damus.entitlements
@@ -13,6 +13,10 @@
com.apple.security.app-sandbox
+ com.apple.security.application-groups
+
+ group.com.damus
+
com.apple.security.device.audio-input
com.apple.security.device.camera
diff --git a/damus/damusApp.swift b/damus/damusApp.swift
index a629200a..d32d57fc 100644
--- a/damus/damusApp.swift
+++ b/damus/damusApp.swift
@@ -12,7 +12,7 @@ struct damusApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
- MainView()
+ MainView(appDelegate: appDelegate)
}
}
}
@@ -21,11 +21,12 @@ struct MainView: View {
@State var needs_setup = false;
@State var keypair: Keypair? = nil;
@StateObject private var orientationTracker = OrientationTracker()
+ var appDelegate: AppDelegate
var body: some View {
Group {
if let kp = keypair, !needs_setup {
- ContentView(keypair: kp)
+ ContentView(keypair: kp, appDelegate: appDelegate)
.environmentObject(orientationTracker)
} else {
SetupView()
@@ -49,15 +50,67 @@ struct MainView: View {
.onAppear {
orientationTracker.setDeviceMajorAxis()
keypair = get_saved_keypair()
+ appDelegate.keypair = keypair
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
+ var keypair: Keypair? = nil
+ var settings: UserSettingsStore? = nil
+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
+
+ func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
+ // Return if this feature is disabled
+ guard let settings = self.settings else { return }
+ if !settings.enable_experimental_push_notifications {
+ return
+ }
+
+ // Send the device token and pubkey to the server
+ let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
+
+ print("Received device token: \(token)")
+
+ guard let pubkey = keypair?.pubkey else {
+ return
+ }
+
+ // Send those as JSON to the server
+ let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()]
+
+ // create post request
+ let url = URL(string: settings.send_device_token_to_localhost ? "http://localhost:8000/user-info" : "https://notify.damus.io:8000/user-info")!
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+
+ // insert json data to the request
+ request.httpBody = try? JSONSerialization.data(withJSONObject: json, options: [])
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ let task = URLSession.shared.dataTask(with: request) { data, response, error in
+ guard let data = data, error == nil else {
+ print(error?.localizedDescription ?? "No data")
+ return
+ }
+
+ if let response = response as? HTTPURLResponse, !(200...299).contains(response.statusCode) {
+ print("Unexpected status code: \(response.statusCode)")
+ return
+ }
+
+ let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
+ if let responseJSON = responseJSON as? [String: Any] {
+ print(responseJSON)
+ }
+ }
+
+ task.resume()
+ }
// Handle the notification in the foreground state
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {