Skip to content

fix(voip): force reconnect on warm-locked accept when AppState != active#7296

Closed
diegolmello wants to merge 5 commits into
feat.voip-lib-newfrom
fix/voip-warm-accept-appstate-gate
Closed

fix(voip): force reconnect on warm-locked accept when AppState != active#7296
diegolmello wants to merge 5 commits into
feat.voip-lib-newfrom
fix/voip-warm-accept-appstate-gate

Conversation

@diegolmello

@diegolmello diegolmello commented May 4, 2026

Copy link
Copy Markdown
Member

Proposed changes

Iterates on the previous warm-locked-accept fix. Naive checkAndReopen no-ops when the SDK still believes the socket is alive (alive() is a 20s window from lastPing). On iOS, TCP can zombie inside that window — the no-op leaves answer SDP and ICE candidates writing to a dead socket and the peer times out at ~10s.

  • Pre-emptive force-reconnect in MediaCallEvents.handleVoipAcceptSucceededFromNative when AppState.currentState !== 'active': dispatch disconnect, close the socket, then checkAndReopen. Foreground accept skips this entirely so healthy sessions are unaffected.
  • setSendSignalFn in MediaSessionInstance now gates methodCall on ddp.loggedIn (not just socket 'open') with an 8s timeout. ddp.send only awaits 'open', but server-side stream-notify-user requires this.userId — sending before relogin = silent server-side drop.
  • Adds voip-debug.log reconnect timing markers (accept markedlogin readyfirst signal sent).

Issue(s)

Follow-up to #7293.

How to test or reproduce

  1. Sign in, wait until fully connected to workspace.
  2. Lock device.
  3. Receive incoming VoIP call.
  4. Accept from lock screen.
  5. Listen for audio. Confirm peer connects (not stuck "calling" for ~10s).
  6. End call. Pull voip-debug.log from the app's Documents folder via Files app — look for [reconnect-timing] lines for elapsed ms.
  7. Regression: kill the app, repeat — cold-launch path should still work.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)

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

Not aimed for merge — opened to trigger CI iOS build for on-device testing.

Summary by CodeRabbit

  • New Features

    • Enabled iOS file sharing and in-place document opening.
    • Added persistent on-device VoIP diagnostic logging with read/clear/flush tools.
    • Enhanced push token registration with structured debug reporting.
    • Added VoIP reconnect timing/tracing and improved media-session signal handling.
  • Bug Fixes

    • Improved VoIP reconnection and call-accept handling when app is backgrounded or inactive.
  • Tests

    • Updated unit tests and mocks; added microtask flush to stabilize async assertions.

When the device stays locked through CallKit accept, the foreground saga
never runs (AppState never reaches 'active'), so nothing nudges the SDK
to reopen its DDP socket if it went stale during background suspension.
The local accept resolves and CallKit shows "connected", but answer SDP
and ICE candidates can never reach the peer because send() blocks waiting
on a socket that won't reopen on its own.

Call checkAndReopen() before each signal send. It is a no-op when the
socket is alive; otherwise it triggers the existing reconnect → relogin
→ resubscribe chain so signaling can flow over a fresh DDP session.
Naive checkAndReopen no-ops when SDK alive() still returns true, but iOS
TCP can zombie inside that 20s window. Pre-emptively dispatch disconnect,
close the socket, then checkAndReopen on warm-locked accept; foreground
accept skips this so healthy sessions are unaffected.

setSendSignalFn now gates methodCall on ddp.loggedIn (not just open) with
8s timeout. send() only awaits open but stream-notify-user requires
this.userId server-side, so sending before relogin = silent drop.

Adds voip-debug.log instrumentation: accept-marked, login-ready,
first-signal-sent timings.
@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a persistent VoIP debug logger and reconnect-timing helper, instruments push and VoIP flows with structured debug traces, extracts/restores media-call state signals, introduces a forced reconnect path for background-accepted VoIP calls, and enables iOS file-sharing/open-in-place.

Changes

VoIP logging, reconnect timing, and integration

Layer / File(s) Summary
New logging service
app/lib/services/voip/voipDebugLogger.ts
New module that appends timestamped tagged lines to a persistent Expo file and echoes to console; exports voipDebugLog, flush/read/clear/getPath/lastError helpers; serializes writes via a promise chain.
Reconnect timing helper
app/lib/services/voip/voipReconnectTiming.ts
New module tracking reconnect start and ensuring a one-time "first signal" elapsed log; exports mark/log/clear functions.
Push notifications instrumentation
app/lib/notifications/push.ts
Replaced console messages with voipDebugLog in registerForPushNotifications and pushNotificationConfigure to log entry, permission states, token acquisition (with metadata), and promise resolve/reject outcomes while preserving return behavior.
REST API: token registration logging
app/lib/services/restApi.ts
Added voipDebugLog calls in registerPushToken for entry, bail conditions, payload before sdk.post('push.token', ...), success logging, and structured error metadata on failure.
Media session: REST replay extraction
app/lib/services/voip/MediaSessionInstance.ts
Extracted REST replay into applyRestStateSignals() which fetches media-calls.stateSignals, logs fetch metadata, processes signals with try/catch and debug logging; init now invokes this method.
Media session: DDP send & stream handling
app/lib/services/voip/MediaSessionInstance.ts
Reworked setSendSignalFn to async flow: optionally call sdk.current.checkAndReopen(), await DDP login (8s timeout) before stream-notify-user, drop on timeout, log outcomes, and record first-signal elapsed; sdk.onStreamData now filters media-signal and logs received signal context.
Media session: call lifecycle logging & safety
app/lib/services/voip/MediaSessionInstance.ts
Added debug logs for session init, call newCall entry, per-call stateChange, ended handling (then terminateNativeCall), and expanded answerCall logging including accept start/resolve/reject and explicit termination when a call is missing after accept.
VoIP accept handling & forced reconnect
app/lib/services/voip/MediaCallEvents.ts
Added entry and dedupe debug logs; when accept succeeds in the same workspace but AppState != 'active', marks reconnect timing, dispatches disconnect, best-effort closes DDP, calls sdk.current.checkAndReopen(), registers one-time DDP login listener to log login elapsed, then invokes applyRestStateSignals(); also logs initial native events and iOS push-token register outcomes.
Testing helper export
app/lib/services/voip/MediaCallEvents.ts
Added exported resetMediaCallEventsStateForTesting() to clear accept-succeeded/accept-failed dedupe sentinels.
iOS config
ios/RocketChatRN/Info.plist
Enabled UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace.
Tests: mocks and timing tick
app/lib/services/voip/*.test.ts
Added Jest mocks for ../sdk, ../../store/auxStore, ../../../actions/connect, and ./voipReconnectTiming; one test inserts a microtask tick before asserting DDP method call.

Sequence Diagram(s)

sequenceDiagram
participant Native as Native OS (VoIP push)
participant JS as JS runtime
participant SDK as sdk.current
participant DDP as DDP/socket
participant REST as REST API

Native->>JS: VoIP push (accept)
JS->>JS: voipDebugLog accept entry
JS->>JS: check AppState
alt AppState != active
  JS->>JS: markVoipReconnectStart(forced=true)
  JS->>store: dispatch disconnect
  JS->>DDP: best-effort close
  JS->>SDK: checkAndReopen()
  SDK->>DDP: reopen/login
  DDP-->>JS: login event
  JS->>JS: logVoipLoginElapsed()
end
JS->>REST: fetch media-calls.stateSignals
REST-->>JS: signals list
JS->>JS: process signals (applyRestStateSignals)
JS->>DDP: setSendSignalFn -> await login (8s) -> stream-notify-user
JS->>JS: logVoipFirstSignalElapsed()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

type: bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: forcing reconnection on warm-locked accept when AppState is not active, which aligns with the core changes in MediaCallEvents and MediaSessionInstance.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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
Review rate limit: 4/8 reviews remaining, refill in 22 minutes and 42 seconds.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/services/voip/MediaCallEvents.ts`:
- Around line 116-134: The async immediately-invoked function expression (the
anonymous async IIFE that calls sdk.current, closes ddpSocket, calls
sdk.current?.checkAndReopen and sets ddpSocket.once to logVoipLoginElapsed) must
have its returned promise handled; append a .catch(...) to the IIFE to log any
unhandled rejection (use voipDebugLog and include the error string) so the
floating promise is not left unhandled per the codebase ESLint rules.

In `@app/lib/services/voip/MediaSessionInstance.ts`:
- Around line 108-138: The pattern inside mediaSessionStore.setSendSignalFn
currently uses a void-wrapped async IIFE to fire-and-forget (void (async () => {
... })()), which triggers the ESLint no-void rule; change this to call the async
IIFE and attach a .catch(...) to the returned promise instead (e.g., (async
function sendSignalWorker() { ... })().catch(err => {
voipDebugLog('sendSignal','unhandled',String(err)); })), keeping the same
internal behavior (calls to sdk.current?.checkAndReopen, awaiting ddpSocket
login, sdk.methodCall, and logVoipFirstSignalElapsed) and using the same
voipDebugLog/error handling inside the try/catch blocks so any unhandled
rejection is logged via .catch rather than silenced with void.

In `@app/lib/services/voip/voipDebugLogger.ts`:
- Line 37: Replace the indirect console call `(globalThis as
any).console['log'](line)` in voipDebugLogger.ts with a direct call to the
global console (use `console.log(line)`), removing the globalThis cast and
bracket notation to satisfy ESLint and React Native environment expectations;
locate the call site where the module logs `line` and update it to use the
direct console API.

In `@ios/RocketChatRN/Info.plist`:
- Around line 98-101: The Info.plist currently enables UIFileSharingEnabled and
LSSupportsOpeningDocumentsInPlace for all builds; change the main Info.plist to
set UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace to false (or
remove them) and move the true settings into a debug-only plist or configuration
so only debug/test builds expose the Documents folder; ensure the debug plist or
build configuration overrides these keys (UIFileSharingEnabled,
LSSupportsOpeningDocumentsInPlace) for Debug targets rather than leaving them
true in the production Info.plist.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba6fe065-bf87-4af1-8aaa-11e2678a91b5

📥 Commits

Reviewing files that changed from the base of the PR and between 57447d0 and 0f005a1.

📒 Files selected for processing (7)
  • app/lib/notifications/push.ts
  • app/lib/services/restApi.ts
  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/voipDebugLogger.ts
  • app/lib/services/voip/voipReconnectTiming.ts
  • ios/RocketChatRN/Info.plist
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/lib/services/voip/voipReconnectTiming.ts
  • app/lib/notifications/push.ts
  • app/lib/services/voip/voipDebugLogger.ts
  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/restApi.ts
  • app/lib/services/voip/MediaSessionInstance.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

**/*.{ts,tsx}: Use TypeScript with strict mode enabled and baseUrl set to app/ for module imports
Support iOS 13.4+ and Android 6.0+ as minimum target platforms

