Group calling participants refactor
This commit is contained in:
parent
be99bbe87a
commit
c85ea814b1
18 changed files with 750 additions and 436 deletions
|
@ -9,16 +9,13 @@ import { missingCaseError } from '../../util/missingCaseError';
|
|||
import { notify } from '../../services/notify';
|
||||
import { calling } from '../../services/calling';
|
||||
import { StateType as RootStateType } from '../reducer';
|
||||
import { ConversationType } from './conversations';
|
||||
import {
|
||||
CallingDeviceType,
|
||||
CallMode,
|
||||
CallState,
|
||||
CallingDeviceType,
|
||||
ChangeIODevicePayloadType,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
GroupCallPeekedParticipantType,
|
||||
GroupCallRemoteParticipantType,
|
||||
GroupCallVideoRequest,
|
||||
MediaDeviceSettings,
|
||||
} from '../../types/Calling';
|
||||
|
@ -34,19 +31,18 @@ import { LatestQueue } from '../../util/LatestQueue';
|
|||
// State
|
||||
|
||||
export interface GroupCallPeekInfoType {
|
||||
conversationIds: Array<string>;
|
||||
creator?: string;
|
||||
uuids: Array<string>;
|
||||
creatorUuid?: string;
|
||||
eraId?: string;
|
||||
maxDevices: number;
|
||||
deviceCount: number;
|
||||
}
|
||||
|
||||
export interface GroupCallParticipantInfoType {
|
||||
conversationId: string;
|
||||
uuid: string;
|
||||
demuxId: number;
|
||||
hasRemoteAudio: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
isSelf: boolean;
|
||||
speakerTime?: number;
|
||||
videoAspectRatio: number;
|
||||
}
|
||||
|
@ -70,15 +66,6 @@ export interface GroupCallStateType {
|
|||
remoteParticipants: Array<GroupCallParticipantInfoType>;
|
||||
}
|
||||
|
||||
export interface ActiveCallType {
|
||||
activeCallState: ActiveCallStateType;
|
||||
call: DirectCallStateType | GroupCallStateType;
|
||||
conversation: ConversationType;
|
||||
isCallFull: boolean;
|
||||
groupCallPeekedParticipants: Array<GroupCallPeekedParticipantType>;
|
||||
groupCallParticipants: Array<GroupCallRemoteParticipantType>;
|
||||
}
|
||||
|
||||
export interface ActiveCallStateType {
|
||||
conversationId: string;
|
||||
joinedAt?: number;
|
||||
|
@ -127,12 +114,12 @@ type GroupCallStateChangeArgumentType = {
|
|||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
joinState: GroupCallJoinState;
|
||||
peekInfo: GroupCallPeekInfoType;
|
||||
peekInfo?: GroupCallPeekInfoType;
|
||||
remoteParticipants: Array<GroupCallParticipantInfoType>;
|
||||
};
|
||||
|
||||
type GroupCallStateChangeActionPayloadType = GroupCallStateChangeArgumentType & {
|
||||
ourConversationId: string;
|
||||
ourUuid: string;
|
||||
};
|
||||
|
||||
export type HangUpType = {
|
||||
|
@ -190,7 +177,7 @@ export type ShowCallLobbyType =
|
|||
joinState: GroupCallJoinState;
|
||||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
peekInfo: GroupCallPeekInfoType;
|
||||
peekInfo?: GroupCallPeekInfoType;
|
||||
remoteParticipants: Array<GroupCallParticipantInfoType>;
|
||||
};
|
||||
|
||||
|
@ -212,9 +199,9 @@ export const getActiveCall = ({
|
|||
getOwn(callsByConversation, activeCallState.conversationId);
|
||||
|
||||
export const isAnybodyElseInGroupCall = (
|
||||
{ conversationIds }: Readonly<GroupCallPeekInfoType>,
|
||||
ourConversationId: string
|
||||
): boolean => conversationIds.some(id => id !== ourConversationId);
|
||||
{ uuids }: Readonly<GroupCallPeekInfoType>,
|
||||
ourUuid: string
|
||||
): boolean => uuids.some(id => id !== ourUuid);
|
||||
|
||||
// Actions
|
||||
|
||||
|
@ -496,7 +483,7 @@ function groupCallStateChange(
|
|||
type: GROUP_CALL_STATE_CHANGE,
|
||||
payload: {
|
||||
...payload,
|
||||
ourConversationId: getState().user.ourConversationId,
|
||||
ourUuid: getState().user.ourUuid,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -808,6 +795,16 @@ export function getEmptyState(): CallingStateType {
|
|||
};
|
||||
}
|
||||
|
||||
function getExistingPeekInfo(
|
||||
conversationId: string,
|
||||
state: CallingStateType
|
||||
): undefined | GroupCallPeekInfoType {
|
||||
const existingCall = getOwn(state.callsByConversation, conversationId);
|
||||
return existingCall?.callMode === CallMode.Group
|
||||
? existingCall.peekInfo
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function removeConversationFromState(
|
||||
state: CallingStateType,
|
||||
conversationId: string
|
||||
|
@ -845,7 +842,12 @@ export function reducer(
|
|||
conversationId: action.payload.conversationId,
|
||||
connectionState: action.payload.connectionState,
|
||||
joinState: action.payload.joinState,
|
||||
peekInfo: action.payload.peekInfo,
|
||||
peekInfo: action.payload.peekInfo ||
|
||||
getExistingPeekInfo(action.payload.conversationId, state) || {
|
||||
uuids: action.payload.remoteParticipants.map(({ uuid }) => uuid),
|
||||
maxDevices: Infinity,
|
||||
deviceCount: action.payload.remoteParticipants.length,
|
||||
},
|
||||
remoteParticipants: action.payload.remoteParticipants,
|
||||
};
|
||||
break;
|
||||
|
@ -1030,11 +1032,18 @@ export function reducer(
|
|||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
joinState,
|
||||
ourConversationId,
|
||||
ourUuid,
|
||||
peekInfo,
|
||||
remoteParticipants,
|
||||
} = action.payload;
|
||||
|
||||
const newPeekInfo = peekInfo ||
|
||||
getExistingPeekInfo(conversationId, state) || {
|
||||
uuids: remoteParticipants.map(({ uuid }) => uuid),
|
||||
maxDevices: Infinity,
|
||||
deviceCount: remoteParticipants.length,
|
||||
};
|
||||
|
||||
let newActiveCallState: ActiveCallStateType | undefined;
|
||||
|
||||
if (connectionState === GroupCallConnectionState.NotConnected) {
|
||||
|
@ -1043,7 +1052,7 @@ export function reducer(
|
|||
? undefined
|
||||
: state.activeCallState;
|
||||
|
||||
if (!isAnybodyElseInGroupCall(peekInfo, ourConversationId)) {
|
||||
if (!isAnybodyElseInGroupCall(newPeekInfo, ourUuid)) {
|
||||
return {
|
||||
...state,
|
||||
callsByConversation: omit(callsByConversation, conversationId),
|
||||
|
@ -1070,7 +1079,7 @@ export function reducer(
|
|||
conversationId,
|
||||
connectionState,
|
||||
joinState,
|
||||
peekInfo,
|
||||
peekInfo: newPeekInfo,
|
||||
remoteParticipants,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,21 +3,23 @@
|
|||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { memoize } from 'lodash';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { CallManager } from '../../components/CallManager';
|
||||
import { calling as callingService } from '../../services/calling';
|
||||
import { getUserUuid, getIntl } from '../selectors/user';
|
||||
import { getMe, getConversationSelector } from '../selectors/conversations';
|
||||
import { getActiveCall, GroupCallParticipantInfoType } from '../ducks/calling';
|
||||
import { getActiveCall } from '../ducks/calling';
|
||||
import { ConversationType } from '../ducks/conversations';
|
||||
import { getIncomingCall } from '../selectors/calling';
|
||||
import {
|
||||
ActiveCallType,
|
||||
CallMode,
|
||||
GroupCallPeekedParticipantType,
|
||||
GroupCallRemoteParticipantType,
|
||||
} from '../../types/Calling';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
|
||||
|
||||
function renderDeviceSelection(): JSX.Element {
|
||||
|
@ -28,7 +30,9 @@ const getGroupCallVideoFrameSource = callingService.getGroupCallVideoFrameSource
|
|||
callingService
|
||||
);
|
||||
|
||||
const mapStateToActiveCallProp = (state: StateType) => {
|
||||
const mapStateToActiveCallProp = (
|
||||
state: StateType
|
||||
): undefined | ActiveCallType => {
|
||||
const { calling } = state;
|
||||
const { activeCallState } = calling;
|
||||
|
||||
|
@ -51,48 +55,59 @@ const mapStateToActiveCallProp = (state: StateType) => {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
// TODO: The way we deal with remote participants isn't ideal. See DESKTOP-949.
|
||||
let isCallFull = false;
|
||||
const groupCallPeekedParticipants: Array<GroupCallPeekedParticipantType> = [];
|
||||
const groupCallParticipants: Array<GroupCallRemoteParticipantType> = [];
|
||||
if (call.callMode === CallMode.Group) {
|
||||
isCallFull = call.peekInfo.deviceCount >= call.peekInfo.maxDevices;
|
||||
|
||||
call.peekInfo.conversationIds.forEach((conversationId: string) => {
|
||||
const peekedConversation = conversationSelector(conversationId);
|
||||
|
||||
if (!peekedConversation) {
|
||||
window.log.error(
|
||||
'Peeked participant has no corresponding conversation'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
groupCallPeekedParticipants.push({
|
||||
avatarPath: peekedConversation.avatarPath,
|
||||
color: peekedConversation.color,
|
||||
firstName: peekedConversation.firstName,
|
||||
isSelf: conversationId === state.user.ourConversationId,
|
||||
name: peekedConversation.name,
|
||||
profileName: peekedConversation.profileName,
|
||||
title: peekedConversation.title,
|
||||
});
|
||||
const conversationSelectorByUuid = memoize<
|
||||
(uuid: string) => undefined | ConversationType
|
||||
>(uuid => {
|
||||
const conversationId = window.ConversationController.ensureContactIds({
|
||||
uuid,
|
||||
});
|
||||
return conversationId ? conversationSelector(conversationId) : undefined;
|
||||
});
|
||||
|
||||
call.remoteParticipants.forEach(
|
||||
(remoteParticipant: GroupCallParticipantInfoType) => {
|
||||
const remoteConversation = conversationSelector(
|
||||
remoteParticipant.conversationId
|
||||
const baseResult = {
|
||||
conversation,
|
||||
hasLocalAudio: activeCallState.hasLocalAudio,
|
||||
hasLocalVideo: activeCallState.hasLocalVideo,
|
||||
joinedAt: activeCallState.joinedAt,
|
||||
pip: activeCallState.pip,
|
||||
settingsDialogOpen: activeCallState.settingsDialogOpen,
|
||||
showParticipantsList: activeCallState.showParticipantsList,
|
||||
};
|
||||
|
||||
switch (call.callMode) {
|
||||
case CallMode.Direct:
|
||||
return {
|
||||
...baseResult,
|
||||
callEndedReason: call.callEndedReason,
|
||||
callMode: CallMode.Direct,
|
||||
callState: call.callState,
|
||||
peekedParticipants: [],
|
||||
remoteParticipants: [
|
||||
{
|
||||
hasRemoteVideo: Boolean(call.hasRemoteVideo),
|
||||
},
|
||||
],
|
||||
};
|
||||
case CallMode.Group: {
|
||||
const ourUuid = getUserUuid(state);
|
||||
|
||||
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
|
||||
const peekedParticipants: Array<GroupCallPeekedParticipantType> = [];
|
||||
|
||||
for (let i = 0; i < call.remoteParticipants.length; i += 1) {
|
||||
const remoteParticipant = call.remoteParticipants[i];
|
||||
|
||||
const remoteConversation = conversationSelectorByUuid(
|
||||
remoteParticipant.uuid
|
||||
);
|
||||
|
||||
if (!remoteConversation) {
|
||||
window.log.error(
|
||||
'Remote participant has no corresponding conversation'
|
||||
);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
groupCallParticipants.push({
|
||||
remoteParticipants.push({
|
||||
avatarPath: remoteConversation.avatarPath,
|
||||
color: remoteConversation.color,
|
||||
demuxId: remoteParticipant.demuxId,
|
||||
|
@ -100,27 +115,55 @@ const mapStateToActiveCallProp = (state: StateType) => {
|
|||
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
|
||||
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
|
||||
isBlocked: Boolean(remoteConversation.isBlocked),
|
||||
isSelf: remoteParticipant.isSelf,
|
||||
isSelf: remoteParticipant.uuid === ourUuid,
|
||||
name: remoteConversation.name,
|
||||
profileName: remoteConversation.profileName,
|
||||
speakerTime: remoteParticipant.speakerTime,
|
||||
title: remoteConversation.title,
|
||||
uuid: remoteParticipant.uuid,
|
||||
videoAspectRatio: remoteParticipant.videoAspectRatio,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
groupCallParticipants.sort((a, b) => a.title.localeCompare(b.title));
|
||||
for (let i = 0; i < call.peekInfo.uuids.length; i += 1) {
|
||||
const peekedParticipantUuid = call.peekInfo.uuids[i];
|
||||
|
||||
const peekedConversation = conversationSelectorByUuid(
|
||||
peekedParticipantUuid
|
||||
);
|
||||
if (!peekedConversation) {
|
||||
window.log.error(
|
||||
'Remote participant has no corresponding conversation'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
peekedParticipants.push({
|
||||
avatarPath: peekedConversation.avatarPath,
|
||||
color: peekedConversation.color,
|
||||
firstName: peekedConversation.firstName,
|
||||
isSelf: peekedParticipantUuid === ourUuid,
|
||||
name: peekedConversation.name,
|
||||
profileName: peekedConversation.profileName,
|
||||
title: peekedConversation.title,
|
||||
uuid: peekedParticipantUuid,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...baseResult,
|
||||
callMode: CallMode.Group,
|
||||
connectionState: call.connectionState,
|
||||
deviceCount: call.peekInfo.deviceCount,
|
||||
joinState: call.joinState,
|
||||
maxDevices: call.peekInfo.maxDevices,
|
||||
peekedParticipants,
|
||||
remoteParticipants,
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw missingCaseError(call);
|
||||
}
|
||||
|
||||
return {
|
||||
activeCallState,
|
||||
call,
|
||||
conversation,
|
||||
isCallFull,
|
||||
groupCallPeekedParticipants,
|
||||
groupCallParticipants,
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToIncomingCallProp = (state: StateType) => {
|
||||
|
@ -147,7 +190,12 @@ const mapStateToProps = (state: StateType) => ({
|
|||
getGroupCallVideoFrameSource,
|
||||
i18n: getIntl(state),
|
||||
incomingCall: mapStateToIncomingCallProp(state),
|
||||
me: getMe(state),
|
||||
me: {
|
||||
...getMe(state),
|
||||
// `getMe` returns a `ConversationType` which might not have a UUID, at least
|
||||
// according to the type. This ensures one is set.
|
||||
uuid: getUserUuid(state),
|
||||
},
|
||||
renderDeviceSelection,
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue