Skip to content

chore: Merge 4.69.0 into master#6965

Merged
diegolmello merged 18 commits intomasterfrom
4.69.0-master
Feb 4, 2026
Merged

chore: Merge 4.69.0 into master#6965
diegolmello merged 18 commits intomasterfrom
4.69.0-master

Conversation

@diegolmello
Copy link
Member

@diegolmello diegolmello commented Feb 4, 2026

Proposed changes

Issue(s)

How to test or reproduce

Screenshots

Types of changes

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

Checklist

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

Further comments

Summary by CodeRabbit

Version 4.69.0 Release Notes

  • New Features

    • Avatar display in video conference call notifications
    • Show More functionality for action items in UI
  • Improvements

    • Enhanced E2E encrypted notification handling and reliability
    • Improved avatar loading and display throughout the app
    • Better connection status feedback during login
    • Enhanced thread messaging support with typing indicators
    • Improved browser selection interface

diegolmello and others added 18 commits January 6, 2026 14:52
* fix: formatting base64 images wrong

* chore: add base64 avatar storybook case

* chore: improve unit tests of getAvatarUrl

* chore: format code and fix lint issues [skip ci]

* fix: unit test

* cleanup

---------

Co-authored-by: OtavioStasiak <OtavioStasiak@users.noreply.github.com>
* fix: hide elements instead of conditional render

* chore: update snapshot test

* fix: remove useMemo

* chore: add case on storybook

* fix: snapshot test

* chore: format code and fix lint issues [skip ci]

---------

Co-authored-by: OtavioStasiak <OtavioStasiak@users.noreply.github.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Walkthrough

This PR implements automated changelog generation for releases, refactors Android and iOS notification systems to support independent operation with avatar/intent handling, adds thread-aware typing indicators, improves avatar URL handling for base64 data, and bumps the app version to 4.69.0 across all platforms.

Changes

Cohort / File(s) Summary
Changelog Generation Infrastructure
.github/workflows/generate-changelog.yml, .github/scripts/prepare-changelog.js, .github/actions/upload-android/action.yml, .github/actions/upload-ios/action.yml, android/fastlane/Fastfile, ios/fastlane/Fastfile
New workflow generates changelogs from git commits since last release tag, with fallback defaults. Upload actions download and prepare changelog artifacts. Android Fastfile skips metadata/images but uploads changelogs; iOS Fastfile truncates to 4000 chars and conditionally adds changelog to beta releases.
CI/CD Workflow Updates
.github/workflows/build-develop.yml, .github/workflows/build-official-android.yml
Added concurrency config and generate-changelog job to build-develop pipeline; all build jobs now depend on generate-changelog and pass trigger: develop. Official Android upload now pulls BUILD_VERSION from build-android instead of upload-hold.
Android Notification System Refactoring
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/NotificationHelper.java, android/app/src/main/java/chat/rocket/reactnative/notification/VideoConfNotification.kt
Reworked notifications to operate independently of React Native by relying consistently on mBundle; added avatar support via new fetchAvatarBitmap helper; extended Ejson with room name field and avatar URI generation methods (supporting both DM and room avatars); VideoConfNotification now fetches and displays caller avatars.
iOS Notification Service Enhancements
ios/NotificationService/NotificationService.swift, ios/NotificationService/Info.plist, ios/NotificationService/NotificationService.entitlements, ios/Shared/RocketChat/API/Request.swift
Introduced consolidated payload decoding, video conference intent integration with avatar enrichment, and message content fetching. Added INSendMessageIntent activation logic to support iOS communication features. Added User-Agent header abstraction via Bundle extension. Decryption support for E2E messages.
iOS Notification Entitlements & User Activity
ios/RocketChatRN/RocketChatRN.entitlements, ios/RocketChatRN/Info.plist, ios/NotificationService/NotificationService.entitlements, ios/NotificationService/Info.plist, ios/Shared/Extensions/Bundle+Extensions.swift
Added com.apple.developer.usernotifications.communication entitlement and NSUserActivityTypes with INSendMessageIntent to both main app and notification service; created Bundle.userAgent computed property for consistent User-Agent formatting.
iOS Project & Payload Model Updates
ios/RocketChatRN.xcodeproj/project.pbxproj, ios/Shared/Models/Payload.swift, ios/ReplyNotification.swift
Updated Xcode project build phases and pod framework references; added username field to Caller struct in payload model; improved reply notification failure handling with background task management and main-thread UI updates.
Typing Indicator Thread Support
app/actions/room.ts, app/containers/MessageComposer/components/ComposerInput.tsx, app/lib/services/restApi.ts, app/sagas/room.js
Extended userTyping action to include optional args with tmid field; ComposerInput now passes tmid when typing in threads; emitTyping passes args to server for version >= 4.0.0; saga forwards args to emitTyping.
Avatar & URL Handling
app/lib/methods/helpers/getAvatarUrl.ts, app/lib/methods/helpers/getAvatarUrl.test.ts, app/lib/methods/helpers/openLink.ts
Added early return in getAvatarURL for base64 data URIs; comprehensive test suite for avatar URL variants. Updated browser/in-app detection in openLink to use title-case values ('Chrome', 'Firefox', 'Brave', 'In_app').
Message Loading & Notifications
app/lib/methods/loadMessagesForRoom.ts, app/lib/notifications/index.ts
Removed COUNT_LIMIT gating and added showThreadMessages: false to batch fetch; added logging for missing ejson in notifications.
UI & Display Updates
app/containers/UIKit/Actions.tsx, app/containers/UIKit/UiKitModal.stories.tsx, app/containers/markdown/components/Timestamp.tsx, app/containers/MessageComposer/components/RecordAudio/ReviewButton.tsx, app/views/RoomsListView/components/Header.tsx, app/views/DefaultBrowserView/index.tsx, app/containers/Avatar/Avatar.stories.tsx
Actions component now always renders elements with hidden overflow logic for show-more; new story for Actions with show more. Timestamp parsing now memoizes timestampMs. ReviewButton icon color changed from fontWhite to fontDefault. Header now includes isLoggingIn in "Connecting" subtitle. DefaultBrowserView enables translation for 'In_app' and 'System_default'. Avatar.stories adds base64 image story variant.
Version Bumps
android/app/build.gradle, ios/RocketChatRN/Info.plist, ios/ShareRocketChatRN/Info.plist, package.json
Bumped app version to 4.69.0 across Android, iOS, and package.json.

Sequence Diagram(s)