Files:

  • app/lib/services/voip/voipReconnectTiming.ts
  • app/lib/notifications/push.ts
  • app/lib/services/voip/voipDebugLogger.ts
  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/restApi.ts
  • app/lib/services/voip/MediaSessionInstance.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use tabs for indentation with single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses when possible
Use ESLint with @rocket.chat/eslint-config base including React, React Native, TypeScript, and Jest plugins

Files:

  • app/lib/services/voip/voipReconnectTiming.ts
  • app/lib/notifications/push.ts
  • app/lib/services/voip/voipDebugLogger.ts
  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/restApi.ts
  • app/lib/services/voip/MediaSessionInstance.ts
app/lib/services/voip/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Implement VoIP features in app/lib/services/voip/ directory using Zustand stores for WebRTC peer-to-peer audio calls with native CallKit (iOS) and Telecom (Android) integration

Files:

  • app/lib/services/voip/voipReconnectTiming.ts
  • app/lib/services/voip/voipDebugLogger.ts
  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaSessionInstance.ts
app/lib/services/restApi.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use REST API via fetch in app/lib/services/restApi.ts for HTTP requests

Files:

  • app/lib/services/restApi.ts
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/services/voip/voipReconnectTiming.ts
  • app/lib/notifications/push.ts
  • app/lib/services/voip/voipDebugLogger.ts
  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/restApi.ts
  • app/lib/services/voip/MediaSessionInstance.ts
🪛 ESLint
app/lib/services/voip/voipDebugLogger.ts

[error] 37-37: 'globalThis' is not defined.

(no-undef)


[error] 37-37: ["log"] is better written in dot notation.

(dot-notation)

app/lib/services/voip/MediaSessionInstance.ts

[error] 110-137: Expected 'undefined' and instead saw 'void'.

(no-void)

🔇 Additional comments (8)
app/lib/services/voip/voipReconnectTiming.ts (1)

1-28: LGTM!

Clean timing utility with proper guards for null state and one-time logging. The module correctly tracks reconnect timing state and provides clear functions for marking, logging, and clearing timing data.

app/lib/services/voip/voipDebugLogger.ts (1)

18-33: LGTM!

The serialized write chain pattern is a solid approach to prevent file corruption from concurrent writes. Error handling preserves chain continuity while tracking the last error.

app/lib/services/restApi.ts (1)

1090-1158: LGTM!

The debug instrumentation is comprehensive and non-intrusive. The error serialization pattern at line 1155 safely captures error properties including non-enumerable ones. The logging points cover entry, bail conditions, request details, success, and failure paths.

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

190-210: LGTM!

