// // Copyright 2025 Element Creations Ltd. // Copyright 2022-2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import Combine import Foundation import UIKit import UserNotifications final class NotificationManager: NSObject, NotificationManagerProtocol { private let notificationCenter: UserNotificationCenterProtocol private let appSettings: AppSettings private var userSession: UserSessionProtocol? private var cancellables = Set() private var notificationsEnabled = false init(notificationCenter: UserNotificationCenterProtocol, appSettings: AppSettings) { self.notificationCenter = notificationCenter self.appSettings = appSettings super.init() } // MARK: NotificationManagerProtocol weak var delegate: NotificationManagerDelegate? func start() { let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, title: L10n.actionQuickReply, options: []) let messageCategory = UNNotificationCategory(identifier: NotificationConstants.Category.message, actions: [replyAction], intentIdentifiers: [], options: []) let inviteCategory = UNNotificationCategory(identifier: NotificationConstants.Category.invite, actions: [], intentIdentifiers: [], options: []) notificationCenter.setNotificationCategories([messageCategory, inviteCategory]) notificationCenter.delegate = self notificationsEnabled = appSettings.enableNotifications MXLog.info("App setting 'enableNotifications' is '\(notificationsEnabled)'") // Listen for changes to AppSettings.enableNotifications appSettings.$enableNotifications .sink { [weak self] newValue in self?.enableNotifications(newValue) } .store(in: &cancellables) NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) .sink { [weak self] _ in self?.removeReceivedWhileOfflineNotification() } .store(in: &cancellables) } func requestAuthorization() { guard appSettings.enableNotifications, !userSession.isNil else { return } Task { do { let permissionGranted = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) MXLog.info("Permission granted: \(permissionGranted)") await MainActor.run { if permissionGranted { self.delegate?.registerForRemoteNotifications() } } } catch { MXLog.error("Request authorization failed: \(error)") } } } func register(with deviceToken: Data) async -> Bool { guard let userSession else { return false } return await setPusher(with: deviceToken, clientProxy: userSession.clientProxy) } func setUserSession(_ userSession: UserSessionProtocol?) { self.userSession = userSession // If notification permissions were given previously then attempt re-registering // for remote notifications on startup. Otherwise let the onboarding flow handle it Task { [weak self] in guard let self else { return } if await notificationCenter.authorizationStatus() == .authorized, appSettings.enableNotifications { await MainActor.run { [weak self] in self?.delegate?.registerForRemoteNotifications() } } let settings = await notificationCenter.notificationSettings() MXLog.info("Notification sound enabled: \(settings.soundSetting == .enabled)") } } func registrationFailed(with error: Error) { MXLog.error("Device token registration failed with error: \(error)") } func showLocalNotification(with title: String, subtitle: String?) async { let content = UNMutableNotificationContent() content.title = title if let subtitle { content.subtitle = subtitle } let request = UNNotificationRequest(identifier: ProcessInfo.processInfo.globallyUniqueString, content: content, trigger: nil) do { try await notificationCenter.add(request) MXLog.info("Show local notification succeeded") } catch { MXLog.error("Show local notification failed: \(error)") } } func removeDeliveredMessageNotifications(for roomID: String) async { let notificationsIdentifiers = await notificationCenter .deliveredNotifications() .filter { $0.request.content.roomID == roomID } .map(\.request.identifier) notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationsIdentifiers) } func removeDeliveredNotificationsForFullyReadRooms(_ rooms: [RoomSummary]) async { let roomsToLastMessageDates = rooms .filter { $0.hasUnreadMessages == false } .reduce(into: [:]) { partialResult, roomSummary in partialResult[roomSummary.id] = roomSummary.lastMessageDate } let notificationsIdentifiers = await notificationCenter .deliveredNotifications() .filter { notification in guard let roomID = notification.request.content.roomID, let lastMessageDate = roomsToLastMessageDates[roomID] else { return false } return notification.date <= lastMessageDate } .map(\.request.identifier) notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationsIdentifiers) } private func removeReceivedWhileOfflineNotification() { notificationCenter.removeDeliveredNotifications(withIdentifiers: [NotificationServiceExtension.receivedWhileOfflineNotificationID]) } private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool { do { let defaultPayload = APNSPayload(aps: APSInfo(mutableContent: 1, alert: APSAlert(locKey: "Notification", locArgs: [])), pusherNotificationClientIdentifier: clientProxy.pusherNotificationClientIdentifier) let configuration = try await PusherConfiguration(identifiers: .init(pushkey: deviceToken.base64EncodedString(), appId: appSettings.pusherAppID), kind: .http(data: .init(url: appSettings.pushGatewayNotifyEndpoint.absoluteString, format: .eventIdOnly, defaultPayload: defaultPayload.toJsonString())), appDisplayName: "\(InfoPlistReader.main.bundleDisplayName) (iOS)", deviceDisplayName: UIDevice.current.name, profileTag: pusherProfileTag(), lang: Bundle.app.preferredLocalizations.first ?? "en") try await clientProxy.setPusher(with: configuration) MXLog.info("Set pusher succeeded") return true } catch { MXLog.error("Set pusher failed: \(error)") return false } } private func pusherProfileTag() -> String { if let currentTag = appSettings.pusherProfileTag { return currentTag } let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" let newTag = (0..<16).map { _ in let offset = Int.random(in: 0.. UNNotificationPresentationOptions { guard appSettings.enableInAppNotifications else { return [] } guard let delegate else { return [.badge, .sound, .list, .banner] } guard delegate.shouldDisplayInAppNotification(content: notification.request.content) else { return [] } return [.badge, .sound, .list, .banner] } @MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { switch response.actionIdentifier { case NotificationConstants.Action.inlineReply: guard let response = response as? UNTextInputNotificationResponse else { return } await delegate?.handleInlineReply(self, content: response.notification.request.content, replyText: response.userText) case UNNotificationDefaultActionIdentifier: await delegate?.notificationTapped(content: response.notification.request.content) default: break } } } extension UNUserNotificationCenter: UserNotificationCenterProtocol { }