Add new toast region for calling button toasts
This commit is contained in:
parent
90eae4b4bf
commit
00d96888e7
7 changed files with 129 additions and 80 deletions
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.CallControls {
|
||||
position: static;
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
|
|
|
@ -25,12 +25,12 @@
|
|||
}
|
||||
|
||||
.CallingToast {
|
||||
@include font-body-1;
|
||||
background-color: $color-gray-75;
|
||||
@include font-subtitle;
|
||||
padding-block: 8px;
|
||||
padding-inline: 12px;
|
||||
border-radius: 22px;
|
||||
color: $color-white;
|
||||
padding-block: 11px;
|
||||
padding-inline: 20px;
|
||||
background-color: $color-gray-80;
|
||||
color: $color-gray-15;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
&__reconnecting {
|
||||
|
@ -39,3 +39,11 @@
|
|||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.CallingButtonToasts .CallingToasts {
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
transform: translateY(-100%);
|
||||
/* stylelint-disable-next-line liberty/use-logical-spec */
|
||||
left: 0;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import {
|
|||
import { AvatarColors } from '../types/Colors';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
useMutedToast,
|
||||
CallingButtonToastsContainer,
|
||||
useReconnectingToast,
|
||||
useScreenSharingStoppedToast,
|
||||
} from './CallingToastManager';
|
||||
|
@ -251,7 +251,6 @@ export function CallScreen({
|
|||
};
|
||||
}, [toggleAudio, toggleVideo]);
|
||||
|
||||
useMutedToast(hasLocalAudio, i18n);
|
||||
useReconnectingToast({ activeCall, i18n });
|
||||
useScreenSharingStoppedToast({ activeCall, i18n });
|
||||
|
||||
|
@ -546,6 +545,13 @@ export function CallScreen({
|
|||
<div className="CallControls__CallTitle">{conversation.title}</div>
|
||||
<div className="CallControls__Status">{callStatus}</div>
|
||||
</div>
|
||||
|
||||
<CallingButtonToastsContainer
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
outgoingRing={undefined}
|
||||
i18n={i18n}
|
||||
/>
|
||||
|
||||
<div className="CallControls__ButtonContainer">
|
||||
<CallingButton
|
||||
buttonType={presentingButtonType}
|
||||
|
|
|
@ -24,7 +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';
|
||||
import { CallingButtonToastsContainer } from './CallingToastManager';
|
||||
|
||||
export type PropsType = {
|
||||
availableCameras: Array<MediaDeviceInfo>;
|
||||
|
@ -228,9 +228,7 @@ export function CallingLobby({
|
|||
toggleParticipants,
|
||||
]);
|
||||
|
||||
useMutedToast(hasLocalAudio, i18n);
|
||||
useWasInitiallyMutedToast(hasLocalAudio, i18n);
|
||||
useOutgoingRingToast(isRingButtonVisible, outgoingRing, i18n);
|
||||
|
||||
return (
|
||||
<FocusTrap>
|
||||
|
@ -283,6 +281,11 @@ export function CallingLobby({
|
|||
<div className="CallControls__CallTitle">{conversation.title}</div>
|
||||
<div className="CallControls__Status">{callStatus}</div>
|
||||
</div>
|
||||
<CallingButtonToastsContainer
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
outgoingRing={outgoingRing}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<div className="CallControls__ButtonContainer">
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
|
@ -349,51 +352,3 @@ 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,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -56,10 +56,12 @@ const CallingToastContext = createContext<CallingToastContextType | null>(null);
|
|||
export function CallingToastProvider({
|
||||
i18n,
|
||||
children,
|
||||
region,
|
||||
maxToasts = 5,
|
||||
}: {
|
||||
i18n: LocalizerType;
|
||||
children: React.ReactNode;
|
||||
region?: React.RefObject<HTMLElement>;
|
||||
maxToasts?: number;
|
||||
}): JSX.Element {
|
||||
const [toasts, setToasts] = React.useState<Array<CallingToastStateType>>([]);
|
||||
|
@ -198,16 +200,20 @@ export function CallingToastProvider({
|
|||
const transitions = useTransition(toasts, {
|
||||
from: item => ({
|
||||
opacity: 0,
|
||||
scale: 0.85,
|
||||
marginTop:
|
||||
// If this is the first toast shown, or if this is replacing the
|
||||
// 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'
|
||||
: `${-1 * TOAST_HEIGHT_PX}px`,
|
||||
}),
|
||||
enter: {
|
||||
opacity: 1,
|
||||
zIndex: 1,
|
||||
scale: 1,
|
||||
marginTop: '0px',
|
||||
config: (key: string) => {
|
||||
if (key === 'marginTop') {
|
||||
|
@ -231,19 +237,22 @@ export function CallingToastProvider({
|
|||
: `${-1 * (TOAST_HEIGHT_PX + TOAST_GAP_PX)}px`,
|
||||
// If this toast is being replaced by another one with the same key, immediately
|
||||
// hide it
|
||||
display: toasts.some(toast => toast.key === item.key)
|
||||
? 'none'
|
||||
: 'block',
|
||||
display:
|
||||
toasts.some(toast => toast.key === item.key) ||
|
||||
maxToasts === toasts.length
|
||||
? 'none'
|
||||
: 'block',
|
||||
config: (key: string) => {
|
||||
if (key === 'zIndex') {
|
||||
return { duration: 0 };
|
||||
}
|
||||
if (key === 'display') {
|
||||
return { duration: 0 };
|
||||
}
|
||||
if (key === 'opacity') {
|
||||
return { duration: 100 };
|
||||
}
|
||||
return {
|
||||
duration: 300,
|
||||
};
|
||||
return { duration: 200 };
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -278,7 +287,7 @@ export function CallingToastProvider({
|
|||
))}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
region?.current ?? document.body
|
||||
)}
|
||||
{children}
|
||||
</CallingToastContext.Provider>
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// 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 { CallMode } from '../types/Calling';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { isReconnecting } from '../util/callingIsReconnecting';
|
||||
import { useCallingToasts } from './CallingToast';
|
||||
import { CallingToastProvider, useCallingToasts } from './CallingToast';
|
||||
import { Spinner } from './Spinner';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
|
||||
type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
|
@ -99,20 +100,17 @@ export function useScreenSharingStoppedToast({
|
|||
}, [activeCall, previousPresenter, showToast, i18n]);
|
||||
}
|
||||
|
||||
export function useMutedToast(
|
||||
hasLocalAudio: boolean,
|
||||
i18n: LocalizerType
|
||||
): void {
|
||||
const [previousHasLocalAudio, setPreviousHasLocalAudio] = useState<
|
||||
undefined | boolean
|
||||
>(undefined);
|
||||
function useMutedToast({
|
||||
hasLocalAudio,
|
||||
i18n,
|
||||
}: {
|
||||
hasLocalAudio: boolean;
|
||||
i18n: LocalizerType;
|
||||
}): void {
|
||||
const previousHasLocalAudio = usePrevious(hasLocalAudio, hasLocalAudio);
|
||||
const { showToast, hideToast } = useCallingToasts();
|
||||
const MUTED_TOAST_KEY = 'muted';
|
||||
|
||||
useEffect(() => {
|
||||
setPreviousHasLocalAudio(hasLocalAudio);
|
||||
}, [hasLocalAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
previousHasLocalAudio !== undefined &&
|
||||
|
@ -130,3 +128,69 @@ export function useMutedToast(
|
|||
}
|
||||
}, [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;
|
||||
}
|
||||
|
|
|
@ -3020,6 +3020,13 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"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",
|
||||
"path": "ts/components/CallsList.tsx",
|
||||
|
|
Loading…
Reference in a new issue