411 lines
12 KiB
TypeScript

import {
GroupCallConnectionState, GroupCallParticipant as TypeGroupCallParticipant,
IS_SCREENSHARE_SUPPORTED, switchCameraInput, toggleSpeaker,
} from '../../../lib/secret-sauce';
import React, {
FC, memo, useCallback, useEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../modules';
import '../../../modules/actions/calls';
import { IAnchorPosition } from '../../../types';
import {
IS_ANDROID,
IS_IOS,
IS_REQUEST_FULLSCREEN_SUPPORTED,
IS_SINGLE_COLUMN_LAYOUT,
} from '../../../util/environment';
import buildClassName from '../../../util/buildClassName';
import {
selectGroupCall,
selectGroupCallParticipant,
selectIsAdminInActiveGroupCall,
} from '../../../modules/selectors/calls';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import Loading from '../../ui/Loading';
import Button from '../../ui/Button';
import DropdownMenu from '../../ui/DropdownMenu';
import MenuItem from '../../ui/MenuItem';
import Modal from '../../ui/Modal';
import MicrophoneButton from './MicrophoneButton';
import AnimatedIcon from '../../common/AnimatedIcon';
import Checkbox from '../../ui/Checkbox';
import GroupCallParticipantMenu from './GroupCallParticipantMenu';
import GroupCallParticipantList from './GroupCallParticipantList';
import GroupCallParticipantStreams from './GroupCallParticipantStreams';
import './GroupCall.scss';
const CAMERA_FLIP_PLAY_SEGMENT: [number, number] = [0, 10];
const PARTICIPANT_HEIGHT = 60;
export type OwnProps = {
groupCallId: string;
};
type StateProps = {
isGroupCallPanelHidden: boolean;
connectionState: GroupCallConnectionState;
title?: string;
meParticipant?: TypeGroupCallParticipant;
participantsCount?: number;
isSpeakerEnabled?: boolean;
isAdmin: boolean;
participants: Record<string, TypeGroupCallParticipant>;
};
const GroupCall: FC<OwnProps & StateProps> = ({
groupCallId,
isGroupCallPanelHidden,
connectionState,
isSpeakerEnabled,
title,
meParticipant,
isAdmin,
participants,
}) => {
const {
toggleGroupCallVideo,
toggleGroupCallPresentation,
leaveGroupCall,
toggleGroupCallPanel,
connectToActiveGroupCall,
playGroupCallSound,
} = getActions();
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const [isLeaving, setIsLeaving] = useState(false);
const [isFullscreen, openFullscreen, closeFullscreen] = useFlag();
const [isSidebarOpen, openSidebar, closeSidebar] = useFlag(true);
const hasVideoParticipants = participants && Object.values(participants).some((l) => l.video || l.presentation);
const isLandscape = isFullscreen && !IS_SINGLE_COLUMN_LAYOUT && hasVideoParticipants;
const [participantMenu, setParticipantMenu] = useState<{
participant: TypeGroupCallParticipant;
anchor: IAnchorPosition;
} | undefined>();
const [isParticipantMenuOpen, openParticipantMenu, closeParticipantMenu] = useFlag();
const [isConfirmLeaveModalOpen, openConfirmLeaveModal, closeConfirmLeaveModal] = useFlag();
const [isEndGroupCallModal, setIsEndGroupCallModal] = useState(false);
const [shouldEndGroupCall, setShouldEndGroupCall] = useState(false);
const hasVideo = meParticipant?.hasVideoStream;
const hasPresentation = meParticipant?.hasPresentationStream;
const isConnecting = connectionState !== 'connected';
const canSelfUnmute = meParticipant?.canSelfUnmute;
const shouldRaiseHand = !canSelfUnmute && meParticipant?.isMuted;
const handleOpenParticipantMenu = useCallback((anchor: HTMLDivElement, participant: TypeGroupCallParticipant) => {
const rect = anchor.getBoundingClientRect();
const container = containerRef.current!;
setParticipantMenu({
anchor: { x: rect.left, y: rect.top - container.offsetTop + PARTICIPANT_HEIGHT },
participant,
});
openParticipantMenu();
}, [openParticipantMenu]);
useEffect(() => {
if (connectionState === 'connected') {
playGroupCallSound({ sound: 'join' });
} else if (connectionState === 'reconnecting') {
playGroupCallSound({ sound: 'connecting' });
}
}, [connectionState, playGroupCallSound]);
const handleCloseConfirmLeaveModal = () => {
closeConfirmLeaveModal();
setIsEndGroupCallModal(false);
};
const MainButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
return ({ onTrigger, isOpen }) => (
<Button
round
size="smaller"
color="translucent"
className={isOpen ? 'active' : undefined}
onClick={onTrigger}
ariaLabel={lang('AccDescrMoreOptions')}
>
<i className="icon-more" />
</Button>
);
}, [lang]);
const handleToggleFullscreen = useCallback(() => {
if (!containerRef.current) return;
if (isFullscreen) {
document.exitFullscreen().then(closeFullscreen);
} else {
containerRef.current.requestFullscreen().then(openFullscreen);
}
}, [closeFullscreen, isFullscreen, openFullscreen]);
const handleToggleSidebar = () => {
if (isSidebarOpen) {
closeSidebar();
} else {
openSidebar();
}
};
const handleStreamsDoubleClick = useCallback(() => {
if (!IS_REQUEST_FULLSCREEN_SUPPORTED) return;
if (!isFullscreen) {
closeSidebar();
handleToggleFullscreen();
} else {
handleToggleFullscreen();
}
}, [closeSidebar, handleToggleFullscreen, isFullscreen]);
const toggleFullscreen = useCallback(() => {
if (isFullscreen) {
closeFullscreen();
} else {
openFullscreen();
}
}, [closeFullscreen, isFullscreen, openFullscreen]);
const handleClose = () => {
toggleGroupCallPanel();
if (isFullscreen) {
closeFullscreen();
}
};
useEffect(() => {
if (!IS_REQUEST_FULLSCREEN_SUPPORTED) return undefined;
const container = containerRef.current;
if (!container) return undefined;
container.addEventListener('fullscreenchange', toggleFullscreen);
return () => {
container.removeEventListener('fullscreenchange', toggleFullscreen);
};
}, [toggleFullscreen]);
const handleClickVideoOrSpeaker = () => {
if (shouldRaiseHand) {
toggleSpeaker();
} else {
toggleGroupCallVideo();
}
};
useEffect(() => {
connectToActiveGroupCall();
}, [connectToActiveGroupCall, groupCallId]);
const endGroupCall = () => {
setIsEndGroupCallModal(true);
setShouldEndGroupCall(true);
openConfirmLeaveModal();
if (isFullscreen) {
handleToggleFullscreen();
}
};
const handleLeaveGroupCall = () => {
if (isAdmin && !isConfirmLeaveModalOpen) {
openConfirmLeaveModal();
if (isFullscreen) {
handleToggleFullscreen();
}
return;
}
playGroupCallSound({ sound: 'leave' });
setIsLeaving(true);
closeConfirmLeaveModal();
};
const handleCloseAnimationEnd = () => {
if (isLeaving) {
leaveGroupCall({
shouldDiscard: shouldEndGroupCall,
});
}
};
return (
<Modal
isOpen={!isGroupCallPanelHidden && !isLeaving}
onClose={toggleGroupCallPanel}
className={buildClassName(
'GroupCall',
IS_SINGLE_COLUMN_LAYOUT && 'single-column',
isLandscape && 'landscape',
!isSidebarOpen && 'no-sidebar',
)}
dialogRef={containerRef}
onCloseAnimationEnd={handleCloseAnimationEnd}
>
<div className="header">
<h3>{title || lang('VoipGroupVoiceChat')}</h3>
{IS_REQUEST_FULLSCREEN_SUPPORTED && (
<Button
round
size="smaller"
color="translucent"
onClick={handleToggleFullscreen}
ariaLabel={lang(isFullscreen ? 'AccExitFullscreen' : 'AccSwitchToFullscreen')}
>
<i className={isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen'} />
</Button>
)}
{isLandscape && (
<Button
round
size="smaller"
color="translucent"
onClick={handleToggleSidebar}
>
<i className="icon-sidebar" />
</Button>
)}
{((IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand) || isAdmin) && (
<DropdownMenu
positionX="right"
trigger={MainButton}
>
{IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand && (
<MenuItem
icon="share-screen"
onClick={toggleGroupCallPresentation}
>
{lang(hasPresentation ? 'VoipChatStopScreenCapture' : 'VoipChatStartScreenCapture')}
</MenuItem>
)}
{isAdmin && (
<MenuItem
icon="phone-discard-outline"
onClick={endGroupCall}
destructive
>
{lang('VoipGroupLeaveAlertEndChat')}
</MenuItem>
)}
</DropdownMenu>
)}
<Button
round
size="smaller"
color="translucent"
onClick={handleClose}
>
<i className="icon-close" />
</Button>
</div>
<div className="scrollable custom-scroll">
<GroupCallParticipantStreams onDoubleClick={handleStreamsDoubleClick} />
{(!isLandscape || isSidebarOpen)
&& <GroupCallParticipantList openParticipantMenu={handleOpenParticipantMenu} />}
</div>
<GroupCallParticipantMenu
participant={participantMenu?.participant}
anchor={participantMenu?.anchor}
isDropdownOpen={isParticipantMenuOpen}
closeDropdown={closeParticipantMenu}
/>
<div className="buttons">
{isConnecting && <Loading />}
<div className="button-wrapper">
<div className="video-buttons">
{hasVideo && (IS_ANDROID || IS_IOS) && (
<button className="smaller-button" onClick={switchCameraInput}>
<AnimatedIcon name="CameraFlip" playSegment={CAMERA_FLIP_PLAY_SEGMENT} size={24} />
</button>
)}
<button
className={buildClassName(
'small-button',
shouldRaiseHand ? 'speaker' : 'camera',
(hasVideo || (shouldRaiseHand && isSpeakerEnabled)) && 'active',
)}
onClick={handleClickVideoOrSpeaker}
>
<i className={shouldRaiseHand ? 'icon-speaker' : (hasVideo ? 'icon-video-stop' : 'icon-video')} />
</button>
</div>
<div className="button-text">
{lang(shouldRaiseHand ? 'VoipSpeaker' : 'VoipCamera')}
</div>
</div>
<MicrophoneButton />
<div className="button-wrapper">
<button className="small-button leave" onClick={handleLeaveGroupCall}>
<i className="icon-phone-discard" />
</button>
<div className="button-text">
{lang('VoipGroupLeave')}
</div>
</div>
</div>
<Modal
isOpen={isConfirmLeaveModalOpen}
onClose={handleCloseConfirmLeaveModal}
className="error"
title={lang(isEndGroupCallModal ? 'VoipGroupEndAlertTitle' : 'VoipGroupLeaveAlertTitle')}
>
<p>{lang(isEndGroupCallModal ? 'VoipGroupEndAlertText' : 'VoipGroupLeaveAlertText')}</p>
{!isEndGroupCallModal && (
<Checkbox
label={lang('VoipGroupEndChat')}
checked={shouldEndGroupCall}
onCheck={setShouldEndGroupCall}
/>
)}
<Button isText className="confirm-dialog-button" onClick={handleLeaveGroupCall}>
{lang(isEndGroupCallModal ? 'VoipGroupEnd' : 'VoipGroupLeave')}
</Button>
<Button isText className="confirm-dialog-button" onClick={handleCloseConfirmLeaveModal}>
{lang('Cancel')}
</Button>
</Modal>
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global, { groupCallId }): StateProps => {
const {
connectionState, title, isSpeakerDisabled, participants, participantsCount,
} = selectGroupCall(global, groupCallId)! || {};
return {
connectionState,
title,
isSpeakerEnabled: !isSpeakerDisabled,
participantsCount,
meParticipant: selectGroupCallParticipant(global, groupCallId, global.currentUserId!),
isGroupCallPanelHidden: Boolean(global.groupCalls.isGroupCallPanelHidden),
isAdmin: selectIsAdminInActiveGroupCall(global),
participants,
};
},
)(GroupCall));