Refactor the NSE so that the original notification content is preserved between all the different processing steps

- building brand new content is probably interfering with how iOS syncs up delivered and seen notifications between devices
- simplify all the various steps and make the flows easier to follow
This commit is contained in:
Stefan Ceriu
2025-04-17 12:21:47 +03:00
committed by Stefan Ceriu
parent 7cf1d08b20
commit 5ad25d8492
6 changed files with 168 additions and 245 deletions
-4
View File
@@ -425,7 +425,6 @@
510C4EDF826CA9C6CEEC6C95 /* ManageRoomMemberSheetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */; }; 510C4EDF826CA9C6CEEC6C95 /* ManageRoomMemberSheetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A34D9BCA1A7D9A56E1EAF1D /* ManageRoomMemberSheetViewModel.swift */; };
5139F4BD5A5DF6F8D11A9BDE /* NotificationPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0BA44B1838E65B507B277 /* NotificationPermissionsScreen.swift */; }; 5139F4BD5A5DF6F8D11A9BDE /* NotificationPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0BA44B1838E65B507B277 /* NotificationPermissionsScreen.swift */; };
513AF15E0E84711B80D04B1B /* ReportRoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */; }; 513AF15E0E84711B80D04B1B /* ReportRoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */; };
518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; };
51B3B19FA5F91B455C807BA7 /* RoomPollsHistoryScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */; }; 51B3B19FA5F91B455C807BA7 /* RoomPollsHistoryScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */; };
523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */; }; 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */; };
52473A4D7B1FBD4CD1E770C8 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; 52473A4D7B1FBD4CD1E770C8 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; };
@@ -1747,7 +1746,6 @@
49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSheetLabelStyle.swift; sourceTree = "<group>"; }; 49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSheetLabelStyle.swift; sourceTree = "<group>"; };
49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
49E6066092ED45E36BB306F7 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 49E6066092ED45E36BB306F7 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = "<group>"; };
4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModelProtocol.swift; sourceTree = "<group>"; };
4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = "<group>"; }; 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = "<group>"; };
4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerMock.swift; sourceTree = "<group>"; }; 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerMock.swift; sourceTree = "<group>"; };
@@ -3955,7 +3953,6 @@
children = ( children = (
4959CECEC984B3995616F427 /* DataProtectionManager.swift */, 4959CECEC984B3995616F427 /* DataProtectionManager.swift */,
EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */, EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */,
49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */,
); );
path = Other; path = Other;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -6733,7 +6730,6 @@
DDB47D29C6865669288BF87C /* UIFont+AttributedStringBuilder.m in Sources */, DDB47D29C6865669288BF87C /* UIFont+AttributedStringBuilder.m in Sources */,
45D6DC594816288983627484 /* UITestsScreenIdentifier.swift in Sources */, 45D6DC594816288983627484 /* UITestsScreenIdentifier.swift in Sources */,
281BED345D59A9A6A99E9D98 /* UNNotificationContent.swift in Sources */, 281BED345D59A9A6A99E9D98 /* UNNotificationContent.swift in Sources */,
518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */,
06B55882911B4BF5B14E9851 /* URL.swift in Sources */, 06B55882911B4BF5B14E9851 /* URL.swift in Sources */,
D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */, D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */,
34357B287357BC0B9715DD51 /* UserAgentBuilder.swift in Sources */, 34357B287357BC0B9715DD51 /* UserAgentBuilder.swift in Sources */,
@@ -39,6 +39,10 @@ extension UNNotificationContent {
@objc var eventID: String? { @objc var eventID: String? {
userInfo[NotificationConstants.UserInfoKey.eventIdentifier] as? String userInfo[NotificationConstants.UserInfoKey.eventIdentifier] as? String
} }
@objc var pusherNotificationClientIdentifier: String? {
userInfo[NotificationConstants.UserInfoKey.pusherNotificationClientIdentifier] as? String
}
} }
extension UNMutableNotificationContent { extension UNMutableNotificationContent {
@@ -68,19 +72,21 @@ extension UNMutableNotificationContent {
userInfo[NotificationConstants.UserInfoKey.eventIdentifier] = newValue userInfo[NotificationConstants.UserInfoKey.eventIdentifier] = newValue
} }
} }
var unreadCount: Int? {
userInfo[NotificationConstants.UserInfoKey.unreadCount] as? Int
}
func addMediaAttachment(using mediaProvider: MediaProviderProtocol?, func addMediaAttachment(using mediaProvider: MediaProviderProtocol,
mediaSource: MediaSourceProxy) async -> UNMutableNotificationContent { mediaSource: MediaSourceProxy) async {
guard let mediaProvider else {
return self
}
switch await mediaProvider.loadFileFromSource(mediaSource) { switch await mediaProvider.loadFileFromSource(mediaSource) {
case .success(let file): case .success(let file):
do { do {
guard let url = file.url else { guard let url = file.url else {
MXLog.error("Couldn't add media attachment: URL is nil") MXLog.error("Couldn't add media attachment: URL is nil")
return self return
} }
let identifier = ProcessInfo.processInfo.globallyUniqueString let identifier = ProcessInfo.processInfo.globallyUniqueString
let newURL = try FileManager.default.copyFileToTemporaryDirectory(file: url, with: "\(identifier).\(url.pathExtension)") let newURL = try FileManager.default.copyFileToTemporaryDirectory(file: url, with: "\(identifier).\(url.pathExtension)")
let attachment = try UNNotificationAttachment(identifier: identifier, let attachment = try UNNotificationAttachment(identifier: identifier,
@@ -89,24 +95,21 @@ extension UNMutableNotificationContent {
attachments.append(attachment) attachments.append(attachment)
} catch { } catch {
MXLog.error("Couldn't add media attachment:: \(error)") MXLog.error("Couldn't add media attachment:: \(error)")
return self return
} }
case .failure(let error): case .failure(let error):
MXLog.error("Couldn't load the file for media attachment: \(error)") MXLog.error("Couldn't load the file for media attachment: \(error)")
} }
return self
} }
func addSenderIcon(using mediaProvider: MediaProviderProtocol?, func addSenderIcon(senderID: String,
senderID: String,
senderName: String, senderName: String,
icon: NotificationIcon, icon: NotificationIcon,
forcePlaceholder: Bool = false) async throws -> UNMutableNotificationContent { forcePlaceholder: Bool = false,
mediaProvider: MediaProviderProtocol) async throws -> UNMutableNotificationContent {
var fetchedImage: INImage? var fetchedImage: INImage?
let image: INImage let image: INImage
if !forcePlaceholder, if !forcePlaceholder,
let mediaProvider,
let mediaSource = icon.mediaSource { let mediaSource = icon.mediaSource {
switch await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 100, height: 100)) { switch await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 100, height: 100)) {
case .success(let data): case .success(let data):
@@ -164,6 +167,7 @@ extension UNMutableNotificationContent {
// Donate the interaction before updating notification content. // Donate the interaction before updating notification content.
try await interaction.donate() try await interaction.donate()
// Update notification content before displaying the // Update notification content before displaying the
// communication notification. // communication notification.
let updatedContent = try updating(from: intent) let updatedContent = try updating(from: intent)
@@ -41,32 +41,4 @@ extension NotificationItemProxyProtocol {
var isDM: Bool { var isDM: Bool {
isRoomDirect && roomJoinedMembers <= 2 isRoomDirect && roomJoinedMembers <= 2
} }
var hasMedia: Bool {
if (isDM && senderAvatarMediaSource != nil) ||
(!isDM && roomAvatarMediaSource != nil) {
return true
}
switch event {
case .invite, .none:
return false
case .timeline(let event):
switch try? event.eventType() {
case .state, .none:
return false
case let .messageLike(content):
switch content {
case let .roomMessage(messageType, _):
switch messageType {
case .image, .video, .audio:
return true
default:
return false
}
default:
return false
}
}
}
}
} }
+117 -116
View File
@@ -18,70 +18,78 @@ struct NotificationContentBuilder {
/// - notificationItem: The notification item /// - notificationItem: The notification item
/// - mediaProvider: Media provider to process also media. May be passed nil to ignore media operations. /// - mediaProvider: Media provider to process also media. May be passed nil to ignore media operations.
/// - Returns: A notification content object if the notification should be displayed. Otherwise nil. /// - Returns: A notification content object if the notification should be displayed. Otherwise nil.
func content(for notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { func process(notificationContent: UNMutableNotificationContent,
notificationItem: NotificationItemProxyProtocol,
mediaProvider: MediaProviderProtocol) async -> UNMutableNotificationContent {
notificationContent.receiverID = notificationItem.receiverID
notificationContent.roomID = notificationItem.roomID
switch notificationItem.event {
case .timeline(let event):
notificationContent.eventID = event.eventId()
case .invite, .none:
notificationContent.eventID = nil
}
// So that the UI groups notification that are received for the same room but also for the same user
// Removing the @ fixes an iOS bug where the notification crashes if the mute button is tapped
notificationContent.threadIdentifier = "\(notificationItem.receiverID)\(notificationItem.roomID)".replacingOccurrences(of: "@", with: "")
MXLog.info("isNoisy: \(notificationItem.isNoisy)")
notificationContent.sound = notificationItem.isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil
switch notificationItem.event { switch notificationItem.event {
case .none: case .none:
return processEmpty(notificationItem: notificationItem) return processEmpty(notificationContent)
case .invite: case .invite:
return try await processInvited(notificationItem: notificationItem, mediaProvider: mediaProvider) return await processInvited(notificationContent: notificationContent,
notificationItem: notificationItem,
mediaProvider: mediaProvider)
case .timeline(let event): case .timeline(let event):
guard let eventType = try? event.eventType() else { guard let eventType = try? event.eventType(),
return processEmpty(notificationItem: notificationItem) case let .messageLike(content) = eventType else {
return processEmpty(notificationContent)
} }
switch eventType { let notificationContent = await processMessageLike(notificationContent: notificationContent,
case let .messageLike(content): notificationItem: notificationItem,
switch content { mediaProvider: mediaProvider)
case .roomMessage(let messageType, _):
return try await processRoomMessage(notificationItem: notificationItem, messageType: messageType, mediaProvider: mediaProvider) switch content {
case .poll(let question): case .roomMessage(let messageType, _):
return try await processPollStartEvent(notificationItem: notificationItem, pollQuestion: question, mediaProvider: mediaProvider) return await processRoomMessage(notificationContent: notificationContent,
case .callInvite: notificationItem: notificationItem,
return try await processCallInviteEvent(notificationItem: notificationItem, mediaProvider: mediaProvider) messageType: messageType,
case .callNotify: mediaProvider: mediaProvider)
return try await processCallNotifyEvent(notificationItem: notificationItem, mediaProvider: mediaProvider) case .poll(let question):
default: notificationContent.body = L10n.commonPollSummary(question)
return processEmpty(notificationItem: notificationItem) return notificationContent
} case .callInvite:
case .state: notificationContent.body = L10n.commonUnsupportedCall
return processEmpty(notificationItem: notificationItem) return notificationContent
case .callNotify:
notificationContent.body = L10n.notificationIncomingCall
return notificationContent
default:
return processEmpty(notificationContent)
} }
} }
} }
// MARK: - Private // MARK: - Private
func baseMutableContent(for notificationItem: NotificationItemProxyProtocol) -> UNMutableNotificationContent { private func processEmpty(_ notificationContent: UNMutableNotificationContent) -> UNMutableNotificationContent {
let notification = UNMutableNotificationContent() notificationContent.title = InfoPlistReader(bundle: .app).bundleDisplayName
notificationContent.body = L10n.notification
notificationContent.categoryIdentifier = NotificationConstants.Category.message
notification.receiverID = notificationItem.receiverID return notificationContent
notification.roomID = notificationItem.roomID
notification.eventID = switch notificationItem.event {
case .timeline(let event): event.eventId()
case .invite, .none: nil
}
// So that the UI groups notification that are received for the same room but also for the same user
// Removing the @ fixes an iOS bug where the notification crashes if the mute button is tapped
notification.threadIdentifier = "\(notificationItem.receiverID)\(notificationItem.roomID)".replacingOccurrences(of: "@", with: "")
MXLog.info("isNoisy: \(notificationItem.isNoisy)")
notification.sound = notificationItem.isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil
return notification
}
private func processEmpty(notificationItem: NotificationItemProxyProtocol) -> UNMutableNotificationContent {
let notification = baseMutableContent(for: notificationItem)
notification.title = InfoPlistReader(bundle: .app).bundleDisplayName
notification.body = L10n.notification
notification.categoryIdentifier = NotificationConstants.Category.message
return notification
} }
private func processInvited(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { private func processInvited(notificationContent: UNMutableNotificationContent,
var notification = baseMutableContent(for: notificationItem) notificationItem: NotificationItemProxyProtocol,
mediaProvider: MediaProviderProtocol) async -> UNMutableNotificationContent {
notification.categoryIdentifier = NotificationConstants.Category.invite notificationContent.categoryIdentifier = NotificationConstants.Category.invite
let body: String let body: String
if !notificationItem.isDM { if !notificationItem.isDM {
@@ -89,83 +97,43 @@ struct NotificationContentBuilder {
} else { } else {
body = L10n.notificationInviteBody body = L10n.notificationInviteBody
} }
notification = try await notification.addSenderIcon(using: mediaProvider,
senderID: notificationItem.senderID,
senderName: notificationItem.senderDisplayName ?? notificationItem.roomDisplayName,
icon: icon(for: notificationItem),
forcePlaceholder: settings.hideInviteAvatars)
notification.body = body
return notification notificationContent.body = body
}
private func processRoomMessage(notificationItem: NotificationItemProxyProtocol, messageType: MessageType, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
var notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider)
let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName do {
notification.body = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, isOutgoing: false).characters) return try await notificationContent.addSenderIcon(senderID: notificationItem.senderID,
senderName: notificationItem.senderDisplayName ?? notificationItem.roomDisplayName,
guard settings.timelineMediaVisibility == .always || icon: icon(for: notificationItem),
(settings.timelineMediaVisibility == .privateOnly && notificationItem.isRoomPrivate) forcePlaceholder: settings.hideInviteAvatars,
else { return notification } mediaProvider: mediaProvider)
} catch {
switch messageType { return notificationContent
case .image(content: let content):
notification = await notification.addMediaAttachment(using: mediaProvider,
mediaSource: .init(source: content.source,
mimeType: content.info?.mimetype))
case .audio(content: let content):
notification = await notification.addMediaAttachment(using: mediaProvider,
mediaSource: .init(source: content.source,
mimeType: content.info?.mimetype))
case .video(content: let content):
notification = await notification.addMediaAttachment(using: mediaProvider,
mediaSource: .init(source: content.source,
mimeType: content.info?.mimetype))
default:
break
} }
return notification
}
private func processPollStartEvent(notificationItem: NotificationItemProxyProtocol, pollQuestion: String, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
let notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider)
notification.body = L10n.commonPollSummary(pollQuestion)
return notification
} }
private func processCallInviteEvent(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { private func processMessageLike(notificationContent: UNMutableNotificationContent,
let notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider) notificationItem: NotificationItemProxyProtocol,
notification.body = L10n.commonUnsupportedCall mediaProvider: MediaProviderProtocol) async -> UNMutableNotificationContent {
return notification notificationContent.title = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
} if notificationContent.title != notificationItem.roomDisplayName {
notificationContent.subtitle = notificationItem.roomDisplayName
private func processCallNotifyEvent(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
let notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider)
notification.body = L10n.notificationIncomingCall
return notification
}
private func processCommonRoomMessage(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
var notification = baseMutableContent(for: notificationItem)
notification.title = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
if notification.title != notificationItem.roomDisplayName {
notification.subtitle = notificationItem.roomDisplayName
} }
notification.categoryIdentifier = NotificationConstants.Category.message notificationContent.categoryIdentifier = NotificationConstants.Category.message
let senderName = if let displayName = notificationItem.senderDisplayName { let senderName = if let displayName = notificationItem.senderDisplayName {
notificationItem.hasMention ? L10n.notificationSenderMentionReply(displayName) : displayName notificationItem.hasMention ? L10n.notificationSenderMentionReply(displayName) : displayName
} else { } else {
notificationItem.roomDisplayName notificationItem.roomDisplayName
} }
notification = try await notification.addSenderIcon(using: mediaProvider,
senderID: notificationItem.senderID, do {
senderName: senderName, return try await notificationContent.addSenderIcon(senderID: notificationItem.senderID,
icon: icon(for: notificationItem)) senderName: senderName,
return notification icon: icon(for: notificationItem),
mediaProvider: mediaProvider)
} catch {
return notificationContent
}
} }
func icon(for notificationItem: NotificationItemProxyProtocol) -> NotificationIcon { func icon(for notificationItem: NotificationItemProxyProtocol) -> NotificationIcon {
@@ -176,4 +144,37 @@ struct NotificationContentBuilder {
groupInfo: .init(name: notificationItem.roomDisplayName, id: notificationItem.roomID)) groupInfo: .init(name: notificationItem.roomDisplayName, id: notificationItem.roomID))
} }
} }
private func processRoomMessage(notificationContent: UNMutableNotificationContent,
notificationItem: NotificationItemProxyProtocol,
messageType: MessageType,
mediaProvider: MediaProviderProtocol) async -> UNMutableNotificationContent {
let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
notificationContent.body = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, isOutgoing: false).characters)
guard settings.timelineMediaVisibility == .always ||
(settings.timelineMediaVisibility == .privateOnly && notificationItem.isRoomPrivate)
else {
return notificationContent
}
switch messageType {
case .image(content: let content):
await notificationContent.addMediaAttachment(using: mediaProvider,
mediaSource: .init(source: content.source,
mimeType: content.info?.mimetype))
case .audio(content: let content):
await notificationContent.addMediaAttachment(using: mediaProvider,
mediaSource: .init(source: content.source,
mimeType: content.info?.mimetype))
case .video(content: let content):
await notificationContent.addMediaAttachment(using: mediaProvider,
mediaSource: .init(source: content.source,
mimeType: content.info?.mimetype))
default:
break
}
return notificationContent
}
} }
+34 -57
View File
@@ -41,8 +41,8 @@ private let notificationContentBuilder = NotificationContentBuilder(messageEvent
settings: settings) settings: settings)
class NotificationServiceExtension: UNNotificationServiceExtension { class NotificationServiceExtension: UNNotificationServiceExtension {
private var handler: ((UNNotificationContent) -> Void)? private var contentHandler: ((UNNotificationContent) -> Void)!
private var modifiedContent: UNMutableNotificationContent? private var notificationContent: UNMutableNotificationContent!
private let appHooks = AppHooks() private let appHooks = AppHooks()
@@ -55,9 +55,9 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
override func didReceive(_ request: UNNotificationRequest, override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory), guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory),
let roomID = request.roomID, let roomID = request.content.roomID,
let eventID = request.eventID, let eventID = request.content.eventID,
let clientID = request.pusherNotificationClientIdentifier, let clientID = request.content.pusherNotificationClientIdentifier,
let credentials = keychainController.restorationTokens().first(where: { $0.restorationToken.pusherNotificationClientIdentifier == clientID }) else { let credentials = keychainController.restorationTokens().first(where: { $0.restorationToken.pusherNotificationClientIdentifier == clientID }) else {
// We cannot process this notification, it might be due to one of these: // We cannot process this notification, it might be due to one of these:
// - Device rebooted and locked // - Device rebooted and locked
@@ -67,10 +67,14 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
return contentHandler(request.content) return contentHandler(request.content)
} }
guard let mutableContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
return contentHandler(request.content)
}
Target.nse.configure(logLevel: settings.logLevel, traceLogPacks: settings.traceLogPacks) Target.nse.configure(logLevel: settings.logLevel, traceLogPacks: settings.traceLogPacks)
handler = contentHandler self.contentHandler = contentHandler
modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent notificationContent = mutableContent
MXLog.info("\(tag) #########################################") MXLog.info("\(tag) #########################################")
@@ -90,7 +94,6 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
await processEvent(eventID, await processEvent(eventID,
roomID: roomID, roomID: roomID,
unreadCount: request.unreadCount,
userSession: userSession) userSession: userSession)
} catch { } catch {
MXLog.error("Failed creating user session with error: \(error)") MXLog.error("Failed creating user session with error: \(error)")
@@ -102,77 +105,51 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
// Called just before the extension will be terminated by the system. // Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
MXLog.warning("\(tag) Extension time will expire") MXLog.warning("\(tag) Extension time will expire")
notify(unreadCount: nil) deliverNotification()
} }
// MARK: - Private // MARK: - Private
private func processEvent(_ eventID: String, private func processEvent(_ eventID: String,
roomID: String, roomID: String,
unreadCount: Int?,
userSession: NSEUserSession) async { userSession: NSEUserSession) async {
MXLog.info("\(tag) Processing event: \(eventID) in room: \(roomID)") MXLog.info("\(tag) Processing event: \(eventID) in room: \(roomID)")
do { guard let notificationItemProxy = await userSession.notificationItemProxy(roomID: roomID, eventID: eventID) else {
guard let itemProxy = await userSession.notificationItemProxy(roomID: roomID, eventID: eventID) else { MXLog.error("\(tag) Failed retrieving notification item")
MXLog.error("\(tag) Failed retrieving notification item") discardNotification()
return discard(unreadCount: unreadCount) return
} }
switch await preprocessNotification(itemProxy) { switch await preprocessNotification(notificationItemProxy) {
case .processedShouldDiscard, .unsupportedShouldDiscard: case .processedShouldDiscard, .unsupportedShouldDiscard:
return discard(unreadCount: unreadCount) discardNotification()
case .shouldDisplay: case .shouldDisplay:
break notificationContent = await notificationContentBuilder.process(notificationContent: notificationContent,
} notificationItem: notificationItemProxy,
mediaProvider: userSession.mediaProvider)
modifiedContent = try await notificationContentBuilder.content(for: itemProxy, mediaProvider: nil) deliverNotification()
guard itemProxy.hasMedia else {
MXLog.info("\(tag) Notification item doesn't contain media")
return notify(unreadCount: unreadCount)
}
MXLog.info("\(tag) Processing media")
if let latestContent = try? await notificationContentBuilder.content(for: itemProxy, mediaProvider: userSession.mediaProvider) {
modifiedContent = latestContent
} else {
MXLog.error("\(tag) Failed processing notification media")
}
return notify(unreadCount: unreadCount)
} catch {
MXLog.error("Failed processing with error: \(error)")
return discard(unreadCount: unreadCount)
} }
} }
private func notify(unreadCount: Int?) { private func deliverNotification() {
guard let modifiedContent else {
MXLog.error("\(tag) Notification modified content invalid")
return discard(unreadCount: unreadCount)
}
MXLog.info("\(tag) Displaying notification") MXLog.info("\(tag) Displaying notification")
if let unreadCount { contentHandler(notificationContent)
modifiedContent.badge = NSNumber(value: unreadCount)
}
handler?(modifiedContent)
cleanUp() cleanUp()
} }
private func discard(unreadCount: Int?) { private func discardNotification() {
MXLog.info("\(tag) Discarding notification") MXLog.info("\(tag) Discarding notification")
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
if let unreadCount { if let unreadCount = notificationContent.unreadCount {
content.badge = NSNumber(value: unreadCount) content.badge = NSNumber(value: unreadCount)
} }
handler?(content) contentHandler(content)
cleanUp() cleanUp()
} }
@@ -181,8 +158,8 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
} }
private func cleanUp() { private func cleanUp() {
handler = nil contentHandler = nil
modifiedContent = nil notificationContent = nil
} }
private func preprocessNotification(_ itemProxy: NotificationItemProxyProtocol) async -> NotificationProcessingResult { private func preprocessNotification(_ itemProxy: NotificationItemProxyProtocol) async -> NotificationProcessingResult {
@@ -1,27 +0,0 @@
//
// Copyright 2022-2024 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 Foundation
import UserNotifications
extension UNNotificationRequest {
var roomID: String? {
content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String
}
var eventID: String? {
content.userInfo[NotificationConstants.UserInfoKey.eventIdentifier] as? String
}
var unreadCount: Int? {
content.userInfo[NotificationConstants.UserInfoKey.unreadCount] as? Int
}
var pusherNotificationClientIdentifier: String? {
content.userInfo[NotificationConstants.UserInfoKey.pusherNotificationClientIdentifier] as? String
}
}