New call UI and controls

This commit is contained in:
ayumi-signal 2023-10-25 06:40:22 -07:00 committed by GitHub
parent 33c5c683c7
commit 8bb355f971
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 741 additions and 360 deletions

View 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>
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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