New call UI and controls
This commit is contained in:
parent
33c5c683c7
commit
8bb355f971
22 changed files with 741 additions and 360 deletions
51
ts/components/CallParticipantCount.tsx
Normal file
51
ts/components/CallParticipantCount.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
groupMemberCount?: number;
|
||||
participantCount: number;
|
||||
toggleParticipants: () => void;
|
||||
};
|
||||
|
||||
export function CallParticipantCount({
|
||||
i18n,
|
||||
groupMemberCount,
|
||||
participantCount,
|
||||
toggleParticipants,
|
||||
}: PropsType): JSX.Element {
|
||||
const count = participantCount || groupMemberCount || 1;
|
||||
const innerText = i18n('icu:CallControls__InfoDisplay--participants', {
|
||||
count: String(count),
|
||||
});
|
||||
|
||||
// Call not started, can't click to show participants
|
||||
if (!participantCount) {
|
||||
return (
|
||||
<span
|
||||
aria-label={i18n('icu:calling__participants', {
|
||||
people: String(count),
|
||||
})}
|
||||
className="CallControls__Status--InactiveCallParticipantCount"
|
||||
>
|
||||
{innerText}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label={i18n('icu:calling__participants', {
|
||||
people: String(count),
|
||||
})}
|
||||
className="CallControls__Status--ParticipantCount"
|
||||
onClick={toggleParticipants}
|
||||
type="button"
|
||||
>
|
||||
{innerText}
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -319,6 +319,18 @@ export function GroupCallMany(): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
export function GroupCallSpeakerView(): JSX.Element {
|
||||
return (
|
||||
<CallScreen
|
||||
{...createProps({
|
||||
callMode: CallMode.Group,
|
||||
viewMode: CallViewMode.Speaker,
|
||||
remoteParticipants: allRemoteParticipants.slice(0, 3),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupCallReconnecting(): JSX.Element {
|
||||
return (
|
||||
<CallScreen
|
||||
|
|
|
@ -17,6 +17,8 @@ import { Avatar, AvatarSize } from './Avatar';
|
|||
import { CallingHeader } from './CallingHeader';
|
||||
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
|
||||
import { CallingButton, CallingButtonType } from './CallingButton';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { TooltipPlacement } from './Tooltip';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import type {
|
||||
ActiveCallType,
|
||||
|
@ -33,11 +35,13 @@ import {
|
|||
import { AvatarColors } from '../types/Colors';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
useMutedToast,
|
||||
useReconnectingToast,
|
||||
useScreenSharingStoppedToast,
|
||||
} from './CallingToastManager';
|
||||
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
||||
import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
|
||||
import { CallParticipantCount } from './CallParticipantCount';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
@ -52,7 +56,7 @@ import {
|
|||
useKeyboardShortcuts,
|
||||
} from '../hooks/useKeyboardShortcuts';
|
||||
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
||||
import { isReconnecting } from '../util/callingIsReconnecting';
|
||||
import { isReconnecting as callingIsReconnecting } from '../util/callingIsReconnecting';
|
||||
|
||||
export type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
|
@ -82,12 +86,6 @@ export type PropsType = {
|
|||
toggleSpeakerView: () => void;
|
||||
};
|
||||
|
||||
type DirectCallHeaderMessagePropsType = {
|
||||
i18n: LocalizerType;
|
||||
callState: CallState;
|
||||
joinedAt: number | null;
|
||||
};
|
||||
|
||||
export const isInSpeakerView = (
|
||||
call: Pick<ActiveCallStateType, 'viewMode'> | undefined
|
||||
): boolean => {
|
||||
|
@ -97,11 +95,11 @@ export const isInSpeakerView = (
|
|||
);
|
||||
};
|
||||
|
||||
function DirectCallHeaderMessage({
|
||||
callState,
|
||||
i18n,
|
||||
function CallDuration({
|
||||
joinedAt,
|
||||
}: DirectCallHeaderMessagePropsType): JSX.Element | null {
|
||||
}: {
|
||||
joinedAt: number | null;
|
||||
}): JSX.Element | null {
|
||||
const [acceptedDuration, setAcceptedDuration] = useState<
|
||||
number | undefined
|
||||
>();
|
||||
|
@ -117,14 +115,8 @@ function DirectCallHeaderMessage({
|
|||
return clearInterval.bind(null, interval);
|
||||
}, [joinedAt]);
|
||||
|
||||
if (callState === CallState.Accepted && acceptedDuration) {
|
||||
return (
|
||||
<>
|
||||
{i18n('icu:callDuration', {
|
||||
duration: renderDuration(acceptedDuration),
|
||||
})}
|
||||
</>
|
||||
);
|
||||
if (acceptedDuration) {
|
||||
return <>{renderDuration(acceptedDuration)}</>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -161,7 +153,6 @@ export function CallScreen({
|
|||
presentingSource,
|
||||
remoteParticipants,
|
||||
showNeedsScreenRecordingPermissionsWarning,
|
||||
showParticipantsList,
|
||||
} = activeCall;
|
||||
|
||||
const isSpeaking = useValueAtFixedRate(
|
||||
|
@ -260,6 +251,7 @@ export function CallScreen({
|
|||
};
|
||||
}, [toggleAudio, toggleVideo]);
|
||||
|
||||
useMutedToast(hasLocalAudio, i18n);
|
||||
useReconnectingToast({ activeCall, i18n });
|
||||
useScreenSharingStoppedToast({ activeCall, i18n });
|
||||
|
||||
|
@ -272,10 +264,10 @@ export function CallScreen({
|
|||
);
|
||||
|
||||
const isSendingVideo = hasLocalVideo || presentingSource;
|
||||
const isReconnecting: boolean = callingIsReconnecting(activeCall);
|
||||
|
||||
let isRinging: boolean;
|
||||
let hasCallStarted: boolean;
|
||||
let headerMessage: ReactNode | undefined;
|
||||
let headerTitle: string | undefined;
|
||||
let isConnected: boolean;
|
||||
let participantCount: number;
|
||||
|
@ -287,14 +279,6 @@ export function CallScreen({
|
|||
activeCall.callState === CallState.Prering ||
|
||||
activeCall.callState === CallState.Ringing;
|
||||
hasCallStarted = !isRinging;
|
||||
headerMessage = (
|
||||
<DirectCallHeaderMessage
|
||||
i18n={i18n}
|
||||
callState={activeCall.callState || CallState.Prering}
|
||||
joinedAt={activeCall.joinedAt}
|
||||
/>
|
||||
);
|
||||
headerTitle = isRinging ? undefined : conversation.title;
|
||||
isConnected = activeCall.callState === CallState.Accepted;
|
||||
participantCount = isConnected ? 2 : 0;
|
||||
remoteParticipantsElement = hasCallStarted ? (
|
||||
|
@ -302,7 +286,7 @@ export function CallScreen({
|
|||
conversation={conversation}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
i18n={i18n}
|
||||
isReconnecting={isReconnecting(activeCall)}
|
||||
isReconnecting={isReconnecting}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
/>
|
||||
) : (
|
||||
|
@ -338,7 +322,7 @@ export function CallScreen({
|
|||
remoteParticipants={activeCall.remoteParticipants}
|
||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||
remoteAudioLevels={activeCall.remoteAudioLevels}
|
||||
isCallReconnecting={isReconnecting(activeCall)}
|
||||
isCallReconnecting={isReconnecting}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -454,6 +438,46 @@ export function CallScreen({
|
|||
presentingButtonType = CallingButtonType.PRESENTING_OFF;
|
||||
}
|
||||
|
||||
const callStatus: ReactNode | string = React.useMemo(() => {
|
||||
if (isRinging) {
|
||||
return i18n('icu:outgoingCallRinging');
|
||||
}
|
||||
if (isReconnecting) {
|
||||
return i18n('icu:callReconnecting');
|
||||
}
|
||||
if (isGroupCall) {
|
||||
return (
|
||||
<CallParticipantCount
|
||||
i18n={i18n}
|
||||
participantCount={participantCount}
|
||||
toggleParticipants={toggleParticipants}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// joinedAt is only available for direct calls
|
||||
if (isConnected) {
|
||||
return <CallDuration joinedAt={activeCall.joinedAt} />;
|
||||
}
|
||||
if (hasLocalVideo) {
|
||||
return i18n('icu:ContactListItem__menu__video-call');
|
||||
}
|
||||
if (hasLocalAudio) {
|
||||
return i18n('icu:CallControls__InfoDisplay--audio-call');
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
i18n,
|
||||
isRinging,
|
||||
isConnected,
|
||||
activeCall.joinedAt,
|
||||
isReconnecting,
|
||||
isGroupCall,
|
||||
participantCount,
|
||||
hasLocalVideo,
|
||||
hasLocalAudio,
|
||||
toggleParticipants,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -489,11 +513,8 @@ export function CallScreen({
|
|||
i18n={i18n}
|
||||
isInSpeakerView={isInSpeakerView(activeCall)}
|
||||
isGroupCall={isGroupCall}
|
||||
message={headerMessage}
|
||||
participantCount={participantCount}
|
||||
showParticipantsList={showParticipantsList}
|
||||
title={headerTitle}
|
||||
toggleParticipants={toggleParticipants}
|
||||
togglePip={togglePip}
|
||||
toggleSettings={toggleSettings}
|
||||
toggleSpeakerView={toggleSpeakerView}
|
||||
|
@ -516,38 +537,56 @@ export function CallScreen({
|
|||
<div className="module-ongoing-call__footer__local-preview-offset" />
|
||||
<div
|
||||
className={classNames(
|
||||
'CallControls',
|
||||
'module-ongoing-call__footer__actions',
|
||||
controlsFadeClass
|
||||
)}
|
||||
>
|
||||
<CallingButton
|
||||
buttonType={presentingButtonType}
|
||||
i18n={i18n}
|
||||
<div className="CallControls__InfoDisplay">
|
||||
<div className="CallControls__CallTitle">{conversation.title}</div>
|
||||
<div className="CallControls__Status">{callStatus}</div>
|
||||
</div>
|
||||
<div className="CallControls__ButtonContainer">
|
||||
<CallingButton
|
||||
buttonType={presentingButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={togglePresenting}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={toggleVideo}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={audioButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={toggleAudio}
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="CallControls__JoinLeaveButtonContainer"
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={togglePresenting}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={toggleVideo}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={audioButtonType}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={toggleAudio}
|
||||
/>
|
||||
<CallingButton
|
||||
buttonType={CallingButtonType.HANG_UP}
|
||||
i18n={i18n}
|
||||
onMouseEnter={onControlsMouseEnter}
|
||||
onMouseLeave={onControlsMouseLeave}
|
||||
onClick={hangUp}
|
||||
/>
|
||||
>
|
||||
<Button
|
||||
className="CallControls__JoinLeaveButton CallControls__JoinLeaveButton--hangup"
|
||||
onClick={hangUp}
|
||||
variant={ButtonVariant.Destructive}
|
||||
>
|
||||
{isGroupCall
|
||||
? i18n('icu:CallControls__JoinLeaveButton--hangup-group')
|
||||
: i18n('icu:CallControls__JoinLeaveButton--hangup-1-1')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="module-ongoing-call__footer__local-preview">
|
||||
{localPreviewNode}
|
||||
|
|
|
@ -26,7 +26,7 @@ export default {
|
|||
},
|
||||
},
|
||||
args: {
|
||||
buttonType: CallingButtonType.HANG_UP,
|
||||
buttonType: CallingButtonType.RING_ON,
|
||||
i18n,
|
||||
onClick: action('on-click'),
|
||||
onMouseEnter: action('on-mouse-enter'),
|
||||
|
|
|
@ -13,7 +13,6 @@ export enum CallingButtonType {
|
|||
AUDIO_DISABLED = 'AUDIO_DISABLED',
|
||||
AUDIO_OFF = 'AUDIO_OFF',
|
||||
AUDIO_ON = 'AUDIO_ON',
|
||||
HANG_UP = 'HANG_UP',
|
||||
PRESENTING_DISABLED = 'PRESENTING_DISABLED',
|
||||
PRESENTING_OFF = 'PRESENTING_OFF',
|
||||
PRESENTING_ON = 'PRESENTING_ON',
|
||||
|
@ -48,88 +47,69 @@ export function CallingButton({
|
|||
|
||||
let classNameSuffix = '';
|
||||
let tooltipContent = '';
|
||||
let label = '';
|
||||
let disabled = false;
|
||||
if (buttonType === CallingButtonType.AUDIO_DISABLED) {
|
||||
classNameSuffix = 'audio--disabled';
|
||||
tooltipContent = i18n('icu:calling__button--audio-disabled');
|
||||
label = i18n('icu:calling__button--audio__label');
|
||||
disabled = true;
|
||||
} else if (buttonType === CallingButtonType.AUDIO_OFF) {
|
||||
classNameSuffix = 'audio--off';
|
||||
tooltipContent = i18n('icu:calling__button--audio-on');
|
||||
label = i18n('icu:calling__button--audio__label');
|
||||
} else if (buttonType === CallingButtonType.AUDIO_ON) {
|
||||
classNameSuffix = 'audio--on';
|
||||
tooltipContent = i18n('icu:calling__button--audio-off');
|
||||
label = i18n('icu:calling__button--audio__label');
|
||||
} else if (buttonType === CallingButtonType.VIDEO_DISABLED) {
|
||||
classNameSuffix = 'video--disabled';
|
||||
tooltipContent = i18n('icu:calling__button--video-disabled');
|
||||
disabled = true;
|
||||
label = i18n('icu:calling__button--video__label');
|
||||
} else if (buttonType === CallingButtonType.VIDEO_OFF) {
|
||||
classNameSuffix = 'video--off';
|
||||
tooltipContent = i18n('icu:calling__button--video-on');
|
||||
label = i18n('icu:calling__button--video__label');
|
||||
} else if (buttonType === CallingButtonType.VIDEO_ON) {
|
||||
classNameSuffix = 'video--on';
|
||||
tooltipContent = i18n('icu:calling__button--video-off');
|
||||
label = i18n('icu:calling__button--video__label');
|
||||
} else if (buttonType === CallingButtonType.HANG_UP) {
|
||||
classNameSuffix = 'hangup';
|
||||
tooltipContent = i18n('icu:calling__hangup');
|
||||
label = i18n('icu:calling__hangup');
|
||||
} else if (buttonType === CallingButtonType.RING_DISABLED) {
|
||||
classNameSuffix = 'ring--disabled';
|
||||
disabled = true;
|
||||
tooltipContent = i18n(
|
||||
'icu:calling__button--ring__disabled-because-group-is-too-large'
|
||||
);
|
||||
label = i18n('icu:calling__button--ring__label');
|
||||
} else if (buttonType === CallingButtonType.RING_OFF) {
|
||||
classNameSuffix = 'ring--off';
|
||||
tooltipContent = i18n('icu:calling__button--ring__on');
|
||||
label = i18n('icu:calling__button--ring__label');
|
||||
tooltipContent = i18n('icu:CallingButton--ring-on');
|
||||
} else if (buttonType === CallingButtonType.RING_ON) {
|
||||
classNameSuffix = 'ring--on';
|
||||
tooltipContent = i18n('icu:calling__button--ring__off');
|
||||
label = i18n('icu:calling__button--ring__label');
|
||||
tooltipContent = i18n('icu:CallingButton__ring-off');
|
||||
} else if (buttonType === CallingButtonType.PRESENTING_DISABLED) {
|
||||
classNameSuffix = 'presenting--disabled';
|
||||
tooltipContent = i18n('icu:calling__button--presenting-disabled');
|
||||
disabled = true;
|
||||
label = i18n('icu:calling__button--presenting__label');
|
||||
} else if (buttonType === CallingButtonType.PRESENTING_ON) {
|
||||
classNameSuffix = 'presenting--on';
|
||||
tooltipContent = i18n('icu:calling__button--presenting-off');
|
||||
label = i18n('icu:calling__button--presenting__label');
|
||||
} else if (buttonType === CallingButtonType.PRESENTING_OFF) {
|
||||
classNameSuffix = 'presenting--off';
|
||||
tooltipContent = i18n('icu:calling__button--presenting-on');
|
||||
label = i18n('icu:calling__button--presenting__label');
|
||||
}
|
||||
|
||||
const className = classNames(
|
||||
'CallingButton__icon',
|
||||
`CallingButton__icon--${classNameSuffix}`
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={tooltipContent}
|
||||
direction={tooltipDirection}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'CallingButton__container',
|
||||
!isVisible && 'CallingButton__container--hidden'
|
||||
<div className="CallingButton">
|
||||
<Tooltip
|
||||
className="CallingButton__tooltip"
|
||||
wrapperClassName={classNames(
|
||||
'CallingButton__button-container',
|
||||
!isVisible && 'CallingButton__button-container--hidden'
|
||||
)}
|
||||
content={tooltipContent}
|
||||
direction={tooltipDirection}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
aria-label={tooltipContent}
|
||||
className={className}
|
||||
className={classNames(
|
||||
'CallingButton__icon',
|
||||
`CallingButton__icon--${classNameSuffix}`
|
||||
)}
|
||||
disabled={disabled}
|
||||
id={uniqueButtonId}
|
||||
onClick={onClick}
|
||||
|
@ -139,10 +119,7 @@ export function CallingButton({
|
|||
>
|
||||
<div />
|
||||
</button>
|
||||
<label className="CallingButton__label" htmlFor={uniqueButtonId}>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,9 +24,7 @@ export default {
|
|||
isGroupCall: false,
|
||||
message: '',
|
||||
participantCount: 0,
|
||||
showParticipantsList: false,
|
||||
title: 'With Someone',
|
||||
toggleParticipants: action('toggle-participants'),
|
||||
togglePip: action('toggle-pip'),
|
||||
toggleSettings: action('toggle-settings'),
|
||||
},
|
||||
|
@ -52,14 +50,7 @@ export function WithParticipants(args: PropsType): JSX.Element {
|
|||
}
|
||||
|
||||
export function WithParticipantsShown(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingHeader
|
||||
{...args}
|
||||
isGroupCall
|
||||
participantCount={10}
|
||||
showParticipantsList
|
||||
/>
|
||||
);
|
||||
return <CallingHeader {...args} isGroupCall participantCount={10} />;
|
||||
}
|
||||
|
||||
export function LongTitle(args: PropsType): JSX.Element {
|
||||
|
|
|
@ -15,9 +15,7 @@ export type PropsType = {
|
|||
message?: ReactNode;
|
||||
onCancel?: () => void;
|
||||
participantCount: number;
|
||||
showParticipantsList: boolean;
|
||||
title?: string;
|
||||
toggleParticipants?: () => void;
|
||||
togglePip?: () => void;
|
||||
toggleSettings: () => void;
|
||||
toggleSpeakerView?: () => void;
|
||||
|
@ -30,9 +28,7 @@ export function CallingHeader({
|
|||
message,
|
||||
onCancel,
|
||||
participantCount,
|
||||
showParticipantsList,
|
||||
title,
|
||||
toggleParticipants,
|
||||
togglePip,
|
||||
toggleSettings,
|
||||
toggleSpeakerView,
|
||||
|
@ -46,48 +42,24 @@ export function CallingHeader({
|
|||
<div className="module-ongoing-call__header-message">{message}</div>
|
||||
) : null}
|
||||
<div className="module-calling-tools">
|
||||
{isGroupCall && participantCount ? (
|
||||
{togglePip && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('icu:calling__participants', {
|
||||
people: String(participantCount),
|
||||
})}
|
||||
content={i18n('icu:calling__pip--on')}
|
||||
className="CallingButton__tooltip"
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:calling__participants', {
|
||||
people: String(participantCount),
|
||||
})}
|
||||
className={classNames(
|
||||
'CallingButton__participants--container',
|
||||
{
|
||||
'CallingButton__participants--shown': showParticipantsList,
|
||||
}
|
||||
)}
|
||||
onClick={toggleParticipants}
|
||||
aria-label={i18n('icu:calling__pip--on')}
|
||||
className="CallSettingsButton__Button"
|
||||
onClick={togglePip}
|
||||
type="button"
|
||||
>
|
||||
<i className="CallingButton__participants" />
|
||||
<span className="CallingButton__participants--count">
|
||||
{participantCount}
|
||||
</span>
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Pip" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('icu:callingDeviceSelection__settings')}
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:callingDeviceSelection__settings')}
|
||||
className="CallingButton__settings"
|
||||
onClick={toggleSettings}
|
||||
type="button"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{isGroupCall && participantCount > 2 && toggleSpeakerView && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
|
@ -96,6 +68,7 @@ export function CallingHeader({
|
|||
? i18n('icu:calling__switch-view--to-grid')
|
||||
: i18n('icu:calling__switch-view--to-speaker')
|
||||
}
|
||||
className="CallingButton__tooltip"
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
|
@ -104,38 +77,53 @@ export function CallingHeader({
|
|||
? i18n('icu:calling__switch-view--to-grid')
|
||||
: i18n('icu:calling__switch-view--to-speaker')
|
||||
}
|
||||
className={
|
||||
isInSpeakerView
|
||||
? 'CallingButton__grid-view'
|
||||
: 'CallingButton__speaker-view'
|
||||
}
|
||||
className="CallSettingsButton__Button"
|
||||
onClick={toggleSpeakerView}
|
||||
type="button"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{togglePip && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip content={i18n('icu:calling__pip--on')} theme={Theme.Dark}>
|
||||
<button
|
||||
aria-label={i18n('icu:calling__pip--on')}
|
||||
className="CallingButton__pip"
|
||||
onClick={togglePip}
|
||||
type="button"
|
||||
/>
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
'CallSettingsButton__Icon',
|
||||
isInSpeakerView
|
||||
? 'CallSettingsButton__Icon--GridView'
|
||||
: 'CallSettingsButton__Icon--SpeakerView'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('icu:callingDeviceSelection__settings')}
|
||||
className="CallingButton__tooltip"
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:callingDeviceSelection__settings')}
|
||||
className="CallSettingsButton__Button"
|
||||
onClick={toggleSettings}
|
||||
type="button"
|
||||
>
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Settings" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{onCancel && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip content={i18n('icu:cancel')} theme={Theme.Dark}>
|
||||
<Tooltip
|
||||
content={i18n('icu:cancel')}
|
||||
theme={Theme.Dark}
|
||||
className="CallingButton__tooltip"
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:cancel')}
|
||||
className="CallingButton__cancel"
|
||||
className="CallSettingsButton__Button CallSettingsButton__Button--Cancel"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
/>
|
||||
>
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Cancel" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
import { CallingButton, CallingButtonType } from './CallingButton';
|
||||
import { TooltipPlacement } from './Tooltip';
|
||||
import { CallBackgroundBlur } from './CallBackgroundBlur';
|
||||
import { CallParticipantCount } from './CallParticipantCount';
|
||||
import { CallingHeader } from './CallingHeader';
|
||||
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
|
||||
import {
|
||||
|
@ -23,6 +24,7 @@ import { useIsOnline } from '../hooks/useIsOnline';
|
|||
import * as KeyboardLayout from '../services/keyboardLayout';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { useCallingToasts } from './CallingToast';
|
||||
import { useMutedToast } from './CallingToastManager';
|
||||
|
||||
export type PropsType = {
|
||||
availableCameras: Array<MediaDeviceInfo>;
|
||||
|
@ -84,7 +86,6 @@ export function CallingLobby({
|
|||
setLocalPreview,
|
||||
setLocalVideo,
|
||||
setOutgoingRing,
|
||||
showParticipantsList,
|
||||
toggleParticipants,
|
||||
toggleSettings,
|
||||
outgoingRing,
|
||||
|
@ -143,8 +144,6 @@ export function CallingLobby({
|
|||
|
||||
const [isCallConnecting, setIsCallConnecting] = React.useState(false);
|
||||
|
||||
useWasInitiallyMutedToast(hasLocalAudio, i18n);
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const videoButtonType = hasLocalVideo
|
||||
? CallingButtonType.VIDEO_ON
|
||||
|
@ -201,6 +200,38 @@ export function CallingLobby({
|
|||
callingLobbyJoinButtonVariant = CallingLobbyJoinButtonVariant.Start;
|
||||
}
|
||||
|
||||
const callStatus = React.useMemo(() => {
|
||||
if (isGroupCall) {
|
||||
return (
|
||||
<CallParticipantCount
|
||||
i18n={i18n}
|
||||
groupMemberCount={groupMembers?.length ?? 0}
|
||||
participantCount={peekedParticipants.length}
|
||||
toggleParticipants={toggleParticipants}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (hasLocalVideo) {
|
||||
return i18n('icu:ContactListItem__menu__video-call');
|
||||
}
|
||||
if (hasLocalAudio) {
|
||||
return i18n('icu:CallControls__InfoDisplay--audio-call');
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
isGroupCall,
|
||||
peekedParticipants.length,
|
||||
i18n,
|
||||
hasLocalVideo,
|
||||
hasLocalAudio,
|
||||
groupMembers?.length,
|
||||
toggleParticipants,
|
||||
]);
|
||||
|
||||
useMutedToast(hasLocalAudio, i18n);
|
||||
useWasInitiallyMutedToast(hasLocalAudio, i18n);
|
||||
useOutgoingRingToast(isRingButtonVisible, outgoingRing, i18n);
|
||||
|
||||
return (
|
||||
<FocusTrap>
|
||||
<div className="module-calling__container">
|
||||
|
@ -222,8 +253,6 @@ export function CallingLobby({
|
|||
i18n={i18n}
|
||||
isGroupCall={isGroupCall}
|
||||
participantCount={peekedParticipants.length}
|
||||
showParticipantsList={showParticipantsList}
|
||||
toggleParticipants={toggleParticipants}
|
||||
toggleSettings={toggleSettings}
|
||||
onCancel={onCallCanceled}
|
||||
/>
|
||||
|
@ -249,37 +278,44 @@ export function CallingLobby({
|
|||
{i18n('icu: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 className="CallControls">
|
||||
<div className="CallControls__InfoDisplay">
|
||||
<div className="CallControls__CallTitle">{conversation.title}</div>
|
||||
<div className="CallControls__Status">{callStatus}</div>
|
||||
</div>
|
||||
<div className="CallControls__ButtonContainer">
|
||||
<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>
|
||||
<div className="CallControls__JoinLeaveButtonContainer">
|
||||
<CallingLobbyJoinButton
|
||||
disabled={!canJoin}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
setIsCallConnecting(true);
|
||||
onJoinCall();
|
||||
}}
|
||||
variant={callingLobbyJoinButtonVariant}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CallingLobbyJoinButton
|
||||
disabled={!canJoin}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
setIsCallConnecting(true);
|
||||
onJoinCall();
|
||||
}}
|
||||
variant={callingLobbyJoinButtonVariant}
|
||||
/>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
);
|
||||
|
@ -313,3 +349,51 @@ function useWasInitiallyMutedToast(
|
|||
}
|
||||
}, [hideToast, wasInitiallyMuted, hasLocalAudio]);
|
||||
}
|
||||
|
||||
function useOutgoingRingToast(
|
||||
isRingButtonVisible: boolean,
|
||||
outgoingRing: boolean,
|
||||
i18n: LocalizerType
|
||||
): void {
|
||||
const [previousOutgoingRing, setPreviousOutgoingRing] = React.useState<
|
||||
undefined | boolean
|
||||
>(undefined);
|
||||
const { showToast, hideToast } = useCallingToasts();
|
||||
const RINGING_TOAST_KEY = 'ringing';
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isRingButtonVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviousOutgoingRing(outgoingRing);
|
||||
}, [isRingButtonVisible, outgoingRing]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isRingButtonVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
previousOutgoingRing !== undefined &&
|
||||
outgoingRing !== previousOutgoingRing
|
||||
) {
|
||||
hideToast(RINGING_TOAST_KEY);
|
||||
showToast({
|
||||
key: RINGING_TOAST_KEY,
|
||||
content: outgoingRing
|
||||
? i18n('icu:CallControls__RingingToast--ringing-on')
|
||||
: i18n('icu:CallControls__RingingToast--ringing-off'),
|
||||
autoClose: true,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
isRingButtonVisible,
|
||||
outgoingRing,
|
||||
previousOutgoingRing,
|
||||
hideToast,
|
||||
showToast,
|
||||
i18n,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -9,9 +9,6 @@ import type { LocalizerType } from '../types/Util';
|
|||
import { Button, ButtonVariant } from './Button';
|
||||
import { Spinner } from './Spinner';
|
||||
|
||||
const PADDING_HORIZONTAL = 48;
|
||||
const PADDING_VERTICAL = 12;
|
||||
|
||||
export enum CallingLobbyJoinButtonVariant {
|
||||
CallIsFull = 'CallIsFull',
|
||||
Join = 'Join',
|
||||
|
@ -47,18 +44,24 @@ export function CallingLobbyJoinButton({
|
|||
|
||||
const childrenByVariant: Record<CallingLobbyJoinButtonVariant, ReactChild> = {
|
||||
[CallingLobbyJoinButtonVariant.CallIsFull]: i18n(
|
||||
'icu:calling__call-is-full'
|
||||
'icu:CallingLobbyJoinButton--call-full'
|
||||
),
|
||||
[CallingLobbyJoinButtonVariant.Loading]: (
|
||||
<Spinner size="18px" svgSize="small" />
|
||||
),
|
||||
[CallingLobbyJoinButtonVariant.Join]: i18n(
|
||||
'icu:CallingLobbyJoinButton--join'
|
||||
),
|
||||
[CallingLobbyJoinButtonVariant.Start]: i18n(
|
||||
'icu:CallingLobbyJoinButton--start'
|
||||
),
|
||||
[CallingLobbyJoinButtonVariant.Loading]: <Spinner svgSize="small" />,
|
||||
[CallingLobbyJoinButtonVariant.Join]: i18n('icu:calling__join'),
|
||||
[CallingLobbyJoinButtonVariant.Start]: i18n('icu:calling__start'),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Boolean(width && height) && (
|
||||
<Button
|
||||
className="module-CallingLobbyJoinButton"
|
||||
className="CallingLobbyJoinButton CallControls__JoinLeaveButton"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
style={{ width, height }}
|
||||
|
@ -79,7 +82,7 @@ export function CallingLobbyJoinButton({
|
|||
{Object.values(CallingLobbyJoinButtonVariant).map(candidateVariant => (
|
||||
<Button
|
||||
key={candidateVariant}
|
||||
className="module-CallingLobbyJoinButton"
|
||||
className="CallingLobbyJoinButton CallControls__JoinLeaveButton"
|
||||
variant={ButtonVariant.Calling}
|
||||
onClick={noop}
|
||||
ref={(button: HTMLButtonElement | null) => {
|
||||
|
@ -95,14 +98,10 @@ export function CallingLobbyJoinButton({
|
|||
// we compute the size, then the font makes the text a bit larger, and
|
||||
// there's a layout issue.
|
||||
setWidth((previousWidth = 0) =>
|
||||
Math.ceil(
|
||||
Math.max(previousWidth, variantWidth + PADDING_HORIZONTAL)
|
||||
)
|
||||
Math.ceil(Math.max(previousWidth, variantWidth))
|
||||
);
|
||||
setHeight((previousHeight = 0) =>
|
||||
Math.ceil(
|
||||
Math.max(previousHeight, variantHeight + PADDING_VERTICAL)
|
||||
)
|
||||
Math.ceil(Math.max(previousHeight, variantHeight))
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -146,16 +146,16 @@ export const CallingParticipantsList = React.memo(
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{participant.hasRemoteAudio === false ? (
|
||||
<span className="module-calling-participants-list__muted--audio" />
|
||||
) : null}
|
||||
<div className="module-calling-participants-list__status">
|
||||
{participant.hasRemoteVideo === false ? (
|
||||
<span className="module-calling-participants-list__muted--video" />
|
||||
) : null}
|
||||
{participant.presenting ? (
|
||||
<span className="module-calling-participants-list__presenting" />
|
||||
) : null}
|
||||
{participant.hasRemoteAudio === false ? (
|
||||
<span className="module-calling-participants-list__muted--audio" />
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
|
|
|
@ -98,3 +98,35 @@ export function useScreenSharingStoppedToast({
|
|||
}
|
||||
}, [activeCall, previousPresenter, showToast, i18n]);
|
||||
}
|
||||
|
||||
export function useMutedToast(
|
||||
hasLocalAudio: boolean,
|
||||
i18n: LocalizerType
|
||||
): void {
|
||||
const [previousHasLocalAudio, setPreviousHasLocalAudio] = useState<
|
||||
undefined | boolean
|
||||
>(undefined);
|
||||
const { showToast, hideToast } = useCallingToasts();
|
||||
const MUTED_TOAST_KEY = 'muted';
|
||||
|
||||
useEffect(() => {
|
||||
setPreviousHasLocalAudio(hasLocalAudio);
|
||||
}, [hasLocalAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
previousHasLocalAudio !== undefined &&
|
||||
hasLocalAudio !== previousHasLocalAudio
|
||||
) {
|
||||
hideToast(MUTED_TOAST_KEY);
|
||||
showToast({
|
||||
key: MUTED_TOAST_KEY,
|
||||
content: hasLocalAudio
|
||||
? i18n('icu:CallControls__MutedToast--unmuted')
|
||||
: i18n('icu:CallControls__MutedToast--muted'),
|
||||
autoClose: true,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}, [hasLocalAudio, previousHasLocalAudio, hideToast, showToast, i18n]);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ const OVERFLOW_SCROLLED_TO_EDGE_THRESHOLD = 20;
|
|||
const OVERFLOW_SCROLL_BUTTON_RATIO = 0.75;
|
||||
|
||||
// This should be an integer, as sub-pixel widths can cause performance issues.
|
||||
export const OVERFLOW_PARTICIPANT_WIDTH = 140;
|
||||
export const OVERFLOW_PARTICIPANT_WIDTH = 107;
|
||||
|
||||
export type PropsType = {
|
||||
getFrameBuffer: () => Buffer;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue