Group calling enhancements
This commit is contained in:
parent
72e4ec95ce
commit
1f0c091e13
27 changed files with 1038 additions and 451 deletions
|
@ -5,6 +5,7 @@ import * as React from 'react';
|
|||
import { noop } from 'lodash';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean, select, text } from '@storybook/addon-knobs';
|
||||
|
||||
import { CallManager, PropsType } from './CallManager';
|
||||
import {
|
||||
|
@ -15,26 +16,47 @@ import {
|
|||
GroupCallJoinState,
|
||||
} from '../types/Calling';
|
||||
import { ConversationTypeType } from '../state/ducks/conversations';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { Colors, ColorType } from '../types/Colors';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const conversation = {
|
||||
const getConversation = () => ({
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: 'ultramarine' as ColorType,
|
||||
title: 'Rick Sanchez',
|
||||
name: 'Rick Sanchez',
|
||||
color: select('Callee color', Colors, 'ultramarine' as ColorType),
|
||||
title: text('Callee Title', 'Rick Sanchez'),
|
||||
name: text('Callee Name', 'Rick Sanchez'),
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
markedUnread: false,
|
||||
type: 'direct' as ConversationTypeType,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
const getCallState = () => ({
|
||||
conversationId: '3051234567',
|
||||
joinedAt: Date.now(),
|
||||
hasLocalAudio: boolean('hasLocalAudio', true),
|
||||
hasLocalVideo: boolean('hasLocalVideo', false),
|
||||
pip: boolean('pip', false),
|
||||
settingsDialogOpen: boolean('settingsDialogOpen', false),
|
||||
showParticipantsList: boolean('showParticipantsList', false),
|
||||
});
|
||||
|
||||
const getIncomingCallState = (extraProps = {}) => ({
|
||||
...extraProps,
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Ringing,
|
||||
isIncoming: true,
|
||||
isVideoCall: boolean('isVideoCall', true),
|
||||
hasRemoteVideo: true,
|
||||
});
|
||||
|
||||
const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||
...storyProps,
|
||||
availableCameras: [],
|
||||
acceptCall: action('accept-call'),
|
||||
cancelCall: action('cancel-call'),
|
||||
|
@ -54,8 +76,8 @@ const defaultProps = {
|
|||
hangUp: action('hang-up'),
|
||||
i18n,
|
||||
me: {
|
||||
color: 'ultramarine' as ColorType,
|
||||
title: 'Morty Smith',
|
||||
color: select('Caller color', Colors, 'ultramarine' as ColorType),
|
||||
title: text('Caller Title', 'Morty Smith'),
|
||||
},
|
||||
renderDeviceSelection: () => <div />,
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
|
@ -66,16 +88,15 @@ const defaultProps = {
|
|||
toggleParticipants: action('toggle-participants'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
};
|
||||
});
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Call Manager (no call)',
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
title: 'Call Manager (ongoing direct call)',
|
||||
props: {
|
||||
const story = storiesOf('Components/CallManager', module);
|
||||
|
||||
story.add('No Call', () => <CallManager {...createProps()} />);
|
||||
|
||||
story.add('Ongoing Direct Call', () => (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: {
|
||||
call: {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
|
@ -85,22 +106,17 @@ const permutations = [
|
|||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
},
|
||||
activeCallState: {
|
||||
conversationId: '3051234567',
|
||||
joinedAt: Date.now(),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
conversation,
|
||||
activeCallState: getCallState(),
|
||||
conversation: getConversation(),
|
||||
groupCallParticipants: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Manager (ongoing group call)',
|
||||
props: {
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Ongoing Group Call', () => (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: {
|
||||
call: {
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
|
@ -109,70 +125,36 @@ const permutations = [
|
|||
joinState: GroupCallJoinState.Joined,
|
||||
remoteParticipants: [],
|
||||
},
|
||||
activeCallState: {
|
||||
conversationId: '3051234567',
|
||||
joinedAt: Date.now(),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
conversation,
|
||||
activeCallState: getCallState(),
|
||||
conversation: getConversation(),
|
||||
groupCallParticipants: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Manager (ringing)',
|
||||
props: {
|
||||
incomingCall: {
|
||||
call: {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Ringing,
|
||||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
},
|
||||
conversation,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Manager (call request needed)',
|
||||
props: {
|
||||
activeCall: {
|
||||
call: {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Ended,
|
||||
callEndedReason: CallEndedReason.RemoteHangupNeedPermission,
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
},
|
||||
activeCallState: {
|
||||
conversationId: '3051234567',
|
||||
joinedAt: Date.now(),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: false,
|
||||
participantsList: false,
|
||||
pip: false,
|
||||
settingsDialogOpen: false,
|
||||
},
|
||||
conversation,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
storiesOf('Components/CallManager', module).add('Iterations', () => {
|
||||
return permutations.map(
|
||||
({ props, title }: { props: Partial<PropsType>; title: string }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<CallManager {...defaultProps} {...props} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
});
|
||||
story.add('Ringing', () => (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
incomingCall: {
|
||||
call: getIncomingCallState(),
|
||||
conversation: getConversation(),
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Call Request Needed', () => (
|
||||
<CallManager
|
||||
{...createProps({
|
||||
activeCall: {
|
||||
call: getIncomingCallState({
|
||||
callEndedReason: CallEndedReason.RemoteHangupNeedPermission,
|
||||
}),
|
||||
activeCallState: getCallState(),
|
||||
conversation: getConversation(),
|
||||
groupCallParticipants: [],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -2,18 +2,20 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { CallingPip } from './CallingPip';
|
||||
import { CallNeedPermissionScreen } from './CallNeedPermissionScreen';
|
||||
import { CallingLobby } from './CallingLobby';
|
||||
import { CallScreen } from './CallScreen';
|
||||
import { CallingLobby } from './CallingLobby';
|
||||
import { CallingParticipantsList } from './CallingParticipantsList';
|
||||
import { CallingPip } from './CallingPip';
|
||||
import { IncomingCallBar } from './IncomingCallBar';
|
||||
import {
|
||||
CallEndedReason,
|
||||
CallMode,
|
||||
CallState,
|
||||
CallEndedReason,
|
||||
CanvasVideoRenderer,
|
||||
VideoFrameSource,
|
||||
GroupCallJoinState,
|
||||
GroupCallRemoteParticipantType,
|
||||
VideoFrameSource,
|
||||
} from '../types/Calling';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
|
@ -35,9 +37,10 @@ import { ColorType } from '../types/Colors';
|
|||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
interface ActiveCallType {
|
||||
call: DirectCallStateType | GroupCallStateType;
|
||||
activeCallState: ActiveCallStateType;
|
||||
call: DirectCallStateType | GroupCallStateType;
|
||||
conversation: ConversationType;
|
||||
groupCallParticipants: Array<GroupCallRemoteParticipantType>;
|
||||
}
|
||||
|
||||
export interface PropsType {
|
||||
|
@ -101,13 +104,19 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
togglePip,
|
||||
toggleSettings,
|
||||
}) => {
|
||||
const { call, activeCallState, conversation } = activeCall;
|
||||
const {
|
||||
joinedAt,
|
||||
call,
|
||||
activeCallState,
|
||||
conversation,
|
||||
groupCallParticipants,
|
||||
} = activeCall;
|
||||
const {
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
settingsDialogOpen,
|
||||
joinedAt,
|
||||
pip,
|
||||
settingsDialogOpen,
|
||||
showParticipantsList,
|
||||
} = activeCallState;
|
||||
|
||||
const cancelActiveCall = useCallback(() => {
|
||||
|
@ -160,6 +169,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
}
|
||||
|
||||
if (showCallLobby) {
|
||||
const participantNames = groupCallParticipants.map(participant =>
|
||||
participant.isSelf
|
||||
? i18n('you')
|
||||
: participant.firstName || participant.title
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<CallingLobby
|
||||
|
@ -168,12 +182,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
// TODO: Set this to `true` for group calls. We can get away with this for
|
||||
// now because it only affects rendering. See DESKTOP-888 and DESKTOP-889.
|
||||
isGroupCall={false}
|
||||
isGroupCall={call.callMode === CallMode.Group}
|
||||
me={me}
|
||||
onCallCanceled={cancelActiveCall}
|
||||
onJoinCall={joinActiveCall}
|
||||
participantNames={participantNames}
|
||||
setLocalPreview={setLocalPreview}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalVideo={setLocalVideo}
|
||||
|
@ -181,20 +194,26 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
{settingsDialogOpen && renderDeviceSelection()}
|
||||
{showParticipantsList && call.callMode === CallMode.Group ? (
|
||||
<CallingParticipantsList
|
||||
i18n={i18n}
|
||||
onClose={toggleParticipants}
|
||||
participants={groupCallParticipants}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Group calls should also support the PiP. See DESKTOP-886.
|
||||
if (pip && call.callMode === CallMode.Direct) {
|
||||
const hasRemoteVideo = Boolean(call.hasRemoteVideo);
|
||||
|
||||
if (pip) {
|
||||
return (
|
||||
<CallingPip
|
||||
call={call}
|
||||
conversation={conversation}
|
||||
createCanvasVideoRenderer={createCanvasVideoRenderer}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSourceForActiveCall}
|
||||
hangUp={hangUp}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
i18n={i18n}
|
||||
setLocalPreview={setLocalPreview}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
|
@ -220,10 +239,19 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
|
|||
setRendererCanvas={setRendererCanvas}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalVideo={setLocalVideo}
|
||||
stickyControls={showParticipantsList}
|
||||
toggleParticipants={toggleParticipants}
|
||||
togglePip={togglePip}
|
||||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
{settingsDialogOpen && renderDeviceSelection()}
|
||||
{showParticipantsList && call.callMode === CallMode.Group ? (
|
||||
<CallingParticipantsList
|
||||
i18n={i18n}
|
||||
onClose={toggleParticipants}
|
||||
participants={groupCallParticipants}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,40 +8,68 @@ import { boolean, select } from '@storybook/addon-knobs';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { CallMode, CallState } from '../types/Calling';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { Colors } from '../types/Colors';
|
||||
import {
|
||||
DirectCallStateType,
|
||||
GroupCallStateType,
|
||||
GroupCallParticipantInfoType,
|
||||
} from '../state/ducks/calling';
|
||||
import { CallScreen, PropsType } from './CallScreen';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const createProps = (
|
||||
function getGroupCallState(
|
||||
remoteParticipants: Array<GroupCallParticipantInfoType>
|
||||
): GroupCallStateType {
|
||||
return {
|
||||
callMode: CallMode.Group,
|
||||
conversationId: '3051234567',
|
||||
connectionState: 2,
|
||||
joinState: 2,
|
||||
remoteParticipants,
|
||||
};
|
||||
}
|
||||
|
||||
function getDirectCallState(
|
||||
overrideProps: {
|
||||
callState?: CallState;
|
||||
hasLocalAudio?: boolean;
|
||||
hasLocalVideo?: boolean;
|
||||
hasRemoteVideo?: boolean;
|
||||
} = {}
|
||||
): PropsType => ({
|
||||
call: {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
): DirectCallStateType {
|
||||
return {
|
||||
callMode: CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: select(
|
||||
'callState',
|
||||
CallState,
|
||||
overrideProps.callState || CallState.Accepted
|
||||
),
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: boolean(
|
||||
'hasRemoteVideo',
|
||||
overrideProps.hasRemoteVideo || false
|
||||
Boolean(overrideProps.hasRemoteVideo)
|
||||
),
|
||||
},
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
};
|
||||
}
|
||||
|
||||
const createProps = (
|
||||
overrideProps: {
|
||||
callState?: CallState;
|
||||
callTypeState?: DirectCallStateType | GroupCallStateType;
|
||||
hasLocalAudio?: boolean;
|
||||
hasLocalVideo?: boolean;
|
||||
hasRemoteVideo?: boolean;
|
||||
remoteParticipants?: Array<GroupCallParticipantInfoType>;
|
||||
} = {}
|
||||
): PropsType => ({
|
||||
call: overrideProps.callTypeState || getDirectCallState(overrideProps),
|
||||
conversation: {
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: 'ultramarine' as ColorType,
|
||||
color: Colors[0],
|
||||
title: 'Rick Sanchez',
|
||||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
|
@ -67,7 +95,7 @@ const createProps = (
|
|||
i18n,
|
||||
joinedAt: Date.now(),
|
||||
me: {
|
||||
color: 'ultramarine' as ColorType,
|
||||
color: Colors[1],
|
||||
name: 'Morty Smith',
|
||||
profileName: 'Morty Smith',
|
||||
title: 'Morty Smith',
|
||||
|
@ -76,6 +104,8 @@ const createProps = (
|
|||
setLocalPreview: action('set-local-preview'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
stickyControls: boolean('stickyControls', false),
|
||||
toggleParticipants: action('toggle-participants'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
});
|
||||
|
@ -87,19 +117,43 @@ story.add('Default', () => {
|
|||
});
|
||||
|
||||
story.add('Pre-Ring', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Prering })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callState: CallState.Prering,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Ringing', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Ringing })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callState: CallState.Ringing,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Reconnecting', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Reconnecting })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callState: CallState.Reconnecting,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Ended', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Ended })} />;
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callState: CallState.Ended,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('hasLocalAudio', () => {
|
||||
|
@ -113,3 +167,53 @@ story.add('hasLocalVideo', () => {
|
|||
story.add('hasRemoteVideo', () => {
|
||||
return <CallScreen {...createProps({ hasRemoteVideo: true })} />;
|
||||
});
|
||||
|
||||
story.add('Group call - 1', () => (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callTypeState: getGroupCallState([
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 1.3,
|
||||
},
|
||||
]),
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Group call - Many', () => (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callTypeState: getGroupCallState([
|
||||
{
|
||||
conversationId: '123',
|
||||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 1.3,
|
||||
},
|
||||
{
|
||||
conversationId: '456',
|
||||
demuxId: 1,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: true,
|
||||
videoAspectRatio: 1.3,
|
||||
},
|
||||
{
|
||||
conversationId: '789',
|
||||
demuxId: 2,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isSelf: false,
|
||||
videoAspectRatio: 1.3,
|
||||
},
|
||||
]),
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
SetRendererCanvasType,
|
||||
} from '../state/ducks/calling';
|
||||
import { Avatar } from './Avatar';
|
||||
import { CallingHeader } from './CallingHeader';
|
||||
import { CallingButton, CallingButtonType } from './CallingButton';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import {
|
||||
|
@ -52,6 +53,8 @@ export type PropsType = {
|
|||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
stickyControls: boolean;
|
||||
toggleParticipants: () => void;
|
||||
togglePip: () => void;
|
||||
toggleSettings: () => void;
|
||||
};
|
||||
|
@ -71,6 +74,8 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
setLocalVideo,
|
||||
setLocalPreview,
|
||||
setRendererCanvas,
|
||||
stickyControls,
|
||||
toggleParticipants,
|
||||
togglePip,
|
||||
toggleSettings,
|
||||
}) => {
|
||||
|
@ -110,14 +115,14 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
}, [joinedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showControls) {
|
||||
if (!showControls || stickyControls) {
|
||||
return noop;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
setShowControls(false);
|
||||
}, 5000);
|
||||
return clearInterval.bind(null, timer);
|
||||
}, [showControls]);
|
||||
}, [showControls, stickyControls]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
|
@ -146,13 +151,13 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
|
||||
let hasRemoteVideo: boolean;
|
||||
let isConnected: boolean;
|
||||
let remoteParticipants: JSX.Element;
|
||||
let remoteParticipantsElement: JSX.Element;
|
||||
|
||||
switch (call.callMode) {
|
||||
case CallMode.Direct:
|
||||
hasRemoteVideo = Boolean(call.hasRemoteVideo);
|
||||
isConnected = call.callState === CallState.Accepted;
|
||||
remoteParticipants = (
|
||||
remoteParticipantsElement = (
|
||||
<DirectCallRemoteParticipant
|
||||
conversation={conversation}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
|
@ -166,7 +171,7 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
remoteParticipant => remoteParticipant.hasRemoteVideo
|
||||
);
|
||||
isConnected = call.connectionState === GroupCallConnectionState.Connected;
|
||||
remoteParticipants = (
|
||||
remoteParticipantsElement = (
|
||||
<GroupCallRemoteParticipants
|
||||
remoteParticipants={call.remoteParticipants}
|
||||
createCanvasVideoRenderer={createCanvasVideoRenderer}
|
||||
|
@ -194,6 +199,9 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
!showControls && !isAudioOnly && isConnected,
|
||||
});
|
||||
|
||||
const remoteParticipants =
|
||||
call.callMode === CallMode.Group ? call.remoteParticipants.length : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -208,40 +216,33 @@ export const CallScreen: React.FC<PropsType> = ({
|
|||
role="group"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling__header',
|
||||
'module-ongoing-call__header',
|
||||
controlsFadeClass
|
||||
)}
|
||||
className={classNames('module-ongoing-call__header', controlsFadeClass)}
|
||||
>
|
||||
<div className="module-calling__header--header-name">
|
||||
{conversation.title}
|
||||
</div>
|
||||
{call.callMode === CallMode.Direct &&
|
||||
renderHeaderMessage(
|
||||
i18n,
|
||||
call.callState || CallState.Prering,
|
||||
acceptedDuration
|
||||
)}
|
||||
<div className="module-calling-tools">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('callingDeviceSelection__settings')}
|
||||
className="module-calling-tools__button module-calling-button__settings"
|
||||
onClick={toggleSettings}
|
||||
/>
|
||||
{/* TODO: Group calls should also support the PiP. See DESKTOP-886. */}
|
||||
{call.callMode === CallMode.Direct && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__pip')}
|
||||
className="module-calling-tools__button module-calling-button__pip"
|
||||
onClick={togglePip}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<CallingHeader
|
||||
canPip
|
||||
conversationTitle={
|
||||
<>
|
||||
{call.callMode === CallMode.Group &&
|
||||
!call.remoteParticipants.length
|
||||
? i18n('calling__in-this-call--zero')
|
||||
: conversation.title}
|
||||
{call.callMode === CallMode.Direct &&
|
||||
renderHeaderMessage(
|
||||
i18n,
|
||||
call.callState || CallState.Prering,
|
||||
acceptedDuration
|
||||
)}
|
||||
</>
|
||||
}
|
||||
i18n={i18n}
|
||||
isGroupCall={call.callMode === CallMode.Group}
|
||||
remoteParticipants={remoteParticipants}
|
||||
toggleParticipants={toggleParticipants}
|
||||
togglePip={togglePip}
|
||||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
</div>
|
||||
{remoteParticipants}
|
||||
{remoteParticipantsElement}
|
||||
<div className="module-ongoing-call__footer">
|
||||
{/* This layout-only element is not ideal.
|
||||
See the comment in _modules.css for more. */}
|
||||
|
|
54
ts/components/CallingHeader.stories.tsx
Normal file
54
ts/components/CallingHeader.stories.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, number } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { CallingHeader, PropsType } from './CallingHeader';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
canPip: boolean('canPip', Boolean(overrideProps.canPip)),
|
||||
conversationTitle: overrideProps.conversationTitle || 'With Someone',
|
||||
i18n,
|
||||
isGroupCall: boolean('isGroupCall', Boolean(overrideProps.isGroupCall)),
|
||||
remoteParticipants: number(
|
||||
'remoteParticipants',
|
||||
overrideProps.remoteParticipants || 0
|
||||
),
|
||||
toggleParticipants: () => action('toggle-participants'),
|
||||
togglePip: () => action('toggle-pip'),
|
||||
toggleSettings: () => action('toggle-settings'),
|
||||
});
|
||||
|
||||
const story = storiesOf('Components/CallingHeader', module);
|
||||
|
||||
story.add('Default', () => <CallingHeader {...createProps()} />);
|
||||
|
||||
story.add('Has Pip', () => (
|
||||
<CallingHeader {...createProps({ canPip: true })} />
|
||||
));
|
||||
|
||||
story.add('With Participants', () => (
|
||||
<CallingHeader
|
||||
{...createProps({
|
||||
canPip: true,
|
||||
isGroupCall: true,
|
||||
remoteParticipants: 10,
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Long Title', () => (
|
||||
<CallingHeader
|
||||
{...createProps({
|
||||
conversationTitle:
|
||||
'What do I got to, what do I got to do to wake you up? To shake you up, to break the structure up?',
|
||||
})}
|
||||
/>
|
||||
));
|
89
ts/components/CallingHeader.tsx
Normal file
89
ts/components/CallingHeader.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import Tooltip from 'react-tooltip-lite';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
canPip?: boolean;
|
||||
conversationTitle: JSX.Element | string;
|
||||
i18n: LocalizerType;
|
||||
isGroupCall?: boolean;
|
||||
remoteParticipants?: number;
|
||||
toggleParticipants?: () => void;
|
||||
togglePip?: () => void;
|
||||
toggleSettings: () => void;
|
||||
};
|
||||
|
||||
export const CallingHeader = ({
|
||||
canPip = false,
|
||||
conversationTitle,
|
||||
i18n,
|
||||
isGroupCall = false,
|
||||
remoteParticipants,
|
||||
toggleParticipants,
|
||||
togglePip,
|
||||
toggleSettings,
|
||||
}: PropsType): JSX.Element => (
|
||||
<div className="module-calling__header">
|
||||
<div className="module-calling__header--header-name">
|
||||
{conversationTitle}
|
||||
</div>
|
||||
<div className="module-calling-tools">
|
||||
{isGroupCall ? (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
arrowSize={6}
|
||||
content={i18n('calling__participants', [
|
||||
String(remoteParticipants),
|
||||
])}
|
||||
direction="down"
|
||||
hoverDelay={0}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__participants', [
|
||||
String(remoteParticipants),
|
||||
])}
|
||||
className="module-calling-button__participants"
|
||||
onClick={toggleParticipants}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
arrowSize={6}
|
||||
content={i18n('callingDeviceSelection__settings')}
|
||||
direction="down"
|
||||
hoverDelay={0}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('callingDeviceSelection__settings')}
|
||||
className="module-calling-button__settings"
|
||||
onClick={toggleSettings}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{canPip && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
arrowSize={6}
|
||||
content={i18n('calling__pip--on')}
|
||||
direction="down"
|
||||
hoverDelay={0}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__pip--on')}
|
||||
className="module-calling-button__pip"
|
||||
onClick={togglePip}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
|
@ -35,6 +35,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 || [],
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalPreview: action('set-local-preview'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
|
@ -81,7 +82,36 @@ story.add('Local Video', () => {
|
|||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call', () => {
|
||||
const props = createProps({ isGroupCall: true });
|
||||
story.add('Group Call - 0', () => {
|
||||
const props = createProps({ isGroupCall: true, participantNames: [] });
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 1', () => {
|
||||
const props = createProps({ isGroupCall: true, participantNames: ['Sam'] });
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 2', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
participantNames: ['Sam', 'Cayce'],
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 3', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
participantNames: ['Sam', 'Cayce', 'April'],
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call - 4', () => {
|
||||
const props = createProps({
|
||||
isGroupCall: true,
|
||||
participantNames: ['Sam', 'Cayce', 'April', 'Logan', 'Carl'],
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
|
|
@ -13,8 +13,10 @@ import {
|
|||
TooltipDirection,
|
||||
} from './CallingButton';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { CallingHeader } from './CallingHeader';
|
||||
import { Spinner } from './Spinner';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
availableCameras: Array<MediaDeviceInfo>;
|
||||
|
@ -31,6 +33,7 @@ export type PropsType = {
|
|||
};
|
||||
onCallCanceled: () => void;
|
||||
onJoinCall: () => void;
|
||||
participantNames: Array<string>;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||
|
@ -48,6 +51,7 @@ export const CallingLobby = ({
|
|||
me,
|
||||
onCallCanceled,
|
||||
onJoinCall,
|
||||
participantNames,
|
||||
setLocalAudio,
|
||||
setLocalPreview,
|
||||
setLocalVideo,
|
||||
|
@ -97,6 +101,8 @@ export const CallingLobby = ({
|
|||
};
|
||||
}, [toggleVideo, toggleAudio]);
|
||||
|
||||
const [isCallConnecting, setIsCallConnecting] = React.useState(false);
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const videoButtonType = hasLocalVideo
|
||||
? CallingButtonType.VIDEO_ON
|
||||
|
@ -109,27 +115,15 @@ export const CallingLobby = ({
|
|||
|
||||
return (
|
||||
<div className="module-calling__container">
|
||||
<div className="module-calling__header">
|
||||
<div className="module-calling__header--header-name">
|
||||
{conversation.title}
|
||||
</div>
|
||||
<div className="module-calling-tools">
|
||||
{isGroupCall ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__participants')}
|
||||
className="module-calling-tools__button module-calling-button__participants"
|
||||
onClick={toggleParticipants}
|
||||
/>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('callingDeviceSelection__settings')}
|
||||
className="module-calling-tools__button module-calling-button__settings"
|
||||
onClick={toggleSettings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CallingHeader
|
||||
conversationTitle={conversation.title}
|
||||
i18n={i18n}
|
||||
isGroupCall={isGroupCall}
|
||||
remoteParticipants={participantNames.length}
|
||||
toggleParticipants={toggleParticipants}
|
||||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
|
||||
<div className="module-calling-lobby__video">
|
||||
{hasLocalVideo && availableCameras.length > 0 ? (
|
||||
<video ref={localVideoRef} autoPlay />
|
||||
|
@ -160,6 +154,32 @@ export const CallingLobby = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{isGroupCall ? (
|
||||
<div className="module-calling-lobby__info">
|
||||
{participantNames.length === 0 &&
|
||||
i18n('calling__lobby-summary--zero')}
|
||||
{participantNames.length === 1 &&
|
||||
i18n('calling__lobby-summary--single', participantNames)}
|
||||
{participantNames.length === 2 &&
|
||||
i18n('calling__lobby-summary--double', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
})}
|
||||
{participantNames.length === 3 &&
|
||||
i18n('calling__lobby-summary--triple', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
third: participantNames[2],
|
||||
})}
|
||||
{participantNames.length > 3 &&
|
||||
i18n('calling__lobby-summary--many', {
|
||||
first: participantNames[0],
|
||||
second: participantNames[1],
|
||||
others: String(participantNames.length - 2),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="module-calling-lobby__actions">
|
||||
<button
|
||||
className="module-button__gray module-calling-lobby__button"
|
||||
|
@ -169,14 +189,29 @@ export const CallingLobby = ({
|
|||
>
|
||||
{i18n('cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="module-button__green module-calling-lobby__button"
|
||||
onClick={onJoinCall}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
{isGroupCall ? i18n('calling__join') : i18n('calling__start')}
|
||||
</button>
|
||||
{isCallConnecting && (
|
||||
<button
|
||||
className="module-button__green module-calling-lobby__button"
|
||||
disabled
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<Spinner svgSize="small" />
|
||||
</button>
|
||||
)}
|
||||
{!isCallConnecting && (
|
||||
<button
|
||||
className="module-button__green module-calling-lobby__button"
|
||||
onClick={() => {
|
||||
setIsCallConnecting(true);
|
||||
onJoinCall();
|
||||
}}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
{isGroupCall ? i18n('calling__join') : i18n('calling__start')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -6,61 +6,76 @@ import { storiesOf } from '@storybook/react';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { CallingParticipantsList, PropsType } from './CallingParticipantsList';
|
||||
import { Colors } from '../types/Colors';
|
||||
import { GroupCallRemoteParticipantType } from '../types/Calling';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const participant = {
|
||||
title: 'Bardock',
|
||||
};
|
||||
function createParticipant(
|
||||
participantProps: Partial<GroupCallRemoteParticipantType>
|
||||
): GroupCallRemoteParticipantType {
|
||||
const randomColor = Math.floor(Math.random() * Colors.length - 1);
|
||||
return {
|
||||
avatarPath: participantProps.avatarPath,
|
||||
color: Colors[randomColor],
|
||||
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
||||
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
||||
isSelf: Boolean(participantProps.isSelf),
|
||||
profileName: participantProps.title,
|
||||
title: String(participantProps.title),
|
||||
};
|
||||
}
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
i18n,
|
||||
onClose: action('on-close'),
|
||||
participants: overrideProps.participants || [participant],
|
||||
participants: overrideProps.participants || [],
|
||||
});
|
||||
|
||||
const story = storiesOf('Components/CallingParticipantsList', module);
|
||||
|
||||
story.add('Default', () => {
|
||||
story.add('No one', () => {
|
||||
const props = createProps();
|
||||
return <CallingParticipantsList {...props} />;
|
||||
});
|
||||
|
||||
story.add('Solo Call', () => {
|
||||
const props = createProps({
|
||||
participants: [
|
||||
createParticipant({
|
||||
title: 'Bardock',
|
||||
}),
|
||||
],
|
||||
});
|
||||
return <CallingParticipantsList {...props} />;
|
||||
});
|
||||
|
||||
story.add('Many Participants', () => {
|
||||
const props = createProps({
|
||||
participants: [
|
||||
{
|
||||
color: 'blue',
|
||||
profileName: 'Son Goku',
|
||||
createParticipant({
|
||||
isSelf: true,
|
||||
title: 'Son Goku',
|
||||
audioMuted: true,
|
||||
videoMuted: true,
|
||||
},
|
||||
{
|
||||
color: 'deep_orange',
|
||||
profileName: 'Rage Trunks',
|
||||
}),
|
||||
createParticipant({
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
title: 'Rage Trunks',
|
||||
},
|
||||
{
|
||||
color: 'indigo',
|
||||
profileName: 'Prince Vegeta',
|
||||
}),
|
||||
createParticipant({
|
||||
hasRemoteAudio: true,
|
||||
title: 'Prince Vegeta',
|
||||
videoMuted: true,
|
||||
},
|
||||
{
|
||||
color: 'pink',
|
||||
profileName: 'Goku Black',
|
||||
}),
|
||||
createParticipant({
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
title: 'Goku Black',
|
||||
},
|
||||
{
|
||||
color: 'green',
|
||||
profileName: 'Supreme Kai Zamasu',
|
||||
}),
|
||||
createParticipant({
|
||||
title: 'Supreme Kai Zamasu',
|
||||
audioMuted: true,
|
||||
videoMuted: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
return <CallingParticipantsList {...props} />;
|
||||
|
|
|
@ -6,23 +6,14 @@
|
|||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Avatar } from './Avatar';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
type ParticipantType = {
|
||||
audioMuted?: boolean;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
videoMuted?: boolean;
|
||||
};
|
||||
import { GroupCallRemoteParticipantType } from '../types/Calling';
|
||||
|
||||
export type PropsType = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly onClose: () => void;
|
||||
readonly participants: Array<ParticipantType>;
|
||||
readonly participants: Array<GroupCallRemoteParticipantType>;
|
||||
};
|
||||
|
||||
export const CallingParticipantsList = React.memo(
|
||||
|
@ -52,11 +43,12 @@ export const CallingParticipantsList = React.memo(
|
|||
<div className="module-calling-participants-list">
|
||||
<div className="module-calling-participants-list__header">
|
||||
<div className="module-calling-participants-list__title">
|
||||
{participants.length > 1
|
||||
? i18n('calling__in-this-call--many', [
|
||||
String(participants.length),
|
||||
])
|
||||
: i18n('calling__in-this-call--one')}
|
||||
{!participants.length && i18n('calling__in-this-call--zero')}
|
||||
{participants.length === 1 && i18n('calling__in-this-call--one')}
|
||||
{participants.length > 1 &&
|
||||
i18n('calling__in-this-call--many', [
|
||||
String(participants.length),
|
||||
])}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
@ -67,37 +59,45 @@ export const CallingParticipantsList = React.memo(
|
|||
/>
|
||||
</div>
|
||||
<ul className="module-calling-participants-list__list">
|
||||
{participants.map((participant: ParticipantType, index: number) => (
|
||||
<li
|
||||
className="module-calling-participants-list__contact"
|
||||
key={index}
|
||||
>
|
||||
<div>
|
||||
<Avatar
|
||||
avatarPath={participant.avatarPath}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
size={32}
|
||||
/>
|
||||
<ContactName
|
||||
i18n={i18n}
|
||||
module="module-calling-participants-list__name"
|
||||
title={participant.title}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{participant.audioMuted ? (
|
||||
<span className="module-calling-participants-list__muted--audio" />
|
||||
) : null}
|
||||
{participant.videoMuted ? (
|
||||
<span className="module-calling-participants-list__muted--video" />
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{participants.map(
|
||||
(participant: GroupCallRemoteParticipantType, index: number) => (
|
||||
<li
|
||||
className="module-calling-participants-list__contact"
|
||||
key={index}
|
||||
>
|
||||
<div>
|
||||
<Avatar
|
||||
avatarPath={participant.avatarPath}
|
||||
color={participant.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
profileName={participant.profileName}
|
||||
title={participant.title}
|
||||
size={32}
|
||||
/>
|
||||
{participant.isSelf ? (
|
||||
<span className="module-calling-participants-list__name">
|
||||
{i18n('you')}
|
||||
</span>
|
||||
) : (
|
||||
<ContactName
|
||||
i18n={i18n}
|
||||
module="module-calling-participants-list__name"
|
||||
title={participant.title}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!participant.hasRemoteAudio ? (
|
||||
<span className="module-calling-participants-list__muted--audio" />
|
||||
) : null}
|
||||
{!participant.hasRemoteVideo ? (
|
||||
<span className="module-calling-participants-list__muted--video" />
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>,
|
||||
|
|
|
@ -2,12 +2,20 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { ConversationTypeType } from '../state/ducks/conversations';
|
||||
import { CallingPip, PropsType } from './CallingPip';
|
||||
import {
|
||||
CallMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
} from '../types/Calling';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
|
@ -21,16 +29,29 @@ const conversation = {
|
|||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
markedUnread: false,
|
||||
type: 'direct' as ConversationTypeType,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
|
||||
const defaultCall = {
|
||||
callMode: CallMode.Direct as CallMode.Direct,
|
||||
conversationId: '3051234567',
|
||||
callState: CallState.Accepted,
|
||||
isIncoming: false,
|
||||
isVideoCall: true,
|
||||
hasRemoteVideo: true,
|
||||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
call: overrideProps.call || defaultCall,
|
||||
conversation: overrideProps.conversation || conversation,
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
createCanvasVideoRenderer: noop as any,
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
getGroupCallVideoFrameSource: noop as any,
|
||||
hangUp: action('hang-up'),
|
||||
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
||||
hasRemoteVideo: boolean(
|
||||
'hasRemoteVideo',
|
||||
overrideProps.hasRemoteVideo || false
|
||||
),
|
||||
i18n,
|
||||
setLocalPreview: action('set-local-preview'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
|
@ -63,3 +84,16 @@ story.add('Contact (no color)', () => {
|
|||
});
|
||||
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,
|
||||
remoteParticipants: [],
|
||||
},
|
||||
});
|
||||
return <CallingPip {...props} />;
|
||||
});
|
||||
|
|
|
@ -2,69 +2,26 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import Tooltip from 'react-tooltip-lite';
|
||||
import { CallingPipRemoteVideo } from './CallingPipRemoteVideo';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import { CanvasVideoRenderer, VideoFrameSource } from '../types/Calling';
|
||||
import {
|
||||
DirectCallStateType,
|
||||
GroupCallStateType,
|
||||
HangUpType,
|
||||
SetLocalPreviewType,
|
||||
SetRendererCanvasType,
|
||||
} from '../state/ducks/calling';
|
||||
import { Avatar } from './Avatar';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
function renderAvatar(
|
||||
{
|
||||
avatarPath,
|
||||
color,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
title,
|
||||
}: {
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
title: string;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
},
|
||||
i18n: LocalizerType
|
||||
): JSX.Element {
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<CallBackgroundBlur avatarPath={avatarPath} color={color}>
|
||||
<div className="module-calling-pip__video--avatar">
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color={color || 'ultramarine'}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
size={52}
|
||||
/>
|
||||
</div>
|
||||
</CallBackgroundBlur>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type PropsType = {
|
||||
conversation: {
|
||||
id: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
title: string;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
};
|
||||
call: DirectCallStateType | GroupCallStateType;
|
||||
conversation: ConversationType;
|
||||
createCanvasVideoRenderer: () => CanvasVideoRenderer;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
hangUp: (_: HangUpType) => void;
|
||||
hasLocalVideo: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
i18n: LocalizerType;
|
||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
|
@ -77,10 +34,12 @@ const PIP_DEFAULT_Y = 56;
|
|||
const PIP_PADDING = 8;
|
||||
|
||||
export const CallingPip = ({
|
||||
call,
|
||||
conversation,
|
||||
createCanvasVideoRenderer,
|
||||
getGroupCallVideoFrameSource,
|
||||
hangUp,
|
||||
hasLocalVideo,
|
||||
hasRemoteVideo,
|
||||
i18n,
|
||||
setLocalPreview,
|
||||
setRendererCanvas,
|
||||
|
@ -88,7 +47,6 @@ export const CallingPip = ({
|
|||
}: PropsType): JSX.Element | null => {
|
||||
const videoContainerRef = React.useRef(null);
|
||||
const localVideoRef = React.useRef(null);
|
||||
const remoteVideoRef = React.useRef(null);
|
||||
|
||||
const [dragState, setDragState] = React.useState({
|
||||
offsetX: 0,
|
||||
|
@ -103,8 +61,7 @@ export const CallingPip = ({
|
|||
|
||||
React.useEffect(() => {
|
||||
setLocalPreview({ element: localVideoRef });
|
||||
setRendererCanvas({ element: remoteVideoRef });
|
||||
}, [setLocalPreview, setRendererCanvas]);
|
||||
}, [setLocalPreview]);
|
||||
|
||||
const handleMouseMove = React.useCallback(
|
||||
(ev: MouseEvent) => {
|
||||
|
@ -211,14 +168,14 @@ export const CallingPip = ({
|
|||
transition: dragState.isDragging ? 'none' : 'transform ease-out 300ms',
|
||||
}}
|
||||
>
|
||||
{hasRemoteVideo ? (
|
||||
<canvas
|
||||
className="module-calling-pip__video--remote"
|
||||
ref={remoteVideoRef}
|
||||
/>
|
||||
) : (
|
||||
renderAvatar(conversation, i18n)
|
||||
)}
|
||||
<CallingPipRemoteVideo
|
||||
call={call}
|
||||
conversation={conversation}
|
||||
createCanvasVideoRenderer={createCanvasVideoRenderer}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
i18n={i18n}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
/>
|
||||
{hasLocalVideo ? (
|
||||
<video
|
||||
className="module-calling-pip__video--local"
|
||||
|
@ -237,10 +194,18 @@ export const CallingPip = ({
|
|||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__pip')}
|
||||
aria-label={i18n('calling__pip--off')}
|
||||
className="module-calling-pip__button--pip"
|
||||
onClick={togglePip}
|
||||
/>
|
||||
>
|
||||
<Tooltip
|
||||
arrowSize={6}
|
||||
content={i18n('calling__pip--off')}
|
||||
hoverDelay={0}
|
||||
>
|
||||
<div />
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
105
ts/components/CallingPipRemoteVideo.tsx
Normal file
105
ts/components/CallingPipRemoteVideo.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { Avatar } from './Avatar';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
||||
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
CallMode,
|
||||
CanvasVideoRenderer,
|
||||
VideoFrameSource,
|
||||
} from '../types/Calling';
|
||||
import {
|
||||
DirectCallStateType,
|
||||
GroupCallStateType,
|
||||
SetRendererCanvasType,
|
||||
} from '../state/ducks/calling';
|
||||
|
||||
export interface PropsType {
|
||||
call: DirectCallStateType | GroupCallStateType;
|
||||
conversation: ConversationType;
|
||||
createCanvasVideoRenderer: () => CanvasVideoRenderer;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
i18n: LocalizerType;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
}
|
||||
|
||||
export const CallingPipRemoteVideo = ({
|
||||
call,
|
||||
conversation,
|
||||
createCanvasVideoRenderer,
|
||||
getGroupCallVideoFrameSource,
|
||||
i18n,
|
||||
setRendererCanvas,
|
||||
}: PropsType): JSX.Element => {
|
||||
if (call.callMode === CallMode.Direct) {
|
||||
if (!call.hasRemoteVideo) {
|
||||
const {
|
||||
avatarPath,
|
||||
color,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
title,
|
||||
} = conversation;
|
||||
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<CallBackgroundBlur avatarPath={avatarPath} color={color}>
|
||||
<div className="module-calling-pip__video--avatar">
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color={color || 'ultramarine'}
|
||||
noteToSelf={false}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
size={52}
|
||||
/>
|
||||
</div>
|
||||
</CallBackgroundBlur>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<DirectCallRemoteParticipant
|
||||
conversation={conversation}
|
||||
hasRemoteVideo={call.hasRemoteVideo}
|
||||
i18n={i18n}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (call.callMode === CallMode.Group) {
|
||||
const speaker = call.remoteParticipants[0];
|
||||
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<GroupCallRemoteParticipant
|
||||
key={speaker.demuxId}
|
||||
createCanvasVideoRenderer={createCanvasVideoRenderer}
|
||||
demuxId={speaker.demuxId}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
hasRemoteVideo={speaker.hasRemoteVideo}
|
||||
height="100%"
|
||||
left={0}
|
||||
top={0}
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('CallingRemoteVideo: Unknown Call Mode');
|
||||
};
|
|
@ -13,10 +13,10 @@ interface PropsType {
|
|||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
hasRemoteAudio: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
height: number;
|
||||
height: number | string;
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
width: number | string;
|
||||
}
|
||||
|
||||
export const GroupCallRemoteParticipant: React.FC<PropsType> = ({
|
||||
|
|
|
@ -5,7 +5,7 @@ import React, { useState, useMemo } from 'react';
|
|||
import Measure from 'react-measure';
|
||||
import { takeWhile, chunk, maxBy, flatten } from 'lodash';
|
||||
import { CanvasVideoRenderer, VideoFrameSource } from '../types/Calling';
|
||||
import { GroupCallRemoteParticipantType } from '../state/ducks/calling';
|
||||
import { GroupCallParticipantInfoType } from '../state/ducks/calling';
|
||||
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';
|
||||
|
||||
const MIN_RENDERED_HEIGHT = 10;
|
||||
|
@ -17,14 +17,14 @@ interface Dimensions {
|
|||
}
|
||||
|
||||
interface GridArrangement {
|
||||
rows: Array<Array<GroupCallRemoteParticipantType>>;
|
||||
rows: Array<Array<GroupCallParticipantInfoType>>;
|
||||
scalar: number;
|
||||
}
|
||||
|
||||
interface PropsType {
|
||||
createCanvasVideoRenderer: () => CanvasVideoRenderer;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
|
||||
remoteParticipants: ReadonlyArray<GroupCallParticipantInfoType>;
|
||||
}
|
||||
|
||||
// This component lays out group call remote participants. It uses a custom layout
|
||||
|
@ -84,7 +84,7 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
|
|||
//
|
||||
// This is primarily memoized for clarity, not performance. We only need the result,
|
||||
// not any of the "intermediate" values.
|
||||
const visibleParticipants: Array<GroupCallRemoteParticipantType> = useMemo(() => {
|
||||
const visibleParticipants: Array<GroupCallParticipantInfoType> = useMemo(() => {
|
||||
// Imagine that we laid out all of the rows end-to-end. That's the maximum total
|
||||
// width. So if there were 5 rows and the container was 100px wide, then we can't
|
||||
// possibly fit more than 500px of participants.
|
||||
|
@ -233,7 +233,7 @@ export const GroupCallRemoteParticipants: React.FC<PropsType> = ({
|
|||
};
|
||||
|
||||
function totalRemoteParticipantWidthAtMinHeight(
|
||||
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>
|
||||
remoteParticipants: ReadonlyArray<GroupCallParticipantInfoType>
|
||||
): number {
|
||||
return remoteParticipants.reduce(
|
||||
(result, { videoAspectRatio }) =>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue