Skip to content

Conversation

@diegolmello
Copy link
Member

@diegolmello diegolmello commented Dec 16, 2025

Proposed changes

This PR enhances push notification avatar handling across iOS and Android platforms. The changes improve avatar fetching, display, and error handling for both regular messages and video conference notifications. Key improvements include:

  • iOS: Enhanced avatar URL handling in NotificationService with proper URL encoding, timeout management, and improved avatar fetching for both direct messages and group/channel notifications
  • Android: Fixed avatar display in push notifications with proper URI generation, timeout handling, and fallback mechanisms
  • Video Conference: Fixed avatar display for video conference call notifications on iOS
  • Error Management: Improved error handling and timeout management for notification processing
  • Code Quality: Refactored notification handling code for better maintainability and reliability

Issue(s)

https://rocketchat.atlassian.net/browse/CORE-1581

How to test or reproduce

  1. iOS/Android Testing:

    • Send a push notification to an iOS device
    • Verify that avatars display correctly for:
      • Direct messages (sender avatar)
      • Group/channel messages (room avatar)
      • Video conference calls (caller avatar)
      • Message ID Only
    • Test with notifications received when app is in background or killed state
  2. Edge Cases:

    • Test with E2E encrypted messages
    • Test with notifications that have missing or invalid avatar data
    • Test with slow network conditions to verify timeout handling

Screenshots

Captura de Tela 2025-12-24 à(s) 10 57 58 Captura de Tela 2025-12-24 à(s) 10 57 01

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

This PR focuses on improving the reliability and user experience of push notification avatars. The changes include:

  • URL Encoding: Proper encoding of credentials and usernames to prevent malformed URLs
  • Timeout Management: Appropriate timeout values for notification service extensions (3 seconds for iOS, 3 seconds for Android) to ensure notifications are delivered promptly
  • Avatar Path Logic: Correct avatar path selection based on room type (direct messages use sender avatar, groups/channels use room avatar)
  • Error Handling: Graceful fallback to default icons when avatar fetching fails
  • Code Refactoring: Improved code organization and maintainability in notification handling classes

The implementation ensures that avatars are fetched efficiently without blocking notification delivery, with proper fallback mechanisms in place for edge cases.

diegolmello and others added 16 commits December 10, 2025 10:48
…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.
…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.
- Added a 3-second timeout for fetching avatars in CustomPushNotification to prevent blocking the FCM service.
- Enhanced error handling in ReplyBroadcast to return early if the bundle is null.
- Updated push notification configuration to re-register the push token with the server if the user is authenticated.
- Modified appStateMiddleware to handle potential errors when clearing notifications.
- Improved notification delivery logic in iOS NotificationService to handle cases with no messageId or notification content.
- Added functionality to encode and store the full payload from the server in the userInfo dictionary under the key "ejson" for improved navigation.
- Added a method to fetch user avatars from the server and attach them to notifications in NotificationService.
- Introduced a userAgent property in Bundle for consistent User-Agent string usage across API requests.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 16, 2025

Walkthrough

Standardizes notification title/message and avatar handling across Android, iOS, and JS; adds an Android VideoConf broadcast receiver; enhances iOS NotificationService with avatar fetching, intent donation and videoconf handling; hardens reply background tasks; updates entitlements, Info.plists, Xcode/CocoaPods references, and request User-Agent handling. (50 words)

Changes

Cohort / File(s) Change Summary
Android: Push notification core
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
Made mBundle volatile; compute and log notificationTitle from ejson.type (uses room/sender/original title per type); use it for content title and MessagingStyle; tightened extractMessage bounds; switched avatar fetches to NotificationHelper.
Android: Avatar URI & EJSON model
android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
Added name field and getCallerAvatarUri(); centralized avatar URI construction via buildAvatarUri with URLEncoder, explicit DM vs room paths, and improved error messaging.
Android: Avatar fetch helper
android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java
Added fetchAvatarBitmap(Context, String, Bitmap?) using Glide with 3s timeout, rounded corners, robust error/interrupt handling, and fallback bitmap.
Android: Video conference notifications & receiver
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt, android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt
VideoConfNotification: fetches caller avatar and sets large icon when available. VideoConfBroadcast: new BroadcastReceiver mapping accept/decline to JS-stored payloads, cancels notifications, and launches MainActivity with stored payload.
Android: Encryption robustness
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
Added column-index validation, flexible privateKey parsing (JSON or JSON-in-string), defensive base64/RSA try/catch, session key parsing and algorithm detection, and improved logging.
iOS: NotificationService — videoconf, avatars, intents
ios/NotificationService/NotificationService.swift, ios/NotificationService/Info.plist, ios/NotificationService/NotificationService.entitlements
Reworked didReceive flow; added finalContent, processVideoConf, avatar fetch helpers, fetchAvatarData..., activateCommunicationIntent to donate INSendMessageIntent; added NSUserActivityTypes/IntentsSupported and communication entitlement.
iOS: Reply handling & background task safety
ios/ReplyNotification.swift
Added user-visible failure notifications for invalid replies, background task with expiration handler, wrapped completion to ensure task cleanup, and replyFailure identifier.
iOS: App entitlements & Info.plist
ios/RocketChatRN/RocketChatRN.entitlements, ios/RocketChatRN/Info.plist
Added com.apple.developer.usernotifications.communication entitlement and INSendMessageIntent to NSUserActivityTypes.
iOS: User-Agent and Request header
ios/Shared/Extensions/Bundle+Extensions.swift, ios/Shared/RocketChat/API/Request.swift
Added Bundle.userAgent computed property and attached User-Agent header to outgoing HTTP requests.
iOS: Models — caller username
ios/Shared/Models/Payload.swift
Added optional username to Caller model to support avatar resolution.
TypeScript: push handling & validation
app/lib/notifications/index.ts
Wrap EJSON parsing for videoconf in try/catch and skip dispatch on parse failure; early validate rid/type/host for regular notifications; refined roomName resolution for DIRECT/OMNICHANNEL; changed init/remove functions to async.
iOS: Xcode / CocoaPods project updates
ios/RocketChatRN.xcodeproj/project.pbxproj
Reorganized Pod/xcconfig/framework references, added new Pod target xcconfigs/frameworks, and updated build phases/scripts and baseConfigurationReferences.

Sequence Diagram(s)

%%{init: {"theme":"default","themeVariables":{"actorBorder":"#2b6cb0","noteBorder":"#718096","noteBkg":"#f7fafc"}}}%%
sequenceDiagram
    autonumber
    participant OS as Android OS
    participant Broadcast as VideoConfBroadcast
    participant Native as VideoConfModule (native storage)
    participant App as MainActivity
    participant JS as JavaScript layer

    Note over OS,Broadcast: User taps call action (accept/decline)
    OS->>Broadcast: deliver(action, extras)
    activate Broadcast
    Broadcast->>Broadcast: validate action & extract extras
    alt valid action
        Broadcast->>Native: storePendingAction(payload JSON)
        Broadcast->>OS: cancel notification (if id)
        Broadcast->>App: startActivity(intent with event + extras)
    else invalid
        Broadcast->>Broadcast: log warning and ignore
    end
    deactivate Broadcast

    App->>JS: deliver intent extras on launch/resume
    JS->>Native: readPendingAction()
    JS->>JS: handle videoconf event (accept/decline)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • OtavioStasiak

Poem

🐰 I hop through payloads with a curious twitch,

I fetch the avatar, stitch title and niche.
I tuck a tiny JSON note and nudge the app awake,
JS peeks my message — accept, decline, or take.
A rabbit cheers: notifications stitched with a flick!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.09% 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
Title check ✅ Passed The title 'feat: Avatar on push notifications' clearly summarizes the main change: adding avatar support to push notifications across iOS and Android platforms.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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 feat.push-avatar-ios

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.

@diegolmello diegolmello temporarily deployed to experimental_ios_build December 16, 2025 13:02 — with GitHub Actions Inactive
@diegolmello diegolmello temporarily deployed to experimental_android_build December 16, 2025 13:02 — 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: 6

🧹 Nitpick comments (11)
ios/Podfile (1)

37-40: LGTM! Optional: Add trailing comma for better diffs.

The removal of react-native-notifications from static frameworks correctly aligns with the notification system migration.

Consider adding a trailing comma after 'simdjson' per Ruby convention:

 $static_framework = [ 
   'WatermelonDB',
-  'simdjson'
+  'simdjson',
 ]

This makes future diffs cleaner when adding items to the array. Based on static analysis hints (RuboCop).

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

11-11: Unused import detected.

The Configuration import appears to be unused 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/VideoConfModule.kt (2)

36-50: Potential race condition in read-then-clear pattern.

The getPendingAction method reads and then clears the value in separate operations. If called concurrently (unlikely but possible), both calls could retrieve the same action before either clears it.

For a single-consumer scenario this is likely acceptable, but consider using commit() instead of apply() for synchronous clearing if atomicity becomes important.


56-65: Log swallowed exception for debuggability.

Per static analysis, the caught exception is silently swallowed. While failures in clearPendingAction may be acceptable, logging helps with debugging.

     @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 but don't propagate - cleanup failures are non-critical
+            android.util.Log.w("VideoConfModule", "Failed to clear pending action", e)
         }
     }
ios/NotificationService/NotificationService.swift (1)

48-51: Sanitize username for filesystem safety.

The username is used directly in the filename without sanitization. Usernames with characters like / or .. could potentially cause issues or path traversal in the temp directory.

             // Save to temp file (UNNotificationAttachment requires file URL)
             let tempDir = FileManager.default.temporaryDirectory
-            let fileName = "\(username)_avatar.png"
+            let sanitizedUsername = username.replacingOccurrences(of: "[^A-Za-z0-9_-]", with: "_", options: .regularExpression)
+            let fileName = "\(sanitizedUsername)_avatar.png"
             let fileURL = tempDir.appendingPathComponent(fileName)
android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt (1)

81-81: Minor: Inconsistent indentation.

Line 81 uses a tab character while the rest of the file uses spaces. This is a minor formatting inconsistency.

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

103-105: Extract duplicate notification ID generation logic.

The notification ID generation logic is duplicated in showIncomingCall and cancelCall. Consider extracting to a private helper function for maintainability.

+    /**
+     * Generates a unique notification ID from room ID and caller ID.
+     */
+    private fun generateNotificationId(rid: String?, callerId: String): Int {
+        val notificationIdStr = ((rid ?: "") + callerId).replace(Regex("[^A-Za-z0-9]"), "")
+        return notificationIdStr.hashCode()
+    }
+
     fun showIncomingCall(bundle: Bundle, ejson: Ejson) {
         val rid = ejson.rid
         // ...
-        val notificationIdStr = (rid + callerId).replace(Regex("[^A-Za-z0-9]"), "")
-        val notificationId = notificationIdStr.hashCode()
+        val notificationId = generateNotificationId(rid, callerId)

Also applies to: 203-205

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

72-75: Consider if both awaits are necessary.

removeAllNotifications returns a promise that should be awaited, but setNotificationsBadgeCount may not require awaiting if it's fire-and-forget. If they're independent operations, they could be parallelized with Promise.all.

 export const removeNotificationsAndBadge = async (): Promise<void> => {
-	await removeAllNotifications();
-	await setNotificationsBadgeCount();
+	await Promise.all([removeAllNotifications(), setNotificationsBadgeCount()]);
 };
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (3)

115-149: Polling loop is functional but consider alternative patterns.

The thread-based polling with 100ms sleep intervals works but is not ideal. Consider using a CountDownLatch or CompletableFuture for cleaner async coordination. However, given this is notification handling code that needs to be reliable, the current approach is acceptable.


519-522: Potential StringIndexOutOfBoundsException.

If pos is -1 (no ":" found), start becomes 0, and message.substring(0) is safe. However, if the message is empty after the ":", this could produce unexpected results. Consider adding a bounds check.

     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;
     }

362-373: Channel creation is correct but could include more configuration.

The channel is created with IMPORTANCE_HIGH which is appropriate. Consider adding description, vibration pattern, or sound configuration similar to the VideoConfNotification channel for consistency.

📜 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 7215cda and 8d10b22.

