From dd3a76f736d4fbf3c46ac378f5cc7ab9e73f3d3a Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 10 Nov 2022 18:27:49 +0400 Subject: [PATCH] Profile Photo: Progressive loading, fix flickering (#2116) --- src/components/common/ProfileInfo.tsx | 1 - src/components/common/ProfilePhoto.scss | 16 ++++ src/components/common/ProfilePhoto.tsx | 105 +++++++++++++----------- src/hooks/useCanvasBlur.ts | 11 +-- 4 files changed, 79 insertions(+), 54 deletions(-) diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index b7a7228ac..87e36ec56 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -185,7 +185,6 @@ const ProfileInfo: FC = ({ chat={chat} photo={photo} isSavedMessages={isSavedMessages} - isFirstPhoto={isFirst} canPlayVideo={Boolean(isActive && canPlayVideo)} onClick={handleProfilePhotoClick} /> diff --git a/src/components/common/ProfilePhoto.scss b/src/components/common/ProfilePhoto.scss index 734dd137e..82f0d0bc1 100644 --- a/src/components/common/ProfilePhoto.scss +++ b/src/components/common/ProfilePhoto.scss @@ -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; + } + } } diff --git a/src/components/common/ProfilePhoto.tsx b/src/components/common/ProfilePhoto.tsx index 58a135a01..d856cb032 100644 --- a/src/components/common/ProfilePhoto.tsx +++ b/src/components/common/ProfilePhoto.tsx @@ -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 = ({ chat, user, photo, - isFirstPhoto, isSavedMessages, canPlayVideo, lastSyncTime, @@ -49,32 +50,32 @@ const ProfilePhoto: FC = ({ const videoRef = useRef(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 = ({ } }, [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 = ({ content = ; } else if (isRepliesChat) { content = ; - } else if (imageSrc) { - if (videoBlobUrl) { - content = ( - - ); - } else { - content = ; - } + } else if (hasMedia) { + content = ( + <> + {isBlurredThumb ? ( + + ) : ( + + )} + {currentPhoto && ( + isVideo ? ( + + ) : ( + + ) + )} + + ); } else if (user) { const userFullName = getUserFullName(user); content = userFullName ? getFirstLetters(userFullName, 2) : undefined; @@ -133,11 +142,11 @@ const ProfilePhoto: FC = ({ isSavedMessages && 'saved-messages', isDeleted && 'deleted-account', isRepliesChat && 'replies-bot-account', - (!isSavedMessages && !imageSrc) && 'no-photo', + (!isSavedMessages && !hasMedia) && 'no-photo', ); return ( -
+
{typeof content === 'string' ? renderText(content, ['hq_emoji']) : content}
); diff --git a/src/hooks/useCanvasBlur.ts b/src/hooks/useCanvasBlur.ts index 9e8f78802..932c25553 100644 --- a/src/hooks/useCanvasBlur.ts +++ b/src/hooks/useCanvasBlur.ts @@ -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(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; }