diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9464d7857..a0e5d03d8 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -11,7 +11,7 @@ { "identity" : "compound-ios", "kind" : "remoteSourceControl", - "location" : "https://github.com/vector-im/compound-ios", + "location" : "https://github.com/vector-im/compound-ios.git", "state" : { "revision" : "b4f34edce9003ba1fde31500312f317b728f4ee3" } diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index a4b9b1bc1..7dda81bdc 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -507,15 +507,14 @@ extension AppCoordinator: NotificationManagerDelegate { func notificationTapped(_ service: NotificationManagerProtocol, content: UNNotificationContent) async { MXLog.info("[AppCoordinator] tappedNotification") - // We store the room identifier into the thread identifier - guard !content.threadIdentifier.isEmpty, + guard let roomID = content.roomID, content.receiverID != nil else { return } // Handle here the account switching when available - handleAppRoute(.room(roomID: content.threadIdentifier)) + handleAppRoute(.room(roomID: roomID)) } func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async { diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index e8765adcd..eb4098ad5 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -155,6 +155,8 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.enableInAppNotifications, defaultValue: true, storageType: .userDefaults(store)) var enableInAppNotifications + let enableLocalPushNotifications = false + /// Tag describing which set of device specific rules a pusher executes. @UserPreference(key: UserDefaultsKeys.pusherProfileTag, storageType: .userDefaults(store)) var pusherProfileTag: String? diff --git a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift index a65139092..916ae01b7 100644 --- a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift +++ b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift @@ -125,9 +125,9 @@ extension UNMutableNotificationContent { iconType: NotificationIconType) async throws -> UNMutableNotificationContent { var image: INImage? if let mediaSource = iconType.mediaSource { - switch await mediaProvider?.loadFileFromSource(mediaSource) { - case .success(let mediaFile): - image = try INImage(imageData: Data(contentsOf: mediaFile.url)) + switch await mediaProvider?.loadImageDataFromSource(mediaSource) { + case .success(let data): + image = INImage(imageData: data) case .failure(let error): MXLog.error("Couldn't add sender icon: \(error)") case .none: diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index b16f6a078..125926bfa 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -110,8 +110,10 @@ class ClientProxy: ClientProxyProtocol { client.setDelegate(delegate: delegate) // Set up sync listener for generating local notifications. - await Task.dispatch(on: clientQueue) { - client.setNotificationDelegate(notificationDelegate: delegate) + if ServiceLocator.shared.settings.enableLocalPushNotifications { + await Task.dispatch(on: clientQueue) { + client.setNotificationDelegate(notificationDelegate: delegate) + } } configureSlidingSync() @@ -326,7 +328,9 @@ class ClientProxy: ClientProxyProtocol { buildAndConfigureVisibleRoomsSlidingSyncList() buildAndConfigureAllRoomsSlidingSyncList() buildAndConfigureInvitesSlidingSyncList() - buildAndConfigureNotificationsSlidingSyncList() + if ServiceLocator.shared.settings.enableLocalPushNotifications { + buildAndConfigureNotificationsSlidingSyncList() + } guard let visibleRoomsListBuilder else { MXLog.error("Visible rooms sliding sync view unavailable") @@ -537,11 +541,13 @@ class ClientProxy: ClientProxyProtocol { MXLog.error("Invites sliding sync view unavailable") } - if let notificationsListBuilder { - MXLog.info("Registering notifications view") - _ = slidingSync?.addList(listBuilder: notificationsListBuilder) - } else { - MXLog.error("Notifications sliding sync view unavailable") + if ServiceLocator.shared.settings.enableLocalPushNotifications { + if let notificationsListBuilder { + MXLog.info("Registering notifications view") + _ = slidingSync?.addList(listBuilder: notificationsListBuilder) + } else { + MXLog.error("Notifications sliding sync view unavailable") + } } } diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index 1fdd6b175..d6a6b7414 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -34,14 +34,19 @@ class NotificationManager: NSObject, NotificationManagerProtocol { weak var delegate: NotificationManagerDelegate? func start() { - let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, - title: L10n.actionQuickReply, - options: []) - let replyCategory = UNNotificationCategory(identifier: NotificationConstants.Category.reply, - actions: [replyAction], - intentIdentifiers: [], - options: []) - notificationCenter.setNotificationCategories([replyCategory]) + // Not implemented yet + // let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, + // title: L10n.actionQuickReply, + // options: []) + let messageCategory = UNNotificationCategory(identifier: NotificationConstants.Category.message, + actions: [], + intentIdentifiers: [], + options: []) + let inviteCategory = UNNotificationCategory(identifier: NotificationConstants.Category.invite, + actions: [], + intentIdentifiers: [], + options: []) + notificationCenter.setNotificationCategories([messageCategory, inviteCategory]) notificationCenter.delegate = self } diff --git a/ElementX/Sources/Services/Notification/NotificationConstants.swift b/ElementX/Sources/Services/Notification/NotificationConstants.swift index 612b666fa..0694b8f58 100644 --- a/ElementX/Sources/Services/Notification/NotificationConstants.swift +++ b/ElementX/Sources/Services/Notification/NotificationConstants.swift @@ -28,7 +28,8 @@ enum NotificationConstants { enum Category { static let discard = "discard" - static let reply = "reply" + static let message = "message" + static let invite = "invite" } enum Action { diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift index c35396ba8..e587e3467 100644 --- a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift +++ b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift @@ -33,13 +33,15 @@ protocol NotificationItemProxyProtocol { var roomDisplayName: String { get } + var roomCanonicalAlias: String? { get } + var roomAvatarMediaSource: MediaSourceProxy? { get } var isNoisy: Bool { get } var isDirect: Bool { get } - var isEncrypted: Bool { get } + var isEncrypted: Bool? { get } } extension NotificationItemProxyProtocol { @@ -73,6 +75,10 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { notificationItem.roomDisplayName } + var roomCanonicalAlias: String? { + notificationItem.roomCanonicalAlias + } + var isNoisy: Bool { notificationItem.isNoisy } @@ -81,8 +87,8 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { notificationItem.isDirect } - var isEncrypted: Bool { - notificationItem.isEncrypted ?? false + var isEncrypted: Bool? { + notificationItem.isEncrypted } var senderAvatarMediaSource: MediaSourceProxy? { @@ -119,13 +125,15 @@ struct EmptyNotificationItemProxy: NotificationItemProxyProtocol { var roomDisplayName: String { "" } + var roomCanonicalAlias: String? { nil } + var roomAvatarURL: String? { nil } var isNoisy: Bool { false } var isDirect: Bool { false } - var isEncrypted: Bool { false } + var isEncrypted: Bool? { nil } var senderAvatarMediaSource: MediaSourceProxy? { nil } @@ -135,6 +143,18 @@ struct EmptyNotificationItemProxy: NotificationItemProxyProtocol { } extension NotificationItemProxyProtocol { + var baseMutableContent: UNMutableNotificationContent { + let notification = UNMutableNotificationContent() + notification.receiverID = receiverID + notification.roomID = roomID + notification.eventID = event.eventID + notification.notificationID = id + notification.sound = isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil + // So that the UI groups notification that are received for the same room but also for the same user + notification.threadIdentifier = "\(receiverID)\(roomID)" + return notification + } + var requiresMediaProvider: Bool { if senderAvatarMediaSource != nil || roomAvatarMediaSource != nil { return true @@ -157,7 +177,6 @@ extension NotificationItemProxyProtocol { } } - // swiftlint: disable cyclomatic_complexity /// Process the receiver item proxy /// - Parameters: /// - receiverId: identifier of the user that has received the notification @@ -169,67 +188,90 @@ extension NotificationItemProxyProtocol { return processEmpty() } else { switch event.type { - case .none, .state: + case .none: return processEmpty() + case let .state(content): + return try await processStateEvent(content: content, mediaProvider: mediaProvider) case let .messageLike(content): switch content { case .roomMessage(messageType: let messageType): - switch messageType { - case .emote(content: let content): - return try await processEmote(content: content, mediaProvider: mediaProvider) - case .image(content: let content): - return try await processImage(content: content, mediaProvider: mediaProvider) - case .audio(content: let content): - return try await processAudio(content: content, mediaProvider: mediaProvider) - case .video(content: let content): - return try await processVideo(content: content, mediaProvider: mediaProvider) - case .file(content: let content): - return try await processFile(content: content, mediaProvider: mediaProvider) - case .notice(content: let content): - return try await processNotice(content: content, mediaProvider: mediaProvider) - case .text(content: let content): - return try await processText(content: content, mediaProvider: mediaProvider) - } + return try await processRoomMessage(messageType: messageType, mediaProvider: mediaProvider) default: return processEmpty() } } } } - - // swiftlint: enable cyclomatic_complexity // MARK: - Private + private func processStateEvent(content: StateEventContent, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + switch content { + case let .roomMemberContent(userId, membershipState): + switch membershipState { + case .invite: + if userId == receiverID { + return try await processInvited(mediaProvider: mediaProvider) + } else { + return processEmpty() + } + default: + return processEmpty() + } + default: + return processEmpty() + } + } + + private func processInvited(mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + var notification = baseMutableContent + + notification.categoryIdentifier = NotificationConstants.Category.invite + + // Sadly as of right now we can't get from the NSE context any information for invited rooms, so we will only display the user name and a simple message + let iconType = NotificationIconType.sender(mediaSource: senderAvatarMediaSource) + notification = try await notification.addSenderIcon(using: mediaProvider, + senderID: event.senderID, + senderName: senderDisplayName ?? event.senderID, + iconType: iconType) + notification.body = L10n.screenInvitesInvitedYou("") + return notification + } + + private func processRoomMessage(messageType: MessageType, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + switch messageType { + case .emote(content: let content): + return try await processEmote(content: content, mediaProvider: mediaProvider) + case .image(content: let content): + return try await processImage(content: content, mediaProvider: mediaProvider) + case .audio(content: let content): + return try await processAudio(content: content, mediaProvider: mediaProvider) + case .video(content: let content): + return try await processVideo(content: content, mediaProvider: mediaProvider) + case .file(content: let content): + return try await processFile(content: content, mediaProvider: mediaProvider) + case .notice(content: let content): + return try await processNotice(content: content, mediaProvider: mediaProvider) + case .text(content: let content): + return try await processText(content: content, mediaProvider: mediaProvider) + } + } + private func processEmpty() -> UNMutableNotificationContent { - let notification = UNMutableNotificationContent() - notification.receiverID = receiverID - notification.roomID = roomID - notification.eventID = event.eventID - notification.notificationID = id + let notification = baseMutableContent notification.title = InfoPlistReader(bundle: .app).bundleDisplayName notification.body = L10n.notification - notification.threadIdentifier = roomID - notification.categoryIdentifier = NotificationConstants.Category.reply - notification.sound = isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil + notification.categoryIdentifier = NotificationConstants.Category.message return notification } private func processCommon(mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { - var notification = UNMutableNotificationContent() - notification.receiverID = receiverID - notification.roomID = roomID - notification.eventID = event.eventID - notification.notificationID = id + var notification = baseMutableContent notification.title = senderDisplayName ?? roomDisplayName if notification.title != roomDisplayName { notification.subtitle = roomDisplayName } - // We can store the room identifier into the thread identifier since it's used for notifications - // that belong to the same group - notification.threadIdentifier = roomID - notification.categoryIdentifier = NotificationConstants.Category.reply - notification.sound = isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil + notification.categoryIdentifier = NotificationConstants.Category.message let senderName = senderDisplayName ?? roomDisplayName let iconType: NotificationIconType diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift index 6f706e9e0..ad17e8e8c 100644 --- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift +++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift @@ -101,14 +101,18 @@ final class NotificationManagerTests: XCTestCase { } func test_whenStart_notificationCategoriesAreSet() throws { - let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, - title: L10n.actionQuickReply, - options: []) - let replyCategory = UNNotificationCategory(identifier: NotificationConstants.Category.reply, - actions: [replyAction], - intentIdentifiers: [], - options: []) - XCTAssertEqual(notificationCenter.notificationCategoriesValue, [replyCategory]) + // let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, + // title: L10n.actionQuickReply, + // options: []) + let messageCategory = UNNotificationCategory(identifier: NotificationConstants.Category.message, + actions: [], + intentIdentifiers: [], + options: []) + let inviteCategory = UNNotificationCategory(identifier: NotificationConstants.Category.invite, + actions: [], + intentIdentifiers: [], + options: []) + XCTAssertEqual(notificationCenter.notificationCategoriesValue, [messageCategory, inviteCategory]) } func test_whenStart_delegateIsSet() throws {