Skip to content

Commit

Permalink
Notifications are now handled when the app is in a killed state (#801)
Browse files Browse the repository at this point in the history
* WIP, added support for receiver id and managed a way to store the app router state

* WIP, added support for receiver id and managed a way to store the app router state

* Notification Manager is now becoming the UNUserNotificationDelegate ASAP

* code improvements + changelog

* fixed Unit Tests

* pr suggestions
  • Loading branch information
Velin92 authored Apr 14, 2023
1 parent 41994ba commit 3d0d883
Show file tree
Hide file tree
Showing 19 changed files with 337 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@
"version" : "1.0.0"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984",
"version" : "0.14.1"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
Expand All @@ -160,15 +169,33 @@
"version" : "1.0.2"
}
},
{
"identity" : "swift-parsing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-parsing",
"state" : {
"revision" : "c6e2241daa46e5c6e5027a93b161bca6ba692bcc",
"version" : "0.12.0"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
"state" : {
"revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334",
"version" : "1.11.0"
}
},
{
"identity" : "swift-url-routing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-url-routing",
"state" : {
"revision" : "2f4f0404b3de0a0711feb7190f724d8a80bc1cfd",
"version" : "0.5.0"
}
},
{
"identity" : "swiftstate",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -204,6 +231,15 @@
"revision" : "1fe824b80d89201652e7eca7c9252269a1d85e25",
"version" : "2.0.1"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "ab8c9f45843694dd16be4297e6d44c0634fd9913",
"version" : "0.8.4"
}
}
],
"version" : 2
Expand Down
77 changes: 43 additions & 34 deletions ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ class AppCoordinator: AppCoordinatorProtocol {

private var userSessionCancellables = Set<AnyCancellable>()
private var cancellables = Set<AnyCancellable>()
private(set) var notificationManager: NotificationManagerProtocol?
let notificationManager: NotificationManagerProtocol

@Consumable private var storedAppRoute: AppRoute?

init() {
navigationRootCoordinator = NavigationRootCoordinator()
Expand All @@ -66,6 +68,10 @@ class AppCoordinator: AppCoordinatorProtocol {
}

userSessionStore = UserSessionStore(backgroundTaskService: backgroundTaskService)

notificationManager = NotificationManager()
notificationManager.delegate = self
notificationManager.start()

guard let currentVersion = Version(InfoPlistReader(bundle: .main).bundleShortVersionString) else {
fatalError("The app's version number **must** use semver for migration purposes.")
Expand Down Expand Up @@ -259,6 +265,10 @@ class AppCoordinator: AppCoordinatorProtocol {
self.userSessionFlowCoordinator = userSessionFlowCoordinator

navigationRootCoordinator.setRootCoordinator(navigationSplitCoordinator)

if let storedAppRoute {
userSessionFlowCoordinator.handleAppRoute(storedAppRoute)
}
}

private func logout(isSoft: Bool) {
Expand Down Expand Up @@ -292,11 +302,10 @@ class AppCoordinator: AppCoordinatorProtocol {
userSession = nil

userSessionFlowCoordinator = nil

notificationManager?.delegate = nil
notificationManager = nil
}

notificationManager.setClientProxy(nil)
}

private func presentSplashScreen(isSoftLogout: Bool = false) {
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())

Expand All @@ -308,34 +317,23 @@ class AppCoordinator: AppCoordinatorProtocol {
}

private func configureNotificationManager() {
guard ServiceLocator.shared.settings.enableNotifications else {
return
}
guard notificationManager == nil else {
return
}

let manager = NotificationManager(clientProxy: userSession.clientProxy)
if manager.isAvailable {
manager.delegate = self
notificationManager = manager
manager.start()

if let appDelegate = AppDelegate.shared {
appDelegate.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
switch callback {
case .registeredNotifications(let deviceToken):
Task { await self?.notificationManager?.register(with: deviceToken) }
case .failedToRegisteredNotifications(let error):
self?.notificationManager?.registrationFailed(with: error)
}
notificationManager.setClientProxy(userSession.clientProxy)
notificationManager.requestAuthorization()

if let appDelegate = AppDelegate.shared {
appDelegate.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
switch callback {
case .registeredNotifications(let deviceToken):
Task { await self?.notificationManager.register(with: deviceToken) }
case .failedToRegisteredNotifications(let error):
self?.notificationManager.registrationFailed(with: error)
}
.store(in: &cancellables)
} else {
MXLog.error("Couldn't register to AppDelegate callbacks")
}
}
.store(in: &cancellables)
} else {
MXLog.error("Couldn't register to AppDelegate callbacks")
}
}

Expand Down Expand Up @@ -437,6 +435,14 @@ class AppCoordinator: AppCoordinatorProtocol {
}
}.store(in: &cancellables)
}

private func handleAppRoute(_ appRoute: AppRoute) {
if let userSessionFlowCoordinator {
userSessionFlowCoordinator.handleAppRoute(appRoute)
} else {
storedAppRoute = appRoute
}
}
}

