mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-22 05:11:55 +01:00
Avatar: Get rid of data-uri avatars
This commit is contained in:
parent
77194245de
commit
09caf525a3
@ -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: {
|
||||
|
@ -2,7 +2,6 @@
|
||||
// and messages media as Blob for smaller size.
|
||||
|
||||
export enum ApiMediaFormat {
|
||||
DataUri,
|
||||
BlobUrl,
|
||||
Lottie,
|
||||
Progressive,
|
||||
|
@ -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;
|
||||
|
@ -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 = '';
|
||||
|
@ -57,7 +57,7 @@ function preloadAvatars() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return mediaLoader.fetch(avatarHash, ApiMediaFormat.DataUri);
|
||||
return mediaLoader.fetch(avatarHash, ApiMediaFormat.BlobUrl);
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user