Skip to content

Conversation

@diegolmello
Copy link
Member

@diegolmello diegolmello commented Jan 7, 2026

Proposed changes

Issue(s)

Same work from #6888, but without #6853 dependency

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

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

    • Android: native TurboModule bridge to store and retrieve pending push notifications for app foreground handling.
  • Improvements

    • Centralized intent handling for notifications and video calls.
    • Video conference payloads include host and callId; parsing is more resilient.
    • Stronger, safer decryption logic with clearer fallback messaging.
  • Bug Fixes

    • Reduced missed/duplicated notification actions and more consistent notification delivery.

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

- Refactored MainActivity to utilize NotificationIntentHandler for managing notification intents.
- Added checkPendingNotification function to handle notifications when the app comes to the foreground.
- Updated push notification configuration to retrieve pending notifications from the native module.
- Enhanced notification handling logic in the notifications module to ensure proper processing of notifications.
- Refactored the E2E notification handling in CustomPushNotification to eliminate the asynchronous processor, streamlining the decryption process.
- Enhanced error handling in the Encryption class for better robustness during key parsing and decryption.
- Added additional fields to VideoConfBroadcast for improved notification data handling.
- Updated notification parsing in the notifications module to include error handling for EJSON parsing failures.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 7, 2026

Walkthrough

Centralizes Android notification intent routing into NotificationIntentHandler, removes the async E2ENotificationProcessor, moves E2E decryption into synchronous paths in CustomPushNotification, and adds a TurboModule PushNotificationModule plus JS hooks to persist and surface pending notifications.

Changes

Cohort / File(s) Summary
Intent routing & MainActivity
android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt, android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt, android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt
MainActivity delegates intent handling to NotificationIntentHandler.handleIntent. New handler centralizes video-conf and regular notification extraction, cancellation, payload construction, and stores payloads via native modules; video-conf payload now includes host and callId.
E2E decryption & notification flow
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java, android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
Removes React-context polling/async processor reliance; CustomPushNotification uses app Context, makes mBundle volatile, simplifies flows and error paths. Encryption adds defensive parsing, algorithm detection, safer decoding, and returns richer key/algorithm results.
Async processor removal
android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java
Entire async polling/decryption processor and its interfaces removed.
TurboModule native bridge
android/app/src/main/java/chat/rocket/reactnative/notification/NativePushNotificationSpec.kt, android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt, android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationTurboPackage.kt
Adds NativePushNotificationSpec, PushNotificationModule (SharedPreferences-backed storage + getPendingNotification/clearPendingNotification), and PushNotificationTurboPackage to register the TurboModule.
MainApplication changes
android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
Removes React context listener setup and adds PushNotificationTurboPackage() to React packages.
JS native bridge & notification flow
app/lib/native/NativePushNotificationAndroid.ts, app/lib/notifications/index.ts, app/lib/notifications/push.ts
Adds TypeScript TurboModule interface and checkPendingNotification() to read native pending notifications; push flow prefers native pending notification before falling back to existing flows; parsing guarded with try/catch.
Saga integration
app/sagas/state.js
Calls checkPendingNotification() when app returns to foreground to surface stored native notifications.
Minor cleanup
android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
Removed unused RNCallback class and Callback import.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant MainActivity
    participant NotificationIntentHandler
    participant CustomPushNotification
    participant PushNotificationModule
    participant SharedPrefs as SharedPreferences
    participant JS as JavaScript

    MainActivity->>NotificationIntentHandler: handleIntent(context,intent)
    alt video conference action
        NotificationIntentHandler->>PushNotificationModule: storePendingNotification(videoConf JSON)
        PushNotificationModule->>SharedPrefs: write KEY_PENDING_NOTIFICATION
    else regular notification tap
        NotificationIntentHandler->>CustomPushNotification: forward extras/ejson
        alt encrypted
            CustomPushNotification->>CustomPushNotification: decryptMessage (synchronous)
            CustomPushNotification->>PushNotificationModule: storePendingNotification(decrypted JSON)
        else not encrypted
            CustomPushNotification->>PushNotificationModule: storePendingNotification(JSON)
        end
        PushNotificationModule->>SharedPrefs: write KEY_PENDING_NOTIFICATION
    end

    JS->>PushNotificationModule: getPendingNotification()
    PushNotificationModule->>SharedPrefs: read & clear KEY_PENDING_NOTIFICATION
    SharedPrefs-->>PushNotificationModule: notification JSON / null
    PushNotificationModule-->>JS: Promise resolves with string|null
    JS->>JS: checkPendingNotification() parses JSON and dispatches onNotification
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • OtavioStasiak

