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
|
// 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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue