Skip to content

Commit f773f88

Browse files
committed
Add communication Notification context to message notifications
Signed-off-by: Finn Behrens <[email protected]>
1 parent b11db76 commit f773f88

File tree

3 files changed

+157
-30
lines changed

3 files changed

+157
-30
lines changed

Riot/SupportingFiles/Riot.entitlements

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
</array>
3535
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
3636
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
37+
<key>com.apple.developer.usernotifications.communication</key>
38+
<true/>
3739
<key>com.apple.security.application-groups</key>
3840
<array>
3941
<string>$(APPLICATION_GROUP_IDENTIFIER)</string>

RiotNSE/Info.plist

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@
2222
<string>$(CURRENT_PROJECT_VERSION)</string>
2323
<key>NSExtension</key>
2424
<dict>
25+
<key>NSExtensionAttributes</key>
26+
<dict>
27+
<key>IntentsRestrictedWhileLocked</key>
28+
<array/>
29+
<key>IntentsSupported</key>
30+
<array>
31+
<string>INStartAudioCallIntent</string>
32+
<string>INStartVideoCallIntent</string>
33+
<string>INSendMessageIntent</string>
34+
</array>
35+
</dict>
2536
<key>NSExtensionPointIdentifier</key>
2637
<string>com.apple.usernotifications.service</string>
2738
<key>NSExtensionPrincipalClass</key>

RiotNSE/NotificationService.swift

Lines changed: 144 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import UserNotifications
1818
import MatrixKit
1919
import MatrixSDK
20+
import Intents
2021

2122
/// The number of milliseconds in one second.
2223
private let MSEC_PER_SEC: TimeInterval = 1000
@@ -296,6 +297,7 @@ class NotificationService: UNNotificationServiceExtension {
296297
switch response {
297298
case .success(let (roomState, eventSenderName)):
298299
var notificationTitle: String?
300+
var notificationSubTitle: String?
299301
var notificationBody: String?
300302
var additionalUserInfo: [AnyHashable: Any]?
301303

@@ -361,9 +363,12 @@ class NotificationService: UNNotificationServiceExtension {
361363
let isReply = event.isReply()
362364

363365
if isReply {
364-
notificationTitle = self.replyTitle(for: eventSenderName, in: roomDisplayName)
366+
notificationTitle = self.replyTitle(for: eventSenderName)
365367
} else {
366-
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
368+
notificationTitle = eventSenderName
369+
}
370+
if !(roomSummary?.isDirect ?? false) {
371+
notificationSubTitle = roomDisplayName
367372
}
368373

369374
if event.isEncrypted && !self.showDecryptedContentInNotifications {
@@ -402,7 +407,10 @@ class NotificationService: UNNotificationServiceExtension {
402407
// If the current user is already joined, display updated displayname/avatar events.
403408
// This is an unexpected path, but has been seen in some circumstances.
404409
if NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId)?.membership == .join {
405-
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
410+
notificationTitle = eventSenderName
411+
if !(roomSummary?.isDirect ?? false) {
412+
notificationSubTitle = roomDisplayName
413+
}
406414

407415
// If the sender's membership is join and hasn't changed.
408416
if let newContent = MXRoomMemberEventContent(fromJSON: event.content),
@@ -441,12 +449,18 @@ class NotificationService: UNNotificationServiceExtension {
441449
}
442450

443451
case .sticker:
444-
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
452+
notificationTitle = eventSenderName
453+
if !(roomSummary?.isDirect ?? false) {
454+
notificationSubTitle = roomDisplayName
455+
}
445456
notificationBody = NSString.localizedUserNotificationString(forKey: "STICKER_FROM_USER", arguments: [eventSenderName as Any])
446457

447458
// Reactions are unexpected notification types, but have been seen in some circumstances.
448459
case .reaction:
449-
notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName)
460+
notificationTitle = eventSenderName
461+
if !(roomSummary?.isDirect ?? false) {
462+
notificationSubTitle = roomDisplayName
463+
}
450464
if let reactionKey = event.relatesTo?.key {
451465
// Try to show the reaction key in the notification.
452466
notificationBody = NSString.localizedUserNotificationString(forKey: "REACTION_FROM_USER", arguments: [eventSenderName, reactionKey])
@@ -479,52 +493,48 @@ class NotificationService: UNNotificationServiceExtension {
479493
MXLog.debug("[NotificationService] notificationContentForEvent: Resetting title and body because app protection is set")
480494
notificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE_PROTECTED", arguments: [])
481495
notificationTitle = nil
496+
notificationSubTitle = nil
482497
}
483498

484499
guard notificationBody != nil else {
485500
MXLog.debug("[NotificationService] notificationContentForEvent: notificationBody is nil")
486501
onComplete(nil)
487502
return
488503
}
489-
504+
490505
let notificationContent = self.notificationContent(withTitle: notificationTitle,
506+
withSubTitle: notificationSubTitle,
491507
body: notificationBody,
492508
threadIdentifier: threadIdentifier,
493509
userId: currentUserId,
494510
event: event,
495511
pushRule: pushRule,
496512
additionalInfo: additionalUserInfo)
497-
498-
MXLog.debug("[NotificationService] notificationContentForEvent: Calling onComplete.")
499-
onComplete(notificationContent)
513+
514+
if #available(iOS 15.0, *) {
515+
self.makeCommunicationNotification(
516+
forEvent: event,
517+
// TODO: use real room/user avatar
518+
senderImage: INImage.systemImageNamed("person.circle.fill"),
519+
body: notificationBody,
520+
notificationContent: notificationContent,
521+
roomDisplayName: roomDisplayName,
522+
senderName: eventSenderName,
523+
roomSummary: roomSummary,
524+
onComplete: onComplete)
525+
} else {
526+
MXLog.debug("[NotificationService] notificationContentForEvent: Calling onComplete.")
527+
onComplete(notificationContent)
528+
}
500529
case .failure(let error):
501530
MXLog.debug("[NotificationService] notificationContentForEvent: error: \(error)")
502531
onComplete(nil)
503532
}
504533
})
505534
}
506535

