From c32047e834ce13a8eae3be72e34627e49d11bf08 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Fri, 10 Sep 2021 17:57:13 +0200 Subject: [PATCH 1/2] Add communication Notification context to message notifications Signed-off-by: Finn Behrens --- Riot/SupportingFiles/Riot.entitlements | 2 + RiotNSE/Info.plist | 11 ++ RiotNSE/NotificationService.swift | 199 +++++++++++++++++++------ 3 files changed, 169 insertions(+), 43 deletions(-) diff --git a/Riot/SupportingFiles/Riot.entitlements b/Riot/SupportingFiles/Riot.entitlements index 965a767d45..d0bc2ad3d8 100644 --- a/Riot/SupportingFiles/Riot.entitlements +++ b/Riot/SupportingFiles/Riot.entitlements @@ -34,6 +34,8 @@ com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) + com.apple.developer.usernotifications.communication + com.apple.security.application-groups $(APPLICATION_GROUP_IDENTIFIER) diff --git a/RiotNSE/Info.plist b/RiotNSE/Info.plist index 5ce763e053..a8498b955d 100644 --- a/RiotNSE/Info.plist +++ b/RiotNSE/Info.plist @@ -22,6 +22,17 @@ $(CURRENT_PROJECT_VERSION) NSExtension + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsSupported + + INStartAudioCallIntent + INStartVideoCallIntent + INSendMessageIntent + + NSExtensionPointIdentifier com.apple.usernotifications.service NSExtensionPrincipalClass diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 0b2ff47588..0a01a87dd4 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -17,6 +17,7 @@ import UserNotifications import MatrixKit import MatrixSDK +import Intents /// The number of milliseconds in one second. private let MSEC_PER_SEC: TimeInterval = 1000 @@ -225,31 +226,30 @@ class NotificationService: UNNotificationServiceExtension { self.notificationContent(forEvent: event, forAccount: userAccount) { (notificationContent) in var isUnwantedNotification = false - // Modify the notification content here... - if let newContent = notificationContent { - content.title = newContent.title - content.subtitle = newContent.subtitle - content.body = newContent.body - content.threadIdentifier = newContent.threadIdentifier - content.categoryIdentifier = newContent.categoryIdentifier - content.userInfo = newContent.userInfo - content.sound = newContent.sound - } else { - // this is an unwanted notification, mark as to be deleted when app is foregrounded again OR a new push came + if notificationContent == nil { content.categoryIdentifier = Constants.toBeRemovedNotificationCategoryIdentifier isUnwantedNotification = true } if self.ongoingVoIPPushRequests[event.eventId] == true { // modify the best attempt content, to be able to use in the future - self.bestAttemptContents[event.eventId] = content + if let notificationContent = notificationContent { + // TODO: this will most likely break the notification context maybe there is a better way + self.bestAttemptContents[event.eventId] = notificationContent.mutableCopy() as? UNMutableNotificationContent + } else { + self.bestAttemptContents[event.eventId] = content + } // There is an ongoing VoIP Push request for this event, wait for it to be completed. // When it completes, it'll continue with the bestAttemptContent. return } else { MXLog.debug("[NotificationService] processEvent: Calling content handler for: \(String(describing: event.eventId)), isUnwanted: \(isUnwantedNotification)") - self.contentHandlers[event.eventId]?(content) + if let notificationContent = notificationContent { + self.contentHandlers[event.eventId]?(notificationContent) + } else { + self.contentHandlers[event.eventId]?(content) + } // clear maps self.contentHandlers.removeValue(forKey: event.eventId) self.bestAttemptContents.removeValue(forKey: event.eventId) @@ -296,6 +296,7 @@ class NotificationService: UNNotificationServiceExtension { switch response { case .success(let (roomState, eventSenderName)): var notificationTitle: String? + var notificationSubTitle: String? var notificationBody: String? var additionalUserInfo: [AnyHashable: Any]? @@ -361,9 +362,12 @@ class NotificationService: UNNotificationServiceExtension { let isReply = event.isReply() if isReply { - notificationTitle = self.replyTitle(for: eventSenderName, in: roomDisplayName) + notificationTitle = self.replyTitle(for: eventSenderName) } else { - notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) + notificationTitle = eventSenderName + } + if !(roomSummary?.isDirect ?? false) { + notificationSubTitle = roomDisplayName } if event.isEncrypted && !self.showDecryptedContentInNotifications { @@ -402,7 +406,10 @@ class NotificationService: UNNotificationServiceExtension { // If the current user is already joined, display updated displayname/avatar events. // This is an unexpected path, but has been seen in some circumstances. if NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId)?.membership == .join { - notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) + notificationTitle = eventSenderName + if !(roomSummary?.isDirect ?? false) { + notificationSubTitle = roomDisplayName + } // If the sender's membership is join and hasn't changed. if let newContent = MXRoomMemberEventContent(fromJSON: event.content), @@ -441,12 +448,18 @@ class NotificationService: UNNotificationServiceExtension { } case .sticker: - notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) + notificationTitle = eventSenderName + if !(roomSummary?.isDirect ?? false) { + notificationSubTitle = roomDisplayName + } notificationBody = NSString.localizedUserNotificationString(forKey: "STICKER_FROM_USER", arguments: [eventSenderName as Any]) // Reactions are unexpected notification types, but have been seen in some circumstances. case .reaction: - notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) + notificationTitle = eventSenderName + if !(roomSummary?.isDirect ?? false) { + notificationSubTitle = roomDisplayName + } if let reactionKey = event.relatesTo?.key { // Try to show the reaction key in the notification. notificationBody = NSString.localizedUserNotificationString(forKey: "REACTION_FROM_USER", arguments: [eventSenderName, reactionKey]) @@ -479,6 +492,7 @@ class NotificationService: UNNotificationServiceExtension { MXLog.debug("[NotificationService] notificationContentForEvent: Resetting title and body because app protection is set") notificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE_PROTECTED", arguments: []) notificationTitle = nil + notificationSubTitle = nil } guard notificationBody != nil else { @@ -486,17 +500,31 @@ class NotificationService: UNNotificationServiceExtension { onComplete(nil) return } - + let notificationContent = self.notificationContent(withTitle: notificationTitle, + withSubTitle: notificationSubTitle, body: notificationBody, threadIdentifier: threadIdentifier, userId: currentUserId, event: event, pushRule: pushRule, additionalInfo: additionalUserInfo) - - MXLog.debug("[NotificationService] notificationContentForEvent: Calling onComplete.") - onComplete(notificationContent) + + if #available(iOS 15.0, *) { + self.makeCommunicationNotification( + forEvent: event, + // TODO: use real room/user avatar + senderImage: INImage.systemImageNamed("person.circle.fill"), + body: notificationBody, + notificationContent: notificationContent, + roomDisplayName: roomDisplayName, + senderName: eventSenderName, + roomSummary: roomSummary, + onComplete: onComplete) + } else { + MXLog.debug("[NotificationService] notificationContentForEvent: Calling onComplete.") + onComplete(notificationContent) + } case .failure(let error): MXLog.debug("[NotificationService] notificationContentForEvent: error: \(error)") onComplete(nil) @@ -504,27 +532,8 @@ class NotificationService: UNNotificationServiceExtension { }) } - /// Returns the default title for message notifications. - /// - Parameters: - /// - eventSenderName: The displayname of the sender. - /// - roomDisplayName: The displayname of the room the message was sent in. - /// - Returns: A string to be used for the notification's title. - private func messageTitle(for eventSenderName: String, in roomDisplayName: String?) -> String { - // Display the room name only if it is different than the sender name - if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName { - return NSString.localizedUserNotificationString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName]) - } else { - return eventSenderName - } - } - - private func replyTitle(for eventSenderName: String, in roomDisplayName: String?) -> String { - // Display the room name only if it is different than the sender name - if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName { - return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName]) - } else { - return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_TITLE", arguments: [eventSenderName]) - } + private func replyTitle(for eventSenderName: String) -> String { + return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_TITLE", arguments: [eventSenderName]) } /// Get the context of an event. @@ -570,6 +579,7 @@ class NotificationService: UNNotificationServiceExtension { } private func notificationContent(withTitle title: String?, + withSubTitle subTitle: String?, body: String?, threadIdentifier: String?, userId: String?, @@ -581,6 +591,9 @@ class NotificationService: UNNotificationServiceExtension { if let title = title { notificationContent.title = title } + if let subTitle = subTitle { + notificationContent.subtitle = subTitle + } if let body = body { notificationContent.body = body } @@ -600,6 +613,106 @@ class NotificationService: UNNotificationServiceExtension { return notificationContent } + @available(iOS 15, *) + private func makeCommunicationNotification(forEvent event: MXEvent, + senderImage: INImage?, + body notificationBody: String?, + notificationContent: UNNotificationContent, + roomDisplayName: String?, + senderName: String, + roomSummary: MXRoomSummary?, + onComplete: @escaping (UNNotificationContent?) -> Void) { + switch event.eventType { + case .roomMessage: + let intent = self.getMessageIntent( + forEvent: event, + body: notificationBody ?? "", + groupName: roomSummary?.isDirect ?? true ? nil : roomDisplayName, + senderName: senderName, + senderImage: senderImage) + intent.setImage(senderImage, forParameterNamed: \.sender) + do { + // TODO: figure out how to set _mentionsCurrentUser and _replyToCurrentUser in the context + let notificationContent = try notificationContent.updating(from: intent) + MXLog.debug("[NotificationService] makeCommunicationNotification: Calling onComplete.") + onComplete(notificationContent) + } catch { + MXLog.debug("[NotificationService] makeCommunicationNotification: error (roomMessage): \(error)") + onComplete(notificationContent) + } + default: + onComplete(notificationContent) + } + } + + + @available(iOS 15, *) + private func getMessageIntent(forEvent event: MXEvent, + body: String, + groupName: String?, + senderName: String, + senderImage: INImage?) -> INSendMessageIntent { + let intentType = getMessageIntentType(event.content["msgtype"] as? String, event) + + let speakableGroupName = groupName.flatMap({ INSpeakableString(spokenPhrase: $0 )}) + + let sender = INPerson( + personHandle: INPersonHandle(value: event.sender, type: .unknown), + nameComponents: nil, + displayName: senderName, + image: senderImage, + contactIdentifier: nil, + customIdentifier: event.sender, + isMe: false, + suggestionType: .instantMessageAddress + ) + + let incomingMessageIntent = INSendMessageIntent( + // TODO: check + recipients: [], + outgoingMessageType: intentType, + content: body, + speakableGroupName: speakableGroupName, + conversationIdentifier: event.roomId, + // TODO: check + serviceName: nil, + sender: sender, + attachments: nil + ) + + let intent = INInteraction(intent: incomingMessageIntent, response: nil) + intent.direction = .incoming + + intent.donate(completion: nil) + + return incomingMessageIntent + } + + @available(iOS 14, *) + private func getMessageIntentType(_ msgType: String?, _ event: MXEvent) -> INOutgoingMessageType { + guard let msgType = msgType else { + return .unknown + } + + switch msgType { + case kMXMessageTypeEmote: + fallthrough + case kMXMessageTypeImage: + fallthrough + case kMXMessageTypeVideo: + fallthrough + case kMXMessageTypeFile: + return .unknown + case kMXMessageTypeAudio: + if event.isVoiceMessage() { + return .outgoingMessageAudio + } + return .unknown + default: + return .outgoingMessageText + } + } + private func notificationUserInfo(forEvent event: MXEvent, andUserId userId: String?, additionalInfo: [AnyHashable: Any]? = nil) -> [AnyHashable: Any] { From 8c62814dc3f8f7ba0d1b24b7afb36f52fb7a09dc Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Fri, 10 Sep 2021 21:25:55 +0200 Subject: [PATCH 2/2] github ci: change to Xcode 13 beta Signed-off-by: Finn Behrens --- .github/workflows/ci-build.yml | 2 +- .github/workflows/ci-tests.yml | 2 +- .github/workflows/release-alpha.yml | 2 +- fastlane/Fastfile | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index d1cd1b3a9d..c86de1d806 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -16,7 +16,7 @@ env: jobs: build: name: Build - runs-on: macos-latest + runs-on: macos-11 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index eac0c885de..65846be15f 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -16,7 +16,7 @@ env: jobs: tests: name: Tests - runs-on: macos-latest + runs-on: macos-11 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index af74f48275..ed175fe207 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -15,7 +15,7 @@ env: jobs: build: name: Release - runs-on: macos-latest + runs-on: macos-11 steps: - uses: actions/checkout@v2 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1637c458db..e909c683af 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,7 +21,7 @@ platform :ios do before_all do # Ensure used Xcode version - xcversion(version: "~> 12.1") + xcversion(version: "~> 13.0") end #### Public ####