290 lines
7.9 KiB
TypeScript
290 lines
7.9 KiB
TypeScript
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
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 { CallingToastProvider, useCallingToasts } from './CallingToast';
|
|
import { usePrevious } from '../hooks/usePrevious';
|
|
import { difference as setDifference } from '../util/setUtil';
|
|
import { isMoreRecentThan } from '../util/timestamp';
|
|
|
|
type PropsType = {
|
|
activeCall: ActiveCallType;
|
|
i18n: LocalizerType;
|
|
};
|
|
|
|
const ME = Symbol('me');
|
|
|
|
function getCurrentPresenter(
|
|
activeCall: Readonly<ActiveCallType>
|
|
): ConversationType | { id: typeof ME } | undefined {
|
|
if (activeCall.presentingSource) {
|
|
return { id: ME };
|
|
}
|
|
if (activeCall.callMode === CallMode.Direct) {
|
|
const isOtherPersonPresenting = activeCall.remoteParticipants.some(
|
|
participant => participant.presenting
|
|
);
|
|
return isOtherPersonPresenting ? activeCall.conversation : undefined;
|
|
}
|
|
if (activeCall.callMode === CallMode.Group) {
|
|
return activeCall.remoteParticipants.find(
|
|
participant => participant.presenting
|
|
);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function useScreenSharingStoppedToast({
|
|
activeCall,
|
|
i18n,
|
|
}: PropsType): void {
|
|
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(() => {
|
|
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')
|
|
: i18n('icu:calling__presenting--person-stopped', {
|
|
name: previousPresenter.title,
|
|
}),
|
|
autoClose: true,
|
|
});
|
|
}
|
|
}, [
|
|
activeCall,
|
|
hideToast,
|
|
currentPresenter,
|
|
previousPresenter,
|
|
showToast,
|
|
i18n,
|
|
]);
|
|
}
|
|
|
|
function useMutedToast({
|
|
hasLocalAudio,
|
|
i18n,
|
|
}: {
|
|
hasLocalAudio: boolean;
|
|
i18n: LocalizerType;
|
|
}): void {
|
|
const previousHasLocalAudio = usePrevious(hasLocalAudio, hasLocalAudio);
|
|
const { showToast, hideToast } = useCallingToasts();
|
|
const MUTED_TOAST_KEY = 'muted';
|
|
|
|
useEffect(() => {
|
|
if (
|
|
previousHasLocalAudio !== undefined &&
|
|
hasLocalAudio !== previousHasLocalAudio
|
|
) {
|
|
hideToast(MUTED_TOAST_KEY);
|
|
showToast({
|
|
key: MUTED_TOAST_KEY,
|
|
content: hasLocalAudio
|
|
? i18n('icu:CallControls__MutedToast--unmuted')
|
|
: i18n('icu:CallControls__MutedToast--muted'),
|
|
autoClose: true,
|
|
dismissable: true,
|
|
});
|
|
}
|
|
}, [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]);
|
|
}
|
|
|
|
function useRaisedHandsToast({
|
|
raisedHands,
|
|
renderRaisedHandsToast,
|
|
}: {
|
|
raisedHands?: Set<number>;
|
|
renderRaisedHandsToast?: (
|
|
hands: Array<number>
|
|
) => JSX.Element | string | undefined;
|
|
}): void {
|
|
const RAISED_HANDS_TOAST_KEY = 'raised-hands';
|
|
const RAISED_HANDS_TOAST_LIFETIME = 4000;
|
|
const LOAD_DELAY = 2000;
|
|
|
|
const { showToast, hideToast } = useCallingToasts();
|
|
|
|
// Hand state is updated after a delay upon joining a call, so it can appear that
|
|
// hands were raised immediately when you join a call. To avoid spurious toasts, add
|
|
// an initial delay before showing toasts.
|
|
const [isLoaded, setIsLoaded] = React.useState<boolean>(false);
|
|
React.useEffect(() => {
|
|
const timeout = setTimeout(() => {
|
|
setIsLoaded(true);
|
|
}, LOAD_DELAY);
|
|
return () => clearTimeout(timeout);
|
|
}, []);
|
|
|
|
const previousRaisedHands = usePrevious(raisedHands, raisedHands);
|
|
const [newHands, loweredHands]: [Set<number>, Set<number>] = isLoaded
|
|
? [
|
|
setDifference(
|
|
raisedHands ?? new Set(),
|
|
previousRaisedHands ?? new Set()
|
|
),
|
|
setDifference(
|
|
previousRaisedHands ?? new Set(),
|
|
raisedHands ?? new Set()
|
|
),
|
|
]
|
|
: [new Set(), new Set()];
|
|
|
|
const toastLastShownAt = useRef<number>(0);
|
|
const handsForLastShownToast = useRef<Set<number>>(new Set());
|
|
|
|
React.useEffect(() => {
|
|
if (raisedHands?.size === 0) {
|
|
hideToast(RAISED_HANDS_TOAST_KEY);
|
|
}
|
|
|
|
if (
|
|
(newHands.size === 0 && loweredHands.size === 0) ||
|
|
!renderRaisedHandsToast
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// If there's an existing raised hand toast (it hasn't faded out yet), then
|
|
// group the newly raised and lowered hands into the existing toast.
|
|
let handsForToast: Array<number>;
|
|
if (
|
|
isMoreRecentThan(toastLastShownAt.current, RAISED_HANDS_TOAST_LIFETIME)
|
|
) {
|
|
handsForToast = [
|
|
...setDifference(handsForLastShownToast.current, loweredHands),
|
|
...newHands,
|
|
];
|
|
|
|
// If someone lowered a hand which isn't present in the existing toast,
|
|
// we can ignore it.
|
|
if (
|
|
newHands.size === 0 &&
|
|
loweredHands.size > 0 &&
|
|
handsForToast.length &&
|
|
handsForToast.length === handsForLastShownToast.current.size
|
|
) {
|
|
return;
|
|
}
|
|
} else {
|
|
handsForToast = [...newHands];
|
|
}
|
|
handsForLastShownToast.current = new Set([...handsForToast]);
|
|
|
|
hideToast(RAISED_HANDS_TOAST_KEY);
|
|
|
|
const content = renderRaisedHandsToast(handsForToast.reverse());
|
|
if (!content) {
|
|
return;
|
|
}
|
|
|
|
// Note: Don't set { dismissable: true } or else the links (Lower or View Queue)
|
|
// will cause nested buttons (dismissable toasts are <button>s)
|
|
showToast({
|
|
key: RAISED_HANDS_TOAST_KEY,
|
|
content,
|
|
autoClose: true,
|
|
lifetime: RAISED_HANDS_TOAST_LIFETIME,
|
|
});
|
|
toastLastShownAt.current = Date.now();
|
|
}, [
|
|
raisedHands,
|
|
previousRaisedHands,
|
|
newHands,
|
|
handsForLastShownToast,
|
|
loweredHands,
|
|
renderRaisedHandsToast,
|
|
hideToast,
|
|
showToast,
|
|
]);
|
|
}
|
|
|
|
type CallingButtonToastsType = {
|
|
hasLocalAudio: boolean;
|
|
outgoingRing: boolean | undefined;
|
|
raisedHands?: Set<number>;
|
|
renderRaisedHandsToast?: (
|
|
hands: Array<number>
|
|
) => JSX.Element | string | undefined;
|
|
i18n: LocalizerType;
|
|
};
|
|
|
|
export function CallingButtonToastsContainer(
|
|
props: CallingButtonToastsType
|
|
): JSX.Element {
|
|
const toastRegionRef = useRef<HTMLDivElement>(null);
|
|
return (
|
|
<CallingToastProvider
|
|
i18n={props.i18n}
|
|
maxNonPersistentToasts={1}
|
|
region={toastRegionRef}
|
|
>
|
|
<div className="CallingButtonToasts__outer">
|
|
<div className="CallingButtonToasts" ref={toastRegionRef} />
|
|
<CallingButtonToasts {...props} />
|
|
</div>
|
|
</CallingToastProvider>
|
|
);
|
|
}
|
|
|
|
function CallingButtonToasts({
|
|
hasLocalAudio,
|
|
outgoingRing,
|
|
raisedHands,
|
|
renderRaisedHandsToast,
|
|
i18n,
|
|
}: CallingButtonToastsType) {
|
|
useMutedToast({ hasLocalAudio, i18n });
|
|
useOutgoingRingToast({ outgoingRing, i18n });
|
|
useRaisedHandsToast({ raisedHands, renderRaisedHandsToast });
|
|
|
|
return null;
|
|
}
|