[MM-49996] Add /call recording slash command (#7062)

* Add  slash command

* Fix alert

* Implement alert for recording errors

* Default locked
This commit is contained in:
Claudio Costa 2023-02-06 21:14:22 -06:00 committed by GitHub
parent e9b6124479
commit ab50164d34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 176 additions and 5 deletions

View file

@ -9,6 +9,7 @@ import InCallManager from 'react-native-incall-manager';
import * as CallsActions from '@calls/actions';
import {getConnectionForTesting} from '@calls/actions/calls';
import * as Permissions from '@calls/actions/permissions';
import {needsRecordingWillBePostedAlert, needsRecordingErrorAlert} from '@calls/alerts';
import * as State from '@calls/state';
import {
myselfLeftCall,
@ -63,6 +64,8 @@ const mockClient = {
]
)),
enableChannelCalls: jest.fn(),
startCallRecording: jest.fn(),
stopCallRecording: jest.fn(),
};
jest.mock('@calls/connection/connection', () => ({
@ -74,6 +77,8 @@ jest.mock('@calls/connection/connection', () => ({
})),
}));
jest.mock('@calls/alerts');
const addFakeCall = (serverUrl: string, channelId: string) => {
const call = {
participants: {
@ -339,4 +344,23 @@ describe('Actions.Calls', () => {
expect(mockClient.enableChannelCalls).toBeCalledWith('channel-1', false);
assert.equal(result.current.enabled['channel-1'], false);
});
it('startCallRecording', async () => {
await act(async () => {
await CallsActions.startCallRecording('server1', 'channel-id');
});
expect(mockClient.startCallRecording).toBeCalledWith('channel-id');
expect(needsRecordingErrorAlert).toBeCalled();
});
it('stopCallRecording', async () => {
await act(async () => {
await CallsActions.stopCallRecording('server1', 'channel-id');
});
expect(mockClient.stopCallRecording).toBeCalledWith('channel-id');
expect(needsRecordingErrorAlert).toBeCalled();
expect(needsRecordingWillBePostedAlert).toBeCalled();
});
});

View file

@ -7,7 +7,7 @@ import {Navigation} from 'react-native-navigation';
import {forceLogoutIfNecessary} from '@actions/remote/session';
import {fetchUsersByIds} from '@actions/remote/user';
import {leaveAndJoinWithAlert, needsRecordingWillBePostedAlert} from '@calls/alerts';
import {leaveAndJoinWithAlert, needsRecordingWillBePostedAlert, needsRecordingErrorAlert} from '@calls/alerts';
import {
getCallsConfig,
getCallsState,
@ -411,6 +411,8 @@ export const endCall = async (serverUrl: string, channelId: string) => {
};
export const startCallRecording = async (serverUrl: string, callId: string) => {
needsRecordingErrorAlert();
const client = NetworkManager.getClient(serverUrl);
let data: ApiResp | RecordingState;
@ -427,6 +429,7 @@ export const startCallRecording = async (serverUrl: string, callId: string) => {
export const stopCallRecording = async (serverUrl: string, callId: string) => {
needsRecordingWillBePostedAlert();
needsRecordingErrorAlert();
const client = NetworkManager.getClient(serverUrl);
@ -481,6 +484,73 @@ export const handleCallsSlashCommand = async (value: string, serverUrl: string,
defaultMessage: 'You\'re not connected to a call in the current channel.',
}),
};
case 'recording': {
if (tokens.length < 3) {
return {handled: false};
}
const action = tokens[2];
const currentCall = getCurrentCall();
const recording = currentCall?.recState;
const isHost = currentCall?.hostId === currentUserId;
if (currentCall?.channelId !== channelId) {
return {
error: intl.formatMessage({
id: 'mobile.calls_not_connected',
defaultMessage: 'You\'re not connected to a call in the current channel.',
}),
};
}
if (action === 'start') {
if (recording && recording.start_at > recording.end_at) {
return {
error: intl.formatMessage({
id: 'mobile.calls_recording_start_in_progress',
defaultMessage: 'A recording is already in progress.',
}),
};
}
if (!isHost) {
return {
error: intl.formatMessage({
id: 'mobile.calls_recording_start_no_permissions',
defaultMessage: 'You don\'t have permissions to start a recording. Please ask the call host to start a recording.',
}),
};
}
await startCallRecording(currentCall.serverUrl, currentCall.channelId);
return {handled: true};
}
if (action === 'stop') {
if (!recording || recording.end_at > recording.start_at) {
return {
error: intl.formatMessage({
id: 'mobile.calls_recording_stop_none_in_progress',
defaultMessage: 'No recording is in progress.',
}),
};
}
if (!isHost) {
return {
error: intl.formatMessage({
id: 'mobile.calls_recording_stop_no_permissions',
defaultMessage: 'You don\'t have permissions to stop the recording. Please ask the call host to stop the recording.',
}),
};
}
await stopCallRecording(currentCall.serverUrl, currentCall.channelId);
return {handled: true};
}
}
}
return {handled: false};

View file

@ -13,6 +13,8 @@ export {
unraiseHand,
setSpeakerphoneOn,
handleCallsSlashCommand,
startCallRecording,
stopCallRecording,
} from './calls';
export {hasMicrophonePermission} from './permissions';

View file

@ -23,12 +23,18 @@ import {isSystemAdmin} from '@utils/user';
import type {LimitRestrictedInfo} from '@calls/observers';
import type {IntlShape} from 'react-intl';
// Only allow one recording alert per call.
let recordingAlertLock = false;
// Only unlock when:
// - Joining a new call.
// - A new recording has started.
// - Host has changed to current user.
let recordingAlertLock = true;
// Only unlock if/when the user starts a recording.
let recordingWillBePostedLock = true;
// Only unlock when starting/stopping a recording.
let recordingErrorLock = true;
export const showLimitRestrictedAlert = (info: LimitRestrictedInfo, intl: IntlShape) => {
const title = intl.formatMessage({
id: 'mobile.calls_participant_limit_title_GA',
@ -224,6 +230,10 @@ const contactAdminAlert = ({formatMessage}: IntlShape) => {
);
};
export const needsRecordingAlert = () => {
recordingAlertLock = false;
};
export const recordingAlert = (isHost: boolean, intl: IntlShape) => {
if (recordingAlertLock) {
return;
@ -311,3 +321,33 @@ export const recordingWillBePostedAlert = (intl: IntlShape) => {
}],
);
};
export const needsRecordingErrorAlert = () => {
recordingErrorLock = false;
};
export const recordingErrorAlert = (intl: IntlShape) => {
if (recordingErrorLock) {
return;
}
recordingErrorLock = true;
const {formatMessage} = intl;
Alert.alert(
formatMessage({
id: 'mobile.calls_host_rec_error_title',
defaultMessage: 'Something went wrong with the recording',
}),
formatMessage({
id: 'mobile.calls_host_rec_error',
defaultMessage: 'Please try to record again. You can also contact your system admin for troubleshooting help.',
}),
[{
text: formatMessage({
id: 'mobile.calls_dismiss',
defaultMessage: 'Dismiss',
}),
}],
);
};

View file

@ -6,7 +6,7 @@ import {useIntl} from 'react-intl';
import {View, Text, TouchableOpacity, Pressable, Platform} from 'react-native';
import {muteMyself, unmuteMyself} from '@calls/actions';
import {recordingAlert, recordingWillBePostedAlert} from '@calls/alerts';
import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} 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';
@ -158,6 +158,11 @@ const CurrentCallBar = ({
recordingWillBePostedAlert(intl);
}
// The host should receive an alert in case of unexpected error.
if (isHost && currentCall?.recState?.err) {
recordingErrorAlert(intl);
}
return (
<>
<View style={style.wrapper}>

View file

@ -21,7 +21,7 @@ import {RTCView} from 'react-native-webrtc';
import {appEntry} from '@actions/remote/entry';
import {leaveCall, muteMyself, setSpeakerphoneOn, unmuteMyself} from '@calls/actions';
import {startCallRecording, stopCallRecording} from '@calls/actions/calls';
import {recordingAlert, recordingWillBePostedAlert} from '@calls/alerts';
import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} 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';
@ -375,6 +375,11 @@ const CallScreen = ({
recordingWillBePostedAlert(intl);
}
// The host should receive an alert in case of unexpected error.
if (isHost && currentCall?.recState?.err) {
recordingErrorAlert(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);

View file

@ -5,6 +5,7 @@ import assert from 'assert';
import {act, renderHook} from '@testing-library/react-hooks';
import {needsRecordingAlert} from '@calls/alerts';
import {
newCurrentCall,
setCallsState,
@ -53,6 +54,8 @@ import {
RecordingState,
} from '../types/calls';
jest.mock('@calls/alerts');
const call1: Call = {
participants: {
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
@ -955,6 +958,8 @@ describe('useCallsState', () => {
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);
act(() => setRecordingState('server1', 'channel-1', {...recState, start_at: recState.start_at + 1}));
expect(needsRecordingAlert).toBeCalled();
});
it('setHost', () => {
@ -1006,5 +1011,7 @@ describe('useCallsState', () => {
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);
act(() => setHost('server1', 'channel-1', 'myUserId'));
expect(needsRecordingAlert).toBeCalled();
});
});

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {needsRecordingAlert} from '@calls/alerts';
import {
getCallsConfig,
getCallsState,
@ -513,6 +514,11 @@ export const setRecordingState = (serverUrl: string, channelId: string, recState
return;
}
// If a new call has started, we reset the alert state so it can be showed again.
if (currentCall.recState && recState.start_at > currentCall.recState.start_at) {
needsRecordingAlert();
}
const nextCurrentCall = {
...currentCall,
recState,
@ -536,6 +542,11 @@ export const setHost = (serverUrl: string, channelId: string, hostId: string) =>
return;
}
// If we are the new host we show the alert again.
if (currentCall.myUserId === hostId) {
needsRecordingAlert();
}
const nextCurrentCall = {
...currentCall,
hostId,

View file

@ -173,4 +173,5 @@ export type RecordingState = {
init_at: number;
start_at: number;
end_at: number;
err?: string;
}

View file

@ -414,6 +414,8 @@
"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_error": "Please try to record again. You can also contact your system admin for troubleshooting help.",
"mobile.calls_host_rec_error_title": "Something went wrong with the recording",
"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",
@ -445,6 +447,10 @@
"mobile.calls_react": "React",
"mobile.calls_rec": "rec",
"mobile.calls_record": "Record",
"mobile.calls_recording_start_in_progress": "A recording is already in progress.",
"mobile.calls_recording_start_no_permissions": "You don't have permissions to start a recording. Please ask the call host to start a recording.",
"mobile.calls_recording_stop_no_permissions": "You don't have permissions to stop the recording. Please ask the call host to stop the recording.",
"mobile.calls_recording_stop_none_in_progress": "No recording is in progress.",
"mobile.calls_request_message": "Calls are currently running in test mode and only system admins can start them. Reach out directly to your system admin for assistance",
"mobile.calls_request_title": "Calls is not currently enabled",
"mobile.calls_see_logs": "See server logs",