MM-48611 - Calls: Calls recording host controls and UI (#6787)

* implement recording permissions and UI

* fix bug when refusing recording while in call thread from call screen

* host controls and UI for recording

* button tweak, and fixing a i18n string issue

* PR comment

* backwards compat & don't show rec controls if not enabled
This commit is contained in:
Christopher Poile 2022-11-30 11:20:00 -05:00 committed by GitHub
parent 6660c134f5
commit 0f8798ca55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 284 additions and 123 deletions

View file

@ -5,6 +5,7 @@ import InCallManager from 'react-native-incall-manager';
import {forceLogoutIfNecessary} from '@actions/remote/session';
import {fetchUsersByIds} from '@actions/remote/user';
import {needsRecordingWillBePostedAlert} from '@calls/alerts';
import {
getCallsConfig,
getCallsState,
@ -27,6 +28,7 @@ import {getChannelById} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getConfig, getLicense} from '@queries/servers/system';
import {getCurrentUser, getUserById} from '@queries/servers/user';
import {logWarning} from '@utils/log';
import {displayUsername, getUserIdFromChannelName, isSystemAdmin} from '@utils/user';
import {newConnection} from '../connection/connection';
@ -37,6 +39,7 @@ import type {
CallParticipant,
CallReactionEmoji,
CallsConnection,
RecordingState,
ServerCallState,
ServerChannelState,
} from '@calls/types/calls';
@ -381,3 +384,35 @@ export const endCall = async (serverUrl: string, channelId: string) => {
return data;
};
export const startCallRecording = async (serverUrl: string, callId: string) => {
const client = NetworkManager.getClient(serverUrl);
let data: ApiResp | RecordingState;
try {
data = await client.startCallRecording(callId);
} catch (error) {
await forceLogoutIfNecessary(serverUrl, error as ClientError);
logWarning('start call recording returned:', error);
return error;
}
return data;
};
export const stopCallRecording = async (serverUrl: string, callId: string) => {
needsRecordingWillBePostedAlert();
const client = NetworkManager.getClient(serverUrl);
let data: ApiResp | RecordingState;
try {
data = await client.stopCallRecording(callId);
} catch (error) {
await forceLogoutIfNecessary(serverUrl, error as ClientError);
logWarning('stop call recording returned:', error);
return error;
}
return data;
};

View file

@ -5,7 +5,7 @@ import {Alert} from 'react-native';
import {Navigation} from 'react-native-navigation';
import {hasMicrophonePermission, joinCall, leaveCall, unmuteMyself} from '@calls/actions';
import {setMicPermissionsGranted, setRecAcknowledged} from '@calls/state';
import {setMicPermissionsGranted} from '@calls/state';
import {errorAlert} from '@calls/utils';
import {Screens} from '@constants';
import DatabaseManager from '@database/manager';
@ -19,6 +19,9 @@ import type {IntlShape} from 'react-intl';
// Only allow one recording alert per call.
let recordingAlertLock = false;
// Only unlock if/when the user starts a recording.
let recordingWillBePostedLock = true;
export const showLimitRestrictedAlert = (maxParticipants: number, intl: IntlShape) => {
const title = intl.formatMessage({
id: 'mobile.calls_participant_limit_title',
@ -81,6 +84,7 @@ export const leaveAndJoinWithAlert = (
id: 'mobile.post.cancel',
defaultMessage: 'Cancel',
}),
style: 'destructive',
},
{
text: formatMessage({
@ -115,6 +119,7 @@ const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolea
}
recordingAlertLock = false;
recordingWillBePostedLock = true; // only unlock if/when the user stops a recording.
const hasPermission = await hasMicrophonePermission();
setMicPermissionsGranted(hasPermission);
@ -134,7 +139,7 @@ const doJoinCall = async (serverUrl: string, channelId: string, isDMorGM: boolea
}
};
export const recordingAlert = (intl: IntlShape) => {
export const recordingAlert = (isHost: boolean, intl: IntlShape) => {
if (recordingAlertLock) {
return;
}
@ -142,42 +147,89 @@ export const recordingAlert = (intl: IntlShape) => {
const {formatMessage} = intl;
const participantTitle = formatMessage({
id: 'mobile.calls_participant_rec_title',
defaultMessage: 'Recording is in progress',
});
const hostTitle = formatMessage({
id: 'mobile.calls_host_rec_title',
defaultMessage: 'You are recording',
});
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.',
});
const hostMessage = formatMessage({
id: 'mobile.calls_host_rec',
defaultMessage: 'You are recording this meeting. Consider letting everyone know that this meeting is being recorded.',
});
const participantButtons = [
{
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: 'destructive',
},
{
text: formatMessage({
id: 'mobile.calls_okay',
defaultMessage: 'Okay',
}),
style: 'default',
},
];
const hostButton = [{
text: formatMessage({
id: 'mobile.calls_dismiss',
defaultMessage: 'Dismiss',
}),
}];
Alert.alert(
isHost ? hostTitle : participantTitle,
isHost ? hostMessage : participantMessage,
isHost ? hostButton : participantButtons,
);
};
export const needsRecordingWillBePostedAlert = () => {
recordingWillBePostedLock = false;
};
export const recordingWillBePostedAlert = (intl: IntlShape) => {
if (recordingWillBePostedLock) {
return;
}
recordingWillBePostedLock = true;
const {formatMessage} = intl;
Alert.alert(
formatMessage({
id: 'mobile.calls_participant_rec_title',
defaultMessage: 'Recording is in progress',
id: 'mobile.calls_host_rec_stopped_title',
defaultMessage: 'Recording has stopped. Processing...',
}),
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(),
},
],
formatMessage({
id: 'mobile.calls_host_rec_stopped',
defaultMessage: 'You can find the recording in this call\'s chat thread once it\'s finished processing.',
}),
[{
text: formatMessage({
id: 'mobile.calls_dismiss',
defaultMessage: 'Dismiss',
}),
}],
);
};

View file

@ -1,7 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ServerChannelState, ServerCallsConfig, ApiResp, ICEServersConfigs} from '@calls/types/calls';
import type {
ServerChannelState,
ServerCallsConfig,
ApiResp,
ICEServersConfigs,
RecordingState,
} from '@calls/types/calls';
export interface ClientCallsMix {
getEnabled: () => Promise<Boolean>;
@ -11,6 +17,8 @@ export interface ClientCallsMix {
enableChannelCalls: (channelId: string, enable: boolean) => Promise<ServerChannelState>;
endCall: (channelId: string) => Promise<ApiResp>;
genTURNCredentials: () => Promise<ICEServersConfigs>;
startCallRecording: (callId: string) => Promise<ApiResp | RecordingState>;
stopCallRecording: (callId: string) => Promise<ApiResp | RecordingState>;
}
const ClientCalls = (superclass: any) => class extends superclass {
@ -67,6 +75,20 @@ const ClientCalls = (superclass: any) => class extends superclass {
{method: 'get'},
);
};
startCallRecording = async (callID: string) => {
return this.doFetch(
`${this.getCallsRoute()}/calls/${callID}/recording/start`,
{method: 'post'},
);
};
stopCallRecording = async (callID: string) => {
return this.doFetch(
`${this.getCallsRoute()}/calls/${callID}/recording/stop`,
{method: 'post'},
);
};
};
export default ClientCalls;

