signal-desktop/ts/components/CallScreen.tsx

698 lines
21 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { noop } from 'lodash';
2020-06-04 18:16:19 +00:00
import classNames from 'classnames';
2023-01-09 18:38:57 +00:00
import type { VideoFrameSource } from '@signalapp/ringrtc';
import type {
ActiveCallStateType,
2020-06-04 18:16:19 +00:00
SetLocalAudioType,
2020-08-27 00:03:42 +00:00
SetLocalPreviewType,
2020-06-04 18:16:19 +00:00
SetLocalVideoType,
2020-08-27 00:03:42 +00:00
SetRendererCanvasType,
2020-06-04 18:16:19 +00:00
} from '../state/ducks/calling';
2022-12-09 20:37:45 +00:00
import { Avatar, AvatarSize } from './Avatar';
import { CallingHeader, getCallViewIconClassname } from './CallingHeader';
import { CallingPreCallInfo, RingMode } from './CallingPreCallInfo';
2020-10-08 01:25:33 +00:00
import { CallingButton, CallingButtonType } from './CallingButton';
2023-10-25 13:40:22 +00:00
import { Button, ButtonVariant } from './Button';
import { TooltipPlacement } from './Tooltip';
import { CallBackgroundBlur } from './CallBackgroundBlur';
import type {
2020-12-02 18:14:03 +00:00
ActiveCallType,
GroupCallVideoRequest,
PresentedSource,
} from '../types/Calling';
import {
2020-11-13 19:57:55 +00:00
CallMode,
CallViewMode,
2020-11-13 19:57:55 +00:00
CallState,
GroupCallConnectionState,
GroupCallJoinState,
2020-11-13 19:57:55 +00:00
} from '../types/Calling';
import { AvatarColors } from '../types/Colors';
import type { ConversationType } from '../state/ducks/conversations';
import {
CallingButtonToastsContainer,
useScreenSharingStoppedToast,
} from './CallingToastManager';
2020-11-13 19:57:55 +00:00
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
2023-10-25 13:40:22 +00:00
import { CallParticipantCount } from './CallParticipantCount';
import type { LocalizerType } from '../types/Util';
2021-05-28 16:15:17 +00:00
import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal';
import { missingCaseError } from '../util/missingCaseError';
2021-09-29 21:20:52 +00:00
import * as KeyboardLayout from '../services/keyboardLayout';
import {
usePresenter,
useActivateSpeakerViewOnPresenting,
} from '../hooks/useActivateSpeakerViewOnPresenting';
2023-02-28 20:01:52 +00:00
import {
CallingAudioIndicator,
SPEAKING_LINGER_MS,
} from './CallingAudioIndicator';
2022-05-10 18:14:08 +00:00
import {
useActiveCallShortcuts,
useKeyboardShortcuts,
} from '../hooks/useKeyboardShortcuts';
2023-02-28 20:01:52 +00:00
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
2023-10-25 13:40:22 +00:00
import { isReconnecting as callingIsReconnecting } from '../util/callingIsReconnecting';
import { usePrevious } from '../hooks/usePrevious';
2023-11-14 22:05:17 +00:00
import { PersistentCallingToast, useCallingToasts } from './CallingToast';
import { Spinner } from './Spinner';
2020-06-04 18:16:19 +00:00
export type PropsType = {
activeCall: ActiveCallType;
2020-11-13 19:57:55 +00:00
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
getPresentingSources: () => void;
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
2022-08-16 23:52:09 +00:00
hangUpActiveCall: (reason: string) => void;
2020-06-04 18:16:19 +00:00
i18n: LocalizerType;
me: ConversationType;
openSystemPreferencesAction: () => unknown;
2022-09-07 15:52:55 +00:00
setGroupCallVideoRequest: (
_: Array<GroupCallVideoRequest>,
speakerHeight: number
) => void;
2020-06-04 18:16:19 +00:00
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
2020-08-27 00:03:42 +00:00
setLocalPreview: (_: SetLocalPreviewType) => void;
setPresenting: (_?: PresentedSource) => void;
2020-08-27 00:03:42 +00:00
setRendererCanvas: (_: SetRendererCanvasType) => void;
2020-11-17 15:07:53 +00:00
stickyControls: boolean;
switchToPresentationView: () => void;
switchFromPresentationView: () => void;
2020-11-17 15:07:53 +00:00
toggleParticipants: () => void;
2020-10-01 00:43:05 +00:00
togglePip: () => void;
toggleScreenRecordingPermissionsDialog: () => unknown;
2020-08-27 00:03:42 +00:00
toggleSettings: () => void;
changeCallView: (mode: CallViewMode) => void;
2020-06-04 18:16:19 +00:00
};
export const isInSpeakerView = (
call: Pick<ActiveCallStateType, 'viewMode'> | undefined
): boolean => {
return Boolean(
call?.viewMode === CallViewMode.Presentation ||
call?.viewMode === CallViewMode.Speaker
);
};
2023-10-25 13:40:22 +00:00
function CallDuration({
2021-09-18 00:20:29 +00:00
joinedAt,
2023-10-25 13:40:22 +00:00
}: {
joinedAt: number | null;
}): JSX.Element | null {
2021-09-18 00:20:29 +00:00
const [acceptedDuration, setAcceptedDuration] = useState<
number | undefined
>();
useEffect(() => {
if (joinedAt == null) {
2021-09-18 00:20:29 +00:00
return noop;
}
// It's really jumpy with a value of 500ms.
const interval = setInterval(() => {
setAcceptedDuration(Date.now() - joinedAt);
}, 100);
return clearInterval.bind(null, interval);
}, [joinedAt]);
2023-10-25 13:40:22 +00:00
if (acceptedDuration) {
return <>{renderDuration(acceptedDuration)}</>;
2021-09-18 00:20:29 +00:00
}
return null;
}
2022-11-18 00:45:19 +00:00
export function CallScreen({
activeCall,
changeCallView,
2020-11-13 19:57:55 +00:00
getGroupCallVideoFrameSource,
getPresentingSources,
groupMembers,
hangUpActiveCall,
i18n,
me,
openSystemPreferencesAction,
setGroupCallVideoRequest,
setLocalAudio,
setLocalVideo,
setLocalPreview,
setPresenting,
setRendererCanvas,
2020-11-17 15:07:53 +00:00
stickyControls,
switchToPresentationView,
switchFromPresentationView,
2020-11-17 15:07:53 +00:00
toggleParticipants,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSettings,
2022-11-18 00:45:19 +00:00
}: PropsType): JSX.Element {
2020-12-02 18:14:03 +00:00
const {
conversation,
hasLocalAudio,
hasLocalVideo,
2022-05-19 03:28:51 +00:00
localAudioLevel,
presentingSource,
remoteParticipants,
showNeedsScreenRecordingPermissionsWarning,
2020-12-02 18:14:03 +00:00
} = activeCall;
2023-02-28 20:01:52 +00:00
const isSpeaking = useValueAtFixedRate(
localAudioLevel > 0,
SPEAKING_LINGER_MS
);
useActivateSpeakerViewOnPresenting({
remoteParticipants,
switchToPresentationView,
switchFromPresentationView,
});
2022-05-10 18:14:08 +00:00
const activeCallShortcuts = useActiveCallShortcuts(hangUpActiveCall);
useKeyboardShortcuts(activeCallShortcuts);
const toggleAudio = useCallback(() => {
setLocalAudio({
enabled: !hasLocalAudio,
});
}, [setLocalAudio, hasLocalAudio]);
2020-06-04 18:16:19 +00:00
const toggleVideo = useCallback(() => {
setLocalVideo({
enabled: !hasLocalVideo,
});
}, [setLocalVideo, hasLocalVideo]);
2020-06-04 18:16:19 +00:00
const togglePresenting = useCallback(() => {
if (presentingSource) {
setPresenting();
} else {
getPresentingSources();
}
}, [getPresentingSources, presentingSource, setPresenting]);
2022-08-16 23:52:09 +00:00
const hangUp = useCallback(() => {
hangUpActiveCall('button click');
}, [hangUpActiveCall]);
2021-08-24 18:38:03 +00:00
const [controlsHover, setControlsHover] = useState(false);
const onControlsMouseEnter = useCallback(() => {
setControlsHover(true);
}, [setControlsHover]);
const onControlsMouseLeave = useCallback(() => {
setControlsHover(false);
}, [setControlsHover]);
const [showControls, setShowControls] = useState(true);
2020-06-04 18:16:19 +00:00
const localVideoRef = useRef<HTMLVideoElement | null>(null);
2020-06-04 18:16:19 +00:00
useEffect(() => {
setLocalPreview({ element: localVideoRef });
return () => {
setLocalPreview({ element: undefined });
};
}, [setLocalPreview, setRendererCanvas]);
2020-06-04 18:16:19 +00:00
useEffect(() => {
2021-08-24 18:38:03 +00:00
if (!showControls || stickyControls || controlsHover) {
return noop;
2020-06-04 18:16:19 +00:00
}
const timer = setTimeout(() => {
setShowControls(false);
2020-06-04 18:16:19 +00:00
}, 5000);
2021-09-18 00:20:29 +00:00
return clearTimeout.bind(null, timer);
2021-08-24 18:38:03 +00:00
}, [showControls, stickyControls, controlsHover]);
useEffect(() => {
const 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')) {
toggleVideo();
eventHandled = true;
2021-09-29 21:20:52 +00:00
} else if (event.shiftKey && (key === 'M' || key === 'm')) {
toggleAudio();
eventHandled = true;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
setShowControls(true);
}
};
2020-06-04 18:16:19 +00:00
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [toggleAudio, toggleVideo]);
2020-06-04 18:16:19 +00:00
useScreenSharingStoppedToast({ activeCall, i18n });
useViewModeChangedToast({ activeCall, i18n });
const currentPresenter = remoteParticipants.find(
participant => participant.presenting
);
const hasRemoteVideo = remoteParticipants.some(
2020-12-02 18:14:03 +00:00
remoteParticipant => remoteParticipant.hasRemoteVideo
);
2021-09-10 17:24:05 +00:00
const isSendingVideo = hasLocalVideo || presentingSource;
2023-10-25 13:40:22 +00:00
const isReconnecting: boolean = callingIsReconnecting(activeCall);
2021-09-10 17:24:05 +00:00
let isRinging: boolean;
let hasCallStarted: boolean;
2020-11-13 19:57:55 +00:00
let isConnected: boolean;
2020-11-23 21:37:39 +00:00
let participantCount: number;
let remoteParticipantsElement: ReactNode;
2020-06-04 18:16:19 +00:00
2020-12-02 18:14:03 +00:00
switch (activeCall.callMode) {
case CallMode.Direct: {
isRinging =
activeCall.callState === CallState.Prering ||
activeCall.callState === CallState.Ringing;
hasCallStarted = !isRinging;
2020-12-02 18:14:03 +00:00
isConnected = activeCall.callState === CallState.Accepted;
2020-11-23 21:37:39 +00:00
participantCount = isConnected ? 2 : 0;
remoteParticipantsElement = hasCallStarted ? (
2020-11-13 19:57:55 +00:00
<DirectCallRemoteParticipant
conversation={conversation}
hasRemoteVideo={hasRemoteVideo}
i18n={i18n}
2023-10-25 13:40:22 +00:00
isReconnecting={isReconnecting}
2020-11-13 19:57:55 +00:00
setRendererCanvas={setRendererCanvas}
/>
) : (
<div className="module-ongoing-call__direct-call-ringing-spacer" />
2020-11-13 19:57:55 +00:00
);
break;
}
2020-11-13 19:57:55 +00:00
case CallMode.Group:
isRinging =
activeCall.outgoingRing &&
!activeCall.remoteParticipants.length &&
!(groupMembers?.length === 1 && groupMembers[0].id === me.id);
hasCallStarted = activeCall.joinState !== GroupCallJoinState.NotJoined;
2020-12-02 18:14:03 +00:00
participantCount = activeCall.remoteParticipants.length + 1;
2020-12-02 18:14:03 +00:00
isConnected =
activeCall.connectionState === GroupCallConnectionState.Connected;
2020-11-17 15:07:53 +00:00
remoteParticipantsElement = (
2020-11-13 19:57:55 +00:00
<GroupCallRemoteParticipants
callViewMode={activeCall.viewMode}
2020-11-13 19:57:55 +00:00
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
i18n={i18n}
2020-12-02 18:14:03 +00:00
remoteParticipants={activeCall.remoteParticipants}
setGroupCallVideoRequest={setGroupCallVideoRequest}
2022-05-19 03:28:51 +00:00
remoteAudioLevels={activeCall.remoteAudioLevels}
2023-10-25 13:40:22 +00:00
isCallReconnecting={isReconnecting}
2020-11-13 19:57:55 +00:00
/>
);
break;
default:
2020-12-02 18:14:03 +00:00
throw missingCaseError(activeCall);
2020-11-13 19:57:55 +00:00
}
let lonelyInCallNode: ReactNode;
2021-09-10 17:24:05 +00:00
let localPreviewNode: ReactNode;
const isLonelyInCall = !activeCall.remoteParticipants.length;
if (isLonelyInCall) {
lonelyInCallNode = (
2021-09-10 17:24:05 +00:00
<div
className={classNames(
'module-ongoing-call__local-preview-fullsize',
presentingSource &&
'module-ongoing-call__local-preview-fullsize--presenting'
)}
>
{isSendingVideo ? (
<video ref={localVideoRef} autoPlay />
) : (
<CallBackgroundBlur avatarPath={me.avatarPath} color={me.color}>
2023-10-31 19:32:56 +00:00
<div className="module-calling__spacer module-calling__camera-is-off-spacer" />
2021-09-10 17:24:05 +00:00
<div className="module-calling__camera-is-off">
2023-03-30 00:03:25 +00:00
{i18n('icu:calling__your-video-is-off')}
2021-09-10 17:24:05 +00:00
</div>
</CallBackgroundBlur>
)}
</div>
);
} else {
localPreviewNode = isSendingVideo ? (
<video
className={classNames(
'module-ongoing-call__footer__local-preview__video',
presentingSource &&
'module-ongoing-call__footer__local-preview__video--presenting'
)}
ref={localVideoRef}
autoPlay
/>
) : (
<CallBackgroundBlur avatarPath={me.avatarPath} color={me.color}>
<Avatar
acceptedMessageRequest
avatarPath={me.avatarPath}
badge={undefined}
2021-09-10 17:24:05 +00:00
color={me.color || AvatarColors[0]}
noteToSelf={false}
conversationType="direct"
i18n={i18n}
isMe
phoneNumber={me.phoneNumber}
profileName={me.profileName}
title={me.title}
// See comment above about `sharedGroupNames`.
sharedGroupNames={[]}
2023-10-31 19:32:56 +00:00
size={AvatarSize.FORTY}
2021-09-10 17:24:05 +00:00
/>
</CallBackgroundBlur>
);
}
let videoButtonType: CallingButtonType;
if (presentingSource) {
videoButtonType = CallingButtonType.VIDEO_DISABLED;
} else if (hasLocalVideo) {
videoButtonType = CallingButtonType.VIDEO_ON;
} else {
videoButtonType = CallingButtonType.VIDEO_OFF;
}
const audioButtonType = hasLocalAudio
? CallingButtonType.AUDIO_ON
: CallingButtonType.AUDIO_OFF;
2020-11-13 19:57:55 +00:00
const isAudioOnly = !hasLocalVideo && !hasRemoteVideo;
const controlsFadeClass = classNames({
'module-ongoing-call__controls--fadeIn':
(showControls || isAudioOnly) && !isConnected,
'module-ongoing-call__controls--fadeOut':
!showControls && !isAudioOnly && isConnected,
});
const isGroupCall = activeCall.callMode === CallMode.Group;
let presentingButtonType: CallingButtonType;
if (presentingSource) {
presentingButtonType = CallingButtonType.PRESENTING_ON;
} else if (currentPresenter) {
presentingButtonType = CallingButtonType.PRESENTING_DISABLED;
} else {
presentingButtonType = CallingButtonType.PRESENTING_OFF;
}
2023-10-25 13:40:22 +00:00
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
2020-11-13 19:57:55 +00:00
className={classNames(
'module-calling__container',
`module-ongoing-call__container--${getCallModeClassSuffix(
2020-12-02 18:14:03 +00:00
activeCall.callMode
)}`,
`module-ongoing-call__container--${
hasCallStarted ? 'call-started' : 'call-not-started'
}`,
{ 'module-ongoing-call__container--hide-controls': !showControls }
2020-11-13 19:57:55 +00:00
)}
2021-09-18 00:20:29 +00:00
onFocus={() => {
setShowControls(true);
}}
onMouseMove={() => {
setShowControls(true);
}}
role="group"
>
2023-11-14 22:05:17 +00:00
{isReconnecting ? (
<PersistentCallingToast>
<span className="CallingToast__reconnecting">
<Spinner svgSize="small" size="16px" />
{i18n('icu:callReconnecting')}
</span>
</PersistentCallingToast>
) : null}
{isLonelyInCall && !isRinging ? (
<PersistentCallingToast>
{i18n('icu:calling__in-this-call--zero')}
</PersistentCallingToast>
) : null}
{currentPresenter ? (
<PersistentCallingToast>
{i18n('icu:calling__presenting--person-ongoing', {
name: currentPresenter.title,
})}
</PersistentCallingToast>
) : null}
{showNeedsScreenRecordingPermissionsWarning ? (
<NeedsScreenRecordingPermissionsModal
toggleScreenRecordingPermissionsDialog={
toggleScreenRecordingPermissionsDialog
}
i18n={i18n}
openSystemPreferencesAction={openSystemPreferencesAction}
/>
) : null}
2020-06-04 18:16:19 +00:00
<div
2020-11-17 15:07:53 +00:00
className={classNames('module-ongoing-call__header', controlsFadeClass)}
2020-06-04 18:16:19 +00:00
>
2020-11-17 15:07:53 +00:00
<CallingHeader
callViewMode={activeCall.viewMode}
changeCallView={changeCallView}
2020-11-17 15:07:53 +00:00
i18n={i18n}
isGroupCall={isGroupCall}
participantCount={participantCount}
2020-11-17 15:07:53 +00:00
togglePip={togglePip}
toggleSettings={toggleSettings}
/>
2020-06-04 18:16:19 +00:00
</div>
{isRinging && (
2023-10-31 19:32:56 +00:00
<>
<div className="module-CallingPreCallInfo-spacer " />
<CallingPreCallInfo
conversation={conversation}
groupMembers={groupMembers}
i18n={i18n}
me={me}
ringMode={RingMode.IsRinging}
/>
</>
)}
2020-11-17 15:07:53 +00:00
{remoteParticipantsElement}
{lonelyInCallNode}
<div className="module-ongoing-call__footer">
<div className="module-calling__spacer CallControls__OuterSpacer" />
<div
className={classNames(
2023-10-25 13:40:22 +00:00
'CallControls',
'module-ongoing-call__footer__actions',
controlsFadeClass
)}
>
2023-10-25 13:40:22 +00:00
<div className="CallControls__InfoDisplay">
<div className="CallControls__CallTitle">{conversation.title}</div>
<div className="CallControls__Status">{callStatus}</div>
</div>
<CallingButtonToastsContainer
hasLocalAudio={hasLocalAudio}
outgoingRing={undefined}
i18n={i18n}
/>
2023-10-25 13:40:22 +00:00
<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"
2021-09-18 00:20:29 +00:00
onMouseEnter={onControlsMouseEnter}
onMouseLeave={onControlsMouseLeave}
2023-10-25 13:40:22 +00:00
>
<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>
{localPreviewNode ? (
<div className="module-ongoing-call__footer__local-preview module-ongoing-call__footer__local-preview--active">
{localPreviewNode}
{!isSendingVideo && (
<div className="CallingStatusIndicator CallingStatusIndicator--Video" />
)}
<CallingAudioIndicator
hasAudio={hasLocalAudio}
audioLevel={localAudioLevel}
shouldShowSpeaking={isSpeaking}
/>
</div>
) : (
<div className="module-ongoing-call__footer__local-preview" />
)}
2020-06-04 18:16:19 +00:00
</div>
</div>
);
2022-11-18 00:45:19 +00:00
}
2020-06-04 18:16:19 +00:00
2020-11-13 19:57:55 +00:00
function getCallModeClassSuffix(
callMode: CallMode.Direct | CallMode.Group
): string {
switch (callMode) {
case CallMode.Direct:
return 'direct';
case CallMode.Group:
return 'group';
default:
throw missingCaseError(callMode);
}
}
2020-06-04 18:16:19 +00:00
function renderDuration(ms: number): string {
const secs = Math.floor((ms / 1000) % 60)
.toString()
.padStart(2, '0');
const mins = Math.floor((ms / 60000) % 60)
.toString()
.padStart(2, '0');
const hours = Math.floor(ms / 3600000);
if (hours > 0) {
return `${hours}:${mins}:${secs}`;
2020-06-04 18:16:19 +00:00
}
return `${mins}:${secs}`;
2020-06-04 18:16:19 +00:00
}
function useViewModeChangedToast({
activeCall,
i18n,
}: {
activeCall: ActiveCallType;
i18n: LocalizerType;
}): void {
const { viewMode } = activeCall;
const previousViewMode = usePrevious(viewMode, viewMode);
const presenterAci = usePresenter(activeCall.remoteParticipants);
const VIEW_MODE_CHANGED_TOAST_KEY = 'view-mode-changed';
const { showToast, hideToast } = useCallingToasts();
useEffect(() => {
if (viewMode !== previousViewMode) {
if (
// If this is an automated change to presentation mode, don't show toast
viewMode === CallViewMode.Presentation ||
// if this is an automated change away from presentation mode, don't show toast
(previousViewMode === CallViewMode.Presentation && !presenterAci)
) {
return;
}
hideToast(VIEW_MODE_CHANGED_TOAST_KEY);
showToast({
key: VIEW_MODE_CHANGED_TOAST_KEY,
content: (
<div className="CallingToast__viewChanged">
<span
className={classNames(
'CallingToast__viewChanged__icon',
getCallViewIconClassname(viewMode)
)}
/>
{i18n('icu:calling__view_mode--updated')}
</div>
),
autoClose: true,
});
}
}, [
showToast,
hideToast,
i18n,
activeCall,
viewMode,
previousViewMode,
presenterAci,
]);
}