Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d609360
feat(notifications): integrate expo-notifications for improved push h…
diegolmello Dec 10, 2025
6fa178e
refactor(notifications): remove react-native-notifications and implem…
diegolmello Dec 10, 2025
557c45c
refactor(notifications): remove react-native-notifications and enhanc…
diegolmello Dec 10, 2025
01e6e60
feat(notifications): implement video conference notification handling
diegolmello Dec 10, 2025
8d0506c
feat(notifications): enhance video conference notification handling f…
diegolmello Dec 10, 2025
4826fbe
feat(notifications): improve video conference notification handling
diegolmello Dec 10, 2025
952068d
refactor(notifications): remove Notifee and Firebase messaging depend…
diegolmello Dec 10, 2025
d44a321
fix lint
diegolmello Dec 10, 2025
f037939
target arm64 on e2e ci
diegolmello Dec 11, 2025
3009671
Fix user agent on push.get
diegolmello Dec 11, 2025
6620a1b
refactor(notifications): migrate video conference notification handli…
diegolmello Dec 11, 2025
7f98e86
chore: format code and fix lint issues [skip ci]
diegolmello Dec 11, 2025
08e0dc4
Merge branch 'develop' into chore.expo-notifications
diegolmello Dec 15, 2025
8af4d01
fix: improve push notification handling and error management
diegolmello Dec 15, 2025
81166aa
feat: enhance notification payload handling in NotificationService
diegolmello Dec 15, 2025
8d10b22
feat: implement avatar fetching for notifications
diegolmello Dec 16, 2025
9a6d3ba
feat: enhance avatar handling in notifications
diegolmello Dec 16, 2025
ed69b47
feat: enhance notification handling and structure
diegolmello Dec 16, 2025
59e2253
Merge branch 'develop' into feat.push-avatar-ios
diegolmello Dec 18, 2025
912ccaf
Merge branch 'develop' into feat.push-avatar-ios
diegolmello Dec 22, 2025
796e175
Fix pbxproj
diegolmello Dec 22, 2025
07a46b3
feat: improve notification handling and error management
diegolmello Dec 22, 2025
d255e10
chore: format code and fix lint issues [skip ci]
diegolmello Dec 22, 2025
88e5516
feat: refine notification title handling based on message type
diegolmello Dec 23, 2025
6d1ca14
Merge branch 'feat.push-avatar-ios' of github.com:RocketChat/Rocket.C…
diegolmello Dec 23, 2025
558cfa2
Merge branch 'develop' into feat.push-avatar-ios
diegolmello Dec 23, 2025
85c4d37
Fix bugsnag on pbxproj
diegolmello Dec 23, 2025
5ee2de6
Fix group on iOS notifications
diegolmello Dec 23, 2025
4dfbe86
Refactor Ejson class to use UnsupportedEncodingException for URI enco…
diegolmello Dec 23, 2025
dbf4205
pod repo-update
diegolmello Dec 23, 2025
a6ddbb3
Fix android push avatars
diegolmello Dec 24, 2025
ddba383
Refactor avatar handling in notifications
diegolmello Dec 24, 2025
2941d2c
Fix avatar on iOS videoconf
diegolmello Dec 24, 2025
fafc323
Refactor NotificationService
diegolmello Dec 24, 2025
32ea524
Merge branch 'develop' into feat.push-avatar-ios
diegolmello Dec 24, 2025
c0a2ffd
Enhance NotificationService for improved avatar handling
diegolmello Dec 24, 2025
8be42dd
Enhance avatar URL handling in NotificationService
diegolmello Dec 24, 2025
48cf4eb
Merge branch 'develop' into feat.push-avatar-ios
diegolmello Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@

import androidx.annotation.Nullable;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import com.facebook.react.bridge.ReactApplicationContext;
import com.google.gson.Gson;

