Group calling participants refactor

This commit is contained in:
Evan Hahn 2020-12-02 12:14:03 -06:00 committed by GitHub
parent be99bbe87a
commit c85ea814b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 750 additions and 436 deletions

View file

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

View file

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