diff --git a/app/AppContainer.tsx b/app/AppContainer.tsx
index 8200dcb773f..53701f36def 100644
--- a/app/AppContainer.tsx
+++ b/app/AppContainer.tsx
@@ -19,7 +19,7 @@ import { ThemeContext } from './theme';
import { setCurrentScreen } from './lib/methods/helpers/log';
import { themes } from './lib/constants/colors';
import { emitter } from './lib/methods/helpers';
-import CallHeader from './containers/CallHeader/CallHeader';
+import MediaCallHeader from './containers/MediaCallHeader/MediaCallHeader';
const createStackNavigator = createNativeStackNavigator;
@@ -53,7 +53,7 @@ const App = memo(({ root, isMasterDetail }: { root: string; isMasterDetail: bool
return (
<>
-
+
> = {}) => {
+ const mockCall = {
+ state: 'active',
+ muted: false,
+ held: false,
+ contact: {
+ displayName: 'Bob Burnquist',
+ username: 'bob.burnquist',
+ sipExtension: '2244'
+ },
+ setMuted: () => {},
+ setHeld: () => {},
+ hangup: () => {},
+ reject: () => {},
+ emitter: {
+ on: () => {},
+ off: () => {}
+ }
+ } as any;
+
+ useCallStore.setState({
+ call: mockCall,
+ callUUID: 'test-uuid',
+ callState: 'active',
+ isMuted: false,
+ isOnHold: false,
+ isSpeakerOn: false,
+ callStartTime: mockCallStartTime,
+ contact: {
+ id: 'user-1',
+ displayName: 'Bob Burnquist',
+ username: 'bob.burnquist',
+ sipExtension: '2244'
+ },
+ focused: true,
+ remoteMute: false,
+ remoteHeld: false,
+ ...overrides
+ });
+};
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => {children};
+
+export default {
+ title: 'MediaCallHeader',
+ component: MediaCallHeader,
+ decorators: [
+ (Story: React.ComponentType) => (
+
+
+
+ )
+ ]
+};
+
+export const NoCall = () => {
+ useCallStore.setState({ call: null });
+ return ;
+};
+
+export const ActiveCall = () => {
+ setStoreState({ callState: 'active', callStartTime: mockCallStartTime });
+ return ;
+};
+
+export const ConnectingCall = () => {
+ setStoreState({ callState: 'accepted', callStartTime: null });
+ return ;
+};
+
+export const Focused = () => {
+ setStoreState({ focused: true });
+ return ;
+};
+
+export const Collapsed = () => {
+ setStoreState({ focused: false });
+ return ;
+};
+
+export const WithRemoteHeld = () => {
+ setStoreState({ callState: 'active', remoteHeld: true });
+ return ;
+};
+
+export const WithRemoteMuted = () => {
+ setStoreState({ callState: 'active', remoteMute: true });
+ return ;
+};
diff --git a/app/containers/MediaCallHeader/MediaCallHeader.test.tsx b/app/containers/MediaCallHeader/MediaCallHeader.test.tsx
new file mode 100644
index 00000000000..c63e829ac9e
--- /dev/null
+++ b/app/containers/MediaCallHeader/MediaCallHeader.test.tsx
@@ -0,0 +1,172 @@
+import React from 'react';
+import { fireEvent, render } from '@testing-library/react-native';
+import { Provider } from 'react-redux';
+
+import MediaCallHeader from './MediaCallHeader';
+import { useCallStore } from '../../lib/services/voip/useCallStore';
+import { mockedStore } from '../../reducers/mockedStore';
+import * as stories from './MediaCallHeader.stories';
+import { generateSnapshots } from '../../../.rnstorybook/generateSnapshots';
+
+// Mock alert
+global.alert = jest.fn();
+
+// Helper to create a mock call
+const createMockCall = (overrides: Record = {}) => ({
+ state: 'active',
+ muted: false,
+ held: false,
+ contact: {
+ displayName: 'Bob Burnquist',
+ username: 'bob.burnquist',
+ sipExtension: '2244'
+ },
+ setMuted: jest.fn(),
+ setHeld: jest.fn(),
+ hangup: jest.fn(),
+ reject: jest.fn(),
+ emitter: {
+ on: jest.fn(),
+ off: jest.fn()
+ },
+ ...overrides
+});
+
+// Helper to set store state for tests
+const setStoreState = (overrides: Partial> = {}) => {
+ const mockCall = createMockCall();
+ useCallStore.setState({
+ call: mockCall as any,
+ callUUID: 'test-uuid',
+ callState: 'active',
+ isMuted: false,
+ isOnHold: false,
+ isSpeakerOn: false,
+ callStartTime: Date.now(),
+ contact: {
+ id: 'user-1',
+ displayName: 'Bob Burnquist',
+ username: 'bob.burnquist',
+ sipExtension: '2244'
+ },
+ focused: true,
+ remoteMute: false,
+ remoteHeld: false,
+ ...overrides
+ });
+};
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+);
+
+describe('MediaCallHeader', () => {
+ beforeEach(() => {
+ useCallStore.getState().reset();
+ jest.clearAllMocks();
+ });
+
+ it('should render empty placeholder when there is no call', () => {
+ useCallStore.setState({ call: null });
+ const { getByTestId, queryByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('media-call-header-empty')).toBeTruthy();
+ expect(queryByTestId('media-call-header')).toBeNull();
+ expect(queryByTestId('media-call-header-collapse')).toBeNull();
+ expect(queryByTestId('media-call-header-content')).toBeNull();
+ expect(queryByTestId('media-call-header-end')).toBeNull();
+ });
+
+ it('should render full header when call exists', () => {
+ setStoreState();
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('media-call-header')).toBeTruthy();
+ expect(getByTestId('media-call-header-collapse')).toBeTruthy();
+ expect(getByTestId('media-call-header-content')).toBeTruthy();
+ expect(getByTestId('media-call-header-end')).toBeTruthy();
+ });
+
+ it('should show caller name in Title', () => {
+ setStoreState({
+ contact: {
+ id: 'user-1',
+ displayName: 'Alice Smith',
+ username: 'alice.smith',
+ sipExtension: '1234'
+ }
+ });
+ const { getByTestId, getByText } = render(
+
+
+
+ );
+
+ expect(getByTestId('call-view-header-title')).toBeTruthy();
+ // Title renders name plus optional Timer in nested Text; match by regex
+ expect(getByText(/Alice Smith/)).toBeTruthy();
+ });
+
+ it('should show subtitle when connecting', () => {
+ setStoreState({ callState: 'ringing' });
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('call-view-header-subtitle')).toBeTruthy();
+ });
+
+ it('should call toggleFocus when collapse button is pressed', () => {
+ setStoreState();
+ const toggleFocus = jest.fn();
+ useCallStore.setState({ toggleFocus });
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ fireEvent.press(getByTestId('media-call-header-collapse'));
+ expect(toggleFocus).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call endCall when end button is pressed', () => {
+ setStoreState();
+ const endCall = jest.fn();
+ useCallStore.setState({ endCall });
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ fireEvent.press(getByTestId('media-call-header-end'));
+ expect(endCall).toHaveBeenCalledTimes(1);
+ });
+
+ it('should show alert when content is pressed', () => {
+ setStoreState();
+ const { getByTestId } = render(
+
+
+
+ );
+
+ fireEvent.press(getByTestId('media-call-header-content'));
+ expect(global.alert).toHaveBeenCalledWith('nav to call room');
+ });
+});
+
+generateSnapshots(stories);
diff --git a/app/containers/CallHeader/CallHeader.tsx b/app/containers/MediaCallHeader/MediaCallHeader.tsx
similarity index 65%
rename from app/containers/CallHeader/CallHeader.tsx
rename to app/containers/MediaCallHeader/MediaCallHeader.tsx
index 37f4f2d61ed..a3a0fa3ad14 100644
--- a/app/containers/CallHeader/CallHeader.tsx
+++ b/app/containers/MediaCallHeader/MediaCallHeader.tsx
@@ -1,11 +1,12 @@
import { StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { useShallow } from 'zustand/react/shallow';
import { useTheme } from '../../theme';
import Collapse from './components/Collapse';
-import Title from './components/Title';
import EndCall from './components/EndCall';
import { useCallStore } from '../../lib/services/voip/useCallStore';
+import { Content } from './components/Content';
const styles = StyleSheet.create({
header: {
@@ -13,34 +14,34 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 12,
- paddingBottom: 4,
borderBottomWidth: StyleSheet.hairlineWidth
}
});
-const CallHeader = () => {
+const MediaCallHeader = () => {
'use memo';
const { colors } = useTheme();
const insets = useSafeAreaInsets();
+ const call = useCallStore(useShallow(state => state.call));
const defaultHeaderStyle = {
backgroundColor: colors.surfaceNeutral,
- paddingTop: insets.top
+ paddingTop: insets.top + 12,
+ paddingBottom: 12
};
- const call = useCallStore(state => state.call);
if (!call) {
- return ;
+ return ;
}
return (
-
+
-
+
);
};
-export default CallHeader;
+export default MediaCallHeader;
diff --git a/app/containers/MediaCallHeader/__snapshots__/MediaCallHeader.test.tsx.snap b/app/containers/MediaCallHeader/__snapshots__/MediaCallHeader.test.tsx.snap
new file mode 100644
index 00000000000..46805f06c9a
--- /dev/null
+++ b/app/containers/MediaCallHeader/__snapshots__/MediaCallHeader.test.tsx.snap
@@ -0,0 +1,2094 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Story Snapshots: ActiveCall should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bob Burnquist
+
+ -
+ 16117:18:53
+
+
+
+
+ 2244
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: Collapsed should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bob Burnquist
+
+ -
+ 16117:18:53
+
+
+
+
+ 2244
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: ConnectingCall should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bob Burnquist
+
+
+
+ Connecting...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: Focused should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bob Burnquist
+
+ -
+ 16117:18:53
+
+
+
+
+ 2244
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: NoCall should match snapshot 1`] = `
+
+
+
+`;
+
+exports[`Story Snapshots: WithRemoteHeld should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bob Burnquist
+
+ -
+ 16117:18:53
+
+
+
+
+ 2244 - On hold
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Story Snapshots: WithRemoteMuted should match snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bob Burnquist
+
+ -
+ 16117:18:53
+
+
+
+
+ 2244 - Muted
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/containers/CallHeader/components/Collapse.tsx b/app/containers/MediaCallHeader/components/Collapse.tsx
similarity index 94%
rename from app/containers/CallHeader/components/Collapse.tsx
rename to app/containers/MediaCallHeader/components/Collapse.tsx
index 6332df73287..1ad769c30c4 100644
--- a/app/containers/CallHeader/components/Collapse.tsx
+++ b/app/containers/MediaCallHeader/components/Collapse.tsx
@@ -9,9 +9,11 @@ const Collapse = () => {
const { colors } = useTheme();
const focused = useCallStore(state => state.focused);
const toggleFocus = useCallStore(state => state.toggleFocus);
+
return (
(
+ alert('nav to call room')} style={styles.button}>
+
+
+
+
+
+);
diff --git a/app/containers/CallHeader/components/EndCall.tsx b/app/containers/MediaCallHeader/components/EndCall.tsx
similarity index 71%
rename from app/containers/CallHeader/components/EndCall.tsx
rename to app/containers/MediaCallHeader/components/EndCall.tsx
index 09f4caa10b3..9ba0968a3d0 100644
--- a/app/containers/CallHeader/components/EndCall.tsx
+++ b/app/containers/MediaCallHeader/components/EndCall.tsx
@@ -10,7 +10,13 @@ const EndCall = () => {
const endCall = useCallStore(state => state.endCall);
return (
-
+
);
};
diff --git a/app/containers/MediaCallHeader/components/Subtitle.tsx b/app/containers/MediaCallHeader/components/Subtitle.tsx
new file mode 100644
index 00000000000..364886e31b6
--- /dev/null
+++ b/app/containers/MediaCallHeader/components/Subtitle.tsx
@@ -0,0 +1,51 @@
+import { StyleSheet, Text } from 'react-native';
+
+import { useTheme } from '../../../theme';
+import { useCallStore } from '../../../lib/services/voip/useCallStore';
+import I18n from '../../../i18n';
+import sharedStyles from '../../../views/Styles';
+
+const styles = StyleSheet.create({
+ headerTitle: {
+ ...sharedStyles.textRegular,
+ fontSize: 12,
+ lineHeight: 16
+ }
+});
+
+const Subtitle = () => {
+ 'use memo';
+
+ const { colors } = useTheme();
+ const contact = useCallStore(state => state.contact);
+ const extension = contact.sipExtension;
+ const remoteHeld = useCallStore(state => state.remoteHeld);
+ const remoteMute = useCallStore(state => state.remoteMute);
+ const callState = useCallStore(state => state.callState);
+ const isConnected = callState === 'active';
+
+ 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(', ');
+ }
+
+ if (!subtitle) {
+ return null;
+ }
+
+ return (
+
+ {subtitle}
+
+ );
+};
+
+export default Subtitle;
diff --git a/app/containers/CallHeader/components/Timer.tsx b/app/containers/MediaCallHeader/components/Timer.tsx
similarity index 95%
rename from app/containers/CallHeader/components/Timer.tsx
rename to app/containers/MediaCallHeader/components/Timer.tsx
index fa784cf864e..56981c57da6 100644
--- a/app/containers/CallHeader/components/Timer.tsx
+++ b/app/containers/MediaCallHeader/components/Timer.tsx
@@ -29,7 +29,7 @@ const Timer = () => {
return () => clearInterval(interval);
}, [callStartTime]);
- return {formatDuration(duration)};
+ return - {formatDuration(duration)};
};
export default Timer;
diff --git a/app/containers/CallHeader/components/Title.tsx b/app/containers/MediaCallHeader/components/Title.tsx
similarity index 56%
rename from app/containers/CallHeader/components/Title.tsx
rename to app/containers/MediaCallHeader/components/Title.tsx
index 36885607bbe..216c466e4f6 100644
--- a/app/containers/CallHeader/components/Title.tsx
+++ b/app/containers/MediaCallHeader/components/Title.tsx
@@ -1,13 +1,18 @@
-import { StyleSheet, Text } from 'react-native';
+import { StyleSheet, Text, View } from 'react-native';
import { useTheme } from '../../../theme';
import { useCallStore } from '../../../lib/services/voip/useCallStore';
-import I18n from '../../../i18n';
import sharedStyles from '../../../views/Styles';
import Timer from './Timer';
+import Status from '../../Status';
const styles = StyleSheet.create({
- headerTitle: {
+ headerTitleContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4
+ },
+ headerTitleText: {
...sharedStyles.textSemibold,
fontSize: 16,
lineHeight: 24
@@ -23,24 +28,16 @@ const Title = () => {
const contact = useCallStore(state => state.contact);
const caller = contact.displayName || contact.username;
- const isConnecting = callState === 'none' || callState === 'ringing' || callState === 'accepted';
const isConnected = callState === 'active';
- const getHeaderTitle = () => {
- if (isConnecting) {
- return I18n.t('Connecting');
- }
- if (isConnected && callStartTime) {
- return `${caller} – `;
- }
- return caller;
- };
-
return (
-
- {getHeaderTitle()}
-
-
+
+
+
+ {caller}
+ {isConnected && callStartTime ? : null}
+
+
);
};
diff --git a/app/lib/services/voip/useCallStore.ts b/app/lib/services/voip/useCallStore.ts
index effa08ba00c..b374b5d30aa 100644
--- a/app/lib/services/voip/useCallStore.ts
+++ b/app/lib/services/voip/useCallStore.ts
@@ -1,16 +1,9 @@
import { create } from 'zustand';
-import { useShallow } from 'zustand/react/shallow';
-import type { CallState, IClientMediaCall } from '@rocket.chat/media-signaling';
+import type { CallState, CallContact, IClientMediaCall } from '@rocket.chat/media-signaling';
import RNCallKeep from 'react-native-callkeep';
import Navigation from '../../navigation/appNavigation';
-interface CallContact {
- displayName?: string;
- username?: string;
- sipExtension?: string;
-}
-
interface CallStoreState {
// Call reference
call: IClientMediaCall | null;
@@ -20,6 +13,8 @@ interface CallStoreState {
callState: CallState;
isMuted: boolean;
isOnHold: boolean;
+ remoteMute: boolean;
+ remoteHeld: boolean;
isSpeakerOn: boolean;
callStartTime: number | null;
focused: boolean;
@@ -31,7 +26,6 @@ interface CallStoreState {
interface CallStoreActions {
setCallUUID: (callUUID: string | null) => void;
setCall: (call: IClientMediaCall, callUUID: string) => void;
- updateFromCall: () => void;
toggleMute: () => void;
toggleHold: () => void;
toggleSpeaker: () => void;
@@ -48,10 +42,12 @@ const initialState: CallStoreState = {
callState: 'none',
isMuted: false,
isOnHold: false,
+ remoteMute: false,
+ remoteHeld: false,
isSpeakerOn: false,
callStartTime: null,
contact: {},
- focused: false
+ focused: true
};
export const useCallStore = create((set, get) => ({
@@ -69,8 +65,11 @@ export const useCallStore = create((set, get) => ({
callState: call.state,
isMuted: call.muted,
isOnHold: call.held,
+ remoteMute: call.remoteMute,
+ remoteHeld: call.remoteHeld,
// isSpeakerOn: call.
contact: {
+ id: call.contact.id,
displayName: call.contact.displayName,
username: call.contact.username,
sipExtension: call.contact.sipExtension
@@ -98,7 +97,9 @@ export const useCallStore = create((set, get) => ({
set({
isMuted: currentCall.muted,
- isOnHold: currentCall.held
+ isOnHold: currentCall.held,
+ remoteMute: currentCall.remoteMute,
+ remoteHeld: currentCall.remoteHeld
});
};
@@ -112,22 +113,6 @@ export const useCallStore = create((set, get) => ({
call.emitter.on('ended', handleEnded);
},
- updateFromCall: () => {
- const { call } = get();
- if (!call) return;
-
- set({
- callState: call.state,
- isMuted: call.muted,
- isOnHold: call.held,
- contact: {
- displayName: call.contact.displayName,
- username: call.contact.username,
- sipExtension: call.contact.sipExtension
- }
- });
- },
-
toggleMute: () => {
const { call, isMuted } = get();
if (!call) return;
@@ -184,21 +169,28 @@ export const useCallStore = create((set, get) => ({
}
}));
-// Selector hooks for better performance
-export const useCallState = () => useCallStore(state => state.callState);
+// const isConnecting = callState === 'none' || callState === 'ringing' || callState === 'accepted';
+// const isConnected = callState === 'active';
+export const useCallState = () => {
+ const callState = useCallStore(state => state.callState);
+ return callState === 'none' || callState === 'ringing' || callState === 'accepted';
+};
+
+// // Selector hooks for better performance
+// export const useCallState = () => useCallStore(state => state.callState);
export const useCallContact = () => useCallStore(state => state.contact);
-export const useCallControls = () =>
- useCallStore(
- useShallow(state => ({
- isMuted: state.isMuted,
- isOnHold: state.isOnHold,
- isSpeakerOn: state.isSpeakerOn
- }))
- );
-export const useCallActions = () =>
- useCallStore(state => ({
- toggleMute: state.toggleMute,
- toggleHold: state.toggleHold,
- toggleSpeaker: state.toggleSpeaker,
- endCall: state.endCall
- }));
+// export const useCallControls = () =>
+// useCallStore(
+// useShallow(state => ({
+// isMuted: state.isMuted,
+// isOnHold: state.isOnHold,
+// isSpeakerOn: state.isSpeakerOn
+// }))
+// );
+// export const useCallActions = () =>
+// useCallStore(state => ({
+// toggleMute: state.toggleMute,
+// toggleHold: state.toggleHold,
+// toggleSpeaker: state.toggleSpeaker,
+// endCall: state.endCall
+// }));
diff --git a/app/views/CallView/CallView.stories.tsx b/app/views/CallView/CallView.stories.tsx
index d7bbaed81a4..1fe80b664df 100644
--- a/app/views/CallView/CallView.stories.tsx
+++ b/app/views/CallView/CallView.stories.tsx
@@ -16,16 +16,7 @@ const styles = StyleSheet.create({
}
});
-// Mock navigation
-// jest.mock('@react-navigation/native', () => ({
-// ...jest.requireActual('@react-navigation/native'),
-// useNavigation: () => ({
-// goBack: () => {}
-// }),
-// useRoute: () => ({
-// params: { callUUID: 'test-uuid' }
-// })
-// }));
+const mockCallStartTime = 1713340800000;
// Helper to set store state for stories
const setStoreState = (overrides: Partial> = {}) => {
@@ -55,7 +46,7 @@ const setStoreState = (overrides: Partial {
- setStoreState({ callState: 'active', callStartTime: new Date().getTime() - 61000 });
+ setStoreState({ callState: 'active', callStartTime: mockCallStartTime - 61000 });
return ;
};
diff --git a/app/views/CallView/__snapshots__/index.test.tsx.snap b/app/views/CallView/__snapshots__/index.test.tsx.snap
index b0404418151..703e4a6d0c6 100644
--- a/app/views/CallView/__snapshots__/index.test.tsx.snap
+++ b/app/views/CallView/__snapshots__/index.test.tsx.snap
@@ -46,7 +46,7 @@ exports[`Story Snapshots: ConnectedCall should match snapshot 1`] = `
style={
[
{
- "borderRadius": 16,
+ "borderRadius": 2,
"height": 120,
"width": 120,
},
@@ -56,7 +56,7 @@ exports[`Story Snapshots: ConnectedCall should match snapshot 1`] = `
testID="avatar"
>
-
-
- 2244
-
-
-
-
-
-
- 2244
-
-
Bob Burnquist
-
-
-
-
- 2244
-
-
- On hold
- ,
-
- Muted
-
-
-
-
Bob Burnquist
-
-
-
-
- 2244
-
-
- Muted
-
-
-
-
- 2244
-
-
- On hold
-
-
-
-
- 2244
-
-
-
-
{
+ 'use memo';
+
+ const { colors } = useTheme();
+
+ const callState = useCallStore(state => state.callState);
+ const isMuted = useCallStore(state => state.isMuted);
+ const isOnHold = useCallStore(state => state.isOnHold);
+ const isSpeakerOn = useCallStore(state => state.isSpeakerOn);
+
+ const toggleMute = useCallStore(state => state.toggleMute);
+ const toggleHold = useCallStore(state => state.toggleHold);
+ const toggleSpeaker = useCallStore(state => state.toggleSpeaker);
+ const endCall = useCallStore(state => state.endCall);
+
+ const isConnecting = callState === 'none' || callState === 'ringing' || callState === 'accepted';
+
+ const handleMessage = () => {
+ // TODO: Navigate to chat with caller
+ // Navigation.navigate('RoomView', { rid, t: 'd' });
+ alert('Message');
+ };
+
+ const handleMore = () => {
+ // TODO: Show action sheet with more options (DTMF, transfer, etc.)
+ alert('More');
+ };
+
+ const handleEndCall = () => {
+ endCall();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/app/views/CallView/components/CallStatusText.tsx b/app/views/CallView/components/CallStatusText.tsx
deleted file mode 100644
index 305c4b66ea1..00000000000
--- a/app/views/CallView/components/CallStatusText.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import { Text } from 'react-native';
-
-import I18n from '../../../i18n';
-import { styles } from '../styles';
-import { useCallControls } from '../../../lib/services/voip/useCallStore';
-import { useTheme } from '../../../theme';
-
-const CallStatusText = (): React.ReactElement => {
- 'use memo';
-
- const { isMuted, isOnHold } = useCallControls();
- const { colors } = useTheme();
-
- if (isOnHold && isMuted) {
- return (
- <>
-
- {I18n.t('On_hold')}, {I18n.t('Muted')}
-
- >
- );
- }
- if (isOnHold) {
- return {I18n.t('On_hold')};
- }
- if (isMuted) {
- return {I18n.t('Muted')};
- }
-
- return ;
-};
-
-export default CallStatusText;
diff --git a/app/views/CallView/components/CallerInfo.stories.tsx b/app/views/CallView/components/CallerInfo.stories.tsx
index 595229ac548..c24c3ac1184 100644
--- a/app/views/CallView/components/CallerInfo.stories.tsx
+++ b/app/views/CallView/components/CallerInfo.stories.tsx
@@ -23,8 +23,7 @@ const setStoreState = (contact: { displayName?: string; username?: string; sipEx
callState: 'active',
isMuted: false,
isOnHold: false,
- isSpeakerOn: false,
- callStartTime: Date.now()
+ isSpeakerOn: false
});
};
@@ -45,15 +44,6 @@ export default {
export const Default = () => ;
-export const WithOnlineStatus = () => ;
-
-export const WithMutedIndicator = () => ;
-
-export const NoExtension = () => {
- setStoreState({ displayName: 'Alice Attali', username: 'alice.attali' });
- return ;
-};
-
export const UsernameOnly = () => {
setStoreState({ username: 'john.doe' });
return ;
diff --git a/app/views/CallView/components/CallerInfo.test.tsx b/app/views/CallView/components/CallerInfo.test.tsx
index 7bd8bd55586..c411cfa5a3d 100644
--- a/app/views/CallView/components/CallerInfo.test.tsx
+++ b/app/views/CallView/components/CallerInfo.test.tsx
@@ -8,6 +8,8 @@ import { mockedStore } from '../../../reducers/mockedStore';
import * as stories from './CallerInfo.stories';
import { generateSnapshots } from '../../../../.rnstorybook/generateSnapshots';
+const mockCallStartTime = 1713340800000;
+
// Helper to set store state for tests
const setStoreState = (contact: { displayName?: string; username?: string; sipExtension?: string }) => {
useCallStore.setState({
@@ -18,7 +20,7 @@ const setStoreState = (contact: { displayName?: string; username?: string; sipEx
isMuted: false,
isOnHold: false,
isSpeakerOn: false,
- callStartTime: Date.now()
+ callStartTime: mockCallStartTime
});
};
@@ -39,7 +41,6 @@ describe('CallerInfo', () => {
expect(getByTestId('caller-info')).toBeTruthy();
expect(getByText('Bob Burnquist')).toBeTruthy();
- expect(getByText('2244')).toBeTruthy();
});
it('should render with username when no display name', () => {
@@ -52,53 +53,6 @@ describe('CallerInfo', () => {
expect(getByText('john.doe')).toBeTruthy();
});
-
- it('should render status container (Status component is currently commented out)', () => {
- setStoreState({ displayName: 'Test User' });
- const { getByTestId } = render(
-
-
-
- );
-
- // The status container exists but Status component is commented out
- // Verify the component renders correctly
- expect(getByTestId('caller-info')).toBeTruthy();
- expect(getByTestId('avatar')).toBeTruthy();
- });
-
- it('should show muted indicator when isMuted is true', () => {
- setStoreState({ displayName: 'Test User' });
- const { getByTestId } = render(
-
-
-
- );
-
- expect(getByTestId('caller-info-muted')).toBeTruthy();
- });
-
- it('should not show muted indicator when isMuted is false', () => {
- setStoreState({ displayName: 'Test User' });
- const { queryByTestId } = render(
-
-
-
- );
-
- expect(queryByTestId('caller-info-muted')).toBeNull();
- });
-
- it('should not show extension when not provided', () => {
- setStoreState({ displayName: 'Test User' });
- const { queryByTestId } = render(
-
-
-
- );
-
- expect(queryByTestId('caller-info-extension')).toBeNull();
- });
});
generateSnapshots(stories);
diff --git a/app/views/CallView/components/CallerInfo.tsx b/app/views/CallView/components/CallerInfo.tsx
index c212e454435..32b5d856b7a 100644
--- a/app/views/CallView/components/CallerInfo.tsx
+++ b/app/views/CallView/components/CallerInfo.tsx
@@ -2,54 +2,28 @@ import React from 'react';
import { Text, View } from 'react-native';
import AvatarContainer from '../../../containers/Avatar';
-import { CustomIcon } from '../../../containers/CustomIcon';
import I18n from '../../../i18n';
import { useCallContact } from '../../../lib/services/voip/useCallStore';
import { styles } from '../styles';
import { useTheme } from '../../../theme';
-// import Status from '../../../containers/Status';
-import sharedStyles from '../../Styles';
-interface ICallerInfo {
- isMuted?: boolean;
-}
-
-const CallerInfo = ({ isMuted = false }: ICallerInfo): React.ReactElement => {
+const CallerInfo = (): React.ReactElement => {
const { colors } = useTheme();
const contact = useCallContact();
const name = contact.displayName || contact.username || I18n.t('Unknown');
- const extension = contact.sipExtension;
const avatarText = contact.username || name;
return (
-
-
- {/* */}
-
-
+
{name}
- {isMuted && (
-
- )}
- {extension ? (
-
- {extension}
-
- ) : null}
);
};
diff --git a/app/views/CallView/components/__snapshots__/CallerInfo.test.tsx.snap b/app/views/CallView/components/__snapshots__/CallerInfo.test.tsx.snap
index a4633b97130..145e5f98944 100644
--- a/app/views/CallView/components/__snapshots__/CallerInfo.test.tsx.snap
+++ b/app/views/CallView/components/__snapshots__/CallerInfo.test.tsx.snap
@@ -35,7 +35,7 @@ exports[`Story Snapshots: Default should match snapshot 1`] = `
style={
[
{
- "borderRadius": 16,
+ "borderRadius": 2,
"height": 120,
"width": 120,
},
@@ -45,7 +45,7 @@ exports[`Story Snapshots: Default should match snapshot 1`] = `
testID="avatar"
>
-
-
- 2244
-
-
-
-`;
-
-exports[`Story Snapshots: NoExtension should match snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-
- Alice Attali
-
-
`;
@@ -322,7 +153,7 @@ exports[`Story Snapshots: UsernameOnly should match snapshot 1`] = `
style={
[
{
- "borderRadius": 16,
+ "borderRadius": 2,
"height": 120,
"width": 120,
},
@@ -332,7 +163,7 @@ exports[`Story Snapshots: UsernameOnly should match snapshot 1`] = `
testID="avatar"
>
-
`;
-
-exports[`Story Snapshots: WithMutedIndicator should match snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-
- Bob Burnquist
-
-
-
-
-
-
- 2244
-
-
-
-`;
-
-exports[`Story Snapshots: WithOnlineStatus should match snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-
- Bob Burnquist
-
-
-
- 2244
-
-
-
-`;
diff --git a/app/views/CallView/index.test.tsx b/app/views/CallView/index.test.tsx
index 66316d13e3e..f4e12d61268 100644
--- a/app/views/CallView/index.test.tsx
+++ b/app/views/CallView/index.test.tsx
@@ -287,7 +287,7 @@ describe('CallView', () => {
expect(getByText('End')).toBeTruthy();
});
- it('should show muted indicator when call is muted and active', () => {
+ it('should render call view when call is muted and active', () => {
setStoreState({ callState: 'active', isMuted: true });
const { getByTestId } = render(
@@ -295,18 +295,19 @@ describe('CallView', () => {
);
- expect(getByTestId('caller-info-muted')).toBeTruthy();
+ expect(getByTestId('caller-info')).toBeTruthy();
+ expect(getByTestId('call-view-mute')).toBeTruthy();
});
- it('should not show muted indicator when call is muted but not active', () => {
+ it('should render call view when call is muted but not yet active', () => {
setStoreState({ callState: 'ringing', isMuted: true });
- const { queryByTestId } = render(
+ const { getByTestId } = render(
);
- expect(queryByTestId('caller-info-muted')).toBeNull();
+ expect(getByTestId('caller-info')).toBeTruthy();
});
it('should show correct icon for speaker button when speaker is on', () => {
diff --git a/app/views/CallView/index.tsx b/app/views/CallView/index.tsx
index 5c455897fad..a6d13848c89 100644
--- a/app/views/CallView/index.tsx
+++ b/app/views/CallView/index.tsx
@@ -2,33 +2,18 @@ import React, { useEffect } from 'react';
import { View } from 'react-native';
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
-import I18n from '../../i18n';
import { useCallStore } from '../../lib/services/voip/useCallStore';
import CallerInfo from './components/CallerInfo';
-import CallActionButton from './components/CallActionButton';
-import CallStatusText from './components/CallStatusText';
import { styles } from './styles';
import { useTheme } from '../../theme';
+import { CallButtons } from './components/CallButtons';
const CallView = (): React.ReactElement | null => {
'use memo';
const { colors } = useTheme();
-
- // Get state from store
const call = useCallStore(state => state.call);
- const callState = useCallStore(state => state.callState);
- const isMuted = useCallStore(state => state.isMuted);
- const isOnHold = useCallStore(state => state.isOnHold);
- const isSpeakerOn = useCallStore(state => state.isSpeakerOn);
-
- // Get actions from store
- const toggleMute = useCallStore(state => state.toggleMute);
- const toggleHold = useCallStore(state => state.toggleHold);
- const toggleSpeaker = useCallStore(state => state.toggleSpeaker);
- const endCall = useCallStore(state => state.endCall);
- // Keep screen awake during call
useEffect(() => {
activateKeepAwakeAsync();
return () => {
@@ -36,78 +21,14 @@ const CallView = (): React.ReactElement | null => {
};
}, []);
- const handleMessage = () => {
- // TODO: Navigate to chat with caller
- // Navigation.navigate('RoomView', { rid, t: 'd' });
- alert('Message');
- };
-
- const handleMore = () => {
- // TODO: Show action sheet with more options (DTMF, transfer, etc.)
- alert('More');
- };
-
- const handleEndCall = () => {
- endCall();
- };
-
if (!call) {
return null;
}
- const isConnecting = callState === 'none' || callState === 'ringing' || callState === 'accepted';
- const isConnected = callState === 'active';
-
return (
- {/* Caller Info */}
-
-
- {/* Status Text */}
- {isConnected && }
-
- {/* Action Buttons */}
-
- {/* First row of buttons */}
-
-
-
-
-
-
- {/* Second row of buttons */}
-
-
-
-
-
-
+
+
);
};
diff --git a/app/views/CallView/styles.ts b/app/views/CallView/styles.ts
index 484af08e061..a22cd8ab119 100644
--- a/app/views/CallView/styles.ts
+++ b/app/views/CallView/styles.ts
@@ -50,7 +50,8 @@ export const styles = StyleSheet.create({
textAlign: 'center'
},
buttonsContainer: {
- padding: 24
+ padding: 24,
+ borderTopWidth: StyleSheet.hairlineWidth
},
buttonsRow: {
flexDirection: 'row',