Profile Photo: Progressive loading, fix flickering (#2116)
This commit is contained in:
parent
b3fe234a71
commit
dd3a76f736
@ -185,7 +185,6 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||
chat={chat}
|
||||
photo={photo}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isFirstPhoto={isFirst}
|
||||
canPlayVideo={Boolean(isActive && canPlayVideo)}
|
||||
onClick={handleProfilePhotoClick}
|
||||
/>
|
||||
|
||||
@ -37,4 +37,20 @@
|
||||
&.saved-messages {
|
||||
font-size: 20rem;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.thumb, .avatar-media {
|
||||
// @optimization
|
||||
&:not(.shown) {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ import React, { memo, useEffect, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
|
||||
import { IS_CANVAS_FILTER_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import {
|
||||
getChatAvatarHash,
|
||||
getChatTitle,
|
||||
@ -18,6 +18,9 @@ import buildClassName from '../../util/buildClassName';
|
||||
import { getFirstLetters } from '../../util/textFormat';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||
|
||||
import Spinner from '../ui/Spinner';
|
||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||
@ -27,7 +30,6 @@ import './ProfilePhoto.scss';
|
||||
type OwnProps = {
|
||||
chat?: ApiChat;
|
||||
user?: ApiUser;
|
||||
isFirstPhoto?: boolean;
|
||||
isSavedMessages?: boolean;
|
||||
photo?: ApiPhoto;
|
||||
lastSyncTime?: number;
|
||||
@ -39,7 +41,6 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
chat,
|
||||
user,
|
||||
photo,
|
||||
isFirstPhoto,
|
||||
isSavedMessages,
|
||||
canPlayVideo,
|
||||
lastSyncTime,
|
||||
@ -49,32 +50,32 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const isDeleted = user && isDeletedUser(user);
|
||||
const isRepliesChat = chat && isChatWithRepliesBot(chat.id);
|
||||
const userOrChat = user || chat;
|
||||
const currentPhoto = photo || userOrChat?.fullInfo?.profilePhoto;
|
||||
const canHaveMedia = userOrChat && !isSavedMessages && !isDeleted && !isRepliesChat;
|
||||
const { isVideo } = currentPhoto || {};
|
||||
|
||||
function getMediaHash(size: 'normal' | 'big', type: 'photo' | 'video' = 'photo') {
|
||||
const userOrChat = user || chat;
|
||||
const profilePhoto = photo || userOrChat?.fullInfo?.profilePhoto;
|
||||
const hasVideo = profilePhoto?.isVideo;
|
||||
const forceAvatar = isFirstPhoto;
|
||||
const avatarHash = canHaveMedia && getChatAvatarHash(userOrChat, 'normal', 'photo');
|
||||
const avatarBlobUrl = useMedia(avatarHash, undefined, undefined, lastSyncTime);
|
||||
|
||||
if (type === 'video' && !hasVideo) return undefined;
|
||||
const photoHash = canHaveMedia && currentPhoto && !isVideo && `photo${currentPhoto.id}?size=c`;
|
||||
const photoBlobUrl = useMedia(photoHash, undefined, undefined, lastSyncTime);
|
||||
|
||||
if (photo && !forceAvatar) {
|
||||
if (hasVideo && type === 'video') {
|
||||
return getVideoAvatarMediaHash(photo);
|
||||
}
|
||||
if (type === 'photo') {
|
||||
return `photo${photo.id}?size=c`;
|
||||
}
|
||||
}
|
||||
const videoHash = canHaveMedia && currentPhoto && isVideo && getVideoAvatarMediaHash(currentPhoto);
|
||||
const videoBlobUrl = useMedia(videoHash, undefined, undefined, lastSyncTime);
|
||||
|
||||
if (!isSavedMessages && !isDeleted && !isRepliesChat && userOrChat) {
|
||||
return getChatAvatarHash(userOrChat, size, type);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
const fullMediaData = videoBlobUrl || photoBlobUrl;
|
||||
const [isVideoReady, markVideoReady] = useFlag();
|
||||
const isFullMediaReady = Boolean(fullMediaData && (!isVideo || isVideoReady));
|
||||
const transitionClassNames = useMediaTransition(isFullMediaReady);
|
||||
const isBlurredThumb = canHaveMedia && !isFullMediaReady && !avatarBlobUrl && currentPhoto?.thumbnail?.dataUri;
|
||||
const blurredThumbCanvasRef = useCanvasBlur(
|
||||
currentPhoto?.thumbnail?.dataUri, !isBlurredThumb, IS_SINGLE_COLUMN_LAYOUT && !IS_CANVAS_FILTER_SUPPORTED,
|
||||
);
|
||||
const hasMedia = currentPhoto || avatarBlobUrl || isBlurredThumb;
|
||||
|
||||
useEffect(() => {
|
||||
if (videoRef.current && !canPlayVideo) {
|
||||
@ -82,12 +83,6 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
}
|
||||
}, [canPlayVideo]);
|
||||
|
||||
const photoHash = getMediaHash('big', 'photo');
|
||||
const photoBlobUrl = useMedia(photoHash, false, ApiMediaFormat.BlobUrl, lastSyncTime);
|
||||
const videoHash = getMediaHash('normal', 'video');
|
||||
const videoBlobUrl = useMedia(videoHash, false, ApiMediaFormat.BlobUrl, lastSyncTime);
|
||||
const imageSrc = videoBlobUrl || photoBlobUrl || photo?.thumbnail?.dataUri;
|
||||
|
||||
let content: TeactNode | undefined;
|
||||
|
||||
if (isSavedMessages) {
|
||||
@ -96,23 +91,37 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
content = <i className="icon-avatar-deleted-account" />;
|
||||
} else if (isRepliesChat) {
|
||||
content = <i className="icon-reply-filled" />;
|
||||
} else if (imageSrc) {
|
||||
if (videoBlobUrl) {
|
||||
content = (
|
||||
<OptimizedVideo
|
||||
canPlay={canPlayVideo}
|
||||
ref={videoRef}
|
||||
src={imageSrc}
|
||||
className="avatar-media"
|
||||
muted
|
||||
disablePictureInPicture
|
||||
loop
|
||||
playsInline
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = <img src={imageSrc} className="avatar-media" alt="" />;
|
||||
}
|
||||
} else if (hasMedia) {
|
||||
content = (
|
||||
<>
|
||||
{isBlurredThumb ? (
|
||||
<canvas ref={blurredThumbCanvasRef} className="thumb" />
|
||||
) : (
|
||||
<img src={avatarBlobUrl} className="thumb" alt="" />
|
||||
)}
|
||||
{currentPhoto && (
|
||||
isVideo ? (
|
||||
<OptimizedVideo
|
||||
canPlay={canPlayVideo}
|
||||
ref={videoRef}
|
||||
src={fullMediaData}
|
||||
className={buildClassName('avatar-media', transitionClassNames)}
|
||||
muted
|
||||
disablePictureInPicture
|
||||
loop
|
||||
playsInline
|
||||
onPlay={markVideoReady}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={fullMediaData}
|
||||
className={buildClassName('avatar-media', transitionClassNames)}
|
||||
alt=""
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else if (user) {
|
||||
const userFullName = getUserFullName(user);
|
||||
content = userFullName ? getFirstLetters(userFullName, 2) : undefined;
|
||||
@ -133,11 +142,11 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
isSavedMessages && 'saved-messages',
|
||||
isDeleted && 'deleted-account',
|
||||
isRepliesChat && 'replies-bot-account',
|
||||
(!isSavedMessages && !imageSrc) && 'no-photo',
|
||||
(!isSavedMessages && !hasMedia) && 'no-photo',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={fullClassName} onClick={imageSrc ? onClick : undefined}>
|
||||
<div className={fullClassName} onClick={hasMedia ? onClick : undefined}>
|
||||
{typeof content === 'string' ? renderText(content, ['hq_emoji']) : content}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { useEffect, useRef } from '../lib/teact/teact';
|
||||
|
||||
import fastBlur from '../lib/fastBlur';
|
||||
import useForceUpdate from './useForceUpdate';
|
||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../util/environment';
|
||||
import fastBlur from '../lib/fastBlur';
|
||||
|
||||
const RADIUS = 2;
|
||||
const ITERATIONS = 2;
|
||||
@ -17,15 +16,17 @@ export default function useCanvasBlur(
|
||||
) {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const forceUpdate = useForceUpdate();
|
||||
const isStarted = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!dataUri || !canvas || isDisabled) {
|
||||
if (!dataUri || !canvas || isDisabled || isStarted.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isStarted.current = true;
|
||||
|
||||
const img = new Image();
|
||||
|
||||
const processBlur = () => {
|
||||
@ -54,7 +55,7 @@ export default function useCanvasBlur(
|
||||
};
|
||||
|
||||
img.src = dataUri;
|
||||
}, [canvasRef, dataUri, forceUpdate, isDisabled, preferredHeight, preferredWidth, withRaf, radius]);
|
||||
}, [dataUri, isDisabled, preferredHeight, preferredWidth, radius, withRaf]);
|
||||
|
||||
return canvasRef;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user