Skip to content

feat: Pre flight#7038

Open
diegolmello wants to merge 17 commits intofeat.voip-lib-newfrom
feat.pre-flight
Open

feat: Pre flight#7038
diegolmello wants to merge 17 commits intofeat.voip-lib-newfrom
feat.pre-flight

Conversation

@diegolmello
Copy link
Member

@diegolmello diegolmello commented Mar 5, 2026

Proposed changes

Issue(s)

https://rocketchat.atlassian.net/browse/VMUX-15

How to test or reproduce

Screenshots

Screenshot_1772830131 Screenshot_1772830122

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

    • New media call UI: contact search, selection, and call button (includes SIP dialing); accessible from multiple screens via a new call flow.
  • Bug Fixes

    • Status icon now centered horizontally and vertically.
  • Chores

    • Added English locale entries and adjusted dark theme color; new default VoIP setting added.
  • Tests

    • Extensive unit and Storybook snapshot coverage for media call UI, hooks, and related components.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

Walkthrough

Adds a New Media Call feature: peer autocomplete service and Zustand store, UI components (FilterHeader, PeerList, PeerItem, SelectedPeer, CreateCall, Container), a useNewMediaCall hook and useSubscription utility, integrates ActionSheet-driven call flow across views, plus Storybook stories and tests.

Changes

