diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 778485bb..886c4b17 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -96,6 +96,7 @@ import forEachReverse from "../../helpers/array/forEachReverse"; import formatNumber from "../../helpers/number/formatNumber"; import findAndSplice from "../../helpers/array/findAndSplice"; import getViewportSlice from "../../helpers/dom/getViewportSlice"; +import SuperIntersectionObserver from "../../helpers/dom/superIntersectionObserver"; const USE_MEDIA_TAILS = false; const IGNORE_ACTIONS: Set = new Set([ @@ -149,7 +150,6 @@ export default class ChatBubbles { private stickyIntersector: StickyIntersector; - private unreadedObserver: IntersectionObserver; private unreaded: Map = new Map(); private unreadedSeen: Set = new Set(); private readPromise: Promise; @@ -194,7 +194,6 @@ export default class ChatBubbles { private resolveLadderAnimation: () => Promise; private emptyPlaceholderMid: number; - private viewsObserver: IntersectionObserver; private viewsMids: Set = new Set(); private sendViewCountersDebounced: () => Promise; @@ -210,6 +209,7 @@ export default class ChatBubbles { private sliceViewportDebounced: DebounceReturnType; private resizeObserver: ResizeObserver; private willScrollOnLoad: boolean; + private observer: SuperIntersectionObserver; // private reactions: Map; @@ -839,38 +839,23 @@ export default class ChatBubbles { }); }); - this.unreadedObserver = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if(entry.isIntersecting) { - const target = entry.target as HTMLElement; - const mid = this.unreaded.get(target as HTMLElement); - this.onUnreadedInViewport(target, mid); - } - }); - }, {root: this.scrollable.container}); + this.observer = new SuperIntersectionObserver({root: this.scrollable.container}); - this.viewsObserver = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if(entry.isIntersecting) { - const mid = +(entry.target as HTMLElement).dataset.mid; - this.viewsObserver.unobserve(entry.target); + this.listenerSetter.add(rootScope)('chat_changing', ({to}) => { + const freeze = to !== this.chat; - if(mid) { - this.viewsMids.add(mid); - this.sendViewCountersDebounced(); - } else { - const {sponsoredMessage} = this; - if(sponsoredMessage && sponsoredMessage.random_id) { - delete sponsoredMessage.random_id; - this.chat.apiManager.invokeApiSingle('channels.viewSponsoredMessage', { - channel: this.appChatsManager.getChannelInput(this.peerId.toChatId()), - random_id: sponsoredMessage.random_id - }); - } - } - } - }); - }, {root: this.scrollable.container}); + const cb = () => { + this.observer.toggleObservingNew(freeze); + }; + + if(!freeze) { + setTimeout(() => { + cb(); + }, 400); + } else { + cb(); + } + }); this.sendViewCountersDebounced = debounce(() => { const mids = [...this.viewsMids]; @@ -880,6 +865,35 @@ export default class ChatBubbles { }, 1000, false, true); } + private unreadedObserverCallback = (entry: IntersectionObserverEntry) => { + if(entry.isIntersecting) { + const target = entry.target as HTMLElement; + const mid = this.unreaded.get(target as HTMLElement); + this.onUnreadedInViewport(target, mid); + } + }; + + private viewsObserverCallback = (entry: IntersectionObserverEntry) => { + if(entry.isIntersecting) { + const mid = +(entry.target as HTMLElement).dataset.mid; + this.observer.unobserve(entry.target, this.viewsObserverCallback); + + if(mid) { + this.viewsMids.add(mid); + this.sendViewCountersDebounced(); + } else { + const {sponsoredMessage} = this; + if(sponsoredMessage && sponsoredMessage.random_id) { + delete sponsoredMessage.random_id; + this.chat.apiManager.invokeApiSingle('channels.viewSponsoredMessage', { + channel: this.appChatsManager.getChannelInput(this.peerId.toChatId()), + random_id: sponsoredMessage.random_id + }); + } + } + } + }; + private createResizeObserver() { if(!('ResizeObserver' in window) || this.resizeObserver) { return; @@ -1158,7 +1172,7 @@ export default class ChatBubbles { private onUnreadedInViewport(target: HTMLElement, mid: number) { this.unreadedSeen.add(mid); - this.unreadedObserver.unobserve(target); + this.observer.unobserve(target, this.unreadedObserverCallback); this.unreaded.delete(target); this.readUnreaded(); } @@ -1912,12 +1926,11 @@ export default class ChatBubbles { } this.bubbleGroups.removeBubble(bubble); - if(this.unreadedObserver) { - this.unreadedObserver.unobserve(bubble); + if(this.observer) { + this.observer.unobserve(bubble, this.unreadedObserverCallback); this.unreaded.delete(bubble); - } - if(this.viewsObserver) { - this.viewsObserver.unobserve(bubble); + + this.observer.unobserve(bubble, this.viewsObserverCallback); this.viewsMids.delete(mid); } //this.unreaded.findAndSplice(mid => mid === id); @@ -2271,13 +2284,11 @@ export default class ChatBubbles { this.listenerSetter.removeAll(); this.lazyLoadQueue.clear(); - this.unreadedObserver && this.unreadedObserver.disconnect(); - this.viewsObserver && this.viewsObserver.disconnect(); + this.observer && this.observer.disconnect(); this.stickyIntersector && this.stickyIntersector.disconnect(); delete this.lazyLoadQueue; - this.unreadedObserver && delete this.unreadedObserver; - this.viewsObserver && delete this.viewsObserver; + this.observer && delete this.observer; this.stickyIntersector && delete this.stickyIntersector; } @@ -2323,15 +2334,13 @@ export default class ChatBubbles { this.stickyIntersector.disconnect(); } - if(this.unreadedObserver) { - this.unreadedObserver.disconnect(); + if(this.observer) { + this.observer.disconnect(); + this.unreaded.clear(); this.unreadedSeen.clear(); this.readPromise = undefined; - } - if(this.viewsObserver) { - this.viewsObserver.disconnect(); this.viewsMids.clear(); } @@ -2914,13 +2923,13 @@ export default class ChatBubbles { contentWrapper.appendChild(bubbleContainer); bubble.appendChild(contentWrapper); - if(!our && !message.pFlags.out && this.unreadedObserver) { + if(!our && !message.pFlags.out && this.observer) { //this.log('not our message', message, message.pFlags.unread); const isUnread = message.pFlags.unread || this.appMessagesManager.isMentionUnread(message) || (this.historyStorage.readMaxId !== undefined && this.historyStorage.readMaxId < message.mid); if(isUnread) { - this.unreadedObserver.observe(bubble); + this.observer.observe(bubble, this.unreadedObserverCallback); this.unreaded.set(bubble, message.mid); } } @@ -3107,8 +3116,8 @@ export default class ChatBubbles { bubble.classList.add('with-beside-button'); } - if(!message.pFlags.is_outgoing && this.viewsObserver) { - this.viewsObserver.observe(bubble); + if(!message.pFlags.is_outgoing && this.observer) { + this.observer.observe(bubble, this.viewsObserverCallback); } } @@ -4429,7 +4438,7 @@ export default class ChatBubbles { text }); - this.viewsObserver.observe(button); + this.observer.observe(button, this.viewsObserverCallback); if(callback) { attachClickEvent(button, callback); diff --git a/src/helpers/dom/superIntersectionObserver.ts b/src/helpers/dom/superIntersectionObserver.ts new file mode 100644 index 00000000..7ba15e40 --- /dev/null +++ b/src/helpers/dom/superIntersectionObserver.ts @@ -0,0 +1,105 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +export type IntersectionTarget = Element; +export type IntersectionCallback = (entry: IntersectionObserverEntry) => void; + +export default class SuperIntersectionObserver { + private observing: Map>; + private observingQueue: SuperIntersectionObserver['observing']; + private observer: IntersectionObserver; + private freezedObservingNew: boolean; + + constructor(init?: IntersectionObserverInit) { + this.observing = new Map(); + this.observingQueue = new Map(); + this.freezedObservingNew = false; + + this.observer = new IntersectionObserver((entries) => { + const observing = this.observing; + for(let i = 0, length = entries.length; i < length; ++i) { + const entry = entries[i]; + const callbacks = observing.get(entry.target); + if(!callbacks) { + debugger; + } + + for(const callback of callbacks) { + try { + callback(entry); + } catch(err) { + console.error('intersection process callback error:', err); + } + } + } + }, init); + } + + public disconnect() { + this.observer.disconnect(); + } + + public toggleObservingNew(value: boolean) { + if(this.freezedObservingNew === value) { + return; + } + + this.freezedObservingNew = value; + + const queue = this.observingQueue; + if(!value && queue.size) { + for(const [target, callbacks] of queue) { + for(const callback of callbacks) { + this.observe(target, callback); + } + } + + queue.clear(); + } + } + + public has(target: IntersectionTarget, callback: IntersectionCallback, observing = this.observing) { + const callbacks = observing.get(target); + return !!(callbacks && callbacks.has(callback)); + } + + public observe(target: IntersectionTarget, callback: IntersectionCallback) { + if(this.freezedObservingNew && this.has(target, callback)) { + return; + } + + const observing = this.freezedObservingNew ? this.observingQueue : this.observing; + let callbacks = observing.get(target); + if(callbacks && callbacks.has(callback)) { + return; + } + + if(!callbacks) { + callbacks = new Set(); + observing.set(target, callbacks); + + if(observing === this.observing) { + this.observer.observe(target); + } + } + + callbacks.add(callback); + } + + public unobserve(target: IntersectionTarget, callback: IntersectionCallback) { + const observing = this.freezedObservingNew && !this.has(target, callback) ? this.observingQueue : this.observing; + const callbacks = observing.get(target); + if(!callbacks) { + return; + } + + callbacks.delete(callback); + if(!callbacks.size) { + observing.delete(target); + this.observer.unobserve(target); + } + } +}