View file

@ -6,6 +6,7 @@ import {StyleSheet, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {typography} from '@utils/typography';
const styles = StyleSheet.create({
@ -16,12 +17,19 @@ const styles = StyleSheet.create({
justifyContent: 'center',
borderRadius: 4,
},
loading: {
paddingLeft: 8,
paddingRight: 8,
color: 'white',
height: 34,
backgroundColor: 'rgba(255, 255, 255, 0.16)',
},
recording: {
paddingLeft: 8,
paddingRight: 8,
backgroundColor: '#D24B4E',
color: 'white',
height: 34,
backgroundColor: '#D24B4E',
},
text: {
color: 'white',
@ -45,6 +53,7 @@ const styles = StyleSheet.create({
});
export enum CallsBadgeType {
Waiting,
Rec,
Host,
}
@ -54,9 +63,11 @@ interface Props {
}
const CallsBadge = ({type}: Props) => {
const isLoading = type === CallsBadgeType.Waiting;
const isRec = type === CallsBadgeType.Rec;
const isParticipant = !(isLoading || isRec);
const text = isRec ? (
const text = isLoading || isRec ? (
<FormattedText
id={'mobile.calls_rec'}
defaultMessage={'rec'}
@ -70,8 +81,17 @@ const CallsBadge = ({type}: Props) => {
/>
);
const containerStyles = [
styles.container,
isLoading && styles.loading,
isRec && styles.recording,
isParticipant && styles.participant,
];
return (
<View style={[styles.container, isRec ? styles.recording : styles.participant]}>
<View style={containerStyles}>
{
isLoading && <Loading/>
}
{
isRec &&
<CompassIcon

View file

@ -7,7 +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 {recordingAlert, recordingWillBePostedAlert} 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';
@ -146,11 +146,16 @@ 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);
// - Recording has started and recording has not ended.
const isHost = Boolean(currentCall?.hostId === myParticipant?.id);
if (currentCall?.recState?.start_at && !currentCall?.recState?.end_at) {
recordingAlert(isHost, intl);
}
// The user should receive a recording finished alert if all of the following conditions apply:
// - Is the host, recording has started, and recording has ended
if (isHost && currentCall?.recState?.start_at && currentCall.recState.end_at) {
recordingWillBePostedAlert(intl);
}
return (

View file

@ -20,7 +20,8 @@ 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 {startCallRecording, stopCallRecording} from '@calls/actions/calls';
import {recordingAlert, recordingWillBePostedAlert} 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';
@ -29,12 +30,14 @@ import PermissionErrorBar from '@calls/components/permission_error_bar';
import ReactionBar from '@calls/components/reaction_bar';
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
import {usePermissionsChecker} from '@calls/hooks';
import {useCallsConfig} from '@calls/state';
import {CallParticipant, CurrentCall} from '@calls/types/calls';
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 {Screens, WebsocketEvents} from '@constants';
import {Preferences, Screens, WebsocketEvents} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import {
@ -226,7 +229,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
overflow: 'hidden',
},
hangUpIcon: {
backgroundColor: '#D24B4E',
backgroundColor: Preferences.THEMES.denim.dndIndicator,
},
screenShareImage: {
flex: 7,
@ -241,6 +244,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
unavailableText: {
color: changeOpacity(theme.sidebarText, 0.32),
},
denimDND: {
color: Preferences.THEMES.denim.dndIndicator,
},
}));
const CallScreen = ({
@ -255,6 +261,8 @@ const CallScreen = ({
const theme = useTheme();
const insets = useSafeAreaInsets();
const {width, height} = useWindowDimensions();
const serverUrl = useServerUrl();
const {EnableRecordings} = useCallsConfig(serverUrl);
usePermissionsChecker(micPermissionsGranted);
const [showControlsInLandscape, setShowControlsInLandscape] = useState(false);
const [showReactions, setShowReactions] = useState(false);
@ -264,7 +272,11 @@ const CallScreen = ({
const showControls = !isLandscape || showControlsInLandscape;
const myParticipant = currentCall?.participants[currentCall.myUserId];
const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed;
const chatThreadTitle = intl.formatMessage({id: 'mobile.calls_chat_thread', defaultMessage: 'Chat thread'});
const callThreadOptionTitle = intl.formatMessage({id: 'mobile.calls_call_thread', defaultMessage: 'Call Thread'});
const recordOptionTitle = intl.formatMessage({id: 'mobile.calls_record', defaultMessage: 'Record'});
const stopRecordingOptionTitle = intl.formatMessage({id: 'mobile.calls_stop_recording', defaultMessage: 'Stop Recording'});
const openChannelOptionTitle = intl.formatMessage({id: 'mobile.calls_call_thread', defaultMessage: 'Open Channel'});
useEffect(() => {
mergeNavigationOptions('Call', {
@ -302,6 +314,26 @@ const CallScreen = ({
setShowControlsInLandscape(!showControlsInLandscape);
}, [showControlsInLandscape]);
const startRecording = useCallback(async () => {
Keyboard.dismiss();
await dismissBottomSheet();
if (!currentCall) {
return;
}
await startCallRecording(currentCall.serverUrl, currentCall.channelId);
}, [currentCall?.channelId, currentCall?.serverUrl]);
const stopRecording = useCallback(async () => {
Keyboard.dismiss();
await dismissBottomSheet();
if (!currentCall) {
return;
}
await stopCallRecording(currentCall.serverUrl, currentCall.channelId);
}, [currentCall?.channelId, currentCall?.serverUrl]);
const switchToThread = useCallback(async () => {
Keyboard.dismiss();
await dismissBottomSheet();
@ -311,7 +343,7 @@ const CallScreen = ({
const activeUrl = await DatabaseManager.getActiveServerUrl();
if (activeUrl === currentCall.serverUrl) {
await dismissAllModalsAndPopToScreen(Screens.THREAD, chatThreadTitle, {rootId: currentCall.threadId});
await dismissAllModalsAndPopToScreen(Screens.THREAD, callThreadOptionTitle, {rootId: currentCall.threadId});
return;
}
@ -323,30 +355,69 @@ const CallScreen = ({
}
await DatabaseManager.setActiveServerDatabase(currentCall.serverUrl);
await appEntry(currentCall.serverUrl, Date.now());
await goToScreen(Screens.THREAD, chatThreadTitle, {rootId: currentCall.threadId});
}, [currentCall?.serverUrl, currentCall?.threadId, fromThreadScreen, componentId, chatThreadTitle]);
await goToScreen(Screens.THREAD, callThreadOptionTitle, {rootId: currentCall.threadId});
}, [currentCall?.serverUrl, currentCall?.threadId, fromThreadScreen, componentId, callThreadOptionTitle]);
const showOtherActions = useCallback(() => {
// The user should receive a recording alert if all of the following conditions apply:
// - Recording has started, recording has not ended
const isHost = Boolean(currentCall?.hostId === myParticipant?.id);
const recording = Boolean(currentCall?.recState?.start_at && !currentCall.recState.end_at);
if (recording) {
recordingAlert(isHost, intl);
}
// The user should receive a recording finished alert if all of the following conditions apply:
// - Is the host, recording has started, and recording has ended
if (isHost && currentCall?.recState?.start_at && currentCall.recState.end_at) {
recordingWillBePostedAlert(intl);
}
// The user should see the loading only if:
// - Recording has been initialized, recording has not been started, and recording has not ended
const waitingForRecording = Boolean(currentCall?.recState?.init_at && !currentCall.recState.start_at && !currentCall.recState.end_at && isHost);
const showOtherActions = useCallback(async () => {
const renderContent = () => {
return (
<View style={style.bottomSheet}>
{
isHost && EnableRecordings && !(waitingForRecording || recording) &&
<SlideUpPanelItem
icon={'record-circle-outline'}
onPress={startRecording}
text={recordOptionTitle}
/>
}
{
isHost && EnableRecordings && (waitingForRecording || recording) &&
<SlideUpPanelItem
icon={'record-square-outline'}
imageStyles={style.denimDND}
onPress={stopRecording}
text={stopRecordingOptionTitle}
textStyles={style.denimDND}
/>
}
<SlideUpPanelItem
icon='message-text-outline'
onPress={switchToThread}
text={chatThreadTitle}
text={callThreadOptionTitle}
/>
</View>
);
};
bottomSheet({
const items = isHost && EnableRecordings ? 3 : 2;
await bottomSheet({
closeButtonId: 'close-other-actions',
renderContent,
snapPoints: [bottomSheetSnapPoint(2, ITEM_HEIGHT, insets.bottom), 10],
snapPoints: [bottomSheetSnapPoint(items, ITEM_HEIGHT, insets.bottom), 10],
title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
theme,
});
}, [insets, intl, theme]);
}, [insets, intl, theme, isHost, EnableRecordings, waitingForRecording, recording, startRecording,
recordOptionTitle, stopRecording, stopRecordingOptionTitle, style, switchToThread, callThreadOptionTitle,
openChannelOptionTitle]);
useEffect(() => {
const listener = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_CALL_END, ({channelId}) => {
@ -369,15 +440,6 @@ 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 = (
@ -464,6 +526,7 @@ const CallScreen = ({
<View
style={[style.header, isLandscape && style.headerLandscape, !showControls && style.headerLandscapeNoControls]}
>
{waitingForRecording && <CallsBadge type={CallsBadgeType.Waiting}/>}
{recording && <CallsBadge type={CallsBadgeType.Rec}/>}
<CallDuration
style={style.time}

View file

@ -13,13 +13,13 @@ import {
setHost,
setMicPermissionsErrorDismissed,
setMicPermissionsGranted,
setRecAcknowledged,
setRecordingState,
useCallsConfig,
useCallsState,
useChannelsWithCalls,
useCurrentCall,
useGlobalCallsState, userReacted,
useGlobalCallsState,
userReacted,
} from '@calls/state';
import {
setCalls,
@ -811,6 +811,7 @@ describe('useCallsState', () => {
last_retrieved_at: 123,
sku_short_name: License.SKU_SHORT_NAME.Professional,
MaxCallParticipants: 8,
EnableRecordings: true,
};
// setup
@ -955,44 +956,6 @@ describe('useCallsState', () => {
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,

View file

@ -212,11 +212,9 @@ export const callStarted = (serverUrl: string, call: Call) => {
return;
}
const nextCurrentCall = {
const nextCurrentCall: CurrentCall = {
...currentCall,
startTime: call.startTime,
threadId: call.threadId,
ownerId: call.ownerId,
...call,
};
setCurrentCall(nextCurrentCall);
};
@ -519,19 +517,6 @@ export const setRecordingState = (serverUrl: string, channelId: string, 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]) {

View file

@ -124,6 +124,7 @@ export type ServerCallsConfig = {
NeedsTURNCredentials: boolean;
sku_short_name: string;
MaxCallParticipants: number;
EnableRecordings: boolean;
}
export type CallsConfig = ServerCallsConfig & {
@ -141,6 +142,7 @@ export const DefaultCallsConfig: CallsConfig = {
last_retrieved_at: 0,
sku_short_name: '',
MaxCallParticipants: 0,
EnableRecordings: false,
};
export type ICEServersConfigs = Array<ConfigurationParamWithUrls | ConfigurationParamWithUrl>;

View file

@ -3,13 +3,15 @@
import assert from 'assert';
import {CallsConfig} from '@calls/types/calls';
import {License} from '@constants';
import {getICEServersConfigs} from './utils';
describe('getICEServersConfigs', () => {
it('backwards compatible case, no ICEServersConfigs present', () => {
const config = {
const config: CallsConfig = {
pluginEnabled: true,
ICEServers: ['stun:stun.example.com:3478'],
AllowEnableCalls: true,
DefaultEnabled: true,
@ -17,6 +19,7 @@ describe('getICEServersConfigs', () => {
last_retrieved_at: 0,
sku_short_name: License.SKU_SHORT_NAME.Professional,
MaxCallParticipants: 8,
EnableRecordings: true,
};
const iceConfigs = getICEServersConfigs(config);
@ -28,7 +31,8 @@ describe('getICEServersConfigs', () => {
});
it('ICEServersConfigs set', () => {
const config = {
const config: CallsConfig = {
pluginEnabled: true,
ICEServersConfigs: [
{
urls: ['stun:stun.example.com:3478'],
@ -43,6 +47,7 @@ describe('getICEServersConfigs', () => {
last_retrieved_at: 0,
sku_short_name: License.SKU_SHORT_NAME.Professional,
MaxCallParticipants: 8,
EnableRecordings: true,
};
const iceConfigs = getICEServersConfigs(config);
@ -57,7 +62,8 @@ describe('getICEServersConfigs', () => {
});
it('Both ICEServers and ICEServersConfigs set', () => {
const config = {
const config: CallsConfig = {
pluginEnabled: true,
ICEServers: ['stun:stuna.example.com:3478'],
ICEServersConfigs: [
{
@ -73,6 +79,7 @@ describe('getICEServersConfigs', () => {
last_retrieved_at: 0,
sku_short_name: License.SKU_SHORT_NAME.Professional,
MaxCallParticipants: 8,
EnableRecordings: true,
};
const iceConfigs = getICEServersConfigs(config);

View file

@ -361,9 +361,10 @@
"mobile.announcement_banner.title": "Announcement",
"mobile.calls_call_ended": "Call ended",
"mobile.calls_call_screen": "Call",
"mobile.calls_chat_thread": "Chat thread",
"mobile.calls_call_thread": "Open Channel",
"mobile.calls_current_call": "Current call",
"mobile.calls_disable": "Disable calls",
"mobile.calls_dismiss": "Dismiss",
"mobile.calls_enable": "Enable calls",
"mobile.calls_end_call_title": "End call",
"mobile.calls_end_msg_channel": "Are you sure you want to end a call with {numParticipants} participants in {displayName}?",
@ -375,6 +376,10 @@
"mobile.calls_error_message": "Error: {error}",
"mobile.calls_error_title": "Error",
"mobile.calls_host": "host",
"mobile.calls_host_rec": "You are recording this meeting. Consider letting everyone know that this meeting is being recorded.",
"mobile.calls_host_rec_stopped": "You can find the recording in this call's chat thread once it's finished processing.",
"mobile.calls_host_rec_stopped_title": "Recording has stopped. Processing...",
"mobile.calls_host_rec_title": "You are recording",
"mobile.calls_join_call": "Join call",
"mobile.calls_lasted": "Lasted {duration}",
"mobile.calls_leave": "Leave",
@ -399,9 +404,11 @@
"mobile.calls_raise_hand": "Raise hand",
"mobile.calls_react": "React",
"mobile.calls_rec": "rec",
"mobile.calls_record": "Record",
"mobile.calls_see_logs": "See server logs",
"mobile.calls_speaker": "Speaker",
"mobile.calls_start_call": "Start Call",
"mobile.calls_stop_recording": "Stop Recording",
"mobile.calls_unmute": "Unmute",
"mobile.calls_viewing_screen": "You are viewing {name}'s screen",
"mobile.calls_you": "(you)",