Expand All @@ -30,9 +27,6 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import chat.rocket.reactnative.BuildConfig;
import chat.rocket.reactnative.MainActivity;
Expand Down Expand Up @@ -61,7 +55,7 @@ public class CustomPushNotification {

// Instance fields
private final Context mContext;
private Bundle mBundle;
private volatile Bundle mBundle;
private final NotificationManager notificationManager;

public CustomPushNotification(Context context, Bundle bundle) {
Expand Down Expand Up @@ -300,9 +294,6 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) {
bundle.putString("senderId", hasSender ? ejson.sender._id : "1");

String avatarUri = ejson != null ? ejson.getAvatarUri() : null;
if (ENABLE_VERBOSE_LOGS) {
Log.d(TAG, "[showNotification] avatarUri=" + (avatarUri != null ? "[present]" : "[null]"));
}
bundle.putString("avatarUri", avatarUri);

// Handle special notification types
Expand Down Expand Up @@ -379,10 +370,27 @@ private Notification.Builder buildNotification(int notificationId) {
Boolean notificationLoaded = mBundle.getBoolean("notificationLoaded", false);
Ejson ejson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class);

// Determine the correct title based on notification type
String notificationTitle = title;
if (ejson != null && ejson.type != null) {
if ("p".equals(ejson.type) || "c".equals(ejson.type)) {
// For groups/channels, use room name if available, otherwise fall back to title
notificationTitle = (ejson.name != null && !ejson.name.isEmpty()) ? ejson.name : title;
} else if ("d".equals(ejson.type)) {
// For direct messages, use title (sender name from server)
notificationTitle = title;
} else if ("l".equals(ejson.type)) {
// For omnichannel, use sender name if available, otherwise fall back to title
notificationTitle = (ejson.sender != null && ejson.sender.name != null && !ejson.sender.name.isEmpty())
? ejson.sender.name : title;
}
}

if (ENABLE_VERBOSE_LOGS) {
Log.d(TAG, "[buildNotification] notId=" + notId);
Log.d(TAG, "[buildNotification] notificationLoaded=" + notificationLoaded);
Log.d(TAG, "[buildNotification] title=" + (title != null ? "[present]" : "[null]"));
Log.d(TAG, "[buildNotification] notificationTitle=" + (notificationTitle != null ? "[present]" : "[null]"));
Log.d(TAG, "[buildNotification] message length=" + (message != null ? message.length() : 0));
}

Expand All @@ -406,7 +414,7 @@ private Notification.Builder buildNotification(int notificationId) {
}

notification
.setContentTitle(title)
.setContentTitle(notificationTitle)
.setContentText(message)
.setContentIntent(pendingIntent)
.setPriority(Notification.PRIORITY_HIGH)
Expand Down Expand Up @@ -455,37 +463,7 @@ private void cancelPreviousFallbackNotifications(Ejson ejson) {
}

private Bitmap getAvatar(String uri) {
if (uri == null || uri.isEmpty()) {
if (ENABLE_VERBOSE_LOGS) {
Log.w(TAG, "getAvatar called with null/empty URI");
}
return largeIcon();
}

if (ENABLE_VERBOSE_LOGS) {
String sanitizedUri = uri;
int queryStart = uri.indexOf("?");
if (queryStart != -1) {
sanitizedUri = uri.substring(0, queryStart) + "?[auth_params]";
}
Log.d(TAG, "Fetching avatar from: " + sanitizedUri);
}

try {
// Use a 3-second timeout to avoid blocking the FCM service for too long
// FCM has a 10-second limit, so we need to fail fast and use fallback icon
Bitmap avatar = Glide.with(mContext)
.asBitmap()
.apply(RequestOptions.bitmapTransform(new RoundedCorners(10)))
.load(uri)
.submit(100, 100)
.get(3, TimeUnit.SECONDS);

return avatar != null ? avatar : largeIcon();
} catch (final ExecutionException | InterruptedException | TimeoutException e) {
Log.e(TAG, "Failed to fetch avatar: " + e.getMessage(), e);
return largeIcon();
}
return NotificationHelper.fetchAvatarBitmap(mContext, uri, largeIcon());
}

private Bitmap largeIcon() {
Expand All @@ -506,7 +484,10 @@ private void notificationIcons(Notification.Builder notification, Bundle bundle)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
String avatarUri = ejson != null ? ejson.getAvatarUri() : null;
if (avatarUri != null) {
notification.setLargeIcon(getAvatar(avatarUri));
Bitmap avatar = getAvatar(avatarUri);
if (avatar != null) {
notification.setLargeIcon(avatar);
}
}
}
}
Expand All @@ -517,8 +498,11 @@ private String extractMessage(String message, Ejson ejson) {
}
if (ejson != null && ejson.type != null && !ejson.type.equals("d")) {
int pos = message.indexOf(":");
int start = pos == -1 ? 0 : pos + 2;
return message.substring(start);
if (pos == -1) {
return message;
}
int start = pos + 2;
return start <= message.length() ? message.substring(start) : "";
}
return message;
}
Expand Down Expand Up @@ -559,7 +543,23 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun
}

