[PATCH damus v1 0/7] Local + Push notification switch, NIP-98, et al

3 views
Skip to first unread message

Daniel D’Aquino

unread,
May 21, 2024, 12:00:51 AMMay 21
to pat...@damus.io, Daniel D’Aquino
Hi Will,

Here is a series of patches that adds a switch to the notification
settings, along with the necessary logic changes and device token
revocation necessary to support this feature.

Added a few related changes as well:
- Better handling of push notifications we cannot suppress
- NIP-98 authentication support to avoid unauthorized changes to the push notification settings on the server
- Some other code organization changes

To accompany this, I will also send a patch series for the
server-side changes necessary to support the new feature.

Please let me know if you have any questions or concerns.

Thank you,
Daniel

Daniel D’Aquino (7):
Fallback push notification suppression while we do not have
entitlement
Add notification mode setting
Move device token sending logic to its own component
Add NIP-98 authentication to push notification client
Send device token when switching to push notifications mode
Revoke device token when user switches to local notification mode
Improve UX feedback around notification mode setting

.../NotificationService.swift | 23 +++-
damus.xcodeproj/project.pbxproj | 4 +
damus/ContentView.swift | 2 +-
damus/Models/DamusState.swift | 2 +
damus/Models/HomeModel.swift | 2 +-
damus/Models/NotificationsManager.swift | 9 +-
damus/Models/PushNotificationClient.swift | 105 ++++++++++++++++++
damus/Models/UserSettingsStore.swift | 33 ++++++
damus/Util/Constants.swift | 2 +
damus/Util/Router.swift | 2 +-
.../Settings/NotificationSettingsView.swift | 56 +++++++++-
damus/damusApp.swift | 48 +-------
12 files changed, 235 insertions(+), 53 deletions(-)
create mode 100644 damus/Models/PushNotificationClient.swift


base-commit: 46185c55d1dcc376842ca26e6ba47caa99a55b66
--
2.44.0


Daniel D’Aquino

unread,
May 21, 2024, 12:00:54 AMMay 21
to pat...@damus.io, Daniel D’Aquino
We do not have the ability to suppress push notifications yet (we are
waiting to receive the entitlement from Apple)

In the meantime, attempt to fallback gracefully where possible

Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
---
.../NotificationService.swift | 21 +++++++++++++++++--
1 file changed, 19 insertions(+), 2 deletions(-)

diff --git a/DamusNotificationService/NotificationService.swift b/DamusNotificationService/NotificationService.swift
index 4d3e8c46..b82a5e73 100644
--- a/DamusNotificationService/NotificationService.swift
+++ b/DamusNotificationService/NotificationService.swift
@@ -40,15 +40,32 @@ class NotificationService: UNNotificationServiceExtension {
return
}

+ // 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
+ if state.mutelist_manager.is_event_muted(nostr_event) {
+ // We cannot really suppress muted notifications until we have the notification supression entitlement.
+ // The best we can do if we ever get those muted notifications (which we generally won't due to server-side processing) is to obscure the details
+ let content = UNMutableNotificationContent()
+ content.title = NSLocalizedString("Muted event", comment: "Title for a push notification which has been muted")
+ content.body = NSLocalizedString("This is an event that has been muted according to your mute list rules. We cannot suppress this notification, but we obscured the details to respect your preferences", comment: "Description for a push notification which has been muted, and explanation that we cannot suppress it")
+ content.sound = UNNotificationSound.default
+ contentHandler(content)
+ return
+ }
+
guard should_display_notification(state: state, event: nostr_event) else {
// We should not display notification for this event. Suppress notification.
- contentHandler(UNNotificationContent())
+ // contentHandler(UNNotificationContent())
+ // TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
+ contentHandler(request.content)
return
}

guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
- contentHandler(UNNotificationContent())
+ // contentHandler(UNNotificationContent())
+ // TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
+ contentHandler(request.content)
return
}

--
2.44.0


