Skip to content

Commit 9a6d3ba

Browse files
committed
feat: enhance avatar handling in notifications
- Updated avatar URI generation to differentiate between direct messages and group/channel notifications, ensuring the correct avatar is fetched based on the message type. - Refactored avatar fetching logic in NotificationService to improve clarity and maintainability. - Added support for Communication Notifications API to update notification content with sender avatars.
1 parent 8d10b22 commit 9a6d3ba

File tree

4 files changed

+121
-46
lines changed

4 files changed

+121
-46
lines changed

android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,6 @@ private MMKV getMMKV() {
5858
}
5959

6060
public String getAvatarUri() {
61-
if (sender == null || sender.username == null || sender.username.isEmpty()) {
62-
Log.w(TAG, "Cannot generate avatar URI: sender or username is null");
63-
return null;
64-
}
65-
6661
String server = serverURL();
6762
if (server == null || server.isEmpty()) {
6863
Log.w(TAG, "Cannot generate avatar URI: serverURL is null");
@@ -77,13 +72,32 @@ public String getAvatarUri() {
7772
return null;
7873
}
7974

80-
String uri = server + "/avatar/" + sender.username + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid;
75+
String avatarPath;
8176

82-
if (BuildConfig.DEBUG) {
83-
Log.d(TAG, "Generated avatar URI for user: " + sender.username);
77+
// For DMs, show sender's avatar; for groups/channels, show room avatar
78+
if ("d".equals(type)) {
79+
// Direct message: use sender's avatar
80+
if (sender == null || sender.username == null || sender.username.isEmpty()) {
81+
Log.w(TAG, "Cannot generate avatar URI: sender or username is null");
82+
return null;
83+
}
84+
avatarPath = "/avatar/" + sender.username;
85+
if (BuildConfig.DEBUG) {
86+
Log.d(TAG, "Generated avatar URI for user: " + sender.username);
87+
}
88+
} else {
89+
// Group/Channel/Livechat: use room avatar
90+
if (rid == null || rid.isEmpty()) {
91+
Log.w(TAG, "Cannot generate avatar URI: rid is null for non-DM");
92+
return null;
93+
}
94+
avatarPath = "/avatar/room/" + rid;
95+
if (BuildConfig.DEBUG) {
96+
Log.d(TAG, "Generated avatar URI for room: " + rid);
97+
}
8498
}
8599

86-
return uri;
100+
return server + avatarPath + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid;
87101
}
88102

89103
public String token() {

ios/NotificationService/NotificationService.entitlements

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>com.apple.developer.usernotifications.communication</key>
6+
<true/>
57
<key>com.apple.security.application-groups</key>
68
<array>
79
<string>group.ios.chat.rocket</string>

ios/NotificationService/NotificationService.swift

Lines changed: 94 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import UserNotifications
2+
import Intents
23

34
class 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)

ios/RocketChatRN/RocketChatRN.entitlements

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
<array>
1313
<string>applinks:go.rocket.chat</string>
1414
</array>
15+
<key>com.apple.developer.usernotifications.communication</key>
16+
<true/>
1517
<key>com.apple.security.application-groups</key>
1618
<array>
1719
<string>group.ios.chat.rocket</string>

0 commit comments

Comments
 (0)