Avatar: Get rid of data-uri avatars

This commit is contained in:
Alexander Zinchuk 2021-10-11 19:47:52 +03:00
parent 77194245de
commit 09caf525a3
12 changed files with 66 additions and 67 deletions

View File

@ -14,7 +14,6 @@ import {
} from '../../../config';
import localDb from '../localDb';
import { getEntityTypeById } from '../gramjsBuilders';
import { blobToDataUri } from '../../../util/files';
import * as cacheApi from '../../../util/cacheApi';
type EntityType = (
@ -221,8 +220,6 @@ async function parseMedia(
data: Buffer, mediaFormat: ApiMediaFormat, mimeType?: string,
): Promise<ApiParsedMedia | undefined> {
switch (mediaFormat) {
case ApiMediaFormat.DataUri:
return blobToDataUri(new Blob([data], { type: mimeType }));
case ApiMediaFormat.BlobUrl:
return new Blob([data], { type: mimeType });
case ApiMediaFormat.Lottie: {

View File

@ -2,7 +2,6 @@
// and messages media as Blob for smaller size.
export enum ApiMediaFormat {
DataUri,
BlobUrl,
Lottie,
Progressive,

View File

@ -1,12 +1,18 @@
import { MouseEvent as ReactMouseEvent } from 'react';
import React, { FC, useCallback, memo } from '../../lib/teact/teact';
import React, { FC, memo, useCallback } from '../../lib/teact/teact';
import { ApiUser, ApiChat, ApiMediaFormat } from '../../api/types';
import { ApiChat, ApiMediaFormat, ApiUser } from '../../api/types';
import { IS_TEST } from '../../config';
import {
getChatAvatarHash, getChatTitle, isChatPrivate,
getUserFullName, isUserOnline, isDeletedUser, getUserColorKey, isChatWithRepliesBot,
getChatAvatarHash,
getChatTitle,
getUserColorKey,
getUserFullName,
isChatPrivate,
isChatWithRepliesBot,
isDeletedUser,
isUserOnline,
} from '../../modules/helpers';
import { getFirstLetters } from '../../util/textFormat';
import buildClassName from '../../util/buildClassName';
@ -52,8 +58,8 @@ const Avatar: FC<OwnProps> = ({
}
}
const dataUri = useMedia(imageHash, false, ApiMediaFormat.DataUri, lastSyncTime);
const { shouldRenderFullMedia, transitionClassNames } = useTransitionForMedia(dataUri, 'slow');
const blobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl, lastSyncTime);
const { shouldRenderFullMedia, transitionClassNames } = useTransitionForMedia(blobUrl, 'slow');
const lang = useLang();
@ -66,7 +72,7 @@ const Avatar: FC<OwnProps> = ({
} else if (isReplies) {
content = <i className="icon-reply-filled" />;
} else if (shouldRenderFullMedia) {
content = <img src={dataUri} className={`${transitionClassNames} avatar-media`} alt="" decoding="async" />;
content = <img src={blobUrl} className={`${transitionClassNames} avatar-media`} alt="" decoding="async" />;
} else if (user) {
const userFullName = getUserFullName(user);
content = userFullName ? getFirstLetters(userFullName, 2) : undefined;

View File

@ -1,11 +1,17 @@
import React, { FC, memo } from '../../lib/teact/teact';
import {
ApiUser, ApiChat, ApiMediaFormat, ApiPhoto,
ApiChat, ApiMediaFormat, ApiPhoto, ApiUser,
} from '../../api/types';
import {
getChatAvatarHash, isDeletedUser, getUserColorKey, getChatTitle, isChatPrivate, getUserFullName, isChatWithRepliesBot,
getChatAvatarHash,
getChatTitle,
getUserColorKey,
getUserFullName,
isChatPrivate,
isChatWithRepliesBot,
isDeletedUser,
} from '../../modules/helpers';
import renderText from './helpers/renderText';
import buildClassName from '../../util/buildClassName';
@ -42,7 +48,7 @@ const ProfilePhoto: FC<OwnProps> = ({
const isDeleted = user && isDeletedUser(user);
const isRepliesChat = chat && isChatWithRepliesBot(chat.id);
function getMediaHash(size: 'normal' | 'big' = 'big', forceAvatar?: boolean) {
function getMediaHash(size: 'normal' | 'big', forceAvatar?: boolean) {
if (photo && !forceAvatar) {
return `photo${photo.id}?size=c`;
}
@ -59,21 +65,11 @@ const ProfilePhoto: FC<OwnProps> = ({
return hash;
}
const imageHash = getMediaHash();
const fullMediaData = useMedia(
imageHash,
false,
imageHash?.startsWith('avatar') ? ApiMediaFormat.DataUri : ApiMediaFormat.BlobUrl,
lastSyncTime,
);
const avatarThumbnailData = useMedia(
!fullMediaData && isFirstPhoto ? getMediaHash('normal', true) : undefined,
false,
ApiMediaFormat.DataUri,
lastSyncTime,
);
const thumbDataUri = useBlurSync(!fullMediaData && photo && photo.thumbnail && photo.thumbnail.dataUri);
const imageSrc = fullMediaData || avatarThumbnailData || thumbDataUri;
const photoBlobUrl = useMedia(getMediaHash('big'), false, ApiMediaFormat.BlobUrl, lastSyncTime);
const avatarMediaHash = isFirstPhoto && !photoBlobUrl ? getMediaHash('normal', true) : undefined;
const avatarBlobUrl = useMedia(avatarMediaHash, false, ApiMediaFormat.BlobUrl, lastSyncTime);
const thumbDataUri = useBlurSync(!photoBlobUrl && photo && photo.thumbnail && photo.thumbnail.dataUri);
const imageSrc = photoBlobUrl || avatarBlobUrl || thumbDataUri;
const prevImageSrc = usePrevious(imageSrc);
let content: string | undefined = '';

View File

@ -57,7 +57,7 @@ function preloadAvatars() {
return undefined;
}
return mediaLoader.fetch(avatarHash, ApiMediaFormat.DataUri);
return mediaLoader.fetch(avatarHash, ApiMediaFormat.BlobUrl);
}));
}

View File

@ -36,11 +36,7 @@ const WallpaperTile: FC<OwnProps> = ({
const localMediaHash = `wallpaper${document.id!}`;
const localBlobUrl = document.previewBlobUrl;
const previewBlobUrl = useMedia(`${localMediaHash}?size=m`);
const thumbRef = useCanvasBlur(
document.thumbnail?.dataUri,
Boolean(previewBlobUrl),
true,
);
const thumbRef = useCanvasBlur(document.thumbnail?.dataUri, Boolean(previewBlobUrl), true);
const {
shouldRenderThumb, shouldRenderFullMedia, transitionClassNames,
} = useTransitionForMedia(previewBlobUrl || localBlobUrl, 'slow');

View File

@ -166,7 +166,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
return message && getMessageMediaHash(message, isFull ? 'viewerFull' : 'viewerPreview');
}
const blobUrlPictogram = useMedia(
const pictogramBlobUrl = useMedia(
message && (isFromSharedMedia || isFromSearch) && getMessageMediaHash(message, 'pictogram'),
undefined,
ApiMediaFormat.BlobUrl,
@ -174,16 +174,14 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
isGhostAnimation && ANIMATION_DURATION,
);
const previewMediaHash = getMediaHash();
const blobUrlPreview = useMedia(
const previewBlobUrl = useMedia(
previewMediaHash,
undefined,
isAvatar && previewMediaHash && previewMediaHash.startsWith('profilePhoto')
? ApiMediaFormat.DataUri
: ApiMediaFormat.BlobUrl,
ApiMediaFormat.BlobUrl,
undefined,
isGhostAnimation && ANIMATION_DURATION,
);
const { mediaData: fullMediaData, downloadProgress } = useMediaWithDownloadProgress(
const { mediaData: fullMediaBlobUrl, downloadProgress } = useMediaWithDownloadProgress(
getMediaHash(true),
undefined,
message && getMessageMediaFormat(message, 'viewerFull'),
@ -192,7 +190,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
);
const localBlobUrl = (photo || video) ? (photo || video)!.blobUrl : undefined;
let bestImageData = (!isVideo && (localBlobUrl || fullMediaData)) || blobUrlPreview || blobUrlPictogram;
let bestImageData = (!isVideo && (localBlobUrl || fullMediaBlobUrl)) || previewBlobUrl || pictogramBlobUrl;
const thumbDataUri = useBlurSync(!bestImageData && message && getMessageMediaThumbDataUri(message));
if (!bestImageData && origin !== MediaViewerOrigin.SearchResult) {
bestImageData = thumbDataUri;
@ -459,7 +457,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
return (
<div key={chatId} className="media-viewer-content">
{renderPhoto(
fullMediaData || blobUrlPreview,
fullMediaBlobUrl || previewBlobUrl,
calculateMediaViewerDimensions(AVATAR_FULL_DIMENSIONS, false),
!IS_SINGLE_COLUMN_LAYOUT && !isZoomed,
)}
@ -476,14 +474,14 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
onClick={handleToggleFooterVisibility}
>
{isPhoto && renderPhoto(
localBlobUrl || fullMediaData || blobUrlPreview || blobUrlPictogram,
localBlobUrl || fullMediaBlobUrl || previewBlobUrl || pictogramBlobUrl,
message && calculateMediaViewerDimensions(dimensions!, hasFooter),
!IS_SINGLE_COLUMN_LAYOUT && !isZoomed,
)}
{isVideo && (
<VideoPlayer
key={messageId}
url={localBlobUrl || fullMediaData}
url={localBlobUrl || fullMediaBlobUrl}
isGif={isGif}
posterData={bestImageData}
posterSize={message && calculateMediaViewerDimensions(dimensions!, hasFooter, true)}
@ -550,7 +548,7 @@ const MediaViewer: FC<StateProps & DispatchProps> = ({
{renderSenderInfo}
</Transition>
<MediaViewerActions
mediaData={fullMediaData || blobUrlPreview}
mediaData={fullMediaBlobUrl || previewBlobUrl}
isVideo={isVideo}
isZoomed={isZoomed}
message={message}

View File

@ -64,7 +64,7 @@ const MediaResult: FC<OwnProps> = ({
return (
<BaseResult
focus={focus}
thumbUrl={shouldRenderFullMedia ? mediaBlobUrl : (thumbnail?.dataUri) || thumbnailDataUrl}
thumbUrl={shouldRenderFullMedia ? mediaBlobUrl : (thumbnail?.dataUri || thumbnailDataUrl)}
transitionClassNames={shouldRenderFullMedia ? transitionClassNames : undefined}
title={title}
description={description}

View File

@ -8,9 +8,9 @@ import { EMPTY_IMAGE_DATA_URI, webpToPngBase64 } from '../util/webpToPng';
import { getMessageMediaThumbDataUri } from '../modules/helpers';
export default function useWebpThumbnail(message?: ApiMessage) {
const thumbnail = message && getMessageMediaThumbDataUri(message);
const thumbDataUri = message && getMessageMediaThumbDataUri(message);
const sticker = message?.content?.sticker;
const shouldDecodeThumbnail = thumbnail && sticker && !isWebpSupported() && thumbnail.includes('image/webp');
const shouldDecodeThumbnail = thumbDataUri && sticker && !isWebpSupported() && thumbDataUri.includes('image/webp');
const [thumbnailDecoded, setThumbnailDecoded] = useState(EMPTY_IMAGE_DATA_URI);
const messageId = message?.id;
@ -19,7 +19,7 @@ export default function useWebpThumbnail(message?: ApiMessage) {
return;
}
webpToPngBase64(`b64-${messageId}`, thumbnail!)
webpToPngBase64(`b64-${messageId}`, thumbDataUri!)
.then(setThumbnailDecoded)
.catch((err) => {
if (DEBUG) {
@ -27,7 +27,7 @@ export default function useWebpThumbnail(message?: ApiMessage) {
console.error(err);
}
});
}, [messageId, shouldDecodeThumbnail, thumbnail]);
}, [messageId, shouldDecodeThumbnail, thumbDataUri]);
return shouldDecodeThumbnail ? thumbnailDecoded : thumbnail;
return shouldDecodeThumbnail ? thumbnailDecoded : thumbDataUri;
}

View File

@ -23,31 +23,38 @@ export async function fetch(
return undefined;
}
const contentType = response.headers.get('Content-Type');
switch (type) {
case Type.Text:
return await response.text();
case Type.Blob: {
// Ignore deprecated data-uri avatars
if (key.startsWith('avatar') && contentType && contentType.startsWith('text')) {
return undefined;
}
const blob = await response.blob();
// Safari does not return correct Content-Type header for webp images.
if (key.substr(0, 7) === 'sticker') {
if (key.startsWith('sticker')) {
return new Blob([blob], { type: 'image/webp' });
}
const shouldRecreate = !blob.type || (!isHtmlAllowed && blob.type.includes('html'));
// iOS Safari fails to preserve `type` in cache
if (!blob.type) {
const contentType = response.headers.get('Content-Type');
if (contentType) {
return new Blob([blob], { type: isHtmlAllowed ? contentType : contentType.replace(/html/gi, '') });
}
let resolvedType = blob.type || contentType;
if (!(shouldRecreate && resolvedType)) {
return blob;
}
// Prevent HTML-in-video attacks (for files that were cached before fix)
if (!isHtmlAllowed && blob.type.includes('html')) {
return new Blob([blob], { type: blob.type.replace(/html/gi, '') });
if (!isHtmlAllowed) {
resolvedType = resolvedType.replace(/html/gi, '');
}
return blob;
return new Blob([blob], { type: resolvedType });
}
case Type.Json:
return await response.json();

View File

@ -17,7 +17,6 @@ import { oggToWav } from './oggToWav';
import { webpToPng } from './webpToPng';
const asCacheApiType = {
[ApiMediaFormat.DataUri]: cacheApi.Type.Text,
[ApiMediaFormat.BlobUrl]: cacheApi.Type.Blob,
[ApiMediaFormat.Lottie]: cacheApi.Type.Json,
[ApiMediaFormat.Progressive]: undefined,
@ -82,6 +81,7 @@ async function fetchFromCacheOrRemote(
if (!MEDIA_CACHE_DISABLED) {
const cacheName = url.startsWith('avatar') ? MEDIA_CACHE_NAME_AVATARS : MEDIA_CACHE_NAME;
const cached = await cacheApi.fetch(cacheName, url, asCacheApiType[mediaFormat]!, isHtmlAllowed);
if (cached) {
let media = cached;

View File

@ -25,17 +25,17 @@ export async function webpToPng(url: string, blob: Blob): Promise<Blob | undefin
return createPng({ result, width, height });
}
export async function webpToPngBase64(key: string, url: string): Promise<string> {
if (isWebpSupported() || url.substr(0, 15) !== 'data:image/webp') {
return url;
export async function webpToPngBase64(key: string, dataUri: string): Promise<string> {
if (isWebpSupported() || dataUri.substr(0, 15) !== 'data:image/webp') {
return dataUri;
}
initWebpWorker();
const pngBlob = await webpToPng(key, dataUriToBlob(url));
const pngBlob = await webpToPng(key, dataUriToBlob(dataUri));
if (!pngBlob) {
throw new Error(`Can't convert webp to png. Url: ${url}`);
throw new Error(`Can't convert webp to png. Url: ${dataUri}`);
}
return blobToDataUri(pngBlob);