Cohort / File(s) Summary
New MediaCall container & exports
app/containers/NewMediaCall/index.tsx, app/containers/NewMediaCall/NewMediaCall.tsx, app/containers/NewMediaCall/Container.tsx, app/containers/NewMediaCall/Container.stories.tsx
Adds NewMediaCall container, container reset on unmount, re-export index and a Container component with story wrapper.
Filter & search UI
app/containers/NewMediaCall/FilterHeader.tsx, app/containers/NewMediaCall/FilterHeader.stories.tsx, app/containers/NewMediaCall/FilterHeader.test.tsx
Debounced search input wired to peer autocomplete store (setFilter, clearSelection, debounced fetchOptions); stories and tests exercise debounce and fetch timing.
Peer list & items
app/containers/NewMediaCall/PeerList.tsx, app/containers/NewMediaCall/PeerList.stories.tsx, app/containers/NewMediaCall/PeerList.test.tsx, app/containers/NewMediaCall/PeerItem.tsx, app/containers/NewMediaCall/PeerItem.stories.tsx, app/containers/NewMediaCall/PeerItem.test.tsx, app/containers/NewMediaCall/PeerItemInner.tsx
FlatList of normalized options, selection handling, pressable PeerItem and presentational PeerItemInner (avatar/SIP icon/status); stories and unit tests added.
Selected peer UI
app/containers/NewMediaCall/SelectedPeer.tsx, app/containers/NewMediaCall/SelectedPeer.stories.tsx, app/containers/NewMediaCall/SelectedPeer.test.tsx
SelectedPeer (store-connected) and SelectedPeerInner render chosen peer with clear action; includes stories and tests.
Call action button
app/containers/NewMediaCall/CreateCall.tsx, app/containers/NewMediaCall/CreateCall.stories.tsx, app/containers/NewMediaCall/CreateCall.test.tsx
CreateCall button uses selectedPeer to call via mediaSessionInstance.startCall(value, type), hides ActionSheet after call; themed, accessible, and covered by tests and stories.
VoIP services & store
app/lib/services/voip/getPeerAutocompleteOptions.ts, app/lib/services/voip/usePeerAutocompleteStore.ts, app/lib/services/voip/usePeerAutocompleteStore.test.ts
Adds TPeerItem type and getPeerAutocompleteOptions (prepends SIP option, normalizes users) and a Zustand peer autocomplete store with fetch/set/clear/reset and tests.
New hook & subscription
app/lib/hooks/useNewMediaCall/index.ts, app/lib/hooks/useNewMediaCall/useNewMediaCall.tsx, app/lib/hooks/useNewMediaCall/useNewMediaCall.test.tsx, app/lib/hooks/useSubscription.ts
Adds useNewMediaCall exposing openNewMediaCall and hasMediaCallPermission (prefills selected peer for DMs and opens ActionSheet) and a useSubscription hook; tests added.
Views: integrate new flow
app/views/NewMessageView/Item.tsx, app/views/NewMessageView/Item.test.tsx, app/views/RoomActionsView/components/CallSection.tsx, app/views/RoomView/components/HeaderCallButton.tsx, app/views/RoomView/RightButtons.tsx, app/views/SidebarView/components/Stacks.tsx
Replaces direct MediaSession calls with useNewMediaCall.openNewMediaCall + ActionSheet-driven NewMediaCall; updates imports, permission gating, testIDs and tests; removes some prior props (e.g., room).
Room info buttons & BaseButton
app/views/RoomInfoView/components/BaseButton.tsx, app/views/RoomInfoView/components/RoomInfoButtons.tsx, app/views/RoomInfoView/components/RoomInfoButtons.test.tsx
Introduces BaseButton and refactors RoomInfoButtons to use BaseButton and useNewMediaCall; tests updated to assert hook-based behavior.
Stories & tests glue
app/containers/NewMediaCall/*.stories.tsx, app/containers/NewMediaCall/*.test.tsx, app/lib/services/voip/*.test.ts, app/lib/hooks/*.test.ts
Extensive Storybook stories and unit tests added across components, store, services, and hooks; snapshot generation included.
Localization, theming, settings, minor UI
app/i18n/locales/en.json, app/lib/constants/colors.ts, app/lib/constants/defaultSettings.ts, app/views/Status/Status.tsx, app/lib/hooks/useMediaCallPermission/useMediaCallPermission.test.ts
Adds i18n keys, adjusts dark theme font color, adds a default SIP integration setting, centers Status icon text, and small test formatting change.
Other minor edits
app/views/NewMessageView/Item.test.tsx, app/views/RoomView/RightButtons.tsx, ...
Various test updates and small prop/signature changes to integrate the new hook-based flow (e.g., removed room prop from HeaderCallButton).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI
    participant Hook as useNewMediaCall
    participant Store as PeerAutocompleteStore
    participant API as getPeerAutocompleteOptions
    participant ActionSheet
    participant Media as MediaSessionInstance

    User->>UI: press call control
    UI->>Hook: openNewMediaCall(rid)
    Hook->>Hook: check hasMediaCallPermission
    alt permission granted
        Hook->>Store: setSelectedPeer(if direct DM)
        Hook->>ActionSheet: showActionSheet(<NewMediaCall/>)
        ActionSheet->>UI: render NewMediaCall
        User->>UI: enter search term
        UI->>API: getPeerAutocompleteOptions(filter, peerInfo)
        API-->>Store: return [sip option, user options]
        Store-->>UI: options updated
        User->>UI: select peer
        UI->>Store: setSelectedPeer(selected)
        User->>UI: press Call
        UI->>Media: startCall(peerId, type)
        Media-->>ActionSheet: hideActionSheet()
    else permission denied
        Hook-->>UI: no ActionSheet opened
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: start call #7024 — Modifies VOIP call-starting flow and MediaSessionInstance-related APIs; strongly related to replacing direct mediaSession calls with hook/ActionSheet flow.
🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Most changes focus on the NewMediaCall feature; however, minor unrelated updates include a dark theme color change and a formatting fix in a test file that are not directly tied to VMUX-15. Remove or justify the out-of-scope changes: (1) dark theme fontDefault color change in app/lib/constants/colors.ts, and (2) formatting-only change in app/lib/hooks/useMediaCallPermission/useMediaCallPermission.test.ts.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: Pre flight' is vague and does not clearly convey the main change. It uses a generic term ('Pre flight') that lacks descriptive details about the actual implementation. Consider using a more descriptive title that explains the feature, e.g., 'feat: Add NewMediaCall component for peer selection and SIP routing' or 'feat: Implement pre-call action sheet for internal vs external call routing'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR implements peer autocomplete UI components, call selection logic, and integration with existing call flows, addressing the pre-flight requirement to differentiate call routing before initiation.

✏️ 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.

@diegolmello diegolmello changed the title feat: pre flight feat: Pre flight Mar 6, 2026
@diegolmello diegolmello marked this pull request as ready for review March 6, 2026 19:19
@diegolmello diegolmello requested a review from Copilot March 6, 2026 19:25
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (8)
app/containers/NewMediaCall/PeerList.tsx (1)

37-46: Extract inline style to StyleSheet for performance.

The inline style object { flexShrink: 1 } creates a new reference on every render, causing unnecessary re-renders of FlatList.

♻️ Suggested fix
+import { FlatList, StyleSheet } from 'react-native';
-import { FlatList } from 'react-native';
+
+const styles = StyleSheet.create({
+	contentContainer: {
+		flexShrink: 1
+	}
+});

 return (
 	<FlatList
 		data={options}
-		contentContainerStyle={{ flexShrink: 1 }}
+		contentContainerStyle={styles.contentContainer}
 		keyExtractor={item => item.value}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/NewMediaCall/PeerList.tsx` around lines 37 - 46, The inline
contentContainerStyle on the FlatList (contentContainerStyle={{ flexShrink: 1
}}) creates a new object each render; move this into a static StyleSheet entry
(e.g., const styles = StyleSheet.create({ contentContainer: { flexShrink: 1 }
})) and replace the inline use with
contentContainerStyle={styles.contentContainer} in the PeerList component where
FlatList is rendered (keep keyExtractor, renderItem, ItemSeparatorComponent
unchanged).
app/containers/NewMediaCall/CreateCall.stories.tsx (1)

38-46: Avoid side effects during render in story components.

Similar to PeerList.stories.tsx, calling setStoreState directly in the story's render body causes state mutation on every render. Consider using a decorator pattern or wrapper component with useEffect to initialize store state.

♻️ Suggested fix using a wrapper
+import { useEffect } from 'react';
+
+const StoreInitWrapper = ({ 
+	selectedPeer, 
+	children 
+}: { 
+	selectedPeer: TPeerItem | null; 
+	children: React.ReactNode 
+}) => {
+	useEffect(() => {
+		usePeerAutocompleteStore.setState({ selectedPeer });
+	}, [selectedPeer]);
+	return <>{children}</>;
+};
+
 export const Disabled = () => {
-	setStoreState(null);
-	return <CreateCall />;
+	return (
+		<StoreInitWrapper selectedPeer={null}>
+			<CreateCall />
+		</StoreInitWrapper>
+	);
 };

 export const Enabled = () => {
-	setStoreState(userPeer);
-	return <CreateCall />;
+	return (
+		<StoreInitWrapper selectedPeer={userPeer}>
+			<CreateCall />
+		</StoreInitWrapper>
+	);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/NewMediaCall/CreateCall.stories.tsx` around lines 38 - 46, The
story functions Disabled and Enabled mutate global state during render by
calling setStoreState directly; change them to initialize state inside a wrapper
component or a story decorator that uses useEffect (or a setup function) to call
setStoreState once on mount before rendering <CreateCall />. Locate the Disabled
and Enabled exports and replace the direct setStoreState calls with a wrapper
component or decorator that invokes setStoreState(userPeer) or
setStoreState(null) inside useEffect so the store is only mutated on mount, not
on every render.
app/containers/NewMediaCall/PeerList.stories.tsx (1)

53-59: Avoid side effects during render in story components.

Calling setStoreState directly in the story's render body causes state mutation on every render. This can lead to unexpected behavior and violates React's render purity expectations, especially with React 19's stricter concurrent rendering.

♻️ Suggested fix using a decorator or wrapper
+import { useEffect } from 'react';
+
+const StoreInitWrapper = ({ options, children }: { options: TPeerItem[]; children: React.ReactNode }) => {
+	useEffect(() => {
+		usePeerAutocompleteStore.setState({
+			options,
+			selectedPeer: null
+		});
+	}, [options]);
+	return <>{children}</>;
+};
+
 export const All = () => {
-	setStoreState(mixedOptions);
 	return (
-		<View style={styles.container}>
-			<PeerList />
-		</View>
+		<StoreInitWrapper options={mixedOptions}>
+			<View style={styles.container}>
+				<PeerList />
+			</View>
+		</StoreInitWrapper>
 	);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/NewMediaCall/PeerList.stories.tsx` around lines 53 - 59, The
story function All currently mutates state during render by calling
setStoreState(mixedOptions); move that side effect out of the render path —
either add a story-level decorator or wrap All in a component that applies
setStoreState inside a useEffect (or a beforeRender/setup function provided by
your story framework) so the mutation runs once on mount rather than on every
render; update the story to render <PeerList /> (with styles.container) only and
apply mixedOptions via the decorator/wrapper instead of calling setStoreState
directly inside All.
app/containers/NewMediaCall/NewMediaCall.stories.tsx (1)

99-133: Same side effect issue: store setup runs during render.

Similar to Container.stories.tsx, setStoreByVariant is called inside the story function body, executing on every re-render. Move the setup to decorators for consistency.

♻️ Suggested approach
+const withStoreVariant = (variant: 'empty' | 'searching' | 'userSelected' | 'sipSelected') => 
+	(Story: React.ComponentType) => {
+		setStoreByVariant(variant);
+		return <Story />;
+	};
+
 export const Empty = () => {
-	setStoreByVariant('empty');
 	return (
 		<View style={styles.container}>
 			<NewMediaCall />
 		</View>
 	);
 };
+Empty.decorators = [withStoreVariant('empty')];

Apply the same pattern to Searching, UserSelected, and SipSelected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/NewMediaCall/NewMediaCall.stories.tsx` around lines 99 - 133,
The stories call setStoreByVariant inside each story function (Empty, Searching,
UserSelected, SipSelected), causing side effects on every render; move these
setup calls into the Storybook decorators (following the pattern used in
Container.stories.tsx) so the store is configured once per story render context.
Specifically, remove setStoreByVariant from the bodies of Empty, Searching,
UserSelected and SipSelected and instead add a decorator that invokes
setStoreByVariant('empty'|'searching'|'userSelected'|'sipSelected') based on the
story name or metadata so each story initializes the store only once before
rendering NewMediaCall.
app/views/RoomInfoView/components/BaseButton.tsx (2)

27-36: Clarify showIcon behavior or rename the prop.

When showIcon is false, the entire component returns null, not just hiding the icon. This makes the prop name misleading—it controls whether the button renders at all, not just icon visibility. Consider renaming to visible or show, or restructuring if the intent is to render a text-only button variant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/RoomInfoView/components/BaseButton.tsx` around lines 27 - 36, The
prop showIcon on the BaseButton component is misleading because when false the
whole component returns null; update the component so its behavior matches
intent: either (A) rename the prop to visible or show across the component and
all call sites (and keep current render logic) to indicate it controls full
visibility, or (B) keep the name showIcon but change the render logic in
BaseButton so the BorderlessButton always renders and only the CustomIcon is
conditionally rendered based on showIcon (leave props like iconName, onPress,
enabled, styles.roomButton, and styles.roomButtonText intact); pick one approach
and update usages of BaseButton accordingly.

19-19: Simplify onPress type signature.

The onPress prop is typed as (prop: any) => void, but BorderlessButton's onPress callback doesn't pass arguments. Use () => void for accuracy and to avoid confusion.

♻️ Suggested fix
-	onPress?: (prop: any) => void;
+	onPress?: () => void;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/RoomInfoView/components/BaseButton.tsx` at line 19, The onPress
prop in BaseButton is incorrectly typed as (prop: any) => void even though
BorderlessButton (the underlying component) calls its onPress without arguments;
update the onPress type on the BaseButton props interface to a no-arg callback
() => void (and ensure any callers match this signature) so the prop signature
accurately reflects BorderlessButton's behavior.
app/containers/NewMediaCall/Container.stories.tsx (1)

59-75: Side effect during render: move store setup out of render body.

Calling setStoreState directly inside the story function body executes on every re-render, which can cause flicker or unexpected behavior in Storybook. Consider using a decorator or useEffect to set up store state before rendering.

♻️ Suggested approach using decorators
+const withStoreState = (filter: string) => (Story: React.ComponentType) => {
+	setStoreState(filter);
+	return <Story />;
+};
+
 export const Default = () => {
-	setStoreState('');
 	return (
 		<Container>
 			<PlaceholderChildren />
 		</Container>
 	);
 };
+Default.decorators = [withStoreState('')];
 
 export const WithFilter = () => {
-	setStoreState('alice');
 	return (
 		<Container>
 			<PlaceholderChildren />
 		</Container>
 	);
 };
+WithFilter.decorators = [withStoreState('alice')];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/NewMediaCall/Container.stories.tsx` around lines 59 - 75, The
stories call setStoreState directly during render (in Default and WithFilter),
causing side effects on every re-render; move the store setup out of the render
by either wrapping the story in a decorator that calls setStoreState before
rendering or by replacing the story function with a small React wrapper that
calls setStoreState inside useEffect (e.g., create a SetupState component used
by Default and WithFilter which calls setStoreState('') or
setStoreState('alice') in useEffect and then returns
<Container><PlaceholderChildren/></Container>), ensuring Container and
PlaceholderChildren remain untouched.
app/containers/NewMediaCall/SelectedPeer.stories.tsx (1)

62-70: Consider using Storybook decorators for store setup.

Calling setStoreState directly during render can cause side effects on re-renders and may behave unexpectedly with React Strict Mode. Using a decorator ensures state is set up before render and cleaned up properly between stories.

♻️ Suggested approach using decorators
// Add a decorator to handle store setup
const withStoreState = (peer: TPeerItem | null) => (Story: React.FC) => {
  React.useEffect(() => {
    usePeerAutocompleteStore.setState({ selectedPeer: peer });
    return () => usePeerAutocompleteStore.setState({ selectedPeer: null });
  }, []);
  return <Story />;
};

export const User = () => (
  <View style={styles.container}>
    <Text style={styles.sectionLabel}>User variant</Text>
    <SelectedPeerInner selectedPeer={userPeer} />
  </View>
);
User.decorators = [withStoreState(userPeer)];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/NewMediaCall/SelectedPeer.stories.tsx` around lines 62 - 70,
The story calls setStoreState(userPeer) directly inside the User render which
causes render-side effects; replace this by creating a Storybook decorator that
sets the store before render and cleans it up after: add a withStoreState
decorator function that uses useEffect to call the store setter (e.g.,
usePeerAutocompleteStore.setState or the existing setStoreState helper) to set
selectedPeer to userPeer on mount and reset to null on unmount, then remove the
inline setStoreState call from the User component and attach User.decorators =
[withStoreState(userPeer)]; ensure SelectedPeerInner remains unchanged and the
decorator references the same userPeer symbol.
🤖 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/NewMediaCall/CreateCall.test.tsx`:
- Around line 116-128: The test in CreateCall.test.tsx asserts the wrong call
type for a SIP peer; update the expectation so mockStartCall is expected to be
called with '+5511999999999' and 'sip' instead of 'user' (i.e., change the
expect(mockStartCall).toHaveBeenCalledWith call to use 'sip'); verify the
assertion references the same mockStartCall used by the CreateCall component and
adjust any related assertions (e.g., other tests that assume 'user') if needed.

In `@app/containers/NewMediaCall/CreateCall.tsx`:
- Around line 22-26: The type-discrimination is wrong: replace the "'number' in
selectedPeer" check with an explicit discriminator on the peer type (e.g. check
selectedPeer.type === 'sip') so that CreateCall's call to
mediaSessionInstance.startCall uses 'sip' for SIP peers and 'user' otherwise;
update the branch in the component where
mediaSessionInstance.startCall(selectedPeer.value, ...) is called (the code
surrounding selectedPeer) to use the type field as the discriminator and add a
safe fallback if selectedPeer.type is undefined.

In `@app/lib/hooks/useSubscription.ts`:
- Around line 10-19: The effect can leave the previous subscription until an
async load completes and a slower request may overwrite a newer rid; to fix,
clear the subscription when rid changes and guard against stale responses by
giving each load a token (or AbortController) and only calling
setSubscription(result) if the token matches the current request for the active
rid; update the useEffect that defines load (and calls getSubscriptionByRoomId)
to generate a unique requestId/AbortController at start, call
setSubscription(null) when starting a new load (or when rid is falsy), and skip
setting state when the requestId/abort shows the response is stale—use the
existing symbols useEffect, load, getSubscriptionByRoomId, setSubscription, and
rid to locate and update the code.

In `@app/lib/services/voip/getPeerAutocompleteOptions.ts`:
- Around line 87-93: The helper currently always prepends a SIP option
(sipOption of type TPeerItem) for non-empty terms; modify
getPeerAutocompleteOptions to accept an allowExternalVoiceCalls (boolean)
parameter and only include/return the sipOption when allowExternalVoiceCalls is
true (otherwise return just userOptions), or alternatively ensure callers
(PeerList/NewMediaCall) filter out items with type === 'sip' when
allow-external-voice-calls is false; update function signature and call sites
accordingly so permission-driven filtering is enforced inside
getPeerAutocompleteOptions or immediately after it.

In `@app/lib/services/voip/usePeerAutocompleteStore.test.ts`:
- Around line 138-151: The test and implementation use console.log for error
handling; change the implementation in usePeerAutocompleteStore (the
fetchOptions error handler) to call console.error('Failed to fetch options')
instead of console.log, and update the test usePeerAutocompleteStore.test to spy
on console.error (jest.spyOn(console, 'error').mockImplementation()) and assert
expect(consoleSpy).toHaveBeenCalledWith('Failed to fetch options'), then restore
the spy; keep the mocked rejection
(mockGetPeerAutocompleteOptions.mockRejectedValue) and the assertion that
usePeerAutocompleteStore.getState().options equals [] unchanged.

In `@app/views/NewMessageView/Item.tsx`:
- Around line 28-34: The handleCallPress function is setting the selected peer
without the optional username, so update the call to
usePeerAutocompleteStore.getState().setSelectedPeer inside handleCallPress to
include the username prop (from the Item component's TPeerItem input) alongside
type, value, and label; mirror the pattern used in PeerList where the full peer
object (including username) is passed so PeerItemInner/Avatar can receive and
display username correctly when opening the NewMediaCall action sheet via
showActionSheetRef.

---

Nitpick comments:
In `@app/containers/NewMediaCall/Container.stories.tsx`:
- Around line 59-75: The stories call setStoreState directly during render (in
Default and WithFilter), causing side effects on every re-render; move the store
setup out of the render by either wrapping the story in a decorator that calls
setStoreState before rendering or by replacing the story function with a small
React wrapper that calls setStoreState inside useEffect (e.g., create a
SetupState component used by Default and WithFilter which calls
setStoreState('') or setStoreState('alice') in useEffect and then returns
<Container><PlaceholderChildren/></Container>), ensuring Container and
PlaceholderChildren remain untouched.

In `@app/containers/NewMediaCall/CreateCall.stories.tsx`:
- Around line 38-46: The story functions Disabled and Enabled mutate global
state during render by calling setStoreState directly; change them to initialize
state inside a wrapper component or a story decorator that uses useEffect (or a
setup function) to call setStoreState once on mount before rendering <CreateCall
/>. Locate the Disabled and Enabled exports and replace the direct setStoreState
calls with a wrapper component or decorator that invokes setStoreState(userPeer)
or setStoreState(null) inside useEffect so the store is only mutated on mount,
not on every render.

In `@app/containers/NewMediaCall/NewMediaCall.stories.tsx`:
- Around line 99-133: The stories call setStoreByVariant inside each story
function (Empty, Searching, UserSelected, SipSelected), causing side effects on
every render; move these setup calls into the Storybook decorators (following
the pattern used in Container.stories.tsx) so the store is configured once per
story render context. Specifically, remove setStoreByVariant from the bodies of
Empty, Searching, UserSelected and SipSelected and instead add a decorator that
invokes setStoreByVariant('empty'|'searching'|'userSelected'|'sipSelected')
based on the story name or metadata so each story initializes the store only
once before rendering NewMediaCall.

In `@app/containers/NewMediaCall/PeerList.stories.tsx`:
- Around line 53-59: The story function All currently mutates state during
render by calling setStoreState(mixedOptions); move that side effect out of the
render path — either add a story-level decorator or wrap All in a component that
applies setStoreState inside a useEffect (or a beforeRender/setup function
provided by your story framework) so the mutation runs once on mount rather than
on every render; update the story to render <PeerList /> (with styles.container)
only and apply mixedOptions via the decorator/wrapper instead of calling
setStoreState directly inside All.

In `@app/containers/NewMediaCall/PeerList.tsx`:
- Around line 37-46: The inline contentContainerStyle on the FlatList
(contentContainerStyle={{ flexShrink: 1 }}) creates a new object each render;
move this into a static StyleSheet entry (e.g., const styles =
StyleSheet.create({ contentContainer: { flexShrink: 1 } })) and replace the
inline use with contentContainerStyle={styles.contentContainer} in the PeerList
component where FlatList is rendered (keep keyExtractor, renderItem,
ItemSeparatorComponent unchanged).

In `@app/containers/NewMediaCall/SelectedPeer.stories.tsx`:
- Around line 62-70: The story calls setStoreState(userPeer) directly inside the
User render which causes render-side effects; replace this by creating a
Storybook decorator that sets the store before render and cleans it up after:
add a withStoreState decorator function that uses useEffect to call the store
setter (e.g., usePeerAutocompleteStore.setState or the existing setStoreState
helper) to set selectedPeer to userPeer on mount and reset to null on unmount,
then remove the inline setStoreState call from the User component and attach
User.decorators = [withStoreState(userPeer)]; ensure SelectedPeerInner remains
unchanged and the decorator references the same userPeer symbol.

In `@app/views/RoomInfoView/components/BaseButton.tsx`:
- Around line 27-36: The prop showIcon on the BaseButton component is misleading
because when false the whole component returns null; update the component so its
behavior matches intent: either (A) rename the prop to visible or show across
the component and all call sites (and keep current render logic) to indicate it
controls full visibility, or (B) keep the name showIcon but change the render
logic in BaseButton so the BorderlessButton always renders and only the
CustomIcon is conditionally rendered based on showIcon (leave props like
iconName, onPress, enabled, styles.roomButton, and styles.roomButtonText
intact); pick one approach and update usages of BaseButton accordingly.
- Line 19: The onPress prop in BaseButton is incorrectly typed as (prop: any) =>
void even though BorderlessButton (the underlying component) calls its onPress
without arguments; update the onPress type on the BaseButton props interface to
a no-arg callback () => void (and ensure any callers match this signature) so
the prop signature accurately reflects BorderlessButton's behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f27baa9e-8b14-417a-b622-97a64f11ee3f

📥 Commits

Reviewing files that changed from the base of the PR and between b1e1a80 and 6b545c5.

⛔ Files ignored due to path filters (16)
  • app/containers/Avatar/__snapshots__/Avatar.test.tsx.snap is excluded by !**/*.snap
  • app/containers/Header/components/HeaderButton/__snapshots__/HeaderButtons.test.tsx.snap is excluded by !**/*.snap
  • app/containers/List/__snapshots__/List.test.tsx.snap is excluded by !**/*.snap
  • app/containers/MediaCallHeader/__snapshots__/MediaCallHeader.test.tsx.snap is excluded by !**/*.snap
  • app/containers/NewMediaCall/__snapshots__/CreateCall.test.tsx.snap is excluded by !**/*.snap
  • app/containers/NewMediaCall/__snapshots__/PeerItem.test.tsx.snap is excluded by !**/*.snap
  • app/containers/NewMediaCall/__snapshots__/PeerList.test.tsx.snap is excluded by !**/*.snap
  • app/containers/NewMediaCall/__snapshots__/SelectedPeer.test.tsx.snap is excluded by !**/*.snap
  • app/containers/RoomHeader/__snapshots__/RoomHeader.test.tsx.snap is excluded by !**/*.snap
  • app/containers/RoomItem/__snapshots__/RoomItem.test.tsx.snap is excluded by !**/*.snap
  • app/containers/RoomTypeIcon/__snapshots__/RoomTypeIcon.test.tsx.snap is excluded by !**/*.snap
  • app/containers/Status/__snapshots__/Status.test.tsx.snap is excluded by !**/*.snap
  • app/views/DiscussionsView/__snapshots__/Item.test.tsx.snap is excluded by !**/*.snap
  • app/views/RoomInfoView/components/__snapshots__/RoomInfoABAC.test.tsx.snap is excluded by !**/*.snap
  • app/views/RoomView/LoadMore/__snapshots__/LoadMore.test.tsx.snap is excluded by !**/*.snap
  • app/views/ThreadMessagesView/__snapshots__/Item.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (39)
  • app/containers/NewMediaCall/Container.stories.tsx
  • app/containers/NewMediaCall/Container.tsx
  • app/containers/NewMediaCall/CreateCall.stories.tsx
  • app/containers/NewMediaCall/CreateCall.test.tsx
  • app/containers/NewMediaCall/CreateCall.tsx
  • app/containers/NewMediaCall/NewMediaCall.stories.tsx
  • app/containers/NewMediaCall/NewMediaCall.tsx
  • app/containers/NewMediaCall/PeerItem.stories.tsx
  • app/containers/NewMediaCall/PeerItem.test.tsx
  • app/containers/NewMediaCall/PeerItem.tsx
  • app/containers/NewMediaCall/PeerItemInner.tsx
  • app/containers/NewMediaCall/PeerList.stories.tsx
  • app/containers/NewMediaCall/PeerList.test.tsx
  • app/containers/NewMediaCall/PeerList.tsx
  • app/containers/NewMediaCall/SelectedPeer.stories.tsx
  • app/containers/NewMediaCall/SelectedPeer.test.tsx
  • app/containers/NewMediaCall/SelectedPeer.tsx
  • app/containers/NewMediaCall/index.tsx
  • app/containers/Status/Status.tsx
  • app/i18n/locales/en.json
  • app/lib/constants/colors.ts
  • app/lib/constants/defaultSettings.ts
  • app/lib/hooks/useMediaCallPermission/useMediaCallPermission.test.ts
  • app/lib/hooks/useNewMediaCall/index.ts
  • app/lib/hooks/useNewMediaCall/useNewMediaCall.test.tsx
  • app/lib/hooks/useNewMediaCall/useNewMediaCall.tsx
  • app/lib/hooks/useSubscription.ts
  • app/lib/services/voip/getPeerAutocompleteOptions.ts
  • app/lib/services/voip/usePeerAutocompleteStore.test.ts
  • app/lib/services/voip/usePeerAutocompleteStore.ts
  • app/views/NewMessageView/Item.test.tsx
  • app/views/NewMessageView/Item.tsx
  • app/views/RoomActionsView/components/CallSection.tsx
  • app/views/RoomInfoView/components/BaseButton.tsx
  • app/views/RoomInfoView/components/RoomInfoButtons.test.tsx
  • app/views/RoomInfoView/components/RoomInfoButtons.tsx
  • app/views/RoomView/RightButtons.tsx
  • app/views/RoomView/components/HeaderCallButton.tsx
  • app/views/SidebarView/components/Stacks.tsx
