forked from Ivasoft/mattermost-mobile
MM-48072 - Calls: Implement recording permissions and UI for participants (#6784)
* implement recording permissions and UI * fix bug when refusing recording while in call thread from call screen * merge conflicts
This commit is contained in:
committed by
GitHub
parent
95af1afe6b
commit
6660c134f5
@@ -11,6 +11,8 @@ import {
|
||||
handleCallChannelDisabled,
|
||||
handleCallChannelEnabled,
|
||||
handleCallEnded,
|
||||
handleCallHostChanged,
|
||||
handleCallRecordingState,
|
||||
handleCallScreenOff,
|
||||
handleCallScreenOn,
|
||||
handleCallStarted,
|
||||
@@ -397,6 +399,12 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
case WebsocketEvents.CALLS_USER_REACTED:
|
||||
handleCallUserReacted(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_RECORDING_STATE:
|
||||
handleCallRecordingState(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_HOST_CHANGED:
|
||||
handleCallHostChanged(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.GROUP_RECEIVED:
|
||||
handleGroupReceivedEvent(serverUrl, msg);
|
||||
|
||||
@@ -67,6 +67,8 @@ const WebsocketEvents = {
|
||||
CALLS_USER_RAISE_HAND: `custom_${Calls.PluginId}_user_raise_hand`,
|
||||
CALLS_USER_UNRAISE_HAND: `custom_${Calls.PluginId}_user_unraise_hand`,
|
||||
CALLS_USER_REACTED: `custom_${Calls.PluginId}_user_reacted`,
|
||||
CALLS_RECORDING_STATE: `custom_${Calls.PluginId}_call_recording_state`,
|
||||
CALLS_HOST_CHANGED: `custom_${Calls.PluginId}_call_host_changed`,
|
||||
GROUP_RECEIVED: 'received_group',
|
||||
GROUP_MEMBER_ADD: 'group_member_add',
|
||||
GROUP_MEMBER_DELETE: 'group_member_delete',
|
||||
|
||||
@@ -89,6 +89,7 @@ const addFakeCall = (serverUrl: string, channelId: string) => {
|
||||
screenOn: '',
|
||||
threadId: 'abcd1234567',
|
||||
ownerId: 'xohi8cki9787fgiryne716u84o',
|
||||
hostId: 'xohi8cki9787fgiryne716u84o',
|
||||
} as Call;
|
||||
act(() => {
|
||||
State.setCallsState(serverUrl, {serverUrl, myUserId: 'myUserId', calls: {}, enabled: {}});
|
||||
|
||||
@@ -163,6 +163,8 @@ const createCallAndAddToIds = (channelId: string, call: ServerCallState, ids: Se
|
||||
screenOn: call.screen_sharing_id,
|
||||
threadId: call.thread_id,
|
||||
ownerId: call.owner_id,
|
||||
hostId: call.host_id,
|
||||
recState: call.recording,
|
||||
} as Call;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,16 +2,23 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Alert} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import {hasMicrophonePermission, joinCall, unmuteMyself} from '@calls/actions';
|
||||
import {setMicPermissionsGranted} from '@calls/state';
|
||||
import {hasMicrophonePermission, joinCall, leaveCall, unmuteMyself} from '@calls/actions';
|
||||
import {setMicPermissionsGranted, setRecAcknowledged} from '@calls/state';
|
||||
import {errorAlert} from '@calls/utils';
|
||||
import {Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModals, dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
// Only allow one recording alert per call.
|
||||
let recordingAlertLock = false;
|
||||
|
||||
export const showLimitRestrictedAlert = (maxParticipants: number, intl: IntlShape) => {
|
||||
const title = intl.formatMessage({
|
||||
id: 'mobile.calls_participant_limit_title',
|
||||
@@ -107,6 +114,7 @@ const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolea
|
||||
return;
|
||||
}
|
||||
|
||||
recordingAlertLock = false;
|
||||
const hasPermission = await hasMicrophonePermission();
|
||||
setMicPermissionsGranted(hasPermission);
|
||||
|
||||
@@ -125,3 +133,51 @@ const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolea
|
||||
setTimeout(() => unmuteMyself(), 1000);
|
||||
}
|
||||
};
|
||||
|
||||
export const recordingAlert = (intl: IntlShape) => {
|
||||
if (recordingAlertLock) {
|
||||
return;
|
||||
}
|
||||
recordingAlertLock = true;
|
||||
|
||||
const {formatMessage} = intl;
|
||||
|
||||
const participantMessage = formatMessage({
|
||||
id: 'mobile.calls_participant_rec',
|
||||
defaultMessage: 'The host has started recording this meeting. By staying in the meeting you give consent to being recorded.',
|
||||
});
|
||||
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.calls_participant_rec_title',
|
||||
defaultMessage: 'Recording is in progress',
|
||||
}),
|
||||
participantMessage,
|
||||
[
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.calls_leave',
|
||||
defaultMessage: 'Leave',
|
||||
}),
|
||||
onPress: async () => {
|
||||
leaveCall();
|
||||
|
||||
// Need to pop the call screen, if it's somewhere in the stack.
|
||||
await dismissAllModals();
|
||||
if (NavigationStore.getNavigationComponents().includes(Screens.CALL)) {
|
||||
await dismissAllModalsAndPopToScreen(Screens.CALL, 'Call');
|
||||
Navigation.pop(Screens.CALL).catch(() => null);
|
||||
}
|
||||
},
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.calls_okay',
|
||||
defaultMessage: 'Okay',
|
||||
}),
|
||||
onPress: () => setRecAcknowledged(),
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
88
app/products/calls/components/calls_badge.tsx
Normal file
88
app/products/calls/components/calls_badge.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 4,
|
||||
},
|
||||
recording: {
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
backgroundColor: '#D24B4E',
|
||||
color: 'white',
|
||||
height: 34,
|
||||
},
|
||||
text: {
|
||||
color: 'white',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
recordingText: {
|
||||
marginLeft: 6,
|
||||
...typography('Body', 75, 'SemiBold'),
|
||||
},
|
||||
participant: {
|
||||
marginTop: 6,
|
||||
paddingLeft: 2,
|
||||
paddingRight: 6,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.16)',
|
||||
},
|
||||
participantText: {
|
||||
...typography('Body', 25, 'SemiBold'),
|
||||
},
|
||||
});
|
||||
|
||||
export enum CallsBadgeType {
|
||||
Rec,
|
||||
Host,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
type: CallsBadgeType;
|
||||
}
|
||||
|
||||
const CallsBadge = ({type}: Props) => {
|
||||
const isRec = type === CallsBadgeType.Rec;
|
||||
|
||||
const text = isRec ? (
|
||||
<FormattedText
|
||||
id={'mobile.calls_rec'}
|
||||
defaultMessage={'rec'}
|
||||
style={[styles.text, styles.recordingText]}
|
||||
/>
|
||||
) : (
|
||||
<FormattedText
|
||||
id={'mobile.calls_host'}
|
||||
defaultMessage={'host'}
|
||||
style={[styles.text, styles.recordingText]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, isRec ? styles.recording : styles.participant]}>
|
||||
{
|
||||
isRec &&
|
||||
<CompassIcon
|
||||
name={'record-circle-outline'}
|
||||
size={12}
|
||||
color={styles.text.color}
|
||||
/>
|
||||
}
|
||||
{text}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallsBadge;
|
||||
@@ -7,6 +7,7 @@ import {View, Text, TouchableOpacity, Pressable, Platform} from 'react-native';
|
||||
import {Options} from 'react-native-navigation';
|
||||
|
||||
import {muteMyself, unmuteMyself} from '@calls/actions';
|
||||
import {recordingAlert} from '@calls/alerts';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import PermissionErrorBar from '@calls/components/permission_error_bar';
|
||||
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
@@ -95,7 +96,8 @@ const CurrentCallBar = ({
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
const {formatMessage} = useIntl();
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
usePermissionsChecker(micPermissionsGranted);
|
||||
|
||||
const goToCallScreen = useCallback(async () => {
|
||||
@@ -143,6 +145,14 @@ const CurrentCallBar = ({
|
||||
|
||||
const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed;
|
||||
|
||||
// The user should receive an alert if all of the following conditions apply:
|
||||
// - Recording has started.
|
||||
// - Recording has not ended.
|
||||
// - The alert has not been dismissed already.
|
||||
if (currentCall?.recState?.start_at && !currentCall?.recState?.end_at && !currentCall?.recAcknowledged) {
|
||||
recordingAlert(intl);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={style.wrapper}>
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
setCallScreenOff,
|
||||
setCallScreenOn,
|
||||
setChannelEnabled,
|
||||
setHost,
|
||||
setRaisedHand,
|
||||
setRecordingState,
|
||||
setUserMuted,
|
||||
setUserVoiceOn,
|
||||
userJoinedCall,
|
||||
@@ -60,6 +62,7 @@ export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage) => {
|
||||
screenOn: '',
|
||||
participants: {},
|
||||
ownerId: msg.data.owner_id,
|
||||
hostId: msg.data.host_id,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -98,3 +101,11 @@ export const handleCallUserUnraiseHand = (serverUrl: string, msg: WebSocketMessa
|
||||
export const handleCallUserReacted = (serverUrl: string, msg: WebSocketMessage) => {
|
||||
userReacted(serverUrl, msg.broadcast.channel_id, msg.data);
|
||||
};
|
||||
|
||||
export const handleCallRecordingState = (serverUrl: string, msg: WebSocketMessage) => {
|
||||
setRecordingState(serverUrl, msg.broadcast.channel_id, msg.data.recState);
|
||||
};
|
||||
|
||||
export const handleCallHostChanged = (serverUrl: string, msg: WebSocketMessage) => {
|
||||
setHost(serverUrl, msg.broadcast.channel_id, msg.data.hostID);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useCallback, useState} from 'react';
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
DeviceEventEmitter,
|
||||
Keyboard,
|
||||
Platform,
|
||||
Pressable,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
Text,
|
||||
useWindowDimensions,
|
||||
DeviceEventEmitter, Keyboard,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
@@ -19,8 +20,10 @@ import {RTCView} from 'react-native-webrtc';
|
||||
|
||||
import {appEntry} from '@actions/remote/entry';
|
||||
import {leaveCall, muteMyself, setSpeakerphoneOn, unmuteMyself} from '@calls/actions';
|
||||
import {recordingAlert} from '@calls/alerts';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import CallDuration from '@calls/components/call_duration';
|
||||
import CallsBadge, {CallsBadgeType} from '@calls/components/calls_badge';
|
||||
import EmojiList from '@calls/components/emoji_list';
|
||||
import PermissionErrorBar from '@calls/components/permission_error_bar';
|
||||
import ReactionBar from '@calls/components/reaction_bar';
|
||||
@@ -31,7 +34,7 @@ import {sortParticipants} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
|
||||
import {WebsocketEvents, Screens} from '@constants';
|
||||
import {Screens, WebsocketEvents} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {
|
||||
@@ -78,6 +81,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
paddingTop: 10,
|
||||
paddingLeft: 14,
|
||||
@@ -114,7 +118,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
usersScrollLandscapeScreenOn: {
|
||||
position: 'absolute',
|
||||
@@ -365,6 +369,15 @@ const CallScreen = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
// The user should receive an alert if all of the following conditions apply:
|
||||
// - Recording has started.
|
||||
// - Recording has not ended.
|
||||
// - The alert has not been dismissed already.
|
||||
const recording = Boolean(currentCall.recState?.start_at && !currentCall.recState?.end_at);
|
||||
if (recording && !currentCall.recAcknowledged) {
|
||||
recordingAlert(intl);
|
||||
}
|
||||
|
||||
let screenShareView = null;
|
||||
if (currentCall.screenShareURL && currentCall.screenOn) {
|
||||
screenShareView = (
|
||||
@@ -423,6 +436,7 @@ const CallScreen = ({
|
||||
` ${intl.formatMessage({id: 'mobile.calls_you', defaultMessage: '(you)'})}`
|
||||
}
|
||||
</Text>
|
||||
{user.id === currentCall.hostId && <CallsBadge type={CallsBadgeType.Host}/>}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
@@ -450,6 +464,7 @@ const CallScreen = ({
|
||||
<View
|
||||
style={[style.header, isLandscape && style.headerLandscape, !showControls && style.headerLandscapeNoControls]}
|
||||
>
|
||||
{recording && <CallsBadge type={CallsBadgeType.Rec}/>}
|
||||
<CallDuration
|
||||
style={style.time}
|
||||
value={currentCall.startTime}
|
||||
|
||||
@@ -10,8 +10,11 @@ import {
|
||||
setCallsState,
|
||||
setChannelsWithCalls,
|
||||
setCurrentCall,
|
||||
setHost,
|
||||
setMicPermissionsErrorDismissed,
|
||||
setMicPermissionsGranted,
|
||||
setRecAcknowledged,
|
||||
setRecordingState,
|
||||
useCallsConfig,
|
||||
useCallsState,
|
||||
useChannelsWithCalls,
|
||||
@@ -47,6 +50,7 @@ import {
|
||||
DefaultCurrentCall,
|
||||
DefaultGlobalCallsState,
|
||||
GlobalCallsState,
|
||||
RecordingState,
|
||||
} from '../types/calls';
|
||||
|
||||
const call1: Call = {
|
||||
@@ -59,6 +63,7 @@ const call1: Call = {
|
||||
screenOn: '',
|
||||
threadId: 'thread-1',
|
||||
ownerId: 'user-1',
|
||||
hostId: 'user-1',
|
||||
};
|
||||
const call2: Call = {
|
||||
participants: {
|
||||
@@ -70,6 +75,7 @@ const call2: Call = {
|
||||
screenOn: '',
|
||||
threadId: 'thread-2',
|
||||
ownerId: 'user-3',
|
||||
hostId: 'user-3',
|
||||
};
|
||||
const call3: Call = {
|
||||
participants: {
|
||||
@@ -81,6 +87,7 @@ const call3: Call = {
|
||||
screenOn: '',
|
||||
threadId: 'thread-3',
|
||||
ownerId: 'user-5',
|
||||
hostId: 'user-5',
|
||||
};
|
||||
|
||||
describe('useCallsState', () => {
|
||||
@@ -212,6 +219,7 @@ describe('useCallsState', () => {
|
||||
screenOn: '',
|
||||
threadId: 'thread-1',
|
||||
ownerId: 'user-1',
|
||||
hostId: 'user-1',
|
||||
},
|
||||
};
|
||||
const expectedChannelsWithCallsState = initialChannelsWithCallsState;
|
||||
@@ -269,6 +277,7 @@ describe('useCallsState', () => {
|
||||
screenOn: '',
|
||||
threadId: 'thread-1',
|
||||
ownerId: 'user-1',
|
||||
hostId: 'user-1',
|
||||
},
|
||||
};
|
||||
const expectedChannelsWithCallsState = initialChannelsWithCallsState;
|
||||
@@ -456,6 +465,7 @@ describe('useCallsState', () => {
|
||||
screenOn: false,
|
||||
threadId: 'thread-1',
|
||||
ownerId: 'user-1',
|
||||
hostId: 'user-1',
|
||||
},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
@@ -888,4 +898,149 @@ describe('useCallsState', () => {
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], expectedCurrentCallState);
|
||||
});
|
||||
|
||||
it('setRecordingState', () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
};
|
||||
const recState: RecordingState = {
|
||||
init_at: 123,
|
||||
start_at: 231,
|
||||
end_at: 345,
|
||||
};
|
||||
const expectedCallsState: CallsState = {
|
||||
...initialCallsState,
|
||||
calls: {
|
||||
...initialCallsState.calls,
|
||||
'channel-1': {
|
||||
...call1,
|
||||
recState,
|
||||
},
|
||||
},
|
||||
};
|
||||
const expectedCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
recState,
|
||||
};
|
||||
|
||||
// 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(() => setRecordingState('server1', 'channel-1', recState));
|
||||
assert.deepEqual((result.current[0] as CallsState), expectedCallsState);
|
||||
assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState);
|
||||
act(() => setRecordingState('server1', 'channel-2', recState));
|
||||
assert.deepEqual((result.current[0] as CallsState).calls['channel-2'], {...call2, recState});
|
||||
assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState);
|
||||
});
|
||||
|
||||
it('setRecAcknowledged', () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
};
|
||||
const expectedCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
recAcknowledged: true,
|
||||
};
|
||||
|
||||
// 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(() => setRecAcknowledged());
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState);
|
||||
});
|
||||
|
||||
it('setHost', () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
};
|
||||
const expectedCallsState: CallsState = {
|
||||
...initialCallsState,
|
||||
calls: {
|
||||
...initialCallsState.calls,
|
||||
'channel-1': {
|
||||
...call1,
|
||||
hostId: 'user-52',
|
||||
},
|
||||
},
|
||||
};
|
||||
const expectedCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: true,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
hostId: 'user-52',
|
||||
};
|
||||
|
||||
// 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(() => setHost('server1', 'channel-1', 'user-52'));
|
||||
assert.deepEqual((result.current[0] as CallsState), expectedCallsState);
|
||||
assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState);
|
||||
act(() => setHost('server1', 'channel-2', 'user-1923'));
|
||||
assert.deepEqual((result.current[0] as CallsState).calls['channel-2'], {...call2, hostId: 'user-1923'});
|
||||
assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,9 @@ import {
|
||||
ChannelsWithCalls,
|
||||
CurrentCall,
|
||||
DefaultCall,
|
||||
DefaultCurrentCall, ReactionStreamEmoji,
|
||||
DefaultCurrentCall,
|
||||
ReactionStreamEmoji,
|
||||
RecordingState,
|
||||
} from '@calls/types/calls';
|
||||
import {REACTION_LIMIT, REACTION_TIMEOUT} from '@constants/calls';
|
||||
|
||||
@@ -493,3 +495,62 @@ const userReactionTimeout = (serverUrl: string, channelId: string, reaction: Cal
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const setRecordingState = (serverUrl: string, channelId: string, recState: RecordingState) => {
|
||||
const callsState = getCallsState(serverUrl);
|
||||
if (!callsState.calls[channelId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCall = {...callsState.calls[channelId], recState};
|
||||
const nextCalls = {...callsState.calls, [channelId]: nextCall};
|
||||
setCallsState(serverUrl, {...callsState, calls: nextCalls});
|
||||
|
||||
// Was it the current call? If so, update that too.
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall || currentCall.channelId !== channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
recState,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const setRecAcknowledged = () => {
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCurrentCall: CurrentCall = {
|
||||
...currentCall,
|
||||
recAcknowledged: true,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const setHost = (serverUrl: string, channelId: string, hostId: string) => {
|
||||
const callsState = getCallsState(serverUrl);
|
||||
if (!callsState.calls[channelId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCall = {...callsState.calls[channelId], hostId};
|
||||
const nextCalls = {...callsState.calls, [channelId]: nextCall};
|
||||
setCallsState(serverUrl, {...callsState, calls: nextCalls});
|
||||
|
||||
// Was it the current call? If so, update that too.
|
||||
const currentCall = getCurrentCall();
|
||||
if (!currentCall || currentCall.channelId !== channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
hostId,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,8 @@ export type Call = {
|
||||
screenOn: string;
|
||||
threadId: string;
|
||||
ownerId: string;
|
||||
recState?: RecordingState;
|
||||
hostId: string;
|
||||
}
|
||||
|
||||
export const DefaultCall: Call = {
|
||||
@@ -42,6 +44,7 @@ export const DefaultCall: Call = {
|
||||
screenOn: '',
|
||||
threadId: '',
|
||||
ownerId: '',
|
||||
hostId: '',
|
||||
};
|
||||
|
||||
export type CurrentCall = Call & {
|
||||
@@ -53,23 +56,20 @@ export type CurrentCall = Call & {
|
||||
voiceOn: Dictionary<boolean>;
|
||||
micPermissionsErrorDismissed: boolean;
|
||||
reactionStream: ReactionStreamEmoji[];
|
||||
recAcknowledged: boolean;
|
||||
}
|
||||
|
||||
export const DefaultCurrentCall: CurrentCall = {
|
||||
...DefaultCall,
|
||||
connected: false,
|
||||
serverUrl: '',
|
||||
myUserId: '',
|
||||
participants: {},
|
||||
channelId: '',
|
||||
startTime: 0,
|
||||
screenOn: '',
|
||||
threadId: '',
|
||||
ownerId: '',
|
||||
screenShareURL: '',
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
micPermissionsErrorDismissed: false,
|
||||
reactionStream: [],
|
||||
recAcknowledged: false,
|
||||
};
|
||||
|
||||
export type CallParticipant = {
|
||||
@@ -101,6 +101,8 @@ export type ServerCallState = {
|
||||
thread_id: string;
|
||||
screen_sharing_id: string;
|
||||
owner_id: string;
|
||||
host_id: string;
|
||||
recording: RecordingState;
|
||||
}
|
||||
|
||||
export type CallsConnection = {
|
||||
@@ -166,3 +168,9 @@ export type ReactionStreamEmoji = {
|
||||
latestTimestamp: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type RecordingState = {
|
||||
init_at: number;
|
||||
start_at: number;
|
||||
end_at: number;
|
||||
}
|
||||
|
||||
@@ -374,6 +374,7 @@
|
||||
"mobile.calls_ended_at": "Ended at",
|
||||
"mobile.calls_error_message": "Error: {error}",
|
||||
"mobile.calls_error_title": "Error",
|
||||
"mobile.calls_host": "host",
|
||||
"mobile.calls_join_call": "Join call",
|
||||
"mobile.calls_lasted": "Lasted {duration}",
|
||||
"mobile.calls_leave": "Leave",
|
||||
@@ -391,9 +392,13 @@
|
||||
"mobile.calls_not_available_option": "(Not available)",
|
||||
"mobile.calls_not_available_title": "Calls is not enabled",
|
||||
"mobile.calls_ok": "OK",
|
||||
"mobile.calls_okay": "Okay",
|
||||
"mobile.calls_participant_limit_title": "Participant limit reached",
|
||||
"mobile.calls_participant_rec": "The host has started recording this meeting. By staying in the meeting you give consent to being recorded.",
|
||||
"mobile.calls_participant_rec_title": "Recording is in progress",
|
||||
"mobile.calls_raise_hand": "Raise hand",
|
||||
"mobile.calls_react": "React",
|
||||
"mobile.calls_rec": "rec",
|
||||
"mobile.calls_see_logs": "See server logs",
|
||||
"mobile.calls_speaker": "Speaker",
|
||||
"mobile.calls_start_call": "Start Call",
|
||||
|
||||
Reference in New Issue
Block a user