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..29ed0c0eaa 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
@@ -296,6 +297,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 +363,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 +407,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 +449,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 +493,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 +501,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 +533,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 +580,7 @@ class NotificationService: UNNotificationServiceExtension {
}
private func notificationContent(withTitle title: String?,
+ withSubTitle subTitle: String?,
body: String?,
threadIdentifier: String?,
userId: String?,
@@ -581,6 +592,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 +614,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,
+ isContactSuggestion: 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] {