Better chat slicing
This commit is contained in:
parent
02c91039d0
commit
b28a1931dc
@ -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) {
|
||||||
|
@ -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
Normal file
73
src/helpers/dom/getViewportSlice.ts
Normal file
@ -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};
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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 : '');
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user