💤 Files with no reviewable changes (1)
  • app/views/RoomView/RightButtons.tsx
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-17T15:56:22.578Z
Learnt from: OtavioStasiak
Repo: RocketChat/Rocket.Chat.ReactNative PR: 6499
File: app/containers/ServerItem/index.tsx:34-36
Timestamp: 2025-12-17T15:56:22.578Z
Learning: In the Rocket.Chat React Native codebase, for radio button components on iOS, include the selection state ("Selected"/"Unselected") in the accessibilityLabel instead of using accessibilityState={{ checked: hasCheck }}, because iOS VoiceOver has known issues with accessibilityRole="radio" + accessibilityState that prevent correct state announcement.

Applied to files:

  • app/views/RoomInfoView/components/RoomInfoButtons.tsx
🔇 Additional comments (23)
app/i18n/locales/en.json (1)

332-332: Localization additions look consistent.

These keys fit the existing locale naming scheme and the copy reads well for the new pre-flight/new-call flow.

Also applies to: 577-577

app/lib/constants/defaultSettings.ts (1)

315-317: LGTM!

The new VoIP_TeamCollab_SIP_Integration_For_Internal_Calls setting follows the established naming convention and type definition pattern. It's appropriately placed alongside other VoIP-related settings and supports the Pre-flight feature for differentiating internal vs external calls.

app/containers/Status/Status.tsx (1)

19-27: LGTM!

The addition of textAlign: 'center' properly complements the existing textAlignVertical: 'center' to ensure the status icon is centered both horizontally and vertically within its bounds. This is the correct approach for centering icon fonts in React Native.

app/lib/hooks/useNewMediaCall/index.ts (1)

1-1: LGTM!

Standard barrel export pattern for module organization.

app/views/RoomActionsView/components/CallSection.tsx (1)

6-6: LGTM!

Clean migration from direct mediaSessionInstance usage to the useNewMediaCall hook. The permission gating and existing functionality are preserved.

Also applies to: 16-16, 29-29

app/containers/NewMediaCall/NewMediaCall.tsx (1)

9-21: LGTM!

The component correctly resets the peer autocomplete store on unmount via the cleanup function. The composition of sub-components is clean and well-organized.

app/containers/NewMediaCall/PeerItem.tsx (1)

1-22: LGTM!

The component properly handles platform-specific press feedback (iOS pressed state vs Android ripple) and delegates content rendering to PeerItemInner. The JSX transform handles React imports automatically.

app/containers/NewMediaCall/PeerItem.stories.tsx (1)

1-57: LGTM!

The story file is well-structured with good coverage of different PeerItem variants (user, SIP, and long label for truncation testing). The static data approach without render-time side effects is appropriate for Storybook.

