// Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only /* eslint-disable react/no-array-index-key */ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { noop } from 'lodash'; import classNames from 'classnames'; import { animated, useSpring } from '@react-spring/web'; import { Avatar, AvatarSize } from './Avatar'; import { ContactName } from './conversation/ContactName'; import { InContactsIcon } from './InContactsIcon'; import type { LocalizerType } from '../types/Util'; import type { ConversationType } from '../state/ducks/conversations'; import { isInSystemContacts } from '../util/isInSystemContacts'; import type { BatchUserActionPayloadType, PendingUserActionPayloadType, } from '../state/ducks/calling'; import { Button, ButtonVariant } from './Button'; import type { ServiceIdString } from '../types/ServiceId'; import { handleOutsideClick } from '../util/handleOutsideClick'; import { Theme } from '../util/theme'; import { ConfirmationDialog } from './ConfirmationDialog'; import { usePrevious } from '../hooks/usePrevious'; import { useReducedMotion } from '../hooks/useReducedMotion'; import { drop } from '../util/drop'; enum ConfirmDialogState { None = 'None', ApproveAll = 'ApproveAll', DenyAll = 'DenyAll', } export type PropsType = { readonly i18n: LocalizerType; readonly participants: Array; // For storybook readonly defaultIsExpanded?: boolean; readonly approveUser: (payload: PendingUserActionPayloadType) => void; readonly batchUserAction: (payload: BatchUserActionPayloadType) => void; readonly denyUser: (payload: PendingUserActionPayloadType) => void; readonly toggleCallLinkPendingParticipantModal: ( conversationId: string ) => void; }; export function CallingPendingParticipants({ defaultIsExpanded, i18n, participants, approveUser, batchUserAction, denyUser, toggleCallLinkPendingParticipantModal, }: PropsType): JSX.Element | null { const reducedMotion = useReducedMotion(); // eslint-disable-next-line react-hooks/exhaustive-deps const [opacitySpringProps, opacitySpringApi] = useSpring( { from: { opacity: 0 }, to: { opacity: 1 }, config: { clamp: true, friction: 22, tension: 360 }, immediate: reducedMotion, }, [] ); // We show the first pending participant. Save this participant, so if all requests // are resolved then we can keep showing the participant while fading out. const lastParticipantRef = React.useRef(); lastParticipantRef.current = participants[0] ?? lastParticipantRef.current; const participantCount = participants.length; const prevParticipantCount = usePrevious(participantCount, participantCount); const [isVisible, setIsVisible] = useState(participantCount > 0); const [isExpanded, setIsExpanded] = useState(defaultIsExpanded ?? false); const [confirmDialogState, setConfirmDialogState] = useState(ConfirmDialogState.None); const [serviceIdsStagedForAction, setServiceIdsStagedForAction] = useState< Array >([]); const expandedListRef = useRef(null); const handleHideAllRequests = useCallback(() => { setIsExpanded(false); }, [setIsExpanded]); // When opening the "Approve all" confirm dialog, save the current list of participants // to ensure we only approve users who the admin has checked. If additional people // request to join while the dialog is open, we don't auto approve those. const stageServiceIdsForAction = useCallback(() => { const serviceIds: Array = []; participants.forEach(participant => { if (participant.serviceId) { serviceIds.push(participant.serviceId); } }); setServiceIdsStagedForAction(serviceIds); }, [participants, setServiceIdsStagedForAction]); const hideConfirmDialog = useCallback(() => { setConfirmDialogState(ConfirmDialogState.None); setServiceIdsStagedForAction([]); }, [setConfirmDialogState]); const handleApprove = useCallback( (participant: ConversationType) => { const { serviceId } = participant; if (!serviceId) { return; } approveUser({ serviceId }); }, [approveUser] ); const handleDeny = useCallback( (participant: ConversationType) => { const { serviceId } = participant; if (!serviceId) { return; } denyUser({ serviceId }); }, [denyUser] ); const handleApproveAll = useCallback(() => { batchUserAction({ action: 'approve', serviceIds: serviceIdsStagedForAction, }); hideConfirmDialog(); }, [serviceIdsStagedForAction, batchUserAction, hideConfirmDialog]); const handleDenyAll = useCallback(() => { batchUserAction({ action: 'deny', serviceIds: serviceIdsStagedForAction, }); hideConfirmDialog(); }, [serviceIdsStagedForAction, batchUserAction, hideConfirmDialog]); const renderApprovalButtons = useCallback( (participant: ConversationType, isEnabled: boolean = true) => { if (participant.serviceId == null) { return null; } return ( <> ); }, [i18n, handleApprove, handleDeny] ); useEffect(() => { if (!isExpanded) { return noop; } return handleOutsideClick( () => { handleHideAllRequests(); return true; }, { containerElements: [expandedListRef], name: 'CallingPendingParticipantsList.expandedList', } ); }, [isExpanded, handleHideAllRequests]); useEffect(() => { if (participantCount > prevParticipantCount) { setIsVisible(true); opacitySpringApi.stop(); drop(Promise.all(opacitySpringApi.start({ opacity: 1 }))); } else if (participantCount === 0) { opacitySpringApi.stop(); drop( Promise.all( opacitySpringApi.start({ to: { opacity: 0 }, onRest: () => { if (!participantCount) { setIsVisible(false); } }, }) ) ); } }, [opacitySpringApi, participantCount, prevParticipantCount, setIsVisible]); if (!isVisible) { return null; } if (confirmDialogState === ConfirmDialogState.ApproveAll) { return ( {i18n('icu:CallingPendingParticipants__ConfirmDialogBody--ApproveAll', { count: serviceIdsStagedForAction.length, })} ); } if (confirmDialogState === ConfirmDialogState.DenyAll) { return ( {i18n('icu:CallingPendingParticipants__ConfirmDialogBody--DenyAll', { count: serviceIdsStagedForAction.length, })} ); } if (isExpanded) { return (
{i18n('icu:CallingPendingParticipants__RequestsToJoin', { count: participantCount, })}
    {participants.map((participant: ConversationType, index: number) => (
  • {renderApprovalButtons(participant)}
  • ))}
); } const participant = lastParticipantRef.current; if (!participant) { return null; } const isExpandable = participantCount > 1; return (
{renderApprovalButtons(participant, participantCount > 0)}
{isExpandable && (
)}
); }