From 7e7eb78a8a4f75c201567de680b52bdfaa954243 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Wed, 27 Oct 2021 22:16:01 +0300 Subject: [PATCH] Better profile avatar loading --- src/components/peerProfileAvatars.ts | 74 +++++++++++++++++++----- src/lib/appManagers/appAvatarsManager.ts | 30 +++++++--- src/scss/partials/_avatar.scss | 10 ++-- src/scss/partials/_profile.scss | 10 +++- 4 files changed, 97 insertions(+), 27 deletions(-) diff --git a/src/components/peerProfileAvatars.ts b/src/components/peerProfileAvatars.ts index 003b6d86..ff40c5c0 100644 --- a/src/components/peerProfileAvatars.ts +++ b/src/components/peerProfileAvatars.ts @@ -1,14 +1,18 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + import PARALLAX_SUPPORTED from "../environment/parallaxSupport"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import { cancelEvent } from "../helpers/dom/cancelEvent"; import { attachClickEvent } from "../helpers/dom/clickEvent"; -import renderImageFromUrl from "../helpers/dom/renderImageFromUrl"; import filterChatPhotosMessages from "../helpers/filterChatPhotosMessages"; import ListLoader from "../helpers/listLoader"; import { fastRaf } from "../helpers/schedulers"; import { Message, ChatFull, MessageAction, Photo } from "../layer"; import appAvatarsManager from "../lib/appManagers/appAvatarsManager"; -import appDownloadManager from "../lib/appManagers/appDownloadManager"; import appMessagesManager, { AppMessagesManager } from "../lib/appManagers/appMessagesManager"; import appPeersManager from "../lib/appManagers/appPeersManager"; import appPhotosManager from "../lib/appManagers/appPhotosManager"; @@ -16,6 +20,9 @@ import appProfileManager from "../lib/appManagers/appProfileManager"; import { openAvatarViewer } from "./avatar"; import Scrollable from "./scrollable"; import SwipeHandler from "./swipeHandler"; +import { wrapPhoto } from "./wrappers"; + +const LOAD_NEAREST = 3; export default class PeerProfileAvatars { private static BASE_CLASS = 'profile-avatars'; @@ -30,6 +37,8 @@ export default class PeerProfileAvatars { private tabs: HTMLDivElement; private listLoader: ListLoader; private peerId: PeerId; + private intersectionObserver: IntersectionObserver; + private loadCallbacks: Map void> = new Map(); constructor(public scrollable: Scrollable) { this.container = document.createElement('div'); @@ -197,6 +206,16 @@ export default class PeerProfileAvatars { }); } }); + + this.intersectionObserver = new IntersectionObserver(entries => { + entries.forEach(entry => { + if(!entry.isIntersecting) { + return; + } + + this.loadNearestToTarget(entry.target); + }); + }); } public setPeer(peerId: PeerId) { @@ -270,6 +289,8 @@ export default class PeerProfileAvatars { const tab = this.tabs.children[id] as HTMLElement; tab.classList.add('active'); + + this.loadNearestToTarget(this.avatars.children[id]); } }); @@ -297,7 +318,7 @@ export default class PeerProfileAvatars { public processItem = (photoId: Photo.photo['id'] | Message.messageService) => { const avatar = document.createElement('div'); - avatar.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar'); + avatar.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar', 'media-container'); let photo: Photo.photo; if(photoId) { @@ -307,20 +328,32 @@ export default class PeerProfileAvatars { } const img = new Image(); - img.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar-image'); + img.classList.add('avatar-photo'); img.draggable = false; - if(photo) { - const size = appPhotosManager.choosePhotoSize(photo, 420, 420, false); - appPhotosManager.preloadPhoto(photo, size).then(() => { - const cacheContext = appDownloadManager.getCacheContext(photo, size.type); - renderImageFromUrl(img, cacheContext.url, () => { - avatar.append(img); + const loadCallback = () => { + if(photo) { + const res = wrapPhoto({ + container: avatar, + photo, + size: appPhotosManager.choosePhotoSize(photo, 420, 420, false), + withoutPreloader: true }); - }); + + [res.images.thumb, res.images.full].filter(Boolean).forEach(img => { + img.classList.add('avatar-photo'); + }); + } else { + const photo = appPeersManager.getPeerPhoto(this.peerId); + appAvatarsManager.putAvatar(avatar, this.peerId, photo, 'photo_big', img); + } + }; + + if(this.avatars.childElementCount <= LOAD_NEAREST) { + loadCallback(); } else { - const photo = appPeersManager.getPeerPhoto(this.peerId); - appAvatarsManager.putAvatar(avatar, this.peerId, photo, 'photo_big', img); + this.intersectionObserver.observe(avatar); + this.loadCallbacks.set(avatar, loadCallback); } this.avatars.append(avatar); @@ -329,4 +362,19 @@ export default class PeerProfileAvatars { return photoId; }; + + private loadNearestToTarget(target: Element) { + const children = Array.from(target.parentElement.children); + const idx = children.indexOf(target); + const slice = children.slice(Math.max(0, idx - LOAD_NEAREST), Math.min(children.length, idx + LOAD_NEAREST)); + + slice.forEach(target => { + const callback = this.loadCallbacks.get(target); + if(callback) { + callback(); + this.loadCallbacks.delete(target); + this.intersectionObserver.unobserve(target); + } + }); + } } diff --git a/src/lib/appManagers/appAvatarsManager.ts b/src/lib/appManagers/appAvatarsManager.ts index 6e266d17..da1b3d2e 100644 --- a/src/lib/appManagers/appAvatarsManager.ts +++ b/src/lib/appManagers/appAvatarsManager.ts @@ -84,11 +84,21 @@ export class AppAvatarsManager { return {cached, loadPromise: getAvatarPromise}; } - public putAvatar(div: HTMLElement, peerId: PeerId, photo: UserProfilePhoto.userProfilePhoto | ChatPhoto.chatPhoto, size: PeerPhotoSize, img = new Image(), onlyThumb = false) { + public putAvatar( + div: HTMLElement, + peerId: PeerId, + photo: UserProfilePhoto.userProfilePhoto | ChatPhoto.chatPhoto, + size: PeerPhotoSize, + img = new Image(), + onlyThumb = false + ) { let {cached, loadPromise} = this.loadAvatar(peerId, photo, size); + img.classList.add('avatar-photo'); + let renderThumbPromise: Promise; let callback: () => void; + let thumbImage: HTMLImageElement; if(cached) { // смотри в misc.ts: renderImageFromUrl callback = () => { @@ -101,12 +111,14 @@ export class AppAvatarsManager { img.classList.add('fade-in'); } - let thumbImage: HTMLImageElement; - if(photo.stripped_thumb) { + if(size === 'photo_big') { // let's load small photo first + const res = this.putAvatar(div, peerId, photo, 'photo_small'); + renderThumbPromise = res.loadPromise; + thumbImage = res.thumbImage; + } else if(photo.stripped_thumb) { thumbImage = new Image(); div.classList.add('avatar-relative'); thumbImage.classList.add('avatar-photo', 'avatar-photo-thumbnail'); - img.classList.add('avatar-photo'); const url = appPhotosManager.getPreviewURLFromBytes(photo.stripped_thumb); renderThumbPromise = renderImageFromUrlPromise(thumbImage, url).then(() => { replaceContent(div, thumbImage); @@ -114,7 +126,7 @@ export class AppAvatarsManager { } callback = () => { - if(photo.stripped_thumb) { + if(thumbImage) { div.append(img); } else { replaceContent(div, img); @@ -140,9 +152,13 @@ export class AppAvatarsManager { const renderPromise = loadPromise .then((url) => renderImageFromUrlPromise(img, url/* , false */)) - .then(() => callback()); + .then(callback); - return {cached, loadPromise: renderThumbPromise || renderPromise}; + return { + cached, + loadPromise: renderThumbPromise || renderPromise, + thumbImage + }; } public s(div: HTMLElement, innerHTML: string, color: string, icon: string) { diff --git a/src/scss/partials/_avatar.scss b/src/scss/partials/_avatar.scss index 39696caa..a6eb2c5a 100644 --- a/src/scss/partials/_avatar.scss +++ b/src/scss/partials/_avatar.scss @@ -212,8 +212,10 @@ avatar-element { } } -.avatar-photo { - position: absolute; - top: 0; - left: 0; +.avatar-relative { + .avatar-photo { + position: absolute; + top: 0; + left: 0; + } } diff --git a/src/scss/partials/_profile.scss b/src/scss/partials/_profile.scss index 746032f7..eeae1628 100644 --- a/src/scss/partials/_profile.scss +++ b/src/scss/partials/_profile.scss @@ -71,8 +71,9 @@ min-height: 100%; display: flex; background-color: #000; + position: relative; - &-image { + .avatar-photo { width: 100%; height: 100%; object-fit: cover; @@ -99,7 +100,8 @@ bottom: .5625rem; pointer-events: none; - .profile-name, .profile-subtitle { + .profile-name, + .profile-subtitle { color: #fff; margin: 0; text-align: left; @@ -322,7 +324,9 @@ } } - &-name, &-subtitle, &-avatar { + &-name, + &-subtitle, + &-avatar { flex: 0 0 auto; } }