diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 51467e5aa..35c72dbbf 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -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; diff --git a/app/constants/calls.ts b/app/constants/calls.ts index e9bc049fa..46003e170 100644 --- a/app/constants/calls.ts +++ b/app/constants/calls.ts @@ -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, diff --git a/app/constants/websocket.ts b/app/constants/websocket.ts index 454551067..51c745284 100644 --- a/app/constants/websocket.ts +++ b/app/constants/websocket.ts @@ -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`, diff --git a/app/products/calls/actions/calls.test.ts b/app/products/calls/actions/calls.test.ts index 15625e64a..1b2e35dba 100644 --- a/app/products/calls/actions/calls.test.ts +++ b/app/products/calls/actions/calls.test.ts @@ -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'); diff --git a/app/products/calls/actions/calls.ts b/app/products/calls/actions/calls.ts index 5d80e32f5..e9c2a3e23 100644 --- a/app/products/calls/actions/calls.ts +++ b/app/products/calls/actions/calls.ts @@ -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(); 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) => { 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), + }, {} as Dictionary), 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); diff --git a/app/products/calls/client/rest.ts b/app/products/calls/client/rest.ts index 8e88e4e0b..423ad7388 100644 --- a/app/products/calls/client/rest.ts +++ b/app/products/calls/client/rest.ts @@ -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; getCallForChannel: (channelId: string) => Promise; getCallsConfig: () => Promise; + getVersion: () => Promise; enableChannelCalls: (channelId: string, enable: boolean) => Promise; endCall: (channelId: string) => Promise; genTURNCredentials: () => Promise; @@ -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}`, diff --git a/app/products/calls/components/current_call_bar/current_call_bar.tsx b/app/products/calls/components/current_call_bar/current_call_bar.tsx index 2b705b605..b4e1d3c51 100644 --- a/app/products/calls/components/current_call_bar/current_call_bar.tsx +++ b/app/products/calls/components/current_call_bar/current_call_bar.tsx @@ -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; + sessionsDict: Dictionary; 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 = ( - {displayUsername(userModelsDict[speaker], intl.locale, teammateNameDisplay)} + {displayUsername(sessionsDict[speaker].userModel, intl.locale, teammateNameDisplay)} {' '} { 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 = ({ > { 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); -} - export default enhanced(CurrentCallBar); diff --git a/app/products/calls/components/join_call_banner/index.ts b/app/products/calls/components/join_call_banner/index.ts index 9ef76ff44..aa6f57188 100644 --- a/app/products/calls/components/join_call_banner/index.ts +++ b/app/products/calls/components/join_call_banner/index.ts @@ -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), }; diff --git a/app/products/calls/components/join_call_banner/join_call_banner.tsx b/app/products/calls/components/join_call_banner/join_call_banner.tsx index 7934c1a26..b31b78435 100644 --- a/app/products/calls/components/join_call_banner/join_call_banner.tsx +++ b/app/products/calls/components/join_call_banner/join_call_banner.tsx @@ -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 = ({ ) => { + 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) => { + 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) => { + // 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) => { + userLeftCall(serverUrl, msg.broadcast.channel_id, msg.data.session_id); +}; + export const handleCallUserMuted = (serverUrl: string, msg: WebSocketMessage) => { - 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) => { - 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) => { - 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) => { - 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) => { @@ -73,7 +103,7 @@ export const handleCallStarted = (serverUrl: string, msg: WebSocketMessage) => { - 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) => { - 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) => { - 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) => { - 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) => { + // 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); }; diff --git a/app/products/calls/observers/index.ts b/app/products/calls/observers/index.ts index 3acefafd3..a54bada61 100644 --- a/app/products/calls/observers/index.ts +++ b/app/products/calls/observers/index.ts @@ -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; }; + +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>; +}; diff --git a/app/products/calls/screens/call_screen/call_screen.tsx b/app/products/calls/screens/call_screen/call_screen.tsx index 313889e7c..acbaff5de 100644 --- a/app/products/calls/screens/call_screen/call_screen.tsx +++ b/app/products/calls/screens/call_screen/call_screen.tsx @@ -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; + sessionsDict: Dictionary; 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(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 = ({ } @@ -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 ( @@ -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)'})}` } - {user.id === currentCall.hostId && } + {sess.userId === currentCall.hostId && } ); })} @@ -668,7 +668,7 @@ const CallScreen = ({ /> {showReactions && - + } {!isLandscape && - {myParticipant.muted ? UnmuteText : MuteText} + {mySession.muted ? UnmuteText : MuteText} } @@ -813,18 +813,18 @@ const CallScreen = ({ onPress={muteUnmuteHandler} > - {myParticipant.muted ? UnmuteText : MuteText} + {mySession.muted ? UnmuteText : MuteText} } {(isLandscape || !isHost) && diff --git a/app/products/calls/screens/call_screen/index.ts b/app/products/calls/screens/call_screen/index.ts index 89fc899d4..3d9ccdf4e 100644 --- a/app/products/calls/screens/call_screen/index.ts +++ b/app/products/calls/screens/call_screen/index.ts @@ -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))), - )), - ); const micPermissionsGranted = observeGlobalCallsState().pipe( switchMap((gs) => of$(gs.micPermissionsGranted)), distinctUntilChanged(), @@ -45,7 +29,7 @@ const enhanced = withObservables([], () => { return { currentCall, - participantsDict, + sessionsDict: observeCurrentSessionsDict(), micPermissionsGranted, teammateNameDisplay, }; diff --git a/app/products/calls/screens/call_screen/raised_hand_banner.tsx b/app/products/calls/screens/call_screen/raised_hand_banner.tsx index fdbc5e3f8..ca9d1e739 100644 --- a/app/products/calls/screens/call_screen/raised_hand_banner.tsx +++ b/app/products/calls/screens/call_screen/raised_hand_banner.tsx @@ -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 ; } - const names = getHandsRaisedNames(raisedHands, currentUserId, intl.locale, teammateNameDisplay, intl); + const names = getHandsRaisedNames(raisedHands, sessionId, intl.locale, teammateNameDisplay, intl); return ( diff --git a/app/products/calls/state/actions.test.ts b/app/products/calls/state/actions.test.ts index 5f8078df9..db5d2d2f9 100644 --- a/app/products/calls/state/actions.test.ts +++ b/app/products/calls/state/actions.test.ts @@ -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: '', diff --git a/app/products/calls/state/actions.ts b/app/products/calls/state/actions.ts index f63e9fd6e..ea1d14498 100644 --- a/app/products/calls/state/actions.ts +++ b/app/products/calls/state/actions.ts @@ -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); }; diff --git a/app/products/calls/types/calls.ts b/app/products/calls/types/calls.ts index 963945fdd..c2bd051d0 100644 --- a/app/products/calls/types/calls.ts +++ b/app/products/calls/types/calls.ts @@ -50,7 +50,7 @@ export const DefaultIncomingCalls: IncomingCalls = { export type Call = { id: string; - participants: Dictionary; + sessions: Dictionary; 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; +}; diff --git a/app/products/calls/utils.ts b/app/products/calls/utils.ts index d7dabc34a..bcfd80a77 100644 --- a/app/products/calls/utils.ts +++ b/app/products/calls/utils.ts @@ -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, presenterID?: string): CallParticipant[] { - if (!participants) { +export function sortSessions(locale: string, teammateNameDisplay: string, sessions?: Dictionary, 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) { - return Object.values(participants).filter((p) => p.raisedHand); +export function getHandsRaised(sessions: Dictionary) { + 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(hasUserId: T[]): string[] { + const ids: string[] = []; + const seen: Record = {}; + for (const p of hasUserId) { + if (!seen[p.userId]) { + ids.push(p.userId); + seen[p.userId] = true; + } + } + return ids; +} + +export function fillUserModels(sessions: Dictionary, models: UserModel[]) { + const idToModel = models.reduce((accum, cur) => { + accum[cur.id] = cur; + return accum; + }, {} as Dictionary); + 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; diff --git a/package-lock.json b/package-lock.json index d4a5af51a..a75d2fce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b1ead55c6..f7091a568 100644 --- a/package.json +++ b/package.json @@ -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",