Browse Source

Lazy load avatars in channels & members list

master
Eduard Kuzmenko 3 years ago
parent
commit
039e238b2f
  1. 1
      src/components/appSearchSuper..ts
  2. 122
      src/components/avatar.ts
  3. 3
      src/components/chat/bubbles.ts
  4. 7
      src/components/chat/messageRender.ts
  5. 3
      src/components/chat/replies.ts
  6. 5
      src/components/sortedUserList.ts
  7. 2
      src/lib/appManagers/apiUpdatesManager.ts
  8. 9
      src/lib/appManagers/appDialogsManager.ts

1
src/components/appSearchSuper..ts

@ -883,6 +883,7 @@ export default class AppSearchSuper {
if(!this.membersList) { if(!this.membersList) {
this.membersList = new SortedUserList(); this.membersList = new SortedUserList();
this.membersList.lazyLoadQueue = this.lazyLoadQueue;
this.membersList.list.addEventListener('click', (e) => { this.membersList.list.addEventListener('click', (e) => {
const li = findUpTag(e.target, 'LI'); const li = findUpTag(e.target, 'LI');
if(!li) { if(!li) {

122
src/components/avatar.ts

@ -9,10 +9,10 @@ import appProfileManager from "../lib/appManagers/appProfileManager";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import { attachClickEvent, cancelEvent } from "../helpers/dom"; import { attachClickEvent, cancelEvent } from "../helpers/dom";
import AppMediaViewer, { AppMediaViewerAvatar } from "./appMediaViewer"; import AppMediaViewer, { AppMediaViewerAvatar } from "./appMediaViewer";
import { Message, Photo } from "../layer"; import { Message } from "../layer";
import appPeersManager from "../lib/appManagers/appPeersManager"; import appPeersManager from "../lib/appManagers/appPeersManager";
import appPhotosManager from "../lib/appManagers/appPhotosManager"; import appPhotosManager from "../lib/appManagers/appPhotosManager";
//import type { LazyLoadQueueIntersector } from "./lazyLoadQueue"; import type { LazyLoadQueueIntersector } from "./lazyLoadQueue";
const onAvatarUpdate = (peerId: number) => { const onAvatarUpdate = (peerId: number) => {
appProfileManager.removeFromAvatarsCache(peerId); appProfileManager.removeFromAvatarsCache(peerId);
@ -98,24 +98,22 @@ export async function openAvatarViewer(target: HTMLElement, peerId: number, midd
} }
} }
const believeMe: Map<number, Set<AvatarElement>> = new Map();
const seen: Set<number> = new Set();
export default class AvatarElement extends HTMLElement { export default class AvatarElement extends HTMLElement {
private peerId: number; private peerId: number;
private isDialog = false; private isDialog = false;
public peerTitle: string; private peerTitle: string;
public loadPromises: Promise<any>[]; public loadPromises: Promise<any>[];
//public lazyLoadQueue: LazyLoadQueueIntersector; public lazyLoadQueue: LazyLoadQueueIntersector;
//private addedToQueue = false; private addedToQueue = false;
constructor() {
super();
// элемент создан
}
connectedCallback() { connectedCallback() {
// браузер вызывает этот метод при добавлении элемента в документ // браузер вызывает этот метод при добавлении элемента в документ
// (может вызываться много раз, если элемент многократно добавляется/удаляется) // (может вызываться много раз, если элемент многократно добавляется/удаляется)
this.isDialog = !!this.getAttribute('dialog'); this.isDialog = this.getAttribute('dialog') === '1';
if(this.getAttribute('clickable') === '') { if(this.getAttribute('clickable') === '') {
this.setAttribute('clickable', 'set'); this.setAttribute('clickable', 'set');
let loading = false; 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) { if(this.lazyLoadQueue) {
this.lazyLoadQueue.unobserve(this); this.lazyLoadQueue.unobserve(this);
} }
} */ }
static get observedAttributes(): string[] { static get observedAttributes(): string[] {
return ['peer', 'dialog', 'peer-title'/* массив имён атрибутов для отслеживания их изменений */]; return ['peer', 'dialog', 'peer-title'/* массив имён атрибутов для отслеживания их изменений */];
@ -152,36 +158,88 @@ export default class AvatarElement extends HTMLElement {
} }
this.peerId = appPeersManager.getPeerMigratedTo(+newValue) || +newValue; 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(); this.update();
} else if(name === 'peer-title') { } else if(name === 'peer-title') {
this.peerTitle = newValue; this.peerTitle = newValue;
} else if(name === 'dialog') { } else if(name === 'dialog') {
this.isDialog = !!+newValue; this.isDialog = newValue === '1';
} }
} }
public update() { public update() {
/* if(this.lazyLoadQueue) { if(this.lazyLoadQueue) {
if(this.addedToQueue) return; if(!seen.has(this.peerId)) {
this.lazyLoadQueue.push({ if(this.addedToQueue) return;
div: this, this.addedToQueue = true;
load: () => {
return appProfileManager.putPhoto(this, this.peerId, this.isDialog, this.peerTitle).finally(() => { let set = believeMe.get(this.peerId);
this.addedToQueue = false; if(!set) {
}); set = new Set();
believeMe.set(this.peerId, set);
} }
});
this.addedToQueue = true; set.add(this);
} else { */
const res = appProfileManager.putPhoto(this, this.peerId, this.isDialog, this.peerTitle); this.lazyLoadQueue.push({
if(this.loadPromises && res && res.cached) { div: this,
this.loadPromises.push(res.loadPromise); load: () => {
res.loadPromise.finally(() => { seen.add(this.peerId);
this.loadPromises = undefined; 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);

3
src/components/chat/bubbles.ts

@ -2560,7 +2560,8 @@ export default class ChatBubbles {
bubbleContainer, bubbleContainer,
message: messageWithReplies, message: messageWithReplies,
messageDiv, messageDiv,
loadPromises loadPromises,
lazyLoadQueue: this.lazyLoadQueue
}); });
if(isFooter) { if(isFooter) {

7
src/components/chat/messageRender.ts

@ -7,6 +7,7 @@
import { getFullDate } from "../../helpers/date"; import { getFullDate } from "../../helpers/date";
import { formatNumber } from "../../helpers/number"; import { formatNumber } from "../../helpers/number";
import RichTextProcessor from "../../lib/richtextprocessor"; import RichTextProcessor from "../../lib/richtextprocessor";
import { LazyLoadQueueIntersector } from "../lazyLoadQueue";
import { wrapReply } from "../wrappers"; import { wrapReply } from "../wrappers";
import Chat from "./chat"; import Chat from "./chat";
import RepliesElement from "./replies"; import RepliesElement from "./replies";
@ -65,18 +66,20 @@ export namespace MessageRender {
return timeSpan; return timeSpan;
}; };
export const renderReplies = ({bubble, bubbleContainer, message, messageDiv, loadPromises}: { export const renderReplies = ({bubble, bubbleContainer, message, messageDiv, loadPromises, lazyLoadQueue}: {
bubble: HTMLElement, bubble: HTMLElement,
bubbleContainer: HTMLElement, bubbleContainer: HTMLElement,
message: any, message: any,
messageDiv: HTMLElement, messageDiv: HTMLElement,
loadPromises?: Promise<any>[] loadPromises?: Promise<any>[],
lazyLoadQueue?: LazyLoadQueueIntersector
}) => { }) => {
const isFooter = !bubble.classList.contains('sticker') && !bubble.classList.contains('emoji-big') && !bubble.classList.contains('round'); const isFooter = !bubble.classList.contains('sticker') && !bubble.classList.contains('emoji-big') && !bubble.classList.contains('round');
const repliesFooter = new RepliesElement(); const repliesFooter = new RepliesElement();
repliesFooter.message = message; repliesFooter.message = message;
repliesFooter.type = isFooter ? 'footer' : 'beside'; repliesFooter.type = isFooter ? 'footer' : 'beside';
repliesFooter.loadPromises = loadPromises; repliesFooter.loadPromises = loadPromises;
repliesFooter.lazyLoadQueue = lazyLoadQueue;
repliesFooter.init(); repliesFooter.init();
bubbleContainer.prepend(repliesFooter); bubbleContainer.prepend(repliesFooter);
return isFooter; return isFooter;

3
src/components/chat/replies.ts

@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import type { LazyLoadQueueIntersector } from "../lazyLoadQueue";
import { formatNumber } from "../../helpers/number"; import { formatNumber } from "../../helpers/number";
import { Message } from "../../layer"; import { Message } from "../../layer";
import appMessagesManager from "../../lib/appManagers/appMessagesManager"; import appMessagesManager from "../../lib/appManagers/appMessagesManager";
@ -26,6 +27,7 @@ export default class RepliesElement extends HTMLElement {
public message: Message.message; public message: Message.message;
public type: 'footer' | 'beside'; public type: 'footer' | 'beside';
public loadPromises: Promise<any>[]; public loadPromises: Promise<any>[];
public lazyLoadQueue: LazyLoadQueueIntersector;
private updated = false; private updated = false;
@ -69,6 +71,7 @@ export default class RepliesElement extends HTMLElement {
avatarElem = new AvatarElement(); avatarElem = new AvatarElement();
avatarElem.setAttribute('dialog', '0'); avatarElem.setAttribute('dialog', '0');
avatarElem.classList.add('avatar-30'); avatarElem.classList.add('avatar-30');
avatarElem.lazyLoadQueue = this.lazyLoadQueue;
if(this.loadPromises) { if(this.loadPromises) {
avatarElem.loadPromises = this.loadPromises; avatarElem.loadPromises = this.loadPromises;

5
src/components/sortedUserList.ts

@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import type { LazyLoadQueueIntersector } from "./lazyLoadQueue";
import appDialogsManager, { DialogDom } from "../lib/appManagers/appDialogsManager"; import appDialogsManager, { DialogDom } from "../lib/appManagers/appDialogsManager";
import { isInDOM, positionElementByIndex, replaceContent } from "../helpers/dom"; import { isInDOM, positionElementByIndex, replaceContent } from "../helpers/dom";
import { getHeavyAnimationPromise } from "../hooks/useHeavyAnimationCheck"; import { getHeavyAnimationPromise } from "../hooks/useHeavyAnimationCheck";
@ -20,6 +21,7 @@ export default class SortedUserList {
public list: HTMLUListElement; public list: HTMLUListElement;
public users: Map<number, SortedUser>; public users: Map<number, SortedUser>;
public sorted: Array<SortedUser>; public sorted: Array<SortedUser>;
public lazyLoadQueue: LazyLoadQueueIntersector;
constructor() { constructor() {
this.list = appDialogsManager.createChatList(); this.list = appDialogsManager.createChatList();
@ -75,7 +77,8 @@ export default class SortedUserList {
avatarSize: 48, avatarSize: 48,
autonomous: true, autonomous: true,
meAsSaved: false, meAsSaved: false,
rippleEnabled: false rippleEnabled: false,
lazyLoadQueue: this.lazyLoadQueue
}); });
const sortedUser: SortedUser = { const sortedUser: SortedUser = {

2
src/lib/appManagers/apiUpdatesManager.ts

@ -596,7 +596,7 @@ export class ApiUpdatesManager {
} }
public saveUpdate(update: Update) { public saveUpdate(update: Update) {
this.debug && this.log('saveUpdate', update); //this.debug && this.log('saveUpdate', update);
rootScope.dispatchEvent(update._, update as any); rootScope.dispatchEvent(update._, update as any);
} }

9
src/lib/appManagers/appDialogsManager.ts

@ -35,6 +35,7 @@ import appNotificationsManager from "./appNotificationsManager";
import PeerTitle from "../../components/peerTitle"; import PeerTitle from "../../components/peerTitle";
import { i18n } from "../langPack"; import { i18n } from "../langPack";
import findUpTag from "../../helpers/dom/findUpTag"; import findUpTag from "../../helpers/dom/findUpTag";
import { LazyLoadQueueIntersector } from "../../components/lazyLoadQueue";
export type DialogDom = { export type DialogDom = {
avatarEl: AvatarElement, avatarEl: AvatarElement,
@ -1213,12 +1214,13 @@ export class AppDialogsManager {
meAsSaved?: boolean, meAsSaved?: boolean,
append?: boolean, append?: boolean,
avatarSize?: number, 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; let dialog: Dialog;
if(typeof(_dialog) === 'number') { if(typeof(_dialog) === 'number') {
@ -1248,6 +1250,7 @@ export class AppDialogsManager {
} }
const avatarEl = new AvatarElement(); const avatarEl = new AvatarElement();
avatarEl.lazyLoadQueue = lazyLoadQueue;
avatarEl.setAttribute('dialog', meAsSaved ? '1' : '0'); avatarEl.setAttribute('dialog', meAsSaved ? '1' : '0');
avatarEl.setAttribute('peer', '' + peerId); avatarEl.setAttribute('peer', '' + peerId);
avatarEl.classList.add('dialog-avatar', 'avatar-' + avatarSize); avatarEl.classList.add('dialog-avatar', 'avatar-' + avatarSize);

Loading…
Cancel
Save