11import UserNotifications
2+ import Intents
23
34class NotificationService : UNNotificationServiceExtension {
45
@@ -8,25 +9,36 @@ class NotificationService: UNNotificationServiceExtension {
89
910 // MARK: - Avatar Fetching
1011
11- func fetchAvatar( from payload: Payload , completion: @escaping ( UNNotificationAttachment ? ) -> Void ) {
12- guard let username = payload. sender? . username else {
13- completion ( nil )
14- return
15- }
16-
12+ /// Fetches avatar image data - sender's avatar for DMs, room avatar for groups/channels
13+ func fetchAvatarData( from payload: Payload , completion: @escaping ( Data ? ) -> Void ) {
1714 let server = payload. host. removeTrailingSlash ( )
1815 guard let credentials = Storage ( ) . getCredentials ( server: server) else {
1916 completion ( nil )
2017 return
2118 }
2219
23- // Build authenticated avatar URL (URL encode username for special characters)
24- guard let encodedUsername = username. addingPercentEncoding ( withAllowedCharacters: . urlPathAllowed) else {
25- completion ( nil )
26- return
20+ // Build avatar path based on room type
21+ let avatarPath : String
22+
23+ if payload. type == . direct {
24+ // Direct message: use sender's avatar
25+ guard let username = payload. sender? . username,
26+ let encodedUsername = username. addingPercentEncoding ( withAllowedCharacters: . urlPathAllowed) else {
27+ completion ( nil )
28+ return
29+ }
30+ avatarPath = " /avatar/ \( encodedUsername) "
31+ } else {
32+ // Group/Channel/Livechat: use room avatar
33+ guard let rid = payload. rid else {
34+ completion ( nil )
35+ return
36+ }
37+ avatarPath = " /avatar/room/ \( rid) "
2738 }
28- let avatarPath = " /avatar/ \( encodedUsername) ?format=png&size=100&rc_token= \( credentials. userToken) &rc_uid= \( credentials. userId) "
29- guard let avatarURL = URL ( string: server + avatarPath) else {
39+
40+ let fullPath = " \( avatarPath) ?format=png&size=100&rc_token= \( credentials. userToken) &rc_uid= \( credentials. userId) "
41+ guard let avatarURL = URL ( string: server + fullPath) else {
3042 completion ( nil )
3143 return
3244 }
@@ -44,27 +56,76 @@ class NotificationService: UNNotificationServiceExtension {
4456 completion ( nil )
4557 return
4658 }
47-
48- // Save to temp file (UNNotificationAttachment requires file URL)
49- let tempDir = FileManager . default. temporaryDirectory
50- let fileName = " \( username) _avatar.png "
51- let fileURL = tempDir. appendingPathComponent ( fileName)
52-
53- do {
54- try data. write ( to: fileURL)
55- let attachment = try UNNotificationAttachment (
56- identifier: " avatar " ,
57- url: fileURL,
58- options: [ UNNotificationAttachmentOptionsTypeHintKey: " public.png " ]
59- )
60- completion ( attachment)
61- } catch {
62- completion ( nil )
63- }
59+ completion ( data)
6460 }
6561 task. resume ( )
6662 }
6763
64+ // MARK: - Communication Notification
65+
66+ /// Updates the notification content with sender avatar using Communication Notifications API
67+ func updateNotificationAsCommunication( payload: Payload , avatarData: Data ? ) {
68+ guard let bestAttemptContent = bestAttemptContent else { return }
69+
70+ let senderName = payload. sender? . name ?? payload. senderName ?? " Unknown "
71+ let senderId = payload. sender? . _id ?? " "
72+ let senderUsername = payload. sender? . username ?? " "
73+
74+ // Create avatar image for the sender
75+ var senderImage : INImage ? = nil
76+ if let data = avatarData {
77+ senderImage = INImage ( imageData: data)
78+ }
79+
80+ // Create the sender as an INPerson
81+ let sender = INPerson (
82+ personHandle: INPersonHandle ( value: senderUsername, type: . unknown) ,
83+ nameComponents: nil ,
84+ displayName: senderName,
85+ image: senderImage,
86+ contactIdentifier: nil ,
87+ customIdentifier: senderId
88+ )
89+
90+ // Determine conversation name (room name for groups, sender name for DMs)
91+ let conversationName : String
92+ if payload. type == . group, let roomName = payload. name {
93+ conversationName = roomName
94+ } else {
95+ conversationName = senderName
96+ }
97+
98+ // Create the messaging intent
99+ let intent = INSendMessageIntent (
100+ recipients: nil ,
101+ outgoingMessageType: . outgoingMessageText,
102+ content: bestAttemptContent. body,
103+ speakableGroupName: INSpeakableString ( spokenPhrase: conversationName) ,
104+ conversationIdentifier: payload. rid ?? " " ,
105+ serviceName: nil ,
106+ sender: sender,
107+ attachments: nil
108+ )
109+
110+ // If it's a group chat, set the group avatar (optional, uses sender avatar as fallback)
111+ if payload. type == . group {
112+ intent. setImage ( senderImage, forParameterNamed: \. speakableGroupName)
113+ }
114+
115+ // Donate the interaction for Siri suggestions
116+ let interaction = INInteraction ( intent: intent, response: nil )
117+ interaction. direction = . incoming
118+ interaction. donate ( completion: nil )
119+
120+ // Update the notification content with the intent
121+ do {
122+ let updatedContent = try bestAttemptContent. updating ( from: intent)
123+ self . bestAttemptContent = updatedContent
124+ } catch {
125+ // Failed to update with intent, will fall back to regular notification
126+ }
127+ }
128+
68129 override func didReceive( _ request: UNNotificationRequest , withContentHandler contentHandler: @escaping ( UNNotificationContent ) -> Void ) {
69130 self . contentHandler = contentHandler
70131 bestAttemptContent = ( request. content. mutableCopy ( ) as? UNMutableNotificationContent )
@@ -154,9 +215,7 @@ class NotificationService: UNNotificationServiceExtension {
154215 bestAttemptContent. body = String ( format: NSLocalizedString ( " Incoming call from %@ " , comment: " " ) , callerName)
155216 bestAttemptContent. categoryIdentifier = " VIDEOCONF "
156217 bestAttemptContent. sound = UNNotificationSound ( named: UNNotificationSoundName ( " ringtone.mp3 " ) )
157- if #available( iOS 15 . 0 , * ) {
158- bestAttemptContent. interruptionLevel = . timeSensitive
159- }
218+ bestAttemptContent. interruptionLevel = . timeSensitive
160219
161220 contentHandler ? ( bestAttemptContent)
162221 }
@@ -185,13 +244,11 @@ class NotificationService: UNNotificationServiceExtension {
185244 }
186245 }
187246
188- // Fetch avatar and deliver notification
189- fetchAvatar ( from: payload) { [ weak self] attachment in
247+ // Fetch avatar and deliver notification with Communication Notification style
248+ fetchAvatarData ( from: payload) { [ weak self] avatarData in
190249 guard let self = self else { return }
191250
192- if let attachment = attachment {
193- self . bestAttemptContent? . attachments = [ attachment]
194- }
251+ self . updateNotificationAsCommunication ( payload: payload, avatarData: avatarData)
195252
196253 if let bestAttemptContent = self . bestAttemptContent {
197254 self . contentHandler ? ( bestAttemptContent)
0 commit comments