Skip to content

Commit ddba383

Browse files
committed
Refactor avatar handling in notifications
- Moved avatar fetching logic to NotificationHelper for better reusability. - Updated CustomPushNotification to utilize the new fetchAvatarBitmap method. - Enhanced Ejson class with a buildAvatarUri helper method for cleaner avatar URI generation. - Added support for fetching caller avatar in VideoConfNotification, improving notification display.
1 parent a6ddbb3 commit ddba383

File tree

4 files changed

+104
-31
lines changed

4 files changed

+104
-31
lines changed

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

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818

1919
import androidx.annotation.Nullable;
2020

21-
import com.bumptech.glide.Glide;
22-
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
23-
import com.bumptech.glide.request.RequestOptions;
2421
import com.facebook.react.bridge.ReactApplicationContext;
2522
import com.google.gson.Gson;
2623

@@ -30,9 +27,6 @@
3027
import java.util.List;
3128
import java.util.Map;
3229
import java.util.concurrent.ConcurrentHashMap;
33-
import java.util.concurrent.ExecutionException;
34-
import java.util.concurrent.TimeUnit;
35-
import java.util.concurrent.TimeoutException;
3630

3731
import chat.rocket.reactnative.BuildConfig;
3832
import chat.rocket.reactnative.MainActivity;
@@ -469,25 +463,7 @@ private void cancelPreviousFallbackNotifications(Ejson ejson) {
469463
}
470464

471465
private Bitmap getAvatar(String uri) {
472-
if (uri == null || uri.isEmpty()) {
473-
return largeIcon();
474-
}
475-
476-
try {
477-
// Use a 3-second timeout to avoid blocking the FCM service for too long
478-
// FCM has a 10-second limit, so we need to fail fast and use fallback icon
479-
Bitmap avatar = Glide.with(mContext)
480-
.asBitmap()
481-
.apply(RequestOptions.bitmapTransform(new RoundedCorners(10)))
482-
.load(uri)
483-
.submit(100, 100)
484-
.get(3, TimeUnit.SECONDS);
485-
486-
return avatar != null ? avatar : largeIcon();
487-
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
488-
Log.e(TAG, "Failed to fetch avatar", e);
489-
return largeIcon();
490-
}
466+
return NotificationHelper.fetchAvatarBitmap(mContext, uri, largeIcon());
491467
}
492468

493469
private Bitmap largeIcon() {

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

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,29 @@ private MMKV getMMKV() {
6060
return MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE);
6161
}
6262

63-
public String getAvatarUri() {
63+
/**
64+
* Helper method to build avatar URI from avatar path.
65+
* Validates server URL and credentials, then constructs the full URI.
66+
*/
67+
private String buildAvatarUri(String avatarPath, String errorContext) {
6468
String server = serverURL();
6569
if (server == null || server.isEmpty()) {
66-
Log.w(TAG, "Cannot generate avatar URI: serverURL is null");
70+
Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: serverURL is null");
6771
return null;
6872
}
6973

7074
String userToken = token();
7175
String uid = userId();
7276

7377
if (userToken.isEmpty() || uid.isEmpty()) {
74-
Log.w(TAG, "Cannot generate avatar URI: missing auth credentials");
78+
Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: missing auth credentials");
7579
return null;
7680
}
7781

82+
return server + avatarPath + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid;
83+
}
84+
85+
public String getAvatarUri() {
7886
String avatarPath;
7987

8088
// For DMs, show sender's avatar; for groups/channels, show room avatar
@@ -104,7 +112,27 @@ public String getAvatarUri() {
104112
}
105113
}
106114

107-
return server + avatarPath + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid;
115+
return buildAvatarUri(avatarPath, "");
116+
}
117+
118+
/**
119+
* Generates avatar URI for video conference caller.
120+
* Returns null if caller username is not available (username is required for avatar endpoint).
121+
*/
122+
public String getCallerAvatarUri() {
123+
// Check if caller exists and has username (required - /avatar/{userId} endpoint doesn't exist)
124+
if (caller == null || caller.username == null || caller.username.isEmpty()) {
125+
Log.w(TAG, "Cannot generate caller avatar URI: caller or username is null");
126+
return null;
127+
}
128+
129+
try {
130+
String avatarPath = "/avatar/" + URLEncoder.encode(caller.username, "UTF-8");
131+
return buildAvatarUri(avatarPath, "caller");
132+
} catch (UnsupportedEncodingException e) {
133+
Log.e(TAG, "Failed to encode caller username", e);
134+
return null;
135+
}
108136
}
109137

