Group calling participants refactor
This commit is contained in:
parent
be99bbe87a
commit
c85ea814b1
18 changed files with 750 additions and 436 deletions
|
@ -35,8 +35,8 @@ const getConversation = () => ({
|
|||
lastUpdated: Date.now(),
|
||||
});
|
||||
|
||||
const getCallState = () => ({
|
||||
conversationId: '3051234567',
|
||||
const getCommonActiveCallData = () => ({
|
||||
conversation: getConversation(),
|
||||
joinedAt: Date.now(),
|
||||
hasLocalAudio: boolean('hasLocalAudio', true),
|
||||
hasLocalVideo: boolean('hasLocalVideo', false),
|
||||
|
@ -69,6 +69,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
hangUp: action('hang-up'),
|
||||
i18n,
|
||||
me: {
|
||||
uuid: 'cb0dd0c8-7393-41e9-a0aa-d631c4109541',
|
||||
color: select('Caller color', Colors, 'ultramarine' as ColorType),
|
||||
title: text('Caller Title', 'Morty Smith'),
|
||||
},
|
||||
|
@ -92,19 +93,11 @@ story.add('Ongoing Direct Call', () => (
|
|||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: {
|
||||
call: {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Accepted,
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
},
|
||||
activeCallState: getCallState(),
|
||||
conversation: getConversation(),
|
||||
isCallFull: false,
|
||||
groupCallPeekedParticipants: [],
|
||||
groupCallParticipants: [],
|
||||
...getCommonActiveCallData(),
|
||||
callMode: CallMode.Direct,
|
||||
callState: CallState.Accepted,
|
||||
peekedParticipants: [],
|
||||
remoteParticipants: [{ hasRemoteVideo: true }],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -114,23 +107,14 @@ story.add('Ongoing Group Call', () => (
|
|||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: {
|
||||
call: {
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
conversationId: '3051234567',
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
peekInfo: {
|
||||
conversationIds: [],
|
||||
maxDevices: 16,
|
||||
deviceCount: 0,
|
||||
},
|
||||
remoteParticipants: [],
|
||||
},
|
||||
activeCallState: getCallState(),
|
||||
conversation: getConversation(),
|
||||
isCallFull: false,
|
||||
groupCallPeekedParticipants: [],
|
||||
groupCallParticipants: [],
|
||||
...getCommonActiveCallData(),
|
||||
callMode: CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
deviceCount: 0,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
maxDevices: 5,
|
||||
peekedParticipants: [],
|
||||
remoteParticipants: [],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -151,14 +135,12 @@ story.add('Call Request Needed', () => (
|
|||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: {
|
||||
call: getIncomingCallState({
|
||||
callEndedReason: CallEndedReason.RemoteHangupNeedPermission,
|
||||
}),
|
||||
activeCallState: getCallState(),
|
||||
conversation: getConversation(),
|
||||
isCallFull: false,
|
||||
groupCallPeekedParticipants: [],
|
||||
groupCallParticipants: [],
|
||||
...getCommonActiveCallData(),
|
||||
callEndedReason: CallEndedReason.RemoteHangupNeedPermission,
|
||||
callMode: CallMode.Direct,
|
||||
callState: CallState.Accepted,
|
||||
peekedParticipants: [],
|
||||
remoteParticipants: [{ hasRemoteVideo: true }],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { CallingParticipantsList } from './CallingParticipantsList';
|
|||
import { CallingPip } from './CallingPip';
|
||||
import { IncomingCallBar } from './IncomingCallBar';
|
||||
import {
|
||||
ActiveCallType,
|
||||
CallEndedReason,
|
||||
CallMode,
|
||||
CallState,
|
||||
|
@ -19,7 +20,6 @@ import {
|
|||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
AcceptCallType,
|
||||
ActiveCallType,
|
||||
CancelCallType,
|
||||
DeclineCallType,
|
||||
DirectCallStateType,
|
||||
|
@ -61,6 +61,7 @@ export interface PropsType {
|
|||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
uuid: string;
|
||||
};
|
||||
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
|
@ -97,21 +98,15 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
toggleSettings,
|
||||
}) => {
|
||||
const {
|
||||
call,
|
||||
activeCallState,
|
||||
conversation,
|
||||
groupCallPeekedParticipants,
|
||||
groupCallParticipants,
|
||||
isCallFull,
|
||||
} = activeCall;
|
||||
const {
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
joinedAt,
|
||||
peekedParticipants,
|
||||
pip,
|
||||
settingsDialogOpen,
|
||||
showParticipantsList,
|
||||
} = activeCallState;
|
||||
} = activeCall;
|
||||
|
||||
const cancelActiveCall = useCallback(() => {
|
||||
cancelCall({ conversationId: conversation.id });
|
||||
|
@ -119,12 +114,18 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
|
||||
const joinActiveCall = useCallback(() => {
|
||||
startCall({
|
||||
callMode: call.callMode,
|
||||
callMode: activeCall.callMode,
|
||||
conversationId: conversation.id,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
});
|
||||
}, [startCall, call.callMode, conversation.id, hasLocalAudio, hasLocalVideo]);
|
||||
}, [
|
||||
startCall,
|
||||
activeCall.callMode,
|
||||
conversation.id,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
]);
|
||||
|
||||
const getGroupCallVideoFrameSourceForActiveCall = useCallback(
|
||||
(demuxId: number) => {
|
||||
|
@ -143,11 +144,12 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
[setGroupCallVideoRequest, conversation.id]
|
||||
);
|
||||
|
||||
let isCallFull: boolean;
|
||||
let showCallLobby: boolean;
|
||||
|
||||
switch (call.callMode) {
|
||||
switch (activeCall.callMode) {
|
||||
case CallMode.Direct: {
|
||||
const { callState, callEndedReason } = call;
|
||||
const { callState, callEndedReason } = activeCall;
|
||||
const ended = callState === CallState.Ended;
|
||||
if (
|
||||
ended &&
|
||||
|
@ -162,22 +164,19 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
);
|
||||
}
|
||||
showCallLobby = !callState;
|
||||
isCallFull = false;
|
||||
break;
|
||||
}
|
||||
case CallMode.Group: {
|
||||
showCallLobby = call.joinState === GroupCallJoinState.NotJoined;
|
||||
showCallLobby = activeCall.joinState === GroupCallJoinState.NotJoined;
|
||||
isCallFull = activeCall.deviceCount >= activeCall.maxDevices;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw missingCaseError(call);
|
||||
throw missingCaseError(activeCall);
|
||||
}
|
||||
|
||||
if (showCallLobby) {
|
||||
const participantNames = groupCallPeekedParticipants.map(participant =>
|
||||
participant.isSelf
|
||||
? i18n('you')
|
||||
: participant.firstName || participant.title
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<CallingLobby
|
||||
|
@ -186,12 +185,12 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
isGroupCall={call.callMode === CallMode.Group}
|
||||
isGroupCall={activeCall.callMode === CallMode.Group}
|
||||
isCallFull={isCallFull}
|
||||
me={me}
|
||||
onCallCanceled={cancelActiveCall}
|
||||
onJoinCall={joinActiveCall}
|
||||
participantNames={participantNames}
|
||||
peekedParticipants={peekedParticipants}
|
||||
setLocalPreview={setLocalPreview}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalVideo={setLocalVideo}
|
||||
|
@ -200,11 +199,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
{settingsDialogOpen && renderDeviceSelection()}
|
||||
{showParticipantsList && call.callMode === CallMode.Group ? (
|
||||
{showParticipantsList && activeCall.callMode === CallMode.Group ? (
|
||||
<CallingParticipantsList
|
||||
i18n={i18n}
|
||||
onClose={toggleParticipants}
|
||||
participants={groupCallParticipants}
|
||||
participants={peekedParticipants}
|
||||
/>
|
||||
) : 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 (
|
||||
<>
|
||||
<CallScreen
|
||||
activeCall={activeCall}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
|
||||
hangUp={hangUp}
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
joinedAt={joinedAt}
|
||||
me={me}
|
||||
|
@ -249,11 +264,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
{settingsDialogOpen && renderDeviceSelection()}
|
||||
{showParticipantsList && call.callMode === CallMode.Group ? (
|
||||
{showParticipantsList && activeCall.callMode === CallMode.Group ? (
|
||||
<CallingParticipantsList
|
||||
i18n={i18n}
|
||||
onClose={toggleParticipants}
|
||||
participants={groupCallParticipants}
|
||||
participants={groupCallParticipantsForParticipantsList}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
|
|
@ -12,102 +12,128 @@ import {
|
|||
CallState,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
GroupCallPeekedParticipantType,
|
||||
GroupCallRemoteParticipantType,
|
||||
} from '../types/Calling';
|
||||
import { Colors } from '../types/Colors';
|
||||
import {
|
||||
DirectCallStateType,
|
||||
GroupCallStateType,
|
||||
} from '../state/ducks/calling';
|
||||
import { CallScreen, PropsType } from './CallScreen';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
function getGroupCallState(): GroupCallStateType {
|
||||
return {
|
||||
callMode: CallMode.Group,
|
||||
conversationId: '3051234567',
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
peekInfo: {
|
||||
conversationIds: [],
|
||||
maxDevices: 16,
|
||||
deviceCount: 0,
|
||||
},
|
||||
remoteParticipants: [],
|
||||
};
|
||||
const conversation = {
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: Colors[0],
|
||||
title: 'Rick Sanchez',
|
||||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
markedUnread: false,
|
||||
type: 'direct' as const,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
|
||||
interface OverridePropsBase {
|
||||
hasLocalAudio?: boolean;
|
||||
hasLocalVideo?: boolean;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
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)
|
||||
),
|
||||
},
|
||||
] as [
|
||||
{
|
||||
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: {
|
||||
callState?: CallState;
|
||||
callTypeState?: DirectCallStateType | GroupCallStateType;
|
||||
groupCallParticipants?: Array<GroupCallRemoteParticipantType>;
|
||||
hasLocalAudio?: boolean;
|
||||
hasLocalVideo?: boolean;
|
||||
hasRemoteVideo?: boolean;
|
||||
} = {}
|
||||
overrideProps: DirectCallOverrideProps | GroupCallOverrideProps = {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
}
|
||||
): PropsType => ({
|
||||
activeCall: {
|
||||
activeCallState: {
|
||||
conversationId: '123',
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
showParticipantsList: false,
|
||||
},
|
||||
call: overrideProps.callTypeState || getDirectCallState(overrideProps),
|
||||
conversation: {
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: Colors[0],
|
||||
title: 'Rick Sanchez',
|
||||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
markedUnread: false,
|
||||
type: 'direct',
|
||||
lastUpdated: Date.now(),
|
||||
},
|
||||
isCallFull: false,
|
||||
groupCallPeekedParticipants: [],
|
||||
groupCallParticipants: overrideProps.groupCallParticipants || [],
|
||||
},
|
||||
activeCall: createActiveCallProp(overrideProps),
|
||||
// We allow `any` here because this is fake and actually comes from RingRTC, which we
|
||||
// can't import.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getGroupCallVideoFrameSource: noop as any,
|
||||
hangUp: action('hang-up'),
|
||||
hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false),
|
||||
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
||||
i18n,
|
||||
joinedAt: Date.now(),
|
||||
me: {
|
||||
color: Colors[1],
|
||||
name: 'Morty Smith',
|
||||
|
@ -135,6 +161,7 @@ story.add('Pre-Ring', () => {
|
|||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callMode: CallMode.Direct,
|
||||
callState: CallState.Prering,
|
||||
})}
|
||||
/>
|
||||
|
@ -145,6 +172,7 @@ story.add('Ringing', () => {
|
|||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callMode: CallMode.Direct,
|
||||
callState: CallState.Ringing,
|
||||
})}
|
||||
/>
|
||||
|
@ -155,6 +183,7 @@ story.add('Reconnecting', () => {
|
|||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callMode: CallMode.Direct,
|
||||
callState: CallState.Reconnecting,
|
||||
})}
|
||||
/>
|
||||
|
@ -165,6 +194,7 @@ story.add('Ended', () => {
|
|||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callMode: CallMode.Direct,
|
||||
callState: CallState.Ended,
|
||||
})}
|
||||
/>
|
||||
|
@ -172,23 +202,45 @@ story.add('Ended', () => {
|
|||
});
|
||||
|
||||
story.add('hasLocalAudio', () => {
|
||||
return <CallScreen {...createProps({ hasLocalAudio: true })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callMode: CallMode.Direct,
|
||||
hasLocalAudio: true,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('hasLocalVideo', () => {
|
||||
return <CallScreen {...createProps({ hasLocalVideo: true })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callMode: CallMode.Direct,
|
||||
hasLocalVideo: true,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('hasRemoteVideo', () => {
|
||||
return <CallScreen {...createProps({ hasRemoteVideo: true })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callMode: CallMode.Direct,
|
||||
hasRemoteVideo: true,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Group call - 1', () => (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callTypeState: getGroupCallState(),
|
||||
groupCallParticipants: [
|
||||
callMode: CallMode.Group,
|
||||
remoteParticipants: [
|
||||
{
|
||||
uuid: '72fa60e5-25fb-472d-8a56-e56867c57dda',
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
|
@ -205,9 +257,10 @@ story.add('Group call - 1', () => (
|
|||
story.add('Group call - Many', () => (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callTypeState: getGroupCallState(),
|
||||
groupCallParticipants: [
|
||||
callMode: CallMode.Group,
|
||||
remoteParticipants: [
|
||||
{
|
||||
uuid: '094586f5-8fc2-4ce2-a152-2dfcc99f4630',
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
|
@ -217,6 +270,7 @@ story.add('Group call - Many', () => (
|
|||
videoAspectRatio: 1.3,
|
||||
},
|
||||
{
|
||||
uuid: 'cb5bdb24-4cbb-4650-8a7a-1a2807051e74',
|
||||
demuxId: 1,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
|
@ -226,6 +280,7 @@ story.add('Group call - Many', () => (
|
|||
videoAspectRatio: 1.3,
|
||||
},
|
||||
{
|
||||
uuid: '2d7d13ae-53dc-4a51-8dc7-976cd85e0b57',
|
||||
demuxId: 2,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
|
@ -242,12 +297,11 @@ story.add('Group call - Many', () => (
|
|||
story.add('Group call - reconnecting', () => (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callTypeState: {
|
||||
...getGroupCallState(),
|
||||
connectionState: GroupCallConnectionState.Reconnecting,
|
||||
},
|
||||
groupCallParticipants: [
|
||||
callMode: CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Reconnecting,
|
||||
remoteParticipants: [
|
||||
{
|
||||
uuid: '33871c64-0c22-45ce-8aa4-0ec237ac4a31',
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
|
|
|
@ -5,7 +5,6 @@ import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|||
import { noop } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
ActiveCallType,
|
||||
HangUpType,
|
||||
SetLocalAudioType,
|
||||
SetLocalPreviewType,
|
||||
|
@ -17,6 +16,7 @@ import { CallingHeader } from './CallingHeader';
|
|||
import { CallingButton, CallingButtonType } from './CallingButton';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import {
|
||||
ActiveCallType,
|
||||
CallMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
|
@ -34,8 +34,6 @@ export type PropsType = {
|
|||
activeCall: ActiveCallType;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
hangUp: (_: HangUpType) => void;
|
||||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
i18n: LocalizerType;
|
||||
joinedAt?: number;
|
||||
me: {
|
||||
|
@ -61,8 +59,6 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
activeCall,
|
||||
getGroupCallVideoFrameSource,
|
||||
hangUp,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
i18n,
|
||||
joinedAt,
|
||||
me,
|
||||
|
@ -76,7 +72,12 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
togglePip,
|
||||
toggleSettings,
|
||||
}) => {
|
||||
const { call, conversation, groupCallParticipants } = activeCall;
|
||||
const {
|
||||
conversation,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
showParticipantsList,
|
||||
} = activeCall;
|
||||
|
||||
const toggleAudio = useCallback(() => {
|
||||
setLocalAudio({
|
||||
|
@ -148,23 +149,25 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
};
|
||||
}, [toggleAudio, toggleVideo]);
|
||||
|
||||
let hasRemoteVideo: boolean;
|
||||
const hasRemoteVideo = activeCall.remoteParticipants.some(
|
||||
remoteParticipant => remoteParticipant.hasRemoteVideo
|
||||
);
|
||||
|
||||
let headerMessage: string | undefined;
|
||||
let headerTitle: string | undefined;
|
||||
let isConnected: boolean;
|
||||
let participantCount: number;
|
||||
let remoteParticipantsElement: JSX.Element;
|
||||
|
||||
switch (call.callMode) {
|
||||
switch (activeCall.callMode) {
|
||||
case CallMode.Direct:
|
||||
hasRemoteVideo = Boolean(call.hasRemoteVideo);
|
||||
headerMessage = renderHeaderMessage(
|
||||
i18n,
|
||||
call.callState || CallState.Prering,
|
||||
activeCall.callState || CallState.Prering,
|
||||
acceptedDuration
|
||||
);
|
||||
headerTitle = conversation.title;
|
||||
isConnected = call.callState === CallState.Accepted;
|
||||
isConnected = activeCall.callState === CallState.Accepted;
|
||||
participantCount = isConnected ? 2 : 0;
|
||||
remoteParticipantsElement = (
|
||||
<DirectCallRemoteParticipant
|
||||
|
@ -176,26 +179,24 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
);
|
||||
break;
|
||||
case CallMode.Group:
|
||||
hasRemoteVideo = call.remoteParticipants.some(
|
||||
remoteParticipant => remoteParticipant.hasRemoteVideo
|
||||
);
|
||||
participantCount = activeCall.groupCallParticipants.length;
|
||||
participantCount = activeCall.remoteParticipants.length + 1;
|
||||
headerMessage = undefined;
|
||||
headerTitle = activeCall.groupCallParticipants.length
|
||||
headerTitle = activeCall.remoteParticipants.length
|
||||
? undefined
|
||||
: i18n('calling__in-this-call--zero');
|
||||
isConnected = call.connectionState === GroupCallConnectionState.Connected;
|
||||
isConnected =
|
||||
activeCall.connectionState === GroupCallConnectionState.Connected;
|
||||
remoteParticipantsElement = (
|
||||
<GroupCallRemoteParticipants
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
i18n={i18n}
|
||||
remoteParticipants={groupCallParticipants}
|
||||
remoteParticipants={activeCall.remoteParticipants}
|
||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(call);
|
||||
throw missingCaseError(activeCall);
|
||||
}
|
||||
|
||||
const videoButtonType = hasLocalVideo
|
||||
|
@ -214,14 +215,12 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
!showControls && !isAudioOnly && isConnected,
|
||||
});
|
||||
|
||||
const { showParticipantsList } = activeCall.activeCallState;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling__container',
|
||||
`module-ongoing-call__container--${getCallModeClassSuffix(
|
||||
call.callMode
|
||||
activeCall.callMode
|
||||
)}`
|
||||
)}
|
||||
onMouseMove={() => {
|
||||
|
@ -229,9 +228,9 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
}}
|
||||
role="group"
|
||||
>
|
||||
{call.callMode === CallMode.Group ? (
|
||||
{activeCall.callMode === CallMode.Group ? (
|
||||
<GroupCallToastManager
|
||||
connectionState={call.connectionState}
|
||||
connectionState={activeCall.connectionState}
|
||||
i18n={i18n}
|
||||
/>
|
||||
) : null}
|
||||
|
@ -241,7 +240,7 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
<CallingHeader
|
||||
canPip
|
||||
i18n={i18n}
|
||||
isGroupCall={call.callMode === CallMode.Group}
|
||||
isGroupCall={activeCall.callMode === CallMode.Group}
|
||||
message={headerMessage}
|
||||
participantCount={participantCount}
|
||||
showParticipantsList={showParticipantsList}
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as React from 'react';
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { CallingLobby, PropsType } from './CallingLobby';
|
||||
|
@ -35,7 +36,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
me: overrideProps.me || { color: 'ultramarine' as ColorType },
|
||||
onCallCanceled: action('on-call-canceled'),
|
||||
onJoinCall: action('on-join-call'),
|
||||
participantNames: overrideProps.participantNames || [],
|
||||
peekedParticipants: overrideProps.peekedParticipants || [],
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalPreview: action('set-local-preview'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
|
@ -47,6 +48,12 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
toggleSettings: action('toggle-settings'),
|
||||
});
|
||||
|
||||
const fakePeekedParticipant = (title: string) => ({
|
||||
isSelf: false,
|
||||
title,
|
||||
uuid: generateUuid(),
|
||||
});
|
||||
|
||||
const story = storiesOf('Components/CallingLobby', module);
|
||||
|
||||
story.add('Default', () => {
|
||||
|
@ -86,44 +93,51 @@ story.add('Local Video', () => {
|
|||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 0', () => {
|
||||
const props = createProps({ isGroupCall: true, participantNames: [] });
|
||||
story.add('Group Call - 0 peeked participants', () => {
|
||||
const props = createProps({ isGroupCall: true, peekedParticipants: [] });
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 1', () => {
|
||||
const props = createProps({ isGroupCall: true, participantNames: ['Sam'] });
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 2', () => {
|
||||
story.add('Group Call - 1 peeked participant', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
participantNames: ['Sam', 'Cayce'],
|
||||
peekedParticipants: ['Sam'].map(fakePeekedParticipant),
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 3', () => {
|
||||
story.add('Group Call - 2 peeked participants', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
participantNames: ['Sam', 'Cayce', 'April'],
|
||||
peekedParticipants: ['Sam', 'Cayce'].map(fakePeekedParticipant),
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 4', () => {
|
||||
story.add('Group Call - 3 peeked participants', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
participantNames: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'],
|
||||
peekedParticipants: ['Sam', 'Cayce', 'April'].map(fakePeekedParticipant),
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 4 (participants list)', () => {
|
||||
story.add('Group Call - 4 peeked participants', () => {
|
||||
const props = createProps({
|
||||
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,
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
|
|
|
@ -31,7 +31,12 @@ export type PropsType = {
|
|||
};
|
||||
onCallCanceled: () => void;
|
||||
onJoinCall: () => void;
|
||||
participantNames: Array<string>;
|
||||
peekedParticipants: Array<{
|
||||
firstName?: string;
|
||||
isSelf: boolean;
|
||||
title: string;
|
||||
uuid: string;
|
||||
}>;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||
|
@ -51,7 +56,7 @@ export const CallingLobby = ({
|
|||
me,
|
||||
onCallCanceled,
|
||||
onJoinCall,
|
||||
participantNames,
|
||||
peekedParticipants,
|
||||
setLocalAudio,
|
||||
setLocalPreview,
|
||||
setLocalVideo,
|
||||
|
@ -114,6 +119,16 @@ export const CallingLobby = ({
|
|||
? CallingButtonType.AUDIO_ON
|
||||
: 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;
|
||||
if (isCallFull) {
|
||||
joinButton = (
|
||||
|
@ -159,7 +174,7 @@ export const CallingLobby = ({
|
|||
title={conversation.title}
|
||||
i18n={i18n}
|
||||
isGroupCall={isGroupCall}
|
||||
participantCount={participantNames.length}
|
||||
participantCount={peekedParticipants.length}
|
||||
showParticipantsList={showParticipantsList}
|
||||
toggleParticipants={toggleParticipants}
|
||||
toggleSettings={toggleSettings}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
|
||||
import { CallingParticipantsList, PropsType } from './CallingParticipantsList';
|
||||
import { Colors } from '../types/Colors';
|
||||
|
@ -29,6 +30,7 @@ function createParticipant(
|
|||
profileName: participantProps.title,
|
||||
title: String(participantProps.title),
|
||||
videoAspectRatio: 1.3,
|
||||
uuid: generateUuid(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -9,18 +9,28 @@ import { Avatar } from './Avatar';
|
|||
import { ContactName } from './conversation/ContactName';
|
||||
import { InContactsIcon } from './InContactsIcon';
|
||||
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 = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly onClose: () => void;
|
||||
readonly participants: Array<GroupCallRemoteParticipantType>;
|
||||
readonly participants: Array<ParticipantType>;
|
||||
};
|
||||
|
||||
export const CallingParticipantsList = React.memo(
|
||||
({ i18n, onClose, participants }: PropsType) => {
|
||||
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(() => {
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
|
@ -70,10 +80,13 @@ export const CallingParticipantsList = React.memo(
|
|||
/>
|
||||
</div>
|
||||
<ul className="module-calling-participants-list__list">
|
||||
{participants.map(
|
||||
(participant: GroupCallRemoteParticipantType, index: number) => (
|
||||
{sortedParticipants.map(
|
||||
(participant: ParticipantType, index: number) => (
|
||||
<li
|
||||
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}
|
||||
>
|
||||
<div>
|
||||
|
@ -110,10 +123,10 @@ export const CallingParticipantsList = React.memo(
|
|||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!participant.hasRemoteAudio ? (
|
||||
{participant.hasAudio === false ? (
|
||||
<span className="module-calling-participants-list__muted--audio" />
|
||||
) : null}
|
||||
{!participant.hasRemoteVideo ? (
|
||||
{participant.hasVideo === false ? (
|
||||
<span className="module-calling-participants-list__muted--video" />
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
@ -9,9 +9,9 @@ import { action } from '@storybook/addon-actions';
|
|||
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { ConversationTypeType } from '../state/ducks/conversations';
|
||||
import { ActiveCallType } from '../state/ducks/calling';
|
||||
import { CallingPip, PropsType } from './CallingPip';
|
||||
import {
|
||||
ActiveCallType,
|
||||
CallMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
|
@ -35,34 +35,26 @@ const conversation = {
|
|||
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,
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Accepted,
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
peekedParticipants: [],
|
||||
remoteParticipants: [{ hasRemoteVideo: true }],
|
||||
};
|
||||
|
||||
const createProps = (
|
||||
overrideProps: Partial<PropsType> = {},
|
||||
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: [],
|
||||
},
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
activeCall: overrideProps.activeCall || defaultCall,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getGroupCallVideoFrameSource: noop as any,
|
||||
hangUp: action('hang-up'),
|
||||
|
@ -82,48 +74,43 @@ story.add('Default', () => {
|
|||
});
|
||||
|
||||
story.add('Contact (with avatar)', () => {
|
||||
const props = createProps(
|
||||
{},
|
||||
{
|
||||
const props = createProps({
|
||||
activeCall: {
|
||||
...defaultCall,
|
||||
conversation: {
|
||||
...conversation,
|
||||
avatarPath: 'https://www.fillmurray.com/64/64',
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
return <CallingPip {...props} />;
|
||||
});
|
||||
|
||||
story.add('Contact (no color)', () => {
|
||||
const props = createProps(
|
||||
{},
|
||||
{
|
||||
const props = createProps({
|
||||
activeCall: {
|
||||
...defaultCall,
|
||||
conversation: {
|
||||
...conversation,
|
||||
color: undefined,
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
return <CallingPip {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call', () => {
|
||||
const props = createProps(
|
||||
{},
|
||||
{
|
||||
call: {
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
conversationId: '3051234567',
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
peekInfo: {
|
||||
conversationIds: [],
|
||||
maxDevices: 16,
|
||||
deviceCount: 0,
|
||||
},
|
||||
remoteParticipants: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
const props = createProps({
|
||||
activeCall: {
|
||||
...getCommonActiveCallData(),
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
peekedParticipants: [],
|
||||
remoteParticipants: [],
|
||||
},
|
||||
});
|
||||
return <CallingPip {...props} />;
|
||||
});
|
||||
|
|
|
@ -5,9 +5,12 @@ import React from 'react';
|
|||
import { minBy, debounce, noop } from 'lodash';
|
||||
import { CallingPipRemoteVideo } from './CallingPipRemoteVideo';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { GroupCallVideoRequest, VideoFrameSource } from '../types/Calling';
|
||||
import {
|
||||
ActiveCallType,
|
||||
GroupCallVideoRequest,
|
||||
VideoFrameSource,
|
||||
} from '../types/Calling';
|
||||
import {
|
||||
HangUpType,
|
||||
SetLocalPreviewType,
|
||||
SetRendererCanvasType,
|
||||
|
|
|
@ -9,12 +9,13 @@ import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
|||
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import {
|
||||
ActiveCallType,
|
||||
CallMode,
|
||||
GroupCallRemoteParticipantType,
|
||||
GroupCallVideoRequest,
|
||||
VideoFrameSource,
|
||||
} from '../types/Calling';
|
||||
import { ActiveCallType, SetRendererCanvasType } from '../state/ducks/calling';
|
||||
import { SetRendererCanvasType } from '../state/ducks/calling';
|
||||
import { usePageVisibility } from '../util/hooks';
|
||||
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
||||
|
||||
|
@ -74,31 +75,31 @@ export const CallingPipRemoteVideo = ({
|
|||
setGroupCallVideoRequest,
|
||||
setRendererCanvas,
|
||||
}: PropsType): JSX.Element => {
|
||||
const { call, conversation, groupCallParticipants } = activeCall;
|
||||
const { conversation } = activeCall;
|
||||
|
||||
const isPageVisible = usePageVisibility();
|
||||
|
||||
const activeGroupCallSpeaker:
|
||||
| undefined
|
||||
| GroupCallRemoteParticipantType = useMemo(() => {
|
||||
if (call.callMode !== CallMode.Group) {
|
||||
if (activeCall.callMode !== CallMode.Group) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return maxBy(
|
||||
groupCallParticipants,
|
||||
activeCall.remoteParticipants,
|
||||
participant => participant.speakerTime || -Infinity
|
||||
);
|
||||
}, [call.callMode, groupCallParticipants]);
|
||||
}, [activeCall.callMode, activeCall.remoteParticipants]);
|
||||
|
||||
useEffect(() => {
|
||||
if (call.callMode !== CallMode.Group) {
|
||||
if (activeCall.callMode !== CallMode.Group) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPageVisible) {
|
||||
setGroupCallVideoRequest(
|
||||
groupCallParticipants.map(participant => {
|
||||
activeCall.remoteParticipants.map(participant => {
|
||||
const isVisible =
|
||||
participant === activeGroupCallSpeaker &&
|
||||
participant.hasRemoteVideo;
|
||||
|
@ -116,19 +117,21 @@ export const CallingPipRemoteVideo = ({
|
|||
);
|
||||
} else {
|
||||
setGroupCallVideoRequest(
|
||||
groupCallParticipants.map(nonRenderedRemoteParticipant)
|
||||
activeCall.remoteParticipants.map(nonRenderedRemoteParticipant)
|
||||
);
|
||||
}
|
||||
}, [
|
||||
call.callMode,
|
||||
groupCallParticipants,
|
||||
activeCall.callMode,
|
||||
activeCall.remoteParticipants,
|
||||
activeGroupCallSpeaker,
|
||||
isPageVisible,
|
||||
setGroupCallVideoRequest,
|
||||
]);
|
||||
|
||||
if (call.callMode === CallMode.Direct) {
|
||||
if (!call.hasRemoteVideo) {
|
||||
if (activeCall.callMode === CallMode.Direct) {
|
||||
const { hasRemoteVideo } = activeCall.remoteParticipants[0];
|
||||
|
||||
if (!hasRemoteVideo) {
|
||||
return <NoVideo activeCall={activeCall} i18n={i18n} />;
|
||||
}
|
||||
|
||||
|
@ -136,7 +139,7 @@ export const CallingPipRemoteVideo = ({
|
|||
<div className="module-calling-pip__video--remote">
|
||||
<DirectCallRemoteParticipant
|
||||
conversation={conversation}
|
||||
hasRemoteVideo={call.hasRemoteVideo}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
i18n={i18n}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
/>
|
||||
|
@ -144,7 +147,7 @@ export const CallingPipRemoteVideo = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (call.callMode === CallMode.Group) {
|
||||
if (activeCall.callMode === CallMode.Group) {
|
||||
if (!activeGroupCallSpeaker) {
|
||||
return <NoVideo activeCall={activeCall} i18n={i18n} />;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
peekInfo: PeekInfo = { joinedMembers: [], deviceCount: 0 }
|
||||
peekInfo: PeekInfo
|
||||
): GroupCallPeekInfoType {
|
||||
return {
|
||||
conversationIds: peekInfo.joinedMembers.map(this.uuidToConversationId),
|
||||
creator: peekInfo.creator && this.uuidToConversationId(peekInfo.creator),
|
||||
uuids: peekInfo.joinedMembers.map(uuidBuffer => {
|
||||
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,
|
||||
maxDevices: peekInfo.maxDevices ?? Infinity,
|
||||
deviceCount: peekInfo.deviceCount,
|
||||
|
@ -592,23 +589,16 @@ export class CallingClass {
|
|||
|
||||
private formatGroupCallForRedux(groupCall: GroupCall) {
|
||||
const localDeviceState = groupCall.getLocalDeviceState();
|
||||
const peekInfo = groupCall.getPeekInfo();
|
||||
|
||||
// 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
|
||||
// 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(
|
||||
groupCall.getRemoteDeviceStates() || [],
|
||||
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
|
||||
// try to handle that case.
|
||||
const joinState: GroupCallJoinState =
|
||||
|
@ -616,8 +606,6 @@ export class CallingClass {
|
|||
? GroupCallJoinState.NotJoined
|
||||
: this.convertRingRtcJoinState(localDeviceState.joinState);
|
||||
|
||||
const ourConversationId = window.ConversationController.getOurConversationId();
|
||||
|
||||
return {
|
||||
connectionState: this.convertRingRtcConnectionState(
|
||||
localDeviceState.connectionState
|
||||
|
@ -625,17 +613,22 @@ export class CallingClass {
|
|||
joinState,
|
||||
hasLocalAudio: !localDeviceState.audioMuted,
|
||||
hasLocalVideo: !localDeviceState.videoMuted,
|
||||
peekInfo: this.formatGroupCallPeekInfoForRedux(peekInfo),
|
||||
peekInfo: peekInfo
|
||||
? this.formatGroupCallPeekInfoForRedux(peekInfo)
|
||||
: undefined,
|
||||
remoteParticipants: remoteDeviceStates.map(remoteDeviceState => {
|
||||
const conversationId = this.uuidToConversationId(
|
||||
remoteDeviceState.userId
|
||||
);
|
||||
let uuid = arrayBufferToUuid(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 {
|
||||
conversationId,
|
||||
uuid,
|
||||
demuxId: remoteDeviceState.demuxId,
|
||||
hasRemoteAudio: !remoteDeviceState.audioMuted,
|
||||
hasRemoteVideo: !remoteDeviceState.videoMuted,
|
||||
isSelf: conversationId === ourConversationId,
|
||||
speakerTime: normalizeGroupCallTimestamp(
|
||||
remoteDeviceState.speakerTime
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -72,19 +72,18 @@ describe('calling duck', () => {
|
|||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.NotJoined,
|
||||
peekInfo: {
|
||||
conversationIds: ['456'],
|
||||
creator: '456',
|
||||
uuids: ['456'],
|
||||
creatorUuid: '456',
|
||||
eraId: 'xyz',
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
uuid: '123',
|
||||
demuxId: 123,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
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 rootState = rootReducer(undefined, noopAction());
|
||||
|
@ -112,7 +111,7 @@ describe('calling duck', () => {
|
|||
...rootState,
|
||||
user: {
|
||||
...rootState.user,
|
||||
ourConversationId,
|
||||
ourUuid,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -275,7 +274,7 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: false,
|
||||
hasLocalVideo: false,
|
||||
peekInfo: {
|
||||
conversationIds: [],
|
||||
uuids: [],
|
||||
maxDevices: 16,
|
||||
deviceCount: 0,
|
||||
},
|
||||
|
@ -296,7 +295,7 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: false,
|
||||
hasLocalVideo: false,
|
||||
peekInfo: {
|
||||
conversationIds: [],
|
||||
uuids: [],
|
||||
maxDevices: 16,
|
||||
deviceCount: 0,
|
||||
},
|
||||
|
@ -320,7 +319,7 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: false,
|
||||
hasLocalVideo: false,
|
||||
peekInfo: {
|
||||
conversationIds: [ourConversationId],
|
||||
uuids: [ourUuid],
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
|
@ -344,7 +343,7 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: false,
|
||||
hasLocalVideo: false,
|
||||
peekInfo: {
|
||||
conversationIds: [],
|
||||
uuids: [],
|
||||
maxDevices: 16,
|
||||
deviceCount: 0,
|
||||
},
|
||||
|
@ -365,7 +364,7 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: false,
|
||||
hasLocalVideo: false,
|
||||
peekInfo: {
|
||||
conversationIds: [ourConversationId],
|
||||
uuids: [ourUuid],
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
|
@ -386,19 +385,18 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
peekInfo: {
|
||||
conversationIds: ['456'],
|
||||
creator: '456',
|
||||
uuids: ['456'],
|
||||
creatorUuid: '456',
|
||||
eraId: 'xyz',
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
uuid: '123',
|
||||
demuxId: 123,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 4 / 3,
|
||||
},
|
||||
],
|
||||
|
@ -413,19 +411,18 @@ describe('calling duck', () => {
|
|||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joining,
|
||||
peekInfo: {
|
||||
conversationIds: ['456'],
|
||||
creator: '456',
|
||||
uuids: ['456'],
|
||||
creatorUuid: '456',
|
||||
eraId: 'xyz',
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
uuid: '123',
|
||||
demuxId: 123,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 4 / 3,
|
||||
},
|
||||
],
|
||||
|
@ -443,7 +440,7 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: false,
|
||||
hasLocalVideo: false,
|
||||
peekInfo: {
|
||||
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
|
@ -459,7 +456,7 @@ describe('calling duck', () => {
|
|||
connectionState: GroupCallConnectionState.NotConnected,
|
||||
joinState: GroupCallJoinState.NotJoined,
|
||||
peekInfo: {
|
||||
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
|
@ -478,17 +475,16 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
peekInfo: {
|
||||
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
uuid: '123',
|
||||
demuxId: 456,
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
|
@ -503,17 +499,16 @@ describe('calling duck', () => {
|
|||
connectionState: GroupCallConnectionState.Connected,
|
||||
joinState: GroupCallJoinState.Joined,
|
||||
peekInfo: {
|
||||
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
uuid: '123',
|
||||
demuxId: 456,
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
|
@ -531,17 +526,16 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
peekInfo: {
|
||||
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
uuid: '123',
|
||||
demuxId: 456,
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
|
@ -561,17 +555,16 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
peekInfo: {
|
||||
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
uuid: '123',
|
||||
demuxId: 456,
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
|
@ -598,17 +591,16 @@ describe('calling duck', () => {
|
|||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
peekInfo: {
|
||||
conversationIds: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
uuids: ['1b9e4d42-1f56-45c5-b6f4-d1be5a54fefa'],
|
||||
maxDevices: 16,
|
||||
deviceCount: 1,
|
||||
},
|
||||
remoteParticipants: [
|
||||
{
|
||||
conversationId: '123',
|
||||
uuid: '123',
|
||||
demuxId: 456,
|
||||
hasRemoteAudio: false,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
|
@ -771,7 +763,7 @@ describe('calling duck', () => {
|
|||
describe('showCallLobby', () => {
|
||||
const { showCallLobby } = actions;
|
||||
|
||||
it('saves the call and makes it active', () => {
|
||||
it('saves a direct call and makes it active', () => {
|
||||
const result = reducer(
|
||||
getEmptyState(),
|
||||
showCallLobby({
|
||||
|
@ -797,6 +789,161 @@ describe('calling duck', () => {
|
|||
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', () => {
|
||||
|
@ -965,10 +1112,10 @@ describe('calling duck', () => {
|
|||
});
|
||||
|
||||
describe('isAnybodyElseInGroupCall', () => {
|
||||
const fakePeekInfo = (conversationIds: Array<string>) => ({
|
||||
conversationIds,
|
||||
maxDevices: 16,
|
||||
deviceCount: conversationIds.length,
|
||||
const fakePeekInfo = (uuids: Array<string>) => ({
|
||||
uuids,
|
||||
maxDevices: 5,
|
||||
deviceCount: uuids.length,
|
||||
});
|
||||
|
||||
it('returns false if the peek info has no participants', () => {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ColorType } from './Colors';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
export enum CallMode {
|
||||
None = 'None',
|
||||
|
@ -9,6 +10,40 @@ export enum CallMode {
|
|||
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
|
||||
// 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.
|
||||
|
@ -58,7 +93,6 @@ export enum GroupCallJoinState {
|
|||
Joined = 2,
|
||||
}
|
||||
|
||||
// TODO: The way we deal with remote participants isn't ideal. See DESKTOP-949.
|
||||
export interface GroupCallPeekedParticipantType {
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
|
@ -67,20 +101,16 @@ export interface GroupCallPeekedParticipantType {
|
|||
name?: string;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
uuid: string;
|
||||
}
|
||||
export interface GroupCallRemoteParticipantType {
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
|
||||
export interface GroupCallRemoteParticipantType
|
||||
extends GroupCallPeekedParticipantType {
|
||||
demuxId: number;
|
||||
firstName?: string;
|
||||
hasRemoteAudio: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
isBlocked: boolean;
|
||||
isSelf: boolean;
|
||||
name?: string;
|
||||
profileName?: string;
|
||||
speakerTime?: number;
|
||||
title: string;
|
||||
videoAspectRatio: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -14400,7 +14400,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingLobby.tsx",
|
||||
"line": " const localVideoRef = React.useRef(null);",
|
||||
"lineNumber": 62,
|
||||
"lineNumber": 67,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
|
@ -14427,7 +14427,7 @@
|
|||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingPip.tsx",
|
||||
"line": " const localVideoRef = React.useRef(null);",
|
||||
"lineNumber": 80,
|
||||
"lineNumber": 83,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Used to get the local video element for rendering."
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { GroupCallVideoRequest } from '../../types/Calling';
|
||||
import { GroupCallVideoRequest } from '../../types/Calling';
|
||||
|
||||
export const nonRenderedRemoteParticipant = ({
|
||||
demuxId,
|
||||
|
|
Loading…
Add table
Reference in a new issue