"I hopped through intents, swift and spry,
Stored pending rings beneath the sky.
Secrets now unwrapped in one quick hop,
Central paths aligned — no more stop! 🐇🔔"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 primary change: fixing push notification handling on killed apps with deep linking support for Android, which aligns with the refactoring of notification processing and the addition of NotificationIntentHandler.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix.android-e2ee-killed-app-without-avatar-work

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: 1

Caution

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

⚠️ Outside diff range comments (1)
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (1)

288-356: Use RoomKeyResult values directly instead of relying on instance field state.

encryptMessageContent obtains RoomKeyResult containing the algorithm value (line 301), but then ignores it and uses the instance field this.algorithm (line 312) instead. Similarly, this.keyId is set by decryptRoomKey and used by encryptMessageContent without being passed between methods. This creates implicit state coupling where the correctness depends on method call order within the same invocation.

Refactor to pass algorithm and keyId from RoomKeyResult directly to EncryptionContent instead of storing them as instance fields:

// In encryptMessageContent:
RoomKeyResult roomKeyResult = decryptRoomKey(room.e2eKey, ejson);
// ... later ...
return new EncryptionContent(roomKeyResult.algorithm, encryptedData, roomKeyResult.keyId, ...);
🤖 Fix all issues with AI agents
In
@android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java:
- Around line 236-247: handleE2ENotification currently drops the notification
when Encryption.shared.decryptMessage returns null; change it so that on null
you populate the bundle with a fallback message (e.g., "Encrypted message - open
app to decrypt") and any minimal metadata needed, set mBundle = bundle, create
an updated Ejson via safeFromJson(bundle.getString("ejson", "{}"), Ejson.class)
or reuse existing ejson, and call showNotification(bundle, updatedEjson, notId)
so a placeholder notification is shown instead of being dropped.
🧹 Nitpick comments (6)
app/lib/notifications/push.ts (2)

218-274: Consider extracting shared notification transformation logic.

The notification transformation code (lines 228-243) duplicates the logic in app/lib/notifications/index.ts (lines 107-121). Consider extracting a shared helper function to avoid maintaining two identical transformations.

♻️ Suggested helper extraction

Create a shared transformation function:

// In a shared location, e.g., app/lib/notifications/utils.ts
export const transformNativeNotificationData = (notificationData: Record<string, any>): INotification => ({
  payload: {
    message: notificationData.message || '',
    style: notificationData.style || '',
    ejson: notificationData.ejson || '',
    collapse_key: notificationData.collapse_key || '',
    notId: notificationData.notId || '',
    msgcnt: notificationData.msgcnt || '',
    title: notificationData.title || '',
    from: notificationData.from || '',
    image: notificationData.image || '',
    soundname: notificationData.soundname || '',
    action: notificationData.action
  },
  identifier: notificationData.notId || ''
});

Then use it in both push.ts and index.ts.


257-273: Simplify promise chain structure.

The consecutive .then() blocks can be combined for better readability. The current structure chains two .then() and two .catch() blocks which is harder to follow.

♻️ Suggested simplification using async/await
-	return NativePushNotificationModule.getPendingNotification()
-		.then(pendingNotification => {
-			if (pendingNotification) {
-				try {
-					// ... transformation logic
-					return transformed;
-				} catch (parseError) {
-					console.error('[push.ts] Error parsing notification data:', parseError);
-					return null;
-				}
-			}
-			return null;
-		})
-		.catch(e => {
-			console.error('[push.ts] Error getting pending notification from native module:', e);
-			return null;
-		})
-		.then(nativeNotification => {
-			if (nativeNotification) {
-				return nativeNotification;
-			}
-			const lastResponse = Notifications.getLastNotificationResponse();
-			if (lastResponse) {
-				return transformNotificationResponse(lastResponse);
-			}
-			return null;
-		})
-		.catch(e => {
-			console.error('[push.ts] Error in promise chain:', e);
-			return null;
-		});
+	try {
+		const pendingNotification = await NativePushNotificationModule.getPendingNotification();
+		if (pendingNotification) {
+			try {
+				const notificationData = JSON.parse(pendingNotification);
+				// ... transformation to INotification
+				return transformed;
+			} catch (parseError) {
+				console.error('[push.ts] Error parsing notification data:', parseError);
+			}
+		}
+	} catch (e) {
+		console.error('[push.ts] Error getting pending notification from native module:', e);
+	}
+	
+	// Fallback to expo-notifications
+	const lastResponse = Notifications.getLastNotificationResponse();
+	if (lastResponse) {
+		return transformNotificationResponse(lastResponse);
+	}
+	return null;

Note: This would require making pushNotificationConfigure an async function.

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

98-133: Missing clearPendingNotification call after successful processing.

After successfully retrieving and processing the pending notification, the native module's clearPendingNotification should be called to prevent reprocessing if checkPendingNotification is invoked again before the native side clears it.

While getPendingNotification in the native module clears the notification after reading (line 54 in PushNotificationModule.kt), an explicit JS-side clear provides a safety net and clearer intent.

♻️ Suggested improvement
 				const notificationData = JSON.parse(pendingNotification);
 				const notification: INotification = {
 					payload: {
 						message: notificationData.message || '',
 						style: notificationData.style || '',
 						ejson: notificationData.ejson || '',
 						collapse_key: notificationData.collapse_key || '',
 						notId: notificationData.notId || '',
 						msgcnt: notificationData.msgcnt || '',
 						title: notificationData.title || '',
 						from: notificationData.from || '',
 						image: notificationData.image || '',
 						soundname: notificationData.soundname || '',
 						action: notificationData.action
 					},
 					identifier: notificationData.notId || ''
 				};
 				onNotification(notification);
+				// Explicitly clear after successful processing (belt-and-suspenders)
+				NativePushNotificationModule.clearPendingNotification?.();
 			} catch (e) {
android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt (2)

47-61: Minor race condition between read and clear operations.

The read and clear are not atomic—if getPendingNotification is called concurrently (unlikely but possible), both calls could read the same notification before either clears it. Consider using an atomic read-and-remove pattern.

♻️ Atomic read-and-clear
     @ReactMethod
     override fun getPendingNotification(promise: Promise) {
         try {
             val prefs = reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
-            val notification = prefs.getString(KEY_PENDING_NOTIFICATION, null)
-
-            // Clear the notification after reading
-            notification?.let {
-                prefs.edit().remove(KEY_PENDING_NOTIFICATION).apply()
-            }
+            val notification: String?
+            synchronized(PushNotificationModule::class.java) {
+                notification = prefs.getString(KEY_PENDING_NOTIFICATION, null)
+                notification?.let {
+                    prefs.edit().remove(KEY_PENDING_NOTIFICATION).commit()
+                }
+            }
 
             promise.resolve(notification)
         } catch (e: Exception) {
             promise.reject("ERROR", e.message)
         }
     }

67-76: Swallowed exception hides potential issues.

Per the static analysis hint, the caught exception is discarded without logging. While errors in clearPendingNotification are non-critical, logging them aids debugging.

♻️ Log the swallowed exception
     @ReactMethod
     override fun clearPendingNotification() {
         try {
             reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
                 .edit()
                 .remove(KEY_PENDING_NOTIFICATION)
                 .apply()
         } catch (e: Exception) {
-            // Ignore errors
+            android.util.Log.w("RocketChat.PushNotificationModule", "Failed to clear pending notification", e)
         }
     }
android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt (1)

70-71: Consider caching the Gson instance.

GsonBuilder().create() is called each time an intent is processed. Since Gson is thread-safe, a single instance can be reused.

♻️ Cache Gson at companion object level
 class NotificationIntentHandler {
 
     companion object {
         private const val TAG = "RocketChat.NotificationIntentHandler"
+        private val gson = GsonBuilder().create()
 
         // ... in handleVideoConfIntent:
-        val gson = GsonBuilder().create()
-        val jsonData = gson.toJson(data)
+        val jsonData = gson.toJson(data)
 
         // ... in handleNotificationIntent:
-        val gson = GsonBuilder().create()
-        val jsonData = gson.toJson(notificationData)
+        val jsonData = gson.toJson(notificationData)

Also applies to: 139-140

📜 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 9a472ea and a98d3d3.

📒 Files selected for processing (14)
  • android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
  • android/app/src/main/java/chat/rocket/reactnative/MainApplication.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/NativePushNotificationSpec.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/NotificationIntentHandler.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationTurboPackage.kt
  • android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfBroadcast.kt
  • app/lib/native/NativePushNotificationAndroid.ts
  • app/lib/notifications/index.ts
  • app/lib/notifications/push.ts
  • app/sagas/state.js
💤 Files with no reviewable changes (1)
  • android/app/src/main/java/chat/rocket/reactnative/notification/E2ENotificationProcessor.java
🧰 Additional context used
🧬 Code graph analysis (5)
app/sagas/state.js (1)
app/lib/notifications/index.ts (1)
  • checkPendingNotification (98-133)
app/lib/notifications/index.ts (6)
app/lib/store/auxStore.ts (1)
  • store (6-6)
app/actions/deepLinking.ts (1)
  • deepLinkingClickCallPush (26-31)
app/actions/app.ts (1)
  • appInit (45-49)
app/lib/methods/roomTypeToApiType.ts (1)
  • types (19-24)
app/sagas/deepLinking.js (2)
  • name (62-62)
  • type (61-61)
app/definitions/INotification.ts (1)
  • INotification (1-16)
app/lib/notifications/push.ts (1)
app/definitions/INotification.ts (1)
  • INotification (1-16)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (1)
  • Encryption (105-511)
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (2)
app/lib/encryption/utils.ts (1)
  • decodePrefixedBase64 (200-219)
ios/Shared/RocketChat/Encryption.swift (1)
  • decodePrefixedBase64 (75-92)
🪛 detekt (1.23.8)
android/app/src/main/java/chat/rocket/reactnative/notification/PushNotificationModule.kt

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

(detekt.exceptions.SwallowedException)

🔇 Additional comments (15)
app/sagas/state.js (1)

32-35: LGTM - fire-and-forget pattern is appropriate here.

The async call with .catch() correctly prevents unhandled promise rejection. Note that checkPendingNotification already has internal try/catch handling (per app/lib/notifications/index.ts), so the outer .catch() provides a safety net but may result in double-logging on errors.

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

51-52: LGTM - consistent with existing field extraction pattern.

The new host and callId fields follow the same safe extraction pattern with empty string defaults as the other fields in the data map.

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

1-9: LGTM - clean TurboModule bridge definition.

The interface correctly defines the contract matching the native PushNotificationModule.kt implementation. Using TurboModuleRegistry.get() (which returns Spec | null) is appropriate, and callers properly null-check before usage.

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

22-22: LGTM - proper TurboPackage registration.

The PushNotificationTurboPackage is correctly imported and added to the packages list, following the established pattern for other TurboPackages in this application.

Also applies to: 45-45

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

29-43: LGTM - clean centralized intent handling.

The refactoring to use NotificationIntentHandler provides good separation of concerns. The null-safe handling in onCreate via intent?.let and the setIntent(intent) call in onNewIntent are both appropriate patterns.

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

9-23: Well-structured TurboModule specification.

The abstract class correctly defines the contract for the push notification native module with proper React Native annotations. The companion object pattern for the module name is idiomatic Kotlin.

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

12-37: Standard TurboModule package implementation.

The package correctly registers the PushNotificationModule with appropriate module info flags. The lazy instantiation pattern in getModule is correct.

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

25-34: Good defensive error handling for video conf notifications.

Wrapping the EJSON parse in try/catch prevents crashes from malformed video conference payloads while allowing the flow to continue.


48-51: Reasonable fallback for incomplete notification data.

Dispatching appInit when essential fields are missing ensures the app opens gracefully rather than failing silently or crashing.

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

24-39: Good synchronous write with proper error handling.

Using commit() instead of apply() ensures the notification is persisted before returning, which is important for the notification tap flow. The success check and exception handling are appropriate.

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

22-31: Clean intent routing with clear separation of concerns.

The dispatcher pattern effectively centralizes notification handling logic that was previously scattered in MainActivity.


121-131: Heuristic for non-serializable detection is fragile but acceptable.

The check !stringValue.startsWith("android.") && !stringValue.contains("@") is a best-effort filter for object references. While imperfect, it's wrapped in try/catch and logs skipped extras, making it safe for production.

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

191-208: Good defensive column access with proper null handling.

Checking column indices before accessing prevents crashes when the database schema differs from expectations. The early return with logging provides clear diagnostics.


248-272: Robust handling of double-encoded private keys.

The fallback parsing for JSON-encoded strings handles edge cases where the private key may be stored as a serialized JSON string within JSON. The validation of required fields (n, e, d) ensures downstream operations won't fail with null pointer exceptions.


320-343: Improved error handling for RSA decryption and session key parsing.

The wrapped operations with explicit error logging will significantly aid debugging E2E decryption failures in production. The null checks prevent silent failures.

@diegolmello diegolmello force-pushed the fix.android-e2ee-killed-app-without-avatar-work branch from a98d3d3 to 7f93c0e Compare January 7, 2026 21:18
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: 2

🤖 Fix all issues with AI agents
In
@android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java:
- Around line 171-189: In handleE2ENotification, guard against a possible null
Encryption.shared before calling decryptMessage: check if Encryption.shared is
null, log a warning (e.g., using Log.w with TAG) and treat as decryption failure
by setting the fallback message ("Encrypted message"), assigning mBundle inside
the synchronized block, and calling showNotification(bundle, ejson, notId);
otherwise proceed to call Encryption.shared.decryptMessage(ejson, mContext) as
before and handle the decrypted result.
- Line 62: Multiple unsynchronized reads of the field mBundle in methods like
processNotification() and buildNotification() can see different Bundle instances
if another thread reassigns mBundle; to fix this either capture the reference
once at method entry (e.g., Bundle bundle = mBundle;) and use that local
variable for all reads in the method, or wrap any sequences that perform
multiple reads of mBundle in synchronized(this) blocks so they see a consistent
object; apply this change to all multi-read sequences that currently read
mBundle (including the locations where other threads perform synchronized writes
that reassign mBundle).
📜 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 a98d3d3 and f29d6b9.

📒 Files selected for processing (4)
  • android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
  • app/lib/native/NativePushNotificationAndroid.ts
  • app/sagas/state.js
💤 Files with no reviewable changes (1)
  • android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/sagas/state.js
  • app/lib/native/NativePushNotificationAndroid.ts
🧰 Additional context used
🧬 Code graph analysis (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (2)
android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt (1)
  • TAG (14-51)
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (1)
  • Encryption (105-511)
🔇 Additional comments (4)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (4)

96-96: LGTM!

The updated error message is clearer and more descriptive.


154-154: LGTM!

The return statement correctly prevents fall-through to the regular notification path after E2E handling is complete.


180-188: Fallback notification properly addresses past review concern.

The implementation now correctly shows a fallback notification with "Encrypted message" when decryption fails, rather than silently dropping it. This ensures users are notified even when E2E keys are unavailable.


215-217: Synchronization lock usage is consistent.

The synchronized block correctly protects the mBundle assignment. Note that within the loadNotificationAndProcess callback (line 126), the code uses synchronized(CustomPushNotification.this) because it's within an anonymous inner class (Callback), whereas here this is used directly. Both refer to the same lock object in their respective contexts, so the synchronization is correct.

@diegolmello diegolmello had a problem deploying to experimental_ios_build January 7, 2026 21:26 — with GitHub Actions Error
@diegolmello diegolmello temporarily deployed to experimental_android_build January 7, 2026 21:26 — with GitHub Actions Inactive
@diegolmello diegolmello had a problem deploying to official_android_build January 7, 2026 21:26 — with GitHub Actions Error
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: 2

Caution

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

⚠️ Outside diff range comments (2)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (2)

197-199: Race condition in notification message list initialization.

The check-then-act pattern on notificationMessages is not atomic. Two threads could both see null for the same notId and both create ArrayLists, with one overwriting the other and losing messages. Additionally, ArrayList is not thread-safe for concurrent add() calls.

Use computeIfAbsent with a thread-safe list:

Suggested fix
-    // Initialize notification message list for this ID
-    if (notificationMessages.get(notId) == null) {
-        notificationMessages.put(notId, new ArrayList<>());
-    }
+    // Initialize notification message list for this ID (atomic operation)
+    notificationMessages.computeIfAbsent(notId, k -> Collections.synchronizedList(new ArrayList<>()));

You'll also need to add the import:

import java.util.Collections;

286-343: Capture mBundle at method entry to ensure consistent reads.

This method reads mBundle multiple times (lines 288–291, 303, 328, 334–335) without synchronization. Apply the same fix as suggested for processNotification(): capture the reference once at the start.

 private Notification.Builder buildNotification(int notificationId) {
+    final Bundle bundle = mBundle;
     String notId = Integer.toString(notificationId);
-    String title = mBundle.getString("title");
-    String message = mBundle.getString("message");
-    Boolean notificationLoaded = mBundle.getBoolean("notificationLoaded", false);
-    Ejson ejson = safeFromJson(mBundle.getString("ejson", "{}"), Ejson.class);
+    String title = bundle.getString("title");
+    String message = bundle.getString("message");
+    Boolean notificationLoaded = bundle.getBoolean("notificationLoaded", false);
+    Ejson ejson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
     // ... and update remaining mBundle references in this method
🤖 Fix all issues with AI agents
In
@android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java:
- Around line 171-189: handleE2ENotification calls
Encryption.shared.decryptMessage(ejson, mContext) without guarding against
Encryption.shared being null; add a null check before calling decryptMessage
(e.g., if Encryption.shared == null) and in that branch treat as decryption
failure: log a warning, set bundle.putString("message", "Encrypted message"),
synchronize mBundle assignment, and call showNotification(bundle, ejson, notId)
so behavior matches the existing fallback path; ensure the non-null path
continues to call decryptMessage and behaves unchanged.
- Line 62: The methods processNotification and buildNotification perform
multiple unsynchronized reads of the volatile field mBundle; capture mBundle
once into a local Bundle variable at the start of each method (e.g., Bundle
bundle = mBundle), perform a single null check on that local reference, and then
use that local bundle throughout the method so all reads operate on the same
Bundle instance (update all uses in processNotification and all code paths in
buildNotification to reference the local variable instead of mBundle).
📜 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 f29d6b9 and b588e4a.

📒 Files selected for processing (1)
  • android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
🧰 Additional context used
🧬 Code graph analysis (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (2)
android/app/src/main/java/chat/rocket/reactnative/notification/RCFirebaseMessagingService.kt (1)
  • TAG (14-51)
android/app/src/main/java/chat/rocket/reactnative/notification/Encryption.java (1)
  • Encryption (105-511)
🔇 Additional comments (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)

215-218: LGTM!

The synchronized block correctly protects the mBundle assignment, ensuring visibility to other threads.

@diegolmello diegolmello had a problem deploying to upload_experimental_android January 7, 2026 21:55 — with GitHub Actions Error
@github-actions
Copy link

github-actions bot commented Jan 7, 2026

Android Build Available

Rocket.Chat Experimental 4.68.1.107999

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNRalD7VxOP4V_ybRJDJW4QKsEeZ6FQ7okTiCcS2SPASA2pr091mlKRS-1gjvGazda11GWP8hlkJ3sEGIZJg

- Replaced direct usage of mBundle with a local bundle variable for improved readability and consistency.
- Enhanced error handling in handleE2ENotification to manage null Encryption singleton cases.
- Updated notification building methods to utilize the local bundle variable, ensuring clarity in data handling.
@diegolmello diegolmello requested a deployment to experimental_ios_build January 8, 2026 14:05 — with GitHub Actions Waiting
@diegolmello diegolmello temporarily deployed to experimental_android_build January 8, 2026 14:05 — with GitHub Actions Inactive
@diegolmello diegolmello requested a deployment to official_android_build January 8, 2026 14:05 — with GitHub Actions Waiting
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

Caution

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

⚠️ Outside diff range comments (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)

206-244: Potential race condition in notification message list initialization.

The check-then-put pattern on lines 208-210 is not atomic. Two threads could both see null and create separate ArrayList instances, with one overwriting the other.

🔧 Recommended fix using computeIfAbsent
-        // Initialize notification message list for this ID
-        if (notificationMessages.get(notId) == null) {
-            notificationMessages.put(notId, new ArrayList<>());
-        }
+        // Initialize notification message list for this ID (thread-safe)
+        notificationMessages.computeIfAbsent(notId, k -> new ArrayList<>());

Note: After this change, line 239's notificationMessages.get(notId).add(bundle) should also be synchronized or use a thread-safe list, since ArrayList is not thread-safe for concurrent modifications.

🧹 Nitpick comments (1)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)

172-200: Consider localizing the fallback message.

The hardcoded string "Encrypted message" on lines 175 and 194 should be moved to string resources for proper localization.

♻️ Suggested improvement
-            bundle.putString("message", "Encrypted message");
+            bundle.putString("message", mContext.getString(R.string.encrypted_message_fallback));

Add to res/values/strings.xml:

<string name="encrypted_message_fallback">Encrypted message</string>
📜 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 b588e4a and 2172f38.

📒 Files selected for processing (1)
  • android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java
⏰ 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 (7)
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (7)

62-62: Good use of volatile for thread visibility.

The volatile modifier ensures visibility of mBundle changes across threads, which is appropriate given the multi-threaded nature of push notification handling.


78-98: LGTM!

The simplified onReceived flow with proper validation and error handling is clean and appropriate.


138-160: Good thread-safety pattern with local bundle capture.

Capturing mBundle into a local bundle variable at the start of the method ensures consistent state throughout method execution, preventing potential race conditions.


226-229: LGTM!

Consistent synchronized update of mBundle maintains thread safety.


297-355: LGTM!

Consistent use of local bundle variable throughout buildNotification ensures thread-safe access and stable notification content during the build process.


455-555: LGTM!

The notificationStyle and notificationReply methods correctly use the passed bundle parameter, maintaining consistency with the thread-safe pattern established in buildNotification.


113-136: The callback invoked by LoadNotification.load() executes on the same background thread that called load() (the FCM message handler thread). The synchronized block in loadNotificationAndProcess correctly ensures thread-safe visibility of the mBundle update, and since processNotification() immediately reads mBundle into a local variable, there is no deadlock or race condition risk. The current implementation is thread-safe.

@diegolmello diegolmello requested a deployment to upload_experimental_android January 8, 2026 14:40 — with GitHub Actions Waiting
@github-actions
Copy link

github-actions bot commented Jan 8, 2026

Android Build Available

Rocket.Chat Experimental 4.68.1.108000

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

Copy link
Contributor

@OtavioStasiak OtavioStasiak left a comment

Choose a reason for hiding this comment

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

Looks good to me!

@diegolmello diegolmello closed this Jan 8, 2026
@diegolmello diegolmello mentioned this pull request Jan 8, 2026
10 tasks
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.

3 participants