Daniel D’Aquino

unread,
May 21, 2024, 12:00:59 AMMay 21
to pat...@damus.io, Daniel D’Aquino
This allows the user to switch between local and push notifications

Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
---
.../NotificationService.swift | 2 +-
damus/Models/HomeModel.swift | 2 +-
damus/Models/NotificationsManager.swift | 9 +++--
damus/Models/UserSettingsStore.swift | 33 +++++++++++++++++++
.../Settings/NotificationSettingsView.swift | 11 +++++++
damus/damusApp.swift | 2 +-
6 files changed, 54 insertions(+), 5 deletions(-)

diff --git a/DamusNotificationService/NotificationService.swift b/DamusNotificationService/NotificationService.swift
index b82a5e73..4c6601cb 100644
--- a/DamusNotificationService/NotificationService.swift
+++ b/DamusNotificationService/NotificationService.swift
@@ -53,7 +53,7 @@ class NotificationService: UNNotificationServiceExtension {
return
}

- guard should_display_notification(state: state, event: nostr_event) else {
+ guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
// We should not display notification for this event. Suppress notification.
// contentHandler(UNNotificationContent())
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift
index a4d95946..85798c9d 100644
--- a/damus/Models/HomeModel.swift
+++ b/damus/Models/HomeModel.swift
@@ -733,7 +733,7 @@ class HomeModel: ContactsDelegate {
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
notification_status.new_events = notifs

- guard should_display_notification(state: damus_state, event: ev),
+ guard should_display_notification(state: damus_state, event: ev, mode: .local),
let notification_object = generate_local_notification_object(from: ev, state: damus_state)
else {
return
diff --git a/damus/Models/NotificationsManager.swift b/damus/Models/NotificationsManager.swift
index 8fcaa554..c32ae753 100644
--- a/damus/Models/NotificationsManager.swift
+++ b/damus/Models/NotificationsManager.swift
@@ -13,7 +13,7 @@ import UIKit
let EVENT_MAX_AGE_FOR_NOTIFICATION: TimeInterval = 12 * 60 * 60

func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent) {
- guard should_display_notification(state: state, event: ev) else {
+ guard should_display_notification(state: state, event: ev, mode: .local) else {
// We should not display notification. Exit.
return
}
@@ -25,7 +25,12 @@ func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent)
create_local_notification(profiles: state.profiles, notify: local_notification)
}

-func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent) -> Bool {
+func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent, mode: UserSettingsStore.NotificationsMode) -> Bool {
+ // Do not show notification if it's coming from a mode different from the one selected by our user
+ guard state.settings.notifications_mode == mode else {
+ return false
+ }
+
if ev.known_kind == nil {
return false
}
diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift
index 8abaa257..5a211b31 100644
--- a/damus/Models/UserSettingsStore.swift
+++ b/damus/Models/UserSettingsStore.swift
@@ -155,6 +155,9 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "like_notification", default_value: true)
var like_notification: Bool

+ @StringSetting(key: "notifications_mode", default_value: .local)
+ var notifications_mode: NotificationsMode
+
@Setting(key: "notification_only_from_following", default_value: false)
var notification_only_from_following: Bool

@@ -326,6 +329,36 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "latest_contact_event_id", default_value: nil)
var latest_contact_event_id_hex: String?

+
+ // MARK: Helper types
+
+ enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
+ var id: String { self.rawValue }
+
+ func to_string() -> String {
+ return rawValue
+ }
+
+ init?(from string: String) {
+ guard let notifications_mode = NotificationsMode(rawValue: string) else {
+ return nil
+ }
+ self = notifications_mode
+ }
+
+ func text_description() -> String {
+ switch self {
+ case .local:
+ NSLocalizedString("Local", comment: "Option for notification mode setting: Local notification mode")
+ case .push:
+ NSLocalizedString("Push", comment: "Option for notification mode setting: Push notification mode")
+ }
+ }
+
+ case local
+ case push
+ }
+
}

