@@ -10,6 +10,32 @@ class NotificationService: UNNotificationServiceExtension {
1010
1111 // MARK: - Avatar Fetching
1212
13+ /// Fetches avatar image data from a given avatar path
14+ private func fetchAvatarDataFromPath( avatarPath: String , server: String , credentials: Credentials , completion: @escaping ( Data ? ) -> Void ) {
15+ let fullPath = " \( avatarPath) ?format=png&size=100&rc_token= \( credentials. userToken) &rc_uid= \( credentials. userId) "
16+ guard let avatarURL = URL ( string: server + fullPath) else {
17+ completion ( nil )
18+ return
19+ }
20+
21+ // Create request with 3-second timeout
22+ var request = URLRequest ( url: avatarURL, timeoutInterval: 3 )
23+ request. httpMethod = " GET "
24+ request. addValue ( Bundle . userAgent, forHTTPHeaderField: " User-Agent " )
25+
26+ let task = URLSession . shared. dataTask ( with: request) { data, response, error in
27+ guard error == nil ,
28+ let httpResponse = response as? HTTPURLResponse ,
29+ httpResponse. statusCode == 200 ,
30+ let data = data else {
31+ completion ( nil )
32+ return
33+ }
34+ completion ( data)
35+ }
36+ task. resume ( )
37+ }
38+
1339 /// Fetches avatar image data - sender's avatar for DMs, room avatar for groups/channels
1440 func fetchAvatarData( from payload: Payload , completion: @escaping ( Data ? ) -> Void ) {
1541 let server = payload. host. removeTrailingSlash ( )
@@ -39,28 +65,27 @@ class NotificationService: UNNotificationServiceExtension {
3965 avatarPath = " /avatar/room/ \( rid) "
4066 }
4167
42- let fullPath = " \( avatarPath) ?format=png&size=100&rc_token= \( credentials. userToken) &rc_uid= \( credentials. userId) "
43- guard let avatarURL = URL ( string: server + fullPath) else {
68+ fetchAvatarDataFromPath ( avatarPath: avatarPath, server: server, credentials: credentials, completion: completion)
69+ }
70+
71+ /// Fetches avatar image data for video conference caller
72+ func fetchCallerAvatarData( from payload: Payload , completion: @escaping ( Data ? ) -> Void ) {
73+ let server = payload. host. removeTrailingSlash ( )
74+
75+ guard let credentials = Storage ( ) . getCredentials ( server: server) else {
4476 completion ( nil )
4577 return
4678 }
4779
48- // Create request with 3-second timeout
49- var request = URLRequest ( url: avatarURL, timeoutInterval: 3 )
50- request. httpMethod = " GET "
51- request. addValue ( Bundle . userAgent, forHTTPHeaderField: " User-Agent " )
52-
53- let task = URLSession . shared. dataTask ( with: request) { data, response, error in
54- guard error == nil ,
55- let httpResponse = response as? HTTPURLResponse ,
56- httpResponse. statusCode == 200 ,
57- let data = data else {
58- completion ( nil )
59- return
60- }
61- completion ( data)
80+ // Check if caller has username (required - /avatar/{userId} endpoint doesn't exist)
81+ guard let username = payload. caller? . username,
82+ let encodedUsername = username. addingPercentEncoding ( withAllowedCharacters: . urlPathAllowed) else {
83+ completion ( nil )
84+ return
6285 }
63- task. resume ( )
86+
87+ let avatarPath = " /avatar/ \( encodedUsername) "
88+ fetchAvatarDataFromPath ( avatarPath: avatarPath, server: server, credentials: credentials, completion: completion)
6489 }
6590
6691 // MARK: - Communication Notification
@@ -225,16 +250,67 @@ class NotificationService: UNNotificationServiceExtension {
225250
226251 // Status 0 (or nil) means incoming call - show notification with actions
227252 let callerName = payload. caller? . name ?? " Unknown "
253+ let callerUsername = payload. caller? . username ?? " "
228254
229255 bestAttemptContent. title = NSLocalizedString ( " Video Call " , comment: " " )
230256 bestAttemptContent. body = String ( format: NSLocalizedString ( " Incoming call from %@ " , comment: " " ) , callerName)
231257 bestAttemptContent. categoryIdentifier = " VIDEOCONF "
232258 bestAttemptContent. sound = UNNotificationSound ( named: UNNotificationSoundName ( " ringtone.mp3 " ) )
233259 bestAttemptContent. interruptionLevel = . timeSensitive
234260
235- contentHandler ? ( bestAttemptContent)
261+ // Fetch caller avatar if username is available
262+ fetchCallerAvatarData ( from: payload) { [ weak self] avatarData in
263+ guard let self = self , let bestAttemptContent = self . bestAttemptContent else {
264+ self ? . contentHandler ? ( bestAttemptContent ?? UNNotificationContent ( ) )
265+ return
266+ }
267+
268+ // Create caller image from avatar data
269+ var callerImage : INImage ? = nil
270+ if let data = avatarData {
271+ callerImage = INImage ( imageData: data)
272+ }
273+
274+ // Create caller as INPerson with avatar (similar to regular message notifications)
275+ let caller = INPerson (
276+ personHandle: INPersonHandle ( value: callerUsername, type: . unknown) ,
277+ nameComponents: nil ,
278+ displayName: callerName,
279+ image: callerImage,
280+ contactIdentifier: nil ,
281+ customIdentifier: nil
282+ )
283+
284+ // Use INSendMessageIntent to display avatar (works better than attachments for call notifications)
285+ // This uses the Communication Notifications API which properly displays avatars
286+ let intent = INSendMessageIntent (
287+ recipients: nil , // No recipients for incoming calls
288+ outgoingMessageType: . outgoingMessageText,
289+ content: bestAttemptContent. body,
290+ speakableGroupName: nil ,
291+ conversationIdentifier: payload. rid ?? " " ,
292+ serviceName: nil ,
293+ sender: caller, // Set caller as sender to display avatar
294+ attachments: nil
295+ )
296+
297+ // Donate the interaction for Siri suggestions
298+ let interaction = INInteraction ( intent: intent, response: nil )
299+ interaction. direction = . incoming
300+ interaction. donate ( completion: nil )
301+
302+ // Update the notification content with the intent to display avatar
303+ do {
304+ let updatedContent = try bestAttemptContent. updating ( from: intent)
305+ self . contentHandler ? ( updatedContent)
306+ } catch {
307+ // Fallback to bestAttemptContent if intent update fails
308+ self . contentHandler ? ( bestAttemptContent)
309+ }
310+ }
236311 }
237312
313+
238314 func processPayload( payload: Payload ) {
239315 // Set notification title based on payload type
240316 let senderName = payload. sender? . name ?? payload. senderName ?? " Unknown "
0 commit comments