fix(voip): Android lock-screen accept — silent audio + subsequent-call blank screen#7212
Conversation
Remove the early `return true` (and the no-op `applyRestStateSignals` call) from the same-workspace branch of `getInitialMediaCallEvents`. Returning `false` lets `app/index.tsx` dispatch `appInit()`, which runs the login saga, which calls `mediaSessionInstance.init(userId)`, which replays the buffered signals and ultimately answers the call — fixing silent audio on cold-start lock-screen accept.
…ch change - Gate cold-start return-false to Android only; iOS keeps existing behavior - Update stale timer test expectations from 15s to 60s - Import emitter directly to avoid barrel-pulling deviceInfo
WalkthroughAndroid VoIP service stop now clears the internal running flag before stopping; a new JS-native hook to stop the service is added. Call termination is centralized into a new helper, navigation readiness is awaited before navigation, and the stale native-call timeout is increased to 60 seconds. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant AppJS as App (JS)
participant Nav as Navigation
participant CallKeep as RNCallKeep
participant AndroidSvc as Android VoIP Service
User->>AppJS: trigger end/answer
AppJS->>Nav: waitForNavigationReady()
Nav-->>AppJS: ready
AppJS->>CallKeep: terminateNativeCall(callId)
CallKeep->>CallKeep: RNCallKeep.endCall(callId)
CallKeep->>AndroidSvc: stopVoipCallService()
AndroidSvc->>AndroidSvc: isRunning = false
AndroidSvc-->>CallKeep: stopped
CallKeep-->>AppJS: termination complete
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ 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. Comment |
Code ReviewFiles reviewed: 10 Fix 1 — Silent audio on cold-start lock-screen accept (
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/lib/services/voip/useCallStore.ts (1)
83-96:⚠️ Potential issue | 🟡 MinorUpdate stale JSDoc comments to reflect the new 60s timer.
STALE_NATIVE_MSwas increased to 60s (line 11), but the JSDoc comments at lines 84 and 95 still describe a "15s timer". Fix to avoid confusing future maintainers.🧹 Proposed change
- /** Sets native-accepted call id and (re)starts the 15s timer. */ + /** Sets native-accepted call id and (re)starts the stale-native timer. */ setNativeAcceptedCallId: (callId: string) => void; /** Clears native-accepted id and related state; cancels the timer. */ resetNativeCallId: () => void; ... - /** Clears UI/call fields but keeps nativeAcceptedCallId. Restarts the 15s timer (media init calls reset and clears the old timer first). */ + /** Clears UI/call fields but keeps nativeAcceptedCallId. Restarts the stale-native timer (media init calls reset and clears the old timer first). */ reset: () => void;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/lib/services/voip/useCallStore.ts` around lines 83 - 96, Update the outdated JSDoc that references a "15s timer" to "60s timer" for the CallStoreActions methods so comments match the new STALE_NATIVE_MS value; specifically change the documentation on setNativeAcceptedCallId and reset (and any other nearby JSDoc in the same interface) to state that they start/restart or clear the 60s timer respectively, ensuring the text mentions "60s" and not "15s".
🧹 Nitpick comments (5)
app/lib/services/voip/MediaSessionInstance.ts (2)
138-163:answerCallmay navigate toCallViewafter the call has already ended.On cold start, if
waitForNavigationReady()takes a long time (or indefinitely, per the no-timeout concern flagged inappNavigation.ts), and the user hangs up / the peer ends the call before navigation is ready, this will stillNavigation.navigate('CallView')aftersetCallstate has been reset by theendedhandler, briefly showing an empty/stale call screen.Consider re-checking the call state (or
useCallStore.getState().call) after the await, and skipping the navigate if the call has already been torn down:♻️ Proposed guard
await mainCall.accept(); RNCallKeep.setCurrentCallActive(callId); useCallStore.getState().setCall(mainCall); await waitForNavigationReady(); + if (useCallStore.getState().call?.callId !== callId) { + return; + } Navigation.navigate('CallView');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/lib/services/voip/MediaSessionInstance.ts` around lines 138 - 163, The answerCall flow can navigate to CallView after the call was torn down while waiting for waitForNavigationReady(); after awaiting waitForNavigationReady() (inside answerCall) re-check the current call state via useCallStore.getState().call (or compare useCallStore.getState().call?.callId to callId) and only call Navigation.navigate('CallView') if the call still exists and matches callId; if the call has been cleared/changed, skip navigation and avoid setting stale state—apply this guard right before Navigation.navigate('CallView') in the answerCall method.
115-135:endedlistener registered here is never removed.On every
newCallemission for a non-hidden call, a new'ended'listener is attached tocall.emitter(line 131) and never removed. In parallel,useCallStore.setCallregisters its own'ended'listener with explicit cleanup. For the callee path, both run when the call ends — which works — but the listener here has no matching.offand will stick around for the lifetime of thecallobject. Minor per-call leak; worth adding a one-shot pattern (emitter.once(...)) or tracking the handler for removal. Also note that thestateChangelistener on line 117 is intentionally empty but similarly not removed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/lib/services/voip/MediaSessionInstance.ts` around lines 115 - 135, The newCall handler registers persistent listeners on call.emitter (call.emitter.on('ended') and call.emitter.on('stateChange')) that are never removed; change these to one-shot listeners or register removable handlers and remove them when the call ends. Specifically, update the instance?.on('newCall', ...) block so the stateChange listener is either added with once or returns a handler reference you call off on, and replace call.emitter.on('ended', ...) with call.emitter.once('ended', ...) or store the ended handler and remove it in the call end flow (coordinating with useCallStore.getState().setCall if needed) to prevent the leak. Ensure you keep the existing terminateNativeCall(call.callId) behavior when the listener fires.app/sagas/deepLinking.js (1)
71-71: Useyield call(...)for consistency and saga testability.At line 93,
waitForNavigationReadyis correctly invoked viayield call(waitForNavigationReady), but here on line 71 the Promise is yielded directly. While redux-saga supports yielding Promises,call(...)produces a declarative effect that is easier to test and supports cancellation/mocking uniformly. Align with the other call site for consistency.♻️ Proposed change
- yield waitForNavigationReady(); + yield call(waitForNavigationReady);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/sagas/deepLinking.js` at line 71, Replace the direct Promise yield with a declarative saga call: change the usage of waitForNavigationReady in deepLinking.js from yielding the Promise (yield waitForNavigationReady()) to using the redux-saga effect (yield call(waitForNavigationReady)); ensure call from 'redux-saga/effects' is imported (it's already used elsewhere in this file) so tests and cancellation/mocking behave consistently with the other call site.app/lib/navigation/appNavigation.ts (1)
79-90: Consider adding a timeout for defensive safety.The
waitForNavigationReady()helper has no timeout: if thenavigationReadyevent never fires (e.g., NavigationContainer fails to mount or initialize), awaiting this promise will hang forever. While this is unlikely in normal operation, adding a safety timeout would help catch critical initialization failures.Optional: Add a timeout that rejects or resolves after a bounded duration (e.g., 5–10 seconds) to prevent indefinite hangs in edge cases where navigation initialization fails.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/lib/navigation/appNavigation.ts` around lines 79 - 90, waitForNavigationReady can hang forever if 'navigationReady' never fires; modify the function to add a safety timeout (e.g., 5–10 seconds) that rejects the promise with a clear Error (or resolves depending on desired behavior) so callers don't wait indefinitely. Keep the early return when navigationRef.current is present; when creating the new Promise, start a timer (store its id), and in both the emitter listener (listener) and the timeout handler ensure you clear the timer and remove the listener via emitter.off('navigationReady', listener) before resolving/rejecting so there are no leaks; update callers if you change rejection vs resolution behavior.app/lib/services/voip/MediaSessionInstance.test.ts (1)
86-97: Minor: duplicated jest.fn() instances betweendefaultand named exports.Each named mock (
getUniqueId,getUniqueIdSync,hasNotch,getReadableVersion) is instantiated twice — once underdefaultand once at the top level — producing independent spies. Tests asserting against one set won't observe calls made through the other, which can silently hide regressions if consumer code switches between default and named imports. Consider defining the mocks once and sharing them across both shapes.♻️ Proposed refactor
-jest.mock('react-native-device-info', () => ({ - default: { - getUniqueId: jest.fn(() => 'test-device-id'), - getUniqueIdSync: jest.fn(() => 'test-device-id'), - hasNotch: jest.fn(() => false), - getReadableVersion: jest.fn(() => '1.0.0') - }, - getUniqueId: jest.fn(() => 'test-device-id'), - getUniqueIdSync: jest.fn(() => 'test-device-id'), - hasNotch: jest.fn(() => false), - getReadableVersion: jest.fn(() => '1.0.0') -})); +jest.mock('react-native-device-info', () => { + const api = { + getUniqueId: jest.fn(() => 'test-device-id'), + getUniqueIdSync: jest.fn(() => 'test-device-id'), + hasNotch: jest.fn(() => false), + getReadableVersion: jest.fn(() => '1.0.0') + }; + return { __esModule: true, default: api, ...api }; +});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/lib/services/voip/MediaSessionInstance.test.ts` around lines 86 - 97, The mock duplicates separate jest.fn instances for the device-info exports causing independent spies; consolidate by creating shared mock functions (e.g., sharedGetUniqueId, sharedGetUniqueIdSync, sharedHasNotch, sharedGetReadableVersion) and use those same mock functions for both the default export and the named exports in the jest.mock call so tests watching getUniqueId/getUniqueIdSync/hasNotch/getReadableVersion observe the same spy across default and named import shapes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/lib/services/voip/terminateNativeCall.ts`:
- Around line 6-15: In terminateNativeCall, guard RNCallKeep.endCall so a thrown
error there doesn't prevent the Android cleanup: call RNCallKeep.endCall(callId)
inside its own try/catch (or use try/finally) and ensure
NativeVoipModule.stopVoipCallService() is always invoked when Platform.OS ===
'android' (with its own try/catch to swallow bridge errors); reference
terminateNativeCall, RNCallKeep.endCall, NativeVoipModule.stopVoipCallService
and the Platform.OS === 'android' check so both side-effects run independently
even if one throws.
---
Outside diff comments:
In `@app/lib/services/voip/useCallStore.ts`:
- Around line 83-96: Update the outdated JSDoc that references a "15s timer" to
"60s timer" for the CallStoreActions methods so comments match the new
STALE_NATIVE_MS value; specifically change the documentation on
setNativeAcceptedCallId and reset (and any other nearby JSDoc in the same
interface) to state that they start/restart or clear the 60s timer respectively,
ensuring the text mentions "60s" and not "15s".
---
Nitpick comments:
In `@app/lib/navigation/appNavigation.ts`:
- Around line 79-90: waitForNavigationReady can hang forever if
'navigationReady' never fires; modify the function to add a safety timeout
(e.g., 5–10 seconds) that rejects the promise with a clear Error (or resolves
depending on desired behavior) so callers don't wait indefinitely. Keep the
early return when navigationRef.current is present; when creating the new
Promise, start a timer (store its id), and in both the emitter listener
(listener) and the timeout handler ensure you clear the timer and remove the
listener via emitter.off('navigationReady', listener) before resolving/rejecting
so there are no leaks; update callers if you change rejection vs resolution
behavior.
In `@app/lib/services/voip/MediaSessionInstance.test.ts`:
- Around line 86-97: The mock duplicates separate jest.fn instances for the
device-info exports causing independent spies; consolidate by creating shared
mock functions (e.g., sharedGetUniqueId, sharedGetUniqueIdSync, sharedHasNotch,
sharedGetReadableVersion) and use those same mock functions for both the default
export and the named exports in the jest.mock call so tests watching
getUniqueId/getUniqueIdSync/hasNotch/getReadableVersion observe the same spy
across default and named import shapes.
In `@app/lib/services/voip/MediaSessionInstance.ts`:
- Around line 138-163: The answerCall flow can navigate to CallView after the
call was torn down while waiting for waitForNavigationReady(); after awaiting
waitForNavigationReady() (inside answerCall) re-check the current call state via
useCallStore.getState().call (or compare useCallStore.getState().call?.callId to
callId) and only call Navigation.navigate('CallView') if the call still exists
and matches callId; if the call has been cleared/changed, skip navigation and
avoid setting stale state—apply this guard right before
Navigation.navigate('CallView') in the answerCall method.
- Around line 115-135: The newCall handler registers persistent listeners on
call.emitter (call.emitter.on('ended') and call.emitter.on('stateChange')) that
are never removed; change these to one-shot listeners or register removable
handlers and remove them when the call ends. Specifically, update the
instance?.on('newCall', ...) block so the stateChange listener is either added
with once or returns a handler reference you call off on, and replace
call.emitter.on('ended', ...) with call.emitter.once('ended', ...) or store the
ended handler and remove it in the call end flow (coordinating with
useCallStore.getState().setCall if needed) to prevent the leak. Ensure you keep
the existing terminateNativeCall(call.callId) behavior when the listener fires.
In `@app/sagas/deepLinking.js`:
- Line 71: Replace the direct Promise yield with a declarative saga call: change
the usage of waitForNavigationReady in deepLinking.js from yielding the Promise
(yield waitForNavigationReady()) to using the redux-saga effect (yield
call(waitForNavigationReady)); ensure call from 'redux-saga/effects' is imported
(it's already used elsewhere in this file) so tests and cancellation/mocking
behave consistently with the other call site.
🪄 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: 8a7bd5e6-8ee5-4341-b4b8-ee282129a36b
📒 Files selected for processing (11)
android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.ktandroid/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.ktapp/lib/native/NativeVoip.tsapp/lib/navigation/appNavigation.tsapp/lib/services/voip/MediaCallEvents.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/services/voip/terminateNativeCall.tsapp/lib/services/voip/useCallStore.test.tsapp/lib/services/voip/useCallStore.tsapp/sagas/deepLinking.js
📜 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 (5)
**/*.{js,jsx,ts,tsx,json}
📄 CodeRabbit inference engine (CLAUDE.md)
Configure Prettier with tabs, single quotes, 130 character width, no trailing commas, arrow parens avoid, and bracket same line
Files:
app/lib/navigation/appNavigation.tsapp/lib/services/voip/terminateNativeCall.tsapp/lib/native/NativeVoip.tsapp/lib/services/voip/useCallStore.tsapp/lib/services/voip/MediaCallEvents.tsapp/sagas/deepLinking.jsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/useCallStore.test.ts
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use ESLint with
@rocket.chat/eslint-configbase configuration including React, React Native, TypeScript, and Jest plugins
Files:
app/lib/navigation/appNavigation.tsapp/lib/services/voip/terminateNativeCall.tsapp/lib/native/NativeVoip.tsapp/lib/services/voip/useCallStore.tsapp/lib/services/voip/MediaCallEvents.tsapp/sagas/deepLinking.jsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/useCallStore.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use TypeScript with strict mode enabled and configure baseUrl to app/ for import resolution
**/*.{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
Files:
app/lib/navigation/appNavigation.tsapp/lib/services/voip/terminateNativeCall.tsapp/lib/native/NativeVoip.tsapp/lib/services/voip/useCallStore.tsapp/lib/services/voip/MediaCallEvents.tsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/useCallStore.test.ts
**/*.{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/navigation/appNavigation.tsapp/lib/services/voip/terminateNativeCall.tsapp/lib/native/NativeVoip.tsapp/lib/services/voip/useCallStore.tsapp/lib/services/voip/MediaCallEvents.tsapp/sagas/deepLinking.jsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/useCallStore.test.ts
app/lib/services/voip/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
Files:
app/lib/services/voip/terminateNativeCall.tsapp/lib/services/voip/useCallStore.tsapp/lib/services/voip/MediaCallEvents.tsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/useCallStore.test.ts
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/stacks/**/*.{ts,tsx} : Use React Navigation 7 for navigation with stacks for InsideStack (authenticated), OutsideStack (login/register), MasterDetailStack (tablets), and ShareExtensionStack
Applied to files:
app/lib/navigation/appNavigation.tsapp/sagas/deepLinking.js
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/index.tsx : Configure Redux provider, theme, navigation, and notifications in app/index.tsx
Applied to files:
app/lib/navigation/appNavigation.tsapp/sagas/deepLinking.js
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/AppContainer.tsx : Implement root navigation container logic in app/AppContainer.tsx to switch between authentication states
Applied to files:
app/lib/navigation/appNavigation.tsapp/sagas/deepLinking.js
📚 Learning: 2026-03-10T15:21:45.098Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 7046
File: app/containers/InAppNotification/NotifierComponent.stories.tsx:46-75
Timestamp: 2026-03-10T15:21:45.098Z
Learning: In `app/containers/InAppNotification/NotifierComponent.tsx` (React Native, Rocket.Chat), `NotifierComponent` is exported as a Redux-connected component via `connect(mapStateToProps)`. The `isMasterDetail` prop is automatically injected from `state.app.isMasterDetail` and does not need to be passed explicitly at call sites or in Storybook stories that use the default (connected) export.
Applied to files:
app/lib/navigation/appNavigation.ts
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/voip/**/*.{ts,tsx} : Implement VoIP with WebRTC peer-to-peer audio calls in app/lib/services/voip/ using Zustand stores instead of Redux, with native CallKit (iOS) and Telecom (Android) integration; keep VoIP and VideoConf separate
Applied to files:
android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.ktandroid/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.ktapp/lib/services/voip/terminateNativeCall.tsapp/lib/native/NativeVoip.tsapp/lib/services/voip/useCallStore.tsapp/lib/services/voip/MediaCallEvents.tsapp/sagas/deepLinking.jsapp/lib/services/voip/MediaSessionInstance.tsapp/lib/services/voip/MediaSessionInstance.test.tsapp/lib/services/voip/useCallStore.test.ts
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to @(app/sagas/videoConf.ts|app/lib/methods/videoConf.ts) : Manage video conferencing via Redux actions/reducers/sagas in app/sagas/videoConf.ts and app/lib/methods/videoConf.ts using server-managed Jitsi integration; do not conflate with VoIP
Applied to files:
app/lib/native/NativeVoip.tsapp/lib/services/voip/MediaCallEvents.tsapp/sagas/deepLinking.jsapp/lib/services/voip/MediaSessionInstance.ts
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/sagas/**/*.{ts,tsx} : Place Redux sagas in app/sagas/ directory with separate files for init, login, rooms, messages, encryption, deepLinking, and videoConf side effects
Applied to files:
app/sagas/deepLinking.js
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/connect.ts : Manage server connection in app/lib/services/connect.ts
Applied to files:
app/sagas/deepLinking.js
📚 Learning: 2026-04-07T17:49:17.538Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat.ReactNative PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-07T17:49:17.538Z
Learning: Applies to app/lib/services/sdk.ts : Use Rocket.Chat JS SDK in app/lib/services/sdk.ts for WebSocket real-time subscriptions
Applied to files:
app/sagas/deepLinking.js
📚 Learning: 2026-03-30T15:49:30.957Z
Learnt from: Rohit3523
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6875
File: app/containers/RoomItem/Actions.tsx:12-12
Timestamp: 2026-03-30T15:49:30.957Z
Learning: In RocketChat/Rocket.Chat.ReactNative, `react-native-worklets` version 0.6.1 does NOT export a built-in Jest mock (e.g., no `react-native-worklets/lib/module/mock`). The correct Jest mock approach for this version is to add a manual mock in `jest.setup.js`: `jest.mock('react-native-worklets', () => ({ scheduleOnRN: jest.fn((fn, ...args) => fn(...args)) }))`.
Applied to files:
app/lib/services/voip/MediaSessionInstance.test.ts
🔇 Additional comments (5)
app/lib/services/voip/MediaCallEvents.ts (1)
268-271: LGTM!The Android early-return correctly defers to the normal
appInit()cold-start path somediaSessionInstance.init()andapplyRestStateSignals()run via the shared startup flow, fixing the silent-audio issue.setNativeAcceptedCallIdis still set above the branch, preserving the accepted-call handoff, and iOS behavior is untouched.android/app/src/main/java/chat/rocket/reactnative/voip/VoipCallService.kt (1)
66-93: LGTM — eagerisRunning = falseprevents the restart race.Setting
isRunningtofalsebeforestopSelf(startId)is the right fix: a subsequentstartService(ACTION_START)arriving before the system actually tears the service down will now correctly re-invokestartForegroundWithNotification(...)instead of being skipped as "already running". This directly addresses the blank-screen-on-subsequent-calls symptom described in the PR.android/app/src/main/java/chat/rocket/reactnative/voip/VoipModule.kt (1)
152-159: LGTM.The
stopVoipCallServiceimplementation mirrors the error-handling pattern used by the other native methods in this module and correctly routes intoVoipCallService.stopService(...)which sendsACTION_STOPand — combined with the eagerisRunning = falseinVoipCallService.onStartCommand— lets subsequent call starts re-initialize the foreground service.app/lib/native/NativeVoip.ts (1)
38-68: LGTM!The new
stopVoipCallServiceTurboModule method is well-documented with platform behavior (Android sendsACTION_STOPtoVoipCallService; iOS no-op) and the JS fallback correctly returnsundefined, matching thevoidreturn type and the pattern used by the other methods.app/lib/services/voip/MediaSessionInstance.test.ts (1)
104-108: LGTM!
waitForNavigationReadystub resolving toundefinedcorrectly matches the new navigation-readiness gate added inMediaSessionInstance(pre-CallViewnavigation) so the existing test flows proceed without hanging.
…Call Both side-effects (RNCallKeep.endCall + Android service stop) now run independently — if endCall throws, stopVoipCallService still fires.
…roid-lockscreen-speaker
|
Android Build Available Rocket.Chat Experimental 4.72.0.108605 Internal App Sharing: https://play.google.com/apps/test/RQVpXLytHNc/ahAO29uNTZSae3PHDazgWFf_y-EsCHMSbFQO1MNw03cSWwmKsgPSUmj-35PUP_Cnj6KXb466WcSgocHza3_Vo32yWS |
|
Android Build Available Rocket.Chat Experimental 4.72.0.108605 |
Proposed changes
Fixes two VoIP bugs on Android when accepting an incoming call from the lock screen while the app is not running:
Silent audio on first call — The Android same-workspace branch of
getInitialMediaCallEventsreturnedtrue, which skippedappInit(). This prevented the login saga from running, somediaSessionInstance.init()never fired,applyRestStateSignals()never processed the offer SDP, and audio never routed. Fix: returnfalseon Android so the normal boot path runs (gated to Android-only — iOS retains existing behavior).Blank screen on subsequent calls — The Android
VoipCallServiceforeground service was never stopped from JS-driven hangup paths. Its staticisRunningflag blocked re-initialization on the next incoming call. Fix: newstopVoipCallServicebridge method +terminateNativeCallJS helper that wrapsRNCallKeep.endCall+ Android service stop at every call termination site.Additional hardening:
answerCallnow awaitswaitForNavigationReady()before navigating toCallView, preventing silent navigation drops during cold-start boot.STALE_NATIVE_MSraised from 15s to 60s to cover worst-case cold-boot handoff on slow devices/networks.isRunningreset:VoipCallService.onStartCommand(ACTION_STOP)now resetsisRunningbeforestopSelf()to prevent race conditions with rapid call cycling.Issue(s)
https://rocketchat.atlassian.net/browse/VMUX-99
How to test or reproduce
Types of changes
Checklist
Merge Order
This PR targets
feat.voip-lib-new(PR #6918). It should be merged intofeat.voip-lib-newbefore #6918 is merged intodevelop.Further comments
Files changed:
android/.../voip/VoipModule.kt— newstopVoipCallServicebridge methodandroid/.../voip/VoipCallService.kt— eagerisRunningreset onACTION_STOPapp/lib/native/NativeVoip.ts— TurboModule spec + fallback forstopVoipCallServiceapp/lib/services/voip/terminateNativeCall.ts— new helper wrappingendCall+ service stopapp/lib/services/voip/MediaCallEvents.ts— Android same-workspace branch returnsfalseapp/lib/services/voip/MediaSessionInstance.ts—waitForNavigationReadyguard +terminateNativeCallmigrationapp/lib/services/voip/useCallStore.ts—STALE_NATIVE_MS15s → 60s +terminateNativeCallmigrationapp/lib/navigation/appNavigation.ts— extractedwaitForNavigationReadyhelperapp/sagas/deepLinking.js— uses sharedwaitForNavigationReadyinstead of inline versionSummary by CodeRabbit
New Features
Bug Fixes
Tests