func pk_setting_key(_ pubkey: Pubkey, key: String) -> String {
diff --git a/damus/Views/Settings/NotificationSettingsView.swift b/damus/Views/Settings/NotificationSettingsView.swift
index 237db5a9..0ffac04e 100644
--- a/damus/Views/Settings/NotificationSettingsView.swift
+++ b/damus/Views/Settings/NotificationSettingsView.swift
@@ -26,6 +26,17 @@ struct NotificationSettingsView: View {

var body: some View {
Form {
+ if settings.enable_experimental_push_notifications {
+ Picker(NSLocalizedString("Notifications mode", comment: "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)."),
+ selection: Binding($settings.notifications_mode)
+ ) {
+ ForEach(UserSettingsStore.NotificationsMode.allCases, id: \.self) { notification_mode in
+ Text(notification_mode.text_description())
+ .tag(notification_mode.rawValue)
+ }
+ }
+ }
+
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)
.toggleStyle(.switch)
diff --git a/damus/damusApp.swift b/damus/damusApp.swift
index acbf76ef..ebd4c72c 100644
--- a/damus/damusApp.swift
+++ b/damus/damusApp.swift
@@ -73,7 +73,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
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 {
+ if !settings.enable_experimental_push_notifications || settings.notifications_mode == .local {
return
}

--
2.44.0


Daniel D’Aquino

unread,
May 21, 2024, 12:01:02 AMMay 21
to pat...@damus.io, Daniel D’Aquino
This commit moves the device token logic to a new
PushNotificationClient, to move complexity from this specific feature
away from damusApp.swift

This commit also slightly improves the handling of device tokens, by
caching it on the client struct even if the user is using local
notifications, so that the device token can be sent to the server immediately after
switching to push notifications mode.

Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
---
damus.xcodeproj/project.pbxproj | 4 ++
damus/ContentView.swift | 2 +-
damus/Models/DamusState.swift | 2 +
damus/Models/PushNotificationClient.swift | 62 +++++++++++++++++++++++
damus/damusApp.swift | 48 ++----------------
5 files changed, 73 insertions(+), 45 deletions(-)
create mode 100644 damus/Models/PushNotificationClient.swift

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
index 8386ebe7..09103f60 100644
--- a/damus.xcodeproj/project.pbxproj
+++ b/damus.xcodeproj/project.pbxproj
@@ -615,6 +615,7 @@
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9462A9AD44700DC3548 /* NativeObject.swift */; };
D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B93D2A9AD44700DC3548 /* Message.swift */; };
D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
+ D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
D7EDED152B11776B0018B19C /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
@@ -1437,6 +1438,7 @@
D7CB5D5E2B11770C00AD4105 /* FollowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowState.swift; sourceTree = "<group>"; };
D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNotificationManagement.swift; sourceTree = "<group>"; };
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; };
+ D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = "<group>"; };
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
D7EDED1D2B11797D0018B19C /* LongformEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformEvent.swift; sourceTree = "<group>"; };
@@ -1663,6 +1665,7 @@
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */,
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
B533694D2B66D791008A805E /* MutelistManager.swift */,
+ D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -3291,6 +3294,7 @@
5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */,
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */,
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
+ D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */,
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
4CA352AC2A76C07F003BB08B /* NewUnmutesNotify.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
index b3846a57..a3bc5bd1 100644
--- a/damus/ContentView.swift
+++ b/damus/ContentView.swift
@@ -308,7 +308,7 @@ struct ContentView: View {
active_sheet = .onboardingSuggestions
hasSeenOnboardingSuggestions = true
}
- self.appDelegate?.settings = damus_state?.settings
+ self.appDelegate?.state = damus_state
}
.sheet(item: $active_sheet) { item in
switch item {
diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift
index 13679e45..2d42463a 100644
--- a/damus/Models/DamusState.swift
+++ b/damus/Models/DamusState.swift
@@ -36,6 +36,7 @@ class DamusState: HeadlessDamusState {
let video: VideoController
let ndb: Ndb
var purple: DamusPurple
+ var push_notification_client: PushNotificationClient

init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter) {
self.pool = pool
@@ -68,6 +69,7 @@ class DamusState: HeadlessDamusState {
keypair: keypair
)
self.quote_reposts = quote_reposts
+ self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
}

@discardableResult
diff --git a/damus/Models/PushNotificationClient.swift b/damus/Models/PushNotificationClient.swift
new file mode 100644
index 00000000..9f862896
--- /dev/null
+++ b/damus/Models/PushNotificationClient.swift
@@ -0,0 +1,62 @@
+//
+// PushNotificationClient.swift
+// damus
+//
+// Created by Daniel D’Aquino on 2024-05-17.
+//
+
+import Foundation
+
+struct PushNotificationClient {
+ let keypair: Keypair
+ let settings: UserSettingsStore
+ private(set) var device_token: Data? = nil
+
+ mutating func set_device_token(new_device_token: Data) async throws {
+ self.device_token = new_device_token
+ if settings.enable_experimental_push_notifications && settings.notifications_mode == .push {
+ try await self.send_token()
+ }
+ }
+
+ 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()
+
+ 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
+ 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()
+ }
+}
diff --git a/damus/damusApp.swift b/damus/damusApp.swift
index ebd4c72c..eedd7b52 100644
--- a/damus/damusApp.swift
+++ b/damus/damusApp.swift
@@ -54,14 +54,12 @@ 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
+ var state: DamusState? = nil

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
@@ -71,51 +69,13 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
}

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 || settings.notifications_mode == .local {
+ guard let state else {
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 = settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_RECEIVER_TEST_URL : Constants.DEVICE_TOKEN_RECEIVER_PRODUCTION_URL
- 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 {
+ try await state.push_notification_client.set_device_token(new_device_token: deviceToken)
}
-
- task.resume()
}

// Handle the notification in the foreground state
--
2.44.0


Daniel D’Aquino

unread,
May 21, 2024, 12:01:06 AMMay 21
to pat...@damus.io, Daniel D’Aquino
Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
---
damus/Models/PushNotificationClient.swift | 51 +++++++++++++----------
1 file changed, 29 insertions(+), 22 deletions(-)

diff --git a/damus/Models/PushNotificationClient.swift b/damus/Models/PushNotificationClient.swift
index 9f862896..ed3fad9a 100644
--- a/damus/Models/PushNotificationClient.swift
+++ b/damus/Models/PushNotificationClient.swift
@@ -33,30 +33,37 @@ struct PushNotificationClient {

// create post request
let url = self.settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_RECEIVER_TEST_URL : Constants.DEVICE_TOKEN_RECEIVER_PRODUCTION_URL
- 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)
+ let json_data = try JSONSerialization.data(withJSONObject: json)
+
+
+ let (data, response) = try await make_nip98_authenticated_request(
+ method: .post,
+ url: url,
+ payload: json_data,
+ payload_type: .json,
+ auth_keypair: self.keypair
+ )
+
+ if let httpResponse = response as? HTTPURLResponse {
+ switch httpResponse.statusCode {
+ case 200:
+ Log.info("Sent device token to Damus push notification server successfully", for: .push_notifications)
+ default:
+ Log.error("Error in sending device_token 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
+ }
+
+
+}
+
+// MARK: Helper structures

- task.resume()
+extension PushNotificationClient {
+ enum ClientError: Error {
+ case http_response_error(status_code: Int, response: Data)
}
}
--
2.44.0


Daniel D’Aquino

unread,
May 21, 2024, 12:01:09 AMMay 21
to pat...@damus.io, Daniel D’Aquino
Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
---
damus/Util/Router.swift | 2 +-
damus/Views/Settings/NotificationSettingsView.swift | 13 +++++++++++--
2 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift
index b737446b..928efcb5 100644
--- a/damus/Util/Router.swift
+++ b/damus/Util/Router.swift
@@ -79,7 +79,7 @@ enum Route: Hashable {
case .AppearanceSettings(let settings):
AppearanceSettingsView(damus_state: damusState, settings: settings)
case .NotificationSettings(let settings):
- NotificationSettingsView(settings: settings)
+ NotificationSettingsView(damus_state: damusState, settings: settings)
case .ZapSettings(let settings):
ZapSettingsView(settings: settings)
case .TranslationSettings(let settings):
diff --git a/damus/Views/Settings/NotificationSettingsView.swift b/damus/Views/Settings/NotificationSettingsView.swift
index 0ffac04e..0767fbe9 100644
--- a/damus/Views/Settings/NotificationSettingsView.swift
+++ b/damus/Views/Settings/NotificationSettingsView.swift
@@ -8,6 +8,7 @@
import SwiftUI

struct NotificationSettingsView: View {
+ let damus_state: DamusState
@ObservedObject var settings: UserSettingsStore

@Environment(\.dismiss) var dismiss
@@ -28,7 +29,15 @@ struct NotificationSettingsView: View {
Form {
if settings.enable_experimental_push_notifications {
Picker(NSLocalizedString("Notifications mode", comment: "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)."),
- selection: Binding($settings.notifications_mode)
+ selection: Binding(
+ get: { settings.notifications_mode },
+ set: { newValue in
+ settings.notifications_mode = newValue
+ if newValue == .push {
+ Task { try await damus_state.push_notification_client.send_token() }
+ }
+ }
+ )
) {
ForEach(UserSettingsStore.NotificationsMode.allCases, id: \.self) { notification_mode in
Text(notification_mode.text_description())
@@ -76,6 +85,6 @@ struct NotificationSettingsView: View {

struct NotificationSettings_Previews: PreviewProvider {
static var previews: some View {
- NotificationSettingsView(settings: UserSettingsStore())
+ NotificationSettingsView(damus_state: test_damus_state, settings: UserSettingsStore())
}
}
--
2.44.0


Daniel D’Aquino

unread,
May 21, 2024, 12:01:11 AMMay 21
to pat...@damus.io, Daniel D’Aquino
Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
---
damus/Models/PushNotificationClient.swift | 38 ++++++++++++++++++-
damus/Util/Constants.swift | 2 +
.../Settings/NotificationSettingsView.swift | 4 ++
3 files changed, 43 insertions(+), 1 deletion(-)

diff --git a/damus/Models/PushNotificationClient.swift b/damus/Models/PushNotificationClient.swift
index ed3fad9a..13e75758 100644
--- a/damus/Models/PushNotificationClient.swift
+++ b/damus/Models/PushNotificationClient.swift
@@ -57,7 +57,43 @@ struct PushNotificationClient {
return
}

-
+ 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()
+
+ 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 (data, response) = try await make_nip98_authenticated_request(
+ method: .post,
+ url: url,
+ payload: json_data,
+ payload_type: .json,
+ auth_keypair: self.keypair
+ )
+
+ if let httpResponse = response as? HTTPURLResponse {
+ switch httpResponse.statusCode {
+ case 200:
+ Log.info("Sent device token removal request to Damus push notification server successfully", for: .push_notifications)
+ default:
+ Log.error("Error in sending device_token removal 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
+ }
}

// MARK: Helper structures
diff --git a/damus/Util/Constants.swift b/damus/Util/Constants.swift
index 7e2207d4..a72ed0c3 100644
--- a/damus/Util/Constants.swift
+++ b/damus/Util/Constants.swift
@@ -12,6 +12,8 @@ class Constants {
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io: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: "https://notify.damus.io: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"

diff --git a/damus/Views/Settings/NotificationSettingsView.swift b/damus/Views/Settings/NotificationSettingsView.swift
index 0767fbe9..7af78d9a 100644
--- a/damus/Views/Settings/NotificationSettingsView.swift
+++ b/damus/Views/Settings/NotificationSettingsView.swift
@@ -36,12 +36,16 @@ struct NotificationSettingsView: View {
if newValue == .push {
Task { try await damus_state.push_notification_client.send_token() }
}
+ else {
+ Task { try await damus_state.push_notification_client.revoke_token() }
+ }
}
)
) {
ForEach(UserSettingsStore.NotificationsMode.allCases, id: \.self) { notification_mode in
Text(notification_mode.text_description())
.tag(notification_mode.rawValue)
+
}
}
}
--
2.44.0


Daniel D’Aquino

unread,
May 21, 2024, 12:01:15 AMMay 21
to pat...@damus.io, Daniel D’Aquino
Changing the notification mode setting requires successfully sending or
revoking the device token to the server. As this is an action that might
fail, it is important to have a clear UX feedback in case this fails.

Testing
--------

PASS

Device: iPhone 15 simulator
iOS: 17.4
Damus: This commit
strfry-push-notify: d6c2ff289c80e0a90874a7499ed6408394659fc9
Coverage:
1. Checked that push notification mode setting is invisible when experimental push notifications mode is disabled
2. Checked that push notification mode setting is visible when experimental push notifications mode is enabled
3. Checked that switching between push and local notifications sends requests to the server
4. Checked that switching to push notification mode will cause local notifications to be suppressed and push notifications will be sent to the APNS server
5. Checked that switching back to local notification mode will cause local notifications to be displayed, and push notifications will NOT be sent to APNS
6. Checked that if the API server is off, switching from local to push notification modes is not possible and shows an error to the user.
7. Checked that sending APNS payload to Apple's test APNS page will actually deliver the push notification successfully.

Closes: https://github.com/damus-io/damus/issues/1704
Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
---
.../Settings/NotificationSettingsView.swift | 62 ++++++++++++++-----
1 file changed, 46 insertions(+), 16 deletions(-)

diff --git a/damus/Views/Settings/NotificationSettingsView.swift b/damus/Views/Settings/NotificationSettingsView.swift
index 7af78d9a..f2711156 100644
--- a/damus/Views/Settings/NotificationSettingsView.swift
+++ b/damus/Views/Settings/NotificationSettingsView.swift
@@ -10,6 +10,7 @@ import SwiftUI
struct NotificationSettingsView: View {
let damus_state: DamusState
@ObservedObject var settings: UserSettingsStore
+ @State var notification_mode_setting_error: String? = nil

@Environment(\.dismiss) var dismiss

@@ -25,27 +26,56 @@ struct NotificationSettingsView: View {
})
}

+ func try_to_set_notifications_mode(new_value: UserSettingsStore.NotificationsMode) {
+ notification_mode_setting_error = nil
+ if new_value == .push {
+ Task {
+ do {
+ try await damus_state.push_notification_client.send_token()
+ settings.notifications_mode = new_value
+ }
+ catch {
+ notification_mode_setting_error = String(format: NSLocalizedString("Error configuring push notifications with the server: %@", comment: "Error label shown when user tries to enable push notifications but something fails"), error.localizedDescription)
+ }
+ }
+ }
+ else {
+ Task {
+ do {
+ try await damus_state.push_notification_client.revoke_token()
+ settings.notifications_mode = new_value
+ }
+ 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)
+ }
+ }
+ }
+ }
+
var body: some View {
Form {
if settings.enable_experimental_push_notifications {
- Picker(NSLocalizedString("Notifications mode", comment: "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)."),
- selection: Binding(
- get: { settings.notifications_mode },
- set: { newValue in
- settings.notifications_mode = newValue
- if newValue == .push {
- Task { try await damus_state.push_notification_client.send_token() }
- }
- else {
- Task { try await damus_state.push_notification_client.revoke_token() }
- }
+ Section(
+ header: Text("General", comment: "Section header for general damus notifications user configuration"),
+ footer: VStack {
+ if let notification_mode_setting_error {
+ Text(notification_mode_setting_error)
+ .foregroundStyle(.damusDangerPrimary)
}
- )
+ }
) {
- ForEach(UserSettingsStore.NotificationsMode.allCases, id: \.self) { notification_mode in
- Text(notification_mode.text_description())
- .tag(notification_mode.rawValue)
-
+ Picker(NSLocalizedString("Notifications mode", comment: "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)."),
+ selection: Binding(
+ get: { settings.notifications_mode },
+ set: { newValue in
+ self.try_to_set_notifications_mode(new_value: newValue)
+ }
+ )
+ ) {
+ ForEach(UserSettingsStore.NotificationsMode.allCases, id: \.self) { notification_mode in
+ Text(notification_mode.text_description())
+ .tag(notification_mode.rawValue)
+ }
}
}
}
--
2.44.0


William Casarin

unread,
May 21, 2024, 12:46:11 PMMay 21
to Daniel D’Aquino, pat...@damus.io
On Tue, May 21, 2024 at 04:00:43AM GMT, Daniel D’Aquino wrote:
>Hi Will,
>
>Here is a series of patches that adds a switch to the notification
>settings, along with the necessary logic changes and device token
>revocation necessary to support this feature.
>
>Added a few related changes as well:
>- Better handling of push notifications we cannot suppress
>- NIP-98 authentication support to avoid unauthorized changes to the push notification settings on the server
>- Some other code organization changes
>
>To accompany this, I will also send a patch series for the
>server-side changes necessary to support the new feature.
>
>Please let me know if you have any questions or concerns.
>
>Thank you,
>Daniel

Can you open up a PR for ci ?

Thanks!

William Casarin

unread,
May 21, 2024, 1:03:35 PMMay 21
to Daniel D’Aquino, pat...@damus.io
On Tue, May 21, 2024 at 04:00:48AM GMT, Daniel D’Aquino wrote:
>We do not have the ability to suppress push notifications yet (we are
>waiting to receive the entitlement from Apple)
>
>In the meantime, attempt to fallback gracefully where possible
>
>Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
>---

although this shouldn't happen now right? due to the new server-side
mutelist notification logic? I'm guessing this is more for when we have
encrypted mutes or if the server doesn't have the latest mutelist.

William Casarin

unread,
May 21, 2024, 2:25:21 PMMay 21
to Daniel D’Aquino, pat...@damus.io
On Tue, May 21, 2024 at 04:00:43AM GMT, Daniel D’Aquino wrote:
>Hi Will,
>
>Here is a series of patches that adds a switch to the notification
>settings, along with the necessary logic changes and device token
>revocation necessary to support this feature.
>
>Added a few related changes as well:
>- Better handling of push notifications we cannot suppress
>- NIP-98 authentication support to avoid unauthorized changes to the push notification settings on the server
>- Some other code organization changes
>
>To accompany this, I will also send a patch series for the
>server-side changes necessary to support the new feature.
>
>Please let me know if you have any questions or concerns.
>
>Thank you,
>Daniel

LGTM!

patchset

Reviewed-by: William Casarin <jb...@jb55.com>

William Casarin

unread,
May 21, 2024, 3:01:48 PMMay 21
to Daniel D’Aquino, pat...@damus.io
On Tue, May 21, 2024 at 04:00:51AM GMT, Daniel D’Aquino wrote:
>This allows the user to switch between local and push notifications
>
>Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
>---

Reviewed-by: William Casarin <jb...@jb55.com>

Daniel D'Aquino

unread,
May 24, 2024, 5:20:28 PMMay 24
to William Casarin, sembene_truestar via patches


> On May 21, 2024, at 10:03, William Casarin <jb...@jb55.com> wrote:
>
> On Tue, May 21, 2024 at 04:00:48AM GMT, Daniel D’Aquino wrote:
>> We do not have the ability to suppress push notifications yet (we are
>> waiting to receive the entitlement from Apple)
>>
>> In the meantime, attempt to fallback gracefully where possible
>>
>> Signed-off-by: Daniel D’Aquino <dan...@daquino.me>
>> ---
>
> although this shouldn't happen now right? due to the new server-side
> mutelist notification logic? I'm guessing this is more for when we have
> encrypted mutes or if the server doesn't have the latest mutelist.

You are correct, this should not happen. However, I think it is worth having this logic to have a sensible fallback in case the other mechanisms fail unexpectedly for whatever reason.
> To unsubscribe from this group and stop receiving emails from it, send an email to patches+u...@damus.io.
>


Daniel D'Aquino

unread,
May 24, 2024, 7:11:47 PMMay 24
to William Casarin, sembene_truestar via patches

On May 21, 2024, at 09:19, William Casarin <jb...@jb55.com> wrote:

On Tue, May 21, 2024 at 04:00:43AM GMT, Daniel D’Aquino wrote:
Hi Will,

Here is a series of patches that adds a switch to the notification
settings, along with the necessary logic changes and device token
revocation necessary to support this feature.

Added a few related changes as well:
- Better handling of push notifications we cannot suppress
- NIP-98 authentication support to avoid unauthorized changes to the push notification settings on the server
- Some other code organization changes

To accompany this, I will also send a patch series for the
server-side changes necessary to support the new feature.

Please let me know if you have any questions or concerns.

Thank you,
Daniel

Can you open up a PR for ci ?

Thanks!


I will wait for the results before pushing.

Daniel D'Aquino

unread,
May 24, 2024, 7:28:33 PMMay 24
to William Casarin, sembene_truestar via patches
On May 24, 2024, at 16:10, Daniel D'Aquino <dan...@daquino.me> wrote:

On May 21, 2024, at 09:19, William Casarin <jb...@jb55.com> wrote:

On Tue, May 21, 2024 at 04:00:43AM GMT, Daniel D’Aquino wrote:
Hi Will,

Here is a series of patches that adds a switch to the notification
settings, along with the necessary logic changes and device token
revocation necessary to support this feature.

Added a few related changes as well:
- Better handling of push notifications we cannot suppress
- NIP-98 authentication support to avoid unauthorized changes to the push notification settings on the server
- Some other code organization changes

To accompany this, I will also send a patch series for the
server-side changes necessary to support the new feature.

Please let me know if you have any questions or concerns.

Thank you,
Daniel

Can you open up a PR for ci ?

Thanks!

I will wait for the results before pushing.

CI checks are all passing, I will push

Daniel D'Aquino

unread,
May 24, 2024, 7:51:23 PMMay 24
to William Casarin, sembene_truestar via patches
On May 24, 2024, at 16:27, Daniel D'Aquino <dan...@daquino.me> wrote:

On May 24, 2024, at 16:10, Daniel D'Aquino <dan...@daquino.me> wrote:

On May 21, 2024, at 09:19, William Casarin <jb...@jb55.com> wrote:

On Tue, May 21, 2024 at 04:00:43AM GMT, Daniel D’Aquino wrote:
Hi Will,

Here is a series of patches that adds a switch to the notification
settings, along with the necessary logic changes and device token
revocation necessary to support this feature.

Added a few related changes as well:
- Better handling of push notifications we cannot suppress
- NIP-98 authentication support to avoid unauthorized changes to the push notification settings on the server
- Some other code organization changes

To accompany this, I will also send a patch series for the
server-side changes necessary to support the new feature.

Please let me know if you have any questions or concerns.

Thank you,
Daniel

Can you open up a PR for ci ?

Thanks!

I will wait for the results before pushing.

CI checks are all passing, I will push

Pushed to master, commits 5a68cfa448f6ecbb29a0d4aef3e4a288ab25178b..2c84184dbdcf0bb674c0e56c04c98c55584dc4e6
Reply all
Reply to author
Forward
0 new messages