⛔ 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 (44)
  • 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 (15 hunks)
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java (2 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/index.tsx (1 hunks)
  • app/lib/native/NativeVideoConfAndroid.ts (1 hunks)
  • app/lib/notifications/index.ts (2 hunks)
  • app/lib/notifications/push.ts (2 hunks)
  • app/lib/notifications/videoConf/backgroundNotificationHandler.ts (0 hunks)
  • app/lib/notifications/videoConf/getInitialNotification.ts (1 hunks)
  • app/lib/store/appStateMiddleware.ts (1 hunks)
  • app/sagas/deepLinking.js (2 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 (0 hunks)
  • ios/NotificationService/NotificationService.swift (3 hunks)
  • ios/Podfile (1 hunks)
  • ios/ReplyNotification.swift (1 hunks)
  • ios/RocketChatRN-Bridging-Header.h (0 hunks)
  • ios/RocketChatRN.xcodeproj/project.pbxproj (36 hunks)
  • ios/Shared/Extensions/Bundle+Extensions.swift (1 hunks)
  • ios/Shared/Models/NotificationType.swift (1 hunks)
  • ios/Shared/Models/Payload.swift (1 hunks)
  • ios/Shared/RocketChat/API/Request.swift (1 hunks)
  • ios/fastlane/Fastfile (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 (7)
  • android/app/build.gradle
  • app/lib/notifications/videoConf/backgroundNotificationHandler.ts
  • ios/RocketChatRN-Bridging-Header.h
  • ios/AppDelegate.swift
  • patches/@react-native-firebase+messaging+21.12.2.patch
  • patches/@notifee+react-native+7.8.2.patch
  • patches/react-native-notifications+5.1.0.patch
🧰 Additional context used
🧬 Code graph analysis (8)
app/lib/store/appStateMiddleware.ts (1)
app/lib/notifications/index.ts (1)
  • removeNotificationsAndBadge (72-75)
app/lib/notifications/push.ts (3)
app/definitions/INotification.ts (1)
  • INotification (1-16)
app/lib/notifications/index.ts (1)
  • onNotification (18-66)
app/lib/services/restApi.ts (1)
  • registerPushToken (989-1007)
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/CustomPushNotification.java (2)
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt (2)
  • createNotificationChannel (54-77)
  • showIncomingCall (85-183)
android/app/src/main/java/chat/rocket/reactnative/notification/LoadNotification.java (1)
  • Notification (24-57)
app/index.tsx (1)
app/lib/notifications/videoConf/getInitialNotification.ts (1)
  • getInitialNotification (13-62)
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)
ios/Shared/Extensions/Bundle+Extensions.swift (1)
app/lib/constants/userAgent.ts (1)
  • userAgent (3-5)
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)

🪛 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 (59)
ios/RocketChatRN.xcodeproj/project.pbxproj (1)

1-3439: Auto-generated CocoaPods project file changes look consistent.

This file is auto-generated by CocoaPods when running pod install. The changes correctly update:

  • Framework references for the NotificationService, RocketChatRN, and Rocket.Chat pod targets
  • xcconfig file references for debug/release configurations
  • Shell script build phases for embedding frameworks and copying resources
  • Resource bundles including new Expo-related privacy bundles (ExpoNotifications_privacy.bundle, etc.)

The structure is consistent across all targets and configurations.

ios/fastlane/Fastfile (1)

125-125: Verify if arm64 architecture constraint is intentional for E2E testing.

The build_experimental_simulator lane restricts builds to ARCHS=arm64, which targets Apple Silicon architectures only. This could prevent E2E test builds on Intel-based Mac systems. However, the CI workflow already runs on macos-15 (Apple Silicon), so this primarily affects local development workflows on Intel Macs.

Confirm whether this architectural constraint is intentional as part of the project's migration to Apple Silicon, or clarify how this relates to the PR's objective of adding avatar images to push notifications.

jest.setup.js (1)

154-156: Appropriate minimal mock for expo-device.

Setting isDevice: true provides a reasonable default for tests simulating physical device behavior.

ios/ReplyNotification.swift (5)

12-18: LGTM - Delegation pattern is well-structured.

The weak reference for originalDelegate correctly avoids retain cycles, and the static shared instance ensures the delegate stays alive for the lifetime of the app.


20-29: LGTM - Proper delegate chain setup.

The configure method correctly captures the original delegate before setting itself, ensuring the delegation chain is preserved.


33-50: LGTM - Clean action routing.

The method correctly intercepts REPLY_ACTION while ensuring all other notification responses are forwarded to expo-notifications.


52-63: LGTM!


65-75: LGTM!

The #available check appropriately guards the call to the original delegate.

react-native.config.js (1)

1-3: LGTM! Configuration correctly updated for the migration.

Removing the Firebase messaging platform override is appropriate since @react-native-firebase/messaging has been removed from the project dependencies as part of the expo-notifications migration.

package.json (1)

64-64: Update expo-notifications to 0.32.15.

expo-notifications has been updated to 0.32.15, but the package.json specifies 0.32.11. expo-device at ^8.0.10 is current, so no update needed there. While no known critical security vulnerabilities were found in these specific versions, staying current with patch releases ensures you have the latest fixes.

app/views/PushTroubleshootView/components/DeviceNotificationSettings.tsx (1)

21-21: Linking.openSettings() is the correct approach for Android notification settings.

The migration from notifee.openNotificationSettings() to Linking.openSettings() is appropriate. On Android, this standard React Native API correctly opens the app's notification settings. The platform-specific implementation is proper, with distinct handling for iOS (app-settings: deep link) and Android (Linking.openSettings()).

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

11-14: LGTM!

The Caller struct is properly defined with Codable conformance for JSON serialization, and optional fields provide appropriate flexibility for video conference notifications.


29-33: LGTM!

The video conference fields are properly structured with optional types, maintaining backward compatibility while supporting the new video conference notification feature.


21-21: Verify messageId usage before making it optional.

Changing messageId from String to String? requires confirming that all consumers handle the optional safely. Review the codebase for any force unwrapping (messageId!) or direct property access that assumes a non-nil value, as these patterns would break with the optional type change.

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

13-19: LGTM!

The migration to Notifications.setBadgeCountAsync is correctly implemented with proper async/await and error handling.


21-27: LGTM!

The migration to Notifications.dismissAllNotificationsAsync is correctly implemented with proper async/await and error handling.


34-80: LGTM!

The notification transformation logic is well-structured with proper platform-specific handling for Android and iOS, appropriate fallbacks, and comprehensive field mapping to the INotification interface.


85-126: LGTM!

The notification categories setup is well-implemented with proper action configurations, localized strings, and appropriate foreground behavior settings for each action type.


131-159: LGTM!

The push notification registration flow is correctly implemented with proper permission handling, device checks, and error handling.


161-223: LGTM!

The notification configuration is well-structured with proper setup of handlers, listeners, and initial notification retrieval. The platform-specific handling for iOS background state is preserved appropriately.

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

129-129: LGTM!

The User-Agent header addition aligns with the cross-platform consistency effort and follows the same pattern used in iOS.

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

14-14: LGTM!

The new videoconf notification type case is properly added, following the existing enum pattern and maintaining Codable conformance.

app/lib/store/appStateMiddleware.ts (1)

20-20: LGTM!

Adding error handling for the async removeNotificationsAndBadge() call is appropriate and prevents unhandled promise rejections. Using console.warn is suitable for this non-critical failure scenario.

app/sagas/troubleshootingNotification.ts (1)

3-3: LGTM!

The migration from Notifee to Expo Notifications for permission checking is correctly implemented and aligns with the broader notification system migration.

Also applies to: 22-23

app/index.tsx (1)

141-144: LGTM!

The video conference initial notification handling is correctly integrated into the app initialization flow with an appropriate early return to prevent duplicate navigation or deep linking.

ios/Shared/RocketChat/API/Request.swift (1)

58-58: Bundle.userAgent extension is properly defined and correctly implemented.

The Bundle.userAgent static var at ios/Shared/Extensions/Bundle+Extensions.swift:17 returns a well-formatted User-Agent string following the pattern "RC Mobile; ios {version}; v{appVersion} ({build})" using ProcessInfo and Bundle metadata.

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

44-51: LGTM! Robust data extraction with proper fallback.

The new extraction pattern with fallback logic and null checks prevents downstream errors and handles missing data gracefully.


87-87: LGTM! Consistent User-Agent header addition.

The User-Agent header is correctly set using the standardized helper method.

app/sagas/deepLinking.js (2)

226-226: LGTM! Safe property access.

Optional chaining prevents runtime errors when caller is undefined or null in video conference notifications.


249-252: LGTM! Proper guard clause.

The early return when host is falsy prevents unnecessary processing and potential errors in subsequent host normalization logic.

ios/Shared/Extensions/Bundle+Extensions.swift (1)

15-23: LGTM! Consistent User-Agent implementation.

The User-Agent format matches the Android implementation in NotificationHelper.java, ensuring consistency across platforms for native API requests. The fallback to "unknown" handles missing bundle info gracefully.

index.js (1)

25-26: LGTM! Clear documentation of architectural change.

The comment clearly documents that video conference notifications are now handled natively in the Android FCM service stack, replacing the previous JavaScript-based approach.

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

3-3: LGTM! Well-documented User-Agent helper.

The implementation is clean and consistent with the iOS counterpart. The format string clearly documents the expected output, and the method uses appropriate Android system APIs.

Also applies to: 28-39

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

78-85: LGTM! Secure FCM service configuration.

The service is properly configured with android:exported="false" for security, and the priority of 100 ensures this custom handler takes precedence for processing FCM messages.


95-103: LGTM! Secure broadcast receiver configuration.

The receiver is properly configured with android:exported="false" and specific intent filters for video conference actions, following Android security best practices.

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

20-43: LGTM! Robust FCM message handling.

The implementation includes proper validation, error handling, and logging. The data-to-Bundle conversion is correct, and the try-catch ensures that FCM processing errors don't crash the service.


45-50: LGTM! Proper token handling delegation.

The comment clearly documents that token management is handled by the expo-notifications JS layer, and the delegation to the superclass is appropriate.

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

1-9: LGTM! Clean TurboModule interface.

The interface definition follows React Native TurboModule patterns correctly. The native Android module is properly registered with the name 'VideoConfModule' (as defined in NativeVideoConfSpec.kt:13), matching the module reference on line 9.

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

12-36: LGTM!

Standard TurboModule package implementation. The module info configuration is correct with isTurboModule = true and appropriate flags for a non-C++ module.

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

9-23: LGTM!

Clean TurboModule spec definition following React Native conventions. The abstract methods provide a clear contract for the concrete VideoConfModule implementation.

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

37-45: LGTM!

New video conference fields align with the payload structure used across the native notification handling code. The inline comments documenting the status values (0=incoming, 4=cancelled) are helpful.


194-197: LGTM!

The Caller class follows the same pattern as the existing Sender class, maintaining consistency in the codebase.

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

45-84: LGTM!

The video conference intent handling is well-structured:

  • Validates the videoConfAction flag before processing
  • Properly cancels the notification when notificationId is valid
  • Builds a consistent JSON payload matching VideoConfBroadcast
  • Clears the flag after processing to prevent re-processing on configuration changes
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt (1)

65-71: LGTM!

The launch intent configuration with FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP is appropriate for launching the activity from a broadcast receiver context.

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

13-28: LGTM!

The Android path correctly:

  • Guards against missing NativeVideoConfModule
  • Uses JSON.parse (appropriate since the native side serializes with Gson)
  • Validates notificationType before dispatching
  • Handles errors gracefully without crashing

30-59: 'DECLINE_ACTION' identifier is correctly configured in iOS notification categories.

The hardcoded 'DECLINE_ACTION' identifier on line 48 matches the action identifier registered in the iOS notification category setup in app/lib/notifications/push.ts (line 116). The 'VIDEOCONF' category is properly configured via Notifications.setNotificationCategoryAsync() with both 'ACCEPT_ACTION' and 'DECLINE_ACTION' identifiers, ensuring consistency across the notification handling pipeline.

ios/NotificationService/NotificationService.swift (3)

9-66: Well-implemented avatar fetching with proper timeout and error handling.

The 3-second timeout prevents blocking notification delivery, URL encoding handles special characters correctly, and all failure paths gracefully return nil to allow the notification to proceed without an avatar. Good defensive coding.


134-162: Video conference handling looks correct.

The status-based routing (status 4 for cancellation, status 0/nil for incoming) and notification cleanup logic are well-implemented. The iOS 15+ interruption level for time-sensitive notifications is appropriate for incoming calls.


188-199: Good integration of avatar fetching with weak self capture.

The [weak self] capture prevents potential retain cycles, and the optional binding ensures safe access to bestAttemptContent before delivery.

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

27-49: Clean migration to new notification architecture.

The documentation clearly explains the notification flow, and removing INotificationsApplication in favor of the new VideoConfTurboPackage is a good architectural decision. The open modifier on the class allows for flexibility in testing or subclassing.

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

54-77: Good notification channel configuration.

The channel setup correctly uses high importance for incoming calls, enables lights and vibration, sets public visibility for lock screen display, and configures appropriate audio attributes for ringtone usage.


159-172: Solid notification builder configuration for incoming calls.

The notification correctly uses CATEGORY_CALL, sets ongoing to prevent accidental dismissal, and properly configures full-screen intent for heads-up display. The action ordering (Decline, Accept) follows platform conventions where the positive action is on the right.


188-195: Correct PendingIntent flag handling for Android 12+.

The version check for FLAG_IMMUTABLE on Android S and above is correct and required for targeting API 31+.

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

22-28: LGTM - Video conference action handling is clear.

The ACCEPT_ACTION/DECLINE_ACTION handling correctly parses the ejson and dispatches the appropriate action with the event type mapped from the identifier.


42-51: Good type-to-path mapping for room navigation.

The mapping correctly handles the different subscription types (channel, direct, group, omnichannel) and the special case for omnichannel using sender name.

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

475-489: Good timeout handling for avatar fetching.

The 3-second Glide timeout with TimeoutException handling prevents blocking the FCM service. This matches the iOS implementation timeout, maintaining consistency across platforms.


325-353: Video conference notification handling is well-structured.

The status-based routing correctly delegates to VideoConfNotification for incoming calls (status 0/null) and cancellation (status 4). The fallback for caller ID from both caller and sender fields handles different payload structures gracefully.

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

5-5: Unused import removed.

The removal of BackHandler is correct since it's no longer used after removing the hardware back button listener.


55-56: Clarify scope of JitsiMeetView changes in notification avatar PR.

This appears to be a large refactoring PR affecting hundreds of files. While the comment correctly notes that WebView handles back navigation natively, the extensive rewrite of JitsiMeetView seems disconnected from avatar fetching for notifications. Confirm whether these structural changes are necessary for the notification feature, and ensure the WebView fallback (when Jitsi app is not installed) is properly tested to verify back button behavior remains safe during active video conferences.

Comment on lines 97 to 105
let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)

