MM-54865 - Calls: Multisession support (#7647)

* add multisession support for calls mobile

* ensure backwards compatibility

* remove extra line
This commit is contained in:
Christopher Poile 2023-11-16 14:30:49 -05:00 committed by GitHub
parent 63857a34d9
commit c9fe72460b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 514 additions and 328 deletions

View file

@ -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';
@ -17,9 +22,9 @@ import {
handleCallRecordingState,
handleCallScreenOff,
handleCallScreenOn,
handleCallStarted,
handleCallUserConnected,
handleCallUserDisconnected,
handleCallStarted, handleCallUserConnected, handleCallUserDisconnected,
handleCallUserJoined,
handleCallUserLeft,
handleCallUserMuted,
handleCallUserRaiseHand,
handleCallUserReacted,
@ -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,
@ -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';
@ -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;

View file

@ -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;
@ -26,6 +33,7 @@ export enum MessageBarType {
export default {
RefreshConfigMillis,
RequiredServer,
MultiSessionCallsVersion,
PluginId,
REACTION_TIMEOUT,
REACTION_LIMIT,

View file

@ -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`,

View file

@ -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: '',
@ -58,6 +57,7 @@ const mockClient = {
DefaultEnabled: true,
last_retrieved_at: 1234,
})),
getVersion: jest.fn(() => ({})),
getPluginsManifests: jest.fn(() => (
[
{id: 'playbooks'},
@ -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(),
@ -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');
@ -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');
@ -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');

View file

@ -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;
@ -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) {
@ -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') {
@ -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
@ -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,
@ -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);

View file

@ -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';
@ -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[]>;
@ -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}`,

View file

@ -23,14 +23,13 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername} from '@utils/user';
import type {CallsTheme, CurrentCall} from '@calls/types/calls';
import type UserModel from '@typings/database/models/servers/user';
import type {CallSession, CallsTheme, CurrentCall} from '@calls/types/calls';
import type {Options} from 'react-native-navigation';
type Props = {
displayName: string;
currentCall: CurrentCall | null;
userModelsDict: Dictionary<UserModel>;
sessionsDict: Dictionary<CallSession>;
teammateNameDisplay: string;
micPermissionsGranted: boolean;
threadScreen?: boolean;
@ -135,7 +134,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => {
const CurrentCallBar = ({
displayName,
currentCall,
userModelsDict,
sessionsDict,
teammateNameDisplay,
micPermissionsGranted,
threadScreen,
@ -169,7 +168,7 @@ const CurrentCallBar = ({
leaveCall();
}, []);
const myParticipant = currentCall?.participants[currentCall.myUserId];
const mySession = currentCall?.sessions[currentCall.mySessionId];
// Since we can only see one user talking, it doesn't really matter who we show here (e.g., we can't
// tell who is speaking louder).
@ -185,7 +184,7 @@ const CurrentCallBar = ({
if (speaker) {
talkingMessage = (
<Text style={style.speakingUser}>
{displayUsername(userModelsDict[speaker], intl.locale, teammateNameDisplay)}
{displayUsername(sessionsDict[speaker].userModel, intl.locale, teammateNameDisplay)}
{' '}
<Text style={style.speakingPostfix}>{
formatMessage({
@ -197,7 +196,7 @@ const CurrentCallBar = ({
}
const muteUnmute = () => {
if (myParticipant?.muted) {
if (mySession?.muted) {
unmuteMyself();
} else {
muteMyself();
@ -208,7 +207,7 @@ const CurrentCallBar = ({
// The user should receive an alert if all of the following conditions apply:
// - Recording has started and recording has not ended.
const isHost = Boolean(currentCall?.hostId === myParticipant?.id);
const isHost = Boolean(currentCall?.hostId === mySession?.userId);
if (currentCall?.recState?.start_at && !currentCall?.recState?.end_at) {
recordingAlert(isHost, intl);
}
@ -233,7 +232,7 @@ const CurrentCallBar = ({
>
<View style={[!speaker && style.avatarOutline]}>
<CallAvatar
userModel={userModelsDict[speaker || '']}
userModel={sessionsDict[speaker || '']?.userModel}
speaking={Boolean(speaker)}
serverUrl={currentCall?.serverUrl || ''}
size={speaker ? 40 : 24}
@ -254,11 +253,11 @@ const CurrentCallBar = ({
<View style={style.buttonContainer}>
<Pressable
onPress={muteUnmute}
style={[style.pressable, style.micIconContainer, myParticipant?.muted && style.muted]}
style={[style.pressable, style.micIconContainer, mySession?.muted && style.muted]}
disabled={!micPermissionsGranted}
>
<UnavailableIconWrapper
name={myParticipant?.muted ? 'microphone-off' : 'microphone'}
name={mySession?.muted ? 'microphone-off' : 'microphone'}
size={24}
unavailable={!micPermissionsGranted}
style={style.micIcon}

View file

@ -5,16 +5,14 @@ import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {observeCurrentSessionsDict} from '@calls/observers';
import {observeCurrentCall, observeGlobalCallsState} from '@calls/state';
import {idsAreEqual} from '@calls/utils';
import DatabaseManager from '@database/manager';
import {observeChannel} from '@queries/servers/channel';
import {observeTeammateNameDisplay, queryUsersById} from '@queries/servers/user';
import {observeTeammateNameDisplay} from '@queries/servers/user';
import CurrentCallBar from './current_call_bar';
import type UserModel from '@typings/database/models/servers/user';
const enhanced = withObservables([], () => {
const currentCall = observeCurrentCall();
const ccServerUrl = currentCall.pipe(
@ -33,17 +31,9 @@ const enhanced = withObservables([], () => {
switchMap((c) => of$(c?.displayName || '')),
distinctUntilChanged(),
);
const participantIds = currentCall.pipe(
distinctUntilChanged((prev, curr) => prev?.participants === curr?.participants), // Did the participants object ref change?
switchMap((call) => (call ? of$(Object.keys(call.participants)) : of$([]))),
distinctUntilChanged((prev, curr) => idsAreEqual(prev, curr)),
);
const userModelsDict = combineLatest([database, participantIds]).pipe(
switchMap(([db, ids]) => (db && ids.length > 0 ? queryUsersById(db, ids).observeWithColumns(['nickname', 'username', 'first_name', 'last_name']) : of$([]))),
switchMap((ps) => of$(arrayToDic(ps))),
);
const teammateNameDisplay = database.pipe(
switchMap((db) => (db ? observeTeammateNameDisplay(db) : of$(''))),
distinctUntilChanged(),
);
const micPermissionsGranted = observeGlobalCallsState().pipe(
switchMap((gs) => of$(gs.micPermissionsGranted)),
@ -53,17 +43,10 @@ const enhanced = withObservables([], () => {
return {
displayName,
currentCall,
userModelsDict,
sessionsDict: observeCurrentSessionsDict(),
teammateNameDisplay,
micPermissionsGranted,
};
});
function arrayToDic(participants: UserModel[]) {
return participants.reduce((accum, cur) => {
accum[cur.id] = cur;
return accum;
}, {} as Dictionary<UserModel>);
}
export default enhanced(CurrentCallBar);

View file

@ -10,7 +10,7 @@ import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import JoinCallBanner from '@calls/components/join_call_banner/join_call_banner';
import {observeIsCallLimitRestricted} from '@calls/observers';
import {observeCallsState} from '@calls/state';
import {idsAreEqual} from '@calls/utils';
import {idsAreEqual, userIds} from '@calls/utils';
import {queryUsersById} from '@queries/servers/user';
import type {WithDatabaseArgs} from '@typings/database/database';
@ -28,10 +28,10 @@ const enhanced = withObservables(['serverUrl', 'channelId'], ({
const callsState = observeCallsState(serverUrl).pipe(
switchMap((state) => of$(state.calls[channelId])),
);
const participants = callsState.pipe(
distinctUntilChanged((prev, curr) => prev?.participants === curr?.participants), // Did the participants object ref change?
switchMap((call) => (call ? of$(Object.keys(call.participants)) : of$([]))),
distinctUntilChanged((prev, curr) => idsAreEqual(prev, curr)), // Continue only if we have a different set of participant ids
const userModels = callsState.pipe(
distinctUntilChanged((prev, curr) => prev?.sessions === curr?.sessions), // Did the userModels object ref change?
switchMap((call) => (call ? of$(userIds(Object.values(call.sessions))) : of$([]))),
distinctUntilChanged((prev, curr) => idsAreEqual(prev, curr)), // Continue only if we have a different set of participant userIds
switchMap((ids) => (ids.length > 0 ? queryUsersById(database, ids).observeWithColumns(['last_picture_update']) : of$([]))),
);
const channelCallStartTime = callsState.pipe(
@ -47,7 +47,7 @@ const enhanced = withObservables(['serverUrl', 'channelId'], ({
return {
callId,
participants,
userModels,
channelCallStartTime,
limitRestrictedInfo: observeIsCallLimitRestricted(database, serverUrl, channelId),
};

View file

@ -25,7 +25,7 @@ type Props = {
channelId: string;
callId: string;
serverUrl: string;
participants: UserModel[];
userModels: UserModel[];
channelCallStartTime: number;
limitRestrictedInfo: LimitRestrictedInfo;
}
@ -134,7 +134,7 @@ const JoinCallBanner = ({
channelId,
callId,
serverUrl,
participants,
userModels,
channelCallStartTime,
limitRestrictedInfo,
}: Props) => {
@ -191,7 +191,7 @@ const JoinCallBanner = ({
<UserAvatarsStack
channelId={channelId}
location={Screens.CHANNEL}
users={participants}
users={userModels}
breakAt={3}
avatarStyle={style.avatarStyle}
overflowContainerStyle={style.overflowContainer}

View file

@ -6,7 +6,7 @@ import {DeviceEventEmitter} from 'react-native';
import {fetchUsersByIds} from '@actions/remote/user';
import {
callEnded,
callStarted,
callStarted, getCallsConfig,
removeIncomingCall,
setCallScreenOff,
setCallScreenOn,
@ -20,6 +20,7 @@ import {
userLeftCall,
userReacted,
} from '@calls/state';
import {isMultiSessionSupported} from '@calls/utils';
import {WebsocketEvents} from '@constants';
import DatabaseManager from '@database/manager';
import {getCurrentUserId} from '@queries/servers/system';
@ -32,6 +33,8 @@ import type {
UserConnectedData,
UserDisconnectedData,
UserDismissedNotification,
UserJoinedData,
UserLeftData,
UserMutedUnmutedData,
UserRaiseUnraiseHandData,
UserReactionData,
@ -39,31 +42,58 @@ import type {
UserVoiceOnOffData,
} from '@mattermost/calls/lib/types';
// DEPRECATED in favour of user_joined (since v0.21.0)
export const handleCallUserConnected = (serverUrl: string, msg: WebSocketMessage<UserConnectedData>) => {
if (isMultiSessionSupported(getCallsConfig(serverUrl).version)) {
return;
}
// Load user model async (if needed).
fetchUsersByIds(serverUrl, [msg.data.userID]);
userJoinedCall(serverUrl, msg.broadcast.channel_id, msg.data.userID);
// Pre v0.21.0, sessionID == userID
userJoinedCall(serverUrl, msg.broadcast.channel_id, msg.data.userID, msg.data.userID);
};
// DEPRECATED in favour of user_left (since v0.21.0)
export const handleCallUserDisconnected = (serverUrl: string, msg: WebSocketMessage<UserDisconnectedData>) => {
if (isMultiSessionSupported(getCallsConfig(serverUrl).version)) {
return;
}
// pre v0.21.0, sessionID == userID
userLeftCall(serverUrl, msg.broadcast.channel_id, msg.data.userID);
};
export const handleCallUserJoined = (serverUrl: string, msg: WebSocketMessage<UserJoinedData>) => {
// Load user model async (if needed).
fetchUsersByIds(serverUrl, [msg.data.user_id]);
userJoinedCall(serverUrl, msg.broadcast.channel_id, msg.data.user_id, msg.data.session_id);
};
export const handleCallUserLeft = (serverUrl: string, msg: WebSocketMessage<UserLeftData>) => {
userLeftCall(serverUrl, msg.broadcast.channel_id, msg.data.session_id);
};
export const handleCallUserMuted = (serverUrl: string, msg: WebSocketMessage<UserMutedUnmutedData>) => {
setUserMuted(serverUrl, msg.broadcast.channel_id, msg.data.userID, true);
// pre v0.21.0, sessionID == userID
setUserMuted(serverUrl, msg.broadcast.channel_id, msg.data.session_id || msg.data.userID, true);
};
export const handleCallUserUnmuted = (serverUrl: string, msg: WebSocketMessage<UserMutedUnmutedData>) => {
setUserMuted(serverUrl, msg.broadcast.channel_id, msg.data.userID, false);
// pre v0.21.0, sessionID == userID
setUserMuted(serverUrl, msg.broadcast.channel_id, msg.data.session_id || msg.data.userID, false);
};
export const handleCallUserVoiceOn = (msg: WebSocketMessage<UserVoiceOnOffData>) => {
setUserVoiceOn(msg.broadcast.channel_id, msg.data.userID, true);
// pre v0.21.0, sessionID == userID
setUserVoiceOn(msg.broadcast.channel_id, msg.data.session_id || msg.data.userID, true);
};
export const handleCallUserVoiceOff = (msg: WebSocketMessage<UserVoiceOnOffData>) => {
setUserVoiceOn(msg.broadcast.channel_id, msg.data.userID, false);
// pre v0.21.0, sessionID == userID
setUserVoiceOn(msg.broadcast.channel_id, msg.data.session_id || msg.data.userID, false);
};
export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage<CallStartData>) => {
@ -73,7 +103,7 @@ export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage<CallS
startTime: msg.data.start_at,
threadId: msg.data.thread_id,
screenOn: '',
participants: {},
sessions: {},
ownerId: msg.data.owner_id,
hostId: msg.data.host_id,
dismissed: {},
@ -97,22 +127,31 @@ export const handleCallChannelDisabled = (serverUrl: string, msg: WebSocketMessa
};
export const handleCallScreenOn = (serverUrl: string, msg: WebSocketMessage<UserScreenOnOffData>) => {
setCallScreenOn(serverUrl, msg.broadcast.channel_id, msg.data.userID);
// pre v0.21.0, sessionID == userID
setCallScreenOn(serverUrl, msg.broadcast.channel_id, msg.data.session_id || msg.data.userID);
};
export const handleCallScreenOff = (serverUrl: string, msg: WebSocketMessage<UserScreenOnOffData>) => {
setCallScreenOff(serverUrl, msg.broadcast.channel_id);
// pre v0.21.0, sessionID == userID
setCallScreenOff(serverUrl, msg.broadcast.channel_id, msg.data.session_id || msg.data.userID);
};
export const handleCallUserRaiseHand = (serverUrl: string, msg: WebSocketMessage<UserRaiseUnraiseHandData>) => {
setRaisedHand(serverUrl, msg.broadcast.channel_id, msg.data.userID, msg.data.raised_hand);
// pre v0.21.0, sessionID == userID
setRaisedHand(serverUrl, msg.broadcast.channel_id, msg.data.session_id || msg.data.userID, msg.data.raised_hand);
};
export const handleCallUserUnraiseHand = (serverUrl: string, msg: WebSocketMessage<UserRaiseUnraiseHandData>) => {
setRaisedHand(serverUrl, msg.broadcast.channel_id, msg.data.userID, msg.data.raised_hand);
// pre v0.21.0, sessionID == userID
setRaisedHand(serverUrl, msg.broadcast.channel_id, msg.data.session_id || msg.data.userID, msg.data.raised_hand);
};
export const handleCallUserReacted = (serverUrl: string, msg: WebSocketMessage<UserReactionData>) => {
// pre v0.21.0, sessionID == userID
if (!isMultiSessionSupported(getCallsConfig(serverUrl).version)) {
msg.data.session_id = msg.data.user_id;
}
userReacted(serverUrl, msg.broadcast.channel_id, msg.data);
};

View file

@ -3,11 +3,16 @@
import {distinctUntilChanged, switchMap, combineLatest, Observable, of as of$} from 'rxjs';
import {observeCallsConfig, observeCallsState} from '@calls/state';
import {observeCallsConfig, observeCallsState, observeCurrentCall} from '@calls/state';
import {fillUserModels, userIds} from '@calls/utils';
import {License} from '@constants';
import DatabaseManager from '@database/manager';
import {observeConfigValue, observeLicense} from '@queries/servers/system';
import {queryUsersById} from '@queries/servers/user';
import UserModel from '@typings/database/models/servers/user';
import {isMinimumServerVersion} from '@utils/helpers';
import type {CallSession} from '@calls/types/calls';
import type {Database} from '@nozbe/watermelondb';
export type LimitRestrictedInfo = {
@ -44,7 +49,7 @@ export const observeIsCallLimitRestricted = (database: Database, serverUrl: stri
distinctUntilChanged(),
);
const callNumOfParticipants = observeCallsState(serverUrl).pipe(
switchMap((cs) => of$(Object.keys(cs.calls[channelId]?.participants || {}).length)),
switchMap((cs) => of$(Object.keys(cs.calls[channelId]?.sessions || {}).length)),
distinctUntilChanged(),
);
const isCloud = observeLicense(database).pipe(
@ -65,3 +70,21 @@ export const observeIsCallLimitRestricted = (database: Database, serverUrl: stri
prev.limitRestricted === curr.limitRestricted && prev.maxParticipants === curr.maxParticipants && prev.isCloudStarter === curr.isCloudStarter),
) as Observable<LimitRestrictedInfo>;
};
export const observeCurrentSessionsDict = () => {
const currentCall = observeCurrentCall();
const database = currentCall.pipe(
switchMap((call) => of$(call ? call.serverUrl : '')),
distinctUntilChanged(),
switchMap((url) => of$(DatabaseManager.serverDatabases[url]?.database)),
);
return combineLatest([database, currentCall]).pipe(
switchMap(([db, call]) => (db && call ? queryUsersById(db, userIds(Object.values(call.sessions))).observeWithColumns(['nickname', 'username', 'first_name', 'last_name', 'last_picture_update']) : of$([])).pipe(
// We now have a UserModel[] one for each userId, but we need the session dictionary with user models
// eslint-disable-next-line max-nested-callbacks
switchMap((ps: UserModel[]) => of$(fillUserModels(call?.sessions || {}, ps))),
)),
) as Observable<Dictionary<CallSession>>;
};

View file

@ -41,7 +41,7 @@ import {
useCallsConfig,
useIncomingCalls,
} from '@calls/state';
import {getHandsRaised, makeCallsTheme, sortParticipants} from '@calls/utils';
import {getHandsRaised, makeCallsTheme, sortSessions} 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';
@ -68,7 +68,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername} from '@utils/user';
import type {CallParticipant, CallsTheme, CurrentCall} from '@calls/types/calls';
import type {CallSession, CallsTheme, CurrentCall} from '@calls/types/calls';
import type {AvailableScreens} from '@typings/screens/navigation';
const avatarL = 96;
@ -79,7 +79,7 @@ const usernameM = 92;
export type Props = {
componentId: AvailableScreens;
currentCall: CurrentCall | null;
participantsDict: Dictionary<CallParticipant>;
sessionsDict: Dictionary<CallSession>;
micPermissionsGranted: boolean;
teammateNameDisplay: string;
fromThreadScreen?: boolean;
@ -317,7 +317,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
const CallScreen = ({
componentId,
currentCall,
participantsDict,
sessionsDict,
micPermissionsGranted,
teammateNameDisplay,
fromThreadScreen,
@ -339,13 +339,13 @@ const CallScreen = ({
const [centerUsers, setCenterUsers] = useState(false);
const [layout, setLayout] = useState<LayoutRectangle | null>(null);
const myParticipant = currentCall?.participants[currentCall.myUserId];
const mySession = currentCall?.sessions[currentCall.mySessionId];
const micPermissionsError = !micPermissionsGranted && !currentCall?.micPermissionsErrorDismissed;
const screenShareOn = Boolean(currentCall?.screenOn);
const isLandscape = width > height;
const smallerAvatar = isLandscape || screenShareOn;
const avatarSize = smallerAvatar ? avatarM : avatarL;
const numParticipants = Object.keys(participantsDict).length;
const numSessions = Object.keys(sessionsDict).length;
const showIncomingCalls = incomingCalls.incomingCalls.length > 0;
const callThreadOptionTitle = intl.formatMessage({id: 'mobile.calls_call_thread', defaultMessage: 'Call Thread'});
@ -389,12 +389,12 @@ const CallScreen = ({
}, []);
const muteUnmuteHandler = useCallback(() => {
if (myParticipant?.muted) {
if (mySession?.muted) {
unmuteMyself();
} else {
muteMyself();
}
}, [myParticipant?.muted]);
}, [mySession?.muted]);
const toggleReactions = useCallback(() => {
setShowReactions((prev) => !prev);
@ -450,7 +450,7 @@ const CallScreen = ({
// 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 isHost = Boolean(currentCall?.hostId === mySession?.userId);
const recording = Boolean(currentCall?.recState?.start_at && !currentCall.recState.end_at);
if (recording) {
recordingAlert(isHost, intl);
@ -541,8 +541,8 @@ const CallScreen = ({
const avatarCellWidth = usernameSize + 20; // name width + padding
const perRow = Math.floor(layout.width / avatarCellWidth);
const totalHeight = Math.ceil(numParticipants / perRow) * avatarCellHeight;
const totalWidth = numParticipants * avatarCellWidth;
const totalHeight = Math.ceil(numSessions / perRow) * avatarCellHeight;
const totalWidth = numSessions * avatarCellWidth;
// If screenShareOn, we care about width, otherwise we care about height.
if ((screenShareOn && totalWidth > layout.width) || (!screenShareOn && totalHeight > layout.height)) {
@ -550,13 +550,13 @@ const CallScreen = ({
} else {
setCenterUsers(true);
}
}, [layout, numParticipants]);
}, [layout, numSessions]);
const onLayout = useCallback((e: LayoutChangeEvent) => {
setLayout(e.nativeEvent.layout);
}, []);
if (!currentCall || !myParticipant) {
if (!currentCall || !mySession) {
return null;
}
@ -576,7 +576,7 @@ const CallScreen = ({
<FormattedText
id={'mobile.calls_viewing_screen'}
defaultMessage={'You are viewing {name}\'s screen'}
values={{name: displayUsername(participantsDict[currentCall.screenOn].userModel, intl.locale, teammateNameDisplay)}}
values={{name: displayUsername(sessionsDict[currentCall.screenOn].userModel, intl.locale, teammateNameDisplay)}}
style={style.screenShareText}
/>
}
@ -584,8 +584,8 @@ const CallScreen = ({
);
}
const raisedHands = getHandsRaised(participantsDict);
const participants = sortParticipants(intl.locale, teammateNameDisplay, participantsDict, currentCall.screenOn);
const raisedHands = getHandsRaised(sessionsDict);
const sessions = sortSessions(intl.locale, teammateNameDisplay, sessionsDict, currentCall.screenOn);
let usersList = null;
if (!screenShareOn || !isLandscape) {
usersList = (
@ -601,20 +601,20 @@ const CallScreen = ({
onPress={toggleControlsInLandscape}
style={style.users}
>
{participants.map((user) => {
{sessions.map((sess) => {
return (
<View
style={[style.user, screenShareOn && style.userScreenOn]}
key={user.id}
key={sess.sessionId}
>
<View style={[screenShareOn && style.profileScreenOn]}>
<CallAvatar
userModel={user.userModel}
speaking={currentCall.voiceOn[user.id]}
muted={user.muted}
sharingScreen={user.id === currentCall.screenOn}
raisedHand={Boolean(user.raisedHand)}
reaction={user.reaction?.emoji}
userModel={sess.userModel}
speaking={currentCall.voiceOn[sess.sessionId]}
muted={sess.muted}
sharingScreen={sess.sessionId === currentCall.screenOn}
raisedHand={Boolean(sess.raisedHand)}
reaction={sess.reaction?.emoji}
size={avatarSize}
serverUrl={currentCall.serverUrl}
/>
@ -623,12 +623,12 @@ const CallScreen = ({
style={[style.username, smallerAvatar && style.usernameShort]}
numberOfLines={1}
>
{displayUsername(user.userModel, intl.locale, teammateNameDisplay)}
{user.id === myParticipant.id &&
{displayUsername(sess.userModel, intl.locale, teammateNameDisplay)}
{sess.sessionId === mySession.sessionId &&
` ${intl.formatMessage({id: 'mobile.calls_you', defaultMessage: '(you)'})}`
}
</Text>
{user.id === currentCall.hostId && <CallsBadge type={CallsBadgeType.Host}/>}
{sess.userId === currentCall.hostId && <CallsBadge type={CallsBadgeType.Host}/>}
</View>
);
})}
@ -668,7 +668,7 @@ const CallScreen = ({
/>
<RaisedHandBanner
raisedHands={raisedHands}
currentUserId={currentCall.myUserId}
sessionId={currentCall.mySessionId}
teammateNameDisplay={teammateNameDisplay}
/>
<Pressable
@ -729,22 +729,22 @@ const CallScreen = ({
]}
>
{showReactions &&
<ReactionBar raisedHand={myParticipant.raisedHand}/>
<ReactionBar raisedHand={mySession.raisedHand}/>
}
{!isLandscape &&
<Pressable
testID='mute-unmute'
style={[style.mute, myParticipant.muted && style.muteMuted]}
style={[style.mute, mySession.muted && style.muteMuted]}
onPress={muteUnmuteHandler}
disabled={!micPermissionsGranted}
>
<UnavailableIconWrapper
name={myParticipant.muted ? 'microphone-off' : 'microphone'}
name={mySession.muted ? 'microphone-off' : 'microphone'}
size={32}
unavailable={!micPermissionsGranted}
style={style.muteIcon}
/>
{myParticipant.muted ? UnmuteText : MuteText}
{mySession.muted ? UnmuteText : MuteText}
</Pressable>
}
<View style={[style.otherButtons, isLandscape && style.otherButtonsLandscape]}>
@ -813,18 +813,18 @@ const CallScreen = ({
onPress={muteUnmuteHandler}
>
<UnavailableIconWrapper
name={myParticipant.muted ? 'microphone-off' : 'microphone'}
name={mySession.muted ? 'microphone-off' : 'microphone'}
size={32}
unavailable={!micPermissionsGranted}
style={[
style.buttonIcon,
isLandscape && style.buttonIconLandscape,
style.muteIconLandscape,
myParticipant?.muted && style.muteIconLandscapeMuted,
mySession?.muted && style.muteIconLandscapeMuted,
]}
errorContainerStyle={isLandscape && style.errorContainerLandscape}
/>
{myParticipant.muted ? UnmuteText : MuteText}
{mySession.muted ? UnmuteText : MuteText}
</Pressable>
}
{(isLandscape || !isHost) &&

View file

@ -2,16 +2,14 @@
// See LICENSE.txt for license information.
import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {of as of$} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {observeCurrentSessionsDict} from '@calls/observers';
import CallScreen from '@calls/screens/call_screen/call_screen';
import {observeCurrentCall, observeGlobalCallsState} from '@calls/state';
import DatabaseManager from '@database/manager';
import {observeTeammateNameDisplay, queryUsersById} from '@queries/servers/user';
import type {CallParticipant} from '@calls/types/calls';
import type UserModel from '@typings/database/models/servers/user';
import {observeTeammateNameDisplay} from '@queries/servers/user';
const enhanced = withObservables([], () => {
const currentCall = observeCurrentCall();
@ -20,20 +18,6 @@ const enhanced = withObservables([], () => {
distinctUntilChanged(),
switchMap((url) => of$(DatabaseManager.serverDatabases[url]?.database)),
);
// TODO: to be optimized https://mattermost.atlassian.net/browse/MM-49338
const participantsDict = combineLatest([database, currentCall]).pipe(
switchMap(([db, call]) => (db && call ? queryUsersById(db, Object.keys(call.participants)).observeWithColumns(['nickname', 'username', 'first_name', 'last_name', 'last_picture_update']) : of$([])).pipe(
// eslint-disable-next-line max-nested-callbacks
switchMap((ps: UserModel[]) => of$(ps.reduce((accum, cur) => {
accum[cur.id] = {
...call!.participants[cur.id],
userModel: cur,
};
return accum;
}, {} as Dictionary<CallParticipant>))),
)),
);
const micPermissionsGranted = observeGlobalCallsState().pipe(
switchMap((gs) => of$(gs.micPermissionsGranted)),
distinctUntilChanged(),
@ -45,7 +29,7 @@ const enhanced = withObservables([], () => {
return {
currentCall,
participantsDict,
sessionsDict: observeCurrentSessionsDict(),
micPermissionsGranted,
teammateNameDisplay,
};

View file

@ -12,11 +12,11 @@ import {useTheme} from '@context/theme';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import type {CallParticipant, CallsTheme} from '@calls/types/calls';
import type {CallSession, CallsTheme} from '@calls/types/calls';
export type Props = {
raisedHands: CallParticipant[];
currentUserId: string;
raisedHands: CallSession[];
sessionId: string;
teammateNameDisplay: string;
}
@ -53,7 +53,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: CallsTheme) => ({
},
}));
export const RaisedHandBanner = ({raisedHands, currentUserId, teammateNameDisplay}: Props) => {
export const RaisedHandBanner = ({raisedHands, sessionId, teammateNameDisplay}: Props) => {
const intl = useIntl();
const theme = useTheme();
const callsTheme = useMemo(() => makeCallsTheme(theme), [theme]);
@ -63,7 +63,7 @@ export const RaisedHandBanner = ({raisedHands, currentUserId, teammateNameDispla
return <View style={style.raisedHandBannerContainer}/>;
}
const names = getHandsRaisedNames(raisedHands, currentUserId, intl.locale, teammateNameDisplay, intl);
const names = getHandsRaisedNames(raisedHands, sessionId, intl.locale, teammateNameDisplay, intl);
return (
<View style={style.raisedHandBannerContainer}>

View file

@ -102,9 +102,9 @@ jest.mock('react-native-navigation', () => ({
const call1: Call = {
id: 'call1',
participants: {
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
sessions: {
session1: {sessionId: 'session1', userId: 'user-1', muted: false, raisedHand: 0},
session2: {sessionId: 'session2', userId: 'user-2', muted: true, raisedHand: 0},
},
channelId: 'channel-1',
startTime: 123,
@ -116,9 +116,9 @@ const call1: Call = {
};
const call2: Call = {
id: 'call2',
participants: {
'user-3': {id: 'user-3', muted: false, raisedHand: 0},
'user-4': {id: 'user-4', muted: true, raisedHand: 0},
sessions: {
session3: {sessionId: 'session3', userId: 'user-3', muted: false, raisedHand: 0},
session4: {sessionId: 'session4', userId: 'user-4', muted: true, raisedHand: 0},
},
channelId: 'channel-2',
startTime: 123,
@ -130,9 +130,9 @@ const call2: Call = {
};
const call3: Call = {
id: 'call3',
participants: {
'user-5': {id: 'user-5', muted: false, raisedHand: 0},
'user-6': {id: 'user-6', muted: true, raisedHand: 0},
sessions: {
session5: {sessionId: 'session5', userId: 'user-5', muted: false, raisedHand: 0},
session6: {sessionId: 'session6', userId: 'user-6', muted: true, raisedHand: 0},
},
channelId: 'channel-3',
startTime: 123,
@ -144,8 +144,8 @@ const call3: Call = {
};
const callDM: Call = {
id: 'callDM',
participants: {
'user-5': {id: 'user-5', muted: false, raisedHand: 0},
sessions: {
session5: {sessionId: 'session5', userId: 'user-5', muted: false, raisedHand: 0},
},
channelId: 'channel-private',
startTime: 123,
@ -206,10 +206,10 @@ describe('useCallsState', () => {
};
const testNewCall1 = {
...call1,
participants: {
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
'user-3': {id: 'user-3', muted: false, raisedHand: 123},
sessions: {
session1: {sessionId: 'session1', userId: 'user-1', muted: false, raisedHand: 0},
session2: {sessionId: 'session2', userId: 'user-2', muted: true, raisedHand: 0},
session3: {sessionId: 'session3', userId: 'user-3', muted: false, raisedHand: 123},
},
};
const test = {
@ -279,10 +279,10 @@ describe('useCallsState', () => {
const expectedCallsState = {
'channel-1': {
id: 'call1',
participants: {
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
'user-3': {id: 'user-3', muted: true, raisedHand: 0},
sessions: {
session1: {sessionId: 'session1', userId: 'user-1', muted: false, raisedHand: 0},
session2: {sessionId: 'session2', userId: 'user-2', muted: true, raisedHand: 0},
session3: {sessionId: 'session3', userId: 'user-3', muted: true, raisedHand: 0},
},
channelId: 'channel-1',
startTime: 123,
@ -313,11 +313,11 @@ describe('useCallsState', () => {
assert.deepEqual(result.current[2], initialCurrentCallState);
// test
act(() => userJoinedCall('server1', 'channel-1', 'user-3'));
act(() => userJoinedCall('server1', 'channel-1', 'user-3', 'session3'));
assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState);
assert.deepEqual(result.current[1], expectedChannelsWithCallsState);
assert.deepEqual(result.current[2], expectedCurrentCallState);
act(() => userJoinedCall('server1', 'invalid-channel', 'user-1'));
act(() => userJoinedCall('server1', 'invalid-channel', 'user-1', 'session1'));
assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState);
assert.deepEqual(result.current[1], expectedChannelsWithCallsState);
assert.deepEqual(result.current[2], expectedCurrentCallState);
@ -341,8 +341,8 @@ describe('useCallsState', () => {
const expectedCallsState = {
'channel-1': {
id: 'call1',
participants: {
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
sessions: {
session2: {sessionId: 'session2', userId: 'user-2', muted: true, raisedHand: 0},
},
channelId: 'channel-1',
startTime: 123,
@ -373,11 +373,11 @@ describe('useCallsState', () => {
assert.deepEqual(result.current[2], initialCurrentCallState);
// test
act(() => userLeftCall('server1', 'channel-1', 'user-1'));
act(() => userLeftCall('server1', 'channel-1', 'session1'));
assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState);
assert.deepEqual(result.current[1], expectedChannelsWithCallsState);
assert.deepEqual(result.current[2], expectedCurrentCallState);
act(() => userLeftCall('server1', 'invalid-channel', 'user-2'));
act(() => userLeftCall('server1', 'invalid-channel', 'session2'));
assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState);
assert.deepEqual(result.current[1], expectedChannelsWithCallsState);
assert.deepEqual(result.current[2], expectedCurrentCallState);
@ -389,7 +389,7 @@ describe('useCallsState', () => {
calls: {
'channel-1': {
...call1,
screenOn: 'user-1',
screenOn: 'session1',
},
},
};
@ -402,13 +402,13 @@ describe('useCallsState', () => {
serverUrl: 'server1',
myUserId: 'myUserId',
...call1,
screenOn: 'user-1',
screenOn: 'session1',
};
const expectedCallsState = {
'channel-1': {
id: 'call1',
participants: {
'user-2': {id: 'user-2', muted: true, raisedHand: 0},
sessions: {
session2: {sessionId: 'session2', userId: 'user-2', muted: true, raisedHand: 0},
},
channelId: 'channel-1',
startTime: 123,
@ -439,7 +439,7 @@ describe('useCallsState', () => {
assert.deepEqual(result.current[2], initialCurrentCallState);
// test
act(() => userLeftCall('server1', 'channel-1', 'user-1'));
act(() => userLeftCall('server1', 'channel-1', 'session1'));
assert.deepEqual((result.current[0] as CallsState).calls, expectedCallsState);
assert.deepEqual(result.current[1], expectedChannelsWithCallsState);
assert.deepEqual(result.current[2], expectedCurrentCallState);
@ -534,22 +534,22 @@ describe('useCallsState', () => {
assert.deepEqual(result.current[2], initialCurrentCallState);
// test
act(() => setUserMuted('server1', 'channel-1', 'user-1', true));
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].participants['user-1'].muted, true);
assert.deepEqual((result.current[2] as CurrentCall | null)?.participants['user-1'].muted, true);
act(() => setUserMuted('server1', 'channel-1', 'session1', true));
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].sessions.session1.muted, true);
assert.deepEqual((result.current[2] as CurrentCall | null)?.sessions.session1.muted, true);
act(() => {
setUserMuted('server1', 'channel-1', 'user-1', false);
setUserMuted('server1', 'channel-1', 'user-2', false);
setUserMuted('server1', 'channel-1', 'session1', false);
setUserMuted('server1', 'channel-1', 'session2', false);
});
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].participants['user-1'].muted, false);
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].participants['user-2'].muted, false);
assert.deepEqual((result.current[2] as CurrentCall | null)?.participants['user-1'].muted, false);
assert.deepEqual((result.current[2] as CurrentCall | null)?.participants['user-2'].muted, false);
act(() => setUserMuted('server1', 'channel-1', 'user-2', true));
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].participants['user-2'].muted, true);
assert.deepEqual((result.current[2] as CurrentCall | null)?.participants['user-2'].muted, true);
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].sessions.session1.muted, false);
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].sessions.session2.muted, false);
assert.deepEqual((result.current[2] as CurrentCall | null)?.sessions.session1.muted, false);
assert.deepEqual((result.current[2] as CurrentCall | null)?.sessions.session2.muted, false);
act(() => setUserMuted('server1', 'channel-1', 'session2', true));
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].sessions.session2.muted, true);
assert.deepEqual((result.current[2] as CurrentCall | null)?.sessions.session2.muted, true);
assert.deepEqual(result.current[0], initialCallsState);
act(() => setUserMuted('server1', 'invalid-channel', 'user-1', true));
act(() => setUserMuted('server1', 'invalid-channel', 'session1', true));
assert.deepEqual(result.current[0], initialCallsState);
});
@ -580,11 +580,11 @@ describe('useCallsState', () => {
assert.deepEqual(result.current[2], initialCurrentCallState);
// test
act(() => setCallScreenOn('server1', 'channel-1', 'user-1'));
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].screenOn, 'user-1');
act(() => setCallScreenOn('server1', 'channel-1', 'session1'));
assert.deepEqual((result.current[0] as CallsState).calls['channel-1'].screenOn, 'session1');
assert.deepEqual(result.current[1], initialChannelsWithCallsState);
assert.deepEqual((result.current[2] as CurrentCall).screenOn, 'user-1');
act(() => setCallScreenOff('server1', 'channel-1'));
assert.deepEqual((result.current[2] as CurrentCall).screenOn, 'session1');
act(() => setCallScreenOff('server1', 'channel-1', 'session1'));
assert.deepEqual(result.current[0], initialCallsState);
assert.deepEqual(result.current[1], initialChannelsWithCallsState);
assert.deepEqual(result.current[2], initialCurrentCallState);
@ -592,7 +592,7 @@ describe('useCallsState', () => {
assert.deepEqual(result.current[0], initialCallsState);
assert.deepEqual(result.current[1], initialChannelsWithCallsState);
assert.deepEqual(result.current[2], initialCurrentCallState);
act(() => setCallScreenOff('server1', 'invalid-channel'));
act(() => setCallScreenOff('server1', 'invalid-channel', 'session1'));
assert.deepEqual(result.current[0], initialCallsState);
assert.deepEqual(result.current[1], initialChannelsWithCallsState);
assert.deepEqual(result.current[2], initialCurrentCallState);
@ -606,9 +606,9 @@ describe('useCallsState', () => {
const expectedCalls = {
'channel-1': {
id: 'call1',
participants: {
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
'user-2': {id: 'user-2', muted: true, raisedHand: 345},
sessions: {
session1: {sessionId: 'session1', userId: 'user-1', muted: false, raisedHand: 0},
session2: {sessionId: 'session2', userId: 'user-2', muted: true, raisedHand: 345},
},
channelId: 'channel-1',
startTime: 123,
@ -643,16 +643,16 @@ describe('useCallsState', () => {
assert.deepEqual(result.current[1], initialCurrentCallState);
// test
act(() => setRaisedHand('server1', 'channel-1', 'user-2', 345));
act(() => setRaisedHand('server1', 'channel-1', 'session2', 345));
assert.deepEqual((result.current[0] as CallsState).calls, expectedCalls);
assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState);
act(() => setRaisedHand('server1', 'invalid-channel', 'user-1', 345));
act(() => setRaisedHand('server1', 'invalid-channel', 'session1', 345));
assert.deepEqual((result.current[0] as CallsState).calls, expectedCalls);
assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState);
// unraise hand:
act(() => setRaisedHand('server1', 'channel-1', 'user-2', 0));
act(() => setRaisedHand('server1', 'channel-1', 'session2', 0));
assert.deepEqual(result.current[0], initialCallsState);
assert.deepEqual(result.current[1], initialCurrentCallState);
});
@ -665,9 +665,9 @@ describe('useCallsState', () => {
};
const newCall1 = {
...call1,
participants: {
...call1.participants,
myUserId: {id: 'myUserId', muted: true, raisedHand: 0},
sessions: {
...call1.sessions,
mySessionId: {sessionId: 'mySessionId', userId: 'myUserId', muted: true, raisedHand: 0},
},
};
const expectedCallsState = {
@ -682,6 +682,7 @@ describe('useCallsState', () => {
connected: true,
serverUrl: 'server1',
myUserId: 'myUserId',
mySessionId: 'mySessionId',
...newCall1,
};
@ -696,14 +697,14 @@ describe('useCallsState', () => {
// test
act(() => {
newCurrentCall('server1', 'channel-1', 'myUserId');
userJoinedCall('server1', 'channel-1', 'myUserId');
userJoinedCall('server1', 'channel-1', 'myUserId', 'mySessionId');
});
assert.deepEqual(result.current[0], expectedCallsState);
assert.deepEqual(result.current[1], expectedCurrentCallState);
act(() => {
myselfLeftCall();
userLeftCall('server1', 'channel-1', 'myUserId');
userLeftCall('server1', 'channel-1', 'mySessionId');
});
assert.deepEqual(result.current[0], initialCallsState);
assert.deepEqual(result.current[1], null);
@ -768,14 +769,14 @@ describe('useCallsState', () => {
// test joining a call and setting url:
act(() => newCurrentCall('server1', 'channel-1', 'myUserId'));
act(() => userJoinedCall('server1', 'channel-1', 'myUserId'));
act(() => userJoinedCall('server1', 'channel-1', 'myUserId', 'mySessionId'));
assert.deepEqual((result.current[1] as CurrentCall | null)?.screenShareURL, '');
act(() => setScreenShareURL('testUrl'));
assert.deepEqual((result.current[1] as CurrentCall | null)?.screenShareURL, 'testUrl');
act(() => {
myselfLeftCall();
userLeftCall('server1', 'channel-1', 'myUserId');
userLeftCall('server1', 'channel-1', 'mySessionId');
setScreenShareURL('test');
});
assert.deepEqual(result.current[0], initialCallsState);
@ -790,9 +791,9 @@ describe('useCallsState', () => {
};
const newCall1 = {
...call1,
participants: {
...call1.participants,
myUserId: {id: 'myUserId', muted: true, raisedHand: 0},
sessions: {
...call1.sessions,
mySessionId: {sessionId: 'mySessionId', userId: 'myUserId', muted: true, raisedHand: 0},
},
};
const expectedCallsState = {
@ -813,7 +814,7 @@ describe('useCallsState', () => {
// test
act(() => newCurrentCall('server1', 'channel-1', 'myUserId'));
act(() => userJoinedCall('server1', 'channel-1', 'myUserId'));
act(() => userJoinedCall('server1', 'channel-1', 'myUserId', 'mySessionId'));
assert.deepEqual((result.current[1] as CurrentCall | null)?.speakerphoneOn, false);
act(() => setSpeakerPhone(true));
assert.deepEqual((result.current[1] as CurrentCall | null)?.speakerphoneOn, true);
@ -836,9 +837,9 @@ describe('useCallsState', () => {
};
const newCall1 = {
...call1,
participants: {
...call1.participants,
myUserId: {id: 'myUserId', muted: true, raisedHand: 0},
sessions: {
...call1.sessions,
mySessionId: {sessionId: 'mySessionId', userId: 'myUserId', muted: true, raisedHand: 0},
},
};
const expectedCallsState = {
@ -868,7 +869,7 @@ describe('useCallsState', () => {
// test
act(() => newCurrentCall('server1', 'channel-1', 'myUserId'));
act(() => userJoinedCall('server1', 'channel-1', 'myUserId'));
act(() => userJoinedCall('server1', 'channel-1', 'myUserId', 'mySessionId'));
assert.deepEqual((result.current[1] as CurrentCall | null)?.audioDeviceInfo, defaultAudioDeviceInfo);
act(() => setAudioDeviceInfo(newAudioDeviceInfo));
assert.deepEqual((result.current[1] as CurrentCall | null)?.audioDeviceInfo, newAudioDeviceInfo);
@ -890,9 +891,9 @@ describe('useCallsState', () => {
};
const newCall1: Call = {
...call1,
participants: {
...call1.participants,
myUserId: {id: 'myUserId', muted: true, raisedHand: 0},
sessions: {
...call1.sessions,
mySessionId: {sessionId: 'mySessionId', userId: 'myUserId', muted: true, raisedHand: 0},
},
};
const expectedCallsState: CallsState = {
@ -906,6 +907,7 @@ describe('useCallsState', () => {
...DefaultCurrentCall,
serverUrl: 'server1',
myUserId: 'myUserId',
mySessionId: 'mySessionId',
connected: true,
...newCall1,
};
@ -930,7 +932,7 @@ describe('useCallsState', () => {
act(() => {
setMicPermissionsGranted(false);
newCurrentCall('server1', 'channel-1', 'myUserId');
userJoinedCall('server1', 'channel-1', 'myUserId');
userJoinedCall('server1', 'channel-1', 'myUserId', 'mySessionId');
});
assert.deepEqual(result.current[0], expectedCallsState);
assert.deepEqual(result.current[1], expectedCurrentCallState);
@ -950,7 +952,7 @@ describe('useCallsState', () => {
act(() => {
myselfLeftCall();
userLeftCall('server1', 'channel-1', 'myUserId');
userLeftCall('server1', 'channel-1', 'mySessionId');
});
assert.deepEqual(result.current[0], initialCallsState);
assert.deepEqual(result.current[1], null);
@ -964,9 +966,9 @@ describe('useCallsState', () => {
};
const newCall1: Call = {
...call1,
participants: {
...call1.participants,
myUserId: {id: 'myUserId', muted: true, raisedHand: 0},
sessions: {
...call1.sessions,
mySessionId: {sessionId: 'mySessionId', userId: 'myUserId', muted: true, raisedHand: 0},
},
};
const expectedCallsState: CallsState = {
@ -980,6 +982,7 @@ describe('useCallsState', () => {
...DefaultCurrentCall,
serverUrl: 'server1',
myUserId: 'myUserId',
mySessionId: 'mySessionId',
connected: true,
...newCall1,
};
@ -995,7 +998,7 @@ describe('useCallsState', () => {
// join call
act(() => {
newCurrentCall('server1', 'channel-1', 'myUserId');
userJoinedCall('server1', 'channel-1', 'myUserId');
userJoinedCall('server1', 'channel-1', 'myUserId', 'mySessionId');
});
assert.deepEqual(result.current[0], expectedCallsState);
assert.deepEqual(result.current[1], currentCallNoAlertNoDismissed);
@ -1058,14 +1061,14 @@ describe('useCallsState', () => {
assert.deepEqual(result.current[1], initialCurrentCallState);
// test
act(() => setUserVoiceOn('channel-1', 'user-1', true));
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {'user-1': true}});
act(() => setUserVoiceOn('channel-1', 'session1', true));
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {session1: true}});
assert.deepEqual(result.current[0], initialCallsState);
act(() => setUserVoiceOn('channel-1', 'user-2', true));
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {'user-1': true, 'user-2': true}});
act(() => setUserVoiceOn('channel-1', 'session2', true));
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {session1: true, session2: true}});
assert.deepEqual(result.current[0], initialCallsState);
act(() => setUserVoiceOn('channel-1', 'user-1', false));
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {'user-2': true}});
act(() => setUserVoiceOn('channel-1', 'session1', false));
assert.deepEqual(result.current[1], {...initialCurrentCallState, voiceOn: {session2: true}});
assert.deepEqual(result.current[0], initialCallsState);
// test that voice state is cleared on reconnect
@ -1128,22 +1131,24 @@ describe('useCallsState', () => {
{name: 'smile', latestTimestamp: 202, count: 1, literal: undefined},
{name: '+1', latestTimestamp: 145, count: 2, literal: undefined},
],
participants: {
...initialCurrentCallState.participants,
'user-1': {
...initialCurrentCallState.participants['user-1'],
sessions: {
...initialCurrentCallState.sessions,
session1: {
...initialCurrentCallState.sessions.session1,
reaction: {
user_id: 'user-1',
session_id: 'session1',
emoji: {name: 'smile', unified: 'something'},
timestamp: 202,
},
},
'user-2': {
...initialCurrentCallState.participants['user-2'],
session2: {
...initialCurrentCallState.sessions.session2,
reaction: {
user_id: 'user-2',
session_id: 'session2',
emoji: {name: '+1', unified: 'something'},
timestamp: 123,
timestamp: 145,
},
},
},
@ -1164,16 +1169,19 @@ describe('useCallsState', () => {
act(() => {
userReacted('server1', 'channel-1', {
user_id: 'user-2',
session_id: 'session2',
emoji: {name: '+1', unified: 'something'},
timestamp: 123,
});
userReacted('server1', 'channel-1', {
user_id: 'user-1',
user_id: 'user-2',
session_id: 'session2',
emoji: {name: '+1', unified: 'something'},
timestamp: 145,
});
userReacted('server1', 'channel-1', {
user_id: 'user-1',
session_id: 'session1',
emoji: {name: 'smile', unified: 'something'},
timestamp: 202,
});
@ -1319,8 +1327,8 @@ describe('useCallsState', () => {
};
const callIStarted: Call = {
id: 'callIStartedid',
participants: {
'user-5': {id: 'user-5', muted: false, raisedHand: 0},
sessions: {
session5: {sessionId: 'session5', userId: 'user-5', muted: false, raisedHand: 0},
},
channelId: 'channel-private2',
startTime: 123,
@ -1332,7 +1340,7 @@ describe('useCallsState', () => {
};
const callImIn: Call = {
id: 'callImInId',
participants: {},
sessions: {},
channelId: 'channel-private2',
startTime: 123,
screenOn: '',

View file

@ -219,7 +219,7 @@ export const setCallForChannel = (serverUrl: string, channelId: string, enabled?
}
};
export const userJoinedCall = (serverUrl: string, channelId: string, userId: string) => {
export const userJoinedCall = (serverUrl: string, channelId: string, userId: string, sessionId: string) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId]) {
return;
@ -227,10 +227,11 @@ export const userJoinedCall = (serverUrl: string, channelId: string, userId: str
const nextCall = {
...callsState.calls[channelId],
participants: {...callsState.calls[channelId].participants},
sessions: {...callsState.calls[channelId].sessions},
};
nextCall.participants[userId] = {
id: userId,
nextCall.sessions[sessionId] = {
userId,
sessionId,
muted: true,
raisedHand: 0,
};
@ -242,46 +243,48 @@ export const userJoinedCall = (serverUrl: string, channelId: string, userId: str
const currentCall = getCurrentCall();
if (currentCall && currentCall.channelId === channelId) {
const voiceOn = {...currentCall.voiceOn};
delete voiceOn[userId];
delete voiceOn[sessionId];
const nextCurrentCall = {
...currentCall,
participants: {...currentCall.participants, [userId]: nextCall.participants[userId]},
sessions: {...currentCall.sessions, [sessionId]: nextCall.sessions[sessionId]},
voiceOn,
};
// If this is the currentUser, that means we've connected to the call we created.
if (userId === nextCurrentCall.myUserId) {
if (userId === nextCurrentCall.myUserId && !nextCurrentCall.connected) {
nextCurrentCall.connected = true;
nextCurrentCall.mySessionId = sessionId;
}
setCurrentCall(nextCurrentCall);
}
// We've joined (from whatever client), so remove that call's notification
if (userId === callsState.myUserId) {
removeIncomingCall(serverUrl, callsState.calls[channelId].id, channelId);
}
};
export const userLeftCall = (serverUrl: string, channelId: string, userId: string) => {
export const userLeftCall = (serverUrl: string, channelId: string, sessionId: string) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId]?.participants[userId]) {
if (!callsState.calls[channelId]?.sessions[sessionId]) {
return;
}
const nextCall = {
...callsState.calls[channelId],
participants: {...callsState.calls[channelId].participants},
sessions: {...callsState.calls[channelId].sessions},
};
delete nextCall.participants[userId];
delete nextCall.sessions[sessionId];
// If they were screensharing, remove that.
if (nextCall.screenOn === userId) {
if (nextCall.screenOn === sessionId) {
nextCall.screenOn = '';
}
const nextCalls = {...callsState.calls};
if (Object.keys(nextCall.participants).length === 0) {
if (Object.keys(nextCall.sessions).length === 0) {
delete nextCalls[channelId];
const callId = callsState.calls[channelId].id;
@ -303,25 +306,24 @@ export const userLeftCall = (serverUrl: string, channelId: string, userId: strin
return;
}
// Was the user me?
if (userId === callsState.myUserId) {
if (sessionId === currentCall.mySessionId) {
myselfLeftCall();
return;
}
// Clear them from the voice list
const voiceOn = {...currentCall.voiceOn};
delete voiceOn[userId];
delete voiceOn[sessionId];
const nextCurrentCall = {
...currentCall,
participants: {...currentCall.participants},
sessions: {...currentCall.sessions},
voiceOn,
};
delete nextCurrentCall.participants[userId];
delete nextCurrentCall.sessions[sessionId];
// If they were screensharing, remove that.
if (nextCurrentCall.screenOn === userId) {
if (nextCurrentCall.screenOn === sessionId) {
nextCurrentCall.screenOn = '';
}
@ -407,18 +409,18 @@ export const callEnded = (serverUrl: string, channelId: string) => {
// currentCall is set to null by the disconnect.
};
export const setUserMuted = (serverUrl: string, channelId: string, userId: string, muted: boolean) => {
export const setUserMuted = (serverUrl: string, channelId: string, sessionId: string, muted: boolean) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId] || !callsState.calls[channelId].participants[userId]) {
if (!callsState.calls[channelId] || !callsState.calls[channelId].sessions[sessionId]) {
return;
}
const nextUser = {...callsState.calls[channelId].participants[userId], muted};
const nextUser = {...callsState.calls[channelId].sessions[sessionId], muted};
const nextCall = {
...callsState.calls[channelId],
participants: {...callsState.calls[channelId].participants},
sessions: {...callsState.calls[channelId].sessions},
};
nextCall.participants[userId] = nextUser;
nextCall.sessions[sessionId] = nextUser;
const nextCalls = {...callsState.calls};
nextCalls[channelId] = nextCall;
setCallsState(serverUrl, {...callsState, calls: nextCalls});
@ -431,15 +433,15 @@ export const setUserMuted = (serverUrl: string, channelId: string, userId: strin
const nextCurrentCall = {
...currentCall,
participants: {
...currentCall.participants,
[userId]: {...currentCall.participants[userId], muted},
sessions: {
...currentCall.sessions,
[sessionId]: {...currentCall.sessions[sessionId], muted},
},
};
setCurrentCall(nextCurrentCall);
};
export const setUserVoiceOn = (channelId: string, userId: string, voiceOn: boolean) => {
export const setUserVoiceOn = (channelId: string, sessionId: string, voiceOn: boolean) => {
const currentCall = getCurrentCall();
if (!currentCall || currentCall.channelId !== channelId) {
return;
@ -447,9 +449,9 @@ export const setUserVoiceOn = (channelId: string, userId: string, voiceOn: boole
const nextVoiceOn = {...currentCall.voiceOn};
if (voiceOn) {
nextVoiceOn[userId] = true;
nextVoiceOn[sessionId] = true;
} else {
delete nextVoiceOn[userId];
delete nextVoiceOn[sessionId];
}
const nextCurrentCall = {
@ -459,18 +461,18 @@ export const setUserVoiceOn = (channelId: string, userId: string, voiceOn: boole
setCurrentCall(nextCurrentCall);
};
export const setRaisedHand = (serverUrl: string, channelId: string, userId: string, timestamp: number) => {
export const setRaisedHand = (serverUrl: string, channelId: string, sessionId: string, timestamp: number) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId] || !callsState.calls[channelId].participants[userId]) {
if (!callsState.calls[channelId] || !callsState.calls[channelId].sessions[sessionId]) {
return;
}
const nextUser = {...callsState.calls[channelId].participants[userId], raisedHand: timestamp};
const nextUser = {...callsState.calls[channelId].sessions[sessionId], raisedHand: timestamp};
const nextCall = {
...callsState.calls[channelId],
participants: {...callsState.calls[channelId].participants},
sessions: {...callsState.calls[channelId].sessions},
};
nextCall.participants[userId] = nextUser;
nextCall.sessions[sessionId] = nextUser;
const nextCalls = {...callsState.calls};
nextCalls[channelId] = nextCall;
setCallsState(serverUrl, {...callsState, calls: nextCalls});
@ -483,21 +485,21 @@ export const setRaisedHand = (serverUrl: string, channelId: string, userId: stri
const nextCurrentCall = {
...currentCall,
participants: {
...currentCall.participants,
[userId]: {...currentCall.participants[userId], raisedHand: timestamp},
sessions: {
...currentCall.sessions,
[sessionId]: {...currentCall.sessions[sessionId], raisedHand: timestamp},
},
};
setCurrentCall(nextCurrentCall);
};
export const setCallScreenOn = (serverUrl: string, channelId: string, userId: string) => {
export const setCallScreenOn = (serverUrl: string, channelId: string, sessionId: string) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId] || !callsState.calls[channelId].participants[userId]) {
if (!callsState.calls[channelId] || !callsState.calls[channelId].sessions[sessionId]) {
return;
}
const nextCall = {...callsState.calls[channelId], screenOn: userId};
const nextCall = {...callsState.calls[channelId], screenOn: sessionId};
const nextCalls = {...callsState.calls};
nextCalls[channelId] = nextCall;
setCallsState(serverUrl, {...callsState, calls: nextCalls});
@ -510,14 +512,14 @@ export const setCallScreenOn = (serverUrl: string, channelId: string, userId: st
const nextCurrentCall = {
...currentCall,
screenOn: userId,
screenOn: sessionId,
};
setCurrentCall(nextCurrentCall);
};
export const setCallScreenOff = (serverUrl: string, channelId: string) => {
export const setCallScreenOff = (serverUrl: string, channelId: string, sessionId: string) => {
const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId]) {
if (!callsState.calls[channelId] || callsState.calls[channelId].screenOn !== sessionId) {
return;
}
@ -634,16 +636,16 @@ export const userReacted = (serverUrl: string, channelId: string, reaction: User
}
// Update the participant.
const nextParticipants = {...currentCall.participants};
if (nextParticipants[reaction.user_id]) {
const nextUser = {...nextParticipants[reaction.user_id], reaction};
nextParticipants[reaction.user_id] = nextUser;
const nextSessions = {...currentCall.sessions};
if (nextSessions[reaction.session_id]) {
const nextUser = {...nextSessions[reaction.session_id], reaction};
nextSessions[reaction.session_id] = nextUser;
}
const nextCurrentCall: CurrentCall = {
...currentCall,
reactionStream: newReactionStream,
participants: nextParticipants,
sessions: nextSessions,
};
setCurrentCall(nextCurrentCall);
@ -661,17 +663,17 @@ const userReactionTimeout = (serverUrl: string, channelId: string, reaction: Use
// Remove the reaction only if it was the last time that emoji was used.
const newReactions = currentCall.reactionStream.filter((e) => e.latestTimestamp !== reaction.timestamp);
const nextParticipants = {...currentCall.participants};
if (nextParticipants[reaction.user_id] && nextParticipants[reaction.user_id].reaction?.timestamp === reaction.timestamp) {
const nextUser = {...nextParticipants[reaction.user_id]};
const nextSessions = {...currentCall.sessions};
if (nextSessions[reaction.session_id] && nextSessions[reaction.session_id].reaction?.timestamp === reaction.timestamp) {
const nextUser = {...nextSessions[reaction.session_id]};
delete nextUser.reaction;
nextParticipants[reaction.user_id] = nextUser;
nextSessions[reaction.session_id] = nextUser;
}
const nextCurrentCall: CurrentCall = {
...currentCall,
reactionStream: newReactions,
participants: nextParticipants,
sessions: nextSessions,
};
setCurrentCall(nextCurrentCall);
};

View file

@ -50,7 +50,7 @@ export const DefaultIncomingCalls: IncomingCalls = {
export type Call = {
id: string;
participants: Dictionary<CallParticipant>;
sessions: Dictionary<CallSession>;
channelId: string;
startTime: number;
screenOn: string;
@ -63,7 +63,7 @@ export type Call = {
export const DefaultCall: Call = {
id: '',
participants: {},
sessions: {},
channelId: '',
startTime: 0,
screenOn: '',
@ -85,6 +85,7 @@ export type CurrentCall = Call & {
connected: boolean;
serverUrl: string;
myUserId: string;
mySessionId: string;
screenShareURL: string;
speakerphoneOn: boolean;
audioDeviceInfo: AudioDeviceInfo;
@ -100,6 +101,7 @@ export const DefaultCurrentCall: CurrentCall = {
connected: false,
serverUrl: '',
myUserId: '',
mySessionId: '',
screenShareURL: '',
speakerphoneOn: false,
audioDeviceInfo: {availableAudioDeviceList: [], selectedAudioDevice: AudioDevice.None},
@ -110,8 +112,9 @@ export const DefaultCurrentCall: CurrentCall = {
callQualityAlertDismissed: 0,
};
export type CallParticipant = {
id: string;
export type CallSession = {
sessionId: string;
userId: string;
muted: boolean;
raisedHand: number;
userModel?: UserModel;
@ -134,11 +137,13 @@ export type CallsConnection = {
export type CallsConfigState = CallsConfig & {
AllowEnableCalls: boolean;
pluginEnabled: boolean;
version: CallsVersion;
last_retrieved_at: number;
}
export const DefaultCallsConfig: CallsConfigState = {
pluginEnabled: false,
version: {},
ICEServers: [], // deprecated
ICEServersConfigs: [],
AllowEnableCalls: false,
@ -152,6 +157,7 @@ export const DefaultCallsConfig: CallsConfigState = {
AllowScreenSharing: true,
EnableSimulcast: false,
EnableRinging: false,
EnableTranscriptions: false,
};
export type ApiResp = {
@ -182,3 +188,8 @@ export type AudioDeviceInfo = {
availableAudioDeviceList: AudioDevice[];
selectedAudioDevice: AudioDevice;
};
export type CallsVersion = {
version?: string;
build?: string;
};

View file

@ -9,26 +9,27 @@ import {NOTIFICATION_SUB_TYPE} from '@constants/push_notification';
import {isMinimumServerVersion} from '@utils/helpers';
import {displayUsername} from '@utils/user';
import type {CallParticipant, CallsTheme} from '@calls/types/calls';
import type {CallSession, CallsTheme, CallsVersion} from '@calls/types/calls';
import type {CallsConfig} from '@mattermost/calls/lib/types';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {IntlShape} from 'react-intl';
import type {RTCIceServer} from 'react-native-webrtc';
const callsMessageRegex = /^\u200b.* is inviting you to a call$/;
export function sortParticipants(locale: string, teammateNameDisplay: string, participants?: Dictionary<CallParticipant>, presenterID?: string): CallParticipant[] {
if (!participants) {
export function sortSessions(locale: string, teammateNameDisplay: string, sessions?: Dictionary<CallSession>, presenterID?: string): CallSession[] {
if (!sessions) {
return [];
}
const users = Object.values(participants);
const sessns = Object.values(sessions);
return users.sort(sortByName(locale, teammateNameDisplay)).sort(sortByState(presenterID));
return sessns.sort(sortByName(locale, teammateNameDisplay)).sort(sortByState(presenterID));
}
const sortByName = (locale: string, teammateNameDisplay: string) => {
return (a: CallParticipant, b: CallParticipant) => {
return (a: CallSession, b: CallSession) => {
const nameA = displayUsername(a.userModel, locale, teammateNameDisplay);
const nameB = displayUsername(b.userModel, locale, teammateNameDisplay);
return nameA.localeCompare(nameB);
@ -36,10 +37,10 @@ const sortByName = (locale: string, teammateNameDisplay: string) => {
};
const sortByState = (presenterID?: string) => {
return (a: CallParticipant, b: CallParticipant) => {
if (a.id === presenterID) {
return (a: CallSession, b: CallSession) => {
if (a.sessionId === presenterID) {
return -1;
} else if (b.id === presenterID) {
} else if (b.sessionId === presenterID) {
return 1;
}
@ -61,13 +62,13 @@ const sortByState = (presenterID?: string) => {
};
};
export function getHandsRaised(participants: Dictionary<CallParticipant>) {
return Object.values(participants).filter((p) => p.raisedHand);
export function getHandsRaised(sessions: Dictionary<CallSession>) {
return Object.values(sessions).filter((s) => s.raisedHand);
}
export function getHandsRaisedNames(participants: CallParticipant[], currentUserId: string, locale: string, teammateNameDisplay: string, intl: IntlShape) {
return participants.sort((a, b) => a.raisedHand - b.raisedHand).map((p) => {
if (p.id === currentUserId) {
export function getHandsRaisedNames(sessions: CallSession[], sessionId: string, locale: string, teammateNameDisplay: string, intl: IntlShape) {
return sessions.sort((a, b) => a.raisedHand - b.raisedHand).map((p) => {
if (p.sessionId === sessionId) {
return intl.formatMessage({id: 'mobile.calls_you_2', defaultMessage: 'You'});
}
return displayUsername(p.userModel, locale, teammateNameDisplay);
@ -87,6 +88,15 @@ export function isSupportedServerCalls(serverVersion?: string) {
return false;
}
export function isMultiSessionSupported(callsVersion: CallsVersion) {
return isMinimumServerVersion(
callsVersion.version,
Calls.MultiSessionCallsVersion.MAJOR_VERSION,
Calls.MultiSessionCallsVersion.MIN_VERSION,
Calls.MultiSessionCallsVersion.PATCH_VERSION,
);
}
export function isCallsCustomMessage(post: PostModel | Post): boolean {
return Boolean(post.type && post.type === Post.POST_TYPES.CUSTOM_CALLS);
}
@ -154,6 +164,34 @@ export function makeCallsTheme(theme: Theme): CallsTheme {
return newTheme;
}
interface HasUserId {
userId: string;
}
export function userIds<T extends HasUserId>(hasUserId: T[]): string[] {
const ids: string[] = [];
const seen: Record<string, boolean> = {};
for (const p of hasUserId) {
if (!seen[p.userId]) {
ids.push(p.userId);
seen[p.userId] = true;
}
}
return ids;
}
export function fillUserModels(sessions: Dictionary<CallSession>, models: UserModel[]) {
const idToModel = models.reduce((accum, cur) => {
accum[cur.id] = cur;
return accum;
}, {} as Dictionary<UserModel>);
const next = {...sessions};
for (const participant of Object.values(next)) {
participant.userModel = idToModel[participant.userId];
}
return sessions;
}
export function isCallsStartedMessage(payload?: NotificationData) {
if (payload?.sub_type === NOTIFICATION_SUB_TYPE.CALLS) {
return true;

8
package-lock.json generated
View file

@ -17,7 +17,7 @@
"@formatjs/intl-pluralrules": "5.2.4",
"@formatjs/intl-relativetimeformat": "11.2.4",
"@gorhom/bottom-sheet": "4.4.7",
"@mattermost/calls": "github:mattermost/calls-common#v0.17.0",
"@mattermost/calls": "github:mattermost/calls-common#v0.21.0",
"@mattermost/compass-icons": "0.1.38",
"@mattermost/react-native-emm": "1.3.5",
"@mattermost/react-native-network-client": "1.4.1",
@ -3406,7 +3406,7 @@
"node_modules/@mattermost/calls": {
"name": "@calls/common",
"version": "0.14.0",
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#43011d8e9c7985b0542628329be098715a43dc03"
"resolved": "git+ssh://git@github.com/mattermost/calls-common.git#4bca3651b2eb5d46fab9a29af000d21d9f56727c"
},
"node_modules/@mattermost/commonmark": {
"version": "0.30.1-0",
@ -25387,8 +25387,8 @@
}
},
"@mattermost/calls": {
"version": "git+ssh://git@github.com/mattermost/calls-common.git#43011d8e9c7985b0542628329be098715a43dc03",
"from": "@mattermost/calls@github:mattermost/calls-common#v0.17.0"
"version": "git+ssh://git@github.com/mattermost/calls-common.git#4bca3651b2eb5d46fab9a29af000d21d9f56727c",
"from": "@mattermost/calls@github:mattermost/calls-common#v0.21.0"
},
"@mattermost/commonmark": {
"version": "0.30.1-0",

View file

@ -18,7 +18,7 @@
"@formatjs/intl-pluralrules": "5.2.4",
"@formatjs/intl-relativetimeformat": "11.2.4",
"@gorhom/bottom-sheet": "4.4.7",
"@mattermost/calls": "github:mattermost/calls-common#v0.17.0",
"@mattermost/calls": "github:mattermost/calls-common#v0.21.0",
"@mattermost/compass-icons": "0.1.38",
"@mattermost/react-native-emm": "1.3.5",
"@mattermost/react-native-network-client": "1.4.1",