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(),
});
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 }],
},
})}
/>

View file

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

View file

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

View file

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

View file

@ -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} />;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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} />;
}