// Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; import { animated, useSpring } from '@react-spring/web'; import { Avatar, AvatarSize } from './Avatar'; import { ContactName } from './conversation/ContactName'; import type { ConversationsByDemuxIdType } from '../types/Calling'; import type { ServiceIdString } from '../types/ServiceId'; import type { LocalizerType } from '../types/Util'; import type { ConversationType } from '../state/ducks/conversations'; import { ModalHost } from './ModalHost'; import { drop } from '../util/drop'; import * as log from '../logging/log'; import { usePrevious } from '../hooks/usePrevious'; export type PropsType = { readonly i18n: LocalizerType; readonly onClose: () => void; readonly onLowerMyHand: () => void; readonly localDemuxId: number | undefined; readonly conversationsByDemuxId: ConversationsByDemuxIdType; readonly raisedHands: Set; readonly localHandRaised: boolean; }; export function CallingRaisedHandsList({ i18n, onClose, onLowerMyHand, localDemuxId, conversationsByDemuxId, raisedHands, localHandRaised, }: PropsType): JSX.Element | null { const ourServiceId: ServiceIdString | undefined = localDemuxId ? conversationsByDemuxId.get(localDemuxId)?.serviceId : undefined; const participants = React.useMemo>(() => { const serviceIds: Set = new Set(); const conversations: Array = []; raisedHands.forEach(demuxId => { const conversation = conversationsByDemuxId.get(demuxId); if (!conversation) { log.warn( 'CallingRaisedHandsList: Failed to get conversationsByDemuxId for demuxId', { demuxId } ); return; } const { serviceId } = conversation; if (serviceId) { if (serviceIds.has(serviceId)) { return; } serviceIds.add(serviceId); } conversations.push(conversation); }); return conversations; }, [raisedHands, conversationsByDemuxId]); return (
{i18n('icu:CallingRaisedHandsList__Title', { count: participants.length, })} {participants.length > 1 ? ( {' '} {i18n('icu:CallingRaisedHandsList__TitleHint')} ) : null}
    {participants.map((participant: ConversationType, index: number) => (
  • {ourServiceId && participant.serviceId === ourServiceId ? ( {i18n('icu:you')} ) : ( )}
    {localHandRaised && ourServiceId && participant.serviceId === ourServiceId && ( )}
  • ))}
); } const BUTTON_OPACITY_SPRING_CONFIG = { mass: 1, tension: 210, friction: 20, precision: 0.01, clamp: true, } as const; const BUTTON_SCALE_SPRING_CONFIG = { mass: 1.5, tension: 230, friction: 8, precision: 0.02, velocity: 0.0025, } as const; export type CallingRaisedHandsListButtonPropsType = { i18n: LocalizerType; raisedHandsCount: number; syncedLocalHandRaised: boolean; onClick: () => void; }; export function CallingRaisedHandsListButton({ i18n, syncedLocalHandRaised, raisedHandsCount, onClick, }: CallingRaisedHandsListButtonPropsType): JSX.Element | null { const [isVisible, setIsVisible] = React.useState(raisedHandsCount > 0); const [opacitySpringProps, opacitySpringApi] = useSpring( { from: { opacity: 0 }, to: { opacity: 1 }, config: BUTTON_OPACITY_SPRING_CONFIG, }, [] ); const [scaleSpringProps, scaleSpringApi] = useSpring( { from: { scale: 0.9 }, to: { scale: 1 }, config: BUTTON_SCALE_SPRING_CONFIG, }, [] ); const prevRaisedHandsCount = usePrevious(raisedHandsCount, raisedHandsCount); const prevSyncedLocalHandRaised = usePrevious( syncedLocalHandRaised, syncedLocalHandRaised ); const onRestAfterAnimateOut = React.useCallback(() => { if (!raisedHandsCount) { setIsVisible(false); } }, [raisedHandsCount]); React.useEffect(() => { if (raisedHandsCount > prevRaisedHandsCount) { setIsVisible(true); opacitySpringApi.stop(); drop(Promise.all(opacitySpringApi.start({ opacity: 1 }))); scaleSpringApi.stop(); drop( Promise.all( scaleSpringApi.start({ from: { scale: 0.99 }, to: { scale: 1 }, config: { velocity: 0.0025 }, }) ) ); } else if (raisedHandsCount === 0) { opacitySpringApi.stop(); drop( Promise.all( opacitySpringApi.start({ to: { opacity: 0 }, onRest: () => onRestAfterAnimateOut, }) ) ); } }, [ raisedHandsCount, prevRaisedHandsCount, opacitySpringApi, scaleSpringApi, onRestAfterAnimateOut, setIsVisible, ]); if (!isVisible) { return null; } // When the last hands are lowered, maintain the last count while fading out to prevent // abrupt label changes. let shownSyncedLocalHandRaised: boolean = syncedLocalHandRaised; let shownRaisedHandsCount: number = raisedHandsCount; if (raisedHandsCount === 0 && prevRaisedHandsCount) { shownRaisedHandsCount = prevRaisedHandsCount; shownSyncedLocalHandRaised = prevSyncedLocalHandRaised; } return ( {shownSyncedLocalHandRaised ? ( <> {i18n('icu:you')} {shownRaisedHandsCount > 1 && ` + ${String(shownRaisedHandsCount - 1)}`} ) : ( shownRaisedHandsCount )} ); }