// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useRef, useState, useEffect, FC, ReactElement } from 'react'; import classNames from 'classnames'; import { LocalizerType } from '../types/Util'; import { GroupCallRemoteParticipantType, VideoFrameSource, } 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 = 140; type PropsType = { getFrameBuffer: () => ArrayBuffer; getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource; i18n: LocalizerType; overflowedParticipants: ReadonlyArray; }; export const GroupCallOverflowArea: FC = ({ getFrameBuffer, getGroupCallVideoFrameSource, i18n, overflowedParticipants, }) => { 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 (
); }