diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index cf1722dc3..5bed8d693 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -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 { switch (mediaFormat) { - case ApiMediaFormat.DataUri: - return blobToDataUri(new Blob([data], { type: mimeType })); case ApiMediaFormat.BlobUrl: return new Blob([data], { type: mimeType }); case ApiMediaFormat.Lottie: { diff --git a/src/api/types/media.ts b/src/api/types/media.ts index 77e42cfce..b5fef6afa 100644 --- a/src/api/types/media.ts +++ b/src/api/types/media.ts @@ -2,7 +2,6 @@ // and messages media as Blob for smaller size. export enum ApiMediaFormat { - DataUri, BlobUrl, Lottie, Progressive, diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index a359b190f..dfc9a34d8 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -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 = ({ } } - 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 = ({ } else if (isReplies) { content = ; } else if (shouldRenderFullMedia) { - content = ; + content = ; } else if (user) { const userFullName = getUserFullName(user); content = userFullName ? getFirstLetters(userFullName, 2) : undefined; diff --git a/src/components/common/ProfilePhoto.tsx b/src/components/common/ProfilePhoto.tsx index 167ab5182..d685f15f0 100644 --- a/src/components/common/ProfilePhoto.tsx +++ b/src/components/common/ProfilePhoto.tsx @@ -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 = ({ 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 = ({ 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 = ''; diff --git a/src/components/common/UiLoader.tsx b/src/components/common/UiLoader.tsx index 037150eb2..c606efda2 100644 --- a/src/components/common/UiLoader.tsx +++ b/src/components/common/UiLoader.tsx @@ -57,7 +57,7 @@ function preloadAvatars() { return undefined; } - return mediaLoader.fetch(avatarHash, ApiMediaFormat.DataUri); + return mediaLoader.fetch(avatarHash, ApiMediaFormat.BlobUrl); })); } diff --git a/src/components/left/settings/WallpaperTile.tsx b/src/components/left/settings/WallpaperTile.tsx index f05cc1b47..c2a7c51be 100644 --- a/src/components/left/settings/WallpaperTile.tsx +++ b/src/components/left/settings/WallpaperTile.tsx @@ -36,11 +36,7 @@ const WallpaperTile: FC = ({ 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'); diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index 893174307..e364541bd 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -166,7 +166,7 @@ const MediaViewer: FC = ({ 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 = ({ 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 = ({ ); 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 = ({ return (
{renderPhoto( - fullMediaData || blobUrlPreview, + fullMediaBlobUrl || previewBlobUrl, calculateMediaViewerDimensions(AVATAR_FULL_DIMENSIONS, false), !IS_SINGLE_COLUMN_LAYOUT && !isZoomed, )} @@ -476,14 +474,14 @@ const MediaViewer: FC = ({ onClick={handleToggleFooterVisibility} > {isPhoto && renderPhoto( - localBlobUrl || fullMediaData || blobUrlPreview || blobUrlPictogram, + localBlobUrl || fullMediaBlobUrl || previewBlobUrl || pictogramBlobUrl, message && calculateMediaViewerDimensions(dimensions!, hasFooter), !IS_SINGLE_COLUMN_LAYOUT && !isZoomed, )} {isVideo && ( = ({ {renderSenderInfo} = ({ return ( { 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; } diff --git a/src/util/cacheApi.ts b/src/util/cacheApi.ts index c6c97a7d9..5ffb5ee7d 100644 --- a/src/util/cacheApi.ts +++ b/src/util/cacheApi.ts @@ -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(); diff --git a/src/util/mediaLoader.ts b/src/util/mediaLoader.ts index 032879426..4113253b6 100644 --- a/src/util/mediaLoader.ts +++ b/src/util/mediaLoader.ts @@ -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; diff --git a/src/util/webpToPng.ts b/src/util/webpToPng.ts index 655e2a9c7..ea3c37f5d 100644 --- a/src/util/webpToPng.ts +++ b/src/util/webpToPng.ts @@ -25,17 +25,17 @@ export async function webpToPng(url: string, blob: Blob): Promise { - if (isWebpSupported() || url.substr(0, 15) !== 'data:image/webp') { - return url; +export async function webpToPngBase64(key: string, dataUri: string): Promise { + 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);