diff --git a/src/components/avatar.ts b/src/components/avatar.ts index b7b3b930..0b6ae5e0 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -119,10 +119,11 @@ const seen: Set = new Set(); export default class AvatarElement extends HTMLElement { private peerId: PeerId; - private isDialog = false; + private isDialog: boolean; private peerTitle: string; public loadPromises: Promise[]; public lazyLoadQueue: LazyLoadQueueIntersector; + public isBig: boolean; private addedToQueue = false; connectedCallback() { @@ -196,7 +197,7 @@ export default class AvatarElement extends HTMLElement { } private r(onlyThumb = false) { - const res = appAvatarsManager.putPhoto(this, this.peerId, this.isDialog, this.peerTitle, onlyThumb); + const res = appAvatarsManager.putPhoto(this, this.peerId, this.isDialog, this.peerTitle, onlyThumb, this.isBig); const promise = res ? res.loadPromise : Promise.resolve(); if(this.loadPromises) { if(res && res.cached) { diff --git a/src/components/call/description.ts b/src/components/call/description.ts index 217e801d..d503d07e 100644 --- a/src/components/call/description.ts +++ b/src/components/call/description.ts @@ -75,6 +75,7 @@ export default class CallDescriptionElement { } } + this.container.classList.toggle('has-duration', connectionState === CALL_STATE.CONNECTED); replaceContent(this.container, element); if(!this.container.parentElement) { diff --git a/src/components/call/index.ts b/src/components/call/index.ts index ea57996c..5aa94b7b 100644 --- a/src/components/call/index.ts +++ b/src/components/call/index.ts @@ -5,6 +5,7 @@ */ 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"; @@ -14,6 +15,7 @@ import MovablePanel from "../../helpers/movablePanel"; import safeAssign from "../../helpers/object/safeAssign"; import toggleClassName from "../../helpers/toggleClassName"; import type { AppAvatarsManager } from "../../lib/appManagers/appAvatarsManager"; +import type { AppCallsManager } from "../../lib/appManagers/appCallsManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import CallInstance from "../../lib/calls/callInstance"; import CALL_STATE from "../../lib/calls/callState"; @@ -21,6 +23,7 @@ import I18n, { i18n } from "../../lib/langPack"; import RichTextProcessor from "../../lib/richtextprocessor"; import rootScope from "../../lib/rootScope"; import animationIntersector from "../animationIntersector"; +import AvatarElement from "../avatar"; import ButtonIcon from "../buttonIcon"; import GroupCallMicrophoneIconMini from "../groupCall/microphoneIconMini"; import { MovableState } from "../movableElement"; @@ -40,6 +43,7 @@ let previousState: MovableState = { export default class PopupCall extends PopupElement { private instance: CallInstance; + private appCallsManager: AppCallsManager; private appAvatarsManager: AppAvatarsManager; private appPeersManager: AppPeersManager; private peerId: PeerId; @@ -76,6 +80,7 @@ export default class PopupCall extends PopupElement { private controlsHover: ControlsHover; constructor(options: { + appCallsManager: AppCallsManager, appAvatarsManager: AppAvatarsManager, appPeersManager: AppPeersManager, instance: CallInstance @@ -96,8 +101,11 @@ export default class PopupCall extends PopupElement { avatarContainer.classList.add(className + '-avatar'); const peerId = this.peerId = this.instance.interlocutorUserId.toPeerId(); - const photo = this.appPeersManager.getPeerPhoto(peerId); - this.appAvatarsManager.putAvatar(avatarContainer, peerId, photo, 'photo_big'); + const avatar = new AvatarElement(); + avatar.isBig = true; + avatar.setAttribute('peer', '' + peerId); + avatar.classList.add('avatar-full'); + avatarContainer.append(avatar); const title = new PeerTitle({ peerId @@ -113,22 +121,28 @@ export default class PopupCall extends PopupElement { const emojisSubtitle = this.emojisSubtitle = document.createElement('div'); emojisSubtitle.classList.add(className + '-emojis'); - container.append(avatarContainer, title, subtitle, emojisSubtitle); + container.append(avatarContainer, title, subtitle); - 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); + 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}).element]); + 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); @@ -193,6 +207,10 @@ export default class PopupCall extends PopupElement { this.updateInstance(); } + public getCallInstance() { + return this.instance; + } + private constructFirstButtons() { const buttons = this.firstButtonsRow = document.createElement('div'); buttons.classList.add(className + '-buttons', 'is-first'); @@ -260,7 +278,7 @@ export default class PopupCall extends PopupElement { const btnAccept = this.btnAccept = this.makeButton({ text: 'Call.Accept', - icon: 'phone', + icon: 'phone_filled', callback: () => { this.instance.acceptCall(); }, diff --git a/src/components/chat/patternRenderer.ts b/src/components/chat/patternRenderer.ts index 61fa1551..8a81e896 100644 --- a/src/components/chat/patternRenderer.ts +++ b/src/components/chat/patternRenderer.ts @@ -5,9 +5,9 @@ */ import { IS_SAFARI } from "../../environment/userAgent"; -import { indexOfAndSplice } from "../../helpers/array"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl"; -import { deepEqual } from "../../helpers/object"; +import deepEqual from "../../helpers/object/deepEqual"; type ChatBackgroundPatternRendererInitOptions = { url: string, diff --git a/src/components/chat/reactions.ts b/src/components/chat/reactions.ts index f937beb7..263c75eb 100644 --- a/src/components/chat/reactions.ts +++ b/src/components/chat/reactions.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { forEachReverse } from "../../helpers/array"; +import forEachReverse from "../../helpers/array/forEachReverse"; import positionElementByIndex from "../../helpers/dom/positionElementByIndex"; import { Message, ReactionCount } from "../../layer"; import appReactionsManager from "../../lib/appManagers/appReactionsManager"; diff --git a/src/components/peerTitle.ts b/src/components/peerTitle.ts index 5dff58b0..0d178131 100644 --- a/src/components/peerTitle.ts +++ b/src/components/peerTitle.ts @@ -12,13 +12,15 @@ import replaceContent from "../helpers/dom/replaceContent"; import appUsersManager from "../lib/appManagers/appUsersManager"; import RichTextProcessor from "../lib/richtextprocessor"; import { NULL_PEER_ID } from "../lib/mtproto/mtproto_config"; +import { limitSymbols } from "../helpers/string"; export type PeerTitleOptions = { peerId?: PeerId, fromName?: string, plainText?: boolean, onlyFirstName?: boolean, - dialog?: boolean + dialog?: boolean, + limitSymbols?: number }; const weakMap: WeakMap = new WeakMap(); @@ -44,6 +46,7 @@ export default class PeerTitle { public plainText = false; public onlyFirstName = false; public dialog = false; + public limitSymbols: number; constructor(options: PeerTitleOptions) { this.element = document.createElement('span'); @@ -64,8 +67,13 @@ export default class PeerTitle { } } - if(this.fromName !== undefined) { - this.element.innerHTML = RichTextProcessor.wrapEmojiText(this.fromName); + let fromName = this.fromName; + if(fromName !== undefined) { + if(this.limitSymbols !== undefined) { + fromName = limitSymbols(fromName, this.limitSymbols, this.limitSymbols); + } + + this.element.innerHTML = RichTextProcessor.wrapEmojiText(fromName); return; } @@ -77,7 +85,7 @@ export default class PeerTitle { if(this.peerId.isUser() && appUsersManager.getUser(this.peerId).pFlags.deleted) { replaceContent(this.element, i18n(this.onlyFirstName ? 'Deleted' : 'HiddenName')); } else { - this.element.innerHTML = appPeersManager.getPeerTitle(this.peerId, this.plainText, this.onlyFirstName); + this.element.innerHTML = appPeersManager.getPeerTitle(this.peerId, this.plainText, this.onlyFirstName, this.limitSymbols); } } else { replaceContent(this.element, i18n(this.onlyFirstName ? 'Saved' : 'SavedMessages')); diff --git a/src/components/popups/index.ts b/src/components/popups/index.ts index 9d725c77..4e2d02b0 100644 --- a/src/components/popups/index.ts +++ b/src/components/popups/index.ts @@ -15,7 +15,7 @@ import ListenerSetter from "../../helpers/listenerSetter"; import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent"; import isSendShortcutPressed from "../../helpers/dom/isSendShortcutPressed"; import { cancelEvent } from "../../helpers/dom/cancelEvent"; -import EventListenerBase from "../../helpers/eventListenerBase"; +import EventListenerBase, { EventListenerListeners } from "../../helpers/eventListenerBase"; import { addFullScreenListener, getFullScreenElement } from "../../helpers/dom/fullScreen"; import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; @@ -52,11 +52,13 @@ const onFullScreenChange = () => { addFullScreenListener(DEFAULT_APPEND_TO, onFullScreenChange); -export default class PopupElement extends EventListenerBase<{ +type PopupListeners = { close: () => void, closeAfterTimeout: () => void -}> { - private static POPUPS: PopupElement[] = []; +}; + +export default class PopupElement extends EventListenerBase { + private static POPUPS: PopupElement[] = []; protected element = document.createElement('div'); protected container = document.createElement('div'); protected header = document.createElement('div'); @@ -181,7 +183,7 @@ export default class PopupElement extends EventListenerBase<{ public show() { this.navigationItem = { type: 'popup', - onPop: this.destroy, + onPop: () => this.destroy(), onEscape: this.onEscape }; @@ -214,8 +216,8 @@ export default class PopupElement extends EventListenerBase<{ appNavigationController.backByItem(this.navigationItem); }; - private destroy = () => { - this.dispatchEvent('close'); + protected destroy() { + this.dispatchEvent('close'); this.element.classList.add('hiding'); this.element.classList.remove('active'); this.listenerSetter.removeAll(); @@ -234,14 +236,14 @@ export default class PopupElement extends EventListenerBase<{ setTimeout(() => { this.element.remove(); - this.dispatchEvent('closeAfterTimeout'); + this.dispatchEvent('closeAfterTimeout'); this.cleanup(); if(!this.withoutOverlay) { animationIntersector.checkAnimations(false); } }, 150); - }; + } public static reAppend() { this.POPUPS.forEach(popup => { diff --git a/src/components/sidebarLeft/tabs/dataAndStorage.ts b/src/components/sidebarLeft/tabs/dataAndStorage.ts index 918559c0..ef4eb791 100644 --- a/src/components/sidebarLeft/tabs/dataAndStorage.ts +++ b/src/components/sidebarLeft/tabs/dataAndStorage.ts @@ -9,7 +9,8 @@ import { attachClickEvent } from "../../../helpers/dom/clickEvent"; import replaceContent from "../../../helpers/dom/replaceContent"; import toggleDisability from "../../../helpers/dom/toggleDisability"; import formatBytes from "../../../helpers/formatBytes"; -import { copy, deepEqual } from "../../../helpers/object"; +import copy from "../../../helpers/object/copy"; +import deepEqual from "../../../helpers/object/deepEqual"; import appStateManager, { AutoDownloadPeerTypeSettings, STATE_INIT } from "../../../lib/appManagers/appStateManager"; import { FormatterArguments, i18n, join, LangPackKey } from "../../../lib/langPack"; import rootScope from "../../../lib/rootScope"; diff --git a/src/components/topbarCall.ts b/src/components/topbarCall.ts index d784b4eb..9a4cd7ff 100644 --- a/src/components/topbarCall.ts +++ b/src/components/topbarCall.ts @@ -29,6 +29,7 @@ import PopupCall from "./call"; import type { AppAvatarsManager } from "../lib/appManagers/appAvatarsManager"; import GroupCallMicrophoneIconMini from "./groupCall/microphoneIconMini"; import CallInstance from "../lib/calls/callInstance"; +import type { AppCallsManager } from "../lib/appManagers/appCallsManager"; function convertCallStateToGroupState(state: CALL_STATE, isMuted: boolean) { switch(state) { @@ -64,11 +65,18 @@ export default class TopbarCall { private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager, private appAvatarsManager: AppAvatarsManager, + private appCallsManager: AppCallsManager ) { const listenerSetter = this.listenerSetter = new ListenerSetter(); - listenerSetter.add(rootScope)('call_instance', ({instance, hasCurrent}) => { - if(!hasCurrent) { + listenerSetter.add(rootScope)('call_instance', ({instance}) => { + if(!this.instance) { + this.updateInstance(instance); + } + }); + + listenerSetter.add(rootScope)('call_accepting', (instance) => { + if(this.instance !== instance) { this.updateInstance(instance); } }); @@ -121,7 +129,8 @@ export default class TopbarCall { this.construct = undefined; } - if(this.instance !== instance) { + const isChangingInstance = this.instance !== instance; + if(isChangingInstance) { this.clearCurrentInstance(); this.instance = instance; @@ -135,6 +144,8 @@ export default class TopbarCall { this.currentDescription = this.callDescription; this.instanceListenerSetter.add(instance)('muted', this.onState); } + + this.container.classList.toggle('is-call', !(instance instanceof GroupCallInstance)); } const isMuted = this.instance.isMuted; @@ -145,7 +156,7 @@ export default class TopbarCall { weave.componentDidMount(); const isClosed = state === GROUP_CALL_STATE.CLOSED; - if(!document.body.classList.contains('is-calling') || isClosed) { + if((!document.body.classList.contains('is-calling') || isChangingInstance) || isClosed) { if(isClosed) { weave.setAmplitude(0); } @@ -257,7 +268,13 @@ export default class TopbarCall { appChatsManager: this.appChatsManager }).show(); } else if(this.instance instanceof CallInstance) { + const hasPopup = PopupElement.getPopup(PopupCall) as PopupCall; + if(hasPopup && hasPopup.getCallInstance() === this.instance) { + return; + } + new PopupCall({ + appCallsManager: this.appCallsManager, appAvatarsManager: this.appAvatarsManager, appPeersManager: this.appPeersManager, instance: this.instance diff --git a/src/helpers/eventListenerBase.ts b/src/helpers/eventListenerBase.ts index 994bb060..813d1adf 100644 --- a/src/helpers/eventListenerBase.ts +++ b/src/helpers/eventListenerBase.ts @@ -50,6 +50,8 @@ import type { ArgumentTypes, SuperReturnType } from "../types"; // MOUNT_CLASS_TO.e = e; export type EventListenerListeners = Record; +// export type EventListenerListeners = Record any>; +// export type EventListenerListeners = {[name in string]: Function}; /** * Better not to remove listeners during setting @@ -151,7 +153,8 @@ export default class EventListenerBase } // * must be protected, but who cares - public dispatchEvent(name: T, ...args: ArgumentTypes) { + public dispatchEvent(name: T, ...args: ArgumentTypes) { + // @ts-ignore this._dispatchEvent(name, false, ...args); } diff --git a/src/index.ts b/src/index.ts index d548778d..1988cd1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -170,6 +170,10 @@ console.timeEnd('get storage1'); */ document.documentElement.classList.add('is-firefox'); } + if(userAgent.IS_MOBILE) { + document.documentElement.classList.add('is-mobile'); + } + if(userAgent.IS_APPLE) { if(userAgent.IS_SAFARI) { document.documentElement.classList.add('is-safari'); diff --git a/src/lib/appManagers/appAvatarsManager.ts b/src/lib/appManagers/appAvatarsManager.ts index da1b3d2e..d4d92056 100644 --- a/src/lib/appManagers/appAvatarsManager.ts +++ b/src/lib/appManagers/appAvatarsManager.ts @@ -169,7 +169,7 @@ export class AppAvatarsManager { } // peerId === peerId || title - public putPhoto(div: HTMLElement, peerId: PeerId, isDialog = false, title = '', onlyThumb = false) { + public putPhoto(div: HTMLElement, peerId: PeerId, isDialog = false, title = '', onlyThumb = false, isBig?: boolean) { const myId = rootScope.myId; //console.log('loadDialogPhoto location:', location, inputPeer); @@ -213,7 +213,7 @@ export class AppAvatarsManager { } if(avatarAvailable/* && false */) { - const size: PeerPhotoSize = 'photo_small'; + const size: PeerPhotoSize = isBig ? 'photo_big' : 'photo_small'; return this.putAvatar(div, peerId, photo, size, undefined, onlyThumb); } } diff --git a/src/lib/appManagers/appCallsManager.ts b/src/lib/appManagers/appCallsManager.ts index 7502eb0c..10a5a0cc 100644 --- a/src/lib/appManagers/appCallsManager.ts +++ b/src/lib/appManagers/appCallsManager.ts @@ -11,6 +11,8 @@ import { MOUNT_CLASS_TO } from "../../config/debug"; import IS_CALL_SUPPORTED from "../../environment/callSupport"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; +import insertInDescendSortedArray from "../../helpers/array/insertInDescendSortedArray"; import AudioAssetPlayer from "../../helpers/audioAssetPlayer"; import bytesCmp from "../../helpers/bytes/bytesCmp"; import safeReplaceObject from "../../helpers/object/safeReplaceObject"; @@ -39,6 +41,7 @@ export class AppCallsManager { private log: ReturnType; private calls: Map; private instances: Map; + private sortedInstances: Array; private tempId: number; private audioAsset: AudioAssetPlayer; @@ -48,6 +51,7 @@ export class AppCallsManager { this.tempId = 0; this.calls = new Map(); this.instances = new Map(); + this.sortedInstances = []; if(!IS_CALL_SUPPORTED) { return; @@ -139,15 +143,7 @@ export class AppCallsManager { } public get currentCall() { - let lastInstance: CallInstance; - for(const [callId, instance] of this.instances) { - lastInstance = instance; - if(instance.connectionState !== CALL_STATE.PENDING) { - break; - } - } - - return lastInstance; + return this.sortedInstances[0]; } public getCallByUserId(userId: UserId) { @@ -207,15 +203,17 @@ export class AppCallsManager { ...options, }); - let wasTryingToJoin = false; call.addEventListener('state', (state) => { const currentCall = this.currentCall; if(state === CALL_STATE.CLOSED) { this.instances.delete(call.id); + indexOfAndSplice(this.sortedInstances, call); + } else { + insertInDescendSortedArray(this.sortedInstances, call, 'sortIndex'); } if(state === CALL_STATE.EXCHANGING_KEYS) { - wasTryingToJoin = true; + call.wasTryingToJoin = true; } const hasConnected = call.connectedAt !== undefined; @@ -227,9 +225,9 @@ export class AppCallsManager { if(currentCall === call || !currentCall) { if(state === CALL_STATE.CLOSED) { - if(!call.isOutgoing && !wasTryingToJoin) { // incoming call has been accepted on other device or ended + if(!call.isOutgoing && !call.wasTryingToJoin) { // incoming call has been accepted on other device or ended this.audioAsset.stopSound(); - } else if(wasTryingToJoin && !hasConnected) { // something has happened during the key exchanging + } else if(call.wasTryingToJoin && !hasConnected) { // something has happened during the key exchanging this.audioAsset.playSound('voip_failed.mp3'); } else { this.audioAsset.playSound(call.discardReason === 'phoneCallDiscardReasonBusy' ? 'call_busy.mp3' : 'call_end.mp3'); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index dbbe31be..2b2a0e55 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -87,6 +87,8 @@ import appReactionsManager from './appReactionsManager'; import PopupCall from '../../components/call'; import copy from '../../helpers/object/copy'; import getObjectKeysAndSort from '../../helpers/object/getObjectKeysAndSort'; +import type GroupCallInstance from '../calls/groupCallInstance'; +import type CallInstance from '../calls/callInstance'; //console.log('appImManager included33!'); @@ -341,20 +343,39 @@ export class AppImManager { }); if(IS_CALL_SUPPORTED || IS_GROUP_CALL_SUPPORTED) { - this.topbarCall = new TopbarCall(appGroupCallsManager, appPeersManager, appChatsManager, appAvatarsManager); + this.topbarCall = new TopbarCall(appGroupCallsManager, appPeersManager, appChatsManager, appAvatarsManager, appCallsManager); } if(IS_CALL_SUPPORTED) { - rootScope.addEventListener('call_instance', ({instance, hasCurrent}) => { - if(hasCurrent) { - return; - } + rootScope.addEventListener('call_instance', ({instance/* , hasCurrent */}) => { + // if(hasCurrent) { + // return; + // } - new PopupCall({ + const popup = new PopupCall({ + appCallsManager, appAvatarsManager, appPeersManager, instance - }).show(); + }); + + instance.addEventListener('acceptCallOverride', () => { + return this.discardCurrentCall(instance.interlocutorUserId.toPeerId(), undefined, instance) + .then(() => { + rootScope.dispatchEvent('call_accepting', instance); + return true; + }) + .catch(() => false); + }); + + popup.addEventListener('close', () => { + const currentCall = appCallsManager.currentCall; + if(currentCall && currentCall !== instance && !instance.wasTryingToJoin) { + instance.hangUp('phoneCallDiscardReasonBusy'); + } + }, {once: true}); + + popup.show(); }); } @@ -944,9 +965,9 @@ export class AppImManager { appCallsManager.startCallInternal(userId, type === 'video'); } - private discardCurrentCall(toPeerId: PeerId) { - if(appCallsManager.currentCall) return this.discardCallConfirmation(toPeerId); - else if(appGroupCallsManager.groupCall) return this.discardGroupCallConfirmation(toPeerId); + private discardCurrentCall(toPeerId: PeerId, ignoreGroupCall?: GroupCallInstance, ignoreCall?: CallInstance) { + if(appGroupCallsManager.groupCall && appGroupCallsManager.groupCall !== ignoreGroupCall) return this.discardGroupCallConfirmation(toPeerId); + else if(appCallsManager.currentCall && appCallsManager.currentCall !== ignoreCall) return this.discardCallConfirmation(toPeerId); else return Promise.resolve(); } @@ -965,8 +986,8 @@ export class AppImManager { } }); - if(appCallsManager.currentCall === currentCall) { - await currentCall.hangUp(); + if(!currentCall.isClosing) { + await currentCall.hangUp('phoneCallDiscardReasonDisconnect'); } } } diff --git a/src/lib/appManagers/appPeersManager.ts b/src/lib/appManagers/appPeersManager.ts index 70fef585..ae32abb0 100644 --- a/src/lib/appManagers/appPeersManager.ts +++ b/src/lib/appManagers/appPeersManager.ts @@ -20,6 +20,7 @@ import I18n from '../langPack'; import { NULL_PEER_ID } from "../mtproto/mtproto_config"; import { getRestrictionReason } from "../../helpers/restrictions"; import isObject from "../../helpers/object/isObject"; +import { limitSymbols } from "../../helpers/string"; // https://github.com/eelcohn/Telegram-API/wiki/Calculating-color-for-a-Telegram-user-on-IRC /* @@ -73,7 +74,7 @@ export class AppPeersManager { return false; } - public getPeerTitle(peerId: PeerId, plainText = false, onlyFirstName = false) { + public getPeerTitle(peerId: PeerId, plainText = false, onlyFirstName = false, _limitSymbols?: number) { if(!peerId) { peerId = rootScope.myId; } @@ -94,6 +95,10 @@ export class AppPeersManager { title = title.split(' ')[0]; } } + + if(_limitSymbols !== undefined) { + title = limitSymbols(title, _limitSymbols, _limitSymbols); + } return plainText ? title : RichTextProcessor.wrapEmojiText(title); } diff --git a/src/lib/appManagers/appReactionsManager.ts b/src/lib/appManagers/appReactionsManager.ts index cf458ad2..1f6cca43 100644 --- a/src/lib/appManagers/appReactionsManager.ts +++ b/src/lib/appManagers/appReactionsManager.ts @@ -8,7 +8,7 @@ import { MOUNT_CLASS_TO } from "../../config/debug"; import assumeType from "../../helpers/assumeType"; import callbackify from "../../helpers/callbackify"; import callbackifyAll from "../../helpers/callbackifyAll"; -import { copy } from "../../helpers/object"; +import copy from "../../helpers/object/copy"; import { AvailableReaction, Message, MessagePeerReaction, MessagesAvailableReactions, Update, Updates } from "../../layer"; import apiManager from "../mtproto/mtprotoworker"; import { ReferenceContext } from "../mtproto/referenceDatabase"; diff --git a/src/lib/calls/callInstance.ts b/src/lib/calls/callInstance.ts index cd5d37ac..80f96933 100644 --- a/src/lib/calls/callInstance.ts +++ b/src/lib/calls/callInstance.ts @@ -34,7 +34,8 @@ export default class CallInstance extends CallInstanceBase<{ state: (state: CALL_STATE) => void, id: (id: CallId, prevId: CallId) => void, muted: (muted: boolean) => void, - mediaState: (mediaState: CallMediaState) => void + mediaState: (mediaState: CallMediaState) => void, + acceptCallOverride: () => Promise, }> { public dh: Partial; public id: CallId; @@ -55,6 +56,7 @@ export default class CallInstance extends CallInstanceBase<{ public release: () => Promise; public _connectionState: CALL_STATE; + public createdAt: number; public connectedAt: number; public discardReason: string; @@ -78,6 +80,9 @@ export default class CallInstance extends CallInstanceBase<{ private wasStartingScreen: boolean; private wasStartingVideo: boolean; + public wasTryingToJoin: boolean; + + public streamManager: StreamManager; constructor(options: { isOutgoing: boolean, @@ -97,6 +102,7 @@ export default class CallInstance extends CallInstanceBase<{ safeAssign(this, options); + this.createdAt = Date.now(); this.offerReceived = false; this.offerSent = false; this.decryptQueue = []; @@ -110,7 +116,7 @@ export default class CallInstance extends CallInstanceBase<{ } }); - const streamManager = new StreamManager(GROUP_CALL_AMPLITUDE_ANALYSE_INTERVAL_MS); + const streamManager = this.streamManager = new StreamManager(GROUP_CALL_AMPLITUDE_ANALYSE_INTERVAL_MS); streamManager.direction = 'sendrecv'; streamManager.types.push('screencast'); if(!this.isOutgoing) { @@ -164,6 +170,14 @@ export default class CallInstance extends CallInstanceBase<{ } } + get sortIndex() { + const connectionState = this.connectionState; + const state = CALL_STATE.CLOSED - connectionState + 1; + let index = state * 10000000000000; + index += 2147483647000 - (connectionState === CALL_STATE.PENDING && this.isOutgoing ? 0 : this.createdAt); + return index; + } + public getVideoElement(type: CallMediaState['type']) { if(type === 'input') return this.elements.get('main'); else { @@ -283,10 +297,6 @@ export default class CallInstance extends CallInstanceBase<{ return connectionState === CALL_STATE.CLOSING || connectionState === CALL_STATE.CLOSED; } - public get streamManager(): StreamManager { - return this.connectionInstance?.streamManager; - } - public get description(): localConferenceDescription { return this.connectionInstance?.description; } @@ -318,6 +328,11 @@ export default class CallInstance extends CallInstanceBase<{ } public async acceptCall() { + const canAccept = (await Promise.all(this.dispatchResultableEvent('acceptCallOverride')))[0] ?? true; + if(this.isClosing || !canAccept) { + return; + } + // this.clearHangUpTimeout(); this.overrideConnectionState(CALL_STATE.EXCHANGING_KEYS); @@ -619,7 +634,7 @@ export default class CallInstance extends CallInstanceBase<{ } public async hangUp(discardReason?: PhoneCallDiscardReason['_'], discardedByOtherParty?: boolean) { - if(this.connectionState === CALL_STATE.CLOSED) { + if(this.isClosing) { return; } diff --git a/src/lib/calls/callInstanceBase.ts b/src/lib/calls/callInstanceBase.ts index 1867cd64..331ee9bb 100644 --- a/src/lib/calls/callInstanceBase.ts +++ b/src/lib/calls/callInstanceBase.ts @@ -11,6 +11,7 @@ import getAudioConstraints from "./helpers/getAudioConstraints"; import getScreenConstraints from "./helpers/getScreenConstraints"; import getStreamCached from "./helpers/getStreamCached"; import getVideoConstraints from "./helpers/getVideoConstraints"; +import stopTrack from "./helpers/stopTrack"; import LocalConferenceDescription from "./localConferenceDescription"; import StreamManager, { StreamItem } from "./streamManager"; @@ -190,6 +191,9 @@ export default abstract class CallInstanceBase if(!isVideo) { player.appendChild(element); + } else { + element.setAttribute('playsinline', 'true'); + element.muted = true; } // audio.play(); @@ -229,6 +233,10 @@ export default abstract class CallInstanceBase if(description) { streamManager.appendToConference(description); } + } else { // if call is declined earlier than stream appears + stream.getTracks().forEach(track => { + stopTrack(track); + }); } } } diff --git a/src/lib/calls/groupCallInstance.ts b/src/lib/calls/groupCallInstance.ts index 1956c2e1..39c239f8 100644 --- a/src/lib/calls/groupCallInstance.ts +++ b/src/lib/calls/groupCallInstance.ts @@ -183,8 +183,6 @@ export default class GroupCallInstance extends CallInstanceBase<{ const clone = element.cloneNode() as typeof element; clone.srcObject = element.srcObject; - clone.setAttribute('playsinline', 'true'); - clone.muted = true; return {video: clone, source}; } diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index 235748ed..2ae10d7b 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -18,7 +18,7 @@ import { encodeEntities } from '../helpers/string'; import { IS_SAFARI } from '../environment/userAgent'; import { MOUNT_CLASS_TO } from '../config/debug'; import IS_EMOJI_SUPPORTED from '../environment/emojiSupport'; -import { copy } from '../helpers/object'; +import copy from '../helpers/object/copy'; const EmojiHelper = { emojiMap: (code: string) => { return code; }, diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 782583f3..28b8f5af 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -17,7 +17,7 @@ import type { PushNotificationObject } from "./serviceWorker/push"; import type { ConnectionStatusChange } from "./mtproto/connectionStatus"; import type { GroupCallId } from "./appManagers/appGroupCallsManager"; import type GroupCallInstance from "./calls/groupCallInstance"; -// import type CallInstance from "./calls/callInstance"; +import type CallInstance from "./calls/callInstance"; import type { StreamAmplitude } from "./calls/streamManager"; import type Chat from "../components/chat/chat"; import { NULL_PEER_ID, UserAuth } from "./mtproto/mtproto_config"; @@ -158,7 +158,8 @@ export type BroadcastEvents = { 'group_call_participant': {groupCallId: GroupCallId, participant: GroupCallParticipant}, // 'group_call_video_track_added': {instance: GroupCallInstance} - 'call_instance': {hasCurrent: boolean, instance: any/* CallInstance */}, + 'call_instance': {hasCurrent: boolean, instance: CallInstance}, + 'call_accepting': CallInstance, // это костыль. используется при параллельном вызове, чтобы заменить звонок в topbarCall 'quick_reaction': string, diff --git a/src/scss/partials/_avatar.scss b/src/scss/partials/_avatar.scss index 1f308c44..6b84edce 100644 --- a/src/scss/partials/_avatar.scss +++ b/src/scss/partials/_avatar.scss @@ -232,3 +232,19 @@ avatar-element { left: 0; } } + +.avatar-full { + position: absolute; + width: 100%; + height: 100%; + border-radius: inherit; + display: inline-flex; + align-items: center; + justify-content: center; + + .avatar-photo { + width: 100% !important; + height: 100% !important; + object-fit: cover; + } +} diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 52b53ca9..e684d07e 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -1383,12 +1383,13 @@ $bubble-beside-button-width: 38px; } &-subtitle { - font-size: .875rem; + font-size: var(--messages-secondary-text-size); color: var(--secondary-text-color); display: flex; align-items: center; margin-top: .0625rem; margin-left: -.1875rem; + line-height: var(--messages-secondary-line-height); &.is-reason:before { margin-right: .0625rem; diff --git a/src/scss/partials/_chatTopbar.scss b/src/scss/partials/_chatTopbar.scss index 15058b4c..b501a522 100644 --- a/src/scss/partials/_chatTopbar.scss +++ b/src/scss/partials/_chatTopbar.scss @@ -239,6 +239,7 @@ body.is-right-column-shown { height: 3.5rem; max-height: 3.5rem; flex: 1 1 auto; + max-width: 100%; } } @@ -360,15 +361,29 @@ body.is-right-column-shown { white-space: nowrap; } - @media only screen and (max-width: 480px) { - .topbar-call-left, - .topbar-call-right { - width: auto; + &:not(.is-call) { + @media only screen and (max-width: 480px) { + .topbar-call-left, + .topbar-call-right { + width: auto; + } + + .group-call-description { + display: none; + } } + } - .group-call-description, - .call-description { - display: none; + &.is-call { + @media only screen and (max-width: 480px) { + .topbar-call-left, + .topbar-call-right { + width: 6.25rem; + } + + .call-description:not(.has-duration) { + display: none; + } } } } @@ -399,6 +414,13 @@ body.is-right-column-shown { &-center { @include sidebar-transform(true); @include text-overflow(); + + @include respond-to(medium-screens) { + // ! it flicks over the left side :( + // body.is-right-column-shown & { + padding: 0 calc(var(--right-column-width) / 2); + // } + } } &-right { diff --git a/src/scss/partials/_fonts.scss b/src/scss/partials/_fonts.scss index ecb39d6b..abc39974 100644 --- a/src/scss/partials/_fonts.scss +++ b/src/scss/partials/_fonts.scss @@ -37,6 +37,13 @@ -moz-osx-font-smoothing: grayscale; } +.tgico-phone_filled { + &:before { + content: $tgico-endcall_filled; + transform: rotate(-135deg); + } +} + .tgico-check { &:before { content: $tgico-check; diff --git a/src/scss/partials/popups/_call.scss b/src/scss/partials/popups/_call.scss index 095965cd..3b267b38 100644 --- a/src/scss/partials/popups/_call.scss +++ b/src/scss/partials/popups/_call.scss @@ -24,10 +24,15 @@ color: #fff; align-items: center; - &.is-full-screen { + &.is-full-screen, + html.is-mobile & { border-radius: 0; } + &.is-full-screen:not(.show-controls) { + cursor: none; + } + &.show-controls, &.no-video { .call-title, @@ -41,10 +46,25 @@ } } + &.show-controls { + .call-video { + opacity: .8; + } + + .call-video-blur { + opacity: .56; + } + } + .popup-header { .btn-icon { color: #fff; } + + .call-emojis { + transform: scale(1.3125); + margin-right: 1rem; + } } &-avatar { @@ -55,11 +75,10 @@ left: 0; z-index: -1; opacity: .7; + border-radius: inherit; - .avatar-photo { - width: 100%; - height: 100%; - object-fit: cover; + .avatar-full { + font-size: 6rem; } } @@ -182,6 +201,7 @@ object-fit: contain; position: absolute; border-radius: inherit; + opacity: 1; &-container { position: absolute; @@ -212,6 +232,13 @@ opacity: .7; border-radius: inherit; } + + &, + &-blur { + @include animation-level(2) { + transition: opacity var(--transition-standard-in); + } + } } // html.emoji-supported & { @@ -234,6 +261,7 @@ z-index: 2; width: 100%; align-items: center; + padding: 0 1rem; } &-party-state { @@ -252,7 +280,6 @@ opacity: 0; transform: scale(0) translateY(0); max-width: 100%; - padding: 0 1rem; @include animation-level(2) { transition: opacity var(--transition-standard-in), transform var(--transition-standard-in); @@ -262,7 +289,7 @@ width: 1.875rem !important; height: 1.875rem !important; margin-right: .25rem; - margin-left: -.625rem; + margin-left: -.25rem; flex: 0 0 auto; } @@ -275,11 +302,12 @@ // opacity: 0 !important; // } } + } - &-text { - max-width: 100%; - @include text-overflow(); - } + &-party-state-text, + &-title { + max-width: 100%; + @include text-overflow(); } &.two-button-rows {