forked from Ivasoft/mattermost-mobile
MM-47763 - Calls: "who is speaking" state (#6721)
* move voiceOn to the currentCall state * simplify * prefer no inline fns
This commit is contained in:
committed by
GitHub
parent
e38e547ce2
commit
7f5dc3c718
@@ -31,5 +31,4 @@ export default keyMirror({
|
||||
SEND_TO_POST_DRAFT: null,
|
||||
CRT_TOGGLED: null,
|
||||
JOIN_CALL_BAR_VISIBLE: null,
|
||||
CURRENT_CALL_BAR_VISIBLE: null,
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View, Text, TouchableOpacity, Pressable, Platform, DeviceEventEmitter} from 'react-native';
|
||||
import {View, Text, TouchableOpacity, Pressable, Platform} from 'react-native';
|
||||
import {Options} from 'react-native-navigation';
|
||||
|
||||
import {muteMyself, unmuteMyself} from '@calls/actions';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import {CurrentCall, VoiceEventData} from '@calls/types/calls';
|
||||
import {CurrentCall} from '@calls/types/calls';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Events, Screens, WebsocketEvents} from '@constants';
|
||||
import {Screens} from '@constants';
|
||||
import {CURRENT_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
@@ -90,45 +90,6 @@ const CurrentCallBar = ({
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const {formatMessage} = useIntl();
|
||||
const [speaker, setSpeaker] = useState<string | null>(null);
|
||||
const [talkingMessage, setTalkingMessage] = useState('');
|
||||
|
||||
const isCurrentCall = Boolean(currentCall);
|
||||
const handleVoiceOn = (data: VoiceEventData) => {
|
||||
if (data.channelId === currentCall?.channelId) {
|
||||
setSpeaker(data.userId);
|
||||
}
|
||||
};
|
||||
const handleVoiceOff = (data: VoiceEventData) => {
|
||||
if (data.channelId === currentCall?.channelId && ((speaker === data.userId) || !speaker)) {
|
||||
setSpeaker(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onVoiceOn = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_ON, handleVoiceOn);
|
||||
const onVoiceOff = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_OFF, handleVoiceOff);
|
||||
DeviceEventEmitter.emit(Events.CURRENT_CALL_BAR_VISIBLE, isCurrentCall);
|
||||
return () => {
|
||||
DeviceEventEmitter.emit(Events.CURRENT_CALL_BAR_VISIBLE, Boolean(false));
|
||||
onVoiceOn.remove();
|
||||
onVoiceOff.remove();
|
||||
};
|
||||
}, [isCurrentCall]);
|
||||
|
||||
useEffect(() => {
|
||||
if (speaker) {
|
||||
setTalkingMessage(formatMessage({
|
||||
id: 'mobile.calls_name_is_talking',
|
||||
defaultMessage: '{name} is talking',
|
||||
}, {name: displayUsername(userModelsDict[speaker], teammateNameDisplay)}));
|
||||
} else {
|
||||
setTalkingMessage(formatMessage({
|
||||
id: 'mobile.calls_noone_talking',
|
||||
defaultMessage: 'No one is talking',
|
||||
}));
|
||||
}
|
||||
}, [speaker, setTalkingMessage]);
|
||||
|
||||
const goToCallScreen = useCallback(async () => {
|
||||
const options: Options = {
|
||||
@@ -150,6 +111,21 @@ const CurrentCallBar = ({
|
||||
|
||||
const myParticipant = currentCall?.participants[currentCall.myUserId];
|
||||
|
||||
// Since we can only see one user talking, it doesn't really matter who we show here (e.g., we can't
|
||||
// tell who is speaking louder).
|
||||
const talkingUsers = Object.keys(currentCall?.voiceOn || {});
|
||||
const speaker = talkingUsers.length > 0 ? talkingUsers[0] : '';
|
||||
let talkingMessage = formatMessage({
|
||||
id: 'mobile.calls_noone_talking',
|
||||
defaultMessage: 'No one is talking',
|
||||
});
|
||||
if (speaker) {
|
||||
talkingMessage = formatMessage({
|
||||
id: 'mobile.calls_name_is_talking',
|
||||
defaultMessage: '{name} is talking',
|
||||
}, {name: displayUsername(userModelsDict[speaker], teammateNameDisplay)});
|
||||
}
|
||||
|
||||
const muteUnmute = () => {
|
||||
if (myParticipant?.muted) {
|
||||
unmuteMyself();
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
setChannelEnabled,
|
||||
setRaisedHand,
|
||||
setUserMuted,
|
||||
setUserVoiceOn,
|
||||
userJoinedCall,
|
||||
userLeftCall,
|
||||
} from '@calls/state';
|
||||
@@ -38,17 +39,11 @@ export const handleCallUserUnmuted = (serverUrl: string, msg: WebSocketMessage)
|
||||
};
|
||||
|
||||
export const handleCallUserVoiceOn = (msg: WebSocketMessage) => {
|
||||
DeviceEventEmitter.emit(WebsocketEvents.CALLS_USER_VOICE_ON, {
|
||||
channelId: msg.broadcast.channel_id,
|
||||
userId: msg.data.userID,
|
||||
});
|
||||
setUserVoiceOn(msg.broadcast.channel_id, msg.data.userID, true);
|
||||
};
|
||||
|
||||
export const handleCallUserVoiceOff = (msg: WebSocketMessage) => {
|
||||
DeviceEventEmitter.emit(WebsocketEvents.CALLS_USER_VOICE_OFF, {
|
||||
channelId: msg.broadcast.channel_id,
|
||||
userId: msg.data.userID,
|
||||
});
|
||||
setUserVoiceOn(msg.broadcast.channel_id, msg.data.userID, false);
|
||||
};
|
||||
|
||||
export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage) => {
|
||||
|
||||
@@ -30,7 +30,7 @@ import CallAvatar from '@calls/components/call_avatar';
|
||||
import CallDuration from '@calls/components/call_duration';
|
||||
import RaisedHandIcon from '@calls/icons/raised_hand_icon';
|
||||
import UnraisedHandIcon from '@calls/icons/unraised_hand_icon';
|
||||
import {CallParticipant, CurrentCall, VoiceEventData} from '@calls/types/calls';
|
||||
import {CallParticipant, CurrentCall} from '@calls/types/calls';
|
||||
import {sortParticipants} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
@@ -260,7 +260,6 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
||||
const insets = useSafeAreaInsets();
|
||||
const {width, height} = useWindowDimensions();
|
||||
const [showControlsInLandscape, setShowControlsInLandscape] = useState(false);
|
||||
const [speakers, setSpeakers] = useState<Dictionary<boolean>>({});
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const isLandscape = width > height;
|
||||
@@ -279,30 +278,6 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVoiceOn = (data: VoiceEventData) => {
|
||||
if (data.channelId === currentCall?.channelId) {
|
||||
setSpeakers((prev) => ({...prev, [data.userId]: true}));
|
||||
}
|
||||
};
|
||||
const handleVoiceOff = (data: VoiceEventData) => {
|
||||
if (data.channelId === currentCall?.channelId && speakers.hasOwnProperty(data.userId)) {
|
||||
setSpeakers((prev) => {
|
||||
const next = {...prev};
|
||||
delete next[data.userId];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onVoiceOn = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_ON, handleVoiceOn);
|
||||
const onVoiceOff = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_USER_VOICE_OFF, handleVoiceOff);
|
||||
return () => {
|
||||
onVoiceOn.remove();
|
||||
onVoiceOff.remove();
|
||||
};
|
||||
}, [speakers, currentCall?.channelId]);
|
||||
|
||||
const leaveCallHandler = useCallback(() => {
|
||||
popTopScreen();
|
||||
leaveCall();
|
||||
@@ -325,6 +300,10 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
||||
}
|
||||
}, [myParticipant?.raisedHand]);
|
||||
|
||||
const toggleSpeakerPhone = useCallback(() => {
|
||||
setSpeakerphoneOn(!currentCall?.speakerphoneOn);
|
||||
}, [currentCall?.speakerphoneOn]);
|
||||
|
||||
const toggleControlsInLandscape = useCallback(() => {
|
||||
setShowControlsInLandscape(!showControlsInLandscape);
|
||||
}, [showControlsInLandscape]);
|
||||
@@ -424,8 +403,8 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
||||
usersList = (
|
||||
<ScrollView
|
||||
alwaysBounceVertical={false}
|
||||
horizontal={currentCall?.screenOn !== ''}
|
||||
contentContainerStyle={[isLandscape && currentCall?.screenOn && style.usersScrollLandscapeScreenOn]}
|
||||
horizontal={currentCall.screenOn !== ''}
|
||||
contentContainerStyle={[isLandscape && currentCall.screenOn && style.usersScrollLandscapeScreenOn]}
|
||||
>
|
||||
<Pressable
|
||||
testID='users-list'
|
||||
@@ -435,12 +414,12 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
||||
{participants.map((user) => {
|
||||
return (
|
||||
<View
|
||||
style={[style.user, currentCall?.screenOn && style.userScreenOn]}
|
||||
style={[style.user, currentCall.screenOn && style.userScreenOn]}
|
||||
key={user.id}
|
||||
>
|
||||
<CallAvatar
|
||||
userModel={user.userModel}
|
||||
volume={speakers[user.id] ? 1 : 0}
|
||||
volume={currentCall.voiceOn[user.id] ? 1 : 0}
|
||||
muted={user.muted}
|
||||
sharingScreen={user.id === currentCall.screenOn}
|
||||
raisedHand={Boolean(user.raisedHand)}
|
||||
@@ -544,12 +523,12 @@ const CallScreen = ({componentId, currentCall, participantsDict, teammateNameDis
|
||||
<Pressable
|
||||
testID={'toggle-speakerphone'}
|
||||
style={style.button}
|
||||
onPress={() => setSpeakerphoneOn(!currentCall?.speakerphoneOn)}
|
||||
onPress={toggleSpeakerPhone}
|
||||
>
|
||||
<CompassIcon
|
||||
name={'volume-high'}
|
||||
size={24}
|
||||
style={[style.buttonIcon, style.speakerphoneIcon, currentCall?.speakerphoneOn && style.speakerphoneIconOn]}
|
||||
style={[style.buttonIcon, style.speakerphoneIcon, currentCall.speakerphoneOn && style.speakerphoneIconOn]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_speaker'}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
setSpeakerPhone,
|
||||
setConfig,
|
||||
setPluginEnabled,
|
||||
setUserVoiceOn,
|
||||
} from '@calls/state/actions';
|
||||
import {License} from '@constants';
|
||||
|
||||
@@ -113,6 +114,7 @@ describe('useCallsState', () => {
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
};
|
||||
const testNewCall1 = {
|
||||
...call1,
|
||||
@@ -179,13 +181,14 @@ describe('useCallsState', () => {
|
||||
const initialChannelsWithCallsState = {
|
||||
'channel-1': true,
|
||||
};
|
||||
const initialCurrentCallState = {
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
} as CurrentCall;
|
||||
voiceOn: {},
|
||||
};
|
||||
const expectedCallsState = {
|
||||
'channel-1': {
|
||||
participants: {
|
||||
@@ -238,13 +241,14 @@ describe('useCallsState', () => {
|
||||
const initialChannelsWithCallsState = {
|
||||
'channel-1': true,
|
||||
};
|
||||
const initialCurrentCallState = {
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
} as CurrentCall;
|
||||
voiceOn: {},
|
||||
};
|
||||
const expectedCallsState = {
|
||||
'channel-1': {
|
||||
participants: {
|
||||
@@ -340,13 +344,14 @@ describe('useCallsState', () => {
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialChannelsWithCallsState = {'channel-1': true, 'channel-2': true};
|
||||
const initialCurrentCallState = {
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
} as CurrentCall;
|
||||
voiceOn: {},
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
@@ -387,13 +392,14 @@ describe('useCallsState', () => {
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialChannelsWithCallsState = {'channel-1': true, 'channel-2': true};
|
||||
const initialCurrentCallState = {
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
} as CurrentCall;
|
||||
voiceOn: {},
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
@@ -445,13 +451,14 @@ describe('useCallsState', () => {
|
||||
ownerId: 'user-1',
|
||||
},
|
||||
};
|
||||
const initialCurrentCallState = {
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
} as CurrentCall;
|
||||
voiceOn: {},
|
||||
};
|
||||
const expectedCurrentCallState = {
|
||||
...initialCurrentCallState,
|
||||
...expectedCalls['channel-1'],
|
||||
@@ -503,13 +510,14 @@ describe('useCallsState', () => {
|
||||
'channel-1': newCall1,
|
||||
},
|
||||
};
|
||||
const expectedCurrentCallState = {
|
||||
const expectedCurrentCallState: CurrentCall = {
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
...newCall1,
|
||||
} as CurrentCall;
|
||||
voiceOn: {},
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
@@ -649,6 +657,50 @@ describe('useCallsState', () => {
|
||||
assert.deepEqual(result.current[1], null);
|
||||
});
|
||||
|
||||
it('voiceOn and Off', () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useCurrentCall()];
|
||||
});
|
||||
act(() => {
|
||||
setCallsState('server1', initialCallsState);
|
||||
setCurrentCall(initialCurrentCallState);
|
||||
});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], initialCurrentCallState);
|
||||
|
||||
// test
|
||||
act(() => setUserVoiceOn('channel-1', 'user-1', true));
|
||||
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {'user-1': true}});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
act(() => setUserVoiceOn('channel-1', 'user-2', true));
|
||||
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {'user-1': true, 'user-2': true}});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
act(() => setUserVoiceOn('channel-1', 'user-1', false));
|
||||
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {'user-2': true}});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
|
||||
// test that voice state is cleared on reconnect
|
||||
act(() => setCalls('server1', 'myUserId', initialCallsState.calls, {}));
|
||||
assert.deepEqual(result.current[1], initialCurrentCallState);
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
});
|
||||
|
||||
it('config', () => {
|
||||
const newConfig = {
|
||||
ICEServers: [],
|
||||
|
||||
@@ -29,9 +29,12 @@ export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<
|
||||
return;
|
||||
}
|
||||
|
||||
// Edge case: if the app went into the background and lost the main ws connection, we don't know who is currently
|
||||
// talking. Instead of guessing, erase voiceOn state (same state as when joining an ongoing call).
|
||||
const nextCall = {
|
||||
...currentCall,
|
||||
...calls[currentCall.channelId],
|
||||
voiceOn: {},
|
||||
};
|
||||
setCurrentCall(nextCall);
|
||||
};
|
||||
@@ -92,9 +95,13 @@ export const userJoinedCall = (serverUrl: string, channelId: string, userId: str
|
||||
// Did the user join the current call? If so, update that too.
|
||||
const currentCall = getCurrentCall();
|
||||
if (currentCall && currentCall.channelId === channelId) {
|
||||
const voiceOn = {...currentCall.voiceOn};
|
||||
delete voiceOn[userId];
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
participants: {...currentCall.participants, [userId]: nextCall.participants[userId]},
|
||||
voiceOn,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
}
|
||||
@@ -108,6 +115,7 @@ export const userJoinedCall = (serverUrl: string, channelId: string, userId: str
|
||||
myUserId: userId,
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -149,9 +157,14 @@ export const userLeftCall = (serverUrl: string, channelId: string, userId: strin
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear them from the voice list
|
||||
const voiceOn = {...currentCall.voiceOn};
|
||||
delete voiceOn[userId];
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
participants: {...currentCall.participants},
|
||||
voiceOn,
|
||||
};
|
||||
delete nextCurrentCall.participants[userId];
|
||||
setCurrentCall(nextCurrentCall);
|
||||
@@ -220,6 +233,26 @@ export const setUserMuted = (serverUrl: string, channelId: string, userId: strin
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const setUserVoiceOn = (channelId: string, userId: string, voiceOn: boolean) => {
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall || currentCall.channelId !== channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextVoiceOn = {...currentCall.voiceOn};
|
||||
if (voiceOn) {
|
||||
nextVoiceOn[userId] = true;
|
||||
} else {
|
||||
delete nextVoiceOn[userId];
|
||||
}
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
voiceOn: nextVoiceOn,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const setRaisedHand = (serverUrl: string, channelId: string, userId: string, timestamp: number) => {
|
||||
const callsState = getCallsState(serverUrl);
|
||||
if (!callsState.calls[channelId] || !callsState.calls[channelId].participants[userId]) {
|
||||
|
||||
@@ -45,6 +45,7 @@ export type CurrentCall = {
|
||||
threadId: string;
|
||||
screenShareURL: string;
|
||||
speakerphoneOn: boolean;
|
||||
voiceOn: Dictionary<boolean>;
|
||||
}
|
||||
|
||||
export type CallParticipant = {
|
||||
|
||||
Reference in New Issue
Block a user