Browse Source

Better chat slicing

master
Eduard Kuzmenko 3 years ago
parent
commit
b28a1931dc
  1. 96
      src/components/chat/bubbles.ts
  2. 44
      src/components/chat/pinnedMessage.ts
  3. 73
      src/helpers/dom/getViewportSlice.ts
  4. 10
      src/helpers/dom/getVisibleRect.ts
  5. 3
      src/lib/appManagers/appImManager.ts
  6. 4
      webpack.common.js

96
src/components/chat/bubbles.ts

@ -95,6 +95,7 @@ import getObjectKeysAndSort from "../../helpers/object/getObjectKeysAndSort";
import forEachReverse from "../../helpers/array/forEachReverse"; import forEachReverse from "../../helpers/array/forEachReverse";
import formatNumber from "../../helpers/number/formatNumber"; import formatNumber from "../../helpers/number/formatNumber";
import findAndSplice from "../../helpers/array/findAndSplice"; import findAndSplice from "../../helpers/array/findAndSplice";
import getViewportSlice from "../../helpers/dom/getViewportSlice";
const USE_MEDIA_TAILS = false; const USE_MEDIA_TAILS = false;
const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([ const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([
@ -155,9 +156,6 @@ export default class ChatBubbles {
private preloader: ProgressivePreloader = null; private preloader: ProgressivePreloader = null;
private loadedTopTimes = 0;
private loadedBottomTimes = 0;
public messagesQueuePromise: Promise<void> = null; public messagesQueuePromise: Promise<void> = null;
private messagesQueue: {message: any, bubble: HTMLElement, reverse: boolean, promises: Promise<void>[]}[] = []; private messagesQueue: {message: any, bubble: HTMLElement, reverse: boolean, promises: Promise<void>[]}[] = [];
private messagesQueueOnRender: () => void = null; private messagesQueueOnRender: () => void = null;
@ -207,6 +205,7 @@ export default class ChatBubbles {
private hoverBubble: HTMLElement; private hoverBubble: HTMLElement;
private hoverReaction: HTMLElement; private hoverReaction: HTMLElement;
private sliceViewportDebounced: () => Promise<void>;
// private reactions: Map<number, ReactionsElement>; // private reactions: Map<number, ReactionsElement>;
@ -660,6 +659,9 @@ export default class ChatBubbles {
} }
}); });
if(!IS_SAFARI) {
this.sliceViewportDebounced = debounce(this.sliceViewport.bind(this), 100, false, true);
}
let middleware: ReturnType<ChatBubbles['getMiddleware']>; let middleware: ReturnType<ChatBubbles['getMiddleware']>;
useHeavyAnimationCheck(() => { useHeavyAnimationCheck(() => {
@ -1737,7 +1739,11 @@ export default class ChatBubbles {
} }
if(this.chat.topbar.pinnedMessage) { 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(); this.setStickyDateManually();
@ -2233,8 +2239,6 @@ export default class ChatBubbles {
this.viewsMids.clear(); this.viewsMids.clear();
} }
this.loadedTopTimes = this.loadedBottomTimes = 0;
this.middleware.clean(); this.middleware.clean();
this.onAnimateLadder = undefined; this.onAnimateLadder = undefined;
@ -3877,9 +3881,13 @@ export default class ChatBubbles {
this.log('performHistoryResult: will render some messages:', history.length, this.isHeavyAnimationInProgress, this.messagesQueuePromise); this.log('performHistoryResult: will render some messages:', history.length, this.isHeavyAnimationInProgress, this.messagesQueuePromise);
} */ } */
let scrollSaver: ScrollSaver; let scrollSaver: ScrollSaver/* , viewportSlice: ReturnType<ChatBubbles['getViewportSlice']> */;
this.messagesQueueOnRender = () => { this.messagesQueueOnRender = () => {
scrollSaver = new ScrollSaver(this.scrollable, reverse); scrollSaver = new ScrollSaver(this.scrollable, reverse);
const viewportSlice = this.getViewportSlice();
this.deleteViewportSlice(viewportSlice);
scrollSaver.save(); scrollSaver.save();
}; };
@ -4370,6 +4378,38 @@ export default class ChatBubbles {
return message; 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<ChatBubbles['getViewportSlice']>) {
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) { private setLoaded(side: SliceSides, value: boolean, checkPlaceholders = true) {
const willChange = this.scrollable.loadedAll[side] !== value; const willChange = this.scrollable.loadedAll[side] !== value;
if(!willChange) { if(!willChange) {
@ -4668,48 +4708,6 @@ export default class ChatBubbles {
return null; 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(() => { promise.then(() => {
// preload more // preload more
//if(!isFirstMessageRender) { //if(!isFirstMessageRender) {

44
src/components/chat/pinnedMessage.ts

@ -21,6 +21,7 @@ import { cancelEvent } from "../../helpers/dom/cancelEvent";
import { attachClickEvent } from "../../helpers/dom/clickEvent"; import { attachClickEvent } from "../../helpers/dom/clickEvent";
import handleScrollSideEvent from "../../helpers/dom/handleScrollSideEvent"; import handleScrollSideEvent from "../../helpers/dom/handleScrollSideEvent";
import debounce from "../../helpers/schedulers/debounce"; import debounce from "../../helpers/schedulers/debounce";
import throttle from "../../helpers/schedulers/throttle";
class AnimatedSuper { class AnimatedSuper {
static DURATION = 200; static DURATION = 200;
@ -213,47 +214,49 @@ class AnimatedCounter {
} }
export default class ChatPinnedMessage { export default class ChatPinnedMessage {
public static LOAD_COUNT = 50; private static LOAD_COUNT = 50;
public static LOAD_OFFSET = 5; private static LOAD_OFFSET = 5;
public pinnedMessageContainer: PinnedContainer; public pinnedMessageContainer: PinnedContainer;
public pinnedMessageBorder: PinnedMessageBorder; private pinnedMessageBorder: PinnedMessageBorder;
public pinnedMaxMid = 0; private pinnedMaxMid = 0;
public pinnedMid = 0; public pinnedMid = 0;
public pinnedIndex = -1; public pinnedIndex = -1;
public wasPinnedIndex = 0; private wasPinnedIndex = 0;
public wasPinnedMediaIndex = 0; private wasPinnedMediaIndex = 0;
public locked = false; public locked = false;
public waitForScrollBottom = false; private waitForScrollBottom = false;
public count = 0; public count = 0;
public mids: number[] = []; private mids: number[] = [];
public offsetIndex = 0; private offsetIndex = 0;
public loading = false; private loading = false;
public loadedBottom = false; private loadedBottom = false;
public loadedTop = false; private loadedTop = false;
public animatedSubtitle: AnimatedSuper; private animatedSubtitle: AnimatedSuper;
public animatedMedia: AnimatedSuper; private animatedMedia: AnimatedSuper;
public animatedCounter: AnimatedCounter; private animatedCounter: AnimatedCounter;
public listenerSetter: ListenerSetter; private listenerSetter: ListenerSetter;
public scrollDownListenerSetter: ListenerSetter = null; private scrollDownListenerSetter: ListenerSetter = null;
public hidden = false; public hidden = false;
public getCurrentIndexPromise: Promise<any> = null; private getCurrentIndexPromise: Promise<any> = null;
public btnOpen: HTMLButtonElement; private btnOpen: HTMLButtonElement;
public setPinnedMessage: () => void; private setPinnedMessage: () => void;
private isStatic = false; private isStatic = false;
private debug = false; private debug = false;
public setCorrectIndexThrottled: (lastScrollDirection?: number) => void;
constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) { constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) {
this.listenerSetter = new ListenerSetter(); this.listenerSetter = new ListenerSetter();
@ -330,6 +333,7 @@ export default class ChatPinnedMessage {
// * 200 - no lags // * 200 - no lags
// * 100 - need test // * 100 - need test
this.setPinnedMessage = debounce(() => this._setPinnedMessage(), 100, true, true); this.setPinnedMessage = debounce(() => this._setPinnedMessage(), 100, true, true);
this.setCorrectIndexThrottled = throttle(this.setCorrectIndex.bind(this), 100, false);
this.isStatic = this.chat.type === 'discussion'; this.isStatic = this.chat.type === 'discussion';
} }

73
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<typeof getVisibleRect>}[];
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<HTMLElement>(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};
}

10
src/helpers/dom/getVisibleRect.ts

@ -4,9 +4,13 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
export default function getVisibleRect(element: HTMLElement, overflowElement: HTMLElement, lookForSticky?: boolean, rect = element.getBoundingClientRect()) { export default function getVisibleRect(
const overflowRect = overflowElement.getBoundingClientRect(); element: HTMLElement,
overflowElement: HTMLElement,
lookForSticky?: boolean,
rect = element.getBoundingClientRect(),
overflowRect = overflowElement.getBoundingClientRect()
) {
let {top: overflowTop, right: overflowRight, bottom: overflowBottom, left: overflowLeft} = overflowRect; let {top: overflowTop, right: overflowRight, bottom: overflowBottom, left: overflowLeft} = overflowRect;
// * respect sticky headers // * respect sticky headers

3
src/lib/appManagers/appImManager.ts

@ -1135,6 +1135,9 @@ export class AppImManager {
//if(bubble) { //if(bubble) {
//const top = bubble.getBoundingClientRect().top; //const top = bubble.getBoundingClientRect().top;
const chatBubbles = chat.bubbles; const chatBubbles = chat.bubbles;
chatBubbles.sliceViewport();
const top = chatBubbles.scrollable.scrollTop; const top = chatBubbles.scrollable.scrollTop;
const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : ''); const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : '');

4
webpack.common.js

@ -12,7 +12,7 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl
const allowedIPs = ['127.0.0.1']; const allowedIPs = ['127.0.0.1'];
const devMode = process.env.NODE_ENV !== 'production'; const devMode = process.env.NODE_ENV !== 'production';
const useLocal = true; const useLocal = true;
const useLocalNotLocal = true; const useLocalNotLocal = false;
if(devMode) { if(devMode) {
console.log('DEVMODE IS ON!'); console.log('DEVMODE IS ON!');
@ -37,7 +37,7 @@ const opts = {
}; };
const domain = 'yourdomain.com'; const domain = 'yourdomain.com';
const localIp = '192.168.100.51'; const localIp = '192.168.100.167';
const middleware = (req, res, next) => { const middleware = (req, res, next) => {
let IP = ''; let IP = '';

Loading…
Cancel
Save