Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

480 lines
15 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import IS_SCREEN_SHARING_SUPPORTED from '../../environment/screenSharingSupport';
import {IS_MOBILE} from '../../environment/userAgent';
import {attachClickEvent} from '../../helpers/dom/clickEvent';
import ControlsHover from '../../helpers/dom/controlsHover';
import findUpClassName from '../../helpers/dom/findUpClassName';
import {addFullScreenListener, cancelFullScreen, isFullScreen, requestFullScreen} from '../../helpers/dom/fullScreen';
import replaceContent from '../../helpers/dom/replaceContent';
import MovablePanel from '../../helpers/movablePanel';
import onMediaLoad from '../../helpers/onMediaLoad';
import themeController from '../../helpers/themeController';
import toggleClassName from '../../helpers/toggleClassName';
import CallInstance from '../../lib/calls/callInstance';
import CALL_STATE from '../../lib/calls/callState';
import I18n, {i18n} from '../../lib/langPack';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import animationIntersector from '../animationIntersector';
import AvatarElement from '../avatar';
import ButtonIcon from '../buttonIcon';
import GroupCallMicrophoneIconMini from '../groupCall/microphoneIconMini';
import {MovableState} from '../movableElement';
import PeerTitle from '../peerTitle';
import PopupElement from '../popups';
import SetTransition from '../singleTransition';
import makeButton from './button';
import CallDescriptionElement from './description';
import callVideoCanvasBlur from './videoCanvasBlur';
const className = 'call';
const MIN_WIDTH = 400;
const MIN_HEIGHT = 580;
const INIT_STATE: MovableState = {
width: MIN_WIDTH,
height: MIN_HEIGHT
};
let previousState: MovableState = {...INIT_STATE};
export default class PopupCall extends PopupElement {
private peerId: PeerId;
private description: CallDescriptionElement;
private emojisSubtitle: HTMLElement;
private partyStates: HTMLElement;
private partyMutedState: HTMLElement;
private firstButtonsRow: HTMLElement;
private secondButtonsRow: HTMLElement;
private declineI18nElement: I18n.IntlElement;
private makeButton: (options: Parameters<typeof makeButton>[2]) => HTMLElement;
private btnAccept: HTMLElement;
private btnDecline: HTMLElement;
private btnVideo: HTMLElement;
private btnScreen: HTMLElement;
private btnMute: HTMLElement;
private btnFullScreen: HTMLButtonElement;
private btnExitFullScreen: HTMLButtonElement;
private movablePanel: MovablePanel;
private microphoneIcon: GroupCallMicrophoneIconMini;
private muteI18nElement: I18n.IntlElement;
private videoContainers: {
input?: HTMLElement,
output?: HTMLElement
};
private controlsHover: ControlsHover;
constructor(private instance: CallInstance) {
super('popup-call', {
withoutOverlay: true,
closable: true
});
this.videoContainers = {};
const {container, listenerSetter} = this;
container.classList.add(className, 'night');
const avatarContainer = document.createElement('div');
avatarContainer.classList.add(className + '-avatar');
const peerId = this.peerId = this.instance.interlocutorUserId.toPeerId();
const avatar = new AvatarElement();
avatar.classList.add('avatar-full');
avatar.updateWithOptions({
isBig: true,
peerId: peerId
});
avatarContainer.append(avatar);
const title = new PeerTitle({
peerId
}).element;
title.classList.add(className + '-title');
const subtitle = document.createElement('div');
subtitle.classList.add(className + '-subtitle');
const description = this.description = new CallDescriptionElement(subtitle);
const emojisSubtitle = this.emojisSubtitle = document.createElement('div');
emojisSubtitle.classList.add(className + '-emojis');
container.append(avatarContainer, title, subtitle);
if(!IS_MOBILE) {
this.btnFullScreen = ButtonIcon('fullscreen');
this.btnExitFullScreen = ButtonIcon('smallscreen hide');
attachClickEvent(this.btnFullScreen, this.onFullScreenClick, {listenerSetter});
attachClickEvent(this.btnExitFullScreen, () => cancelFullScreen(), {listenerSetter});
addFullScreenListener(this.container, this.onFullScreenChange, listenerSetter);
this.header.prepend(this.btnExitFullScreen);
this.header.append(this.btnFullScreen);
container.append(emojisSubtitle);
} else {
this.header.append(emojisSubtitle);
}
this.partyStates = document.createElement('div');
this.partyStates.classList.add(className + '-party-states');
this.partyMutedState = document.createElement('div');
this.partyMutedState.classList.add(className + '-party-state');
const stateText = i18n('VoipUserMicrophoneIsOff', [new PeerTitle({peerId, onlyFirstName: true, limitSymbols: 18}).element]);
stateText.classList.add(className + '-party-state-text');
const mutedIcon = new GroupCallMicrophoneIconMini(false, true);
mutedIcon.setState(false, false);
this.partyMutedState.append(
mutedIcon.container,
stateText
);
this.partyStates.append(this.partyMutedState);
this.container.append(this.partyStates);
this.makeButton = makeButton.bind(null, className, this.listenerSetter);
this.constructFirstButtons();
this.constructSecondButtons();
listenerSetter.add(instance)('state', () => {
this.updateInstance();
});
listenerSetter.add(instance)('mediaState', () => {
this.updateInstance();
});
this.movablePanel = new MovablePanel({
listenerSetter,
movableOptions: {
minWidth: MIN_WIDTH,
minHeight: MIN_HEIGHT,
element: this.element,
verifyTouchTarget: (e) => {
const target = e.target;
if(findUpClassName(target, 'call-button') ||
findUpClassName(target, 'btn-icon') ||
isFullScreen()) {
return false;
}
return true;
}
},
// onResize: () => this.toggleBigLayout(),
previousState: !this.instance.wasTryingToJoin && !this.instance.isOutgoing ? {...INIT_STATE} : previousState
});
const movableElement = this.movablePanel.movable;
if(movableElement) {
this.listenerSetter.add(movableElement)('resize', () => {
this.resizeVideoContainers();
});
}
const controlsHover = this.controlsHover = new ControlsHover();
controlsHover.setup({
element: this.container,
listenerSetter: this.listenerSetter,
showOnLeaveToClassName: 'call-buttons'
});
controlsHover.showControls(false);
this.addEventListener('close', () => {
const {movablePanel} = this;
previousState = movablePanel.state;
this.microphoneIcon.destroy();
movablePanel.destroy();
});
this.updateInstance();
}
public getCallInstance() {
return this.instance;
}
private constructFirstButtons() {
const buttons = this.firstButtonsRow = document.createElement('div');
buttons.classList.add(className + '-buttons', 'is-first');
const toggleDisability = toggleClassName.bind(null, 'btn-disabled');
const btnVideo = this.btnVideo = this.makeButton({
text: 'Call.Camera',
icon: 'videocamera_filled',
callback: () => {
const toggle = toggleDisability([btnVideo, btnScreen], true);
this.instance.toggleVideoSharing().finally(toggle);
}
});
const btnScreen = this.btnScreen = this.makeButton({
text: 'Call.Screen',
icon: 'sharescreen_filled',
callback: () => {
const toggle = toggleDisability([btnVideo, btnScreen], true);
this.instance.toggleScreenSharing().finally(toggle);
}
});
if(!IS_SCREEN_SHARING_SUPPORTED) {
btnScreen.classList.add('hide');
this.container.classList.add('no-screen');
}
this.muteI18nElement = new I18n.IntlElement({
key: 'Call.Mute'
});
const btnMute = this.btnMute = this.makeButton({
text: this.muteI18nElement.element,
callback: () => {
this.instance.toggleMuted();
}
});
const microphoneIcon = this.microphoneIcon = new GroupCallMicrophoneIconMini(true, true);
btnMute.firstElementChild.append(microphoneIcon.container);
// btnVideo.classList.add('disabled');
// btnScreen.classList.add('disabled');
buttons.append(btnVideo, btnScreen, btnMute);
this.container.append(buttons);
}
private constructSecondButtons() {
const buttons = this.secondButtonsRow = document.createElement('div');
buttons.classList.add(className + '-buttons', 'is-second');
this.declineI18nElement = new I18n.IntlElement({
key: 'Call.Decline'
});
const btnDecline = this.btnDecline = this.makeButton({
text: this.declineI18nElement.element,
icon: 'endcall_filled',
callback: () => {
this.instance.hangUp('phoneCallDiscardReasonHangup');
},
isDanger: true
});
const btnAccept = this.btnAccept = this.makeButton({
text: 'Call.Accept',
icon: 'phone_filled',
callback: () => {
this.instance.acceptCall();
},
isConfirm: true
});
buttons.append(btnDecline, btnAccept);
this.container.append(buttons);
}
private onFullScreenClick = () => {
requestFullScreen(this.container);
};
private onFullScreenChange = () => {
const isFull = isFullScreen();
const {btnFullScreen, btnExitFullScreen} = this;
const wasFullScreen = this.container.classList.contains('is-full-screen');
this.container.classList.toggle('is-full-screen', isFull);
btnFullScreen && btnFullScreen.classList.toggle('hide', isFull);
btnExitFullScreen && btnExitFullScreen.classList.toggle('hide', !isFull);
this.btnClose.classList.toggle('hide', isFull);
if(isFull !== wasFullScreen) {
animationIntersector.checkAnimations(isFull);
themeController.setThemeColor(isFull ? '#000000' : undefined);
this.resizeVideoContainers();
}
};
private createVideoContainer(video: HTMLVideoElement) {
const _className = className + '-video';
const container = document.createElement('div');
container.classList.add(_className + '-container');
video.classList.add(_className);
if(video.paused) {
video.play();
}
attachClickEvent(container, () => {
if(!container.classList.contains('small')) {
return;
}
const big = Object.values(this.videoContainers).find((container) => !container.classList.contains('small'));
big.classList.add('small');
big.style.cssText = container.style.cssText;
container.classList.remove('small');
container.style.cssText = '';
this.resizeVideoContainers();
});
const canvas = callVideoCanvasBlur(video);
canvas.classList.add(_className + '-blur');
container.append(canvas, video);
return container;
}
private updateInstance() {
const {instance} = this;
const {connectionState} = instance;
if(connectionState === CALL_STATE.CLOSED) {
if(this.container.classList.contains('is-full-screen')) {
cancelFullScreen();
}
this.btnVideo.classList.add('disabled');
this.hide();
return;
}
const isPendingIncoming = !instance.isOutgoing && connectionState === CALL_STATE.PENDING;
this.declineI18nElement.compareAndUpdate({
key: connectionState === CALL_STATE.PENDING ? 'Call.Decline' : 'Call.End'
});
this.btnAccept.classList.toggle('disable', !isPendingIncoming);
this.btnAccept.classList.toggle('hide-me', !isPendingIncoming);
this.container.classList.toggle('two-button-rows', isPendingIncoming);
const isMuted = instance.isMuted;
const onFrame = () => {
this.btnMute.firstElementChild.classList.toggle('active', isMuted);
};
const player = this.microphoneIcon.getItem().player;
this.microphoneIcon.setState(!isMuted, !isMuted, onFrame);
if(!player) {
onFrame();
}
this.muteI18nElement.compareAndUpdate({
key: isMuted ? 'VoipUnmute' : 'Call.Mute'
});
const isSharingVideo = instance.isSharingVideo;
this.btnVideo.firstElementChild.classList.toggle('active', isSharingVideo);
const isSharingScreen = instance.isSharingScreen;
this.btnScreen.firstElementChild.classList.toggle('active', isSharingScreen);
const outputState = instance.getMediaState('output');
SetTransition(this.partyMutedState, 'is-visible', !!outputState?.muted, 300);
const containers = this.videoContainers;
const oldContainers = {...containers};
['input' as const, 'output' as const].forEach((type) => {
const mediaState = instance.getMediaState(type);
const video = instance.getVideoElement(type) as HTMLVideoElement;
const hasFrame = !!(video && video.videoWidth && video.videoHeight);
if(video && !hasFrame && !video.dataset.hasPromise) {
video.dataset.hasPromise = '1';
// container.classList.add('hide');
onMediaLoad(video).then(() => {
delete video.dataset.hasPromise;
this.updateInstance();
// this.resizeVideoContainers();
// container.classList.remove('hide');
});
}
const isActive = !!video && hasFrame && !!(mediaState && (mediaState.videoState === 'active' || mediaState.screencastState === 'active'));
let videoContainer = containers[type];
if(isActive && video && !videoContainer) {
videoContainer = containers[type] = this.createVideoContainer(video);
this.container.append(videoContainer);
}
if(!isActive && videoContainer) {
videoContainer.remove();
delete containers[type];
}
});
{
const input = containers.input;
const output = containers.output;
if(Object.keys(oldContainers).length !== Object.keys(containers).length && input) {
input.classList.toggle('small', !!output);
}
if(output && !input) {
output.classList.remove('small');
}
}
this.resizeVideoContainers();
this.container.classList.toggle('no-video', !Object.keys(containers).length);
if(!this.emojisSubtitle.textContent && connectionState < CALL_STATE.EXCHANGING_KEYS) {
Promise.resolve(instance.getEmojisFingerprint()).then((emojis) => {
replaceContent(this.emojisSubtitle, wrapEmojiText(emojis.join('')));
});
}
this.setDescription();
}
private resizeVideoContainers() {
Object.values(this.videoContainers).forEach((container) => {
const isSmall = container.classList.contains('small');
if(isSmall) {
const video = container.querySelector('video');
const popupWidth = this.movablePanel.state;
const MAX_WIDTH_PX = 240;
const MAX_HEIGHT_PX = 240;
const isVertical = video.videoHeight > video.videoWidth;
const MAX_SIZE = isVertical ? MAX_HEIGHT_PX : MAX_WIDTH_PX;
const biggestSideSize = 1 / 3 * (isFullScreen() ? 0xFFFF : (isVertical ? popupWidth.height : popupWidth.width));
const widthRatio = isVertical ? video.videoWidth / video.videoHeight : 1;
const heightRatio = isVertical ? 1 : video.videoHeight / video.videoWidth;
container.style.width = biggestSideSize * widthRatio + 'px';
container.style.height = biggestSideSize * heightRatio + 'px';
container.style.maxWidth = MAX_SIZE * widthRatio + 'px';
container.style.maxHeight = MAX_SIZE * heightRatio + 'px';
} else {
container.style.cssText = '';
}
});
}
private setDescription() {
this.description.update(this.instance);
}
}