String title = bundle.getString("title");
messageStyle.setConversationTitle(title);
// Determine the correct conversation title based on notification type
Ejson bundleEjson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
String conversationTitle = title;
if (bundleEjson != null && bundleEjson.type != null) {
if ("p".equals(bundleEjson.type) || "c".equals(bundleEjson.type)) {
// For groups/channels, use room name if available, otherwise fall back to title
conversationTitle = (bundleEjson.name != null && !bundleEjson.name.isEmpty()) ? bundleEjson.name : title;
} else if ("d".equals(bundleEjson.type)) {
// For direct messages, use title (sender name from server)
conversationTitle = title;
} else if ("l".equals(bundleEjson.type)) {
// For omnichannel, use sender name if available, otherwise fall back to title
conversationTitle = (bundleEjson.sender != null && bundleEjson.sender.name != null && !bundleEjson.sender.name.isEmpty())
? bundleEjson.sender.name : title;
}
}
messageStyle.setConversationTitle(conversationTitle);

if (bundles != null) {
for (Bundle data : bundles) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.tencent.mmkv.MMKV;

import java.math.BigInteger;
import java.net.URLEncoder;
import java.io.UnsupportedEncodingException;

import chat.rocket.reactnative.BuildConfig;
import chat.rocket.reactnative.storage.MMKVKeyManager;
Expand Down Expand Up @@ -40,6 +42,7 @@ public class Ejson {
String notificationType;
String messageType;
String senderName;
String name; // Room name for groups/channels
String msg;
Integer status; // For video conf: 0=incoming, 4=cancelled

Expand All @@ -57,33 +60,79 @@ private MMKV getMMKV() {
return MMKV.mmkvWithID("default", MMKV.SINGLE_PROCESS_MODE);
}

public String getAvatarUri() {
if (sender == null || sender.username == null || sender.username.isEmpty()) {
Log.w(TAG, "Cannot generate avatar URI: sender or username is null");
return null;
}

/**
* Helper method to build avatar URI from avatar path.
* Validates server URL and credentials, then constructs the full URI.
*/
private String buildAvatarUri(String avatarPath, String errorContext) {
String server = serverURL();
if (server == null || server.isEmpty()) {
Log.w(TAG, "Cannot generate avatar URI: serverURL is null");
Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: serverURL is null");
return null;
}

String userToken = token();
String uid = userId();

if (userToken.isEmpty() || uid.isEmpty()) {
Log.w(TAG, "Cannot generate avatar URI: missing auth credentials (token=" + !userToken.isEmpty() + ", uid=" + !uid.isEmpty() + ")");
Log.w(TAG, "Cannot generate " + errorContext + " avatar URI: missing auth credentials");
return null;
}

String uri = server + "/avatar/" + sender.username + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid;
return server + avatarPath + "?format=png&size=100&rc_token=" + userToken + "&rc_uid=" + uid;
}

public String getAvatarUri() {
String avatarPath;

// For DMs, show sender's avatar; for groups/channels, show room avatar
if ("d".equals(type)) {
// Direct message: use sender's avatar
if (sender == null || sender.username == null || sender.username.isEmpty()) {
Log.w(TAG, "Cannot generate avatar URI: sender or username is null");
return null;
}
try {
avatarPath = "/avatar/" + URLEncoder.encode(sender.username, "UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Failed to encode username", e);
return null;
}
} else {
// Group/Channel/Livechat: use room avatar
if (rid == null || rid.isEmpty()) {
Log.w(TAG, "Cannot generate avatar URI: rid is null for non-DM");
return null;
}
try {
avatarPath = "/avatar/room/" + URLEncoder.encode(rid, "UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Failed to encode rid", e);
return null;
}
}

if (BuildConfig.DEBUG) {
Log.d(TAG, "Generated avatar URI for user: " + sender.username);
return buildAvatarUri(avatarPath, "");
}

/**
* Generates avatar URI for video conference caller.
* Returns null if caller username is not available (username is required for avatar endpoint).
*/
public String getCallerAvatarUri() {
// Check if caller exists and has username (required - /avatar/{userId} endpoint doesn't exist)
if (caller == null || caller.username == null || caller.username.isEmpty()) {
Log.w(TAG, "Cannot generate caller avatar URI: caller or username is null");
return null;
}

return uri;
try {
String avatarPath = "/avatar/" + URLEncoder.encode(caller.username, "UTF-8");
return buildAvatarUri(avatarPath, "caller");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Failed to encode caller username", e);
return null;
}
}

public String token() {
Expand Down Expand Up @@ -194,6 +243,7 @@ static class Sender {
static class Caller {
String _id;
String name;
String username;
}

static class Content {
Expand Down
Loading
Loading