import type { GroupCallParticipant } from '../../../lib/secret-sauce'; import type { FC } from '../../../lib/teact/teact'; import React, { memo, useEffect, useState, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config'; import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets'; import buildClassName from '../../../util/buildClassName'; import useRunThrottled from '../../../hooks/useRunThrottled'; import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import { selectIsAdminInActiveGroupCall } from '../../../global/selectors/calls'; import Menu from '../../ui/Menu'; import MenuItem from '../../ui/MenuItem'; import AnimatedIcon from '../../common/AnimatedIcon'; import DeleteMemberModal from '../../right/DeleteMemberModal'; import './GroupCallParticipantMenu.scss'; const SPEAKER_ICON_DISABLED_SEGMENT: [number, number] = [0, 17]; const SPEAKER_ICON_ENABLED_SEGMENT: [number, number] = [17, 34]; type OwnProps = { participant?: GroupCallParticipant; onCloseAnimationEnd: VoidFunction; onClose: VoidFunction; isDropdownOpen: boolean; positionX?: 'left' | 'right'; positionY?: 'top' | 'bottom'; transformOriginX?: number; transformOriginY?: number; style?: string; menuRef?: React.RefObject; }; type StateProps = { isAdmin: boolean; }; const VOLUME_ZERO = 0; const VOLUME_LOW = 50; const VOLUME_MEDIUM = 100; const VOLUME_NORMAL = 150; const VOLUME_CHANGE_THROTTLE = 500; const SPEAKER_ICON_SIZE = 24; const GroupCallParticipantMenu: FC = ({ participant, onCloseAnimationEnd, onClose, isDropdownOpen, isAdmin, positionY, menuRef, positionX, style, transformOriginY, transformOriginX, }) => { const { toggleGroupCallMute, setGroupCallParticipantVolume, toggleGroupCallPanel, openChat, requestToSpeak, } = getActions(); const lang = useLang(); const [isDeleteUserModalOpen, openDeleteUserModal, closeDeleteUserModal] = useFlag(); const id = participant?.id; const { isMutedByMe, isMuted, isSelf, canSelfUnmute, } = participant || {}; const isRaiseHand = Boolean(participant?.raiseHandRating); const shouldRaiseHand = !canSelfUnmute && isMuted; const [localVolume, setLocalVolume] = useState( isMutedByMe ? VOLUME_ZERO : ((participant?.volume || GROUP_CALL_DEFAULT_VOLUME) / GROUP_CALL_VOLUME_MULTIPLIER), ); const [shouldPlay, setShouldPlay] = useState(false); const isLocalVolumeZero = localVolume === VOLUME_ZERO; const speakerIconPlaySegment = isLocalVolumeZero ? SPEAKER_ICON_DISABLED_SEGMENT : SPEAKER_ICON_ENABLED_SEGMENT; useEffect(() => { if (isDropdownOpen) return; setShouldPlay(false); }, [isDropdownOpen]); const handleSetLocalVolume = useLastCallback((volume: number) => { setLocalVolume(volume); const isNewLocalVolumeZero = volume === VOLUME_ZERO; setShouldPlay(isNewLocalVolumeZero !== isLocalVolumeZero); }); useEffect(() => { setLocalVolume(isMutedByMe ? VOLUME_ZERO : ((participant?.volume || GROUP_CALL_DEFAULT_VOLUME) / GROUP_CALL_VOLUME_MULTIPLIER)); // We only want to initialize local volume when switching participants and ignore following updates from server // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps }, [id]); const runThrottled = useRunThrottled(VOLUME_CHANGE_THROTTLE); const handleRemove = useLastCallback((e: React.SyntheticEvent) => { e.stopPropagation(); openDeleteUserModal(); onClose(); }); const handleCancelRequestToSpeak = useLastCallback((e: React.SyntheticEvent) => { e.stopPropagation(); requestToSpeak({ value: false, }); onClose(); }); const handleMute = useLastCallback((e: React.SyntheticEvent) => { e.stopPropagation(); onClose(); if (!isAdmin) { handleSetLocalVolume(isMutedByMe ? GROUP_CALL_DEFAULT_VOLUME / GROUP_CALL_VOLUME_MULTIPLIER : VOLUME_ZERO); } else if (shouldRaiseHand) { handleSetLocalVolume((participant?.volume ?? GROUP_CALL_DEFAULT_VOLUME) / GROUP_CALL_VOLUME_MULTIPLIER); } toggleGroupCallMute({ participantId: id!, value: isAdmin ? !shouldRaiseHand : !isMutedByMe, }); }); const handleOpenProfile = useLastCallback((e: React.SyntheticEvent) => { e.stopPropagation(); toggleGroupCallPanel(); openChat({ id, }); onClose(); }); const handleChangeVolume = (e: React.ChangeEvent) => { const value = Number(e.target.value); handleSetLocalVolume(value); runThrottled(() => { if (value === VOLUME_ZERO) { toggleGroupCallMute({ participantId: id!, value: true, }); } else { setGroupCallParticipantVolume({ participantId: id!, volume: Math.floor(value * GROUP_CALL_VOLUME_MULTIPLIER), }); } }); }; return (
{!isSelf && !shouldRaiseHand && (
= VOLUME_LOW && localVolume < VOLUME_MEDIUM && 'medium', localVolume >= VOLUME_MEDIUM && localVolume < VOLUME_NORMAL && 'normal', localVolume >= VOLUME_NORMAL && 'high', )} >
{localVolume}%
)}
{(isRaiseHand && isSelf) && ( {lang('VoipGroupCancelRaiseHand')} )} {!isSelf && {lang('VoipGroupOpenProfile')}} {!isSelf && ( // TODO cross mic {isAdmin ? lang(shouldRaiseHand ? 'VoipGroupAllowToSpeak' : 'VoipMute') : lang(isMutedByMe ? 'VoipGroupUnmuteForMe' : 'VoipGroupMuteForMe')} )} {!isSelf && isAdmin && ( // TODO replace with hand {lang('VoipGroupUserRemove')} )}
{!isSelf && isAdmin && ( )}
); }; export default memo(withGlobal( (global): StateProps => { return { isAdmin: selectIsAdminInActiveGroupCall(global), }; }, )(GroupCallParticipantMenu));