Skip to content

Conversation

@diegolmello
Copy link
Member

@diegolmello diegolmello commented Dec 10, 2025

Proposed changes

Issue(s)

How to test or reproduce

Screenshots

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Summary by CodeRabbit

  • New Features

    • Added video conference notifications with accept/decline actions and incoming-call UI on mobile.
  • Improvements

    • Reworked notification delivery and initialization for more reliable handling across platforms.
    • Better deep-linking from notification actions and improved badge/count synchronization.
    • Streamlined reply handling and media/decryption flows for notifications.
  • Bug Fixes

    • Removed legacy background handlers and fixed various notification-edge behaviors to reduce missed actions.

✏️ Tip: You can customize this high-level summary in your review settings.

…andling and migrate notification logic

- Added expo-notifications for push notification management, replacing react-native-notifications.
- Implemented device token registration and notification response handling.
- Enhanced badge count management and notification dismissal methods.
- Set up notification categories for iOS to support actions like reply and video conference responses.
- Updated MainApplication to reflect new notification architecture.
…ent custom FCM handling

- Removed react-native-notifications dependency and related mock configurations.
- Introduced RCFirebaseMessagingService for handling FCM messages and routing to CustomPushNotification.
- Updated CustomPushNotification to manage notifications without react-native-notifications, enhancing E2E decryption and MessagingStyle support.
- Adjusted MainApplication and notification classes to reflect the new architecture and improve notification processing.
- Cleaned up unused imports and code related to the previous notification system.
…e reply handling

- Removed react-native-notifications dependency and related code from the project.
- Implemented a custom reply notification handler in ReplyNotification to manage direct replies from iOS notifications.
- Updated AppDelegate to configure the new reply notification handler.
- Adjusted Podfile and Podfile.lock to reflect the removal of react-native-notifications and added necessary Expo modules.
- Cleaned up imports and ensured compatibility with the new notification architecture.
- Introduced a new video conference notification system, replacing the previous handling with a custom implementation.
- Added VideoConfNotification class to manage incoming call notifications with accept/decline actions.
- Implemented VideoConfBroadcast receiver to handle notification actions and store them for the JS layer.
- Updated MainActivity to process video conference intents and integrate with the new notification system.
- Enhanced getInitialNotification to check for pending video conference actions.
- Updated AndroidManifest.xml to register the new broadcast receiver.
- Cleaned up related code and ensured compatibility with the new notification architecture.
…or iOS

- Added support for video conference notifications in the iOS NotificationService.
- Implemented logic to process incoming video call notifications, including handling call status and displaying appropriate alerts.
- Updated Payload and NotificationType models to accommodate video conference data.
- Enhanced getInitialNotification to check for video conference actions on iOS using expo-notifications.
- Improved error handling for notification responses.
- Enhanced the onNotification function to streamline processing of video conference notifications, including accept and decline actions.
- Updated getInitialNotification to handle video conference actions more effectively, ensuring proper event dispatching based on user interaction.
- Improved error handling and code readability by reducing nested conditions and clarifying logic flow.
…encies

- Removed @notifee/react-native and @react-native-firebase/messaging from the project, along with related code and configurations.
- Updated notification handling to utilize expo-notifications instead, ensuring a more streamlined approach to push notifications.
- Cleaned up package.json, yarn.lock, and Podfile.lock to reflect the removal of obsolete dependencies.
- Deleted background notification handler and adjusted notification settings management accordingly.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

Walkthrough

Migrates notifications from react-native-notifications/Notifee to Expo (expo-notifications/expo-device), removes react-native-firebase messaging dependency, and adds native video-conference notification handling on Android and iOS with TurboModule bridging and persistence of pending actions.

Changes