// MARK: - AuthenticationCoordinatorDelegate
Expand Down Expand Up @@ -472,11 +478,14 @@ extension AppCoordinator: NotificationManagerDelegate {
MXLog.info("[AppCoordinator] tappedNotification")

// We store the room identifier into the thread identifier
guard !content.threadIdentifier.isEmpty else {
guard !content.threadIdentifier.isEmpty,
content.receiverID != nil else {
return
}

userSessionFlowCoordinator?.handleAppRoute(.room(roomID: content.threadIdentifier))
// Handle here the account switching when available

handleAppRoute(.room(roomID: content.threadIdentifier))
}

func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async {
Expand Down
2 changes: 1 addition & 1 deletion ElementX/Sources/Application/AppCoordinatorProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
import Foundation

protocol AppCoordinatorProtocol: CoordinatorProtocol {
var notificationManager: NotificationManagerProtocol? { get }
var notificationManager: NotificationManagerProtocol { get }
}
4 changes: 1 addition & 3 deletions ElementX/Sources/Application/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ final class AppSettings: ObservableObject {
}

let pushGatewayBaseURL = URL(staticString: "https://matrix.org/_matrix/push/v1/notify")

let enableNotifications = true


// MARK: - Bug report

let bugReportServiceBaseURL = URL(staticString: "https://riot.im/bugreports")
Expand Down
34 changes: 34 additions & 0 deletions ElementX/Sources/Application/Navigation/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,40 @@

import Foundation

import URLRouting

enum AppRoute {
case room(roomID: String)
}

struct AppRouterManager {
private let deeplinkRouter = OneOf {
Route(.case(AppRoute.room(roomID:))) {
// Check with product if this is the expect path
Path { "room" }
Query {
Field("id") { Parse(.string) }
}
}
}

private let permalinkRouter = OneOf {
Route(.case(AppRoute.room(roomID:))) {
Host("matrix.to")
Path {
"#"
Parse(.string)
}
}
}

func route(from url: URL) -> AppRoute? {
var route: AppRoute?
if let deeplinkRoute = try? deeplinkRouter.match(url: url) {
route = deeplinkRoute
} else if let permalinkRoute = try? permalinkRouter.match(url: url) {
route = permalinkRoute
}
return route
}
}
97 changes: 97 additions & 0 deletions ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,103 @@ class BugReportServiceMock: BugReportServiceProtocol {
}
}
}
class NotificationManagerMock: NotificationManagerProtocol {
var delegate: NotificationManagerDelegate?

//MARK: - start

var startCallsCount = 0
var startCalled: Bool {
return startCallsCount > 0
}
var startClosure: (() -> Void)?

func start() {
startCallsCount += 1
startClosure?()
}
//MARK: - register

var registerWithCallsCount = 0
var registerWithCalled: Bool {
return registerWithCallsCount > 0
}
var registerWithReceivedDeviceToken: Data?
var registerWithReceivedInvocations: [Data] = []
var registerWithReturnValue: Bool!
var registerWithClosure: ((Data) async -> Bool)?

func register(with deviceToken: Data) async -> Bool {
registerWithCallsCount += 1
registerWithReceivedDeviceToken = deviceToken
registerWithReceivedInvocations.append(deviceToken)
if let registerWithClosure = registerWithClosure {
return await registerWithClosure(deviceToken)
} else {
return registerWithReturnValue
}
}
//MARK: - registrationFailed

var registrationFailedWithCallsCount = 0
var registrationFailedWithCalled: Bool {
return registrationFailedWithCallsCount > 0
}
var registrationFailedWithReceivedError: Error?
var registrationFailedWithReceivedInvocations: [Error] = []
var registrationFailedWithClosure: ((Error) -> Void)?

func registrationFailed(with error: Error) {
registrationFailedWithCallsCount += 1
registrationFailedWithReceivedError = error
registrationFailedWithReceivedInvocations.append(error)
registrationFailedWithClosure?(error)
}
//MARK: - showLocalNotification

var showLocalNotificationWithSubtitleCallsCount = 0
var showLocalNotificationWithSubtitleCalled: Bool {
return showLocalNotificationWithSubtitleCallsCount > 0
}
var showLocalNotificationWithSubtitleReceivedArguments: (title: String, subtitle: String?)?
var showLocalNotificationWithSubtitleReceivedInvocations: [(title: String, subtitle: String?)] = []
var showLocalNotificationWithSubtitleClosure: ((String, String?) async -> Void)?

func showLocalNotification(with title: String, subtitle: String?) async {
showLocalNotificationWithSubtitleCallsCount += 1
showLocalNotificationWithSubtitleReceivedArguments = (title: title, subtitle: subtitle)
showLocalNotificationWithSubtitleReceivedInvocations.append((title: title, subtitle: subtitle))
await showLocalNotificationWithSubtitleClosure?(title, subtitle)
}
//MARK: - setClientProxy

var setClientProxyCallsCount = 0
var setClientProxyCalled: Bool {
return setClientProxyCallsCount > 0
}
var setClientProxyReceivedClientProxy: ClientProxyProtocol?
var setClientProxyReceivedInvocations: [ClientProxyProtocol?] = []
var setClientProxyClosure: ((ClientProxyProtocol?) -> Void)?

func setClientProxy(_ clientProxy: ClientProxyProtocol?) {
setClientProxyCallsCount += 1
setClientProxyReceivedClientProxy = clientProxy
setClientProxyReceivedInvocations.append(clientProxy)
setClientProxyClosure?(clientProxy)
}
//MARK: - requestAuthorization

var requestAuthorizationCallsCount = 0
var requestAuthorizationCalled: Bool {
return requestAuthorizationCallsCount > 0
}
var requestAuthorizationClosure: (() -> Void)?

func requestAuthorization() {
requestAuthorizationCallsCount += 1
requestAuthorizationClosure?()
}
}
class RoomMemberProxyMock: RoomMemberProxyProtocol {
var userID: String {
get { return underlyingUserID }
Expand Down
37 changes: 37 additions & 0 deletions ElementX/Sources/Other/Consumable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

@propertyWrapper struct Consumable<Value> {
var wrappedValue: Value? {
mutating get {
defer {
value = nil
}
return value
}
set {
value = newValue
}
}

private var value: Value?

init(value: Value? = nil) {
self.value = value
}
}
Loading

0 comments on commit 3d0d883

Please sign in to comment.