Better profile avatar loading

This commit is contained in:
morethanwords 2021-10-27 22:16:01 +03:00
parent 1d388fe62a
commit 7e7eb78a8a
4 changed files with 97 additions and 27 deletions

View File

@ -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 PARALLAX_SUPPORTED from "../environment/parallaxSupport";
import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport";
import { cancelEvent } from "../helpers/dom/cancelEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent";
import { attachClickEvent } from "../helpers/dom/clickEvent"; import { attachClickEvent } from "../helpers/dom/clickEvent";
import renderImageFromUrl from "../helpers/dom/renderImageFromUrl";
import filterChatPhotosMessages from "../helpers/filterChatPhotosMessages"; import filterChatPhotosMessages from "../helpers/filterChatPhotosMessages";
import ListLoader from "../helpers/listLoader"; import ListLoader from "../helpers/listLoader";
import { fastRaf } from "../helpers/schedulers"; import { fastRaf } from "../helpers/schedulers";
import { Message, ChatFull, MessageAction, Photo } from "../layer"; import { Message, ChatFull, MessageAction, Photo } from "../layer";
import appAvatarsManager from "../lib/appManagers/appAvatarsManager"; import appAvatarsManager from "../lib/appManagers/appAvatarsManager";
import appDownloadManager from "../lib/appManagers/appDownloadManager";
import appMessagesManager, { AppMessagesManager } from "../lib/appManagers/appMessagesManager"; import appMessagesManager, { AppMessagesManager } from "../lib/appManagers/appMessagesManager";
import appPeersManager from "../lib/appManagers/appPeersManager"; import appPeersManager from "../lib/appManagers/appPeersManager";
import appPhotosManager from "../lib/appManagers/appPhotosManager"; import appPhotosManager from "../lib/appManagers/appPhotosManager";
@ -16,6 +20,9 @@ import appProfileManager from "../lib/appManagers/appProfileManager";
import { openAvatarViewer } from "./avatar"; import { openAvatarViewer } from "./avatar";
import Scrollable from "./scrollable"; import Scrollable from "./scrollable";
import SwipeHandler from "./swipeHandler"; import SwipeHandler from "./swipeHandler";
import { wrapPhoto } from "./wrappers";
const LOAD_NEAREST = 3;
export default class PeerProfileAvatars { export default class PeerProfileAvatars {
private static BASE_CLASS = 'profile-avatars'; private static BASE_CLASS = 'profile-avatars';
@ -30,6 +37,8 @@ export default class PeerProfileAvatars {
private tabs: HTMLDivElement; private tabs: HTMLDivElement;
private listLoader: ListLoader<Photo.photo['id'] | Message.messageService, Photo.photo['id'] | Message.messageService>; private listLoader: ListLoader<Photo.photo['id'] | Message.messageService, Photo.photo['id'] | Message.messageService>;
private peerId: PeerId; private peerId: PeerId;
private intersectionObserver: IntersectionObserver;
private loadCallbacks: Map<Element, () => void> = new Map();
constructor(public scrollable: Scrollable) { constructor(public scrollable: Scrollable) {
this.container = document.createElement('div'); 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) { public setPeer(peerId: PeerId) {
@ -270,6 +289,8 @@ export default class PeerProfileAvatars {
const tab = this.tabs.children[id] as HTMLElement; const tab = this.tabs.children[id] as HTMLElement;
tab.classList.add('active'); 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) => { public processItem = (photoId: Photo.photo['id'] | Message.messageService) => {
const avatar = document.createElement('div'); 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; let photo: Photo.photo;
if(photoId) { if(photoId) {
@ -307,20 +328,32 @@ export default class PeerProfileAvatars {
} }
const img = new Image(); const img = new Image();
img.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar-image'); img.classList.add('avatar-photo');
img.draggable = false; img.draggable = false;
if(photo) { const loadCallback = () => {
const size = appPhotosManager.choosePhotoSize(photo, 420, 420, false); if(photo) {
appPhotosManager.preloadPhoto(photo, size).then(() => { const res = wrapPhoto({
const cacheContext = appDownloadManager.getCacheContext(photo, size.type); container: avatar,
renderImageFromUrl(img, cacheContext.url, () => { photo,
avatar.append(img); 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 { } else {
const photo = appPeersManager.getPeerPhoto(this.peerId); this.intersectionObserver.observe(avatar);
appAvatarsManager.putAvatar(avatar, this.peerId, photo, 'photo_big', img); this.loadCallbacks.set(avatar, loadCallback);
} }
this.avatars.append(avatar); this.avatars.append(avatar);
@ -329,4 +362,19 @@ export default class PeerProfileAvatars {
return photoId; 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);
}
});
}
} }

View File

@ -84,11 +84,21 @@ export class AppAvatarsManager {
return {cached, loadPromise: getAvatarPromise}; 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); let {cached, loadPromise} = this.loadAvatar(peerId, photo, size);
img.classList.add('avatar-photo');
let renderThumbPromise: Promise<void>; let renderThumbPromise: Promise<void>;
let callback: () => void; let callback: () => void;
let thumbImage: HTMLImageElement;
if(cached) { if(cached) {
// смотри в misc.ts: renderImageFromUrl // смотри в misc.ts: renderImageFromUrl
callback = () => { callback = () => {
@ -101,12 +111,14 @@ export class AppAvatarsManager {
img.classList.add('fade-in'); img.classList.add('fade-in');
} }
let thumbImage: HTMLImageElement; if(size === 'photo_big') { // let's load small photo first
if(photo.stripped_thumb) { const res = this.putAvatar(div, peerId, photo, 'photo_small');
renderThumbPromise = res.loadPromise;
thumbImage = res.thumbImage;
} else if(photo.stripped_thumb) {
thumbImage = new Image(); thumbImage = new Image();
div.classList.add('avatar-relative'); div.classList.add('avatar-relative');
thumbImage.classList.add('avatar-photo', 'avatar-photo-thumbnail'); thumbImage.classList.add('avatar-photo', 'avatar-photo-thumbnail');
img.classList.add('avatar-photo');
const url = appPhotosManager.getPreviewURLFromBytes(photo.stripped_thumb); const url = appPhotosManager.getPreviewURLFromBytes(photo.stripped_thumb);
renderThumbPromise = renderImageFromUrlPromise(thumbImage, url).then(() => { renderThumbPromise = renderImageFromUrlPromise(thumbImage, url).then(() => {
replaceContent(div, thumbImage); replaceContent(div, thumbImage);
@ -114,7 +126,7 @@ export class AppAvatarsManager {
} }
callback = () => { callback = () => {
if(photo.stripped_thumb) { if(thumbImage) {
div.append(img); div.append(img);
} else { } else {
replaceContent(div, img); replaceContent(div, img);
@ -140,9 +152,13 @@ export class AppAvatarsManager {
const renderPromise = loadPromise const renderPromise = loadPromise
.then((url) => renderImageFromUrlPromise(img, url/* , false */)) .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) { public s(div: HTMLElement, innerHTML: string, color: string, icon: string) {

View File

@ -212,8 +212,10 @@ avatar-element {
} }
} }
.avatar-photo { .avatar-relative {
position: absolute; .avatar-photo {
top: 0; position: absolute;
left: 0; top: 0;
left: 0;
}
} }

View File

@ -71,8 +71,9 @@
min-height: 100%; min-height: 100%;
display: flex; display: flex;
background-color: #000; background-color: #000;
position: relative;
&-image { .avatar-photo {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
@ -99,7 +100,8 @@
bottom: .5625rem; bottom: .5625rem;
pointer-events: none; pointer-events: none;
.profile-name, .profile-subtitle { .profile-name,
.profile-subtitle {
color: #fff; color: #fff;
margin: 0; margin: 0;
text-align: left; text-align: left;
@ -322,7 +324,9 @@
} }
} }
&-name, &-subtitle, &-avatar { &-name,
&-subtitle,
&-avatar {
flex: 0 0 auto; flex: 0 0 auto;
} }
} }