// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReactElement } from 'react'; import React, { useRef, useState, useEffect } from 'react'; import classNames from 'classnames'; import type { VideoFrameSource } from '@signalapp/ringrtc'; import type { LocalizerType } from '../types/Util'; import type { GroupCallRemoteParticipantType } from '../types/Calling'; import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant'; const OVERFLOW_SCROLLED_TO_EDGE_THRESHOLD = 20; const OVERFLOW_SCROLL_BUTTON_RATIO = 0.75; // This should be an integer, as sub-pixel widths can cause performance issues. export const OVERFLOW_PARTICIPANT_WIDTH = 107; export type PropsType = { getFrameBuffer: () => Buffer; getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; i18n: LocalizerType; isCallReconnecting: boolean; onClickRaisedHand?: () => void; onParticipantVisibilityChanged: ( demuxId: number, isVisible: boolean ) => unknown; overflowedParticipants: ReadonlyArray; remoteAudioLevels: Map; remoteParticipantsCount: number; }; export function GroupCallOverflowArea({ getFrameBuffer, getGroupCallVideoFrameSource, i18n, isCallReconnecting, onClickRaisedHand, onParticipantVisibilityChanged, overflowedParticipants, remoteAudioLevels, remoteParticipantsCount, }: PropsType): JSX.Element | null { const overflowRef = useRef(null); const [overflowScrollTop, setOverflowScrollTop] = useState(0); // This assumes that these values will change along with re-renders. If that's not true, // we should add these values to the component's state. let visibleHeight: number; let scrollMax: number; if (overflowRef.current) { visibleHeight = overflowRef.current.clientHeight; scrollMax = overflowRef.current.scrollHeight - visibleHeight; } else { visibleHeight = 0; scrollMax = 0; } const hasOverflowedParticipants = Boolean(overflowedParticipants.length); useEffect(() => { // If there aren't any overflowed participants, we want to clear the scroll position // so we don't hold onto stale values. if (!hasOverflowedParticipants) { setOverflowScrollTop(0); } }, [hasOverflowedParticipants]); if (!hasOverflowedParticipants) { return null; } const isScrolledToTop = overflowScrollTop < OVERFLOW_SCROLLED_TO_EDGE_THRESHOLD; const isScrolledToBottom = overflowScrollTop > scrollMax - OVERFLOW_SCROLLED_TO_EDGE_THRESHOLD; return (
{ const el = overflowRef.current; if (!el) { return; } el.scrollTo({ top: Math.max( el.scrollTop - visibleHeight * OVERFLOW_SCROLL_BUTTON_RATIO, 0 ), left: 0, behavior: 'smooth', }); }} placement="top" />
{ // Ideally this would use `event.target.scrollTop`, but that does not seem to be // available, so we use the ref. const el = overflowRef.current; if (!el) { return; } setOverflowScrollTop(el.scrollTop); }} > {overflowedParticipants.map(remoteParticipant => ( ))}
{ const el = overflowRef.current; if (!el) { return; } el.scrollTo({ top: Math.min( el.scrollTop + visibleHeight * OVERFLOW_SCROLL_BUTTON_RATIO, scrollMax ), left: 0, behavior: 'smooth', }); }} placement="bottom" />
); } function OverflowAreaScrollMarker({ i18n, isHidden, onClick, placement, }: { i18n: LocalizerType; isHidden: boolean; onClick: () => void; placement: 'top' | 'bottom'; }): ReactElement { const baseClassName = 'module-ongoing-call__participants__overflow__scroll-marker'; return (
); }