[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:
parent
e9b6124479
commit
ab50164d34
10 changed files with 176 additions and 5 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export {
|
|||
unraiseHand,
|
||||
setSpeakerphoneOn,
|
||||
handleCallsSlashCommand,
|
||||
startCallRecording,
|
||||
stopCallRecording,
|
||||
} from './calls';
|
||||
|
||||
export {hasMicrophonePermission} from './permissions';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}),
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -173,4 +173,5 @@ export type RecordingState = {
|
|||
init_at: number;
|
||||
start_at: number;
|
||||
end_at: number;
|
||||
err?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue