From 039e238b2ff7d3abde98c98a08de05ffae7d79a4 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Mon, 3 May 2021 17:41:55 +0400 Subject: [PATCH] Lazy load avatars in channels & members list --- src/components/appSearchSuper..ts | 1 + src/components/avatar.ts | 122 +++++++++++++++++------ src/components/chat/bubbles.ts | 3 +- src/components/chat/messageRender.ts | 7 +- src/components/chat/replies.ts | 3 + src/components/sortedUserList.ts | 5 +- src/lib/appManagers/apiUpdatesManager.ts | 2 +- src/lib/appManagers/appDialogsManager.ts | 9 +- 8 files changed, 112 insertions(+), 40 deletions(-) diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index eeaea68d..715edfe5 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -883,6 +883,7 @@ export default class AppSearchSuper { if(!this.membersList) { this.membersList = new SortedUserList(); + this.membersList.lazyLoadQueue = this.lazyLoadQueue; this.membersList.list.addEventListener('click', (e) => { const li = findUpTag(e.target, 'LI'); if(!li) { diff --git a/src/components/avatar.ts b/src/components/avatar.ts index 842653dd..035c3e3d 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -9,10 +9,10 @@ import appProfileManager from "../lib/appManagers/appProfileManager"; import rootScope from "../lib/rootScope"; import { attachClickEvent, cancelEvent } from "../helpers/dom"; import AppMediaViewer, { AppMediaViewerAvatar } from "./appMediaViewer"; -import { Message, Photo } from "../layer"; +import { Message } from "../layer"; import appPeersManager from "../lib/appManagers/appPeersManager"; import appPhotosManager from "../lib/appManagers/appPhotosManager"; -//import type { LazyLoadQueueIntersector } from "./lazyLoadQueue"; +import type { LazyLoadQueueIntersector } from "./lazyLoadQueue"; const onAvatarUpdate = (peerId: number) => { appProfileManager.removeFromAvatarsCache(peerId); @@ -98,24 +98,22 @@ export async function openAvatarViewer(target: HTMLElement, peerId: number, midd } } +const believeMe: Map> = new Map(); +const seen: Set = new Set(); + export default class AvatarElement extends HTMLElement { private peerId: number; private isDialog = false; - public peerTitle: string; + private peerTitle: string; public loadPromises: Promise[]; - //public lazyLoadQueue: LazyLoadQueueIntersector; - //private addedToQueue = false; - - constructor() { - super(); - // элемент создан - } + public lazyLoadQueue: LazyLoadQueueIntersector; + private addedToQueue = false; connectedCallback() { // браузер вызывает этот метод при добавлении элемента в документ // (может вызываться много раз, если элемент многократно добавляется/удаляется) - this.isDialog = !!this.getAttribute('dialog'); + this.isDialog = this.getAttribute('dialog') === '1'; if(this.getAttribute('clickable') === '') { this.setAttribute('clickable', 'set'); let loading = false; @@ -131,13 +129,21 @@ export default class AvatarElement extends HTMLElement { } } - /* disconnectedCallback() { + disconnectedCallback() { // браузер вызывает этот метод при удалении элемента из документа // (может вызываться много раз, если элемент многократно добавляется/удаляется) + const set = believeMe.get(this.peerId); + if(set && set.has(this)) { + set.delete(this); + if(!set.size) { + believeMe.delete(this.peerId); + } + } + if(this.lazyLoadQueue) { this.lazyLoadQueue.unobserve(this); } - } */ + } static get observedAttributes(): string[] { return ['peer', 'dialog', 'peer-title'/* массив имён атрибутов для отслеживания их изменений */]; @@ -152,36 +158,88 @@ export default class AvatarElement extends HTMLElement { } this.peerId = appPeersManager.getPeerMigratedTo(+newValue) || +newValue; + + const wasPeerId = +oldValue; + if(wasPeerId) { + const set = believeMe.get(wasPeerId); + if(set) { + set.delete(this); + if(!set.size) { + believeMe.delete(wasPeerId); + } + } + } + this.update(); } else if(name === 'peer-title') { this.peerTitle = newValue; } else if(name === 'dialog') { - this.isDialog = !!+newValue; + this.isDialog = newValue === '1'; } } public update() { - /* if(this.lazyLoadQueue) { - if(this.addedToQueue) return; - this.lazyLoadQueue.push({ - div: this, - load: () => { - return appProfileManager.putPhoto(this, this.peerId, this.isDialog, this.peerTitle).finally(() => { - this.addedToQueue = false; - }); + if(this.lazyLoadQueue) { + if(!seen.has(this.peerId)) { + if(this.addedToQueue) return; + this.addedToQueue = true; + + let set = believeMe.get(this.peerId); + if(!set) { + set = new Set(); + believeMe.set(this.peerId, set); } - }); - this.addedToQueue = true; - } else { */ - const res = appProfileManager.putPhoto(this, this.peerId, this.isDialog, this.peerTitle); - if(this.loadPromises && res && res.cached) { - this.loadPromises.push(res.loadPromise); - res.loadPromise.finally(() => { - this.loadPromises = undefined; + + set.add(this); + + this.lazyLoadQueue.push({ + div: this, + load: () => { + seen.add(this.peerId); + return this.update(); + } }); + + return; + } else if(this.addedToQueue) { + this.lazyLoadQueue.unobserve(this); } - //} + } + + seen.add(this.peerId); + + const res = appProfileManager.putPhoto(this, this.peerId, this.isDialog, this.peerTitle); + const promise = res ? res.loadPromise : Promise.resolve(); + if(this.loadPromises) { + if(res && res.cached) { + this.loadPromises.push(promise); + } + + promise.finally(() => { + this.loadPromises = undefined; + }); + } + + if(this.addedToQueue) { + promise.finally(() => { + this.addedToQueue = false; + }); + } + + const set = believeMe.get(this.peerId); + if(set) { + set.delete(this); + const arr = Array.from(set); + believeMe.delete(this.peerId); + + + for(let i = 0, length = arr.length; i < length; ++i) { + arr[i].update(); + } + } + + return promise; } } -customElements.define("avatar-element", AvatarElement); +customElements.define('avatar-element', AvatarElement); diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 6df58ee3..f504c94c 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -2560,7 +2560,8 @@ export default class ChatBubbles { bubbleContainer, message: messageWithReplies, messageDiv, - loadPromises + loadPromises, + lazyLoadQueue: this.lazyLoadQueue }); if(isFooter) { diff --git a/src/components/chat/messageRender.ts b/src/components/chat/messageRender.ts index fb0c2ed6..0abe533f 100644 --- a/src/components/chat/messageRender.ts +++ b/src/components/chat/messageRender.ts @@ -7,6 +7,7 @@ import { getFullDate } from "../../helpers/date"; import { formatNumber } from "../../helpers/number"; import RichTextProcessor from "../../lib/richtextprocessor"; +import { LazyLoadQueueIntersector } from "../lazyLoadQueue"; import { wrapReply } from "../wrappers"; import Chat from "./chat"; import RepliesElement from "./replies"; @@ -65,18 +66,20 @@ export namespace MessageRender { return timeSpan; }; - export const renderReplies = ({bubble, bubbleContainer, message, messageDiv, loadPromises}: { + export const renderReplies = ({bubble, bubbleContainer, message, messageDiv, loadPromises, lazyLoadQueue}: { bubble: HTMLElement, bubbleContainer: HTMLElement, message: any, messageDiv: HTMLElement, - loadPromises?: Promise[] + loadPromises?: Promise[], + lazyLoadQueue?: LazyLoadQueueIntersector }) => { const isFooter = !bubble.classList.contains('sticker') && !bubble.classList.contains('emoji-big') && !bubble.classList.contains('round'); const repliesFooter = new RepliesElement(); repliesFooter.message = message; repliesFooter.type = isFooter ? 'footer' : 'beside'; repliesFooter.loadPromises = loadPromises; + repliesFooter.lazyLoadQueue = lazyLoadQueue; repliesFooter.init(); bubbleContainer.prepend(repliesFooter); return isFooter; diff --git a/src/components/chat/replies.ts b/src/components/chat/replies.ts index 9b20f42a..340e1686 100644 --- a/src/components/chat/replies.ts +++ b/src/components/chat/replies.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import type { LazyLoadQueueIntersector } from "../lazyLoadQueue"; import { formatNumber } from "../../helpers/number"; import { Message } from "../../layer"; import appMessagesManager from "../../lib/appManagers/appMessagesManager"; @@ -26,6 +27,7 @@ export default class RepliesElement extends HTMLElement { public message: Message.message; public type: 'footer' | 'beside'; public loadPromises: Promise[]; + public lazyLoadQueue: LazyLoadQueueIntersector; private updated = false; @@ -69,6 +71,7 @@ export default class RepliesElement extends HTMLElement { avatarElem = new AvatarElement(); avatarElem.setAttribute('dialog', '0'); avatarElem.classList.add('avatar-30'); + avatarElem.lazyLoadQueue = this.lazyLoadQueue; if(this.loadPromises) { avatarElem.loadPromises = this.loadPromises; diff --git a/src/components/sortedUserList.ts b/src/components/sortedUserList.ts index 6161b486..bd67dbe9 100644 --- a/src/components/sortedUserList.ts +++ b/src/components/sortedUserList.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import type { LazyLoadQueueIntersector } from "./lazyLoadQueue"; import appDialogsManager, { DialogDom } from "../lib/appManagers/appDialogsManager"; import { isInDOM, positionElementByIndex, replaceContent } from "../helpers/dom"; import { getHeavyAnimationPromise } from "../hooks/useHeavyAnimationCheck"; @@ -20,6 +21,7 @@ export default class SortedUserList { public list: HTMLUListElement; public users: Map; public sorted: Array; + public lazyLoadQueue: LazyLoadQueueIntersector; constructor() { this.list = appDialogsManager.createChatList(); @@ -75,7 +77,8 @@ export default class SortedUserList { avatarSize: 48, autonomous: true, meAsSaved: false, - rippleEnabled: false + rippleEnabled: false, + lazyLoadQueue: this.lazyLoadQueue }); const sortedUser: SortedUser = { diff --git a/src/lib/appManagers/apiUpdatesManager.ts b/src/lib/appManagers/apiUpdatesManager.ts index 8592787a..bd19ce77 100644 --- a/src/lib/appManagers/apiUpdatesManager.ts +++ b/src/lib/appManagers/apiUpdatesManager.ts @@ -596,7 +596,7 @@ export class ApiUpdatesManager { } public saveUpdate(update: Update) { - this.debug && this.log('saveUpdate', update); + //this.debug && this.log('saveUpdate', update); rootScope.dispatchEvent(update._, update as any); } diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index b64964a4..c59636b5 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -35,6 +35,7 @@ import appNotificationsManager from "./appNotificationsManager"; import PeerTitle from "../../components/peerTitle"; import { i18n } from "../langPack"; import findUpTag from "../../helpers/dom/findUpTag"; +import { LazyLoadQueueIntersector } from "../../components/lazyLoadQueue"; export type DialogDom = { avatarEl: AvatarElement, @@ -1213,12 +1214,13 @@ export class AppDialogsManager { meAsSaved?: boolean, append?: boolean, avatarSize?: number, - autonomous?: boolean + autonomous?: boolean, + lazyLoadQueue?: LazyLoadQueueIntersector, }) { - return this.addDialog(options.dialog, options.container, options.drawStatus, options.rippleEnabled, options.onlyFirstName, options.meAsSaved, options.append, options.avatarSize, options.autonomous); + return this.addDialog(options.dialog, options.container, options.drawStatus, options.rippleEnabled, options.onlyFirstName, options.meAsSaved, options.append, options.avatarSize, options.autonomous, options.lazyLoadQueue); } - public addDialog(_dialog: Dialog | number, container?: HTMLUListElement | Scrollable | false, drawStatus = true, rippleEnabled = true, onlyFirstName = false, meAsSaved = true, append = true, avatarSize = 54, autonomous = !!container) { + public addDialog(_dialog: Dialog | number, container?: HTMLUListElement | Scrollable | false, drawStatus = true, rippleEnabled = true, onlyFirstName = false, meAsSaved = true, append = true, avatarSize = 54, autonomous = !!container, lazyLoadQueue?: LazyLoadQueueIntersector) { let dialog: Dialog; if(typeof(_dialog) === 'number') { @@ -1248,6 +1250,7 @@ export class AppDialogsManager { } const avatarEl = new AvatarElement(); + avatarEl.lazyLoadQueue = lazyLoadQueue; avatarEl.setAttribute('dialog', meAsSaved ? '1' : '0'); avatarEl.setAttribute('peer', '' + peerId); avatarEl.classList.add('dialog-avatar', 'avatar-' + avatarSize);