From 5f807a6552f68324e7e9dc41b89b14fb170831b5 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Fri, 17 Sep 2021 20:50:29 +0400 Subject: [PATCH] fixes & fixes --- src/components/chat/bubbles.ts | 71 ++-- .../sidebarLeft/tabs/archivedTab.ts | 6 +- src/components/sortedUserList.ts | 137 +++---- src/components/transition.ts | 42 +- src/components/wrappers.ts | 53 +++ src/helpers/array.ts | 10 +- src/helpers/dom/positionElementByIndex.ts | 4 +- src/helpers/schedulers.ts | 21 + src/helpers/sortedList.ts | 163 ++++++++ src/lib/appManagers/appDialogsManager.ts | 371 ++++++++---------- src/lib/appManagers/appMessagesManager.ts | 90 ++--- src/lib/appManagers/appUsersManager.ts | 2 +- src/lib/mtproto/mtproto_config.ts | 1 + src/lib/rootScope.ts | 1 + src/lib/storages/dialogs.ts | 98 +++-- src/scss/partials/_chatBubble.scss | 4 + src/scss/partials/_document.scss | 4 + src/scss/style.scss | 29 +- 18 files changed, 661 insertions(+), 446 deletions(-) create mode 100644 src/helpers/sortedList.ts diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index af61f18c..80d8214b 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -149,7 +149,7 @@ export default class ChatBubbles { private replyFollowHistory: number[] = []; private isHeavyAnimationInProgress = false; - private scrollingToNewBubble: HTMLElement; + private scrollingToBubble: HTMLElement; private isFirstLoad = true; private needReflowScroll: boolean; @@ -225,7 +225,7 @@ export default class ChatBubbles { this.setBubblePosition(bubble, message, false); //this.log('history_update', this.bubbles[mid], mid, message); - if(this.scrollingToNewBubble) { + if(this.scrollingToBubble) { this.scrollToBubbleEnd(); } @@ -1420,6 +1420,12 @@ export default class ChatBubbles { return; } */ + if(!scrolledDown) { + if(this.scrollingToBubble && this.scrollingToBubble === this.getLastBubble()) { + scrolledDown = true; + } + } + const promise = this.performHistoryResult(mids, false, true); if(scrolledDown) { promise.then(() => { @@ -1437,6 +1443,13 @@ export default class ChatBubbles { } } + public getLastBubble() { + const lastDateGroup = this.getLastDateGroup(); + if(lastDateGroup) { + return lastDateGroup.lastElementChild as HTMLElement; + } + } + public scrollToBubble( element: HTMLElement, position: ScrollLogicalPosition, @@ -1458,21 +1471,17 @@ export default class ChatBubbles { return this.scrollable.scrollIntoViewNew(element, position, 4, undefined, forceDirection, forceDuration); } - public scrollToBubbleEnd(bubble?: HTMLElement) { - if(!bubble) { - const lastDateGroup = this.getLastDateGroup(); - if(lastDateGroup) { - bubble = lastDateGroup.lastElementChild as HTMLElement; - } - } + public scrollToBubbleEnd(bubble = this.getLastBubble()) { /* if(DEBUG) { this.log('scrollToNewLastBubble: will scroll into view:', bubble); } */ if(bubble) { - this.scrollingToNewBubble = bubble; + this.scrollingToBubble = bubble; + const middleware = this.getMiddleware(); this.scrollToBubble(bubble, 'end').then(() => { - this.scrollingToNewBubble = null; + if(!middleware()) return; + this.scrollingToBubble = undefined; }); } } @@ -1517,7 +1526,7 @@ export default class ChatBubbles { const date = new Date(message.date * 1000); date.setHours(0, 0, 0); const dateTimestamp = date.getTime(); - if(!(dateTimestamp in this.dateMessages)) { + if(!this.dateMessages[dateTimestamp]) { let dateElement: HTMLElement; const today = new Date(); @@ -1549,8 +1558,8 @@ export default class ChatBubbles { } } - const div = document.createElement('div'); - div.className = 'bubble service is-date'; + const bubble = document.createElement('div'); + bubble.className = 'bubble service is-date'; const bubbleContent = document.createElement('div'); bubbleContent.classList.add('bubble-content'); const serviceMsg = document.createElement('div'); @@ -1559,36 +1568,38 @@ export default class ChatBubbles { serviceMsg.append(dateElement); bubbleContent.append(serviceMsg); - div.append(bubbleContent); + bubble.append(bubbleContent); ////////this.log('need to render date message', dateTimestamp, str); const container = document.createElement('div'); container.className = 'bubbles-date-group'; + container.append(bubble); + + this.dateMessages[dateTimestamp] = { + div: bubble, + container, + firstTimestamp: date.getTime() + }; const haveTimestamps = getObjectKeysAndSort(this.dateMessages, 'asc'); - let i = 0; + let i = 0, length = haveTimestamps.length, insertBefore: HTMLElement; // there can be 'first bubble' (e.g. bot description) so can't insert by index for(; i < haveTimestamps.length; ++i) { const t = haveTimestamps[i]; + insertBefore = this.dateMessages[t].container; if(dateTimestamp < t) { break; } } - - this.dateMessages[dateTimestamp] = { - div, - container, - firstTimestamp: date.getTime() - }; - container.append(div); - - positionElementByIndex(container, this.chatInner, i); + if(i === length && insertBefore) { + insertBefore = insertBefore.nextElementSibling as HTMLElement; + } - /* if(reverse) { - this.chatInner.prepend(container); - } else { + if(!insertBefore) { this.chatInner.append(container); - } */ + } else { + this.chatInner.insertBefore(container, insertBefore); + } if(this.stickyIntersector) { this.stickyIntersector.observeStickyHeaderChanges(container); @@ -1676,6 +1687,8 @@ export default class ChatBubbles { this.onAnimateLadder = undefined; this.resolveLadderAnimation = undefined; this.emptyPlaceholderMid = undefined; + + this.scrollingToBubble = undefined; ////console.timeEnd('appImManager cleanup'); } diff --git a/src/components/sidebarLeft/tabs/archivedTab.ts b/src/components/sidebarLeft/tabs/archivedTab.ts index a3aeb799..c8f64e8e 100644 --- a/src/components/sidebarLeft/tabs/archivedTab.ts +++ b/src/components/sidebarLeft/tabs/archivedTab.ts @@ -15,7 +15,7 @@ export default class AppArchivedTab extends SliderSuperTab { this.container.id = 'chats-archived-container'; this.setTitle('ArchivedChats'); - if(!appDialogsManager.chatLists[AppArchivedTab.filterId]) { + if(!appDialogsManager.sortedLists[AppArchivedTab.filterId]) { const chatList = appDialogsManager.createChatList(); appDialogsManager.generateScrollable(chatList, AppArchivedTab.filterId).container.append(chatList); appDialogsManager.setListClickListener(chatList, null, true); @@ -40,7 +40,7 @@ export default class AppArchivedTab extends SliderSuperTab { // вообще, так делать нельзя, но нет времени чтобы переделать главный чатлист на слайд... onOpenAfterTimeout() { - appDialogsManager.chatLists[this.wasFilterId].innerHTML = ''; + appDialogsManager.sortedLists[this.wasFilterId].clear(); } onClose() { @@ -49,7 +49,7 @@ export default class AppArchivedTab extends SliderSuperTab { } onCloseAfterTimeout() { - appDialogsManager.chatLists[AppArchivedTab.filterId].innerHTML = ''; + appDialogsManager.sortedLists[AppArchivedTab.filterId].clear(); return super.onCloseAfterTimeout(); } } diff --git a/src/components/sortedUserList.ts b/src/components/sortedUserList.ts index 41f6d75d..5556c929 100644 --- a/src/components/sortedUserList.ts +++ b/src/components/sortedUserList.ts @@ -8,21 +8,19 @@ import type { LazyLoadQueueIntersector } from "./lazyLoadQueue"; import appDialogsManager, { DialogDom } from "../lib/appManagers/appDialogsManager"; import { getHeavyAnimationPromise } from "../hooks/useHeavyAnimationCheck"; import appUsersManager from "../lib/appManagers/appUsersManager"; -import { insertInDescendSortedArray } from "../helpers/array"; import isInDOM from "../helpers/dom/isInDOM"; import positionElementByIndex from "../helpers/dom/positionElementByIndex"; import replaceContent from "../helpers/dom/replaceContent"; import { safeAssign } from "../helpers/object"; +import { fastRaf } from "../helpers/schedulers"; +import SortedList, { SortedElementBase } from "../helpers/sortedList"; -type SortedUser = { - peerId: number, - status: number, +interface SortedUser extends SortedElementBase { dom: DialogDom -}; -export default class SortedUserList { +} + +export default class SortedUserList extends SortedList { protected static SORT_INTERVAL = 30e3; - protected users: Map; - protected sorted: Array; public list: HTMLUListElement; protected lazyLoadQueue: LazyLoadQueueIntersector; @@ -35,17 +33,53 @@ export default class SortedUserList { rippleEnabled: SortedUserList['rippleEnabled'], new: boolean }> = {}) { + super({ + getIndex: (id) => appUsersManager.getUserStatusForSort(id), + onDelete: (element) => element.dom.listEl.remove(), + onUpdate: (element) => { + const status = appUsersManager.getUserStatusString(element.id); + replaceContent(element.dom.lastMessageSpan, status); + }, + onSort: (element, idx) => positionElementByIndex(element.dom.listEl, this.list, idx), + onElementCreate: (base) => { + const {dom} = appDialogsManager.addDialogNew({ + dialog: base.id, + container: false, + drawStatus: false, + avatarSize: this.avatarSize, + autonomous: true, + meAsSaved: false, + rippleEnabled: this.rippleEnabled, + lazyLoadQueue: this.lazyLoadQueue + }); + + (base as SortedUser).dom = dom; + return base as SortedUser; + }, + updateElementWith: fastRaf, + updateListWith: async(callback) => { + if(!isInDOM(this.list)) { + return callback(false); + } + + await getHeavyAnimationPromise(); + + if(!isInDOM(this.list)) { + return callback(false); + } + + callback(true); + } + }); + safeAssign(this, options); this.list = appDialogsManager.createChatList({new: options.new}); - this.users = new Map(); - this.sorted = []; - let timeout: number; const doTimeout = () => { timeout = window.setTimeout(() => { - this.updateList().then((good) => { + this.updateList((good) => { if(good) { doTimeout(); } @@ -55,83 +89,4 @@ export default class SortedUserList { doTimeout(); } - - public async updateList() { - if(!isInDOM(this.list)) { - return false; - } - - await getHeavyAnimationPromise(); - - if(!isInDOM(this.list)) { - return false; - } - - this.users.forEach(user => { - this.update(user.peerId, true); - }); - - this.sorted.forEach((sortedUser, idx) => { - positionElementByIndex(sortedUser.dom.listEl, this.list, idx); - }); - - return true; - } - - public has(peerId: number) { - return this.users.has(peerId); - } - - public add(peerId: number) { - if(this.has(peerId)) { - return; - } - - const {dom} = appDialogsManager.addDialogNew({ - dialog: peerId, - container: false, - drawStatus: false, - avatarSize: this.avatarSize, - autonomous: true, - meAsSaved: false, - rippleEnabled: this.rippleEnabled, - lazyLoadQueue: this.lazyLoadQueue - }); - - const sortedUser: SortedUser = { - peerId, - status: appUsersManager.getUserStatusForSort(peerId), - dom - }; - - this.users.set(peerId, sortedUser); - this.update(peerId); - } - - public delete(peerId: number) { - const user = this.users.get(peerId); - if(!user) { - return; - } - - user.dom.listEl.remove(); - this.users.delete(peerId); - - const idx = this.sorted.indexOf(user); - if(idx !== -1) { - this.sorted.splice(idx, 1); - } - } - - public update(peerId: number, batch = false) { - const sortedUser = this.users.get(peerId); - sortedUser.status = appUsersManager.getUserStatusForSort(peerId); - const status = appUsersManager.getUserStatusString(peerId); - replaceContent(sortedUser.dom.lastMessageSpan, status); - - const idx = insertInDescendSortedArray(this.sorted, sortedUser, 'status'); - if(!batch) { - positionElementByIndex(sortedUser.dom.listEl, this.list, idx); - } - } } diff --git a/src/components/transition.ts b/src/components/transition.ts index fa1651f6..5d777ba6 100644 --- a/src/components/transition.ts +++ b/src/components/transition.ts @@ -8,8 +8,6 @@ import rootScope from "../lib/rootScope"; import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise"; import { dispatchHeavyAnimationEvent } from "../hooks/useHeavyAnimationCheck"; import whichChild from "../helpers/dom/whichChild"; -import findUpClassName from "../helpers/dom/findUpClassName"; -import { isSafari } from "../helpers/userAgent"; import { cancelEvent } from "../helpers/dom/cancelEvent"; function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) { @@ -33,13 +31,13 @@ function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, t function slideTabs(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) { // Jolly Cobra's // Workaround for scrollable content flickering during animation. - const scrollableContainer = findUpClassName(tabContent, 'scrollable-y'); - if(scrollableContainer && scrollableContainer.style.overflowY !== 'hidden') { - // const scrollBarWidth = scrollableContainer.offsetWidth - scrollableContainer.clientWidth; - scrollableContainer.style.overflowY = 'hidden'; - // scrollableContainer.style.paddingRight = `${scrollBarWidth}px`; - // this.container.classList.add('sliding'); - } + // const scrollableContainer = findUpClassName(tabContent, 'scrollable-y'); + // if(scrollableContainer && scrollableContainer.style.overflowY !== 'hidden') { + // // const scrollBarWidth = scrollableContainer.offsetWidth - scrollableContainer.clientWidth; + // scrollableContainer.style.overflowY = 'hidden'; + // // scrollableContainer.style.paddingRight = `${scrollBarWidth}px`; + // // this.container.classList.add('sliding'); + // } //window.requestAnimationFrame(() => { const width = prevTabContent.getBoundingClientRect().width; @@ -62,22 +60,22 @@ function slideTabs(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight return () => { prevTabContent.style.transform = ''; - if(scrollableContainer) { - // Jolly Cobra's // Workaround for scrollable content flickering during animation. - if(isSafari) { // ! safari doesn't respect sticky header, so it flicks when overflow is changing - scrollableContainer.style.display = 'none'; - } + // if(scrollableContainer) { + // // Jolly Cobra's // Workaround for scrollable content flickering during animation. + // if(isSafari) { // ! safari doesn't respect sticky header, so it flicks when overflow is changing + // scrollableContainer.style.display = 'none'; + // } - scrollableContainer.style.overflowY = ''; + // scrollableContainer.style.overflowY = ''; - if(isSafari) { - void scrollableContainer.offsetLeft; // reflow - scrollableContainer.style.display = ''; - } + // if(isSafari) { + // void scrollableContainer.offsetLeft; // reflow + // scrollableContainer.style.display = ''; + // } - // scrollableContainer.style.paddingRight = '0'; - // this.container.classList.remove('sliding'); - } + // // scrollableContainer.style.paddingRight = '0'; + // // this.container.classList.remove('sliding'); + // } }; } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 3920516a..0dc73339 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -1005,6 +1005,59 @@ export function renderImageWithFadeIn(container: HTMLElement, }); } +// export function renderImageWithFadeIn(container: HTMLElement, +// image: HTMLImageElement, +// url: string, +// needFadeIn: boolean, +// aspecter = container, +// thumbImage?: HTMLImageElement +// ) { +// if(needFadeIn) { +// // image.classList.add('fade-in-new', 'not-yet'); +// image.classList.add('fade-in'); +// } + +// return new Promise((resolve) => { +// /* if(photo._ === 'document') { +// console.error('wrapPhoto: will render document', photo, size, cacheContext); +// return resolve(); +// } */ + +// renderImageFromUrl(image, url, () => { +// sequentialDom.mutateElement(container, () => { +// aspecter.append(image); +// // (needFadeIn ? getHeavyAnimationPromise() : Promise.resolve()).then(() => { + +// // fastRaf(() => { +// resolve(); +// // }); + +// if(needFadeIn) { +// fastRaf(() => { +// /* if(!image.isConnected) { +// alert('aaaa'); +// } */ +// // fastRaf(() => { +// image.classList.remove('not-yet'); +// // }); +// }); + +// image.addEventListener('transitionend', () => { +// sequentialDom.mutate(() => { +// image.classList.remove('fade-in-new'); + +// if(thumbImage) { +// thumbImage.remove(); +// } +// }); +// }, {once: true}); +// } +// // }); +// }); +// }); +// }); +// } + export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, onlyThumb, emoji, width, height, withThumb, loop, loadPromises, needFadeIn}: { doc: MyDocument, div: HTMLElement, diff --git a/src/helpers/array.ts b/src/helpers/array.ts index 2cc5214b..a83cf4b4 100644 --- a/src/helpers/array.ts +++ b/src/helpers/array.ts @@ -38,14 +38,22 @@ export function forEachReverse(array: Array, callback: (value: T, index?: }; export function insertInDescendSortedArray(array: Array, element: T, property: K, pos?: number) { + const sortProperty: number = element[property]; + if(pos === undefined) { pos = array.indexOf(element); if(pos !== -1) { + const prev = array[pos - 1]; + const next = array[pos + 1]; + if((!prev || prev[property] >= sortProperty) && (!next || next[property] <= sortProperty)) { + // console.warn('same pos', pos, sortProperty, prev, next); + return pos; + } + array.splice(pos, 1); } } - const sortProperty: number = element[property]; const len = array.length; if(!len || sortProperty <= array[len - 1][property]) { return array.push(element) - 1; diff --git a/src/helpers/dom/positionElementByIndex.ts b/src/helpers/dom/positionElementByIndex.ts index 74f1506d..89a11aad 100644 --- a/src/helpers/dom/positionElementByIndex.ts +++ b/src/helpers/dom/positionElementByIndex.ts @@ -17,7 +17,9 @@ export default function positionElementByIndex(element: HTMLElement, container: pos += 1; } - if(container.childElementCount > pos) { + if(!pos) { + container.prepend(element); + } else if(container.childElementCount > pos) { container.insertBefore(element, container.children[pos]); } else { container.append(element); diff --git a/src/helpers/schedulers.ts b/src/helpers/schedulers.ts index ce09bb0f..ec861a0d 100644 --- a/src/helpers/schedulers.ts +++ b/src/helpers/schedulers.ts @@ -63,6 +63,27 @@ export function fastRaf(callback: NoneToVoidFunction) { } } +let fastRafConventionalCallbacks: NoneToVoidFunction[] | undefined, processing = false; +export function fastRafConventional(callback: NoneToVoidFunction) { + if(!fastRafConventionalCallbacks) { + fastRafConventionalCallbacks = [callback]; + + requestAnimationFrame(() => { + processing = true; + for(let i = 0; i < fastRafConventionalCallbacks.length; ++i) { + fastRafConventionalCallbacks[i](); + } + + fastRafConventionalCallbacks = undefined; + processing = false; + }); + } else if(processing) { + callback(); + } else { + fastRafConventionalCallbacks.push(callback); + } +} + let rafPromise: Promise; export function fastRafPromise() { if(rafPromise) return rafPromise; diff --git a/src/helpers/sortedList.ts b/src/helpers/sortedList.ts new file mode 100644 index 00000000..557bba96 --- /dev/null +++ b/src/helpers/sortedList.ts @@ -0,0 +1,163 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import { insertInDescendSortedArray } from "./array"; +import { getMiddleware } from "./middleware"; +import { safeAssign } from "./object"; + +export type SortedElementBase = { + id: number, + index: number +}; + +export default class SortedList { + protected elements: Map; + protected sorted: Array; + + protected getIndex: (id: number) => number; + protected onDelete: (element: SortedElement) => void; + protected onUpdate: (element: SortedElement) => void; + protected onSort: (element: SortedElement, idx: number) => void; + protected onElementCreate: (base: SortedElementBase, batch: boolean) => SortedElement; + + protected updateElementWith = (callback: () => void) => callback(); + protected updateListWith = (callback: (canUpdate: boolean | undefined) => void) => callback(true); + + protected middleware = getMiddleware(); + + constructor(options: { + getIndex: SortedList['getIndex'], + onDelete?: SortedList['onDelete'], + onUpdate?: SortedList['onUpdate'], + onSort?: SortedList['onSort'], + onElementCreate: SortedList['onElementCreate'], + + updateElementWith?: SortedList['updateElementWith'], + updateListWith?: SortedList['updateListWith'] + }) { + safeAssign(this, options); + + this.elements = new Map(); + this.sorted = []; + } + + public clear() { + this.middleware.clean(); + this.elements.clear(); + this.sorted.length = 0; + } + + protected _updateList() { + this.elements.forEach(element => { + this.update(element.id, true); + }); + + if(this.onSort) { + this.sorted.forEach((element, idx) => { + this.onSort(element, idx); + }); + } + } + + public updateList(callback: (updated: boolean) => void) { + const middleware = this.middleware.get(); + this.updateListWith((canUpdate) => { + if(!middleware() || (canUpdate !== undefined && !canUpdate)) { + return callback(false); + } + + this._updateList(); + + callback(true); + }); + } + + public has(id: number) { + return this.elements.has(id); + } + + public get(id: number) { + return this.elements.get(id); + } + + public getAll() { + return this.elements; + } + + public add(id: number, batch = false, updateElementWith?: SortedList['updateElementWith'], updateBatch = batch) { + let element = this.get(id); + if(element) { + return element; + } + + const base: SortedElementBase = { + id, + index: 0 + }; + + element = this.onElementCreate(base, batch); + this.elements.set(id, element); + this.update(id, updateBatch, element, updateElementWith); + + return element; + } + + public delete(id: number, noScheduler?: boolean) { + const element = this.elements.get(id); + if(!element) { + return false; + } + + this.elements.delete(id); + + const idx = this.sorted.indexOf(element); + if(idx !== -1) { + this.sorted.splice(idx, 1); + } + + if(this.onDelete) { + if(noScheduler) { + this.onDelete(element); + } else { + const middleware = this.middleware.get(); + this.updateElementWith(() => { + if(!middleware()) { + return; + } + + this.onDelete(element); + }); + } + } + + return true; + } + + public update(id: number, batch = false, element = this.get(id), updateElementWith?: SortedList['updateElementWith']) { + if(!element) { + return; + } + + element.index = this.getIndex(id); + this.onUpdate && this.onUpdate(element); + + const idx = insertInDescendSortedArray(this.sorted, element, 'index'); + if(!batch && this.onSort) { + const middleware = this.middleware.get(); + (updateElementWith || this.updateElementWith)(() => { + if(!middleware()) { + return; + } + + // * в случае пересортировки этого же элемента во время ожидания вызовется вторая такая же. нужно соблюдать последовательность событий + this.onSort(element, idx); + /* if(this.get(id) === element) { + this.onSort(element, this.sorted.indexOf(element)); + } */ + }); + } + } +} diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 4028a2a8..f220612a 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -43,7 +43,7 @@ import replaceContent from "../../helpers/dom/replaceContent"; import ConnectionStatusComponent from "../../components/connectionStatus"; import appChatsManager from "./appChatsManager"; import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl"; -import { fastRafPromise } from "../../helpers/schedulers"; +import { fastRaf, fastRafConventional, fastRafPromise } from "../../helpers/schedulers"; import SortedUserList from "../../components/sortedUserList"; import { isTouchSupported } from "../../helpers/touchSupport"; import handleTabSwipe from "../../helpers/dom/handleTabSwipe"; @@ -52,6 +52,8 @@ import isInDOM from "../../helpers/dom/isInDOM"; import appPhotosManager, { MyPhoto } from "./appPhotosManager"; import { MyDocument } from "./appDocsManager"; import { setSendingStatus } from "../../components/sendingStatus"; +import SortedList, { SortedElementBase } from "../../helpers/sortedList"; +import debounce from "../../helpers/schedulers/debounce"; export type DialogDom = { avatarEl: AvatarElement, @@ -68,14 +70,56 @@ export type DialogDom = { subtitleEl: HTMLElement }; +interface SortedDialog extends SortedElementBase { + dom: DialogDom, + loadPromises?: Promise[] +} + +class SortedDialogList extends SortedList { + constructor(public list: HTMLUListElement, public indexKey: ReturnType) { + super({ + getIndex: (id) => appMessagesManager.getDialogOnly(id)[this.indexKey], + onDelete: (element) => { + element.dom.listEl.remove(); + appDialogsManager.onListLengthChange(); + }, + onSort: (element, idx) => { + const willChangeLength = element.dom.listEl.parentElement !== this.list; + positionElementByIndex(element.dom.listEl, this.list, idx); + + if(willChangeLength) { + appDialogsManager.onListLengthChange(); + } + }, + onElementCreate: (base, batch) => { + const loadPromises: Promise[] = batch ? [] : undefined; + + const {dom} = appDialogsManager.addListDialog({dialog: base.id, loadPromises, isBatch: batch}); + (base as SortedDialog).dom = dom; + + if(loadPromises?.length) { + (base as SortedDialog).loadPromises = loadPromises; + Promise.all(loadPromises).finally(() => { + delete (base as SortedDialog).loadPromises; + }); + } + + return base as SortedDialog; + }, + updateElementWith: fastRafConventional + }); + } + + public clear() { + this.list.innerHTML = ''; + super.clear(); + } +} + //const testScroll = false; //let testTopSlice = 1; export class AppDialogsManager { - private chatList: HTMLUListElement; - - private doms: {[peerId: number]: DialogDom} = {}; - private chatsContainer = document.getElementById('chatlist-container') as HTMLDivElement; private chatsPreloader: HTMLElement; @@ -87,7 +131,8 @@ export class AppDialogsManager { private contextMenu = new DialogsContextMenu(); - public chatLists: {[filterId: number]: HTMLUListElement} = {}; + public sortedList: SortedDialogList; + public sortedLists: {[filterId: number]: SortedDialogList} = {}; public scrollables: {[filterId: number]: Scrollable} = {}; public filterId: number; private folders: {[k in 'menu' | 'container' | 'menuScrollContainer']: HTMLElement} = { @@ -111,7 +156,6 @@ export class AppDialogsManager { //private topOffsetIndex = 0; private sliceTimeout: number; - private reorderDialogsTimeout: number; private lastActiveElements: Set = new Set(); @@ -122,6 +166,8 @@ export class AppDialogsManager { private indexKey: ReturnType; + public onListLengthChange: () => Promise; + constructor() { this.chatsPreloader = putPreloader(null, true); @@ -170,7 +216,7 @@ export class AppDialogsManager { orderIndex: 0 }); - this.chatList = this.chatLists[this.filterId]; + this.sortedList = this.sortedLists[this.filterId]; this.scroll = this.scrollables[this.filterId]; /* if(testScroll) { @@ -212,7 +258,7 @@ export class AppDialogsManager { rootScope.addEventListener('dialog_flush', ({peerId}) => { const dialog = appMessagesManager.getDialogOnly(peerId); if(dialog) { - this.setLastMessage(dialog); + this.setLastMessage(dialog, undefined, undefined, undefined, undefined, undefined, true); this.validateDialogForFilter(dialog); this.setFiltersUnreadCount(); } @@ -313,7 +359,7 @@ export class AppDialogsManager { elements.container.remove(); elements.menu.remove(); - delete this.chatLists[filter.id]; + delete this.sortedLists[filter.id]; delete this.scrollables[filter.id]; delete this.filtersRendered[filter.id]; @@ -328,6 +374,9 @@ export class AppDialogsManager { const filter = appMessagesManager.filtersStorage.getFilter(filterId); const renderedFilter = this.filtersRendered[filterId]; + const sortedList = this.sortedLists[filterId]; + sortedList.indexKey = appMessagesManager.dialogsStorage.getDialogIndexKey(filterId); + positionElementByIndex(renderedFilter.menu, containerToAppend, filter.orderIndex); positionElementByIndex(renderedFilter.container, this.folders.container, filter.orderIndex); }); @@ -393,13 +442,13 @@ export class AppDialogsManager { if(this.filterId === id) return; - this.chatLists[id].innerHTML = ''; + this.sortedLists[id].clear(); this.setFilterId(id); this.onTabChange(); }, () => { - for(const folderId in this.chatLists) { + for(const folderId in this.sortedLists) { if(+folderId !== this.filterId) { - this.chatLists[folderId].innerHTML = ''; + this.sortedLists[folderId].clear(); } } }, undefined, foldersScrollable); @@ -436,6 +485,12 @@ export class AppDialogsManager { setTimeout(() => { lottieLoader.loadLottieWorkers(); }, 200); + + this.onListLengthChange = debounce(this._onListLengthChange, 100, false, true); + } + + public get chatList() { + return this.sortedList.list; } public setFilterId(filterId: number) { @@ -484,7 +539,7 @@ export class AppDialogsManager { } private isDialogMustBeInViewport(dialog: Dialog) { - if(dialog.migratedTo !== undefined) return false; + if(dialog.migratedTo !== undefined || !this.testDialogForFilter(dialog)) return false; //return true; const topOffset = this.getOffsetIndex('top'); const bottomOffset = this.getOffsetIndex('bottom'); @@ -498,69 +553,31 @@ export class AppDialogsManager { } private deleteDialog(peerId: number) { - const dom = this.getDialogDom(peerId); - if(dom) { - dom.listEl.remove(); - delete this.doms[peerId]; - - this.onListLengthChange(); - - return true; - } - - return false; + this.sortedList.delete(peerId); } private updateDialog(dialog: Dialog) { - if(!dialog) { - return; - } - if(this.isDialogMustBeInViewport(dialog)) { - if(!this.doms.hasOwnProperty(dialog.peerId)) { - const ret = this.addListDialog({dialog}); - if(ret) { - const idx = appMessagesManager.getDialogByPeerId(dialog.peerId)[1]; - positionElementByIndex(ret.dom.listEl, this.chatList, idx); - this.onListLengthChange(); - } else { - return; - } - } + this.sortedList.add(dialog.peerId); } else { this.deleteDialog(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(); + const dom = this.getDialogDom(dialog.peerId); + if(dom) { + this.setLastMessage(dialog, undefined, dom, undefined, undefined, undefined, true); + this.sortedList.update(dialog.peerId); } } public onTabChange = () => { - this.doms = {}; this.scroll = this.scrollables[this.filterId]; this.scroll.loadedAll.top = true; this.scroll.loadedAll.bottom = false; this.offsets.top = this.offsets.bottom = 0; this.loadDialogsPromise = undefined; - this.chatList = this.chatLists[this.filterId]; + this.sortedList = this.sortedLists[this.filterId]; this.onChatsScroll(); }; @@ -603,35 +620,35 @@ export class AppDialogsManager { * Удалит неподходящие чаты из списка, но не добавит их(!) */ private validateListForFilter() { - // !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) { - this.deleteDialog(peerId); + const filter = appMessagesManager.filtersStorage.getFilter(this.filterId); + this.sortedList.getAll().forEach((element) => { + const dialog = appMessagesManager.getDialogOnly(element.id); + if(!this.testDialogForFilter(dialog, filter || null)) { + this.deleteDialog(element.id); } - } + }); } /** - * Удалит неподходящие чат из списка, но не добавит его(!) + * Удалит неподходящий чат из списка, но не добавит его(!) */ private validateDialogForFilter(dialog: Dialog, filter?: MyDialogFilter) { - if(this.filterId <= 1 || !this.doms[dialog.peerId]) { + if(!this.getDialogDom(dialog.peerId)) { return; } - if(!filter) { - filter = appMessagesManager.filtersStorage.getFilter(this.filterId); + if(!this.testDialogForFilter(dialog, filter)) { + this.deleteDialog(dialog.peerId); } + } - if(!appMessagesManager.filtersStorage.testDialogForFilter(dialog, filter)) { - this.deleteDialog(dialog.peerId); + public testDialogForFilter(dialog: Dialog, filter = appMessagesManager.filtersStorage.getFilter(this.filterId)) { + if((filter && !appMessagesManager.filtersStorage.testDialogForFilter(dialog, filter)) || + (!filter && this.filterId !== dialog.folder_id)) { + return false; } + + return true; } public generateScrollable(list: HTMLUListElement, filterId: number) { @@ -642,8 +659,13 @@ export class AppDialogsManager { scrollable.onScrolledBottom = this.onChatsScroll; scrollable.setVirtualContainer(list); - this.chatLists[filterId] = list; + const sortedDialogList = new SortedDialogList(list, appMessagesManager.dialogsStorage ? appMessagesManager.dialogsStorage.getDialogIndexKey(filterId) : 'index'); + this.scrollables[filterId] = scrollable; + this.sortedLists[filterId] = sortedDialogList; + + // list.classList.add('hide'); + // scrollable.container.style.backgroundColor = '#' + (Math.random() * (16 ** 6 - 1) | 0).toString(16); return scrollable; } @@ -723,15 +745,12 @@ export class AppDialogsManager { if(this.loadDialogsPromise/* || 1 === 1 */) return this.loadDialogsPromise; const promise = new Promise(async(resolve) => { - if(!this.chatList.childElementCount) { - const container = this.chatList.parentElement; - container.append(this.chatsPreloader); - } - + const {chatList, filterId} = this; + //return; - const filterId = this.filterId; - let loadCount = 30/*this.chatsLoadCount */; + // let loadCount = 30/*this.chatsLoadCount */; + let loadCount = windowSize.windowH / 72 * 1.25 | 0; let offsetIndex = 0; const {index: currentOffsetIndex} = this.getOffsetIndex(side); @@ -753,8 +772,9 @@ export class AppDialogsManager { //console.time('getDialogs time'); const getConversationsResult = appMessagesManager.getConversations('', offsetIndex, loadCount, filterId, true); - if(getConversationsResult.cached) { - this.chatsPreloader.remove(); + if(!getConversationsResult.cached && !chatList.childElementCount) { + const container = chatList.parentElement; + container.append(this.chatsPreloader); } const result = await getConversationsResult.promise; @@ -766,43 +786,37 @@ export class AppDialogsManager { //console.timeEnd('getDialogs time'); // * loaded all - //if(!result.dialogs.length || this.chatList.childElementCount === result.count) { + //if(!result.dialogs.length || chatList.childElementCount === result.count) { // !result.dialogs.length не подходит, так как при супердревном диалоге getConversations его не выдаст. - //if(this.chatList.childElementCount === result.count) { + //if(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][this.indexKey] < offsetIndex)) { - this.scroll.loadedAll[side] = true; - } + } else if(result.isTopEnd) { + this.scroll.loadedAll[side] = true; } if(result.dialogs.length) { const dialogs = side === 'top' ? result.dialogs.slice().reverse() : result.dialogs; - const container = document.createDocumentFragment(); const loadPromises: Promise[] = []; - const append = side === 'bottom'; + + const callbacks: (() => void)[] = []; + const cccc = (callback: () => void) => { + callbacks.push(callback); + }; + dialogs.forEach((dialog) => { - this.addListDialog({ - dialog, - container, - append, - loadPromises, - isBatch: true - }); + const element = this.sortedList.add(dialog.peerId, true, cccc, false); + if(element.loadPromises) { + loadPromises.push(...element.loadPromises); + } }); - if(container.childElementCount) { - if(loadPromises.length) { - await Promise.all(loadPromises).finally(); - } + await Promise.all(loadPromises).finally(); - this.scroll[append ? 'append' : 'prepend'](container); - } + callbacks.forEach(callback => callback()); } const offsetDialog = result.dialogs[side === 'top' ? 0 : result.dialogs.length - 1]; @@ -810,9 +824,7 @@ export class AppDialogsManager { this.offsets[side] = offsetDialog[this.indexKey]; } - this.onListLengthChange(); - - this.log.debug('getDialogs ' + loadCount + ' dialogs by offset:', offsetIndex, result, this.chatList.childElementCount); + this.log.debug('getDialogs ' + loadCount + ' dialogs by offset:', offsetIndex, result, chatList.childElementCount); setTimeout(() => { this.scroll.onScroll(); @@ -821,7 +833,10 @@ export class AppDialogsManager { this.log.error(err); } - this.chatsPreloader.remove(); + if(this.chatsPreloader.parentElement) { + this.chatsPreloader.remove(); + } + resolve(); }).finally(() => { this.loadDialogsPromise = undefined; @@ -860,10 +875,11 @@ export class AppDialogsManager { return; } - const part = this.chatList.parentElement as HTMLElement; + const chatList = this.chatList; + const part = chatList.parentElement as HTMLElement; let placeholderContainer = (Array.from(part.children) as HTMLElement[]).find(el => el.matches('.empty-placeholder')); - const needPlaceholder = this.scroll.loadedAll.bottom && !this.chatList.childElementCount/* || true */; - // this.chatList.style.display = 'none'; + const needPlaceholder = this.scroll.loadedAll.bottom && !chatList.childElementCount/* || true */; + // chatList.style.display = 'none'; if(needPlaceholder && placeholderContainer) { return; @@ -944,15 +960,16 @@ export class AppDialogsManager { part.classList.add('with-placeholder'); } - private onListLengthChange = () => { + public _onListLengthChange = () => { this.checkIfPlaceholderNeeded(); if(this.filterId > 0) return; - const count = this.chatList.childElementCount; + const chatList = this.chatList; + const count = chatList.childElementCount; - const parts = this.chatList.parentElement.parentElement; - const bottom = this.chatList.parentElement.nextElementSibling as HTMLElement; + const parts = chatList.parentElement.parentElement; + const bottom = chatList.parentElement.nextElementSibling as HTMLElement; const hasContacts = !!bottom.childElementCount; if(count >= 10) { if(hasContacts) { @@ -1019,14 +1036,11 @@ export class AppDialogsManager { }; public onChatsRegularScroll = () => { + // return; + if(this.sliceTimeout) clearTimeout(this.sliceTimeout); this.sliceTimeout = window.setTimeout(() => { this.sliceTimeout = undefined; - - if(this.reorderDialogsTimeout) { - this.onChatsRegularScroll(); - return; - } if(!this.chatList.childElementCount || this.processContact) { return; @@ -1040,6 +1054,10 @@ export class AppDialogsManager { observer.observe(el); }); */ + fastRafConventional(() => { + + const perf = performance.now(); + const scrollTopWas = this.scroll.scrollTop; const firstElementChild = this.chatList.firstElementChild; @@ -1115,16 +1133,20 @@ export class AppDialogsManager { //alert('left length:' + children.length); this.scroll.scrollTop = firstElement.offsetTop - elementOverflow; + + this.log('slice time', performance.now() - perf); /* const firstElementRect = firstElement.getBoundingClientRect(); const scrollTop = */ //this.scroll.scrollIntoView(firstElement, false); + }); }, 200); }; private setOffsets() { - const firstDialog = this.getDialogFromElement(this.chatList.firstElementChild as HTMLElement); - const lastDialog = this.getDialogFromElement(this.chatList.lastElementChild as HTMLElement); + const chatList = this.chatList; + const firstDialog = this.getDialogFromElement(chatList.firstElementChild as HTMLElement); + const lastDialog = this.getDialogFromElement(chatList.lastElementChild as HTMLElement); this.offsets.top = firstDialog[this.indexKey]; this.offsets.bottom = lastDialog[this.indexKey]; @@ -1230,71 +1252,14 @@ export class AppDialogsManager { return list; } - private reorderDialogs() { - //const perf = performance.now(); - if(this.reorderDialogsTimeout) { - return; - } - - if(this.loadDialogsPromise) { - this.loadDialogsPromise.then(() => { - this.reorderDialogs(); - }); - - return; - } - - this.reorderDialogsTimeout = window.requestAnimationFrame(() => { - this.reorderDialogsTimeout = 0; - - if(this.loadDialogsPromise) { - this.loadDialogsPromise.then(() => { - this.reorderDialogs(); - }); - - return; - } - - const dialogs = appMessagesManager.dialogsStorage.getFolder(this.filterId); - - const currentOrder = (Array.from(this.chatList.children) as HTMLElement[]).map(el => +el.dataset.peerId); - - const {index} = this.getOffsetIndex('top'); - const pos = dialogs.findIndex(dialog => dialog[this.indexKey] <= index); - - const offset = Math.max(0, pos); - dialogs.forEach((dialog, index) => { - const dom = this.getDialogDom(dialog.peerId); - if(!dom) { - return; - } - - const needIndex = index - offset; - if(needIndex > currentOrder.length) { - this.deleteDialog(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, loadPromises?: Promise[], - isBatch = false + isBatch = false, + setUnread = false ) { ///////console.log('setlastMessage:', lastMessage); if(!dom) { @@ -1408,16 +1373,16 @@ export class AppDialogsManager { replaceContent(dom.lastTimeSpan, formatDateAccordingToTodayNew(new Date(date * 1000))); } else dom.lastTimeSpan.textContent = ''; - if(this.doms[peerId] === dom) { - this.setUnreadMessages(dialog, isBatch); - } else { // means search - dom.listEl.dataset.mid = lastMessage.mid; + if(setUnread !== null) { + if(setUnread) { + this.setUnreadMessages(dialog, dom, isBatch); + } else { // means search + dom.listEl.dataset.mid = lastMessage.mid; + } } } - private setUnreadMessages(dialog: Dialog, isBatch = false) { - const dom = this.getDialogDom(dialog.peerId); - + private setUnreadMessages(dialog: Dialog, dom = this.getDialogDom(dialog.peerId), isBatch = false) { if(dialog.folder_id === 1) { this.accumulateArchivedUnread(); } @@ -1523,7 +1488,9 @@ export class AppDialogsManager { } private getDialogDom(peerId: number) { - return this.doms[peerId]; + // return this.doms[peerId]; + const element = this.sortedList.get(peerId); + return element?.dom; } private getDialog(dialog: Dialog | number): Dialog { @@ -1543,32 +1510,20 @@ export class AppDialogsManager { return dialog; } - private addListDialog(options: Parameters[0] & {isBatch?: boolean}) { + public addListDialog(options: Parameters[0] & {isBatch?: boolean}) { const dialog = this.getDialog(options.dialog); - const filter = appMessagesManager.filtersStorage.getFilter(this.filterId); - if((filter && !appMessagesManager.filtersStorage.testDialogForFilter(dialog, filter)) || - (!filter && this.filterId !== dialog.folder_id)) { - return; - } - - if(!options.container) { - options.container = this.scroll; - } - options.autonomous = false; const ret = this.addDialogNew(options); if(ret) { - this.doms[dialog.peerId] = ret.dom; - const isMuted = appNotificationsManager.isPeerLocalMuted(dialog.peerId, true); if(isMuted) { ret.dom.listEl.classList.add('is-muted'); } - this.setLastMessage(dialog, undefined, undefined, undefined, options.loadPromises, options.isBatch); + this.setLastMessage(dialog, undefined, ret.dom, undefined, options.loadPromises, options.isBatch, true); } return ret; @@ -1745,7 +1700,7 @@ export class AppDialogsManager { } dom.lastMessageSpan.classList.remove('user-typing'); - this.setLastMessage(dialog, null, dom); + this.setLastMessage(dialog, null, dom, undefined, undefined, undefined, null); } } diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index f69bba74..df8b47a1 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -51,7 +51,7 @@ import PeerTitle from "../../components/peerTitle"; import { forEachReverse } from "../../helpers/array"; import htmlToDocumentFragment from "../../helpers/dom/htmlToDocumentFragment"; import htmlToSpan from "../../helpers/dom/htmlToSpan"; -import { REPLIES_PEER_ID, SERVICE_PEER_ID } from "../mtproto/mtproto_config"; +import { MUTE_UNTIL, REPLIES_PEER_ID, SERVICE_PEER_ID } from "../mtproto/mtproto_config"; import formatCallDuration from "../../helpers/formatCallDuration"; import appAvatarsManager from "./appAvatarsManager"; import telegramMeWebManager from "../mtproto/telegramMeWebManager"; @@ -1379,8 +1379,8 @@ export class AppMessagesManager { //if(!options.isGroupedItem) { this.saveMessages([message], {storage, isOutgoing: true}); + this.setDialogTopMessage(message); setTimeout(() => { - this.setDialogTopMessage(message); rootScope.dispatchEvent('history_append', {storage, peerId, mid: messageId}); }, 0); } @@ -3096,7 +3096,7 @@ export class AppMessagesManager { return true; } - public canDeleteMessage(message: any) { + public canDeleteMessage(message: MyMessage) { return message && ( message.peerId > 0 || message.fromId === rootScope.myId @@ -3553,7 +3553,7 @@ export class AppMessagesManager { } } - handleNewMessages = () => { + private handleNewMessages = () => { clearTimeout(this.newMessagesHandleTimeout); this.newMessagesHandleTimeout = 0; @@ -3561,7 +3561,7 @@ export class AppMessagesManager { this.newMessagesToHandle = {}; }; - handleNewDialogs = () => { + private handleNewDialogs = () => { let newMaxSeenId = 0; const obj = this.newDialogsToHandle; for(const peerId in obj) { @@ -3593,8 +3593,9 @@ export class AppMessagesManager { } if(this.newDialogsHandlePromise) return this.newDialogsHandlePromise; - return this.newDialogsHandlePromise = new Promise((resolve) => { + return this.newDialogsHandlePromise = new Promise((resolve) => { setTimeout(() => { + resolve(); this.newDialogsHandlePromise = undefined; this.handleNewDialogs(); }, 0); @@ -4113,7 +4114,7 @@ export class AppMessagesManager { } if((message as Message.message).fwd_from) { - notifyPeerToHandle.fwdCount++; + ++notifyPeerToHandle.fwdCount; } notifyPeerToHandle.topMessage = message; @@ -4322,17 +4323,10 @@ export class AppMessagesManager { }; private onUpdateChannelAvailableMessages = (update: Update.updateChannelAvailableMessages) => { - const channelId: number = update.channel_id; - const messages: number[] = []; - const peerId: number = -channelId; + const peerId: number = -update.channel_id; const history = this.getHistoryStorage(peerId).history.slice; - if(history.length) { - history.forEach((msgId: number) => { - if(!update.available_min_id || msgId <= update.available_min_id) { - messages.push(msgId); - } - }); - } + const availableMinId = appMessagesIdsManager.generateMessageId(update.available_min_id); + const messages = history.filter(mid => mid <= availableMinId); (update as any as Update.updateDeleteChannelMessages).messages = messages; this.onUpdateDeleteMessages(update as any as Update.updateDeleteChannelMessages); @@ -4368,17 +4362,14 @@ export class AppMessagesManager { return this.getHistoryStorage(+splitted[0], +splitted[1]); }); - [this.getHistoryStorage(peerId)].concat(threadsStorages).forEach(historyStorage => { - for(const mid in historyUpdated.msgs) { - historyStorage.history.delete(+mid); + const historyStorage = this.getHistoryStorage(peerId); + [historyStorage].concat(threadsStorages).forEach(historyStorage => { + for(const mid of historyUpdated.msgs) { + historyStorage.history.delete(mid); } - if(historyUpdated.count && - historyStorage.count !== null && - historyStorage.count > 0) { - historyStorage.count -= historyUpdated.count; - if(historyStorage.count < 0) { - historyStorage.count = 0; - } + + if(historyUpdated.count && historyStorage.count) { + historyStorage.count = Math.max(0, historyStorage.count - historyUpdated.count); } }); @@ -4392,12 +4383,21 @@ export class AppMessagesManager { if(historyUpdated.unread) { foundDialog.unread_count -= historyUpdated.unread; + } + if(historyUpdated.unreadMentions || historyUpdated.unread) { rootScope.dispatchEvent('dialog_unread', {peerId}); } if(historyUpdated.msgs.has(foundDialog.top_message)) { - this.reloadConversation(peerId); + const slice = historyStorage.history.first; + if(slice.isEnd(SliceEnd.Bottom) && slice.length) { + const mid = slice[0]; + const message = this.getMessageByPeer(peerId, mid); + this.setDialogTopMessage(message, foundDialog); + } else { + this.reloadConversation(peerId); + } } } }; @@ -4420,7 +4420,7 @@ export class AppMessagesManager { const dialog = this.getDialogOnly(peerId); if(!!dialog !== needDialog) { if(needDialog) { - this.reloadConversation(-channelId); + this.reloadConversation(peerId); } else { if(dialog) { this.dialogsStorage.dropDialog(peerId); @@ -4431,13 +4431,12 @@ export class AppMessagesManager { }; private onUpdateChannelReload = (update: Update.updateChannelReload) => { - const channelId = update.channel_id; - const peerId = -channelId; + const peerId = update.channel_id; this.dialogsStorage.dropDialog(peerId); delete this.historiesStorage[peerId]; - this.reloadConversation(-channelId).then(() => { + this.reloadConversation(peerId).then(() => { rootScope.dispatchEvent('history_reload', peerId); }); }; @@ -4447,7 +4446,7 @@ export class AppMessagesManager { const peerId = -update.channel_id; const mid = appMessagesIdsManager.generateMessageId(update.id); const message: Message.message = this.getMessageByPeer(peerId, mid); - if(!message.deleted && message.views && message.views < views) { + if(!message.deleted && message.views !== undefined && message.views < views) { message.views = views; rootScope.dispatchEvent('message_views', {peerId, mid, views}); } @@ -4458,7 +4457,7 @@ export class AppMessagesManager { const fromId = SERVICE_PEER_ID; const peerId = fromId; const messageId = this.generateTempMessageId(peerId); - const message: any = { + const message: Message.message = { _: 'message', id: messageId, from_id: appPeersManager.getOutputPeer(fromId), @@ -4474,7 +4473,7 @@ export class AppMessagesManager { _: 'user', id: fromId, pFlags: {verified: true}, - access_hash: 0, + access_hash: '0', first_name: 'Telegram', phone: '42777' }]); @@ -4595,7 +4594,7 @@ export class AppMessagesManager { } }; - public setDialogToStateIfMessageIsTop(message: any) { + public setDialogToStateIfMessageIsTop(message: MyMessage) { const dialog = this.getDialogOnly(message.peerId); if(dialog && dialog.top_message === message.mid) { this.dialogsStorage.setDialogToState(dialog); @@ -4642,9 +4641,9 @@ export class AppMessagesManager { return promise; } - private checkPendingMessage(message: any) { + private checkPendingMessage(message: MyMessage) { const randomId = this.pendingByMessageId[message.mid]; - let pendingMessage: any; + let pendingMessage: ReturnType; if(randomId) { const pendingData = this.pendingByRandomId[randomId]; if(pendingMessage = this.finalizePendingMessage(randomId, message)) { @@ -4666,7 +4665,7 @@ export class AppMessagesManager { mute = !appNotificationsManager.isPeerLocalMuted(peerId, false); } - settings.mute_until = mute ? 0x7FFFFFFF : 0; + settings.mute_until = mute ? MUTE_UNTIL : 0; return appNotificationsManager.updateNotifySettings({ _: 'inputNotifyPeer', @@ -4684,7 +4683,7 @@ export class AppMessagesManager { } } - public finalizePendingMessage(randomId: string, finalMessage: any) { + public finalizePendingMessage(randomId: string, finalMessage: MyMessage) { const pendingData = this.pendingByRandomId[randomId]; // this.log('pdata', randomID, pendingData) @@ -4699,7 +4698,7 @@ export class AppMessagesManager { // this.log('pending', randomID, historyStorage.pending) - const message = this.getMessageFromStorage(storage, tempId); + const message: Message.message = this.getMessageFromStorage(storage, tempId); if(!message.deleted) { delete message.pFlags.is_outgoing; delete message.pending; @@ -5382,17 +5381,18 @@ export class AppMessagesManager { message.deleted = true; - if(message._ !== 'messageService' && message.grouped_id) { - const groupedStorage = this.groupedMessagesStorage[message.grouped_id]; + const groupedId = (message as Message.message).grouped_id; + if(groupedId) { + const groupedStorage = this.groupedMessagesStorage[groupedId]; if(groupedStorage) { delete groupedStorage[mid]; if(!history.albums) history.albums = {}; - (history.albums[message.grouped_id] || (history.albums[message.grouped_id] = new Set())).add(mid); + (history.albums[groupedId] || (history.albums[groupedId] = new Set())).add(mid); if(!Object.keys(groupedStorage).length) { delete history.albums; - delete this.groupedMessagesStorage[message.grouped_id]; + delete this.groupedMessagesStorage[groupedId]; } } } diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index d0952d5c..86afb1db 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -344,7 +344,7 @@ export class AppUsersManager { }); } - public saveApiUsers(apiUsers: any[], override?: boolean) { + public saveApiUsers(apiUsers: MTUser[], override?: boolean) { apiUsers.forEach((user) => this.saveApiUser(user, override)); } diff --git a/src/lib/mtproto/mtproto_config.ts b/src/lib/mtproto/mtproto_config.ts index 60f6443b..1cc683e7 100644 --- a/src/lib/mtproto/mtproto_config.ts +++ b/src/lib/mtproto/mtproto_config.ts @@ -11,3 +11,4 @@ export type UserAuth = {dcID: number | string, date: number, id: number}; export const REPLIES_PEER_ID = 1271266957; export const SERVICE_PEER_ID = 777000; +export const MUTE_UNTIL = 0x7FFFFFFF; diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index da616c0e..bc8f0e3d 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -43,6 +43,7 @@ export type BroadcastEvents = { 'dialog_migrate': {migrateFrom: number, migrateTo: number}, //'dialog_top': Dialog, 'dialog_notify_settings': Dialog, + // 'dialog_order': {dialog: Dialog, pos: number}, 'dialogs_multiupdate': {[peerId: string]: Dialog}, 'dialogs_archived_unread': {count: number}, diff --git a/src/lib/storages/dialogs.ts b/src/lib/storages/dialogs.ts index eb5e9e68..180e383b 100644 --- a/src/lib/storages/dialogs.ts +++ b/src/lib/storages/dialogs.ts @@ -27,6 +27,7 @@ import rootScope from "../rootScope"; import { safeReplaceObject } from "../../helpers/object"; import { AppStateManager } from "../appManagers/appStateManager"; import { SliceEnd } from "../../helpers/slicedArray"; +import { MyDialogFilter } from "./filters"; export type FolderDialog = { dialog: Dialog, @@ -84,6 +85,14 @@ export default class DialogsStorage { } }); + // to set new indexes + rootScope.addEventListener('filter_order', () => { + // ! MUST BE REFACTORED ! + for(let id in this.appMessagesManager.filtersStorage.filters) { + this.getFolder(+id, false); + } + }); + rootScope.addMultipleEventsListeners({ updateFolderPeers: this.onUpdateFolderPeers, @@ -186,19 +195,7 @@ export default class DialogsStorage { const indexStr = this.getDialogIndexKey(id); for(const peerId in this.dialogs) { const dialog = this.dialogs[peerId]; - if(this.appMessagesManager.filtersStorage.testDialogForFilter(dialog, filter) && (!skipMigrated || dialog.migratedTo === undefined)) { - let index: number; - - const pinnedIndex = filter.pinned_peers.indexOf(dialog.peerId); - if(pinnedIndex !== -1) { - index = this.generateDialogIndex(this.generateDialogPinnedDateByIndex(filter.pinned_peers.length - 1 - pinnedIndex), true); - } else if(dialog.pFlags?.pinned) { - index = this.generateIndexForDialog(dialog, true); - } else { - index = dialog.index; - } - - dialog[indexStr] = index; + if(this.setDialogIndexInFilter(dialog, indexStr, filter) && (!skipMigrated || dialog.migratedTo === undefined)) { insertInDescendSortedArray(dialogs, dialog, indexStr, -1); } } @@ -209,6 +206,23 @@ export default class DialogsStorage { // return dialogs.map(d => d.dialog); } + private setDialogIndexInFilter(dialog: Dialog, indexKey: ReturnType, filter: MyDialogFilter) { + let index: number; + + if(this.appMessagesManager.filtersStorage.testDialogForFilter(dialog, filter)) { + const pinnedIndex = filter.pinned_peers.indexOf(dialog.peerId); + if(pinnedIndex !== -1) { + index = this.generateDialogIndex(this.generateDialogPinnedDateByIndex(filter.pinned_peers.length - 1 - pinnedIndex), true); + } else if(dialog.pFlags?.pinned) { + index = this.generateIndexForDialog(dialog, true); + } else { + index = dialog.index; + } + } + + return dialog[indexKey] = index; + } + public getDialog(peerId: number, folderId?: number, skipMigrated = true): [Dialog, number] | [] { const folders: Dialog[][] = []; @@ -286,8 +300,17 @@ export default class DialogsStorage { } const index = this.generateDialogIndex(topDate, isPinned); - if(justReturn) return index; + if(justReturn) { + return index; + } + dialog.index = index; + + // ! MUST BE REFACTORED ! + for(let id in this.appMessagesManager.filtersStorage.filters) { + const filter = this.appMessagesManager.filtersStorage.filters[id]; + this.setDialogIndexInFilter(dialog, this.getDialogIndexKey(+id), filter); + } } public generateDialogPinnedDateByIndex(pinnedIndex: number) { @@ -327,11 +350,12 @@ export default class DialogsStorage { public setDialogToState(dialog: Dialog) { const historyStorage = this.appMessagesManager.getHistoryStorage(dialog.peerId); - const history = [].concat(historyStorage.history.slice); + const messagesStorage = this.appMessagesManager.getMessagesStorage(dialog.peerId); + const history = historyStorage.history.slice; let incomingMessage: any; for(let i = 0, length = history.length; i < length; ++i) { const mid = history[i]; - const message = this.appMessagesManager.getMessageByPeer(dialog.peerId, mid); + const message: MyMessage = this.appMessagesManager.getMessageFromStorage(messagesStorage, mid); if(!message.pFlags.is_outgoing) { incomingMessage = message; @@ -339,7 +363,7 @@ export default class DialogsStorage { if(fromId !== dialog.peerId) { this.appStateManager.requestPeer(fromId, 'topMessage_' + dialog.peerId, 1); } - + break; } } @@ -372,24 +396,29 @@ export default class DialogsStorage { if(pos !== -1) { dialogs.splice(pos, 1); } - + //if(!this.dialogs[dialog.peerId]) { this.dialogs[dialog.peerId] = dialog; - + this.setDialogToState(dialog); - //} - + //} + + // let pos: number; if(offsetDate && !dialog.pFlags.pinned && (!this.dialogsOffsetDate[dialog.folder_id] || offsetDate < this.dialogsOffsetDate[dialog.folder_id])) { - if(pos !== -1) { - // So the dialog jumped to the last position + if(pos !== -1) { // So the dialog jumped to the last position + // dialogs.splice(pos, 1); return false; } + this.dialogsOffsetDate[dialog.folder_id] = offsetDate; } - insertInDescendSortedArray(dialogs, dialog, 'index', pos); + /* const newPos = */insertInDescendSortedArray(dialogs, dialog, 'index', pos); + /* if(pos !== -1 && pos !== newPos) { + rootScope.dispatchEvent('dialog_order', {dialog, pos: newPos}); + } */ } public dropDialog(peerId: number): [Dialog, number] | [] { @@ -600,12 +629,13 @@ export default class DialogsStorage { } public getDialogs(query = '', offsetIndex?: number, limit = 20, folderId = 0, skipMigrated = false): { - cached: boolean; + cached: boolean, promise: Promise<{ - dialogs: Dialog[]; - count: number; - isEnd: boolean; - }>; + dialogs: Dialog[], + count: number, + isTopEnd: boolean, + isEnd: boolean + }> } { if(folderId > 1) { const fillContactsResult = this.appUsersManager.fillContacts(); @@ -659,12 +689,14 @@ export default class DialogsStorage { } const loadedAll = this.isDialogsLoaded(realFolderId); - if(query || loadedAll || curDialogStorage.length >= offset + limit) { + if(query || loadedAll || curDialogStorage.length >= (offset + limit)) { + const dialogs = curDialogStorage.slice(offset, offset + limit); return { cached: true, promise: Promise.resolve({ - dialogs: curDialogStorage.slice(offset, offset + limit), + dialogs, count: loadedAll ? curDialogStorage.length : null, + isTopEnd: curDialogStorage.length && ((dialogs[0] && dialogs[0] === curDialogStorage[0]) || curDialogStorage[0][indexStr] < offsetIndex), isEnd: loadedAll && (offset + limit) >= curDialogStorage.length }) }; @@ -689,9 +721,11 @@ export default class DialogsStorage { //this.log.warn(offset, offset + limit, curDialogStorage.dialogs.length, this.dialogs.length); + const dialogs = curDialogStorage.slice(offset, offset + limit); return { - dialogs: curDialogStorage.slice(offset, offset + limit), + dialogs, count: result.count === undefined ? curDialogStorage.length : result.count, + isTopEnd: curDialogStorage.length && ((dialogs[0] && dialogs[0] === curDialogStorage[0]) || curDialogStorage[0][indexStr] < offsetIndex), // isEnd: this.isDialogsLoaded(realFolderId) && (offset + limit) >= curDialogStorage.length isEnd: result.isEnd }; diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index a274757e..f97d438a 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -632,6 +632,10 @@ $bubble-margin: .25rem; > .thumbnail { opacity: .8; + + &.fade-in { + animation: thumbnail-fade-in-opacity .2s ease-in-out forwards; + } } } } diff --git a/src/scss/partials/_document.scss b/src/scss/partials/_document.scss index 700317aa..8ebc4931 100644 --- a/src/scss/partials/_document.scss +++ b/src/scss/partials/_document.scss @@ -12,6 +12,10 @@ .media-photo { border-radius: inherit; + + &.thumbnail { + left: 0; + } } &-ico { diff --git a/src/scss/style.scss b/src/scss/style.scss index 34e641c7..d758430f 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -931,6 +931,15 @@ img.emoji { } } +/* .fade-in-new { + opacity: 1; + transition: opacity .2s ease-in-out; + + &.not-yet { + opacity: 0; + } +} */ + .show-more { padding-top: 13px; padding-bottom: 13px; @@ -1169,12 +1178,6 @@ middle-ellipsis-element { .media-sticker, .media-round, .media-poster { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - @include animation-level(2) { &.fade-in { animation: fade-in-opacity .2s ease-in-out forwards; @@ -1184,14 +1187,14 @@ middle-ellipsis-element { animation: fade-out-opacity .2s ease-in-out forwards; } } -} -.media-photo.thumbnail { - @include animation-level(2) { - &.fade-in { - animation: thumbnail-fade-in-opacity .2s ease-in-out forwards; - } - } + // & ~ & { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + // } } .media-video {