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

@ -35,8 +35,8 @@ const getConversation = () => ({
lastUpdated: Date.now(), lastUpdated: Date.now(),
}); });
const getCallState = () => ({ const getCommonActiveCallData = () => ({
conversationId: '3051234567', conversation: getConversation(),
joinedAt: Date.now(), joinedAt: Date.now(),
hasLocalAudio: boolean('hasLocalAudio', true), hasLocalAudio: boolean('hasLocalAudio', true),
hasLocalVideo: boolean('hasLocalVideo', false), hasLocalVideo: boolean('hasLocalVideo', false),
@ -69,6 +69,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
hangUp: action('hang-up'), hangUp: action('hang-up'),
i18n, i18n,
me: { me: {
uuid: 'cb0dd0c8-7393-41e9-a0aa-d631c4109541',
color: select('Caller color', Colors, 'ultramarine' as ColorType), color: select('Caller color', Colors, 'ultramarine' as ColorType),
title: text('Caller Title', 'Morty Smith'), title: text('Caller Title', 'Morty Smith'),
}, },
@ -92,19 +93,11 @@ story.add('Ongoing Direct Call', () => (
<CallManager <CallManager
{...createProps({ {...createProps({
activeCall: { activeCall: {
call: { ...getCommonActiveCallData(),
callMode: CallMode.Direct as CallMode.Direct, callMode: CallMode.Direct,
conversationId: '3051234567',
callState: CallState.Accepted, callState: CallState.Accepted,
isIncoming: false, peekedParticipants: [],
isVideoCall: true, remoteParticipants: [{ hasRemoteVideo: true }],
hasRemoteVideo: true,
},
activeCallState: getCallState(),
conversation: getConversation(),
isCallFull: false,
groupCallPeekedParticipants: [],
groupCallParticipants: [],
}, },
})} })}
/> />
@ -114,24 +107,15 @@ story.add('Ongoing Group Call', () => (
<CallManager <CallManager
{...createProps({ {...createProps({
activeCall: { activeCall: {
call: { ...getCommonActiveCallData(),
callMode: CallMode.Group as CallMode.Group, callMode: CallMode.Group,
conversationId: '3051234567',
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
peekInfo: {
conversationIds: [],
maxDevices: 16,
deviceCount: 0, deviceCount: 0,
}, joinState: GroupCallJoinState.Joined,
maxDevices: 5,
peekedParticipants: [],
remoteParticipants: [], remoteParticipants: [],
}, },
activeCallState: getCallState(),
conversation: getConversation(),
isCallFull: false,
groupCallPeekedParticipants: [],
groupCallParticipants: [],
},
})} })}
/> />
)); ));
@ -151,14 +135,12 @@ story.add('Call Request Needed', () => (
<CallManager <CallManager
{...createProps({ {...createProps({
activeCall: { activeCall: {
call: getIncomingCallState({ ...getCommonActiveCallData(),
callEndedReason: CallEndedReason.RemoteHangupNeedPermission, callEndedReason: CallEndedReason.RemoteHangupNeedPermission,
}), callMode: CallMode.Direct,
activeCallState: getCallState(), callState: CallState.Accepted,
conversation: getConversation(), peekedParticipants: [],
isCallFull: false, remoteParticipants: [{ hasRemoteVideo: true }],
groupCallPeekedParticipants: [],
groupCallParticipants: [],
}, },
})} })}
/> />

View file

@ -9,6 +9,7 @@ import { CallingParticipantsList } from './CallingParticipantsList';
import { CallingPip } from './CallingPip'; import { CallingPip } from './CallingPip';
import { IncomingCallBar } from './IncomingCallBar'; import { IncomingCallBar } from './IncomingCallBar';
import { import {
ActiveCallType,
CallEndedReason, CallEndedReason,
CallMode, CallMode,
CallState, CallState,
@ -19,7 +20,6 @@ import {
import { ConversationType } from '../state/ducks/conversations'; import { ConversationType } from '../state/ducks/conversations';
import { import {
AcceptCallType, AcceptCallType,
ActiveCallType,
CancelCallType, CancelCallType,
DeclineCallType, DeclineCallType,
DirectCallStateType, DirectCallStateType,
@ -61,6 +61,7 @@ export interface PropsType {
phoneNumber?: string; phoneNumber?: string;
profileName?: string; profileName?: string;
title: string; title: string;
uuid: string;
}; };
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void; setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
setLocalAudio: (_: SetLocalAudioType) => void; setLocalAudio: (_: SetLocalAudioType) => void;
@ -97,21 +98,15 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
toggleSettings, toggleSettings,
}) => { }) => {
const { const {
call,
activeCallState,
conversation, conversation,
groupCallPeekedParticipants,
groupCallParticipants,
isCallFull,
} = activeCall;
const {
hasLocalAudio, hasLocalAudio,
hasLocalVideo, hasLocalVideo,
joinedAt, joinedAt,
peekedParticipants,
pip, pip,
settingsDialogOpen, settingsDialogOpen,
showParticipantsList, showParticipantsList,
} = activeCallState; } = activeCall;
const cancelActiveCall = useCallback(() => { const cancelActiveCall = useCallback(() => {
cancelCall({ conversationId: conversation.id }); cancelCall({ conversationId: conversation.id });
@ -119,12 +114,18 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
const joinActiveCall = useCallback(() => { const joinActiveCall = useCallback(() => {
startCall({ startCall({
callMode: call.callMode, callMode: activeCall.callMode,
conversationId: conversation.id, conversationId: conversation.id,
hasLocalAudio, hasLocalAudio,
hasLocalVideo, hasLocalVideo,
}); });
}, [startCall, call.callMode, conversation.id, hasLocalAudio, hasLocalVideo]); }, [
startCall,
activeCall.callMode,
conversation.id,
hasLocalAudio,
hasLocalVideo,
]);
const getGroupCallVideoFrameSourceForActiveCall = useCallback( const getGroupCallVideoFrameSourceForActiveCall = useCallback(
(demuxId: number) => { (demuxId: number) => {
@ -143,11 +144,12 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
[setGroupCallVideoRequest, conversation.id] [setGroupCallVideoRequest, conversation.id]
); );
let isCallFull: boolean;
let showCallLobby: boolean; let showCallLobby: boolean;
switch (call.callMode) { switch (activeCall.callMode) {
case CallMode.Direct: { case CallMode.Direct: {
const { callState, callEndedReason } = call; const { callState, callEndedReason } = activeCall;
const ended = callState === CallState.Ended; const ended = callState === CallState.Ended;
if ( if (
ended && ended &&
@ -162,22 +164,19 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
); );
} }
showCallLobby = !callState; showCallLobby = !callState;
isCallFull = false;
break; break;
} }
case CallMode.Group: { case CallMode.Group: {
showCallLobby = call.joinState === GroupCallJoinState.NotJoined; showCallLobby = activeCall.joinState === GroupCallJoinState.NotJoined;
isCallFull = activeCall.deviceCount >= activeCall.maxDevices;
break; break;
} }
default: default:
throw missingCaseError(call); throw missingCaseError(activeCall);
} }
if (showCallLobby) { if (showCallLobby) {
const participantNames = groupCallPeekedParticipants.map(participant =>
participant.isSelf
? i18n('you')
: participant.firstName || participant.title
);
return ( return (
<> <>
<CallingLobby <CallingLobby
@ -186,12 +185,12 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
hasLocalAudio={hasLocalAudio} hasLocalAudio={hasLocalAudio}
hasLocalVideo={hasLocalVideo} hasLocalVideo={hasLocalVideo}
i18n={i18n} i18n={i18n}
isGroupCall={call.callMode === CallMode.Group} isGroupCall={activeCall.callMode === CallMode.Group}
isCallFull={isCallFull} isCallFull={isCallFull}
me={me} me={me}
onCallCanceled={cancelActiveCall} onCallCanceled={cancelActiveCall}
onJoinCall={joinActiveCall} onJoinCall={joinActiveCall}
participantNames={participantNames} peekedParticipants={peekedParticipants}
setLocalPreview={setLocalPreview} setLocalPreview={setLocalPreview}
setLocalAudio={setLocalAudio} setLocalAudio={setLocalAudio}
setLocalVideo={setLocalVideo} setLocalVideo={setLocalVideo}
@ -200,11 +199,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
toggleSettings={toggleSettings} toggleSettings={toggleSettings}
/> />
{settingsDialogOpen && renderDeviceSelection()} {settingsDialogOpen && renderDeviceSelection()}
{showParticipantsList && call.callMode === CallMode.Group ? ( {showParticipantsList && activeCall.callMode === CallMode.Group ? (
<CallingParticipantsList <CallingParticipantsList
i18n={i18n} i18n={i18n}
onClose={toggleParticipants} onClose={toggleParticipants}
participants={groupCallParticipants} participants={peekedParticipants}
/> />
) : null} ) : null}
</> </>
@ -227,14 +226,30 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
); );
} }
const groupCallParticipantsForParticipantsList =
activeCall.callMode === CallMode.Group
? [
...activeCall.remoteParticipants.map(participant => ({
...participant,
hasAudio: participant.hasRemoteAudio,
hasVideo: participant.hasRemoteVideo,
isSelf: false,
})),
{
...me,
hasAudio: hasLocalAudio,
hasVideo: hasLocalVideo,
isSelf: true,
},
]
: [];
return ( return (
<> <>
<CallScreen <CallScreen
activeCall={activeCall} activeCall={activeCall}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall} getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
hangUp={hangUp} hangUp={hangUp}
hasLocalAudio={hasLocalAudio}
hasLocalVideo={hasLocalVideo}
i18n={i18n} i18n={i18n}
joinedAt={joinedAt} joinedAt={joinedAt}
me={me} me={me}
@ -249,11 +264,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
toggleSettings={toggleSettings} toggleSettings={toggleSettings}
/> />
{settingsDialogOpen && renderDeviceSelection()} {settingsDialogOpen && renderDeviceSelection()}
{showParticipantsList && call.callMode === CallMode.Group ? ( {showParticipantsList && activeCall.callMode === CallMode.Group ? (
<CallingParticipantsList <CallingParticipantsList
i18n={i18n} i18n={i18n}
onClose={toggleParticipants} onClose={toggleParticipants}
participants={groupCallParticipants} participants={groupCallParticipantsForParticipantsList}
/> />
) : null} ) : null}
</> </>

View file

@ -12,78 +12,18 @@ import {
CallState, CallState,
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
GroupCallPeekedParticipantType,
GroupCallRemoteParticipantType, GroupCallRemoteParticipantType,
} from '../types/Calling'; } from '../types/Calling';
import { Colors } from '../types/Colors'; import { Colors } from '../types/Colors';
import {
DirectCallStateType,
GroupCallStateType,
} from '../state/ducks/calling';
import { CallScreen, PropsType } from './CallScreen'; import { CallScreen, PropsType } from './CallScreen';
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
import { missingCaseError } from '../util/missingCaseError';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
function getGroupCallState(): GroupCallStateType { const conversation = {
return {
callMode: CallMode.Group,
conversationId: '3051234567',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
peekInfo: {
conversationIds: [],
maxDevices: 16,
deviceCount: 0,
},
remoteParticipants: [],
};
}
function getDirectCallState(
overrideProps: {
callState?: CallState;
hasRemoteVideo?: boolean;
} = {}
): DirectCallStateType {
return {
callMode: CallMode.Direct,
conversationId: '3051234567',
callState: select(
'callState',
CallState,
overrideProps.callState || CallState.Accepted
),
hasRemoteVideo: boolean(
'hasRemoteVideo',
Boolean(overrideProps.hasRemoteVideo)
),
isIncoming: false,
isVideoCall: true,
};
}
const createProps = (
overrideProps: {
callState?: CallState;
callTypeState?: DirectCallStateType | GroupCallStateType;
groupCallParticipants?: Array<GroupCallRemoteParticipantType>;
hasLocalAudio?: boolean;
hasLocalVideo?: boolean;
hasRemoteVideo?: boolean;
} = {}
): PropsType => ({
activeCall: {
activeCallState: {
conversationId: '123',
hasLocalAudio: true,
hasLocalVideo: true,
pip: false,
settingsDialogOpen: false,
showParticipantsList: false,
},
call: overrideProps.callTypeState || getDirectCallState(overrideProps),
conversation: {
id: '3051234567', id: '3051234567',
avatarPath: undefined, avatarPath: undefined,
color: Colors[0], color: Colors[0],
@ -92,22 +32,108 @@ const createProps = (
phoneNumber: '3051234567', phoneNumber: '3051234567',
profileName: 'Rick Sanchez', profileName: 'Rick Sanchez',
markedUnread: false, markedUnread: false,
type: 'direct', type: 'direct' as const,
lastUpdated: Date.now(), lastUpdated: Date.now(),
};
interface OverridePropsBase {
hasLocalAudio?: boolean;
hasLocalVideo?: boolean;
}
interface DirectCallOverrideProps extends OverridePropsBase {
callMode: CallMode.Direct;
callState?: CallState;
hasRemoteVideo?: boolean;
}
interface GroupCallOverrideProps extends OverridePropsBase {
callMode: CallMode.Group;
connectionState?: GroupCallConnectionState;
peekedParticipants?: Array<GroupCallPeekedParticipantType>;
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
}
const createActiveDirectCallProp = (
overrideProps: DirectCallOverrideProps
) => ({
callMode: CallMode.Direct as CallMode.Direct,
conversation,
callState: select(
'callState',
CallState,
overrideProps.callState || CallState.Accepted
),
peekedParticipants: [] as [],
remoteParticipants: [
{
hasRemoteVideo: boolean(
'hasRemoteVideo',
Boolean(overrideProps.hasRemoteVideo)
),
}, },
isCallFull: false, ] as [
groupCallPeekedParticipants: [], {
groupCallParticipants: overrideProps.groupCallParticipants || [], hasRemoteVideo: boolean;
}, }
],
});
const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
callMode: CallMode.Group as CallMode.Group,
connectionState:
overrideProps.connectionState || GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
maxDevices: 5,
deviceCount: (overrideProps.remoteParticipants || []).length,
// Because remote participants are a superset, we can use them in place of peeked
// participants.
peekedParticipants:
overrideProps.peekedParticipants || overrideProps.remoteParticipants || [],
remoteParticipants: overrideProps.remoteParticipants || [],
});
const createActiveCallProp = (
overrideProps: DirectCallOverrideProps | GroupCallOverrideProps
) => {
const baseResult = {
joinedAt: Date.now(),
conversation,
hasLocalAudio: boolean(
'hasLocalAudio',
overrideProps.hasLocalAudio || false
),
hasLocalVideo: boolean(
'hasLocalVideo',
overrideProps.hasLocalVideo || false
),
pip: false,
settingsDialogOpen: false,
showParticipantsList: false,
};
switch (overrideProps.callMode) {
case CallMode.Direct:
return { ...baseResult, ...createActiveDirectCallProp(overrideProps) };
case CallMode.Group:
return { ...baseResult, ...createActiveGroupCallProp(overrideProps) };
default:
throw missingCaseError(overrideProps);
}
};
const createProps = (
overrideProps: DirectCallOverrideProps | GroupCallOverrideProps = {
callMode: CallMode.Direct as CallMode.Direct,
}
): PropsType => ({
activeCall: createActiveCallProp(overrideProps),
// We allow `any` here because this is fake and actually comes from RingRTC, which we // We allow `any` here because this is fake and actually comes from RingRTC, which we
// can't import. // can't import.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
getGroupCallVideoFrameSource: noop as any, getGroupCallVideoFrameSource: noop as any,
hangUp: action('hang-up'), hangUp: action('hang-up'),
hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false),
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
i18n, i18n,
joinedAt: Date.now(),
me: { me: {
color: Colors[1], color: Colors[1],
name: 'Morty Smith', name: 'Morty Smith',
@ -135,6 +161,7 @@ story.add('Pre-Ring', () => {
return ( return (
<CallScreen <CallScreen
{...createProps({ {...createProps({
callMode: CallMode.Direct,
callState: CallState.Prering, callState: CallState.Prering,
})} })}
/> />
@ -145,6 +172,7 @@ story.add('Ringing', () => {
return ( return (
<CallScreen <CallScreen
{...createProps({ {...createProps({
callMode: CallMode.Direct,
callState: CallState.Ringing, callState: CallState.Ringing,
})} })}
/> />
@ -155,6 +183,7 @@ story.add('Reconnecting', () => {
return ( return (
<CallScreen <CallScreen
{...createProps({ {...createProps({
callMode: CallMode.Direct,
callState: CallState.Reconnecting, callState: CallState.Reconnecting,
})} })}
/> />
@ -165,6 +194,7 @@ story.add('Ended', () => {
return ( return (
<CallScreen <CallScreen
{...createProps({ {...createProps({
callMode: CallMode.Direct,
callState: CallState.Ended, callState: CallState.Ended,
})} })}
/> />
@ -172,23 +202,45 @@ story.add('Ended', () => {
}); });
story.add('hasLocalAudio', () => { story.add('hasLocalAudio', () => {
return <CallScreen {...createProps({ hasLocalAudio: true })} />; return (
<CallScreen
{...createProps({
callMode: CallMode.Direct,
hasLocalAudio: true,
})}
/>
);
}); });
story.add('hasLocalVideo', () => { story.add('hasLocalVideo', () => {
return <CallScreen {...createProps({ hasLocalVideo: true })} />; return (
<CallScreen
{...createProps({
callMode: CallMode.Direct,
hasLocalVideo: true,
})}
/>
);
}); });
story.add('hasRemoteVideo', () => { story.add('hasRemoteVideo', () => {
return <CallScreen {...createProps({ hasRemoteVideo: true })} />; return (
<CallScreen
{...createProps({
callMode: CallMode.Direct,
hasRemoteVideo: true,
})}
/>
);
}); });
story.add('Group call - 1', () => ( story.add('Group call - 1', () => (
<CallScreen <CallScreen
{...createProps({ {...createProps({
callTypeState: getGroupCallState(), callMode: CallMode.Group,
groupCallParticipants: [ remoteParticipants: [
{ {
uuid: '72fa60e5-25fb-472d-8a56-e56867c57dda',
demuxId: 0, demuxId: 0,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -205,9 +257,10 @@ story.add('Group call - 1', () => (
story.add('Group call - Many', () => ( story.add('Group call - Many', () => (
<CallScreen <CallScreen
{...createProps({ {...createProps({
callTypeState: getGroupCallState(), callMode: CallMode.Group,
groupCallParticipants: [ remoteParticipants: [
{ {
uuid: '094586f5-8fc2-4ce2-a152-2dfcc99f4630',
demuxId: 0, demuxId: 0,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -217,6 +270,7 @@ story.add('Group call - Many', () => (
videoAspectRatio: 1.3, videoAspectRatio: 1.3,
}, },
{ {
uuid: 'cb5bdb24-4cbb-4650-8a7a-1a2807051e74',
demuxId: 1, demuxId: 1,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -226,6 +280,7 @@ story.add('Group call - Many', () => (
videoAspectRatio: 1.3, videoAspectRatio: 1.3,
}, },
{ {
uuid: '2d7d13ae-53dc-4a51-8dc7-976cd85e0b57',
demuxId: 2, demuxId: 2,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
@ -242,12 +297,11 @@ story.add('Group call - Many', () => (
story.add('Group call - reconnecting', () => ( story.add('Group call - reconnecting', () => (
<CallScreen <CallScreen
{...createProps({ {...createProps({
callTypeState: { callMode: CallMode.Group,
...getGroupCallState(),
connectionState: GroupCallConnectionState.Reconnecting, connectionState: GroupCallConnectionState.Reconnecting,
}, remoteParticipants: [
groupCallParticipants: [
{ {
uuid: '33871c64-0c22-45ce-8aa4-0ec237ac4a31',
demuxId: 0, demuxId: 0,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,

View file

@ -5,7 +5,6 @@ import React, { useState, useRef, useEffect, useCallback } from 'react';
import { noop } from 'lodash'; import { noop } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
ActiveCallType,
HangUpType, HangUpType,
SetLocalAudioType, SetLocalAudioType,
SetLocalPreviewType, SetLocalPreviewType,
@ -17,6 +16,7 @@ import { CallingHeader } from './CallingHeader';
import { CallingButton, CallingButtonType } from './CallingButton'; import { CallingButton, CallingButtonType } from './CallingButton';
import { CallBackgroundBlur } from './CallBackgroundBlur'; import { CallBackgroundBlur } from './CallBackgroundBlur';
import { import {
ActiveCallType,
CallMode, CallMode,
CallState, CallState,
GroupCallConnectionState, GroupCallConnectionState,
@ -34,8 +34,6 @@ export type PropsType = {
activeCall: ActiveCallType; activeCall: ActiveCallType;
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
hangUp: (_: HangUpType) => void; hangUp: (_: HangUpType) => void;
hasLocalAudio: boolean;
hasLocalVideo: boolean;
i18n: LocalizerType; i18n: LocalizerType;
joinedAt?: number; joinedAt?: number;
me: { me: {
@ -61,8 +59,6 @@ export const CallScreen: React.FC<PropsType> = ({
activeCall, activeCall,
getGroupCallVideoFrameSource, getGroupCallVideoFrameSource,
hangUp, hangUp,
hasLocalAudio,
hasLocalVideo,
i18n, i18n,
joinedAt, joinedAt,
me, me,
@ -76,7 +72,12 @@ export const CallScreen: React.FC<PropsType> = ({
togglePip, togglePip,
toggleSettings, toggleSettings,
}) => { }) => {
const { call, conversation, groupCallParticipants } = activeCall; const {
conversation,
hasLocalAudio,
hasLocalVideo,
showParticipantsList,
} = activeCall;
const toggleAudio = useCallback(() => { const toggleAudio = useCallback(() => {
setLocalAudio({ setLocalAudio({
@ -148,23 +149,25 @@ export const CallScreen: React.FC<PropsType> = ({
}; };
}, [toggleAudio, toggleVideo]); }, [toggleAudio, toggleVideo]);
let hasRemoteVideo: boolean; const hasRemoteVideo = activeCall.remoteParticipants.some(
remoteParticipant => remoteParticipant.hasRemoteVideo
);
let headerMessage: string | undefined; let headerMessage: string | undefined;
let headerTitle: string | undefined; let headerTitle: string | undefined;
let isConnected: boolean; let isConnected: boolean;
let participantCount: number; let participantCount: number;
let remoteParticipantsElement: JSX.Element; let remoteParticipantsElement: JSX.Element;
switch (call.callMode) { switch (activeCall.callMode) {
case CallMode.Direct: case CallMode.Direct:
hasRemoteVideo = Boolean(call.hasRemoteVideo);
headerMessage = renderHeaderMessage( headerMessage = renderHeaderMessage(
i18n, i18n,
call.callState || CallState.Prering, activeCall.callState || CallState.Prering,
acceptedDuration acceptedDuration
); );
headerTitle = conversation.title; headerTitle = conversation.title;
isConnected = call.callState === CallState.Accepted; isConnected = activeCall.callState === CallState.Accepted;
participantCount = isConnected ? 2 : 0; participantCount = isConnected ? 2 : 0;
remoteParticipantsElement = ( remoteParticipantsElement = (
<DirectCallRemoteParticipant <DirectCallRemoteParticipant
@ -176,26 +179,24 @@ export const CallScreen: React.FC<PropsType> = ({
); );
break; break;
case CallMode.Group: case CallMode.Group:
hasRemoteVideo = call.remoteParticipants.some( participantCount = activeCall.remoteParticipants.length + 1;
remoteParticipant => remoteParticipant.hasRemoteVideo
);
participantCount = activeCall.groupCallParticipants.length;
headerMessage = undefined; headerMessage = undefined;
headerTitle = activeCall.groupCallParticipants.length headerTitle = activeCall.remoteParticipants.length
? undefined ? undefined
: i18n('calling__in-this-call--zero'); : i18n('calling__in-this-call--zero');
isConnected = call.connectionState === GroupCallConnectionState.Connected; isConnected =
activeCall.connectionState === GroupCallConnectionState.Connected;
remoteParticipantsElement = ( remoteParticipantsElement = (
<GroupCallRemoteParticipants <GroupCallRemoteParticipants
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n} i18n={i18n}
remoteParticipants={groupCallParticipants} remoteParticipants={activeCall.remoteParticipants}
setGroupCallVideoRequest={setGroupCallVideoRequest} setGroupCallVideoRequest={setGroupCallVideoRequest}
/> />
); );
break; break;
default: default:
throw missingCaseError(call); throw missingCaseError(activeCall);
} }
const videoButtonType = hasLocalVideo const videoButtonType = hasLocalVideo
@ -214,14 +215,12 @@ export const CallScreen: React.FC<PropsType> = ({
!showControls && !isAudioOnly && isConnected, !showControls && !isAudioOnly && isConnected,
}); });
const { showParticipantsList } = activeCall.activeCallState;
return ( return (
<div <div
className={classNames( className={classNames(
'module-calling__container', 'module-calling__container',
`module-ongoing-call__container--${getCallModeClassSuffix( `module-ongoing-call__container--${getCallModeClassSuffix(
call.callMode activeCall.callMode
)}` )}`
)} )}
onMouseMove={() => { onMouseMove={() => {
@ -229,9 +228,9 @@ export const CallScreen: React.FC<PropsType> = ({
}} }}
role="group" role="group"
> >
{call.callMode === CallMode.Group ? ( {activeCall.callMode === CallMode.Group ? (
<GroupCallToastManager <GroupCallToastManager
connectionState={call.connectionState} connectionState={activeCall.connectionState}
i18n={i18n} i18n={i18n}
/> />
) : null} ) : null}
@ -241,7 +240,7 @@ export const CallScreen: React.FC<PropsType> = ({
<CallingHeader <CallingHeader
canPip canPip
i18n={i18n} i18n={i18n}
isGroupCall={call.callMode === CallMode.Group} isGroupCall={activeCall.callMode === CallMode.Group}
message={headerMessage} message={headerMessage}
participantCount={participantCount} participantCount={participantCount}
showParticipantsList={showParticipantsList} showParticipantsList={showParticipantsList}

View file

@ -5,6 +5,7 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs'; import { boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { v4 as generateUuid } from 'uuid';
import { ColorType } from '../types/Colors'; import { ColorType } from '../types/Colors';
import { CallingLobby, PropsType } from './CallingLobby'; import { CallingLobby, PropsType } from './CallingLobby';
@ -35,7 +36,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
me: overrideProps.me || { color: 'ultramarine' as ColorType }, me: overrideProps.me || { color: 'ultramarine' as ColorType },
onCallCanceled: action('on-call-canceled'), onCallCanceled: action('on-call-canceled'),
onJoinCall: action('on-join-call'), onJoinCall: action('on-join-call'),
participantNames: overrideProps.participantNames || [], peekedParticipants: overrideProps.peekedParticipants || [],
setLocalAudio: action('set-local-audio'), setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'), setLocalPreview: action('set-local-preview'),
setLocalVideo: action('set-local-video'), setLocalVideo: action('set-local-video'),
@ -47,6 +48,12 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
toggleSettings: action('toggle-settings'), toggleSettings: action('toggle-settings'),
}); });
const fakePeekedParticipant = (title: string) => ({
isSelf: false,
title,
uuid: generateUuid(),
});
const story = storiesOf('Components/CallingLobby', module); const story = storiesOf('Components/CallingLobby', module);
story.add('Default', () => { story.add('Default', () => {
@ -86,44 +93,51 @@ story.add('Local Video', () => {
return <CallingLobby {...props} />; return <CallingLobby {...props} />;
}); });
story.add('Group Call - 0', () => { story.add('Group Call - 0 peeked participants', () => {
const props = createProps({ isGroupCall: true, participantNames: [] }); const props = createProps({ isGroupCall: true, peekedParticipants: [] });
return <CallingLobby {...props} />; return <CallingLobby {...props} />;
}); });
story.add('Group Call - 1', () => { story.add('Group Call - 1 peeked participant', () => {
const props = createProps({ isGroupCall: true, participantNames: ['Sam'] });
return <CallingLobby {...props} />;
});
story.add('Group Call - 2', () => {
const props = createProps({ const props = createProps({
isGroupCall: true, isGroupCall: true,
participantNames: ['Sam', 'Cayce'], peekedParticipants: ['Sam'].map(fakePeekedParticipant),
}); });
return <CallingLobby {...props} />; return <CallingLobby {...props} />;
}); });
story.add('Group Call - 3', () => { story.add('Group Call - 2 peeked participants', () => {
const props = createProps({ const props = createProps({
isGroupCall: true, isGroupCall: true,
participantNames: ['Sam', 'Cayce', 'April'], peekedParticipants: ['Sam', 'Cayce'].map(fakePeekedParticipant),
}); });
return <CallingLobby {...props} />; return <CallingLobby {...props} />;
}); });
story.add('Group Call - 4', () => { story.add('Group Call - 3 peeked participants', () => {
const props = createProps({ const props = createProps({
isGroupCall: true, isGroupCall: true,
participantNames: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'], peekedParticipants: ['Sam', 'Cayce', 'April'].map(fakePeekedParticipant),
}); });
return <CallingLobby {...props} />; return <CallingLobby {...props} />;
}); });
story.add('Group Call - 4 (participants list)', () => { story.add('Group Call - 4 peeked participants', () => {
const props = createProps({ const props = createProps({
isGroupCall: true, isGroupCall: true,
participantNames: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'], peekedParticipants: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'].map(
fakePeekedParticipant
),
});
return <CallingLobby {...props} />;
});
story.add('Group Call - 4 peeked participants (participants list)', () => {
const props = createProps({
isGroupCall: true,
peekedParticipants: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'].map(
fakePeekedParticipant
),
showParticipantsList: true, showParticipantsList: true,
}); });
return <CallingLobby {...props} />; return <CallingLobby {...props} />;

View file

@ -31,7 +31,12 @@ export type PropsType = {
}; };
onCallCanceled: () => void; onCallCanceled: () => void;
onJoinCall: () => void; onJoinCall: () => void;
participantNames: Array<string>; peekedParticipants: Array<{
firstName?: string;
isSelf: boolean;
title: string;
uuid: string;
}>;
setLocalAudio: (_: SetLocalAudioType) => void; setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void; setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void; setLocalPreview: (_: SetLocalPreviewType) => void;
@ -51,7 +56,7 @@ export const CallingLobby = ({
me, me,
onCallCanceled, onCallCanceled,
onJoinCall, onJoinCall,
participantNames, peekedParticipants,
setLocalAudio, setLocalAudio,
setLocalPreview, setLocalPreview,
setLocalVideo, setLocalVideo,
@ -114,6 +119,16 @@ export const CallingLobby = ({
? CallingButtonType.AUDIO_ON ? CallingButtonType.AUDIO_ON
: CallingButtonType.AUDIO_OFF; : CallingButtonType.AUDIO_OFF;
// It should be rare to see yourself in this list, but it's possible if (1) you rejoin
// quickly, causing the server to return stale state (2) you have joined on another
// device.
// TODO: Improve the "it's you" case; see DESKTOP-926.
const participantNames = peekedParticipants.map(participant =>
participant.isSelf
? i18n('you')
: participant.firstName || participant.title
);
let joinButton: JSX.Element; let joinButton: JSX.Element;
if (isCallFull) { if (isCallFull) {
joinButton = ( joinButton = (
@ -159,7 +174,7 @@ export const CallingLobby = ({
title={conversation.title} title={conversation.title}
i18n={i18n} i18n={i18n}
isGroupCall={isGroupCall} isGroupCall={isGroupCall}
participantCount={participantNames.length} participantCount={peekedParticipants.length}
showParticipantsList={showParticipantsList} showParticipantsList={showParticipantsList}
toggleParticipants={toggleParticipants} toggleParticipants={toggleParticipants}
toggleSettings={toggleSettings} toggleSettings={toggleSettings}

View file

@ -4,6 +4,7 @@
import * as React from 'react'; import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { v4 as generateUuid } from 'uuid';
import { CallingParticipantsList, PropsType } from './CallingParticipantsList'; import { CallingParticipantsList, PropsType } from './CallingParticipantsList';
import { Colors } from '../types/Colors'; import { Colors } from '../types/Colors';
@ -29,6 +30,7 @@ function createParticipant(
profileName: participantProps.title, profileName: participantProps.title,
title: String(participantProps.title), title: String(participantProps.title),
videoAspectRatio: 1.3, videoAspectRatio: 1.3,
uuid: generateUuid(),
}; };
} }

View file

@ -9,18 +9,28 @@ import { Avatar } from './Avatar';
import { ContactName } from './conversation/ContactName'; import { ContactName } from './conversation/ContactName';
import { InContactsIcon } from './InContactsIcon'; import { InContactsIcon } from './InContactsIcon';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { GroupCallRemoteParticipantType } from '../types/Calling'; import { GroupCallPeekedParticipantType } from '../types/Calling';
interface ParticipantType extends GroupCallPeekedParticipantType {
hasAudio?: boolean;
hasVideo?: boolean;
}
export type PropsType = { export type PropsType = {
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
readonly onClose: () => void; readonly onClose: () => void;
readonly participants: Array<GroupCallRemoteParticipantType>; readonly participants: Array<ParticipantType>;
}; };
export const CallingParticipantsList = React.memo( export const CallingParticipantsList = React.memo(
({ i18n, onClose, participants }: PropsType) => { ({ i18n, onClose, participants }: PropsType) => {
const [root, setRoot] = React.useState<HTMLElement | null>(null); const [root, setRoot] = React.useState<HTMLElement | null>(null);
const sortedParticipants = React.useMemo<Array<ParticipantType>>(
() => participants.sort((a, b) => a.title.localeCompare(b.title)),
[participants]
);
React.useEffect(() => { React.useEffect(() => {
const div = document.createElement('div'); const div = document.createElement('div');
document.body.appendChild(div); document.body.appendChild(div);
@ -70,10 +80,13 @@ export const CallingParticipantsList = React.memo(
/> />
</div> </div>
<ul className="module-calling-participants-list__list"> <ul className="module-calling-participants-list__list">
{participants.map( {sortedParticipants.map(
(participant: GroupCallRemoteParticipantType, index: number) => ( (participant: ParticipantType, index: number) => (
<li <li
className="module-calling-participants-list__contact" className="module-calling-participants-list__contact"
// It's tempting to use `participant.uuid` as the `key` here, but that
// can result in duplicate keys for participants who have joined on
// multiple devices.
key={index} key={index}
> >
<div> <div>
@ -110,10 +123,10 @@ export const CallingParticipantsList = React.memo(
)} )}
</div> </div>
<div> <div>
{!participant.hasRemoteAudio ? ( {participant.hasAudio === false ? (
<span className="module-calling-participants-list__muted--audio" /> <span className="module-calling-participants-list__muted--audio" />
) : null} ) : null}
{!participant.hasRemoteVideo ? ( {participant.hasVideo === false ? (
<span className="module-calling-participants-list__muted--video" /> <span className="module-calling-participants-list__muted--video" />
) : null} ) : null}
</div> </div>

View file

@ -9,9 +9,9 @@ import { action } from '@storybook/addon-actions';
import { ColorType } from '../types/Colors'; import { ColorType } from '../types/Colors';
import { ConversationTypeType } from '../state/ducks/conversations'; import { ConversationTypeType } from '../state/ducks/conversations';
import { ActiveCallType } from '../state/ducks/calling';
import { CallingPip, PropsType } from './CallingPip'; import { CallingPip, PropsType } from './CallingPip';
import { import {
ActiveCallType,
CallMode, CallMode,
CallState, CallState,
GroupCallConnectionState, GroupCallConnectionState,
@ -35,34 +35,26 @@ const conversation = {
lastUpdated: Date.now(), lastUpdated: Date.now(),
}; };
const defaultCall = { const getCommonActiveCallData = () => ({
conversation,
hasLocalAudio: boolean('hasLocalAudio', true),
hasLocalVideo: boolean('hasLocalVideo', false),
joinedAt: Date.now(),
pip: true,
settingsDialogOpen: false,
showParticipantsList: false,
});
const defaultCall: ActiveCallType = {
...getCommonActiveCallData(),
callMode: CallMode.Direct as CallMode.Direct, callMode: CallMode.Direct as CallMode.Direct,
conversationId: '3051234567',
callState: CallState.Accepted, callState: CallState.Accepted,
isIncoming: false, peekedParticipants: [],
isVideoCall: true, remoteParticipants: [{ hasRemoteVideo: true }],
hasRemoteVideo: true,
}; };
const createProps = ( const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
overrideProps: Partial<PropsType> = {}, activeCall: overrideProps.activeCall || defaultCall,
activeCall: Partial<ActiveCallType> = {}
): PropsType => ({
activeCall: {
activeCallState: {
conversationId: '123',
hasLocalAudio: true,
hasLocalVideo: true,
pip: false,
settingsDialogOpen: false,
showParticipantsList: true,
},
call: activeCall.call || defaultCall,
conversation: activeCall.conversation || conversation,
isCallFull: false,
groupCallPeekedParticipants: [],
groupCallParticipants: [],
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
getGroupCallVideoFrameSource: noop as any, getGroupCallVideoFrameSource: noop as any,
hangUp: action('hang-up'), hangUp: action('hang-up'),
@ -82,48 +74,43 @@ story.add('Default', () => {
}); });
story.add('Contact (with avatar)', () => { story.add('Contact (with avatar)', () => {
const props = createProps( const props = createProps({
{}, activeCall: {
{ ...defaultCall,
conversation: { conversation: {
...conversation, ...conversation,
avatarPath: 'https://www.fillmurray.com/64/64', avatarPath: 'https://www.fillmurray.com/64/64',
}, },
} },
); });
return <CallingPip {...props} />; return <CallingPip {...props} />;
}); });
story.add('Contact (no color)', () => { story.add('Contact (no color)', () => {
const props = createProps( const props = createProps({
{}, activeCall: {
{ ...defaultCall,
conversation: { conversation: {
...conversation, ...conversation,
color: undefined, color: undefined,
}, },
} },
); });
return <CallingPip {...props} />; return <CallingPip {...props} />;
}); });
story.add('Group Call', () => { story.add('Group Call', () => {
const props = createProps( const props = createProps({
{}, activeCall: {
{ ...getCommonActiveCallData(),
call: {
callMode: CallMode.Group as CallMode.Group, callMode: CallMode.Group as CallMode.Group,
conversationId: '3051234567',
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined, joinState: GroupCallJoinState.Joined,
peekInfo: { maxDevices: 5,
conversationIds: [],
maxDevices: 16,
deviceCount: 0, deviceCount: 0,
}, peekedParticipants: [],
remoteParticipants: [], remoteParticipants: [],
}, },
} });
);
return <CallingPip {...props} />; return <CallingPip {...props} />;
}); });

View file

@ -5,9 +5,12 @@ import React from 'react';
import { minBy, debounce, noop } from 'lodash'; import { minBy, debounce, noop } from 'lodash';
import { CallingPipRemoteVideo } from './CallingPipRemoteVideo'; import { CallingPipRemoteVideo } from './CallingPipRemoteVideo';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { GroupCallVideoRequest, VideoFrameSource } from '../types/Calling';
import { import {
ActiveCallType, ActiveCallType,
GroupCallVideoRequest,
VideoFrameSource,
} from '../types/Calling';
import {
HangUpType, HangUpType,
SetLocalPreviewType, SetLocalPreviewType,
SetRendererCanvasType, SetRendererCanvasType,

View file

@ -9,12 +9,13 @@ import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant'; import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { import {
ActiveCallType,
CallMode, CallMode,
GroupCallRemoteParticipantType, GroupCallRemoteParticipantType,
GroupCallVideoRequest, GroupCallVideoRequest,
VideoFrameSource, VideoFrameSource,
} from '../types/Calling'; } from '../types/Calling';
import { ActiveCallType, SetRendererCanvasType } from '../state/ducks/calling'; import { SetRendererCanvasType } from '../state/ducks/calling';
import { usePageVisibility } from '../util/hooks'; import { usePageVisibility } from '../util/hooks';
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant'; import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
@ -74,31 +75,31 @@ export const CallingPipRemoteVideo = ({
setGroupCallVideoRequest, setGroupCallVideoRequest,
setRendererCanvas, setRendererCanvas,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const { call, conversation, groupCallParticipants } = activeCall; const { conversation } = activeCall;
const isPageVisible = usePageVisibility(); const isPageVisible = usePageVisibility();
const activeGroupCallSpeaker: const activeGroupCallSpeaker:
| undefined | undefined
| GroupCallRemoteParticipantType = useMemo(() => { | GroupCallRemoteParticipantType = useMemo(() => {
if (call.callMode !== CallMode.Group) { if (activeCall.callMode !== CallMode.Group) {
return undefined; return undefined;
} }
return maxBy( return maxBy(
groupCallParticipants, activeCall.remoteParticipants,
participant => participant.speakerTime || -Infinity participant => participant.speakerTime || -Infinity
); );
}, [call.callMode, groupCallParticipants]); }, [activeCall.callMode, activeCall.remoteParticipants]);
useEffect(() => { useEffect(() => {
if (call.callMode !== CallMode.Group) { if (activeCall.callMode !== CallMode.Group) {
return; return;
} }
if (isPageVisible) { if (isPageVisible) {
setGroupCallVideoRequest( setGroupCallVideoRequest(
groupCallParticipants.map(participant => { activeCall.remoteParticipants.map(participant => {
const isVisible = const isVisible =
participant === activeGroupCallSpeaker && participant === activeGroupCallSpeaker &&
participant.hasRemoteVideo; participant.hasRemoteVideo;
@ -116,19 +117,21 @@ export const CallingPipRemoteVideo = ({
); );
} else { } else {
setGroupCallVideoRequest( setGroupCallVideoRequest(
groupCallParticipants.map(nonRenderedRemoteParticipant) activeCall.remoteParticipants.map(nonRenderedRemoteParticipant)
); );
} }
}, [ }, [
call.callMode, activeCall.callMode,
groupCallParticipants, activeCall.remoteParticipants,
activeGroupCallSpeaker, activeGroupCallSpeaker,
isPageVisible, isPageVisible,
setGroupCallVideoRequest, setGroupCallVideoRequest,
]); ]);
if (call.callMode === CallMode.Direct) { if (activeCall.callMode === CallMode.Direct) {
if (!call.hasRemoteVideo) { const { hasRemoteVideo } = activeCall.remoteParticipants[0];
if (!hasRemoteVideo) {
return <NoVideo activeCall={activeCall} i18n={i18n} />; return <NoVideo activeCall={activeCall} i18n={i18n} />;
} }
@ -136,7 +139,7 @@ export const CallingPipRemoteVideo = ({
<div className="module-calling-pip__video--remote"> <div className="module-calling-pip__video--remote">
<DirectCallRemoteParticipant <DirectCallRemoteParticipant
conversation={conversation} conversation={conversation}
hasRemoteVideo={call.hasRemoteVideo} hasRemoteVideo={hasRemoteVideo}
i18n={i18n} i18n={i18n}
setRendererCanvas={setRendererCanvas} setRendererCanvas={setRendererCanvas}
/> />
@ -144,7 +147,7 @@ export const CallingPipRemoteVideo = ({
); );
} }
if (call.callMode === CallMode.Group) { if (activeCall.callMode === CallMode.Group) {
if (!activeGroupCallSpeaker) { if (!activeGroupCallSpeaker) {
return <NoVideo activeCall={activeCall} i18n={i18n} />; return <NoVideo activeCall={activeCall} i18n={i18n} />;
} }

View file

@ -566,24 +566,21 @@ export class CallingClass {
} }
} }
private uuidToConversationId(userId: ArrayBuffer): string {
const result = window.ConversationController.ensureContactIds({
uuid: arrayBufferToUuid(userId),
});
if (!result) {
throw new Error(
'Calling.uuidToConversationId: no conversation found for that UUID'
);
}
return result;
}
public formatGroupCallPeekInfoForRedux( public formatGroupCallPeekInfoForRedux(
peekInfo: PeekInfo = { joinedMembers: [], deviceCount: 0 } peekInfo: PeekInfo
): GroupCallPeekInfoType { ): GroupCallPeekInfoType {
return { return {
conversationIds: peekInfo.joinedMembers.map(this.uuidToConversationId), uuids: peekInfo.joinedMembers.map(uuidBuffer => {
creator: peekInfo.creator && this.uuidToConversationId(peekInfo.creator), let uuid = arrayBufferToUuid(uuidBuffer);
if (!uuid) {
window.log.error(
'Calling.formatGroupCallPeekInfoForRedux: could not convert peek UUID ArrayBuffer to string; using fallback UUID'
);
uuid = '00000000-0000-0000-0000-000000000000';
}
return uuid;
}),
creatorUuid: peekInfo.creator && arrayBufferToUuid(peekInfo.creator),
eraId: peekInfo.eraId, eraId: peekInfo.eraId,
maxDevices: peekInfo.maxDevices ?? Infinity, maxDevices: peekInfo.maxDevices ?? Infinity,
deviceCount: peekInfo.deviceCount, deviceCount: peekInfo.deviceCount,
@ -592,23 +589,16 @@ export class CallingClass {
private formatGroupCallForRedux(groupCall: GroupCall) { private formatGroupCallForRedux(groupCall: GroupCall) {
const localDeviceState = groupCall.getLocalDeviceState(); const localDeviceState = groupCall.getLocalDeviceState();
const peekInfo = groupCall.getPeekInfo();
// RingRTC doesn't ensure that the demux ID is unique. This can happen if someone // RingRTC doesn't ensure that the demux ID is unique. This can happen if someone
// leaves the call and quickly rejoins; RingRTC will tell us that there are two // leaves the call and quickly rejoins; RingRTC will tell us that there are two
// participants with the same demux ID in the call. // participants with the same demux ID in the call. This should be rare.
const remoteDeviceStates = uniqBy( const remoteDeviceStates = uniqBy(
groupCall.getRemoteDeviceStates() || [], groupCall.getRemoteDeviceStates() || [],
remoteDeviceState => remoteDeviceState.demuxId remoteDeviceState => remoteDeviceState.demuxId
); );
// `GroupCall.prototype.getPeekInfo()` won't return anything at first, so we try to
// set a reasonable default based on the remote device states (which is likely an
// empty array at this point, but we handle the case where it is not).
const peekInfo = groupCall.getPeekInfo() || {
joinedMembers: remoteDeviceStates.map(({ userId }) => userId),
deviceCount: remoteDeviceStates.length,
};
// It should be impossible to be disconnected and Joining or Joined. Just in case, we // It should be impossible to be disconnected and Joining or Joined. Just in case, we
// try to handle that case. // try to handle that case.
const joinState: GroupCallJoinState = const joinState: GroupCallJoinState =
@ -616,8 +606,6 @@ export class CallingClass {
? GroupCallJoinState.NotJoined ? GroupCallJoinState.NotJoined
: this.convertRingRtcJoinState(localDeviceState.joinState); : this.convertRingRtcJoinState(localDeviceState.joinState);
const ourConversationId = window.ConversationController.getOurConversationId();
return { return {
connectionState: this.convertRingRtcConnectionState( connectionState: this.convertRingRtcConnectionState(
localDeviceState.connectionState localDeviceState.connectionState
@ -625,17 +613,22 @@ export class CallingClass {
joinState, joinState,
hasLocalAudio: !localDeviceState.audioMuted, hasLocalAudio: !localDeviceState.audioMuted,
hasLocalVideo: !localDeviceState.videoMuted, hasLocalVideo: !localDeviceState.videoMuted,
peekInfo: this.formatGroupCallPeekInfoForRedux(peekInfo), peekInfo: peekInfo
? this.formatGroupCallPeekInfoForRedux(peekInfo)
: undefined,
remoteParticipants: remoteDeviceStates.map(remoteDeviceState => { remoteParticipants: remoteDeviceStates.map(remoteDeviceState => {
const conversationId = this.uuidToConversationId( let uuid = arrayBufferToUuid(remoteDeviceState.userId);
remoteDeviceState.userId if (!uuid) {
window.log.error(
'Calling.formatGroupCallForRedux: could not convert remote participant UUID ArrayBuffer to string; using fallback UUID'
); );
uuid = '00000000-0000-0000-0000-000000000000';
}
return { return {
conversationId, uuid,
demuxId: remoteDeviceState.demuxId, demuxId: remoteDeviceState.demuxId,
hasRemoteAudio: !remoteDeviceState.audioMuted, hasRemoteAudio: !remoteDeviceState.audioMuted,
hasRemoteVideo: !remoteDeviceState.videoMuted, hasRemoteVideo: !remoteDeviceState.videoMuted,
isSelf: conversationId === ourConversationId,
speakerTime: normalizeGroupCallTimestamp( speakerTime: normalizeGroupCallTimestamp(
remoteDeviceState.speakerTime remoteDeviceState.speakerTime
), ),

View file

@ -9,16 +9,13 @@ import { missingCaseError } from '../../util/missingCaseError';
import { notify } from '../../services/notify'; import { notify } from '../../services/notify';
import { calling } from '../../services/calling'; import { calling } from '../../services/calling';
import { StateType as RootStateType } from '../reducer'; import { StateType as RootStateType } from '../reducer';
import { ConversationType } from './conversations';
import { import {
CallingDeviceType,
CallMode, CallMode,
CallState, CallState,
CallingDeviceType,
ChangeIODevicePayloadType, ChangeIODevicePayloadType,
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
GroupCallPeekedParticipantType,
GroupCallRemoteParticipantType,
GroupCallVideoRequest, GroupCallVideoRequest,
MediaDeviceSettings, MediaDeviceSettings,
} from '../../types/Calling'; } from '../../types/Calling';
@ -34,19 +31,18 @@ import { LatestQueue } from '../../util/LatestQueue';
// State // State
export interface GroupCallPeekInfoType { export interface GroupCallPeekInfoType {
conversationIds: Array<string>; uuids: Array<string>;
creator?: string; creatorUuid?: string;
eraId?: string; eraId?: string;
maxDevices: number; maxDevices: number;
deviceCount: number; deviceCount: number;
} }
export interface GroupCallParticipantInfoType { export interface GroupCallParticipantInfoType {
conversationId: string; uuid: string;
demuxId: number; demuxId: number;
hasRemoteAudio: boolean; hasRemoteAudio: boolean;
hasRemoteVideo: boolean; hasRemoteVideo: boolean;
isSelf: boolean;
speakerTime?: number; speakerTime?: number;
videoAspectRatio: number; videoAspectRatio: number;
} }
@ -70,15 +66,6 @@ export interface GroupCallStateType {
remoteParticipants: Array<GroupCallParticipantInfoType>; remoteParticipants: Array<GroupCallParticipantInfoType>;
} }
export interface ActiveCallType {
activeCallState: ActiveCallStateType;
call: DirectCallStateType | GroupCallStateType;
conversation: ConversationType;
isCallFull: boolean;
groupCallPeekedParticipants: Array<GroupCallPeekedParticipantType>;
groupCallParticipants: Array<GroupCallRemoteParticipantType>;
}
export interface ActiveCallStateType { export interface ActiveCallStateType {
conversationId: string; conversationId: string;
joinedAt?: number; joinedAt?: number;
@ -127,12 +114,12 @@ type GroupCallStateChangeArgumentType = {
hasLocalAudio: boolean; hasLocalAudio: boolean;
hasLocalVideo: boolean; hasLocalVideo: boolean;
joinState: GroupCallJoinState; joinState: GroupCallJoinState;
peekInfo: GroupCallPeekInfoType; peekInfo?: GroupCallPeekInfoType;
remoteParticipants: Array<GroupCallParticipantInfoType>; remoteParticipants: Array<GroupCallParticipantInfoType>;
}; };
type GroupCallStateChangeActionPayloadType = GroupCallStateChangeArgumentType & { type GroupCallStateChangeActionPayloadType = GroupCallStateChangeArgumentType & {
ourConversationId: string; ourUuid: string;
}; };
export type HangUpType = { export type HangUpType = {
@ -190,7 +177,7 @@ export type ShowCallLobbyType =
joinState: GroupCallJoinState; joinState: GroupCallJoinState;
hasLocalAudio: boolean; hasLocalAudio: boolean;
hasLocalVideo: boolean; hasLocalVideo: boolean;
peekInfo: GroupCallPeekInfoType; peekInfo?: GroupCallPeekInfoType;
remoteParticipants: Array<GroupCallParticipantInfoType>; remoteParticipants: Array<GroupCallParticipantInfoType>;
}; };
@ -212,9 +199,9 @@ export const getActiveCall = ({
getOwn(callsByConversation, activeCallState.conversationId); getOwn(callsByConversation, activeCallState.conversationId);
export const isAnybodyElseInGroupCall = ( export const isAnybodyElseInGroupCall = (
{ conversationIds }: Readonly<GroupCallPeekInfoType>, { uuids }: Readonly<GroupCallPeekInfoType>,
ourConversationId: string ourUuid: string
): boolean => conversationIds.some(id => id !== ourConversationId); ): boolean => uuids.some(id => id !== ourUuid);
// Actions // Actions
@ -496,7 +483,7 @@ function groupCallStateChange(
type: GROUP_CALL_STATE_CHANGE, type: GROUP_CALL_STATE_CHANGE,
payload: { payload: {
...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( function removeConversationFromState(
state: CallingStateType, state: CallingStateType,
conversationId: string conversationId: string
@ -845,7 +842,12 @@ export function reducer(
conversationId: action.payload.conversationId, conversationId: action.payload.conversationId,
connectionState: action.payload.connectionState, connectionState: action.payload.connectionState,
joinState: action.payload.joinState, 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, remoteParticipants: action.payload.remoteParticipants,
}; };
break; break;
@ -1030,11 +1032,18 @@ export function reducer(
hasLocalAudio, hasLocalAudio,
hasLocalVideo, hasLocalVideo,
joinState, joinState,
ourConversationId, ourUuid,
peekInfo, peekInfo,
remoteParticipants, remoteParticipants,
} = action.payload; } = action.payload;
const newPeekInfo = peekInfo ||
getExistingPeekInfo(conversationId, state) || {
uuids: remoteParticipants.map(({ uuid }) => uuid),
maxDevices: Infinity,
deviceCount: remoteParticipants.length,
};
let newActiveCallState: ActiveCallStateType | undefined; let newActiveCallState: ActiveCallStateType | undefined;
if (connectionState === GroupCallConnectionState.NotConnected) { if (connectionState === GroupCallConnectionState.NotConnected) {
@ -1043,7 +1052,7 @@ export function reducer(
? undefined ? undefined
: state.activeCallState; : state.activeCallState;
if (!isAnybodyElseInGroupCall(peekInfo, ourConversationId)) { if (!isAnybodyElseInGroupCall(newPeekInfo, ourUuid)) {
return { return {
...state, ...state,
callsByConversation: omit(callsByConversation, conversationId), callsByConversation: omit(callsByConversation, conversationId),
@ -1070,7 +1079,7 @@ export function reducer(
conversationId, conversationId,
connectionState, connectionState,
joinState, joinState,
peekInfo, peekInfo: newPeekInfo,
remoteParticipants, remoteParticipants,
}, },
}, },

View file

@ -3,21 +3,23 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { memoize } from 'lodash';
import { mapDispatchToProps } from '../actions'; import { mapDispatchToProps } from '../actions';
import { CallManager } from '../../components/CallManager'; import { CallManager } from '../../components/CallManager';
import { calling as callingService } from '../../services/calling'; import { calling as callingService } from '../../services/calling';
import { getUserUuid, getIntl } from '../selectors/user';
import { getMe, getConversationSelector } from '../selectors/conversations'; 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 { getIncomingCall } from '../selectors/calling';
import { import {
ActiveCallType,
CallMode, CallMode,
GroupCallPeekedParticipantType, GroupCallPeekedParticipantType,
GroupCallRemoteParticipantType, GroupCallRemoteParticipantType,
} from '../../types/Calling'; } from '../../types/Calling';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { missingCaseError } from '../../util/missingCaseError';
import { getIntl } from '../selectors/user';
import { SmartCallingDeviceSelection } from './CallingDeviceSelection'; import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
function renderDeviceSelection(): JSX.Element { function renderDeviceSelection(): JSX.Element {
@ -28,7 +30,9 @@ const getGroupCallVideoFrameSource = callingService.getGroupCallVideoFrameSource
callingService callingService
); );
const mapStateToActiveCallProp = (state: StateType) => { const mapStateToActiveCallProp = (
state: StateType
): undefined | ActiveCallType => {
const { calling } = state; const { calling } = state;
const { activeCallState } = calling; const { activeCallState } = calling;
@ -51,48 +55,59 @@ const mapStateToActiveCallProp = (state: StateType) => {
return undefined; return undefined;
} }
// TODO: The way we deal with remote participants isn't ideal. See DESKTOP-949. const conversationSelectorByUuid = memoize<
let isCallFull = false; (uuid: string) => undefined | ConversationType
const groupCallPeekedParticipants: Array<GroupCallPeekedParticipantType> = []; >(uuid => {
const groupCallParticipants: Array<GroupCallRemoteParticipantType> = []; const conversationId = window.ConversationController.ensureContactIds({
if (call.callMode === CallMode.Group) { uuid,
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,
}); });
return conversationId ? conversationSelector(conversationId) : undefined;
}); });
call.remoteParticipants.forEach( const baseResult = {
(remoteParticipant: GroupCallParticipantInfoType) => { conversation,
const remoteConversation = conversationSelector( hasLocalAudio: activeCallState.hasLocalAudio,
remoteParticipant.conversationId 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) { if (!remoteConversation) {
window.log.error( window.log.error(
'Remote participant has no corresponding conversation' 'Remote participant has no corresponding conversation'
); );
return; continue;
} }
groupCallParticipants.push({ remoteParticipants.push({
avatarPath: remoteConversation.avatarPath, avatarPath: remoteConversation.avatarPath,
color: remoteConversation.color, color: remoteConversation.color,
demuxId: remoteParticipant.demuxId, demuxId: remoteParticipant.demuxId,
@ -100,27 +115,55 @@ const mapStateToActiveCallProp = (state: StateType) => {
hasRemoteAudio: remoteParticipant.hasRemoteAudio, hasRemoteAudio: remoteParticipant.hasRemoteAudio,
hasRemoteVideo: remoteParticipant.hasRemoteVideo, hasRemoteVideo: remoteParticipant.hasRemoteVideo,
isBlocked: Boolean(remoteConversation.isBlocked), isBlocked: Boolean(remoteConversation.isBlocked),
isSelf: remoteParticipant.isSelf, isSelf: remoteParticipant.uuid === ourUuid,
name: remoteConversation.name, name: remoteConversation.name,
profileName: remoteConversation.profileName, profileName: remoteConversation.profileName,
speakerTime: remoteParticipant.speakerTime, speakerTime: remoteParticipant.speakerTime,
title: remoteConversation.title, title: remoteConversation.title,
uuid: remoteParticipant.uuid,
videoAspectRatio: remoteParticipant.videoAspectRatio, 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 { return {
activeCallState, ...baseResult,
call, callMode: CallMode.Group,
conversation, connectionState: call.connectionState,
isCallFull, deviceCount: call.peekInfo.deviceCount,
groupCallPeekedParticipants, joinState: call.joinState,
groupCallParticipants, maxDevices: call.peekInfo.maxDevices,
peekedParticipants,
remoteParticipants,
}; };
}
default:
throw missingCaseError(call);
}
}; };
const mapStateToIncomingCallProp = (state: StateType) => { const mapStateToIncomingCallProp = (state: StateType) => {
@ -147,7 +190,12 @@ const mapStateToProps = (state: StateType) => ({
getGroupCallVideoFrameSource, getGroupCallVideoFrameSource,
i18n: getIntl(state), i18n: getIntl(state),
incomingCall: mapStateToIncomingCallProp(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, renderDeviceSelection,
}); });

View file

@ -72,19 +72,18 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined, joinState: GroupCallJoinState.NotJoined,
peekInfo: { peekInfo: {
conversationIds: ['456'], uuids: ['456'],
creator: '456', creatorUuid: '456',
eraId: 'xyz', eraId: 'xyz',
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
conversationId: '123', uuid: '123',
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
isSelf: false,
videoAspectRatio: 4 / 3, videoAspectRatio: 4 / 3,
}, },
], ],
@ -104,7 +103,7 @@ describe('calling duck', () => {
}, },
}; };
const ourConversationId = 'ebf5fd79-9344-4ec1-b5c9-af463572caf5'; const ourUuid = 'ebf5fd79-9344-4ec1-b5c9-af463572caf5';
const getEmptyRootState = () => { const getEmptyRootState = () => {
const rootState = rootReducer(undefined, noopAction()); const rootState = rootReducer(undefined, noopAction());
@ -112,7 +111,7 @@ describe('calling duck', () => {
...rootState, ...rootState,
user: { user: {
...rootState.user, ...rootState.user,
ourConversationId, ourUuid,
}, },
}; };
}; };
@ -275,7 +274,7 @@ describe('calling duck', () => {
hasLocalAudio: false, hasLocalAudio: false,
hasLocalVideo: false, hasLocalVideo: false,
peekInfo: { peekInfo: {
conversationIds: [], uuids: [],
maxDevices: 16, maxDevices: 16,
deviceCount: 0, deviceCount: 0,
}, },
@ -296,7 +295,7 @@ describe('calling duck', () => {
hasLocalAudio: false, hasLocalAudio: false,
hasLocalVideo: false, hasLocalVideo: false,
peekInfo: { peekInfo: {
conversationIds: [], uuids: [],
maxDevices: 16, maxDevices: 16,
deviceCount: 0, deviceCount: 0,
}, },
@ -320,7 +319,7 @@ describe('calling duck', () => {
hasLocalAudio: false, hasLocalAudio: false,
hasLocalVideo: false, hasLocalVideo: false,
peekInfo: { peekInfo: {
conversationIds: [ourConversationId], uuids: [ourUuid],
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
@ -344,7 +343,7 @@ describe('calling duck', () => {
hasLocalAudio: false, hasLocalAudio: false,
hasLocalVideo: false, hasLocalVideo: false,
peekInfo: { peekInfo: {
conversationIds: [], uuids: [],
maxDevices: 16, maxDevices: 16,
deviceCount: 0, deviceCount: 0,
}, },
@ -365,7 +364,7 @@ describe('calling duck', () => {
hasLocalAudio: false, hasLocalAudio: false,
hasLocalVideo: false, hasLocalVideo: false,
peekInfo: { peekInfo: {
conversationIds: [ourConversationId], uuids: [ourUuid],
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
@ -386,19 +385,18 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
peekInfo: { peekInfo: {
conversationIds: ['456'], uuids: ['456'],
creator: '456', creatorUuid: '456',
eraId: 'xyz', eraId: 'xyz',
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
conversationId: '123', uuid: '123',
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
isSelf: false,
videoAspectRatio: 4 / 3, videoAspectRatio: 4 / 3,
}, },
], ],
@ -413,19 +411,18 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joining, joinState: GroupCallJoinState.Joining,
peekInfo: { peekInfo: {
conversationIds: ['456'], uuids: ['456'],
creator: '456', creatorUuid: '456',
eraId: 'xyz', eraId: 'xyz',
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
conversationId: '123', uuid: '123',
demuxId: 123, demuxId: 123,
hasRemoteAudio: true, hasRemoteAudio: true,
hasRemoteVideo: true, hasRemoteVideo: true,
isSelf: false,
videoAspectRatio: 4 / 3, videoAspectRatio: 4 / 3,
}, },
], ],
@ -443,7 +440,7 @@ describe('calling duck', () => {
hasLocalAudio: false, hasLocalAudio: false,
hasLocalVideo: false, hasLocalVideo: false,
peekInfo: { peekInfo: {
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'], uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
@ -459,7 +456,7 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.NotConnected, connectionState: GroupCallConnectionState.NotConnected,
joinState: GroupCallJoinState.NotJoined, joinState: GroupCallJoinState.NotJoined,
peekInfo: { peekInfo: {
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'], uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
@ -478,17 +475,16 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
peekInfo: { peekInfo: {
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'], uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
conversationId: '123', uuid: '123',
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
isSelf: false,
videoAspectRatio: 16 / 9, videoAspectRatio: 16 / 9,
}, },
], ],
@ -503,17 +499,16 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined, joinState: GroupCallJoinState.Joined,
peekInfo: { peekInfo: {
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'], uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
conversationId: '123', uuid: '123',
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
isSelf: false,
videoAspectRatio: 16 / 9, videoAspectRatio: 16 / 9,
}, },
], ],
@ -531,17 +526,16 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: false, hasLocalVideo: false,
peekInfo: { peekInfo: {
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'], uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
conversationId: '123', uuid: '123',
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
isSelf: false,
videoAspectRatio: 16 / 9, videoAspectRatio: 16 / 9,
}, },
], ],
@ -561,17 +555,16 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: true, hasLocalVideo: true,
peekInfo: { peekInfo: {
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'], uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
conversationId: '123', uuid: '123',
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
isSelf: false,
videoAspectRatio: 16 / 9, videoAspectRatio: 16 / 9,
}, },
], ],
@ -598,17 +591,16 @@ describe('calling duck', () => {
hasLocalAudio: true, hasLocalAudio: true,
hasLocalVideo: true, hasLocalVideo: true,
peekInfo: { peekInfo: {
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'], uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
maxDevices: 16, maxDevices: 16,
deviceCount: 1, deviceCount: 1,
}, },
remoteParticipants: [ remoteParticipants: [
{ {
conversationId: '123', uuid: '123',
demuxId: 456, demuxId: 456,
hasRemoteAudio: false, hasRemoteAudio: false,
hasRemoteVideo: true, hasRemoteVideo: true,
isSelf: false,
videoAspectRatio: 16 / 9, videoAspectRatio: 16 / 9,
}, },
], ],
@ -771,7 +763,7 @@ describe('calling duck', () => {
describe('showCallLobby', () => { describe('showCallLobby', () => {
const { showCallLobby } = actions; const { showCallLobby } = actions;
it('saves the call and makes it active', () => { it('saves a direct call and makes it active', () => {
const result = reducer( const result = reducer(
getEmptyState(), getEmptyState(),
showCallLobby({ showCallLobby({
@ -797,6 +789,161 @@ describe('calling duck', () => {
settingsDialogOpen: false, settingsDialogOpen: false,
}); });
}); });
it('saves a group call and makes it active', () => {
const result = reducer(
getEmptyState(),
showCallLobby({
callMode: CallMode.Group,
conversationId: 'fake-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: ['456'],
creatorUuid: '456',
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: '123',
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
videoAspectRatio: 4 / 3,
},
],
})
);
assert.deepEqual(result.callsByConversation['fake-conversation-id'], {
callMode: CallMode.Group,
conversationId: 'fake-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: ['456'],
creatorUuid: '456',
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: '123',
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
videoAspectRatio: 4 / 3,
},
],
});
assert.deepEqual(
result.activeCallState?.conversationId,
'fake-conversation-id'
);
});
it('chooses fallback peek info if none is sent and there is no existing call', () => {
const result = reducer(
getEmptyState(),
showCallLobby({
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: undefined,
remoteParticipants: [],
})
);
const call =
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: [],
maxDevices: Infinity,
deviceCount: 0,
});
});
it("doesn't overwrite an existing group call's peek info if none was sent", () => {
const result = reducer(
stateWithGroupCall,
showCallLobby({
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: undefined,
remoteParticipants: [
{
uuid: '123',
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
videoAspectRatio: 4 / 3,
},
],
})
);
const call =
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: ['456'],
creatorUuid: '456',
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
});
});
it("can overwrite an existing group call's peek info", () => {
const result = reducer(
stateWithGroupCall,
showCallLobby({
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
hasLocalAudio: true,
hasLocalVideo: true,
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: ['999'],
creatorUuid: '999',
eraId: 'abc',
maxDevices: 5,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: '123',
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
videoAspectRatio: 4 / 3,
},
],
})
);
const call =
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: ['999'],
creatorUuid: '999',
eraId: 'abc',
maxDevices: 5,
deviceCount: 1,
});
});
}); });
describe('startCall', () => { describe('startCall', () => {
@ -965,10 +1112,10 @@ describe('calling duck', () => {
}); });
describe('isAnybodyElseInGroupCall', () => { describe('isAnybodyElseInGroupCall', () => {
const fakePeekInfo = (conversationIds: Array<string>) => ({ const fakePeekInfo = (uuids: Array<string>) => ({
conversationIds, uuids,
maxDevices: 16, maxDevices: 5,
deviceCount: conversationIds.length, deviceCount: uuids.length,
}); });
it('returns false if the peek info has no participants', () => { it('returns false if the peek info has no participants', () => {

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ColorType } from './Colors'; import { ColorType } from './Colors';
import { ConversationType } from '../state/ducks/conversations';
export enum CallMode { export enum CallMode {
None = 'None', None = 'None',
@ -9,6 +10,40 @@ export enum CallMode {
Group = 'Group', Group = 'Group',
} }
interface ActiveCallBaseType {
conversation: ConversationType;
hasLocalAudio: boolean;
hasLocalVideo: boolean;
joinedAt?: number;
pip: boolean;
settingsDialogOpen: boolean;
showParticipantsList: boolean;
}
interface ActiveDirectCallType extends ActiveCallBaseType {
callMode: CallMode.Direct;
callState?: CallState;
callEndedReason?: CallEndedReason;
peekedParticipants: [];
remoteParticipants: [
{
hasRemoteVideo: boolean;
}
];
}
interface ActiveGroupCallType extends ActiveCallBaseType {
callMode: CallMode.Group;
connectionState: GroupCallConnectionState;
joinState: GroupCallJoinState;
maxDevices: number;
deviceCount: number;
peekedParticipants: Array<GroupCallPeekedParticipantType>;
remoteParticipants: Array<GroupCallRemoteParticipantType>;
}
export type ActiveCallType = ActiveDirectCallType | ActiveGroupCallType;
// Ideally, we would import many of these directly from RingRTC. But because Storybook // Ideally, we would import many of these directly from RingRTC. But because Storybook
// cannot import RingRTC (as it runs in the browser), we have these copies. That also // cannot import RingRTC (as it runs in the browser), we have these copies. That also
// means we have to convert the "real" enum to our enum in some cases. // means we have to convert the "real" enum to our enum in some cases.
@ -58,7 +93,6 @@ export enum GroupCallJoinState {
Joined = 2, Joined = 2,
} }
// TODO: The way we deal with remote participants isn't ideal. See DESKTOP-949.
export interface GroupCallPeekedParticipantType { export interface GroupCallPeekedParticipantType {
avatarPath?: string; avatarPath?: string;
color?: ColorType; color?: ColorType;
@ -67,20 +101,16 @@ export interface GroupCallPeekedParticipantType {
name?: string; name?: string;
profileName?: string; profileName?: string;
title: string; title: string;
uuid: string;
} }
export interface GroupCallRemoteParticipantType {
avatarPath?: string; export interface GroupCallRemoteParticipantType
color?: ColorType; extends GroupCallPeekedParticipantType {
demuxId: number; demuxId: number;
firstName?: string;
hasRemoteAudio: boolean; hasRemoteAudio: boolean;
hasRemoteVideo: boolean; hasRemoteVideo: boolean;
isBlocked: boolean; isBlocked: boolean;
isSelf: boolean;
name?: string;
profileName?: string;
speakerTime?: number; speakerTime?: number;
title: string;
videoAspectRatio: number; videoAspectRatio: number;
} }

View file

@ -14400,7 +14400,7 @@
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/CallingLobby.tsx", "path": "ts/components/CallingLobby.tsx",
"line": " const localVideoRef = React.useRef(null);", "line": " const localVideoRef = React.useRef(null);",
"lineNumber": 62, "lineNumber": 67,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-10-26T19:12:24.410Z", "updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Used to get the local video element for rendering." "reasonDetail": "Used to get the local video element for rendering."
@ -14427,7 +14427,7 @@
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/CallingPip.tsx", "path": "ts/components/CallingPip.tsx",
"line": " const localVideoRef = React.useRef(null);", "line": " const localVideoRef = React.useRef(null);",
"lineNumber": 80, "lineNumber": 83,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-10-26T19:12:24.410Z", "updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Used to get the local video element for rendering." "reasonDetail": "Used to get the local video element for rendering."

View file

@ -1,7 +1,7 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { GroupCallVideoRequest } from '../../types/Calling'; import { GroupCallVideoRequest } from '../../types/Calling';
export const nonRenderedRemoteParticipant = ({ export const nonRenderedRemoteParticipant = ({
demuxId, demuxId,