sequenceDiagram
    participant GH as GitHub Actions
    participant Gen as Generate Changelog Job
    participant Artifact as Artifact Store
    participant Build as Build Jobs
    participant Upload as Upload Actions

    GH->>Gen: Trigger on workflow_call
    Gen->>Gen: Checkout with full history
    Gen->>Gen: Find latest release tag
    Gen->>Gen: Generate changelog.txt from commits
    Gen->>Artifact: Upload as release-changelog artifact
    
    Build->>Build: Run eslint & tests
    Build->>Build: Await generate-changelog
    
    Build->>Upload: Pass trigger: develop
    Upload->>Artifact: Download release-changelog
    Upload->>Upload: Prepare changelog metadata
    Upload->>Upload: Truncate to limits (Android: 500, iOS: 4000 chars)
    Upload->>Upload: Upload to Play Store / TestFlight
Loading
sequenceDiagram
    participant NotService as NotificationService
    participant Payload as Payload Decoder
    participant RCChat as RocketChat
    participant Intent as INSendMessageIntent
    participant Avatar as Avatar Fetcher
    participant FinalContent as Final Content

    NotService->>Payload: Decode ejson from notification
    alt Is Video Conference
        Payload->>RCChat: Initialize Rocketchat
        Payload->>Avatar: Fetch caller avatar
        Avatar->>FinalContent: Return Bitmap
        FinalContent->>Intent: Create INSendMessageIntent with avatar
        Intent->>Intent: Donate interaction
        Intent->>NotService: Deliver enriched content
    else Is Message with Content
        Payload->>NotService: Fetch message content from server
        NotService->>FinalContent: Deliver message content
    else General Payload
        NotService->>FinalContent: Process general payload
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • chore: changelog in beta release #6899 — Both PRs directly implement the changelog generation infrastructure (.github/workflows/generate-changelog.yml, prepare-changelog.js, and related upload action changes) with nearly identical code.
  • chore: Migrate to expo-notifications #6846 — Both PRs refactor the Android and iOS notification subsystems (CustomPushNotification, Ejson, NotificationHelper, NotificationService) with overlapping avatar and metadata handling logic.
  • chore: Bump version to 4.69.0 #6883 — Both PRs bump the app version to 4.69.0 across package.json, android/app/build.gradle, and iOS Info.plist files.

Suggested reviewers

  • OtavioStasiak

🐰 A changelog springs forth from commits past,
While rabbits hop through threads at last,
Avatars fetch with swift delight, 🖼️
Notifications shine both day and night,
Version bumped to 4.69—what a sight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.86% 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 'chore: Merge 4.69.0 into master' accurately describes the main purpose of this PR: merging the 4.69.0 release branch into the master branch, as confirmed by the source branch (4.69.0-master) and target branch (master).

✏️ 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 4.69.0-master

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Reviews will stop working after February 8, 2026 if the new IP is not added to your allowlist.


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 requested a deployment to experimental_ios_build February 4, 2026 14:08 — with GitHub Actions Waiting
@diegolmello diegolmello requested a deployment to experimental_android_build February 4, 2026 14:08 — with GitHub Actions Waiting
@diegolmello diegolmello requested a deployment to official_android_build February 4, 2026 14:08 — with GitHub Actions Waiting
@diegolmello diegolmello merged commit 76f5a7a into master Feb 4, 2026
4 of 10 checks passed
@diegolmello diegolmello deleted the 4.69.0-master branch February 4, 2026 14:09
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: 9

Caution

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

⚠️ Outside diff range comments (2)
ios/ReplyNotification.swift (1)

103-133: ⚠️ Potential issue | 🟠 Major

Potential double completion handler invocation.

If the expiration handler fires (lines 105-111), it calls completionHandler(). Subsequently, when sendMessage completes, the callback's defer block (lines 116-122) will call completionHandler() again. Double-calling the completion handler is undefined behavior and may cause crashes or other issues.

Consider using a flag to ensure the completion handler is called exactly once:

🛡️ Proposed fix to prevent double completion
     var backgroundTask: UIBackgroundTaskIdentifier = .invalid
+    var didComplete = false
     backgroundTask = UIApplication.shared.beginBackgroundTask {
       // Expiration handler - called if system needs to reclaim resources
       if backgroundTask != .invalid {
         UIApplication.shared.endBackgroundTask(backgroundTask)
         backgroundTask = .invalid
       }
-      completionHandler()
+      if !didComplete {
+        didComplete = true
+        completionHandler()
+      }
     }
     
     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()
+          if !didComplete {
+            didComplete = true
+            completionHandler()
+          }
         }
app/lib/methods/helpers/openLink.ts (1)

55-66: ⚠️ Potential issue | 🟠 Major

Keep backward compatibility for the in-app browser flag.

If prior versions stored inApp (or other casing), switching to 'In_app' will route users to external browsers unexpectedly. Consider accepting both values.