app/containers/NewMediaCall/PeerItem.test.tsx (1)

1-80: LGTM!

The test file provides good coverage for PeerItem:

  • Validates rendering with correct testIDs and labels for both user and SIP types
  • Verifies press interactions trigger the callback with the correct item
  • Includes snapshot generation for visual regression testing
app/containers/NewMediaCall/SelectedPeer.test.tsx (1)

1-87: LGTM!

Solid test coverage for SelectedPeer:

  • Correctly tests null rendering when no peer is selected
  • Validates user and SIP peer label display
  • Verifies the clear button invokes clearSelection
  • Proper store state initialization in beforeEach prevents test pollution
app/containers/NewMediaCall/PeerList.test.tsx (1)

1-130: LGTM!

Comprehensive test coverage for PeerList:

  • Tests conditional rendering based on selectedPeer state
  • Validates option selection triggers correct store actions (setSelectedPeer, setFilter, fetchOptions)
  • Covers both user and SIP item selection paths
  • Properly isolates tests with beforeEach store reset
app/lib/hooks/useNewMediaCall/useNewMediaCall.test.tsx (1)

1-106: LGTM!

Well-structured tests for the useNewMediaCall hook:

  • Properly mocks all dependencies (useSubscription, useMediaCallPermission, ActionSheet, store)
  • Tests the three key scenarios: DM peer exists, no DM peer, and missing room
  • The helper expectNewMediaCallActionSheet reduces duplication and improves readability
  • Validates that the action sheet opens regardless of permission state, allowing users to access the pre-flight UI
app/containers/NewMediaCall/PeerList.tsx (1)

30-33: Both function calls serve distinct purposes and are both necessary.

fetchOptions('') clears the options array (line 28-30 in usePeerAutocompleteStore.ts checks for empty term and sets options to empty array), while setFilter('') updates the filter state. These are complementary operations, not redundant ones. When a peer is selected, clearing both the options and the filter is the correct behavior.

			> Likely an incorrect or invalid review comment.
app/views/RoomInfoView/components/RoomInfoButtons.tsx (1)

40-154: Clean refactor to use the new hook and BaseButton.

The migration from useMediaCallPermission to useNewMediaCall is well-implemented. The button visibility logic using hasMediaCallPermission and the action wiring to openNewMediaCall are correctly integrated. The extraction of BaseButton improves reusability.

app/containers/NewMediaCall/Container.tsx (1)

12-50: Well-structured container with proper debouncing.

The debounced fetch pattern and store integration are correctly implemented. The component cleanly separates concerns between UI rendering and state management.

app/containers/NewMediaCall/PeerItemInner.tsx (1)

10-32: Clean implementation differentiating SIP and user peers.

The component appropriately handles the two peer types with distinct visual treatments: phone icon for SIP and avatar with status for users. The layout and styling are well-organized.

