tweb/src/lib/appManagers/appDialogsManager.ts
2022-08-31 06:23:20 +02:00

2209 lines
72 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 {FOLDER_ID_ALL, FOLDER_ID_ARCHIVE, NULL_PEER_ID, REAL_FOLDERS, REAL_FOLDER_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 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';
import filterAsync from '../../helpers/array/filterAsync';
import forEachReverse from '../../helpers/array/forEachReverse';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import whichChild from '../../helpers/dom/whichChild';
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<typeof groupCallActiveIcon>,
mentionsBadge?: HTMLElement,
lastMessageSpan: HTMLSpanElement,
containerEl: HTMLElement,
listEl: HTMLElement,
subtitleEl: HTMLElement,
setLastMessagePromise?: CancellablePromise<void>,
setUnreadMessagePromise?: CancellablePromise<void>
};
interface SortedDialog extends SortedElementBase {
dom: DialogDom,
loadPromises?: Promise<any>[]
}
function setPromiseMiddleware<T extends {[smth in K as K]?: CancellablePromise<void>}, K extends keyof T>(obj: T, key: K) {
const oldPromise: CancellablePromise<void> = obj[key] as any;
if(oldPromise) {
oldPromise.reject();
}
// @ts-ignore
const deferred = obj[key] = deferredPromise<void>();
deferred.catch(() => {}).finally(() => {
if((obj[key] as any) === deferred) {
delete obj[key];
}
});
const middleware = middlewarePromise(() => (obj[key] as any) === deferred);
return {deferred, middleware};
}
class SortedDialogList extends SortedList<SortedDialog> {
constructor(
public managers: AppManagers,
public list: HTMLUListElement,
public indexKey: ReturnType<typeof getDialogIndexKey>,
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<any>[] = 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.textContent = '';
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<void>;
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<void>;
private sliceTimeout: number;
private lastActiveElements: Set<HTMLElement> = new Set();
private offsets: {top: number, bottom: number} = {top: 0, bottom: 0};
private loadContacts: () => void;
private processContact: (peerId: PeerId) => void;
private indexKey: ReturnType<typeof getDialogIndexKey>;
private initedListeners = false;
private onListLengthChange: () => Promise<void>;
private loadedDialogsAtLeastOnce = false;
private allChatsIntlElement: I18n.IntlElement;
private emptyDialogsPlaceholderSubtitle: I18n.IntlElement;
private updateContactsLengthPromise: Promise<number>;
private filtersNavigationItem: NavigationItem;
private managers: AppManagers;
private selectTab: ReturnType<typeof horizontalMenu>;
public start() {
const managers = this.managers = getProxiedManagers();
this.contextMenu = new DialogsContextMenu(managers);
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 = `<div class="rp"><avatar-element style="background-color: rgb(166, 149, 231); font-size: 0px;"><img src="assets/img/pepe.jpg"></avatar-element><div class="user-caption"><p><span class="user-title">${i}</span><span><span class="message-status"></span><span class="message-time">18:33</span></span></p><p><span class="user-last-message"><b>-_-_-_-: </b>qweasd</span><span></span></p></div></div>`;
i++;
this.scroll.append(li);
};
for(let i = 0; i < 500; ++i) {
add();
}
(window as any).addElement = add;
} */
rootScope.addEventListener('premium_toggle', async(isPremium) => {
if(isPremium) {
return;
}
const isFolderAvailable = await this.managers.filtersStorage.isFilterIdAvailable(this.filterId);
if(!isFolderAvailable) {
selectTab(whichChild(this.filtersRendered[FOLDER_ID_ALL].menu), false);
}
});
rootScope.addEventListener('state_cleared', () => {
const clearCurrent = REAL_FOLDERS.has(this.filterId);
// setTimeout(() =>
apiManagerProxy.getState().then(async(state) => {
this.loadedDialogsAtLeastOnce = false;
/* const clearPromises: Promise<any>[] = [];
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());
} */
if(clearCurrent) {
this.sortedList.clear();
this.onTabChange();
}
this.onStateLoaded(state);
})// , 5000);
});
this.setFilterId(FOLDER_ID_ALL, FOLDER_ID_ALL);
this.addFilter({
id: FOLDER_ID_ALL,
title: '',
localId: FOLDER_ID_ALL
});
const foldersScrollable = new ScrollableX(this.folders.menuScrollContainer);
bottomPart.prepend(this.folders.menuScrollContainer);
const selectTab = this.selectTab = horizontalMenu(this.folders.menu, this.folders.container, async(id, tabContent) => {
/* if(id !== 0) {
id += 1;
} */
const _id = id;
id = +tabContent.dataset.filterId || FOLDER_ID_ALL;
const isFilterAvailable = REAL_FOLDERS.has(id) || await this.managers.filtersStorage.isFilterIdAvailable(id);
if(!isFilterAvailable) {
return false;
}
const wasFilterId = this.filterId;
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(wasFilterId === id) return;
this.sortedLists[id].clear();
const promise = this.setFilterIdAndChangeTab(id).then(({cached, renderPromise}) => {
if(cached) {
return renderPromise;
}
});
if(wasFilterId !== -1) {
return promise;
}
}, () => {
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<any> = isLoadedMain ? Promise.resolve() : this.managers.appMessagesManager.getConversationsAll('', 0);
const b: Promise<any> = 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, false);
}
public get chatList() {
return this.sortedList.list;
}
public setFilterId(filterId: number, localId: MyDialogFilter['localId']) {
this.filterId = filterId;
this.indexKey = getDialogIndexKey(REAL_FOLDERS.has(filterId) ? filterId as REAL_FOLDER_ID : localId);
}
public async setFilterIdAndChangeTab(filterId: number) {
this.filterId = filterId;
this.indexKey = await this.managers.dialogsStorage.getDialogIndexKeyByFilterId(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, dialog] of dialogs) {
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
});
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(REAL_FOLDERS.has(filter.id)) {
return;
}
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();
elements.container.remove();
elements.menu.remove();
delete this.sortedLists[filter.id];
delete this.scrollables[filter.id];
delete this.filtersRendered[filter.id];
this.onFiltersLengthChange();
if(this.filterId === filter.id) {
this.selectTab(0, false);
}
});
rootScope.addEventListener('filter_order', async(order) => {
order = order.slice();
indexOfAndSplice(order, FOLDER_ID_ARCHIVE);
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.localId);
positionElementByIndex(renderedFilter.container, this.folders.container, filter.localId);
});
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 filtersArr = state.filtersArr;
const haveFilters = filtersArr.length > REAL_FOLDERS.size;
// const filter = filtersArr.find((filter) => filter.id !== FOLDER_ID_ARCHIVE);
const addFilters = (filters: MyDialogFilter[]) => {
for(const filter of filters) {
this.addFilter(filter);
}
};
let addFiltersPromise: Promise<any>;
if(haveFilters) {
addFilters(filtersArr);
} else {
addFiltersPromise = this.managers.filtersStorage.getDialogFilters().then(addFilters);
}
const loadDialogsPromise = this.onChatsScroll();
await loadDialogsPromise;
this.loadDialogsRenderPromise = undefined;
addFiltersPromise && await addFiltersPromise;
// this.folders.menu.children[0].classList.add('active');
this.filterId = -1;
this.selectTab(0, false);
if(!this.initedListeners) {
this.initListeners();
this.initedListeners = true;
}
haveFilters && this.showFiltersPromise && await this.showFiltersPromise;
this.managers.appNotificationsManager.getNotifyPeerTypeSettings();
await (await loadDialogsPromise).renderPromise.catch(noop);
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) {
if(filterId === FOLDER_ID_ALL) {
return;
}
const unreadSpan = this.filtersRendered[filterId]?.unread;
if(!unreadSpan) {
return;
}
const {unreadUnmutedCount, unreadCount} = await this.managers.dialogsStorage.getFolderUnreadCount(filterId);
unreadSpan.classList.toggle('badge-gray', !unreadUnmutedCount);
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 ||
(!REAL_FOLDERS.has(this.filterId) ? 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<AppDialogsManager['addFilter']>[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.localId),
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<DialogFilter, 'title' | 'id' | 'localId'>) {
if(filter.id === FOLDER_ID_ARCHIVE) {
return;
}
const containerToAppend = this.folders.menu as HTMLElement;
const renderedFilter = this.filtersRendered[filter.id];
if(renderedFilter) {
positionElementByIndex(renderedFilter.menu, containerToAppend, filter.localId);
positionElementByIndex(renderedFilter.container, this.folders.container, filter.localId);
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 === FOLDER_ID_ALL) 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);
menuTab.dataset.filterId = '' + filter.id;
positionElementByIndex(menuTab, containerToAppend, filter.localId);
// 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.localId);
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<void>((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;
} */
this.log.warn('load', side);
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<boolean>();
const renderPromise = new Promise<void>(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 === FOLDER_ID_ARCHIVE ? 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<any>[] = [];
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 === FOLDER_ID_ARCHIVE) {
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<AppDialogsManager['generateEmptyPlaceholder']>, 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 !== FOLDER_ID_ALL) 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 promise = filterAsync(contacts.splice(0, pageCount), this.verifyPeerIdForContacts);
promise.then((arr) => {
arr.forEach((peerId) => {
sortedUserList.add(peerId);
});
});
if(!contacts.length) {
this.loadContacts = undefined;
}
};
this.loadContacts();
this.processContact = async(peerId) => {
if(peerId.isAnyChat()) {
return;
}
const good = await 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;
}
const peerId = elem.dataset.peerId.toPeerId();
if(e.ctrlKey || e.metaKey) {
window.open((elem as HTMLAnchorElement).href || ('#' + peerId), '_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 lastMsgId = +elem.dataset.mid || undefined;
setPeerFunc({
peerId, lastMsgId
});
} else {
setPeerFunc();
}
}, {capture: true});
// cancel link click
// ! do not change it to attachClickEvent
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 || lastMessage.mid !== dialog.top_message) {
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<any> | HTMLElement)[] = [];
if(lastMessage && !draftMessage && !isRestricted) {
const media: MyDocument | MyPhoto = getMediaFromMessage(lastMessage);
const videoTypes: Set<MyDocument['type']> = 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<void>
}) {
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<void>
) {
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<AppDialogsManager['addDialogNew']>[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<any>[] = [];
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<Parameters<AppDialogsManager['addDialogNew']>[0], 'dialog'> & {
message: MyMessage,
peerId: PeerId,
query?: string
}) {
const {peerId, message, query} = options;
const ret = this.addDialogNew({
...options,
...getMessageSenderPeerIdOrName(message),
peerId
});
this.setLastMessage({_: 'dialog', peerId} as any, message, ret.dom, query);
if(message.peerId !== peerId) {
ret.dom.listEl.dataset.peerId = '' + message.peerId;
}
return ret;
}
public addDialogNew(options: {
peerId: Parameters<AppDialogsManager['addDialog']>[0],
container?: Parameters<AppDialogsManager['addDialog']>[1],
rippleEnabled?: boolean,
onlyFirstName?: boolean,
meAsSaved?: boolean,
append?: boolean,
avatarSize?: number,
autonomous?: boolean,
lazyLoadQueue?: LazyLoadQueue,
loadPromises?: Promise<any>[],
fromName?: string,
noIcons?: boolean
}) {
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, options.noIcons);
}
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<any>[],
fromName?: string,
noIcons?: boolean
) {
// 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,
withIcons: !noIcons
});
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');
if(!autonomous) (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;