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",