Calling: Lobby
This commit is contained in:
parent
358ee4ab72
commit
59a181bd30
21 changed files with 1146 additions and 388 deletions
36
ts/components/CallBackgroundBlur.tsx
Normal file
36
ts/components/CallBackgroundBlur.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ColorType } from '../types/Colors';
|
||||
|
||||
export type PropsType = {
|
||||
avatarPath?: string;
|
||||
children: React.ReactNode;
|
||||
color?: ColorType;
|
||||
};
|
||||
|
||||
export const CallBackgroundBlur = ({
|
||||
avatarPath,
|
||||
children,
|
||||
color,
|
||||
}: PropsType): JSX.Element => {
|
||||
const backgroundProps = avatarPath
|
||||
? {
|
||||
style: {
|
||||
backgroundImage: `url("${avatarPath}")`,
|
||||
},
|
||||
}
|
||||
: {
|
||||
className: classNames(
|
||||
'module-calling__background',
|
||||
`module-background-color__${color || 'default'}`
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="module-calling__background" {...backgroundProps} />
|
||||
<div className="module-calling__background--blur" />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -15,6 +15,7 @@ const callDetails = {
|
|||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: 'ultramarine' as ColorType,
|
||||
title: 'Rick Sanchez',
|
||||
|
@ -27,6 +28,7 @@ const defaultProps = {
|
|||
acceptCall: action('accept-call'),
|
||||
callDetails,
|
||||
callState: CallState.Accepted,
|
||||
cancelCall: action('cancel-call'),
|
||||
closeNeedPermissionScreen: action('close-need-permission-screen'),
|
||||
declineCall: action('decline-call'),
|
||||
hangUp: action('hang-up'),
|
||||
|
@ -41,6 +43,8 @@ const defaultProps = {
|
|||
setLocalVideo: action('set-local-video'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
settingsDialogOpen: false,
|
||||
startCall: action('start-call'),
|
||||
toggleParticipants: action('toggle-participants'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
};
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
import React from 'react';
|
||||
import { CallingPip } from './CallingPip';
|
||||
import { CallNeedPermissionScreen } from './CallNeedPermissionScreen';
|
||||
import { CallingLobby } from './CallingLobby';
|
||||
import { CallScreen, PropsType as CallScreenPropsType } from './CallScreen';
|
||||
import {
|
||||
IncomingCallBar,
|
||||
PropsType as IncomingCallBarPropsType,
|
||||
} from './IncomingCallBar';
|
||||
import { CallState, CallEndedReason } from '../types/Calling';
|
||||
import { CallDetailsType } from '../state/ducks/calling';
|
||||
import { CallDetailsType, OutgoingCallType } from '../state/ducks/calling';
|
||||
|
||||
type CallManagerPropsType = {
|
||||
callDetails?: CallDetailsType;
|
||||
callState?: CallState;
|
||||
callEndedReason?: CallEndedReason;
|
||||
callState?: CallState;
|
||||
cancelCall: () => void;
|
||||
pip: boolean;
|
||||
closeNeedPermissionScreen: () => void;
|
||||
renderDeviceSelection: () => JSX.Element;
|
||||
settingsDialogOpen: boolean;
|
||||
startCall: (payload: OutgoingCallType) => void;
|
||||
toggleParticipants: () => void;
|
||||
};
|
||||
|
||||
type PropsType = IncomingCallBarPropsType &
|
||||
|
@ -28,6 +32,7 @@ export const CallManager = ({
|
|||
callDetails,
|
||||
callState,
|
||||
callEndedReason,
|
||||
cancelCall,
|
||||
closeNeedPermissionScreen,
|
||||
declineCall,
|
||||
hangUp,
|
||||
|
@ -42,10 +47,12 @@ export const CallManager = ({
|
|||
setLocalVideo,
|
||||
setRendererCanvas,
|
||||
settingsDialogOpen,
|
||||
startCall,
|
||||
toggleParticipants,
|
||||
togglePip,
|
||||
toggleSettings,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
if (!callDetails || !callState) {
|
||||
if (!callDetails) {
|
||||
return null;
|
||||
}
|
||||
const incoming = callDetails.isIncoming;
|
||||
|
@ -68,6 +75,31 @@ export const CallManager = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!callState) {
|
||||
return (
|
||||
<>
|
||||
<CallingLobby
|
||||
callDetails={callDetails}
|
||||
callState={callState}
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
i18n={i18n}
|
||||
isGroupCall={false}
|
||||
onCallCanceled={cancelCall}
|
||||
onJoinCall={() => {
|
||||
startCall({ callDetails });
|
||||
}}
|
||||
setLocalPreview={setLocalPreview}
|
||||
setLocalAudio={setLocalAudio}
|
||||
setLocalVideo={setLocalVideo}
|
||||
toggleParticipants={toggleParticipants}
|
||||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
{settingsDialogOpen && renderDeviceSelection()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (outgoing || ongoing) {
|
||||
if (pip) {
|
||||
return (
|
||||
|
|
|
@ -5,7 +5,7 @@ import { action } from '@storybook/addon-actions';
|
|||
|
||||
import { CallState } from '../types/Calling';
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { CallScreen } from './CallScreen';
|
||||
import { CallScreen, PropsType } from './CallScreen';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
|
@ -16,6 +16,7 @@ const callDetails = {
|
|||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: 'ultramarine' as ColorType,
|
||||
title: 'Rick Sanchez',
|
||||
|
@ -24,13 +25,20 @@ const callDetails = {
|
|||
profileName: 'Rick Sanchez',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
callDetails,
|
||||
callState: CallState.Accepted,
|
||||
callState: select(
|
||||
'callState',
|
||||
CallState,
|
||||
overrideProps.callState || CallState.Accepted
|
||||
),
|
||||
hangUp: action('hang-up'),
|
||||
hasLocalAudio: true,
|
||||
hasLocalVideo: true,
|
||||
hasRemoteVideo: true,
|
||||
hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false),
|
||||
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
||||
hasRemoteVideo: boolean(
|
||||
'hasRemoteVideo',
|
||||
overrideProps.hasRemoteVideo || false
|
||||
),
|
||||
i18n,
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalPreview: action('set-local-preview'),
|
||||
|
@ -38,82 +46,38 @@ const defaultProps = {
|
|||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
};
|
||||
});
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Call Screen',
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
title: 'Call Screen (Pre-ring)',
|
||||
props: {
|
||||
callState: CallState.Prering,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Screen (Ringing)',
|
||||
props: {
|
||||
callState: CallState.Ringing,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Screen (Reconnecting)',
|
||||
props: {
|
||||
callState: CallState.Reconnecting,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Call Screen (Ended)',
|
||||
props: {
|
||||
callState: CallState.Ended,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Calling (no local audio)',
|
||||
props: {
|
||||
...defaultProps,
|
||||
hasLocalAudio: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Calling (no local video)',
|
||||
props: {
|
||||
...defaultProps,
|
||||
hasLocalVideo: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Calling (no remote video)',
|
||||
props: {
|
||||
...defaultProps,
|
||||
hasRemoteVideo: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
const story = storiesOf('Components/CallScreen', module);
|
||||
|
||||
storiesOf('Components/CallScreen', module)
|
||||
.add('Knobs Playground', () => {
|
||||
const callState = select('callState', CallState, CallState.Accepted);
|
||||
const hasLocalAudio = boolean('hasLocalAudio', true);
|
||||
const hasLocalVideo = boolean('hasLocalVideo', true);
|
||||
const hasRemoteVideo = boolean('hasRemoteVideo', true);
|
||||
story.add('Default', () => {
|
||||
return <CallScreen {...createProps()} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<CallScreen
|
||||
{...defaultProps}
|
||||
callState={callState}
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
hasLocalVideo={hasLocalVideo}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<CallScreen {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
||||
story.add('Pre-Ring', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Prering })} />;
|
||||
});
|
||||
|
||||
story.add('Ringing', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Ringing })} />;
|
||||
});
|
||||
|
||||
story.add('Reconnecting', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Reconnecting })} />;
|
||||
});
|
||||
|
||||
story.add('Ended', () => {
|
||||
return <CallScreen {...createProps({ callState: CallState.Ended })} />;
|
||||
});
|
||||
|
||||
story.add('hasLocalAudio', () => {
|
||||
return <CallScreen {...createProps({ hasLocalAudio: true })} />;
|
||||
});
|
||||
|
||||
story.add('hasLocalVideo', () => {
|
||||
return <CallScreen {...createProps({ hasLocalVideo: true })} />;
|
||||
});
|
||||
|
||||
story.add('hasRemoteVideo', () => {
|
||||
return <CallScreen {...createProps({ hasRemoteVideo: true })} />;
|
||||
});
|
||||
|
|
|
@ -9,30 +9,10 @@ import {
|
|||
SetRendererCanvasType,
|
||||
} from '../state/ducks/calling';
|
||||
import { Avatar } from './Avatar';
|
||||
import { CallingButton, CallingButtonType } from './CallingButton';
|
||||
import { CallState } from '../types/Calling';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
type CallingButtonProps = {
|
||||
classNameSuffix: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const CallingButton = ({
|
||||
classNameSuffix,
|
||||
onClick,
|
||||
}: CallingButtonProps): JSX.Element => {
|
||||
const className = classNames(
|
||||
'module-ongoing-call__icon',
|
||||
`module-ongoing-call__icon${classNameSuffix}`
|
||||
);
|
||||
|
||||
return (
|
||||
<button type="button" className={className} onClick={onClick}>
|
||||
<div />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export type PropsType = {
|
||||
callDetails?: CallDetailsType;
|
||||
callState?: CallState;
|
||||
|
@ -137,10 +117,10 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
|||
|
||||
let eventHandled = false;
|
||||
|
||||
if (event.key === 'V') {
|
||||
if (event.shiftKey && (event.key === 'V' || event.key === 'v')) {
|
||||
this.toggleVideo();
|
||||
eventHandled = true;
|
||||
} else if (event.key === 'M') {
|
||||
} else if (event.shiftKey && (event.key === 'M' || event.key === 'm')) {
|
||||
this.toggleAudio();
|
||||
eventHandled = true;
|
||||
}
|
||||
|
@ -225,42 +205,41 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
|||
!showControls && !isAudioOnly && callState === CallState.Accepted,
|
||||
});
|
||||
|
||||
const toggleAudioSuffix = hasLocalAudio
|
||||
? '--audio--enabled'
|
||||
: '--audio--disabled';
|
||||
const toggleVideoSuffix = hasLocalVideo
|
||||
? '--video--enabled'
|
||||
: '--video--disabled';
|
||||
const videoButtonType = hasLocalVideo
|
||||
? CallingButtonType.VIDEO_ON
|
||||
: CallingButtonType.VIDEO_OFF;
|
||||
const audioButtonType = hasLocalAudio
|
||||
? CallingButtonType.AUDIO_ON
|
||||
: CallingButtonType.AUDIO_OFF;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="module-ongoing-call"
|
||||
className="module-calling__container"
|
||||
onMouseMove={this.showControls}
|
||||
role="group"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling__header',
|
||||
'module-ongoing-call__header',
|
||||
controlsFadeClass
|
||||
)}
|
||||
>
|
||||
<div className="module-ongoing-call__header-name">
|
||||
<div className="module-calling__header--header-name">
|
||||
{callDetails.title}
|
||||
</div>
|
||||
{this.renderMessage(callState)}
|
||||
<div className="module-ongoing-call__settings">
|
||||
<div className="module-calling-tools">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('callingDeviceSelection__settings')}
|
||||
className="module-ongoing-call__settings--button"
|
||||
className="module-calling-tools__button module-calling-button__settings"
|
||||
onClick={toggleSettings}
|
||||
/>
|
||||
</div>
|
||||
<div className="module-ongoing-call__pip">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('calling__pip')}
|
||||
className="module-ongoing-call__pip--button"
|
||||
className="module-calling-tools__button module-calling-button__pip"
|
||||
onClick={togglePip}
|
||||
/>
|
||||
</div>
|
||||
|
@ -276,18 +255,24 @@ export class CallScreen extends React.Component<PropsType, StateType> {
|
|||
)}
|
||||
>
|
||||
<CallingButton
|
||||
classNameSuffix={toggleVideoSuffix}
|
||||
buttonType={videoButtonType}
|
||||
i18n={i18n}
|
||||
onClick={this.toggleVideo}
|
||||
tooltipDistance={24}
|
||||
/>
|
||||
<CallingButton
|
||||
classNameSuffix={toggleAudioSuffix}
|
||||
buttonType={audioButtonType}
|
||||
i18n={i18n}
|
||||
onClick={this.toggleAudio}
|
||||
tooltipDistance={24}
|
||||
/>
|
||||
<CallingButton
|
||||
classNameSuffix="--hangup"
|
||||
buttonType={CallingButtonType.HANG_UP}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
hangUp({ callId: callDetails.callId });
|
||||
}}
|
||||
tooltipDistance={24}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
76
ts/components/CallingButton.stories.tsx
Normal file
76
ts/components/CallingButton.stories.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { number, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import {
|
||||
CallingButton,
|
||||
CallingButtonType,
|
||||
PropsType,
|
||||
TooltipDirection,
|
||||
} from './CallingButton';
|
||||
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 => ({
|
||||
buttonType: select(
|
||||
'buttonType',
|
||||
CallingButtonType,
|
||||
overrideProps.buttonType || CallingButtonType.HANG_UP
|
||||
),
|
||||
i18n,
|
||||
onClick: action('on-click'),
|
||||
tooltipDirection: select(
|
||||
'tooltipDirection',
|
||||
TooltipDirection,
|
||||
overrideProps.tooltipDirection || TooltipDirection.DOWN
|
||||
),
|
||||
tooltipDistance: number(
|
||||
'tooltipDistance',
|
||||
overrideProps.tooltipDistance || 16
|
||||
),
|
||||
});
|
||||
|
||||
const story = storiesOf('Components/CallingButton', module);
|
||||
|
||||
story.add('Default', () => {
|
||||
const props = createProps();
|
||||
return <CallingButton {...props} />;
|
||||
});
|
||||
|
||||
story.add('Audio On', () => {
|
||||
const props = createProps({
|
||||
buttonType: CallingButtonType.AUDIO_ON,
|
||||
});
|
||||
return <CallingButton {...props} />;
|
||||
});
|
||||
|
||||
story.add('Audio Off', () => {
|
||||
const props = createProps({
|
||||
buttonType: CallingButtonType.AUDIO_OFF,
|
||||
});
|
||||
return <CallingButton {...props} />;
|
||||
});
|
||||
|
||||
story.add('Video On', () => {
|
||||
const props = createProps({
|
||||
buttonType: CallingButtonType.VIDEO_ON,
|
||||
});
|
||||
return <CallingButton {...props} />;
|
||||
});
|
||||
|
||||
story.add('Video Off', () => {
|
||||
const props = createProps({
|
||||
buttonType: CallingButtonType.VIDEO_OFF,
|
||||
});
|
||||
return <CallingButton {...props} />;
|
||||
});
|
||||
|
||||
story.add('Tooltip right', () => {
|
||||
const props = createProps({
|
||||
tooltipDirection: TooltipDirection.RIGHT,
|
||||
});
|
||||
return <CallingButton {...props} />;
|
||||
});
|
76
ts/components/CallingButton.tsx
Normal file
76
ts/components/CallingButton.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Tooltip from 'react-tooltip-lite';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export enum TooltipDirection {
|
||||
UP = 'up',
|
||||
RIGHT = 'right',
|
||||
DOWN = 'down',
|
||||
LEFT = 'left',
|
||||
}
|
||||
|
||||
export enum CallingButtonType {
|
||||
AUDIO_OFF = 'AUDIO_OFF',
|
||||
AUDIO_ON = 'AUDIO_ON',
|
||||
HANG_UP = 'HANG_UP',
|
||||
VIDEO_OFF = 'VIDEO_OFF',
|
||||
VIDEO_ON = 'VIDEO_ON',
|
||||
}
|
||||
|
||||
export type PropsType = {
|
||||
buttonType: CallingButtonType;
|
||||
i18n: LocalizerType;
|
||||
onClick: () => void;
|
||||
tooltipDirection?: TooltipDirection;
|
||||
tooltipDistance?: number;
|
||||
};
|
||||
|
||||
export const CallingButton = ({
|
||||
buttonType,
|
||||
i18n,
|
||||
onClick,
|
||||
tooltipDirection = TooltipDirection.DOWN,
|
||||
tooltipDistance = 16,
|
||||
}: PropsType): JSX.Element => {
|
||||
let classNameSuffix = '';
|
||||
let tooltipContent = '';
|
||||
if (buttonType === CallingButtonType.AUDIO_OFF) {
|
||||
classNameSuffix = 'audio--disabled';
|
||||
tooltipContent = i18n('calling__button--audio-on');
|
||||
} else if (buttonType === CallingButtonType.AUDIO_ON) {
|
||||
classNameSuffix = 'audio--enabled';
|
||||
tooltipContent = i18n('calling__button--audio-off');
|
||||
} else if (buttonType === CallingButtonType.VIDEO_OFF) {
|
||||
classNameSuffix = 'video--disabled';
|
||||
tooltipContent = i18n('calling__button--video-on');
|
||||
} else if (buttonType === CallingButtonType.VIDEO_ON) {
|
||||
classNameSuffix = 'video--enabled';
|
||||
tooltipContent = i18n('calling__button--video-off');
|
||||
} else if (buttonType === CallingButtonType.HANG_UP) {
|
||||
classNameSuffix = 'hangup';
|
||||
tooltipContent = i18n('calling__hangup');
|
||||
}
|
||||
|
||||
const className = classNames(
|
||||
'module-calling-button__icon',
|
||||
`module-calling-button__icon--${classNameSuffix}`
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label={tooltipContent}
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Tooltip
|
||||
arrowSize={6}
|
||||
content={tooltipContent}
|
||||
direction={tooltipDirection}
|
||||
distance={tooltipDistance}
|
||||
hoverDelay={0}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
59
ts/components/CallingLobby.stories.tsx
Normal file
59
ts/components/CallingLobby.stories.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { ColorType } from '../types/Colors';
|
||||
import { CallingLobby, PropsType } from './CallingLobby';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const callDetails = {
|
||||
callId: 0,
|
||||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: 'ultramarine' as ColorType,
|
||||
title: 'Rick Sanchez',
|
||||
name: 'Rick Sanchez',
|
||||
phoneNumber: '3051234567',
|
||||
profileName: 'Rick Sanchez',
|
||||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
callDetails,
|
||||
hasLocalAudio: boolean('hasLocalAudio', overrideProps.hasLocalAudio || false),
|
||||
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
||||
i18n,
|
||||
isGroupCall: boolean('isGroupCall', overrideProps.isGroupCall || false),
|
||||
onCallCanceled: action('on-call-canceled'),
|
||||
onJoinCall: action('on-join-call'),
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
setLocalPreview: action('set-local-preview'),
|
||||
setLocalVideo: action('set-local-video'),
|
||||
toggleParticipants: action('toggle-participants'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
});
|
||||
|
||||
const story = storiesOf('Components/CallingLobby', module);
|
||||
|
||||
story.add('Default', () => {
|
||||
const props = createProps();
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Local Video', () => {
|
||||
const props = createProps({
|
||||
hasLocalVideo: true,
|
||||
});
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
||||
|
||||
story.add('Group Call', () => {
|
||||
const props = createProps({ isGroupCall: true });
|
||||
return <CallingLobby {...props} />;
|
||||
});
|
181
ts/components/CallingLobby.tsx
Normal file
181
ts/components/CallingLobby.tsx
Normal file
|
@ -0,0 +1,181 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
CallDetailsType,
|
||||
SetLocalAudioType,
|
||||
SetLocalPreviewType,
|
||||
SetLocalVideoType,
|
||||
} from '../state/ducks/calling';
|
||||
import { CallState } from '../types/Calling';
|
||||
import {
|
||||
CallingButton,
|
||||
CallingButtonType,
|
||||
TooltipDirection,
|
||||
} from './CallingButton';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
callDetails: CallDetailsType;
|
||||
callState?: CallState;
|
||||
hasLocalAudio: boolean;
|
||||
hasLocalVideo: boolean;
|
||||
i18n: LocalizerType;
|
||||
isGroupCall: boolean;
|
||||
onCallCanceled: () => void;
|
||||
onJoinCall: () => void;
|
||||
setLocalAudio: (_: SetLocalAudioType) => void;
|
||||
setLocalVideo: (_: SetLocalVideoType) => void;
|
||||
setLocalPreview: (_: SetLocalPreviewType) => void;
|
||||
toggleParticipants: () => void;
|
||||
toggleSettings: () => void;
|
||||
};
|
||||
|
||||
export const CallingLobby = ({
|
||||
callDetails,
|
||||
hasLocalAudio,
|
||||
hasLocalVideo,
|
||||
i18n,
|
||||
isGroupCall = false,
|
||||
onCallCanceled,
|
||||
onJoinCall,
|
||||
setLocalAudio,
|
||||
setLocalPreview,
|
||||
setLocalVideo,
|
||||
toggleParticipants,
|
||||
toggleSettings,
|
||||
}: PropsType): JSX.Element => {
|
||||
const localVideoRef = React.useRef(null);
|
||||
|
||||
const toggleAudio = React.useCallback((): void => {
|
||||
if (!callDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalAudio({ enabled: !hasLocalAudio });
|
||||
}, [callDetails, hasLocalAudio, setLocalAudio]);
|
||||
|
||||
const toggleVideo = React.useCallback((): void => {
|
||||
if (!callDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalVideo({ enabled: !hasLocalVideo });
|
||||
}, [callDetails, hasLocalVideo, setLocalVideo]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLocalPreview({ element: localVideoRef });
|
||||
|
||||
return () => {
|
||||
setLocalPreview({ element: undefined });
|
||||
};
|
||||
}, [setLocalPreview]);
|
||||
|
||||
React.useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent): void {
|
||||
let eventHandled = false;
|
||||
|
||||
if (event.shiftKey && (event.key === 'V' || event.key === 'v')) {
|
||||
toggleVideo();
|
||||
eventHandled = true;
|
||||
} else if (event.shiftKey && (event.key === 'M' || event.key === 'm')) {
|
||||
toggleAudio();
|
||||
eventHandled = true;
|
||||
}
|
||||
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [toggleVideo, toggleAudio]);
|
||||
|
||||
const videoButtonType = hasLocalVideo
|
||||
? CallingButtonType.VIDEO_ON
|
||||
: CallingButtonType.VIDEO_OFF;
|
||||
const audioButtonType = hasLocalAudio
|
||||
? CallingButtonType.AUDIO_ON
|
||||
: CallingButtonType.AUDIO_OFF;
|
||||
|
||||
return (
|
||||
<div className="module-calling__container">
|
||||
<div className="module-calling__header">
|
||||
<div className="module-calling__header--header-name">
|
||||
{callDetails.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>
|
||||
<div className="module-calling-lobby__video">
|
||||
{hasLocalVideo ? (
|
||||
<video ref={localVideoRef} autoPlay />
|
||||
) : (
|
||||
<CallBackgroundBlur
|
||||
avatarPath={callDetails.avatarPath}
|
||||
color={callDetails.color}
|
||||
>
|
||||
<div className="module-calling-lobby__video-off--icon" />
|
||||
<span className="module-calling-lobby__video-off--text">
|
||||
{i18n('calling__your-video-is-off')}
|
||||
</span>
|
||||
</CallBackgroundBlur>
|
||||
)}
|
||||
|
||||
<div className="module-calling__buttons">
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
i18n={i18n}
|
||||
onClick={toggleVideo}
|
||||
tooltipDirection={TooltipDirection.UP}
|
||||
tooltipDistance={24}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={audioButtonType}
|
||||
i18n={i18n}
|
||||
onClick={toggleAudio}
|
||||
tooltipDirection={TooltipDirection.UP}
|
||||
tooltipDistance={24}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="module-calling-lobby__actions">
|
||||
<button
|
||||
className="module-button__gray module-calling-lobby__button"
|
||||
onClick={onCallCanceled}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -15,6 +15,7 @@ const callDetails = {
|
|||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
color: 'ultramarine' as ColorType,
|
||||
title: 'Rick Sanchez',
|
||||
|
@ -24,7 +25,7 @@ const callDetails = {
|
|||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
callDetails,
|
||||
callDetails: overrideProps.callDetails || callDetails,
|
||||
hangUp: action('hang-up'),
|
||||
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
|
||||
hasRemoteVideo: boolean(
|
||||
|
@ -43,3 +44,23 @@ story.add('Default', () => {
|
|||
const props = createProps();
|
||||
return <CallingPip {...props} />;
|
||||
});
|
||||
|
||||
story.add('Contact (with avatar)', () => {
|
||||
const props = createProps({
|
||||
callDetails: {
|
||||
...callDetails,
|
||||
avatarPath: 'https://www.fillmurray.com/64/64',
|
||||
},
|
||||
});
|
||||
return <CallingPip {...props} />;
|
||||
});
|
||||
|
||||
story.add('Contact (no color)', () => {
|
||||
const props = createProps({
|
||||
callDetails: {
|
||||
...callDetails,
|
||||
color: undefined,
|
||||
},
|
||||
});
|
||||
return <CallingPip {...props} />;
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
SetRendererCanvasType,
|
||||
} from '../state/ducks/calling';
|
||||
import { Avatar } from './Avatar';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
function renderAvatar(
|
||||
|
@ -21,35 +22,24 @@ function renderAvatar(
|
|||
title,
|
||||
} = callDetails;
|
||||
|
||||
const backgroundStyle = avatarPath
|
||||
? {
|
||||
backgroundImage: `url("${avatarPath}")`,
|
||||
}
|
||||
: {
|
||||
backgroundColor: color,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<div
|
||||
className="module-calling-pip__video--background"
|
||||
style={backgroundStyle}
|
||||
/>
|
||||
<div className="module-calling-pip__video--blur" />
|
||||
<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 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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ const defaultProps = {
|
|||
isIncoming: true,
|
||||
isVideoCall: true,
|
||||
|
||||
id: '3051234567',
|
||||
avatarPath: undefined,
|
||||
contactColor: 'ultramarine' as ColorType,
|
||||
name: 'Rick Sanchez',
|
||||
|
|
|
@ -31,7 +31,7 @@ const CallButton = ({
|
|||
}: CallButtonProps): JSX.Element => {
|
||||
return (
|
||||
<button
|
||||
className={`module-incoming-call__icon module-incoming-call__button--${classSuffix}`}
|
||||
className={`module-incoming-call__button module-incoming-call__button--${classSuffix}`}
|
||||
onClick={onClick}
|
||||
tabIndex={tabIndex}
|
||||
type="button"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue