Skip to content

Commit

Permalink
MM-54865 - Calls: Multisession support (#7647)
Browse files Browse the repository at this point in the history
* add multisession support for calls mobile

* ensure backwards compatibility

* remove extra line
  • Loading branch information
cpoile authored Nov 16, 2023
1 parent 63857a3 commit c9fe724
Show file tree
Hide file tree
Showing 21 changed files with 514 additions and 328 deletions.
68 changes: 57 additions & 11 deletions app/actions/websocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
import {markChannelAsViewed} from '@actions/local/channel';
import {dataRetentionCleanup} from '@actions/local/systems';
import {markChannelAsRead} from '@actions/remote/channel';
import {deferredAppEntryActions, entry, handleEntryAfterLoadNavigation, registerDeviceToken} from '@actions/remote/entry/common';
import {
deferredAppEntryActions,
entry,
handleEntryAfterLoadNavigation,
registerDeviceToken,
} from '@actions/remote/entry/common';
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
import {openAllUnreadChannels} from '@actions/remote/preference';
import {autoUpdateTimezone} from '@actions/remote/user';
Expand All @@ -17,9 +22,9 @@ import {
handleCallRecordingState,
handleCallScreenOff,
handleCallScreenOn,
handleCallStarted,
handleCallUserConnected,
handleCallUserDisconnected,
handleCallStarted, handleCallUserConnected, handleCallUserDisconnected,
handleCallUserJoined,
handleCallUserLeft,
handleCallUserMuted,
handleCallUserRaiseHand,
handleCallUserReacted,
Expand Down Expand Up @@ -51,8 +56,14 @@ import {setTeamLoading} from '@store/team_load_store';
import {isTablet} from '@utils/helpers';
import {logDebug, logInfo} from '@utils/log';

import {handleCategoryCreatedEvent, handleCategoryDeletedEvent, handleCategoryOrderUpdatedEvent, handleCategoryUpdatedEvent} from './category';
import {handleChannelConvertedEvent, handleChannelCreatedEvent,
import {
handleCategoryCreatedEvent,
handleCategoryDeletedEvent,
handleCategoryOrderUpdatedEvent,
handleCategoryUpdatedEvent,
} from './category';
import {
handleChannelConvertedEvent, handleChannelCreatedEvent,
handleChannelDeletedEvent,
handleChannelMemberUpdatedEvent,
handleChannelUnarchiveEvent,
Expand All @@ -61,15 +72,39 @@ import {handleChannelConvertedEvent, handleChannelCreatedEvent,
handleMultipleChannelsViewedEvent,
handleDirectAddedEvent,
handleUserAddedToChannelEvent,
handleUserRemovedFromChannelEvent} from './channel';
import {handleGroupMemberAddEvent, handleGroupMemberDeleteEvent, handleGroupReceivedEvent, handleGroupTeamAssociatedEvent, handleGroupTeamDissociateEvent} from './group';
handleUserRemovedFromChannelEvent,
} from './channel';
import {
handleGroupMemberAddEvent,
handleGroupMemberDeleteEvent,
handleGroupReceivedEvent,
handleGroupTeamAssociatedEvent,
handleGroupTeamDissociateEvent,
} from './group';
import {handleOpenDialogEvent} from './integrations';
import {handleNewPostEvent, handlePostAcknowledgementAdded, handlePostAcknowledgementRemoved, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts';
import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
import {
handleNewPostEvent,
handlePostAcknowledgementAdded,
handlePostAcknowledgementRemoved,
handlePostDeleted,
handlePostEdited,
handlePostUnread,
} from './posts';
import {
handlePreferenceChangedEvent,
handlePreferencesChangedEvent,
handlePreferencesDeletedEvent,
} from './preferences';
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
import {handleLicenseChangedEvent, handleConfigChangedEvent} from './system';
import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent, handleTeamArchived, handleTeamRestored} from './teams';
import {
handleLeaveTeamEvent,
handleUserAddedToTeamEvent,
handleUpdateTeamEvent,
handleTeamArchived,
handleTeamRestored,
} from './teams';
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
import {handleUserUpdatedEvent, handleUserTypingEvent, handleStatusChangedEvent} from './users';

Expand Down Expand Up @@ -347,12 +382,23 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
case WebsocketEvents.CALLS_CHANNEL_DISABLED:
handleCallChannelDisabled(serverUrl, msg);
break;

// DEPRECATED in favour of user_joined (since v0.21.0)
case WebsocketEvents.CALLS_USER_CONNECTED:
handleCallUserConnected(serverUrl, msg);
break;

// DEPRECATED in favour of user_left (since v0.21.0)
case WebsocketEvents.CALLS_USER_DISCONNECTED:
handleCallUserDisconnected(serverUrl, msg);
break;

case WebsocketEvents.CALLS_USER_JOINED:
handleCallUserJoined(serverUrl, msg);
break;
case WebsocketEvents.CALLS_USER_LEFT:
handleCallUserLeft(serverUrl, msg);
break;
case WebsocketEvents.CALLS_USER_MUTED:
handleCallUserMuted(serverUrl, msg);
break;
Expand Down
8 changes: 8 additions & 0 deletions app/constants/calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ const RequiredServer = {
PATCH_VERSION: 0,
};

const MultiSessionCallsVersion = {
FULL_VERSION: '0.21.0',
MAJOR_VERSION: 0,
MIN_VERSION: 21,
PATCH_VERSION: 0,
};

const PluginId = 'com.mattermost.calls';

const REACTION_TIMEOUT = 10000;
Expand All @@ -26,6 +33,7 @@ export enum MessageBarType {
export default {
RefreshConfigMillis,
RequiredServer,
MultiSessionCallsVersion,
PluginId,
REACTION_TIMEOUT,
REACTION_LIMIT,
Expand Down
7 changes: 7 additions & 0 deletions app/constants/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,15 @@ const WebsocketEvents = {
APPS_FRAMEWORK_REFRESH_BINDINGS: 'custom_com.mattermost.apps_refresh_bindings',
CALLS_CHANNEL_ENABLED: `custom_${Calls.PluginId}_channel_enable_voice`,
CALLS_CHANNEL_DISABLED: `custom_${Calls.PluginId}_channel_disable_voice`,

// DEPRECATED in favour of user_joined (since v0.21.0)
CALLS_USER_CONNECTED: `custom_${Calls.PluginId}_user_connected`,

// DEPRECATED in favour of user_left (since v0.21.0)
CALLS_USER_DISCONNECTED: `custom_${Calls.PluginId}_user_disconnected`,

CALLS_USER_JOINED: `custom_${Calls.PluginId}_user_joined`,
CALLS_USER_LEFT: `custom_${Calls.PluginId}_user_left`,
CALLS_USER_MUTED: `custom_${Calls.PluginId}_user_muted`,
CALLS_USER_UNMUTED: `custom_${Calls.PluginId}_user_unmuted`,
CALLS_USER_VOICE_ON: `custom_${Calls.PluginId}_user_voice_on`,
Expand Down
28 changes: 14 additions & 14 deletions app/products/calls/actions/calls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@ const mockClient = {
getCalls: jest.fn(() => [
{
call: {
users: ['user-1', 'user-2'],
states: {
'user-1': {unmuted: true},
'user-2': {unmuted: false},
sessions: {
session1: {session_id: 'session1', user_id: 'user-1', unmuted: true},
session2: {session_id: 'session1', user_id: 'user-1', unmuted: false},
},
start_at: 123,
screen_sharing_id: '',
Expand All @@ -58,6 +57,7 @@ const mockClient = {
DefaultEnabled: true,
last_retrieved_at: 1234,
})),
getVersion: jest.fn(() => ({})),
getPluginsManifests: jest.fn(() => (
[
{id: 'playbooks'},
Expand Down Expand Up @@ -101,13 +101,13 @@ jest.mock('react-native-navigation', () => ({
const addFakeCall = (serverUrl: string, channelId: string) => {
const call: Call = {
id: 'call',
participants: {
xohi8cki9787fgiryne716u84o: {id: 'xohi8cki9787fgiryne716u84o', muted: false, raisedHand: 0},
xohi8cki9787fgiryne716u841: {id: 'xohi8cki9787fgiryne716u84o', muted: true, raisedHand: 0},
xohi8cki9787fgiryne716u842: {id: 'xohi8cki9787fgiryne716u84o', muted: false, raisedHand: 0},
xohi8cki9787fgiryne716u843: {id: 'xohi8cki9787fgiryne716u84o', muted: true, raisedHand: 0},
xohi8cki9787fgiryne716u844: {id: 'xohi8cki9787fgiryne716u84o', muted: false, raisedHand: 0},
xohi8cki9787fgiryne716u845: {id: 'xohi8cki9787fgiryne716u84o', muted: true, raisedHand: 0},
sessions: {
a23456abcdefghijklmnopqrs: {sessionId: 'a23456abcdefghijklmnopqrs', userId: 'xohi8cki9787fgiryne716u84o', muted: false, raisedHand: 0},
a12345667890bcdefghijklmn1: {sessionId: 'a12345667890bcdefghijklmn1', userId: 'xohi8cki9787fgiryne716u84o', muted: true, raisedHand: 0},
a12345667890bcdefghijklmn2: {sessionId: 'a12345667890bcdefghijklmn2', userId: 'xohi8cki9787fgiryne716u84o', muted: false, raisedHand: 0},
a12345667890bcdefghijklmn3: {sessionId: 'a12345667890bcdefghijklmn3', userId: 'xohi8cki9787fgiryne716u84o', muted: true, raisedHand: 0},
a12345667890bcdefghijklmn4: {sessionId: 'a12345667890bcdefghijklmn4', userId: 'xohi8cki9787fgiryne716u84o', muted: false, raisedHand: 0},
a12345667890bcdefghijklmn5: {sessionId: 'a12345667890bcdefghijklmn5', userId: 'xohi8cki9787fgiryne716u84o', muted: true, raisedHand: 0},
},
channelId,
startTime: (new Date()).getTime(),
Expand Down Expand Up @@ -205,7 +205,7 @@ describe('Actions.Calls', () => {

// manually call newCurrentConnection because newConnection is mocked
newCurrentCall('server1', 'channel-id', 'myUserId');
userJoinedCall('server1', 'channel-id', 'myUserId');
userJoinedCall('server1', 'channel-id', 'myUserId', 'mySessionId');
});
assert.equal(response!.data, 'channel-id');
assert.equal((result.current[1] as CurrentCall | null)?.channelId, 'channel-id');
Expand Down Expand Up @@ -239,7 +239,7 @@ describe('Actions.Calls', () => {

// manually call newCurrentConnection because newConnection is mocked
newCurrentCall('server1', 'channel-id', 'myUserId');
userJoinedCall('server1', 'channel-id', 'myUserId');
userJoinedCall('server1', 'channel-id', 'myUserId', 'mySessionId');
});
assert.equal(response!.data, 'channel-id');
assert.equal((result.current[1] as CurrentCall | null)?.channelId, 'channel-id');
Expand Down Expand Up @@ -269,7 +269,7 @@ describe('Actions.Calls', () => {

// manually call newCurrentConnection because newConnection is mocked
newCurrentCall('server1', 'channel-id', 'myUserId');
userJoinedCall('server1', 'channel-id', 'myUserId');
userJoinedCall('server1', 'channel-id', 'myUserId', 'mySessionId');
});
assert.equal(response!.data, 'channel-id');
assert.equal((result.current[1] as CurrentCall | null)?.channelId, 'channel-id');
Expand Down
58 changes: 42 additions & 16 deletions app/products/calls/actions/calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ import {displayUsername, getUserIdFromChannelName, isSystemAdmin} from '@utils/u

import {newConnection} from '../connection/connection';

import type {AudioDevice, Call, CallParticipant, CallsConnection} from '@calls/types/calls';
import type {CallChannelState, CallState, EmojiData} from '@mattermost/calls/lib/types';
import type {AudioDevice, Call, CallSession, CallsConnection} from '@calls/types/calls';
import type {CallChannelState, CallState, EmojiData, SessionState} from '@mattermost/calls/lib/types';
import type {IntlShape} from 'react-intl';

let connection: CallsConnection | null = null;
Expand All @@ -59,8 +59,8 @@ export const loadConfig = async (serverUrl: string, force = false) => {

try {
const client = NetworkManager.getClient(serverUrl);
const data = await client.getCallsConfig();
const nextConfig = {...data, last_retrieved_at: now};
const configs = await Promise.all([client.getCallsConfig(), client.getVersion()]);
const nextConfig = {...configs[0], version: configs[1], last_retrieved_at: now};
setConfig(serverUrl, nextConfig);
return {data: nextConfig};
} catch (error) {
Expand All @@ -87,7 +87,7 @@ export const loadCalls = async (serverUrl: string, userId: string) => {

for (const channel of resp) {
if (channel.call) {
callsResults[channel.channel_id] = createCallAndAddToIds(channel.channel_id, channel.call, ids);
callsResults[channel.channel_id] = createCallAndAddToIds(channel.channel_id, convertOldCallToNew(channel.call), ids);
}

if (typeof channel.enabled !== 'undefined') {
Expand Down Expand Up @@ -119,7 +119,7 @@ export const loadCallForChannel = async (serverUrl: string, channelId: string) =
let call: Call | undefined;
const ids = new Set<string>();
if (resp.call) {
call = createCallAndAddToIds(channelId, resp.call, ids);
call = createCallAndAddToIds(channelId, convertOldCallToNew(resp.call), ids);
}

// Batch load user models async because we'll need them later
Expand All @@ -132,22 +132,48 @@ export const loadCallForChannel = async (serverUrl: string, channelId: string) =
return {data: {call, enabled: resp.enabled}};
};

// Converts pre-0.21.0 call to 0.21.0+ call. Can be removed when we stop supporting pre-0.21.0
// Also can be removed: all code prefaced with a "Pre v0.21.0, sessionID == userID" comment
// Does nothing if the call is in the new format.
const convertOldCallToNew = (call: CallState): CallState => {
if (call.sessions) {
return call;
}

return {
...call,
sessions: call.users.reduce((accum, cur, curIdx) => {
accum.push({
session_id: cur,
user_id: cur,
unmuted: call.states && call.states[curIdx] ? call.states[curIdx].unmuted : false,
raised_hand: call.states && call.states[curIdx] ? call.states[curIdx].raised_hand : 0,
});
return accum;
}, [] as SessionState[]),
screen_sharing_session_id: call.screen_sharing_id,
};
};

const createCallAndAddToIds = (channelId: string, call: CallState, ids: Set<string>) => {
return {
participants: call.users.reduce((accum, cur, curIdx) => {
sessions: Object.values(call.sessions).reduce((accum, cur) => {
// Add the id to the set of UserModels we want to ensure are loaded.
ids.add(cur);
ids.add(cur.user_id);

// Create the CallParticipant
const muted = call.states && call.states[curIdx] ? !call.states[curIdx].unmuted : true;
const raisedHand = call.states && call.states[curIdx] ? call.states[curIdx].raised_hand : 0;
accum[cur] = {id: cur, muted, raisedHand};
accum[cur.session_id] = {
userId: cur.user_id,
sessionId: cur.session_id,
raisedHand: cur.raised_hand || 0,
muted: !cur.unmuted,
};
return accum;
}, {} as Dictionary<CallParticipant>),
}, {} as Dictionary<CallSession>),
channelId,
id: call.id,
startTime: call.start_at,
screenOn: call.screen_sharing_id,
screenOn: call.screen_sharing_session_id,
threadId: call.thread_id,
ownerId: call.owner_id,
hostId: call.host_id,
Expand Down Expand Up @@ -351,12 +377,12 @@ export const getEndCallMessage = async (serverUrl: string, channelId: string, cu
return msg;
}

const numParticipants = Object.keys(call.participants).length;
const numSessions = Object.keys(call.sessions).length;

msg = intl.formatMessage({
id: 'mobile.calls_end_msg_channel',
defaultMessage: 'Are you sure you want to end a call with {numParticipants} participants in {displayName}?',
}, {numParticipants, displayName: channel.displayName});
defaultMessage: 'Are you sure you want to end a call with {numSessions} participants in {displayName}?',
}, {numSessions, displayName: channel.displayName});

if (channel.type === General.DM_CHANNEL) {
const otherID = getUserIdFromChannelName(currentUserId, channel.name);
Expand Down
14 changes: 13 additions & 1 deletion app/products/calls/client/rest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import type {ApiResp} from '@calls/types/calls';
import type {ApiResp, CallsVersion} from '@calls/types/calls';
import type {CallChannelState, CallRecordingState, CallsConfig} from '@mattermost/calls/lib/types';
import type {RTCIceServer} from 'react-native-webrtc';

Expand All @@ -10,6 +10,7 @@ export interface ClientCallsMix {
getCalls: () => Promise<CallChannelState[]>;
getCallForChannel: (channelId: string) => Promise<CallChannelState>;
getCallsConfig: () => Promise<CallsConfig>;
getVersion: () => Promise<CallsVersion>;
enableChannelCalls: (channelId: string, enable: boolean) => Promise<CallChannelState>;
endCall: (channelId: string) => Promise<ApiResp>;
genTURNCredentials: () => Promise<RTCIceServer[]>;
Expand Down Expand Up @@ -52,6 +53,17 @@ const ClientCalls = (superclass: any) => class extends superclass {
) as CallsConfig;
};

getVersion = async () => {
try {
return this.doFetch(
`${this.getCallsRoute()}/version`,
{method: 'get'},
);
} catch (e) {
return {};
}
};

enableChannelCalls = async (channelId: string, enable: boolean) => {
return this.doFetch(
`${this.getCallsRoute()}/${channelId}`,
Expand Down
Loading

0 comments on commit c9fe724

Please sign in to comment.