110138
public String token() {
@@ -215,6 +243,7 @@ static class Sender {
215243
static class Caller {
216244
String _id;
217245
String name;
246+
String username;
218247
}
219248

220249
static class Content {

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
package chat.rocket.reactnative.notification;
22

3-
import android.os.Build;
3+
import android.content.Context;
4+
import android.graphics.Bitmap;
5+
import android.util.Log;
6+
7+
import androidx.annotation.Nullable;
8+
9+
import com.bumptech.glide.Glide;
10+
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
11+
import com.bumptech.glide.request.RequestOptions;
12+
13+
import java.util.concurrent.ExecutionException;
14+
import java.util.concurrent.TimeUnit;
15+
import java.util.concurrent.TimeoutException;
416

517
import chat.rocket.reactnative.BuildConfig;
618

@@ -32,10 +44,44 @@ public static String sanitizeUrl(String url) {
3244
* @return User-Agent string
3345
*/
3446
public static String getUserAgent() {
35-
String systemVersion = Build.VERSION.RELEASE;
47+
String systemVersion = android.os.Build.VERSION.RELEASE;
3648
String appVersion = BuildConfig.VERSION_NAME;
3749
int buildNumber = BuildConfig.VERSION_CODE;
3850
return String.format("RC Mobile; android %s; v%s (%d)", systemVersion, appVersion, buildNumber);
3951
}
52+
53+
/**
54+
* Fetches avatar bitmap from URI using Glide.
55+
* Uses a 3-second timeout to avoid blocking the FCM service for too long.
56+
*
57+
* @param context The application context
58+
* @param uri The avatar URI to fetch
59+
* @param fallbackIcon Optional fallback bitmap (null if no fallback desired)
60+
* @return Avatar bitmap, or fallbackIcon if fetch fails, or null if no fallback provided
61+
*/
62+
public static Bitmap fetchAvatarBitmap(Context context, String uri, @Nullable Bitmap fallbackIcon) {
63+
if (uri == null || uri.isEmpty()) {
64+
return fallbackIcon;
65+
}
66+
67+
try {
68+
// Use a 3-second timeout to avoid blocking the FCM service for too long
69+
// FCM has a 10-second limit, so we need to fail fast and use fallback icon
70+
Bitmap avatar = Glide.with(context)
71+
.asBitmap()
72+
.apply(RequestOptions.bitmapTransform(new RoundedCorners(10)))
73+
.load(uri)
74+
.submit(100, 100)
75+
.get(3, TimeUnit.SECONDS);
76+
77+
return avatar != null ? avatar : fallbackIcon;
78+
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
79+
Log.e("NotificationHelper", "Failed to fetch avatar", e);
80+
if (e instanceof InterruptedException) {
81+
Thread.currentThread().interrupt();
82+
}
83+
return fallbackIcon;
84+
}
85+
}
4086
}
4187

android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.content.Context
88
import android.content.Intent
99
import android.media.AudioAttributes
1010
import android.media.RingtoneManager
11+
import android.graphics.Bitmap
1112
import android.os.Build
1213
import android.os.Bundle
1314
import android.util.Log
@@ -155,6 +156,14 @@ class VideoConfNotification(private val context: Context) {
155156
val packageName = context.packageName
156157
val smallIconResId = context.resources.getIdentifier("ic_notification", "drawable", packageName)
157158

159+
// Fetch caller avatar
160+
val avatarUri = ejson.getCallerAvatarUri()
161+
val avatarBitmap = if (avatarUri != null) {
162+
getAvatar(avatarUri)
163+
} else {
164+
null
165+
}
166+
158167
// Build notification
159168
val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply {
160169
setSmallIcon(smallIconResId)
@@ -169,6 +178,11 @@ class VideoConfNotification(private val context: Context) {
169178
setContentIntent(fullScreenPendingIntent)
170179
addAction(0, "Decline", declinePendingIntent)
171180
addAction(0, "Accept", acceptPendingIntent)
181+
182+
// Set large icon (avatar) if available
183+
if (avatarBitmap != null) {
184+
setLargeIcon(avatarBitmap)
185+
}
172186
}
173187

174188
// Set sound for pre-O devices
@@ -194,6 +208,14 @@ class VideoConfNotification(private val context: Context) {
194208
return PendingIntent.getActivity(context, requestCode, intent, flags)
195209
}
196210

211+
/**
212+
* Fetches avatar bitmap from URI using Glide.
213+
* Returns null if fetch fails or times out, in which case notification will display without avatar.
214+
*/
215+
private fun getAvatar(uri: String): Bitmap? {
216+
return NotificationHelper.fetchAvatarBitmap(context, uri, null)
217+
}
218+
197219
/**
198220
* Cancels a video call notification.
199221
*

0 commit comments

Comments
 (0)