507-
/// Returns the default title for message notifications.
508-
/// - Parameters:
509-
/// - eventSenderName: The displayname of the sender.
510-
/// - roomDisplayName: The displayname of the room the message was sent in.
511-
/// - Returns: A string to be used for the notification's title.
512-
private func messageTitle(for eventSenderName: String, in roomDisplayName: String?) -> String {
513-
// Display the room name only if it is different than the sender name
514-
if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName {
515-
return NSString.localizedUserNotificationString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName])
516-
} else {
517-
return eventSenderName
518-
}
519-
}
520-
521-
private func replyTitle(for eventSenderName: String, in roomDisplayName: String?) -> String {
522-
// Display the room name only if it is different than the sender name
523-
if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName {
524-
return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName])
525-
} else {
526-
return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_TITLE", arguments: [eventSenderName])
527-
}
536+
private func replyTitle(for eventSenderName: String) -> String {
537+
return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_TITLE", arguments: [eventSenderName])
528538
}
529539

530540
/// Get the context of an event.
@@ -570,6 +580,7 @@ class NotificationService: UNNotificationServiceExtension {
570580
}
571581

572582
private func notificationContent(withTitle title: String?,
583+
withSubTitle subTitle: String?,
573584
body: String?,
574585
threadIdentifier: String?,
575586
userId: String?,
@@ -581,6 +592,9 @@ class NotificationService: UNNotificationServiceExtension {
581592
if let title = title {
582593
notificationContent.title = title
583594
}
595+
if let subTitle = subTitle {
596+
notificationContent.subtitle = subTitle
597+
}
584598
if let body = body {
585599
notificationContent.body = body
586600
}
@@ -600,6 +614,106 @@ class NotificationService: UNNotificationServiceExtension {
600614
return notificationContent
601615
}
602616

617+
@available(iOS 15, *)
618+
private func makeCommunicationNotification(forEvent event: MXEvent,
619+
senderImage: INImage?,
620+
body notificationBody: String?,
621+
notificationContent: UNNotificationContent,
622+
roomDisplayName: String?,
623+
senderName: String,
624+
roomSummary: MXRoomSummary?,
625+
onComplete: @escaping (UNNotificationContent?) -> Void) {
626+
switch event.eventType {
627+
case .roomMessage:
628+
let intent = self.getMessageIntent(
629+
forEvent: event,
630+
body: notificationBody ?? "",
631+
groupName: roomSummary?.isDirect ?? true ? nil : roomDisplayName,
632+
senderName: senderName,
633+
senderImage: senderImage)
634+
intent.setImage(senderImage, forParameterNamed: \.sender)
635+
do {
636+
// TODO: figure out how to set _mentionsCurrentUser and _replyToCurrentUser in the context
637+
let notificationContent = try notificationContent.updating(from: intent)
638+
MXLog.debug("[NotificationService] makeCommunicationNotification: Calling onComplete.")
639+
onComplete(notificationContent)
640+
} catch {
641+
MXLog.debug("[NotificationService] makeCommunicationNotification: error (roomMessage): \(error)")
642+
onComplete(notificationContent)
643+
}
644+
default:
645+
onComplete(notificationContent)
646+
}
647+
}
648+
649+
650+
@available(iOS 15, *)
651+
private func getMessageIntent(forEvent event: MXEvent,
652+
body: String,
653+
groupName: String?,
654+
senderName: String,
655+
senderImage: INImage?) -> INSendMessageIntent {
656+
let intentType = getMessageIntentType(event.content["msgtype"] as? String, event)
657+
658+
let speakableGroupName = groupName.flatMap({ INSpeakableString(spokenPhrase: $0 )})
659+
660+
let sender = INPerson(
661+
personHandle: INPersonHandle(value: event.sender, type: .unknown),
662+
nameComponents: nil,
663+
displayName: senderName,
664+
image: senderImage,
665+
contactIdentifier: nil,
666+
customIdentifier: event.sender,
667+
isContactSuggestion: false,
668+
suggestionType: .instantMessageAddress
669+
)
670+
671+
let incomingMessageIntent = INSendMessageIntent(
672+
// TODO: check
673+
recipients: [],
674+
outgoingMessageType: intentType,
675+
content: body,
676+
speakableGroupName: speakableGroupName,
677+
conversationIdentifier: event.roomId,
678+
// TODO: check
679+
serviceName: nil,
680+
sender: sender,
681+
attachments: nil
682+
)
683+
684+
let intent = INInteraction(intent: incomingMessageIntent, response: nil)
685+
intent.direction = .incoming
686+
687+
intent.donate(completion: nil)
688+
689+
return incomingMessageIntent
690+
}
691+
692+
@available(iOS 14, *)
693+
private func getMessageIntentType(_ msgType: String?, _ event: MXEvent) -> INOutgoingMessageType {
694+
guard let msgType = msgType else {
695+
return .unknown
696+
}
697+
698+
switch msgType {
699+
case kMXMessageTypeEmote:
700+
fallthrough
701+
case kMXMessageTypeImage:
702+
fallthrough
703+
case kMXMessageTypeVideo:
704+
fallthrough
705+
case kMXMessageTypeFile:
706+
return .unknown
707+
case kMXMessageTypeAudio:
708+
if event.isVoiceMessage() {
709+
return .outgoingMessageAudio
710+
}
711+
return .unknown
712+
default:
713+
return .outgoingMessageText
714+
}
715+
}
716+
603717
private func notificationUserInfo(forEvent event: MXEvent,
604718
andUserId userId: String?,
605719
additionalInfo: [AnyHashable: Any]? = nil) -> [AnyHashable: Any] {

0 commit comments

Comments
 (0)