diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts
index 109132be4..5ba00ca07 100644
--- a/app/actions/websocket/index.ts
+++ b/app/actions/websocket/index.ts
@@ -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);
diff --git a/app/constants/websocket.ts b/app/constants/websocket.ts
index b92e26e64..465cd55aa 100644
--- a/app/constants/websocket.ts
+++ b/app/constants/websocket.ts
@@ -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',
diff --git a/app/products/calls/actions/calls.test.ts b/app/products/calls/actions/calls.test.ts
index 4b298cec7..672787e82 100644
--- a/app/products/calls/actions/calls.test.ts
+++ b/app/products/calls/actions/calls.test.ts
@@ -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: {}});
diff --git a/app/products/calls/actions/calls.ts b/app/products/calls/actions/calls.ts
index e30aea07c..7e0da4e9d 100644
--- a/app/products/calls/actions/calls.ts
+++ b/app/products/calls/actions/calls.ts
@@ -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;
};
diff --git a/app/products/calls/alerts.ts b/app/products/calls/alerts.ts
index 986fc0af3..f34ccc1de 100644
--- a/app/products/calls/alerts.ts
+++ b/app/products/calls/alerts.ts
@@ -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(),
+ },
+ ],
+ );
+};
diff --git a/app/products/calls/components/calls_badge.tsx b/app/products/calls/components/calls_badge.tsx
new file mode 100644
index 000000000..848461c9e
--- /dev/null
+++ b/app/products/calls/components/calls_badge.tsx
@@ -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 ? (
+
+ ) : (
+
+ );
+
+ return (
+
+ {
+ isRec &&
+
+ }
+ {text}
+
+ );
+};
+
+export default CallsBadge;
diff --git a/app/products/calls/components/current_call_bar/current_call_bar.tsx b/app/products/calls/components/current_call_bar/current_call_bar.tsx
index 3a6bb1f78..32e625154 100644
--- a/app/products/calls/components/current_call_bar/current_call_bar.tsx
+++ b/app/products/calls/components/current_call_bar/current_call_bar.tsx
@@ -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 (
<>
diff --git a/app/products/calls/connection/websocket_event_handlers.ts b/app/products/calls/connection/websocket_event_handlers.ts
index f747585ea..1cd0c6032 100644
--- a/app/products/calls/connection/websocket_event_handlers.ts
+++ b/app/products/calls/connection/websocket_event_handlers.ts
@@ -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);
+};
diff --git a/app/products/calls/screens/call_screen/call_screen.tsx b/app/products/calls/screens/call_screen/call_screen.tsx
index 4a9f92ef3..fcf4431ec 100644
--- a/app/products/calls/screens/call_screen/call_screen.tsx
+++ b/app/products/calls/screens/call_screen/call_screen.tsx
@@ -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)'})}`
}
+ {user.id === currentCall.hostId && }
);
})}
@@ -450,6 +464,7 @@ const CallScreen = ({
+ {recording && }
{
@@ -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);
+ });
});
diff --git a/app/products/calls/state/actions.ts b/app/products/calls/state/actions.ts
index ee5be353d..ad0c8942e 100644
--- a/app/products/calls/state/actions.ts
+++ b/app/products/calls/state/actions.ts
@@ -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);
+};
diff --git a/app/products/calls/types/calls.ts b/app/products/calls/types/calls.ts
index bfdf8fc66..08bb6b322 100644
--- a/app/products/calls/types/calls.ts
+++ b/app/products/calls/types/calls.ts
@@ -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;
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;
+}
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index c81cc17ed..c5841f1f2 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -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",