app/containers/NewMediaCall/SelectedPeer.tsx (1)

16-40: Good separation between container and presentation.

The split between SelectedPeer (store connection) and SelectedPeerInner (presentation) enables easier testing. Accessibility is properly handled with accessibilityLabel and accessibilityRole.

One minor consideration: SelectedPeerInner still accesses the store directly for clearSelection (line 18), which slightly couples it to the store. If you want full testability, consider passing clearSelection as a prop.

app/views/RoomInfoView/components/RoomInfoButtons.test.tsx (2)

14-26: Tests properly updated for the new hook.

The mock structure correctly reflects the new useNewMediaCall API with openNewMediaCall and hasMediaCallPermission. Using noopOpenNewMediaCall for non-call-verification tests and mockOpenNewMediaCall for assertion is a clean pattern.


114-126: Voice call test correctly verifies the new flow.

The test properly sets up the mock with mockOpenNewMediaCall and verifies it's called when pressing the voice call button.

app/views/SidebarView/components/Stacks.tsx (1)

5-38: LGTM!

The refactoring cleanly replaces the previous VOIP logic with the new useNewMediaCall hook. The permission-gated conditional rendering is well-structured, and the Fragment wrapper correctly groups the item with its separator.

app/containers/NewMediaCall/SelectedPeer.stories.tsx (1)

26-60: Good test fixture coverage.

The fixtures effectively cover the main peer types (user, sip) and edge cases (long username for truncation testing). The All story provides a convenient overview for visual testing.

app/views/NewMessageView/Item.test.tsx (2)

10-35: LGTM!

The mock setup is well-structured. Jest hoists jest.mock calls, so the NewMediaCall import on line 10 correctly receives the mocked version, making the type comparison on line 116 work as expected.


129-139: Good guard test for falsy userId.

This properly validates that the call flow is short-circuited when userId is empty, preventing invalid peer data from being set.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new “pre-flight” media-call flow (action sheet with peer search/selection) and wires existing call entry points to open this flow, alongside related theming and snapshot updates.

Changes:

  • Introduces NewMediaCall action sheet UI with a Zustand-backed peer autocomplete store and a REST-backed autocomplete option builder (user + SIP).
  • Refactors call triggers (sidebar, room header/actions/info, new message) to use useNewMediaCall instead of directly calling the media session.
  • Updates dark theme fontDefault, centers icon text alignment, adds i18n strings, and regenerates snapshots.

Reviewed changes

Copilot reviewed 55 out of 55 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
app/views/ThreadMessagesView/snapshots/Item.test.tsx.snap Snapshot update for dark theme font color.
app/views/SidebarView/components/Stacks.tsx Sidebar “Voice call” entry now opens new media call flow (permission-gated).
app/views/RoomView/components/HeaderCallButton.tsx Header call button now opens new media call pre-flight for media calls.
app/views/RoomView/RightButtons.tsx Removes room prop from HeaderCallButton usage.
app/views/RoomView/LoadMore/snapshots/LoadMore.test.tsx.snap Snapshot update for dark theme font color.
app/views/RoomInfoView/components/snapshots/RoomInfoABAC.test.tsx.snap Snapshot update for dark theme font color.
app/views/RoomInfoView/components/RoomInfoButtons.tsx Refactors voice call button to open new media call flow; extracts BaseButton.
app/views/RoomInfoView/components/RoomInfoButtons.test.tsx Updates tests to mock and assert useNewMediaCall behavior.
app/views/RoomInfoView/components/BaseButton.tsx New shared base button component extracted from RoomInfo buttons.
app/views/RoomActionsView/components/CallSection.tsx Room actions voice call now opens new media call flow via useNewMediaCall.
app/views/NewMessageView/Item.tsx New message list call button now sets selected peer and opens NewMediaCall sheet.
app/views/NewMessageView/Item.test.tsx Updates tests to assert new action sheet flow + selected peer state.
app/views/DiscussionsView/snapshots/Item.test.tsx.snap Snapshot update for dark theme font color.
app/lib/services/voip/usePeerAutocompleteStore.ts New Zustand store for filter/selection/options and async fetching.
app/lib/services/voip/usePeerAutocompleteStore.test.ts Unit tests for the new peer autocomplete store.
app/lib/services/voip/getPeerAutocompleteOptions.ts New autocomplete option builder (SIP option + user options), supports SIP routing setting.
app/lib/hooks/useSubscription.ts New hook to load a subscription model by room id.
app/lib/hooks/useNewMediaCall/useNewMediaCall.tsx New hook that opens the NewMediaCall sheet and preselects DM peer when possible.
app/lib/hooks/useNewMediaCall/useNewMediaCall.test.tsx Unit tests for useNewMediaCall.
app/lib/hooks/useNewMediaCall/index.ts Barrel export for useNewMediaCall.
app/lib/hooks/useMediaCallPermission/useMediaCallPermission.test.ts Minor formatting change in expectations.
app/lib/constants/defaultSettings.ts Adds VoIP_TeamCollab_SIP_Integration_For_Internal_Calls boolean setting.
app/lib/constants/colors.ts Updates dark theme fontDefault color.
app/i18n/locales/en.json Adds “New call” + “Enter username or number” strings.
app/containers/Status/snapshots/Status.test.tsx.snap Snapshot update for centered icon text alignment.
app/containers/Status/Status.tsx Adds textAlign: 'center' to status icon text style.
app/containers/RoomTypeIcon/snapshots/RoomTypeIcon.test.tsx.snap Snapshot update for centered icon text alignment.
app/containers/RoomItem/snapshots/RoomItem.test.tsx.snap Snapshot update for centered icon text alignment.
app/containers/RoomHeader/snapshots/RoomHeader.test.tsx.snap Snapshot update for dark theme font color.
app/containers/NewMediaCall/index.tsx Barrel export for NewMediaCall.
app/containers/NewMediaCall/snapshots/SelectedPeer.test.tsx.snap New snapshots for SelectedPeer stories.
app/containers/NewMediaCall/snapshots/PeerList.test.tsx.snap New snapshots for PeerList stories.
app/containers/NewMediaCall/snapshots/PeerItem.test.tsx.snap New snapshots for PeerItem stories.
app/containers/NewMediaCall/snapshots/CreateCall.test.tsx.snap New snapshots for CreateCall stories.
app/containers/NewMediaCall/SelectedPeer.tsx New component rendering the currently selected peer chip with clear action.
app/containers/NewMediaCall/SelectedPeer.test.tsx Tests for SelectedPeer behavior.
app/containers/NewMediaCall/SelectedPeer.stories.tsx Stories for SelectedPeer variants.
app/containers/NewMediaCall/PeerList.tsx New list of peer options that sets selection in the store.
app/containers/NewMediaCall/PeerList.test.tsx Tests for PeerList rendering and selection behavior.
app/containers/NewMediaCall/PeerList.stories.tsx Stories for PeerList.
app/containers/NewMediaCall/PeerItemInner.tsx Shared inner rendering for user vs SIP items (avatar/status vs icon).
app/containers/NewMediaCall/PeerItem.tsx Pressable option row component for peers.
app/containers/NewMediaCall/PeerItem.test.tsx Tests for PeerItem selection behavior.
app/containers/NewMediaCall/PeerItem.stories.tsx Stories for PeerItem variants.
app/containers/NewMediaCall/NewMediaCall.tsx New action sheet body wiring selection + list + create call, with store reset on unmount.
app/containers/NewMediaCall/NewMediaCall.stories.tsx Stories for NewMediaCall flow states.
app/containers/NewMediaCall/CreateCall.tsx Call CTA button that starts the call based on selected peer and closes sheet.
app/containers/NewMediaCall/CreateCall.test.tsx Tests for CreateCall enabled/disabled and call invocation behavior.
app/containers/NewMediaCall/CreateCall.stories.tsx Stories for CreateCall enabled/disabled.
app/containers/NewMediaCall/Container.tsx New container with search input, debounced fetching, and title/label.
app/containers/NewMediaCall/Container.stories.tsx Stories for Container with/without filter.
app/containers/MediaCallHeader/snapshots/MediaCallHeader.test.tsx.snap Snapshot update for centered icon text alignment and updated timer text.
app/containers/List/snapshots/List.test.tsx.snap Snapshot update for dark theme font color.
app/containers/Header/components/HeaderButton/snapshots/HeaderButtons.test.tsx.snap Snapshot update for dark theme font color.
app/containers/Avatar/snapshots/Avatar.test.tsx.snap Snapshot update for centered icon text alignment.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 59 out of 59 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (5)

