Skip to content

Commit

Permalink
Add AppDelegate and AccountStore implementation of Push notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
kloenk committed Aug 20, 2021
1 parent 664abef commit 3e44507
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 0 deletions.
1 change: 1 addition & 0 deletions Configs/GlobalConfig.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

NIO_NAMESPACE = com.example.nio
DEVELOPMENT_TEAM = Z123456789
NIO_PUSHER_URL = sentinel.nio.chat

APPGROUP = $(NIO_NAMESPACE)
PRODUCT_BUNDLE_IDENTIFIER = $(NIO_NAMESPACE).iOS
Expand Down
4 changes: 4 additions & 0 deletions Nio.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
4BFEFD86246F414D00CCF4A0 /* NioShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4BFEFD7C246F414D00CCF4A0 /* NioShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4BFEFD8C246F458000CCF4A0 /* GetURL.js in Resources */ = {isa = PBXBuildFile; fileRef = 4BFEFD8B246F458000CCF4A0 /* GetURL.js */; };
4BFEFD92246F686000CCF4A0 /* ShareContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFEFD91246F686000CCF4A0 /* ShareContentView.swift */; };
9504FC0326CFD560007E89E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9504FC0226CFD560007E89E1 /* AppDelegate.swift */; };
955A0D3D26BC1B310027D188 /* MXSession+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A0D3C26BC1B310027D188 /* MXSession+Async.swift */; };
955A0D3E26BC1B310027D188 /* MXSession+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A0D3C26BC1B310027D188 /* MXSession+Async.swift */; };
955A0D4026BC1BCD0027D188 /* Continuation+MX.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A0D3F26BC1BCD0027D188 /* Continuation+MX.swift */; };
Expand Down Expand Up @@ -374,6 +375,7 @@
4BFEFD8F246F5EE000CCF4A0 /* NioShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NioShareExtension.entitlements; sourceTree = "<group>"; };
4BFEFD90246F5EF500CCF4A0 /* Nio.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Nio.entitlements; sourceTree = "<group>"; };
4BFEFD91246F686000CCF4A0 /* ShareContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContentView.swift; sourceTree = "<group>"; };
9504FC0226CFD560007E89E1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
955A0D3C26BC1B310027D188 /* MXSession+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MXSession+Async.swift"; sourceTree = "<group>"; };
955A0D3F26BC1BCD0027D188 /* Continuation+MX.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Continuation+MX.swift"; sourceTree = "<group>"; };
955A0D4226BC1E2C0027D188 /* MXAutoDiscovery+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MXAutoDiscovery+Async.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -688,6 +690,7 @@
390D63BA246F4BEE00B8F640 /* Resources */,
39C931F3238449C2004449E1 /* Supporting Files */,
39C931E42384328B004449E1 /* Preview Content */,
9504FC0226CFD560007E89E1 /* AppDelegate.swift */,
);
path = Nio;
sourceTree = "<group>";
Expand Down Expand Up @@ -1318,6 +1321,7 @@
3902B8A52395A77800698B87 /* LoadingView.swift in Sources */,
CADF662424614A3300F5063F /* ReactionGroupView.swift in Sources */,
392221B6243F88FD004D8794 /* RoomNameEventView.swift in Sources */,
9504FC0326CFD560007E89E1 /* AppDelegate.swift in Sources */,
4B0A2E47245E2EF800A79443 /* MultilineTextField.swift in Sources */,
A51F762C25D6E9950061B4A4 /* MessageTextViewWrapper.swift in Sources */,
A51BF8CE254C2FE5000FB0A4 /* NioApp.swift in Sources */,
Expand Down
108 changes: 108 additions & 0 deletions Nio/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// AppDelegate.swift
// AppDelegate
//
// Created by Finn Behrens on 20.08.21.
// Copyright © 2021 Kilian Koeltzsch. All rights reserved.
//

import Foundation
import UIKit

import MatrixSDK

import NioKit

@MainActor
class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
public static var shared = AppDelegate();

var isPushAllowed: Bool = false

@Published
var deviceToken: String?

func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
Self.shared = self
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
return true
}

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
let notificationCenter = UNUserNotificationCenter.current()

self.createMessageActions(notificationCenter: notificationCenter)

Task.init(priority: .userInitiated) {
do {
let state = try await notificationCenter.requestAuthorization(options: [.badge, .sound, .alert])
self.isPushAllowed = state
application.registerForRemoteNotifications()
} catch {
print("error requesting UNUserNotificationCenter: \(error.localizedDescription)")
}

// todo: requestSiriAuthorization()
}

return true
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.reduce("", {$0 + String(format: "%02X", $1)})
print("remote notifications token: \(tokenString)")
self.deviceToken = deviceToken.base64EncodedString()
// FIXME: dispatch a background process to set the token
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
// TODO: show notification to user
print("error registering token: \(error.localizedDescription)")
}
}

extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
// TODO: render app specific banner instead of os banner
// TODO: skip if the notification is for the current shown room
// TODO: special rendering for the preferences notifications
return [.banner, .sound]
}