♻️ Suggested compatibility check
-		if (browser === 'In_app') {
+		if (browser === 'In_app' || browser === 'inApp') {
🤖 Fix all issues with AI agents
In @.github/scripts/prepare-changelog.js:
- Around line 3-4: The script reads process.env.BUILD_VERSION into buildVersion
and reads changelog.txt into input without validation; add explicit checks:
verify buildVersion is defined (process.env.BUILD_VERSION) and exit or throw a
clear error message if missing, and verify changelog.txt exists (use
fs.existsSync or try/catch around fs.readFileSync for input) and surface a
helpful ENOENT-style message instead of crashing; also ensure any downstream use
of buildVersion to construct the output path uses the validated buildVersion to
avoid writing to "undefined.txt".

In `@app/containers/MessageComposer/components/RecordAudio/ReviewButton.tsx`:
- Line 25: The CustomIcon in ReviewButton.tsx is using a non-semantic color
token causing insufficient contrast; update the icon color prop from
colors.fontDefault to the semantic token colors.buttonFontPrimary (i.e., change
the prop on <CustomIcon name={'arrow-right'} size={24} color={...} /> to use
colors.buttonFontPrimary) so the primary button icon meets contrast requirements
across themes.

In `@app/lib/methods/helpers/openLink.ts`:
- Around line 26-35: The branch that sets schemeUrl uses direct title-case
comparisons on the browser variable, which will miss legacy stored values like
"googlechrome", "firefox", "brave" or values with trailing ":"; normalize the
browser value first (trim and toLowerCase, strip trailing ":"), map known legacy
strings (e.g., "googlechrome" → "chrome") to the canonical names, then use the
normalized value in the existing branching that assigns schemeUrl (referencing
browser, schemeUrl, scheme.chrome/scheme.chromeSecure, scheme.firefox,
scheme.brave, protocol, and isSecure) so legacy preferences route to the
intended scheme logic.

In `@app/lib/methods/loadMessagesForRoom.ts`:
- Around line 23-27: The early-return guard currently checks allMessages.length
>= COUNT which can underfetch main messages if thread replies are included;
update the guard in loadMessagesForRoom to check mainMessagesCount >= COUNT (the
same counter used in the recursion stop condition) so the function only stops
once the desired number of non-thread messages has been gathered; locate the
block that builds params (including showThreadMessages: false) and replace the
allMessages length check with mainMessagesCount to ensure consistency with the
existing recursion logic that uses mainMessagesCount and COUNT.
- Line 79: In loadMessagesForRoom, update the pagination check that currently
reads `if (!lastMessageRecord && data.length === COUNT)` to use a
greater-or-equal comparison so thread messages that push the batch past COUNT
still trigger the load-more marker; change the condition to `data.length >=
COUNT` referencing the same `lastMessageRecord`, `data`, and `COUNT` variables
so the function correctly detects when the page limit has been hit.

In `@app/lib/services/restApi.ts`:
- Around line 953-963: The parameter type for emitTyping is incorrect: change
the signature from room: IRoom to room: string (e.g., rename to rid if you
prefer) so callers that pass a room id string (as seen in app/sagas/room.js
where emitTyping(rid, ... ) is used) match the function type; update the
declaration of emitTyping (and any exported type references) to accept a string,
keep the rest of the implementation (template literals using `${room}` and args
handling) unchanged, and run TypeScript checks to ensure no other call sites
expect an IRoom object.

In `@ios/NotificationService/NotificationService.swift`:
- Around line 37-39: The early return in processVideoConf when
bestAttemptContent is nil fails to invoke contentHandler, which can cause the
extension to be terminated and the notification not delivered; update
processVideoConf to always call contentHandler before returning (e.g., if
bestAttemptContent is nil, call contentHandler?(bestAttemptContent) or otherwise
pass the original UNMutableNotificationContent) so the system receives a
response; ensure you reference bestAttemptContent and contentHandler in the fix
and keep the rest of processVideoConf logic unchanged.
- Around line 75-77: In processPayload(payload: Payload) the early guard return
for bestAttemptContent currently exits without invoking contentHandler which can
cause the notification extension to time out; update processPayload to call
contentHandler with the original request.content (or bestAttemptContent if
available) before returning when bestAttemptContent is nil, mirroring the fix
applied to processVideoConf and ensuring contentHandler is always invoked on
every exit path.

In `@ios/RocketChatRN.xcodeproj/project.pbxproj`:
- Around line 1575-1591: The shell script in the PBXShellScriptBuildPhase
(shellScript surrounding FRAMEWORK_EXECUTABLE_PATH) incorrectly uses command
substitution in the lipo checks and contains a stray "echo $"—replace any "if
$(lipo ... -verify_arch ...)" usage with a direct status check "if lipo ...
-verify_arch ...; then" so the exit code is tested rather than executing output,
and remove/replace the stray "echo $" with a plain "echo" (apply the same fixes
to both ShellScript phases where lipo -verify_arch and the stray echo occur).
🧹 Nitpick comments (8)
app/containers/Avatar/Avatar.stories.tsx (1)

15-16: Consider shrinking or externalizing the base64 fixture.

The inline base64 blob is extremely large for a story file and makes diffs, navigation, and tooling heavier. A smaller base64 sample (e.g., a tiny 1×1 PNG) or an externalized fixture constant would keep the story focused while still covering data‑URI support.

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

77-79: Good addition for observability.

The else branch correctly logs when ejson is missing from the payload before falling through to the existing appInit() dispatch. This improves debugging without changing behavior.

Optional: Consider logging additional context

If this warning becomes relevant during debugging, you might find it helpful to log a bit more context about the notification structure:

 } else {
-   console.warn('[notifications/index.ts] No ejson in payload, dispatching appInit');
+   console.warn('[notifications/index.ts] No ejson in payload, dispatching appInit', {
+     hasPayload: !!push?.payload,
+     identifier: push?.identifier
+   });
 }

This would help distinguish between completely malformed notifications vs. intentional notifications without ejson.

app/containers/UIKit/Actions.tsx (1)

9-14: Consider accessibility for hidden elements.

Elements hidden with height: 0 remain in the accessibility tree and may still be announced by screen readers. For proper accessibility, consider hiding them from assistive technologies:

♻️ Suggested improvement
 const styles = StyleSheet.create({
 	hidden: {
 		overflow: 'hidden',
 		height: 0
 	}
 });
+
+const hiddenAccessibilityProps = {
+	accessible: false,
+	importantForAccessibility: 'no-hide-descendants' as const
+};

Then in the View:

<View
  key={element.actionId || `action-${index}`}
  style={!isVisible ? styles.hidden : undefined}
  {...(!isVisible ? hiddenAccessibilityProps : {})}
>
android/app/src/main/java/chat/rocket/reactnative/notification/Ejson.java (1)

56-76: Consider URL-encoding token and uid query parameters.

The rc_token and rc_uid values are appended directly without URL encoding. While these values are typically alphanumeric, URL-encoding them would provide defense-in-depth against potential special characters.

♻️ Proposed fix
         String finalUri = server + avatarPath + "?format=png&size=100";
         if (!userToken.isEmpty() && !uid.isEmpty()) {
-            finalUri += "&rc_token=" + userToken + "&rc_uid=" + uid;
+            try {
+                finalUri += "&rc_token=" + URLEncoder.encode(userToken, "UTF-8") 
+                         + "&rc_uid=" + URLEncoder.encode(uid, "UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                // UTF-8 is always supported, but handle gracefully
+                finalUri += "&rc_token=" + userToken + "&rc_uid=" + uid;
+            }
         }
android/app/src/main/java/chat/rocket/reactnative/notification/CustomPushNotification.java (1)

455-459: Unused variable bundleEjson.

The bundleEjson variable is parsed but not used - conversationTitle is just set to title. If this was intended to support room name display (using ejson.name), the implementation appears incomplete. Otherwise, this variable can be removed.

♻️ Option A: Remove unused variable
             String title = bundle.getString("title");
-            // Determine the correct conversation title based on notification type
-            Ejson bundleEjson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
             String conversationTitle = title;
             messageStyle.setConversationTitle(conversationTitle);
♻️ Option B: Use ejson.name if available
             String title = bundle.getString("title");
             // Determine the correct conversation title based on notification type
             Ejson bundleEjson = safeFromJson(bundle.getString("ejson", "{}"), Ejson.class);
-            String conversationTitle = title;
+            String conversationTitle = (bundleEjson != null && bundleEjson.name != null && !bundleEjson.name.isEmpty())
+                    ? bundleEjson.name
+                    : title;
             messageStyle.setConversationTitle(conversationTitle);
app/lib/methods/helpers/getAvatarUrl.ts (1)

28-30: Simplify the redundant truthiness check.

The condition !!avatar && avatar?.startsWith('data:') has redundant guards. If avatar is falsy, the && short-circuits, making the optional chaining unnecessary. If avatar is truthy, the optional chaining is also unnecessary.

♻️ Suggested simplification
-	if (!!avatar && avatar?.startsWith('data:')) {
+	if (avatar?.startsWith('data:')) {
 		return avatar;
 	}
ios/NotificationService/NotificationService.swift (2)

89-89: Unnecessary optional cast.

bestAttemptContent.body is already a String, so the as? String cast is redundant.

♻️ Suggested simplification
-            if let body = bestAttemptContent.body as? String {
+            let body = bestAttemptContent.body
+            if !body.isEmpty {

207-207: Prefer .isEmpty over .count > 0.

Using .isEmpty is more idiomatic in Swift and potentially more efficient.

♻️ Suggested change
-            if identifiersToRemove.count > 0 {
+            if !identifiersToRemove.isEmpty {

Comment on lines +3 to +4
const buildVersion = process.env.BUILD_VERSION;
const input = fs.readFileSync("changelog.txt", "utf8");
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add validation for required inputs.

If BUILD_VERSION is not set, the script will write to android/fastlane/metadata/android/en-US/changelogs/undefined.txt. Similarly, if changelog.txt doesn't exist, the script crashes with an unhelpful ENOENT error.

🛡️ Proposed fix to add input validation
 const fs = require("fs");
 
 const buildVersion = process.env.BUILD_VERSION;
+if (!buildVersion) {
+  console.error("BUILD_VERSION environment variable is required");
+  process.exit(1);
+}
+
+if (!fs.existsSync("changelog.txt")) {
+  console.error("changelog.txt not found");
+  process.exit(1);
+}
+
 const input = fs.readFileSync("changelog.txt", "utf8");
📝 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
const buildVersion = process.env.BUILD_VERSION;
const input = fs.readFileSync("changelog.txt", "utf8");
const buildVersion = process.env.BUILD_VERSION;
if (!buildVersion) {
console.error("BUILD_VERSION environment variable is required");
process.exit(1);
}
if (!fs.existsSync("changelog.txt")) {
console.error("changelog.txt not found");
process.exit(1);
}
const input = fs.readFileSync("changelog.txt", "utf8");
🤖 Prompt for AI Agents
In @.github/scripts/prepare-changelog.js around lines 3 - 4, The script reads
process.env.BUILD_VERSION into buildVersion and reads changelog.txt into input
without validation; add explicit checks: verify buildVersion is defined
(process.env.BUILD_VERSION) and exit or throw a clear error message if missing,
and verify changelog.txt exists (use fs.existsSync or try/catch around
fs.readFileSync for input) and surface a helpful ENOENT-style message instead of
crashing; also ensure any downstream use of buildVersion to construct the output
path uses the validated buildVersion to avoid writing to "undefined.txt".

hitSlop={hitSlop}>
<View accessible accessibilityLabel={i18n.t('Review_message')} accessibilityRole='button'>
<CustomIcon name={'arrow-right'} size={24} color={colors.fontWhite} />
<CustomIcon name={'arrow-right'} size={24} color={colors.fontDefault} />
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.tsx" -o -name "*.ts" | grep -i "color\|theme" | head -20

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1370


🏁 Script executed:

find . -type f \( -name "ReviewButton.tsx" -o -name "*color*" -o -name "*theme*" \) | head -30

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 2932


🏁 Script executed:

cat -n app/containers/MessageComposer/components/RecordAudio/ReviewButton.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1370


🏁 Script executed:

cat -n app/lib/constants/colors.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 10667


🏁 Script executed:

cat -n app/theme.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1371


🏁 Script executed:

head -100 app/definitions/ITheme.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 268


🏁 Script executed:

python3 << 'EOF'
from math import sqrt

def hex_to_rgb(hex_color):
    hex_color = hex_color.lstrip('#')
    return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))

def relative_luminance(r, g, b):
    r, g, b = r/255, g/255, b/255
    r = r/12.92 if r <= 0.03928 else ((r+0.055)/1.055)**2.4
    g = g/12.92 if g <= 0.03928 else ((g+0.055)/1.055)**2.4
    b = b/12.92 if b <= 0.03928 else ((b+0.055)/1.055)**2.4
    return 0.2126*r + 0.7152*g + 0.0722*b

def contrast_ratio(color1, color2):
    r1, g1, b1 = hex_to_rgb(color1)
    r2, g2, b2 = hex_to_rgb(color2)
    l1 = relative_luminance(r1, g1, b1)
    l2 = relative_luminance(r2, g2, b2)
    lighter = max(l1, l2)
    darker = min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)

# Light Theme
light_bg = '#156FF5'
light_font_default = '#2F343D'
light_button_font_primary = '#FFFFFF'

# Dark Theme
dark_bg = '#095AD2'
dark_font_default = '#E4E7EA'
dark_button_font_primary = '#FFFFFF'

# Black Theme
black_bg = '#3976D1'
black_font_default = '#E4E7EA'
black_button_font_primary = '#FFFFFF'

print("=== Light Theme ===")
print(f"Background: {light_bg}, Icon: {light_font_default} (current)")
ratio_light_current = contrast_ratio(light_bg, light_font_default)
print(f"Contrast Ratio: {ratio_light_current:.2f}:1")
print(f"WCAG AA (4.5:1) Pass: {ratio_light_current >= 4.5}")
print(f"WCAG AAA (7:1) Pass: {ratio_light_current >= 7}")
print()

print(f"Background: {light_bg}, Icon: {light_button_font_primary} (with buttonFontPrimary)")
ratio_light_fixed = contrast_ratio(light_bg, light_button_font_primary)
print(f"Contrast Ratio: {ratio_light_fixed:.2f}:1")
print(f"WCAG AA (4.5:1) Pass: {ratio_light_fixed >= 4.5}")
print(f"WCAG AAA (7:1) Pass: {ratio_light_fixed >= 7}")
print()

print("=== Dark Theme ===")
print(f"Background: {dark_bg}, Icon: {dark_font_default} (current)")
ratio_dark_current = contrast_ratio(dark_bg, dark_font_default)
print(f"Contrast Ratio: {ratio_dark_current:.2f}:1")
print(f"WCAG AA (4.5:1) Pass: {ratio_dark_current >= 4.5}")
print(f"WCAG AAA (7:1) Pass: {ratio_dark_current >= 7}")
print()

print(f"Background: {dark_bg}, Icon: {dark_button_font_primary} (with buttonFontPrimary)")
ratio_dark_fixed = contrast_ratio(dark_bg, dark_button_font_primary)
print(f"Contrast Ratio: {ratio_dark_fixed:.2f}:1")
print(f"WCAG AA (4.5:1) Pass: {ratio_dark_fixed >= 4.5}")
print(f"WCAG AAA (7:1) Pass: {ratio_dark_fixed >= 7}")
print()

print("=== Black Theme ===")
print(f"Background: {black_bg}, Icon: {black_font_default} (current)")
ratio_black_current = contrast_ratio(black_bg, black_font_default)
print(f"Contrast Ratio: {ratio_black_current:.2f}:1")
print(f"WCAG AA (4.5:1) Pass: {ratio_black_current >= 4.5}")
print(f"WCAG AAA (7:1) Pass: {ratio_black_current >= 7}")
print()

print(f"Background: {black_bg}, Icon: {black_button_font_primary} (with buttonFontPrimary)")
ratio_black_fixed = contrast_ratio(black_bg, black_button_font_primary)
print(f"Contrast Ratio: {ratio_black_fixed:.2f}:1")
print(f"WCAG AA (4.5:1) Pass: {ratio_black_fixed >= 4.5}")
print(f"WCAG AAA (7:1) Pass: {ratio_black_fixed >= 7}")
EOF

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 921


Use semantic color token for primary button icon.

The current colors.fontDefault creates insufficient contrast with colors.buttonBackgroundPrimaryDefault in light and black themes. Light theme achieves only 2.75:1 contrast (WCAG AA requires 4.5:1); black theme achieves 3.61:1. A semantic buttonFontPrimary token exists (#FFFFFF) that provides proper contrast across all themes (4.54:1 light, 6.17:1 dark, 4.48:1 black). Change color={colors.fontDefault} to color={colors.buttonFontPrimary}.

🤖 Prompt for AI Agents
In `@app/containers/MessageComposer/components/RecordAudio/ReviewButton.tsx` at
line 25, The CustomIcon in ReviewButton.tsx is using a non-semantic color token
causing insufficient contrast; update the icon color prop from
colors.fontDefault to the semantic token colors.buttonFontPrimary (i.e., change
the prop on <CustomIcon name={'arrow-right'} size={24} color={...} /> to use
colors.buttonFontPrimary) so the primary button icon meets contrast requirements
across themes.

Comment on lines +26 to 35
if (browser === 'Chrome') {
if (!isSecure) {
schemeUrl = url.replace(protocol, scheme.chrome);
} else {
schemeUrl = url.replace(protocol, scheme.chromeSecure);
}
} else if (browser === 'firefox') {
} else if (browser === 'Firefox') {
schemeUrl = `${scheme.firefox}//open-url?url=${url}`;
} else if (browser === 'brave') {
} else if (browser === 'Brave') {
schemeUrl = `${scheme.brave}//open-url?url=${url}`;
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

Normalize stored browser values to avoid preference regressions.

Title-case comparisons will miss existing stored values like googlechrome, firefox, or brave (and any with trailing :), which can bypass the intended scheme routing. Consider normalizing and mapping legacy values before branching.

✅ Suggested normalization
-const appSchemeURL = (url: string, browser: string): string => {
+const appSchemeURL = (url: string, browser: string): string => {
 	let schemeUrl = url;
 	const parsedUrl = parse(url, true);
 	const { protocol } = parsedUrl;
 	const isSecure = ['https:'].includes(protocol);
+	const normalized = browser.toLowerCase();

-	if (browser === 'Chrome') {
+	if (normalized === 'chrome' || normalized === 'googlechrome') {
 		if (!isSecure) {
 			schemeUrl = url.replace(protocol, scheme.chrome);
 		} else {
 			schemeUrl = url.replace(protocol, scheme.chromeSecure);
 		}
-	} else if (browser === 'Firefox') {
+	} else if (normalized === 'firefox') {
 		schemeUrl = `${scheme.firefox}//open-url?url=${url}`;
-	} else if (browser === 'Brave') {
+	} else if (normalized === 'brave') {
 		schemeUrl = `${scheme.brave}//open-url?url=${url}`;
 	}
📝 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
if (browser === 'Chrome') {
if (!isSecure) {
schemeUrl = url.replace(protocol, scheme.chrome);
} else {
schemeUrl = url.replace(protocol, scheme.chromeSecure);
}
} else if (browser === 'firefox') {
} else if (browser === 'Firefox') {
schemeUrl = `${scheme.firefox}//open-url?url=${url}`;
} else if (browser === 'brave') {
} else if (browser === 'Brave') {
schemeUrl = `${scheme.brave}//open-url?url=${url}`;
const appSchemeURL = (url: string, browser: string): string => {
let schemeUrl = url;
const parsedUrl = parse(url, true);
const { protocol } = parsedUrl;
const isSecure = ['https:'].includes(protocol);
const normalized = browser.toLowerCase();
if (normalized === 'chrome' || normalized === 'googlechrome') {
if (!isSecure) {
schemeUrl = url.replace(protocol, scheme.chrome);
} else {
schemeUrl = url.replace(protocol, scheme.chromeSecure);
}
} else if (normalized === 'firefox') {
schemeUrl = `${scheme.firefox}//open-url?url=${url}`;
} else if (normalized === 'brave') {
schemeUrl = `${scheme.brave}//open-url?url=${url}`;
}
🤖 Prompt for AI Agents
In `@app/lib/methods/helpers/openLink.ts` around lines 26 - 35, The branch that
sets schemeUrl uses direct title-case comparisons on the browser variable, which
will miss legacy stored values like "googlechrome", "firefox", "brave" or values
with trailing ":"; normalize the browser value first (trim and toLowerCase,
strip trailing ":"), map known legacy strings (e.g., "googlechrome" → "chrome")
to the canonical names, then use the normalized value in the existing branching
that assigns schemeUrl (referencing browser, schemeUrl,
scheme.chrome/scheme.chromeSecure, scheme.firefox, scheme.brave, protocol, and
isSecure) so legacy preferences route to the intended scheme logic.

Comment on lines +23 to +27
if (allMessages.length >= COUNT) {
return;
}

const params = { roomId, count: COUNT, ...(lastTs && { latest: lastTs }) };
const params = { roomId, showThreadMessages: false, count: COUNT, ...(lastTs && { latest: lastTs }) };
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n app/lib/methods/loadMessagesForRoom.ts | head -100

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3563


🌐 Web query:

Rocket.Chat REST API channels.history groups.history im.history showThreadMessages parameter support documentation

💡 Result:

  • /api/v1/channels.history: Supports showThreadMessages (boolean) — documented as “Whether thread messages should be included in the response.” [1]
  • /api/v1/dm.history (the current DM history endpoint in the docs): Supports showThreadMessages (boolean) — documented as “Choose to display thread message in the message history.” [2]
  • /api/v1/groups.history: No showThreadMessages parameter is documented on the endpoint’s query-parameter list (it shows roomId, latest, oldest, inclusive, unreads, offset, count, etc., but not showThreadMessages). [3]
  • If you need thread messages explicitly, Rocket.Chat also provides /api/v1/chat.getThreadMessages (“Get thread messages”). [4]

Sources: Get Channel History [1], DM History [2], Get Group History [3], Get Thread Messages [4].


Guard logic mismatch can cause underfetching of main messages.

The guard at line 23 exits when allMessages.length >= COUNT, but the recursion at line 56 only stops when mainMessagesCount >= COUNT. This mismatch is problematic because allMessages includes both main messages and thread replies (if they slip through), while mainMessagesCount only counts messages without tmid.

If the showThreadMessages: false parameter is not honored by the server—particularly for groups.history where it is not even documented—the fetch will stop early while mainMessagesCount remains below 50, returning fewer main messages than intended.

Change the guard to check mainMessagesCount instead:

Suggested fix
-		if (allMessages.length >= COUNT) {
+		if (mainMessagesCount >= COUNT) {
 			return;
 		}
📝 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
if (allMessages.length >= COUNT) {
return;
}
const params = { roomId, count: COUNT, ...(lastTs && { latest: lastTs }) };
const params = { roomId, showThreadMessages: false, count: COUNT, ...(lastTs && { latest: lastTs }) };
if (mainMessagesCount >= COUNT) {
return;
}
const params = { roomId, showThreadMessages: false, count: COUNT, ...(lastTs && { latest: lastTs }) };
🤖 Prompt for AI Agents
In `@app/lib/methods/loadMessagesForRoom.ts` around lines 23 - 27, The
early-return guard currently checks allMessages.length >= COUNT which can
underfetch main messages if thread replies are included; update the guard in
loadMessagesForRoom to check mainMessagesCount >= COUNT (the same counter used
in the recursion stop condition) so the function only stops once the desired
number of non-thread messages has been gathered; locate the block that builds
params (including showThreadMessages: false) and replace the allMessages length
check with mainMessagesCount to ensure consistency with the existing recursion
logic that uses mainMessagesCount and COUNT.

const lastMessage = data[data.length - 1];
const lastMessageRecord = await getMessageById(lastMessage._id as string);
if (!lastMessageRecord && (data.length === COUNT || data.length >= COUNT_LIMIT)) {
if (!lastMessageRecord && data.length === COUNT) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine the file in question
cat -n app/lib/methods/loadMessagesForRoom.ts | head -100

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3563


🌐 Web query:

Rocket.Chat REST API channels.history maximum count parameter behavior pagination

💡 Result:

For GET /api/v1/channels.history, pagination is offset + count:

Maximum / “why doesn’t count return more?”

Rocket.Chat caps count via the workspace REST API setting:

  • Max Record Amount (API_Upper_Count_Limit) is the ceiling; if you request a higher count, Rocket.Chat clamps it down to this limit. (legal.pimenta.chat)
  • Default Count (API_Default_Count) is used if you omit count. (legal.pimenta.chat)

“Get everything” behavior (count=0)

  • If Allow Getting Everything (API_Allow_Infinite_Count) is enabled, you may use count=0 to return all records for that endpoint. (docs.rocket.chat)
  • If it’s disabled, count=0 returns 0 results. (docs.rocket.chat)

Practical pagination pattern

  1. Choose a stable sort (e.g., by timestamp descending). (developer.rocket.chat)
  2. Call with count = N (≤ Max Record Amount) and offset = 0.
  3. Repeat with offset += N until the response returns fewer than N messages. (developer.rocket.chat)

Citations:


🏁 Script executed:

# Look at the fetchBatch function more carefully and trace the logic
sed -n '13,64p' app/lib/methods/loadMessagesForRoom.ts | cat -n

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1809


Use >= COUNT to detect pagination limit.

The strict equality check misses cases where data.length exceeds COUNT due to thread messages included in the batch. Since thread messages aren't filtered from the accumulated result, the final batch can push the count beyond 50 while still indicating pagination should stop. Change line 79 to data.length >= COUNT to correctly identify when the message load has hit the COUNT threshold and a load-more marker is needed for older messages.

Fix
-				if (!lastMessageRecord && data.length === COUNT) {
+				if (!lastMessageRecord && data.length >= COUNT) {
📝 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
if (!lastMessageRecord && data.length === COUNT) {
if (!lastMessageRecord && data.length >= COUNT) {
🤖 Prompt for AI Agents
In `@app/lib/methods/loadMessagesForRoom.ts` at line 79, In loadMessagesForRoom,
update the pagination check that currently reads `if (!lastMessageRecord &&
data.length === COUNT)` to use a greater-or-equal comparison so thread messages
that push the batch past COUNT still trigger the load-more marker; change the
condition to `data.length >= COUNT` referencing the same `lastMessageRecord`,
`data`, and `COUNT` variables so the function correctly detects when the page
limit has been hit.

Comment on lines +953 to 963
export const emitTyping = (room: IRoom, typing = true, args: { tmid?: string } = {}) => {
const { login, settings, server } = reduxStore.getState();
const { UI_Use_Real_Name } = settings;
const { version: serverVersion } = server;
const { user } = login;
const name = UI_Use_Real_Name ? user.name : user.username;
if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '4.0.0')) {
return sdk.methodCall('stream-notify-room', `${room}/user-activity`, name, typing ? ['user-typing'] : []);
return sdk.methodCall('stream-notify-room', `${room}/user-activity`, name, typing ? ['user-typing'] : [], args);
}
return sdk.methodCall('stream-notify-room', `${room}/typing`, name, typing);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "emitTyping\(" --type ts --type js -C 2

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 544


🏁 Script executed:

# Check the context around the emitTyping calls in room.js
head -100 app/sagas/room.js | tail -70

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1848


🏁 Script executed:

# Check the actual function signature and imports in restApi.ts around line 953
sed -n '1,30p' app/lib/services/restApi.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1276


🏁 Script executed:

# Search for IRoom type definition
rg -n "type IRoom|interface IRoom" --type ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 4880


🏁 Script executed:

# Check the IRoom interface definition
sed -n '18,100p' app/definitions/IRoom.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 1916


🏁 Script executed:

# Check the IRoom interface in app/reducers/room.ts
sed -n '1,30p' app/reducers/room.ts

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 767


🏁 Script executed:

# Check what IRoom is actually imported in restApi.ts - is it from definitions or local?
sed -n '1,20p' app/lib/services/restApi.ts | grep -A 5 "import"

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 636


Type mismatch: room parameter should accept string, not IRoom object.

The function is called in app/sagas/room.js lines 39 and 57 passing rid as a string (e.g., emitTyping(rid, false) and emitTyping(rid, status, args)), but the signature declares room: IRoom. The function body uses room as a string in template literals (`${room}/user-activity`), confirming the parameter should be a string rid, not an IRoom object from app/definitions/IRoom.ts.

🔧 Suggested fix
-export const emitTyping = (room: IRoom, typing = true, args: { tmid?: string } = {}) => {
+export const emitTyping = (rid: string, typing = true, args: { tmid?: string } = {}) => {
	const { login, settings, server } = reduxStore.getState();
	const { UI_Use_Real_Name } = settings;
	const { version: serverVersion } = server;
	const { user } = login;
	const name = UI_Use_Real_Name ? user.name : user.username;
	if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '4.0.0')) {
-		return sdk.methodCall('stream-notify-room', `${room}/user-activity`, name, typing ? ['user-typing'] : [], args);
+		return sdk.methodCall('stream-notify-room', `${rid}/user-activity`, name, typing ? ['user-typing'] : [], args);
	}
-	return sdk.methodCall('stream-notify-room', `${room}/typing`, name, typing);
+	return sdk.methodCall('stream-notify-room', `${rid}/typing`, name, typing);
};
🤖 Prompt for AI Agents
In `@app/lib/services/restApi.ts` around lines 953 - 963, The parameter type for
emitTyping is incorrect: change the signature from room: IRoom to room: string
(e.g., rename to rid if you prefer) so callers that pass a room id string (as
seen in app/sagas/room.js where emitTyping(rid, ... ) is used) match the
function type; update the declaration of emitTyping (and any exported type
references) to accept a string, keep the rest of the implementation (template
literals using `${room}` and args handling) unchanged, and run TypeScript checks
to ensure no other call sites expect an IRoom object.

Comment on lines +37 to 39
func processVideoConf(payload: Payload) {
guard let bestAttemptContent = bestAttemptContent else { return }

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing contentHandler call on early return may cause notification delivery failure.

If bestAttemptContent is nil, the function returns without calling contentHandler. Notification Service Extensions must always call the content handler before returning, otherwise the system will terminate the extension after ~30 seconds and deliver the original notification with potential delays.

🐛 Proposed fix
 func processVideoConf(payload: Payload) {
-    guard let bestAttemptContent = bestAttemptContent else { return }
+    guard let bestAttemptContent = bestAttemptContent else {
+        contentHandler?(UNNotificationContent())
+        return
+    }

Alternatively, if you want to deliver the original notification:

guard let bestAttemptContent = bestAttemptContent else {
    if let original = self.bestAttemptContent {
        contentHandler?(original)
    }
    return
}
📝 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
func processVideoConf(payload: Payload) {
guard let bestAttemptContent = bestAttemptContent else { return }
func processVideoConf(payload: Payload) {
guard let bestAttemptContent = bestAttemptContent else {
contentHandler?(UNNotificationContent())
return
}
🤖 Prompt for AI Agents
In `@ios/NotificationService/NotificationService.swift` around lines 37 - 39, The
early return in processVideoConf when bestAttemptContent is nil fails to invoke
contentHandler, which can cause the extension to be terminated and the
notification not delivered; update processVideoConf to always call
contentHandler before returning (e.g., if bestAttemptContent is nil, call
contentHandler?(bestAttemptContent) or otherwise pass the original
UNMutableNotificationContent) so the system receives a response; ensure you
reference bestAttemptContent and contentHandler in the fix and keep the rest of
processVideoConf logic unchanged.

Comment on lines 75 to +77
func processPayload(payload: Payload) {
// If is a encrypted message
if payload.messageType == .e2e {
if let rid = payload.rid {
let decryptedMessage: String?

if let content = payload.content, (content.algorithm == "rc.v1.aes-sha2" || content.algorithm == "rc.v2.aes-sha2") {
decryptedMessage = rocketchat?.decryptContent(rid: rid, content: content)
} else if let msg = payload.msg, !msg.isEmpty {
// Fallback to msg field
decryptedMessage = rocketchat?.decryptContent(rid: rid, content: EncryptedContent(algorithm: "rc.v1.aes-sha2", ciphertext: msg, kid: nil, iv: nil))
} else {
decryptedMessage = nil
guard let bestAttemptContent = bestAttemptContent else { return }

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

Same issue: Missing contentHandler call on early return.

This guard has the same problem as processVideoConf - returning without calling contentHandler will cause the extension to time out.

🐛 Proposed fix
 func processPayload(payload: Payload) {
-    guard let bestAttemptContent = bestAttemptContent else { return }
+    guard let bestAttemptContent = bestAttemptContent else {
+        contentHandler?(UNNotificationContent())
+        return
+    }
🤖 Prompt for AI Agents
In `@ios/NotificationService/NotificationService.swift` around lines 75 - 77, In
processPayload(payload: Payload) the early guard return for bestAttemptContent
currently exits without invoking contentHandler which can cause the notification
extension to time out; update processPayload to call contentHandler with the
original request.content (or bestAttemptContent if available) before returning
when bestAttemptContent is nil, mirroring the fix applied to processVideoConf
and ensuring contentHandler is always invoked on every exit path.

Comment on lines +1575 to 1591
1E1EA8082326CCE300E22452 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh\"\n";
showEnvVarsInLog = 0;
shellScript = "echo \"Target architectures: $ARCHS\"\n\nAPP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\nFRAMEWORK_TMP_PATH=\"$FRAMEWORK_EXECUTABLE_PATH-tmp\"\n\n# remove simulator's archs if location is not simulator's directory\ncase \"${TARGET_BUILD_DIR}\" in\n*\"iphonesimulator\")\necho \"No need to remove archs\"\n;;\n*)\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"i386\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"i386\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"i386 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"x86_64\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"x86_64\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"x86_64 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\n;;\nesac\n\necho \"Completed for executable $FRAMEWORK_EXECUTABLE_PATH\"\necho $\n\ndone\n";
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and get its size
wc -l ios/RocketChatRN.xcodeproj/project.pbxproj

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 125


🏁 Script executed:

# Read the specified line ranges to verify the issues
echo "=== Lines 1575-1591 ==="
sed -n '1575,1591p' ios/RocketChatRN.xcodeproj/project.pbxproj

echo ""
echo "=== Lines 1803-1819 ==="
sed -n '1803,1819p' ios/RocketChatRN.xcodeproj/project.pbxproj

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 3486


Fix the lipo conditionals and stray echo in the framework‑thinning script (2 occurrences).

The if $(lipo ...) pattern uses command substitution, which executes the command's output as a shell command—this fails because lipo -verify_arch outputs text like "valid on arm64", not executable commands. Use if lipo ...; then to properly check exit status. Also remove the stray echo $ (should be echo).

Both issues appear at lines 1575–1591 and 1803–1819.

Proposed fix (apply to both ShellScript phases)
- if $(lipo "$FRAMEWORK_EXECUTABLE_PATH" -verify_arch "i386") ; then
+ if lipo "$FRAMEWORK_EXECUTABLE_PATH" -verify_arch "i386" ; then
   lipo -output "$FRAMEWORK_TMP_PATH" -remove "i386" "$FRAMEWORK_EXECUTABLE_PATH"
   echo "i386 architecture removed"
   rm "$FRAMEWORK_EXECUTABLE_PATH"
   mv "$FRAMEWORK_TMP_PATH" "$FRAMEWORK_EXECUTABLE_PATH"
 fi
- if $(lipo "$FRAMEWORK_EXECUTABLE_PATH" -verify_arch "x86_64") ; then
+ if lipo "$FRAMEWORK_EXECUTABLE_PATH" -verify_arch "x86_64" ; then
   lipo -output "$FRAMEWORK_TMP_PATH" -remove "x86_64" "$FRAMEWORK_EXECUTABLE_PATH"
   echo "x86_64 architecture removed"
   rm "$FRAMEWORK_EXECUTABLE_PATH"
   mv "$FRAMEWORK_TMP_PATH" "$FRAMEWORK_EXECUTABLE_PATH"
 fi
- echo $
+ echo
📝 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
1E1EA8082326CCE300E22452 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-defaults-NotificationService-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-defaults-RocketChatRN/Pods-defaults-RocketChatRN-frameworks.sh\"\n";
showEnvVarsInLog = 0;
shellScript = "echo \"Target architectures: $ARCHS\"\n\nAPP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\nFRAMEWORK_TMP_PATH=\"$FRAMEWORK_EXECUTABLE_PATH-tmp\"\n\n# remove simulator's archs if location is not simulator's directory\ncase \"${TARGET_BUILD_DIR}\" in\n*\"iphonesimulator\")\necho \"No need to remove archs\"\n;;\n*)\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"i386\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"i386\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"i386 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\nif $(lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"x86_64\") ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"x86_64\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"x86_64 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\n;;\nesac\n\necho \"Completed for executable $FRAMEWORK_EXECUTABLE_PATH\"\necho $\n\ndone\n";
};
1E1EA8082326CCE300E22452 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"Target architectures: $ARCHS\"\n\nAPP_PATH=\"${TARGET_BUILD_DIR}/${WRAPPER_NAME}\"\n\nfind \"$APP_PATH\" -name '*.framework' -type d | while read -r FRAMEWORK\ndo\nFRAMEWORK_EXECUTABLE_NAME=$(defaults read \"$FRAMEWORK/Info.plist\" CFBundleExecutable)\nFRAMEWORK_EXECUTABLE_PATH=\"$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME\"\necho \"Executable is $FRAMEWORK_EXECUTABLE_PATH\"\necho $(lipo -info \"$FRAMEWORK_EXECUTABLE_PATH\")\n\nFRAMEWORK_TMP_PATH=\"$FRAMEWORK_EXECUTABLE_PATH-tmp\"\n\n# remove simulator's archs if location is not simulator's directory\ncase \"${TARGET_BUILD_DIR}\" in\n*\"iphonesimulator\")\necho \"No need to remove archs\"\n;;\n*)\nif lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"i386\" ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"i386\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"i386 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\nif lipo \"$FRAMEWORK_EXECUTABLE_PATH\" -verify_arch \"x86_64\" ; then\nlipo -output \"$FRAMEWORK_TMP_PATH\" -remove \"x86_64\" \"$FRAMEWORK_EXECUTABLE_PATH\"\necho \"x86_64 architecture removed\"\nrm \"$FRAMEWORK_EXECUTABLE_PATH\"\nmv \"$FRAMEWORK_TMP_PATH\" \"$FRAMEWORK_EXECUTABLE_PATH\"\nfi\n;;\nesac\n\necho \"Completed for executable $FRAMEWORK_EXECUTABLE_PATH\"\necho\n\ndone\n";
};
🤖 Prompt for AI Agents
In `@ios/RocketChatRN.xcodeproj/project.pbxproj` around lines 1575 - 1591, The
shell script in the PBXShellScriptBuildPhase (shellScript surrounding
FRAMEWORK_EXECUTABLE_PATH) incorrectly uses command substitution in the lipo
checks and contains a stray "echo $"—replace any "if $(lipo ... -verify_arch
...)" usage with a direct status check "if lipo ... -verify_arch ...; then" so
the exit code is tested rather than executing output, and remove/replace the
stray "echo $" with a plain "echo" (apply the same fixes to both ShellScript
phases where lipo -verify_arch and the stray echo occur).

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