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;
|
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 {
|
&__background {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
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 {
|
&__direct-call-ringing-spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -3833,6 +3796,7 @@ button.module-image__border-overlay:focus {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-block-start: 24px;
|
margin-block-start: 24px;
|
||||||
z-index: $z-index-above-base;
|
z-index: $z-index-above-base;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
|
||||||
&__grid--wrapper {
|
&__grid--wrapper {
|
||||||
margin-block-start: 26px;
|
margin-block-start: 26px;
|
||||||
|
@ -4158,11 +4122,11 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-calling-tools {
|
.module-calling-tools {
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(32px + var(--title-bar-drag-area-height));
|
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 {
|
&__button {
|
||||||
margin-inline-end: 12px;
|
margin-inline-end: 12px;
|
||||||
|
@ -4173,6 +4137,7 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
.ContextMenu__container {
|
.ContextMenu__container {
|
||||||
background: none;
|
background: none;
|
||||||
|
text-wrap: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: $z-index-negative;
|
z-index: $z-index-negative;
|
||||||
top: 28px;
|
top: 28px;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
|
||||||
&--camera-is-on {
|
&--camera-is-on {
|
||||||
@include lonely-local-video-preview;
|
@include lonely-local-video-preview;
|
||||||
|
|
|
@ -36,7 +36,6 @@ import { AvatarColors } from '../types/Colors';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import {
|
import {
|
||||||
CallingButtonToastsContainer,
|
CallingButtonToastsContainer,
|
||||||
useReconnectingToast,
|
|
||||||
useScreenSharingStoppedToast,
|
useScreenSharingStoppedToast,
|
||||||
} from './CallingToastManager';
|
} from './CallingToastManager';
|
||||||
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant';
|
||||||
|
@ -61,7 +60,8 @@ import {
|
||||||
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
|
||||||
import { isReconnecting as callingIsReconnecting } from '../util/callingIsReconnecting';
|
import { isReconnecting as callingIsReconnecting } from '../util/callingIsReconnecting';
|
||||||
import { usePrevious } from '../hooks/usePrevious';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
import { useCallingToasts } from './CallingToast';
|
import { PersistentCallingToast, useCallingToasts } from './CallingToast';
|
||||||
|
import { Spinner } from './Spinner';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
activeCall: ActiveCallType;
|
activeCall: ActiveCallType;
|
||||||
|
@ -256,7 +256,6 @@ export function CallScreen({
|
||||||
};
|
};
|
||||||
}, [toggleAudio, toggleVideo]);
|
}, [toggleAudio, toggleVideo]);
|
||||||
|
|
||||||
useReconnectingToast({ activeCall, i18n });
|
|
||||||
useScreenSharingStoppedToast({ activeCall, i18n });
|
useScreenSharingStoppedToast({ activeCall, i18n });
|
||||||
useViewModeChangedToast({ activeCall, i18n });
|
useViewModeChangedToast({ activeCall, i18n });
|
||||||
|
|
||||||
|
@ -273,7 +272,6 @@ export function CallScreen({
|
||||||
|
|
||||||
let isRinging: boolean;
|
let isRinging: boolean;
|
||||||
let hasCallStarted: boolean;
|
let hasCallStarted: boolean;
|
||||||
let headerTitle: string | undefined;
|
|
||||||
let isConnected: boolean;
|
let isConnected: boolean;
|
||||||
let participantCount: number;
|
let participantCount: number;
|
||||||
let remoteParticipantsElement: ReactNode;
|
let remoteParticipantsElement: ReactNode;
|
||||||
|
@ -307,16 +305,6 @@ export function CallScreen({
|
||||||
hasCallStarted = activeCall.joinState !== GroupCallJoinState.NotJoined;
|
hasCallStarted = activeCall.joinState !== GroupCallJoinState.NotJoined;
|
||||||
participantCount = activeCall.remoteParticipants.length + 1;
|
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 =
|
isConnected =
|
||||||
activeCall.connectionState === GroupCallConnectionState.Connected;
|
activeCall.connectionState === GroupCallConnectionState.Connected;
|
||||||
remoteParticipantsElement = (
|
remoteParticipantsElement = (
|
||||||
|
@ -487,6 +475,29 @@ export function CallScreen({
|
||||||
}}
|
}}
|
||||||
role="group"
|
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 ? (
|
{showNeedsScreenRecordingPermissionsWarning ? (
|
||||||
<NeedsScreenRecordingPermissionsModal
|
<NeedsScreenRecordingPermissionsModal
|
||||||
toggleScreenRecordingPermissionsDialog={
|
toggleScreenRecordingPermissionsDialog={
|
||||||
|
@ -505,7 +516,6 @@ export function CallScreen({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isGroupCall={isGroupCall}
|
isGroupCall={isGroupCall}
|
||||||
participantCount={participantCount}
|
participantCount={participantCount}
|
||||||
title={headerTitle}
|
|
||||||
togglePip={togglePip}
|
togglePip={togglePip}
|
||||||
toggleSettings={toggleSettings}
|
toggleSettings={toggleSettings}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -18,14 +18,11 @@ export default {
|
||||||
argTypes: {
|
argTypes: {
|
||||||
isGroupCall: { control: { type: 'boolean' } },
|
isGroupCall: { control: { type: 'boolean' } },
|
||||||
participantCount: { control: { type: 'number' } },
|
participantCount: { control: { type: 'number' } },
|
||||||
title: { control: { type: 'text' } },
|
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
i18n,
|
i18n,
|
||||||
isGroupCall: false,
|
isGroupCall: false,
|
||||||
message: '',
|
|
||||||
participantCount: 0,
|
participantCount: 0,
|
||||||
title: 'With Someone',
|
|
||||||
togglePip: action('toggle-pip'),
|
togglePip: action('toggle-pip'),
|
||||||
callViewMode: CallViewMode.Paginated,
|
callViewMode: CallViewMode.Paginated,
|
||||||
changeCallView: action('change-call-view'),
|
changeCallView: action('change-call-view'),
|
||||||
|
@ -40,32 +37,8 @@ export function LobbyStyle(args: PropsType): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<CallingHeader
|
<CallingHeader
|
||||||
{...args}
|
{...args}
|
||||||
title={undefined}
|
|
||||||
togglePip={undefined}
|
togglePip={undefined}
|
||||||
onCancel={action('onClose')}
|
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
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
@ -14,10 +13,8 @@ export type PropsType = {
|
||||||
callViewMode?: CallViewMode;
|
callViewMode?: CallViewMode;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isGroupCall?: boolean;
|
isGroupCall?: boolean;
|
||||||
message?: ReactNode;
|
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
participantCount: number;
|
participantCount: number;
|
||||||
title?: string;
|
|
||||||
togglePip?: () => void;
|
togglePip?: () => void;
|
||||||
toggleSettings: () => void;
|
toggleSettings: () => void;
|
||||||
changeCallView?: (mode: CallViewMode) => void;
|
changeCallView?: (mode: CallViewMode) => void;
|
||||||
|
@ -28,21 +25,12 @@ export function CallingHeader({
|
||||||
changeCallView,
|
changeCallView,
|
||||||
i18n,
|
i18n,
|
||||||
isGroupCall = false,
|
isGroupCall = false,
|
||||||
message,
|
|
||||||
onCancel,
|
onCancel,
|
||||||
participantCount,
|
participantCount,
|
||||||
title,
|
|
||||||
togglePip,
|
togglePip,
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
return (
|
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">
|
<div className="module-calling-tools">
|
||||||
{isGroupCall &&
|
{isGroupCall &&
|
||||||
participantCount > 2 &&
|
participantCount > 2 &&
|
||||||
|
@ -154,7 +142,6 @@ export function CallingHeader({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { v4 as uuid } from 'uuid';
|
||||||
import { useIsMounted } from '../hooks/useIsMounted';
|
import { useIsMounted } from '../hooks/useIsMounted';
|
||||||
import type { LocalizerType } from '../types/I18N';
|
import type { LocalizerType } from '../types/I18N';
|
||||||
import { usePrevious } from '../hooks/usePrevious';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
|
import { difference } from '../util/setUtil';
|
||||||
|
|
||||||
const DEFAULT_LIFETIME = 5000;
|
const DEFAULT_LIFETIME = 5000;
|
||||||
|
|
||||||
|
@ -57,12 +58,12 @@ export function CallingToastProvider({
|
||||||
i18n,
|
i18n,
|
||||||
children,
|
children,
|
||||||
region,
|
region,
|
||||||
maxToasts = 5,
|
maxNonPersistentToasts = 5,
|
||||||
}: {
|
}: {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
region?: React.RefObject<HTMLElement>;
|
region?: React.RefObject<HTMLElement>;
|
||||||
maxToasts?: number;
|
maxNonPersistentToasts?: number;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [toasts, setToasts] = React.useState<Array<CallingToastStateType>>([]);
|
const [toasts, setToasts] = React.useState<Array<CallingToastStateType>>([]);
|
||||||
const previousToasts = usePrevious([], toasts);
|
const previousToasts = usePrevious([], toasts);
|
||||||
|
@ -137,8 +138,15 @@ export function CallingToastProvider({
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.length === maxToasts) {
|
const persistentToasts = state.filter(({ autoClose }) => !autoClose);
|
||||||
const toastToBePushedOut = state.at(-1);
|
const nonPersistentToasts = state.filter(({ autoClose }) => autoClose);
|
||||||
|
|
||||||
|
if (
|
||||||
|
nonPersistentToasts.length === maxNonPersistentToasts &&
|
||||||
|
maxNonPersistentToasts > 0
|
||||||
|
) {
|
||||||
|
const toastToBePushedOut = nonPersistentToasts.pop();
|
||||||
|
|
||||||
if (toastToBePushedOut) {
|
if (toastToBePushedOut) {
|
||||||
clearToastTimeout(toastToBePushedOut.key);
|
clearToastTimeout(toastToBePushedOut.key);
|
||||||
}
|
}
|
||||||
|
@ -146,15 +154,19 @@ export function CallingToastProvider({
|
||||||
|
|
||||||
if (toast.autoClose) {
|
if (toast.autoClose) {
|
||||||
startTimer(key, DEFAULT_LIFETIME);
|
startTimer(key, DEFAULT_LIFETIME);
|
||||||
|
nonPersistentToasts.unshift({ ...toast, key });
|
||||||
|
} else {
|
||||||
|
persistentToasts.unshift({ ...toast, key });
|
||||||
}
|
}
|
||||||
shownToasts.current.add(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;
|
return key;
|
||||||
},
|
},
|
||||||
[startTimer, clearToastTimeout, maxToasts]
|
[startTimer, clearToastTimeout, maxNonPersistentToasts]
|
||||||
);
|
);
|
||||||
|
|
||||||
const pauseAll = useCallback(() => {
|
const pauseAll = useCallback(() => {
|
||||||
|
@ -197,22 +209,43 @@ export function CallingToastProvider({
|
||||||
|
|
||||||
const TOAST_HEIGHT_PX = 42;
|
const TOAST_HEIGHT_PX = 42;
|
||||||
const TOAST_GAP_PX = 8;
|
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, {
|
const transitions = useTransition(toasts, {
|
||||||
from: item => ({
|
from: item => {
|
||||||
|
const enteringItemIndex = toasts.findIndex(
|
||||||
|
toast => toast.key === item.key
|
||||||
|
);
|
||||||
|
const isToastReplacingAnExistingOneAtThisPosition = toastsRemoved.has(
|
||||||
|
previousToasts[enteringItemIndex]
|
||||||
|
);
|
||||||
|
return {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
zIndex: item.autoClose ? 1 : 2,
|
||||||
scale: 0.85,
|
scale: 0.85,
|
||||||
marginTop:
|
marginTop:
|
||||||
// If this is the first toast shown, or if this is replacing the
|
// If this toast is replacing an existing one, don't slide-down, just fade-in
|
||||||
// first toast, we just fade-in (and don't slide down)
|
// Note: this just refers to toasts added / removed within one render cycle;
|
||||||
previousToasts.length === 0 ||
|
// this will almost always be when replacing toasts that are related
|
||||||
item.key === previousToasts[0].key ||
|
// Note: this
|
||||||
maxToasts === toasts.length
|
// 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'
|
? '0px'
|
||||||
: `${-1 * TOAST_HEIGHT_PX}px`,
|
: `${-1 * TOAST_HEIGHT_PX}px`,
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
enter: {
|
enter: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
zIndex: 1,
|
|
||||||
scale: 1,
|
scale: 1,
|
||||||
marginTop: '0px',
|
marginTop: '0px',
|
||||||
config: (key: string) => {
|
config: (key: string) => {
|
||||||
|
@ -226,22 +259,23 @@ export function CallingToastProvider({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
leave: item => {
|
leave: item => {
|
||||||
|
const leavingItemIndex = previousToasts.findIndex(
|
||||||
|
toast => toast.key === item.key
|
||||||
|
);
|
||||||
|
const isToastBeingReplacedByANewOneAtThisPosition = toastsAdded.has(
|
||||||
|
toasts[leavingItemIndex]
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
// If the last toast in the list is leaving, we don't need to move it up.
|
// If the last toast in the list is leaving, we don't need to move it up.
|
||||||
marginTop:
|
marginTop:
|
||||||
previousToasts.findIndex(toast => toast.key === item.key) ===
|
leavingItemIndex === previousToasts.length - 1
|
||||||
previousToasts.length - 1
|
|
||||||
? '0px'
|
? '0px'
|
||||||
: `${-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 a new toast at this position, disappear
|
||||||
// hide it
|
// immediately (don't interfere with new one coming in)
|
||||||
display:
|
display: isToastBeingReplacedByANewOneAtThisPosition ? 'none' : 'block',
|
||||||
toasts.some(toast => toast.key === item.key) ||
|
|
||||||
maxToasts === toasts.length
|
|
||||||
? 'none'
|
|
||||||
: 'block',
|
|
||||||
config: (key: string) => {
|
config: (key: string) => {
|
||||||
if (key === 'zIndex') {
|
if (key === 'zIndex') {
|
||||||
return { duration: 0 };
|
return { duration: 0 };
|
||||||
|
@ -252,7 +286,7 @@ export function CallingToastProvider({
|
||||||
if (key === 'opacity') {
|
if (key === 'opacity') {
|
||||||
return { duration: 100 };
|
return { duration: 100 };
|
||||||
}
|
}
|
||||||
return { duration: 200 };
|
return { clamp: true, duration: 200 };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -301,6 +335,7 @@ function CallingToast(
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
const className = classNames(
|
const className = classNames(
|
||||||
'CallingToast',
|
'CallingToast',
|
||||||
|
!props.autoClose && 'CallingToast--persistent',
|
||||||
props.dismissable && 'CallingToast--dismissable'
|
props.dismissable && 'CallingToast--dismissable'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -360,3 +395,21 @@ export function useCallingToasts(): CallingToastContextType {
|
||||||
[wrappedShowToast, callingToastContext]
|
[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
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 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 { CallingToastProvider, useCallingToasts } from './CallingToast';
|
import { CallingToastProvider, useCallingToasts } from './CallingToast';
|
||||||
import { Spinner } from './Spinner';
|
|
||||||
import { usePrevious } from '../hooks/usePrevious';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
|
@ -16,35 +14,13 @@ type PropsType = {
|
||||||
i18n: LocalizerType;
|
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');
|
const ME = Symbol('me');
|
||||||
|
|
||||||
function getCurrentPresenter(
|
function getCurrentPresenter(
|
||||||
activeCall: Readonly<ActiveCallType>
|
activeCall: Readonly<ActiveCallType>
|
||||||
): ConversationType | typeof ME | undefined {
|
): ConversationType | { id: typeof ME } | undefined {
|
||||||
if (activeCall.presentingSource) {
|
if (activeCall.presentingSource) {
|
||||||
return ME;
|
return { id: ME };
|
||||||
}
|
}
|
||||||
if (activeCall.callMode === CallMode.Direct) {
|
if (activeCall.callMode === CallMode.Direct) {
|
||||||
const isOtherPersonPresenting = activeCall.remoteParticipants.some(
|
const isOtherPersonPresenting = activeCall.remoteParticipants.some(
|
||||||
|
@ -64,30 +40,21 @@ export function useScreenSharingStoppedToast({
|
||||||
activeCall,
|
activeCall,
|
||||||
i18n,
|
i18n,
|
||||||
}: PropsType): void {
|
}: PropsType): void {
|
||||||
const [previousPresenter, setPreviousPresenter] = useState<
|
const { showToast, hideToast } = useCallingToasts();
|
||||||
undefined | { id: string | typeof ME; title?: string }
|
|
||||||
>(undefined);
|
const SOMEONE_STOPPED_PRESENTING_TOAST_KEY = 'someone_stopped_presenting';
|
||||||
const { showToast } = useCallingToasts();
|
|
||||||
|
const currentPresenter = useMemo(
|
||||||
|
() => getCurrentPresenter(activeCall),
|
||||||
|
[activeCall]
|
||||||
|
);
|
||||||
|
const previousPresenter = usePrevious(currentPresenter, currentPresenter);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentPresenter = getCurrentPresenter(activeCall);
|
if (previousPresenter && !currentPresenter) {
|
||||||
if (currentPresenter === ME) {
|
hideToast(SOMEONE_STOPPED_PRESENTING_TOAST_KEY);
|
||||||
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) {
|
|
||||||
showToast({
|
showToast({
|
||||||
|
key: SOMEONE_STOPPED_PRESENTING_TOAST_KEY,
|
||||||
content:
|
content:
|
||||||
previousPresenter.id === ME
|
previousPresenter.id === ME
|
||||||
? i18n('icu:calling__presenting--you-stopped')
|
? i18n('icu:calling__presenting--you-stopped')
|
||||||
|
@ -97,7 +64,14 @@ export function useScreenSharingStoppedToast({
|
||||||
autoClose: true,
|
autoClose: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [activeCall, previousPresenter, showToast, i18n]);
|
}, [
|
||||||
|
activeCall,
|
||||||
|
hideToast,
|
||||||
|
currentPresenter,
|
||||||
|
previousPresenter,
|
||||||
|
showToast,
|
||||||
|
i18n,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useMutedToast({
|
function useMutedToast({
|
||||||
|
@ -175,7 +149,7 @@ export function CallingButtonToastsContainer(
|
||||||
return (
|
return (
|
||||||
<CallingToastProvider
|
<CallingToastProvider
|
||||||
i18n={props.i18n}
|
i18n={props.i18n}
|
||||||
maxToasts={1}
|
maxNonPersistentToasts={1}
|
||||||
region={toastRegionRef}
|
region={toastRegionRef}
|
||||||
>
|
>
|
||||||
<div className="CallingButtonToasts" ref={toastRegionRef} />
|
<div className="CallingButtonToasts" ref={toastRegionRef} />
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
import { isEqual, remove, toggle } from '../../util/setUtil';
|
import { difference, isEqual, remove, toggle } from '../../util/setUtil';
|
||||||
|
|
||||||
describe('set utilities', () => {
|
describe('set utilities', () => {
|
||||||
const original = new Set([1, 2, 3]);
|
const original = new Set([1, 2, 3]);
|
||||||
|
@ -82,4 +82,21 @@ describe('set utilities', () => {
|
||||||
assert.deepStrictEqual(original, new Set([1, 2, 3]));
|
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",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2023-10-26T13:57:41.860Z"
|
"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",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/CallsList.tsx",
|
"path": "ts/components/CallsList.tsx",
|
||||||
|
|
|
@ -27,3 +27,14 @@ export const isEqual = (
|
||||||
a: Readonly<Set<unknown>>,
|
a: Readonly<Set<unknown>>,
|
||||||
b: Readonly<Set<unknown>>
|
b: Readonly<Set<unknown>>
|
||||||
): boolean => a === b || (a.size === b.size && every(a, item => b.has(item)));
|
): 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…
Add table
Add a link
Reference in a new issue