Add new toast region for calling button toasts

This commit is contained in:
trevor-signal 2023-10-26 14:26:25 -04:00 committed by GitHub
parent 90eae4b4bf
commit 00d96888e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 129 additions and 80 deletions

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
.CallControls { .CallControls {
position: static; position: relative;
bottom: 0; bottom: 0;
display: flex; display: flex;
flex-grow: 0; flex-grow: 0;

View file

@ -25,12 +25,12 @@
} }
.CallingToast { .CallingToast {
@include font-body-1; @include font-subtitle;
background-color: $color-gray-75; padding-block: 8px;
padding-inline: 12px;
border-radius: 22px; border-radius: 22px;
color: $color-white; background-color: $color-gray-80;
padding-block: 11px; color: $color-gray-15;
padding-inline: 20px;
text-align: center; text-align: center;
user-select: none; user-select: none;
&__reconnecting { &__reconnecting {
@ -39,3 +39,11 @@
gap: 8px; gap: 8px;
} }
} }
.CallingButtonToasts .CallingToasts {
position: absolute;
top: -16px;
transform: translateY(-100%);
/* stylelint-disable-next-line liberty/use-logical-spec */
left: 0;
}

View file

@ -35,7 +35,7 @@ import {
import { AvatarColors } from '../types/Colors'; import { AvatarColors } from '../types/Colors';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import { import {
useMutedToast, CallingButtonToastsContainer,
useReconnectingToast, useReconnectingToast,
useScreenSharingStoppedToast, useScreenSharingStoppedToast,
} from './CallingToastManager'; } from './CallingToastManager';
@ -251,7 +251,6 @@ export function CallScreen({
}; };
}, [toggleAudio, toggleVideo]); }, [toggleAudio, toggleVideo]);
useMutedToast(hasLocalAudio, i18n);
useReconnectingToast({ activeCall, i18n }); useReconnectingToast({ activeCall, i18n });
useScreenSharingStoppedToast({ activeCall, i18n }); useScreenSharingStoppedToast({ activeCall, i18n });
@ -546,6 +545,13 @@ export function CallScreen({
<div className="CallControls__CallTitle">{conversation.title}</div> <div className="CallControls__CallTitle">{conversation.title}</div>
<div className="CallControls__Status">{callStatus}</div> <div className="CallControls__Status">{callStatus}</div>
</div> </div>
<CallingButtonToastsContainer
hasLocalAudio={hasLocalAudio}
outgoingRing={undefined}
i18n={i18n}
/>
<div className="CallControls__ButtonContainer"> <div className="CallControls__ButtonContainer">
<CallingButton <CallingButton
buttonType={presentingButtonType} buttonType={presentingButtonType}

View file

@ -24,7 +24,7 @@ import { useIsOnline } from '../hooks/useIsOnline';
import * as KeyboardLayout from '../services/keyboardLayout'; import * as KeyboardLayout from '../services/keyboardLayout';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import { useCallingToasts } from './CallingToast'; import { useCallingToasts } from './CallingToast';
import { useMutedToast } from './CallingToastManager'; import { CallingButtonToastsContainer } from './CallingToastManager';
export type PropsType = { export type PropsType = {
availableCameras: Array<MediaDeviceInfo>; availableCameras: Array<MediaDeviceInfo>;
@ -228,9 +228,7 @@ export function CallingLobby({
toggleParticipants, toggleParticipants,
]); ]);
useMutedToast(hasLocalAudio, i18n);
useWasInitiallyMutedToast(hasLocalAudio, i18n); useWasInitiallyMutedToast(hasLocalAudio, i18n);
useOutgoingRingToast(isRingButtonVisible, outgoingRing, i18n);
return ( return (
<FocusTrap> <FocusTrap>
@ -283,6 +281,11 @@ export function CallingLobby({
<div className="CallControls__CallTitle">{conversation.title}</div> <div className="CallControls__CallTitle">{conversation.title}</div>
<div className="CallControls__Status">{callStatus}</div> <div className="CallControls__Status">{callStatus}</div>
</div> </div>
<CallingButtonToastsContainer
hasLocalAudio={hasLocalAudio}
outgoingRing={outgoingRing}
i18n={i18n}
/>
<div className="CallControls__ButtonContainer"> <div className="CallControls__ButtonContainer">
<CallingButton <CallingButton
buttonType={videoButtonType} buttonType={videoButtonType}
@ -349,51 +352,3 @@ function useWasInitiallyMutedToast(
} }
}, [hideToast, wasInitiallyMuted, hasLocalAudio]); }, [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

@ -56,10 +56,12 @@ const CallingToastContext = createContext<CallingToastContextType | null>(null);
export function CallingToastProvider({ export function CallingToastProvider({
i18n, i18n,
children, children,
region,
maxToasts = 5, maxToasts = 5,
}: { }: {
i18n: LocalizerType; i18n: LocalizerType;
children: React.ReactNode; children: React.ReactNode;
region?: React.RefObject<HTMLElement>;
maxToasts?: number; maxToasts?: number;
}): JSX.Element { }): JSX.Element {
const [toasts, setToasts] = React.useState<Array<CallingToastStateType>>([]); const [toasts, setToasts] = React.useState<Array<CallingToastStateType>>([]);
@ -198,16 +200,20 @@ export function CallingToastProvider({
const transitions = useTransition(toasts, { const transitions = useTransition(toasts, {
from: item => ({ from: item => ({
opacity: 0, opacity: 0,
scale: 0.85,
marginTop: marginTop:
// If this is the first toast shown, or if this is replacing the // If this is the first toast shown, or if this is replacing the
// first toast, we just fade-in (and don't slide down) // first toast, we just fade-in (and don't slide down)
previousToasts.length === 0 || item.key === previousToasts[0].key previousToasts.length === 0 ||
item.key === previousToasts[0].key ||
maxToasts === toasts.length
? '0px' ? '0px'
: `${-1 * TOAST_HEIGHT_PX}px`, : `${-1 * TOAST_HEIGHT_PX}px`,
}), }),
enter: { enter: {
opacity: 1, opacity: 1,
zIndex: 1, zIndex: 1,
scale: 1,
marginTop: '0px', marginTop: '0px',
config: (key: string) => { config: (key: string) => {
if (key === 'marginTop') { if (key === 'marginTop') {
@ -231,19 +237,22 @@ export function CallingToastProvider({
: `${-1 * (TOAST_HEIGHT_PX + TOAST_GAP_PX)}px`, : `${-1 * (TOAST_HEIGHT_PX + TOAST_GAP_PX)}px`,
// If this toast is being replaced by another one with the same key, immediately // If this toast is being replaced by another one with the same key, immediately
// hide it // hide it
display: toasts.some(toast => toast.key === item.key) display:
toasts.some(toast => toast.key === item.key) ||
maxToasts === toasts.length
? 'none' ? 'none'
: 'block', : 'block',
config: (key: string) => { config: (key: string) => {
if (key === 'zIndex') { if (key === 'zIndex') {
return { duration: 0 }; return { duration: 0 };
} }
if (key === 'display') {
return { duration: 0 };
}
if (key === 'opacity') { if (key === 'opacity') {
return { duration: 100 }; return { duration: 100 };
} }
return { return { duration: 200 };
duration: 300,
};
}, },
}; };
}, },
@ -278,7 +287,7 @@ export function CallingToastProvider({
))} ))}
</div> </div>
</div>, </div>,
document.body region?.current ?? document.body
)} )}
{children} {children}
</CallingToastContext.Provider> </CallingToastContext.Provider>

View file

@ -1,14 +1,15 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import type { ActiveCallType } from '../types/Calling'; import type { ActiveCallType } from '../types/Calling';
import { CallMode } from '../types/Calling'; import { CallMode } from '../types/Calling';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import { isReconnecting } from '../util/callingIsReconnecting'; import { isReconnecting } from '../util/callingIsReconnecting';
import { useCallingToasts } from './CallingToast'; import { CallingToastProvider, useCallingToasts } from './CallingToast';
import { Spinner } from './Spinner'; import { Spinner } from './Spinner';
import { usePrevious } from '../hooks/usePrevious';
type PropsType = { type PropsType = {
activeCall: ActiveCallType; activeCall: ActiveCallType;
@ -99,20 +100,17 @@ export function useScreenSharingStoppedToast({
}, [activeCall, previousPresenter, showToast, i18n]); }, [activeCall, previousPresenter, showToast, i18n]);
} }
export function useMutedToast( function useMutedToast({
hasLocalAudio: boolean, hasLocalAudio,
i18n: LocalizerType i18n,
): void { }: {
const [previousHasLocalAudio, setPreviousHasLocalAudio] = useState< hasLocalAudio: boolean;
undefined | boolean i18n: LocalizerType;
>(undefined); }): void {
const previousHasLocalAudio = usePrevious(hasLocalAudio, hasLocalAudio);
const { showToast, hideToast } = useCallingToasts(); const { showToast, hideToast } = useCallingToasts();
const MUTED_TOAST_KEY = 'muted'; const MUTED_TOAST_KEY = 'muted';
useEffect(() => {
setPreviousHasLocalAudio(hasLocalAudio);
}, [hasLocalAudio]);
useEffect(() => { useEffect(() => {
if ( if (
previousHasLocalAudio !== undefined && previousHasLocalAudio !== undefined &&
@ -130,3 +128,69 @@ export function useMutedToast(
} }
}, [hasLocalAudio, previousHasLocalAudio, hideToast, showToast, i18n]); }, [hasLocalAudio, previousHasLocalAudio, hideToast, showToast, i18n]);
} }
function useOutgoingRingToast({
outgoingRing,
i18n,
}: {
outgoingRing?: boolean;
i18n: LocalizerType;
}): void {
const { showToast, hideToast } = useCallingToasts();
const previousOutgoingRing = usePrevious(outgoingRing, outgoingRing);
const RINGING_TOAST_KEY = 'ringing';
React.useEffect(() => {
if (outgoingRing === undefined) {
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,
});
}
}, [outgoingRing, previousOutgoingRing, hideToast, showToast, i18n]);
}
type CallingButtonToastsType = {
hasLocalAudio: boolean;
outgoingRing: boolean | undefined;
i18n: LocalizerType;
};
export function CallingButtonToastsContainer(
props: CallingButtonToastsType
): JSX.Element {
const toastRegionRef = useRef<HTMLDivElement>(null);
return (
<CallingToastProvider
i18n={props.i18n}
maxToasts={1}
region={toastRegionRef}
>
<div className="CallingButtonToasts" ref={toastRegionRef} />
<CallingButtonToasts {...props} />
</CallingToastProvider>
);
}
function CallingButtonToasts({
hasLocalAudio,
outgoingRing,
i18n,
}: CallingButtonToastsType) {
useMutedToast({ hasLocalAudio, i18n });
useOutgoingRingToast({ outgoingRing, i18n });
return null;
}

View file

@ -3020,6 +3020,13 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2023-10-10T17:05:02.468Z" "updated": "2023-10-10T17:05:02.468Z"
}, },
{
"rule": "React-useRef",
"path": "ts/components/CallingToastManager.tsx",
"line": " const toastRegionRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2023-10-26T13:57:41.860Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/CallsList.tsx", "path": "ts/components/CallsList.tsx",