[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:
Christopher Poile 2022-03-09 08:49:15 -05:00 committed by GitHub
parent 16aaaafa8c
commit 0e08d3823f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 150 additions and 48 deletions

View file

@ -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

View file

@ -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);
});
});
});
});

View file

@ -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}

View file

@ -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),
};

View file

@ -22,4 +22,5 @@ export default keyMirror({
RECEIVED_RAISE_HAND_CALL: null,
RECEIVED_UNRAISE_HAND_CALL: null,
SET_SCREENSHARE_URL: null,
SET_SPEAKERPHONE: null,
});

View file

@ -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();

View file

@ -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,
};
}

View file

@ -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,
});

View file

@ -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;
}

View file

@ -9,6 +9,7 @@ export type CallsState = {
enabled: Dictionary<boolean>;
joined: string;
screenShareURL: string;
speakerphoneOn: boolean;
}
export type Call = {

1
package-lock.json generated
View file

@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
"name": "mattermost-mobile",
"version": "1.49.1",
"hasInstallScript": true,
"license": "Apache 2.0",