fix(voip): show CallKit UI when call is active in background#7128
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📜 Recent 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). (2)
WalkthroughAdds an iOS-only RNCallKeep listener to reconcile CallKit mute actions with the JS call store and updates the call store to mark the active call in CallKit when a media call becomes active. Tests and mocks added to verify listener behavior and CallKeep interactions. Changes
Sequence DiagramsequenceDiagram
actor OS as iOS CallKit
participant RNCK as RNCallKeep Listener
participant Store as useCallStore
participant App as JS App State
OS->>RNCK: didPerformSetMutedCallAction({ callUUID, muted })
RNCK->>Store: getState()
Store-->>RNCK: { call, callId, nativeAcceptedCallId, isMuted, toggleMute }
RNCK->>RNCK: normalize UUIDs & validate active call
alt incoming muted != isMuted
RNCK->>App: invoke toggleMute()
App->>Store: update isMuted
else incoming muted == isMuted or invalid UUID or no active call
RNCK-->>RNCK: no-op
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 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 |
When a VoIP call is active and the app is backgrounded, iOS was not showing the call in the system UI (lock screen, Control Center, Dynamic Island) because two CallKit actions were missing. - Call RNCallKeep.setCurrentCallActive() in handleStateChange when callState transitions to 'active', so iOS shows the ongoing call. - Wire performSetMutedCallAction to toggleMute() so the mute button in the system UI syncs correctly with the WebRTC layer.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/MediaCallEvents.ts`:
- Around line 93-99: The handler registered on RNCallKeep for
'performSetMutedCallAction' currently ignores the incoming callUUID and may
toggle mute for a different/stale session; update the listener in
MediaCallEvents.ts to mirror the pattern used in didToggleHoldCallAction by
extracting the incoming callUUID (the callUUID/_callUUID from the event) and
comparing it to the active JS call identifier from useCallStore.getState()
(e.g., activeCall.callUUID or whatever the store exposes), and only call
toggleMute() when the UUIDs match and muted !== isMuted; keep using the existing
symbols: RNCallKeep.addEventListener('performSetMutedCallAction'),
useCallStore.getState(), toggleMute, isMuted, and the callUUID check used in
didToggleHoldCallAction.
🪄 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: c49793ea-b288-4899-a0c4-1ed74ff319ef
📒 Files selected for processing (3)
app/lib/services/voip/MediaCallEvents.tsapp/lib/services/voip/useCallStore.test.tsapp/lib/services/voip/useCallStore.ts
📜 Review details
🧰 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/services/voip/MediaCallEvents.tsapp/lib/services/voip/useCallStore.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/services/voip/MediaCallEvents.tsapp/lib/services/voip/useCallStore.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/services/voip/MediaCallEvents.tsapp/lib/services/voip/useCallStore.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/MediaCallEvents.tsapp/lib/services/voip/useCallStore.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/services/voip/MediaCallEvents.tsapp/lib/services/voip/useCallStore.tsapp/lib/services/voip/useCallStore.test.ts
🧠 Learnings (3)
📓 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
Learnt from: OtavioStasiak
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6499
File: app/containers/ServerItem/index.tsx:34-36
Timestamp: 2025-12-17T15:56:22.578Z
Learning: In the Rocket.Chat React Native codebase, for radio button components on iOS, include the selection state ("Selected"/"Unselected") in the accessibilityLabel instead of using accessibilityState={{ checked: hasCheck }}, because iOS VoiceOver has known issues with accessibilityRole="radio" + accessibilityState that prevent correct state announcement.
📚 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:
app/lib/services/voip/MediaCallEvents.tsapp/lib/services/voip/useCallStore.tsapp/lib/services/voip/useCallStore.test.ts
📚 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/useCallStore.test.ts
🔇 Additional comments (2)
app/lib/services/voip/useCallStore.ts (1)
179-183: Good lifecycle hook for active-call promotion.Placing this in the active state transition is the right timing to keep native UI in sync with call progression.
app/lib/services/voip/useCallStore.test.ts (1)
14-22: Test mock update is aligned with the new store behavior.This mock expansion keeps the test environment consistent with the newly introduced call-state/native-sync paths.
Prevent a stale/other CallKit session from flipping mute on the active JS call by mirroring the UUID check pattern used in didToggleHoldCallAction. Also adds iOS-specific tests for the mute handler.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/MediaCallEvents.ios.test.ts`:
- Around line 6-16: The test file introduces unused imports and mock variables
causing ESLint no-unused-vars errors; prune or use them: remove unused imports
DeviceEventEmitter, RNCallKeep, DEEP_LINKING, the VoipPayload type,
NativeVoipModule, and any unused references to getInitialMediaCallEvents,
setupMediaCallEvents, useCallStore if they’re not referenced in the test, and
delete mockDispatch and mockSetNativeAcceptedCallId if not used; alternatively,
if the test should exercise those APIs, reference them in the test body (e.g.,
call setupMediaCallEvents/getInitialMediaCallEvents or wire useCallStore) so
they are used. Ensure only necessary symbols remain to satisfy
`@typescript-eslint/no-unused-vars`.
In `@app/lib/services/voip/MediaCallEvents.test.ts`:
- Around line 90-96: Remove the unused helper function getMuteHandler from the
test file: delete the entire function definition that references
mockAddEventListener and performs the find/throw/return logic (the
getMuteHandler helper is not used anywhere in MediaCallEvents.test.ts and is
flagged by ESLint), ensuring no other code references it; run the tests/lint to
confirm the warning is resolved.
In `@app/lib/services/voip/MediaCallEvents.ts`:
- Line 93: The RNCallKeep mute listener is using the wrong event name so the
handler never runs; update the addEventListener call in MediaCallEvents (the
RNCallKeep.addEventListener invocation that listens for mute) to use the correct
event name "didPerformSetMutedCallAction" instead of "performSetMutedCallAction"
so system mute actions trigger the existing handler for ({ muted, callUUID }) in
MediaCallEvents.ts.
🪄 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: 6ed33d18-4c2f-4f6d-a6cb-2900d2cd6c30
📒 Files selected for processing (3)
app/lib/services/voip/MediaCallEvents.ios.test.tsapp/lib/services/voip/MediaCallEvents.test.tsapp/lib/services/voip/MediaCallEvents.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 (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/services/voip/MediaCallEvents.tsapp/lib/services/voip/MediaCallEvents.test.tsapp/lib/services/voip/MediaCallEvents.ios.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/services/voip/MediaCallEvents.tsapp/lib/services/voip/MediaCallEvents.test.tsapp/lib/services/voip/MediaCallEvents.ios.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/services/voip/MediaCallEvents.tsapp/lib/services/voip/MediaCallEvents.test.tsapp/lib/services/voip/MediaCallEvents.ios.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/MediaCallEvents.tsapp/lib/services/voip/MediaCallEvents.test.tsapp/lib/services/voip/MediaCallEvents.ios.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/services/voip/MediaCallEvents.tsapp/lib/services/voip/MediaCallEvents.test.tsapp/lib/services/voip/MediaCallEvents.ios.test.ts
🧠 Learnings (2)
📓 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/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:
app/lib/services/voip/MediaCallEvents.tsapp/lib/services/voip/MediaCallEvents.test.tsapp/lib/services/voip/MediaCallEvents.ios.test.ts
🪛 ESLint
app/lib/services/voip/MediaCallEvents.test.ts
[error] 90-90: 'getMuteHandler' is defined but never used.
(@typescript-eslint/no-unused-vars)
app/lib/services/voip/MediaCallEvents.ios.test.ts
[error] 6-6: 'DeviceEventEmitter' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 7-7: 'RNCallKeep' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 9-9: 'DEEP_LINKING' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 10-10: 'VoipPayload' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 11-11: 'NativeVoipModule' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 12-12: 'getInitialMediaCallEvents' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 16-16: 'mockSetNativeAcceptedCallId' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
🪛 GitHub Actions: Format Code with Prettier
app/lib/services/voip/MediaCallEvents.ios.test.ts
[error] 6-10: typescript-eslint/no-unused-vars: 'DeviceEventEmitter' is defined but never used.
🪛 GitHub Check: format
app/lib/services/voip/MediaCallEvents.test.ts
[failure] 90-90:
'getMuteHandler' is defined but never used
app/lib/services/voip/MediaCallEvents.ios.test.ts
[failure] 16-16:
'mockSetNativeAcceptedCallId' is assigned a value but never used
[failure] 12-12:
'getInitialMediaCallEvents' is defined but never used
[failure] 11-11:
'NativeVoipModule' is defined but never used
[failure] 10-10:
'VoipPayload' is defined but never used
[failure] 9-9:
'DEEP_LINKING' is defined but never used
[failure] 7-7:
'RNCallKeep' is defined but never used
[failure] 6-6:
'DeviceEventEmitter' is defined but never used
- Change performSetMutedCallAction → didPerformSetMutedCallAction in MediaCallEvents.ts so CallKit mute events actually fire - Remove unused imports/variables from MediaCallEvents.ios.test.ts (DeviceEventEmitter, RNCallKeep, DEEP_LINKING, VoipPayload, NativeVoipModule, getInitialMediaCallEvents, mockDispatch, etc.) - Remove unused getMuteHandler helper from MediaCallEvents.test.ts
There was a problem hiding this comment.
🧹 Nitpick comments (2)
app/lib/services/voip/MediaCallEvents.ios.test.ts (2)
32-36: Prefer an interface-based mock shape overas object.Using an explicit interface here improves strict typing and avoids blunt casting.
♻️ Suggested typing cleanup
+interface ActiveCallBase { + call: object | null; + callId: string | null; + nativeAcceptedCallId: string | null; +} + -const activeCallBase = { - call: {} as object, +const activeCallBase: ActiveCallBase = { + call: {}, callId: 'uuid-1', - nativeAcceptedCallId: null as string | null + nativeAcceptedCallId: null };As per coding guidelines: "Prefer interfaces over type aliases for defining object shapes in TypeScript".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/lib/services/voip/MediaCallEvents.ios.test.ts` around lines 32 - 36, Replace the blunt cast "call: {} as object" by declaring an explicit interface for the mock shape (e.g., interface ActiveCall { call: CallShape; callId: string; nativeAcceptedCallId: string | null }) and a separate interface/type for the call payload (e.g., CallShape) and then type the activeCallBase constant with that interface instead of using "as object"; update the activeCallBase variable to use the newly declared interface for full structural typing and remove the "as object" cast.
62-79: Add a case-insensitive UUID matching test to lock in current behavior.
MediaCallEvents.tsnormalizes UUIDs to lowercase before comparison; adding one mixed-case test would prevent regressions there.🧪 Suggested test addition
it('drops event when callUUID does not match active call id', () => { setupMediaCallEvents(); getMuteHandler()({ muted: true, callUUID: 'uuid-2' }); expect(toggleMute).not.toHaveBeenCalled(); }); +it('matches UUIDs case-insensitively', () => { + getState.mockReturnValue({ ...activeCallBase, callId: 'UUID-1', isMuted: false, toggleMute }); + setupMediaCallEvents(); + getMuteHandler()({ muted: true, callUUID: 'uuid-1' }); + expect(toggleMute).toHaveBeenCalledTimes(1); +}); + it('drops event when there is no active call object even if UUIDs match', () => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/lib/services/voip/MediaCallEvents.ios.test.ts` around lines 62 - 79, Add a test that verifies UUID matching is case-insensitive by invoking setupMediaCallEvents(), then calling getMuteHandler() with a mixed-case callUUID (e.g., 'UUID-1' or 'Uuid-1') while the active call id in getState (activeCallBase) is 'uuid-1', and assert toggleMute is called; this locks in the behavior in MediaCallEvents.ts where UUIDs are normalized to lowercase—use the existing test helpers getMuteHandler, setupMediaCallEvents, activeCallBase, and toggleMute to mirror the first test but supply a mixed-case callUUID and the same expectations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@app/lib/services/voip/MediaCallEvents.ios.test.ts`:
- Around line 32-36: Replace the blunt cast "call: {} as object" by declaring an
explicit interface for the mock shape (e.g., interface ActiveCall { call:
CallShape; callId: string; nativeAcceptedCallId: string | null }) and a separate
interface/type for the call payload (e.g., CallShape) and then type the
activeCallBase constant with that interface instead of using "as object"; update
the activeCallBase variable to use the newly declared interface for full
structural typing and remove the "as object" cast.
- Around line 62-79: Add a test that verifies UUID matching is case-insensitive
by invoking setupMediaCallEvents(), then calling getMuteHandler() with a
mixed-case callUUID (e.g., 'UUID-1' or 'Uuid-1') while the active call id in
getState (activeCallBase) is 'uuid-1', and assert toggleMute is called; this
locks in the behavior in MediaCallEvents.ts where UUIDs are normalized to
lowercase—use the existing test helpers getMuteHandler, setupMediaCallEvents,
activeCallBase, and toggleMute to mirror the first test but supply a mixed-case
callUUID and the same expectations.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d43ae4dd-1d95-4f20-8dfc-8ee85ebf6262
📒 Files selected for processing (2)
app/lib/services/voip/MediaCallEvents.ios.test.tsapp/lib/services/voip/MediaCallEvents.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/lib/services/voip/MediaCallEvents.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). (2)
- GitHub Check: ESLint and Test / run-eslint-and-test
- GitHub Check: format
🧰 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/services/voip/MediaCallEvents.ios.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/services/voip/MediaCallEvents.ios.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/services/voip/MediaCallEvents.ios.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/MediaCallEvents.ios.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/services/voip/MediaCallEvents.ios.test.ts
🧠 Learnings (3)
📓 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/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:
app/lib/services/voip/MediaCallEvents.ios.test.ts
📚 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/MediaCallEvents.ios.test.ts
🔇 Additional comments (1)
app/lib/services/voip/MediaCallEvents.ios.test.ts (1)
57-92: Good coverage for the new CallKit mute reconciliation path.These cases are well-targeted and directly validate registration plus the expected toggle/no-toggle branches.
MediaCallEvents.ios.test.ts was missing mocks for store, NativeVoip, MediaSessionInstance, and restApi, causing Jest to traverse the full import chain into @rocket.chat/media-signaling (ESM) which fails to parse in the node test environment.
Flow Diagram — VoIP CallKit IntegrationMute Sync Flow (
|
…/Decline (#7215) * merge feat.voip-lib * feat(voip): enhance call handling with UUID mapping and event listeners * Base call UI * feat(voip): integrate Zustand for call state management and enhance CallView UI * feat(voip): add simulateCall function for mock call handling in UI development * refactor(CallView): update button handlers and improve UI responsiveness * Add pause-shape-unfilled icon * Base CallHeader * toggleFocus * collapse buttons * Header components * Hide header when no call * Timer * Add use memo * Add voice call item on sidebar * cleanup * Temp use @rocket.chat/media-signaling from .tgz * cleanup * Check module and permissions to enable voip * Refactor stop method to use optional chaining for media signal listeners * voip push first test * Add VoIP call handling with pending call management - Implemented VoIP push notification handling in index.js, including storing call info for later processing. - Added CallKeep event handlers for answering and ending calls from a cold start. - Introduced a new CallIdUUID module to convert call IDs to deterministic UUIDs for compatibility with CallKit. - Created a pending call store to manage incoming calls when the app is not fully initialized. - Updated deep linking actions to include VoIP call handling. - Enhanced MediaSessionInstance to process pending calls and manage call states effectively. * Remove pending store and create getInitialEvents on app/index * Attempt to make iOS calls work from cold state * lint and format * Patch callkeep ios * Temp send iOS voip push token on gcm * Temp fix require cycle * chore: format code and fix lint issues [skip ci] * CallIDUUID module on android and voip push * Add setCallUUID on useCallStore to persist calls accepted on native Android * remove callkeep from notification * Android Incoming Call UI POC * Refactor VoIP handling: Migrate VoIP-related classes to a new package structure, removing deprecated modules and consolidating functionality. Update imports in MainApplication and NotificationIntentHandler to reflect changes. This cleanup enhances code organization and prepares for future VoIP feature enhancements. * Remove VoipForegroundService * cleanup and use caller instead of callerName * Cleanup and make iOS build again * Refactor VoIP handling: Remove unused event emissions for call answered and declined, switch from SharedPreferences to in-memory storage for pending VoIP call data, and update method signatures for better clarity. This cleanup enhances performance and prepares for future VoIP feature improvements. * Refactor VoIP handling: Introduce a new VoipPayload class to encapsulate call data, streamline notification processing, and enhance method signatures across the VoIP module. This update improves code clarity and prepares for future feature enhancements. * Migrate react-native-voip-push-notifications to VoipModule * Refactor VoIP module: Update package structure by moving VoipTurboPackage to the main package and removing the obsolete NativeVoipSpec class. Adjust imports in MainApplication and VoipModule to reflect these changes, enhancing code organization and maintainability. * Unify emitters * Move CallKeep listeners from MediaSessionInstance to getInitialEvents * Clear callkeep on endcall * Unify getInitialEvents logic * getInitialEvents -> MediaCallEvents * chore: format code and fix lint issues [skip ci] * feat(Android): Add full screen incoming call (#6977) * feat: Update call UI (#6990) * feat: Handle audio routing, e.g., Bluetooth headset vs. internal speaker switching (#6992) * fix: empty space when not on call (#6993) * feat: Dialpad (#7000) * action: organized translations * feat: start call (#7024) * chore: format code and fix lint issues * feat: Pre flight (#7038) * action: organized translations * feat: Receive voip push notifications from backend (#7045) * feat: Refactor media session handling and improve disconnect logic (#7065) * feat: Control incoming call from native (#7066) * feat: Voice message blocks (#7057) * feat: native accept success event (#7068) * feat(voip): call waiting, busy detection, and videoconf blocking (#7077) * action: organized translations * feat(voip): tap-to-hide call controls with animations (#7078) * feat(voip): navigate to call DM from message button and header (#7082) * feat(voip): tablet and landscape layout (#7110) * chore: develop into feat.voip-lib-new (RN 81 + Expo 54 + reanimated 4 + true-sheet + iOS 26) (#7114) * chore: format code and fix lint issues * feat(voip): android landscape layout for IncomingCallActivity (#7116) * Update agents files * feat(voip): Support a11y (#7106) * Fix content cutting on iOS on some edge cases * pods * Ignore .worktrees on jest * chore: Merge develop into feat.voip-lib-new (#7129) * fix(voip): show CallKit UI when call is active in background (#7128) * chore: Update media-signaling to 0.2.0 (#7153) * feat(voip): migrate iOS accept/reject from DDP to REST (#7124) * Fix icons * feat(voip): migrate Android accept/reject from DDP to REST (#7127) * test(voip): integration tests for CallView pipeline (#7161) * feat(voip): display video conf provider as subtitle (#7160) * fix(voip): CallView button grid and correct landscape/dialpad layouts (#7164) * fix(voip): prevent stale MMKV cache on Android first-install accept MMKVKeyManager.initialize ran in MainApplication.onCreate before the JS engine started and opened the default MMKV file via the Tencent 1.2 JAR when it was still empty. Tencent caches instances per-ID in a singleton registry, so that empty-state view was held for the rest of the process. JS later wrote credentials through react-native-mmkv (MMKV Core 2.0), which has its own separate registry. When a VoIP push arrived, Ejson.getMMKV() got the cached empty Tencent instance and reported "No userId found in MMKV for server". Closing and reopening the app cleared the cache, which is why only the very first call after install failed. Drop the open/verify block — the encryption key is already cached from SecureKeystore, so no MMKV handle is needed here. The first Tencent instance is now created inside Ejson.getMMKV() after JS has written, so it scans the file fresh. * fix(voip): prevent duplicate ringtone on Android incoming call (#7158) * fix(voip): set explicit snaps for NewMediaCall bottom sheet (#7165) * Update app/lib/services/voip/MediaSessionStore.ts Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> * fix: make startVoipFork reactive to permissions-changed (#7151) * fix(android): remove MediaProjectionService from merged manifest (#7190) * fix(voip): Phone account creation (#7170) * feat: add Enable Mobile Ringing toggle in user preferences (#7155) * fix(voip): ship blockers for PushKit, licensing, outbound calls, push tokens (#7167) * fix(android): Play Store mic discoverability, safer FCM logs, avatar auth via headers (#7171) * fix(ios): serialize VoipService bridge statics (#7169) * fix(voip): Android DDP thread safety and VoipPayload bundle parity (#7168) * chore(voip): dead-code and hygiene sweep (#7174) * refactor(voip): decouple navigateToCallRoom from Redux and backfill REST/connect tests (#7176) * test(voip): tighten ringing endCall assertion and add VideoConf VoIP-lock saga coverage (#7177) * fix(ios): harden VoIP DDP WebSocket client on receive failures and TLS (#7173) * refactor(voip): MediaCallEvents Redux adapters and resetVoipState (#7178) * refactor(voip): decouple peer autocomplete from Redux; simplify NewMediaCall (#7175) * fix(ios): add NS_SWIFT_NAME to Challenge.runChallenge for Swift 6.2 compatibility Swift 6.2 (Xcode 26.x / macos-26 runner) auto-renames the Objective-C method runChallenge:didReceiveChallenge:completionHandler: to run(_:didReceive:completionHandler:) when imported into Swift. Add NS_SWIFT_NAME to explicitly pin the Swift import name, preventing the compiler from applying its heuristics. This keeps the existing Swift call site in DDPClient.swift working without changes. * fix(ios): cancel old URLSession/webSocketTask before reconnecting in DDPClient.connect (#7197) * fix(ios): add NSLock to nativeAcceptHandledCallIds and 10s REST timeout to handleNativeAccept (#7198) * feat(android): create VoipCallService with FOREGROUND_SERVICE_MICROPHONE (#7199) * fix(android): start VoipCallService on accept, stop on hangup/timeout, install end-call listener (#7200) * fix(voip): enable DM nav for users with SIP extension (#7203) * fix(android): handle null VoiceConnection in answerIncomingCall, notify JS (#7201) * fix(voip): resolve closure capture ordering in handleNativeAccept (#7209) * fix(android): integrate VoIP modules with SSL-pinned OkHttpClient (#7208) * fix(push): gate id and voipToken behind server version checks, fix VideoConf caller extra (#7210) * fix(voip): remove sensitive data from production logs (#7207) * fix(android): remove isRunning guard + add double-tap guard on Accept/Decline - VoipCallService: remove if (!isRunning) guard, call startForeground unconditionally (idempotent on Android, fixes Android 14+ foreground service requirement) - IncomingCallActivity: add AtomicBoolean guard on handleAccept/handleDecline to prevent double-tap from triggering multiple service starts --------- Co-authored-by: diegolmello <diegolmello@users.noreply.github.com> Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com>
Proposed changes
When a VoIP call is active and the app is backgrounded, iOS was not showing the call in the system UI (lock screen, Control Center, Dynamic Island) due to two missing CallKit integrations.
Fix 1 —
setCurrentCallActiveon active state transition:RNCallKeep.setCurrentCallActive()was only called in the local answer path (answerCall()), but never when the call transitioned toactivethrough other paths (e.g., remote user answering an outgoing call). Added the call inhandleStateChangeinsidesetCall(useCallStore.ts) whennewState === 'active'.Fix 2 —
performSetMutedCallActionnot wired:The mute button in the system UI (lock screen, Control Center, Dynamic Island) had no effect because
performSetMutedCallActionwas unwired. Added a listener insetupMediaCallEvents(MediaCallEvents.ts) that syncs mute state viatoggleMute().Hold (
didToggleHoldCallAction) was already correctly wired — no changes needed.Issue(s)
https://rocketchat.atlassian.net/browse/VMUX-75
How to test or reproduce
Screenshots
Types of changes
Checklist
Further comments
Summary by CodeRabbit
New Features
Tests