From 0e08d3823f57de56c7d5383408c93cac12cc4f3c Mon Sep 17 00:00:00 2001 From: Christopher Poile Date: Wed, 9 Mar 2022 08:49:15 -0500 Subject: [PATCH] [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 --- .../__snapshots__/call_screen.test.js.snap | 36 +++++++---- .../calls/screens/call/call_screen.test.js | 44 ++++++++++++- .../calls/screens/call/call_screen.tsx | 64 +++++++++++-------- app/products/calls/screens/call/index.ts | 6 +- .../calls/store/action_types/calls.ts | 1 + .../calls/store/actions/calls.test.js | 3 + app/products/calls/store/actions/calls.ts | 27 ++++++-- app/products/calls/store/reducers/calls.ts | 11 ++++ app/products/calls/store/selectors/calls.ts | 4 ++ app/products/calls/store/types/calls.ts | 1 + package-lock.json | 1 + 11 files changed, 150 insertions(+), 48 deletions(-) diff --git a/app/products/calls/screens/call/__snapshots__/call_screen.test.js.snap b/app/products/calls/screens/call/__snapshots__/call_screen.test.js.snap index 08ceff413..00bd86bba 100644 --- a/app/products/calls/screens/call/__snapshots__/call_screen.test.js.snap +++ b/app/products/calls/screens/call/__snapshots__/call_screen.test.js.snap @@ -243,6 +243,7 @@ exports[`CallScreen Landscape should match snapshot 1`] = ` - Settings + Speaker - Settings + Speaker - Settings + Speaker - Settings + Speaker - Settings + Speaker - Settings + Speaker { }); 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(); 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(); 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(); + + 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(); + + wrapper.find({testID: 'toggle-speakerphone'}).simulate('press'); + expect(props.actions.setSpeakerphoneOn).toHaveBeenCalledWith(false); + }); }); }); }); diff --git a/app/products/calls/screens/call/call_screen.tsx b/app/products/calls/screens/call/call_screen.tsx index c8ee7eeb4..157016039 100644 --- a/app/products/calls/screens/call/call_screen.tsx +++ b/app/products/calls/screens/call/call_screen.tsx @@ -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(null); + const [speaker, setSpeaker] = useState(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'} /> - {displayUsername(props.call?.participants[user.id].profile, props.teammateNameDisplay)} + + {displayUsername(props.call?.participants[user.id].profile, props.teammateNameDisplay)} + ); })} @@ -371,13 +393,9 @@ const CallScreen = (props: Props) => { style={style.muteIcon} /> {props.currentParticipant?.muted && - {'Unmute'}} + {'Unmute'}} {!props.currentParticipant?.muted && - {'Mute'}} + {'Mute'}} } { size={24} style={{...style.buttonIcon, ...style.hangUpIcon}} /> - {'Leave'} + {'Leave'} { size={24} style={style.buttonIcon} /> - {'Chat thread'} + {'Chat thread'} - {'Settings'} + {'Speaker'} { let store; const {newClient} = require('@mmproducts/calls/connection'); + InCallManager.setSpeakerphoneOn = jest.fn(); beforeEach(async () => { newClient.mockClear(); diff --git a/app/products/calls/store/actions/calls.ts b/app/products/calls/store/actions/calls.ts index b3736a1b0..e608b43ff 100644 --- a/app/products/calls/store/actions/calls.ts +++ b/app/products/calls/store/actions/calls.ts @@ -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, + }; +} diff --git a/app/products/calls/store/reducers/calls.ts b/app/products/calls/store/reducers/calls.ts index db72040c5..2f83603b3 100644 --- a/app/products/calls/store/reducers/calls.ts +++ b/app/products/calls/store/reducers/calls.ts @@ -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, }); diff --git a/app/products/calls/store/selectors/calls.ts b/app/products/calls/store/selectors/calls.ts index f58505511..09d3312b2 100644 --- a/app/products/calls/store/selectors/calls.ts +++ b/app/products/calls/store/selectors/calls.ts @@ -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; +} diff --git a/app/products/calls/store/types/calls.ts b/app/products/calls/store/types/calls.ts index 04bbeb152..540d42392 100644 --- a/app/products/calls/store/types/calls.ts +++ b/app/products/calls/store/types/calls.ts @@ -9,6 +9,7 @@ export type CallsState = { enabled: Dictionary; joined: string; screenShareURL: string; + speakerphoneOn: boolean; } export type Call = { diff --git a/package-lock.json b/package-lock.json index 9a89c04ca..3bb5affdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "mattermost-mobile", "version": "1.49.1", "hasInstallScript": true, "license": "Apache 2.0",