Promise handling correctly uses .then()/.catch() chains rather than void expressions, complying with the codebase's no-void: error ESLint rule. The debug logging covers both initial registration and token refresh paths.

app/lib/services/voip/MediaSessionInstance.ts (2)

89-106: LGTM!

The init method properly chains async operations and the REST state signals replay is well-structured with appropriate error logging.


207-252: LGTM!

The answerCall method has comprehensive error handling with proper cleanup paths (terminating native call, resetting state) when accept fails or call is not found. Debug logging covers all branches.

app/lib/services/voip/MediaCallEvents.ts (2)

160-170: LGTM!

The VoIP push token registration correctly uses .then()/.catch() for promise handling with appropriate debug logging on both success and failure paths.


273-350: LGTM!

The getInitialMediaCallEvents function has proper async/await structure with a comprehensive try/catch wrapper. Debug logging at entry and for initial events provides good visibility into cold-start behavior.

Comment on lines +116 to +134
(async () => {
try {
const driver = await (sdk.current as any)?.socket;
const ddpSocket = driver?.ddp;
try {
ddpSocket?.connection?.close();
voipDebugLog('VoipAcceptSucceeded', 'forced ws close');
} catch (e) {
voipDebugLog('VoipAcceptSucceeded', 'forced ws close threw', String(e));
}
sdk.current?.checkAndReopen?.();
voipDebugLog('VoipAcceptSucceeded', 'checkAndReopen kicked');
ddpSocket?.once?.('login', () => {
logVoipLoginElapsed();
});
} catch (e) {
voipDebugLog('VoipAcceptSucceeded', 'force reconnect threw', String(e));
}
})();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Attach .catch() to the async IIFE for consistent promise handling.

While the body has internal try/catch, the floating async IIFE should attach .catch() per codebase ESLint conventions. This also catches any edge cases where the async boundary itself might reject.

Proposed fix
 		if (forceReconnect) {
 			// connectedListener (connect.ts:113) early-returns if redux meteor.connected is still true.
 			// Force redux flip so loginRequest fires on the new 'connected' event.
 			store.dispatch(disconnectAction());
 			(async () => {
 				try {
 					const driver = await (sdk.current as any)?.socket;
 					const ddpSocket = driver?.ddp;
 					try {
 						ddpSocket?.connection?.close();
 						voipDebugLog('VoipAcceptSucceeded', 'forced ws close');
 					} catch (e) {
 						voipDebugLog('VoipAcceptSucceeded', 'forced ws close threw', String(e));
 					}
 					sdk.current?.checkAndReopen?.();
 					voipDebugLog('VoipAcceptSucceeded', 'checkAndReopen kicked');
 					ddpSocket?.once?.('login', () => {
 						logVoipLoginElapsed();
 					});
 				} catch (e) {
 					voipDebugLog('VoipAcceptSucceeded', 'force reconnect threw', String(e));
 				}
-			})();
+			})().catch(e => voipDebugLog('VoipAcceptSucceeded', 'reconnect IIFE rejected', String(e)));
 		}

Based on learnings: "In this Rocket.Chat React Native codebase, the ESLint rule no-void: error is enforced. When you see a promise returned from an async call that is not awaited (a "floating promise"), handle the promise explicitly by attaching .catch(...)."

