From b2fd8db1439c46000a7a6a85391feaa94fdbea90 Mon Sep 17 00:00:00 2001 From: DSingh0304 Date: Sun, 28 Dec 2025 15:28:02 +0530 Subject: [PATCH 1/8] feat: Implement 'Mark as Read' action for push notifications and update Firebase configuration. --- android/app/src/main/AndroidManifest.xml | 4 + .../notification/CustomPushNotification.java | 236 +++++++++++------- .../notification/MarkAsReadBroadcast.java | 86 +++++++ app/i18n/locales/en.json | 1 + app/lib/notifications/push.ts | 9 +- ios/ReplyNotification.swift | 56 +++-- .../RocketChat/API/Requests/MarkAsRead.swift | 34 +++ ios/Shared/RocketChat/RocketChat.swift | 37 ++- 8 files changed, 347 insertions(+), 116 deletions(-) create mode 100644 android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java create mode 100644 ios/Shared/RocketChat/API/Requests/MarkAsRead.swift diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4fb70ab1a89..c88798ebd33 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -92,6 +92,10 @@ android:enabled="true" android:exported="true" > + > notificationMessages = new ConcurrentHashMap<>(); - + // Constants public static final String KEY_REPLY = "KEY_REPLY"; public static final String NOTIFICATION_ID = "NOTIFICATION_ID"; private static final String CHANNEL_ID = "rocketchatrn_channel_01"; private static final String CHANNEL_NAME = "All"; - + // Instance fields private final Context mContext; private Bundle mBundle; private final NotificationManager notificationManager; - + public CustomPushNotification(Context context, Bundle bundle) { this.mContext = context; this.mBundle = bundle; this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - + // Ensure notification channel exists createNotificationChannel(); } @@ -84,7 +85,7 @@ public static void setReactContext(ReactApplicationContext context) { public static void clearMessages(int notId) { notificationMessages.remove(Integer.toString(notId)); } - + /** * Check if React Native is initialized */ @@ -94,35 +95,37 @@ private boolean isReactInitialized() { public void onReceived() { String notId = mBundle.getString("notId"); - + if (notId == null || notId.isEmpty()) { Log.w(TAG, "Missing notification ID, ignoring notification"); return; } - + try { Integer.parseInt(notId); } catch (NumberFormatException e) { Log.w(TAG, "Invalid notification ID format: " + notId); return; } - - // Check if React is ready - needed for MMKV access (avatars, encryption, message-id-only) + + // Check if React is ready - needed for MMKV access (avatars, encryption, + // message-id-only) if (!isReactInitialized()) { Log.w(TAG, "React not initialized yet, waiting before processing notification..."); - + // Wait for React to initialize with timeout new Thread(() -> { int attempts = 0; int maxAttempts = 50; // 5 seconds total (50 * 100ms) - + while (!isReactInitialized() && attempts < maxAttempts) { try { Thread.sleep(100); // Wait 100ms attempts++; - + if (attempts % 10 == 0 && ENABLE_VERBOSE_LOGS) { - Log.d(TAG, "Still waiting for React initialization... (" + (attempts * 100) + "ms elapsed)"); + Log.d(TAG, + "Still waiting for React initialization... (" + (attempts * 100) + "ms elapsed)"); } } catch (InterruptedException e) { Log.e(TAG, "Wait interrupted", e); @@ -130,7 +133,7 @@ public void onReceived() { return; } } - + if (isReactInitialized()) { Log.i(TAG, "React initialized after " + (attempts * 100) + "ms, proceeding with notification"); try { @@ -139,7 +142,8 @@ public void onReceived() { Log.e(TAG, "Failed to process notification after React initialization", e); } } else { - Log.e(TAG, "Timeout waiting for React initialization after " + (maxAttempts * 100) + "ms, processing without MMKV"); + Log.e(TAG, "Timeout waiting for React initialization after " + (maxAttempts * 100) + + "ms, processing without MMKV"); try { handleNotification(); } catch (Exception e) { @@ -147,25 +151,26 @@ public void onReceived() { } } }).start(); - + return; // Exit early, notification will be processed in the thread } - + if (ENABLE_VERBOSE_LOGS) { Log.d(TAG, "React already initialized, proceeding with notification"); } - + try { handleNotification(); } catch (Exception e) { Log.e(TAG, "Failed to process notification on main thread", e); } } - + private void handleNotification() { Ejson receivedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class); - if (receivedEjson != null && receivedEjson.notificationType != null && receivedEjson.notificationType.equals("message-id-only")) { + if (receivedEjson != null && receivedEjson.notificationType != null + && receivedEjson.notificationType.equals("message-id-only")) { Log.d(TAG, "Detected message-id-only notification, will fetch full content from server"); loadNotificationAndProcess(receivedEjson); return; // Exit early, notification will be processed in callback @@ -174,43 +179,52 @@ private void handleNotification() { // For non-message-id-only notifications, process immediately processNotification(); } - + private void loadNotificationAndProcess(Ejson ejson) { notificationLoad(ejson, new Callback() { @Override public void call(@Nullable Bundle bundle) { if (bundle != null) { Log.d(TAG, "Successfully loaded notification content from server, updating notification props"); - + if (ENABLE_VERBOSE_LOGS) { - Log.d(TAG, "[BEFORE update] bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false)); - Log.d(TAG, "[BEFORE update] bundle.title=" + (bundle.getString("title") != null ? "[present]" : "[null]")); - Log.d(TAG, "[BEFORE update] bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0)); + Log.d(TAG, "[BEFORE update] bundle.notificationLoaded=" + + bundle.getBoolean("notificationLoaded", false)); + Log.d(TAG, "[BEFORE update] bundle.title=" + + (bundle.getString("title") != null ? "[present]" : "[null]")); + Log.d(TAG, "[BEFORE update] bundle.message length=" + + (bundle.getString("message") != null ? bundle.getString("message").length() : 0)); } - - synchronized(CustomPushNotification.this) { + + synchronized (CustomPushNotification.this) { mBundle = bundle; } } else { - Log.w(TAG, "Failed to load notification content from server, will display placeholder notification"); + Log.w(TAG, + "Failed to load notification content from server, will display placeholder notification"); } - + processNotification(); } }); } - + private void processNotification() { Ejson loadedEjson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class); String notId = mBundle.getString("notId", "1"); if (ENABLE_VERBOSE_LOGS) { Log.d(TAG, "[processNotification] notId=" + notId); - Log.d(TAG, "[processNotification] bundle.notificationLoaded=" + mBundle.getBoolean("notificationLoaded", false)); - Log.d(TAG, "[processNotification] bundle.title=" + (mBundle.getString("title") != null ? "[present]" : "[null]")); - Log.d(TAG, "[processNotification] bundle.message length=" + (mBundle.getString("message") != null ? mBundle.getString("message").length() : 0)); - Log.d(TAG, "[processNotification] loadedEjson.notificationType=" + (loadedEjson != null ? loadedEjson.notificationType : "null")); - Log.d(TAG, "[processNotification] loadedEjson.sender=" + (loadedEjson != null && loadedEjson.sender != null ? loadedEjson.sender.username : "null")); + Log.d(TAG, "[processNotification] bundle.notificationLoaded=" + + mBundle.getBoolean("notificationLoaded", false)); + Log.d(TAG, "[processNotification] bundle.title=" + + (mBundle.getString("title") != null ? "[present]" : "[null]")); + Log.d(TAG, "[processNotification] bundle.message length=" + + (mBundle.getString("message") != null ? mBundle.getString("message").length() : 0)); + Log.d(TAG, "[processNotification] loadedEjson.notificationType=" + + (loadedEjson != null ? loadedEjson.notificationType : "null")); + Log.d(TAG, "[processNotification] loadedEjson.sender=" + + (loadedEjson != null && loadedEjson.sender != null ? loadedEjson.sender.username : "null")); } // Handle E2E encrypted notifications @@ -238,7 +252,7 @@ private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) { if (reactApplicationContext != null) { // Fast path: decrypt immediately String decrypted = Encryption.shared.decryptMessage(ejson, reactApplicationContext); - + if (decrypted != null) { bundle.putString("message", decrypted); mBundle = bundle; @@ -249,35 +263,35 @@ private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) { } return; } - + // Slow path: wait for React context asynchronously Log.i(TAG, "Waiting for React context to decrypt E2E notification"); - + E2ENotificationProcessor processor = new E2ENotificationProcessor( - // Context provider - () -> reactApplicationContext, - - // Callback - new E2ENotificationProcessor.NotificationCallback() { - @Override - public void onDecryptionComplete(Bundle decryptedBundle, Ejson decryptedEjson, String notificationId) { - mBundle = decryptedBundle; - Ejson finalEjson = safeFromJson(decryptedBundle.getString("ejson", "{}"), Ejson.class); - showNotification(decryptedBundle, finalEjson, notificationId); - } - - @Override - public void onDecryptionFailed(Bundle originalBundle, Ejson originalEjson, String notificationId) { - Log.w(TAG, "E2E decryption failed for notification"); - } - - @Override - public void onTimeout(Bundle originalBundle, Ejson originalEjson, String notificationId) { - Log.w(TAG, "Timeout waiting for React context for E2E notification"); - } - } - ); - + // Context provider + () -> reactApplicationContext, + + // Callback + new E2ENotificationProcessor.NotificationCallback() { + @Override + public void onDecryptionComplete(Bundle decryptedBundle, Ejson decryptedEjson, + String notificationId) { + mBundle = decryptedBundle; + Ejson finalEjson = safeFromJson(decryptedBundle.getString("ejson", "{}"), Ejson.class); + showNotification(decryptedBundle, finalEjson, notificationId); + } + + @Override + public void onDecryptionFailed(Bundle originalBundle, Ejson originalEjson, String notificationId) { + Log.w(TAG, "E2E decryption failed for notification"); + } + + @Override + public void onTimeout(Bundle originalBundle, Ejson originalEjson, String notificationId) { + Log.w(TAG, "Timeout waiting for React context for E2E notification"); + } + }); + processor.processAsync(bundle, ejson, notId); } @@ -298,7 +312,7 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) { bundle.putLong("time", new Date().getTime()); bundle.putString("username", hasSender ? ejson.sender.username : title); 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]")); @@ -312,11 +326,15 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) { } else { // Show regular notification if (ENABLE_VERBOSE_LOGS) { - Log.d(TAG, "[Before add to notificationMessages] notId=" + notId + ", bundle.message length=" + (bundle.getString("message") != null ? bundle.getString("message").length() : 0) + ", bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false)); + Log.d(TAG, + "[Before add to notificationMessages] notId=" + notId + ", bundle.message length=" + + (bundle.getString("message") != null ? bundle.getString("message").length() : 0) + + ", bundle.notificationLoaded=" + bundle.getBoolean("notificationLoaded", false)); } notificationMessages.get(notId).add(bundle); if (ENABLE_VERBOSE_LOGS) { - Log.d(TAG, "[After add] notificationMessages[" + notId + "].size=" + notificationMessages.get(notId).size()); + Log.d(TAG, "[After add] notificationMessages[" + notId + "].size=" + + notificationMessages.get(notId).size()); } postNotification(Integer.parseInt(notId)); } @@ -328,7 +346,7 @@ private void showNotification(Bundle bundle, Ejson ejson, String notId) { */ private void handleVideoConfNotification(Bundle bundle, Ejson ejson) { VideoConfNotification videoConf = new VideoConfNotification(mContext); - + Integer status = ejson.status; String rid = ejson.rid; // Video conf uses 'caller' field, regular messages use 'sender' @@ -338,9 +356,9 @@ private void handleVideoConfNotification(Bundle bundle, Ejson ejson) { } else if (ejson.sender != null && ejson.sender._id != null) { callerId = ejson.sender._id; } - + Log.d(TAG, "Video conf notification - status: " + status + ", rid: " + rid); - + if (status == null || status == 0) { // Incoming call - show notification videoConf.showIncomingCall(bundle, ejson); @@ -358,14 +376,13 @@ private void postNotification(int notificationId) { notificationManager.notify(notificationId, notification.build()); } } - + private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( - CHANNEL_ID, - CHANNEL_NAME, - NotificationManager.IMPORTANCE_HIGH - ); + CHANNEL_ID, + CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH); if (notificationManager != null) { notificationManager.createNotificationChannel(channel); } @@ -390,12 +407,14 @@ private Notification.Builder buildNotification(int notificationId) { Intent intent = new Intent(mContext, MainActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.putExtras(mBundle); - + PendingIntent pendingIntent; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - pendingIntent = PendingIntent.getActivity(mContext, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + pendingIntent = PendingIntent.getActivity(mContext, notificationId, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } else { - pendingIntent = PendingIntent.getActivity(mContext, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); + pendingIntent = PendingIntent.getActivity(mContext, notificationId, intent, + PendingIntent.FLAG_UPDATE_CURRENT); } Notification.Builder notification; @@ -417,11 +436,14 @@ private Notification.Builder buildNotification(int notificationId) { notificationIcons(notification, mBundle); notificationDismiss(notification, notificationId); - // if notificationType is null (RC < 3.5) or notificationType is different of message-id-only or notification was loaded successfully - if (ejson == null || ejson.notificationType == null || !ejson.notificationType.equals("message-id-only") || notificationLoaded) { + // if notificationType is null (RC < 3.5) or notificationType is different of + // message-id-only or notification was loaded successfully + if (ejson == null || ejson.notificationType == null || !ejson.notificationType.equals("message-id-only") + || notificationLoaded) { Log.i(TAG, "[buildNotification] ✅ Rendering FULL notification style"); notificationStyle(notification, notificationId, mBundle); notificationReply(notification, notificationId, mBundle); + notificationMarkAsRead(notification, notificationId, mBundle); } else { Log.w(TAG, "[buildNotification] ⚠️ Rendering FALLBACK notification"); // Cancel previous fallback notifications from same server @@ -430,7 +452,7 @@ private Notification.Builder buildNotification(int notificationId) { return notification; } - + private void cancelPreviousFallbackNotifications(Ejson ejson) { for (Map.Entry> bundleList : notificationMessages.entrySet()) { Iterator iterator = bundleList.getValue().iterator(); @@ -461,7 +483,7 @@ private Bitmap getAvatar(String uri) { } return largeIcon(); } - + if (ENABLE_VERBOSE_LOGS) { String sanitizedUri = uri; int queryStart = uri.indexOf("?"); @@ -470,7 +492,7 @@ private Bitmap getAvatar(String uri) { } 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 @@ -480,7 +502,7 @@ private Bitmap getAvatar(String uri) { .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); @@ -533,7 +555,8 @@ private void notificationStyle(Notification.Builder notification, int notId, Bun List bundles = notificationMessages.get(Integer.toString(notId)); if (ENABLE_VERBOSE_LOGS) { - Log.d(TAG, "[notificationStyle] notId=" + notId + ", bundles=" + (bundles != null ? bundles.size() : "null")); + Log.d(TAG, + "[notificationStyle] notId=" + notId + ", bundles=" + (bundles != null ? bundles.size() : "null")); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { @@ -612,9 +635,11 @@ private void notificationReply(Notification.Builder notification, int notificati PendingIntent replyPendingIntent; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); } else { - replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT); + replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT); } RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY) @@ -631,11 +656,44 @@ private void notificationReply(Notification.Builder notification, int notificati .addAction(replyAction); } + private void notificationMarkAsRead(Notification.Builder notification, int notificationId, Bundle bundle) { + String notId = bundle.getString("notId", "1"); + String ejson = bundle.getString("ejson", "{}"); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || notId.equals("1") || ejson.equals("{}")) { + return; + } + String label = "Mark as read"; + + final Resources res = mContext.getResources(); + String packageName = mContext.getPackageName(); + int smallIconResId = res.getIdentifier("ic_notification", "drawable", packageName); + + Intent markAsReadIntent = new Intent(mContext, MarkAsReadBroadcast.class); + markAsReadIntent.setAction(MarkAsReadBroadcast.KEY_MARK_AS_READ); + markAsReadIntent.putExtra("pushNotification", bundle); + + PendingIntent markAsReadPendingIntent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + markAsReadPendingIntent = PendingIntent.getBroadcast(mContext, notificationId + 1000, markAsReadIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } else { + markAsReadPendingIntent = PendingIntent.getBroadcast(mContext, notificationId + 1000, markAsReadIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + Notification.Action markAsReadAction = new Notification.Action.Builder(smallIconResId, label, + markAsReadPendingIntent) + .build(); + + notification.addAction(markAsReadAction); + } + private void notificationDismiss(Notification.Builder notification, int notificationId) { Intent intent = new Intent(mContext, DismissNotification.class); intent.putExtra(NOTIFICATION_ID, notificationId); - PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); + PendingIntent dismissPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, intent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); notification.setDeleteIntent(dismissPendingIntent); } @@ -644,7 +702,7 @@ private void notificationLoad(Ejson ejson, Callback callback) { LoadNotification loadNotification = new LoadNotification(); loadNotification.load(ejson, callback); } - + /** * Safely parses JSON string to object with error handling. */ diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java b/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java new file mode 100644 index 00000000000..4f23071baf4 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java @@ -0,0 +1,86 @@ +package chat.rocket.reactnative.notification; + +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import com.google.gson.Gson; +import com.wix.reactnativenotifications.core.NotificationIntentAdapter; + +import java.io.IOException; + +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class MarkAsReadBroadcast extends BroadcastReceiver { + private static final String TAG = "RocketChat.MarkAsRead"; + public static final String KEY_MARK_AS_READ = "KEY_MARK_AS_READ"; + + @Override + public void onReceive(Context context, Intent intent) { + Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); + NotificationManager notificationManager = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + + String notId = bundle.getString("notId"); + + Gson gson = new Gson(); + Ejson ejson = gson.fromJson(bundle.getString("ejson", "{}"), Ejson.class); + + try { + int id = Integer.parseInt(notId); + markAsRead(ejson, id, notificationManager); + } catch (NumberFormatException e) { + Log.e(TAG, "Invalid notification ID: " + notId, e); + } + } + + protected void markAsRead(final Ejson ejson, final int notId, final NotificationManager notificationManager) { + String serverURL = ejson.serverURL(); + String rid = ejson.rid; + + if (serverURL == null || rid == null) { + Log.e(TAG, "Missing serverURL or rid"); + return; + } + + final OkHttpClient client = new OkHttpClient(); + final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + String json = String.format("{\"rid\":\"%s\"}", rid); + + CustomPushNotification.clearMessages(notId); + + RequestBody body = RequestBody.create(JSON, json); + Request request = new Request.Builder() + .header("x-auth-token", ejson.token()) + .header("x-user-id", ejson.userId()) + .url(String.format("%s/api/v1/subscriptions.read", serverURL)) + .post(body) + .build(); + + client.newCall(request).enqueue(new okhttp3.Callback() { + @Override + public void onFailure(Call call, IOException e) { + Log.e(TAG, "Mark as read FAILED: " + e.getMessage()); + } + + @Override + public void onResponse(Call call, final Response response) throws IOException { + if (response.isSuccessful()) { + Log.d(TAG, "Mark as read SUCCESS"); + notificationManager.cancel(notId); + } else { + Log.e(TAG, String.format("Mark as read FAILED status %s", response.code())); + } + } + }); + } +} diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 20adc12eb90..d40c30c0bb7 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -499,6 +499,7 @@ "Mark_as_unread_Info": "Display room as unread when there are unread messages", "Mark_read": "Mark read", "Mark_unread": "Mark unread", + "Mark_as_read": "Mark as read", "Markdown_tools": "Markdown tools", "Max_number_of_users_allowed_is_number": "Max number of users allowed is {{maxUsers}}", "Max_number_of_uses": "Max number of uses", diff --git a/app/lib/notifications/push.ts b/app/lib/notifications/push.ts index c5725ec6f4c..85249cc411f 100644 --- a/app/lib/notifications/push.ts +++ b/app/lib/notifications/push.ts @@ -88,7 +88,7 @@ const setupNotificationCategories = async (): Promise => { } try { - // Message category with Reply action + // Message category with Reply and Mark as Read actions await Notifications.setNotificationCategoryAsync('MESSAGE', [ { identifier: 'REPLY_ACTION', @@ -100,6 +100,13 @@ const setupNotificationCategories = async (): Promise => { options: { opensAppToForeground: false } + }, + { + identifier: 'MARK_AS_READ_ACTION', + buttonTitle: I18n.t('Mark_as_read'), + options: { + opensAppToForeground: false + } } ]); diff --git a/ios/ReplyNotification.swift b/ios/ReplyNotification.swift index 4c3ea7c2183..d98b70675b0 100644 --- a/ios/ReplyNotification.swift +++ b/ios/ReplyNotification.swift @@ -16,20 +16,20 @@ import UserNotifications class ReplyNotification: NSObject, UNUserNotificationCenterDelegate { private static var shared: ReplyNotification? private weak var originalDelegate: UNUserNotificationCenterDelegate? - + @objc public static func configure() { let instance = ReplyNotification() shared = instance - + // Store the original delegate (expo-notifications) and set ourselves as the delegate let center = UNUserNotificationCenter.current() instance.originalDelegate = center.delegate center.delegate = instance } - + // MARK: - UNUserNotificationCenterDelegate - + func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, @@ -40,7 +40,13 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate { handleReplyAction(response: response, completionHandler: completionHandler) return } - + + // Handle MARK_AS_READ_ACTION natively + if response.actionIdentifier == "MARK_AS_READ_ACTION" { + handleMarkAsReadAction(response: response, completionHandler: completionHandler) + return + } + // Forward to original delegate (expo-notifications) if let originalDelegate = originalDelegate { originalDelegate.userNotificationCenter?(center, didReceive: response, withCompletionHandler: completionHandler) @@ -48,7 +54,7 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate { completionHandler() } } - + func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, @@ -61,7 +67,7 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate { completionHandler([]) } } - + func userNotificationCenter( _ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification? @@ -73,17 +79,17 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate { } } } - + // MARK: - Reply Handling - + private func handleReplyAction(response: UNNotificationResponse, completionHandler: @escaping () -> Void) { guard let textResponse = response as? UNTextInputNotificationResponse else { completionHandler() return } - + let userInfo = response.notification.request.content.userInfo - + guard let ejsonString = userInfo["ejson"] as? String, let ejsonData = ejsonString.data(using: .utf8), let payload = try? JSONDecoder().decode(Payload.self, from: ejsonData), @@ -91,11 +97,11 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate { completionHandler() return } - + let message = textResponse.userText let rocketchat = RocketChat(server: payload.host.removeTrailingSlash()) let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) - + rocketchat.sendMessage(rid: rid, message: message, threadIdentifier: payload.tmid) { response in // Ensure we're on the main thread for UI operations DispatchQueue.main.async { @@ -103,7 +109,7 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate { UIApplication.shared.endBackgroundTask(backgroundTask) completionHandler() } - + guard let response = response, response.success else { // Show failure notification let content = UNMutableNotificationContent() @@ -115,4 +121,26 @@ class ReplyNotification: NSObject, UNUserNotificationCenterDelegate { } } } + + private func handleMarkAsReadAction(response: UNNotificationResponse, completionHandler: @escaping () -> Void) { + let userInfo = response.notification.request.content.userInfo + + guard let ejsonString = userInfo["ejson"] as? String, + let ejsonData = ejsonString.data(using: .utf8), + let payload = try? JSONDecoder().decode(Payload.self, from: ejsonData), + let rid = payload.rid else { + completionHandler() + return + } + + let rocketchat = RocketChat(server: payload.host.removeTrailingSlash()) + let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) + + rocketchat.markAsRead(rid: rid) { response in + DispatchQueue.main.async { + UIApplication.shared.endBackgroundTask(backgroundTask) + completionHandler() + } + } + } } diff --git a/ios/Shared/RocketChat/API/Requests/MarkAsRead.swift b/ios/Shared/RocketChat/API/Requests/MarkAsRead.swift new file mode 100644 index 00000000000..3e45d63e3d8 --- /dev/null +++ b/ios/Shared/RocketChat/API/Requests/MarkAsRead.swift @@ -0,0 +1,34 @@ +// +// MarkAsRead.swift +// RocketChatRN +// +// Created for Mark as Read notification action +// Copyright © 2025 Rocket.Chat. All rights reserved. +// + +import Foundation + +struct MarkAsReadBody: Codable { + let rid: String +} + +struct MarkAsReadResponse: Response { + var success: Bool +} + +final class MarkAsReadRequest: Request { + typealias ResponseType = MarkAsReadResponse + + let method: HTTPMethod = .post + let path = "/api/v1/subscriptions.read" + + let rid: String + + init(rid: String) { + self.rid = rid + } + + func body() -> Data? { + return try? JSONEncoder().encode(MarkAsReadBody(rid: rid)) + } +} diff --git a/ios/Shared/RocketChat/RocketChat.swift b/ios/Shared/RocketChat/RocketChat.swift index 83d50139e6d..dc45c647be4 100644 --- a/ios/Shared/RocketChat/RocketChat.swift +++ b/ios/Shared/RocketChat/RocketChat.swift @@ -11,50 +11,50 @@ import Foundation final class RocketChat { typealias Server = String typealias RoomId = String - + let server: Server let api: API? private var encryptionQueue = DispatchQueue(label: "chat.rocket.encryptionQueue") - + init(server: Server) { self.server = server self.api = API(server: server) } - + func getPushWithId(_ msgId: String, completion: @escaping((Notification?) -> Void)) { api?.fetch(request: PushRequest(msgId: msgId), retry: Retry(retries: 4)) { response in switch response { case .resource(let response): let notification = response.data.notification completion(notification) - + case .error: completion(nil) break } } } - + func sendMessage(rid: String, message: String, threadIdentifier: String?, completion: @escaping((MessageResponse?) -> Void)) { let id = String.random(length: 17) - + let encrypted = Database(server: server).readRoomEncrypted(for: rid) - + if encrypted { let encryption = Encryption(server: server, rid: rid) guard let content = encryption.encryptContent(message) else { return } - + // For backward compatibility, also set msg field let msg = content.algorithm == "rc.v2.aes-sha2" ? "" : content.ciphertext - + api?.fetch(request: SendMessageRequest(id: id, roomId: rid, text: msg, content: content, threadIdentifier: threadIdentifier, messageType: .e2e)) { response in switch response { case .resource(let response): completion(response) - + case .error: completion(nil) break @@ -65,7 +65,7 @@ final class RocketChat { switch response { case .resource(let response): completion(response) - + case .error: completion(nil) break @@ -73,11 +73,24 @@ final class RocketChat { } } } - + func decryptContent(rid: String, content: EncryptedContent) -> String? { encryptionQueue.sync { let encryption = Encryption(server: server, rid: rid) return encryption.decryptContent(content: content) } } + + func markAsRead(rid: String, completion: @escaping((MarkAsReadResponse?) -> Void)) { + api?.fetch(request: MarkAsReadRequest(rid: rid)) { response in + switch response { + case .resource(let response): + completion(response) + + case .error: + completion(nil) + break + } + } + } } From e487fe417bc2078a7191ee6426ae9cf50dbfcde2 Mon Sep 17 00:00:00 2001 From: DSingh0304 Date: Tue, 30 Dec 2025 09:47:13 +0530 Subject: [PATCH 2/8] feat: Introduce development commands, local Docker setup, Firebase credentials, and a memory monitor, while refactoring Android notification handling and updating Firebase configuration. --- .../notification/MarkAsReadBroadcast.java | 24 +++++++++++-------- ios/Shared/RocketChat/RocketChat.swift | 10 +++----- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java b/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java index 4f23071baf4..82b8ff2e0fc 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java @@ -22,6 +22,8 @@ public class MarkAsReadBroadcast extends BroadcastReceiver { private static final String TAG = "RocketChat.MarkAsRead"; public static final String KEY_MARK_AS_READ = "KEY_MARK_AS_READ"; + private static final OkHttpClient client = new OkHttpClient(); + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); @Override public void onReceive(Context context, Intent intent) { @@ -51,13 +53,8 @@ protected void markAsRead(final Ejson ejson, final int notId, final Notification return; } - final OkHttpClient client = new OkHttpClient(); - final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - String json = String.format("{\"rid\":\"%s\"}", rid); - CustomPushNotification.clearMessages(notId); - RequestBody body = RequestBody.create(JSON, json); Request request = new Request.Builder() .header("x-auth-token", ejson.token()) @@ -74,11 +71,18 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(Call call, final Response response) throws IOException { - if (response.isSuccessful()) { - Log.d(TAG, "Mark as read SUCCESS"); - notificationManager.cancel(notId); - } else { - Log.e(TAG, String.format("Mark as read FAILED status %s", response.code())); + try { + if (response.isSuccessful()) { + Log.d(TAG, "Mark as read SUCCESS"); + CustomPushNotification.clearMessages(notId); + notificationManager.cancel(notId); + } else { + Log.e(TAG, String.format("Mark as read FAILED status %s", response.code())); + } + } finally { + if (response.body() != null) { + response.body().close(); + } } } }); diff --git a/ios/Shared/RocketChat/RocketChat.swift b/ios/Shared/RocketChat/RocketChat.swift index dc45c647be4..bafdf2c8cc4 100644 --- a/ios/Shared/RocketChat/RocketChat.swift +++ b/ios/Shared/RocketChat/RocketChat.swift @@ -22,7 +22,7 @@ final class RocketChat { self.api = API(server: server) } - func getPushWithId(_ msgId: String, completion: @escaping((Notification?) -> Void)) { + func getPushWithId(_ msgId: String, completion: @escaping (Notification?) -> Void) { api?.fetch(request: PushRequest(msgId: msgId), retry: Retry(retries: 4)) { response in switch response { case .resource(let response): @@ -31,12 +31,11 @@ final class RocketChat { case .error: completion(nil) - break } } } - func sendMessage(rid: String, message: String, threadIdentifier: String?, completion: @escaping((MessageResponse?) -> Void)) { + func sendMessage(rid: String, message: String, threadIdentifier: String?, completion: @escaping (MessageResponse?) -> Void) { let id = String.random(length: 17) let encrypted = Database(server: server).readRoomEncrypted(for: rid) @@ -57,7 +56,6 @@ final class RocketChat { case .error: completion(nil) - break } } } else { @@ -68,7 +66,6 @@ final class RocketChat { case .error: completion(nil) - break } } } @@ -81,7 +78,7 @@ final class RocketChat { } } - func markAsRead(rid: String, completion: @escaping((MarkAsReadResponse?) -> Void)) { + func markAsRead(rid: String, completion: @escaping (MarkAsReadResponse?) -> Void) { api?.fetch(request: MarkAsReadRequest(rid: rid)) { response in switch response { case .resource(let response): @@ -89,7 +86,6 @@ final class RocketChat { case .error: completion(nil) - break } } } From 8c3bc3834d0945d2d6e0297ebb09fa195b37b69f Mon Sep 17 00:00:00 2001 From: DSingh0304 Date: Tue, 30 Dec 2025 09:59:37 +0530 Subject: [PATCH 3/8] feat: Add local development setup files, update Firebase configuration, and improve Android notification async handling. --- .../notification/MarkAsReadBroadcast.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java b/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java index 82b8ff2e0fc..5bf2f9eb24b 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/MarkAsReadBroadcast.java @@ -27,6 +27,9 @@ public class MarkAsReadBroadcast extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { + // Keep receiver alive for async network operation + final PendingResult pendingResult = goAsync(); + Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); NotificationManager notificationManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); @@ -38,18 +41,22 @@ public void onReceive(Context context, Intent intent) { try { int id = Integer.parseInt(notId); - markAsRead(ejson, id, notificationManager); + markAsRead(ejson, id, notificationManager, pendingResult); } catch (NumberFormatException e) { Log.e(TAG, "Invalid notification ID: " + notId, e); + pendingResult.finish(); } } - protected void markAsRead(final Ejson ejson, final int notId, final NotificationManager notificationManager) { + protected void markAsRead(final Ejson ejson, final int notId, + final NotificationManager notificationManager, + final PendingResult pendingResult) { String serverURL = ejson.serverURL(); String rid = ejson.rid; if (serverURL == null || rid == null) { Log.e(TAG, "Missing serverURL or rid"); + pendingResult.finish(); return; } @@ -67,6 +74,7 @@ protected void markAsRead(final Ejson ejson, final int notId, final Notification @Override public void onFailure(Call call, IOException e) { Log.e(TAG, "Mark as read FAILED: " + e.getMessage()); + pendingResult.finish(); } @Override @@ -83,6 +91,7 @@ public void onResponse(Call call, final Response response) throws IOException { if (response.body() != null) { response.body().close(); } + pendingResult.finish(); } } }); From 65823fa9f629e5228a60e670ad6ae7a5bd6a1752 Mon Sep 17 00:00:00 2001 From: DSingh0304 Date: Sun, 22 Feb 2026 08:48:24 +0000 Subject: [PATCH 4/8] action: organized translations --- app/i18n/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index d40c30c0bb7..433511219d2 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -495,11 +495,11 @@ "Logout": "Logout", "Logout_failed": "Logout failed!", "Logout_from_other_logged_in_locations": "Logout from other logged in locations", + "Mark_as_read": "Mark as read", "Mark_as_unread": "Mark as unread", "Mark_as_unread_Info": "Display room as unread when there are unread messages", "Mark_read": "Mark read", "Mark_unread": "Mark unread", - "Mark_as_read": "Mark as read", "Markdown_tools": "Markdown tools", "Max_number_of_users_allowed_is_number": "Max number of users allowed is {{maxUsers}}", "Max_number_of_uses": "Max number of uses", From 85baa2cc761bf0f6abc70ef0885dc952272d21c1 Mon Sep 17 00:00:00 2001 From: Deep Shekhar Singh Date: Thu, 26 Mar 2026 16:51:30 +0530 Subject: [PATCH 5/8] feat: provide ReactApplicationContext earlier for E2E decryption and centralize fallback notification logic. --- .../notification/CustomPushNotification.java | 49 +++++++++++++------ .../notification/PushNotificationModule.kt | 5 ++ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java index 6938896355e..1c43bd57ee0 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java @@ -1,5 +1,8 @@ package chat.rocket.reactnative.notification; +import com.facebook.react.bridge.ReactApplicationContext; + + import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -47,6 +50,8 @@ public class CustomPushNotification { // Shared state private static final Gson gson = new Gson(); private static final Map> notificationMessages = new ConcurrentHashMap<>(); + private static volatile ReactApplicationContext reactApplicationContext; + // Constants public static final String KEY_REPLY = "KEY_REPLY"; @@ -72,6 +77,11 @@ public static void clearMessages(int notId) { notificationMessages.remove(Integer.toString(notId)); } + public static void setReactApplicationContext(ReactApplicationContext reactContext) { + reactApplicationContext = reactContext; + } + + /** * Check if React Native is initialized */ @@ -246,13 +256,22 @@ private boolean isE2ENotification(Ejson ejson) { * No longer waits for React Native initialization. */ private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) { + ReactApplicationContext reactContext = reactApplicationContext; // Check if React context is immediately available - if (reactApplicationContext != null) { + if (reactContext != null) { // Fast path: decrypt immediately - String decrypted = Encryption.shared.decryptMessage(ejson, reactApplicationContext); + String decrypted = Encryption.shared.decryptMessage(ejson, reactContext); if (decrypted != null) { bundle.putString("message", decrypted); + synchronized (this) { + mBundle = bundle; + } + showNotification(bundle, ejson, notId); + return; + } + } + // Decrypt immediately using regular Android Context (mContext) // This works without React Native initialization String decrypted = Encryption.shared.decryptMessage(ejson, mContext); @@ -262,6 +281,7 @@ private void handleE2ENotification(Bundle bundle, Ejson ejson, String notId) { synchronized(this) { mBundle = bundle; } + showNotification(bundle, ejson, notId); return; } @@ -284,27 +304,28 @@ public void onDecryptionComplete(Bundle decryptedBundle, Ejson decryptedEjson, @Override public void onDecryptionFailed(Bundle originalBundle, Ejson originalEjson, String notificationId) { - Log.w(TAG, "E2E decryption failed for notification"); + Log.w(TAG, "E2E decryption failed for notification, showing fallback notification"); + showFallbackNotification(originalBundle, originalEjson, notificationId); } @Override public void onTimeout(Bundle originalBundle, Ejson originalEjson, String notificationId) { - Log.w(TAG, "Timeout waiting for React context for E2E notification"); + Log.w(TAG, "Timeout waiting for React context for E2E notification, showing fallback notification"); + showFallbackNotification(originalBundle, originalEjson, notificationId); } }); processor.processAsync(bundle, ejson, notId); - showNotification(bundle, ejson, notId); - } else { - Log.w(TAG, "E2E decryption failed for notification, showing fallback notification"); - // Show fallback notification so user knows a message arrived - // Use a placeholder message since we can't decrypt - bundle.putString("message", "Encrypted message"); - synchronized(this) { - mBundle = bundle; - } - showNotification(bundle, ejson, notId); + } + + private void showFallbackNotification(Bundle bundle, Ejson ejson, String notId) { + // Show fallback notification so user knows a message arrived + // Use a placeholder message since we can't decrypt + bundle.putString("message", "Encrypted message"); + synchronized(this) { + mBundle = bundle; } + showNotification(bundle, ejson, notId); } /** diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt index 543558c1a8d..cfc8a17a289 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt @@ -11,6 +11,11 @@ import com.facebook.react.bridge.ReactMethod */ class PushNotificationModule(reactContext: ReactApplicationContext) : NativePushNotificationSpec(reactContext) { + init { + CustomPushNotification.setReactApplicationContext(reactContext) + } + + companion object { private const val PREFS_NAME = "RocketChatPrefs" private const val KEY_PENDING_NOTIFICATION = "pendingNotification" From 3f598e805c762c787389d5da4d143b5bb20e3694 Mon Sep 17 00:00:00 2001 From: Deep Shekhar Singh Date: Thu, 26 Mar 2026 16:57:33 +0530 Subject: [PATCH 6/8] Refactor: Delegate avatar fetching to `NotificationHelper.fetchAvatarBitmap` instead of direct Glide usage with timeout. --- .../notification/CustomPushNotification.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java index 1c43bd57ee0..8dff7eb550e 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java @@ -546,21 +546,6 @@ private Bitmap getAvatar(String uri) { 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()); } From 6fca3184110bf4898386ddcb537b8eea7d2f8aed Mon Sep 17 00:00:00 2001 From: Deep Shekhar Singh Date: Thu, 26 Mar 2026 17:18:41 +0530 Subject: [PATCH 7/8] Refactor CustomPushNotification.java for clarity --- .../notification/CustomPushNotification.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java index 8dff7eb550e..d74ff220fef 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java @@ -51,8 +51,6 @@ public class CustomPushNotification { private static final Gson gson = new Gson(); private static final Map> notificationMessages = new ConcurrentHashMap<>(); private static volatile ReactApplicationContext reactApplicationContext; - - // Constants public static final String KEY_REPLY = "KEY_REPLY"; public static final String NOTIFICATION_ID = "NOTIFICATION_ID"; @@ -80,8 +78,6 @@ public static void clearMessages(int notId) { public static void setReactApplicationContext(ReactApplicationContext reactContext) { reactApplicationContext = reactContext; } - - /** * Check if React Native is initialized */ @@ -89,15 +85,12 @@ private boolean isReactInitialized() { return reactApplicationContext != null; } - public void onReceived() { String notId = mBundle.getString("notId"); - if (notId == null || notId.isEmpty()) { Log.w(TAG, "Missing notification ID, ignoring notification"); return; } - try { Integer.parseInt(notId); } catch (NumberFormatException e) { @@ -155,8 +148,7 @@ public void onReceived() { if (ENABLE_VERBOSE_LOGS) { Log.d(TAG, "React already initialized, proceeding with notification"); } - - + // Process notification immediately - no need to wait for React Native // MMKV is initialized at app startup, so all notification types can work without React try { From 60fa7741fcc1e4d1db9002829d62e7dfb2fed3db Mon Sep 17 00:00:00 2001 From: Deep Shekhar Singh Date: Thu, 26 Mar 2026 17:21:08 +0530 Subject: [PATCH 8/8] Remove unnecessary blank lines in PushNotificationModule.kt --- .../rocket/reactnative/notification/PushNotificationModule.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt b/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt index cfc8a17a289..3e6c27a5871 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt @@ -14,8 +14,6 @@ class PushNotificationModule(reactContext: ReactApplicationContext) : NativePush init { CustomPushNotification.setReactApplicationContext(reactContext) } - - companion object { private const val PREFS_NAME = "RocketChatPrefs" private const val KEY_PENDING_NOTIFICATION = "pendingNotification"