tweb/src/lib/storages/dialogs.ts
Eduard Kuzmenko 0191f3e554 some fixes
2022-08-19 21:27:29 +02:00

1226 lines
41 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
*
* Originally from:
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import type {Chat, DialogPeer, Message, MessageAction, MessageMedia, MessagesPeerDialogs, Update} from '../../layer';
import type {Dialog, MyMessage} from '../appManagers/appMessagesManager';
import tsNow from '../../helpers/tsNow';
import SearchIndex from '../searchIndex';
import {SliceEnd} from '../../helpers/slicedArray';
import {MyDialogFilter} from './filters';
import {FOLDER_ID_ALL, FOLDER_ID_ARCHIVE, NULL_PEER_ID, REAL_FOLDERS, REAL_FOLDER_ID} from '../mtproto/mtproto_config';
import {NoneToVoidFunction} from '../../types';
import ctx from '../../environment/ctx';
import AppStorage from '../storage';
import type DATABASE_STATE from '../../config/databases/state';
import forEachReverse from '../../helpers/array/forEachReverse';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import insertInDescendSortedArray from '../../helpers/array/insertInDescendSortedArray';
import safeReplaceObject from '../../helpers/object/safeReplaceObject';
import getServerMessageId from '../appManagers/utils/messageId/getServerMessageId';
import generateMessageId from '../appManagers/utils/messageId/generateMessageId';
import {AppManager} from '../appManagers/manager';
import getDialogIndexKey from '../appManagers/utils/dialogs/getDialogIndexKey';
import isObject from '../../helpers/object/isObject';
import getDialogIndex from '../appManagers/utils/dialogs/getDialogIndex';
import getPeerIdsFromMessage from '../appManagers/utils/messages/getPeerIdsFromMessage';
import {AppStoragesManager} from '../appManagers/appStoragesManager';
import defineNotNumerableProperties from '../../helpers/object/defineNotNumerableProperties';
import setDialogIndex from '../appManagers/utils/dialogs/setDialogIndex';
export type FolderDialog = {
dialog: Dialog,
index: number
};
export type Folder = {
dialogs: Dialog[],
id: number,
unreadMessagesCount: number,
unreadPeerIds: Set<PeerId>,
unreadUnmutedPeerIds: Set<PeerId>,
dispatchUnreadTimeout?: number
};
export const GLOBAL_FOLDER_ID: REAL_FOLDER_ID = undefined;
// let spentTime = 0;
export default class DialogsStorage extends AppManager {
private storage: AppStoragesManager['storages']['dialogs'];
private dialogs: {[peerId: PeerId]: Dialog};
private folders: {[folderId: number]: Folder} = {};
private allDialogsLoaded: {[folder_id: number]: boolean};
private dialogsOffsetDate: {[folder_id: number]: number};
private pinnedOrders: {[folder_id: number]: PeerId[]};
private dialogsNum: number;
private dialogsIndex: SearchIndex<PeerId>;
private cachedResults: {
query: string,
count: number,
dialogs: Dialog[],
folderId: number
};
protected after() {
this.clear(true);
this.rootScope.addEventListener('language_change', () => {
const peerId = this.appUsersManager.getSelf().id.toPeerId(false);
const dialog = this.getDialogOnly(peerId);
if(dialog) {
const peerText = this.appPeersManager.getPeerSearchText(peerId);
this.dialogsIndex.indexObject(peerId, peerText);
}
});
const onFilterUpdate = (filter: MyDialogFilter) => {
const dialogs = this.getCachedDialogs(false);
for(let i = 0; i < dialogs.length; ++i) {
this.processDialogForFilter(dialogs[i], filter);
}
};
this.rootScope.addEventListener('filter_order', () => {
const dialogs = this.getCachedDialogs(false);
// const indexKeys: ReturnType<DialogsStorage['getDialogIndexKey']>[] = [];
for(const filterId in this.folders) {
if(+filterId > 1) {
delete this.folders[filterId];
}
// indexKeys.push(this.getDialogIndexKey(+filterId));
}
for(let i = 0; i < dialogs.length; ++i) {
const dialog = dialogs[i];
// for(const indexKey of indexKeys) {
// delete dialog[indexKey];
// }
this.processDialogForFilters(dialog);
}
});
this.rootScope.addEventListener('filter_update', onFilterUpdate);
this.rootScope.addEventListener('filter_new', onFilterUpdate);
this.rootScope.addEventListener('filter_delete', (filter) => {
const dialogs = this.getCachedDialogs(false);
const indexKey = this.getDialogIndexKeyByFilterId(filter.id);
for(let i = 0; i < dialogs.length; ++i) {
const dialog = dialogs[i];
delete dialog[indexKey];
}
delete this.folders[filter.id];
});
this.rootScope.addEventListener('dialog_notify_settings', (dialog) => {
this.processDialogForFilters(dialog);
this.prepareDialogUnreadCountModifying(dialog)();
});
this.rootScope.addEventListener('chat_update', (chatId) => {
const chat: Chat.chat = this.appChatsManager.getChat(chatId);
const peerId = chatId.toPeerId(true);
if(chat.pFlags.left && this.getDialogOnly(peerId)) {
this.dropDialogOnDeletion(peerId);
}
});
this.apiUpdatesManager.addMultipleEventsListeners({
updateFolderPeers: this.onUpdateFolderPeers,
updateDialogPinned: this.onUpdateDialogPinned,
updatePinnedDialogs: this.onUpdatePinnedDialogs
});
return Promise.all([
this.appStateManager.getState(),
this.appStoragesManager.loadStorage('dialogs')
]).then(([state, {results: dialogs, storage}]) => {
this.storage = storage;
this.dialogs = this.storage.getCache();
for(const folderId of REAL_FOLDERS) {
const order = state.pinnedOrders[folderId];
if(!order) {
continue;
}
const _order = this.pinnedOrders[folderId];
_order.splice(0, _order.length, ...order);
}
if(dialogs.length) {
AppStorage.freezeSaving<typeof DATABASE_STATE>(this.setDialogsFromState.bind(this, dialogs), ['chats', 'dialogs', 'messages', 'users']);
}
this.allDialogsLoaded = state.allDialogsLoaded || {};
if(dialogs.length) {
this.appDraftsManager.addMissedDialogs();
}
});
}
private setDialogsFromState(dialogs: Dialog[]) {
for(let i = 0, length = dialogs.length; i < length; ++i) {
const dialog = dialogs[i];
if(!dialog) {
continue;
}
// if(dialog.peerId !== SERVICE_PEER_ID) {
dialog.top_message = getServerMessageId(dialog.top_message); // * fix outgoing message to avoid copying dialog
// }
if(dialog.topMessage) {
this.appMessagesManager.saveMessages([dialog.topMessage]);
}
for(let i = 0; i <= 21; ++i) {
const indexKey: ReturnType<typeof getDialogIndexKey> = `index_${i}` as any;
delete dialog[indexKey];
}
// delete dialog.indexes;
this.saveDialog(dialog, undefined, true);
// ! WARNING, убрать это когда нужно будет делать чтобы pending сообщения сохранялись
const message = this.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message);
if(!message) {
this.appMessagesManager.reloadConversation(dialog.peerId);
}
}
}
public isDialogsLoaded(folderId: number) {
return !!this.allDialogsLoaded[folderId];
}
public setDialogsLoaded(folderId: number, loaded: boolean) {
if(folderId === GLOBAL_FOLDER_ID && loaded) {
this.allDialogsLoaded[FOLDER_ID_ALL] = loaded;
this.allDialogsLoaded[FOLDER_ID_ARCHIVE] = loaded;
} else {
this.allDialogsLoaded[folderId] = loaded;
}
if(Array.from(REAL_FOLDERS).every((folderId) => this.allDialogsLoaded[folderId])) {
this.allDialogsLoaded[GLOBAL_FOLDER_ID] = true;
}
this.appStateManager.pushToState('allDialogsLoaded', this.allDialogsLoaded);
}
public clear = (init = false) => {
if(!init) {
this.storage.clear();
this.setDialogsLoaded(FOLDER_ID_ALL, false);
this.setDialogsLoaded(FOLDER_ID_ARCHIVE, false);
this.setDialogsLoaded(GLOBAL_FOLDER_ID, false);
for(const folderId of REAL_FOLDERS) {
this.resetPinnedOrder(folderId);
}
this.savePinnedOrders();
} else {
this.allDialogsLoaded = {};
this.pinnedOrders = {};
for(const folderId of REAL_FOLDERS) {
this.pinnedOrders[folderId] = [];
}
}
this.folders = {};
this.dialogsOffsetDate = {};
this.dialogsNum = 0;
this.dialogsIndex = new SearchIndex({
clearBadChars: true,
ignoreCase: true,
latinize: true,
includeTag: true
});
this.cachedResults = {
query: '',
count: 0,
dialogs: [],
folderId: 0
};
};
public handleDialogUnpinning(dialog: Dialog, folderId: number) {
delete dialog.pFlags.pinned;
indexOfAndSplice(this.pinnedOrders[folderId], dialog.peerId);
this.savePinnedOrders();
}
public savePinnedOrders() {
this.appStateManager.pushToState('pinnedOrders', this.pinnedOrders);
}
public resetPinnedOrder(folderId: number) {
this.pinnedOrders[folderId].length = 0;
}
public getPinnedOrders(folderId: number) {
return this.pinnedOrders[folderId];
}
public getOffsetDate(folderId: number): number {
const offsetDate = this.dialogsOffsetDate[folderId] || 0;
if(folderId === GLOBAL_FOLDER_ID && !offsetDate) { // make request not from beginning if we have loaded some dialogs
return Math.min(...Array.from(REAL_FOLDERS).sort((a, b) => a - b));
}
return offsetDate;
}
private generateFolder(id: number) {
const folder: Folder = {
dialogs: [],
id,
unreadMessagesCount: 0,
unreadPeerIds: new Set(),
unreadUnmutedPeerIds: new Set()
};
defineNotNumerableProperties(folder, ['dispatchUnreadTimeout']);
return folder;
}
public getFolder(id: number) {
return this.folders[id] ??= this.generateFolder(id);
}
public getFolderDialogs(id: number, skipMigrated = true): Dialog[] {
if(id === GLOBAL_FOLDER_ID) { // * it won't be sorted
return this.getCachedDialogs(skipMigrated);
}
const folder = this.getFolder(id);
return skipMigrated ? folder.dialogs.filter((dialog) => dialog.migratedTo === undefined) : folder.dialogs;
}
public getNextDialog(currentPeerId: PeerId, next: boolean, filterId: number) {
const folder = this.getFolderDialogs(filterId, true);
let dialog: Dialog;
if(!currentPeerId) {
if(next) {
dialog = folder[0];
}
} else {
const idx = folder.findIndex((dialog) => dialog.peerId === currentPeerId);
if(idx !== -1) {
const nextIndex = next ? idx + 1 : idx - 1;
dialog = folder[nextIndex];
}
}
return dialog;
}
public getDialogIndexKeyByFilterId(filterId: number) {
if(REAL_FOLDERS.has(filterId)) return getDialogIndexKey(filterId as REAL_FOLDER_ID);
const filter = this.filtersStorage.getFilter(filterId);
return getDialogIndexKey(filter.localId);
}
public isPeerUnmuted(peerId: PeerId) {
return !this.appNotificationsManager.isPeerLocalMuted(peerId, true);
}
public getFolderUnreadCount(filterId: number) {
const folder = this.getFolder(filterId);
return {unreadUnmutedCount: folder.unreadUnmutedPeerIds.size, unreadCount: folder.unreadPeerIds.size};
}
public getCachedDialogs(skipMigrated?: boolean) {
const arrays = Array.from(REAL_FOLDERS).map((folderId) => this.getFolderDialogs(folderId, skipMigrated));
return [].concat(...arrays) as typeof arrays[0];
}
private setDialogIndexInFilter(dialog: Dialog, indexKey: ReturnType<typeof getDialogIndexKey>, filter: MyDialogFilter) {
let index: number;
const isRealFolder = REAL_FOLDERS.has(filter.id);
/* if(isRealFolder) {
// index = getDialogIndex(dialog, indexKey);
index = this.generateIndexForDialog(dialog, true);
} else */if(this.filtersStorage.testDialogForFilter(dialog, filter)) {
const pinnedIndex = filter.pinnedPeerIds.indexOf(dialog.peerId);
if(pinnedIndex !== -1) {
index = this.generateDialogIndex(this.generateDialogPinnedDateByIndex(filter.pinnedPeerIds.length - 1 - pinnedIndex), true);
} else if(dialog.pFlags?.pinned || isRealFolder) {
index = this.generateIndexForDialog(dialog, true, undefined, !isRealFolder);
} else {
index = getDialogIndex(dialog) ?? this.generateIndexForDialog(dialog, true);
}
}
// if(!dialog.hasOwnProperty(indexKey)) {
// defineNotNumerableProperties(dialog, [indexKey]);
// }
return setDialogIndex(dialog, indexKey, index);
}
public getDialog(peerId: PeerId, folderId?: number, skipMigrated = true): [Dialog, number] | [] {
const folders: Dialog[][] = [];
if(folderId === undefined) {
folders.push(...Array.from(REAL_FOLDERS).map((folderId) => this.getFolder(folderId).dialogs));
} else {
folders.push(this.getFolderDialogs(folderId, false));
}
for(const folder of folders) {
let i = 0, skipped = 0;
for(let length = folder.length; i < length; ++i) {
const dialog = folder[i];
if(dialog.peerId === peerId) {
return [dialog, i - skipped];
} else if(skipMigrated && dialog.migratedTo !== undefined) {
++skipped;
}
}
}
return [];
}
public getDialogOnly(peerId: PeerId) {
return this.dialogs[peerId];
}
public getDialogIndex(peerId: PeerId | Dialog, indexKey: ReturnType<typeof getDialogIndexKey>) {
const dialog = isObject(peerId) ? peerId : this.getDialogOnly(peerId);
return getDialogIndex(dialog, indexKey);
}
/*
var date = Date.now() / 1000 | 0;
var m = date * 0x10000;
var k = (date + 1) * 0x10000;
k - m;
65536
*/
public generateDialogIndex(date?: number, isPinned?: boolean) {
if(date === undefined) {
date = tsNow(true) + this.timeManager.getServerTimeOffset();
}
return (date * 0x10000) + (isPinned ? 0 : ((++this.dialogsNum) & 0xFFFF));
}
public processDialogForFilters(dialog: Dialog) {
// let perf = performance.now();
const filters = this.filtersStorage.getFilters();
for(const id in filters) {
const filter = filters[id];
this.processDialogForFilter(dialog, filter);
}
// spentTime += (performance.now() - perf);
// console.log('generate index time:', spentTime);
}
public processDialogForFilter(dialog: Dialog, filter: MyDialogFilter) {
const indexKey = this.getDialogIndexKeyByFilterId(filter.id);
const folder = this.getFolder(filter.id);
const dialogs = folder.dialogs;
const wasIndex = dialogs.findIndex((d) => d.peerId === dialog.peerId);
const wasDialog = dialogs[wasIndex];
const wasDialogIndex = this.getDialogIndex(wasDialog, indexKey);
const newDialogIndex = this.setDialogIndexInFilter(dialog, indexKey, filter);
if(wasDialogIndex === newDialogIndex) {
return false;
}
if((!wasDialogIndex && newDialogIndex) || (wasIndex && !newDialogIndex)) {
this.prepareFolderUnreadCountModifyingByDialog(filter.id, dialog, !!newDialogIndex);
}
if(wasIndex !== -1) {
dialogs.splice(wasIndex, 1);
}
if(newDialogIndex) {
insertInDescendSortedArray(dialogs, dialog, (dialog) => this.getDialogIndex(dialog, indexKey), -1);
}
return true;
}
public prepareDialogUnreadCountModifying(dialog: Dialog) {
const callbacks: NoneToVoidFunction[] = [
this.prepareFolderUnreadCountModifyingByDialog(dialog.folder_id, dialog)
];
const filters = this.filtersStorage.getFilters();
for(const id in filters) {
const filter = filters[id];
if(this.filtersStorage.testDialogForFilter(dialog, filter)) {
callbacks.push(this.prepareFolderUnreadCountModifyingByDialog(filter.id, dialog));
}
}
return () => callbacks.forEach((callback) => callback());
}
public prepareFolderUnreadCountModifyingByDialog(folderId: number, dialog: Dialog, toggle?: boolean) {
const wasUnreadCount = this.appMessagesManager.getDialogUnreadCount(dialog);
const wasUnmuted = this.isPeerUnmuted(dialog.peerId);
if(toggle !== undefined) {
const addMessagesCount = toggle ? wasUnreadCount : -wasUnreadCount;
this.modifyFolderUnreadCount(folderId, addMessagesCount, !!wasUnreadCount, wasUnreadCount && wasUnmuted, dialog);
return;
}
return () => {
const newUnreadCount = this.appMessagesManager.getDialogUnreadCount(dialog);
const newUnmuted = this.isPeerUnmuted(dialog.peerId);
const addMessagesCount = newUnreadCount - wasUnreadCount;
this.modifyFolderUnreadCount(folderId, addMessagesCount, !!newUnreadCount, newUnreadCount && newUnmuted, dialog);
};
}
public modifyFolderUnreadCount(
folderId: number,
addMessagesCount: number,
toggleDialog: boolean,
toggleUnmuted: boolean,
dialog: Dialog
) {
const folder = this.getFolder(folderId);
if(addMessagesCount) {
folder.unreadMessagesCount = Math.max(0, folder.unreadMessagesCount + addMessagesCount);
}
const {peerId} = dialog;
if(toggleDialog) {
folder.unreadPeerIds.add(peerId);
} else {
folder.unreadPeerIds.delete(peerId);
}
if(toggleUnmuted) {
folder.unreadUnmutedPeerIds.add(peerId);
} else {
folder.unreadUnmutedPeerIds.delete(peerId);
}
if(folder.dispatchUnreadTimeout === undefined) {
folder.dispatchUnreadTimeout = ctx.setTimeout(() => {
folder.dispatchUnreadTimeout = undefined;
const _folder = {...folder};
delete _folder.dialogs;
this.rootScope.dispatchEvent('folder_unread', _folder);
}, 0);
}
}
public generateIndexForDialog(dialog: Dialog, justReturn?: boolean, message?: MyMessage, noPinnedOrderUpdate?: boolean) {
if(!justReturn) {
return;
}
let topDate = 0, isPinned: boolean;
if(dialog.pFlags.pinned && !noPinnedOrderUpdate) {
topDate = this.generateDialogPinnedDate(dialog);
isPinned = true;
} else {
if(!message) {
message = this.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message);
}
topDate = (message as Message.message)?.date || topDate;
const channelId = this.appPeersManager.isChannel(dialog.peerId) && dialog.peerId.toChatId();
if(channelId) {
const channel: Chat.channel = this.appChatsManager.getChat(channelId);
if(!topDate || (channel.date && channel.date > topDate)) {
topDate = channel.date;
}
}
if(dialog.draft?._ === 'draftMessage' && dialog.draft.date > topDate) {
topDate = dialog.draft.date;
}
}
if(!topDate) {
topDate = tsNow(true);
}
const index = this.generateDialogIndex(topDate, isPinned);
if(justReturn) {
return index;
}
const indexKey = getDialogIndexKey(dialog.folder_id);
setDialogIndex(dialog, indexKey, index);
}
public generateDialogPinnedDateByIndex(pinnedIndex: number) {
return 0x7fff0000 + (pinnedIndex & 0xFFFF); // 0xFFFF - потому что в папках может быть бесконечное число пиннедов
}
public generateDialogPinnedDate(dialog: Dialog) {
const order = this.pinnedOrders[dialog.folder_id];
let pinnedIndex = order.indexOf(dialog.peerId);
if(pinnedIndex === -1) {
order.unshift(dialog.peerId);
pinnedIndex = 0;
this.savePinnedOrders();
}
return this.generateDialogPinnedDateByIndex(order.length - 1 - pinnedIndex);
}
/* public generateDialog(peerId: PeerId) {
const dialog: Dialog = {
_: 'dialog',
pFlags: {},
peer: this.appPeersManager.getOutputPeer(peerId),
top_message: 0,
read_inbox_max_id: 0,
read_outbox_max_id: 0,
unread_count: 0,
unread_mentions_count: 0,
notify_settings: {
_: 'peerNotifySettings',
},
};
return dialog;
} */
public setDialogToState(dialog: Dialog) {
const {peerId, pts} = dialog;
const historyStorage = this.appMessagesManager.getHistoryStorage(peerId);
const messagesStorage = this.appMessagesManager.getHistoryMessagesStorage(peerId);
const history = historyStorage.history.slice;
let incomingMessage: MyMessage;
for(let i = 0, length = history.length; i < length; ++i) {
const mid = history[i];
const message: MyMessage = this.appMessagesManager.getMessageFromStorage(messagesStorage, mid);
if(message && !message.pFlags.is_outgoing/* || peerId === SERVICE_PEER_ID */) {
incomingMessage = message;
const peerIds = getPeerIdsFromMessage(message);
this.peersStorage.requestPeersForKey(peerIds, `topMessage_${peerId}`);
break;
}
}
dialog.topMessage = incomingMessage;
// DO NOT TOUCH THESE LINES, SOME REAL MAGIC HERE.
// * Read service chat when refreshing page with outgoing & getting new service outgoing message
/* if(incomingMessage && dialog.read_inbox_max_id >= dialog.top_message) {
dialog.unread_count = 0;
}
dialog.read_inbox_max_id = this.appMessagesIdsManager.clearMessageId(dialog.read_inbox_max_id);
dialog.read_outbox_max_id = this.appMessagesIdsManager.clearMessageId(dialog.read_outbox_max_id); */
// CAN TOUCH NOW
if(peerId.isAnyChat() && pts) {
const newPts = this.apiUpdatesManager.getChannelState(peerId.toChatId(), pts).pts;
dialog.pts = newPts;
}
this.storage.set({
[peerId]: dialog
});
this.peersStorage.requestPeer(peerId, 'dialog');
/* for(let id in this.filtersStorage.filters) {
const filter = this.filtersStorage.filters[id];
if(this.filtersStorage.testDialogForFilter(dialog, filter)) {
}
} */
}
public pushDialog(dialog: Dialog, offsetDate?: number, ignoreOffsetDate?: boolean, saveGlobalOffset?: boolean) {
const {folder_id, peerId} = dialog;
// const dialogs = this.getFolderDialogs(folder_id, false);
// const pos = dialogs.findIndex((d) => d.peerId === peerId);
// if(pos !== -1) {
// dialogs.splice(pos, 1);
// }
// if(!this.dialogs[peerId]) {
this.dialogs[peerId] = dialog;
this.setDialogToState(dialog);
// }
if(offsetDate === undefined) {
offsetDate = this.getDialogOffsetDate(dialog);
}
this.processDialogForFilters(dialog);
if(offsetDate && !dialog.pFlags.pinned) {
if(saveGlobalOffset) {
const savedGlobalOffsetDate = this.dialogsOffsetDate[GLOBAL_FOLDER_ID];
if(!savedGlobalOffsetDate || offsetDate < savedGlobalOffsetDate) {
this.dialogsOffsetDate[GLOBAL_FOLDER_ID] = offsetDate;
}
}
const savedOffsetDate = this.dialogsOffsetDate[folder_id];
if(!savedOffsetDate || offsetDate < savedOffsetDate) {
// if(pos !== -1) {
if(!ignoreOffsetDate && !this.isDialogsLoaded(folder_id)) {
this.clearDialogFromState(dialog, true);
return;
}
this.dialogsOffsetDate[folder_id] = offsetDate;
}
}
// if(pos === -1) {
// this.prepareFolderUnreadCountModifyingByDialog(folder_id, dialog, true);
// }
// const indexKey = getDialogIndexKey(folder_id);
// /* const newPos = */insertInDescendSortedArray(dialogs, dialog, (dialog) => getDialogIndex(dialog, indexKey), -1);
/* if(pos !== -1 && pos !== newPos) {
rootScope.dispatchEvent('dialog_order', {dialog, pos: newPos});
} */
}
public dropDialog(peerId: PeerId): ReturnType<DialogsStorage['getDialog']> {
const foundDialog = this.getDialog(peerId, undefined, false);
const [dialog, index] = foundDialog;
if(dialog) {
delete this.dialogs[peerId];
const folder = this.getFolder(dialog.folder_id);
folder.dialogs.splice(index, 1);
const wasPinned = indexOfAndSplice(this.pinnedOrders[dialog.folder_id], peerId) !== undefined;
this.processDialogForFilters(dialog);
this.dialogsIndex.indexObject(peerId, '');
if(wasPinned) {
this.savePinnedOrders();
}
this.clearDialogFromState(dialog, false);
}
return foundDialog;
}
public clearDialogFromState(dialog: Dialog, keepLocal: boolean) {
const peerId = dialog.peerId;
this.peersStorage.requestPeersForKey([], `topMessage_${peerId}`);
this.peersStorage.releasePeer(peerId, 'dialog');
this.storage.delete(peerId, keepLocal);
}
public dropDialogWithEvent(peerId: PeerId) {
const dropped = this.dropDialog(peerId);
if(dropped.length) {
this.rootScope.dispatchEvent('dialog_drop', {peerId, dialog: dropped[0]});
}
return dropped;
}
/**
* leaving chat, leaving channel, deleting private dialog
*/
public dropDialogOnDeletion(peerId: PeerId) {
this.dropDialogWithEvent(peerId);
this.rootScope.dispatchEvent('peer_deleted', peerId);
}
public applyDialogs(dialogsResult: MessagesPeerDialogs.messagesPeerDialogs) {
// * В эту функцию попадут только те диалоги, в которых есть read_inbox_max_id и read_outbox_max_id, в отличие от тех, что будут в getTopMessages
// ! fix 'dialogFolder', maybe there is better way to do it, this only can happen by 'messages.getPinnedDialogs' by folder_id: 0
forEachReverse(dialogsResult.dialogs, (dialog, idx) => {
if(dialog._ === 'dialogFolder') {
dialogsResult.dialogs.splice(idx, 1);
}
});
this.appUsersManager.saveApiUsers(dialogsResult.users);
this.appChatsManager.saveApiChats(dialogsResult.chats);
this.appMessagesManager.saveMessages(dialogsResult.messages);
// this.appMessagesManager.log('applyConversation', dialogsResult);
const updatedDialogs: Map<PeerId, Dialog> = new Map();
(dialogsResult.dialogs as Dialog[]).forEach((dialog) => {
const peerId = this.appPeersManager.getPeerId(dialog.peer);
let topMessage = dialog.top_message;
const topPendingMessage = this.appMessagesManager.pendingTopMsgs[peerId];
if(topPendingMessage) {
if(!topMessage ||
(this.appMessagesManager.getMessageByPeer(peerId, topPendingMessage) as MyMessage)?.date > (this.appMessagesManager.getMessageByPeer(peerId, topMessage) as MyMessage)?.date) {
dialog.top_message = topMessage = topPendingMessage;
this.appMessagesManager.getHistoryStorage(peerId).maxId = topPendingMessage;
}
}
/* const d = Object.assign({}, dialog);
if(peerId === 239602833) {
this.log.error('applyConversation lun', dialog, d);
} */
if(topMessage || dialog.draft?._ === 'draftMessage') {
this.saveDialog(dialog);
updatedDialogs.set(peerId, dialog);
} else {
this.dropDialogWithEvent(peerId);
}
const updates = this.appMessagesManager.newUpdatesAfterReloadToHandle[peerId];
if(updates !== undefined) {
for(const update of updates) {
updates.delete(update);
this.apiUpdatesManager.saveUpdate(update);
}
if(!updates.size) {
delete this.appMessagesManager.newUpdatesAfterReloadToHandle[peerId];
}
}
});
if(updatedDialogs.size) {
this.rootScope.dispatchEvent('dialogs_multiupdate', updatedDialogs);
}
}
private getDialogOffsetDate(dialog: Dialog) {
const message = this.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message);
return message?.date || 0;
}
/**
* Won't save migrated from peer, forbidden peers, left and kicked
*/
public saveDialog(dialog: Dialog, folderId = dialog.folder_id ?? FOLDER_ID_ALL, ignoreOffsetDate?: boolean, saveGlobalOffset?: boolean) {
const peerId = this.appPeersManager.getPeerId(dialog.peer);
if(!peerId) {
console.error('saveConversation no peerId???', dialog, folderId);
return;
}
if(dialog._ !== 'dialog'/* || peerId === 239602833 */) {
console.error('saveConversation not regular dialog', dialog, Object.assign({}, dialog));
}
const channelId = this.appPeersManager.isChannel(peerId) ? peerId.toChatId() : NULL_PEER_ID;
if(peerId.isAnyChat()) {
const chat: Chat = this.appChatsManager.getChat(peerId.toChatId());
// ! chatForbidden stays for chat where you're kicked
if(
chat._ === 'channelForbidden' ||
// || chat._ === 'chatForbidden'
(chat as Chat.chat).pFlags.left
// || (chat as any).pFlags.kicked
) {
return;
}
}
const peerText = this.appPeersManager.getPeerSearchText(peerId);
this.dialogsIndex.indexObject(peerId, peerText);
const wasDialogBefore = this.getDialogOnly(peerId);
let mid: number, message: MyMessage;
if(dialog.top_message) {
mid = generateMessageId(dialog.top_message);// dialog.top_message;
// preserve outgoing message
const wasTopMessage = wasDialogBefore?.top_message && this.appMessagesManager.getMessageByPeer(peerId, wasDialogBefore.top_message) as MyMessage;
if(wasTopMessage?.pFlags?.is_outgoing && wasDialogBefore.top_message >= mid) {
mid = wasDialogBefore.top_message;
}
message = this.appMessagesManager.getMessageByPeer(peerId, mid);
} else {
mid = this.appMessagesManager.generateTempMessageId(peerId);
message = {
_: 'message',
id: mid,
mid,
from_id: this.appPeersManager.getOutputPeer(this.appUsersManager.getSelf().id.toPeerId(false)),
peer_id: this.appPeersManager.getOutputPeer(peerId),
deleted: true,
pFlags: {out: true},
date: 0,
message: ''
};
this.appMessagesManager.saveMessages([message], {isOutgoing: true});
}
if(!message?.pFlags) {
this.appMessagesManager.log.error('saveConversation no message:', dialog, message);
}
if(!channelId && peerId.isAnyChat()) {
const chat = this.appChatsManager.getChat(peerId.toChatId());
if(chat && chat.migrated_to && chat.pFlags.deactivated) {
const migratedToPeer = this.appPeersManager.getPeerId(chat.migrated_to);
this.appMessagesManager.migratedFromTo[peerId] = migratedToPeer;
this.appMessagesManager.migratedToFrom[migratedToPeer] = peerId;
dialog.migratedTo = migratedToPeer;
// return;
}
}
dialog.top_message = mid;
// dialog.unread_count = wasDialogBefore && dialog.read_inbox_max_id === getServerMessageId(wasDialogBefore.read_inbox_max_id) ? wasDialogBefore.unread_count : dialog.unread_count;
dialog.read_inbox_max_id = generateMessageId(wasDialogBefore && !dialog.read_inbox_max_id ? wasDialogBefore.read_inbox_max_id : dialog.read_inbox_max_id);
dialog.read_outbox_max_id = generateMessageId(wasDialogBefore && !dialog.read_outbox_max_id ? wasDialogBefore.read_outbox_max_id : dialog.read_outbox_max_id);
if(dialog.folder_id === undefined) {
if(dialog._ === 'dialog') {
// ! СЛОЖНО ! СМОТРИ В getTopMessages
dialog.folder_id = wasDialogBefore ? wasDialogBefore.folder_id : folderId;
}/* else if(dialog._ === 'dialogFolder') {
dialog.folder_id = dialog.folder.id;
} */
}
dialog.draft = this.appDraftsManager.saveDraft(peerId, 0, dialog.draft);
dialog.peerId = peerId;
// dialog.indexes ??= {} as any;
// if(dialog.peerId === -) {
// debugger;
// }
// Because we saved message without dialog present
if(message && message.pFlags.is_outgoing) {
const isOut = message.pFlags.out;
if(mid > dialog[isOut ? 'read_outbox_max_id' : 'read_inbox_max_id']) {
message.pFlags.unread = true;
if(!dialog.unread_count && !isOut) {
++dialog.unread_count;
}
} else {
delete message.pFlags.unread;
}
}
const historyStorage = this.appMessagesManager.getHistoryStorage(peerId);
const slice = historyStorage.history.slice;
/* if(historyStorage === undefined) { // warning
historyStorage.history.push(mid);
} else */if(!slice.length) {
historyStorage.history.unshift(mid);
historyStorage.count ||= 1;
if(this.appMessagesManager.mergeReplyKeyboard(historyStorage, message)) {
this.rootScope.dispatchEvent('history_reply_markup', {peerId});
}
} else if(!slice.isEnd(SliceEnd.Bottom)) { // * this will probably never happen, however, if it does, then it will fix slice with top_message
const slice = historyStorage.history.insertSlice([mid]);
slice.setEnd(SliceEnd.Bottom);
historyStorage.count ||= 1;
if(this.appMessagesManager.mergeReplyKeyboard(historyStorage, message)) {
this.rootScope.dispatchEvent('history_reply_markup', {peerId});
}
}
historyStorage.maxId = mid;
historyStorage.readMaxId = dialog.read_inbox_max_id;
historyStorage.readOutboxMaxId = dialog.read_outbox_max_id;
this.appNotificationsManager.savePeerSettings({
peerId,
settings: dialog.notify_settings
});
if(channelId && dialog.pts) {
this.apiUpdatesManager.addChannelState(channelId, dialog.pts);
}
this.generateIndexForDialog(dialog);
if(wasDialogBefore) {
// fix unread count
const releaseUnreadCount = this.dialogsStorage.prepareDialogUnreadCountModifying(wasDialogBefore);
safeReplaceObject(wasDialogBefore, dialog);
releaseUnreadCount();
}
this.pushDialog(dialog, message?.date, ignoreOffsetDate, saveGlobalOffset);
}
public getDialogs(query = '', offsetIndex?: number, limit = 20, folderId: number = 0, skipMigrated = false): {
dialogs: Dialog[],
count: number,
isTopEnd: boolean,
isEnd: boolean
} | Promise<{
dialogs: Dialog[],
count: number,
isTopEnd: boolean,
isEnd: boolean
}> {
if(!REAL_FOLDERS.has(folderId)) {
const promises: Promise<any>[] = [];
const fillContactsResult = this.appUsersManager.fillContacts();
if(!fillContactsResult.cached) {
promises.push(fillContactsResult.promise);
}
const reloadMissingDialogsPromise = this.filtersStorage.reloadMissingPeerIds(folderId);
if(reloadMissingDialogsPromise) {
promises.push(reloadMissingDialogsPromise);
}
if(promises.length) {
return Promise.all(promises).then(() => {
return this.getDialogs(query, offsetIndex, limit, folderId, skipMigrated);
});
}
}
// let's load only first pages by certain folderId. next pages will load without folder filtering
const realFolderId: REAL_FOLDER_ID = !REAL_FOLDERS.has(folderId) || this.getOffsetDate(folderId) ? GLOBAL_FOLDER_ID : folderId as REAL_FOLDER_ID;
let curDialogStorage = this.getFolderDialogs(folderId, skipMigrated);
const indexKey = this.getDialogIndexKeyByFilterId(folderId);
if(query) {
if(!limit || this.cachedResults.query !== query || this.cachedResults.folderId !== folderId) {
this.cachedResults.query = query;
this.cachedResults.folderId = folderId;
const results = this.dialogsIndex.search(query);
const dialogs: Dialog[] = [];
for(const peerId in this.dialogs) {
const dialog = this.dialogs[peerId];
if(results.has(dialog.peerId) && dialog.folder_id === folderId) {
dialogs.push(dialog);
}
}
dialogs.sort((d1, d2) => this.getDialogIndex(d2, indexKey) - this.getDialogIndex(d1, indexKey));
this.cachedResults.dialogs = dialogs;
this.cachedResults.count = dialogs.length;
}
curDialogStorage = this.cachedResults.dialogs;
} else {
this.cachedResults.query = '';
}
let offset = 0;
if(offsetIndex > 0) {
for(let length = curDialogStorage.length; offset < length; ++offset) {
if(offsetIndex > this.getDialogIndex(curDialogStorage[offset], indexKey)) {
break;
}
}
}
const loadedAll = this.isDialogsLoaded(realFolderId);
const isEnoughDialogs = curDialogStorage.length >= (offset + limit);
if(query || loadedAll || isEnoughDialogs) {
const dialogs = curDialogStorage.slice(offset, offset + limit);
return {
dialogs,
count: loadedAll ? curDialogStorage.length : null,
isTopEnd: curDialogStorage.length && ((dialogs[0] && dialogs[0] === curDialogStorage[0]) || this.getDialogIndex(curDialogStorage[0], indexKey) < offsetIndex),
isEnd: (query || loadedAll) && (offset + limit) >= curDialogStorage.length
};
}
return this.appMessagesManager.getTopMessages(limit, realFolderId).then((result) => {
// const curDialogStorage = this[folderId];
if(skipMigrated) {
curDialogStorage = this.getFolderDialogs(folderId, skipMigrated);
}
offset = 0;
if(offsetIndex > 0) {
for(let length = curDialogStorage.length; offset < length; ++offset) {
if(offsetIndex > this.getDialogIndex(curDialogStorage[offset], indexKey)) {
break;
}
}
}
// this.log.warn(offset, offset + limit, curDialogStorage.dialogs.length, this.dialogs.length);
const dialogs = curDialogStorage.slice(offset, offset + limit);
return {
dialogs,
count: result.count === undefined ? curDialogStorage.length : result.count,
isTopEnd: curDialogStorage.length && ((dialogs[0] && dialogs[0] === curDialogStorage[0]) || this.getDialogIndex(curDialogStorage[0], indexKey) < offsetIndex),
// isEnd: this.isDialogsLoaded(realFolderId) && (offset + limit) >= curDialogStorage.length
isEnd: result.isEnd
};
});
}
// only 0 and 1 folders
private onUpdateFolderPeers = (update: Update.updateFolderPeers) => {
// this.log('updateFolderPeers', update);
const peers = update.folder_peers;
peers.forEach((folderPeer) => {
const {folder_id, peer} = folderPeer;
const peerId = this.appPeersManager.getPeerId(peer);
const dialog = this.dropDialog(peerId)[0];
if(dialog) {
if(dialog.pFlags?.pinned) {
this.handleDialogUnpinning(dialog, folder_id);
}
dialog.folder_id = folder_id as REAL_FOLDER_ID;
this.generateIndexForDialog(dialog);
this.pushDialog(dialog); // need for simultaneously updatePinnedDialogs
}
this.appMessagesManager.scheduleHandleNewDialogs(peerId, dialog);
});
};
private onUpdateDialogPinned = (update: Update.updateDialogPinned) => {
const folderId = update.folder_id ?? FOLDER_ID_ALL;
// this.log('updateDialogPinned', update);
const peerId = this.appPeersManager.getPeerId((update.peer as DialogPeer.dialogPeer).peer);
const dialog = this.getDialogOnly(peerId);
// этот код внизу никогда не сработает, в папках за пиннед отвечает updateDialogFilter
/* if(update.folder_id > 1) {
const filter = this.filtersStorage.filters[update.folder_id];
if(update.pFlags.pinned) {
filter.pinned_peers.unshift(peerId);
} else {
filter.pinned_peers.findAndSplice((p) => p === peerId);
}
} */
if(dialog) {
if(!update.pFlags.pinned) {
this.handleDialogUnpinning(dialog, folderId);
} else { // means set
dialog.pFlags.pinned = true;
}
this.generateIndexForDialog(dialog);
}
this.appMessagesManager.scheduleHandleNewDialogs(peerId, dialog);
};
private onUpdatePinnedDialogs = (update: Update.updatePinnedDialogs) => {
const folderId = update.folder_id ?? FOLDER_ID_ALL;
const handleOrder = (order: PeerId[]) => {
this.resetPinnedOrder(folderId);
this.pinnedOrders[folderId].push(...order);
this.savePinnedOrders();
order.reverse(); // index must be higher
order.forEach((peerId) => {
newPinned[peerId] = true;
const dialog = this.getDialogOnly(peerId);
this.appMessagesManager.scheduleHandleNewDialogs(peerId, dialog);
if(!dialog) {
return;
}
dialog.pFlags.pinned = true;
this.generateIndexForDialog(dialog);
});
const dialogs = this.getFolderDialogs(folderId, false);
for(const dialog of dialogs) {
if(!dialog.pFlags.pinned) {
break;
}
const peerId = dialog.peerId;
if(!newPinned[peerId]) {
this.appMessagesManager.scheduleHandleNewDialogs(peerId);
}
}
};
// this.log('updatePinnedDialogs', update);
const newPinned: {[peerId: PeerId]: true} = {};
if(!update.order) {
this.apiManager.invokeApi('messages.getPinnedDialogs', {
folder_id: folderId
}).then((dialogsResult) => {
// * for test reordering and rendering
// dialogsResult.dialogs.reverse();
this.applyDialogs(dialogsResult);
handleOrder(dialogsResult.dialogs.map((d) => d.peerId));
/* dialogsResult.dialogs.forEach((dialog) => {
newPinned[dialog.peerId] = true;
});
this.dialogsStorage.getFolder(folderId).forEach((dialog) => {
const peerId = dialog.peerId;
if(dialog.pFlags.pinned && !newPinned[peerId]) {
this.newDialogsToHandle[peerId] = {reload: true};
this.scheduleHandleNewDialogs();
}
}); */
});
return;
}
// this.log('before order:', this.dialogsStorage[0].map((d) => d.peerId));
handleOrder(update.order.map((peer) => this.appPeersManager.getPeerId((peer as DialogPeer.dialogPeer).peer)));
};
}