Browse Source

Better profile avatar loading

master
morethanwords 3 years ago
parent
commit
7e7eb78a8a
  1. 74
      src/components/peerProfileAvatars.ts
  2. 30
      src/lib/appManagers/appAvatarsManager.ts
  3. 10
      src/scss/partials/_avatar.scss
  4. 10
      src/scss/partials/_profile.scss

74
src/components/peerProfileAvatars.ts

@ -1,14 +1,18 @@ @@ -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"; @@ -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 { @@ -30,6 +37,8 @@ export default class PeerProfileAvatars {
private tabs: HTMLDivElement;
private listLoader: ListLoader<Photo.photo['id'] | Message.messageService, Photo.photo['id'] | Message.messageService>;
private peerId: PeerId;
private intersectionObserver: IntersectionObserver;
private loadCallbacks: Map<Element, () => void> = new Map();
constructor(public scrollable: Scrollable) {
this.container = document.createElement('div');
@ -197,6 +206,16 @@ export default class PeerProfileAvatars { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
}
});
}
}

30
src/lib/appManagers/appAvatarsManager.ts

@ -84,11 +84,21 @@ export class AppAvatarsManager { @@ -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<void>;
let callback: () => void;
let thumbImage: HTMLImageElement;
if(cached) {
// смотри в misc.ts: renderImageFromUrl
callback = () => {
@ -101,12 +111,14 @@ export class AppAvatarsManager { @@ -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 { @@ -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 { @@ -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) {

10
src/scss/partials/_avatar.scss

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

10
src/scss/partials/_profile.scss

@ -71,8 +71,9 @@ @@ -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 @@ @@ -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 @@ @@ -322,7 +324,9 @@
}
}
&-name, &-subtitle, &-avatar {
&-name,
&-subtitle,
&-avatar {
flex: 0 0 auto;
}
}

Loading…
Cancel
Save