📝 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
(async () => {
try {
const driver = await (sdk.current as any)?.socket;
const ddpSocket = driver?.ddp;
try {
ddpSocket?.connection?.close();
voipDebugLog('VoipAcceptSucceeded', 'forced ws close');
} catch (e) {
voipDebugLog('VoipAcceptSucceeded', 'forced ws close threw', String(e));
}
sdk.current?.checkAndReopen?.();
voipDebugLog('VoipAcceptSucceeded', 'checkAndReopen kicked');
ddpSocket?.once?.('login', () => {
logVoipLoginElapsed();
});
} catch (e) {
voipDebugLog('VoipAcceptSucceeded', 'force reconnect threw', String(e));
}
})();
if (forceReconnect) {
// connectedListener (connect.ts:113) early-returns if redux meteor.connected is still true.
// Force redux flip so loginRequest fires on the new 'connected' event.
store.dispatch(disconnectAction());
(async () => {
try {
const driver = await (sdk.current as any)?.socket;
const ddpSocket = driver?.ddp;
try {
ddpSocket?.connection?.close();
voipDebugLog('VoipAcceptSucceeded', 'forced ws close');
} catch (e) {
voipDebugLog('VoipAcceptSucceeded', 'forced ws close threw', String(e));
}
sdk.current?.checkAndReopen?.();
voipDebugLog('VoipAcceptSucceeded', 'checkAndReopen kicked');
ddpSocket?.once?.('login', () => {
logVoipLoginElapsed();
});
} catch (e) {
voipDebugLog('VoipAcceptSucceeded', 'force reconnect threw', String(e));
}
})().catch(e => voipDebugLog('VoipAcceptSucceeded', 'reconnect IIFE rejected', String(e)));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/services/voip/MediaCallEvents.ts` around lines 116 - 134, The async
immediately-invoked function expression (the anonymous async IIFE that calls
sdk.current, closes ddpSocket, calls sdk.current?.checkAndReopen and sets
ddpSocket.once to logVoipLoginElapsed) must have its returned promise handled;
append a .catch(...) to the IIFE to log any unhandled rejection (use
voipDebugLog and include the error string) so the floating promise is not left
unhandled per the codebase ESLint rules.

Comment thread app/lib/services/voip/MediaSessionInstance.ts
Comment thread app/lib/services/voip/voipDebugLogger.ts Outdated
Comment on lines +98 to +101
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict file-sharing/document-in-place to debug builds only

Setting both keys to true in the main app plist broadens data exposure (Documents visible over Finder/iTunes and editable in place by other apps). For a messaging app, this is a production privacy/security posture regression unless strictly required. Please move this behavior to a debug/test-only plist (or set these to false in release).

Suggested safe default in main plist
-	<key>UIFileSharingEnabled</key>
-	<true/>
-	<key>LSSupportsOpeningDocumentsInPlace</key>
-	<true/>
+	<key>UIFileSharingEnabled</key>
+	<false/>
+	<key>LSSupportsOpeningDocumentsInPlace</key>
+	<false/>
📝 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
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>
<false/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/RocketChatRN/Info.plist` around lines 98 - 101, The Info.plist currently
enables UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace for all
builds; change the main Info.plist to set UIFileSharingEnabled and
LSSupportsOpeningDocumentsInPlace to false (or remove them) and move the true
settings into a debug-only plist or configuration so only debug/test builds
expose the Documents folder; ensure the debug plist or build configuration
overrides these keys (UIFileSharingEnabled, LSSupportsOpeningDocumentsInPlace)
for Debug targets rather than leaving them true in the production Info.plist.

@diegolmello diegolmello closed this May 4, 2026
@diegolmello diegolmello reopened this May 4, 2026
…-warm-accept-appstate-gate

# Conflicts:
#	app/lib/notifications/push.ts
#	app/lib/services/voip/MediaSessionInstance.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
app/lib/services/voip/MediaSessionInstance.ts (1)

107-134: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

void IIFE still present — ESLint no-void: error is failing CI.

The fix proposed in the previous review was marked as addressed but the void (async () => {...})() pattern is still in the current code. Static analysis confirms the failure on line 107.

🔧 Proposed fix
-		void (async () => {
+		(async () => {
 			try {
 				try {
 					sdk.current?.checkAndReopen?.();
 				} catch (e) {
 					voipDebugLog('sendSignal', 'checkAndReopen threw', String(e));
 				}
 				const driver = await (sdk.current as any)?.socket;
 				const ddpSocket = driver?.ddp;
 				if (ddpSocket && !ddpSocket.loggedIn) {
 					voipDebugLog('sendSignal', 'awaiting login', { type: (signal as any)?.type });
 					await Promise.race([
 						new Promise<void>(resolve => ddpSocket.once('login', () => resolve())),
 						new Promise<void>(resolve => setTimeout(resolve, 8000))
 					]);
 					if (!ddpSocket.loggedIn) {
 						voipDebugLog('sendSignal', 'login wait timed out, dropping', { type: (signal as any)?.type });
 						return;
 					}
 					voipDebugLog('sendSignal', 'login ready, proceeding');
 				}
 				await sdk.methodCall('stream-notify-user', `${userId}/media-calls`, JSON.stringify(signal));
 				voipDebugLog('sendSignal', 'methodCall resolved', { type: (signal as any)?.type });
 				logVoipFirstSignalElapsed();
 			} catch (e: unknown) {
 				voipDebugLog('sendSignal', 'methodCall rejected', String(e));
 			}
-		})();
+		})().catch(e => voipDebugLog('sendSignal', 'unhandled', String(e)));

Based on learnings: "In this Rocket.Chat React Native codebase, the ESLint rule no-void: error is enforced. When you see a promise returned from an async call that is not awaited (a 'floating promise'), do not silence it with the void somePromise() pattern. Instead, handle the promise explicitly by attaching .catch(...)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/services/voip/MediaSessionInstance.ts` around lines 107 - 134, The
IIFE uses the disallowed "void (async () => {...})()" pattern causing the
no-void ESLint error; replace the void-silenced IIFE with an explicit async call
whose returned promise is handled (attach .catch) or move the body into a named
async helper and call it, then call .catch(...) to log errors; keep the existing
internals (sdk.current?.checkAndReopen, reading driver/ddpSocket, awaiting
login, sdk.methodCall, voipDebugLog, logVoipFirstSignalElapsed) but remove the
leading "void" and ensure any rejection is handled by the appended .catch that
calls voipDebugLog('sendSignal', 'methodCall rejected', String(e)) (or similar)
so no floating promise remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@app/lib/services/voip/MediaSessionInstance.ts`:
- Around line 107-134: The IIFE uses the disallowed "void (async () => {...})()"
pattern causing the no-void ESLint error; replace the void-silenced IIFE with an
explicit async call whose returned promise is handled (attach .catch) or move
the body into a named async helper and call it, then call .catch(...) to log
errors; keep the existing internals (sdk.current?.checkAndReopen, reading
driver/ddpSocket, awaiting login, sdk.methodCall, voipDebugLog,
logVoipFirstSignalElapsed) but remove the leading "void" and ensure any
rejection is handled by the appended .catch that calls
voipDebugLog('sendSignal', 'methodCall rejected', String(e)) (or similar) so no
floating promise remains.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5a71d572-7f8b-42dd-9fec-330ea3a9d9aa

📥 Commits

Reviewing files that changed from the base of the PR and between 0f005a1 and 1563490.

📒 Files selected for processing (2)
  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaSessionInstance.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaSessionInstance.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

**/*.{ts,tsx}: Use TypeScript with strict mode enabled and baseUrl set to app/ for module imports
Support iOS 13.4+ and Android 6.0+ as minimum target platforms

Files:

  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaSessionInstance.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use tabs for indentation with single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses when possible
Use ESLint with @rocket.chat/eslint-config base including React, React Native, TypeScript, and Jest plugins

Files:

  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaSessionInstance.ts
app/lib/services/voip/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Implement VoIP features in app/lib/services/voip/ directory using Zustand stores for WebRTC peer-to-peer audio calls with native CallKit (iOS) and Telecom (Android) integration

Files:

  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaSessionInstance.ts
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/services/voip/MediaCallEvents.ts
  • app/lib/services/voip/MediaSessionInstance.ts
🪛 ESLint
app/lib/services/voip/MediaSessionInstance.ts

[error] 107-134: Expected 'undefined' and instead saw 'void'.

(no-void)

🪛 GitHub Check: ESLint and Test / run-eslint-and-test
app/lib/services/voip/MediaSessionInstance.ts

[failure] 107-107:
Expected 'undefined' and instead saw 'void'

🔇 Additional comments (1)
app/lib/services/voip/MediaCallEvents.ts (1)

116-134: ⚡ Quick win

Attach .catch() to the fire-and-forget async IIFE.

The reconnect IIFE's returned Promise is unhandled. While the internal try/catch prevents observable rejections, attaching .catch() is the codebase convention for floating async IIFEs and was flagged in the prior review round without being resolved.

♻️ Proposed fix
-			})();
+			})().catch(e => voipDebugLog('VoipAcceptSucceeded', 'reconnect IIFE rejected', String(e)));

Based on learnings: "In this Rocket.Chat React Native codebase, the ESLint rule no-void: error is enforced. When you see a promise returned from an async call that is not awaited (a 'floating promise'), handle the promise explicitly by attaching .catch(...)."

@diegolmello diegolmello had a problem deploying to approve_e2e_testing May 4, 2026 19:08 — with GitHub Actions Failure
@diegolmello diegolmello temporarily deployed to experimental_ios_build May 4, 2026 19:12 — with GitHub Actions Inactive
@diegolmello diegolmello had a problem deploying to official_android_build May 4, 2026 19:12 — with GitHub Actions Failure
@diegolmello diegolmello had a problem deploying to official_ios_build May 4, 2026 19:12 — with GitHub Actions Failure
@diegolmello diegolmello temporarily deployed to experimental_android_build May 4, 2026 19:12 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
app/lib/services/voip/MediaCallEvents.ios.test.ts (2)

46-58: ⚡ Quick win

AppState is missing from the react-native mock.

The new production code gates reconnect logic on AppState.currentState !== 'active'. The mock only provides Platform, DeviceEventEmitter, and NativeEventEmitter, so AppState resolves to undefined in any test that reaches that branch. The existing VoipAcceptSucceeded test is safe today (it uses a foreign host and exits on the deep-link branch), but any test for the same-workspace warm-accept path will throw TypeError: Cannot read properties of undefined (reading 'currentState').

🛠️ Proposed fix
 jest.mock('react-native', () => ({
 	Platform: { OS: 'ios' },
+	AppState: { currentState: 'active' },
 	DeviceEventEmitter: {
 		addListener(eventType: string, listener: (payload: unknown) => void) {
 			return mockMakeAddListener(mockNativeVoipListeners)(eventType, listener);
 		}
 	},
 	NativeEventEmitter: class {
 		addListener(eventType: string, listener: (payload: unknown) => void) {
 			return mockMakeAddListener(mockNativeVoipListeners)(eventType, listener);
 		}
 	}
 }));

Tests that want to exercise the reconnect path can then override it per-test via jest.spyOn or by mutating the mock's currentState value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/services/voip/MediaCallEvents.ios.test.ts` around lines 46 - 58, The
react-native mock used in MediaCallEvents.ios.test.ts is missing AppState,
causing AppState.currentState to be undefined; update the jest.mock block to
include an AppState export with a currentState property (e.g., AppState: {
currentState: 'active' }) so production code that checks AppState.currentState
won't throw, and document that tests can override it per-test via jest.spyOn or
by mutating the mock's AppState.currentState; ensure this change is applied
alongside the existing Platform, DeviceEventEmitter and NativeEventEmitter mocks
so behavior remains unchanged for other tests.

121-138: 🏗️ Heavy lift

New mocks are wired up but the reconnect path has no test coverage.

The four mocks correctly support the background-accept reconnect path, but no it() block exercises it. The core bug fix — dispatching disconnect, closing the socket, and calling checkAndReopen when AppState.currentState !== 'active' on a same-workspace accept — is entirely unverified by this test file.

Additionally, sdk.current is mocked as null, so even if a test were added today, sdk.current?.disconnect() and sdk.current?.checkAndReopen() would silently no-op. To assert those calls, the mock needs a non-null ref with spy methods:

-jest.mock('../sdk', () => ({
-	__esModule: true,
-	default: { current: null }
-}));
+const mockSdkRef = {
+	disconnect: jest.fn(),
+	checkAndReopen: jest.fn(),
+	loggedIn: false
+};
+jest.mock('../sdk', () => ({
+	__esModule: true,
+	default: { current: mockSdkRef }
+}));

A minimal test for the new path would look roughly like:

it('forces reconnect when VoipAcceptSucceeded fires with AppState !== active (same workspace)', async () => {
    (AppState as any).currentState = 'background';
    mockServerSelector.mockReturnValue('https://same.example.com');
    setupMediaCallEvents(makeTestAdapters());
    emitNativeVoipEvent('VoipAcceptSucceeded',
        buildIncomingPayload({ callId: 'bg-uuid', host: 'https://same.example.com' }));
    await Promise.resolve();
    const { store } = jest.requireMock('../../store/auxStore');
    expect(store.dispatch).toHaveBeenCalledWith({ type: 'METEOR.DISCONNECT' });
    expect(mockSdkRef.disconnect).toHaveBeenCalled();
    expect(mockSdkRef.checkAndReopen).toHaveBeenCalled();
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/services/voip/MediaCallEvents.ios.test.ts` around lines 121 - 138,
Add a test that exercises the background-accept reconnect path: create a
non-null mocked sdk ref with spy methods (e.g., mockSdkRef = { disconnect:
jest.fn(), checkAndReopen: jest.fn() } and set the jest mock for '../sdk' to
return default: { current: mockSdkRef }), set (AppState as any).currentState =
'background', make mockServerSelector return the same workspace URL, call
setupMediaCallEvents(makeTestAdapters()), emit the native VoIP event
(emitNativeVoipEvent('VoipAcceptSucceeded', buildIncomingPayload({ callId:
'bg-uuid', host: 'https://same.example.com' }))), await a microtask, then assert
that the auxStore store.dispatch was called with disconnect
(jest.requireMock('../../store/auxStore').store.dispatch), and that
mockSdkRef.disconnect and mockSdkRef.checkAndReopen were called.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/lib/services/voip/MediaCallEvents.ios.test.ts`:
- Around line 46-58: The react-native mock used in MediaCallEvents.ios.test.ts
is missing AppState, causing AppState.currentState to be undefined; update the
jest.mock block to include an AppState export with a currentState property
(e.g., AppState: { currentState: 'active' }) so production code that checks
AppState.currentState won't throw, and document that tests can override it
per-test via jest.spyOn or by mutating the mock's AppState.currentState; ensure
this change is applied alongside the existing Platform, DeviceEventEmitter and
NativeEventEmitter mocks so behavior remains unchanged for other tests.
- Around line 121-138: Add a test that exercises the background-accept reconnect
path: create a non-null mocked sdk ref with spy methods (e.g., mockSdkRef = {
disconnect: jest.fn(), checkAndReopen: jest.fn() } and set the jest mock for
'../sdk' to return default: { current: mockSdkRef }), set (AppState as
any).currentState = 'background', make mockServerSelector return the same
workspace URL, call setupMediaCallEvents(makeTestAdapters()), emit the native
VoIP event (emitNativeVoipEvent('VoipAcceptSucceeded', buildIncomingPayload({
callId: 'bg-uuid', host: 'https://same.example.com' }))), await a microtask,
then assert that the auxStore store.dispatch was called with disconnect
(jest.requireMock('../../store/auxStore').store.dispatch), and that
mockSdkRef.disconnect and mockSdkRef.checkAndReopen were called.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d40ee847-62a5-4082-aaaa-0de04cdde7e6

📥 Commits

Reviewing files that changed from the base of the PR and between a4749c7 and e04613d.

📒 Files selected for processing (3)
  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaCallEvents.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
✅ Files skipped from review due to trivial changes (1)
  • app/lib/services/voip/MediaCallEvents.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use descriptive names for functions, variables, and classes that clearly convey their purpose
Write comments that explain the 'why' behind code decisions, not the 'what'
Keep functions small and focused on a single responsibility
Use const by default, let when reassignment is needed, and avoid var
Prefer async/await over .then() chains for handling asynchronous operations
Use explicit error handling with try/catch blocks for async operations
Avoid deeply nested code; refactor complex logic into helper functions

Files:

  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript for type safety; add explicit type annotations to function parameters and return types
Prefer interfaces over type aliases for defining object shapes in TypeScript
Use enums for sets of related constants rather than magic strings or numbers

**/*.{ts,tsx}: Use TypeScript with strict mode enabled and baseUrl set to app/ for module imports
Support iOS 13.4+ and Android 6.0+ as minimum target platforms

Files:

  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use tabs for indentation with single quotes, 130 character line width, no trailing commas, and avoid arrow function parentheses when possible
Use ESLint with @rocket.chat/eslint-config base including React, React Native, TypeScript, and Jest plugins

Files:

  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
app/lib/services/voip/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Implement VoIP features in app/lib/services/voip/ directory using Zustand stores for WebRTC peer-to-peer audio calls with native CallKit (iOS) and Telecom (Android) integration

Files:

  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
🧠 Learnings (1)
📚 Learning: 2026-04-30T17:07:51.020Z
Learnt from: diegolmello
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7274
File: app/lib/services/voip/MediaCallEvents.ts:0-0
Timestamp: 2026-04-30T17:07:51.020Z
Learning: In this Rocket.Chat React Native codebase, the ESLint rule `no-void: error` is enforced. When you see a promise returned from an async call that is not awaited (a “floating promise”), do not silence it with the `void somePromise()` pattern. Instead, handle the promise explicitly by attaching `.catch(...)` (or otherwise awaiting/handling the error) so unhandled-rejection risks are addressed in a way that satisfies the existing ESLint configuration.

Applied to files:

  • app/lib/services/voip/MediaCallEvents.ios.test.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
🔇 Additional comments (1)
app/lib/services/voip/MediaSessionInstance.test.ts (1)

59-65: ⚡ Quick win

The test is correct; the mock setup appropriately reflects the code path being tested.

The production code path in setSendSignalFn extracts the DDP socket via const ddpSocket = (await sdk.current?.socket)?.ddp (not directly from the mocked sdk default export). Since the test does not mock sdk.current.socket, ddpSocket is undefined, and the login gate condition if (ddpSocket && !ddpSocket.loggedIn) is correctly skipped. The test verifies the happy path where the socket is unavailable and methodCall proceeds directly. The setImmediate flush allows the async IIFE to execute before asserting the call.

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

iOS Build Available

Rocket.Chat Experimental 4.72.0.108753

@diegolmello diegolmello had a problem deploying to upload_experimental_android May 4, 2026 19:54 — with GitHub Actions Failure
@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

Android Build Available

Rocket.Chat Experimental 4.72.0.108752

Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNQ2i7C5YNXO5Cd71c7kFYSHjLFozZq9jflF-Mz-BsFNXgnd7G2ZHuFc6Ci5cbFsbygi11aJvP0D3steMXiq

@diegolmello diegolmello closed this May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant