From b28a1931dc9b988187a84630c2488d4dc01ed4fb Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Tue, 29 Mar 2022 18:31:31 +0300 Subject: [PATCH] Better chat slicing --- src/components/chat/bubbles.ts | 96 ++++++++++++++-------------- src/components/chat/pinnedMessage.ts | 44 +++++++------ src/helpers/dom/getViewportSlice.ts | 73 +++++++++++++++++++++ src/helpers/dom/getVisibleRect.ts | 10 ++- src/lib/appManagers/appImManager.ts | 3 + webpack.common.js | 4 +- 6 files changed, 156 insertions(+), 74 deletions(-) create mode 100644 src/helpers/dom/getViewportSlice.ts diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 9ba3462d..e2b71a04 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -95,6 +95,7 @@ import getObjectKeysAndSort from "../../helpers/object/getObjectKeysAndSort"; import forEachReverse from "../../helpers/array/forEachReverse"; import formatNumber from "../../helpers/number/formatNumber"; import findAndSplice from "../../helpers/array/findAndSplice"; +import getViewportSlice from "../../helpers/dom/getViewportSlice"; const USE_MEDIA_TAILS = false; const IGNORE_ACTIONS: Set = new Set([ @@ -154,9 +155,6 @@ export default class ChatBubbles { private bubbleGroups: BubbleGroups; private preloader: ProgressivePreloader = null; - - private loadedTopTimes = 0; - private loadedBottomTimes = 0; public messagesQueuePromise: Promise = null; private messagesQueue: {message: any, bubble: HTMLElement, reverse: boolean, promises: Promise[]}[] = []; @@ -207,6 +205,7 @@ export default class ChatBubbles { private hoverBubble: HTMLElement; private hoverReaction: HTMLElement; + private sliceViewportDebounced: () => Promise; // private reactions: Map; @@ -660,6 +659,9 @@ export default class ChatBubbles { } }); + if(!IS_SAFARI) { + this.sliceViewportDebounced = debounce(this.sliceViewport.bind(this), 100, false, true); + } let middleware: ReturnType; useHeavyAnimationCheck(() => { @@ -1737,7 +1739,11 @@ export default class ChatBubbles { } if(this.chat.topbar.pinnedMessage) { - this.chat.topbar.pinnedMessage.setCorrectIndex(this.scrollable.lastScrollDirection); + this.chat.topbar.pinnedMessage.setCorrectIndexThrottled(this.scrollable.lastScrollDirection); + } + + if(this.sliceViewportDebounced) { + this.sliceViewportDebounced(); } this.setStickyDateManually(); @@ -2233,8 +2239,6 @@ export default class ChatBubbles { this.viewsMids.clear(); } - this.loadedTopTimes = this.loadedBottomTimes = 0; - this.middleware.clean(); this.onAnimateLadder = undefined; @@ -3877,9 +3881,13 @@ export default class ChatBubbles { this.log('performHistoryResult: will render some messages:', history.length, this.isHeavyAnimationInProgress, this.messagesQueuePromise); } */ - let scrollSaver: ScrollSaver; + let scrollSaver: ScrollSaver/* , viewportSlice: ReturnType */; this.messagesQueueOnRender = () => { scrollSaver = new ScrollSaver(this.scrollable, reverse); + + const viewportSlice = this.getViewportSlice(); + this.deleteViewportSlice(viewportSlice); + scrollSaver.save(); }; @@ -4370,6 +4378,38 @@ export default class ChatBubbles { return message; } + public getViewportSlice() { + return getViewportSlice({ + overflowElement: this.scrollable.container, + selector: '.bubbles-date-group .bubble:not(.is-date)', + extraSize: Math.max(700, windowSize.height) * 2 + }); + } + + public deleteViewportSlice(slice: ReturnType) { + const {invisibleTop, invisibleBottom} = slice; + const invisible = invisibleTop.concat(invisibleBottom); + + if(invisibleTop.length) this.setLoaded('top', false); + if(invisibleBottom.length) this.setLoaded('bottom', false); + + const mids = invisible.map(({element}) => +element.dataset.mid); + this.deleteMessagesByIds(mids, false); + } + + public sliceViewport() { + if(IS_SAFARI) { + return; + } + + // const scrollSaver = new ScrollSaver(this.scrollable, true); + // scrollSaver.save(); + const slice = this.getViewportSlice(); + // if(IS_SAFARI) slice.invisibleTop = []; + this.deleteViewportSlice(slice); + // scrollSaver.restore(false); + } + private setLoaded(side: SliceSides, value: boolean, checkPlaceholders = true) { const willChange = this.scrollable.loadedAll[side] !== value; if(!willChange) { @@ -4668,48 +4708,6 @@ export default class ChatBubbles { return null; } - /* false && */!isFirstMessageRender && promise.then(() => { - if(reverse) { - this.loadedTopTimes++; - this.loadedBottomTimes = Math.max(0, --this.loadedBottomTimes); - } else { - this.loadedBottomTimes++; - this.loadedTopTimes = Math.max(0, --this.loadedTopTimes); - } - - let ids: number[]; - if((reverse && this.loadedTopTimes > 2) || (!reverse && this.loadedBottomTimes > 2)) { - ids = getObjectKeysAndSort(this.bubbles); - } - - //let removeCount = loadCount / 2; - const safeCount = realLoadCount * 2; // cause i've been runningrunningrunning all day - //this.log('getHistory: slice loadedTimes:', reverse, pageCount, this.loadedTopTimes, this.loadedBottomTimes, ids?.length, safeCount); - if(ids && ids.length > safeCount) { - if(reverse) { - //ids = ids.slice(-removeCount); - //ids = ids.slice(removeCount * 2); - ids = ids.slice(safeCount); - this.setLoaded('bottom', false); - - //this.log('getHistory: slice bottom messages:', ids.length, loadCount); - //this.getHistoryBottomPromise = undefined; // !WARNING, это нужно для обратной загрузки истории, если запрос словил флуд - } else { - //ids = ids.slice(0, removeCount); - //ids = ids.slice(0, ids.length - (removeCount * 2)); - ids = ids.slice(0, ids.length - safeCount); - this.setLoaded('top', false); - - //this.log('getHistory: slice up messages:', ids.length, loadCount); - //this.getHistoryTopPromise = undefined; // !WARNING, это нужно для обратной загрузки истории, если запрос словил флуд - } - - //this.log('getHistory: will slice ids:', ids, reverse); - - this.deleteMessagesByIds(ids, false); - } - }); - promise.then(() => { // preload more //if(!isFirstMessageRender) { diff --git a/src/components/chat/pinnedMessage.ts b/src/components/chat/pinnedMessage.ts index 07e76e10..1cb3c56e 100644 --- a/src/components/chat/pinnedMessage.ts +++ b/src/components/chat/pinnedMessage.ts @@ -21,6 +21,7 @@ import { cancelEvent } from "../../helpers/dom/cancelEvent"; import { attachClickEvent } from "../../helpers/dom/clickEvent"; import handleScrollSideEvent from "../../helpers/dom/handleScrollSideEvent"; import debounce from "../../helpers/schedulers/debounce"; +import throttle from "../../helpers/schedulers/throttle"; class AnimatedSuper { static DURATION = 200; @@ -213,47 +214,49 @@ class AnimatedCounter { } export default class ChatPinnedMessage { - public static LOAD_COUNT = 50; - public static LOAD_OFFSET = 5; + private static LOAD_COUNT = 50; + private static LOAD_OFFSET = 5; public pinnedMessageContainer: PinnedContainer; - public pinnedMessageBorder: PinnedMessageBorder; + private pinnedMessageBorder: PinnedMessageBorder; - public pinnedMaxMid = 0; + private pinnedMaxMid = 0; public pinnedMid = 0; public pinnedIndex = -1; - public wasPinnedIndex = 0; - public wasPinnedMediaIndex = 0; + private wasPinnedIndex = 0; + private wasPinnedMediaIndex = 0; public locked = false; - public waitForScrollBottom = false; + private waitForScrollBottom = false; public count = 0; - public mids: number[] = []; - public offsetIndex = 0; + private mids: number[] = []; + private offsetIndex = 0; - public loading = false; - public loadedBottom = false; - public loadedTop = false; + private loading = false; + private loadedBottom = false; + private loadedTop = false; - public animatedSubtitle: AnimatedSuper; - public animatedMedia: AnimatedSuper; - public animatedCounter: AnimatedCounter; + private animatedSubtitle: AnimatedSuper; + private animatedMedia: AnimatedSuper; + private animatedCounter: AnimatedCounter; - public listenerSetter: ListenerSetter; - public scrollDownListenerSetter: ListenerSetter = null; + private listenerSetter: ListenerSetter; + private scrollDownListenerSetter: ListenerSetter = null; public hidden = false; - public getCurrentIndexPromise: Promise = null; - public btnOpen: HTMLButtonElement; + private getCurrentIndexPromise: Promise = null; + private btnOpen: HTMLButtonElement; - public setPinnedMessage: () => void; + private setPinnedMessage: () => void; private isStatic = false; private debug = false; + public setCorrectIndexThrottled: (lastScrollDirection?: number) => void; + constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) { this.listenerSetter = new ListenerSetter(); @@ -330,6 +333,7 @@ export default class ChatPinnedMessage { // * 200 - no lags // * 100 - need test this.setPinnedMessage = debounce(() => this._setPinnedMessage(), 100, true, true); + this.setCorrectIndexThrottled = throttle(this.setCorrectIndex.bind(this), 100, false); this.isStatic = this.chat.type === 'discussion'; } diff --git a/src/helpers/dom/getViewportSlice.ts b/src/helpers/dom/getViewportSlice.ts new file mode 100644 index 00000000..ef24fc67 --- /dev/null +++ b/src/helpers/dom/getViewportSlice.ts @@ -0,0 +1,73 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import getVisibleRect from "./getVisibleRect"; + +export type ViewportSlicePart = {element: HTMLElement, rect: DOMRect, visibleRect: ReturnType}[]; + +export default function getViewportSlice({overflowElement, selector, extraSize}: { + overflowElement: HTMLElement, + selector: string, + extraSize?: number +}) { + const perf = performance.now(); + const overflowRect = overflowElement.getBoundingClientRect(); + const elements = Array.from(overflowElement.querySelectorAll(selector)); + + const invisibleTop: ViewportSlicePart = [], + visible: typeof invisibleTop = [], + invisibleBottom: typeof invisibleTop = []; + let foundVisible = false; + for(const element of elements) { + const rect = element.getBoundingClientRect(); + const visibleRect = getVisibleRect(element, overflowElement, false, rect, overflowRect); + + const isVisible = !!visibleRect; + let array: typeof invisibleTop; + if(isVisible) { + foundVisible = true; + array = visible; + } else if(foundVisible) { + array = invisibleBottom; + } else { + array = invisibleTop; + } + + array.push({ + element, + rect, + visibleRect + }); + } + + if(extraSize && visible.length) { + const maxTop = visible[0].rect.top; + const minTop = maxTop - extraSize; + const minBottom = visible[visible.length - 1].rect.bottom; + const maxBottom = minBottom + extraSize; + + for(let length = invisibleTop.length, i = length - 1; i >= 0; --i) { + const element = invisibleTop[i]; + if(element.rect.top >= minTop) { + invisibleTop.splice(i, 1); + visible.unshift(element); + } + } + + for(let i = 0, length = invisibleBottom.length; i < length; ++i) { + const element = invisibleBottom[i]; + if(element.rect.bottom <= maxBottom) { + invisibleBottom.splice(i--, 1); + --length; + visible.push(element); + } + } + } + + console.log('getViewportSlice time:', performance.now() - perf); + + return {invisibleTop, visible, invisibleBottom}; +} diff --git a/src/helpers/dom/getVisibleRect.ts b/src/helpers/dom/getVisibleRect.ts index f9042b99..940b56f8 100644 --- a/src/helpers/dom/getVisibleRect.ts +++ b/src/helpers/dom/getVisibleRect.ts @@ -4,9 +4,13 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -export default function getVisibleRect(element: HTMLElement, overflowElement: HTMLElement, lookForSticky?: boolean, rect = element.getBoundingClientRect()) { - const overflowRect = overflowElement.getBoundingClientRect(); - +export default function getVisibleRect( + element: HTMLElement, + overflowElement: HTMLElement, + lookForSticky?: boolean, + rect = element.getBoundingClientRect(), + overflowRect = overflowElement.getBoundingClientRect() +) { let {top: overflowTop, right: overflowRight, bottom: overflowBottom, left: overflowLeft} = overflowRect; // * respect sticky headers diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 078f1909..175c0e1e 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -1135,6 +1135,9 @@ export class AppImManager { //if(bubble) { //const top = bubble.getBoundingClientRect().top; const chatBubbles = chat.bubbles; + + chatBubbles.sliceViewport(); + const top = chatBubbles.scrollable.scrollTop; const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : ''); diff --git a/webpack.common.js b/webpack.common.js index 79ad9325..0de80978 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -12,7 +12,7 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl const allowedIPs = ['127.0.0.1']; const devMode = process.env.NODE_ENV !== 'production'; const useLocal = true; -const useLocalNotLocal = true; +const useLocalNotLocal = false; if(devMode) { console.log('DEVMODE IS ON!'); @@ -37,7 +37,7 @@ const opts = { }; const domain = 'yourdomain.com'; -const localIp = '192.168.100.51'; +const localIp = '192.168.100.167'; const middleware = (req, res, next) => { let IP = '';