tweb/src/lib/appManagers/appDialogsManager.ts

1436 lines
46 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 AvatarElement from "../../components/avatar";
import DialogsContextMenu from "../../components/dialogsContextMenu";
import { horizontalMenu } from "../../components/horizontalMenu";
import { attachContextMenuListener, putPreloader } from "../../components/misc";
import { ripple } from "../../components/ripple";
//import Scrollable from "../../components/scrollable";
import Scrollable, { ScrollableX, SliceSides } from "../../components/scrollable";
import { formatDateAccordingToTodayNew } from "../../helpers/date";
import { isSafari } from "../../helpers/userAgent";
import { logger, LogTypes } from "../logger";
import { RichTextProcessor } from "../richtextprocessor";
import rootScope from "../rootScope";
import { positionElementByIndex, replaceContent } from "../../helpers/dom";
import apiUpdatesManager from "./apiUpdatesManager";
import appPeersManager from './appPeersManager';
import appImManager from "./appImManager";
import appMessagesManager, { Dialog } from "./appMessagesManager";
import {MyDialogFilter as DialogFilter} from "../storages/filters";
import appStateManager from "./appStateManager";
import appUsersManager from "./appUsersManager";
import Button from "../../components/button";
import SetTransition from "../../components/singleTransition";
import sessionStorage from '../sessionStorage';
import appDraftsManager, { MyDraftMessage } from "./appDraftsManager";
import ProgressivePreloader from "../../components/preloader";
import App from "../../config/app";
import DEBUG, { MOUNT_CLASS_TO } from "../../config/debug";
import appNotificationsManager from "./appNotificationsManager";
import PeerTitle from "../../components/peerTitle";
import { i18n } from "../langPack";
import findUpTag from "../../helpers/dom/findUpTag";
export type DialogDom = {
avatarEl: AvatarElement,
captionDiv: HTMLDivElement,
titleSpan: HTMLSpanElement,
titleSpanContainer: HTMLSpanElement,
statusSpan: HTMLSpanElement,
lastTimeSpan: HTMLSpanElement,
unreadMessagesSpan: HTMLSpanElement,
lastMessageSpan: HTMLSpanElement,
containerEl: HTMLElement,
listEl: HTMLLIElement,
muteAnimationTimeout?: number
};
//const testScroll = false;
//let testTopSlice = 1;
class ConnectionStatusComponent {
public static CHANGE_STATE_DELAY = 1000;
private statusContainer: HTMLElement;
private statusEl: HTMLElement;
private statusPreloader: ProgressivePreloader;
private currentText = '';
private connecting = false;
private updating = false;
private log: ReturnType<typeof logger>;
private setFirstConnectionTimeout: number;
private setStateTimeout: number;
constructor(chatsContainer: HTMLElement) {
this.log = logger('CS');
this.statusContainer = document.createElement('div');
this.statusContainer.classList.add('connection-status');
this.statusEl = Button('btn-primary bg-warning connection-status-button', {noRipple: true});
this.statusPreloader = new ProgressivePreloader({cancelable: false});
this.statusPreloader.constructContainer({color: 'transparent', bold: true});
this.statusContainer.append(this.statusEl);
chatsContainer.prepend(this.statusContainer);
rootScope.on('connection_status_change', (e) => {
const status = e;
console.log(status);
this.setConnectionStatus();
});
rootScope.on('state_synchronizing', (e) => {
const channelId = e;
if(!channelId) {
this.updating = true;
DEBUG && this.log('updating', this.updating);
this.setState();
}
});
rootScope.on('state_synchronized', (e) => {
const channelId = e;
DEBUG && this.log('state_synchronized', channelId);
if(!channelId) {
this.updating = false;
DEBUG && this.log('updating', this.updating);
this.setState();
}
});
this.setFirstConnectionTimeout = window.setTimeout(this.setConnectionStatus, ConnectionStatusComponent.CHANGE_STATE_DELAY + 1e3);
/* let bool = true;
document.addEventListener('dblclick', () => {
rootScope.broadcast('connection_status_change', {
dcId: 2,
isFileDownload: false,
isFileNetworker: false,
isFileUpload: false,
name: "NET-2",
online: bool = !bool,
_: "networkerStatus"
});
}); */
}
private setConnectionStatus = () => {
sessionStorage.get('dc').then(baseDcId => {
if(!baseDcId) {
baseDcId = App.baseDcId;
}
if(this.setFirstConnectionTimeout) {
clearTimeout(this.setFirstConnectionTimeout);
this.setFirstConnectionTimeout = 0;
}
const status = rootScope.connectionStatus['NET-' + baseDcId];
const online = status && status.online;
if(this.connecting && online) {
apiUpdatesManager.forceGetDifference();
}
this.connecting = !online;
DEBUG && this.log('connecting', this.connecting);
this.setState();
});
};
private setStatusText = (text: string) => {
if(this.currentText === text) return;
this.statusEl.innerText = this.currentText = text;
this.statusPreloader.attach(this.statusEl);
};
private setState = () => {
const timeout = ConnectionStatusComponent.CHANGE_STATE_DELAY;
if(this.connecting) {
this.setStatusText('Waiting for network...');
} else if(this.updating) {
this.setStatusText('Updating...');
}
DEBUG && this.log('setState', this.connecting || this.updating);
window.requestAnimationFrame(() => {
if(this.setStateTimeout) clearTimeout(this.setStateTimeout);
const cb = () => {
SetTransition(this.statusContainer, 'is-shown', this.connecting || this.updating, 200);
this.setStateTimeout = 0;
DEBUG && this.log('setState: isShown:', this.connecting || this.updating);
};
this.setStateTimeout = window.setTimeout(cb, timeout);
//cb();
/* if(timeout) this.setStateTimeout = window.setTimeout(cb, timeout);
else cb(); */
});
};
}
export class AppDialogsManager {
public chatList: HTMLUListElement;
public doms: {[peerId: number]: DialogDom} = {};
public chatsContainer = document.getElementById('chatlist-container') as HTMLDivElement;
private chatsPreloader: HTMLElement;
public loadDialogsPromise: Promise<any>;
public scroll: Scrollable = null;
public _scroll: Scrollable = null;
private log = logger('DIALOGS', LogTypes.Log | LogTypes.Error | LogTypes.Warn | LogTypes.Debug);
public contextMenu = new DialogsContextMenu();
public chatLists: {[filterId: number]: HTMLUListElement};
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 allUnreadCount: HTMLElement;
private accumulateArchivedTimeout: number;
//private topOffsetIndex = 0;
private sliceTimeout: number;
private reorderDialogsTimeout: number;
private lastActiveElements: Set<HTMLElement> = new Set();
constructor() {
const archivedChatList = this.createChatList();
this.chatLists = {
1: archivedChatList
};
this.setListClickListener(archivedChatList, null, true);
//this.setListClickListener(archivedChatList, null, true); // * to test peer changing
this.chatsPreloader = putPreloader(null, true);
this.allUnreadCount = this.folders.menu.querySelector('.badge');
this.folders.menuScrollContainer = this.folders.menu.parentElement;
const bottomPart = document.createElement('div');
bottomPart.classList.add('connection-status-bottom');
bottomPart.append(this.folders.container);
this.scroll = this._scroll = new Scrollable(bottomPart, 'CL', 500);
this.scroll.container.addEventListener('scroll', this.onChatsRegularScroll);
this.scroll.onScrolledTop = this.onChatsScrollTop;
this.scroll.onScrolledBottom = this.onChatsScroll;
//this.scroll.attachSentinels();
/* 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();
}
});
} */
this.filterId = 0;
this.addFilter({
id: this.filterId,
title: '',
titleEl: i18n('ChatList.Filter.All'),
orderIndex: 0
});
this.chatList = this.chatLists[this.filterId];
this.scroll.setVirtualContainer(this.chatList);
/* 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.on('user_update', (userId) => {
//console.log('updating user:', user, dialog);
const dom = this.getDialogDom(userId);
if(dom && !appUsersManager.isBot(userId) && userId !== rootScope.myId) {
const user = appUsersManager.getUser(userId);
const online = user.status?._ === 'userStatusOnline';
dom.avatarEl.classList.toggle('is-online', online);
}
});
/* rootScope.$on('dialog_top', (e) => {
const dialog = e;
this.setLastMessage(dialog);
this.setDialogPosition(dialog);
this.setFiltersUnreadCount();
}); */
rootScope.on('dialog_flush', (e) => {
const peerId: number = e.peerId;
const dialog = appMessagesManager.getDialogOnly(peerId);
if(dialog) {
this.setLastMessage(dialog);
this.validateForFilter();
this.setFiltersUnreadCount();
}
});
rootScope.on('dialogs_multiupdate', (e) => {
const dialogs = e;
for(const id in dialogs) {
const dialog = dialogs[id];
this.updateDialog(dialog);
}
this.validateForFilter();
this.setFiltersUnreadCount();
});
rootScope.on('dialog_drop', (e) => {
const {peerId, dialog} = e;
const dom = this.getDialogDom(peerId);
if(dom) {
dom.listEl.remove();
delete this.doms[peerId];
}
this.setFiltersUnreadCount();
});
rootScope.on('dialog_unread', (e) => {
const info = e;
const dialog = appMessagesManager.getDialogOnly(info.peerId);
if(dialog) {
this.setUnreadMessages(dialog);
this.validateForFilter();
this.setFiltersUnreadCount();
}
});
rootScope.on('dialog_notify_settings', (dialog) => {
this.setUnreadMessages(dialog); // возможно это не нужно, но нужно менять is-muted
});
rootScope.on('dialog_draft', (e) => {
const dialog = appMessagesManager.getDialogOnly(e.peerId);
if(dialog) {
this.updateDialog(dialog);
}
});
rootScope.on('peer_changed', (e) => {
const peerId = e;
//const perf = performance.now();
for(const element of this.lastActiveElements) {
if(+element.dataset.peerId !== peerId) {
element.classList.remove('active');
this.lastActiveElements.delete(element);
}
}
const elements = Array.from(document.querySelectorAll(`[data-autonomous="0"] li[data-peer-id="${peerId}"]`)) as HTMLElement[];
elements.forEach(element => {
element.classList.add('active');
this.lastActiveElements.add(element);
});
//this.log('peer_changed total time:', performance.now() - perf);
});
rootScope.on('filter_update', (e) => {
const filter: DialogFilter = e;
if(!this.filtersRendered[filter.id]) {
this.addFilter(filter);
return;
} else if(filter.id === this.filterId) { // это нет тут смысла вызывать, так как будет dialogs_multiupdate
//this.validateForFilter();
const folder = appMessagesManager.dialogsStorage.getFolder(filter.id);
this.validateForFilter();
for(let i = 0, length = folder.length; i < length; ++i) {
const dialog = folder[i];
this.updateDialog(dialog);
}
this.setFiltersUnreadCount();
}
const elements = this.filtersRendered[filter.id];
elements.title.innerHTML = RichTextProcessor.wrapEmojiText(filter.title);
});
rootScope.on('filter_delete', (e) => {
const filter: DialogFilter = e;
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.chatLists[filter.id];
delete this.filtersRendered[filter.id];
if(Object.keys(this.filtersRendered).length <= 1) {
this.folders.menuScrollContainer.classList.add('hide');
}
});
rootScope.on('filter_order', (e) => {
const order = e;
const containerToAppend = this.folders.menu as HTMLElement;
order.forEach((filterId) => {
const filter = appMessagesManager.filtersStorage.filters[filterId];
const renderedFilter = this.filtersRendered[filterId];
positionElementByIndex(renderedFilter.menu, containerToAppend, filter.orderIndex);
positionElementByIndex(renderedFilter.container, this.folders.container, filter.orderIndex);
});
/* if(this.filterId) {
const tabIndex = order.indexOf(this.filterId) + 1;
selectTab.prevId = tabIndex;
} */
});
rootScope.on('peer_typings', (e) => {
const {peerId, typings} = e;
const dialog = appMessagesManager.getDialogOnly(peerId);
if(!dialog) return;
if(typings.length) {
this.setTyping(dialog);
} else {
this.unsetTyping(dialog);
}
});
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(this.filterId === id) return;
this.chatLists[id].innerHTML = '';
this.scroll.setVirtualContainer(this.chatLists[id]);
this.filterId = id;
this.onTabChange();
}, () => {
for(const folderId in this.chatLists) {
if(+folderId !== this.filterId) {
this.chatLists[folderId].innerHTML = '';
}
}
}, undefined, foldersScrollable);
//selectTab(0);
(this.folders.menu.firstElementChild as HTMLElement).click();
appMessagesManager.construct();
appStateManager.getState().then(async(state) => {
appNotificationsManager.getNotifyPeerTypeSettings();
const renderFiltersPromise = appMessagesManager.filtersStorage.getDialogFilters().then((filters) => {
for(const filter of filters) {
this.addFilter(filter);
}
});
if(state.filters && Object.keys(state.filters).length) {
await renderFiltersPromise;
if(this.showFiltersPromise) {
await this.showFiltersPromise;
}
}
if(appStateManager.storagesResults.dialogs.length) {
appDraftsManager.getAllDrafts();
appDraftsManager.addMissedDialogs();
}
return this.loadDialogs();
}).then(() => {
//return;
const isLoadedMain = appMessagesManager.dialogsStorage.isDialogsLoaded(0);
const isLoadedArchive = appMessagesManager.dialogsStorage.isDialogsLoaded(1);
const wasLoaded = isLoadedMain || isLoadedArchive;
const a: Promise<any> = isLoadedMain ? Promise.resolve() : appMessagesManager.getConversationsAll('', 0);
const b: Promise<any> = isLoadedArchive ? Promise.resolve() : appMessagesManager.getConversationsAll('', 1);
a.finally(() => {
b.then(() => {
this.accumulateArchivedUnread();
if(wasLoaded) {
(apiUpdatesManager.updatesState.syncLoading || Promise.resolve()).then(() => {
appMessagesManager.refreshConversations();
});
}
});
});
});
new ConnectionStatusComponent(this.chatsContainer);
this.chatsContainer.append(bottomPart);
}
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 = appMessagesManager.getDialogByPeerId(peerId);
return {index: dialog[0].index, pos: dialog[1]};
}
}
return {index: 0, pos: -1};
}
private isDialogMustBeInViewport(dialog: Dialog) {
//return true;
const topOffset = this.getOffset('top');
const bottomOffset = this.getOffset('bottom');
if(!topOffset.index && !bottomOffset.index) {
return true;
}
const index = dialog.index;
return (!topOffset.index || index <= topOffset.index) && (!bottomOffset.index || index >= bottomOffset.index);
}
private updateDialog(dialog: Dialog) {
if(!dialog) {
return;
}
if(this.isDialogMustBeInViewport(dialog)) {
if(!this.doms.hasOwnProperty(dialog.peerId)) {
const ret = this.addDialogNew({dialog});
if(ret) {
const idx = appMessagesManager.getDialogByPeerId(dialog.peerId)[1];
positionElementByIndex(ret.dom.listEl, this.chatList, idx);
} else {
return;
}
}
} else {
const dom = this.getDialogDom(dialog.peerId);
if(dom) {
dom.listEl.remove();
delete this.doms[dialog.peerId];
}
return;
}
/* const topOffset = this.getOffset('top');
if(topOffset.index && dialog.index > topOffset.index) {
const dom = this.getDialogDom(dialog.peerId);
if(dom) {
dom.listEl.remove();
delete this.doms[dialog.peerId];
}
return;
}
if(!this.doms.hasOwnProperty(dialog.peerId)) {
this.addDialogNew({dialog});
} */
if(this.getDialogDom(dialog.peerId)) {
this.setLastMessage(dialog);
this.reorderDialogs();
}
}
onTabChange = () => {
this.doms = {};
this.scroll.loadedAll.top = true;
this.scroll.loadedAll.bottom = false;
this.loadDialogsPromise = undefined;
this.chatList = this.chatLists[this.filterId];
this.loadDialogs();
};
private setFilterUnreadCount(filterId: number, folder?: Dialog[]) {
const unreadSpan = filterId === 0 ? this.allUnreadCount : this.filtersRendered[filterId]?.unread;
if(!unreadSpan) {
return;
}
folder = folder || appMessagesManager.dialogsStorage.getFolder(filterId);
let mutedCount = 0;
let notMutedCount = 0;
folder.forEach(dialog => {
const isMuted = appNotificationsManager.isPeerLocalMuted(dialog.peerId, true);
if(isMuted && filterId === 0) {
return;
}
const value = +!!dialog.unread_count || +dialog.pFlags.unread_mark || 0; // * unread_mark can be undefined
if(isMuted) mutedCount += value;
else notMutedCount += value;
});
unreadSpan.classList.toggle('badge-gray', mutedCount && !notMutedCount);
const sum = mutedCount + notMutedCount;
unreadSpan.innerText = sum ? '' + sum : '';
}
private setFiltersUnreadCount() {
for(const filterId in this.filtersRendered) {
this.setFilterUnreadCount(+filterId);
}
this.setFilterUnreadCount(0);
}
/**
* Удалит неподходящие чаты из списка, но не добавит их(!)
*/
private validateForFilter() {
// !WARNING, возможно это было зачем-то, но комментарий исправил архивирование
//if(this.filterId === 0) return;
const folder = appMessagesManager.dialogsStorage.getFolder(this.filterId);
for(const _peerId in this.doms) {
const peerId = +_peerId;
// если больше не подходит по фильтру, удаляем
if(folder.findIndex((dialog) => dialog.peerId === peerId) === -1) {
const listEl = this.doms[peerId].listEl;
listEl.remove();
delete this.doms[peerId];
}
}
}
private addFilter(filter: Pick<DialogFilter, 'title' | 'id' | 'orderIndex'> & Partial<{titleEl: HTMLElement}>) {
if(this.filtersRendered[filter.id]) 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.titleEl) titleSpan.append(filter.titleEl);
else titleSpan.innerHTML = RichTextProcessor.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);
menuTab.append(span);
ripple(menuTab);
const containerToAppend = this.folders.menu as HTMLElement;
positionElementByIndex(menuTab, containerToAppend, filter.orderIndex);
//containerToAppend.append(li);
const ul = this.createChatList();
const div = document.createElement('div');
div.append(ul);
div.dataset.filterId = '' + filter.id;
//this.folders.container.append(div);
positionElementByIndex(div, this.folders.container, filter.orderIndex);
this.chatLists[filter.id] = ul;
this.setListClickListener(ul, null, true);
this.filtersRendered[filter.id] = {
menu: menuTab,
container: div,
unread: unreadSpan,
title: titleSpan
};
if(!this.showFiltersPromise && Object.keys(this.filtersRendered).length > 1) {
this.showFiltersPromise = new Promise<void>((resolve) => {
window.setTimeout(() => {
this.showFiltersPromise = undefined;
this.folders.menuScrollContainer.classList.remove('hide');
this.setFiltersUnreadCount();
resolve();
}, 0);
});
}
}
private loadDialogs(side: SliceSides = 'bottom') {
/* if(testScroll) {
return;
} */
if(this.loadDialogsPromise/* || 1 === 1 */) return this.loadDialogsPromise;
const promise = new Promise<void>(async(resolve, reject) => {
if(!this.chatList.childElementCount) {
const container = this.chatList.parentElement;
container.append(this.chatsPreloader);
}
//return;
const filterId = this.filterId;
let loadCount = 30/*this.chatsLoadCount */;
const storage = appMessagesManager.dialogsStorage.getFolder(filterId);
let offsetIndex = 0;
if(side === 'top') {
const element = this.chatList.firstElementChild as HTMLElement;
if(element) {
const peerId = +element.dataset.peerId;
const index = storage.findIndex(dialog => dialog.peerId === peerId);
const needIndex = Math.max(0, index - loadCount);
loadCount = index - needIndex;
offsetIndex = storage[needIndex].index + 1;
}
} else {
const element = this.chatList.lastElementChild as HTMLElement;
if(element) {
const peerId = +element.dataset.peerId;
const dialog = storage.find(dialog => dialog.peerId === peerId);
offsetIndex = dialog.index;
}
}
//let offset = storage[storage.length - 1]?.index || 0;
try {
//console.time('getDialogs time');
const getConversationPromise = (this.filterId > 1 ? appUsersManager.getContacts() as Promise<any> : Promise.resolve()).then(() => {
return appMessagesManager.getConversations('', offsetIndex, loadCount, filterId);
});
const result = await getConversationPromise;
if(this.filterId !== filterId) {
return;
}
//console.timeEnd('getDialogs time');
// * loaded all
//if(!result.dialogs.length || this.chatList.childElementCount === result.count) {
// !result.dialogs.length не подходит, так как при супердревном диалоге getConversations его не выдаст.
//if(this.chatList.childElementCount === result.count) {
if(side === 'bottom') {
if(result.isEnd) {
this.scroll.loadedAll[side] = true;
}
} else {
const storage = appMessagesManager.dialogsStorage.getFolder(filterId);
if(!result.dialogs.length || (storage.length && storage[0].index < offsetIndex)) {
this.scroll.loadedAll[side] = true;
}
}
if(result.dialogs.length) {
const dialogs = side === 'top' ? result.dialogs.slice().reverse() : result.dialogs;
dialogs.forEach((dialog) => {
this.addDialogNew({
dialog,
append: side === 'bottom'
});
});
}
this.log.debug('getDialogs ' + loadCount + ' dialogs by offset:', offsetIndex, result, this.chatList.childElementCount);
setTimeout(() => {
this.scroll.onScroll();
}, 0);
} catch(err) {
this.log.error(err);
}
this.chatsPreloader.remove();
resolve();
});
return this.loadDialogsPromise = promise.finally(() => {
this.loadDialogsPromise = undefined;
});
}
public onChatsRegularScroll = () => {
if(this.sliceTimeout) clearTimeout(this.sliceTimeout);
this.sliceTimeout = window.setTimeout(() => {
this.sliceTimeout = undefined;
if(!this.chatList.childElementCount) {
return;
}
/* const observer = new IntersectionObserver((entries) => {
const
});
Array.from(this.chatList.children).forEach(el => {
observer.observe(el);
}); */
//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 offsetTop = this.folders.container.offsetTop;
const firstY = rectContainer.y + offsetTop;
const lastY = rectContainer.y;
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 = isSafari ? [] : 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 => {
el.remove();
const peerId = +el.dataset.peerId;
delete this.doms[peerId];
});
//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;
/* const firstElementRect = firstElement.getBoundingClientRect();
const scrollTop = */
//this.scroll.scrollIntoView(firstElement, false);
}, 200);
};
public onChatsScrollTop = () => {
this.onChatsScroll('top');
};
public onChatsScroll = (side: SliceSides = 'bottom') => {
if(this.scroll.loadedAll[side] || this.loadDialogsPromise) return;
this.log('onChatsScroll', side);
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;
//cancelEvent(e);
this.log('dialogs click list');
const target = e.target as HTMLElement;
const elem = findUpTag(target, 'LI');
if(!elem) {
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;
const lastMsgId = +elem.dataset.mid || undefined;
setPeerFunc(peerId, lastMsgId);
} else {
setPeerFunc(0);
}
}, {capture: true});
if(DEBUG) {
list.addEventListener('dblclick', (e) => {
const li = findUpTag(e.target, 'LI');
if(li) {
const peerId = +li.dataset.peerId;
this.log('debug dialog:', appMessagesManager.getDialogByPeerId(peerId));
}
});
}
if(withContext) {
attachContextMenuListener(list, this.contextMenu.onContextMenu);
}
}
public createChatList(/* options: {
avatarSize?: number,
handheldsSize?: number,
//size?: number,
} = {} */) {
const list = document.createElement('ul');
list.classList.add('chatlist'/* ,
'chatlist-avatar-' + (options.avatarSize || 54) *//* , 'chatlist-' + (options.size || 72) */);
/* if(options.handheldsSize) {
list.classList.add('chatlist-handhelds-' + options.handheldsSize);
} */
return list;
}
private reorderDialogs() {
//const perf = performance.now();
if(this.reorderDialogsTimeout) {
window.cancelAnimationFrame(this.reorderDialogsTimeout);
}
this.reorderDialogsTimeout = window.requestAnimationFrame(() => {
this.reorderDialogsTimeout = 0;
const offset = Math.max(0, this.getOffset('top').pos);
const dialogs = appMessagesManager.dialogsStorage.getFolder(this.filterId);
const currentOrder = (Array.from(this.chatList.children) as HTMLElement[]).map(el => +el.dataset.peerId);
dialogs.forEach((dialog, index) => {
const dom = this.getDialogDom(dialog.peerId);
if(!dom) {
return;
}
const needIndex = index - offset;
if(needIndex > currentOrder.length) {
dom.listEl.remove();
delete this.doms[dialog.peerId];
return;
}
const peerIdByIndex = currentOrder[needIndex];
if(peerIdByIndex !== dialog.peerId) {
if(positionElementByIndex(dom.listEl, this.chatList, needIndex)) {
this.log.debug('setDialogPosition:', dialog, dom, peerIdByIndex, needIndex);
}
}
});
//this.log('Reorder time:', performance.now() - perf);
});
}
public setLastMessage(dialog: Dialog, lastMessage?: any, dom?: DialogDom, highlightWord?: string) {
///////console.log('setlastMessage:', lastMessage);
if(!dom) {
dom = this.getDialogDom(dialog.peerId);
if(!dom) {
//this.log.error('no dom for dialog:', dialog, lastMessage, dom, highlightWord);
return;
}
}
let draftMessage: MyDraftMessage;
if(!lastMessage) {
if(dialog.draft && dialog.draft._ === 'draftMessage') {
draftMessage = dialog.draft;
}
lastMessage = appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message);
}
if(lastMessage._ === 'messageEmpty'/* || (lastMessage._ === 'messageService' && !lastMessage.rReply) */) {
dom.lastMessageSpan.innerHTML = '';
dom.lastTimeSpan.innerHTML = '';
delete dom.listEl.dataset.mid;
return;
}
let peer = dialog.peer;
let peerId = dialog.peerId;
//let peerId = appMessagesManager.getMessagePeer(lastMessage);
//console.log('setting last message:', lastMessage);
/* if(!dom.lastMessageSpan.classList.contains('user-typing')) */ {
dom.lastMessageSpan.textContent = '';
if(highlightWord && lastMessage.message) {
dom.lastMessageSpan.append(appMessagesManager.wrapMessageForReply(lastMessage, undefined, undefined, false, highlightWord));
} else if(draftMessage) {
dom.lastMessageSpan.append(appMessagesManager.wrapMessageForReply(draftMessage));
} else if(!lastMessage.deleted) {
dom.lastMessageSpan.append(appMessagesManager.wrapMessageForReply(lastMessage));
}
/* if(lastMessage.from_id === auth.id) { // You: */
if(draftMessage) {
const bold = document.createElement('b');
bold.classList.add('danger');
bold.append(i18n('Draft'));
bold.append(': ');
dom.lastMessageSpan.prepend(bold);
} else if(peer._ !== 'peerUser' && peerId !== lastMessage.fromId && !lastMessage.action) {
const sender = appPeersManager.getPeer(lastMessage.fromId);
if(sender && sender.id) {
const senderBold = document.createElement('b');
if(sender.id === rootScope.myId) {
senderBold.append(i18n('FromYou'));
} else {
//str = sender.first_name || sender.last_name || sender.username;
senderBold.append(new PeerTitle({
peerId: lastMessage.fromId,
onlyFirstName: true,
}).element);
}
senderBold.append(': ');
//console.log(sender, senderBold.innerText);
dom.lastMessageSpan.prepend(senderBold);
} //////// else console.log('no sender', lastMessage, peerId);
}
}
if(!lastMessage.deleted || draftMessage/* && lastMessage._ !== 'draftMessage' */) {
const date = draftMessage ? Math.max(draftMessage.date, lastMessage.date || 0) : lastMessage.date;
dom.lastTimeSpan.textContent = '';
dom.lastTimeSpan.append(formatDateAccordingToTodayNew(new Date(date * 1000)));
} else dom.lastTimeSpan.textContent = '';
if(this.doms[peerId] === dom) {
this.setUnreadMessages(dialog);
} else { // means search
dom.listEl.dataset.mid = lastMessage.mid;
}
}
private setUnreadMessages(dialog: Dialog) {
const dom = this.getDialogDom(dialog.peerId);
if(dialog.folder_id === 1) {
this.accumulateArchivedUnread();
}
if(!dom) {
//this.log.error('setUnreadMessages no dom!', dialog);
return;
}
const isMuted = appNotificationsManager.isPeerLocalMuted(dialog.peerId, true);
const wasMuted = dom.listEl.classList.contains('is-muted');
if(isMuted !== wasMuted) {
SetTransition(dom.listEl, 'is-muted', isMuted, 200);
}
const lastMessage = dialog.draft && dialog.draft._ === 'draftMessage' ?
dialog.draft :
appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message);
if(lastMessage._ !== 'messageEmpty' && !lastMessage.deleted &&
lastMessage.pFlags.out && lastMessage.peerId !== rootScope.myId/* &&
dialog.read_outbox_max_id */) { // maybe comment, 06.20.2020
const outgoing = (lastMessage.pFlags && lastMessage.pFlags.unread)
/* && dialog.read_outbox_max_id !== 0 */; // maybe uncomment, 31.01.2020
//console.log('outgoing', outgoing, lastMessage);
if(outgoing) {
dom.statusSpan.classList.remove('tgico-checks');
dom.statusSpan.classList.add('tgico-check');
} else {
dom.statusSpan.classList.remove('tgico-check');
dom.statusSpan.classList.add('tgico-checks');
}
} else dom.statusSpan.classList.remove('tgico-check', 'tgico-checks');
dom.unreadMessagesSpan.innerText = '';
const filter = appMessagesManager.filtersStorage.filters[this.filterId];
let isPinned: boolean;
if(filter) {
isPinned = filter.pinned_peers.findIndex(peerId => peerId === dialog.peerId) !== -1;
} else {
isPinned = !!dialog.pFlags.pinned;
}
if(isPinned) {
dom.unreadMessagesSpan.classList.add('tgico-chatspinned', 'tgico');
} else {
dom.unreadMessagesSpan.classList.remove('tgico-chatspinned', 'tgico');
}
if(dialog.unread_count || dialog.pFlags.unread_mark) {
//dom.unreadMessagesSpan.innerText = '' + (dialog.unread_count ? formatNumber(dialog.unread_count, 1) : ' ');
dom.unreadMessagesSpan.innerText = '' + (dialog.unread_count || ' ');
dom.unreadMessagesSpan.classList.add('unread');
} else {
dom.unreadMessagesSpan.classList.remove('unread');
}
}
private accumulateArchivedUnread() {
if(this.accumulateArchivedTimeout) return;
this.accumulateArchivedTimeout = window.setTimeout(() => {
this.accumulateArchivedTimeout = 0;
const dialogs = appMessagesManager.dialogsStorage.getFolder(1);
const sum = dialogs.reduce((acc, dialog) => acc + dialog.unread_count, 0);
rootScope.broadcast('dialogs_archived_unread', {count: sum});
}, 0);
}
private getDialogDom(peerId: number) {
return this.doms[peerId];
}
public addDialogNew(options: {
dialog: Dialog | number,
container?: HTMLUListElement | Scrollable | false,
drawStatus?: boolean,
rippleEnabled?: boolean,
onlyFirstName?: boolean,
meAsSaved?: boolean,
append?: boolean,
avatarSize?: number,
autonomous?: boolean
}) {
return this.addDialog(options.dialog, options.container, options.drawStatus, options.rippleEnabled, options.onlyFirstName, options.meAsSaved, options.append, options.avatarSize, options.autonomous);
}
public addDialog(_dialog: Dialog | number, container?: HTMLUListElement | Scrollable | false, drawStatus = true, rippleEnabled = true, onlyFirstName = false, meAsSaved = true, append = true, avatarSize = 54, autonomous = !!container) {
let dialog: Dialog;
if(typeof(_dialog) === 'number') {
let originalDialog = appMessagesManager.getDialogOnly(_dialog);
if(!originalDialog) {
originalDialog = {
peerId: _dialog,
peer: appPeersManager.getOutputPeer(_dialog),
pFlags: {}
} as any;
}
dialog = originalDialog;
} else {
dialog = _dialog;
}
const peerId: number = dialog.peerId;
if(container === undefined) {
if(this.doms[peerId]) return;
const filter = appMessagesManager.filtersStorage.filters[this.filterId];
if((filter && !appMessagesManager.filtersStorage.testDialogForFilter(dialog, filter)) || (!filter && this.filterId !== dialog.folder_id)) {
return;
}
}
const avatarEl = new AvatarElement();
avatarEl.setAttribute('dialog', meAsSaved ? '1' : '0');
avatarEl.setAttribute('peer', '' + peerId);
avatarEl.classList.add('dialog-avatar', 'avatar-' + avatarSize);
if(drawStatus && peerId !== rootScope.myId && dialog.peer) {
const peer = dialog.peer;
switch(peer._) {
case 'peerUser':
const user = appUsersManager.getUser(peerId);
//console.log('found user', user);
if(user.status && user.status._ === 'userStatusOnline') {
avatarEl.classList.add('is-online');
}
break;
default:
break;
}
}
const captionDiv = document.createElement('div');
captionDiv.classList.add('user-caption');
const titleSpanContainer = document.createElement('span');
titleSpanContainer.classList.add('user-title');
const peerTitle = new PeerTitle({
peerId,
dialog: meAsSaved,
onlyFirstName,
plainText: false
});
titleSpanContainer.append(peerTitle.element);
//p.classList.add('')
// в других случаях иконка верификации не нужна (а первый - это главные чатлисты)
//if(!container) {
const peer = appPeersManager.getPeer(peerId);
// for muted icon
titleSpanContainer.classList.add('tgico'); // * эта строка будет актуальна только для !container, но ладно
if(peer?.pFlags?.verified) {
titleSpanContainer.classList.add('is-verified');
titleSpanContainer.append(generateVerifiedIcon());
}
//}
const span = document.createElement('span');
span.classList.add('user-last-message');
//captionDiv.append(titleSpan);
//captionDiv.append(span);
const li = document.createElement('li');
if(rippleEnabled) {
ripple(li);
}
li.append(avatarEl, captionDiv);
li.dataset.peerId = '' + peerId;
const statusSpan = document.createElement('span');
statusSpan.classList.add('message-status');
const lastTimeSpan = document.createElement('span');
lastTimeSpan.classList.add('message-time');
const unreadMessagesSpan = document.createElement('div');
unreadMessagesSpan.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 messageP = document.createElement('p');
messageP.classList.add('dialog-subtitle');
messageP.append(span, unreadMessagesSpan);
captionDiv.append(titleP, messageP);
const dom: DialogDom = {
avatarEl,
captionDiv,
titleSpan: peerTitle.element,
titleSpanContainer,
statusSpan,
lastTimeSpan,
unreadMessagesSpan,
lastMessageSpan: span,
containerEl: li,
listEl: li
};
/* let good = false;
for(const folderId in this.chatLists) {
if(this.chatLists[folderId] === container) {
good = true;
}
} */
const method: 'append' | 'prepend' = append ? 'append' : 'prepend';
if(container === undefined/* || good */) {
this.scroll[method](li);
this.doms[dialog.peerId] = dom;
/* if(container) {
container.append(li);
} */
const isMuted = appNotificationsManager.isPeerLocalMuted(dialog.peerId, true);
if(isMuted) {
li.classList.add('is-muted');
}
this.setLastMessage(dialog);
} else if(container) {
container[method](li);
}
if(!autonomous && appImManager.chat?.peerId === peerId) {
li.classList.add('active');
this.lastActiveElements.add(li);
}
return {dom, dialog};
}
public setTyping(dialog: Dialog) {
const dom = this.getDialogDom(dialog.peerId);
if(!dom) {
return;
}
let typingElement = dom.lastMessageSpan.querySelector('.peer-typing-container') as HTMLElement;
if(typingElement) {
appImManager.getPeerTyping(dialog.peerId, typingElement);
} else {
typingElement = appImManager.getPeerTyping(dialog.peerId);
replaceContent(dom.lastMessageSpan, typingElement);
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.setLastMessage(dialog, null, dom);
}
}
export function generateVerifiedIcon() {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttributeNS(null, 'viewBox', '0 0 24 24');
svg.setAttributeNS(null, 'width', '24');
svg.setAttributeNS(null, 'height', '24');
svg.classList.add('verified-icon');
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttributeNS(null, 'href', '#verified-background');
use.classList.add('verified-background');
const use2 = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use2.setAttributeNS(null, 'href', '#verified-check');
use2.classList.add('verified-check');
svg.append(use, use2);
return svg;
}
const appDialogsManager = new AppDialogsManager();
MOUNT_CLASS_TO.appDialogsManager = appDialogsManager;
export default appDialogsManager;