app/views/NewMessageView/Item.test.tsx:1

  • This assertion will fail because Item.tsx now calls setSelectedPeer with an additional username field. Update the expectation to include username, or switch to expect.objectContaining({ type, value, label, username }) to avoid brittle exact-shape matching.
    app/lib/hooks/useSubscription.ts:1
  • The async load() can resolve after rid changes or after unmount, causing stale updates (and potential 'setState on unmounted component' warnings). Add a cancellation guard (e.g., let cancelled = false in the effect cleanup) and only call setSubscription when the effect instance is still current.
    app/lib/hooks/useNewMediaCall/useNewMediaCall.tsx:1
  • Because useSubscription(rid) populates asynchronously, room can be undefined at press time even for DMs, so the DM peer won’t be preselected (regression vs the prior direct-room call behavior). Consider making openNewMediaCall resolve the subscription on-demand (await getSubscriptionByRoomId(rid) inside the handler) or accept the room object from callers that already have it.
    app/lib/services/voip/usePeerAutocompleteStore.ts:1
  • Concurrent fetchOptions calls can race (e.g., a slower request for an older term resolves after a newer one) and overwrite options with stale results. Track a request id / latest term in state and only apply results if they match the most recent request, or cancel in-flight requests where possible.
    app/views/RoomInfoView/components/BaseButton.tsx:1
  • The onPress type uses any, which weakens type safety for a shared UI primitive. Prefer a narrower type such as () => void (if you ignore the event) or (event: GestureResponderEvent) => void to match React Native press handlers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +29 to +31
setSelectedPeer(peerItem);
setFilter('');
fetchOptions('');
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

fetchOptions('') immediately clears options, but the store already clears options on empty terms; depending on how fetchOptions is implemented/used elsewhere, this can be redundant work. Consider replacing with a direct set({ options: [] }) action or extending reset/clear behavior to avoid the extra async call path.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

84-90: ⚠️ Potential issue | 🔴 Critical

Gate the SIP option behind external-call permission.

Lines 84-90 still prepend a type: 'sip' entry for every non-empty term. In the current flow, that value is stored as-is, rendered by app/containers/NewMediaCall/PeerList.tsx, and then passed straight to mediaSessionInstance.startCall(...) by app/containers/NewMediaCall/CreateCall.tsx. That means internal-only users can still be presented an external-call path unless the caller filters it first.

🔒 Suggested direction
 export const getPeerAutocompleteOptions = async ({
 	filter,
-	peerInfo
+	peerInfo,
+	allowExternalVoiceCalls
 }: {
 	filter: string;
 	peerInfo?: TPeerItem | null;
+	allowExternalVoiceCalls: boolean;
 }): Promise<TPeerItem[]> => {
@@
-	const sipOption: TPeerItem = {
-		type: 'sip',
-		value: term,
-		label: term
-	};
-
-	return [sipOption, ...userOptions];
+	const sipOptions: TPeerItem[] = allowExternalVoiceCalls
+		? [
+				{
+					type: 'sip',
+					value: term,
+					label: term
+				}
+		  ]
+		: [];
+
+	return [...sipOptions, ...userOptions];
 };