// prepare notification actions
func createMessageActions(notificationCenter: UNUserNotificationCenter) {
let likeAction = UNNotificationAction(
identifier: "chat.nio.reaction.emoji.like",
title: "like",
options: [],
icon: UNNotificationActionIcon(systemImageName: "hand.thumbsup")
)

// TODO: decide if dislike is a desctructive action, and should get the os tag for desctructive
let dislikeAction = UNNotificationAction(
identifier: "chat.nio.reaction.emoji.dislike",
title: "dislike",
options: [],
icon: UNNotificationActionIcon(systemImageName: "hand.thumbsdown")
)

let replyAction = UNTextInputNotificationAction(
identifier: "chat.nio.reaction.msg",
title: "Message",
options: .authenticationRequired,
icon: UNNotificationActionIcon(systemImageName: "text.bubble"),
textInputButtonTitle: "Reply",
textInputPlaceholder: "Message"
)

let messageReplyAction = UNNotificationCategory(
identifier: "chat.nio.message.reply",
actions: [likeAction, dislikeAction, replyAction],
intentIdentifiers: [],
options: [.allowInCarPlay, .hiddenPreviewsShowTitle]
)

notificationCenter.setNotificationCategories([messageReplyAction])
}
}
2 changes: 2 additions & 0 deletions Nio/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NioPusherUrl</key>
<string>$(NIO_PUSHER_URL)</string>
<key>AppGroup</key>
<string>$(APPGROUP)</string>
<key>CFBundleDevelopmentRegion</key>
Expand Down
4 changes: 4 additions & 0 deletions Nio/Nio.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.usernotifications.communication</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
Expand Down
4 changes: 4 additions & 0 deletions Nio/NioApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import NioKit

@main
struct NioApp: App {
#if os(iOS)
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#endif

@StateObject private var accountStore = AccountStore()

@AppStorage("accentColor") private var accentColor: Color = .purple
Expand Down
49 changes: 49 additions & 0 deletions Nio/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,45 @@ private struct MacSettingsView: View {
}

private struct SettingsView: View {
@EnvironmentObject var store: AccountStore

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

@StateObject private var appIconTitle = AppIconTitle()
let logoutAction: () -> Void

private let bundleVersion = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as! String
private let pusherUrl = Bundle.main.object(forInfoDictionaryKey: "NioPusherUrl") as? String

@Environment(\.presentationMode) private var presentationMode

/// Update the pusher config for the accountStore
private func updatePusher() {
Task(priority: .userInitiated) {
guard let deviceToken = AppDelegate.shared.deviceToken else {
// TODO: show banner informing of missing token
print("missing deviceToken")
return
}

guard let pusherUrl = pusherUrl else {
// should never happen
print("pusherUrl not set")
return
}

do {
try await store.setPusher(url: pusherUrl, deviceToken: deviceToken)
} catch {
// TODO: inform of failure
print("failed to update pusher: \(error.localizedDescription)")
}
print("pusher updated")
// TODO: inform of success
}
}

var body: some View {
NavigationView {
Form {
Expand All @@ -87,6 +120,22 @@ private struct SettingsView: View {
Text(verbatim: L10n.Settings.logOut)
}
}

Section("Version") {
Text(bundleVersion)
}
.onTapGesture {
showDeveloperSettings.toggle()
// TODO: show banner informing of activated developer settings
}

if showDeveloperSettings {
Section("Developer") {
Button(action: updatePusher) {
Text("Refresh pusher config")
}.disabled(pusherUrl == nil || (pusherUrl?.isEmpty ?? true))
}
}
}
.navigationBarTitle(L10n.Settings.title, displayMode: .inline)
.toolbar {
Expand Down
22 changes: 22 additions & 0 deletions NioKit/Extensions/MXRestClient+Async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,26 @@ extension MXRestClient {
self.wellKnow({continuation.resume(returning: $0!)}, failure: {continuation.resume(throwing: $0!)})
}
}

func pushers() async throws -> [MXPusher] {
return try await withCheckedThrowingContinuation {continuation in
self.pushers({ continuation.resume(returning: $0 ?? []) }, failure: { continuation.resume(throwing: $0!) })
}
}

func setPusher(
pushKey: String,
kind: MXPusherKind,
appId: String,
appDisplayName: String,
deviceDisplayName: String,
profileTag: String,
lang: String,
data: [String: Any],
append: Bool
) async throws {
return try await withCheckedThrowingContinuation {continuation in
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) })
}
}
}
41 changes: 41 additions & 0 deletions NioKit/Session/AccountStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,50 @@ public class AccountStore: ObservableObject {
self.objectWillChange.send()
}
}

public func setPusher(url: String, enable: Bool = true, deviceToken: String) async throws {
guard let session = session else {
throw AccountStoreError.noSession
}

let appId = Bundle.main.bundleIdentifier ?? "nio.chat"
let lang = NSLocale.preferredLanguages.first ?? "en-US"

// TODO: generate a pusher profile and use it, instead of a (hopefully) not existing tag
let profileTag = "gloaable"

let data: [String: Any] = [
"url": "https://\(url)/_matrix/push/v1/notify",
"format": "event_id_only",
"default_payload": [
"aps": [
"mutable-content": 1,
"content-available": 1,
// TODO: add acount info, if we ever enable multi accounting
"alert": [
"loc-key": "MESSAGE",
"loc-args": [],
]
]
]
]

try await session.matrixRestClient.setPusher(
pushKey: deviceToken,
kind: enable ? .http : .none,
appId: appId,
appDisplayName: "Nio",
deviceDisplayName: "Nio iOS",
profileTag: profileTag,
lang: lang,
data: data,
append: false
)
}
}

enum AccountStoreError: Error {
case noCredentials
case noSession
case invalidUrl
}

0 comments on commit 3e44507

Please sign in to comment.