rocketchat.sendMessage(rid: rid, message: message, threadIdentifier: payload.tmid) { response in
// Ensure we're on the main thread for UI operations
DispatchQueue.main.async {
defer {
UIApplication.shared.endBackgroundTask(backgroundTask)
completionHandler()
}
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

Provide an expiration handler for the background task.

Passing nil to expirationHandler means if the system needs to reclaim resources before the network request completes, the task will be terminated without cleanup. This could leave the completion handler uncalled.

-    let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
+    var backgroundTask: UIBackgroundTaskIdentifier = .invalid
+    backgroundTask = UIApplication.shared.beginBackgroundTask {
+      UIApplication.shared.endBackgroundTask(backgroundTask)
+      backgroundTask = .invalid
+    }
     
     rocketchat.sendMessage(rid: rid, message: message, threadIdentifier: payload.tmid) { response in
       // Ensure we're on the main thread for UI operations
       DispatchQueue.main.async {
         defer {
-          UIApplication.shared.endBackgroundTask(backgroundTask)
+          if backgroundTask != .invalid {
+            UIApplication.shared.endBackgroundTask(backgroundTask)
+            backgroundTask = .invalid
+          }
           completionHandler()
         }
📝 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
let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
rocketchat.sendMessage(rid: rid, message: message, threadIdentifier: payload.tmid) { response in
// Ensure we're on the main thread for UI operations
DispatchQueue.main.async {
defer {
UIApplication.shared.endBackgroundTask(backgroundTask)
completionHandler()
}
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
backgroundTask = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
rocketchat.sendMessage(rid: rid, message: message, threadIdentifier: payload.tmid) { response in
// Ensure we're on the main thread for UI operations
DispatchQueue.main.async {
defer {
if backgroundTask != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
completionHandler()
}
🤖 Prompt for AI Agents
In ios/ReplyNotification.swift around lines 97 to 105, the background task is
started with a nil expirationHandler which can allow the system to terminate the
task without cleanup; change beginBackgroundTask to supply an expirationHandler
that calls UIApplication.shared.endBackgroundTask(backgroundTask) and invokes
the completionHandler (on the main thread) to ensure cleanup if the system
expires the task, and guard/end the backgroundTask only if it's not
UIBackgroundTaskIdentifier.invalid.

Comment on lines 139 to 152
jest.mock('expo-notifications', () => ({
getDevicePushTokenAsync: jest.fn(() => Promise.resolve({ data: 'mock-token' })),
getPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })),
requestPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })),
setBadgeCountAsync: jest.fn(() => Promise.resolve(true)),
dismissAllNotificationsAsync: jest.fn(() => Promise.resolve()),
setNotificationHandler: jest.fn(),
setNotificationCategoryAsync: jest.fn(() => Promise.resolve()),
addNotificationReceivedListener: jest.fn(() => ({ remove: jest.fn() })),
addNotificationResponseReceivedListener: jest.fn(() => ({ remove: jest.fn() })),
addPushTokenListener: jest.fn(() => ({ remove: jest.fn() })),
getLastNotificationResponse: jest.fn(() => null),
DEFAULT_ACTION_IDENTIFIER: 'expo.modules.notifications.actions.DEFAULT'
}));
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
# Find all expo-notifications imports and method calls to verify mock coverage
echo "=== Searching for expo-notifications imports ==="
rg -n "from 'expo-notifications'" -g '!jest.setup.js' -g '!node_modules' --type=ts --type=tsx --type=js --type=jsx

echo -e "\n=== Searching for Notifications. method calls ==="
rg -nP "Notifications\." -g '!jest.setup.js' -g '!node_modules' --type=ts --type=tsx --type=js --type=jsx | head -50

echo -e "\n=== Searching for specific expo-notifications API calls ==="
rg -n "getDevicePushTokenAsync|getPermissionsAsync|requestPermissionsAsync|setBadgeCountAsync|dismissAllNotificationsAsync|setNotificationHandler|setNotificationCategoryAsync|addNotificationReceivedListener|addNotificationResponseReceivedListener|addPushTokenListener|getLastNotificationResponse" -g '!jest.setup.js' -g '!node_modules' --type=ts --type=tsx --type=js --type=jsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 339


🏁 Script executed:

#!/bin/bash
# Retry without problematic file type flags
echo "=== Searching for expo-notifications imports ==="
rg -n "from 'expo-notifications'" -g '!jest.setup.js' -g '!node_modules'

echo -e "\n=== Searching for Notifications. method calls ==="
rg -n "Notifications\." -g '!jest.setup.js' -g '!node_modules' | head -50

echo -e "\n=== Searching for specific expo-notifications API calls ==="
rg -n "getDevicePushTokenAsync|getPermissionsAsync|requestPermissionsAsync|setBadgeCountAsync|dismissAllNotificationsAsync|setNotificationHandler|setNotificationCategoryAsync|addNotificationReceivedListener|addNotificationResponseReceivedListener|addPushTokenListener|getLastNotificationResponse" -g '!jest.setup.js' -g '!node_modules'

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3923


🏁 Script executed:

#!/bin/bash
# Check the actual method call in getInitialNotification.ts
cat -n app/lib/notifications/videoConf/getInitialNotification.ts | sed -n '30,36p'

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 449


🏁 Script executed:

#!/bin/bash
# Verify that the synchronous version is also used
cat -n app/lib/notifications/push.ts | sed -n '215,220p'

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 354


Add missing getLastNotificationResponseAsync method to expo-notifications mock.

The mock provides getLastNotificationResponse() (synchronous), but the codebase also uses getLastNotificationResponseAsync() (asynchronous) at app/lib/notifications/videoConf/getInitialNotification.ts:33. Both methods need to be mocked:

getLastNotificationResponse: jest.fn(() => null),
getLastNotificationResponseAsync: jest.fn(() => Promise.resolve(null)),

Without the async version, tests exercising the video conference notification path will fail.

🤖 Prompt for AI Agents
In jest.setup.js around lines 139 to 152, the expo-notifications mock is missing
getLastNotificationResponseAsync which the codebase calls; add an entry to the
mocked object named getLastNotificationResponseAsync that is a jest.fn returning
Promise.resolve(null) (keep the existing synchronous getLastNotificationResponse
as-is) so tests using the async API for initial notification handling will not
fail.

@diegolmello diegolmello had a problem deploying to upload_experimental_android December 16, 2025 13:49 — with GitHub Actions Error
@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.68.0.107859

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNSiAJ5ygEz8GR1Rrnnn3bC77pDWG9tjeQViLkWBVyO1nepTv_aX5udjWLz4BhmduivlomUXEO6HyB6FRhF_

@github-actions
Copy link

iOS Build Available

Rocket.Chat Experimental 4.68.0.107860

- Updated avatar URI generation to differentiate between direct messages and group/channel notifications, ensuring the correct avatar is fetched based on the message type.
- Refactored avatar fetching logic in NotificationService to improve clarity and maintainability.
- Added support for Communication Notifications API to update notification content with sender avatars.
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

♻️ Duplicate comments (1)
ios/NotificationService/NotificationService.swift (1)

134-134: Force unwrap could crash on malformed UTF-8 strings.

The force unwrap on .data(using: .utf8)! could cause a crash if the ejson string contains invalid UTF-8 sequences. This issue was previously flagged and remains unresolved.

Apply this diff to use safe optional binding:

-            let ejson = (bestAttemptContent.userInfo["ejson"] as? String ?? "").data(using: .utf8)!
-            guard let data = try? (JSONDecoder().decode(Payload.self, from: ejson)) else {
+            guard let ejson = (bestAttemptContent.userInfo["ejson"] as? String ?? "").data(using: .utf8),
+                  let data = try? JSONDecoder().decode(Payload.self, from: ejson) else {
                 contentHandler(bestAttemptContent)
                 return
             }
🧹 Nitpick comments (4)
ios/RocketChatRN/RocketChatRN.entitlements (1)

15-16: LGTM! Communication notification entitlement added correctly.

The entitlement enables Communication Notifications API support for displaying sender avatars, which aligns with the PR objectives.

Operational requirements:

  • Ensure this entitlement is enabled in your Apple Developer Portal for both development and production provisioning profiles
  • App Store review may require explanation of why the app uses communication notification capabilities
ios/NotificationService/NotificationService.swift (3)

40-44: Consider using URLComponents for safer URL construction.

String concatenation for URL construction works but could fail silently with malformed server URLs. URLComponents would provide more robust URL validation.

Apply this diff to use URLComponents:

-        let fullPath = "\(avatarPath)?format=png&size=100&rc_token=\(credentials.userToken)&rc_uid=\(credentials.userId)"
-        guard let avatarURL = URL(string: server + fullPath) else {
+        guard var components = URLComponents(string: server) else {
+            completion(nil)
+            return
+        }
+        components.path = avatarPath
+        components.queryItems = [
+            URLQueryItem(name: "format", value: "png"),
+            URLQueryItem(name: "size", value: "100"),
+            URLQueryItem(name: "rc_token", value: credentials.userToken),
+            URLQueryItem(name: "rc_uid", value: credentials.userId)
+        ]
+        guard let avatarURL = components.url else {
             completion(nil)
             return
         }

75-75: Remove redundant nil initialization.

Initializing an optional variable with nil is redundant in Swift.

Apply this diff:

-        var senderImage: INImage? = nil
+        var senderImage: INImage?

121-126: Consider logging Communication Notification update failures.

While the silent fallback to regular notifications is appropriate, logging the error would help diagnose issues during development and debugging.

Apply this diff:

         do {
             let updatedContent = try bestAttemptContent.updating(from: intent)
             self.bestAttemptContent = updatedContent
         } catch {
-            // Failed to update with intent, will fall back to regular notification
+            NSLog("Failed to update notification with intent: \(error.localizedDescription)")
         }
📜 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 8d10b22 and 9a6d3ba.

📒 Files selected for processing (4)
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java (3 hunks)
  • ios/NotificationService/NotificationService.entitlements (1 hunks)
  • ios/NotificationService/NotificationService.swift (3 hunks)
  • ios/RocketChatRN/RocketChatRN.entitlements (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ios/NotificationService/NotificationService.swift (4)
ios/Shared/Extensions/String+Extensions.swift (1)
  • removeTrailingSlash (25-31)
ios/Shared/RocketChat/Storage.swift (1)
  • getCredentials (11-19)
ios/Shared/Extensions/Bundle+Extensions.swift (1)
  • string (8-14)
ios/Shared/RocketChat/RocketChat.swift (1)
  • getPushWithId (25-37)
🪛 SwiftLint (0.57.0)
ios/NotificationService/NotificationService.swift

[Warning] 75-75: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (5)
ios/NotificationService/NotificationService.entitlements (1)

5-6: LGTM! Entitlement correctly added to NotificationService extension.

The communication notification entitlement is properly configured for the notification service extension, consistent with the main app target. This allows the extension to modify notification content with sender information and avatars.

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

37-44: LGTM! Video conference fields added appropriately.

The new fields caller, callId, and status are well-documented and use appropriate types for video conference notification support.


208-211: LGTM! Caller class follows existing patterns.

The Caller class structure is consistent with the existing Sender class and appropriately models video conference caller information.

ios/NotificationService/NotificationService.swift (2)

1-2: LGTM!

The Intents framework import is necessary for the Communication Notifications API functionality introduced in this PR.


247-256: LGTM!

The avatar fetching integration is well-implemented with proper memory management using [weak self] to prevent retain cycles. The asynchronous flow ensures the notification is delivered after avatar fetch completes or times out.

@diegolmello diegolmello had a problem deploying to experimental_android_build December 16, 2025 14:50 — with GitHub Actions Error
@diegolmello diegolmello deployed to experimental_android_build December 24, 2025 13:20 — with GitHub Actions Active
@diegolmello diegolmello deployed to experimental_ios_build December 24, 2025 13:20 — with GitHub Actions Active
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 (2)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)

373-387: Consider extracting duplicated title logic.

The notification title derivation based on ejson.type is duplicated between buildNotification (lines 373-387) and notificationStyle (lines 546-562). Consider extracting this into a helper method to reduce duplication and ensure consistency.

🔎 Proposed refactor
+    private String deriveNotificationTitle(Ejson ejson, String fallbackTitle) {
+        if (ejson == null || ejson.type == null) {
+            return fallbackTitle;
+        }
+        if ("p".equals(ejson.type) || "c".equals(ejson.type)) {
+            return (ejson.name != null && !ejson.name.isEmpty()) ? ejson.name : fallbackTitle;
+        } else if ("d".equals(ejson.type)) {
+            return fallbackTitle;
+        } else if ("l".equals(ejson.type)) {
+            return (ejson.sender != null && ejson.sender.name != null && !ejson.sender.name.isEmpty()) 
+                ? ejson.sender.name : fallbackTitle;
+        }
+        return fallbackTitle;
+    }

Then use it in both locations:

String notificationTitle = deriveNotificationTitle(ejson, title);
ios/NotificationService/NotificationService.swift (1)

86-104: Consider extracting the prefix-stripping logic.

The nested conditionals for stripping sender prefixes from group/channel message bodies (lines 89-99) are complex and could benefit from extraction into a helper method for improved readability and testability.

📜 Review details

Configuration used: Organization 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 dbf4205 and 32ea524.

📒 Files selected for processing (7)
  • android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt
  • ios/NotificationService/NotificationService.swift
  • ios/Shared/Models/Payload.swift
🧰 Additional context used
🧬 Code graph analysis (3)
android/app/src/main/java/chat/rocket/reactnative/notification/NotificationHelper.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt (1)
  • context (22-232)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt (1)
  • getAvatar (215-217)
android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java (1)
app/sagas/login.js (8)
  • server (86-86)
  • server (230-230)
  • server (299-299)
  • server (375-375)
  • token (322-322)
  • token (389-389)
  • userId (134-134)
  • userId (341-341)
🪛 SwiftLint (0.57.0)
ios/NotificationService/NotificationService.swift

[Warning] 139-139: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 154-154: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 155-155: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

⏰ 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). (2)
  • GitHub Check: Build iOS Experimental / Build
  • GitHub Check: Build Android Experimental / Build
🔇 Additional comments (17)
ios/Shared/Models/Payload.swift (1)

11-15: LGTM!

The addition of the optional username field to the Caller struct aligns with the Android-side changes in Ejson.java and enables avatar generation for video conference callers using the /avatar/{username} endpoint.

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

159-166: LGTM!

The avatar fetching logic correctly handles the nullable avatarUri from getCallerAvatarUri() and gracefully falls back to no avatar when the URI is null or the fetch fails.


211-217: LGTM!

The getAvatar helper correctly delegates to NotificationHelper.fetchAvatarBitmap with a null fallback, allowing the notification to proceed without an avatar if fetching fails or times out.

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

62-85: LGTM!

The fetchAvatarBitmap implementation is well-structured:

  • 3-second timeout is appropriate given FCM's 10-second limit
  • Proper null/empty URI guard returning fallback
  • Correctly restores thread interrupt status on InterruptedException
  • Graceful fallback on all error paths
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (2)

465-467: LGTM!

Good refactor delegating avatar fetching to NotificationHelper.fetchAvatarBitmap with largeIcon() as the fallback. This centralizes the timeout and error handling logic.


501-505: LGTM!

The bounds check fix properly handles edge cases like ":" (colon only) where pos=0 and start=2 would exceed the message length. The early return when no colon is found is also a good improvement.

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

191-201: LGTM!

Good defensive programming. Validating column indices before accessing prevents crashes if the schema changes or the column is missing. The explicit logging helps with debugging.


248-272: LGTM!

The double-encoding handling is a good defensive measure—JSON values can sometimes be double-encoded depending on how they were stored. Validating the required RSA key fields (n, e, d) before proceeding prevents cryptic failures downstream.


288-356: LGTM!

Significantly improved error handling in decryptRoomKey:

  • Early return on null/empty e2eKey
  • Granular try/catch blocks with specific logging
  • Null checks after each fallible operation
  • Proper algorithm state management

This makes debugging E2E decryption issues much easier.

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

63-83: LGTM!

The buildAvatarUri helper centralizes server URL and credential validation, reducing code duplication. The errorContext parameter provides useful context in log messages for debugging different avatar types.


85-116: LGTM!

The refactored getAvatarUri properly:

  • URL-encodes sender.username and rid to handle special characters (addressing past review feedback)
  • Uses the API 24-compatible URLEncoder.encode(String, String) overload
  • Distinguishes between DM (sender avatar) and group/channel (room avatar)

118-136: LGTM!

The getCallerAvatarUri method follows the same robust pattern as getAvatarUri:

  • Validates caller and username before proceeding
  • URL-encodes the username
  • Delegates to buildAvatarUri for consistent URI construction
ios/NotificationService/NotificationService.swift (5)

1-10: LGTM!

The addition of the Intents framework and finalContent property appropriately supports the new communication intent functionality for richer notifications with avatars.


13-34: LGTM!

The refactored guard-based payload extraction is much safer than the previous force-unwrap approach and properly handles malformed data with graceful fallback.


38-74: LGTM!

The video conference handling correctly manages call cancellations, sets appropriate notification priority with timeSensitive, and properly uses weak self to prevent retain cycles in the avatar fetch closure.


201-241: LGTM!

The message content fetching logic properly handles placeholder removal, server fallbacks, and uses weak self to prevent retain cycles.


243-250: LGTM!

The decryption helper appropriately handles different E2E encryption algorithm versions with safe optional returns.

@diegolmello diegolmello had a problem deploying to upload_experimental_android December 24, 2025 13:49 — with GitHub Actions Error
@github-actions
Copy link

iOS Build Available

Rocket.Chat Experimental 4.68.0.107949

@github-actions
Copy link

Android Build Available

Rocket.Chat Experimental 4.68.0.107948

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNTMSv1I2bYQ2c16qHto-fIin-txKTcqrTUTvb26CmqsruD5K3maAnkNLQZNSYYlgEuGPjqvOqhp7qIes3cv

- Updated type casting for body content to ensure proper string handling.
- Simplified variable declarations for senderImage, recipients, and speakableGroupName.
- Configured URLSession with specific timeouts for better resource management during avatar fetching.
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

🧹 Nitpick comments (3)
ios/NotificationService/NotificationService.swift (3)

89-100: Simplify: Redundant optional cast.

bestAttemptContent.body is already of type String (non-optional per UNMutableNotificationContent API), so casting it to String? is unnecessary. You can access it directly.

🔎 Proposed simplification
-                if let body = bestAttemptContent.body as? String {
+                let body = bestAttemptContent.body
+                if !body.isEmpty {
                     let prefix = "\(senderUsername): "
                     if body.hasPrefix(prefix) {
                         bestAttemptContent.body = String(body.dropFirst(prefix.count))
                     } else {
                         // Try with sender name (display name) as fallback
                         let senderNamePrefix = "\(senderName): "
                         if body.hasPrefix(senderNamePrefix) {
                             bestAttemptContent.body = String(body.dropFirst(senderNamePrefix.count))
                         }
                     }
                 }
-                }

158-159: Use optional binding instead of force unwrap.

While the force unwrap is safe here (checked by the ternary condition), idiomatic Swift prefers optional binding.

🔎 Proposed simplification
-        speakableGroupName = (groupName != nil) ? INSpeakableString(spokenPhrase: groupName!) : nil
+        if let groupName = groupName {
+            speakableGroupName = INSpeakableString(spokenPhrase: groupName)
+        }

314-320: URL-encode room ID in avatar path.

Line 319: The room ID (rid) is used directly in the URL path without encoding. While room IDs are typically alphanumeric, encoding ensures correctness if they contain special characters.

🔎 Proposed fix
         } else {
-            guard let rid = payload.rid else {
+            guard let rid = payload.rid,
+                  let encoded = rid.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
                 completion(nil)
                 return
             }
-            avatarPath = "/avatar/room/\(rid)"
+            avatarPath = "/avatar/room/\(encoded)"
         }
📜 Review details

Configuration used: Organization 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 32ea524 and c0a2ffd.

📒 Files selected for processing (1)
  • ios/NotificationService/NotificationService.swift
🧰 Additional context used
🧬 Code graph analysis (1)
ios/NotificationService/NotificationService.swift (2)
ios/Shared/RocketChat/RocketChat.swift (1)
  • decryptContent (77-82)
ios/Shared/RocketChat/Storage.swift (1)
  • getCredentials (11-19)
⏰ 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). (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🔇 Additional comments (5)
ios/NotificationService/NotificationService.swift (5)

1-10: LGTM: Imports and properties are well-structured.

The addition of the Intents framework and finalContent property appropriately supports the new avatar and intent-based notification features.


13-34: LGTM: Clean refactoring with safe parsing and routing.

The guard-based flow safely parses the ejson payload and routes to appropriate processors with proper error handling.


38-74: LGTM: Video conference handling is correct.

Proper handling of cancelled calls, appropriate use of weak self to prevent retain cycles, and sensible fallback to bestAttemptContent when intent update fails.


201-250: LGTM: Helper methods handle edge cases well.

Both fetchMessageContent and decryptMessage have appropriate error handling and fallback paths to ensure notifications are always delivered.


183-185: This API usage is correct according to Apple documentation.

The setImage(_:forParameterNamed: \.speakableGroupName) API is the documented and supported way to attach an image to the speakableGroupName parameter in INSendMessageIntent (available since iOS 12). This matches the official Apple Developer documentation and is used correctly in this code.

Note: Some developers report that the group image and name appear in the system UI only when there are at least 2 recipients in the conversation, but this is a display behavior detail rather than an API usage issue.

Likely an incorrect or invalid review comment.

- Implemented URL encoding for user credentials to prevent malformed URLs during avatar fetching.
- Updated the full avatar path construction to use encoded credentials for improved reliability.
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 (4)
ios/NotificationService/NotificationService.swift (4)

89-89: Simplify: body is already String, not Any.

UNMutableNotificationContent.body is declared as String (not optional, not Any), so the optional cast as? String is unnecessary. You can bind it directly: let body = bestAttemptContent.body.

🔎 Proposed simplification
-                if let body = bestAttemptContent.body as? String {
+                let body = bestAttemptContent.body
+                if !body.isEmpty {

157-159: Prefer optional map over force unwrap.

While the force unwrap on Line 158 is safe due to the nil-check on Line 157, Swift's optional map is more idiomatic and eliminates the force unwrap.

🔎 Proposed refactor
-        if isGroup {
-            speakableGroupName = (groupName != nil) ? INSpeakableString(spokenPhrase: groupName!) : nil
+        if isGroup {
+            speakableGroupName = groupName.map { INSpeakableString(spokenPhrase: $0) }

263-264: Prefer URLComponents for URL construction.

String concatenation on Line 264 is fragile. Use URLComponents to safely build the URL with base, path, and query items, which handles edge cases like missing slashes and special characters more robustly.

🔎 Proposed refactor
-        let fullPath = "\(avatarPath)?format=png&size=100&rc_token=\(encodedToken)&rc_uid=\(encodedUserId)"
-        guard let avatarURL = URL(string: server + fullPath) else {
+        guard var components = URLComponents(string: server) else {
             completion(nil)
             return
         }
+        components.path = avatarPath
+        components.queryItems = [
+            URLQueryItem(name: "format", value: "png"),
+            URLQueryItem(name: "size", value: "100"),
+            URLQueryItem(name: "rc_token", value: credentials.userToken),
+            URLQueryItem(name: "rc_uid", value: credentials.userId)
+        ]
+        guard let avatarURL = components.url else {
+            completion(nil)
+            return
+        }

Note: URLQueryItem handles encoding automatically, so you can pass the raw credentials without manual encoding.


322-327: Consider URL-encoding the room ID.

Line 326 interpolates rid directly into the path without encoding. While Rocket.Chat room IDs are typically alphanumeric, encoding ensures special characters don't cause malformed URLs or unexpected behavior.

🔎 Proposed fix
         } else {
-            guard let rid = payload.rid else {
+            guard let rid = payload.rid,
+                  let encoded = rid.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
                 completion(nil)
                 return
             }
-            avatarPath = "/avatar/room/\(rid)"
+            avatarPath = "/avatar/room/\(encoded)"
         }
📜 Review details

Configuration used: Organization 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 c0a2ffd and 8be42dd.

📒 Files selected for processing (1)
  • ios/NotificationService/NotificationService.swift
🧰 Additional context used
🧬 Code graph analysis (1)
ios/NotificationService/NotificationService.swift (5)
ios/Shared/RocketChat/API/Request.swift (2)
  • request (45-72)
  • body (41-43)
ios/RocketChat Watch App/Database/Database.swift (1)
  • removeTrailingSlash (125-131)
ios/Shared/RocketChat/RocketChat.swift (1)
  • decryptContent (77-82)
ios/Shared/RocketChat/Encryption.swift (1)
  • decryptContent (175-210)
ios/Shared/RocketChat/Storage.swift (1)
  • getCredentials (11-19)
⏰ 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). (2)
  • GitHub Check: ESLint and Test / run-eslint-and-test
  • GitHub Check: format
🔇 Additional comments (6)
ios/NotificationService/NotificationService.swift (6)

1-10: LGTM: Clean property setup.

The new Intents import and finalContent property properly support the avatar-aware communication intent flow.


13-34: LGTM: Robust error handling.

The guard-based pattern safely handles JSON decoding failures and routes notifications correctly by type.


38-74: LGTM: Video conference handling is sound.

Properly handles call cancellations, sets time-sensitive priority, and gracefully falls back when avatar fetching fails or intent updating produces nil.


199-241: LGTM: Robust message content fetching.

Properly removes placeholders, fetches from server with appropriate fallbacks, and updates ejson userInfo for correct in-app navigation.


243-250: LGTM: Safe E2E decryption.

Correctly checks encryption algorithms and safely delegates to the encryption module with optional chaining.


293-303: LGTM: Caller avatar fetching is correct.

Properly encodes the username with .urlPathAllowed and safely retrieves credentials before constructing the avatar path.

@diegolmello diegolmello changed the title feat(iOS): Avatar on push notifications feat: Avatar on push notifications Dec 24, 2025
@diegolmello
Copy link
Member Author

image

I'm seeing this on Android Studio.
#6863 is an attempt to remove ReactApplication from FCM logic, but I got stuck on WatermelonDB LOCK 🤦🏼‍♂️

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