Skip to content

Commit 3e44507

Browse files
committed
Add AppDelegate and AccountStore implementation of Push notifications
1 parent 664abef commit 3e44507

File tree

9 files changed

+235
-0
lines changed

9 files changed

+235
-0
lines changed

Configs/GlobalConfig.xcconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
NIO_NAMESPACE = com.example.nio
1212
DEVELOPMENT_TEAM = Z123456789
13+
NIO_PUSHER_URL = sentinel.nio.chat
1314

1415
APPGROUP = $(NIO_NAMESPACE)
1516
PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).iOS

Nio.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
4BFEFD86246F414D00CCF4A0 /* NioShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4BFEFD7C246F414D00CCF4A0 /* NioShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
6363
4BFEFD8C246F458000CCF4A0 /* GetURL.js in Resources */ = {isa = PBXBuildFile; fileRef = 4BFEFD8B246F458000CCF4A0 /* GetURL.js */; };
6464
4BFEFD92246F686000CCF4A0 /* ShareContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFEFD91246F686000CCF4A0 /* ShareContentView.swift */; };
65+
9504FC0326CFD560007E89E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9504FC0226CFD560007E89E1 /* AppDelegate.swift */; };
6566
955A0D3D26BC1B310027D188 /* MXSession+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A0D3C26BC1B310027D188 /* MXSession+Async.swift */; };
6667
955A0D3E26BC1B310027D188 /* MXSession+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A0D3C26BC1B310027D188 /* MXSession+Async.swift */; };
6768
955A0D4026BC1BCD0027D188 /* Continuation+MX.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A0D3F26BC1BCD0027D188 /* Continuation+MX.swift */; };
@@ -374,6 +375,7 @@
374375
4BFEFD8F246F5EE000CCF4A0 /* NioShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NioShareExtension.entitlements; sourceTree = "<group>"; };
375376
4BFEFD90246F5EF500CCF4A0 /* Nio.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Nio.entitlements; sourceTree = "<group>"; };
376377
4BFEFD91246F686000CCF4A0 /* ShareContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContentView.swift; sourceTree = "<group>"; };
378+
9504FC0226CFD560007E89E1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
377379
955A0D3C26BC1B310027D188 /* MXSession+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MXSession+Async.swift"; sourceTree = "<group>"; };
378380
955A0D3F26BC1BCD0027D188 /* Continuation+MX.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Continuation+MX.swift"; sourceTree = "<group>"; };
379381
955A0D4226BC1E2C0027D188 /* MXAutoDiscovery+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MXAutoDiscovery+Async.swift"; sourceTree = "<group>"; };
@@ -688,6 +690,7 @@
688690
390D63BA246F4BEE00B8F640 /* Resources */,
689691
39C931F3238449C2004449E1 /* Supporting Files */,
690692
39C931E42384328B004449E1 /* Preview Content */,
693+
9504FC0226CFD560007E89E1 /* AppDelegate.swift */,
691694
);
692695
path = Nio;
693696
sourceTree = "<group>";
@@ -1318,6 +1321,7 @@
13181321
3902B8A52395A77800698B87 /* LoadingView.swift in Sources */,
13191322
CADF662424614A3300F5063F /* ReactionGroupView.swift in Sources */,
13201323
392221B6243F88FD004D8794 /* RoomNameEventView.swift in Sources */,
1324+
9504FC0326CFD560007E89E1 /* AppDelegate.swift in Sources */,
13211325
4B0A2E47245E2EF800A79443 /* MultilineTextField.swift in Sources */,
13221326
A51F762C25D6E9950061B4A4 /* MessageTextViewWrapper.swift in Sources */,
13231327
A51BF8CE254C2FE5000FB0A4 /* NioApp.swift in Sources */,

Nio/AppDelegate.swift

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//
2+
// AppDelegate.swift
3+
// AppDelegate
4+
//
5+
// Created by Finn Behrens on 20.08.21.
6+
// Copyright © 2021 Kilian Koeltzsch. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import UIKit
11+
12+
import MatrixSDK
13+
14+
import NioKit
15+
16+
@MainActor
17+
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
18+
public static var shared = AppDelegate();
19+
20+
var isPushAllowed: Bool = false
21+
22+
@Published
23+
var deviceToken: String?
24+
25+
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
26+
Self.shared = self
27+
let notificationCenter = UNUserNotificationCenter.current()
28+
notificationCenter.delegate = self
29+
return true
30+
}
31+
32+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
33+
let notificationCenter = UNUserNotificationCenter.current()
34+
35+
self.createMessageActions(notificationCenter: notificationCenter)
36+
37+
Task.init(priority: .userInitiated) {
38+
do {
39+
let state = try await notificationCenter.requestAuthorization(options: [.badge, .sound, .alert])
40+
self.isPushAllowed = state
41+
application.registerForRemoteNotifications()
42+
} catch {
43+
print("error requesting UNUserNotificationCenter: \(error.localizedDescription)")
44+
}
45+
46+
// todo: requestSiriAuthorization()
47+
}
48+
49+
return true
50+
}
51+
52+
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
53+
let tokenString = deviceToken.reduce("", {$0 + String(format: "%02X", $1)})
54+
print("remote notifications token: \(tokenString)")
55+
self.deviceToken = deviceToken.base64EncodedString()
56+
// FIXME: dispatch a background process to set the token
57+
}
58+
59+
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
60+
// TODO: show notification to user
61+
print("error registering token: \(error.localizedDescription)")
62+
}
63+
}
64+
65+
extension AppDelegate: UNUserNotificationCenterDelegate {
66+
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
67+
// TODO: render app specific banner instead of os banner
68+
// TODO: skip if the notification is for the current shown room
69+
// TODO: special rendering for the preferences notifications
70+
return [.banner, .sound]
71+
}
72+
73+
// prepare notification actions
74+
func createMessageActions(notificationCenter: UNUserNotificationCenter) {
75+
let likeAction = UNNotificationAction(
76+
identifier: "chat.nio.reaction.emoji.like",
77+
title: "like",
78+
options: [],
79+
icon: UNNotificationActionIcon(systemImageName: "hand.thumbsup")
80+
)
81+
82+
// TODO: decide if dislike is a desctructive action, and should get the os tag for desctructive
83+
let dislikeAction = UNNotificationAction(
84+
identifier: "chat.nio.reaction.emoji.dislike",
85+
title: "dislike",
86+
options: [],
87+
icon: UNNotificationActionIcon(systemImageName: "hand.thumbsdown")
88+
)
89+
90+
let replyAction = UNTextInputNotificationAction(
91+
identifier: "chat.nio.reaction.msg",
92+
title: "Message",
93+
options: .authenticationRequired,
94+
icon: UNNotificationActionIcon(systemImageName: "text.bubble"),
95+
textInputButtonTitle: "Reply",
96+
textInputPlaceholder: "Message"
97+
)
98+
99+
let messageReplyAction = UNNotificationCategory(
100+
identifier: "chat.nio.message.reply",
101+
actions: [likeAction, dislikeAction, replyAction],
102+
intentIdentifiers: [],
103+
options: [.allowInCarPlay, .hiddenPreviewsShowTitle]
104+
)
105+
106+
notificationCenter.setNotificationCategories([messageReplyAction])
107+
}
108+
}

Nio/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>NioPusherUrl</key>
6+
<string>$(NIO_PUSHER_URL)</string>
57
<key>AppGroup</key>
68
<string>$(APPGROUP)</string>
79
<key>CFBundleDevelopmentRegion</key>

Nio/Nio.entitlements

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>aps-environment</key>
6+
<string>development</string>
7+
<key>com.apple.developer.usernotifications.communication</key>
8+
<true/>
59
<key>com.apple.security.app-sandbox</key>
610
<true/>
711
<key>com.apple.security.application-groups</key>

Nio/NioApp.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import NioKit
33

44
@main
55
struct NioApp: App {
6+
#if os(iOS)
7+
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
8+
#endif
9+
610
@StateObject private var accountStore = AccountStore()
711

812
@AppStorage("accentColor") private var accentColor: Color = .purple

Nio/Settings/SettingsView.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,45 @@ private struct MacSettingsView: View {
5555
}
5656

5757
private struct SettingsView: View {
58+
@EnvironmentObject var store: AccountStore
59+
5860
@AppStorage("accentColor") private var accentColor: Color = .purple
61+
@AppStorage("showDeveloperSettings") private var showDeveloperSettings = false
62+
5963
@StateObject private var appIconTitle = AppIconTitle()
6064
let logoutAction: () -> Void
6165

66+
private let bundleVersion = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as! String
67+
private let pusherUrl = Bundle.main.object(forInfoDictionaryKey: "NioPusherUrl") as? String
68+
6269
@Environment(\.presentationMode) private var presentationMode
6370

71+
/// Update the pusher config for the accountStore
72+
private func updatePusher() {
73+
Task(priority: .userInitiated) {
74+
guard let deviceToken = AppDelegate.shared.deviceToken else {
75+
// TODO: show banner informing of missing token
76+
print("missing deviceToken")
77+
return
78+
}
79+
80+
guard let pusherUrl = pusherUrl else {
81+
// should never happen
82+
print("pusherUrl not set")
83+
return
84+
}
85+
86+
do {
87+
try await store.setPusher(url: pusherUrl, deviceToken: deviceToken)
88+
} catch {
89+
// TODO: inform of failure
90+
print("failed to update pusher: \(error.localizedDescription)")
91+
}
92+
print("pusher updated")
93+
// TODO: inform of success
94+
}
95+
}
96+
6497
var body: some View {
6598
NavigationView {
6699
Form {
@@ -87,6 +120,22 @@ private struct SettingsView: View {
87120
Text(verbatim: L10n.Settings.logOut)
88121
}
89122
}
123+
124+
Section("Version") {
125+
Text(bundleVersion)
126+
}
127+
.onTapGesture {
128+
showDeveloperSettings.toggle()
129+
// TODO: show banner informing of activated developer settings
130+
}
131+
132+
if showDeveloperSettings {
133+
Section("Developer") {
134+
Button(action: updatePusher) {
135+
Text("Refresh pusher config")
136+
}.disabled(pusherUrl == nil || (pusherUrl?.isEmpty ?? true))
137+
}
138+
}
90139
}
91140
.navigationBarTitle(L10n.Settings.title, displayMode: .inline)
92141
.toolbar {

NioKit/Extensions/MXRestClient+Async.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,26 @@ extension MXRestClient {
2020
self.wellKnow({continuation.resume(returning: $0!)}, failure: {continuation.resume(throwing: $0!)})
2121
}
2222
}
23+
24+
func pushers() async throws -> [MXPusher] {
25+
return try await withCheckedThrowingContinuation {continuation in
26+
self.pushers({ continuation.resume(returning: $0 ?? []) }, failure: { continuation.resume(throwing: $0!) })
27+
}
28+
}
29+
30+
func setPusher(
31+
pushKey: String,
32+
kind: MXPusherKind,
33+
appId: String,
34+
appDisplayName: String,
35+
deviceDisplayName: String,
36+
profileTag: String,
37+
lang: String,
38+
data: [String: Any],
39+
append: Bool
40+
) async throws {
41+
return try await withCheckedThrowingContinuation {continuation in
42+
self.setPusher(pushKey: pushKey, kind: kind, appId: appId, appDisplayName: appDisplayName, deviceDisplayName: deviceDisplayName, profileTag: profileTag, lang: lang, data: data, append: append, completion: { continuation.resume(with: $0) })
43+
}
44+
}
2345
}

NioKit/Session/AccountStore.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,50 @@ public class AccountStore: ObservableObject {
156156
self.objectWillChange.send()
157157
}
158158
}
159+
160+
public func setPusher(url: String, enable: Bool = true, deviceToken: String) async throws {
161+
guard let session = session else {
162+
throw AccountStoreError.noSession
163+
}
164+
165+
let appId = Bundle.main.bundleIdentifier ?? "nio.chat"
166+
let lang = NSLocale.preferredLanguages.first ?? "en-US"
167+
168+
// TODO: generate a pusher profile and use it, instead of a (hopefully) not existing tag
169+
let profileTag = "gloaable"
170+
171+
let data: [String: Any] = [
172+
"url": "https://\(url)/_matrix/push/v1/notify",
173+
"format": "event_id_only",
174+
"default_payload": [
175+
"aps": [
176+
"mutable-content": 1,
177+
"content-available": 1,
178+
// TODO: add acount info, if we ever enable multi accounting
179+
"alert": [
180+
"loc-key": "MESSAGE",
181+
"loc-args": [],
182+
]
183+
]
184+
]
185+
]
186+
187+
try await session.matrixRestClient.setPusher(
188+
pushKey: deviceToken,
189+
kind: enable ? .http : .none,
190+
appId: appId,
191+
appDisplayName: "Nio",
192+
deviceDisplayName: "Nio iOS",
193+
profileTag: profileTag,
194+
lang: lang,
195+
data: data,
196+
append: false
197+
)
198+
}
159199
}
160200

161201
enum AccountStoreError: Error {
162202
case noCredentials
203+
case noSession
163204
case invalidUrl
164205
}

0 commit comments

Comments
 (0)