signal-desktop/ts/components/CallingToastManager.tsx
2021-05-20 17:37:19 -07:00

163 lines
4.3 KiB
TypeScript

// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import {
ActiveCallType,
CallMode,
GroupCallConnectionState,
} from '../types/Calling';
import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util';
type PropsType = {
activeCall: ActiveCallType;
i18n: LocalizerType;
};
type ToastType =
| {
message: string;
type: 'dismissable' | 'static';
}
| undefined;
function getReconnectingToast({ activeCall, i18n }: PropsType): ToastType {
if (
activeCall.callMode === CallMode.Group &&
activeCall.connectionState === GroupCallConnectionState.Reconnecting
) {
return {
message: i18n('callReconnecting'),
type: 'static',
};
}
return undefined;
}
const ME = Symbol('me');
function getCurrentPresenter(
activeCall: Readonly<ActiveCallType>
): ConversationType | typeof ME | undefined {
if (activeCall.presentingSource) {
return 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;
}
function useScreenSharingToast({ activeCall, i18n }: PropsType): ToastType {
const [result, setResult] = useState<undefined | ToastType>(undefined);
const [previousPresenter, setPreviousPresenter] = useState<
undefined | { id: string | typeof ME; title?: string }
>(undefined);
const previousPresenterId = previousPresenter?.id;
const previousPresenterTitle = previousPresenter?.title;
useEffect(() => {
const currentPresenter = getCurrentPresenter(activeCall);
if (!currentPresenter && previousPresenterId) {
if (previousPresenterId === ME) {
setResult({
type: 'dismissable',
message: i18n('calling__presenting--you-stopped'),
});
} else if (previousPresenterTitle) {
setResult({
type: 'dismissable',
message: i18n('calling__presenting--person-stopped', [
previousPresenterTitle,
]),
});
}
}
}, [activeCall, i18n, previousPresenterId, previousPresenterTitle]);
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]);
return result;
}
const DEFAULT_DELAY = 5000;
// In the future, this component should show toasts when users join or leave. See
// DESKTOP-902.
export const CallingToastManager: React.FC<PropsType> = props => {
const reconnectingToast = getReconnectingToast(props);
const screenSharingToast = useScreenSharingToast(props);
let toast: ToastType;
if (reconnectingToast) {
toast = reconnectingToast;
} else if (screenSharingToast) {
toast = screenSharingToast;
}
const [toastMessage, setToastMessage] = useState('');
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const dismissToast = useCallback(() => {
if (timeoutRef) {
setToastMessage('');
}
}, [setToastMessage, timeoutRef]);
useEffect(() => {
if (toast) {
if (toast.type === 'dismissable') {
if (timeoutRef && timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(dismissToast, DEFAULT_DELAY);
}
setToastMessage(toast.message);
}
return () => {
if (timeoutRef && timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [dismissToast, setToastMessage, timeoutRef, toast]);
const isVisible = Boolean(toastMessage);
return (
<button
className={classNames('module-ongoing-call__toast', {
'module-ongoing-call__toast--hidden': !isVisible,
})}
type="button"
onClick={dismissToast}
>
{toastMessage}
</button>
);
};