376 lines
12 KiB
TypeScript
376 lines
12 KiB
TypeScript
import type { FC } from '../../../lib/teact/teact';
|
|
import React, {
|
|
memo, useCallback, useEffect, useMemo, useRef,
|
|
} from '../../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
import '../../../global/actions/calls';
|
|
|
|
import type { ApiPhoneCall, ApiUser } from '../../../api/types';
|
|
|
|
import {
|
|
IS_ANDROID,
|
|
IS_IOS,
|
|
IS_REQUEST_FULLSCREEN_SUPPORTED,
|
|
IS_SINGLE_COLUMN_LAYOUT,
|
|
} from '../../../util/environment';
|
|
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { selectPhoneCallUser } from '../../../global/selectors/calls';
|
|
import useLang from '../../../hooks/useLang';
|
|
import renderText from '../../common/helpers/renderText';
|
|
import useFlag from '../../../hooks/useFlag';
|
|
import { formatMediaDuration } from '../../../util/dateFormat';
|
|
import {
|
|
getStreams, IS_SCREENSHARE_SUPPORTED, switchCameraInputP2p, toggleStreamP2p,
|
|
} from '../../../lib/secret-sauce';
|
|
import useInterval from '../../../hooks/useInterval';
|
|
import useForceUpdate from '../../../hooks/useForceUpdate';
|
|
|
|
import Modal from '../../ui/Modal';
|
|
import Avatar from '../../common/Avatar';
|
|
import Button from '../../ui/Button';
|
|
import PhoneCallButton from './PhoneCallButton';
|
|
import AnimatedIcon from '../../common/AnimatedIcon';
|
|
|
|
import styles from './PhoneCall.module.scss';
|
|
|
|
type StateProps = {
|
|
user?: ApiUser;
|
|
phoneCall?: ApiPhoneCall;
|
|
isOutgoing: boolean;
|
|
isCallPanelVisible?: boolean;
|
|
};
|
|
|
|
const PhoneCall: FC<StateProps> = ({
|
|
user,
|
|
isOutgoing,
|
|
phoneCall,
|
|
isCallPanelVisible,
|
|
}) => {
|
|
const lang = useLang();
|
|
const {
|
|
hangUp, acceptCall, playGroupCallSound, toggleGroupCallPanel, connectToActivePhoneCall,
|
|
} = getActions();
|
|
// eslint-disable-next-line no-null/no-null
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [isFullscreen, openFullscreen, closeFullscreen] = useFlag();
|
|
|
|
const toggleFullscreen = useCallback(() => {
|
|
if (isFullscreen) {
|
|
closeFullscreen();
|
|
} else {
|
|
openFullscreen();
|
|
}
|
|
}, [closeFullscreen, isFullscreen, openFullscreen]);
|
|
|
|
const handleToggleFullscreen = useCallback(() => {
|
|
if (!containerRef.current) return;
|
|
|
|
if (isFullscreen) {
|
|
document.exitFullscreen().then(closeFullscreen);
|
|
} else {
|
|
containerRef.current.requestFullscreen().then(openFullscreen);
|
|
}
|
|
}, [closeFullscreen, isFullscreen, openFullscreen]);
|
|
|
|
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 handleClose = useCallback(() => {
|
|
toggleGroupCallPanel();
|
|
if (isFullscreen) {
|
|
closeFullscreen();
|
|
}
|
|
}, [closeFullscreen, isFullscreen, toggleGroupCallPanel]);
|
|
|
|
const isDiscarded = phoneCall?.state === 'discarded';
|
|
const isBusy = phoneCall?.reason === 'busy';
|
|
|
|
const isIncomingRequested = phoneCall?.state === 'requested' && !isOutgoing;
|
|
const isOutgoingRequested = (phoneCall?.state === 'requested' || phoneCall?.state === 'waiting') && isOutgoing;
|
|
const isActive = phoneCall?.state === 'active';
|
|
const isConnected = phoneCall?.isConnected;
|
|
|
|
const [isHangingUp, startHangingUp, stopHangingUp] = useFlag();
|
|
const handleHangUp = useCallback(() => {
|
|
startHangingUp();
|
|
hangUp();
|
|
}, [hangUp, startHangingUp]);
|
|
|
|
useEffect(() => {
|
|
if (isHangingUp) {
|
|
playGroupCallSound({ sound: 'end' });
|
|
} else if (isIncomingRequested) {
|
|
playGroupCallSound({ sound: 'incoming' });
|
|
} else if (isBusy) {
|
|
playGroupCallSound({ sound: 'busy' });
|
|
} else if (isDiscarded) {
|
|
playGroupCallSound({ sound: 'end' });
|
|
} else if (isOutgoingRequested) {
|
|
playGroupCallSound({ sound: 'ringing' });
|
|
} else if (isConnected) {
|
|
playGroupCallSound({ sound: 'connect' });
|
|
}
|
|
}, [isBusy, isDiscarded, isIncomingRequested, isOutgoingRequested, isConnected, playGroupCallSound, isHangingUp]);
|
|
|
|
useEffect(() => {
|
|
if (phoneCall?.id) {
|
|
stopHangingUp();
|
|
} else {
|
|
connectToActivePhoneCall();
|
|
}
|
|
}, [connectToActivePhoneCall, phoneCall?.id, stopHangingUp]);
|
|
|
|
const forceUpdate = useForceUpdate();
|
|
|
|
useInterval(() => {
|
|
forceUpdate();
|
|
}, isConnected ? 1000 : undefined);
|
|
|
|
const callStatus = useMemo(() => {
|
|
const state = phoneCall?.state;
|
|
if (isHangingUp) {
|
|
return lang('lng_call_status_hanging');
|
|
}
|
|
if (isBusy) return 'busy';
|
|
if (state === 'requesting') {
|
|
return lang('lng_call_status_requesting');
|
|
} else if (state === 'requested') {
|
|
return isOutgoing ? lang('lng_call_status_ringing') : lang('lng_call_status_incoming');
|
|
} else if (state === 'waiting') {
|
|
return lang('lng_call_status_waiting');
|
|
} else if (state === 'active' && isConnected) {
|
|
return undefined;
|
|
} else {
|
|
return lang('lng_call_status_exchanging');
|
|
}
|
|
}, [isBusy, isConnected, isHangingUp, isOutgoing, lang, phoneCall?.state]);
|
|
|
|
const hasVideo = phoneCall?.videoState === 'active';
|
|
const hasPresentation = phoneCall?.screencastState === 'active';
|
|
|
|
const streams = getStreams();
|
|
const hasOwnAudio = streams?.ownAudio?.getTracks()[0].enabled;
|
|
const hasOwnPresentation = streams?.ownPresentation?.getTracks()[0].enabled;
|
|
const hasOwnVideo = streams?.ownVideo?.getTracks()[0].enabled;
|
|
|
|
const [isHidingPresentation, startHidingPresentation, stopHidingPresentation] = useFlag();
|
|
const [isHidingVideo, startHidingVideo, stopHidingVideo] = useFlag();
|
|
|
|
const handleTogglePresentation = useCallback(() => {
|
|
if (hasOwnPresentation) {
|
|
startHidingPresentation();
|
|
}
|
|
if (hasOwnVideo) {
|
|
startHidingVideo();
|
|
}
|
|
setTimeout(async () => {
|
|
await toggleStreamP2p('presentation');
|
|
stopHidingPresentation();
|
|
stopHidingVideo();
|
|
}, 250);
|
|
}, [
|
|
hasOwnPresentation, hasOwnVideo, startHidingPresentation, startHidingVideo, stopHidingPresentation, stopHidingVideo,
|
|
]);
|
|
|
|
const handleToggleVideo = useCallback(() => {
|
|
if (hasOwnVideo) {
|
|
startHidingVideo();
|
|
}
|
|
if (hasOwnPresentation) {
|
|
startHidingPresentation();
|
|
}
|
|
setTimeout(async () => {
|
|
await toggleStreamP2p('video');
|
|
stopHidingPresentation();
|
|
stopHidingVideo();
|
|
}, 250);
|
|
}, [
|
|
hasOwnPresentation, hasOwnVideo, startHidingPresentation, startHidingVideo, stopHidingPresentation, stopHidingVideo,
|
|
]);
|
|
|
|
const handleToggleAudio = useCallback(() => {
|
|
void toggleStreamP2p('audio');
|
|
}, []);
|
|
|
|
const [isEmojiOpen, openEmoji, closeEmoji] = useFlag();
|
|
|
|
const [isFlipping, startFlipping, stopFlipping] = useFlag();
|
|
|
|
const handleFlipCamera = useCallback(() => {
|
|
startFlipping();
|
|
switchCameraInputP2p();
|
|
setTimeout(stopFlipping, 250);
|
|
}, [startFlipping, stopFlipping]);
|
|
|
|
const timeElapsed = phoneCall?.startDate && (Number(new Date()) / 1000 - phoneCall.startDate);
|
|
|
|
useEffect(() => {
|
|
if (phoneCall?.state === 'discarded') {
|
|
setTimeout(hangUp, 250);
|
|
}
|
|
}, [hangUp, phoneCall?.reason, phoneCall?.state]);
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={phoneCall && phoneCall?.state !== 'discarded' && !isCallPanelVisible}
|
|
onClose={handleClose}
|
|
className={buildClassName(
|
|
styles.root,
|
|
IS_SINGLE_COLUMN_LAYOUT && styles.singleColumn,
|
|
)}
|
|
dialogRef={containerRef}
|
|
>
|
|
<Avatar
|
|
user={user}
|
|
size="jumbo"
|
|
className={hasVideo || hasPresentation ? styles.blurred : ''}
|
|
noLoop={phoneCall?.state !== 'requesting'}
|
|
/>
|
|
{phoneCall?.screencastState === 'active' && streams?.presentation
|
|
&& <video className={styles.mainVideo} muted autoPlay playsInline srcObject={streams.presentation} />}
|
|
{phoneCall?.videoState === 'active' && streams?.video
|
|
&& <video className={styles.mainVideo} muted autoPlay playsInline srcObject={streams.video} />}
|
|
<video
|
|
className={buildClassName(
|
|
styles.secondVideo,
|
|
!isHidingPresentation && hasOwnPresentation && styles.visible,
|
|
isFullscreen && styles.fullscreen,
|
|
)}
|
|
muted
|
|
autoPlay
|
|
playsInline
|
|
srcObject={streams?.ownPresentation}
|
|
/>
|
|
<video
|
|
className={buildClassName(
|
|
styles.secondVideo,
|
|
!isHidingVideo && hasOwnVideo && styles.visible,
|
|
isFullscreen && styles.fullscreen,
|
|
)}
|
|
muted
|
|
autoPlay
|
|
playsInline
|
|
srcObject={streams?.ownVideo}
|
|
/>
|
|
<div className={styles.header}>
|
|
{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>
|
|
)}
|
|
|
|
<Button
|
|
round
|
|
size="smaller"
|
|
color="translucent"
|
|
onClick={handleClose}
|
|
className={styles.closeButton}
|
|
>
|
|
<i className="icon-close" />
|
|
</Button>
|
|
</div>
|
|
<div
|
|
className={buildClassName(styles.emojisBackdrop, isEmojiOpen && styles.open)}
|
|
onClick={!isEmojiOpen ? openEmoji : closeEmoji}
|
|
>
|
|
<div className={buildClassName(styles.emojis, isEmojiOpen && styles.open)}>
|
|
{phoneCall?.isConnected && phoneCall?.emojis && renderText(phoneCall.emojis, ['emoji'])}
|
|
</div>
|
|
<div className={buildClassName(styles.emojiTooltip, isEmojiOpen && styles.open)}>
|
|
{lang('CallEmojiKeyTooltip', user?.firstName).replace('%%', '%')}
|
|
</div>
|
|
</div>
|
|
<div className={styles.userInfo}>
|
|
<h1>{user?.firstName}</h1>
|
|
<span className={styles.status}>{callStatus || formatMediaDuration(timeElapsed || 0)}</span>
|
|
</div>
|
|
<div className={styles.buttons}>
|
|
<PhoneCallButton
|
|
onClick={handleToggleAudio}
|
|
icon="microphone"
|
|
isDisabled={!isActive}
|
|
isActive={hasOwnAudio}
|
|
label={lang(hasOwnAudio ? 'lng_call_mute_audio' : 'lng_call_unmute_audio')}
|
|
/>
|
|
<PhoneCallButton
|
|
onClick={handleToggleVideo}
|
|
icon="video"
|
|
isDisabled={!isActive}
|
|
isActive={hasOwnVideo}
|
|
label={lang(hasOwnVideo ? 'lng_call_stop_video' : 'lng_call_start_video')}
|
|
/>
|
|
{hasOwnVideo && (IS_ANDROID || IS_IOS) && (
|
|
<PhoneCallButton
|
|
onClick={handleFlipCamera}
|
|
customIcon={(
|
|
<AnimatedIcon
|
|
tgsUrl={LOCAL_TGS_URLS.CameraFlip}
|
|
playSegment={!isFlipping ? [0, 1] : [0, 10]}
|
|
size={32}
|
|
/>
|
|
)}
|
|
isDisabled={!isActive}
|
|
label={lang('VoipFlip')}
|
|
/>
|
|
)}
|
|
{IS_SCREENSHARE_SUPPORTED && (
|
|
<PhoneCallButton
|
|
onClick={handleTogglePresentation}
|
|
icon="share-screen"
|
|
isDisabled={!isActive}
|
|
isActive={hasOwnPresentation}
|
|
label={lang('lng_call_screencast')}
|
|
/>
|
|
)}
|
|
{isIncomingRequested && (
|
|
<PhoneCallButton
|
|
onClick={acceptCall}
|
|
icon="phone-discard"
|
|
isDisabled={isDiscarded}
|
|
label={lang('lng_call_accept')}
|
|
className={styles.accept}
|
|
iconClassName={styles.acceptIcon}
|
|
/>
|
|
)}
|
|
<PhoneCallButton
|
|
onClick={handleHangUp}
|
|
icon="phone-discard"
|
|
isDisabled={isDiscarded}
|
|
label={lang(isIncomingRequested ? 'lng_call_decline' : 'lng_call_end_call')}
|
|
className={styles.leave}
|
|
/>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal(
|
|
(global): StateProps => {
|
|
const { phoneCall, currentUserId } = global;
|
|
|
|
return {
|
|
isCallPanelVisible: Boolean(global.isCallPanelVisible),
|
|
user: selectPhoneCallUser(global),
|
|
isOutgoing: phoneCall?.adminId === currentUserId,
|
|
phoneCall,
|
|
};
|
|
},
|
|
)(PhoneCall));
|