/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import animationIntersector from '../../components/animationIntersector'; import appSidebarLeft, { LEFT_COLUMN_ACTIVE_CLASSNAME } from "../../components/sidebarLeft"; import appSidebarRight, { RIGHT_COLUMN_ACTIVE_CLASSNAME } from '../../components/sidebarRight'; import mediaSizes, { ScreenSize } from '../../helpers/mediaSizes'; import { logger, LogTypes } from "../logger"; import rootScope from '../rootScope'; import Chat, { ChatType } from '../../components/chat/chat'; import PopupNewMedia, { getCurrentNewMediaPopup } from '../../components/popups/newMedia'; import MarkupTooltip from '../../components/chat/markupTooltip'; import IS_TOUCH_SUPPORTED from '../../environment/touchSupport'; import SetTransition from '../../components/singleTransition'; import ChatDragAndDrop from '../../components/chat/dragAndDrop'; import { doubleRaf } from '../../helpers/schedulers'; import lottieLoader from '../rlottie/lottieLoader'; import useHeavyAnimationCheck, { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import stateStorage from '../stateStorage'; import { MOUNT_CLASS_TO } from '../../config/debug'; import appNavigationController from '../../components/appNavigationController'; import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search'; import I18n, { i18n, join, LangPackKey } from '../langPack'; import { ChatFull, ChatInvite, ChatParticipant, ChatParticipants, SendMessageAction } from '../../layer'; import { hslaStringToHex } from '../../helpers/color'; import PeerTitle from '../../components/peerTitle'; import PopupPeer from '../../components/popups/peer'; import blurActiveElement from '../../helpers/dom/blurActiveElement'; import cancelEvent from '../../helpers/dom/cancelEvent'; import disableTransition from '../../helpers/dom/disableTransition'; import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd'; import replaceContent from '../../helpers/dom/replaceContent'; import whichChild from '../../helpers/dom/whichChild'; import PopupElement from '../../components/popups'; import singleInstance, { InstanceDeactivateReason, SingleInstance } from '../mtproto/singleInstance'; import PopupStickers from '../../components/popups/stickers'; import PopupJoinChatInvite from '../../components/popups/joinChatInvite'; import { toast, toastNew } from '../../components/toast'; import debounce from '../../helpers/schedulers/debounce'; import pause from '../../helpers/schedulers/pause'; import { InternalLink, InternalLinkTypeMap, INTERNAL_LINK_TYPE } from './internalLink'; import MEDIA_MIME_TYPES_SUPPORTED from '../../environment/mediaMimeTypesSupport'; import { NULL_PEER_ID } from '../mtproto/mtproto_config'; import telegramMeWebManager from '../mtproto/telegramMeWebManager'; import { ONE_DAY } from '../../helpers/date'; import type { GroupCallId, MyGroupCall } from './appGroupCallsManager'; import TopbarCall from '../../components/topbarCall'; import confirmationPopup from '../../components/confirmationPopup'; import IS_GROUP_CALL_SUPPORTED from '../../environment/groupCallSupport'; import IS_CALL_SUPPORTED from '../../environment/callSupport'; import { CallType } from '../calls/types'; import { Modify, SendMessageEmojiInteractionData } from '../../types'; import htmlToSpan from '../../helpers/dom/htmlToSpan'; import getVisibleRect from '../../helpers/dom/getVisibleRect'; import { simulateClickEvent } from '../../helpers/dom/clickEvent'; 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'; import numberThousandSplitter from '../../helpers/number/numberThousandSplitter'; import ChatBackgroundPatternRenderer from '../../components/chat/patternRenderer'; import { IS_FIREFOX } from '../../environment/userAgent'; import compareVersion from '../../helpers/compareVersion'; import { AppManagers } from './managers'; import uiNotificationsManager from './uiNotificationsManager'; import appMediaPlaybackController from '../../components/appMediaPlaybackController'; import { PHONE_NUMBER_REG_EXP } from '../richTextProcessor'; import wrapEmojiText from '../richTextProcessor/wrapEmojiText'; import wrapRichText from '../richTextProcessor/wrapRichText'; import wrapUrl from '../richTextProcessor/wrapUrl'; import generateMessageId from './utils/messageId/generateMessageId'; import getUserStatusString from '../../components/wrappers/getUserStatusString'; import getChatMembersString from '../../components/wrappers/getChatMembersString'; import { STATE_INIT } from '../../config/state'; import CacheStorageController from '../cacheStorage'; import themeController from '../../helpers/themeController'; import overlayCounter from '../../helpers/overlayCounter'; import appDialogsManager from './appDialogsManager'; import idleController from '../../helpers/idleController'; import EventListenerBase from '../../helpers/eventListenerBase'; import { AckedResult } from '../mtproto/superMessagePort'; import groupCallsController from '../calls/groupCallsController'; import callsController from '../calls/callsController'; import getFilesFromEvent from '../../helpers/files/getFilesFromEvent'; import apiManagerProxy from '../mtproto/mtprotoworker'; import wrapPeerTitle from '../../components/wrappers/peerTitle'; import appRuntimeManager from './appRuntimeManager'; export const CHAT_ANIMATION_GROUP = 'chat'; export type ChatSavedPosition = { mids: number[], top: number }; export type ChatSetPeerOptions = { peerId?: PeerId, lastMsgId?: number, threadId?: number, startParam?: string }; export type ChatSetInnerPeerOptions = Modify & { type?: ChatType }; export class AppImManager extends EventListenerBase<{ chat_changing: (details: {from: Chat, to: Chat}) => void, peer_changed: (peerId: PeerId) => void, peer_changing: (chat: Chat) => void, }> { public columnEl = document.getElementById('column-center') as HTMLDivElement; public chatsContainer: HTMLElement; public offline = false; public updateStatusInterval = 0; public log: ReturnType; public setPeerPromise: Promise = null; public tabId = -1; public chats: Chat[] = []; private prevTab: HTMLElement; private chatsSelectTabDebounced: () => void; public markupTooltip: MarkupTooltip; private backgroundPromises: {[slug: string]: Promise}; private topbarCall: TopbarCall; public emojiAnimationContainer: HTMLDivElement; private lastBackgroundUrl: string; public managers: AppManagers; public cacheStorage = new CacheStorageController('cachedFiles'); get myId() { return rootScope.myId; } get chat(): Chat { return this.chats[this.chats.length - 1]; } public construct(managers: AppManagers) { this.managers = managers; const { apiUpdatesManager } = managers; apiUpdatesManager.attach(I18n.lastRequestedLangCode); appMediaPlaybackController.construct(managers); uiNotificationsManager.construct(managers); // uiNotificationsManager.start(); this.log = logger('IM', LogTypes.Log | LogTypes.Warn | LogTypes.Debug | LogTypes.Error); this.backgroundPromises = {}; STATE_INIT.settings.themes.forEach((theme) => { if(theme.background.slug) { const url = 'assets/img/' + theme.background.slug + '.svg' + (IS_FIREFOX ? '?1' : ''); this.backgroundPromises[theme.background.slug] = Promise.resolve(url); } }); this.selectTab(0); idleController.addEventListener('change', (idle) => { this.offline = idle; this.updateStatus(); if(idle) { clearInterval(this.updateStatusInterval); } else { this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3); } }); this.chatsContainer = document.createElement('div'); this.chatsContainer.classList.add('chats-container', 'tabs-container'); this.chatsContainer.dataset.animation = 'navigation'; this.emojiAnimationContainer = document.createElement('div'); this.emojiAnimationContainer.classList.add('emoji-animation-container'); this.appendEmojiAnimationContainer(mediaSizes.activeScreen); this.columnEl.append(this.chatsContainer); this.createNewChat(); this.chatsSelectTab(this.chat.container); appNavigationController.onHashChange = this.onHashChange; //window.addEventListener('hashchange', this.onHashChange); this.setSettings(); rootScope.addEventListener('settings_updated', this.setSettings); useHeavyAnimationCheck(() => { animationIntersector.setOnlyOnePlayableGroup('lock'); animationIntersector.checkAnimations(true); }, () => { animationIntersector.setOnlyOnePlayableGroup(''); animationIntersector.checkAnimations(false); }); if(IS_FIREFOX && apiManagerProxy.oldVersion && compareVersion(apiManagerProxy.oldVersion, '1.4.3') === -1) { this.deleteFilesIterative((response) => { return response.headers.get('Content-Type') === 'image/svg+xml'; }).then(() => { this.applyCurrentTheme(); }); } else { this.applyCurrentTheme(); } // * fix simultaneous opened both sidebars, can happen when floating sidebar is opened with left sidebar mediaSizes.addEventListener('changeScreen', (from, to) => { if(document.body.classList.contains(LEFT_COLUMN_ACTIVE_CLASSNAME) && document.body.classList.contains(RIGHT_COLUMN_ACTIVE_CLASSNAME)) { appSidebarRight.toggleSidebar(false); } this.appendEmojiAnimationContainer(to); }); mediaSizes.addEventListener('resize', () => { // const perf = performance.now(); const rect = this.chatsContainer.getBoundingClientRect(); ChatBackgroundPatternRenderer.resizeInstances(rect.width, rect.height).then(() => { // this.log.warn('resize bg time:', performance.now() - perf); // for(const chat of this.chats) { // if(chat.renderDarkPattern) { // chat.renderDarkPattern(); // } // } }); }); this.addEventListener('peer_changing', (chat) => { this.saveChatPosition(chat); }); rootScope.addEventListener('theme_change', () => { this.applyCurrentTheme(); }); rootScope.addEventListener('choosing_sticker', (choosing) => { this.setChoosingStickerTyping(!choosing); }); rootScope.addEventListener('peer_typings', ({peerId, typings}) => { const chat = this.chat; if( !chat || chat.peerId !== peerId || overlayCounter.isOverlayActive || ( mediaSizes.activeScreen === ScreenSize.mobile && this.tabId !== 1 ) ) { return; } const typing = typings.find((typing) => typing.action._ === 'sendMessageEmojiInteraction'); if(typing?.action?._ === 'sendMessageEmojiInteraction') { const action = typing.action; const bubble = chat.bubbles.bubbles[generateMessageId(typing.action.msg_id)]; if(bubble && bubble.classList.contains('emoji-big') && bubble.classList.contains('sticker') && getVisibleRect(bubble, chat.bubbles.scrollable.container)) { const stickerWrapper: HTMLElement = bubble.querySelector('.media-sticker-wrapper:not(.bubble-hover-reaction-sticker):not(.reaction-sticker)'); const data: SendMessageEmojiInteractionData = JSON.parse(action.interaction.data); data.a.forEach((a) => { setTimeout(() => { simulateClickEvent(stickerWrapper); }, a.t * 1000); }); this.managers.appMessagesManager.setTyping(peerId, { _: 'sendMessageEmojiInteractionSeen', emoticon: action.emoticon }); } } }); const onInstanceDeactivated = (reason: InstanceDeactivateReason) => { const isUpdated = reason === 'version'; const popup = new PopupElement('popup-instance-deactivated', undefined, {overlayClosable: true}); const c = document.createElement('div'); c.classList.add('instance-deactivated-container'); (popup as any).container.replaceWith(c); const header = document.createElement('div'); header.classList.add('header'); header.append(i18n(isUpdated ? 'Deactivated.Version.Title' : 'Deactivated.Title')); const subtitle = document.createElement('div'); subtitle.classList.add('subtitle'); subtitle.append(i18n(isUpdated ? 'Deactivated.Version.Subtitle' : 'Deactivated.Subtitle')); c.append(header, subtitle); document.body.classList.add('deactivated'); const onClose = isUpdated ? () => { appRuntimeManager.reload(); } : () => { document.body.classList.add('deactivated-backwards'); singleInstance.activateInstance(); setTimeout(() => { document.body.classList.remove('deactivated', 'deactivated-backwards'); }, 333); }; popup.addEventListener('close', onClose); popup.show(); }; singleInstance.addEventListener('deactivated', onInstanceDeactivated); if(singleInstance.deactivatedReason) { onInstanceDeactivated(singleInstance.deactivatedReason); } // remove scroll listener when setting chat to tray this.addEventListener('chat_changing', ({to}) => { this.toggleChatGradientAnimation(to); }); rootScope.addEventListener('service_notification', (update) => { confirmationPopup({ button: {langKey: 'OK', isCancel: true}, description: wrapRichText(update.message) }); }); apiManagerProxy.addEventListener('notificationBuild', (options) => { if(this.chat.peerId === options.message.peerId && !idleController.isIdle) { return; } uiNotificationsManager.buildNotification(options); }); this.addEventListener('peer_changed', async(peerId) => { document.body.classList.toggle('has-chat', !!peerId); let str: string; if(peerId) { const username = await this.managers.appPeersManager.getPeerUsername(peerId); str = username ? '@' + username : '' + peerId; } appNavigationController.overrideHash(str); apiManagerProxy.updateTabState('chatPeerIds', this.chats.map((chat) => chat.peerId).filter(Boolean)); }); // stateStorage.get('chatPositions').then((c) => { stateStorage.setToCache('chatPositions', /* c || */{}); // }); if(IS_CALL_SUPPORTED || IS_GROUP_CALL_SUPPORTED) { this.topbarCall = new TopbarCall(managers); } if(IS_CALL_SUPPORTED) { callsController.addEventListener('instance', ({instance/* , hasCurrent */}) => { // if(hasCurrent) { // return; // } const popup = new PopupCall(instance); instance.addEventListener('acceptCallOverride', () => { return this.discardCurrentCall(instance.interlocutorUserId.toPeerId(), undefined, instance) .then(() => { callsController.dispatchEvent('accepting', instance); return true; }) .catch(() => false); }); popup.addEventListener('close', () => { const currentCall = callsController.currentCall; if(currentCall && currentCall !== instance && !instance.wasTryingToJoin) { instance.hangUp('phoneCallDiscardReasonBusy'); } }, {once: true}); popup.show(); }); callsController.addEventListener('incompatible', (userId) => { toastNew({ langPackKey: 'VoipPeerIncompatible', langPackArguments: [ new PeerTitle({peerId: userId.toPeerId()}).element ] }); }); } // ! do not remove this line // ! instance can be deactivated before the UI starts, because it waits in background for RAF that is delayed singleInstance.activateInstance(); const setAuthorized = () => { telegramMeWebManager.setAuthorized(true); }; setInterval(setAuthorized, ONE_DAY); setAuthorized(); this.addAnchorListener<{}>({ name: 'showMaskedAlert', callback: (params, element) => { const href = element.href; const a = element.cloneNode(true) as HTMLAnchorElement; a.className = 'anchor-url'; a.innerText = href; a.removeAttribute('onclick'); new PopupPeer('popup-masked-url', { titleLangKey: 'OpenUrlTitle', descriptionLangKey: 'OpenUrlAlert2', descriptionLangArgs: [a], buttons: [{ langKey: 'Open', callback: () => { a.click(); }, }] }).show(); } }); this.addAnchorListener<{uriParams: {command: string, bot: string}}>({ name: 'execBotCommand', callback: ({uriParams}) => { const {command, bot} = uriParams; /* const promise = bot ? this.openUsername(bot).then(() => this.chat.peerId) : Promise.resolve(this.chat.peerId); promise.then((peerId) => { this.managers.appMessagesManager.sendText(peerId, '/' + command); }); */ this.managers.appMessagesManager.sendText(this.chat.peerId, '/' + command + (bot ? '@' + bot : '')); //console.log(command, bot); } }); this.addAnchorListener<{uriParams: {hashtag: string}}>({ name: 'searchByHashtag', callback: ({uriParams}) => { const {hashtag} = uriParams; if(!hashtag) { return; } this.chat.initSearch('#' + hashtag + ' '); } }); this.addAnchorListener<{pathnameParams: ['addstickers', string]}>({ name: 'addstickers', callback: ({pathnameParams}) => { const link: InternalLink = { _: INTERNAL_LINK_TYPE.STICKER_SET, set: pathnameParams[1] }; this.processInternalLink(link); } }); // Support old t.me/joinchat/asd and new t.me/+asd this.addAnchorListener<{pathnameParams: ['joinchat', string]}>({ name: 'joinchat', callback: ({pathnameParams}) => { const link: InternalLink = { _: INTERNAL_LINK_TYPE.JOIN_CHAT, invite: pathnameParams[1] || decodeURIComponent(pathnameParams[0]).slice(1) }; this.processInternalLink(link); } }); if(IS_GROUP_CALL_SUPPORTED) { this.addAnchorListener<{ uriParams: Omit }>({ name: 'voicechat', protocol: 'tg', callback: ({uriParams}) => { const link = this.makeLink(INTERNAL_LINK_TYPE.VOICE_CHAT, uriParams); this.processInternalLink(link); } }); } this.addAnchorListener<{ // pathnameParams: ['c', string, string], // uriParams: {thread?: number} // } | { // pathnameParams: [string, string?], // uriParams: {comment?: number} pathnameParams: ['c', string, string] | [string, string?], uriParams: {thread?: string, comment?: string} | {comment?: string, start?: string} }>({ name: 'im', callback: async({pathnameParams, uriParams}) => { let link: InternalLink; if(PHONE_NUMBER_REG_EXP.test(pathnameParams[0])) { link = { _: INTERNAL_LINK_TYPE.USER_PHONE_NUMBER, phone: pathnameParams[0].slice(1) }; } else if(pathnameParams[0] === 'c') { link = { _: INTERNAL_LINK_TYPE.PRIVATE_POST, channel: pathnameParams[1], post: pathnameParams[2], thread: 'thread' in uriParams && uriParams.thread, comment: uriParams.comment }; } else { link = { _: INTERNAL_LINK_TYPE.MESSAGE, domain: pathnameParams[0], post: pathnameParams[1], comment: uriParams.comment, start: 'start' in uriParams ? uriParams.start : undefined }; } this.processInternalLink(link); } }); this.addAnchorListener<{ uriParams: { domain: string, // telegrampassport scope?: string, nonce?: string, payload?: string, bot_id?: string, public_key?: string, callback_url?: string, // regular start?: string, startgroup?: string, game?: string, voicechat?: string, post?: string, thread?: string, comment?: string, phone?: string } }>({ name: 'resolve', protocol: 'tg', callback: ({uriParams}) => { let link: InternalLink; if(uriParams.phone) { link = this.makeLink(INTERNAL_LINK_TYPE.USER_PHONE_NUMBER, uriParams as Required); } else if(uriParams.domain === 'telegrampassport') { } else { link = this.makeLink(INTERNAL_LINK_TYPE.MESSAGE, uriParams); } this.processInternalLink(link); } }); this.addAnchorListener<{ uriParams: { channel: string, post: string, thread?: string, comment?: string } }>({ name: 'privatepost', protocol: 'tg', callback: ({uriParams}) => { const link = this.makeLink(INTERNAL_LINK_TYPE.PRIVATE_POST, uriParams); this.processInternalLink(link); } }); this.addAnchorListener<{ uriParams: { set: string } }>({ name: 'addstickers', protocol: 'tg', callback: ({uriParams}) => { const link = this.makeLink(INTERNAL_LINK_TYPE.STICKER_SET, uriParams); this.processInternalLink(link); } }); ['joinchat' as const, 'join' as const].forEach((name) => { this.addAnchorListener<{ uriParams: { invite: string } }>({ name, protocol: 'tg', callback: ({uriParams}) => { const link = this.makeLink(INTERNAL_LINK_TYPE.JOIN_CHAT, uriParams); this.processInternalLink(link); } }); }); this.onHashChange(true); this.attachKeydownListener(); } private deleteFilesIterative(callback: (response: Response) => boolean) { return this.cacheStorage.timeoutOperation((cache) => { const perf = performance.now(); return cache.keys().then((requests) => { const promises = requests.map((request) => { return cache.match(request).then((response) => { return callback(response); }); }); return Promise.all(promises).then((values) => { values.map((isBad, idx) => { if(!isBad) { return; } const request = requests[idx]; return cache.delete(request); }); return Promise.all(values.filter(Boolean)); }); }).then(() => { this.log('deleted files', performance.now() - perf); }); }); } private toggleChatGradientAnimation(activatingChat: Chat) { this.chats.forEach((chat) => { if(chat.gradientRenderer) { chat.gradientRenderer.scrollAnimate(rootScope.settings.animationsEnabled && chat === activatingChat); } }); } private appendEmojiAnimationContainer(screen: ScreenSize) { const appendTo = screen === ScreenSize.mobile ? this.columnEl : document.body; if(this.emojiAnimationContainer.parentElement !== appendTo) { appendTo.append(this.emojiAnimationContainer) } } private attachKeydownListener() { const IGNORE_KEYS = new Set(['PageUp', 'PageDown', 'Meta', 'Control']); const onKeyDown = (e: KeyboardEvent) => { const key = e.key; if(overlayCounter.isOverlayActive || IGNORE_KEYS.has(key)) return; const target = e.target as HTMLElement; //if(target.tagName === 'INPUT') return; //this.log('onkeydown', e, document.activeElement); const chat = this.chat; if(e.code === 'KeyC' && (e.ctrlKey || e.metaKey) && target.tagName !== 'INPUT') { return; } else if(e.altKey && (key === 'ArrowUp' || key === 'ArrowDown')) { cancelEvent(e); this.managers.dialogsStorage.getNextDialog(this.chat.peerId, key === 'ArrowDown', appDialogsManager.filterId).then((dialog) => { if(dialog) { this.setPeer({peerId: dialog.peerId}); } }); } else if(key === 'ArrowUp' && this.chat.type !== 'scheduled') { if(!chat.input.editMsgId && chat.input.isInputEmpty()) { this.managers.appMessagesManager.getFirstMessageToEdit(chat.peerId, chat.threadId).then((message) => { if(message) { chat.input.initMessageEditing(message.mid); cancelEvent(e); // * prevent from scrolling } }); } else { return; } } else if(key === 'ArrowDown') { return; } if( chat?.input?.messageInput && e.target !== chat.input.messageInput && target.tagName !== 'INPUT' && !target.hasAttribute('contenteditable') && !IS_TOUCH_SUPPORTED && (!mediaSizes.isMobile || this.tabId === 1) && !chat.selection.isSelecting && !chat.input.recording ) { chat.input.messageInput.focus(); placeCaretAtEnd(chat.input.messageInput); // clone and dispatch same event to new input. it is needed for sending message if input was blurred const newEvent = new KeyboardEvent(e.type, e); chat.input.messageInput.dispatchEvent(newEvent); } }; document.body.addEventListener('keydown', onKeyDown); } private makeLink(type: T, uriParams: Omit) { return { _: type, ...uriParams } as any as InternalLinkTypeMap[T]; } public async processInternalLink(link: InternalLink) { switch(link?._) { case INTERNAL_LINK_TYPE.MESSAGE: { const postId = link.post ? generateMessageId(+link.post) : undefined; const commentId = link.comment ? generateMessageId(+link.comment) : undefined; this.openUsername({ userName: link.domain, lastMsgId: postId, commentId, startParam: link.start }); break; } case INTERNAL_LINK_TYPE.PRIVATE_POST: { const chatId = link.channel.toChatId(); const peerId = chatId.toPeerId(true); const chat = await this.managers.appChatsManager.getChat(chatId); if(chat.deleted) { try { await this.managers.appChatsManager.resolveChannel(chatId); } catch(err) { toastNew({langPackKey: 'LinkNotFound'}); throw err; } } const postId = generateMessageId(+link.post); const threadId = link.thread ? generateMessageId(+link.thread) : undefined; if(threadId) this.openThread(peerId, postId, threadId); else this.setInnerPeer({ peerId, lastMsgId: postId, threadId }); break; } case INTERNAL_LINK_TYPE.STICKER_SET: { new PopupStickers({id: link.set}).show(); break; } case INTERNAL_LINK_TYPE.JOIN_CHAT: { this.managers.appChatsManager.checkChatInvite(link.invite).then((chatInvite) => { if((chatInvite as ChatInvite.chatInvitePeek).chat) { this.managers.appChatsManager.saveApiChat((chatInvite as ChatInvite.chatInvitePeek).chat, true); } // console.log(chatInvite); if(chatInvite._ === 'chatInviteAlready' || chatInvite._ === 'chatInvitePeek'/* && chatInvite.expires > tsNow(true) */) { this.setInnerPeer({ peerId: chatInvite.chat.id.toPeerId(true) }); return; } new PopupJoinChatInvite(link.invite, chatInvite); }, (err) => { if(err.type === 'INVITE_HASH_EXPIRED') { toast(i18n('InviteExpired')); } }); break; } case INTERNAL_LINK_TYPE.VOICE_CHAT: { if(IS_GROUP_CALL_SUPPORTED) { this.joinGroupCall(link.chat_id.toPeerId(true), link.id); } break; } case INTERNAL_LINK_TYPE.USER_PHONE_NUMBER: { this.managers.appUsersManager.resolvePhone(link.phone).then((user) => { this.setInnerPeer({ peerId: user.id.toPeerId(false) }); }).catch((err) => { if(err.type === 'PHONE_NOT_OCCUPIED') { toastNew({langPackKey: 'Alert.UserDoesntExists'}); } }); break; } default: { this.log.warn('Not supported internal link:', link); break; } } } public openUrl(url: string) { const {url: wrappedUrl, onclick} = wrapUrl(url); const a = document.createElement('a'); a.href = wrappedUrl; (window as any)[onclick](a); } private addAnchorListener(options: { name: 'showMaskedAlert' | 'execBotCommand' | 'searchByHashtag' | 'addstickers' | 'im' | 'resolve' | 'privatepost' | 'addstickers' | 'voicechat' | 'joinchat' | 'join', protocol?: 'tg', callback: (params: Params, element?: HTMLAnchorElement) => boolean | any, noPathnameParams?: boolean, noUriParams?: boolean }) { (window as any)[(options.protocol ? options.protocol + '_' : '') + options.name] = (element?: HTMLAnchorElement/* , e: Event */) => { cancelEvent(null); const href = element.href; let pathnameParams: any[]; let uriParams: any; if(!options.noPathnameParams) pathnameParams = new URL(element.href).pathname.split('/').slice(1); if(!options.noUriParams) uriParams = this.parseUriParams(href); const res = options.callback({pathnameParams, uriParams} as Params, element); return res === undefined ? res : false; }; } private parseUriParams(uri: string, splitted = uri.split('?')) { const params: any = {}; if(!splitted[1]) return params; splitted[1].split('&').forEach((item) => { params[item.split('=')[0]] = decodeURIComponent(item.split('=')[1]); }); return params; } private onHashChange = (saveState?: boolean) => { const hash = location.hash; if(!saveState) { appNavigationController.replaceState(); } const splitted = hash.split('?'); const params = this.parseUriParams(hash, splitted); this.log('hashchange', hash, splitted[0], params); if(!hash) { return; } if(params.tgaddr) { const {onclick} = wrapUrl(params.tgaddr); if(onclick) { const a = document.createElement('a'); a.href = params.tgaddr; (window as any)[onclick](a); } return; } switch(splitted[0]) { default: { params.p = splitted[0].slice(1); } case '#/im': { const p: string = params.p; let postId = params.post !== undefined ? generateMessageId(+params.post) : undefined; switch(p[0]) { case '@': { this.openUsername({ userName: p, lastMsgId: postId }); break; } default: { // peerId this.setInnerPeer({ peerId: postId ? p.toPeerId(true) : p.toPeerId(), lastMsgId: postId }); break; } } } } //appNavigationController.replaceState(); //location.hash = ''; }; public openUsername(options: { userName: string, lastMsgId?: number, threadId?: number, commentId?: number, startParam?: string }) { const {userName, lastMsgId, threadId, commentId, startParam} = options; return this.managers.appUsersManager.resolveUsername(userName).then((peer) => { const isUser = peer._ === 'user'; const peerId = peer.id.toPeerId(!isUser); if(threadId) { return this.openThread(peerId, lastMsgId, threadId); } else if(commentId) { return this.openComment(peerId, lastMsgId, commentId); } return this.setInnerPeer({ peerId, lastMsgId, startParam: startParam }); }, (err) => { if(err.type === 'USERNAME_NOT_OCCUPIED') { toastNew({langPackKey: 'NoUsernameFound'}); } else if(err.type === 'USERNAME_INVALID') { toastNew({langPackKey: 'Alert.UserDoesntExists'}); } }); } /** * Opens thread when peerId of discussion group is known */ public openThread(peerId: PeerId, lastMsgId: number, threadId: number) { return this.managers.appMessagesManager.wrapSingleMessage(peerId, threadId).then((message) => { // const message: Message = this.managers.appMessagesManager.getMessageByPeer(peerId, threadId); if(!message) { lastMsgId = undefined; } else { this.managers.appMessagesManager.generateThreadServiceStartMessage(message); } return this.setInnerPeer({ peerId, lastMsgId, threadId, type: 'discussion' }); }); } /** * Opens comment directly from original channel */ public openComment(peerId: PeerId, msgId: number, commentId: number) { return this.managers.appMessagesManager.getDiscussionMessage(peerId, msgId).then((message) => { return this.openThread(message.peerId, commentId, message.mid); }); } public async callUser(userId: UserId, type: CallType) { const call = callsController.getCallByUserId(userId); if(call) { return; } const userFull = await this.managers.appProfileManager.getProfile(userId); if(userFull.pFlags.phone_calls_private) { confirmationPopup({ descriptionLangKey: 'Call.PrivacyErrorMessage', descriptionLangArgs: [new PeerTitle({peerId: userId.toPeerId()}).element], button: { langKey: 'OK', isCancel: true } }); return; } await this.discardCurrentCall(userId.toPeerId()); callsController.startCallInternal(userId, type === 'video'); } private discardCurrentCall(toPeerId: PeerId, ignoreGroupCall?: GroupCallInstance, ignoreCall?: CallInstance) { if(groupCallsController.groupCall && groupCallsController.groupCall !== ignoreGroupCall) return this.discardGroupCallConfirmation(toPeerId); else if(callsController.currentCall && callsController.currentCall !== ignoreCall) return this.discardCallConfirmation(toPeerId); else return Promise.resolve(); } private async discardCallConfirmation(toPeerId: PeerId) { const currentCall = callsController.currentCall; if(currentCall) { await confirmationPopup({ titleLangKey: 'Call.Confirm.Discard.Call.Header', descriptionLangKey: toPeerId.isUser() ? 'Call.Confirm.Discard.Call.ToCall.Text' : 'Call.Confirm.Discard.Call.ToVoice.Text', descriptionLangArgs: [ new PeerTitle({peerId: currentCall.interlocutorUserId.toPeerId(false)}).element, new PeerTitle({peerId: toPeerId}).element ], button: { langKey: 'OK' } }); if(!currentCall.isClosing) { await currentCall.hangUp('phoneCallDiscardReasonDisconnect'); } } } private async discardGroupCallConfirmation(toPeerId: PeerId) { const currentGroupCall = groupCallsController.groupCall; if(currentGroupCall) { await confirmationPopup({ titleLangKey: 'Call.Confirm.Discard.Voice.Header', descriptionLangKey: toPeerId.isUser() ? 'Call.Confirm.Discard.Voice.ToCall.Text' : 'Call.Confirm.Discard.Voice.ToVoice.Text', descriptionLangArgs: [ new PeerTitle({peerId: currentGroupCall.chatId.toPeerId(true)}).element, new PeerTitle({peerId: toPeerId}).element ], button: { langKey: 'OK' } }); if(groupCallsController.groupCall === currentGroupCall) { await currentGroupCall.hangUp(); } } } public async joinGroupCall(peerId: PeerId, groupCallId?: GroupCallId) { const chatId = peerId.toChatId(); const hasRights = this.managers.appChatsManager.hasRights(chatId, 'manage_call'); const next = async() => { const chatFull = await this.managers.appProfileManager.getChatFull(chatId); let call: MyGroupCall; if(!chatFull.call) { if(!hasRights) { return; } call = await this.managers.appGroupCallsManager.createGroupCall(chatId); } else { call = chatFull.call; } groupCallsController.joinGroupCall(chatId, call.id, true, false); }; if(groupCallId) { const groupCall = await this.managers.appGroupCallsManager.getGroupCallFull(groupCallId); if(groupCall._ === 'groupCallDiscarded') { if(!hasRights) { toastNew({ langPackKey: 'VoiceChat.Chat.Ended' }); return; } await confirmationPopup({ descriptionLangKey: 'VoiceChat.Chat.StartNew', button: { langKey: 'VoiceChat.Chat.StartNew.OK' } }); } } // await this.discardCurrentCall(peerId); next(); }; public setCurrentBackground(broadcastEvent = false): ReturnType { const theme = themeController.getTheme(); if(theme.background.slug) { const defaultTheme = STATE_INIT.settings.themes.find((t) => t.name === theme.name); // const isDefaultBackground = theme.background.blur === defaultTheme.background.blur && // theme.background.slug === defaultTheme.background.slug; // if(!isDefaultBackground) { return this.getBackground(theme.background.slug).then((url) => { return this.setBackground(url, broadcastEvent); }, () => { // * if NO_ENTRY_FOUND theme.background = copy(defaultTheme.background); // * reset background return this.setCurrentBackground(true); }); // } } return this.setBackground('', broadcastEvent); } private getBackground(slug: string) { if(this.backgroundPromises[slug]) return this.backgroundPromises[slug]; return this.backgroundPromises[slug] = this.cacheStorage.getFile('backgrounds/' + slug).then((blob) => { return URL.createObjectURL(blob); }); } public setBackground(url: string, broadcastEvent = true): Promise { this.lastBackgroundUrl = url; const promises = this.chats.map((chat) => chat.setBackground(url)); return promises[promises.length - 1].then(() => { if(broadcastEvent) { rootScope.dispatchEvent('background_change'); } }); } public saveChatPosition(chat: Chat) { if(!(['chat', 'discussion'] as ChatType[]).includes(chat.type) || !chat.peerId) { return; } //const bubble = chat.bubbles.getBubbleByPoint('top'); //if(bubble) { //const top = bubble.getBoundingClientRect().top; const chatBubbles = chat.bubbles; const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : ''); const chatPositions = stateStorage.getFromCache('chatPositions'); if(!(chatBubbles.scrollable.getDistanceToEnd() <= 16 && chatBubbles.scrollable.loadedAll.bottom) && chatBubbles.getRenderedLength()) { chatBubbles.sliceViewport(true); const top = chatBubbles.scrollable.scrollTop; const position = { mids: getObjectKeysAndSort(chatBubbles.bubbles, 'desc').filter((mid) => !chatBubbles.skippedMids.has(mid)), top }; chatPositions[key] = position; this.log('saved chat position:', position); } else { delete chatPositions[key]; this.log('deleted chat position'); } stateStorage.set({chatPositions}, true); //} } public getChatSavedPosition(chat: Chat): ChatSavedPosition { if(!(['chat', 'discussion'] as ChatType[]).includes(chat.type) || !chat.peerId) { return; } const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : ''); const cache = stateStorage.getFromCache('chatPositions'); return cache && cache[key]; } public applyCurrentTheme(slug?: string, backgroundUrl?: string, broadcastEvent?: boolean) { if(backgroundUrl) { this.backgroundPromises[slug] = Promise.resolve(backgroundUrl); } themeController.setTheme(); return this.setCurrentBackground(broadcastEvent === undefined ? !!slug : broadcastEvent); } private setSettings = () => { document.documentElement.style.setProperty('--messages-text-size', rootScope.settings.messagesTextSize + 'px'); document.body.classList.toggle('animation-level-0', !rootScope.settings.animationsEnabled); document.body.classList.toggle('animation-level-1', false); document.body.classList.toggle('animation-level-2', rootScope.settings.animationsEnabled); this.chatsSelectTabDebounced = debounce(() => { const topbar = this.chat.topbar; if(topbar.pinnedMessage) { // * буду молиться богам, чтобы это ничего не сломало, но это исправляет получение пиннеда после анимации topbar.pinnedMessage.setCorrectIndex(0); } this.managers.apiFileManager.setQueueId(this.chat.bubbles.lazyLoadQueue.queueId); }, rootScope.settings.animationsEnabled ? 250 : 0, false, true); lottieLoader.setLoop(rootScope.settings.stickers.loop); animationIntersector.checkAnimations(false); for(const chat of this.chats) { chat.setAutoDownloadMedia(); } I18n.setTimeFormat(rootScope.settings.timeFormat); this.toggleChatGradientAnimation(this.chat); }; // * не могу использовать тут TransitionSlider, так как мне нужен отрисованный блок рядом // * (или под текущим чатом) чтобы правильно отрендерить чат (напр. scrollTop) private chatsSelectTab(tab: HTMLElement, animate?: boolean) { if(this.prevTab === tab) { return; } if(animate === false && this.prevTab) { // * will be used for Safari iOS history swipe disableTransition([tab, this.prevTab].filter(Boolean)); } if(this.prevTab) { this.prevTab.classList.remove('active'); this.chatsSelectTabDebounced(); // ! нужно переделать на animation, так как при лаге анимация будет длиться не 250мс if(rootScope.settings.animationsEnabled && animate !== false) { dispatchHeavyAnimationEvent(pause(250 + 150), 250 + 150); } const prevIdx = whichChild(this.prevTab); const idx = whichChild(tab); if(idx > prevIdx) { appNavigationController.pushItem({ type: 'chat', onPop: (canAnimate) => { this.setPeer({}, canAnimate); blurActiveElement(); } }); } } tab.classList.add('active'); this.prevTab = tab; } private init() { document.addEventListener('paste', this.onDocumentPaste, true); if(!IS_TOUCH_SUPPORTED) { this.attachDragAndDropListeners(); } //if(!isTouchSupported) { this.markupTooltip = new MarkupTooltip(this); this.markupTooltip.handleSelection(); //} } private attachDragAndDropListeners() { const drops: ChatDragAndDrop[] = []; const mediaDrops: ChatDragAndDrop[] = []; let mounted = false; const toggle = async(e: DragEvent, mount: boolean) => { if(mount === mounted) return; const _types = e.dataTransfer.types; // @ts-ignore const isFiles = _types.contains ? _types.contains('Files') : _types.indexOf('Files') >= 0; const newMediaPopup = getCurrentNewMediaPopup(); const types: string[] = await getFilesFromEvent(e, true); if(!isFiles || (!(await this.canDrag()) && !newMediaPopup)) { // * skip dragging text case counter = 0; return; } const _dropsContainer = newMediaPopup ? mediaDropsContainer : dropsContainer; const _drops = newMediaPopup ? mediaDrops : drops; if(mount && !_drops.length) { const force = isFiles && !types.length; // * can't get file items not from 'drop' on Safari const foundMedia = types.filter((t) => MEDIA_MIME_TYPES_SUPPORTED.has(t)).length; // const foundDocuments = types.length - foundMedia; this.log('drag files', types); if(newMediaPopup) { newMediaPopup.appendDrops(_dropsContainer); if(types.length || force) { _drops.push(new ChatDragAndDrop(_dropsContainer, { header: 'Preview.Dragging.AddItems', headerArgs: [types.length], onDrop: (e: DragEvent) => { toggle(e, false); this.log('drop', e); this.onDocumentPaste(e, 'document'); } })); } } else { if(types.length || force) { _drops.push(new ChatDragAndDrop(_dropsContainer, { icon: 'dragfiles', header: 'Chat.DropTitle', subtitle: 'Chat.DropAsFilesDesc', onDrop: (e: DragEvent) => { toggle(e, false); this.log('drop', e); this.onDocumentPaste(e, 'document'); } })); } // if((foundMedia && !foundDocuments) || force) { if(foundMedia || force) { _drops.push(new ChatDragAndDrop(_dropsContainer, { icon: 'dragmedia', header: 'Chat.DropTitle', subtitle: 'Chat.DropQuickDesc', onDrop: (e: DragEvent) => { toggle(e, false); this.log('drop', e); this.onDocumentPaste(e, 'media'); } })); } this.chat.container.append(_dropsContainer); } } //if(!mount) return; SetTransition(_dropsContainer, 'is-visible', mount, 200, () => { if(!mount) { _drops.forEach((drop) => { drop.destroy(); }); _drops.length = 0; } }); if(mount) { _drops.forEach((drop) => { drop.setPath(); }); } else { counter = 0; } document.body.classList.toggle('is-dragging', mount); mounted = mount; }; /* document.body.addEventListener('dragover', (e) => { cancelEvent(e); }); */ let counter = 0; document.body.addEventListener('dragenter', (e) => { counter++; }); document.body.addEventListener('dragover', (e) => { //this.log('dragover', e/* , e.dataTransfer.types[0] */); toggle(e, true); cancelEvent(e); }); document.body.addEventListener('dragleave', (e) => { //this.log('dragleave', e, counter); //if((e.pageX <= 0 || e.pageX >= this.managers.appPhotosManager.windowW) || (e.pageY <= 0 || e.pageY >= this.managers.appPhotosManager.windowH)) { counter--; if(counter === 0) { //if(!findUpClassName(e.target, 'drops-container')) { toggle(e, false); } }); const dropsContainer = document.createElement('div'); dropsContainer.classList.add('drops-container'); const mediaDropsContainer = dropsContainer.cloneNode(true) as HTMLElement; } private async canDrag() { const chat = this.chat; const peerId = chat?.peerId; return !(!peerId || overlayCounter.isOverlayActive || !(await chat.canSend('send_media'))); } private onDocumentPaste = async(e: ClipboardEvent | DragEvent, attachType?: 'media' | 'document') => { const newMediaPopup = getCurrentNewMediaPopup(); //console.log('document paste'); //console.log('item', event.clipboardData.getData()); if(e instanceof DragEvent) { const _types = e.dataTransfer.types; // @ts-ignore const isFiles = _types.contains ? _types.contains('Files') : _types.indexOf('Files') >= 0; if(isFiles) { cancelEvent(e); } } const files = await getFilesFromEvent(e); if(!(await this.canDrag()) && !newMediaPopup) return; if(files.length) { if(newMediaPopup) { newMediaPopup.addFiles(files); return; } const chatInput = this.chat.input; chatInput.willAttachType = attachType || (MEDIA_MIME_TYPES_SUPPORTED.has(files[0].type) ? 'media' : 'document'); PopupElement.createPopup(PopupNewMedia, this.chat, files, chatInput.willAttachType); } }; public selectTab(id: number, animate?: boolean) { if(animate === false) { // * will be used for Safari iOS history swipe disableTransition([appSidebarLeft.sidebarEl, this.columnEl, appSidebarRight.sidebarEl]); } document.body.classList.toggle(LEFT_COLUMN_ACTIVE_CLASSNAME, id === 0); const prevTabId = this.tabId; this.log('selectTab', id, prevTabId); let animationPromise: Promise = rootScope.settings.animationsEnabled ? doubleRaf() : Promise.resolve(); if(prevTabId !== -1 && prevTabId !== id && rootScope.settings.animationsEnabled && animate !== false && mediaSizes.activeScreen !== ScreenSize.large) { const transitionTime = (mediaSizes.isMobile ? 250 : 200) + 100; // * cause transition time could be > 250ms animationPromise = pause(transitionTime); dispatchHeavyAnimationEvent(animationPromise, transitionTime); // ! it's very heavy operation. will blink in firefox /* this.columnEl.classList.add('disable-hover'); animationPromise.finally(() => { this.columnEl.classList.remove('disable-hover'); }); */ } this.tabId = id; blurActiveElement(); if(mediaSizes.isMobile && prevTabId === 2 && id < 2) { document.body.classList.remove(RIGHT_COLUMN_ACTIVE_CLASSNAME); } if(prevTabId !== -1 && id > prevTabId) { if(id < 2 || !appNavigationController.findItemByType('im')) { appNavigationController.pushItem({ type: 'im', onPop: (canAnimate) => { //this.selectTab(prevTabId, !isSafari); this.setPeer({}, canAnimate); } }); } } const onImTabChange = (window as any).onImTabChange; onImTabChange && onImTabChange(id); //this._selectTab(id, mediaSizes.isMobile); //document.body.classList.toggle(RIGHT_COLUMN_ACTIVE_CLASSNAME, id === 2); return animationPromise; } public updateStatus() { return this.managers.appUsersManager.updateMyOnlineStatus(this.offline); } private createNewChat() { const chat = new Chat( this, this.managers ); if(this.chats.length) { chat.setBackground(this.lastBackgroundUrl, true); } this.chats.push(chat); return chat; } private spliceChats(fromIndex: number, justReturn = true, animate?: boolean, spliced?: Chat[]) { if(fromIndex >= this.chats.length) return; const chatFrom = this.chat; if(this.chats.length > 1 && justReturn) { this.dispatchEvent('peer_changing', this.chat); } if(!spliced) { spliced = this.chats.splice(fromIndex, this.chats.length - fromIndex); } const chatTo = this.chat; this.dispatchEvent('chat_changing', {from: chatFrom, to: chatTo}); // * -1 because one item is being sliced when closing the chat by calling .removeByType for(let i = 0; i < spliced.length - 1; ++i) { appNavigationController.removeByType('chat', true); } // * fix middle chat z-index on animation if(spliced.length > 1) { spliced.slice(0, -1).forEach((chat) => { chat.container.remove(); }); } this.chatsSelectTab(chatTo.container, animate); if(justReturn) { this.dispatchEvent('peer_changed', chatTo.peerId); const searchTab = appSidebarRight.getTab(AppPrivateSearchTab); if(searchTab) { searchTab.close(); } appSidebarRight.replaceSharedMediaTab(chatTo.sharedMediaTab); } spliced.forEach((chat) => { chat.beforeDestroy(); }); setTimeout(() => { //chat.setPeer(0); spliced.forEach((chat) => { chat.destroy(); }); }, 250 + 100); } public async setPeer(options: ChatSetPeerOptions = {}, animate?: boolean): Promise { if(this.init) { this.init(); this.init = null; } options.peerId ??= NULL_PEER_ID; const {peerId, lastMsgId} = options; const chat = this.chat; const chatIndex = this.chats.indexOf(chat); if(!peerId) { if(chatIndex > 0) { this.spliceChats(chatIndex, undefined, animate); return; } else if(mediaSizes.activeScreen === ScreenSize.medium) { // * floating sidebar case this.selectTab(+!this.tabId, animate); return; } } else if(chatIndex > 0 && chat.peerId && chat.peerId !== peerId) { // const firstChat = this.chats[0]; // if(firstChat.peerId !== chat.peerId) { /* // * slice idx > 0, set background and slice first, so new one will be the first const spliced = this.chats.splice(1, this.chats.length - 1); this.createNewChat(); this.chats.splice(0, 1); */ const spliced = this.chats.splice(1, this.chats.length - 1); if(this.chat.peerId === peerId) { this.spliceChats(0, true, true, spliced); return; } else { const ret = this.setPeer(options); this.spliceChats(0, false, false, spliced); return ret; } // } else { // this.spliceChats(1, false, animate); // } //return ret; } // * don't reset peer if returning if(peerId === chat.peerId && mediaSizes.activeScreen <= ScreenSize.medium && document.body.classList.contains(LEFT_COLUMN_ACTIVE_CLASSNAME)) { this.selectTab(1, animate); return false; } if(peerId || mediaSizes.activeScreen !== ScreenSize.mobile) { const result = await chat.setPeer(peerId, lastMsgId, options.startParam); // * wait for cached render const promise = result?.cached ? result.promise : Promise.resolve(); if(peerId) { Promise.all([ promise, chat.setBackgroundPromise ]).then(() => { //window.requestAnimationFrame(() => { setTimeout(() => { // * setTimeout is better here setTimeout(() => { this.chatsSelectTab(this.chat.container); }, 0); this.selectTab(1, animate); }, 0); }); } } if(!peerId) { this.selectTab(0, animate); return false; } } public setInnerPeer(options: ChatSetInnerPeerOptions) { const {peerId} = options; if(peerId === NULL_PEER_ID || !peerId) { return; } if(options.threadId) { options.type = 'discussion'; } const type = options.type ??= 'chat'; // * prevent opening already opened peer const existingIndex = this.chats.findIndex((chat) => chat.peerId === peerId && chat.type === type); if(existingIndex !== -1) { this.spliceChats(existingIndex + 1); return this.setPeer(options); } const oldChat = this.chat; let chat = oldChat; if(oldChat.inited) { // * use first not inited chat chat = this.createNewChat(); } if(type) { chat.setType(type); if(options.threadId) { chat.threadId = options.threadId; } } this.dispatchEvent('chat_changing', {from: oldChat, to: chat}); //this.chatsSelectTab(chat.container); return this.setPeer(options); } public openScheduled(peerId: PeerId) { this.setInnerPeer({ peerId, type: 'scheduled' }); } private getTypingElement(action: SendMessageAction) { const el = document.createElement('span'); let c = 'peer-typing'; el.classList.add(c); el.dataset.action = action._; switch(action._) { case 'sendMessageTypingAction': { //default: { c += '-text'; for(let i = 0; i < 3; ++i) { const dot = document.createElement('span'); dot.className = c + '-dot'; el.append(dot); } break; } case 'sendMessageUploadAudioAction': case 'sendMessageUploadDocumentAction': case 'sendMessageUploadRoundAction': case 'sendMessageUploadVideoAction': case 'sendMessageUploadPhotoAction': { c += '-upload'; /* const trail = document.createElement('span'); trail.className = c + '-trail'; el.append(trail); */ break; } case 'sendMessageRecordAudioAction': case 'sendMessageRecordRoundAction': case 'sendMessageRecordVideoAction': { c += '-record'; break; } case 'sendMessageEmojiInteractionSeen': case 'sendMessageChooseStickerAction': { c += '-choosing-sticker'; for(let i = 0; i < 2; ++i) { const eye = document.createElement('div'); eye.className = c + '-eye'; el.append(eye); } break; } } el.classList.add(c); return el; } public async getPeerTyping(peerId: PeerId, container?: HTMLElement) { // const log = this.log.bindPrefix('getPeerTyping-' + peerId); // log('getting peer typing'); const isUser = peerId.isUser(); if(isUser && await this.managers.appUsersManager.isBot(peerId)) { // log('a bot'); return; } const typings = await this.managers.appProfileManager.getPeerTypings(peerId); if(!typings?.length) { // log('have no typing'); return; } const typing = typings[0]; const langPackKeys: { [peerType in 'private' | 'chat' | 'multi']?: Partial<{[action in SendMessageAction['_']]: LangPackKey}> } = { private: { 'sendMessageTypingAction': 'Peer.Activity.User.TypingText', 'sendMessageUploadAudioAction': 'Peer.Activity.User.SendingFile', 'sendMessageUploadDocumentAction': 'Peer.Activity.User.SendingFile', 'sendMessageUploadPhotoAction': 'Peer.Activity.User.SendingPhoto', 'sendMessageUploadVideoAction': 'Peer.Activity.User.SendingVideo', 'sendMessageUploadRoundAction': 'Peer.Activity.User.SendingVideo', 'sendMessageRecordVideoAction': 'Peer.Activity.User.RecordingVideo', 'sendMessageRecordAudioAction': 'Peer.Activity.User.RecordingAudio', 'sendMessageRecordRoundAction': 'Peer.Activity.User.RecordingVideo', 'sendMessageGamePlayAction': 'Peer.Activity.User.PlayingGame', 'sendMessageChooseStickerAction': 'Peer.Activity.User.ChoosingSticker', 'sendMessageEmojiInteractionSeen': 'Peer.Activity.User.EnjoyingAnimations' }, chat: { 'sendMessageTypingAction': 'Peer.Activity.Chat.TypingText', 'sendMessageUploadAudioAction': 'Peer.Activity.Chat.SendingFile', 'sendMessageUploadDocumentAction': 'Peer.Activity.Chat.SendingFile', 'sendMessageUploadPhotoAction': 'Peer.Activity.Chat.SendingPhoto', 'sendMessageUploadVideoAction': 'Peer.Activity.Chat.SendingVideo', 'sendMessageUploadRoundAction': 'Peer.Activity.Chat.SendingVideo', 'sendMessageRecordVideoAction': 'Peer.Activity.Chat.RecordingVideo', 'sendMessageRecordAudioAction': 'Peer.Activity.Chat.RecordingAudio', 'sendMessageRecordRoundAction': 'Peer.Activity.Chat.RecordingVideo', 'sendMessageGamePlayAction': 'Peer.Activity.Chat.PlayingGame', 'sendMessageChooseStickerAction': 'Peer.Activity.Chat.ChoosingSticker', 'sendMessageEmojiInteractionSeen': 'Peer.Activity.Chat.EnjoyingAnimations' }, multi: { 'sendMessageTypingAction': 'Peer.Activity.Chat.Multi.TypingText1', 'sendMessageUploadAudioAction': 'Peer.Activity.Chat.Multi.SendingFile1', 'sendMessageUploadDocumentAction': 'Peer.Activity.Chat.Multi.SendingFile1', 'sendMessageUploadPhotoAction': 'Peer.Activity.Chat.Multi.SendingPhoto1', 'sendMessageUploadVideoAction': 'Peer.Activity.Chat.Multi.SendingVideo1', 'sendMessageUploadRoundAction': 'Peer.Activity.Chat.Multi.SendingVideo1', 'sendMessageRecordVideoAction': 'Peer.Activity.Chat.Multi.RecordingVideo1', 'sendMessageRecordAudioAction': 'Peer.Activity.Chat.Multi.RecordingAudio1', 'sendMessageRecordRoundAction': 'Peer.Activity.Chat.Multi.RecordingVideo1', 'sendMessageGamePlayAction': 'Peer.Activity.Chat.Multi.PlayingGame1', 'sendMessageChooseStickerAction': 'Peer.Activity.Chat.Multi.ChoosingSticker1' } }; const mapa = isUser ? langPackKeys.private : (typings.length > 1 ? langPackKeys.multi : langPackKeys.chat); let action = typing.action; if(typings.length > 1) { const s: any = {}; typings.forEach((typing) => { const type = typing.action._; if(s[type] === undefined) s[type] = 0; ++s[type]; }); if(Object.keys(s).length > 1) { action = { _: 'sendMessageTypingAction' }; } } const langPackKey = mapa[action._]; if(!langPackKey) { // log('no langPackKey'); return; } let peerTitlePromise: Promise; let args: any[]; if(peerId.isAnyChat()) { const peerTitle = new PeerTitle(); peerTitlePromise = peerTitle.update({peerId: typing.userId.toPeerId(false), onlyFirstName: true}); args = [ peerTitle.element, typings.length - 1 ]; await peerTitlePromise; } if(!container) { container = document.createElement('span'); container.classList.add('online', 'peer-typing-container'); } container.classList.toggle('peer-typing-flex', action._ === 'sendMessageChooseStickerAction' || action._ === 'sendMessageEmojiInteractionSeen'); let typingElement = container.firstElementChild as HTMLElement; if(!typingElement) { typingElement = this.getTypingElement(action); container.prepend(typingElement); } else { if(typingElement.dataset.action !== action._) { typingElement.replaceWith(this.getTypingElement(action)); } } if(action._ === 'sendMessageEmojiInteractionSeen') { if(args) { args.pop(); } else { args = []; } const span = htmlToSpan(wrapEmojiText(action.emoticon)); args.push(span); } const descriptionElement = i18n(langPackKey, args); descriptionElement.classList.add('peer-typing-description'); if(container.childElementCount > 1) container.lastElementChild.replaceWith(descriptionElement); else container.append(descriptionElement); // log('returning typing'); return container; } private async getChatStatus(chatId: ChatId): Promise> { const typingEl = await this.getPeerTyping(chatId.toPeerId(true)); if(typingEl) { return {cached: true, result: Promise.resolve(typingEl)}; } const result = await this.managers.acknowledged.appProfileManager.getChatFull(chatId); const dooo = async(chatInfo: ChatFull) => { // this.chat.log('chatInfo res:', chatInfo); const participants_count = (chatInfo as ChatFull.channelFull).participants_count || ((chatInfo as ChatFull.chatFull).participants as ChatParticipants.chatParticipants)?.participants?.length || 1; //if(participants_count) { let subtitle = await getChatMembersString(chatId); if(participants_count < 2) { return subtitle; } const onlines = await this.managers.appProfileManager.getOnlines(chatId); if(onlines > 1) { const span = document.createElement('span'); span.append(...join([subtitle, i18n('OnlineCount', [numberThousandSplitter(onlines)])], false)); subtitle = span; } return subtitle; //} }; const promise = Promise.resolve(result.result).then(dooo); return { cached: result.cached, result: promise }; } private async getUserStatus(userId: UserId, ignoreSelf?: boolean) { const result: AckedResult = { cached: true, result: Promise.resolve(undefined as HTMLElement) }; const user = await this.managers.appUsersManager.getUser(userId); if(!user || (user.pFlags.self && !ignoreSelf)) { return result; } const subtitle = getUserStatusString(user); if(!user.pFlags.bot) { let typingEl = await this.getPeerTyping(userId.toPeerId()); if(!typingEl && user.status?._ === 'userStatusOnline') { typingEl = document.createElement('span'); typingEl.classList.add('online'); typingEl.append(subtitle); } if(typingEl) { result.result = Promise.resolve(typingEl); return result; } } result.result = Promise.resolve(subtitle); return result; } private async getPeerStatus(peerId: PeerId, ignoreSelf?: boolean) { if(!peerId) return; let promise: Promise>; if(peerId.isAnyChat()) { promise = this.getChatStatus(peerId.toChatId()); } else { promise = this.getUserStatus(peerId.toUserId(), ignoreSelf); } return promise; } public async setPeerStatus( peerId: PeerId, element: HTMLElement, needClear: boolean, useWhitespace: boolean, middleware: () => boolean, ignoreSelf?: boolean ) { // const log = this.log.bindPrefix('status-' + peerId); // log('setting status', element); if(!needClear) { // * good good good const typingContainer = element.querySelector('.peer-typing-container') as HTMLElement; if(typingContainer && await this.getPeerTyping(peerId, typingContainer)) { // log('already have a status'); return; } } const result = await this.getPeerStatus(peerId, ignoreSelf); // log('getPeerStatus result', result); if(!middleware()) { // log.warn('middleware'); return; } const set = async() => { const subtitle = result && await result.result; if(!middleware()) { return; } return () => replaceContent(element, subtitle || placeholder); }; const placeholder = useWhitespace ? '‎' : ''; // ! HERE U CAN FIND WHITESPACE if(!result || result.cached) { return await set(); } else if(needClear) { return () => { element.textContent = placeholder; return set().then((callback) => callback && callback()); }; } } public setChoosingStickerTyping(cancel: boolean) { this.managers.appMessagesManager.setTyping(this.chat.peerId, {_: cancel ? 'sendMessageCancelAction' : 'sendMessageChooseStickerAction'}); } } const appImManager = new AppImManager(); MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appImManager = appImManager); export default appImManager;