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 ( - + - + <Content /> <EndCall /> </View> ); }; -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`] = ` +<View + style={ + { + "flex": 1, + } + } +> + <View + style={ + [ + { + "alignItems": "center", + "borderBottomWidth": 0.5, + "flexDirection": "row", + "justifyContent": "space-between", + "paddingHorizontal": 12, + }, + { + "backgroundColor": "#E4E7EA", + "borderBottomColor": "#CBCED1", + "paddingBottom": 12, + "paddingTop": 12, + }, + ] + } + testID="media-call-header" + > + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginLeft": -5, + "marginRight": 0, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={13} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-collapse" + > + <View + accessibilityLabel="[missing "en.Minimize" translation]" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "flex": 1, + "paddingHorizontal": 4, + } + } + testID="media-call-header-content" + > + <View + style={ + { + "alignItems": "flex-start", + "flex": 1, + "flexDirection": "column", + "justifyContent": "space-evenly", + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 4, + } + } + testID="call-view-header-title" + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#6C727A", + "fontSize": 12, + }, + [ + { + "lineHeight": 12, + }, + [ + { + "height": 12, + "textAlignVertical": "center", + "width": 12, + }, + undefined, + ], + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 24, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + Bob Burnquist + <Text> + - + 16117:18:53 + </Text> + </Text> + </View> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "400", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="call-view-header-subtitle" + > + 2244 + </Text> + </View> + </View> + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginRight": -5, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={14} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-end" + > + <View + accessibilityLabel="End" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#D40C26", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: Collapsed should match snapshot 1`] = ` +<View + style={ + { + "flex": 1, + } + } +> + <View + style={ + [ + { + "alignItems": "center", + "borderBottomWidth": 0.5, + "flexDirection": "row", + "justifyContent": "space-between", + "paddingHorizontal": 12, + }, + { + "backgroundColor": "#E4E7EA", + "borderBottomColor": "#CBCED1", + "paddingBottom": 12, + "paddingTop": 12, + }, + ] + } + testID="media-call-header" + > + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginLeft": -5, + "marginRight": 0, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={15} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-collapse" + > + <View + accessibilityLabel="[missing "en.Minimize" translation]" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "flex": 1, + "paddingHorizontal": 4, + } + } + testID="media-call-header-content" + > + <View + style={ + { + "alignItems": "flex-start", + "flex": 1, + "flexDirection": "column", + "justifyContent": "space-evenly", + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 4, + } + } + testID="call-view-header-title" + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#6C727A", + "fontSize": 12, + }, + [ + { + "lineHeight": 12, + }, + [ + { + "height": 12, + "textAlignVertical": "center", + "width": 12, + }, + undefined, + ], + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 24, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + Bob Burnquist + <Text> + - + 16117:18:53 + </Text> + </Text> + </View> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "400", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="call-view-header-subtitle" + > + 2244 + </Text> + </View> + </View> + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginRight": -5, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={16} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-end" + > + <View + accessibilityLabel="End" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#D40C26", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: ConnectingCall should match snapshot 1`] = ` +<View + style={ + { + "flex": 1, + } + } +> + <View + style={ + [ + { + "alignItems": "center", + "borderBottomWidth": 0.5, + "flexDirection": "row", + "justifyContent": "space-between", + "paddingHorizontal": 12, + }, + { + "backgroundColor": "#E4E7EA", + "borderBottomColor": "#CBCED1", + "paddingBottom": 12, + "paddingTop": 12, + }, + ] + } + testID="media-call-header" + > + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginLeft": -5, + "marginRight": 0, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={17} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-collapse" + > + <View + accessibilityLabel="[missing "en.Minimize" translation]" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "flex": 1, + "paddingHorizontal": 4, + } + } + testID="media-call-header-content" + > + <View + style={ + { + "alignItems": "flex-start", + "flex": 1, + "flexDirection": "column", + "justifyContent": "space-evenly", + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 4, + } + } + testID="call-view-header-title" + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#6C727A", + "fontSize": 12, + }, + [ + { + "lineHeight": 12, + }, + [ + { + "height": 12, + "textAlignVertical": "center", + "width": 12, + }, + undefined, + ], + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 24, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + Bob Burnquist + </Text> + </View> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "400", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="call-view-header-subtitle" + > + Connecting... + </Text> + </View> + </View> + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginRight": -5, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={18} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-end" + > + <View + accessibilityLabel="End" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#D40C26", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: Focused should match snapshot 1`] = ` +<View + style={ + { + "flex": 1, + } + } +> + <View + style={ + [ + { + "alignItems": "center", + "borderBottomWidth": 0.5, + "flexDirection": "row", + "justifyContent": "space-between", + "paddingHorizontal": 12, + }, + { + "backgroundColor": "#E4E7EA", + "borderBottomColor": "#CBCED1", + "paddingBottom": 12, + "paddingTop": 12, + }, + ] + } + testID="media-call-header" + > + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginLeft": -5, + "marginRight": 0, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={19} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-collapse" + > + <View + accessibilityLabel="[missing "en.Minimize" translation]" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "flex": 1, + "paddingHorizontal": 4, + } + } + testID="media-call-header-content" + > + <View + style={ + { + "alignItems": "flex-start", + "flex": 1, + "flexDirection": "column", + "justifyContent": "space-evenly", + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 4, + } + } + testID="call-view-header-title" + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#6C727A", + "fontSize": 12, + }, + [ + { + "lineHeight": 12, + }, + [ + { + "height": 12, + "textAlignVertical": "center", + "width": 12, + }, + undefined, + ], + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 24, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + Bob Burnquist + <Text> + - + 16117:18:53 + </Text> + </Text> + </View> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "400", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="call-view-header-subtitle" + > + 2244 + </Text> + </View> + </View> + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginRight": -5, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={20} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-end" + > + <View + accessibilityLabel="End" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#D40C26", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: NoCall should match snapshot 1`] = ` +<View + style={ + { + "flex": 1, + } + } +> + <View + style={ + { + "backgroundColor": "#E4E7EA", + "paddingBottom": 12, + "paddingTop": 12, + } + } + testID="media-call-header-empty" + /> +</View> +`; + +exports[`Story Snapshots: WithRemoteHeld should match snapshot 1`] = ` +<View + style={ + { + "flex": 1, + } + } +> + <View + style={ + [ + { + "alignItems": "center", + "borderBottomWidth": 0.5, + "flexDirection": "row", + "justifyContent": "space-between", + "paddingHorizontal": 12, + }, + { + "backgroundColor": "#E4E7EA", + "borderBottomColor": "#CBCED1", + "paddingBottom": 12, + "paddingTop": 12, + }, + ] + } + testID="media-call-header" + > + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginLeft": -5, + "marginRight": 0, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={21} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-collapse" + > + <View + accessibilityLabel="[missing "en.Minimize" translation]" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "flex": 1, + "paddingHorizontal": 4, + } + } + testID="media-call-header-content" + > + <View + style={ + { + "alignItems": "flex-start", + "flex": 1, + "flexDirection": "column", + "justifyContent": "space-evenly", + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 4, + } + } + testID="call-view-header-title" + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#6C727A", + "fontSize": 12, + }, + [ + { + "lineHeight": 12, + }, + [ + { + "height": 12, + "textAlignVertical": "center", + "width": 12, + }, + undefined, + ], + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 24, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + Bob Burnquist + <Text> + - + 16117:18:53 + </Text> + </Text> + </View> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "400", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="call-view-header-subtitle" + > + 2244 - On hold + </Text> + </View> + </View> + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginRight": -5, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={22} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-end" + > + <View + accessibilityLabel="End" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#D40C26", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: WithRemoteMuted should match snapshot 1`] = ` +<View + style={ + { + "flex": 1, + } + } +> + <View + style={ + [ + { + "alignItems": "center", + "borderBottomWidth": 0.5, + "flexDirection": "row", + "justifyContent": "space-between", + "paddingHorizontal": 12, + }, + { + "backgroundColor": "#E4E7EA", + "borderBottomColor": "#CBCED1", + "paddingBottom": 12, + "paddingTop": 12, + }, + ] + } + testID="media-call-header" + > + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginLeft": -5, + "marginRight": 0, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={23} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-collapse" + > + <View + accessibilityLabel="[missing "en.Minimize" translation]" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "flex": 1, + "paddingHorizontal": 4, + } + } + testID="media-call-header-content" + > + <View + style={ + { + "alignItems": "flex-start", + "flex": 1, + "flexDirection": "column", + "justifyContent": "space-evenly", + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 4, + } + } + testID="call-view-header-title" + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#6C727A", + "fontSize": 12, + }, + [ + { + "lineHeight": 12, + }, + [ + { + "height": 12, + "textAlignVertical": "center", + "width": 12, + }, + undefined, + ], + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 24, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + Bob Burnquist + <Text> + - + 16117:18:53 + </Text> + </Text> + </View> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "400", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="call-view-header-subtitle" + > + 2244 - Muted + </Text> + </View> + </View> + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + "justifyContent": "center", + }, + { + "marginRight": -5, + }, + {}, + ] + } + > + <RNGestureHandlerButton + activeOpacity={0.3} + borderless={true} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={24} + handlerType="NativeViewGestureHandler" + hitSlop={ + { + "bottom": 5, + "left": 5, + "right": 5, + "top": 5, + } + } + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[MockFunction]} + style={ + [ + { + "opacity": 1, + "padding": 6, + }, + { + "cursor": undefined, + }, + ] + } + testID="media-call-header-end" + > + <View + accessibilityLabel="End" + accessible={true} + style={ + { + "opacity": 1, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#D40C26", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </View> + </View> +</View> +`; 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 ( <HeaderButton.Container left> <HeaderButton.Item + testID='media-call-header-collapse' accessibilityLabel={I18n.t('Minimize')} onPress={toggleFocus} iconName={focused ? 'arrow-collapse' : 'arrow-expand'} diff --git a/app/containers/MediaCallHeader/components/Content.tsx b/app/containers/MediaCallHeader/components/Content.tsx new file mode 100644 index 00000000000..566918a42c2 --- /dev/null +++ b/app/containers/MediaCallHeader/components/Content.tsx @@ -0,0 +1,26 @@ +import { Pressable, StyleSheet, View } from 'react-native'; + +import Title from './Title'; +import Subtitle from './Subtitle'; + +const styles = StyleSheet.create({ + button: { + flex: 1, + paddingHorizontal: 4 + }, + container: { + flex: 1, + flexDirection: 'column', + justifyContent: 'space-evenly', + alignItems: 'flex-start' + } +}); + +export const Content = () => ( + <Pressable testID='media-call-header-content' onPress={() => alert('nav to call room')} style={styles.button}> + <View style={styles.container}> + <Title /> + <Subtitle /> + </View> + </Pressable> +); 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 ( <HeaderButton.Container> - <HeaderButton.Item accessibilityLabel={I18n.t('End')} onPress={endCall} iconName='phone-end' color={colors.fontDanger} /> + <HeaderButton.Item + testID='media-call-header-end' + accessibilityLabel={I18n.t('End')} + onPress={endCall} + iconName='phone-end' + color={colors.fontDanger} + /> </HeaderButton.Container> ); }; 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 ( + <Text style={[styles.headerTitle, { color: colors.fontSecondaryInfo }]} testID='call-view-header-subtitle' numberOfLines={1}> + {subtitle} + </Text> + ); +}; + +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 <Text>{formatDuration(duration)}</Text>; + return <Text> - {formatDuration(duration)}</Text>; }; 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 ( - <Text style={[styles.headerTitle, { color: colors.fontDefault }]} testID='call-view-header-title'> - {getHeaderTitle()} - <Timer /> - </Text> + <View style={styles.headerTitleContainer} testID='call-view-header-title'> + <Status id={contact.id || ''} size={12} /> + <Text style={[styles.headerTitleText, { color: colors.fontDefault }]} numberOfLines={1}> + {caller} + {isConnected && callStartTime ? <Timer /> : null} + </Text> + </View> ); }; 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<CallStore>((set, get) => ({ @@ -69,8 +65,11 @@ export const useCallStore = create<CallStore>((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<CallStore>((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<CallStore>((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<CallStore>((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<ReturnType<typeof useCallStore.getState>> = {}) => { @@ -55,7 +46,7 @@ const setStoreState = (overrides: Partial<ReturnType<typeof useCallStore.getStat isMuted: false, isOnHold: false, isSpeakerOn: false, - callStartTime: Date.now(), + callStartTime: mockCallStartTime, contact: { displayName: 'Bob Burnquist', username: 'bob.burnquist', @@ -94,7 +85,7 @@ export default { }; export const ConnectedCall = () => { - setStoreState({ callState: 'active', callStartTime: new Date().getTime() - 61000 }); + setStoreState({ callState: 'active', callStartTime: mockCallStartTime - 61000 }); return <CallView />; }; 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" > <ViewManagerAdapter_ExpoImage - borderRadius={16} + borderRadius={2} containerViewRef={"[React.ref]"} contentFit="cover" contentPosition={ @@ -83,7 +83,7 @@ exports[`Story Snapshots: ConnectedCall should match snapshot 1`] = ` } style={ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, } @@ -91,21 +91,6 @@ exports[`Story Snapshots: ConnectedCall should match snapshot 1`] = ` transition={null} width={120} /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> </View> </View> <View @@ -140,47 +125,18 @@ exports[`Story Snapshots: ConnectedCall should match snapshot 1`] = ` Bob Burnquist </Text> </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "marginBottom": 8, - "textAlign": "center", - }, - { - "color": "#6C727A", - }, - ] - } - testID="caller-info-extension" - > - 2244 - </Text> </View> - <Text - style={ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "textAlign": "center", - } - } - > -   - </Text> <View style={ - { - "padding": 24, - } + [ + { + "borderTopWidth": 0.5, + "padding": 24, + }, + { + "borderTopColor": "#EBECEF", + }, + ] } > <View @@ -842,7 +798,7 @@ exports[`Story Snapshots: ConnectingCall should match snapshot 1`] = ` style={ [ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, }, @@ -852,7 +808,7 @@ exports[`Story Snapshots: ConnectingCall should match snapshot 1`] = ` testID="avatar" > <ViewManagerAdapter_ExpoImage - borderRadius={16} + borderRadius={2} containerViewRef={"[React.ref]"} contentFit="cover" contentPosition={ @@ -879,7 +835,7 @@ exports[`Story Snapshots: ConnectingCall should match snapshot 1`] = ` } style={ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, } @@ -887,21 +843,6 @@ exports[`Story Snapshots: ConnectingCall should match snapshot 1`] = ` transition={null} width={120} /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> </View> </View> <View @@ -936,33 +877,18 @@ exports[`Story Snapshots: ConnectingCall should match snapshot 1`] = ` Bob Burnquist </Text> </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "marginBottom": 8, - "textAlign": "center", - }, - { - "color": "#6C727A", - }, - ] - } - testID="caller-info-extension" - > - 2244 - </Text> </View> <View style={ - { - "padding": 24, - } + [ + { + "borderTopWidth": 0.5, + "padding": 24, + }, + { + "borderTopColor": "#EBECEF", + }, + ] } > <View @@ -1628,7 +1554,7 @@ exports[`Story Snapshots: MutedAndOnHold should match snapshot 1`] = ` style={ [ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, }, @@ -1638,7 +1564,7 @@ exports[`Story Snapshots: MutedAndOnHold should match snapshot 1`] = ` testID="avatar" > <ViewManagerAdapter_ExpoImage - borderRadius={16} + borderRadius={2} containerViewRef={"[React.ref]"} contentFit="cover" contentPosition={ @@ -1665,7 +1591,7 @@ exports[`Story Snapshots: MutedAndOnHold should match snapshot 1`] = ` } style={ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, } @@ -1673,21 +1599,6 @@ exports[`Story Snapshots: MutedAndOnHold should match snapshot 1`] = ` transition={null} width={120} /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> </View> </View> <View @@ -1721,93 +1632,20 @@ exports[`Story Snapshots: MutedAndOnHold should match snapshot 1`] = ` > Bob Burnquist </Text> - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#D40C26", - "fontSize": NaN, - }, - [ - { - "lineHeight": NaN, - }, - { - "marginLeft": 8, - }, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - testID="caller-info-muted" - > -  - </Text> </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "marginBottom": 8, - "textAlign": "center", - }, - { - "color": "#6C727A", - }, - ] - } - testID="caller-info-extension" - > - 2244 - </Text> </View> - <Text + <View style={ [ { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "textAlign": "center", + "borderTopWidth": 0.5, + "padding": 24, }, { - "color": "#2F343D", + "borderTopColor": "#EBECEF", }, ] } - > - On hold - , - <Text - style={ - { - "color": "#8E6300", - } - } - > - Muted - </Text> - </Text> - <View - style={ - { - "padding": 24, - } - } > <View style={ @@ -2468,7 +2306,7 @@ exports[`Story Snapshots: MutedCall should match snapshot 1`] = ` style={ [ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, }, @@ -2478,7 +2316,7 @@ exports[`Story Snapshots: MutedCall should match snapshot 1`] = ` testID="avatar" > <ViewManagerAdapter_ExpoImage - borderRadius={16} + borderRadius={2} containerViewRef={"[React.ref]"} contentFit="cover" contentPosition={ @@ -2505,7 +2343,7 @@ exports[`Story Snapshots: MutedCall should match snapshot 1`] = ` } style={ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, } @@ -2513,21 +2351,6 @@ exports[`Story Snapshots: MutedCall should match snapshot 1`] = ` transition={null} width={120} /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> </View> </View> <View @@ -2561,83 +2384,20 @@ exports[`Story Snapshots: MutedCall should match snapshot 1`] = ` > Bob Burnquist </Text> - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#D40C26", - "fontSize": NaN, - }, - [ - { - "lineHeight": NaN, - }, - { - "marginLeft": 8, - }, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - testID="caller-info-muted" - > -  - </Text> </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "marginBottom": 8, - "textAlign": "center", - }, - { - "color": "#6C727A", - }, - ] - } - testID="caller-info-extension" - > - 2244 - </Text> </View> - <Text + <View style={ [ { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "textAlign": "center", + "borderTopWidth": 0.5, + "padding": 24, }, { - "color": "#8E6300", + "borderTopColor": "#EBECEF", }, ] } - > - Muted - </Text> - <View - style={ - { - "padding": 24, - } - } > <View style={ @@ -3298,7 +3058,7 @@ exports[`Story Snapshots: OnHoldCall should match snapshot 1`] = ` style={ [ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, }, @@ -3308,7 +3068,7 @@ exports[`Story Snapshots: OnHoldCall should match snapshot 1`] = ` testID="avatar" > <ViewManagerAdapter_ExpoImage - borderRadius={16} + borderRadius={2} containerViewRef={"[React.ref]"} contentFit="cover" contentPosition={ @@ -3335,7 +3095,7 @@ exports[`Story Snapshots: OnHoldCall should match snapshot 1`] = ` } style={ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, } @@ -3343,21 +3103,6 @@ exports[`Story Snapshots: OnHoldCall should match snapshot 1`] = ` transition={null} width={120} /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> </View> </View> <View @@ -3392,53 +3137,19 @@ exports[`Story Snapshots: OnHoldCall should match snapshot 1`] = ` Bob Burnquist </Text> </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "marginBottom": 8, - "textAlign": "center", - }, - { - "color": "#6C727A", - }, - ] - } - testID="caller-info-extension" - > - 2244 - </Text> </View> - <Text + <View style={ [ { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "textAlign": "center", + "borderTopWidth": 0.5, + "padding": 24, }, { - "color": "#2F343D", + "borderTopColor": "#EBECEF", }, ] } - > - On hold - </Text> - <View - style={ - { - "padding": 24, - } - } > <View style={ @@ -4099,7 +3810,7 @@ exports[`Story Snapshots: SpeakerOn should match snapshot 1`] = ` style={ [ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, }, @@ -4109,7 +3820,7 @@ exports[`Story Snapshots: SpeakerOn should match snapshot 1`] = ` testID="avatar" > <ViewManagerAdapter_ExpoImage - borderRadius={16} + borderRadius={2} containerViewRef={"[React.ref]"} contentFit="cover" contentPosition={ @@ -4136,7 +3847,7 @@ exports[`Story Snapshots: SpeakerOn should match snapshot 1`] = ` } style={ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, } @@ -4144,21 +3855,6 @@ exports[`Story Snapshots: SpeakerOn should match snapshot 1`] = ` transition={null} width={120} /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> </View> </View> <View @@ -4193,47 +3889,18 @@ exports[`Story Snapshots: SpeakerOn should match snapshot 1`] = ` Bob Burnquist </Text> </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "marginBottom": 8, - "textAlign": "center", - }, - { - "color": "#6C727A", - }, - ] - } - testID="caller-info-extension" - > - 2244 - </Text> </View> - <Text - style={ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "textAlign": "center", - } - } - > -   - </Text> <View style={ - { - "padding": 24, - } + [ + { + "borderTopWidth": 0.5, + "padding": 24, + }, + { + "borderTopColor": "#EBECEF", + }, + ] } > <View diff --git a/app/views/CallView/components/CallButtons.tsx b/app/views/CallView/components/CallButtons.tsx new file mode 100644 index 00000000000..5eddb2b786d --- /dev/null +++ b/app/views/CallView/components/CallButtons.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { View } from 'react-native'; + +import I18n from '../../../i18n'; +import { useCallStore } from '../../../lib/services/voip/useCallStore'; +import CallActionButton from './CallActionButton'; +import { styles } from '../styles'; +import { useTheme } from '../../../theme'; + +export const CallButtons = () => { + '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 ( + <View style={[styles.buttonsContainer, { borderTopColor: colors.strokeExtraLight }]}> + <View style={styles.buttonsRow}> + <CallActionButton + icon={isSpeakerOn ? 'audio' : 'audio-disabled'} + label={I18n.t('Speaker')} + onPress={toggleSpeaker} + variant={isSpeakerOn ? 'active' : 'default'} + testID='call-view-speaker' + /> + <CallActionButton + icon={'pause-shape-unfilled'} + label={isOnHold ? I18n.t('Unhold') : I18n.t('Hold')} + onPress={toggleHold} + variant={isOnHold ? 'active' : 'default'} + disabled={isConnecting} + testID='call-view-hold' + /> + <CallActionButton + icon={isMuted ? 'microphone-disabled' : 'microphone'} + label={isMuted ? I18n.t('Unmute') : I18n.t('Mute')} + onPress={toggleMute} + variant={isMuted ? 'active' : 'default'} + disabled={isConnecting} + testID='call-view-mute' + /> + </View> + + <View style={styles.buttonsRow}> + <CallActionButton icon='message' label={I18n.t('Message')} onPress={handleMessage} testID='call-view-message' /> + <CallActionButton + icon='phone-end' + label={isConnecting ? I18n.t('Cancel') : I18n.t('End')} + onPress={handleEndCall} + variant='danger' + testID='call-view-end' + /> + <CallActionButton icon='kebab' label={I18n.t('More')} onPress={handleMore} testID='call-view-more' /> + </View> + </View> + ); +}; 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 ( - <> - <Text style={[styles.statusText, { color: colors.fontDefault }]}> - {I18n.t('On_hold')}, <Text style={{ color: colors.statusFontWarning }}>{I18n.t('Muted')}</Text> - </Text> - </> - ); - } - if (isOnHold) { - return <Text style={[styles.statusText, { color: colors.fontDefault }]}>{I18n.t('On_hold')}</Text>; - } - if (isMuted) { - return <Text style={[styles.statusText, { color: colors.statusFontWarning }]}>{I18n.t('Muted')}</Text>; - } - - return <Text style={styles.statusText}> </Text>; -}; - -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 = () => <CallerInfo />; -export const WithOnlineStatus = () => <CallerInfo />; - -export const WithMutedIndicator = () => <CallerInfo isMuted />; - -export const NoExtension = () => { - setStoreState({ displayName: 'Alice Attali', username: 'alice.attali' }); - return <CallerInfo />; -}; - export const UsernameOnly = () => { setStoreState({ username: 'john.doe' }); return <CallerInfo />; 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( - <Wrapper> - <CallerInfo /> - </Wrapper> - ); - - // The status container exists but Status component is commented out - // Verify the component renders correctly - expect(getByTestId('caller-info')).toBeTruthy(); - expect(getByTestId('avatar')).toBeTruthy(); - }); - - it('should show muted indicator when isMuted is true', () => { - setStoreState({ displayName: 'Test User' }); - const { getByTestId } = render( - <Wrapper> - <CallerInfo isMuted /> - </Wrapper> - ); - - expect(getByTestId('caller-info-muted')).toBeTruthy(); - }); - - it('should not show muted indicator when isMuted is false', () => { - setStoreState({ displayName: 'Test User' }); - const { queryByTestId } = render( - <Wrapper> - <CallerInfo isMuted={false} /> - </Wrapper> - ); - - expect(queryByTestId('caller-info-muted')).toBeNull(); - }); - - it('should not show extension when not provided', () => { - setStoreState({ displayName: 'Test User' }); - const { queryByTestId } = render( - <Wrapper> - <CallerInfo /> - </Wrapper> - ); - - 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 ( <View style={styles.callerInfoContainer} testID='caller-info'> <View style={styles.avatarContainer}> - <AvatarContainer text={avatarText} size={120} borderRadius={16}> - <View style={[sharedStyles.status, { backgroundColor: colors.surfaceHover }]}> - {/* <Status size={20} id={contact.username} /> */} - </View> - </AvatarContainer> + <AvatarContainer text={avatarText} size={120} borderRadius={2} /> </View> <View style={styles.callerRow}> <Text style={[styles.caller, { color: colors.fontDefault }]} numberOfLines={1} testID='caller-info-name'> {name} </Text> - {isMuted && ( - <CustomIcon - name='microphone-disabled' - size={20} - color={colors.fontDanger} - style={styles.mutedIndicator} - testID='caller-info-muted' - /> - )} </View> - {extension ? ( - <Text style={[styles.callerExtension, { color: colors.fontSecondaryInfo }]} testID='caller-info-extension'> - {extension} - </Text> - ) : null} </View> ); }; 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" > <ViewManagerAdapter_ExpoImage - borderRadius={16} + borderRadius={2} containerViewRef={"[React.ref]"} contentFit="cover" contentPosition={ @@ -72,7 +72,7 @@ exports[`Story Snapshots: Default should match snapshot 1`] = ` } style={ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, } @@ -80,21 +80,6 @@ exports[`Story Snapshots: Default should match snapshot 1`] = ` transition={null} width={120} /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> </View> </View> <View @@ -129,160 +114,6 @@ exports[`Story Snapshots: Default should match snapshot 1`] = ` Bob Burnquist </Text> </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "marginBottom": 8, - "textAlign": "center", - }, - { - "color": "#6C727A", - }, - ] - } - testID="caller-info-extension" - > - 2244 - </Text> - </View> -</View> -`; - -exports[`Story Snapshots: NoExtension should match snapshot 1`] = ` -<View - style={ - { - "flex": 1, - "minHeight": 300, - "padding": 24, - } - } -> - <View - style={ - { - "alignItems": "center", - "flex": 1, - "justifyContent": "center", - "paddingHorizontal": 24, - } - } - testID="caller-info" - > - <View - style={ - { - "marginBottom": 16, - "position": "relative", - } - } - > - <View - accessibilityLabel="alice.attali's avatar" - accessible={true} - style={ - [ - { - "borderRadius": 16, - "height": 120, - "width": 120, - }, - undefined, - ] - } - testID="avatar" - > - <ViewManagerAdapter_ExpoImage - borderRadius={16} - containerViewRef={"[React.ref]"} - contentFit="cover" - contentPosition={ - { - "left": "50%", - "top": "50%", - } - } - height={120} - nativeViewRef={"[React.ref]"} - onError={[Function]} - onLoad={[Function]} - onLoadStart={[Function]} - onProgress={[Function]} - placeholder={[]} - priority="high" - source={ - [ - { - "headers": undefined, - "uri": "https://open.rocket.chat/avatar/alice.attali?format=png&size=240", - }, - ] - } - style={ - { - "borderRadius": 16, - "height": 120, - "width": 120, - } - } - transition={null} - width={120} - /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> - </View> - </View> - <View - style={ - { - "alignItems": "center", - "flexDirection": "row", - "justifyContent": "center", - } - } - > - <Text - numberOfLines={1} - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 24, - "fontWeight": "700", - "lineHeight": 32, - "marginBottom": 4, - "textAlign": "center", - }, - { - "color": "#2F343D", - }, - ] - } - testID="caller-info-name" - > - Alice Attali - </Text> - </View> </View> </View> `; @@ -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" > <ViewManagerAdapter_ExpoImage - borderRadius={16} + borderRadius={2} containerViewRef={"[React.ref]"} contentFit="cover" contentPosition={ @@ -359,7 +190,7 @@ exports[`Story Snapshots: UsernameOnly should match snapshot 1`] = ` } style={ { - "borderRadius": 16, + "borderRadius": 2, "height": 120, "width": 120, } @@ -367,21 +198,6 @@ exports[`Story Snapshots: UsernameOnly should match snapshot 1`] = ` transition={null} width={120} /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> </View> </View> <View @@ -419,340 +235,3 @@ exports[`Story Snapshots: UsernameOnly should match snapshot 1`] = ` </View> </View> `; - -exports[`Story Snapshots: WithMutedIndicator should match snapshot 1`] = ` -<View - style={ - { - "flex": 1, - "minHeight": 300, - "padding": 24, - } - } -> - <View - style={ - { - "alignItems": "center", - "flex": 1, - "justifyContent": "center", - "paddingHorizontal": 24, - } - } - testID="caller-info" - > - <View - style={ - { - "marginBottom": 16, - "position": "relative", - } - } - > - <View - accessibilityLabel="bob.burnquist's avatar" - accessible={true} - style={ - [ - { - "borderRadius": 16, - "height": 120, - "width": 120, - }, - undefined, - ] - } - testID="avatar" - > - <ViewManagerAdapter_ExpoImage - borderRadius={16} - containerViewRef={"[React.ref]"} - contentFit="cover" - contentPosition={ - { - "left": "50%", - "top": "50%", - } - } - height={120} - nativeViewRef={"[React.ref]"} - onError={[Function]} - onLoad={[Function]} - onLoadStart={[Function]} - onProgress={[Function]} - placeholder={[]} - priority="high" - source={ - [ - { - "headers": undefined, - "uri": "https://open.rocket.chat/avatar/bob.burnquist?format=png&size=240", - }, - ] - } - style={ - { - "borderRadius": 16, - "height": 120, - "width": 120, - } - } - transition={null} - width={120} - /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> - </View> - </View> - <View - style={ - { - "alignItems": "center", - "flexDirection": "row", - "justifyContent": "center", - } - } - > - <Text - numberOfLines={1} - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 24, - "fontWeight": "700", - "lineHeight": 32, - "marginBottom": 4, - "textAlign": "center", - }, - { - "color": "#2F343D", - }, - ] - } - testID="caller-info-name" - > - Bob Burnquist - </Text> - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#D40C26", - "fontSize": 20, - }, - [ - { - "lineHeight": 20, - }, - { - "marginLeft": 8, - }, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - testID="caller-info-muted" - > -  - </Text> - </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "marginBottom": 8, - "textAlign": "center", - }, - { - "color": "#6C727A", - }, - ] - } - testID="caller-info-extension" - > - 2244 - </Text> - </View> -</View> -`; - -exports[`Story Snapshots: WithOnlineStatus should match snapshot 1`] = ` -<View - style={ - { - "flex": 1, - "minHeight": 300, - "padding": 24, - } - } -> - <View - style={ - { - "alignItems": "center", - "flex": 1, - "justifyContent": "center", - "paddingHorizontal": 24, - } - } - testID="caller-info" - > - <View - style={ - { - "marginBottom": 16, - "position": "relative", - } - } - > - <View - accessibilityLabel="bob.burnquist's avatar" - accessible={true} - style={ - [ - { - "borderRadius": 16, - "height": 120, - "width": 120, - }, - undefined, - ] - } - testID="avatar" - > - <ViewManagerAdapter_ExpoImage - borderRadius={16} - containerViewRef={"[React.ref]"} - contentFit="cover" - contentPosition={ - { - "left": "50%", - "top": "50%", - } - } - height={120} - nativeViewRef={"[React.ref]"} - onError={[Function]} - onLoad={[Function]} - onLoadStart={[Function]} - onProgress={[Function]} - placeholder={[]} - priority="high" - source={ - [ - { - "headers": undefined, - "uri": "https://open.rocket.chat/avatar/bob.burnquist?format=png&size=240", - }, - ] - } - style={ - { - "borderRadius": 16, - "height": 120, - "width": 120, - } - } - transition={null} - width={120} - /> - <View - style={ - [ - { - "borderRadius": 10, - "bottom": -2, - "position": "absolute", - "right": -2, - }, - { - "backgroundColor": "#F2F3F5", - }, - ] - } - /> - </View> - </View> - <View - style={ - { - "alignItems": "center", - "flexDirection": "row", - "justifyContent": "center", - } - } - > - <Text - numberOfLines={1} - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 24, - "fontWeight": "700", - "lineHeight": 32, - "marginBottom": 4, - "textAlign": "center", - }, - { - "color": "#2F343D", - }, - ] - } - testID="caller-info-name" - > - Bob Burnquist - </Text> - </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 18, - "fontWeight": "400", - "lineHeight": 26, - "marginBottom": 8, - "textAlign": "center", - }, - { - "color": "#6C727A", - }, - ] - } - testID="caller-info-extension" - > - 2244 - </Text> - </View> -</View> -`; 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( <Wrapper> @@ -295,18 +295,19 @@ describe('CallView', () => { </Wrapper> ); - 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( <Wrapper> <CallView /> </Wrapper> ); - 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 ( <View style={[styles.contentContainer, { backgroundColor: colors.surfaceLight }]}> - {/* Caller Info */} - <CallerInfo isMuted={isMuted && isConnected} /> - - {/* Status Text */} - {isConnected && <CallStatusText />} - - {/* Action Buttons */} - <View style={styles.buttonsContainer}> - {/* First row of buttons */} - <View style={styles.buttonsRow}> - <CallActionButton - icon={isSpeakerOn ? 'audio' : 'audio-disabled'} - label={I18n.t('Speaker')} - onPress={toggleSpeaker} - variant={isSpeakerOn ? 'active' : 'default'} - testID='call-view-speaker' - /> - <CallActionButton - icon={'pause-shape-unfilled'} - label={isOnHold ? I18n.t('Unhold') : I18n.t('Hold')} - onPress={toggleHold} - variant={isOnHold ? 'active' : 'default'} - disabled={isConnecting} - testID='call-view-hold' - /> - <CallActionButton - icon={isMuted ? 'microphone-disabled' : 'microphone'} - label={isMuted ? I18n.t('Unmute') : I18n.t('Mute')} - onPress={toggleMute} - variant={isMuted ? 'active' : 'default'} - disabled={isConnecting} - testID='call-view-mute' - /> - </View> - - {/* Second row of buttons */} - <View style={styles.buttonsRow}> - <CallActionButton icon='message' label={I18n.t('Message')} onPress={handleMessage} testID='call-view-message' /> - <CallActionButton - icon='phone-end' - label={isConnecting ? I18n.t('Cancel') : I18n.t('End')} - onPress={handleEndCall} - variant='danger' - testID='call-view-end' - /> - <CallActionButton icon='kebab' label={I18n.t('More')} onPress={handleMore} testID='call-view-more' /> - </View> - </View> + <CallerInfo /> + <CallButtons /> </View> ); }; 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',