feat: Update call UI#6990
Conversation
… introduce CallButtons component for action handling
… Title component for improved layout and state management
WalkthroughReplaces CallHeader with MediaCallHeader, adds new MediaCallHeader stories and tests, extracts Content/Subtitle components, adds CallButtons, updates CallView to compose CallerInfo + CallButtons, and extends the VoIP call store with remoteMute/remoteHeld and related selector changes. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/views/CallView/components/CallerInfo.test.tsx (1)
55-67:⚠️ Potential issue | 🟡 MinorStale test name and comment — the status container no longer exists in
CallerInfo.The test description says "Status component is currently commented out" and the inline comment says "The status container exists but Status component is commented out", but
CallerInfo.tsxno longer renders any status container at all — it was removed entirely in this PR. The test only validatestestID='caller-info'andtestID='avatar', so the description should reflect that.🔧 Suggested update
- it('should render status container (Status component is currently commented out)', () => { + it('should render caller info and avatar', () => { setStoreState({ displayName: 'Test User' }); const { getByTestId } = render( <Wrapper> <CallerInfo /> </Wrapper> ); - // The status container exists but Status component is commented out - // Verify the component renders correctly expect(getByTestId('caller-info')).toBeTruthy(); expect(getByTestId('avatar')).toBeTruthy(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/views/CallView/components/CallerInfo.test.tsx` around lines 55 - 67, The test name and inline comments for the CallerInfo test are stale because the status container was removed; update the it(...) description to reflect that the component should render caller info and avatar only (e.g., "should render caller info and avatar"), remove or replace comments about a commented-out Status component, and keep assertions against testIDs 'caller-info' and 'avatar' in the CallerInfo test to match the current implementation of the CallerInfo component.app/lib/services/voip/useCallStore.ts (1)
80-113:⚠️ Potential issue | 🟠 MajorEvent listeners are never unsubscribed; repeated
setCallcalls will stack handlers.When
setCallis called, it subscribes tostateChange,trackStateChange, andendedon the call emitter, but there is no corresponding cleanup. IfsetCallis invoked again (e.g., a new incoming call replaces an existing one beforereset), the previous listeners remain active on the old emitter and could trigger stale state updates orNavigation.back()unexpectedly.Consider storing a cleanup function and invoking it at the start of
setCall(and inreset):🛡️ Suggested approach
export const useCallStore = create<CallStore>((set, get) => ({ ...initialState, + _cleanup: null as (() => void) | null, setCall: (call: IClientMediaCall, callUUID: string) => { + // Unsubscribe previous listeners + get()._cleanup?.(); + // ... existing set({...}) ... call.emitter.on('stateChange', handleStateChange); call.emitter.on('trackStateChange', handleTrackStateChange); call.emitter.on('ended', handleEnded); + + set({ + _cleanup: () => { + call.emitter.off('stateChange', handleStateChange); + call.emitter.off('trackStateChange', handleTrackStateChange); + call.emitter.off('ended', handleEnded); + } + }); }, reset: () => { + get()._cleanup?.(); set(initialState); }🤖 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 80 - 113, setCall currently attaches listeners (handleStateChange, handleTrackStateChange, handleEnded) to call.emitter but never removes them, causing handler buildup; modify setCall to run a stored cleanup before attaching new listeners (store a cleanup function or reference to the current emitter and attached handlers), call emitter.off/removeListener for each handler when cleaning up, and ensure reset also invokes this cleanup; keep the existing handler function names (handleStateChange, handleTrackStateChange, handleEnded), and remove those exact functions from the previous call.emitter before assigning a new call.
🧹 Nitpick comments (6)
app/views/CallView/components/CallButtons.tsx (1)
38-40:handleEndCallis a single-line passthrough — wireendCalldirectly.♻️ Suggested simplification
- const handleEndCall = () => { - endCall(); - }; return ( ... <CallActionButton icon='phone-end' label={isConnecting ? I18n.t('Cancel') : I18n.t('End')} - onPress={handleEndCall} + onPress={endCall} variant='danger' testID='call-view-end' />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/views/CallView/components/CallButtons.tsx` around lines 38 - 40, The handleEndCall function is a one-line passthrough to endCall; remove the unnecessary wrapper by deleting the handleEndCall declaration and replace any references (e.g., onClick or props that currently use handleEndCall) to call endCall directly so callers use endCall instead of the intermediary function.app/lib/services/voip/useCallStore.ts (2)
179-196: Large block of commented-out code.These dormant selectors add noise. If they're planned for future use, a brief
// TODO:with context would help; otherwise consider removing them to keep the file clean.🤖 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 179 - 196, Clean up the large commented-out selector block around useCallContact: either remove the unused commented selectors (useCallState, useCallControls, useCallActions) to reduce noise, or keep them but replace the block with a concise TODO explaining intended future use and expected API (e.g., “// TODO: reintroduce selector hooks useCallState, useCallControls, useCallActions when optimizing render performance — selectors will return callState, contact, and control flags and actions”). Update references to the exact symbol names useCallState, useCallContact, useCallControls, and useCallActions so reviewers can easily find the decision.
172-177:useCallStatereturns a boolean (isConnecting), not the call state — the name is misleading.Consumers reading
useCallState()would expect to receive aCallStatevalue, but the hook returnstruewhen connecting (none | ringing | accepted) andfalsewhen active. Consider renaming touseIsConnecting(or similar) to make the semantics clear.🤖 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 172 - 177, The hook useCallState is misnamed because it returns a boolean indicating "connecting" rather than the CallState value; rename the function to useIsConnecting (and update its export) and keep its implementation (reading callState via useCallStore and returning callState === 'none' || callState === 'ringing' || callState === 'accepted'); search for and update all callers to import/use useIsConnecting instead of useCallState (or add a short deprecated wrapper export named useCallState that forwards to useIsConnecting if you need a transitional compatibility layer).app/containers/MediaCallHeader/MediaCallHeader.stories.tsx (1)
70-73: CallingDate.now()during render violates component purity rules.As flagged by ESLint (
react-hooks/purity),Date.now()is an impure call inside the render body. While this is a Storybook story and won't affect production, it can cause non-deterministic snapshot output (the timer value changes every run). Consider using a fixed timestamp:♻️ Suggested fix
export const ActiveCall = () => { - setStoreState({ callState: 'active', callStartTime: Date.now() }); + setStoreState({ callState: 'active', callStartTime: 1700000000000 }); return <MediaCallHeader />; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/MediaCallHeader/MediaCallHeader.stories.tsx` around lines 70 - 73, The story ActiveCall currently calls Date.now() during render which is impure; replace that dynamic call with a deterministic fixed timestamp (e.g. a numeric literal or a named constant like FIXED_CALL_START) when calling setStoreState so the render is pure and snapshots are stable; update the ActiveCall function to use the fixed timestamp for setStoreState({ callState: 'active', callStartTime: ... }) before returning <MediaCallHeader />.app/containers/MediaCallHeader/components/Subtitle.tsx (1)
27-38: Subtitle string-building logic is correct but could be clearer.The mutable
subtitlewith conditional concatenation and doublefilter(Boolean)call works, but is a bit dense to follow. A small refactor improves readability:♻️ Optional cleanup
- let subtitle = ''; - - if (!isConnected) { - subtitle = I18n.t('Connecting'); - } else { - subtitle = extension ? `${extension}` : ''; - const remoteState = []; - remoteState.push(remoteHeld ? I18n.t('On_hold') : null); - remoteState.push(remoteMute ? I18n.t('Muted') : null); - subtitle += remoteState.filter(Boolean).length > 0 && extension ? ' - ' : ''; - subtitle += remoteState.filter(Boolean).join(', '); - } + let subtitle = ''; + + if (!isConnected) { + subtitle = I18n.t('Connecting'); + } else { + const parts: string[] = []; + if (extension) parts.push(extension); + const remoteStates = [ + remoteHeld && I18n.t('On_hold'), + remoteMute && I18n.t('Muted') + ].filter(Boolean).join(', '); + if (remoteStates) parts.push(remoteStates); + subtitle = parts.join(' - '); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/MediaCallHeader/components/Subtitle.tsx` around lines 27 - 38, The subtitle building is correct but hard to follow; replace the mutable concatenation in the isConnected branch by building small arrays and joining them: compute remoteParts from remoteHeld/remoteMute (using I18n.t) and compute a leftPart from extension (or empty), then set subtitle to either leftPart plus " - " plus remoteParts.join(', ') only when remoteParts is non-empty and leftPart exists, otherwise join whichever part exists. Update references in this block: subtitle, isConnected, extension, remoteState/remoteParts, remoteHeld, and remoteMute so you remove the repeated remoteState.filter(Boolean) calls and make the logic clearer.app/containers/MediaCallHeader/MediaCallHeader.tsx (1)
3-3:useShallowprovides no benefit here; narrow the selector to a boolean.
callis only used for theif (!call)guard on line 34 — no properties are read. WithuseShallow, the component still re-renders on every top-level property change of the call object (e.g. mute toggled, hold toggled). A scalar selector eliminates all those spurious re-renders:♻️ Proposed refactor
-import { useShallow } from 'zustand/react/shallow'; ... -const call = useCallStore(useShallow(state => state.call)); +const hasCall = useCallStore(state => !!state.call); ... -if (!call) { +if (!hasCall) {Also applies to: 26-26
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/MediaCallHeader/MediaCallHeader.tsx` at line 3, The component currently uses useShallow with a selector that returns the full call object causing rerenders; change the selector in MediaCallHeader to return a boolean (e.g. const callExists = useStore(s => !!s.call)) and use that boolean for the guard (if (!callExists) return ...), update any references from the previous selector (e.g. replace calls to call in the guard) and remove the now-unused useShallow import; ensure the new selector is used where the component only needs to know presence of call, not its properties.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
app/containers/MediaCallHeader/__snapshots__/MediaCallHeader.test.tsx.snapis excluded by!**/*.snapapp/views/CallView/__snapshots__/index.test.tsx.snapis excluded by!**/*.snapapp/views/CallView/components/__snapshots__/CallerInfo.test.tsx.snapis excluded by!**/*.snap
📒 Files selected for processing (19)
app/AppContainer.tsxapp/containers/MediaCallHeader/MediaCallHeader.stories.tsxapp/containers/MediaCallHeader/MediaCallHeader.test.tsxapp/containers/MediaCallHeader/MediaCallHeader.tsxapp/containers/MediaCallHeader/components/Collapse.tsxapp/containers/MediaCallHeader/components/Content.tsxapp/containers/MediaCallHeader/components/EndCall.tsxapp/containers/MediaCallHeader/components/Subtitle.tsxapp/containers/MediaCallHeader/components/Timer.tsxapp/containers/MediaCallHeader/components/Title.tsxapp/lib/services/voip/useCallStore.tsapp/views/CallView/CallView.stories.tsxapp/views/CallView/components/CallButtons.tsxapp/views/CallView/components/CallStatusText.tsxapp/views/CallView/components/CallerInfo.test.tsxapp/views/CallView/components/CallerInfo.tsxapp/views/CallView/index.test.tsxapp/views/CallView/index.tsxapp/views/CallView/styles.ts
💤 Files with no reviewable changes (2)
- app/views/CallView/components/CallStatusText.tsx
- app/views/CallView/CallView.stories.tsx
🧰 Additional context used
🧬 Code graph analysis (8)
app/containers/MediaCallHeader/MediaCallHeader.test.tsx (2)
app/lib/services/voip/useCallStore.ts (1)
useCallStore(53-170).rnstorybook/generateSnapshots.tsx (1)
generateSnapshots(10-22)
app/views/CallView/components/CallButtons.tsx (4)
app/theme.tsx (1)
useTheme(29-29)app/lib/services/voip/useCallStore.ts (1)
useCallStore(53-170)app/views/CallView/styles.ts (1)
styles(5-75)app/lib/constants/colors.ts (1)
colors(280-302)
app/containers/MediaCallHeader/MediaCallHeader.stories.tsx (1)
app/lib/services/voip/useCallStore.ts (1)
useCallStore(53-170)
app/containers/MediaCallHeader/components/Subtitle.tsx (3)
app/theme.tsx (1)
useTheme(29-29)app/lib/services/voip/useCallStore.ts (1)
useCallStore(53-170)app/lib/constants/colors.ts (1)
colors(280-302)
app/containers/MediaCallHeader/components/EndCall.tsx (1)
app/lib/constants/colors.ts (1)
colors(280-302)
app/containers/MediaCallHeader/components/Title.tsx (1)
app/lib/constants/colors.ts (1)
colors(280-302)
app/views/CallView/index.tsx (5)
app/theme.tsx (1)
useTheme(29-29)app/lib/services/voip/useCallStore.ts (1)
useCallStore(53-170)app/views/CallView/styles.ts (1)
styles(5-75)app/lib/constants/colors.ts (1)
colors(280-302)app/views/CallView/components/CallButtons.tsx (1)
CallButtons(10-83)
app/views/CallView/components/CallerInfo.tsx (4)
app/theme.tsx (1)
useTheme(29-29)app/lib/services/voip/useCallStore.ts (1)
useCallContact(181-181)app/views/CallView/styles.ts (1)
styles(5-75)app/containers/message/MessageAvatar.tsx (1)
AvatarContainer(13-17)
🪛 ESLint
app/containers/MediaCallHeader/MediaCallHeader.stories.tsx
[error] 71-71: Error: Cannot call impure function during render
Date.now is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
/home/jailuser/git/app/containers/MediaCallHeader/MediaCallHeader.stories.tsx:71:54
69 |
70 | export const ActiveCall = () => {
71 | setStoreState({ callState: 'active', callStartTime: Date.now() });
| ^^^^^^^^^^ Cannot call impure function
72 | return ;
73 | };
74 |
(react-hooks/purity)
🔇 Additional comments (12)
app/containers/MediaCallHeader/components/EndCall.tsx (1)
13-19: LGTM — testID addition is consistent with the broader test-ID strategy acrossMediaCallHeadercomponents.app/views/CallView/styles.ts (1)
52-55: LGTM —borderTopColorcorrectly deferred to the inline style inCallButtons.tsxwhere theme context is available.app/containers/MediaCallHeader/components/Collapse.tsx (1)
15-21: LGTM — testID addition is in line with the rest of the MediaCallHeader test-ID rollout.app/containers/MediaCallHeader/components/Timer.tsx (1)
32-32: LGTM — the" - "prefix matches the header format shown in the PR screenshots and theTextnesting is valid in React Native.app/views/CallView/components/CallerInfo.tsx (1)
10-29: LGTM — clean simplification; the prop removal and reduced render surface align with the newCallButtonscomposition model.app/containers/MediaCallHeader/components/Title.tsx (1)
33-41: LGTM — the composedView+Status+Textlayout is clean, and inlineTimertext nesting is a valid React Native pattern.app/AppContainer.tsx (1)
22-22: LGTM!Clean swap from
CallHeadertoMediaCallHeader. Rendering it aboveNavigationContainerensures it remains visible across all navigation states.Also applies to: 56-56
app/views/CallView/index.tsx (1)
11-34: LGTM!The simplified composition of
CallerInfoandCallButtonsis clean and delegates state management to the child components. The keep-awake lifecycle is appropriately scoped to the component's mount/unmount.app/views/CallView/index.test.tsx (1)
290-311: LGTM!The updated tests correctly verify that the call view renders under different state combinations. The shift from checking a specific muted indicator to checking component presence aligns with the refactor that moved mute display logic into
CallButtons.app/containers/MediaCallHeader/MediaCallHeader.test.tsx (1)
1-172: LGTM!Comprehensive test suite covering all the key interactions and rendering states of
MediaCallHeader. Good use of store state overrides per test case and proper mock isolation for action handlers.app/containers/MediaCallHeader/MediaCallHeader.stories.tsx (1)
55-63: This review comment is incorrect —MediaCallHeaderdoes not use Redux.After checking the component and its descendants, there is no usage of Redux hooks (
useSelector,useDispatch) or Redux context. The component uses Zustand'suseCallStoreinstead. ThemockedStoreimported in the test file is unused, and theProviderwrapper there is superfluous. The missing ReduxProviderin the story decorator is not a concern since the component has no Redux dependency.Likely an incorrect or invalid review comment.
app/containers/MediaCallHeader/MediaCallHeader.tsx (1)
21-47: LGTM — clean refactor.The rename to
MediaCallHeader, testID additions on both render paths, the empty-state placeholder retaining safe-area padding without a separator border, and theContentswap are all consistent and well-structured. The React Compiler directive at line 22 is a'use memo'— a valid React Compiler directive that opts the function into compilation, useful for annotation-mode incremental adoption.
🤖 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/containers/MediaCallHeader/components/Content.tsx`:
- Around line 19-25: The Pressable in the Content component currently calls a
placeholder alert('nav to call room') on onPress; replace this stub with the
real navigation or state toggle: import and call the existing hook/action (e.g.,
useCallStore and its toggleFocus or the project's navigation callback) inside
the onPress handler, or conditionally call alert only under __DEV__; update the
Content component's onPress to invoke the appropriate function (toggleFocus or
navigation) instead of alert to avoid shipping a raw alert in production.
In `@app/views/CallView/components/CallButtons.tsx`:
- Around line 27-36: Remove the alert() debug stubs from handleMessage and
handleMore and replace them with no-op handlers (or disable/hide the associated
buttons) until the real implementations are added; specifically, in the
functions handleMessage and handleMore remove alert('Message') and alert('More')
and either leave an empty function body or call a shared noop (e.g., const noop
= () => {}) and, when ready, implement Navigation.navigate('RoomView', { rid, t:
'd' }) inside handleMessage and the action-sheet/DTMF/transfer UI inside
handleMore.
---
Outside diff comments:
In `@app/lib/services/voip/useCallStore.ts`:
- Around line 80-113: setCall currently attaches listeners (handleStateChange,
handleTrackStateChange, handleEnded) to call.emitter but never removes them,
causing handler buildup; modify setCall to run a stored cleanup before attaching
new listeners (store a cleanup function or reference to the current emitter and
attached handlers), call emitter.off/removeListener for each handler when
cleaning up, and ensure reset also invokes this cleanup; keep the existing
handler function names (handleStateChange, handleTrackStateChange, handleEnded),
and remove those exact functions from the previous call.emitter before assigning
a new call.
In `@app/views/CallView/components/CallerInfo.test.tsx`:
- Around line 55-67: The test name and inline comments for the CallerInfo test
are stale because the status container was removed; update the it(...)
description to reflect that the component should render caller info and avatar
only (e.g., "should render caller info and avatar"), remove or replace comments
about a commented-out Status component, and keep assertions against testIDs
'caller-info' and 'avatar' in the CallerInfo test to match the current
implementation of the CallerInfo component.
---
Nitpick comments:
In `@app/containers/MediaCallHeader/components/Subtitle.tsx`:
- Around line 27-38: The subtitle building is correct but hard to follow;
replace the mutable concatenation in the isConnected branch by building small
arrays and joining them: compute remoteParts from remoteHeld/remoteMute (using
I18n.t) and compute a leftPart from extension (or empty), then set subtitle to
either leftPart plus " - " plus remoteParts.join(', ') only when remoteParts is
non-empty and leftPart exists, otherwise join whichever part exists. Update
references in this block: subtitle, isConnected, extension,
remoteState/remoteParts, remoteHeld, and remoteMute so you remove the repeated
remoteState.filter(Boolean) calls and make the logic clearer.
In `@app/containers/MediaCallHeader/MediaCallHeader.stories.tsx`:
- Around line 70-73: The story ActiveCall currently calls Date.now() during
render which is impure; replace that dynamic call with a deterministic fixed
timestamp (e.g. a numeric literal or a named constant like FIXED_CALL_START)
when calling setStoreState so the render is pure and snapshots are stable;
update the ActiveCall function to use the fixed timestamp for setStoreState({
callState: 'active', callStartTime: ... }) before returning <MediaCallHeader />.
In `@app/containers/MediaCallHeader/MediaCallHeader.tsx`:
- Line 3: The component currently uses useShallow with a selector that returns
the full call object causing rerenders; change the selector in MediaCallHeader
to return a boolean (e.g. const callExists = useStore(s => !!s.call)) and use
that boolean for the guard (if (!callExists) return ...), update any references
from the previous selector (e.g. replace calls to call in the guard) and remove
the now-unused useShallow import; ensure the new selector is used where the
component only needs to know presence of call, not its properties.
In `@app/lib/services/voip/useCallStore.ts`:
- Around line 179-196: Clean up the large commented-out selector block around
useCallContact: either remove the unused commented selectors (useCallState,
useCallControls, useCallActions) to reduce noise, or keep them but replace the
block with a concise TODO explaining intended future use and expected API (e.g.,
“// TODO: reintroduce selector hooks useCallState, useCallControls,
useCallActions when optimizing render performance — selectors will return
callState, contact, and control flags and actions”). Update references to the
exact symbol names useCallState, useCallContact, useCallControls, and
useCallActions so reviewers can easily find the decision.
- Around line 172-177: The hook useCallState is misnamed because it returns a
boolean indicating "connecting" rather than the CallState value; rename the
function to useIsConnecting (and update its export) and keep its implementation
(reading callState via useCallStore and returning callState === 'none' ||
callState === 'ringing' || callState === 'accepted'); search for and update all
callers to import/use useIsConnecting instead of useCallState (or add a short
deprecated wrapper export named useCallState that forwards to useIsConnecting if
you need a transitional compatibility layer).
In `@app/views/CallView/components/CallButtons.tsx`:
- Around line 38-40: The handleEndCall function is a one-line passthrough to
endCall; remove the unnecessary wrapper by deleting the handleEndCall
declaration and replace any references (e.g., onClick or props that currently
use handleEndCall) to call endCall directly so callers use endCall instead of
the intermediary function.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (7)
app/views/CallView/components/CallerInfo.stories.tsx (1)
47-50: Optional: move store setup out of render to avoid render-time side effects.
setStoreStateis called directly in the story function body, which is a side effect during render. While harmless for Storybook today (ZustandsetStateis idempotent), it can fire multiple times under React Strict Mode. Using a story-level decorator keeps the pattern consistent with theDefaultstory.♻️ Proposed refactor
-export const UsernameOnly = () => { - setStoreState({ username: 'john.doe' }); - return <CallerInfo />; -}; +export const UsernameOnly = { + decorators: [ + (Story: React.ComponentType) => { + setStoreState({ username: 'john.doe' }); + return <Story />; + } + ], + render: () => <CallerInfo /> +};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/views/CallView/components/CallerInfo.stories.tsx` around lines 47 - 50, The UsernameOnly story currently calls setStoreState({ username: 'john.doe' }) directly during render (in the UsernameOnly function); move this store setup into a story-level decorator (matching how Default is implemented) or into a before-render/setup helper so the side effect does not run during render/Strict Mode; locate the UsernameOnly export and replace the direct setStoreState call with a decorator that calls setStoreState before rendering <CallerInfo /> to keep store initialization consistent with Default.app/containers/MediaCallHeader/MediaCallHeader.test.tsx (2)
36-57: Shared utility opportunity — mirrors the helper inMediaCallHeader.stories.tsx.Same refactor opportunity noted in the stories file: extracting a common
buildMockCallStatefactory to a shared test utility would remove this near-duplicate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/MediaCallHeader/MediaCallHeader.test.tsx` around lines 36 - 57, The test helper setStoreState duplicates the mock state builder used in MediaCallHeader.stories.tsx; refactor by extracting a shared factory (e.g., buildMockCallState) into a test utilities module and have setStoreState call that factory instead of inlining values. Update references to use the new factory when constructing the mock call and state (notably createMockCall, useCallStore.setState, and the contact/call fields) and import the shared buildMockCallState in both the test and stories to remove duplication.
159-169: This test is validating a navigation placeholder — it should be replaced when real navigation lands.
Content.tsxcurrently firesalert('nav to call room')on press (from the relevant snippet), which is a stub for the real navigation to the call room. Both the component and this assertion will need updating together once the real nav is implemented.Would you like me to open a follow-up issue to track replacing the
alert()stub with actual navigation and updating the assertion to match (e.g.,expect(Navigation.navigate).toHaveBeenCalledWith('CallView', { callUUID: ... }))?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/MediaCallHeader/MediaCallHeader.test.tsx` around lines 159 - 169, The test and component are asserting a placeholder alert call; replace the alert stub in Content.tsx with the real navigation call and update the test to assert navigation instead of alert: in Content.tsx (the onPress handler that currently calls alert('nav to call room')) call the app navigation API (e.g., Navigation.navigate('CallView', { callUUID })) via the same prop or module used across the app, and in MediaCallHeader.test.tsx replace the global.alert expectation with a mock/spied Navigation.navigate assertion (targeting the 'media-call-header-content' testID), ensuring you mock or inject Navigation in the test so expect(Navigation.navigate).toHaveBeenCalledWith('CallView', { callUUID: expect.any(String) }) or the exact params your implementation uses.app/containers/MediaCallHeader/MediaCallHeader.stories.tsx (2)
16-55: Consider extracting shared test utilities to avoid duplication with the test file.
setStoreStateand the mock call shape here are near-duplicates of what's defined inMediaCallHeader.test.tsx(lines 36–57). The only intentional differences are: callbacks use plain functions here vs.jest.fn()in tests, and the start time is a fixed constant vs.Date.now(). A shared__tests__/testUtils.ts(exporting abuildMockCallStatefactory and acreateMockCallhelper with an injectablecallbackFactoryargument) would consolidate this.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/MediaCallHeader/MediaCallHeader.stories.tsx` around lines 16 - 55, Extract the duplicated mock setup into a shared helper: create a factory (e.g., buildMockCallState) and a createMockCall helper that accepts an optional callbackFactory (defaulting to jest.fn for tests or a noop for stories) and an optional startTime parameter instead of using mockCallStartTime/Date.now inline; replace the inline mockCall and setStoreState usage in MediaCallHeader.stories (and mirror-change MediaCallHeader.test.tsx) to call buildMockCallState()/createMockCall and import useCallStore to setState with the returned object so both tests and stories reuse the same factory.
71-74:NoCallstory does a partial merge that preserves residual state from prior stories.
useCallStore.setState({ call: null })is a merge-in that only nullscall; all other fields (contact,callState, etc.) remain from whatever state was set last.MediaCallHeaderonly gate-checkscall, so this is visually correct today. However, if aSubtitleor other child component is added that reads additional fields in the null-call branch, this could silently exhibit stale data.Consider a full reset:
🛡️ Proposed fix
export const NoCall = () => { - useCallStore.setState({ call: null }); + useCallStore.getState().reset(); return <MediaCallHeader />; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/MediaCallHeader/MediaCallHeader.stories.tsx` around lines 71 - 74, The story NoCall currently calls useCallStore.setState({ call: null }) which merges and leaves other fields (contact, callState, etc.) stale; instead reset the entire call store to its initial clean state before rendering MediaCallHeader. Replace the partial merge with a full reset by importing or referencing the store's initial state (e.g., initialCallState from the module that defines useCallStore) and calling useCallStore.setState(() => initialCallState) (or otherwise assign the exact initial object) so no residual fields remain when MediaCallHeader renders.app/containers/MediaCallHeader/MediaCallHeader.tsx (2)
41-41:Content'sonPressis a navigation placeholder (alert('nav to call room')).From
Content.tsx(relevant snippet, line 19):onPress={() => alert('nav to call room')}. This stub is referenced in the test and baked into both the component and assertion. Replace with the real navigation call before shipping.Would you like me to generate the implementation that navigates to the call room (e.g.,
Navigation.navigate('CallView', { callUUID: store.callUUID })) and the matching test assertion?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/MediaCallHeader/MediaCallHeader.tsx` at line 41, Replace the placeholder alert in the Content component's onPress with the real navigation call: remove onPress={() => alert('nav to call room')} and call your navigation helper (e.g., Navigation.navigate('CallView', { callUUID: store.callUUID })) so pressing the header navigates to the call room; update the test assertion to expect the Navigation.navigate call with the 'CallView' route and the store.callUUID payload instead of the alert. Ensure you reference the Content component's onPress handler in MediaCallHeader and import/Mock the Navigation module used by your app and read callUUID from the same store used by the component.
3-3:useShallowis unnecessary for a direct single-property accessor.
useShallowis intended for when you want to "construct a single object with multiple state-picks inside, similar to redux'smapStateToProps."useShallowis "not required but recommended when you want to return an object or array but not needed when you return primitive values."
state => state.callreturns an existing stored reference —shallowis not necessary because the selected data is not a complex object constructed inline; it is not needed when the returned value can be compared usingObject.is. Default reference equality is the correct choice here. TheuseShallowimport can be dropped.♻️ Proposed fix
-import { useShallow } from 'zustand/react/shallow'; ... - const call = useCallStore(useShallow(state => state.call)); + const call = useCallStore(state => state.call);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/containers/MediaCallHeader/MediaCallHeader.tsx` at line 3, Remove the unnecessary shallow comparator import and usage: delete the import "useShallow" and update the selector call that uses "useShallow" (the selector currently written as state => state.call with shallow) to simply call the zustand selector without the shallow comparator. Target the import line "import { useShallow } from 'zustand/react/shallow';" and the selector usage where "useShallow" is passed; remove the import and the shallow argument so the hook selects state.call directly.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
app/containers/MediaCallHeader/__snapshots__/MediaCallHeader.test.tsx.snapis excluded by!**/*.snapapp/views/CallView/__snapshots__/index.test.tsx.snapis excluded by!**/*.snapapp/views/CallView/components/__snapshots__/CallerInfo.test.tsx.snapis excluded by!**/*.snap
📒 Files selected for processing (10)
app/containers/MediaCallHeader/MediaCallHeader.stories.tsxapp/containers/MediaCallHeader/MediaCallHeader.test.tsxapp/containers/MediaCallHeader/MediaCallHeader.tsxapp/containers/MediaCallHeader/components/Collapse.tsxapp/containers/MediaCallHeader/components/Content.tsxapp/containers/MediaCallHeader/components/EndCall.tsxapp/views/CallView/CallView.stories.tsxapp/views/CallView/components/CallerInfo.stories.tsxapp/views/CallView/components/CallerInfo.test.tsxapp/views/CallView/index.test.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- app/containers/MediaCallHeader/components/Content.tsx
- app/containers/MediaCallHeader/components/Collapse.tsx
- app/views/CallView/index.test.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
app/containers/MediaCallHeader/MediaCallHeader.tsx (4)
app/theme.tsx (1)
useTheme(29-29)app/lib/services/voip/useCallStore.ts (1)
useCallStore(53-170)app/lib/constants/colors.ts (1)
colors(280-302)app/containers/MediaCallHeader/components/Content.tsx (1)
Content(19-26)
app/containers/MediaCallHeader/MediaCallHeader.stories.tsx (2)
app/lib/services/voip/useCallStore.ts (1)
useCallStore(53-170)app/views/CallView/CallView.stories.tsx (1)
ConnectingCall(92-95)
app/containers/MediaCallHeader/MediaCallHeader.test.tsx (2)
app/lib/services/voip/useCallStore.ts (1)
useCallStore(53-170).rnstorybook/generateSnapshots.tsx (1)
generateSnapshots(10-22)
🔇 Additional comments (6)
app/views/CallView/components/CallerInfo.test.tsx (2)
11-12: Good: Fixed timestamp for deterministic tests.Using a constant instead of
Date.now()ensures snapshot stability and reproducible test results.
34-44: Tests align well with the simplified CallerInfo component.The two test cases cover the essential rendering paths (display name and username fallback), and
generateSnapshots(stories)provides visual regression coverage. ThesipExtension: '2244'in the store setup on line 35 is no longer asserted, which is consistent with the component no longer rendering it — just noting it's harmless residual test data.app/views/CallView/components/CallerInfo.stories.tsx (1)
17-28: LGTM. Removal ofcallStartTimeand the trimmed store state are consistent with the CallerInfo simplification described in the PR.app/views/CallView/CallView.stories.tsx (1)
71-85: LGTM — replacing the navigation mock with a realNavigationContaineris cleaner.Using the actual
NavigationContainerin the decorator removes the risk of the mock diverging from the real implementation over time.app/containers/MediaCallHeader/components/EndCall.tsx (1)
13-19: LGTM!The
testIDaddition is consistent with the pattern applied acrossCollapse,Content, and the main header container, giving the full header complete testing coverage.app/containers/MediaCallHeader/MediaCallHeader.test.tsx (1)
129-157: LGTM on the interaction tests.Injecting mock action functions via
useCallStore.setState({ toggleFocus })/useCallStore.setState({ endCall })aftersetStoreState()is a clean, focused pattern for verifying specific action calls without re-wiring the whole store.
🤖 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/containers/MediaCallHeader/MediaCallHeader.test.tsx`:
- Line 98: Update the stale test description string in the test case that
currently reads "should show caller name in Title" (the "it" block in
MediaCallHeader.test.tsx) to reflect the refactor that moved Title into a
private implementation; change it to something like "should show caller name in
Content" or "should show caller name in header" so the description matches the
component behavior.
In `@app/views/CallView/CallView.stories.tsx`:
- Line 19: The stories use a hardcoded mockCallStartTime (constant
mockCallStartTime in CallView.stories.tsx) that is far in the past, so
subtracting 61000 no longer yields ~61s; update the stories to produce
deterministic, realistic elapsed timers by either setting mockCallStartTime to a
value near the current time (e.g., Date.now() captured at story render) or by
freezing/mocking Date.now for the story suite, and remove any relative offsets
(e.g., the -61000 in ConnectedCall and any children stories like MutedCall,
OnHoldCall) unless Date.now is also mocked; ensure the chosen approach is
applied consistently across all stories that inherit callStartTime to avoid
MM:SS overflow in snapshots and visual tests.
---
Nitpick comments:
In `@app/containers/MediaCallHeader/MediaCallHeader.stories.tsx`:
- Around line 16-55: Extract the duplicated mock setup into a shared helper:
create a factory (e.g., buildMockCallState) and a createMockCall helper that
accepts an optional callbackFactory (defaulting to jest.fn for tests or a noop
for stories) and an optional startTime parameter instead of using
mockCallStartTime/Date.now inline; replace the inline mockCall and setStoreState
usage in MediaCallHeader.stories (and mirror-change MediaCallHeader.test.tsx) to
call buildMockCallState()/createMockCall and import useCallStore to setState
with the returned object so both tests and stories reuse the same factory.
- Around line 71-74: The story NoCall currently calls useCallStore.setState({
call: null }) which merges and leaves other fields (contact, callState, etc.)
stale; instead reset the entire call store to its initial clean state before
rendering MediaCallHeader. Replace the partial merge with a full reset by
importing or referencing the store's initial state (e.g., initialCallState from
the module that defines useCallStore) and calling useCallStore.setState(() =>
initialCallState) (or otherwise assign the exact initial object) so no residual
fields remain when MediaCallHeader renders.
In `@app/containers/MediaCallHeader/MediaCallHeader.test.tsx`:
- Around line 36-57: The test helper setStoreState duplicates the mock state
builder used in MediaCallHeader.stories.tsx; refactor by extracting a shared
factory (e.g., buildMockCallState) into a test utilities module and have
setStoreState call that factory instead of inlining values. Update references to
use the new factory when constructing the mock call and state (notably
createMockCall, useCallStore.setState, and the contact/call fields) and import
the shared buildMockCallState in both the test and stories to remove
duplication.
- Around line 159-169: The test and component are asserting a placeholder alert
call; replace the alert stub in Content.tsx with the real navigation call and
update the test to assert navigation instead of alert: in Content.tsx (the
onPress handler that currently calls alert('nav to call room')) call the app
navigation API (e.g., Navigation.navigate('CallView', { callUUID })) via the
same prop or module used across the app, and in MediaCallHeader.test.tsx replace
the global.alert expectation with a mock/spied Navigation.navigate assertion
(targeting the 'media-call-header-content' testID), ensuring you mock or inject
Navigation in the test so
expect(Navigation.navigate).toHaveBeenCalledWith('CallView', { callUUID:
expect.any(String) }) or the exact params your implementation uses.
In `@app/containers/MediaCallHeader/MediaCallHeader.tsx`:
- Line 41: Replace the placeholder alert in the Content component's onPress with
the real navigation call: remove onPress={() => alert('nav to call room')} and
call your navigation helper (e.g., Navigation.navigate('CallView', { callUUID:
store.callUUID })) so pressing the header navigates to the call room; update the
test assertion to expect the Navigation.navigate call with the 'CallView' route
and the store.callUUID payload instead of the alert. Ensure you reference the
Content component's onPress handler in MediaCallHeader and import/Mock the
Navigation module used by your app and read callUUID from the same store used by
the component.
- Line 3: Remove the unnecessary shallow comparator import and usage: delete the
import "useShallow" and update the selector call that uses "useShallow" (the
selector currently written as state => state.call with shallow) to simply call
the zustand selector without the shallow comparator. Target the import line
"import { useShallow } from 'zustand/react/shallow';" and the selector usage
where "useShallow" is passed; remove the import and the shallow argument so the
hook selects state.call directly.
In `@app/views/CallView/components/CallerInfo.stories.tsx`:
- Around line 47-50: The UsernameOnly story currently calls setStoreState({
username: 'john.doe' }) directly during render (in the UsernameOnly function);
move this store setup into a story-level decorator (matching how Default is
implemented) or into a before-render/setup helper so the side effect does not
run during render/Strict Mode; locate the UsernameOnly export and replace the
direct setStoreState call with a decorator that calls setStoreState before
rendering <CallerInfo /> to keep store initialization consistent with Default.
| // params: { callUUID: 'test-uuid' } | ||
| // }) | ||
| // })); | ||
| const mockCallStartTime = 1713340800000; |
There was a problem hiding this comment.
mockCallStartTime - 61000 no longer produces a ~61 s elapsed duration.
mockCallStartTime is 1713340800000 (April 2024), so the UI timer — which computes Date.now() - callStartTime at render time — will display approximately 22 months of elapsed time rather than ~61 seconds. The -61000 offset is now semantically inert. The same applies to every story that inherits the base callStartTime: mockCallStartTime (MutedCall, OnHoldCall, etc.) — all will show a multi-day duration.
If the only goal is preventing Date.now() from being called during store initialisation, this is harmless. But if the stories are used for snapshot tests or visual comparison, the displayed timer will be wrong and may overflow a MM:SS format.
For true display-level determinism the timer's Date.now() call would also need to be frozen, e.g.:
+// In test/story setup (e.g. via jest.spyOn or a Storybook global decorator)
+jest.spyOn(Date, 'now').mockReturnValue(mockCallStartTime);Or, keep the constant close to the present and update it periodically:
-const mockCallStartTime = 1713340800000;
+const mockCallStartTime = 1740441600000; // Feb 25, 2026 — update when staleAnd for ConnectedCall, use an offset relative to mockCallStartTime only if Date.now is also mocked; otherwise the simpler approach is:
-setStoreState({ callState: 'active', callStartTime: mockCallStartTime - 61000 });
+setStoreState({ callState: 'active', callStartTime: mockCallStartTime });Also applies to: 88-88
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/views/CallView/CallView.stories.tsx` at line 19, The stories use a
hardcoded mockCallStartTime (constant mockCallStartTime in CallView.stories.tsx)
that is far in the past, so subtracting 61000 no longer yields ~61s; update the
stories to produce deterministic, realistic elapsed timers by either setting
mockCallStartTime to a value near the current time (e.g., Date.now() captured at
story render) or by freezing/mocking Date.now for the story suite, and remove
any relative offsets (e.g., the -61000 in ConnectedCall and any children stories
like MutedCall, OnHoldCall) unless Date.now is also mocked; ensure the chosen
approach is applied consistently across all stories that inherit callStartTime
to avoid MM:SS overflow in snapshots and visual tests.
…/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
Issue(s)
How to test or reproduce
Screenshots
Types of changes
Checklist
Further comments
Summary by CodeRabbit
New Features
Refactor
Style