Browse Source

Fix reading messages in hidden chat

master
Eduard Kuzmenko 3 years ago
parent
commit
b9ccab6735
  1. 113
      src/components/chat/bubbles.ts
  2. 105
      src/helpers/dom/superIntersectionObserver.ts

113
src/components/chat/bubbles.ts

@ -96,6 +96,7 @@ 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"; import getViewportSlice from "../../helpers/dom/getViewportSlice";
import SuperIntersectionObserver from "../../helpers/dom/superIntersectionObserver";
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([
@ -149,7 +150,6 @@ export default class ChatBubbles {
private stickyIntersector: StickyIntersector; private stickyIntersector: StickyIntersector;
private unreadedObserver: IntersectionObserver;
private unreaded: Map<HTMLElement, number> = new Map(); private unreaded: Map<HTMLElement, number> = new Map();
private unreadedSeen: Set<number> = new Set(); private unreadedSeen: Set<number> = new Set();
private readPromise: Promise<void>; private readPromise: Promise<void>;
@ -194,7 +194,6 @@ export default class ChatBubbles {
private resolveLadderAnimation: () => Promise<any>; private resolveLadderAnimation: () => Promise<any>;
private emptyPlaceholderMid: number; private emptyPlaceholderMid: number;
private viewsObserver: IntersectionObserver;
private viewsMids: Set<number> = new Set(); private viewsMids: Set<number> = new Set();
private sendViewCountersDebounced: () => Promise<void>; private sendViewCountersDebounced: () => Promise<void>;
@ -210,6 +209,7 @@ export default class ChatBubbles {
private sliceViewportDebounced: DebounceReturnType<ChatBubbles['sliceViewport']>; private sliceViewportDebounced: DebounceReturnType<ChatBubbles['sliceViewport']>;
private resizeObserver: ResizeObserver; private resizeObserver: ResizeObserver;
private willScrollOnLoad: boolean; private willScrollOnLoad: boolean;
private observer: SuperIntersectionObserver;
// private reactions: Map<number, ReactionsElement>; // private reactions: Map<number, ReactionsElement>;
@ -839,38 +839,23 @@ export default class ChatBubbles {
}); });
}); });
this.unreadedObserver = new IntersectionObserver((entries) => { this.observer = new SuperIntersectionObserver({root: this.scrollable.container});
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.viewsObserver = new IntersectionObserver((entries) => { this.listenerSetter.add(rootScope)('chat_changing', ({to}) => {
entries.forEach(entry => { const freeze = to !== this.chat;
if(entry.isIntersecting) {
const mid = +(entry.target as HTMLElement).dataset.mid;
this.viewsObserver.unobserve(entry.target);
if(mid) { const cb = () => {
this.viewsMids.add(mid); this.observer.toggleObservingNew(freeze);
this.sendViewCountersDebounced(); };
} else {
const {sponsoredMessage} = this; if(!freeze) {
if(sponsoredMessage && sponsoredMessage.random_id) { setTimeout(() => {
delete sponsoredMessage.random_id; cb();
this.chat.apiManager.invokeApiSingle('channels.viewSponsoredMessage', { }, 400);
channel: this.appChatsManager.getChannelInput(this.peerId.toChatId()), } else {
random_id: sponsoredMessage.random_id cb();
}); }
} });
}
}
});
}, {root: this.scrollable.container});
this.sendViewCountersDebounced = debounce(() => { this.sendViewCountersDebounced = debounce(() => {
const mids = [...this.viewsMids]; const mids = [...this.viewsMids];
@ -880,6 +865,35 @@ export default class ChatBubbles {
}, 1000, false, true); }, 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() { private createResizeObserver() {
if(!('ResizeObserver' in window) || this.resizeObserver) { if(!('ResizeObserver' in window) || this.resizeObserver) {
return; return;
@ -1158,7 +1172,7 @@ export default class ChatBubbles {
private onUnreadedInViewport(target: HTMLElement, mid: number) { private onUnreadedInViewport(target: HTMLElement, mid: number) {
this.unreadedSeen.add(mid); this.unreadedSeen.add(mid);
this.unreadedObserver.unobserve(target); this.observer.unobserve(target, this.unreadedObserverCallback);
this.unreaded.delete(target); this.unreaded.delete(target);
this.readUnreaded(); this.readUnreaded();
} }
@ -1912,12 +1926,11 @@ export default class ChatBubbles {
} }
this.bubbleGroups.removeBubble(bubble); this.bubbleGroups.removeBubble(bubble);
if(this.unreadedObserver) { if(this.observer) {
this.unreadedObserver.unobserve(bubble); this.observer.unobserve(bubble, this.unreadedObserverCallback);
this.unreaded.delete(bubble); this.unreaded.delete(bubble);
}
if(this.viewsObserver) { this.observer.unobserve(bubble, this.viewsObserverCallback);
this.viewsObserver.unobserve(bubble);
this.viewsMids.delete(mid); this.viewsMids.delete(mid);
} }
//this.unreaded.findAndSplice(mid => mid === id); //this.unreaded.findAndSplice(mid => mid === id);
@ -2271,13 +2284,11 @@ export default class ChatBubbles {
this.listenerSetter.removeAll(); this.listenerSetter.removeAll();
this.lazyLoadQueue.clear(); this.lazyLoadQueue.clear();
this.unreadedObserver && this.unreadedObserver.disconnect(); this.observer && this.observer.disconnect();
this.viewsObserver && this.viewsObserver.disconnect();
this.stickyIntersector && this.stickyIntersector.disconnect(); this.stickyIntersector && this.stickyIntersector.disconnect();
delete this.lazyLoadQueue; delete this.lazyLoadQueue;
this.unreadedObserver && delete this.unreadedObserver; this.observer && delete this.observer;
this.viewsObserver && delete this.viewsObserver;
this.stickyIntersector && delete this.stickyIntersector; this.stickyIntersector && delete this.stickyIntersector;
} }
@ -2323,15 +2334,13 @@ export default class ChatBubbles {
this.stickyIntersector.disconnect(); this.stickyIntersector.disconnect();
} }
if(this.unreadedObserver) { if(this.observer) {
this.unreadedObserver.disconnect(); this.observer.disconnect();
this.unreaded.clear(); this.unreaded.clear();
this.unreadedSeen.clear(); this.unreadedSeen.clear();
this.readPromise = undefined; this.readPromise = undefined;
}
if(this.viewsObserver) {
this.viewsObserver.disconnect();
this.viewsMids.clear(); this.viewsMids.clear();
} }
@ -2914,13 +2923,13 @@ export default class ChatBubbles {
contentWrapper.appendChild(bubbleContainer); contentWrapper.appendChild(bubbleContainer);
bubble.appendChild(contentWrapper); 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); //this.log('not our message', message, message.pFlags.unread);
const isUnread = message.pFlags.unread || const isUnread = message.pFlags.unread ||
this.appMessagesManager.isMentionUnread(message) || this.appMessagesManager.isMentionUnread(message) ||
(this.historyStorage.readMaxId !== undefined && this.historyStorage.readMaxId < message.mid); (this.historyStorage.readMaxId !== undefined && this.historyStorage.readMaxId < message.mid);
if(isUnread) { if(isUnread) {
this.unreadedObserver.observe(bubble); this.observer.observe(bubble, this.unreadedObserverCallback);
this.unreaded.set(bubble, message.mid); this.unreaded.set(bubble, message.mid);
} }
} }
@ -3107,8 +3116,8 @@ export default class ChatBubbles {
bubble.classList.add('with-beside-button'); bubble.classList.add('with-beside-button');
} }
if(!message.pFlags.is_outgoing && this.viewsObserver) { if(!message.pFlags.is_outgoing && this.observer) {
this.viewsObserver.observe(bubble); this.observer.observe(bubble, this.viewsObserverCallback);
} }
} }
@ -4429,7 +4438,7 @@ export default class ChatBubbles {
text text
}); });
this.viewsObserver.observe(button); this.observer.observe(button, this.viewsObserverCallback);
if(callback) { if(callback) {
attachClickEvent(button, callback); attachClickEvent(button, callback);

105
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<IntersectionTarget, Set<IntersectionCallback>>;
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);
}
}
}
Loading…
Cancel
Save