/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import type { MyDialogFilter as DialogFilter, MyDialogFilter } from "../storages/filters"; import type LazyLoadQueue from "../../components/lazyLoadQueue"; import type { Dialog, MyMessage } from "./appMessagesManager"; import type { MyPhoto } from "./appPhotosManager"; import type { MyDocument } from "./appDocsManager"; import type { State } from "../../config/state"; import AvatarElement from "../../components/avatar"; import DialogsContextMenu from "../../components/dialogsContextMenu"; import { horizontalMenu } from "../../components/horizontalMenu"; import ripple from "../../components/ripple"; import Scrollable, { ScrollableX, SliceSides } from "../../components/scrollable"; import { formatDateAccordingToTodayNew } from "../../helpers/date"; import { IS_MOBILE_SAFARI, IS_SAFARI } from "../../environment/userAgent"; import { logger, LogTypes } from "../logger"; import rootScope from "../rootScope"; import appImManager from "./appImManager"; import Button from "../../components/button"; import SetTransition from "../../components/singleTransition"; import { MyDraftMessage } from "./appDraftsManager"; import DEBUG, { MOUNT_CLASS_TO } from "../../config/debug"; import PeerTitle from "../../components/peerTitle"; import I18n, { FormatterArguments, i18n, LangPackKey, _i18n } from "../langPack"; import findUpTag from "../../helpers/dom/findUpTag"; import lottieLoader from "../rlottie/lottieLoader"; import { wrapPhoto } from "../../components/wrappers"; import AppEditFolderTab from "../../components/sidebarLeft/tabs/editFolder"; import appSidebarLeft, { SettingSection } from "../../components/sidebarLeft"; import { attachClickEvent } from "../../helpers/dom/clickEvent"; import positionElementByIndex from "../../helpers/dom/positionElementByIndex"; import replaceContent from "../../helpers/dom/replaceContent"; import ConnectionStatusComponent from "../../components/connectionStatus"; import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl"; import { fastRafConventional, fastRafPromise } from "../../helpers/schedulers"; import SortedUserList from "../../components/sortedUserList"; import IS_TOUCH_SUPPORTED from "../../environment/touchSupport"; import handleTabSwipe from "../../helpers/dom/handleTabSwipe"; import windowSize from "../../helpers/windowSize"; import isInDOM from "../../helpers/dom/isInDOM"; import { setSendingStatus } from "../../components/sendingStatus"; import SortedList, { SortedElementBase } from "../../helpers/sortedList"; import debounce from "../../helpers/schedulers/debounce"; import { NULL_PEER_ID } from "../mtproto/mtproto_config"; import groupCallActiveIcon from "../../components/groupCallActiveIcon"; import { Chat, Message, NotifyPeer } from "../../layer"; import IS_GROUP_CALL_SUPPORTED from "../../environment/groupCallSupport"; import mediaSizes from "../../helpers/mediaSizes"; import appNavigationController, { NavigationItem } from "../../components/appNavigationController"; import assumeType from "../../helpers/assumeType"; import generateTitleIcons from "../../components/generateTitleIcons"; import appMediaPlaybackController from "../../components/appMediaPlaybackController"; import setInnerHTML from "../../helpers/dom/setInnerHTML"; import { AppManagers } from "./managers"; import appSidebarRight from "../../components/sidebarRight"; import PopupElement from "../../components/popups"; import choosePhotoSize from "./utils/photos/choosePhotoSize"; import wrapEmojiText from "../richTextProcessor/wrapEmojiText"; import wrapMessageForReply from "../../components/wrappers/messageForReply"; import isMessageRestricted from "./utils/messages/isMessageRestricted"; import getMediaFromMessage from "./utils/messages/getMediaFromMessage"; import getMessageSenderPeerIdOrName from "./utils/messages/getMessageSenderPeerIdOrName"; import wrapStickerEmoji from "../../components/wrappers/stickerEmoji"; import getDialogIndexKey from "./utils/dialogs/getDialogIndexKey"; import getProxiedManagers from "./getProxiedManagers"; import getDialogIndex from "./utils/dialogs/getDialogIndex"; import { attachContextMenuListener } from "../../helpers/dom/attachContextMenuListener"; import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise"; import wrapPeerTitle from "../../components/wrappers/peerTitle"; import middlewarePromise from "../../helpers/middlewarePromise"; import appDownloadManager from "./appDownloadManager"; import groupCallsController from "../calls/groupCallsController"; import callsController from "../calls/callsController"; import cancelEvent from "../../helpers/dom/cancelEvent"; import noop from "../../helpers/noop"; import DialogsPlaceholder from "../../helpers/dialogsPlaceholder"; import pause from "../../helpers/schedulers/pause"; import apiManagerProxy from "../mtproto/mtprotoworker"; export const DIALOG_LIST_ELEMENT_TAG = 'A'; export type DialogDom = { avatarEl: AvatarElement, captionDiv: HTMLDivElement, titleSpan: HTMLSpanElement, titleSpanContainer: HTMLSpanElement, statusSpan: HTMLSpanElement, lastTimeSpan: HTMLSpanElement, unreadBadge: HTMLElement, callIcon?: ReturnType, mentionsBadge?: HTMLElement, lastMessageSpan: HTMLSpanElement, containerEl: HTMLElement, listEl: HTMLElement, subtitleEl: HTMLElement, setLastMessagePromise?: CancellablePromise, setUnreadMessagePromise?: CancellablePromise }; interface SortedDialog extends SortedElementBase { dom: DialogDom, loadPromises?: Promise[] } function setPromiseMiddleware}, K extends keyof T>(obj: T, key: K) { const oldPromise = obj[key]; if(oldPromise) { oldPromise.reject(); } // @ts-ignore const deferred = obj[key] = deferredPromise(); deferred.catch(() => {}).finally(() => { if(obj[key] === deferred) { delete obj[key]; } }); const middleware = middlewarePromise(() => obj[key] === deferred); return {deferred, middleware}; } class SortedDialogList extends SortedList { constructor( public managers: AppManagers, public list: HTMLUListElement, public indexKey: ReturnType, public onListLengthChange?: () => void, ) { super({ getIndex: (element) => managers.dialogsStorage.getDialogIndex(element.id, this.indexKey), onDelete: (element) => { element.dom.listEl.remove(); this.onListLengthChange && this.onListLengthChange(); }, onSort: (element, idx) => { const willChangeLength = element.dom.listEl.parentElement !== this.list; positionElementByIndex(element.dom.listEl, this.list, idx); if(willChangeLength) { this.onListLengthChange && this.onListLengthChange(); } }, onElementCreate: (base, batch) => { const loadPromises: Promise[] = batch ? [] : undefined; const {dom} = appDialogsManager.addListDialog({peerId: base.id, loadPromises, isBatch: batch}); (base as SortedDialog).dom = dom; if(loadPromises?.length) { (base as SortedDialog).loadPromises = loadPromises; Promise.all(loadPromises).finally(() => { delete (base as SortedDialog).loadPromises; }); } return base as SortedDialog; }, updateElementWith: fastRafConventional }); } public clear() { this.list.innerHTML = ''; super.clear(); } } //const testScroll = false; //let testTopSlice = 1; export class AppDialogsManager { private chatsContainer = document.getElementById('chatlist-container') as HTMLDivElement; private loadDialogsPromise: Promise<{cached: boolean, renderPromise: AppDialogsManager['loadDialogsRenderPromise']}>; private loadDialogsRenderPromise: Promise; private scroll: Scrollable = null; private log = logger('DIALOGS', LogTypes.Log | LogTypes.Error | LogTypes.Warn | LogTypes.Debug); private contextMenu: DialogsContextMenu; private sortedList: SortedDialogList; public placeholders: {[filterId: number]: DialogsPlaceholder} = {}; public sortedLists: {[filterId: number]: SortedDialogList} = {}; public scrollables: {[filterId: number]: Scrollable} = {}; public filterId: number; private folders: {[k in 'menu' | 'container' | 'menuScrollContainer']: HTMLElement} = { menu: document.getElementById('folders-tabs'), menuScrollContainer: null, container: document.getElementById('folders-container') }; private filtersRendered: { [filterId: string]: { menu: HTMLElement, container: HTMLElement, unread: HTMLElement, title: HTMLElement } } = {}; private showFiltersPromise: Promise; private allUnreadCount: HTMLElement; private sliceTimeout: number; private lastActiveElements: Set = new Set(); private offsets: {top: number, bottom: number} = {top: 0, bottom: 0}; private loadContacts: () => void; private processContact: (peerId: PeerId) => void; private indexKey: ReturnType; private initedListeners = false; private onListLengthChange: () => Promise; private loadedDialogsAtLeastOnce = false; private allChatsIntlElement: I18n.IntlElement; private emptyDialogsPlaceholderSubtitle: I18n.IntlElement; private updateContactsLengthPromise: Promise; private filtersNavigationItem: NavigationItem; private managers: AppManagers; constructor() { const managers = this.managers = getProxiedManagers(); this.contextMenu = new DialogsContextMenu(managers); this.allUnreadCount = this.folders.menu.querySelector('.badge'); this.folders.menuScrollContainer = this.folders.menu.parentElement; this.onListLengthChange = debounce(this._onListLengthChange, 100, false, true); const bottomPart = document.createElement('div'); bottomPart.classList.add('connection-status-bottom'); bottomPart.append(this.folders.container); /* if(isTouchSupported && isSafari) { let allowUp: boolean, allowDown: boolean, slideBeginY: number; const container = this.scroll.container; container.addEventListener('touchstart', (event) => { allowUp = container.scrollTop > 0; allowDown = (container.scrollTop < container.scrollHeight - container.clientHeight); // @ts-ignore slideBeginY = event.pageY; }); container.addEventListener('touchmove', (event: any) => { var up = (event.pageY > slideBeginY); var down = (event.pageY < slideBeginY); slideBeginY = event.pageY; if((up && allowUp) || (down && allowDown)) { event.stopPropagation(); } else if(up || down) { event.preventDefault(); } }); } */ if(IS_TOUCH_SUPPORTED) { handleTabSwipe({ element: this.folders.container, onSwipe: (xDiff) => { const prevId = selectTab.prevId(); selectTab(xDiff > 0 ? prevId + 1 : prevId - 1); } }); } this.allChatsIntlElement = new I18n.IntlElement({ key: 'FilterAllChatsShort' }); /* if(testScroll) { let i = 0; let add = () => { let li = document.createElement('li'); li.dataset.id = '' + i; li.id = '' + i; li.innerHTML = `

${i}18:33

-_-_-_-: qweasd

`; i++; this.scroll.append(li); }; for(let i = 0; i < 500; ++i) { add(); } (window as any).addElement = add; } */ rootScope.addEventListener('state_cleared', () => { //setTimeout(() => apiManagerProxy.getState().then(async(state) => { this.loadedDialogsAtLeastOnce = false; /* const clearPromises: Promise[] = []; for(const name in this.managers.appStateManager.storagesResults) { const results = this.managers.appStateManager.storagesResults[name as keyof AppStateManager['storages']]; const storage = this.managers.appStateManager.storages[name as keyof AppStateManager['storages']]; results.length = 0; clearPromises.push(storage.clear()); } */ this.sortedList.clear(); this.onTabChange(); this.onStateLoaded(state); })//, 5000); }); this.setFilterId(0, 0); this.addFilter({ id: this.filterId, title: '', orderIndex: 0 }); const foldersScrollable = new ScrollableX(this.folders.menuScrollContainer); bottomPart.prepend(this.folders.menuScrollContainer); const selectTab = horizontalMenu(this.folders.menu, this.folders.container, (id, tabContent) => { /* if(id !== 0) { id += 1; } */ id = +tabContent.dataset.filterId || 0; if(!IS_MOBILE_SAFARI) { if(id) { if(!this.filtersNavigationItem) { this.filtersNavigationItem = { type: 'filters', onPop: () => { selectTab(0); this.filtersNavigationItem = undefined; } }; appNavigationController.spliceItems(1, 0, this.filtersNavigationItem); } } else if(this.filtersNavigationItem) { appNavigationController.removeItem(this.filtersNavigationItem); this.filtersNavigationItem = undefined; } } if(this.filterId === id) return; this.sortedLists[id].clear(); return this.setFilterIdAndChangeTab(id).then(({cached, renderPromise}) => { if(cached) { return renderPromise; } }); }, () => { for(const folderId in this.sortedLists) { if(+folderId !== this.filterId) { this.sortedLists[folderId].clear(); const placeholder = this.placeholders[folderId]; if(placeholder) { placeholder.remove(); } } } }, undefined, foldersScrollable); apiManagerProxy.getState().then((state) => { // * it should've had a better place :( appMediaPlaybackController.setPlaybackParams(state.playbackParams); appMediaPlaybackController.addEventListener('playbackParams', (params) => { this.managers.appStateManager.pushToState('playbackParams', params); }); return this.onStateLoaded(state); })/* .then(() => { const isLoadedMain = this.managers.appMessagesManager.dialogsStorage.isDialogsLoaded(0); const isLoadedArchive = this.managers.appMessagesManager.dialogsStorage.isDialogsLoaded(1); const wasLoaded = isLoadedMain || isLoadedArchive; const a: Promise = isLoadedMain ? Promise.resolve() : this.managers.appMessagesManager.getConversationsAll('', 0); const b: Promise = isLoadedArchive ? Promise.resolve() : this.managers.appMessagesManager.getConversationsAll('', 1); a.finally(() => { b.then(() => { if(wasLoaded) { (apiUpdatesManager.updatesState.syncLoading || Promise.resolve()).then(() => { this.managers.appMessagesManager.refreshConversations(); }); } }); }); }) */; mediaSizes.addEventListener('resize', () => { this.changeFiltersAllChatsKey(); }); new ConnectionStatusComponent(this.managers, this.chatsContainer); this.chatsContainer.append(bottomPart); setTimeout(() => { lottieLoader.loadLottieWorkers(); }, 200); PopupElement.MANAGERS = rootScope.managers = managers; appDownloadManager.construct(managers); appSidebarLeft.construct(managers); appSidebarRight.construct(managers); groupCallsController.construct(managers); callsController.construct(managers); appImManager.construct(managers); // start this.sortedList = this.sortedLists[this.filterId]; this.scroll = this.scrollables[this.filterId]; //selectTab(0); (this.folders.menu.firstElementChild as HTMLElement).click(); } public get chatList() { return this.sortedList.list; } public setFilterId(filterId: number, orderIndex: MyDialogFilter['orderIndex']) { this.indexKey = getDialogIndexKey(orderIndex); this.filterId = filterId; } public async setFilterIdAndChangeTab(filterId: number) { this.indexKey = await this.managers.dialogsStorage.getDialogIndexKeyByFilterId(filterId); this.filterId = filterId; return this.onTabChange(); } private setOnlineStatus(element: HTMLElement, online: boolean) { const className = 'is-online'; const hasClassName = element.classList.contains(className); !hasClassName && online && element.classList.add(className); SetTransition(element, 'is-visible', online, 250, online ? undefined : () => { element.classList.remove(className); }, online && !hasClassName ? 2 : 0); } private initListeners() { rootScope.addEventListener('user_update', async(userId) => { //console.log('updating user:', user, dialog); const peerId = userId.toPeerId(); const dom = this.getDialogDom(peerId); if(dom && peerId !== rootScope.myId && !(await this.managers.appUsersManager.isBot(userId))) { const user = await this.managers.appUsersManager.getUser(userId); const online = user.status?._ === 'userStatusOnline'; this.setOnlineStatus(dom.avatarEl, online); } }); rootScope.addEventListener('chat_update', async(chatId) => { const peerId = chatId.toPeerId(true); const dialog = await this.managers.appMessagesManager.getDialogOnly(peerId); if(dialog) { this.processDialogForCallStatus(dialog); } }); rootScope.addEventListener('folder_unread', (folder) => { this.setFilterUnreadCount(folder.id); }); rootScope.addEventListener('contacts_update', (userId) => { this.processContact && this.processContact(userId.toPeerId()); }); rootScope.addEventListener('dialog_flush', ({dialog}) => { if(!dialog) { return; } this.setLastMessageN({ dialog, setUnread: true }); this.validateDialogForFilter(dialog); this.setFiltersUnreadCount(); }); rootScope.addEventListener('dialogs_multiupdate', (dialogs) => { for(const peerId in dialogs) { const dialog = dialogs[peerId]; this.updateDialog(dialog); if(this.processContact) { this.processContact(peerId.toPeerId()); } this.validateDialogForFilter(dialog); } }); rootScope.addEventListener('dialog_drop', ({peerId}) => { this.deleteDialog(peerId); if(this.processContact) { this.processContact(peerId); } }); rootScope.addEventListener('dialog_unread', ({dialog}) => { if(!dialog) { return; } this.setUnreadMessagesN({dialog}); this.validateDialogForFilter(dialog); }); rootScope.addEventListener('dialog_notify_settings', (dialog) => { this.validateDialogForFilter(dialog); this.setUnreadMessagesN({dialog}); // возможно это не нужно, но нужно менять is-muted this.setFiltersUnreadCount(); }); rootScope.addEventListener('dialog_draft', ({dialog, drop, peerId}) => { if(drop) { this.sortedList.delete(peerId); } else { this.updateDialog(dialog); } if(this.processContact) { this.processContact(peerId); } }); appImManager.addEventListener('peer_changed', (peerId) => { //const perf = performance.now(); for(const element of this.lastActiveElements) { if(element.dataset.peerId.toPeerId() !== peerId) { this.setDialogActive(element, false); } } const elements = Array.from(document.querySelectorAll(`[data-autonomous="0"] .chatlist-chat[data-peer-id="${peerId}"]`)) as HTMLElement[]; elements.forEach((element) => { this.setDialogActive(element, true); }); //this.log('peer_changed total time:', performance.now() - perf); }); rootScope.addEventListener('filter_update', async(filter) => { if(!this.filtersRendered[filter.id]) { this.addFilter(filter); return; } else if(filter.id === this.filterId) { // это нет тут смысла вызывать, так как будет dialogs_multiupdate const dialogs = await this.managers.dialogsStorage.getCachedDialogs(true); await this.validateListForFilter(); for(let i = 0, length = dialogs.length; i < length; ++i) { const dialog = dialogs[i]; this.updateDialog(dialog); } } const elements = this.filtersRendered[filter.id]; setInnerHTML(elements.title, wrapEmojiText(filter.title)); }); rootScope.addEventListener('filter_delete', (filter) => { const elements = this.filtersRendered[filter.id]; if(!elements) return; // set tab //(this.folders.menu.firstElementChild.children[Math.max(0, filter.id - 2)] as HTMLElement).click(); (this.folders.menu.firstElementChild as HTMLElement).click(); elements.container.remove(); elements.menu.remove(); delete this.sortedLists[filter.id]; delete this.scrollables[filter.id]; delete this.filtersRendered[filter.id]; this.onFiltersLengthChange(); }); rootScope.addEventListener('filter_order', async(order) => { const containerToAppend = this.folders.menu as HTMLElement; const r = await Promise.all(order.map(async(filterId) => { return { indexKey: await this.managers.dialogsStorage.getDialogIndexKeyByFilterId(filterId), filter: await this.managers.filtersStorage.getFilter(filterId) }; })); order.forEach((filterId, idx) => { const {indexKey, filter} = r[idx]; const renderedFilter = this.filtersRendered[filterId]; const sortedList = this.sortedLists[filterId]; sortedList.indexKey = indexKey; positionElementByIndex(renderedFilter.menu, containerToAppend, filter.orderIndex); positionElementByIndex(renderedFilter.container, this.folders.container, filter.orderIndex); }); this.indexKey = await this.managers.dialogsStorage.getDialogIndexKeyByFilterId(this.filterId); /* if(this.filterId) { const tabIndex = order.indexOf(this.filterId) + 1; selectTab.prevId = tabIndex; } */ }); rootScope.addEventListener('peer_typings', async({peerId, typings}) => { const dialog = await this.managers.appMessagesManager.getDialogOnly(peerId); if(!dialog) return; if(typings.length) { this.setTyping(dialog); } else { this.unsetTyping(dialog); } }); } private setDialogActive(listEl: HTMLElement, active: boolean) { // @ts-ignore const dom = listEl.dialogDom as DialogDom; listEl.classList.toggle('active', active); if(active) { this.lastActiveElements.add(listEl); } else { this.lastActiveElements.delete(listEl); } if(dom?.callIcon) { dom.callIcon.setActive(active); } } private async onStateLoaded(state: State) { const loadDialogsPromise = this.onChatsScroll(); if(!this.initedListeners) { this.initListeners(); this.initedListeners = true; } const haveFilters = !!(state.filters && Object.keys(state.filters).length); const getDialogsFiltersPromise = haveFilters ? Promise.resolve(Object.values(state.filters).sort((a, b) => a.orderIndex - b.orderIndex)) : this.managers.filtersStorage.getDialogFilters(); const renderFiltersPromise = getDialogsFiltersPromise.then((filters) => { for(const filter of filters) { this.addFilter(filter); } }); if(haveFilters) { await renderFiltersPromise; if(this.showFiltersPromise) { await this.showFiltersPromise; } } if(state.notifySettings) { const promises: Promise[] = []; for(const key in state.notifySettings) { assumeType>(key); const promise = this.managers.appNotificationsManager.savePeerSettings({ key, settings: state.notifySettings[key] }); promises.push(promise); } await Promise.all(promises); } this.managers.appNotificationsManager.getNotifyPeerTypeSettings(); await (await loadDialogsPromise).renderPromise; this.managers.appMessagesManager.fillConversations(); } /* private getOffset(side: 'top' | 'bottom'): {index: number, pos: number} { if(!this.scroll.loadedAll[side]) { const element = (side === 'top' ? this.chatList.firstElementChild : this.chatList.lastElementChild) as HTMLElement; if(element) { const peerId = element.dataset.peerId; const dialog = this.managers.appMessagesManager.getDialogByPeerId(peerId); return {index: dialog[0].index, pos: dialog[1]}; } } return {index: 0, pos: -1}; } */ private getOffsetIndex(side: 'top' | 'bottom') { return {index: this.scroll.loadedAll[side] ? 0 : this.offsets[side]}; } private isDialogMustBeInViewport(dialog: Dialog) { if(dialog.migratedTo !== undefined || !this.testDialogForFilter(dialog)) return false; //return true; const topOffset = this.getOffsetIndex('top'); const bottomOffset = this.getOffsetIndex('bottom'); if(!topOffset.index && !bottomOffset.index) { return true; } const index = getDialogIndex(dialog, this.indexKey); return (!topOffset.index || index <= topOffset.index) && (!bottomOffset.index || index >= bottomOffset.index); } private deleteDialog(peerId: PeerId) { this.sortedList.delete(peerId); } private updateDialog(dialog: Dialog) { if(this.isDialogMustBeInViewport(dialog)) { if(!this.sortedList.has(dialog.peerId)) { this.sortedList.add(dialog.peerId); return; } } else { this.deleteDialog(dialog.peerId); return; } const dom = this.getDialogDom(dialog.peerId); if(dom) { this.setLastMessageN({ dialog, dom, setUnread: true }); this.sortedList.update(dialog.peerId); } } public onTabChange = () => { this.scroll = this.scrollables[this.filterId]; this.scroll.loadedAll.top = true; this.scroll.loadedAll.bottom = false; this.offsets.top = this.offsets.bottom = 0; this.loadDialogsRenderPromise = undefined; this.loadDialogsPromise = undefined; this.sortedList = this.sortedLists[this.filterId]; return this.onChatsScroll(); }; private async setFilterUnreadCount(filterId: number) { const unreadSpan = filterId === 0 ? this.allUnreadCount : this.filtersRendered[filterId]?.unread; if(!unreadSpan) { return; } const {foundUnmuted, unreadCount} = await this.managers.dialogsStorage.getFolderUnreadCount(filterId); unreadSpan.classList.toggle('badge-gray', !foundUnmuted); unreadSpan.innerText = unreadCount ? '' + unreadCount : ''; } private setFiltersUnreadCount() { for(const filterId in this.filtersRendered) { this.setFilterUnreadCount(+filterId); } } /** * Удалит неподходящие чаты из списка, но не добавит их(!) */ private async validateListForFilter() { this.sortedList.getAll().forEach(async(element) => { const dialog = await this.managers.appMessagesManager.getDialogOnly(element.id); if(!this.testDialogForFilter(dialog)) { this.deleteDialog(element.id); } }); } /** * Удалит неподходящий чат из списка, но не добавит его(!) */ private validateDialogForFilter(dialog: Dialog) { if(!this.getDialogDom(dialog.peerId)) { return; } if(!this.testDialogForFilter(dialog)) { this.deleteDialog(dialog.peerId); } } public testDialogForFilter(dialog: Dialog) { if( !dialog || (this.filterId > 1 ? getDialogIndex(dialog, this.indexKey) === undefined : this.filterId !== dialog.folder_id) // (filter && !(await this.managers.filtersStorage.testDialogForFilter(dialog, filter))) ) { return false; } return true; } public generateScrollable(list: HTMLUListElement, filter: Parameters[0]) { const filterId = filter.id; const scrollable = new Scrollable(null, 'CL', 500); scrollable.container.addEventListener('scroll', this.onChatsRegularScroll); scrollable.container.dataset.filterId = '' + filterId; scrollable.onScrolledTop = this.onChatsScrollTop; scrollable.onScrolledBottom = this.onChatsScroll; scrollable.setVirtualContainer(list); const sortedDialogList = new SortedDialogList( this.managers, list, getDialogIndexKey(filter.orderIndex), this.onListLengthChange ); this.scrollables[filterId] = scrollable; this.sortedLists[filterId] = sortedDialogList; // list.classList.add('hide'); // scrollable.container.style.backgroundColor = '#' + (Math.random() * (16 ** 6 - 1) | 0).toString(16); return scrollable; } private addFilter(filter: Pick) { if(filter.id === 1) { return; } const containerToAppend = this.folders.menu as HTMLElement; const renderedFilter = this.filtersRendered[filter.id]; if(renderedFilter) { positionElementByIndex(renderedFilter.menu, containerToAppend, filter.orderIndex); positionElementByIndex(renderedFilter.container, this.folders.container, filter.orderIndex); return; } const menuTab = document.createElement('div'); menuTab.classList.add('menu-horizontal-div-item'); const span = document.createElement('span'); const titleSpan = document.createElement('span'); titleSpan.classList.add('text-super'); if(filter.id === 0) titleSpan.append(this.allChatsIntlElement.element); else setInnerHTML(titleSpan, wrapEmojiText(filter.title)); const unreadSpan = document.createElement('div'); unreadSpan.classList.add('badge', 'badge-20', 'badge-primary'); const i = document.createElement('i'); span.append(titleSpan, unreadSpan, i); ripple(menuTab); menuTab.append(span); positionElementByIndex(menuTab, containerToAppend, filter.orderIndex); //containerToAppend.append(li); const ul = this.createChatList(); const scrollable = this.generateScrollable(ul, filter); scrollable.container.classList.add('tabs-tab', 'chatlist-parts'); /* const parts = document.createElement('div'); parts.classList.add('chatlist-parts'); */ const top = document.createElement('div'); top.classList.add('chatlist-top'); const bottom = document.createElement('div'); bottom.classList.add('chatlist-bottom'); top.append(ul); scrollable.container.append(top, bottom); /* parts.append(top, bottom); scrollable.container.append(parts); */ const div = scrollable.container; //this.folders.container.append(div); positionElementByIndex(scrollable.container, this.folders.container, filter.orderIndex); this.setListClickListener(ul, null, true); this.filtersRendered[filter.id] = { menu: menuTab, container: div, unread: unreadSpan, title: titleSpan }; this.onFiltersLengthChange(); } private changeFiltersAllChatsKey() { const scrollable = this.folders.menuScrollContainer.firstElementChild; const key: LangPackKey = scrollable.scrollWidth > scrollable.clientWidth ? 'FilterAllChatsShort' : 'FilterAllChats'; this.allChatsIntlElement.compareAndUpdate({key}); } private onFiltersLengthChange() { if(!this.showFiltersPromise) { this.showFiltersPromise = new Promise((resolve) => { window.setTimeout(() => { const length = Object.keys(this.filtersRendered).length; const show = length > 1; const wasShowing = !this.folders.menuScrollContainer.classList.contains('hide'); if(show !== wasShowing) { this.folders.menuScrollContainer.classList.toggle('hide', !show); if(show && !wasShowing) { this.setFiltersUnreadCount(); } this.chatsContainer.classList.toggle('has-filters', show); } this.changeFiltersAllChatsKey(); this.showFiltersPromise = undefined; resolve(); }, 0); }); } return this.showFiltersPromise; } private loadDialogs(side: SliceSides) { /* if(testScroll) { return; } */ if(this.loadDialogsPromise || this.loadDialogsRenderPromise/* || 1 === 1 */) return this.loadDialogsPromise; else if(this.scroll.loadedAll[side]) { return Promise.resolve({ cached: true, renderPromise: Promise.resolve() }); } const cachedInfoPromise = deferredPromise(); const renderPromise = new Promise(async(resolve, reject) => { const {chatList, filterId, indexKey} = this; //return; // let loadCount = 30/*this.chatsLoadCount */; let loadCount = windowSize.height / 72 * 1.25 | 0; let offsetIndex = 0; const {index: currentOffsetIndex} = this.getOffsetIndex(side); if(currentOffsetIndex) { if(side === 'top') { const storage = await this.managers.dialogsStorage.getFolderDialogs(filterId, true); const index = storage.findIndex((dialog) => getDialogIndex(dialog, indexKey) <= currentOffsetIndex); const needIndex = Math.max(0, index - loadCount); loadCount = index - needIndex; offsetIndex = getDialogIndex(storage[needIndex], indexKey) + 1; } else { offsetIndex = currentOffsetIndex; } } //let offset = storage[storage.length - 1]?.index || 0; let placeholder = this.placeholders[filterId]; try { const getConversationsResult = this.managers.acknowledged.appMessagesManager.getConversations('', offsetIndex, loadCount, filterId, true); if( !chatList.childElementCount && !placeholder && ( !this.loadedDialogsAtLeastOnce || !(await getConversationsResult).cached ) ) { placeholder = this.placeholders[filterId] = new DialogsPlaceholder(); const getRectFrom = filterId === 1 ? this.chatsContainer : this.folders.container; placeholder.attach({ container: chatList.parentElement, getRectFrom, onRemove: () => { delete this.placeholders[filterId]; }, blockScrollable: this.scroll }); cachedInfoPromise.resolve(false); } const a = await getConversationsResult; const result = await a.result; if(this.loadDialogsRenderPromise !== renderPromise) { reject(); cachedInfoPromise.reject(); return; } cachedInfoPromise.resolve(a.cached); //console.timeEnd('getDialogs time'); // * loaded all //if(!result.dialogs.length || chatList.childElementCount === result.count) { // !result.dialogs.length не подходит, так как при супердревном диалоге getConversations его не выдаст. //if(chatList.childElementCount === result.count) { if(side === 'bottom') { if(result.isEnd) { this.scroll.loadedAll[side] = true; } } else if(result.isTopEnd) { this.scroll.loadedAll[side] = true; } this.loadedDialogsAtLeastOnce = true; if(result.dialogs.length) { const dialogs = side === 'top' ? result.dialogs.slice().reverse() : result.dialogs; const loadPromises: Promise[] = []; const callbacks: (() => void)[] = []; const cccc = (callback: () => void) => { callbacks.push(callback); }; dialogs.forEach((dialog) => { // :( // const isBuggedDialog = !this.managers.appMessagesManager.getDialogOnly(dialog.peerId); // if(isBuggedDialog) { // return; // } const element = this.sortedList.add(dialog.peerId, true, /* undefined, false, */cccc, false); if(element.loadPromises) { loadPromises.push(...element.loadPromises); } }); loadPromises.push(fastRafPromise()); // it is needed here await Promise.all(loadPromises).finally(); if(this.loadDialogsRenderPromise !== renderPromise) { reject(); cachedInfoPromise.reject(); return; } callbacks.forEach((callback) => callback()); } else { this.onListLengthChange(); } const offsetDialog = result.dialogs[side === 'top' ? 0 : result.dialogs.length - 1]; if(offsetDialog) { this.offsets[side] = getDialogIndex(offsetDialog, indexKey); } this.log.debug('getDialogs ' + loadCount + ' dialogs by offset:', offsetIndex, result, chatList.childElementCount); setTimeout(() => { this.scroll.onScroll(); }, 0); } catch(err) { this.log.error(err); } if(placeholder) { // await pause(500); placeholder.detach(chatList.childElementCount); } resolve(); }).finally(() => { if(this.loadDialogsRenderPromise === renderPromise) { this.loadDialogsRenderPromise = undefined; this.loadDialogsPromise = undefined; } }); this.loadDialogsRenderPromise = renderPromise; return this.loadDialogsPromise = cachedInfoPromise.then((cached) => ({ cached, renderPromise })); } private generateEmptyPlaceholder(options: { title: LangPackKey, subtitle?: LangPackKey, subtitleArgs?: FormatterArguments, classNameType: string }) { const BASE_CLASS = 'empty-placeholder'; const container = document.createElement('div'); container.classList.add(BASE_CLASS, BASE_CLASS + '-' + options.classNameType); const header = document.createElement('div'); header.classList.add(BASE_CLASS + '-header'); _i18n(header, options.title); const subtitle = document.createElement('div'); subtitle.classList.add(BASE_CLASS + '-subtitle'); if(options.subtitle) { _i18n(subtitle, options.subtitle, options.subtitleArgs); } container.append(header, subtitle); return {container, header, subtitle}; } private checkIfPlaceholderNeeded() { if(this.filterId === 1) { return; } const chatList = this.chatList; const part = chatList.parentElement as HTMLElement; let placeholderContainer = (Array.from(part.children) as HTMLElement[]).find((el) => el.matches('.empty-placeholder')); const needPlaceholder = this.scroll.loadedAll.bottom && !chatList.childElementCount/* || true */; // chatList.style.display = 'none'; if(needPlaceholder && placeholderContainer) { return; } else if(!needPlaceholder) { if(placeholderContainer) { part.classList.remove('with-placeholder'); placeholderContainer.remove(); } return; } let placeholder: ReturnType, type: 'dialogs' | 'folder'; if(!this.filterId) { placeholder = this.generateEmptyPlaceholder({ title: 'ChatList.Main.EmptyPlaceholder.Title', classNameType: type = 'dialogs' }); placeholderContainer = placeholder.container; const img = document.createElement('img'); img.classList.add('empty-placeholder-dialogs-icon'); this.emptyDialogsPlaceholderSubtitle = new I18n.IntlElement({ element: placeholder.subtitle }); Promise.all([ this.updateContactsLength(false), renderImageFromUrlPromise(img, 'assets/img/EmptyChats.svg'), fastRafPromise() ]).then(([usersLength]) => { placeholderContainer.classList.add('visible'); part.classList.toggle('has-contacts', !!usersLength); }); placeholderContainer.prepend(img); } else { placeholder = this.generateEmptyPlaceholder({ title: 'FilterNoChatsToDisplay', subtitle: 'FilterNoChatsToDisplayInfo', classNameType: type = 'folder' }); placeholderContainer = placeholder.container; const div = document.createElement('div'); const emoji = '📂'; const size = 128; wrapStickerEmoji({ div, emoji: emoji, width: size, height: size }); placeholderContainer.prepend(div); const button = Button('btn-primary btn-color-primary btn-control tgico', { text: 'FilterHeaderEdit', icon: 'settings' }); attachClickEvent(button, async() => { appSidebarLeft.createTab(AppEditFolderTab).open(await this.managers.filtersStorage.getFilter(this.filterId)); }); placeholderContainer.append(button); } part.append(placeholderContainer); part.classList.add('with-placeholder'); part.dataset.placeholderType = type; } private updateContactsLength(updatePartClassName: boolean) { if(this.updateContactsLengthPromise) return this.updateContactsLengthPromise; return this.updateContactsLengthPromise = this.managers.appUsersManager.getContacts().then((users) => { const subtitle = this.emptyDialogsPlaceholderSubtitle; if(subtitle) { let key: LangPackKey, args: FormatterArguments; if(users.length/* && false */) { key = 'ChatList.Main.EmptyPlaceholder.Subtitle'; args = [i18n('Contacts.Count', [users.length])]; } else { key = 'ChatList.Main.EmptyPlaceholder.SubtitleNoContacts'; args = []; } subtitle.compareAndUpdate({ key, args }); } if(updatePartClassName) { const chatList = this.chatList; const part = chatList.parentElement as HTMLElement; part.classList.toggle('has-contacts', !!users.length); } this.updateContactsLengthPromise = undefined; return users.length; }); } private removeContactsPlaceholder() { const chatList = this.chatList; const parts = chatList.parentElement.parentElement; const bottom = chatList.parentElement.nextElementSibling as HTMLElement; parts.classList.remove('with-contacts'); bottom.innerHTML = ''; this.loadContacts = undefined; this.processContact = undefined; } private _onListLengthChange = () => { if(!this.loadedDialogsAtLeastOnce) { return; } this.checkIfPlaceholderNeeded(); if(this.filterId > 0) return; const chatList = this.chatList; const count = chatList.childElementCount; const parts = chatList.parentElement.parentElement; const bottom = chatList.parentElement.nextElementSibling as HTMLElement; const hasContacts = !!bottom.childElementCount; if(count >= 10) { if(hasContacts) { this.removeContactsPlaceholder(); } return; } else if(hasContacts) return; parts.classList.add('with-contacts'); const section = new SettingSection({ name: 'Contacts', noDelimiter: true, fakeGradientDelimiter: true }); section.container.classList.add('hide'); this.managers.appUsersManager.getContactsPeerIds(undefined, undefined, 'online').then((contacts) => { let ready = false; const onListLengthChange = () => { if(ready) { section.container.classList.toggle('hide', !sortedUserList.list.childElementCount); } this.updateContactsLength(true); }; const sortedUserList = new SortedUserList({ avatarSize: 42, createChatListOptions: { dialogSize: 48, new: true }, autonomous: false, onListLengthChange, managers: this.managers }); this.loadContacts = () => { const pageCount = windowSize.height / 60 | 0; const arr = contacts.splice(0, pageCount).filter(this.verifyPeerIdForContacts); arr.forEach((peerId) => { sortedUserList.add(peerId); }); if(!contacts.length) { this.loadContacts = undefined; } }; this.loadContacts(); this.processContact = (peerId) => { if(peerId.isAnyChat()) { return; } const good = this.verifyPeerIdForContacts(peerId); const added = sortedUserList.has(peerId); if(!added && good) sortedUserList.add(peerId); else if(added && !good) sortedUserList.delete(peerId); }; const list = sortedUserList.list; list.classList.add('chatlist-new'); this.setListClickListener(list); section.content.append(list); ready = true; onListLengthChange(); }); bottom.append(section.container); }; private verifyPeerIdForContacts = async(peerId: PeerId) => { return await this.managers.appPeersManager.isContact(peerId) && !(await this.managers.appMessagesManager.getDialogOnly(peerId)); }; public onChatsRegularScroll = () => { // return; if(this.sliceTimeout) clearTimeout(this.sliceTimeout); this.sliceTimeout = window.setTimeout(() => { this.sliceTimeout = undefined; if(!this.chatList.childElementCount || this.processContact) { return; } /* const observer = new IntersectionObserver((entries) => { const }); Array.from(this.chatList.children).forEach((el) => { observer.observe(el); }); */ fastRafConventional(() => { const perf = performance.now(); const scrollTopWas = this.scroll.scrollTop; const firstElementChild = this.chatList.firstElementChild; const rectContainer = this.scroll.container.getBoundingClientRect(); const rectTarget = firstElementChild.getBoundingClientRect(); const children = Array.from(this.scroll.splitUp.children) as HTMLElement[]; // const padding = 8; // const offsetTop = this.folders.container.offsetTop; let offsetTop = this.scroll.splitUp.offsetTop; if(offsetTop && scrollTopWas < offsetTop) offsetTop -= scrollTopWas; // const offsetTop = scrollTopWas < padding ? padding - scrollTopWas : 0; const firstY = rectContainer.y + offsetTop; const lastY = rectContainer.y/* - 8 */; // 8px - .chatlist padding-bottom const firstElement = findUpTag(document.elementFromPoint(Math.ceil(rectTarget.x), Math.ceil(firstY + 1)), firstElementChild.tagName) as HTMLElement; const lastElement = findUpTag(document.elementFromPoint(Math.ceil(rectTarget.x), Math.floor(lastY + rectContainer.height - 1)), firstElementChild.tagName) as HTMLElement; //alert('got element:' + rect.y); if(!firstElement || !lastElement) { return; } //alert('got element:' + !!firstElement); const firstElementRect = firstElement.getBoundingClientRect(); const elementOverflow = firstElementRect.y - firstY; const sliced: HTMLElement[] = []; const firstIndex = children.indexOf(firstElement); const lastIndex = children.indexOf(lastElement); const saveLength = 10; const sliceFromStart = IS_SAFARI ? [] : children.slice(0, Math.max(0, firstIndex - saveLength)); const sliceFromEnd = children.slice(lastIndex + saveLength); /* if(sliceFromStart.length !== sliceFromEnd.length) { console.log('not equal', sliceFromStart.length, sliceFromEnd.length); } if(sliceFromStart.length > sliceFromEnd.length) { const diff = sliceFromStart.length - sliceFromEnd.length; sliceFromStart.splice(0, diff); } else if(sliceFromEnd.length > sliceFromStart.length) { const diff = sliceFromEnd.length - sliceFromStart.length; sliceFromEnd.splice(sliceFromEnd.length - diff, diff); } */ if(sliceFromStart.length) { this.scroll.loadedAll.top = false; } if(sliceFromEnd.length) { this.scroll.loadedAll.bottom = false; } sliced.push(...sliceFromStart); sliced.push(...sliceFromEnd); sliced.forEach((el) => { const peerId = el.dataset.peerId.toPeerId(); this.deleteDialog(peerId); }); this.setOffsets(); //this.log('[slicer] elements', firstElement, lastElement, rect, sliced, sliceFromStart.length, sliceFromEnd.length); //this.log('[slicer] reset scrollTop', this.scroll.scrollTop, firstElement.offsetTop, firstElementRect.y, rect.y, elementOverflow); //alert('left length:' + children.length); this.scroll.scrollTop = firstElement.offsetTop - elementOverflow; this.log('slice time', performance.now() - perf); /* const firstElementRect = firstElement.getBoundingClientRect(); const scrollTop = */ //this.scroll.scrollIntoView(firstElement, false); }); }, 200); }; private async setOffsets() { const chatList = this.chatList; const firstDialog = await this.getDialogFromElement(chatList.firstElementChild as HTMLElement); const lastDialog = await this.getDialogFromElement(chatList.lastElementChild as HTMLElement); const indexKey = this.indexKey; this.offsets.top = getDialogIndex(firstDialog, indexKey); this.offsets.bottom = getDialogIndex(lastDialog, indexKey); } private getDialogFromElement(element: HTMLElement) { return this.managers.appMessagesManager.getDialogOnly(element.dataset.peerId.toPeerId()); } public onChatsScrollTop = () => { return this.onChatsScroll('top'); }; public onChatsScroll = (side: SliceSides = 'bottom') => { if(this.scroll.loadedAll[side]) { if(this.loadContacts) { this.loadContacts(); } } this.log('onChatsScroll', side); return this.loadDialogs(side); }; public setListClickListener(list: HTMLUListElement, onFound?: () => void, withContext = false, autonomous = false, openInner = false) { let lastActiveListElement: HTMLElement; const setPeerFunc = (openInner ? appImManager.setInnerPeer : appImManager.setPeer).bind(appImManager); list.dataset.autonomous = '' + +autonomous; list.addEventListener('mousedown', (e) => { if(e.button !== 0) return; this.log('dialogs click list'); const target = e.target as HTMLElement; const elem = findUpTag(target, DIALOG_LIST_ELEMENT_TAG); if(!elem) { return; } if(e.ctrlKey || e.metaKey) { window.open((elem as HTMLAnchorElement).href, '_blank'); cancelEvent(e); return; } if(autonomous) { const sameElement = lastActiveListElement === elem; if(lastActiveListElement && !sameElement) { lastActiveListElement.classList.remove('active'); } if(elem) { elem.classList.add('active'); lastActiveListElement = elem; this.lastActiveElements.add(elem); } } if(elem) { if(onFound) onFound(); const peerId = elem.dataset.peerId.toPeerId(); const lastMsgId = +elem.dataset.mid || undefined; setPeerFunc({ peerId, lastMsgId }); } else { setPeerFunc(); } }, {capture: true}); // cancel link click list.addEventListener('click', (e) => { if(e.button === 0) { cancelEvent(e); } }, {capture: true}); if(DEBUG) { list.addEventListener('dblclick', (e) => { const li = findUpTag(e.target, DIALOG_LIST_ELEMENT_TAG); if(li) { const peerId = li.dataset.peerId.toPeerId(); this.log('debug dialog:', this.managers.appMessagesManager.getDialogByPeerId(peerId)); } }); } if(withContext) { attachContextMenuListener(list, this.contextMenu.onContextMenu); } } public createChatList(options: { // avatarSize?: number, // handheldsSize?: number, // size?: number, new?: boolean, dialogSize?: number, ignoreClick?: boolean } = {}) { const list = document.createElement('ul'); list.classList.add('chatlist'/* , 'chatlist-avatar-' + (options.avatarSize || 54) *//* , 'chatlist-' + (options.size || 72) */); if(options.new) { list.classList.add('chatlist-new'); } if(options.dialogSize) { list.classList.add('chatlist-' + options.dialogSize); } // if(options.ignoreClick) { // list.classList.add('disable-hover'); // } /* if(options.handheldsSize) { list.classList.add('chatlist-handhelds-' + options.handheldsSize); } */ return list; } public setLastMessageN(options: { dialog: Dialog, lastMessage?: Message.message | Message.messageService, dom?: DialogDom, highlightWord?: string, isBatch?: boolean, setUnread?: boolean }) { const promise = this.setLastMessage(options.dialog, options.lastMessage, options.dom, options.highlightWord, options.isBatch, options.setUnread); return promise.catch(noop); } private async setLastMessage( dialog: Dialog, lastMessage: Message.message | Message.messageService, dom: DialogDom, highlightWord?: string, isBatch = false, setUnread = false ) { if(!dom) { dom = this.getDialogDom(dialog.peerId); if(!dom) { return; } } const {deferred: promise, middleware} = setPromiseMiddleware(dom, 'setLastMessagePromise'); let draftMessage: MyDraftMessage; if(!lastMessage) { if(dialog.draft?._ === 'draftMessage') { draftMessage = dialog.draft; } lastMessage = dialog.topMessage; if(!lastMessage) { const promise = this.managers.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); lastMessage = await middleware(promise); } } if(setUnread) { this.setUnreadMessagesN({dialog, dom, isBatch, setLastMessagePromise: promise}); } if(!lastMessage/* || (lastMessage._ === 'messageService' && !lastMessage.rReply) */) { dom.lastMessageSpan.textContent = ''; dom.lastTimeSpan.textContent = ''; delete dom.listEl.dataset.mid; promise.resolve(); return; } const peerId = dialog.peerId; const isRestricted = lastMessage && isMessageRestricted(lastMessage as Message.message); /* if(!dom.lastMessageSpan.classList.contains('user-typing')) */ { let mediaContainer: HTMLElement; const willPrepend: (Promise | HTMLElement)[] = []; if(lastMessage && !draftMessage && !isRestricted) { const media: MyDocument | MyPhoto = getMediaFromMessage(lastMessage); const videoTypes: Set = new Set(['video', 'gif', 'round']); if(media && (media._ === 'photo' || videoTypes.has(media.type))) { const size = choosePhotoSize(media, 20, 20); if(size._ !== 'photoSizeEmpty') { mediaContainer = document.createElement('div'); mediaContainer.classList.add('dialog-subtitle-media'); if((media as MyDocument).type === 'round') { mediaContainer.classList.add('is-round'); } willPrepend.push(wrapPhoto({ photo: media, message: lastMessage, container: mediaContainer, withoutPreloader: true, size }).then(() => mediaContainer)); if(videoTypes.has((media as MyDocument).type)) { const playIcon = document.createElement('span'); playIcon.classList.add('tgico-play'); mediaContainer.append(playIcon); } } } } /* if(lastMessage.from_id === auth.id) { // You: */ if(draftMessage) { const bold = document.createElement('b'); bold.classList.add('danger'); bold.append(i18n('Draft'), ': '); willPrepend.unshift(bold); } else if(peerId.isAnyChat() && peerId !== lastMessage.fromId && !(lastMessage as Message.messageService).action) { const senderBold = document.createElement('b'); if(lastMessage.fromId === rootScope.myId) { senderBold.append(i18n('FromYou')); willPrepend.unshift(senderBold); } else { //str = sender.first_name || sender.last_name || sender.username; const p = middleware(wrapPeerTitle({ peerId: lastMessage.fromId, onlyFirstName: true, })).then((element) => { senderBold.prepend(element); return senderBold; }, noop); willPrepend.unshift(p); } senderBold.append(': '); //console.log(sender, senderBold.innerText); } const withoutMediaType = !!mediaContainer && !!(lastMessage as Message.message)?.message; let fragment: DocumentFragment; if(highlightWord && (lastMessage as Message.message).message) { fragment = await middleware(wrapMessageForReply(lastMessage, undefined, undefined, false, highlightWord, withoutMediaType)); } else if(draftMessage) { fragment = await middleware(wrapMessageForReply(draftMessage)); } else if(lastMessage) { fragment = await middleware(wrapMessageForReply(lastMessage, undefined, undefined, false, undefined, withoutMediaType)); } else { // rare case fragment = document.createDocumentFragment(); } if(willPrepend.length) { const elements = await middleware(Promise.all(willPrepend)); fragment.prepend(...elements); } replaceContent(dom.lastMessageSpan, fragment); } if(lastMessage || draftMessage/* && lastMessage._ !== 'draftMessage' */) { const date = draftMessage ? Math.max(draftMessage.date, lastMessage.date || 0) : lastMessage.date; replaceContent(dom.lastTimeSpan, formatDateAccordingToTodayNew(new Date(date * 1000))); } else dom.lastTimeSpan.textContent = ''; if(setUnread !== null && !setUnread) { // means search dom.listEl.dataset.mid = '' + lastMessage.mid; } promise.resolve(); } private setUnreadMessagesN(options: { dialog: Dialog, dom?: DialogDom, isBatch?: boolean, setLastMessagePromise?: Promise }) { return this.setUnreadMessages(options.dialog, options.dom, options.isBatch, options.setLastMessagePromise).catch(() => {}); } private async setUnreadMessages( dialog: Dialog, dom = this.getDialogDom(dialog.peerId), isBatch = false, setLastMessagePromise?: Promise ) { if(!dom) { //this.log.error('setUnreadMessages no dom!', dialog); return; } const {deferred, middleware} = setPromiseMiddleware(dom, 'setUnreadMessagePromise'); const isMuted = await middleware(this.managers.appNotificationsManager.isPeerLocalMuted(dialog.peerId, true)); const wasMuted = dom.listEl.classList.contains('is-muted'); let setStatusMessage: MyMessage; if(dialog.draft?._ !== 'draftMessage') { const lastMessage: MyMessage = await middleware(this.managers.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message)); if(lastMessage && lastMessage.pFlags.out && lastMessage.peerId !== rootScope.myId) { setStatusMessage = lastMessage; } } const filter = await middleware(this.managers.filtersStorage.getFilter(this.filterId)); let isPinned: boolean; if(filter) { isPinned = filter.pinnedPeerIds.indexOf(dialog.peerId) !== -1; } else { isPinned = !!dialog.pFlags.pinned; } const isDialogUnread = await middleware(this.managers.appMessagesManager.isDialogUnread(dialog)); const hasUnreadBadge = isPinned || isDialogUnread; // dom.messageEl.classList.toggle('has-badge', hasBadge); // * have to await all promises before modifying something if(setLastMessagePromise) { try { await middleware(setLastMessagePromise); } catch(err) { // return; } } const transitionDuration = isBatch ? 0 : 200; if(isMuted !== wasMuted) { SetTransition(dom.listEl, 'is-muted', isMuted, transitionDuration); } setSendingStatus(dom.statusSpan, setStatusMessage, true); const isUnreadBadgeMounted = isInDOM(dom.unreadBadge); if(hasUnreadBadge && !isUnreadBadgeMounted) { dom.subtitleEl.append(dom.unreadBadge); } const hasMentionsBadge = dialog.unread_mentions_count && (dialog.unread_mentions_count > 1 || dialog.unread_count > 1); const isMentionBadgeMounted = dom.mentionsBadge && isInDOM(dom.mentionsBadge); if(hasMentionsBadge) { if(!dom.mentionsBadge) { dom.mentionsBadge = document.createElement('div'); dom.mentionsBadge.className = 'dialog-subtitle-badge badge badge-24 mention mention-badge'; dom.mentionsBadge.innerText = '@'; dom.subtitleEl.insertBefore(dom.mentionsBadge, dom.lastMessageSpan.nextSibling); } } SetTransition(dom.unreadBadge, 'is-visible', hasUnreadBadge, transitionDuration, hasUnreadBadge ? undefined : () => { dom.unreadBadge.remove(); }, !isUnreadBadgeMounted ? 2 : 0); if(dom.mentionsBadge) { SetTransition(dom.mentionsBadge, 'is-visible', hasMentionsBadge, transitionDuration, hasMentionsBadge ? undefined : () => { dom.mentionsBadge.remove(); delete dom.mentionsBadge; }, !isMentionBadgeMounted ? 2 : 0); } if(!hasUnreadBadge) { deferred.resolve(); return; } if(isPinned) { dom.unreadBadge.classList.add('tgico-chatspinned', 'tgico'); } else { dom.unreadBadge.classList.remove('tgico-chatspinned', 'tgico'); } let isUnread = true, isMention = false; if(dialog.unread_mentions_count && dialog.unread_count === 1) { dom.unreadBadge.innerText = '@'; isMention = true; // dom.unreadBadge.classList.add('tgico-mention', 'tgico'); } else if(isDialogUnread) { //dom.unreadMessagesSpan.innerText = '' + (dialog.unread_count ? formatNumber(dialog.unread_count, 1) : ' '); dom.unreadBadge.innerText = '' + (dialog.unread_count || ' '); } else { dom.unreadBadge.innerText = ''; isUnread = false; } dom.unreadBadge.classList.toggle('unread', isUnread); dom.unreadBadge.classList.toggle('mention', isMention); deferred.resolve(); } private getDialogDom(peerId: PeerId) { // return this.doms[peerId]; const element = this.sortedList.get(peerId); return element?.dom; } private async getDialog(dialog: Dialog | PeerId) { if(typeof(dialog) !== 'object') { const originalDialog = await this.managers.appMessagesManager.getDialogOnly(dialog); if(!originalDialog) { const peerId = dialog || NULL_PEER_ID; return { peerId, peer: await this.managers.appPeersManager.getOutputPeer(peerId), pFlags: {} } as any as Dialog; } return originalDialog; } return dialog as Dialog; } private setCallStatus(dom: DialogDom, visible: boolean) { let {callIcon, listEl} = dom; if(!callIcon && visible) { const {canvas, startAnimation} = dom.callIcon = callIcon = groupCallActiveIcon(listEl.classList.contains('active')); canvas.classList.add('dialog-group-call-icon'); listEl.append(canvas); startAnimation(); } if(!callIcon) { return; } SetTransition(dom.callIcon.canvas, 'is-visible', visible, 200, visible ? undefined : () => { dom.callIcon.canvas.remove(); dom.callIcon = undefined; }, visible ? 2 : 0); } public addListDialog(options: Parameters[0] & {isBatch?: boolean}) { options.autonomous = false; const ret = this.addDialogNew(options); if(ret) { const promise = this.getDialog(options.peerId).then((dialog) => { const {peerId} = dialog; const promises: Promise[] = []; if(!peerId.isUser()) { promises.push(this.processDialogForCallStatus(dialog, ret.dom)); } if(peerId !== rootScope.myId && peerId.isUser()) { promises.push(this.managers.appUsersManager.getUser(peerId).then((user) => { if(user.status?._ === 'userStatusOnline') { this.setOnlineStatus(ret.dom.avatarEl, true); } })); } promises.push(this.setLastMessageN({ dialog, dom: ret.dom, isBatch: options.isBatch, setUnread: true })); return Promise.all(promises); }); if(options.loadPromises) { options.loadPromises.push(promise); } } return ret; } private async processDialogForCallStatus(dialog: Dialog, dom?: DialogDom) { if(!IS_GROUP_CALL_SUPPORTED) { return; } if(!dom) dom = this.getDialogDom(dialog.peerId); if(!dom) return; const chat: Chat.chat | Chat.channel = await this.managers.appChatsManager.getChat(dialog.peerId.toChatId()); this.setCallStatus(dom, !!(chat.pFlags.call_active && chat.pFlags.call_not_empty)); } /** * use for rendering search result */ public addDialogAndSetLastMessage(options: Omit[0], 'dialog'> & { message: MyMessage, peerId: PeerId, query?: string }) { const {peerId, message, query} = options; const ret = this.addDialogNew({ ...options, ...getMessageSenderPeerIdOrName(message), peerId, }); // this.setLastMessage(ret.dialog, message, ret.dom, query); if(message.peerId !== peerId) { ret.dom.listEl.dataset.peerId = '' + message.peerId; } return ret; } public addDialogNew(options: { peerId: Parameters[0], container?: Parameters[1], rippleEnabled?: boolean, onlyFirstName?: boolean, meAsSaved?: boolean, append?: boolean, avatarSize?: number, autonomous?: boolean, lazyLoadQueue?: LazyLoadQueue, loadPromises?: Promise[], fromName?: string }) { return this.addDialog(options.peerId, options.container, options.rippleEnabled, options.onlyFirstName, options.meAsSaved, options.append, options.avatarSize, options.autonomous, options.lazyLoadQueue, options.loadPromises, options.fromName); } public addDialog( peerId: PeerId, container?: HTMLElement | Scrollable | DocumentFragment | false, rippleEnabled = true, onlyFirstName = false, meAsSaved = true, append = true, avatarSize = 54, autonomous = !!container, lazyLoadQueue?: LazyLoadQueue, loadPromises?: Promise[], fromName?: string ) { // const dialog = await this.getDialog(_dialog); const avatarEl = new AvatarElement(); avatarEl.classList.add('dialog-avatar', 'avatar-' + avatarSize); avatarEl.updateWithOptions({ loadPromises, lazyLoadQueue, isDialog: !!meAsSaved, peerId, peerTitle: fromName }); const captionDiv = document.createElement('div'); captionDiv.classList.add('user-caption'); const titleSpanContainer = document.createElement('span'); titleSpanContainer.classList.add('user-title'); const peerTitle = new PeerTitle(); const peerTitlePromise = peerTitle.update({ peerId, fromName, dialog: meAsSaved, onlyFirstName, plainText: false }); if(loadPromises) { loadPromises.push(peerTitlePromise); } titleSpanContainer.append(peerTitle.element); //p.classList.add('') // в других случаях иконка верификации не нужна (а первый - это главные чатлисты) //if(!container) { // for muted icon titleSpanContainer.classList.add('tgico'); // * эта строка будет актуальна только для !container, но ладно const titleIconsPromise = generateTitleIcons(peerId).then((elements) => { titleSpanContainer.append(...elements); }); if(loadPromises) { loadPromises.push(titleIconsPromise); } //} const span = document.createElement('span'); span.classList.add('user-last-message'); span.setAttribute('dir', 'auto'); //captionDiv.append(titleSpan); //captionDiv.append(span); const li = document.createElement(DIALOG_LIST_ELEMENT_TAG); li.classList.add('chatlist-chat'); (li as HTMLAnchorElement).href = '#' + peerId; if(rippleEnabled) { ripple(li); } li.append(avatarEl, captionDiv); li.dataset.peerId = '' + peerId; const statusSpan = document.createElement('span'); statusSpan.classList.add('message-status', 'sending-status'/* , 'transition', 'reveal' */); const lastTimeSpan = document.createElement('span'); lastTimeSpan.classList.add('message-time'); const unreadBadge = document.createElement('div'); unreadBadge.className = 'dialog-subtitle-badge badge badge-24'; const titleP = document.createElement('p'); titleP.classList.add('dialog-title'); const rightSpan = document.createElement('span'); rightSpan.classList.add('dialog-title-details'); rightSpan.append(statusSpan, lastTimeSpan); titleP.append(titleSpanContainer, rightSpan); const subtitleEl = document.createElement('p'); subtitleEl.classList.add('dialog-subtitle'); subtitleEl.append(span); captionDiv.append(titleP, subtitleEl); const dom: DialogDom = { avatarEl, captionDiv, titleSpan: peerTitle.element, titleSpanContainer, statusSpan, lastTimeSpan, unreadBadge, lastMessageSpan: span, containerEl: li, listEl: li, subtitleEl }; /* let good = false; for(const folderId in this.chatLists) { if(this.chatLists[folderId] === container) { good = true; } } */ if(container) { const method = append ? 'append' : 'prepend'; container[method](li); } if(!autonomous) { // @ts-ignore li.dialogDom = dom; if(appImManager.chat?.peerId === peerId) { this.setDialogActive(li, true); } } return {dom}; } public async setTyping(dialog: Dialog) { const dom = this.getDialogDom(dialog.peerId); if(!dom) { return; } const oldTypingElement = dom.lastMessageSpan.querySelector('.peer-typing-container') as HTMLElement; const newTypingElement = await appImManager.getPeerTyping(dialog.peerId, oldTypingElement); if(!oldTypingElement && newTypingElement) { replaceContent(dom.lastMessageSpan, newTypingElement); dom.lastMessageSpan.classList.add('user-typing'); } } public unsetTyping(dialog: Dialog) { const dom = this.getDialogDom(dialog.peerId); if(!dom) { return; } dom.lastMessageSpan.classList.remove('user-typing'); this.setLastMessageN({ dialog, lastMessage: null, dom, setUnread: null }); } } const appDialogsManager = new AppDialogsManager(); MOUNT_CLASS_TO.appDialogsManager = appDialogsManager; export default appDialogsManager;