From ff06f0305b7ff5430b9ed1153d1e0c4b5e9db070 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Fri, 7 May 2021 01:02:21 +0400 Subject: [PATCH] Improved sliced array Added tests for sliced array Fix scrolling foreign channels from top Fix loading top comments Load new history of foreign channels every 30 seconds --- src/components/chat/bubbles.ts | 63 ++++++-- src/helpers/slicedArray.ts | 179 +++++++++++++++------- src/lib/appManagers/appImManager.ts | 8 +- src/lib/appManagers/appMessagesManager.ts | 172 ++++++++++++--------- src/lib/appManagers/appPhotosManager.ts | 2 +- src/lib/storages/dialogs.ts | 7 +- src/tests/slicedArray.test.ts | 121 +++++++++++++++ 7 files changed, 408 insertions(+), 144 deletions(-) create mode 100644 src/tests/slicedArray.test.ts diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index e0fd7cc2..671e0f3e 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -130,6 +130,8 @@ export default class ChatBubbles { public isFirstLoad = true; private needReflowScroll: boolean; + private fetchNewPromise: Promise; + constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager, private storage: typeof sessionStorage) { //this.chat.log.error('Bubbles construction'); @@ -1399,6 +1401,7 @@ export default class ChatBubbles { this.messagesQueuePromise = null; this.getHistoryTopPromise = this.getHistoryBottomPromise = undefined; + this.fetchNewPromise = undefined; if(this.stickyIntersector) { this.stickyIntersector.disconnect(); @@ -1618,8 +1621,7 @@ export default class ChatBubbles { this.chat.dispatchEvent('setPeer', lastMsgId, !isJump); - const isFetchIntervalNeeded = () => peerId < 0 && !this.appChatsManager.isInChat(peerId) && false; - const needFetchInterval = isFetchIntervalNeeded(); + const needFetchInterval = this.appMessagesManager.isFetchIntervalNeeded(peerId); const needFetchNew = savedPosition || needFetchInterval; if(!needFetchNew) { // warning @@ -1627,20 +1629,44 @@ export default class ChatBubbles { this.scrollable.loadedAll.bottom = true; } } else { + const middleware = this.getMiddleware(); Promise.all([setPeerPromise, getHeavyAnimationPromise()]).then(() => { + if(!middleware()) { + return; + } + this.scrollable.checkForTriggers(); if(needFetchInterval) { - const middleware = this.getMiddleware(); - const interval = window.setInterval(() => { - if(!middleware() || !isFetchIntervalNeeded()) { - clearInterval(interval); - return; - } - - this.scrollable.loadedAll.bottom = false; - this.loadMoreHistory(false); - }, 30e3); + const f = () => { + this.fetchNewPromise = new Promise((resolve) => { + if(!middleware() || !this.appMessagesManager.isFetchIntervalNeeded(peerId)) { + resolve(); + return; + } + + this.appMessagesManager.getNewHistory(peerId, this.chat.threadId).then((historyStorage) => { + if(!middleware()) { + resolve(); + return; + } + + const slice = historyStorage.history.slice; + const isBottomEnd = slice.isEnd(SliceEnd.Bottom); + if(this.scrollable.loadedAll.bottom !== isBottomEnd) { + this.scrollable.loadedAll.bottom = isBottomEnd; + this.onScroll(); + } + + setTimeout(f, 30e3); + resolve(); + }); + }).finally(() => { + this.fetchNewPromise = undefined; + }); + }; + + f(); } }); } @@ -2626,9 +2652,15 @@ export default class ChatBubbles { } */ const historyStorage = this.appMessagesManager.getHistoryStorage(this.peerId, this.chat.threadId); - if(history.includes(historyStorage.maxId)) { + const firstSlice = historyStorage.history.first; + const lastSlice = historyStorage.history.last; + if(firstSlice.isEnd(SliceEnd.Bottom) && history.includes(firstSlice[0])) { this.scrollable.loadedAll.bottom = true; } + + if(lastSlice.isEnd(SliceEnd.Top) && history.includes(lastSlice[lastSlice.length - 1])) { + this.scrollable.loadedAll.top = true; + } //console.time('appImManager render history'); @@ -2818,8 +2850,9 @@ export default class ChatBubbles { additionMsgIds = [additionMsgId]; } else { const historyStorage = this.appMessagesManager.getHistoryStorage(peerId, this.chat.threadId); - if(historyStorage.history.length < loadCount && !historyStorage.history.slice.isEnd(SliceEnd.Both)) { - additionMsgIds = historyStorage.history.slice.slice(); + const slice = historyStorage.history.slice; + if(slice.length < loadCount && !slice.isEnd(SliceEnd.Both)) { + additionMsgIds = slice.slice(); // * filter last album, because we don't know is it the last item for(let i = additionMsgIds.length - 1; i >= 0; --i) { diff --git a/src/helpers/slicedArray.ts b/src/helpers/slicedArray.ts index 5ec02dba..807be1d7 100644 --- a/src/helpers/slicedArray.ts +++ b/src/helpers/slicedArray.ts @@ -4,6 +4,8 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import { MOUNT_CLASS_TO } from "../config/debug"; + /** * Descend sorted storage */ @@ -14,15 +16,19 @@ export enum SliceEnd { None = 0, Top = 1, Bottom = 2, - Both = 4 + Both = SliceEnd.Top | SliceEnd.Bottom }; export interface Slice extends Array { - slicedArray: SlicedArray; + //slicedArray: SlicedArray; end: SliceEnd; isEnd: (side: SliceEnd) => boolean; setEnd: (side: SliceEnd) => void; + unsetEnd: (side: SliceEnd) => void; + + slice: (from?: number, to?: number) => Slice; + splice: (start: number, deleteCount: number, ...items: ItemType[]) => Slice; } export interface SliceConstructor { @@ -35,51 +41,84 @@ export default class SlicedArray { private sliceConstructor: SliceConstructor; constructor() { - const self = this; - this.sliceConstructor = class Slice extends Array implements Slice { - slicedArray: SlicedArray; - end: SliceEnd = SliceEnd.None; + // @ts-ignore + this.sliceConstructor = SlicedArray.getSliceConstructor(this); - constructor(...items: ItemType[]) { - super(...items); - this.slicedArray = self; - } + const first = this.constructSlice(); + //first.setEnd(SliceEnd.Bottom); + this.slices = [first]; + } - isEnd(side: SliceEnd) { - if(this.end & side) { + private static getSliceConstructor(slicedArray: SlicedArray) { + return class Slice extends Array implements Slice { + //slicedArray: SlicedArray; + end: SliceEnd = SliceEnd.None; + + /* constructor(...items: ItemType[]) { + super(...items); + //this.slicedArray = slicedArray; + } */ + + isEnd(side: SliceEnd): boolean { + if((this.end & side) === side) { return true; - } - + }/* else if(!this.slicedArray) { + return false; + } */ + + let isEnd = false; if(side === SliceEnd.Top) { - const slice = self.last; - return slice.end & side ? this.includes(slice[slice.length - 1]) || !slice.length : false; + const slice = slicedArray.last; + isEnd = slice.end & side ? this.includes(slice[slice.length - 1])/* || !slice.length */ : false; } else if(side === SliceEnd.Bottom) { - const slice = self.first; - return slice.end & side ? this.includes(slice[0]) || !slice.length : false; - }/* else if(side === SliceEnd.Both) { - - } */ + const slice = slicedArray.first; + isEnd = slice.end & side ? this.includes(slice[0])/* || !slice.length */ : false; + } else if(side === SliceEnd.Both) { + return this.isEnd(SliceEnd.Top) && this.isEnd(SliceEnd.Bottom); + } - return false; + if(isEnd) { + this.setEnd(side); + } + + return isEnd; } - + setEnd(side: SliceEnd) { this.end |= side; + } - if(side !== SliceEnd.Both && this.end & SliceEnd.Top && this.end & SliceEnd.Bottom) { - this.end |= SliceEnd.Both; + unsetEnd(side: SliceEnd) { + this.end ^= side; + } + + splice(start: number, deleteCount: number, ...items: ItemType[]) { + const ret = super.splice(start, deleteCount, ...items); + + if(!this.length) { + const slices = slicedArray.slices as number[][]; + const idx = slices.indexOf(this); + if(idx !== -1) { + if(slices.length === 1) { // left empty slice without ends + this.unsetEnd(SliceEnd.Both); + } else { // delete this slice + slices.splice(idx, 1); + } + } } + + return ret; } } - - const first = this.constructSlice(); - first.setEnd(SliceEnd.Bottom); - this.slices = [first]; } public constructSlice(...items: ItemType[]) { //const slice = new Slice(this, ...items); - const slice = new this.sliceConstructor(...items); + // can't pass items directly to constructor because first argument is length + const slice = new this.sliceConstructor(items.length); + for(let i = 0, length = items.length; i < length; ++i) { + slice[i] = items[i]; + } return slice; // ! code below will slow execution in 15 times @@ -128,7 +167,7 @@ export default class SlicedArray { */ } - public insertSlice(slice: ItemType[]) { + public insertSlice(slice: ItemType[], flatten = true) { if(!slice.length) { return; } @@ -136,15 +175,15 @@ export default class SlicedArray { const first = this.slices[0]; if(!first.length) { first.push(...slice); - return; + return first; } const lowerBound = slice[slice.length - 1]; const upperBound = slice[0]; - let foundSlice: Slice, lowerIndex = -1, upperIndex = -1; - for(let i = 0; i < this.slices.length; ++i) { - foundSlice = this.slices[i]; + let foundSlice: Slice, lowerIndex = -1, upperIndex = -1, foundSliceIndex = 0; + for(; foundSliceIndex < this.slices.length; ++foundSliceIndex) { + foundSlice = this.slices[foundSliceIndex]; lowerIndex = foundSlice.indexOf(lowerBound); upperIndex = foundSlice.indexOf(upperBound); @@ -173,29 +212,38 @@ export default class SlicedArray { } this.slices.splice(insertIndex, 0, this.constructSlice(...slice)); + foundSliceIndex = insertIndex; } - this.flatten(); - } - - private flatten() { - if(this.slices.length < 2) { - return; + if(flatten) { + return this.flatten(foundSliceIndex); } + } - for(let i = 0, length = this.slices.length; i < (length - 1); ++i) { - const prevSlice = this.slices[i]; - const nextSlice = this.slices[i + 1]; + private flatten(foundSliceIndex: number) { + if(this.slices.length >= 2) { + for(let i = 0, length = this.slices.length; i < (length - 1); ++i) { + const prevSlice = this.slices[i]; + const nextSlice = this.slices[i + 1]; + + const upperIndex = prevSlice.indexOf(nextSlice[0]); + if(upperIndex !== -1) { + prevSlice.setEnd(nextSlice.end); + this.slices.splice(i + 1, 1); - const upperIndex = prevSlice.indexOf(nextSlice[0]); - if(upperIndex !== -1) { - prevSlice.setEnd(nextSlice.end); - this.slices.splice(i + 1, 1); - length--; + if(i < foundSliceIndex) { + --foundSliceIndex; + } - this.insertSlice(nextSlice); + --length; // respect array bounds + --i; // repeat from the same place + + this.insertSlice(nextSlice, false); + } } } + + return this.slices[foundSliceIndex]; } // * @@ -217,7 +265,7 @@ export default class SlicedArray { } public findSlice(item: ItemType) { - for(let i = 0; i < this.slices.length; ++i) { + for(let i = 0, length = this.slices.length; i < length; ++i) { const slice = this.slices[i]; const index = slice.indexOf(item); if(index !== -1) { @@ -295,6 +343,8 @@ export default class SlicedArray { const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit; const bottomWasMeantToLoad = Math.abs(add_offset); + // can use 'slice' here to check because if it's end, then 'sliced' is out of 'slice' + // useful when there is only 1 message in chat on its reopening const topFulfilled = (slice.length - sliceOffset) >= topWasMeantToLoad || (slice.isEnd(SliceEnd.Top) ? (sliced.setEnd(SliceEnd.Top), true) : false); const bottomFulfilled = (sliceOffset - bottomWasMeantToLoad) >= 0 || (slice.isEnd(SliceEnd.Bottom) ? (sliced.setEnd(SliceEnd.Bottom), true) : false); @@ -308,19 +358,40 @@ export default class SlicedArray { } public unshift(...items: ItemType[]) { - this.first.unshift(...items); + let slice = this.first; + if(!slice.length) { + slice.setEnd(SliceEnd.Bottom); + } else if(!slice.isEnd(SliceEnd.Bottom)) { + slice = this.constructSlice(); + slice.setEnd(SliceEnd.Bottom); + this.slices.unshift(slice); + } + + slice.unshift(...items); } public push(...items: ItemType[]) { - this.last.push(...items); + let slice = this.last; + if(!slice.length) { + slice.setEnd(SliceEnd.Top); + } else if(!slice.isEnd(SliceEnd.Top)) { + slice = this.constructSlice(); + slice.setEnd(SliceEnd.Top); + this.slices.push(slice); + } + + slice.push(...items); } public delete(item: ItemType) { const found = this.findSlice(item); if(found) { found.slice.splice(found.index, 1); + return true; } + + return false; } } -(window as any).slicedArray = new SlicedArray(); +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.SlicedArray = SlicedArray); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 0d22c8b1..bce20395 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -50,6 +50,7 @@ import { copy, getObjectKeysAndSort } from '../../helpers/object'; import { getFilesFromEvent } from '../../helpers/files'; import PeerTitle from '../../components/peerTitle'; import PopupPeer from '../../components/popups/peer'; +import { SliceEnd } from '../../helpers/slicedArray'; //console.log('appImManager included33!'); @@ -445,10 +446,11 @@ export class AppImManager { return; } else if(e.code === 'ArrowUp') { if(!chat.input.editMsgId && chat.input.isInputEmpty()) { - const history = appMessagesManager.getHistoryStorage(chat.peerId, chat.threadId); - if(history.history.length) { + const historyStorage = appMessagesManager.getHistoryStorage(chat.peerId, chat.threadId); + const slice = historyStorage.history.slice; + if(slice.isEnd(SliceEnd.Bottom) && slice.length) { let goodMid: number; - for(const mid of history.history.slice) { + for(const mid of slice) { const message = chat.getMessage(mid); const good = this.myId === chat.peerId ? message.fromId === this.myId : message.pFlags.out; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index c31a3da6..5a09f187 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -14,7 +14,7 @@ import ProgressivePreloader from "../../components/preloader"; import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; import { tsNow } from "../../helpers/date"; import { createPosterForVideo } from "../../helpers/files"; -import { copy, defineNotNumerableProperties, getObjectKeysAndSort } from "../../helpers/object"; +import { copy, getObjectKeysAndSort } from "../../helpers/object"; import { randomLong } from "../../helpers/random"; import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string"; import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo } from "../../layer"; @@ -168,7 +168,7 @@ export class AppMessagesManager { public migratedFromTo: {[peerId: number]: number} = {}; public migratedToFrom: {[peerId: number]: number} = {}; - public newMessagesHandlePromise = 0; + public newMessagesHandleTimeout = 0; public newMessagesToHandle: {[peerId: string]: Set} = {}; public newDialogsHandlePromise: Promise; public newDialogsToHandle: {[peerId: string]: Dialog} = {}; @@ -1301,14 +1301,17 @@ export class AppMessagesManager { /* if(options.threadId && this.threadsStorage[peerId]) { delete this.threadsStorage[peerId][options.threadId]; } */ - if(options.threadId) { - const historyStorage = this.getHistoryStorage(peerId, options.threadId); - historyStorage.history.unshift(messageId); + const storages: HistoryStorage[] = [ + this.getHistoryStorage(peerId), + options.threadId ? this.getHistoryStorage(peerId, options.threadId) : undefined + ]; + + for(const storage of storages) { + if(storage) { + storage.history.unshift(messageId); + } } - const historyStorage = this.getHistoryStorage(peerId); - historyStorage.history.unshift(messageId); - //if(!options.isGroupedItem) { this.saveMessages([message], {storage, isOutgoing: true}); setTimeout(() => { @@ -1514,7 +1517,6 @@ export class AppMessagesManager { if(pendingData) { const {peerId, tempId, storage} = pendingData; const historyStorage = this.getHistoryStorage(peerId); - const pos = historyStorage.history.findSlice(tempId); apiUpdatesManager.processUpdateMessage({ _: 'updateShort', @@ -1524,9 +1526,7 @@ export class AppMessagesManager { } }); - if(pos) { - pos.slice.splice(pos.index, 1); - } + historyStorage.history.delete(tempId); delete this.pendingByRandomId[randomId]; delete storage[tempId]; @@ -1627,7 +1627,7 @@ export class AppMessagesManager { // ! ВНИМАНИЕ: ОЧЕНЬ СЛОЖНАЯ ЛОГИКА: // ! если делать запрос сначала по папке 0, потом по папке 1, по индексу 0 в массиве будет один и тот же диалог, с dialog.pFlags.pinned, ЛОЛ??? // ! т.е., с запросом folder_id: 1, и exclude_pinned: 0, в результате будут ещё и закреплённые с папки 0 - return apiManager.invokeApi('messages.getDialogs', { + return apiManager.invokeApiSingle('messages.getDialogs', { folder_id: folderId, offset_date: offsetDate, offset_id: offsetId, @@ -3271,7 +3271,7 @@ export class AppMessagesManager { } public getDiscussionMessage(peerId: number, mid: number) { - return apiManager.invokeApi('messages.getDiscussionMessage', { + return apiManager.invokeApiSingle('messages.getDiscussionMessage', { peer: appPeersManager.getInputPeerById(peerId), msg_id: this.getServerMessageId(mid) }).then(result => { @@ -3295,9 +3295,20 @@ export class AppMessagesManager { }); } + private handleNewMessage(peerId: number, mid: number) { + if(this.newMessagesToHandle[peerId] === undefined) { + this.newMessagesToHandle[peerId] = new Set(); + } + + this.newMessagesToHandle[peerId].add(mid); + if(!this.newMessagesHandleTimeout) { + this.newMessagesHandleTimeout = window.setTimeout(this.handleNewMessages, 0); + } + } + handleNewMessages = () => { - clearTimeout(this.newMessagesHandlePromise); - this.newMessagesHandlePromise = 0; + clearTimeout(this.newMessagesHandleTimeout); + this.newMessagesHandleTimeout = 0; rootScope.broadcast('history_multiappend', this.newMessagesToHandle); this.newMessagesToHandle = {}; @@ -3404,6 +3415,7 @@ export class AppMessagesManager { return promise; } + // TODO: cancel notification by peer when this function is being called public readHistory(peerId: number, maxId = 0, threadId?: number, force = false) { //return Promise.resolve(); // console.trace('start read') @@ -3452,7 +3464,7 @@ export class AppMessagesManager { _: 'updateReadChannelInbox', max_id: maxId, channel_id: -peerId - } + } as Update.updateReadChannelInbox }); } else { if(!historyStorage.readPromise) { @@ -3477,21 +3489,10 @@ export class AppMessagesManager { _: 'updateReadHistoryInbox', max_id: maxId, peer: appPeersManager.getOutputPeer(peerId) - } + } as Update.updateReadHistoryInbox }); } - if(!threadId && historyStorage && historyStorage.history.length) { - const slice = historyStorage.history.slice; - for(const mid of slice) { - const message = this.getMessageByPeer(peerId, mid); - if(message && !message.pFlags.out) { - message.pFlags.unread = false; - appNotificationsManager.cancel('msg' + mid); - } - } - } - appNotificationsManager.soundReset(appPeersManager.getPeerString(peerId)); if(historyStorage.readPromise) { @@ -3534,7 +3535,7 @@ export class AppMessagesManager { _: 'updateChannelReadMessagesContents', channel_id: channelId, messages: msgIds - } + } as Update.updateChannelReadMessagesContents }); }); } else { @@ -3548,7 +3549,7 @@ export class AppMessagesManager { messages: msgIds, pts: affectedMessages.pts, pts_count: affectedMessages.pts_count - } + } as Update.updateReadMessagesContents }); }); } @@ -3694,15 +3695,8 @@ export class AppMessagesManager { return false; } - const history = historyStorage.history.slice; - const topMsgId = history[0]; - history.unshift(message.mid); - if(message.mid < topMsgId) { - //this.log.error('this should\'nt have happenned!', message, history); - history.sort((a, b) => { - return b - a; - }); - } + const slice = historyStorage.history.insertSlice([message.mid]); + slice.setEnd(SliceEnd.Bottom); if(historyStorage.count !== null) { historyStorage.count++; @@ -3717,14 +3711,7 @@ export class AppMessagesManager { } if(!pendingMessage) { - if(this.newMessagesToHandle[peerId] === undefined) { - this.newMessagesToHandle[peerId] = new Set(); - } - - this.newMessagesToHandle[peerId].add(message.mid); - if(!this.newMessagesHandlePromise) { - this.newMessagesHandlePromise = window.setTimeout(this.handleNewMessages, 0); - } + this.handleNewMessage(peerId, message.mid); } if(isLocalThreadUpdate) { @@ -4471,7 +4458,7 @@ export class AppMessagesManager { return Promise.resolve(Object.keys(storage).map(id => +id)); } - return apiManager.invokeApi('messages.getScheduledHistory', { + return apiManager.invokeApiSingle('messages.getScheduledHistory', { peer: appPeersManager.getInputPeerById(peerId), hash: 0 }).then(historyResult => { @@ -4506,6 +4493,36 @@ export class AppMessagesManager { }); } + public isFetchIntervalNeeded(peerId: number) { + return peerId < 0 && !appChatsManager.isInChat(peerId); + } + + public async getNewHistory(peerId: number, threadId?: number) { + if(!this.isFetchIntervalNeeded(peerId)) { + return; + } + + const historyStorage = this.getHistoryStorage(peerId, threadId); + const slice = historyStorage.history.slice; + if(!slice.isEnd(SliceEnd.Bottom)) { + return; + } + + delete historyStorage.maxId; + slice.unsetEnd(SliceEnd.Bottom); + + let historyResult = this.getHistory(peerId, slice[0], 0, 50, threadId); + if(historyResult instanceof Promise) { + historyResult = await historyResult; + } + + for(let i = 0, length = historyResult.history.length; i < length; ++i) { + this.handleNewMessage(peerId, historyResult.history[i]); + } + + return historyStorage; + } + /** * * https://core.telegram.org/api/offsets, offset_id is inclusive */ @@ -4567,7 +4584,7 @@ export class AppMessagesManager { } const haveSlice = historyStorage.history.sliceMe(maxId, offset, limit); - if(haveSlice && (haveSlice.slice.length === limit || (haveSlice.fulfilled & SliceEnd.Both))) { + if(haveSlice && (haveSlice.slice.length === limit || (haveSlice.fulfilled & SliceEnd.Both) === SliceEnd.Both)) { return { count: historyStorage.count, history: haveSlice.slice, @@ -4587,10 +4604,15 @@ export class AppMessagesManager { public fillHistoryStorage(peerId: number, offset_id: number, limit: number, add_offset: number, historyStorage: HistoryStorage, threadId?: number): Promise { return this.requestHistory(peerId, offset_id, limit, add_offset, undefined, threadId).then((historyResult) => { - historyStorage.count = (historyResult as MessagesMessages.messagesMessagesSlice).count || historyResult.messages.length; + const {offset_id_offset, count, messages} = historyResult as MessagesMessages.messagesMessagesSlice; - const offsetIdOffset = (historyResult as MessagesMessages.messagesMessagesSlice).offset_id_offset || 0; - const isTopEnd = offsetIdOffset >= (historyStorage.count - limit) || historyStorage.count < (limit + add_offset); + historyStorage.count = count || messages.length; + const offsetIdOffset = offset_id_offset || 0; + + const topWasMeantToLoad = add_offset < 0 ? limit + add_offset : limit; + + const isTopEnd = offsetIdOffset >= (historyStorage.count - topWasMeantToLoad) || historyStorage.count < topWasMeantToLoad; + const isBottomEnd = !offsetIdOffset || (add_offset < 0 && (offsetIdOffset + add_offset) <= 0); /* if(!maxId && historyResult.messages.length) { maxId = this.incrementMessageId((historyResult.messages[0] as MyMessage).mid, 1); @@ -4598,13 +4620,14 @@ export class AppMessagesManager { const wasTotalCount = historyStorage.history.length; */ - historyResult.messages.forEach((message) => { + const mids = messages.map((message) => { if(this.mergeReplyKeyboard(historyStorage, message)) { rootScope.broadcast('history_reply_markup', {peerId}); } + + return (message as MyMessage).mid; }); - const mids = historyResult.messages.map((message) => (message as MyMessage).mid); // * add bound manually. // * offset_id will be inclusive only if there is 'add_offset' <= -1 (-1 - will only include the 'offset_id') if(offset_id && !mids.includes(offset_id) && offsetIdOffset < historyStorage.count) { @@ -4618,10 +4641,16 @@ export class AppMessagesManager { mids.splice(i, 0, offset_id); } - historyStorage.history.insertSlice(mids); - - if(isTopEnd) { - historyStorage.history.last.setEnd(SliceEnd.Top); + const slice = historyStorage.history.insertSlice(mids); + if(slice) { + if(isTopEnd) { + slice.setEnd(SliceEnd.Top); + } + + if(isBottomEnd) { + slice.setEnd(SliceEnd.Bottom); + historyStorage.maxId = slice[0]; // ! WARNING + } } /* const isBackLimit = offset < 0 && -offset !== fullLimit; @@ -4681,7 +4710,7 @@ export class AppMessagesManager { options.msg_id = this.getServerMessageId(threadId) || 0; } - const promise: ReturnType = apiManager.invokeApi(threadId ? 'messages.getReplies' : 'messages.getHistory', options, { + const promise: ReturnType = apiManager.invokeApiSingle(threadId ? 'messages.getReplies' : 'messages.getHistory', options, { //timeout: APITIMEOUT, noErrorBox: true }) as any; @@ -4699,21 +4728,24 @@ export class AppMessagesManager { apiUpdatesManager.addChannelState(-peerId, (historyResult as MessagesMessages.messagesChannelMessages).pts); } - let length = historyResult.messages.length; + let length = historyResult.messages.length, count = (historyResult as MessagesMessages.messagesMessagesSlice).count; if(length && historyResult.messages[length - 1].deleted) { historyResult.messages.splice(length - 1, 1); length--; - (historyResult as MessagesMessages.messagesMessagesSlice).count--; + count--; } // will load more history if last message is album grouped (because it can be not last item) - const historyStorage = this.getHistoryStorage(peerId, threadId); // historyResult.messages: desc sorted - if(length && (historyResult.messages[length - 1] as Message.message).grouped_id - && (historyStorage.history.length + historyResult.messages.length) < (historyResult as MessagesMessages.messagesMessagesSlice).count) { - return this.requestHistory(peerId, (historyResult.messages[length - 1] as Message.message).mid, 10, 0, offsetDate, threadId).then((_historyResult) => { - return historyResult; - }); + const historyStorage = this.getHistoryStorage(peerId, threadId); + const oldestMessage: Message.message = historyResult.messages[length - 1] as any; + if(length && oldestMessage.grouped_id) { + const foundSlice = historyStorage.history.findSlice(oldestMessage.mid); + if(foundSlice && (foundSlice.slice.length + historyResult.messages.length) < count) { + return this.requestHistory(peerId, oldestMessage.mid, 10, 0, offsetDate, threadId).then((_historyResult) => { + return historyResult; + }); + } } return historyResult; @@ -4760,12 +4792,12 @@ export class AppMessagesManager { let promise: Promise; if(+peerId < 0 && appPeersManager.isChannel(+peerId)) { - promise = apiManager.invokeApi('channels.getMessages', { + promise = apiManager.invokeApiSingle('channels.getMessages', { channel: appChatsManager.getChannelInput(-+peerId), id: msgIds }); } else { - promise = apiManager.invokeApi('messages.getMessages', { + promise = apiManager.invokeApiSingle('messages.getMessages', { id: msgIds }); } diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index 7f420204..2e47153b 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -13,7 +13,7 @@ import type { DownloadOptions } from "../mtproto/apiFileManager"; import { bytesFromHex } from "../../helpers/bytes"; import { CancellablePromise } from "../../helpers/cancellablePromise"; import { getFileNameByLocation } from "../../helpers/fileName"; -import { safeReplaceArrayInObject, defineNotNumerableProperties, isObject } from "../../helpers/object"; +import { safeReplaceArrayInObject, isObject } from "../../helpers/object"; import { isSafari } from "../../helpers/userAgent"; import { InputFileLocation, InputMedia, Photo, PhotoSize, PhotosPhotos } from "../../layer"; import apiManager from "../mtproto/mtprotoworker"; diff --git a/src/lib/storages/dialogs.ts b/src/lib/storages/dialogs.ts index 7fe9735f..dc2a20a5 100644 --- a/src/lib/storages/dialogs.ts +++ b/src/lib/storages/dialogs.ts @@ -25,6 +25,7 @@ import { forEachReverse, insertInDescendSortedArray } from "../../helpers/array" import rootScope from "../rootScope"; import { safeReplaceObject } from "../../helpers/object"; import { AppStateManager } from "../appManagers/appStateManager"; +import { SliceEnd } from "../../helpers/slicedArray"; export default class DialogsStorage { private storage: AppStateManager['storages']['dialogs']; @@ -490,13 +491,17 @@ export default class DialogsStorage { } const historyStorage = this.appMessagesManager.getHistoryStorage(peerId); + const slice = historyStorage.history.slice; /* if(historyStorage === undefined) { // warning historyStorage.history.push(mid); if(this.mergeReplyKeyboard(historyStorage, message)) { rootScope.broadcast('history_reply_markup', {peerId}); } - } else */if(!historyStorage.history.slice.length) { + } else */if(!slice.length) { historyStorage.history.unshift(mid); + } 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.maxId = mid; diff --git a/src/tests/slicedArray.test.ts b/src/tests/slicedArray.test.ts new file mode 100644 index 00000000..b5a399bb --- /dev/null +++ b/src/tests/slicedArray.test.ts @@ -0,0 +1,121 @@ +import SlicedArray, { Slice } from "../helpers/slicedArray"; + +test('Slicing returns new Slice', () => { + const sliced = new SlicedArray(); + const newSlice = sliced.slice.slice(); + expect(newSlice.isEnd).toBeDefined(); +}); + +describe('Inserting', () => { + const sliced = new SlicedArray(); + + // @ts-ignore + const slices = sliced.slices; + + const arr = [100, 99, 98, 97, 96, 95]; + const distantArr = arr.slice(-2).map(v => v - 2); + const missingArr = [arr[arr.length - 1], arr[arr.length - 1] - 1, distantArr[0]]; + + const startValue = 90; + const values: number[] = []; + const valuesPerArray = 3; + const totalArrays = 10; + for(let i = 0, length = valuesPerArray * totalArrays; i < length; ++i) { + values.push(startValue - i); + } + const arrays: number[][] = []; + for(let i = 0; i < totalArrays; ++i) { + arrays.push(values.slice(valuesPerArray * i, valuesPerArray * (i + 1))); + } + + test('Insert & flatten', () => { + const idx = 2; + + sliced.insertSlice(arr.slice(0, idx + 1)); + sliced.insertSlice(arr.slice(idx)); + + expect([...sliced.first]).toEqual(arr); + }); + + test('Insert inner values', () => { + sliced.insertSlice(arr.slice(1, -1)); + + expect([...sliced.first]).toEqual(arr); + }); + + test('Insert distant slice', () => { + const length = slices.length; + sliced.insertSlice(distantArr); + + expect(slices.length).toEqual(length + 1); + }); + + test('Insert intersection & join them', () => { + const length = slices.length; + sliced.insertSlice(missingArr); + + expect(slices.length).toEqual(length - 1); + }); + + let returnedSlice: Slice; + test('Insert arrays with gap & join them', () => { + slices[0].length = 0; + + for(const arr of arrays) { + sliced.insertSlice(arr); + } + + expect(slices.length).toEqual(totalArrays); + + returnedSlice = sliced.insertSlice(values.slice(0, -valuesPerArray + 1)); + + expect(slices.length).toEqual(1); + }); + + test('Return inserted & flattened slice', () => { + expect(slices[0]).toEqual(returnedSlice); + }); +}); + +describe('Slicing', () => { + const sliced = new SlicedArray(); + + // @ts-ignore + const slices = sliced.slices; + + const VALUES_LENGTH = 100; + const INCREMENTOR = 0xFFFF; + const values: number[] = []; + for(let i = 0; i < VALUES_LENGTH; ++i) { + values[i] = i + INCREMENTOR * i; + } + values.sort((a, b) => b - a); + sliced.insertSlice(values); + + const addOffset = 40; + const limit = 40; + + const r = (func: (idx: number) => void) => { + const max = VALUES_LENGTH * 3; + for(let i = 0; i < max; ++i) { + const idx = Math.random() * max | 0; + func(idx); + } + }; + + describe('Positive addOffset', () => { + test('From the start', () => { + const {slice} = sliced.sliceMe(0, addOffset, limit); + expect([...slice]).toEqual(values.slice(addOffset, addOffset + limit)); + }); + + test('From existing offsetId', () => { + r((idx) => { + const value = values[idx] || 1; + idx += 1; // because is it inclusive + const {slice} = sliced.sliceMe(value, addOffset, limit); + expect([...slice]).toEqual(values.slice(idx + addOffset, idx + addOffset + limit)); + }); + }); + }); +});