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:
parent
6660c134f5
commit
0f8798ca55
11 changed files with 284 additions and 123 deletions
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}),
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
Loading…
Reference in a new issue