Cohort / File(s) Summary
Dependency & config
package.json, android/app/build.gradle, ios/Podfile, react-native.config.js, jest.setup.js, patches/*
Remove react-native-notifications, @notifee/react-native, @react-native-firebase/messaging; add expo-notifications and expo-device; update Podfile, build gradle, RN config, test mocks and delete legacy notifee/fcm patches.
Android: FCM & manifest
android/app/src/main/AndroidManifest.xml, android/app/src/main/java/.../notification/RCFirebaseMessagingService.kt
Add RCFirebaseMessagingService to receive FCM data messages and delegate to CustomPushNotification. Manifest entries added for service and broadcast receiver.
Android: VideoConf native modules & package
android/app/src/main/java/.../notification/NativeVideoConfSpec.kt, VideoConfModule.kt, VideoConfTurboPackage.kt, VideoConfNotification.kt, VideoConfBroadcast.kt, VideoConfModule.kt
Introduce TurboModule spec, module, turbo package, notification builder (full‑screen incoming calls), BroadcastReceiver for accept/decline, and persistent pending-action storage in SharedPreferences.
Android: Notification core refactor
android/app/src/main/java/.../notification/CustomPushNotification.java, Ejson.java, Encryption.java, ReplyBroadcast.java, LoadNotification.java, NotificationHelper.java
Remove AppLifecycleFacade dependency, centralize React context via CustomPushNotification, add Ejson fields for videoconf (caller, callId, status), add User-Agent header, add notification channel logic and E2E decryption flow.
Android: Activity/Application changes
android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt, MainApplication.kt
MainActivity handles onNewIntent and initial intent videoConfAction processing; MainApplication removes INotificationsApplication and adds VideoConfTurboPackage to packages.
Android: native helper surface
android/app/src/main/java/.../notification/VideoConfModule.kt, NotificationHelper.java, NativeVideoConfSpec.kt, VideoConfTurboPackage.kt
New public APIs for storing/reading/clearing pending video-conf actions and user-agent helper.
iOS: Notification service & models
ios/NotificationService/NotificationService.swift, ios/Shared/Models/NotificationType.swift, ios/Shared/Models/Payload.swift, ios/Shared/RocketChat/API/Request.swift
Add videoconf processing in Notification Service, extend payload/model with caller/callId/status, and add User-Agent header for requests.
iOS: Delegate & Reply handling
ios/AppDelegate.swift, ios/ReplyNotification.swift, ios/RocketChatRN-Bridging-Header.h, ios/RocketChatRN.xcodeproj/project.pbxproj
Replace RNNotifications wiring with ReplyNotification as UNUserNotificationCenterDelegate; remove RNNotifications imports; add Expo privacy bundles to project resources.
JS/TS: Expo notifications + videoConf bridging
app/lib/notifications/push.ts, app/lib/notifications/index.ts, app/lib/notifications/videoConf/getInitialNotification.ts, app/lib/native/NativeVideoConfAndroid.ts, app/index.tsx
Migrate JS push layer to expo-notifications (register, handlers, badge, removal); implement getInitialNotification to consult native pending action (Android) and expo last response (iOS); add TurboModule TypeScript bridge for VideoConfModule; adjust index to avoid loading removed Android background handler.
JS: removed background handler & related files
app/lib/notifications/videoConf/backgroundNotificationHandler.ts, index.js
Remove Notifee-based background video conference handler; skip dynamic require for Android handler.
App logic & minor fixes
app/sagas/deepLinking.js, app/sagas/troubleshootingNotification.ts, app/views/*, ios/fastlane/Fastfile, jest.setup.js
Minor fixes: optional chaining for caller, early exits, permissions API switch to expo-notifications, settings link change, simulator ARCH tweak, and test mocks updated to expo APIs.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant FCM as Firebase
    participant Sys as Android OS
    participant RCFMSvc as RCFirebaseMessagingService
    participant Custom as CustomPushNotification
    participant VideoNotif as VideoConfNotification
    participant User as User (tap accept/decline)
    participant Broadcast as VideoConfBroadcast
    participant VideoModule as VideoConfModule (SharedPrefs)
    participant MainAct as MainActivity
    participant JS as React Native JS

    FCM->>Sys: push data message (data payload)
    Sys->>RCFMSvc: deliver message
    RCFMSvc->>Custom: onReceived(bundle)
    Custom->>VideoNotif: show incoming call notification (build)
    Note right of VideoNotif: full-screen + actions
    User->>VideoNotif: taps Accept/Decline action
    VideoNotif->>Broadcast: send broadcast intent (ACTION_ACCEPT/DECLINE)
    Broadcast->>VideoModule: storePendingAction(json)
    Broadcast->>MainAct: startActivity with extras
    MainAct->>VideoModule: getPendingAction()
    VideoModule-->>MainAct: returns action JSON
    MainAct->>JS: dispatch deepLinkingClickCallPush (via Gson -> JS)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas to focus during review:

  • Android CustomPushNotification constructor/lifecycle and React-context use
  • VideoConf native classes: Pending-action persistence, intent extras, Gson serialization consistency
  • MainActivity onNewIntent handling and notification cancellation semantics
  • Expo notifications integration in push.ts: async flows, initial-notification resolution, and listener cleanup
  • iOS NotificationService and ReplyNotification delegate changes: ensure forward-to-original-delegate behavior and time-sensitive category handling

Possibly related PRs

Suggested reviewers

  • OtavioStasiak

Poem

🐰
I hopped through code with whiskers bright,
Replaced old bells with Expo light.
Calls now ring from native ground,
Pending hops are safely found.
A tiny rabbit cheers — notifications sound! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.53% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the changeset: migrating the notification system from react-native-notifications to expo-notifications.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore.expo-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/lib/notifications/push.ts (1)

161-215: Use the correct async Expo API getLastNotificationResponseAsync and fix the require-await violation

Two issues in pushNotificationConfigure:

  1. handleNotification is declared async but has no await, triggering the require-await ESLint rule.
  2. Notifications.getLastNotificationResponse() is treated as a synchronous API, but Expo's actual API is getLastNotificationResponseAsync(), which is async and returns a Promise. This pattern is already correctly implemented elsewhere in the codebase (e.g., app/lib/notifications/videoConf/getInitialNotification.ts).

Make pushNotificationConfigure itself async, change the handler to return a Promise without the async keyword, and use await with getLastNotificationResponseAsync:

-export const pushNotificationConfigure = (onNotification: (notification: INotification) => void): Promise<any> => {
+export const pushNotificationConfigure = async (onNotification: (notification: INotification) => void): Promise<any> => {
   if (configured) {
-    return Promise.resolve({ configured: true });
+    return { configured: true };
   }

   configured = true;

   // Set up how notifications should be handled when the app is in foreground
   Notifications.setNotificationHandler({
-    handleNotification: async () => ({
+    handleNotification: () =>
+      Promise.resolve({
         shouldShowAlert: false,
         shouldPlaySound: false,
         shouldSetBadge: false,
         shouldShowBanner: false,
         shouldShowList: false
-    })
+      })
   });

   // ... (rest of code)

-  // Get initial notification (app was opened by tapping a notification)
-  const lastResponse = Notifications.getLastNotificationResponse();
+  // Get initial notification (app was opened by tapping a notification)
+  const lastResponse = await Notifications.getLastNotificationResponseAsync();
   if (lastResponse) {
-    return Promise.resolve(transformNotificationResponse(lastResponse));
+    return transformNotificationResponse(lastResponse);
   }

-  return Promise.resolve(null);
+  return null;
 };

Call sites already treat pushNotificationConfigure as returning a Promise, so this change is compatible.

🧹 Nitpick comments (11)
app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx (1)

17-22: Confirm Linking.openSettings support and consider a small safety net

The switch to Linking.openSettings() on non‑iOS looks good and matches the intent of opening app settings, but this API depends on the React Native version and can reject on some devices/ROMs. It would be safer to both (a) confirm the app’s minimum RN version guarantees Linking.openSettings availability, and (b) optionally add a .catch to avoid unhandled rejections and, if you want, fall back to Linking.openURL('app-settings:').

-  } else {
-    Linking.openSettings();
-  }
+  } else {
+    Linking.openSettings().catch(() => {
+      // Optional: fallback if openSettings isn't supported
+      Linking.openURL('app-settings:').catch(() => {
+        // Swallow or log if even this fails
+      });
+    });
+  }
app/views/JitsiMeetView/index.tsx (1)

49-57: Relying on WebView instead of BackHandler in the failure path is reasonable

Dropping the BackHandler listener in the Linking.openURL failure path avoids globally blocking the hardware back button and lets the WebView / navigator handle navigation, which is safer and more predictable. Consider optionally logging the error or showing a lightweight message if openURL fails for reasons other than “app not installed”, but the current behavior is acceptable as-is.

ios/Podfile (1)

36-40: Consider adding a trailing comma for consistency.

The static analysis tool suggests adding a trailing comma after the last item in the multiline array. This is a Ruby style convention that makes future diffs cleaner when adding new entries.

 $static_framework = [ 
   'WatermelonDB',
   'simdjson',
-  'react-native-mmkv-storage'
+  'react-native-mmkv-storage',
 ]
ios/ReplyNotification.swift (1)

52-63: Consider the fallback presentation behavior.

When originalDelegate is nil, completionHandler([]) suppresses foreground notification presentation. While expo-notifications should always be present, if for any reason the delegate becomes nil, users won't see foreground notifications.

This is likely acceptable since expo-notifications is a core dependency, but worth being aware of during testing.

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

113-147: Thread lifecycle: consider cleanup mechanism.

The background thread used to wait for React initialization has no cancellation mechanism. If the notification is no longer relevant (e.g., user dismisses it before React initializes), the thread continues running and may post a stale notification.

Consider using a ScheduledExecutorService or storing a reference to enable cancellation:

+    private volatile boolean shouldCancelWait = false;
+    
     // Wait for React to initialize with timeout
     new Thread(() -> {
         int attempts = 0;
         int maxAttempts = 50;
         
-        while (!isReactInitialized() && attempts < maxAttempts) {
+        while (!isReactInitialized() && attempts < maxAttempts && !shouldCancelWait) {

238-244: Inconsistent synchronization on mBundle.

At line 190, mBundle assignment is synchronized, but here and at line 262, it's not. Since mBundle can be accessed from multiple threads (background polling thread, E2E callback), consider making all assignments consistent:

             if (decrypted != null) {
                 bundle.putString("message", decrypted);
-                mBundle = bundle;
+                synchronized(this) {
+                    mBundle = bundle;
+                }

Alternatively, declare mBundle as volatile if only single-assignment visibility is needed without compound operations.


342-350: Consider using named constants for video conf status values.

Magic numbers 0 and 4 for call status are unclear. Defining constants would improve readability and maintainability:

+    private static final int VIDEO_CONF_STATUS_INCOMING = 0;
+    private static final int VIDEO_CONF_STATUS_ENDED = 4;
+    
     ...
     
-    if (status == null || status == 0) {
+    if (status == null || status == VIDEO_CONF_STATUS_INCOMING) {
         // Incoming call - show notification
         videoConf.showIncomingCall(bundle, ejson);
-    } else if (status == 4) {
+    } else if (status == VIDEO_CONF_STATUS_ENDED) {
         // Call cancelled/ended - dismiss notification
         videoConf.cancelCall(rid, callerId);
android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt (1)

10-14: Remove unused Configuration import.

The Configuration import on line 11 is not used in this file.

 import android.content.Intent
-import android.content.res.Configuration
 import chat.rocket.reactnative.notification.VideoConfModule
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.java (1)

54-68: Consider null safety for caller fields.

The extras.getString() calls for callerId and callerName default to empty strings, but if these fields contain sensitive information, logging them at line 72 might be a compliance concern depending on whether rid contains user-identifiable data.

Consider removing rid from the debug log if it could contain user-identifiable information:

-        Log.d(TAG, "Stored video conf action: " + event + " for rid: " + extras.getString("rid"));
+        Log.d(TAG, "Stored video conf action: " + event);
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.java (1)

38-54: Consider using commit() instead of apply() for the clear operation.

The apply() method is asynchronous. If the app terminates before the write completes, the action could be processed again on next launch. Using commit() ensures the clear happens synchronously before resolving the promise.

             // Clear the action after reading
             if (action != null) {
-                prefs.edit().remove(KEY_VIDEO_CONF_ACTION).apply();
+                prefs.edit().remove(KEY_VIDEO_CONF_ACTION).commit();
             }
app/lib/notifications/push.ts (1)

12-26: Async badge/dismiss helpers look fine; consider centralizing logging

The async wrappers with try/catch around setBadgeCountAsync and dismissAllNotificationsAsync look good. If you have a central logging/analytics facility, you might want to use it instead of console.log so failures in production are easier to track.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1bd62a2 and 952068d.

⛔ Files ignored due to path filters (2)
  • ios/Podfile.lock is excluded by !**/*.lock
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (37)
  • android/app/build.gradle (0 hunks)
  • android/app/src/main/AndroidManifest.xml (2 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt (2 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (16 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java (3 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (2 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.java (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.java (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.java (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.java (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfPackage.java (1 hunks)
  • app/index.tsx (1 hunks)
  • app/lib/notifications/index.ts (2 hunks)
  • app/lib/notifications/push.ts (3 hunks)
  • app/lib/notifications/videoConf/backgroundNotificationHandler.ts (0 hunks)
  • app/lib/notifications/videoConf/getInitialNotification.ts (1 hunks)
  • app/sagas/deepLinking.js (3 hunks)
  • app/sagas/troubleshootingNotification.ts (2 hunks)
  • app/views/JitsiMeetView/index.tsx (2 hunks)
  • app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx (1 hunks)
  • index.js (1 hunks)
  • ios/AppDelegate.swift (1 hunks)
  • ios/NotificationService/NotificationService.swift (2 hunks)
  • ios/Podfile (1 hunks)
  • ios/ReplyNotification.swift (1 hunks)
  • ios/RocketChatRN-Bridging-Header.h (0 hunks)
  • ios/RocketChatRN.xcodeproj/project.pbxproj (6 hunks)
  • ios/Shared/Models/NotificationType.swift (1 hunks)
  • ios/Shared/Models/Payload.swift (1 hunks)
  • jest.setup.js (1 hunks)
  • package.json (1 hunks)
  • patches/@notifee+react-native+7.8.2.patch (0 hunks)
  • patches/@react-native-firebase+messaging+21.12.2.patch (0 hunks)
  • patches/react-native-notifications+5.1.0.patch (0 hunks)
  • react-native.config.js (1 hunks)
💤 Files with no reviewable changes (6)
  • android/app/build.gradle
  • ios/RocketChatRN-Bridging-Header.h
  • app/lib/notifications/videoConf/backgroundNotificationHandler.ts
  • patches/@react-native-firebase+messaging+21.12.2.patch
  • patches/react-native-notifications+5.1.0.patch
  • patches/@notifee+react-native+7.8.2.patch
🧰 Additional context used
🧬 Code graph analysis (11)
app/lib/notifications/push.ts (3)
app/definitions/INotification.ts (1)
  • INotification (1-16)
app/lib/notifications/index.ts (1)
  • onNotification (18-66)
app/sagas/deepLinking.js (1)
  • token (36-36)
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.java (1)
  • VideoConfNotification (25-236)
app/lib/notifications/videoConf/getInitialNotification.ts (2)
app/lib/store/auxStore.ts (1)
  • store (6-6)
app/actions/deepLinking.ts (1)
  • deepLinkingClickCallPush (26-31)
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)
  • CustomPushNotification (45-659)
ios/NotificationService/NotificationService.swift (2)
ios/Shared/Extensions/String+Extensions.swift (1)
  • removeTrailingSlash (25-31)
ios/Shared/RocketChat/RocketChat.swift (1)
  • getPushWithId (25-37)
android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)
  • CustomPushNotification (45-659)
android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (2)
android/app/src/main/java/chat/rocket/reactnative/networking/SSLPinningTurboPackage.java (1)
  • SSLPinningTurboPackage (15-45)
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfPackage.java (1)
  • VideoConfPackage (17-32)
app/index.tsx (1)
app/lib/notifications/videoConf/getInitialNotification.ts (1)
  • getInitialNotification (14-63)
ios/ReplyNotification.swift (4)
ios/NotificationService/NotificationService.swift (1)
  • didReceive (9-56)
ios/Shared/Extensions/String+Extensions.swift (1)
  • removeTrailingSlash (25-31)
ios/Shared/RocketChat/RocketChat.swift (1)
  • sendMessage (39-75)
ios/Shared/RocketChat/API/Requests/SendMessage.swift (1)
  • body (64-66)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java (1)
  • Notification (25-58)
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java (1)
  • Notification (25-58)
🪛 GitHub Actions: Format Code with Prettier
app/lib/notifications/push.ts

[error] 170-170: Async method 'handleNotification' has no 'await' expression. ESLint: 'require-await' rule triggered.

🪛 GitHub Check: format
app/lib/notifications/push.ts

[failure] 170-170:
Async method 'handleNotification' has no 'await' expression

🪛 RuboCop (1.81.7)
ios/Podfile

[convention] 39-39: Put a comma after the last item of a multiline array.

(Style/TrailingCommaInArrayLiteral)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test
🔇 Additional comments (50)
ios/RocketChatRN.xcodeproj/project.pbxproj (1)

1543-1547: LGTM - Standard privacy bundle integration for Expo modules

The changes correctly add required privacy manifest bundles (ExpoApplication_privacy.bundle, ExpoNotifications_privacy.bundle, ExpoDevice_privacy.bundle) to all three targets (RocketChatRN, Rocket.Chat, NotificationService). This is a standard CocoaPods-generated update that ensures compliance with Apple's App Privacy Details requirements when using these Expo modules.

The pattern is consistent with other privacy bundles already in the project, and having these bundles in all targets ensures proper privacy declarations across the main app and extensions.

Also applies to: 1576-1580, 1636-1640, 1669-1673, 1973-1977, 2006-2010

app/views/JitsiMeetView/index.tsx (1)

5-5: Import cleanup after BackHandler removal looks good

BackHandler is no longer used and everything still imported from react-native here is referenced in the file, so this simplification is correct and keeps the module tidy.

index.js (1)

25-27: LGTM!

The comment clearly documents the architectural change, explaining that Android video conference notifications are now handled natively in the RCFirebaseMessagingService -> CustomPushNotification -> VideoConfNotification chain. This helps future maintainers understand why the background handler was removed.

ios/Shared/Models/Payload.swift (3)

11-14: LGTM!

The new Caller struct properly mirrors the Android Ejson.Caller class, maintaining consistency across platforms. The _id naming follows the Rocket.Chat/MongoDB convention and will decode correctly from JSON.


29-33: LGTM!

The video conference fields are appropriately defined as optionals, ensuring backward compatibility with existing notifications that don't include these fields.


21-21: Callers properly handle the optional messageId.

The only usage in NotificationService.swift (line 45) correctly guards access with if let messageId = data.messageId, ensuring safe handling of the optional type. No unsafe unwrapping patterns detected.

app/sagas/deepLinking.js (3)

49-56: LGTM!

Minor formatting change with no functional impact.


226-226: Good use of optional chaining.

Adding params.caller?._id prevents potential TypeError when caller is undefined or null, improving robustness for edge cases in video conference notifications.


249-251: LGTM!

The early return when host is falsy prevents errors during host normalization and is consistent with the similar guard in handleOpen() at line 142.

android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (4)

25-36: Excellent documentation of the notification architecture.

The comment clearly explains the hybrid approach where JS uses expo-notifications for token registration while native code handles FCM message processing, notification display, E2E decryption, and direct reply. This is valuable for future maintainers understanding the system.


37-37: LGTM!

Removing INotificationsApplication from the class declaration completes the migration away from react-native-notifications.


45-45: LGTM!

VideoConfPackage is properly added to the packages list, enabling the VideoConfModule native module for JS-native communication.


68-72: [rewritten review comment]
[classification tag]

android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (2)

320-327: LGTM - Context fallback pattern is sound.

The fallback to CustomPushNotification.reactApplicationContext is properly guarded with a null check before use. The static volatile field ensures visibility across threads.


369-375: LGTM - Direct context assignment is appropriate here.

Unlike decryptMessage which receives a Context parameter, encryptMessageContent has no incoming context to check, so directly using the static field with a null guard is the correct approach.

react-native.config.js (1)

1-3: LGTM - Config cleanup aligns with dependency removal.

The platform-specific exclusion for @react-native-firebase/messaging is no longer needed since the package is being removed from the project. The empty dependencies object is a clean approach.

ios/AppDelegate.swift (1)

30-31: LGTM - Notification handler initialization is properly placed.

ReplyNotification.configure() is called after Firebase/MMKV initialization but before React Native starts, ensuring the notification delegate chain is established early. The ExpoAppDelegate base class handles the standard remote notification lifecycle methods that were removed.

ios/ReplyNotification.swift (2)

16-28: LGTM - Delegate chain pattern is well-implemented.

The weak reference to originalDelegate prevents retain cycles, and the configuration properly preserves the expo-notifications delegate while intercepting reply actions. This is the correct pattern for delegate interception.


79-117: LGTM - Reply handling with proper background task management.

The defer block ensures endBackgroundTask and completionHandler are always called, even on success. The pattern correctly handles:

  • Payload decoding failures (early return with completionHandler)
  • Background task lifecycle (safe to call endBackgroundTask even if invalid)
  • Failure notification display on error
jest.setup.js (1)

139-156: LGTM! Comprehensive expo-notifications mock setup.

The mock provides a complete API surface for testing, including proper subscription cleanup with { remove: jest.fn() } returns for all listener methods. This aligns well with the expo-notifications migration.

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

360-371: LGTM! Notification channel creation is correct.

Proper Android O+ check with IMPORTANCE_HIGH for push notifications. The null check on notificationManager adds resilience.


393-397: LGTM! Correct PendingIntent flags for Android S+.

Using FLAG_IMMUTABLE for content intents on Android S+ is correct. The reply intent at line 611 correctly uses FLAG_MUTABLE since RemoteInput requires mutability.

ios/Shared/Models/NotificationType.swift (1)

11-15: LGTM! Clean addition of videoconf notification type.

The new case aligns with the cross-platform video conference notification support and integrates properly with the existing Codable enum.

app/index.tsx (1)

141-144: LGTM! Correct integration of video conference initial notification handling.

The placement is appropriate—after push notification handling but before deep linking—ensuring video conference notifications from cold start are properly dispatched via deepLinkingClickCallPush. The early return prevents duplicate processing.

app/sagas/troubleshootingNotification.ts (1)

22-23: LGTM! Clean migration to expo-notifications permissions API.

The permission check correctly uses status === 'granted'. This is slightly stricter than the previous notifee implementation (which may have considered undetermined as enabled), which is the correct behavior for troubleshooting diagnostics.

android/app/src/main/AndroidManifest.xml (2)

78-85: LGTM! Firebase messaging service properly configured.

The service is correctly set with exported=false and a high priority intent-filter to ensure it receives FCM messages before other potential handlers.


95-103: LGTM! VideoConfBroadcast receiver properly secured.

Good use of exported=false with explicit intent-filter actions using the app's package namespace. This ensures only the app can trigger these video conference actions.

android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.java (2)

22-45: LGTM! Solid FCM message handling implementation.

Good defensive checks (empty data validation) and proper error handling. The try-catch ensures a malformed notification doesn't crash the service.


47-53: LGTM! Token refresh handling is appropriate.

Delegating token management to the expo-notifications JS layer via getDevicePushTokenAsync() is the correct approach for this migration.

android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt (1)

45-85: LGTM! Video conference intent handling is well-implemented.

Good defensive patterns: early return when event is missing, clearing the flag to prevent re-processing, and proper notification cancellation. The JSON serialization via Gson for JS interop is appropriate.

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

38-45: LGTM! Video conference fields are well-documented.

The inline comments explaining the purpose (// For video conf notifications) and status values (// For video conf: 0=incoming, 4=cancelled) improve code readability. Using Integer instead of int for status correctly allows null-checking for the incoming call case.


232-235: LGTM! Caller class structure is consistent with existing Sender class.

The new Caller inner class mirrors the Sender class structure, maintaining consistency in how user identity data is modeled.


77-80: No thread safety issue: reactApplicationContext is declared volatile. The field in CustomPushNotification is properly declared with the volatile keyword (line 50), which provides visibility guarantees for reads and writes across threads. This is a standard and safe pattern for shared mutable state in multi-threaded Java code. No race condition exists here.

Likely an incorrect or invalid review comment.

ios/NotificationService/NotificationService.swift (1)

58-86: LGTM! Video conference notification handling is well-structured.

Good implementation with proper status-based branching, iOS 15+ availability check for interruptionLevel, and appropriate use of localized strings. The ringtone.mp3 file is present in the project bundle, confirming the sound resource dependency is satisfied.

android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.java (3)

1-22: LGTM! Well-structured BroadcastReceiver setup.

Good use of constants for the TAG and clear class documentation explaining the purpose.


24-46: Proper validation and action mapping.

The null checks and action-to-event mapping logic are correct. Early returns on invalid input prevent unnecessary processing.


74-80: Appropriate activity launch flags for BroadcastReceiver context.

The flags FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP are correct when starting an activity from a non-Activity context. The extras forwarding ensures MainActivity receives all necessary data.

android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.java (3)

1-32: LGTM! Standard React Native module setup.

Proper use of @ReactModule annotation and clear constants for SharedPreferences.


59-68: Silent error handling is acceptable for cleanup operations.

The clearPendingAction method appropriately ignores errors since failing to clear a pending action is not critical.


74-77: LGTM! Static method allows native-to-native calls.

Using a static method for storePendingAction is correct since it's called from VideoConfBroadcast before the React context may be available.

app/lib/notifications/videoConf/getInitialNotification.ts (4)

1-8: LGTM! Proper imports for cross-platform handling.

Clean separation of platform-specific dependencies.


14-29: LGTM! Android path with proper error handling.

The null checks and try-catch ensure graceful degradation if the native module is unavailable or returns invalid data.


40-42: Type narrowing could be simplified.

The trigger type checking is verbose but necessary for TypeScript safety. This is acceptable given expo-notifications' type definitions.


31-60: The VIDEOCONF notification category and action identifiers are properly configured.

The code correctly checks for 'DECLINE_ACTION' on line 49. The iOS notification category setup in app/lib/notifications/push.ts (lines 107–117) properly registers both ACCEPT_ACTION and DECLINE_ACTION for the VIDEOCONF category, matching what the TypeScript code expects and what the iOS native code sets as the category identifier in NotificationService.swift.

app/lib/notifications/index.ts (2)

18-28: LGTM! Clear separation of button action handling.

Correctly distinguishes between explicit Accept/Decline button presses and other notification interactions.


30-64: Good fallback to appInit() when notification cannot be parsed.

The try-catch ensures the app initializes even if notification parsing fails. The types mapping correctly handles all subscription types.

android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfPackage.java (1)

1-32: LGTM! Standard ReactPackage implementation.

Correctly registers the VideoConfModule for React Native access. VideoConfPackage is properly imported and registered in MainApplication.kt (line 45).

app/lib/notifications/push.ts (1)

131-159: The current implementation correctly pairs device token retrieval with device token updates

The code properly uses getDevicePushTokenAsync() to obtain the native device token (APNs/FCM) and addPushTokenListener() to listen for device token updates. addPushTokenListener is specifically designed for native device token changes, not Expo tokens, so there is no mismatch here. The token type remains consistent as a native device token throughout.

Likely an incorrect or invalid review comment.

android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.java (2)

47-72: Notification channel configuration follows current Android best practices

The channel is correctly configured with IMPORTANCE_HIGH for urgent head-up behavior, ringtone audio set via channel.setSound() with AudioAttributes.USAGE_NOTIFICATION_RINGTONE for proper audio routing, and public lockscreen visibility. This aligns with Android 8.0+ patterns for incoming call notifications. The null-check on notificationManager before creating the channel is good defensive coding.


214-235: Cancel helpers use hashCode() — note 32-bit collision risk

cancelCall reuses (rid + callerId).hashCode() from showIncomingCall, and cancelById correctly wraps NotificationManager.cancel. The ID generation is consistent between both methods.

However, String.hashCode() carries practical caveats: it produces stable values across process restarts on Android but is not guaranteed by the hashCode specification, and 32-bit collisions are possible with large uid spaces. For production use, consider either accepting the collision risk or using NotificationManager.notify(tag, id, ...) with a string tag to avoid the integer hash altogether.

Comment on lines 510 to 520
private String extractMessage(String message, Ejson ejson) {
if (message == null) {
return "";
}
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, message.length());
return message.substring(start);
}
return message;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential StringIndexOutOfBoundsException in extractMessage.

If the message contains a colon at position pos where pos + 2 > message.length(), substring(start) will throw. For example, message "a:" has pos=1, start=3, but length=2.

     private String extractMessage(String message, Ejson ejson) {
         if (message == null) {
             return "";
         }
         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 = Math.min(pos + 2, message.length());
+            return message.substring(start);
         }
         return message;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private String extractMessage(String message, Ejson ejson) {
if (message == null) {
return "";
}
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, message.length());
return message.substring(start);
}
return message;
}
private String extractMessage(String message, Ejson ejson) {
if (message == null) {
return "";
}
if (ejson != null && ejson.type != null && !ejson.type.equals("d")) {
int pos = message.indexOf(":");
if (pos == -1) {
return message;
}
int start = Math.min(pos + 2, message.length());
return message.substring(start);
}
return message;
}
🤖 Prompt for AI Agents
In
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
around lines 510 to 520, the code computes start = pos == -1 ? 0 : pos + 2 and
then calls message.substring(start), which can throw
StringIndexOutOfBoundsException when pos+2 > message.length(); change the start
calculation to clamp it within the message bounds (for example start = pos == -1
? 0 : Math.min(pos + 2, message.length())) so substring(start) is safe and
returns an empty string when the colon is at the end.

Comment on lines +44 to 51
// Extract bundle directly from intent extras
bundle = intent.getBundleExtra("pushNotification");
if (bundle == null) {
bundle = intent.getExtras();
}
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

String notId = bundle.getString("notId");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add null check for bundle to prevent potential NPE.

If both intent.getBundleExtra("pushNotification") and intent.getExtras() return null, the subsequent bundle.getString("notId") at line 51 will throw a NullPointerException.

             // Extract bundle directly from intent extras
             bundle = intent.getBundleExtra("pushNotification");
             if (bundle == null) {
                 bundle = intent.getExtras();
             }
+            if (bundle == null) {
+                return;
+            }
             notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
🤖 Prompt for AI Agents
In
android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java
around lines 44 to 51, the code assumes bundle is non-null but both
intent.getBundleExtra("pushNotification") and intent.getExtras() can be null,
causing a NullPointerException when calling bundle.getString("notId"); add a
null check immediately after attempting to populate bundle and handle the null
case by logging a warning and returning early (or providing a safe default)
before accessing bundle, ensuring any subsequent use of notificationManager or
bundle fields is only done when bundle is confirmed non-null.

Comment on lines 80 to 206
public void showIncomingCall(Bundle bundle, Ejson ejson) {
String rid = ejson.rid;
// Video conf uses 'caller' field, regular messages use 'sender'
String callerId = "";
String callerName = "Unknown";

if (ejson.caller != null) {
callerId = ejson.caller._id != null ? ejson.caller._id : "";
callerName = ejson.caller.name != null ? ejson.caller.name : "Unknown";
} else if (ejson.sender != null) {
// Fallback to sender if caller is not present
callerId = ejson.sender._id != null ? ejson.sender._id : "";
callerName = ejson.sender.name != null ? ejson.sender.name : (ejson.senderName != null ? ejson.senderName : "Unknown");
}

// Generate unique notification ID from rid + callerId
String notificationIdStr = (rid + callerId).replaceAll("[^A-Za-z0-9]", "");
int notificationId = notificationIdStr.hashCode();

Log.d(TAG, "Showing incoming call notification from: " + callerName);

// Create intent data for actions - include all required fields for JS
Bundle intentData = new Bundle();
intentData.putString("rid", rid != null ? rid : "");
intentData.putString("notificationType", "videoconf");
intentData.putString("callerId", callerId);
intentData.putString("callerName", callerName);
intentData.putString("host", ejson.host != null ? ejson.host : "");
intentData.putString("callId", ejson.callId != null ? ejson.callId : "");
intentData.putString("ejson", bundle.getString("ejson", "{}"));
intentData.putInt("notificationId", notificationId);

Log.d(TAG, "Intent data - rid: " + rid + ", callerId: " + callerId + ", host: " + ejson.host + ", callId: " + ejson.callId);

// Full screen intent - opens app when notification is tapped
Intent fullScreenIntent = new Intent(context, MainActivity.class);
fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
fullScreenIntent.putExtras(intentData);
fullScreenIntent.putExtra("event", "default");

PendingIntent fullScreenPendingIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
fullScreenPendingIntent = PendingIntent.getActivity(
context, notificationId, fullScreenIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
} else {
fullScreenPendingIntent = PendingIntent.getActivity(
context, notificationId, fullScreenIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
}

// Accept action - directly opens MainActivity (Android 12+ blocks trampoline pattern)
Intent acceptIntent = new Intent(context, MainActivity.class);
acceptIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
acceptIntent.putExtras(intentData);
acceptIntent.putExtra("event", "accept");
acceptIntent.putExtra("videoConfAction", true);
acceptIntent.setAction(ACTION_ACCEPT + "_" + notificationId); // Unique action to differentiate intents

PendingIntent acceptPendingIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
acceptPendingIntent = PendingIntent.getActivity(
context, notificationId + 1, acceptIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
} else {
acceptPendingIntent = PendingIntent.getActivity(
context, notificationId + 1, acceptIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
}

// Decline action - directly opens MainActivity
Intent declineIntent = new Intent(context, MainActivity.class);
declineIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
declineIntent.putExtras(intentData);
declineIntent.putExtra("event", "decline");
declineIntent.putExtra("videoConfAction", true);
declineIntent.setAction(ACTION_DECLINE + "_" + notificationId); // Unique action to differentiate intents

PendingIntent declinePendingIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
declinePendingIntent = PendingIntent.getActivity(
context, notificationId + 2, declineIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
} else {
declinePendingIntent = PendingIntent.getActivity(
context, notificationId + 2, declineIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
}

// Get icons
Resources res = context.getResources();
String packageName = context.getPackageName();
int smallIconResId = res.getIdentifier("ic_notification", "drawable", packageName);

// Build notification
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(smallIconResId)
.setContentTitle("Incoming call")
.setContentText("Video call from " + callerName)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setAutoCancel(false)
.setOngoing(true)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setContentIntent(fullScreenPendingIntent)
.addAction(0, "Decline", declinePendingIntent)
.addAction(0, "Accept", acceptPendingIntent);

// Set sound for pre-O devices
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Uri ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
builder.setSound(ringtoneUri);
}

// Show notification
if (notificationManager != null) {
notificationManager.notify(notificationId, builder.build());
Log.d(TAG, "Video call notification displayed with ID: " + notificationId);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Android NotificationCompat.Builder setSmallIcon(0) missing resource crash behavior

💡 Result:

Short answer: Passing 0 to setSmallIcon (i.e. no valid drawable resource) will cause the notification to fail at post time and crash the app — typically with errors like "Invalid notification (no valid small icon)" / "Couldn't create icon: StatusBarIcon" or a RemoteServiceException (Bad notification posted). The platform docs state small icon is required and NotificationCompat expects a valid resource id. [1][2][3][4]

Fix: Always pass a valid drawable resource id (not 0). Provide a default notification icon (manifest meta-data or app drawable) and/or guard your code before calling setSmallIcon(…) so you never call it with 0. [1][4]

Sources:

  • Android docs: small icon is required / NotificationCompat.Builder setSmallIcon. [1][4]
  • Reports and stack traces showing "Invalid notification (no valid small icon)" / RemoteServiceException when icon/resource invalid. [2][3]

🏁 Script executed:

find . -name "VideoConfNotification.java" -type f

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 169


🏁 Script executed:

wc -l ./android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 173


🏁 Script executed:

cat -n ./android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 12205


Fix critical small icon crash and add null-safety guards to showIncomingCall

The method has several robustness issues that can crash the app:

  1. Critical: Small icon resource lookup can crash notifications
    Line 178–182: If ic_notification drawable is missing or renamed, getIdentifier() returns 0, and setSmallIcon(0) will crash the notification post with RemoteServiceException: Bad notification posted or "Invalid notification (no valid small icon)". Always validate the resource ID before passing it to setSmallIcon():
int smallIconResId = res.getIdentifier("ic_notification", "drawable", packageName);
+ if (smallIconResId == 0) {
+     smallIconResId = android.R.drawable.sym_call_incoming; // or your app's default icon
+ }
  1. Null-safety for ejson and bundle
    Line 81 dereferences ejson.rid without null checks; line 109 calls bundle.getString() without null validation. Either can crash if upstream parsing fails:
+ if (ejson == null) {
+     Log.w(TAG, "showIncomingCall called with null Ejson payload");
+     return;
+ }
+ if (bundle == null) {
+     bundle = new Bundle();
+ }
  String rid = ejson.rid;
  1. Notification ID collision when both rid and callerId are empty
    Line 96–97: If both are empty, notificationIdStr becomes "", and hashCode() returns 0, causing multiple such calls to reuse notification ID 0. Consider a fallback: notificationIdStr = (rid + callerId).isEmpty() ? String.valueOf(System.currentTimeMillis()) : (rid + callerId).

  2. Hard-coded user-facing strings
    Lines 183–193: "Incoming call", "Video call from ", "Accept", "Decline" should be moved to string resources for localization and consistency.

🤖 Prompt for AI Agents
In
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.java
around lines 80–206, make showIncomingCall null-safe and robust: check bundle
and ejson for null before dereferencing (guard ejson.rid, ejson.caller/sender,
ejson.host/callId and use empty defaults), validate bundle before calling
bundle.getString("ejson", "{}"), and return/log if required data is missing;
validate the drawable resource id from getIdentifier() and if it's 0 use a safe
fallback (app icon or a bundled default) before calling setSmallIcon(); avoid
notification ID collisions by using a fallback when (rid + callerId) is empty
(e.g., use System.currentTimeMillis() or a secure random value to build
notificationIdStr); and replace hard-coded UI strings ("Incoming call", "Video
call from ", "Accept", "Decline") with string resources referenced via
context.getString(...).

Comment on lines +72 to 78
export const removeNotificationsAndBadge = async (): Promise<void> => {
await removeAllNotifications();
await setNotificationsBadgeCount();
};
export const initializePushNotifications = (): Promise<INotification | { configured: boolean }> | undefined => {
setBadgeCount();
export const initializePushNotifications = async (): Promise<INotification | { configured: boolean } | null> => {
await setNotificationsBadgeCount();
return pushNotificationConfigure(onNotification);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "removeNotificationsAndBadge|initializePushNotifications" --type ts --type tsx -A 2 -B 2

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 105


🏁 Script executed:

rg -n "removeNotificationsAndBadge|initializePushNotifications" --type typescript -A 2 -B 2

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 112


🏁 Script executed:

rg -n "removeNotificationsAndBadge|initializePushNotifications" -A 2 -B 2

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2124


🏁 Script executed:

cat -n app/lib/store/appStateMiddleware.ts | head -30

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1105


Fix unhandled promise in app/lib/store/appStateMiddleware.ts.

removeNotificationsAndBadge() is called at line 20 without awaiting the returned promise. Since the parent function handleAppStateChange is not async, add .catch() to handle any rejections or refactor the callback to be async. The call to initializePushNotifications() in app/index.tsx is correctly handled with await.

🤖 Prompt for AI Agents
In app/lib/notifications/index.ts around lines 72-78 the exported async helpers
can reject and cause unhandled promise rejections when callers don’t await them
(removeNotificationsAndBadge is called without await). Wrap the internal awaits
in try/catch (or .catch) and log/handle errors so the function always resolves
(e.g., try { await removeAllNotifications(); await setNotificationsBadgeCount();
} catch (err) { logger.warn(...) } and similarly guard pushNotificationConfigure
in initializePushNotifications), ensuring these functions do not propagate
unhandled rejections to callers that are not async.

Comment on lines +45 to 54
if let messageId = data.messageId {
self.rocketchat?.getPushWithId(messageId) { notification in
if let notification = notification {
self.bestAttemptContent?.title = notification.title
self.bestAttemptContent?.body = notification.text
self.processPayload(payload: notification.payload)
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing contentHandler call when messageId is nil.

If messageId is nil, the if let block is skipped and contentHandler is never invoked, causing the notification service extension to time out (approximately 30 seconds) before iOS terminates it.

             // Request the content from server
             if let messageId = data.messageId {
                 self.rocketchat?.getPushWithId(messageId) { notification in
                     if let notification = notification {
                         self.bestAttemptContent?.title = notification.title
                         self.bestAttemptContent?.body = notification.text
                         self.processPayload(payload: notification.payload)
                     }
                 }
+            } else {
+                // No messageId available, deliver the notification as-is
+                if let bestAttemptContent = self.bestAttemptContent {
+                    self.contentHandler?(bestAttemptContent)
+                }
             }
🤖 Prompt for AI Agents
In ios/NotificationService/NotificationService.swift around lines 45 to 54, the
code skips calling contentHandler when messageId is nil (or when getPushWithId
returns nil), which causes the extension to time out; ensure contentHandler is
invoked in every control path: when messageId is nil immediately call
contentHandler(self.bestAttemptContent ?? self.request.content), and also call
contentHandler with the current bestAttemptContent (or original request.content)
inside the completion handler if notification is nil or after processing the
payload, guaranteeing the extension always finishes.

Comment on lines +64 to +72
"expo-device": "^8.0.10",
"expo-document-picker": "13.1.4",
"expo-file-system": "18.1.7",
"expo-haptics": "14.1.3",
"expo-image": "^2.3.2",
"expo-keep-awake": "14.1.3",
"expo-local-authentication": "16.0.3",
"expo-navigation-bar": "^4.2.4",
"expo-notifications": "^0.32.14",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

expo-notifications 0.32 Expo SDK 53 compatibility requirements

💡 Result:

Short answer

  • expo-notifications 0.32 (0.32.x) is published as the current 0.32 release series and is compatible with Expo SDK 53. The package's package.json lists generic peerDependencies (expo/react/react-native: "*"), and the SDK 53 docs and changelog reference expo-notifications updates for SDK 53. [1][2][3]

Key compatibility/requirements for using expo-notifications 0.32 with Expo SDK 53

  • You must install expo-notifications (e.g. npx expo install expo-notifications or npm/yarn). Its package.json shows it as the 0.32.x release and expects to be used inside an Expo project. [2][3]
  • Push notifications on Android: from SDK 53 remote push notifications are not available in Expo Go — a development build (or standalone/EAS build) is required to test/send push notifications on Android. Local notifications still work in Expo Go. [1]
  • App config / build-time plugin: expo-notifications requires using its config plugin (or the built-in plugin) and proper native build configuration (EAS / npx expo run:android|ios) to enable push token registration and entitlements (especially for iOS). Users have reported that omitting the plugin in app.json/app.config.* prevents iOS push from working. Add the plugin and follow the push-notifications setup guide when building. [1][4]
  • Platform / OS build requirements: follow SDK 53 platform requirements (compile/target SDK and Xcode versions) when building native apps. See the Expo SDK 53 platform table for supported OS/Xcode/compileSdk targets. [5]

References

  • Expo SDK 53 notifications docs (push-notifications behavior in Expo Go / configuration). [1]
  • expo-notifications package (0.32.10) package.json on UNPKG (shows 0.32.x release metadata). [2]
  • expo-notifications CHANGELOG (0.32.x release notes). [3]
  • GitHub issue noting need to add expo-notifications config plugin for iOS push on SDK 53. [4]
  • Expo SDK platform/compile requirements table (SDK 53). [5]

If you want, I can paste the exact install/config steps for SDK 53 (app.json plugin entry, EAS build steps, and the minimal code to request permissions and get the device token).


🌐 Web query:

expo-device 8.0 Expo SDK 53 compatibility

💡 Result:

Short answer: No — [email protected] is not the version bundled with Expo SDK 53. SDK 53 was released in Apr/May 2025 and expects the v7.x family of Expo modules; expo-device 8.0.x was released later (Aug–Oct 2025). Use npx expo install expo-device (or pin to the v7.x matching your SDK) to get the correct module for SDK 53. [1][2]

Sources

  • Expo SDK 53 release notes. [1]
  • expo-device changelog showing 8.0.x release dates. [2]

Fix expo-device version — 8.0.x is incompatible with Expo SDK 53.

The expo-device version should be from the v7.x family (e.g., ^7.x.x), not 8.0.x. Expo SDK 53 was released in April/May 2025 and expects expo-device v7.x; version 8.0.x was released later and is not compatible with this SDK version. Update the dependency to use the correct version range.

The expo-notifications version (^0.32.14) is compatible with SDK 53 and requires no changes.

🤖 Prompt for AI Agents
In package.json around lines 64 to 72, the expo-device dependency is pinned to
^8.0.10 which is incompatible with Expo SDK 53; change the expo-device entry to
a v7.x range (for example ^7.0.0 or ^7.x.x) so it matches SDK 53 expectations,
then run yarn/npm install and verify metro/build to ensure no version conflicts;
leave expo-notifications (^0.32.14) as-is.

@diegolmello diegolmello temporarily deployed to approve_e2e_testing December 10, 2025 21:25 — with GitHub Actions Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/lib/notifications/push.ts (1)

190-206: Missing listener cleanup could cause memory leaks.

The listeners added via addPushTokenListener and addNotificationResponseReceivedListener are never removed. While the configured flag prevents re-initialization, this could still cause issues during testing or hot-reloading.

Consider storing the subscriptions and exposing a cleanup function:

let tokenSubscription: Notifications.Subscription | null = null;
let responseSubscription: Notifications.Subscription | null = null;

// In pushNotificationConfigure:
tokenSubscription = Notifications.addPushTokenListener(tokenData => {
  deviceToken = tokenData.data;
});

responseSubscription = Notifications.addNotificationResponseReceivedListener(response => {
  // ...
});

// Export cleanup function:
export const cleanupPushNotifications = () => {
  tokenSubscription?.remove();
  responseSubscription?.remove();
  configured = false;
};
🧹 Nitpick comments (4)
app/lib/notifications/videoConf/getInitialNotification.ts (3)

8-8: Add TypeScript types for VideoConfModule.

The VideoConfModule lacks TypeScript type definitions, which reduces type safety and IDE support.

Consider defining an interface for the module:

interface IVideoConfModule {
  getPendingAction: () => Promise<string | null>;
}

const { VideoConfModule } = NativeModules as { VideoConfModule?: IVideoConfModule };

17-24: Consider validating parsed data structure.

The code parses JSON but only checks notificationType. If the parsed data has an unexpected structure, it could cause issues downstream in deepLinkingClickCallPush.

Consider adding basic validation:

 const data = JSON.parse(pendingAction);
-if (data?.notificationType === 'videoconf') {
+if (data && typeof data === 'object' && data.notificationType === 'videoconf') {
   store.dispatch(deepLinkingClickCallPush(data));
   return true;
 }

40-42: Type guard relies on runtime property checking.

The condition 'type' in trigger && trigger.type === 'push' uses runtime checks that TypeScript can't fully validate. This is acceptable but worth noting.

If expo-notifications exports type guards or discriminated unions for triggers, prefer those for better type safety.

app/lib/notifications/push.ts (1)

33-79: Consider improving type safety for payload transformation.

The extensive use of Record<string, any> and fallback to empty strings could hide missing data issues. While functional, stronger typing would catch transformation errors earlier.

Consider defining intermediate types:

interface RemoteMessagePayload {
  message?: string;
  style?: string;
  ejson?: string;
  collapse_key?: string;
  notId?: string;
  msgcnt?: string;
  title?: string;
  from?: string;
  image?: string;
  soundname?: string;
}

This would make the transformation more self-documenting and catch missing fields during development.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 952068d and d44a321.

📒 Files selected for processing (2)
  • app/lib/notifications/push.ts (3 hunks)
  • app/lib/notifications/videoConf/getInitialNotification.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/lib/notifications/push.ts (2)
app/definitions/INotification.ts (1)
  • INotification (1-16)
app/lib/notifications/index.ts (1)
  • onNotification (18-66)
app/lib/notifications/videoConf/getInitialNotification.ts (2)
app/lib/store/auxStore.ts (1)
  • store (6-6)
app/actions/deepLinking.ts (1)
  • deepLinkingClickCallPush (26-31)
🔇 Additional comments (6)
app/lib/notifications/videoConf/getInitialNotification.ts (1)

10-14: LGTM!

The signature change to return Promise<boolean> is well-documented and provides clear semantics for whether a video conference action was dispatched.

app/lib/notifications/push.ts (5)

12-18: LGTM!

The migration to setBadgeCountAsync correctly updates the signature to return Promise<void> and includes appropriate error handling.


20-26: LGTM!

Properly migrated to dismissAllNotificationsAsync with consistent error handling.


84-125: LGTM!

The notification categories are properly configured with appropriate actions and localized titles. The structure correctly distinguishes between MESSAGE (reply in background) and VIDEOCONF (foreground) use cases.


130-158: LGTM!

The permission request flow is correctly implemented with proper device checks and graceful error handling.


209-212: No issues identified. The code correctly calls Notifications.getLastNotificationResponse() without await, which is the appropriate synchronous method in expo-notifications v0.32.14. The library also provides Notifications.getLastNotificationResponseAsync() for async use cases, but the synchronous variant is intentionally used here and is the correct choice.

});

// Set up notification categories for iOS
setupNotificationCategories();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing await for setupNotificationCategories.

The async function setupNotificationCategories() is called without await, which means configuration might continue before categories are set up. While this may work in practice due to iOS's asynchronous nature, it's inconsistent with async patterns.

Apply this diff to await the call:

-	setupNotificationCategories();
+	await setupNotificationCategories();

Note: This requires making pushNotificationConfigure async or restructuring the promise chain.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/lib/notifications/push.ts around line 180, the async function
setupNotificationCategories() is invoked without awaiting it, risking
continuation before categories are configured; update pushNotificationConfigure
to be async (or return a Promise) and add await setupNotificationCategories();
so the call is awaited, or alternatively chain the promise (return
setupNotificationCategories().then(...)) to ensure configuration waits for
completion; ensure callers of pushNotificationConfigure are updated to handle
the async return.

@github-actions
Copy link

iOS Build Available

Rocket.Chat Experimental 4.68.0.107809

@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.68.0.107808

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNR5VeS_aT_14h-wCQVjHcPgTUTPbsNvxCauJr7aPm-67ifOGf8bQEkD5sD41uX9kFBaHueVG6_KzWKS4c1p

…ng to TurboModules

- Replaced the existing VideoConfModule and related classes with a TurboModule implementation for improved performance and integration.
- Updated MainApplication to use VideoConfTurboPackage instead of the legacy VideoConfPackage.
- Enhanced notification handling by introducing a new User-Agent header in relevant requests.
- Removed obsolete Java classes and streamlined the notification architecture to utilize Kotlin for better maintainability.
- Improved the handling of video conference actions and ensured compatibility with the new TurboModule system.
@diegolmello diegolmello force-pushed the chore.expo-notifications branch from c6f20e7 to 6620a1b Compare December 11, 2025 18:32
@diegolmello diegolmello temporarily deployed to approve_e2e_testing December 11, 2025 18:32 — with GitHub Actions Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
ios/fastlane/Fastfile (1)

115-125: Forcing ARCHS=arm64 limits simulator builds to Apple Silicon Macs

Adding ARCHS=arm64 to xcargs will prevent this lane from building on Intel Macs (which need x86_64 for iOS Simulator). If your CI or contributors still use Intel hardware, this lane will start failing there.

If the intent is to fix Apple Silicon / arm64-simulator issues, consider one of:

  • Gate this via an env var or separate lane (e.g., build_experimental_simulator_apple_silicon).
  • Let Xcode choose architectures (no explicit ARCHS) and instead handle problematic pods via their own settings.
  • Document in the README / iOS build doc that this lane now assumes Apple Silicon-only.

Please confirm this trade-off is acceptable for your build matrix.

android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.kt (1)

56-65: Consider logging errors in clearPendingAction for debugging.

While ignoring errors in a cleanup method may be intentional, logging the exception would aid debugging without affecting functionality.

Apply this diff to add logging:

     @ReactMethod
     override fun clearPendingAction() {
         try {
             reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
                 .edit()
                 .remove(KEY_VIDEO_CONF_ACTION)
                 .apply()
         } catch (e: Exception) {
-            // Ignore errors
+            Log.w("VideoConfModule", "Failed to clear pending action", e)
         }
     }
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt (1)

188-195: Consider adding FLAG_IMMUTABLE for pre-S devices.

For security best practices, FLAG_IMMUTABLE is recommended on Android M+ when the PendingIntent doesn't need to be modified. Currently, pre-S devices don't include this flag.

     private fun createPendingIntent(requestCode: Int, intent: Intent): PendingIntent {
         val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
             PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
         } else {
             PendingIntent.FLAG_UPDATE_CURRENT
         }
         return PendingIntent.getActivity(context, requestCode, intent, flags)
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c6f20e7 and 6620a1b.

📒 Files selected for processing (14)
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/NativeVideoConfSpec.kt (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java (2 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java (2 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.kt (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt (1 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfTurboPackage.kt (1 hunks)
  • app/lib/native/NativeVideoConfAndroid.ts (1 hunks)
  • app/lib/notifications/videoConf/getInitialNotification.ts (1 hunks)
  • ios/Shared/RocketChat/API/Request.swift (2 hunks)
  • ios/fastlane/Fastfile (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • android/app/src/main/java/chat/rocket/reactnative/notification/ReplyBroadcast.java
🧰 Additional context used
🧬 Code graph analysis (5)
android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java (1)
  • NotificationHelper (10-40)
app/lib/notifications/videoConf/getInitialNotification.ts (2)
app/lib/store/auxStore.ts (1)
  • store (6-6)
app/actions/deepLinking.ts (1)
  • deepLinkingClickCallPush (26-31)
android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)
  • CustomPushNotification (45-659)
android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (1)
android/app/src/main/java/chat/rocket/reactnative/networking/SSLPinningTurboPackage.java (1)
  • SSLPinningTurboPackage (15-45)
ios/Shared/RocketChat/API/Request.swift (1)
app/lib/constants/userAgent.ts (1)
  • userAgent (3-5)
🪛 detekt (1.23.8)
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.kt

[warning] 62-62: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (19)
ios/Shared/RocketChat/API/Request.swift (2)

58-58: LGTM!

The User-Agent header is now set using the new computed property, which improves maintainability and provides proper identification of the client.


74-82: Format inconsistency between platforms and Bundle.main usage in notification service extension warrant verification.

The implementation constructs a custom User-Agent string. Two concerns should be addressed:

  1. Format inconsistency: The React Native app uses a browser-style User-Agent (Mozilla/5.0 (iPhone...)), while this iOS notification service extension uses a custom format (RC Mobile; ios ...). Confirm this difference is intentional and appropriate for the API requirements.

  2. Bundle.main in notification extension: Since this code is in the NotificationService extension, Bundle.main refers to the extension's bundle, not the main app's bundle. If version info must match the main app, consider using Bundle(identifier: "com.yourapp.mainapp") or sharing version data via app groups.

android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java (1)

130-130: LGTM! User-Agent header added for better request tracking.

The addition of the User-Agent header using NotificationHelper.getUserAgent() will help with server-side analytics and debugging by providing Android version, app version, and build number information.

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

29-39: LGTM! Well-documented User-Agent helper method.

The getUserAgent() method is implemented correctly with clear formatting and appropriate usage of Android Build and BuildConfig constants.

android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfModule.kt (1)

12-50: LGTM! SharedPreferences pattern correctly implemented.

The module correctly uses SharedPreferences to pass video conference actions from native to JS. The getPendingAction method properly clears the action after reading to ensure one-time consumption.

app/lib/native/NativeVideoConfAndroid.ts (1)

1-9: LGTM! Clean TurboModule interface definition.

The interface correctly defines the video conference module bridge with appropriate return types that match the native Kotlin implementation.

android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt (2)

20-38: LGTM! Action validation and mapping correctly implemented.

The broadcast receiver properly validates the action and extras before processing, and correctly maps native action constants to event strings for the JS layer.


47-55: No issues found with caller field key consistency.

The Android implementation correctly uses the same field names as iOS. Both platforms receive caller._id and caller.name from the server payload. Android's intermediate representation in intent extras ("callerId" and "callerName" strings) is necessary since Intent.putExtra() only accepts primitive types, but these are correctly reconstructed back to the caller._id and caller.name structure in VideoConfBroadcast and MainActivity, matching the iOS approach. The field naming is consistent throughout the notification flow.

android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfTurboPackage.kt (1)

12-37: LGTM! Standard TurboReactPackage implementation.

The package correctly implements TurboReactPackage with appropriate module info flags and follows the same pattern as other TurboModule packages in the codebase (e.g., SSLPinningTurboPackage).

app/lib/notifications/videoConf/getInitialNotification.ts (2)

13-28: LGTM! Android path correctly retrieves and dispatches pending video conference actions.

The Android implementation properly checks for the native module's existence, parses the pending action JSON, validates the notification type, and dispatches the action to the store.


30-59: No action required. The DECLINE_ACTION constant is consistently defined and used across the codebase. It is properly registered with expo-notifications.setNotificationCategoryAsync() in app/lib/notifications/push.ts and all references in the JS/TS layer (including the code under review) use the same string identifier. The native iOS implementation is abstracted by the expo-notifications library, so no separate native code verification is necessary.

android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (1)

23-46: LGTM! VideoConfTurboPackage correctly registered and architecture documented.

The changes properly integrate the VideoConfTurboPackage into the React Native package list and provide helpful documentation about the notification architecture. The removal of INotificationsApplication interface aligns with the migration to expo-notifications.

android/app/src/main/java/chat/rocket/reactnative/notification/NativeVideoConfSpec.kt (1)

9-23: LGTM!

Clean TurboModule spec definition. The abstract class correctly defines the API surface for video conference action handling, with proper module naming and React method annotations that will be inherited by the concrete implementation.

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

85-183: Well-structured incoming call notification implementation.

The notification handling correctly:

  • Extracts caller info with appropriate fallbacks
  • Generates unique notification IDs from rid + callerId
  • Uses proper intent flags for Android 12+ trampoline restrictions
  • Configures full-screen intent for call-style UX
  • Handles pre-O sound configuration separately

54-77: LGTM!

Notification channel creation properly handles Android O+ requirements with appropriate importance level, visibility, and audio attributes for incoming call notifications.

android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt (4)

1-6: LGTM!

Package declaration and imports are appropriate for the Firebase messaging service implementation.


8-13: LGTM!

Documentation clearly explains the service's purpose and responsibilities.


14-18: LGTM!

Idiomatic Kotlin implementation with proper FirebaseMessagingService inheritance.


23-27: Notification-only FCM payloads are not processed.

The code only handles FCM messages with a data payload (remoteMessage.data). Messages containing only a notification payload (without data) are ignored at line 24-26. Confirm this is intentional for the Rocket.Chat integration, or add a comment explaining why notification payloads are not processed.

Comment on lines +37 to +42
try {
val notification = CustomPushNotification(this, bundle)
notification.onReceived()
} catch (e: Exception) {
Log.e(TAG, "Error processing FCM message", e)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find CustomPushNotification class definition
find . -name "*.kt" -o -name "*.java" | xargs rg -l "class CustomPushNotification" 2>/dev/null

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 170


🏁 Script executed:

# Also search for the file directly
fd -e kt -e java "CustomPushNotification" .

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 170


🏁 Script executed:

# Check the RCFirebaseMessagingService file to understand context
cat -n android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt | head -60

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2013


🏁 Script executed:

# Search for WorkManager, JobScheduler usage in the notification package
rg -n "WorkManager|JobScheduler|HandlerThread|CoroutineScope|launch|async" android/app/src/main/java/chat/rocket/reactnative/notification/ --type kotlin --type java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1039


🏁 Script executed:

# Read the full CustomPushNotification.java file
wc -l android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 172


🏁 Script executed:

# Read CustomPushNotification.java in sections
head -100 android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3472


🏁 Script executed:

# Find and read the onReceived method
ast-grep --pattern $'onReceived() {
  $$$
}'

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 60


🏁 Script executed:

# Search for the onReceived method with context
rg -A 30 "fun onReceived|void onReceived" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1398


🏁 Script executed:

# Check E2ENotificationProcessor to see if it uses proper async patterns
cat -n android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java | head -80

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3639


🏁 Script executed:

# Read more of the onReceived method
rg -A 80 "public void onReceived" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3566


🏁 Script executed:

# Check if E2ENotificationProcessor.processAsync is actually used
rg -n "E2ENotificationProcessor\|processAsync" android/app/src/main/java/chat/rocket/reactnative/notification/

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 60


🏁 Script executed:

# Check for Thread spawning in the onReceived flow
rg -B 5 -A 10 "new Thread" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 944


🏁 Script executed:

# Look for any blocking/synchronous calls after notification.onReceived() in RCFirebaseMessagingService
rg -A 5 "notification.onReceived" android/app/src/main/java/chat/rocket/reactnative/notification/

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 781


🏁 Script executed:

# Find the handleNotification and processNotification methods
rg -A 50 "private void handleNotification" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2980


🏁 Script executed:

# Check what processNotification does
rg -A 30 "private void processNotification" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1647


🏁 Script executed:

# Look for blocking operations: network calls, MMKV access, Glide image loading with .get()
rg -n "\.get\(\)|\.execute\(\)|\.load\(|Glide|MMKV|notificationManager\.notify" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java | head -30

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 636


🏁 Script executed:

# Check for E2E encryption handling and if it's synchronous
rg -B 3 -A 15 "notificationType.*encrypted|E2E|decryption|decrypt" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java | head -60

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2531


🏁 Script executed:

# Find and read showNotification method
rg -B 5 -A 80 "private void showNotification\(" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java | head -120

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3795


🏁 Script executed:

# Check specifically around the Glide.get() call
rg -B 10 -A 5 "Glide.with.*\.get\(\)" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 60


🏁 Script executed:

# Check if there's any error handling or timeout for the Glide operation
rg -B 5 -A 10 "catch.*Exception.*Glide|try.*Glide" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 60


🏁 Script executed:

# Find buildNotification method
rg -B 5 -A 100 "private.*Notification\.Builder buildNotification" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java | head -150

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 4987


🏁 Script executed:

# Search for the exact context of Glide.get() to understand when it's called
rg -B 20 "\.get\(\)" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java | grep -A 20 "Glide"

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 338


🏁 Script executed:

# Find all calls to getAvatar
rg -n "getAvatar\(" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 259


🏁 Script executed:

# Get context around getAvatar() calls
rg -B 5 -A 5 "getAvatar\(" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1360


🏁 Script executed:

# Check the full getAvatar method including error handling
rg -A 40 "private Bitmap getAvatar\(" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1659


🏁 Script executed:

# Check if there's any timeout configuration in Glide usage
rg -B 10 "submit\(100, 100\)" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 502


🏁 Script executed:

# Check for any other blocking operations like notificationLoad
rg -B 5 -A 20 "private void notificationLoad" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java | head -40

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1017


🏁 Script executed:

# Verify the complete execution path for message-id-only notifications
rg -B 3 -A 10 "loadNotificationAndProcess" android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java | head -30

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1408


Consider the blocking Glide image loading operations within the FCM service lifecycle.

The implementation defers work to a background thread when React is not initialized (good), but when React is already available, buildNotification() calls Glide.get() synchronously to fetch avatar images—this blocks until the image is downloaded and decoded. Combined with the 5-second wait for React initialization, this can approach or exceed the 10-second FCM service timeout in edge cases.

Either add an explicit timeout to the Glide operation, move avatar loading to a separate WorkManager task, or ensure image loading fails fast with a fallback icon rather than blocking the FCM callback.

🤖 Prompt for AI Agents
In
android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt
around lines 37-42, the current code can block the FCM service because
buildNotification() calls Glide synchronously to fetch avatars; update the
implementation so image decoding cannot block the FCM callback: either (1)
perform the Glide load asynchronously with an explicit short timeout and fall
back to a default icon on timeout/error, (2) or offload full notification
building (including avatar fetching) to a background job via WorkManager and
immediately acknowledge the message in the FCM service, or (3) ensure Glide
requests use a synchronous-with-timeout helper that cancels after a few hundred
milliseconds and returns the fallback; finally make sure any path that fails to
obtain an avatar still posts a notification quickly using the fallback icon.

Comment on lines +45 to +50
override fun onNewToken(token: String) {
Log.d(TAG, "FCM token refreshed")
// Token handling is done by expo-notifications JS layer
// which uses getDevicePushTokenAsync()
super.onNewToken(token)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for getDevicePushTokenAsync usage in JS/TS files
echo "=== Searching for getDevicePushTokenAsync usage ==="
rg -l 'getDevicePushTokenAsync' --type=ts --type=tsx --type=js 2>/dev/null || echo "No files found"

# Search for token registration patterns
echo -e "\n=== Searching for token-related updates and registration ==="
rg -n 'registerForPushNotificationsAsync|registerDeviceToken|updateToken' --type=ts --type=tsx --type=js --type=java --type=kotlin -A 3 2>/dev/null | head -50

# Check for native event emission patterns
echo -e "\n=== Searching for event emission to JS ==="
rg -n 'sendEvent|DeviceEventEmitter|NativeModules.*emit' --type=java --type=kotlin android/ 2>/dev/null | head -30

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 250


🏁 Script executed:

#!/bin/bash
# Find JS layer integration files related to push notifications
echo "=== Searching JS push notification integration files ==="
fd -e ts -e tsx -e js | xargs grep -l 'expo-notifications\|push.*notification' 2>/dev/null | head -20

# Look for initialization and token handling in JS
echo -e "\n=== Searching for push initialization and token handling ==="
rg -n 'expo.*notification|getPushToken|device.*token' --type=ts --type=tsx --type=js -B 2 -A 2 2>/dev/null | head -60

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 749


🏁 Script executed:

#!/bin/bash
# Check the RCFirebaseMessagingService for complete implementation
echo "=== Viewing RCFirebaseMessagingService.kt complete context ==="
if [ -f "android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt" ]; then
  wc -l "android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt"
  echo "---"
  cat -n "android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt"
else
  find . -name "RCFirebaseMessagingService.kt" 2>/dev/null
fi

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2176


🏁 Script executed:

#!/bin/bash
# Search for how the token reaches Rocket.Chat server
echo "=== Searching for Rocket.Chat server token registration ==="
rg -n 'registerPushToken|updatePushToken|push.*register' --type=ts --type=tsx --type=js -B 2 -A 5 2>/dev/null | head -80

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 137


🏁 Script executed:

#!/bin/bash
# Read the main push notification handler
echo "=== Content of app/lib/notifications/push.ts ==="
wc -l app/lib/notifications/push.ts
echo "---"
cat -n app/lib/notifications/push.ts

echo -e "\n=== Check if there's initialization in app/index.tsx ==="
grep -n 'push\|notification\|token' app/index.tsx | head -30

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 8181


🏁 Script executed:

#!/bin/bash
# Search more specifically for where tokens are obtained
echo "=== Search for getPushToken or similar ==="
rg -n 'getPushToken|pushToken|getToken|notify.*token' --type=ts --type=tsx --type=js -B 2 -A 5 2>/dev/null | head -100

# Look for any native module calls from JS
echo -e "\n=== Search for NativeModules or native bridge calls ==="
rg -n 'NativeModules|native.*push|native.*notification|bridge' --type=ts --type=tsx --type=js -B 1 -A 2 2>/dev/null | head -60

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 177


🏁 Script executed:

#!/bin/bash
# Look specifically at troubleshooting notification saga for insights
echo "=== Content of troubleshootingNotification.ts ==="
wc -l app/sagas/troubleshootingNotification.ts
if [ $? -eq 0 ]; then
  cat -n app/sagas/troubleshootingNotification.ts
fi

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2266


🏁 Script executed:

#!/bin/bash
# Search for where server registration happens
echo "=== Search for server push registration ==="
rg -n 'registerPush|pushToken|deviceToken' --type=ts --type=tsx --type=js -i -B 3 -A 3 2>/dev/null | head -120

# Check package.json for expo-notifications version and setup
echo -e "\n=== Check expo-notifications setup ==="
if [ -f "package.json" ]; then
  grep -A 5 -B 5 'expo-notifications' package.json
fi

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 525


🏁 Script executed:

#!/bin/bash
# Find where deviceToken is used
echo "=== Searching for deviceToken usage ==="
rg -n 'deviceToken' --type=ts --type=tsx --type=js -B 2 -A 5 2>/dev/null

echo -e "\n=== Search for user settings/profile update endpoints ==="
rg -n 'user.*push|push.*device|registerDevice|addPushToken' --type=ts --type=tsx --type=js -i -B 2 -A 3 2>/dev/null | head -80

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 176


🏁 Script executed:

#!/bin/bash
# Look for API calls that might send the token
echo "=== Search for notification/push related API calls ==="
rg -n 'notifications/push|notification.*device|api.*push' --type=ts --type=tsx --type=js -B 1 -A 2 2>/dev/null

# Check if there's a method to set push token in user settings
echo -e "\n=== Search in methods or services for push token registration ==="
find app/lib/methods app/lib/services -name "*.ts" -o -name "*.tsx" -o -name "*.js" 2>/dev/null | xargs grep -l 'push\|notification\|token' 2>/dev/null

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1316


🏁 Script executed:

#!/bin/bash
# Check the notifications initialization flow
echo "=== Look for the full initializePushNotifications implementation ==="
rg -n 'export.*initializePushNotifications|function initializePushNotifications' --type=ts --type=tsx --type=js -A 20 2>/dev/null

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 146


🏁 Script executed:

#!/bin/bash
# Find the notifications module entry point
echo "=== Search for notifications folder structure ==="
find app/lib/notifications -type f -name "*.ts" -o -name "*.tsx" -o -name "*.js" 2>/dev/null | sort

echo -e "\n=== Check notifications/index.ts or similar ==="
if [ -f "app/lib/notifications/index.ts" ]; then
  cat -n app/lib/notifications/index.ts
else
  echo "index.ts not found in notifications folder"
fi

echo -e "\n=== List all files in notifications ==="
ls -la app/lib/notifications/ 2>/dev/null || echo "Directory not found"

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3684


🏁 Script executed:

#!/bin/bash
# Search more carefully for where deviceToken might be exported/imported
echo "=== Search for 'from.*notifications' or 'import.*deviceToken' ==="
rg -n "from.*['\"].*notifications" --type=ts --type=tsx --type=js 2>/dev/null | head -30

echo -e "\n=== Search for any file that might handle server registration ==="
rg -l 'users\.info|users\.update|user\.setPreferences|notification' app/lib/services/ 2>/dev/null

echo -e "\n=== Check restApi.ts for push-related endpoints ==="
if [ -f "app/lib/services/restApi.ts" ]; then
  grep -n 'push\|notification\|token' app/lib/services/restApi.ts 2>/dev/null | head -30
fi

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1075


🏁 Script executed:

#!/bin/bash
# Look at package.json to understand how push is initialized
echo "=== Check if there's any initialization in package.json scripts ==="
cat package.json | grep -A 5 -B 5 'start\|native\|expo'

echo -e "\n=== Search for app initialization and push setup ==="
rg -n 'pushNotificationConfigure|push.*notify|expo.*notify' --type=ts --type=tsx --type=js 2>/dev/null

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 7578


🏁 Script executed:

#!/bin/bash
# Search for the function that calls the push token API
echo "=== Find function names in restApi.ts that use push.token ==="
grep -n "export.*=" app/lib/services/restApi.ts | grep -A 2 -B 2 "push\|notification" | head -40

echo -e "\n=== Search for calls to push token registration (look for patterns like sendPushToken, registerPushToken) ==="
rg -n 'push\.token|sendPushToken|registerPushToken' --type=ts --type=tsx --type=js -B 3 -A 3 2>/dev/null | head -80

echo -e "\n=== Get exact function names around line 991-1001 in restApi.ts ==="
sed -n '985,1015p' app/lib/services/restApi.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1675


🏁 Script executed:

#!/bin/bash
# Search for where these push functions are called
echo "=== Search for where push token functions are invoked ==="
rg -n 'savePushToken|deletePushToken|getPushToken' --type=ts --type=tsx --type=js 2>/dev/null

echo -e "\n=== Look for import of restApi functions and their usage ==="
rg -n 'from.*restApi|import.*savePush|import.*deletePush' --type=ts --type=tsx --type=js 2>/dev/null | head -20

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 197


🏁 Script executed:

#!/bin/bash
# Check sagas for push token registration
echo "=== Search app/sagas for push token handling ==="
find app/sagas -name "*.ts" -o -name "*.js" 2>/dev/null | xargs grep -l 'push\|token' 2>/dev/null

echo -e "\n=== Check app/actions for push token actions ==="
find app/actions -name "*.ts" -o -name "*.js" 2>/dev/null | xargs grep -l 'push\|token' 2>/dev/null

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 403


🏁 Script executed:

#!/bin/bash
# Search all saga files for registerPushToken usage
echo "=== Search for registerPushToken in saga files ==="
grep -rn 'registerPushToken' app/sagas/ 2>/dev/null

echo -e "\n=== Search for registerPushToken in entire codebase ==="
rg -n 'registerPushToken' --type=ts --type=tsx --type=js 2>/dev/null

echo -e "\n=== Check init.js for push token registration ==="
cat -n app/sagas/init.js | head -100

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 4119


🏁 Script executed:

#!/bin/bash
# Check login.js for push token handling
echo "=== Check app/sagas/login.js ==="
wc -l app/sagas/login.js
echo "---"
grep -n 'push\|token\|notification\|registerPush' app/sagas/login.js 2>/dev/null

echo -e "\n=== Check selectServer.ts ==="
if [ -f "app/sagas/selectServer.ts" ]; then
  grep -n 'push\|token\|registerPush' app/sagas/selectServer.ts 2>/dev/null
fi

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1021


🏁 Script executed:

#!/bin/bash
# Look for all imports of registerPushToken
echo "=== Search for imports and usage of registerPushToken ==="
rg -n "registerPushToken|removePushToken" --type=ts --type=tsx --type=js 2>/dev/null

# Also check if there's middleware or interceptors
echo -e "\n=== Check middleware or effects that might auto-register ==="
rg -n 'addPushTokenListener|tokenData' --type=ts --type=tsx --type=js -B 2 -A 5 2>/dev/null | head -60

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 197


🏁 Script executed:

#!/bin/bash
# Look at the complete login.js to understand the token refresh flow
echo "=== View registerPushTokenFork function in context ==="
sed -n '185,250p' app/sagas/login.js

echo -e "\n=== Check if there's any listener or effect that calls registerPushToken on token change ==="
rg -n 'addPushTokenListener|tokenListener|token.*listener' --type=ts --type=tsx --type=js -B 3 -A 10 2>/dev/null

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1688


🏁 Script executed:

#!/bin/bash
# Verify the complete push.ts token listener implementation
echo "=== Verify push.ts addPushTokenListener and what happens after ==="
sed -n '185,215p' app/lib/notifications/push.ts

echo -e "\n=== Search for any saga or action triggered on token update ==="
rg -n 'pushToken|tokenRefresh|token.*update' --type=ts --type=tsx --type=js app/sagas/ 2>/dev/null

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1017


Token refresh from FCM is not synced to the Rocket.Chat server.

While expo-notifications correctly detects FCM token refreshes via addPushTokenListener() (line 190 in app/lib/notifications/push.ts), the updated token is never re-registered with the server. The registerPushToken() function in app/lib/services/restApi.ts is only called during login (app/sagas/login.js:240), not when the token is refreshed.

When FCM refreshes the token:

  1. expo-notifications updates deviceToken in the JS layer
  2. But registerPushToken() is never invoked to send the new token to the server
  3. The server retains the stale token, causing push notifications to fail after token refresh

Either the Kotlin onNewToken() method needs to explicitly notify the JS layer to re-register, or the JS layer needs to call registerPushToken() when the token listener fires.

🤖 Prompt for AI Agents
In
android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt
around lines 45-50, onNewToken currently only logs and calls super but does not
inform the JS layer so the server never receives refreshed tokens; modify
onNewToken to send the new token to the React Native JS layer (for example emit
an event via the ReactContext / RCTDeviceEventEmitter or write the token to a
shared place the JS bridge reads) so the JS side can call registerPushToken(),
and ensure you still call super.onNewToken(token) after emitting; additionally
add/verify a corresponding JS listener that calls registerPushToken() when the
token event is received.

Comment on lines +154 to +160
// Get icons
val packageName = context.packageName
val smallIconResId = context.resources.getIdentifier("ic_notification", "drawable", packageName)

// Build notification
val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply {
setSmallIcon(smallIconResId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle missing icon resource gracefully.

getIdentifier returns 0 if the resource is not found. Using 0 as the small icon resource ID can cause the notification to fail silently or display incorrectly on some Android versions.

         // Get icons
         val packageName = context.packageName
         val smallIconResId = context.resources.getIdentifier("ic_notification", "drawable", packageName)
+        val validIconResId = if (smallIconResId != 0) smallIconResId else android.R.drawable.ic_dialog_info
 
         // Build notification
         val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply {
-            setSmallIcon(smallIconResId)
+            setSmallIcon(validIconResId)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Get icons
val packageName = context.packageName
val smallIconResId = context.resources.getIdentifier("ic_notification", "drawable", packageName)
// Build notification
val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply {
setSmallIcon(smallIconResId)
// Get icons
val packageName = context.packageName
val smallIconResId = context.resources.getIdentifier("ic_notification", "drawable", packageName)
val validIconResId = if (smallIconResId != 0) smallIconResId else android.R.drawable.ic_dialog_info
// Build notification
val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply {
setSmallIcon(validIconResId)
🤖 Prompt for AI Agents
In
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt
around lines 154-160, getIdentifier may return 0 for "ic_notification" which
will break setSmallIcon; check if smallIconResId == 0 and handle it by falling
back to a valid icon resource (for example the app icon from
context.applicationInfo.icon or a safe android.R.drawable.* fallback), then pass
that non-zero id to setSmallIcon; also consider logging a warning when the
custom drawable is missing so it can be fixed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
app/sagas/deepLinking.js (2)

226-239: Guard against missing caller._id before notifying video-conference events

Using optional chaining avoids a hard crash when params.caller is absent, but if that happens you’ll now send requests to notifyUser with a path like 'undefined/video-conference' and uid in the payload as undefined. That’s likely not a meaningful or valid request.

Consider bailing out (and logging) when uid is missing so you don’t emit malformed events:

-			const uid = params.caller?._id;
-			const { rid, callId, event } = params;
+			const uid = params.caller?._id;
+			if (!uid) {
+				log('Missing caller uid in OPEN_VIDEO_CONF params', params);
+				return;
+			}
+			const { rid, callId, event } = params;

249-252: Early return on missing host prevents crashes but may leave UX in a no-op state

The new !host guard avoids host.slice throwing, which is good, but it now makes handleClickCallPush a silent no-op when host is missing. In handleOpen, the analogous case calls fallbackNavigation() so the app at least initializes instead of “doing nothing”.

You may want to mirror that behavior here:

-	if (!host) {
-		return;
-	}
+	if (!host) {
+		yield fallbackNavigation();
+		return;
+	}

This keeps the app from feeling unresponsive when a malformed/partial push payload arrives.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6620a1b and 7f98e86.

📒 Files selected for processing (1)
  • app/sagas/deepLinking.js (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/sagas/deepLinking.js (2)
app/sagas/login.js (1)
  • params (423-423)
app/sagas/rooms.js (1)
  • params (145-145)

@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.68.0.107811

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNSU5-XHlfn0jLRCr9n9Twy6EZIMaa0dyPAZ-72bYqXKvcqnxC2RQtwSdN_Td4cFqoC6mPP1DqqpWhGj8VRH

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants