Add experimental push notification support
I added support for the experimental push notifications feature. There are many improvements to be made, so this feature is currently opt-in only. If the user does not opt-in, their device tokens will not be sent out and thus they will receive no push notifications. We should perform more testing on real-life staging environments before fully releasing this feature. Testing ------- Testing was done gradually during development. Device: iOS simulators iOS: 17 Damus version: A few different but recent prototypes Rough coverage: 1. Checked that no device tokens are sent out when setting is off 2. Checked that I can successfully receive device tokens when feature is ON and set to localhost. 3. Checked sending test push notifications of types "note" (kind: 1), reaction (kind: 7) and DMs (kind 4) works and shows a generic but reasonable push notification message 4. Checked that clicking on the notifications above take the user to the correct screen Closes: https://github.com/damus-io/damus/issues/67 Changelog-Added: Add experimental push notification support Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
committed by
William Casarin
parent
878b1caa95
commit
ad75d8546c
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.damus</string>
|
||||
</array>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user