Skip to content

feat: Update call UI#6990

Merged
diegolmello merged 7 commits into
feat.voip-lib-newfrom
VMUX-43
Feb 18, 2026
Merged

feat: Update call UI#6990
diegolmello merged 7 commits into
feat.voip-lib-newfrom
VMUX-43

Conversation

@diegolmello

@diegolmello diegolmello commented Feb 17, 2026

Copy link
Copy Markdown
Member

Proposed changes

Issue(s)

How to test or reproduce

Screenshots

Screenshot_1771361766 Screenshot_1771361764 Screenshot_1771361757 Screenshot_1771361752 Screenshot_1771361829 Screenshot_1771361815

Types of changes

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

Checklist

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

Further comments

Summary by CodeRabbit

  • New Features

    • In-call action buttons added for speaker, hold, mute, message and end call.
    • Tapable call header content now navigates to the call room.
    • Subtitle now shows connection and remote state (Connecting, On hold, Muted).
  • Refactor

    • Redesigned call header with clearer status and caller layout.
    • Simplified Call View and caller info for a leaner in-call UI.
  • Style

    • Call timer prefixed with a leading " - " for visual consistency.

@coderabbitai

coderabbitai Bot commented Feb 17, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Replaces 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

Cohort / File(s) Summary
App container
app/AppContainer.tsx
Swaps rendered CallHeader for MediaCallHeader and updates import.
MediaCallHeader core
app/containers/MediaCallHeader/MediaCallHeader.tsx
Renames component/export to MediaCallHeader, uses useCallStore with useShallow, adjusts header padding/bottom, adds testIDs, replaces Title with Content in layout.
MediaCallHeader stories & tests
app/containers/MediaCallHeader/MediaCallHeader.stories.tsx, app/containers/MediaCallHeader/MediaCallHeader.test.tsx
Adds Storybook stories for multiple call states and comprehensive unit tests covering render states, interactions (collapse/end/content press), and snapshots.
MediaCallHeader subcomponents
app/containers/MediaCallHeader/components/*
Collapse.tsx, Content.tsx, EndCall.tsx, Subtitle.tsx, Title.tsx, Timer.tsx
Adds Content component (pressable with nav alert), introduces Subtitle (connection/remote state logic), refactors Title to include Status plus inline Timer, prefixes Timer text with " - ", and adds testID attributes to Collapse/EndCall.
VoIP call store
app/lib/services/voip/useCallStore.ts
Imports CallContact type, adds remoteMute and remoteHeld to state and initialState, sets focused default true, removes updateFromCall, populates remote fields in setCall and event handlers, and changes useCallState to return a derived boolean.
CallView composition
app/views/CallView/index.tsx, app/views/CallView/styles.ts, app/views/CallView/components/CallButtons.tsx
Simplifies CallView to render CallerInfo and new CallButtons; removes prior internal button logic; adds CallButtons component (mute/hold/speaker/end actions) and a borderTopWidth to buttons container styles.
CallerInfo and related tests/stories
app/views/CallView/components/CallerInfo.tsx, app/views/CallView/components/CallerInfo.test.tsx, app/views/CallView/components/CallerInfo.stories.tsx
Removes isMuted prop, extension display, and Status UI from CallerInfo; updates tests/stories to remove assertions/stories that depended on those elements and uses deterministic mockCallStartTime in stories/tests.
Removed component
app/views/CallView/components/CallStatusText.tsx
Deletes CallStatusText; status rendering responsibilities moved into MediaCallHeader (Subtitle).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through headers, swapped old for new,
Content clicked, subtitles whispering true,
Buttons to press, timers that softly chime,
Remote mutes and holds now tracked in time,
A little rabbit clap — the UI feels new! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'feat: Update call UI' is vague and generic, using broad terminology that doesn't convey the specific technical nature of the changes beyond a surface-level description. Consider a more specific title such as 'feat: Refactor call UI with MediaCallHeader and reorganize call controls' to better reflect the substantial changes in component structure.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR addresses all objectives from VMUX-43: fixes collapsible header bugs, updates UI to new design standards shown in screenshots, and reorganizes call controls into dedicated components.
Out of Scope Changes check ✅ Passed All changes align with VMUX-43 objectives—MediaCallHeader component restructuring, call control refactoring, and subtitle/status updates all directly support the collapsible header fix and new UI design.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🟡 Minor

Stale 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.tsx no longer renders any status container at all — it was removed entirely in this PR. The test only validates testID='caller-info' and testID='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 | 🟠 Major

Event listeners are never unsubscribed; repeated setCall calls will stack handlers.

When setCall is called, it subscribes to stateChange, trackStateChange, and ended on the call emitter, but there is no corresponding cleanup. If setCall is invoked again (e.g., a new incoming call replaces an existing one before reset), the previous listeners remain active on the old emitter and could trigger stale state updates or Navigation.back() unexpectedly.

Consider storing a cleanup function and invoking it at the start of setCall (and in reset):

🛡️ 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: handleEndCall is a single-line passthrough — wire endCall directly.

♻️ 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: useCallState returns a boolean (isConnecting), not the call state — the name is misleading.

Consumers reading useCallState() would expect to receive a CallState value, but the hook returns true when connecting (none | ringing | accepted) and false when active. Consider renaming to useIsConnecting (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: Calling Date.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 subtitle with conditional concatenation and double filter(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: useShallow provides no benefit here; narrow the selector to a boolean.

call is only used for the if (!call) guard on line 34 — no properties are read. With useShallow, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 18b0b0a and 0f99a7a.

⛔ Files ignored due to path filters (3)
  • app/containers/MediaCallHeader/__snapshots__/MediaCallHeader.test.tsx.snap is excluded by !**/*.snap
  • app/views/CallView/__snapshots__/index.test.tsx.snap is excluded by !**/*.snap
  • app/views/CallView/components/__snapshots__/CallerInfo.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (19)
  • app/AppContainer.tsx
  • app/containers/MediaCallHeader/MediaCallHeader.stories.tsx
  • app/containers/MediaCallHeader/MediaCallHeader.test.tsx
  • app/containers/MediaCallHeader/MediaCallHeader.tsx
  • app/containers/MediaCallHeader/components/Collapse.tsx
  • app/containers/MediaCallHeader/components/Content.tsx
  • app/containers/MediaCallHeader/components/EndCall.tsx
  • app/containers/MediaCallHeader/components/Subtitle.tsx
  • app/containers/MediaCallHeader/components/Timer.tsx
  • app/containers/MediaCallHeader/components/Title.tsx
  • app/lib/services/voip/useCallStore.ts
  • app/views/CallView/CallView.stories.tsx
  • app/views/CallView/components/CallButtons.tsx
  • app/views/CallView/components/CallStatusText.tsx
  • app/views/CallView/components/CallerInfo.test.tsx
  • app/views/CallView/components/CallerInfo.tsx
  • app/views/CallView/index.test.tsx
  • app/views/CallView/index.tsx
  • app/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 across MediaCallHeader components.

app/views/CallView/styles.ts (1)

52-55: LGTM — borderTopColor correctly deferred to the inline style in CallButtons.tsx where 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 the Text nesting 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 new CallButtons composition model.

app/containers/MediaCallHeader/components/Title.tsx (1)

33-41: LGTM — the composed View + Status + Text layout is clean, and inline Timer text nesting is a valid React Native pattern.

app/AppContainer.tsx (1)

22-22: LGTM!

Clean swap from CallHeader to MediaCallHeader. Rendering it above NavigationContainer ensures 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 CallerInfo and CallButtons is 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 — MediaCallHeader does 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's useCallStore instead. The mockedStore imported in the test file is unused, and the Provider wrapper there is superfluous. The missing Redux Provider in 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 the Content swap 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.

Comment thread app/containers/MediaCallHeader/components/Content.tsx
Comment thread app/views/CallView/components/CallButtons.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 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.

setStoreState is called directly in the story function body, which is a side effect during render. While harmless for Storybook today (Zustand setState is idempotent), it can fire multiple times under React Strict Mode. Using a story-level decorator keeps the pattern consistent with the Default story.

♻️ 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 in MediaCallHeader.stories.tsx.

Same refactor opportunity noted in the stories file: extracting a common buildMockCallState factory 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.tsx currently fires alert('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.

setStoreState and the mock call shape here are near-duplicates of what's defined in MediaCallHeader.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 a buildMockCallState factory and a createMockCall helper with an injectable callbackFactory argument) 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: NoCall story does a partial merge that preserves residual state from prior stories.

useCallStore.setState({ call: null }) is a merge-in that only nulls call; all other fields (contact, callState, etc.) remain from whatever state was set last. MediaCallHeader only gate-checks call, so this is visually correct today. However, if a Subtitle or 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's onPress is 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: useShallow is unnecessary for a direct single-property accessor.

useShallow is intended for when you want to "construct a single object with multiple state-picks inside, similar to redux's mapStateToProps." useShallow is "not required but recommended when you want to return an object or array but not needed when you return primitive values."

state => state.call returns an existing stored reference — shallow is not necessary because the selected data is not a complex object constructed inline; it is not needed when the returned value can be compared using Object.is. Default reference equality is the correct choice here. The useShallow import 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0f99a7a and a48bf11.

⛔ Files ignored due to path filters (3)
  • app/containers/MediaCallHeader/__snapshots__/MediaCallHeader.test.tsx.snap is excluded by !**/*.snap
  • app/views/CallView/__snapshots__/index.test.tsx.snap is excluded by !**/*.snap
  • app/views/CallView/components/__snapshots__/CallerInfo.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (10)
  • app/containers/MediaCallHeader/MediaCallHeader.stories.tsx
  • app/containers/MediaCallHeader/MediaCallHeader.test.tsx
  • app/containers/MediaCallHeader/MediaCallHeader.tsx
  • app/containers/MediaCallHeader/components/Collapse.tsx
  • app/containers/MediaCallHeader/components/Content.tsx
  • app/containers/MediaCallHeader/components/EndCall.tsx
  • app/views/CallView/CallView.stories.tsx
  • app/views/CallView/components/CallerInfo.stories.tsx
  • app/views/CallView/components/CallerInfo.test.tsx
  • app/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. The sipExtension: '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 of callStartTime and 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 real NavigationContainer is cleaner.

Using the actual NavigationContainer in 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 testID addition is consistent with the pattern applied across Collapse, 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 }) after setStoreState() 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.

Comment thread app/containers/MediaCallHeader/MediaCallHeader.test.tsx
// params: { callUUID: 'test-uuid' }
// })
// }));
const mockCallStartTime = 1713340800000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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 stale

And 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.

@OtavioStasiak OtavioStasiak left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@diegolmello diegolmello merged commit fc9aca6 into feat.voip-lib-new Feb 18, 2026
15 of 20 checks passed
@diegolmello diegolmello deleted the VMUX-43 branch February 18, 2026 19:31
This was referenced Mar 3, 2026
diegolmello added a commit that referenced this pull request Apr 22, 2026
…/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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants