signal-desktop/ts/components/CallingLobby.tsx

290 lines
8.4 KiB
TypeScript
Raw Normal View History

// Copyright 2020-2021 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
2021-11-12 19:34:02 +00:00
import FocusTrap from 'focus-trap-react';
import classNames from 'classnames';
import type {
2020-10-08 01:25:33 +00:00
SetLocalAudioType,
SetLocalPreviewType,
SetLocalVideoType,
} from '../state/ducks/calling';
2020-11-19 18:11:35 +00:00
import { CallingButton, CallingButtonType } from './CallingButton';
import { TooltipPlacement } from './Tooltip';
2020-10-08 01:25:33 +00:00
import { CallBackgroundBlur } from './CallBackgroundBlur';
2020-11-17 15:07:53 +00:00
import { CallingHeader } from './CallingHeader';
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
import {
CallingLobbyJoinButton,
CallingLobbyJoinButtonVariant,
} from './CallingLobbyJoinButton';
import type { AvatarColorType } from '../types/Colors';
import type { LocalizerType } from '../types/Util';
2021-10-26 22:59:08 +00:00
import type { UUIDStringType } from '../types/UUID';
2021-10-19 13:53:11 +00:00
import { useIsOnline } from '../hooks/useIsOnline';
2021-09-29 21:20:52 +00:00
import * as KeyboardLayout from '../services/keyboardLayout';
import type { ConversationType } from '../state/ducks/conversations';
2021-09-02 22:34:38 +00:00
import { isConversationTooBigToRing } from '../conversations/isConversationTooBigToRing';
2020-10-08 01:25:33 +00:00
export type PropsType = {
availableCameras: Array<MediaDeviceInfo>;
conversation: Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'isMe'
2021-09-02 22:34:38 +00:00
| 'memberships'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'type'
| 'unblurredAvatarPath'
>;
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
2020-10-08 01:25:33 +00:00
hasLocalAudio: boolean;
hasLocalVideo: boolean;
i18n: LocalizerType;
isGroupCall: boolean;
isGroupCallOutboundRingEnabled: boolean;
isCallFull?: boolean;
me: {
avatarPath?: string;
id: string;
2021-05-28 16:15:17 +00:00
color?: AvatarColorType;
2021-10-26 22:59:08 +00:00
uuid: UUIDStringType;
};
2020-10-08 01:25:33 +00:00
onCallCanceled: () => void;
onJoinCall: () => void;
outgoingRing: boolean;
peekedParticipants: Array<ConversationType>;
2020-10-08 01:25:33 +00:00
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
setOutgoingRing: (_: boolean) => void;
2020-11-20 19:39:50 +00:00
showParticipantsList: boolean;
2020-10-08 01:25:33 +00:00
toggleParticipants: () => void;
toggleSettings: () => void;
};
export const CallingLobby = ({
availableCameras,
conversation,
groupMembers,
2020-10-08 01:25:33 +00:00
hasLocalAudio,
hasLocalVideo,
i18n,
isGroupCall = false,
isGroupCallOutboundRingEnabled,
isCallFull = false,
me,
2020-10-08 01:25:33 +00:00
onCallCanceled,
onJoinCall,
2020-12-02 18:14:03 +00:00
peekedParticipants,
2020-10-08 01:25:33 +00:00
setLocalAudio,
setLocalPreview,
setLocalVideo,
setOutgoingRing,
2020-11-20 19:39:50 +00:00
showParticipantsList,
2020-10-08 01:25:33 +00:00
toggleParticipants,
toggleSettings,
outgoingRing,
2020-10-08 01:25:33 +00:00
}: PropsType): JSX.Element => {
const localVideoRef = React.useRef<null | HTMLVideoElement>(null);
2020-10-08 01:25:33 +00:00
const shouldShowLocalVideo = hasLocalVideo && availableCameras.length > 0;
2020-10-08 01:25:33 +00:00
const toggleAudio = React.useCallback((): void => {
setLocalAudio({ enabled: !hasLocalAudio });
}, [hasLocalAudio, setLocalAudio]);
2020-10-08 01:25:33 +00:00
const toggleVideo = React.useCallback((): void => {
setLocalVideo({ enabled: !hasLocalVideo });
}, [hasLocalVideo, setLocalVideo]);
2020-10-08 01:25:33 +00:00
const toggleOutgoingRing = React.useCallback((): void => {
setOutgoingRing(!outgoingRing);
}, [outgoingRing, setOutgoingRing]);
2020-10-08 01:25:33 +00:00
React.useEffect(() => {
setLocalPreview({ element: localVideoRef });
return () => {
setLocalPreview({ element: undefined });
};
}, [setLocalPreview]);
React.useEffect(() => {
function handleKeyDown(event: KeyboardEvent): void {
let eventHandled = false;
2021-09-29 21:20:52 +00:00
const key = KeyboardLayout.lookup(event);
if (event.shiftKey && (key === 'V' || key === 'v')) {
2020-10-08 01:25:33 +00:00
toggleVideo();
eventHandled = true;
2021-09-29 21:20:52 +00:00
} else if (event.shiftKey && (key === 'M' || key === 'm')) {
2020-10-08 01:25:33 +00:00
toggleAudio();
eventHandled = true;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [toggleVideo, toggleAudio]);
2021-10-19 13:53:11 +00:00
const isOnline = useIsOnline();
2020-11-17 15:07:53 +00:00
const [isCallConnecting, setIsCallConnecting] = React.useState(false);
// eslint-disable-next-line no-nested-ternary
2020-10-08 01:25:33 +00:00
const videoButtonType = hasLocalVideo
? CallingButtonType.VIDEO_ON
: availableCameras.length === 0
? CallingButtonType.VIDEO_DISABLED
2020-10-08 01:25:33 +00:00
: CallingButtonType.VIDEO_OFF;
2020-10-08 01:25:33 +00:00
const audioButtonType = hasLocalAudio
? CallingButtonType.AUDIO_ON
: CallingButtonType.AUDIO_OFF;
2021-09-02 22:34:38 +00:00
const isGroupTooLargeToRing = isConversationTooBigToRing(conversation);
const isRingButtonVisible: boolean =
isGroupCall &&
isGroupCallOutboundRingEnabled &&
peekedParticipants.length === 0 &&
(groupMembers || []).length > 1;
2021-09-02 22:34:38 +00:00
let preCallInfoRingMode: RingMode;
if (isGroupCall) {
preCallInfoRingMode =
outgoingRing && !isGroupTooLargeToRing
? RingMode.WillRing
: RingMode.WillNotRing;
} else {
preCallInfoRingMode = RingMode.WillRing;
}
let ringButtonType:
| CallingButtonType.RING_DISABLED
| CallingButtonType.RING_ON
| CallingButtonType.RING_OFF;
if (isRingButtonVisible) {
2021-09-02 22:34:38 +00:00
if (isGroupTooLargeToRing) {
ringButtonType = CallingButtonType.RING_DISABLED;
} else if (outgoingRing) {
ringButtonType = CallingButtonType.RING_ON;
} else {
ringButtonType = CallingButtonType.RING_OFF;
}
} else {
ringButtonType = CallingButtonType.RING_DISABLED;
}
2021-10-19 13:53:11 +00:00
const canJoin = !isCallFull && !isCallConnecting && isOnline;
let callingLobbyJoinButtonVariant: CallingLobbyJoinButtonVariant;
if (isCallFull) {
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.CallIsFull;
} else if (isCallConnecting) {
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Loading;
} else if (peekedParticipants.length) {
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Join;
} else {
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Start;
}
2020-10-08 01:25:33 +00:00
return (
2021-11-12 19:34:02 +00:00
<FocusTrap>
<div className="module-calling__container">
{shouldShowLocalVideo ? (
<video
className="module-CallingLobby__local-preview module-CallingLobby__local-preview--camera-is-on"
ref={localVideoRef}
autoPlay
/>
) : (
<CallBackgroundBlur
className="module-CallingLobby__local-preview module-CallingLobby__local-preview--camera-is-off"
avatarPath={me.avatarPath}
color={me.color}
/>
)}
2020-11-17 15:07:53 +00:00
2021-11-12 19:34:02 +00:00
<CallingHeader
i18n={i18n}
2021-11-12 19:34:02 +00:00
isGroupCall={isGroupCall}
participantCount={peekedParticipants.length}
showParticipantsList={showParticipantsList}
toggleParticipants={toggleParticipants}
toggleSettings={toggleSettings}
onCancel={onCallCanceled}
/>
2021-11-12 19:34:02 +00:00
<CallingPreCallInfo
conversation={conversation}
groupMembers={groupMembers}
i18n={i18n}
2021-11-12 19:34:02 +00:00
isCallFull={isCallFull}
me={me}
peekedParticipants={peekedParticipants}
ringMode={preCallInfoRingMode}
/>
2021-11-12 19:34:02 +00:00
<div
className={classNames(
'module-CallingLobby__camera-is-off',
`module-CallingLobby__camera-is-off--${
shouldShowLocalVideo ? 'invisible' : 'visible'
}`
)}
>
{i18n('calling__your-video-is-off')}
</div>
<div className="module-calling__buttons module-calling__buttons--inline">
<CallingButton
buttonType={videoButtonType}
i18n={i18n}
onClick={toggleVideo}
tooltipDirection={TooltipPlacement.Top}
/>
<CallingButton
buttonType={audioButtonType}
i18n={i18n}
onClick={toggleAudio}
tooltipDirection={TooltipPlacement.Top}
/>
<CallingButton
buttonType={ringButtonType}
i18n={i18n}
isVisible={isRingButtonVisible}
onClick={toggleOutgoingRing}
tooltipDirection={TooltipPlacement.Top}
/>
</div>
<CallingLobbyJoinButton
disabled={!canJoin}
i18n={i18n}
2021-11-12 19:34:02 +00:00
onClick={() => {
setIsCallConnecting(true);
onJoinCall();
}}
variant={callingLobbyJoinButtonVariant}
/>
2020-10-08 01:25:33 +00:00
</div>
2021-11-12 19:34:02 +00:00
</FocusTrap>
2020-10-08 01:25:33 +00:00
);
};