[MM-42332] Calls: Add speakerphone button (#6037)
* add speakerphone button, set speakerphone mode in redux/InCallManager * move speakerphone activation into the actions file * rename style * revert local project changes * tests
This commit is contained in:
parent
16aaaafa8c
commit
0e08d3823f
11 changed files with 150 additions and 48 deletions
|
|
@ -243,6 +243,7 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
|
|||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
|
|
@ -250,9 +251,10 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
|
|||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="toggle-speakerphone"
|
||||
>
|
||||
<CompassIcon
|
||||
name="settings-outline"
|
||||
name="volume-high"
|
||||
size={24}
|
||||
style={
|
||||
Object {
|
||||
|
|
@ -274,7 +276,7 @@ exports[`CallScreen Landscape should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
Settings
|
||||
Speaker
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
|
|
@ -553,6 +555,7 @@ exports[`CallScreen Landscape should match snapshot with screenshare 1`] = `
|
|||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
|
|
@ -560,9 +563,10 @@ exports[`CallScreen Landscape should match snapshot with screenshare 1`] = `
|
|||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="toggle-speakerphone"
|
||||
>
|
||||
<CompassIcon
|
||||
name="settings-outline"
|
||||
name="volume-high"
|
||||
size={24}
|
||||
style={
|
||||
Object {
|
||||
|
|
@ -584,7 +588,7 @@ exports[`CallScreen Landscape should match snapshot with screenshare 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
Settings
|
||||
Speaker
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
|
|
@ -938,6 +942,7 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
|
|||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
|
|
@ -945,9 +950,10 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
|
|||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="toggle-speakerphone"
|
||||
>
|
||||
<CompassIcon
|
||||
name="settings-outline"
|
||||
name="volume-high"
|
||||
size={24}
|
||||
style={
|
||||
Object {
|
||||
|
|
@ -969,7 +975,7 @@ exports[`CallScreen Portrait should match snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
Settings
|
||||
Speaker
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
|
|
@ -1320,6 +1326,7 @@ exports[`CallScreen Portrait should match snapshot with screenshare 1`] = `
|
|||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
|
|
@ -1327,9 +1334,10 @@ exports[`CallScreen Portrait should match snapshot with screenshare 1`] = `
|
|||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="toggle-speakerphone"
|
||||
>
|
||||
<CompassIcon
|
||||
name="settings-outline"
|
||||
name="volume-high"
|
||||
size={24}
|
||||
style={
|
||||
Object {
|
||||
|
|
@ -1351,7 +1359,7 @@ exports[`CallScreen Portrait should match snapshot with screenshare 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
Settings
|
||||
Speaker
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
|
|
@ -1593,6 +1601,7 @@ exports[`CallScreen should show controls in landscape view on click the screen s
|
|||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
|
|
@ -1600,9 +1609,10 @@ exports[`CallScreen should show controls in landscape view on click the screen s
|
|||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="toggle-speakerphone"
|
||||
>
|
||||
<CompassIcon
|
||||
name="settings-outline"
|
||||
name="volume-high"
|
||||
size={24}
|
||||
style={
|
||||
Object {
|
||||
|
|
@ -1624,7 +1634,7 @@ exports[`CallScreen should show controls in landscape view on click the screen s
|
|||
}
|
||||
}
|
||||
>
|
||||
Settings
|
||||
Speaker
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
|
|
@ -1949,6 +1959,7 @@ exports[`CallScreen should show controls in landscape view on click the users li
|
|||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
|
|
@ -1956,9 +1967,10 @@ exports[`CallScreen should show controls in landscape view on click the users li
|
|||
"flexDirection": "column",
|
||||
}
|
||||
}
|
||||
testID="toggle-speakerphone"
|
||||
>
|
||||
<CompassIcon
|
||||
name="settings-outline"
|
||||
name="volume-high"
|
||||
size={24}
|
||||
style={
|
||||
Object {
|
||||
|
|
@ -1980,7 +1992,7 @@ exports[`CallScreen should show controls in landscape view on click the users li
|
|||
}
|
||||
}
|
||||
>
|
||||
Settings
|
||||
Speaker
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
|
|
|
|||
|
|
@ -77,7 +77,11 @@ describe('CallScreen', () => {
|
|||
});
|
||||
|
||||
test('should show controls in landscape view on click the screen share', () => {
|
||||
const props = {...baseProps, call: {...baseProps.call, screenOn: 'user-2-id'}, screenShareURL: 'screen-share-url'};
|
||||
const props = {
|
||||
...baseProps,
|
||||
call: {...baseProps.call, screenOn: 'user-2-id'},
|
||||
screenShareURL: 'screen-share-url',
|
||||
};
|
||||
const wrapper = shallow(<CallScreen {...props}/>);
|
||||
wrapper.find({testID: 'screen-share-container'}).simulate('press');
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
|
|
@ -108,7 +112,11 @@ describe('CallScreen', () => {
|
|||
});
|
||||
|
||||
test('should match snapshot with screenshare', () => {
|
||||
const props = {...baseProps, call: {...baseProps.call, screenOn: 'user-2-id'}, screenShareURL: 'screen-share-url'};
|
||||
const props = {
|
||||
...baseProps,
|
||||
call: {...baseProps.call, screenOn: 'user-2-id'},
|
||||
screenShareURL: 'screen-share-url',
|
||||
};
|
||||
const wrapper = shallow(<CallScreen {...props}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
|
|
@ -166,6 +174,38 @@ describe('CallScreen', () => {
|
|||
expect(props.actions.muteMyself).not.toHaveBeenCalled();
|
||||
expect(props.actions.unmuteMyself).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should turn speakerphone on if it is off', () => {
|
||||
const setSpeakerphoneOn = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
actions: {
|
||||
...baseProps.actions,
|
||||
setSpeakerphoneOn,
|
||||
},
|
||||
speakerphoneOn: false,
|
||||
};
|
||||
const wrapper = shallow(<CallScreen {...props}/>);
|
||||
|
||||
wrapper.find({testID: 'toggle-speakerphone'}).simulate('press');
|
||||
expect(props.actions.setSpeakerphoneOn).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('should turn speakerphone off if it is on', () => {
|
||||
const setSpeakerphoneOn = jest.fn();
|
||||
const props = {
|
||||
...baseProps,
|
||||
actions: {
|
||||
...baseProps.actions,
|
||||
setSpeakerphoneOn,
|
||||
},
|
||||
speakerphoneOn: true,
|
||||
};
|
||||
const wrapper = shallow(<CallScreen {...props}/>);
|
||||
|
||||
wrapper.find({testID: 'toggle-speakerphone'}).simulate('press');
|
||||
expect(props.actions.setSpeakerphoneOn).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,14 +2,24 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useCallback, useState} from 'react';
|
||||
import {Keyboard, View, Text, Platform, Pressable, SafeAreaView, ScrollView, useWindowDimensions, DeviceEventEmitter} from 'react-native';
|
||||
import {
|
||||
Keyboard,
|
||||
View,
|
||||
Text,
|
||||
Platform,
|
||||
Pressable,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
useWindowDimensions,
|
||||
DeviceEventEmitter,
|
||||
} from 'react-native';
|
||||
import {RTCView} from 'react-native-webrtc';
|
||||
|
||||
import {showModalOverCurrentContext, mergeNavigationOptions, popTopScreen, goToScreen} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {THREAD} from '@constants/screen';
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
import {ActionFunc, GenericAction} from '@mm-redux/types/actions';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import CallAvatar from '@mmproducts/calls/components/call_avatar';
|
||||
import CallDuration from '@mmproducts/calls/components/call_duration';
|
||||
|
|
@ -23,13 +33,15 @@ type Props = {
|
|||
actions: {
|
||||
muteMyself: (channelId: string) => GenericAction;
|
||||
unmuteMyself: (channelId: string) => GenericAction;
|
||||
leaveCall: () => GenericAction;
|
||||
setSpeakerphoneOn: (newState: boolean) => GenericAction;
|
||||
leaveCall: () => ActionFunc;
|
||||
};
|
||||
theme: Theme;
|
||||
call: Call|null;
|
||||
call: Call | null;
|
||||
currentParticipant: CallParticipant;
|
||||
teammateNameDisplay: string;
|
||||
screenShareURL: string;
|
||||
speakerphoneOn: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((props: any) => {
|
||||
|
|
@ -153,6 +165,10 @@ const getStyleSheet = makeStyleSheetFromTheme((props: any) => {
|
|||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
},
|
||||
speakerphoneIcon: {
|
||||
color: props.speakerphoneOn ? 'black' : props.theme.sidebarText,
|
||||
backgroundColor: props.speakerphoneOn ? 'white' : 'rgba(255,255,255,0.12)',
|
||||
},
|
||||
otherButtons: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
@ -208,6 +224,7 @@ const CallScreen = (props: Props) => {
|
|||
const [showControlsInLandscape, setShowControlsInLandscape] = useState(false);
|
||||
|
||||
const style = getStyleSheet({...props, showControlsInLandscape, isLandscape});
|
||||
|
||||
useEffect(() => {
|
||||
mergeNavigationOptions('Call', {
|
||||
layout: {
|
||||
|
|
@ -219,7 +236,7 @@ const CallScreen = (props: Props) => {
|
|||
});
|
||||
}, []);
|
||||
|
||||
const [speaker, setSpeaker] = useState<UserProfile|null>(null);
|
||||
const [speaker, setSpeaker] = useState<UserProfile | null>(null);
|
||||
const handleVoiceOn = (data: VoiceEventData) => {
|
||||
if (data.channelId === props.call?.channelId) {
|
||||
setSpeaker(props.call.participants[data.userId].profile);
|
||||
|
|
@ -242,8 +259,7 @@ const CallScreen = (props: Props) => {
|
|||
|
||||
const showOtherActions = () => {
|
||||
const screen = 'CallOtherActions';
|
||||
const passProps = {
|
||||
};
|
||||
const passProps = {};
|
||||
|
||||
Keyboard.dismiss();
|
||||
const otherActionsRequest = requestAnimationFrame(() => {
|
||||
|
|
@ -277,6 +293,10 @@ const CallScreen = (props: Props) => {
|
|||
}
|
||||
}, [props.call?.channelId, props.currentParticipant]);
|
||||
|
||||
const toggleSpeakerphoneHandler = () => {
|
||||
props.actions.setSpeakerphoneOn(!props.speakerphoneOn);
|
||||
};
|
||||
|
||||
const toggleControlsInLandscape = useCallback(() => {
|
||||
setShowControlsInLandscape(!showControlsInLandscape);
|
||||
}, [showControlsInLandscape]);
|
||||
|
|
@ -328,7 +348,9 @@ const CallScreen = (props: Props) => {
|
|||
muted={user.muted}
|
||||
size={props.call?.screenOn ? 'm' : 'l'}
|
||||
/>
|
||||
<Text style={style.username}>{displayUsername(props.call?.participants[user.id].profile, props.teammateNameDisplay)}</Text>
|
||||
<Text style={style.username}>
|
||||
{displayUsername(props.call?.participants[user.id].profile, props.teammateNameDisplay)}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
|
@ -371,13 +393,9 @@ const CallScreen = (props: Props) => {
|
|||
style={style.muteIcon}
|
||||
/>
|
||||
{props.currentParticipant?.muted &&
|
||||
<Text
|
||||
style={style.buttonText}
|
||||
>{'Unmute'}</Text>}
|
||||
<Text style={style.buttonText}>{'Unmute'}</Text>}
|
||||
{!props.currentParticipant?.muted &&
|
||||
<Text
|
||||
style={style.buttonText}
|
||||
>{'Mute'}</Text>}
|
||||
<Text style={style.buttonText}>{'Mute'}</Text>}
|
||||
</Pressable>}
|
||||
<View style={style.otherButtons}>
|
||||
<Pressable
|
||||
|
|
@ -390,9 +408,7 @@ const CallScreen = (props: Props) => {
|
|||
size={24}
|
||||
style={{...style.buttonIcon, ...style.hangUpIcon}}
|
||||
/>
|
||||
<Text
|
||||
style={style.buttonText}
|
||||
>{'Leave'}</Text>
|
||||
<Text style={style.buttonText}>{'Leave'}</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={style.button}
|
||||
|
|
@ -403,21 +419,19 @@ const CallScreen = (props: Props) => {
|
|||
size={24}
|
||||
style={style.buttonIcon}
|
||||
/>
|
||||
<Text
|
||||
style={style.buttonText}
|
||||
>{'Chat thread'}</Text>
|
||||
<Text style={style.buttonText}>{'Chat thread'}</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
testID={'toggle-speakerphone'}
|
||||
style={style.button}
|
||||
onPress={toggleSpeakerphoneHandler}
|
||||
>
|
||||
<CompassIcon
|
||||
name='settings-outline'
|
||||
name={'volume-high'}
|
||||
size={24}
|
||||
style={style.buttonIcon}
|
||||
style={{...style.buttonIcon, ...style.speakerphoneIcon}}
|
||||
/>
|
||||
<Text
|
||||
style={style.buttonText}
|
||||
>{'Settings'}</Text>
|
||||
<Text style={style.buttonText}>{'Speaker'}</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={style.button}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import {bindActionCreators, Dispatch} from 'redux';
|
|||
|
||||
import {getTheme, getTeammateNameDisplaySetting} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {muteMyself, unmuteMyself, leaveCall} from '@mmproducts/calls//store/actions/calls';
|
||||
import {getCurrentCall, getScreenShareURL} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {muteMyself, unmuteMyself, leaveCall, setSpeakerphoneOn} from '@mmproducts/calls//store/actions/calls';
|
||||
import {getCurrentCall, getScreenShareURL, isSpeakerphoneOn} from '@mmproducts/calls/store/selectors/calls';
|
||||
|
||||
import CallScreen from './call_screen';
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ function mapStateToProps(state: GlobalState) {
|
|||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
currentParticipant: currentCall && currentCall.participants[currentUserId],
|
||||
screenShareURL: getScreenShareURL(state),
|
||||
speakerphoneOn: isSpeakerphoneOn(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||
actions: bindActionCreators({
|
||||
muteMyself,
|
||||
unmuteMyself,
|
||||
setSpeakerphoneOn,
|
||||
leaveCall,
|
||||
}, dispatch),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ export default keyMirror({
|
|||
RECEIVED_RAISE_HAND_CALL: null,
|
||||
RECEIVED_UNRAISE_HAND_CALL: null,
|
||||
SET_SCREENSHARE_URL: null,
|
||||
SET_SPEAKERPHONE: null,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
import assert from 'assert';
|
||||
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import configureStore from '@test/test_store';
|
||||
|
||||
|
|
@ -64,6 +66,7 @@ export function addFakeCall(channelId) {
|
|||
describe('Actions.Calls', () => {
|
||||
let store;
|
||||
const {newClient} = require('@mmproducts/calls/connection');
|
||||
InCallManager.setSpeakerphoneOn = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
newClient.mockClear();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {logError} from '@mm-redux/actions/errors';
|
||||
import {forceLogoutIfNecessary} from '@mm-redux/actions/helpers';
|
||||
|
|
@ -102,6 +104,7 @@ export function joinCall(channelId: string): ActionFunc {
|
|||
ws.disconnect();
|
||||
ws = null;
|
||||
}
|
||||
dispatch(setSpeakerphoneOn(false));
|
||||
|
||||
try {
|
||||
ws = await newClient(channelId, () => null, setScreenShareURL);
|
||||
|
|
@ -126,13 +129,15 @@ export function joinCall(channelId: string): ActionFunc {
|
|||
};
|
||||
}
|
||||
|
||||
export function leaveCall(): GenericAction {
|
||||
if (ws) {
|
||||
ws.disconnect();
|
||||
ws = null;
|
||||
}
|
||||
return {
|
||||
type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL,
|
||||
export function leaveCall(): ActionFunc {
|
||||
return async (dispatch: DispatchFunc) => {
|
||||
if (ws) {
|
||||
ws.disconnect();
|
||||
ws = null;
|
||||
}
|
||||
dispatch(setSpeakerphoneOn(false));
|
||||
dispatch({type: CallsTypes.RECEIVED_MYSELF_LEFT_CALL});
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -149,3 +154,11 @@ export function unmuteMyself(): GenericAction {
|
|||
}
|
||||
return {type: 'empty'};
|
||||
}
|
||||
|
||||
export function setSpeakerphoneOn(newState: boolean): GenericAction {
|
||||
InCallManager.setSpeakerphoneOn(newState);
|
||||
return {
|
||||
type: CallsTypes.SET_SPEAKERPHONE,
|
||||
data: newState,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,9 +168,20 @@ function screenShareURL(state = '', action: GenericAction) {
|
|||
}
|
||||
}
|
||||
|
||||
function speakerphoneOn(state = false, action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case CallsTypes.SET_SPEAKERPHONE: {
|
||||
return action.data;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
calls,
|
||||
enabled,
|
||||
joined,
|
||||
screenShareURL,
|
||||
speakerphoneOn,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,3 +23,7 @@ export function isCallsEnabled(state: GlobalState) {
|
|||
export function getScreenShareURL(state: GlobalState) {
|
||||
return state.entities.calls.screenShareURL;
|
||||
}
|
||||
|
||||
export function isSpeakerphoneOn(state: GlobalState) {
|
||||
return state.entities.calls.speakerphoneOn;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export type CallsState = {
|
|||
enabled: Dictionary<boolean>;
|
||||
joined: string;
|
||||
screenShareURL: string;
|
||||
speakerphoneOn: boolean;
|
||||
}
|
||||
|
||||
export type Call = {
|
||||
|
|
|
|||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -5,6 +5,7 @@
|
|||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mattermost-mobile",
|
||||
"version": "1.49.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache 2.0",
|
||||
|
|
|
|||
Loading…
Reference in a new issue