If you prefer not to thread the permission into this helper, the equivalent filter needs to happen immediately after this function returns, before PeerList renders the options.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/lib/services/voip/getPeerAutocompleteOptions.ts` around lines 84 - 90,
The function getPeerAutocompleteOptions currently always prepends a sipOption
(type: 'sip') which enables an external-call path for all users; modify
getPeerAutocompleteOptions to only include the sipOption when the caller has
external-call permission by either: 1) adding a boolean parameter (e.g.,
allowExternalCall or canExternalCall) and only prepending sipOption when true,
or 2) querying the existing permission helper (e.g., hasPermission /
canMakeExternalCalls) inside getPeerAutocompleteOptions before creating
sipOption; update any call sites to pass the permission flag if you choose the
parameter approach so only authorized users see the SIP option.
🧹 Nitpick comments (1)
app/containers/NewMediaCall/PeerList.tsx (1)

17-31: Avoid rebuilding TPeerItem before storing it.

Line 17 already receives a TPeerItem. Reconstructing it here duplicates the union logic and makes future shape changes easier to miss. Passing the original option keeps the type source in one place.

♻️ Proposed simplification
 const handleSelectOption = (option: TPeerItem) => {
-	const peerItem: TPeerItem =
-		option.type === 'sip'
-			? { type: 'sip', value: option.value, label: option.label }
-			: {
-					type: 'user',
-					value: option.value,
-					label: option.label,
-					username: option.username,
-					callerId: option.callerId
-			  };
-
-	setSelectedPeer(peerItem);
+	setSelectedPeer(option);
 	setFilter('');
 	fetchOptions('');
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/NewMediaCall/PeerList.tsx` around lines 17 - 31, The handler
handleSelectOption rebuilds a TPeerItem unnecessarily; instead of reconstructing
the union object, pass the received option directly to setSelectedPeer (i.e.,
call setSelectedPeer(option)), then clear the filter and call fetchOptions as
before (keep setFilter('') and fetchOptions('')); update handleSelectOption to
remove the manual TPeerItem construction so future changes to TPeerItem shape
are not duplicated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/lib/services/voip/getPeerAutocompleteOptions.ts`:
- Around line 84-90: The function getPeerAutocompleteOptions currently always
prepends a sipOption (type: 'sip') which enables an external-call path for all
users; modify getPeerAutocompleteOptions to only include the sipOption when the
caller has external-call permission by either: 1) adding a boolean parameter
(e.g., allowExternalCall or canExternalCall) and only prepending sipOption when
true, or 2) querying the existing permission helper (e.g., hasPermission /
canMakeExternalCalls) inside getPeerAutocompleteOptions before creating
sipOption; update any call sites to pass the permission flag if you choose the
parameter approach so only authorized users see the SIP option.

---

Nitpick comments:
In `@app/containers/NewMediaCall/PeerList.tsx`:
- Around line 17-31: The handler handleSelectOption rebuilds a TPeerItem
unnecessarily; instead of reconstructing the union object, pass the received
option directly to setSelectedPeer (i.e., call setSelectedPeer(option)), then
clear the filter and call fetchOptions as before (keep setFilter('') and
fetchOptions('')); update handleSelectOption to remove the manual TPeerItem
construction so future changes to TPeerItem shape are not duplicated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 04d13d37-efae-42f3-9525-52e566f5d030

📥 Commits

Reviewing files that changed from the base of the PR and between 6b545c5 and 6974083.

⛔ Files ignored due to path filters (1)
  • app/containers/NewMediaCall/__snapshots__/FilterHeader.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (14)
  • app/containers/NewMediaCall/Container.stories.tsx
  • app/containers/NewMediaCall/Container.tsx
  • app/containers/NewMediaCall/CreateCall.test.tsx
  • app/containers/NewMediaCall/CreateCall.tsx
  • app/containers/NewMediaCall/FilterHeader.stories.tsx
  • app/containers/NewMediaCall/FilterHeader.test.tsx
  • app/containers/NewMediaCall/FilterHeader.tsx
  • app/containers/NewMediaCall/NewMediaCall.tsx
  • app/containers/NewMediaCall/PeerList.tsx
  • app/lib/hooks/useSubscription.ts
  • app/lib/services/voip/getPeerAutocompleteOptions.ts
  • app/lib/services/voip/usePeerAutocompleteStore.test.ts
  • app/lib/services/voip/usePeerAutocompleteStore.ts
  • app/views/NewMessageView/Item.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/containers/NewMediaCall/NewMediaCall.tsx
  • app/lib/hooks/useSubscription.ts
  • app/lib/services/voip/usePeerAutocompleteStore.ts
  • app/containers/NewMediaCall/Container.stories.tsx
  • app/containers/NewMediaCall/Container.tsx
📜 Review details
🔇 Additional comments (4)
app/views/NewMessageView/Item.tsx (1)

28-33: Pre-flight flow wiring looks correct.

Setting the selected peer before opening NewMediaCall, and carrying username through with the peer payload, keeps the downstream action-sheet UI consistent with the selected user.

app/containers/NewMediaCall/FilterHeader.tsx (1)

20-28: Debounced search handoff looks correct.

Lines 24-27 update the local filter immediately, clear any stale selection, and defer the fetch, which matches the expected autocomplete UX.

app/containers/NewMediaCall/FilterHeader.test.tsx (1)

39-69: Good coverage for the debounce contract.

This test pins the important behavior: filter updates happen immediately, while fetchOptions waits for textInputDebounceTime.

app/containers/NewMediaCall/CreateCall.tsx (1)

17-24: Correctly routing the selected peer type now.

Lines 22-23 forward selectedPeer.type directly into startCall(...), which matches both the TPeerItem discriminator and the MediaSessionInstance contract.

@diegolmello diegolmello requested a deployment to approve_e2e_testing March 6, 2026 20:57 — with GitHub Actions Waiting
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 59 out of 59 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
app/containers/NewMediaCall/PeerList.tsx (2)

32-41: FlatList configuration looks good; minor style optimization available.

The keyboardShouldPersistTaps='always' is correct for allowing selection while the keyboard is visible. The keyExtractor using item.value is safe since TPeerItem guarantees value is always a string for both union members.

The inline contentContainerStyle object creates a new reference on each render. Extracting it is a minor optimization.

♻️ Optional: extract style constant
+const contentStyle = { flexShrink: 1 };
+
 export const PeerList = () => {
   ...
   return (
     <FlatList
       data={options}
-      contentContainerStyle={{ flexShrink: 1 }}
+      contentContainerStyle={contentStyle}
       keyExtractor={item => item.value}
       ...
     />
   );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/NewMediaCall/PeerList.tsx` around lines 32 - 41, The inline
contentContainerStyle object passed to FlatList causes a new reference on each
render; extract it to a stable constant (e.g., const
flatListContentContainerStyle = { flexShrink: 1 }) and use that constant in the
FlatList's contentContainerStyle prop to avoid unnecessary re-renders; update
the component where FlatList is rendered (look for FlatList and
contentContainerStyle usage in PeerList.tsx) to reference the new constant.

15-28: Implementation is correct; consider memoizing for stability.

The type normalization for 'sip' vs 'user' items is handled correctly, ensuring all required fields are passed. For optimal FlatList performance, handleSelectOption could be wrapped in useCallback to maintain a stable reference, though the impact is minimal since the component returns null after selection anyway.

♻️ Optional: wrap in useCallback
+import { useCallback } from 'react';
+import { FlatList } from 'react-native';
-import { FlatList } from 'react-native';

...

-	const handleSelectOption = (option: TPeerItem) => {
+	const handleSelectOption = useCallback((option: TPeerItem) => {
 		const peerItem: TPeerItem =
 			option.type === 'sip'
 				? { type: 'sip', value: option.value, label: option.label }
 				: {
 						type: 'user',
 						value: option.value,
 						label: option.label,
 						username: option.username,
 						callerId: option.callerId
 				  };

 		setSelectedPeer(peerItem);
-	};
+	}, [setSelectedPeer]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/containers/NewMediaCall/PeerList.tsx` around lines 15 - 28, The handler
handleSelectOption should be memoized to provide a stable reference for
consumers (e.g., FlatList); wrap handleSelectOption in React.useCallback, keep
its logic identical, and include setSelectedPeer in the dependency array (and
any other external values if used). Locate the function handleSelectOption and
replace its declaration with a useCallback-wrapped version that returns the same
TPeerItem normalization and calls setSelectedPeer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/containers/NewMediaCall/PeerList.tsx`:
- Around line 32-41: The inline contentContainerStyle object passed to FlatList
causes a new reference on each render; extract it to a stable constant (e.g.,
const flatListContentContainerStyle = { flexShrink: 1 }) and use that constant
in the FlatList's contentContainerStyle prop to avoid unnecessary re-renders;
update the component where FlatList is rendered (look for FlatList and
contentContainerStyle usage in PeerList.tsx) to reference the new constant.
- Around line 15-28: The handler handleSelectOption should be memoized to
provide a stable reference for consumers (e.g., FlatList); wrap
handleSelectOption in React.useCallback, keep its logic identical, and include
setSelectedPeer in the dependency array (and any other external values if used).
Locate the function handleSelectOption and replace its declaration with a
useCallback-wrapped version that returns the same TPeerItem normalization and
calls setSelectedPeer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8d5f00e7-838b-420f-bee3-da0643e381db

📥 Commits

Reviewing files that changed from the base of the PR and between b8cfd58 and 59fb65f.

⛔ Files ignored due to path filters (1)
  • app/containers/MediaCallHeader/__snapshots__/MediaCallHeader.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (3)
  • app/containers/NewMediaCall/PeerList.test.tsx
  • app/containers/NewMediaCall/PeerList.tsx
  • app/lib/services/voip/usePeerAutocompleteStore.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/containers/NewMediaCall/PeerList.test.tsx
  • app/lib/services/voip/usePeerAutocompleteStore.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ESLint and Test / run-eslint-and-test
🔇 Additional comments (2)
app/containers/NewMediaCall/PeerList.tsx (2)

8-12: Good use of granular Zustand selectors.

Using separate usePeerAutocompleteStore calls with individual selectors is the recommended pattern for Zustand—it ensures the component only re-renders when the specific subscribed state changes, rather than on any store update.


1-42: Overall implementation is solid.

The component correctly integrates with the Zustand store, handles both peer types properly, and follows standard React Native patterns for list rendering. The early return when a peer is selected keeps the UI clean. Good work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants