Convert CallingHeader texts to toasts
This commit is contained in:
parent
f180f66e77
commit
292ef1b6f5
10 changed files with 268 additions and 270 deletions
|
@ -3605,28 +3605,6 @@ button.module-image__border-overlay:focus {
|
|||
z-index: $z-index-calling;
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
color: #ffffff;
|
||||
font-style: normal;
|
||||
padding-bottom: 19px;
|
||||
padding-top: calc(24px + var(--title-bar-drag-area-height));
|
||||
text-align: center;
|
||||
@include calling-text-shadow;
|
||||
width: 100%;
|
||||
|
||||
&--header-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.009em;
|
||||
line-height: 21px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__background {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
@ -3808,21 +3786,6 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
background: linear-gradient($color-black-alpha-40, transparent);
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: $z-index-above-above-base;
|
||||
}
|
||||
|
||||
&__header-message {
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.0025em;
|
||||
}
|
||||
|
||||
&__direct-call-ringing-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
@ -3833,6 +3796,7 @@ button.module-image__border-overlay:focus {
|
|||
width: 100%;
|
||||
margin-block-start: 24px;
|
||||
z-index: $z-index-above-base;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&__grid--wrapper {
|
||||
margin-block-start: 26px;
|
||||
|
@ -4158,11 +4122,11 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
|
||||
.module-calling-tools {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
top: calc(32px + var(--title-bar-drag-area-height));
|
||||
width: 100%;
|
||||
inset-inline-end: 0;
|
||||
z-index: $z-index-above-above-base;
|
||||
display: flex;
|
||||
|
||||
&__button {
|
||||
margin-inline-end: 12px;
|
||||
|
@ -4173,6 +4137,7 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
.ContextMenu__container {
|
||||
background: none;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
position: absolute;
|
||||
z-index: $z-index-negative;
|
||||
top: 28px;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&--camera-is-on {
|
||||
@include lonely-local-video-preview;
|
||||
|
|
|
@ -36,7 +36,6 @@ import { AvatarColors } from '../types/Colors';
|
|||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import {
|
||||
CallingButtonToastsContainer,
|
||||
useReconnectingToast,
|
||||
useScreenSharingStoppedToast,
|
||||
} from './CallingToastManager';
|
||||
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
||||
|
@ -61,7 +60,8 @@ import {
|
|||
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
||||
import { isReconnecting as callingIsReconnecting } from '../util/callingIsReconnecting';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import { useCallingToasts } from './CallingToast';
|
||||
import { PersistentCallingToast, useCallingToasts } from './CallingToast';
|
||||
import { Spinner } from './Spinner';
|
||||
|
||||
export type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
|
@ -256,7 +256,6 @@ export function CallScreen({
|
|||
};
|
||||
}, [toggleAudio, toggleVideo]);
|
||||
|
||||
useReconnectingToast({ activeCall, i18n });
|
||||
useScreenSharingStoppedToast({ activeCall, i18n });
|
||||
useViewModeChangedToast({ activeCall, i18n });
|
||||
|
||||
|
@ -273,7 +272,6 @@ export function CallScreen({
|
|||
|
||||
let isRinging: boolean;
|
||||
let hasCallStarted: boolean;
|
||||
let headerTitle: string | undefined;
|
||||
let isConnected: boolean;
|
||||
let participantCount: number;
|
||||
let remoteParticipantsElement: ReactNode;
|
||||
|
@ -307,16 +305,6 @@ export function CallScreen({
|
|||
hasCallStarted = activeCall.joinState !== GroupCallJoinState.NotJoined;
|
||||
participantCount = activeCall.remoteParticipants.length + 1;
|
||||
|
||||
if (isRinging) {
|
||||
headerTitle = undefined;
|
||||
} else if (currentPresenter) {
|
||||
headerTitle = i18n('icu:calling__presenting--person-ongoing', {
|
||||
name: currentPresenter.title,
|
||||
});
|
||||
} else if (!activeCall.remoteParticipants.length) {
|
||||
headerTitle = i18n('icu:calling__in-this-call--zero');
|
||||
}
|
||||
|
||||
isConnected =
|
||||
activeCall.connectionState === GroupCallConnectionState.Connected;
|
||||
remoteParticipantsElement = (
|
||||
|
@ -487,6 +475,29 @@ export function CallScreen({
|
|||
}}
|
||||
role="group"
|
||||
>
|
||||
{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={
|
||||
|
@ -505,7 +516,6 @@ export function CallScreen({
|
|||
i18n={i18n}
|
||||
isGroupCall={isGroupCall}
|
||||
participantCount={participantCount}
|
||||
title={headerTitle}
|
||||
togglePip={togglePip}
|
||||
toggleSettings={toggleSettings}
|
||||
/>
|
||||
|
|
|
@ -18,14 +18,11 @@ export default {
|
|||
argTypes: {
|
||||
isGroupCall: { control: { type: 'boolean' } },
|
||||
participantCount: { control: { type: 'number' } },
|
||||
title: { control: { type: 'text' } },
|
||||
},
|
||||
args: {
|
||||
i18n,
|
||||
isGroupCall: false,
|
||||
message: '',
|
||||
participantCount: 0,
|
||||
title: 'With Someone',
|
||||
togglePip: action('toggle-pip'),
|
||||
callViewMode: CallViewMode.Paginated,
|
||||
changeCallView: action('change-call-view'),
|
||||
|
@ -40,32 +37,8 @@ export function LobbyStyle(args: PropsType): JSX.Element {
|
|||
return (
|
||||
<CallingHeader
|
||||
{...args}
|
||||
title={undefined}
|
||||
togglePip={undefined}
|
||||
onCancel={action('onClose')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function WithParticipants(args: PropsType): JSX.Element {
|
||||
return <CallingHeader {...args} isGroupCall participantCount={10} />;
|
||||
}
|
||||
|
||||
export function WithParticipantsShown(args: PropsType): JSX.Element {
|
||||
return <CallingHeader {...args} isGroupCall participantCount={10} />;
|
||||
}
|
||||
|
||||
export function LongTitle(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingHeader
|
||||
{...args}
|
||||
title="What do I got to, what do I got to do to wake you up? To shake you up, to break the structure up?"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TitleWithMessage(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<CallingHeader {...args} title="Hello world" message="Goodbye earth" />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
@ -14,10 +13,8 @@ export type PropsType = {
|
|||
callViewMode?: CallViewMode;
|
||||
i18n: LocalizerType;
|
||||
isGroupCall?: boolean;
|
||||
message?: ReactNode;
|
||||
onCancel?: () => void;
|
||||
participantCount: number;
|
||||
title?: string;
|
||||
togglePip?: () => void;
|
||||
toggleSettings: () => void;
|
||||
changeCallView?: (mode: CallViewMode) => void;
|
||||
|
@ -28,132 +25,122 @@ export function CallingHeader({
|
|||
changeCallView,
|
||||
i18n,
|
||||
isGroupCall = false,
|
||||
message,
|
||||
onCancel,
|
||||
participantCount,
|
||||
title,
|
||||
togglePip,
|
||||
toggleSettings,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<div className="module-calling__header">
|
||||
{title ? (
|
||||
<div className="module-calling__header--header-name">{title}</div>
|
||||
) : null}
|
||||
{message ? (
|
||||
<div className="module-ongoing-call__header-message">{message}</div>
|
||||
) : null}
|
||||
<div className="module-calling-tools">
|
||||
{isGroupCall &&
|
||||
participantCount > 2 &&
|
||||
callViewMode &&
|
||||
changeCallView && (
|
||||
<div className="module-calling-tools__button">
|
||||
<ContextMenu
|
||||
ariaLabel={i18n('icu:calling__change-view')}
|
||||
i18n={i18n}
|
||||
menuOptions={[
|
||||
{
|
||||
icon: 'CallSettingsButton__Icon--PaginatedView',
|
||||
label: i18n('icu:calling__view_mode--paginated'),
|
||||
onClick: () => changeCallView(CallViewMode.Paginated),
|
||||
value: CallViewMode.Paginated,
|
||||
},
|
||||
{
|
||||
icon: 'CallSettingsButton__Icon--OverflowView',
|
||||
label: i18n('icu:calling__view_mode--overflow'),
|
||||
onClick: () => changeCallView(CallViewMode.Overflow),
|
||||
value: CallViewMode.Overflow,
|
||||
},
|
||||
{
|
||||
icon: 'CallSettingsButton__Icon--SpeakerView',
|
||||
label: i18n('icu:calling__view_mode--speaker'),
|
||||
onClick: () => changeCallView(CallViewMode.Speaker),
|
||||
value: CallViewMode.Speaker,
|
||||
},
|
||||
]}
|
||||
<div className="module-calling-tools">
|
||||
{isGroupCall &&
|
||||
participantCount > 2 &&
|
||||
callViewMode &&
|
||||
changeCallView && (
|
||||
<div className="module-calling-tools__button">
|
||||
<ContextMenu
|
||||
ariaLabel={i18n('icu:calling__change-view')}
|
||||
i18n={i18n}
|
||||
menuOptions={[
|
||||
{
|
||||
icon: 'CallSettingsButton__Icon--PaginatedView',
|
||||
label: i18n('icu:calling__view_mode--paginated'),
|
||||
onClick: () => changeCallView(CallViewMode.Paginated),
|
||||
value: CallViewMode.Paginated,
|
||||
},
|
||||
{
|
||||
icon: 'CallSettingsButton__Icon--OverflowView',
|
||||
label: i18n('icu:calling__view_mode--overflow'),
|
||||
onClick: () => changeCallView(CallViewMode.Overflow),
|
||||
value: CallViewMode.Overflow,
|
||||
},
|
||||
{
|
||||
icon: 'CallSettingsButton__Icon--SpeakerView',
|
||||
label: i18n('icu:calling__view_mode--speaker'),
|
||||
onClick: () => changeCallView(CallViewMode.Speaker),
|
||||
value: CallViewMode.Speaker,
|
||||
},
|
||||
]}
|
||||
theme={Theme.Dark}
|
||||
popperOptions={{
|
||||
placement: 'bottom',
|
||||
strategy: 'absolute',
|
||||
}}
|
||||
value={
|
||||
// If it's Presentation we want to still show Speaker as selected
|
||||
callViewMode === CallViewMode.Presentation
|
||||
? CallViewMode.Speaker
|
||||
: callViewMode
|
||||
}
|
||||
>
|
||||
<Tooltip
|
||||
content={i18n('icu:calling__change-view')}
|
||||
className="CallingButton__tooltip"
|
||||
theme={Theme.Dark}
|
||||
popperOptions={{
|
||||
placement: 'bottom',
|
||||
strategy: 'absolute',
|
||||
}}
|
||||
value={
|
||||
// If it's Presentation we want to still show Speaker as selected
|
||||
callViewMode === CallViewMode.Presentation
|
||||
? CallViewMode.Speaker
|
||||
: callViewMode
|
||||
}
|
||||
>
|
||||
<Tooltip
|
||||
content={i18n('icu:calling__change-view')}
|
||||
className="CallingButton__tooltip"
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<div className="CallSettingsButton__Button">
|
||||
<span
|
||||
className={classNames(
|
||||
'CallSettingsButton__Icon',
|
||||
getCallViewIconClassname(callViewMode)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
)}
|
||||
<div className="CallSettingsButton__Button">
|
||||
<span
|
||||
className={classNames(
|
||||
'CallSettingsButton__Icon',
|
||||
getCallViewIconClassname(callViewMode)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</ContextMenu>
|
||||
</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>
|
||||
{togglePip && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('icu:callingDeviceSelection__settings')}
|
||||
content={i18n('icu:calling__pip--on')}
|
||||
className="CallingButton__tooltip"
|
||||
theme={Theme.Dark}
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:callingDeviceSelection__settings')}
|
||||
aria-label={i18n('icu:calling__pip--on')}
|
||||
className="CallSettingsButton__Button"
|
||||
onClick={toggleSettings}
|
||||
onClick={togglePip}
|
||||
type="button"
|
||||
>
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Settings" />
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Pip" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{togglePip && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('icu:calling__pip--on')}
|
||||
className="CallingButton__tooltip"
|
||||
theme={Theme.Dark}
|
||||
)}
|
||||
{onCancel && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('icu:cancel')}
|
||||
theme={Theme.Dark}
|
||||
className="CallingButton__tooltip"
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:cancel')}
|
||||
className="CallSettingsButton__Button CallSettingsButton__Button--Cancel"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:calling__pip--on')}
|
||||
className="CallSettingsButton__Button"
|
||||
onClick={togglePip}
|
||||
type="button"
|
||||
>
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Pip" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{onCancel && (
|
||||
<div className="module-calling-tools__button">
|
||||
<Tooltip
|
||||
content={i18n('icu:cancel')}
|
||||
theme={Theme.Dark}
|
||||
className="CallingButton__tooltip"
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('icu:cancel')}
|
||||
className="CallSettingsButton__Button CallSettingsButton__Button--Cancel"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Cancel" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="CallSettingsButton__Icon CallSettingsButton__Icon--Cancel" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { v4 as uuid } from 'uuid';
|
|||
import { useIsMounted } from '../hooks/useIsMounted';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import { difference } from '../util/setUtil';
|
||||
|
||||
const DEFAULT_LIFETIME = 5000;
|
||||
|
||||
|
@ -57,12 +58,12 @@ export function CallingToastProvider({
|
|||
i18n,
|
||||
children,
|
||||
region,
|
||||
maxToasts = 5,
|
||||
maxNonPersistentToasts = 5,
|
||||
}: {
|
||||
i18n: LocalizerType;
|
||||
children: React.ReactNode;
|
||||
region?: React.RefObject<HTMLElement>;
|
||||
maxToasts?: number;
|
||||
maxNonPersistentToasts?: number;
|
||||
}): JSX.Element {
|
||||
const [toasts, setToasts] = React.useState<Array<CallingToastStateType>>([]);
|
||||
const previousToasts = usePrevious([], toasts);
|
||||
|
@ -137,8 +138,15 @@ export function CallingToastProvider({
|
|||
return state;
|
||||
}
|
||||
|
||||
if (state.length === maxToasts) {
|
||||
const toastToBePushedOut = state.at(-1);
|
||||
const persistentToasts = state.filter(({ autoClose }) => !autoClose);
|
||||
const nonPersistentToasts = state.filter(({ autoClose }) => autoClose);
|
||||
|
||||
if (
|
||||
nonPersistentToasts.length === maxNonPersistentToasts &&
|
||||
maxNonPersistentToasts > 0
|
||||
) {
|
||||
const toastToBePushedOut = nonPersistentToasts.pop();
|
||||
|
||||
if (toastToBePushedOut) {
|
||||
clearToastTimeout(toastToBePushedOut.key);
|
||||
}
|
||||
|
@ -146,15 +154,19 @@ export function CallingToastProvider({
|
|||
|
||||
if (toast.autoClose) {
|
||||
startTimer(key, DEFAULT_LIFETIME);
|
||||
nonPersistentToasts.unshift({ ...toast, key });
|
||||
} else {
|
||||
persistentToasts.unshift({ ...toast, key });
|
||||
}
|
||||
shownToasts.current.add(key);
|
||||
|
||||
return [{ ...toast, key }, ...state.slice(0, maxToasts - 1)];
|
||||
// Show persistent toasts at top of list always
|
||||
return [...persistentToasts, ...nonPersistentToasts];
|
||||
});
|
||||
|
||||
return key;
|
||||
},
|
||||
[startTimer, clearToastTimeout, maxToasts]
|
||||
[startTimer, clearToastTimeout, maxNonPersistentToasts]
|
||||
);
|
||||
|
||||
const pauseAll = useCallback(() => {
|
||||
|
@ -197,22 +209,43 @@ export function CallingToastProvider({
|
|||
|
||||
const TOAST_HEIGHT_PX = 42;
|
||||
const TOAST_GAP_PX = 8;
|
||||
|
||||
const curToasts = new Set(toasts);
|
||||
const prevToasts = new Set(previousToasts);
|
||||
|
||||
const toastsRemoved = difference(prevToasts, curToasts);
|
||||
const toastsAdded = difference(curToasts, prevToasts);
|
||||
|
||||
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 ||
|
||||
maxToasts === toasts.length
|
||||
? '0px'
|
||||
: `${-1 * TOAST_HEIGHT_PX}px`,
|
||||
}),
|
||||
from: item => {
|
||||
const enteringItemIndex = toasts.findIndex(
|
||||
toast => toast.key === item.key
|
||||
);
|
||||
const isToastReplacingAnExistingOneAtThisPosition = toastsRemoved.has(
|
||||
previousToasts[enteringItemIndex]
|
||||
);
|
||||
return {
|
||||
opacity: 0,
|
||||
zIndex: item.autoClose ? 1 : 2,
|
||||
scale: 0.85,
|
||||
marginTop:
|
||||
// If this toast is replacing an existing one, don't slide-down, just fade-in
|
||||
// Note: this just refers to toasts added / removed within one render cycle;
|
||||
// this will almost always be when replacing toasts that are related
|
||||
// Note: this
|
||||
// Example:
|
||||
// previous current
|
||||
// "Muted" "Unmuted"
|
||||
//
|
||||
// The previous toast should disappear and the new one should fade-in in its
|
||||
// place, so it looks like a replacement.
|
||||
isToastReplacingAnExistingOneAtThisPosition
|
||||
? '0px'
|
||||
: `${-1 * TOAST_HEIGHT_PX}px`,
|
||||
};
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
zIndex: 1,
|
||||
scale: 1,
|
||||
marginTop: '0px',
|
||||
config: (key: string) => {
|
||||
|
@ -226,22 +259,23 @@ export function CallingToastProvider({
|
|||
},
|
||||
},
|
||||
leave: item => {
|
||||
const leavingItemIndex = previousToasts.findIndex(
|
||||
toast => toast.key === item.key
|
||||
);
|
||||
const isToastBeingReplacedByANewOneAtThisPosition = toastsAdded.has(
|
||||
toasts[leavingItemIndex]
|
||||
);
|
||||
return {
|
||||
zIndex: 0,
|
||||
opacity: 0,
|
||||
// If the last toast in the list is leaving, we don't need to move it up.
|
||||
marginTop:
|
||||
previousToasts.findIndex(toast => toast.key === item.key) ===
|
||||
previousToasts.length - 1
|
||||
leavingItemIndex === previousToasts.length - 1
|
||||
? '0px'
|
||||
: `${-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) ||
|
||||
maxToasts === toasts.length
|
||||
? 'none'
|
||||
: 'block',
|
||||
// If this toast is being replaced by a new toast at this position, disappear
|
||||
// immediately (don't interfere with new one coming in)
|
||||
display: isToastBeingReplacedByANewOneAtThisPosition ? 'none' : 'block',
|
||||
config: (key: string) => {
|
||||
if (key === 'zIndex') {
|
||||
return { duration: 0 };
|
||||
|
@ -252,7 +286,7 @@ export function CallingToastProvider({
|
|||
if (key === 'opacity') {
|
||||
return { duration: 100 };
|
||||
}
|
||||
return { duration: 200 };
|
||||
return { clamp: true, duration: 200 };
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -301,6 +335,7 @@ function CallingToast(
|
|||
): JSX.Element {
|
||||
const className = classNames(
|
||||
'CallingToast',
|
||||
!props.autoClose && 'CallingToast--persistent',
|
||||
props.dismissable && 'CallingToast--dismissable'
|
||||
);
|
||||
|
||||
|
@ -360,3 +395,21 @@ export function useCallingToasts(): CallingToastContextType {
|
|||
[wrappedShowToast, callingToastContext]
|
||||
);
|
||||
}
|
||||
|
||||
export function PersistentCallingToast({
|
||||
children,
|
||||
}: {
|
||||
children: string | JSX.Element;
|
||||
}): null {
|
||||
const { showToast } = useCallingToasts();
|
||||
const toastId = useRef<string>(uuid());
|
||||
useEffect(() => {
|
||||
showToast({
|
||||
key: toastId.current,
|
||||
content: children,
|
||||
autoClose: false,
|
||||
});
|
||||
}, [children, showToast]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef } 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 { CallingToastProvider, useCallingToasts } from './CallingToast';
|
||||
import { Spinner } from './Spinner';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
|
||||
type PropsType = {
|
||||
|
@ -16,35 +14,13 @@ type PropsType = {
|
|||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export function useReconnectingToast({ activeCall, i18n }: PropsType): void {
|
||||
const { showToast, hideToast } = useCallingToasts();
|
||||
const RECONNECTING_TOAST_KEY = 'reconnecting';
|
||||
|
||||
useEffect(() => {
|
||||
if (isReconnecting(activeCall)) {
|
||||
showToast({
|
||||
key: RECONNECTING_TOAST_KEY,
|
||||
content: (
|
||||
<span className="CallingToast__reconnecting">
|
||||
<Spinner svgSize="small" size="16px" />
|
||||
{i18n('icu:callReconnecting')}
|
||||
</span>
|
||||
),
|
||||
autoClose: false,
|
||||
});
|
||||
} else {
|
||||
hideToast(RECONNECTING_TOAST_KEY);
|
||||
}
|
||||
}, [activeCall, i18n, showToast, hideToast]);
|
||||
}
|
||||
|
||||
const ME = Symbol('me');
|
||||
|
||||
function getCurrentPresenter(
|
||||
activeCall: Readonly<ActiveCallType>
|
||||
): ConversationType | typeof ME | undefined {
|
||||
): ConversationType | { id: typeof ME } | undefined {
|
||||
if (activeCall.presentingSource) {
|
||||
return ME;
|
||||
return { id: ME };
|
||||
}
|
||||
if (activeCall.callMode === CallMode.Direct) {
|
||||
const isOtherPersonPresenting = activeCall.remoteParticipants.some(
|
||||
|
@ -64,30 +40,21 @@ export function useScreenSharingStoppedToast({
|
|||
activeCall,
|
||||
i18n,
|
||||
}: PropsType): void {
|
||||
const [previousPresenter, setPreviousPresenter] = useState<
|
||||
undefined | { id: string | typeof ME; title?: string }
|
||||
>(undefined);
|
||||
const { showToast } = useCallingToasts();
|
||||
const { showToast, hideToast } = useCallingToasts();
|
||||
|
||||
const SOMEONE_STOPPED_PRESENTING_TOAST_KEY = 'someone_stopped_presenting';
|
||||
|
||||
const currentPresenter = useMemo(
|
||||
() => getCurrentPresenter(activeCall),
|
||||
[activeCall]
|
||||
);
|
||||
const previousPresenter = usePrevious(currentPresenter, currentPresenter);
|
||||
|
||||
useEffect(() => {
|
||||
const currentPresenter = getCurrentPresenter(activeCall);
|
||||
if (currentPresenter === ME) {
|
||||
setPreviousPresenter({
|
||||
id: ME,
|
||||
});
|
||||
} else if (!currentPresenter) {
|
||||
setPreviousPresenter(undefined);
|
||||
} else {
|
||||
const { id, title } = currentPresenter;
|
||||
setPreviousPresenter({ id, title });
|
||||
}
|
||||
}, [activeCall]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentPresenter = getCurrentPresenter(activeCall);
|
||||
|
||||
if (!currentPresenter && previousPresenter && previousPresenter.title) {
|
||||
if (previousPresenter && !currentPresenter) {
|
||||
hideToast(SOMEONE_STOPPED_PRESENTING_TOAST_KEY);
|
||||
showToast({
|
||||
key: SOMEONE_STOPPED_PRESENTING_TOAST_KEY,
|
||||
content:
|
||||
previousPresenter.id === ME
|
||||
? i18n('icu:calling__presenting--you-stopped')
|
||||
|
@ -97,7 +64,14 @@ export function useScreenSharingStoppedToast({
|
|||
autoClose: true,
|
||||
});
|
||||
}
|
||||
}, [activeCall, previousPresenter, showToast, i18n]);
|
||||
}, [
|
||||
activeCall,
|
||||
hideToast,
|
||||
currentPresenter,
|
||||
previousPresenter,
|
||||
showToast,
|
||||
i18n,
|
||||
]);
|
||||
}
|
||||
|
||||
function useMutedToast({
|
||||
|
@ -175,7 +149,7 @@ export function CallingButtonToastsContainer(
|
|||
return (
|
||||
<CallingToastProvider
|
||||
i18n={props.i18n}
|
||||
maxToasts={1}
|
||||
maxNonPersistentToasts={1}
|
||||
region={toastRegionRef}
|
||||
>
|
||||
<div className="CallingButtonToasts" ref={toastRegionRef} />
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { isEqual, remove, toggle } from '../../util/setUtil';
|
||||
import { difference, isEqual, remove, toggle } from '../../util/setUtil';
|
||||
|
||||
describe('set utilities', () => {
|
||||
const original = new Set([1, 2, 3]);
|
||||
|
@ -82,4 +82,21 @@ describe('set utilities', () => {
|
|||
assert.deepStrictEqual(original, new Set([1, 2, 3]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('difference', () => {
|
||||
it('returns the difference of two sets', () => {
|
||||
assert.deepStrictEqual(
|
||||
difference(new Set(['a', 'b', 'c']), new Set(['a', 'b', 'c'])),
|
||||
new Set()
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
difference(new Set(['a', 'b', 'c']), new Set([])),
|
||||
new Set(['a', 'b', 'c'])
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
difference(new Set(['a', 'b', 'c']), new Set(['d'])),
|
||||
new Set(['a', 'b', 'c'])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3026,6 +3026,13 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-10-26T13:57:41.860Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingToast.tsx",
|
||||
"line": " const toastId = useRef<string>(uuid());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-11-14T16:52:45.342Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallsList.tsx",
|
||||
|
|
|
@ -27,3 +27,14 @@ export const isEqual = (
|
|||
a: Readonly<Set<unknown>>,
|
||||
b: Readonly<Set<unknown>>
|
||||
): boolean => a === b || (a.size === b.size && every(a, item => b.has(item)));
|
||||
|
||||
export const difference = <T>(
|
||||
a: Readonly<Set<T>>,
|
||||
b: Readonly<Set<T>>
|
||||
): Set<T> => {
|
||||
const result = new Set([...a]);
|
||||
for (const item of b) {
|
||||
result.delete(item);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue