Removing Delivered Notifications (#1305)

* completed

* changelog

* tests

* more tests

* better naming

* fix

* a bit more waiting time

* generated mocks

* xcodegen
This commit is contained in:
Mauro
2023-07-11 10:44:06 +02:00
committed by GitHub
parent 98dabe8500
commit 28fa1140b7
15 changed files with 245 additions and 8 deletions
@@ -197,6 +197,25 @@ class BugReportServiceMock: BugReportServiceProtocol {
}
}
}
class NotificationCenterMock: NotificationCenterProtocol {
//MARK: - post
var postNameObjectCallsCount = 0
var postNameObjectCalled: Bool {
return postNameObjectCallsCount > 0
}
var postNameObjectReceivedArguments: (aName: NSNotification.Name, anObject: Any?)?
var postNameObjectReceivedInvocations: [(aName: NSNotification.Name, anObject: Any?)] = []
var postNameObjectClosure: ((NSNotification.Name, Any?) -> Void)?
func post(name aName: NSNotification.Name, object anObject: Any?) {
postNameObjectCallsCount += 1
postNameObjectReceivedArguments = (aName: aName, anObject: anObject)
postNameObjectReceivedInvocations.append((aName: aName, anObject: anObject))
postNameObjectClosure?(aName, anObject)
}
}
class NotificationManagerMock: NotificationManagerProtocol {
var delegate: NotificationManagerDelegate?
@@ -1365,6 +1384,39 @@ class UserNotificationCenterMock: UserNotificationCenterProtocol {
return requestAuthorizationOptionsReturnValue
}
}
//MARK: - deliveredNotifications
var deliveredNotificationsCallsCount = 0
var deliveredNotificationsCalled: Bool {
return deliveredNotificationsCallsCount > 0
}
var deliveredNotificationsReturnValue: [UNNotification]!
var deliveredNotificationsClosure: (() async -> [UNNotification])?
func deliveredNotifications() async -> [UNNotification] {
deliveredNotificationsCallsCount += 1
if let deliveredNotificationsClosure = deliveredNotificationsClosure {
return await deliveredNotificationsClosure()
} else {
return deliveredNotificationsReturnValue
}
}
//MARK: - removeDeliveredNotifications
var removeDeliveredNotificationsWithIdentifiersCallsCount = 0
var removeDeliveredNotificationsWithIdentifiersCalled: Bool {
return removeDeliveredNotificationsWithIdentifiersCallsCount > 0
}
var removeDeliveredNotificationsWithIdentifiersReceivedIdentifiers: [String]?
var removeDeliveredNotificationsWithIdentifiersReceivedInvocations: [[String]] = []
var removeDeliveredNotificationsWithIdentifiersClosure: (([String]) -> Void)?
func removeDeliveredNotifications(withIdentifiers identifiers: [String]) {
removeDeliveredNotificationsWithIdentifiersCallsCount += 1
removeDeliveredNotificationsWithIdentifiersReceivedIdentifiers = identifiers
removeDeliveredNotificationsWithIdentifiersReceivedInvocations.append(identifiers)
removeDeliveredNotificationsWithIdentifiersClosure?(identifiers)
}
//MARK: - setNotificationCategories
var setNotificationCategoriesCallsCount = 0
@@ -0,0 +1,22 @@
//
// 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
extension Notification.Name {
static let roomMarkedAsRead = Notification.Name("roomMarkedAsRead")
static let invitesScreenAppeared = Notification.Name("invitesScreenAppeared")
}
@@ -0,0 +1,24 @@
//
// 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
// sourcery: AutoMockable
protocol NotificationCenterProtocol: AnyObject {
func post(name aName: NSNotification.Name, object anObject: Any?)
}
extension NotificationCenter: NotificationCenterProtocol { }
@@ -48,4 +48,5 @@ struct InvitesScreenRoomDetails: Identifiable {
enum InvitesScreenViewAction {
case accept(InvitesScreenRoomDetails)
case decline(InvitesScreenRoomDetails)
case appeared
}
@@ -24,6 +24,7 @@ class InvitesScreenViewModel: InvitesScreenViewModelType, InvitesScreenViewModel
private let appSettings: AppSettings
private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol
private let notificationCenterProtocol: NotificationCenterProtocol
private let previouslySeenInvites: Set<String>
private let actionsSubject: PassthroughSubject<InvitesScreenViewModelAction, Never> = .init()
@@ -36,11 +37,13 @@ class InvitesScreenViewModel: InvitesScreenViewModelType, InvitesScreenViewModel
init(userSession: UserSessionProtocol,
appSettings: AppSettings,
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol) {
userIndicatorController: UserIndicatorControllerProtocol,
notificationCenterProtocol: NotificationCenterProtocol = NotificationCenter.default) {
self.userSession = userSession
self.appSettings = appSettings
self.analytics = analytics
self.userIndicatorController = userIndicatorController
self.notificationCenterProtocol = notificationCenterProtocol
previouslySeenInvites = appSettings.seenInvites
super.init(initialViewState: InvitesScreenViewState(), imageProvider: userSession.mediaProvider)
@@ -55,6 +58,8 @@ class InvitesScreenViewModel: InvitesScreenViewModelType, InvitesScreenViewModel
accept(invite: invite)
case .decline(let invite):
startDeclineFlow(invite: invite)
case .appeared:
notificationCenterProtocol.post(name: .invitesScreenAppeared, object: nil)
}
}
@@ -51,8 +51,11 @@ struct InvitesScreen: View {
noInvitesContent
}
}
.onAppear {
context.send(viewAction: .appeared)
}
}
private var noInvitesContent: some View {
Text(L10n.screenInvitesEmptyList)
.font(.compound.bodyLG)
@@ -33,18 +33,21 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private let appSettings: AppSettings
private let analytics: AnalyticsService
private unowned let userIndicatorController: UserIndicatorControllerProtocol
private let notificationCenterProtocol: NotificationCenterProtocol
init(timelineController: RoomTimelineControllerProtocol,
mediaProvider: MediaProviderProtocol,
roomProxy: RoomProxyProtocol,
appSettings: AppSettings,
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol) {
userIndicatorController: UserIndicatorControllerProtocol,
notificationCenterProtocol: NotificationCenterProtocol = NotificationCenter.default) {
self.roomProxy = roomProxy
self.timelineController = timelineController
self.appSettings = appSettings
self.analytics = analytics
self.userIndicatorController = userIndicatorController
self.notificationCenterProtocol = notificationCenterProtocol
super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomID,
roomTitle: roomProxy.roomTitle,
@@ -214,6 +217,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
private func markRoomAsRead() async {
notificationCenterProtocol.post(name: .roomMarkedAsRead, object: roomProxy.id)
_ = await timelineController.markRoomAsRead()
}
@@ -19,7 +19,7 @@ import Foundation
import UIKit
import UserNotifications
class NotificationManager: NSObject, NotificationManagerProtocol {
final class NotificationManager: NSObject, NotificationManagerProtocol {
private let notificationCenter: UserNotificationCenterProtocol
private let appSettings: AppSettings
@@ -31,6 +31,8 @@ class NotificationManager: NSObject, NotificationManagerProtocol {
appSettings: AppSettings) {
self.notificationCenter = notificationCenter
self.appSettings = appSettings
super.init()
addObservers()
}
// MARK: NotificationManagerProtocol
@@ -169,6 +171,47 @@ class NotificationManager: NSObject, NotificationManagerProtocol {
delegate?.unregisterForRemoteNotifications()
}
}
private func addObservers() {
NotificationCenter
.default
.publisher(for: .roomMarkedAsRead)
.sink { [weak self] notification in
guard let roomID = notification.object as? String else {
return
}
self?.removeMessageNotifications(for: roomID)
}
.store(in: &cancellables)
NotificationCenter
.default
.publisher(for: .invitesScreenAppeared)
.sink { [weak self] _ in
self?.removeInviteNotifications()
}
.store(in: &cancellables)
}
private func removeMessageNotifications(for roomID: String) {
Task {
let notificationsIdentifiers = await notificationCenter
.deliveredNotifications()
.filter { $0.request.content.roomID == roomID }
.map(\.request.identifier)
notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationsIdentifiers)
}
}
private func removeInviteNotifications() {
Task {
let notificationsIdentifiers = await notificationCenter
.deliveredNotifications()
.filter { $0.request.content.categoryIdentifier == NotificationConstants.Category.invite }
.map(\.request.identifier)
notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationsIdentifiers)
}
}
}
// MARK: - UNUserNotificationCenterDelegate
@@ -17,10 +17,14 @@
import Foundation
import UserNotifications
// Sadly we can't mock this because the delegate needs to be weak
// Use UserNotificationCenterSpy
protocol UserNotificationCenterProtocol: AnyObject {
var delegate: UNUserNotificationCenterDelegate? { get set }
func add(_ request: UNNotificationRequest) async throws
func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool
func deliveredNotifications() async -> [UNNotification]
func removeDeliveredNotifications(withIdentifiers identifiers: [String])
func setNotificationCategories(_ categories: Set<UNNotificationCategory>)
func authorizationStatus() async -> UNAuthorizationStatus
}