feat(media-proxy): sign attachment URLs and gate origins (#2830)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-18 15:57:32 +02:00
1 parent 025c01ab13
commit 522cf08e61
108 files changed
+10148 -441

No files matched your search

@@ -4,6 +4,7 @@ import {useAnimatedMediaVideoPlayback} from '@app/features/app/hooks/useAnimated
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
import styles from '@app/features/channel/components/AutocompleteEmoji.module.css';
import type {FavoriteMeme} from '@app/features/expressions/models/FavoriteMeme';
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
import {buildStaticGifPreviewURL} from '@app/features/messaging/utils/MediaProxyUtils';
import {MusicNoteIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
@@ -32,6 +33,7 @@ export const AutocompleteMemePreview = observer(({meme}: {meme: FavoriteMeme}) =
const isVideoMeme = contentType.startsWith('video/');
const isAnimatedImageMeme = !isVideoMeme && !isAudioMeme && contentType.includes('gif');
const motionAllowed = useShouldAnimate({kind: 'gif', isAnimated: isVideoMeme || isAnimatedImageMeme});
const memeUrl = AttachmentUrlRefresher.fresh(meme.url, {refreshUnsigned: true});
if (isAudioMeme) {
return (
<div className={styles.audioIconWrapper} data-flx="channel.autocomplete-meme-preview.audio-icon-wrapper">
@@ -44,16 +46,14 @@ export const AutocompleteMemePreview = observer(({meme}: {meme: FavoriteMeme}) =
);
}
if (isVideoMeme && motionAllowed) {
return (
<AutocompleteMemeVideo src={meme.url} data-flx="channel.autocomplete-meme-preview.autocomplete-meme-video" />
);
return <AutocompleteMemeVideo src={memeUrl} data-flx="channel.autocomplete-meme-preview.autocomplete-meme-video" />;
}
const needsStillFrame = (isVideoMeme || isAnimatedImageMeme) && !motionAllowed;
return (
<img
draggable={false}
className={styles.memeIcon}
src={needsStillFrame ? buildStaticGifPreviewURL(meme.url) : meme.url}
src={needsStillFrame ? buildStaticGifPreviewURL(memeUrl) : memeUrl}
alt={meme.name}
aria-hidden={true}
data-flx="channel.autocomplete-meme-preview.meme-icon"
@@ -2,6 +2,7 @@
import {splitMediaAndFileAttachments} from '@app/features/channel/components/MessageAttachmentUtils';
import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions';
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
import {mapAttachmentsWithExpiry} from '@app/features/messaging/utils/AttachmentExpiryUtils';
import UserSettings from '@app/features/user/state/UserSettings';
import type {MessageAttachment} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
@@ -13,11 +14,25 @@ interface AttachmentRenderingState {
shouldUseMosaic: boolean;
}
function freshAttachmentUrl(url: string | null): string | null {
return url === null ? null : AttachmentUrlRefresher.fresh(url);
}
export function withFreshAttachmentUrls(attachment: MessageAttachment): MessageAttachment {
const url = freshAttachmentUrl(attachment.url);
const proxyUrl = freshAttachmentUrl(attachment.proxy_url);
if (url === attachment.url && proxyUrl === attachment.proxy_url) return attachment;
return {...attachment, url, proxy_url: proxyUrl};
}
export const getAttachmentRenderingState = (
snapshotAttachments?: ReadonlyArray<MessageAttachment> | null,
): AttachmentRenderingState => {
const attachments = snapshotAttachments ?? [];
const expiryApplied = mapAttachmentsWithExpiry(attachments, DeveloperOptions.mockAttachmentStates);
const expiryApplied = mapAttachmentsWithExpiry(attachments, DeveloperOptions.mockAttachmentStates).map((entry) => ({
isExpired: entry.isExpired,
attachment: withFreshAttachmentUrls(entry.attachment),
}));
const enrichedAttachments = expiryApplied.map((entry) => entry.attachment);
const activeAttachments = expiryApplied.filter((entry) => !entry.isExpired).map((entry) => entry.attachment);
const {mediaAttachments} = splitMediaAndFileAttachments(activeAttachments);
@@ -20,6 +20,7 @@ import FavoriteMemes from '@app/features/expressions/state/FavoriteMemes';
import {AUDIO_DESCRIPTOR, GIFS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
import {useNearViewport} from '@app/features/messaging/hooks/useNearViewport';
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
import {buildStaticGifPreviewURL} from '@app/features/messaging/utils/MediaProxyUtils';
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
import {DeleteIcon, EditIcon} from '@app/features/ui/action_menu/ContextMenuIcons';
@@ -428,7 +429,8 @@ const GridItem = observer(
const isAudio = contentType.startsWith('audio/');
const isGifImage = !isVideo && contentType.toLowerCase().includes('gif');
const shouldAnimateGif = useShouldAnimate({kind: 'gif', isAnimated: !isAudio && (isVideo || isGifImage)});
const thumbnailSrc = isGifImage && !shouldAnimateGif ? buildStaticGifPreviewURL(url) : url;
const memeUrl = AttachmentUrlRefresher.fresh(url, {refreshUnsigned: true});
const thumbnailSrc = isGifImage && !shouldAnimateGif ? buildStaticGifPreviewURL(memeUrl) : memeUrl;
const videoPlaybackAllowed = useAnimatedMediaVideoPlayback(videoRef, {
enabled: isVisible && !isAudio && isVideo,
shouldPlay: shouldAnimateGif,
@@ -471,7 +473,7 @@ const GridItem = observer(
disablePictureInPicture={true}
disableRemotePlayback={true}
preload={shouldAnimateGif ? 'auto' : 'metadata'}
src={url}
src={memeUrl}
data-flx="channel.mobile-memes-picker.grid-item.gif"
/>
)}
@@ -26,6 +26,7 @@ import Channels from '@app/features/channel/state/Channels';
import Guilds from '@app/features/guild/state/Guilds';
import {SUPPRESS_EMBEDS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
import {getEmbedMediaDimensions} from '@app/features/messaging/utils/MediaDimensionConfig';
import {buildAnimatedImageProxyURL} from '@app/features/messaging/utils/MediaProxyUtils';
import {buildMessageEmbedCopyText} from '@app/features/messaging/utils/MessageCopyTextUtils';
@@ -39,6 +40,7 @@ import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import MobileLayout from '@app/features/ui/state/MobileLayout';
import {MessageAttachmentFlags, MessageEmbedTypes, Permissions} from '@fluxer/constants/src/ChannelConstants';
import {GuildOperations} from '@fluxer/constants/src/GuildConstants';
import type {EmbedMedia, MessageEmbed} from '@fluxer/schema/src/domains/message/EmbedSchemas';
import {useLingui} from '@lingui/react/macro';
import {XIcon} from '@phosphor-icons/react';
import {clsx} from 'clsx';
@@ -47,7 +49,29 @@ import type React from 'react';
import {type FC, useCallback, useMemo} from 'react';
const mediaFocusRingClass = messageStyles.mediaFocusRing;
export const Embed: FC<EmbedProps> = observer(({embed, message, embedIndex, contextualEmbeds, onDelete, isPreview}) => {
function withFreshEmbedMediaUrls(media: EmbedMedia | undefined): EmbedMedia | undefined {
if (!media) return media;
const url = AttachmentUrlRefresher.fresh(media.url);
const proxyUrl = media.proxy_url === undefined ? undefined : AttachmentUrlRefresher.fresh(media.proxy_url);
if (url === media.url && proxyUrl === media.proxy_url) return media;
return {...media, url, proxy_url: proxyUrl};
}
function withFreshEmbedUrls(embed: MessageEmbed): MessageEmbed {
const image = withFreshEmbedMediaUrls(embed.image);
const thumbnail = withFreshEmbedMediaUrls(embed.thumbnail);
const video = withFreshEmbedMediaUrls(embed.video);
const audio = withFreshEmbedMediaUrls(embed.audio);
if (image === embed.image && thumbnail === embed.thumbnail && video === embed.video && audio === embed.audio) {
return embed;
}
return {...embed, image, thumbnail, video, audio};
}
export const Embed: FC<EmbedProps> = observer((props: EmbedProps) => {
const {message, embedIndex, contextualEmbeds, onDelete, isPreview} = props;
const embed = withFreshEmbedUrls(props.embed);
const {i18n} = useLingui();
const {enabled: isMobile} = MobileLayout;
const channel = Channels.getChannel(message.channelId);
@@ -9,6 +9,10 @@ import {CsvAttachmentTablePanel} from '@app/features/channel/components/embeds/a
import {TextualAttachmentCodePanel} from '@app/features/channel/components/embeds/attachments/TextualAttachmentCodePanel';
import styles from '@app/features/channel/components/embeds/attachments/TextualAttachmentPreview.module.css';
import {TextualAttachmentPreviewBottomSheet} from '@app/features/channel/components/embeds/attachments/TextualAttachmentPreviewBottomSheet';
import {
fetchTextualPreviewText,
PreviewSizeLimitError,
} from '@app/features/channel/components/embeds/attachments/TextualAttachmentPreviewFetch';
import {TextualAttachmentPreviewFooter} from '@app/features/channel/components/embeds/attachments/TextualAttachmentPreviewFooter';
import {TextualAttachmentPreviewModal} from '@app/features/channel/components/embeds/attachments/TextualAttachmentPreviewModal';
import {
@@ -45,56 +49,6 @@ import {type MouseEvent, useCallback, useEffect, useMemo, useState} from 'react'
const logger = new Logger('TextualAttachmentPreview');
class PreviewSizeLimitError extends Error {
constructor() {
super('Attachment preview exceeds the size limit');
this.name = 'PreviewSizeLimitError';
}
}
async function readPreviewText(response: Response): Promise<string> {
const contentLength = response.headers.get('content-length');
if (contentLength !== null) {
const parsedContentLength = Number(contentLength);
if (Number.isFinite(parsedContentLength) && parsedContentLength > TEXT_PREVIEW_MAX_BYTES) {
throw new PreviewSizeLimitError();
}
}
const body = response.body;
if (!body) {
throw new Error('Attachment preview response has no readable body');
}
const reader = body.getReader();
const chunks: Array<Uint8Array> = [];
let totalBytes = 0;
try {
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
if (!value) {
continue;
}
totalBytes += value.byteLength;
if (totalBytes > TEXT_PREVIEW_MAX_BYTES) {
await reader.cancel().catch(() => undefined);
throw new PreviewSizeLimitError();
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder().decode(bytes);
}
export const TextualAttachmentPreview = observer(function TextualAttachmentPreview({
attachment,
spoilerHidden = false,
@@ -146,13 +100,7 @@ export const TextualAttachmentPreview = observer(function TextualAttachmentPrevi
setStatus('loading');
setPreviewError(null);
const controller = new AbortController();
fetch(attachment.url, {signal: controller.signal})
.then((response) => {
if (!response.ok) {
throw new Error(`Attachment preview request failed: ${response.status} ${response.statusText}`);
}
return readPreviewText(response);
})
fetchTextualPreviewText(attachment.url, controller.signal)
.then((value) => {
if (controller.signal.aborted) {
return;
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
import {TEXT_PREVIEW_MAX_BYTES} from '@app/features/messaging/utils/AttachmentPreviewUtils';
const SIGNATURE_REFUSAL_STATUSES = new Set([401, 403, 404, 410]);
export class PreviewSizeLimitError extends Error {
constructor() {
super('Attachment preview exceeds the size limit');
this.name = 'PreviewSizeLimitError';
}
}
export function isAttachmentSignatureRefusal(status: number): boolean {
return SIGNATURE_REFUSAL_STATUSES.has(status);
}
export async function readPreviewText(response: Response): Promise<string> {
const contentLength = response.headers.get('content-length');
if (contentLength !== null) {
const parsedContentLength = Number(contentLength);
if (Number.isFinite(parsedContentLength) && parsedContentLength > TEXT_PREVIEW_MAX_BYTES) {
throw new PreviewSizeLimitError();
}
}
const body = response.body;
if (!body) {
throw new Error('Attachment preview response has no readable body');
}
const reader = body.getReader();
const chunks: Array<Uint8Array> = [];
let totalBytes = 0;
try {
while (true) {
const {done, value} = await reader.read();
if (done) {
break;
}
if (!value) {
continue;
}
totalBytes += value.byteLength;
if (totalBytes > TEXT_PREVIEW_MAX_BYTES) {
await reader.cancel().catch(() => undefined);
throw new PreviewSizeLimitError();
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder().decode(bytes);
}
function requestFailed(response: Response): Error {
return new Error(`Attachment preview request failed: ${response.status} ${response.statusText}`);
}
export async function fetchTextualPreviewText(url: string, signal: AbortSignal): Promise<string> {
const target = await AttachmentUrlRefresher.refresh(url);
const response = await fetch(target, {signal});
if (response.ok) {
return readPreviewText(response);
}
if (!isAttachmentSignatureRefusal(response.status)) {
throw requestFailed(response);
}
const refreshed = await AttachmentUrlRefresher.refresh(url, {force: true});
if (refreshed === target) {
throw requestFailed(response);
}
const retried = await fetch(refreshed, {signal});
if (!retried.ok) {
throw requestFailed(retried);
}
return readPreviewText(retried);
}
@@ -19,6 +19,7 @@ import {safePause, safePlay} from '@app/features/channel/components/GifVideoPool
import {useMaybeMessageViewContext} from '@app/features/channel/components/MessageViewContext';
import type {Channel} from '@app/features/channel/models/Channel';
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
import {useAttachmentRefreshOnError} from '@app/features/messaging/hooks/useAttachmentRefreshOnError';
import {useDeleteAttachment} from '@app/features/messaging/hooks/useDeleteAttachment';
import {useMatureMedia} from '@app/features/messaging/hooks/useMatureMedia';
import {useMediaFavorite} from '@app/features/messaging/hooks/useMediaFavorite';
@@ -389,6 +390,7 @@ export const EmbedGifv: FC<
const messageViewContext = useMaybeMessageViewContext();
const mediaCalculator = useEmbedMediaCalculator();
const videoRef = useRef<HTMLVideoElement>(null);
const handleVideoError = useAttachmentRefreshOnError(videoProxyURL);
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
const {isPointerInside, hasFocusInside} = useMediaSurfaceInteraction(containerElement);
const {shouldBlur, gateReason, canReveal, reveal: revealSensitiveMedia} = useMatureMedia(nsfw, channelId);
@@ -632,6 +634,7 @@ export const EmbedGifv: FC<
height={dimensions.height}
tabIndex={-1}
aria-label={i18n._(ANIMATED_GIF_VIDEO_DESCRIPTOR)}
onError={handleVideoError}
data-embed-media="gifv"
data-flx="channel.embeds.media.embed-gifv.video-element"
/>
@@ -6,6 +6,7 @@ import styles from '@app/features/channel/components/embeds/media/VoiceMessagePl
import {useMaybeMessageViewContext} from '@app/features/channel/components/MessageViewContext';
import {PAUSE_DESCRIPTOR, PLAY_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
import {useAttachmentRefreshOnError} from '@app/features/messaging/hooks/useAttachmentRefreshOnError';
import {buildMediaProxyURL} from '@app/features/messaging/utils/MediaProxyUtils';
import {Logger} from '@app/features/platform/utils/AppLogger';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
@@ -119,6 +120,7 @@ const VoiceMessagePlayer: React.FC<VoiceMessagePlayerProps> = observer(
const {i18n} = useLingui();
const messageViewContext = useMaybeMessageViewContext();
const effectiveSrc = buildMediaProxyURL(src);
const handleMediaError = useAttachmentRefreshOnError(effectiveSrc);
const [hasStarted, setHasStarted] = useState(false);
const [wantsMetadata, setWantsMetadata] = useState(false);
const [prePlayCurrentTime, setPrePlayCurrentTime] = useState(0);
@@ -324,6 +326,7 @@ const VoiceMessagePlayer: React.FC<VoiceMessagePlayerProps> = observer(
ref={mediaRef as React.RefObject<HTMLAudioElement>}
src={hasStarted || wantsMetadata ? effectiveSrc : undefined}
preload={wantsMetadata ? 'metadata' : 'none'}
onError={handleMediaError}
data-flx="channel.embeds.media.voice-message-player.audio"
>
<track kind="captions" data-flx="channel.embeds.media.voice-message-player.track" />
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {stripAttachmentSignature} from '@app/features/messaging/utils/AttachmentCdnUrl';
import {inferFormatContentType, PREVIEW_FORMAT_PRIORITY} from '@fluxer/schema/src/domains/gif/GifMediaFormatKeys';
export interface FavoriteGifMediaFormat {
@@ -107,16 +108,36 @@ export function pickCanonicalPreviewFormat(
export const STORED_PREVIEW_DEVICE_PIXEL_RATIO = 2;
export function stripFavoriteGifEntrySignatures(entry: FavoriteGifEntry): FavoriteGifEntry {
const url = stripAttachmentSignature(entry.url);
const proxyUrl = stripAttachmentSignature(entry.proxy_url);
const media: Record<string, FavoriteGifMediaFormat> = {};
let mediaChanged = false;
for (const [key, format] of Object.entries(entry.media)) {
const src = stripAttachmentSignature(format.src);
const proxySrc = stripAttachmentSignature(format.proxy_src);
if (src === format.src && proxySrc === format.proxy_src) {
media[key] = format;
continue;
}
mediaChanged = true;
media[key] = {...format, src, proxy_src: proxySrc};
}
if (url === entry.url && proxyUrl === entry.proxy_url && !mediaChanged) return entry;
return {...entry, url, proxy_url: proxyUrl, media};
}
export function slimFavoriteGifEntry(entry: FavoriteGifEntry): FavoriteGifEntry {
const best = pickBestPreviewFormat(entry.media, 'any', {
const stripped = stripFavoriteGifEntrySignatures(entry);
const best = pickBestPreviewFormat(stripped.media, 'any', {
cssWidth: PREVIEW_TILE_CSS_WIDTH,
devicePixelRatio: STORED_PREVIEW_DEVICE_PIXEL_RATIO,
});
if (best == null) {
return Object.keys(entry.media).length === 0 ? entry : {...entry, media: {}};
return Object.keys(stripped.media).length === 0 ? stripped : {...stripped, media: {}};
}
return {
...entry,
...stripped,
proxy_url: best.format.proxy_src,
width: best.format.width,
height: best.format.height,
@@ -16,6 +16,7 @@ import FavoriteGif from '@app/features/expressions/state/FavoriteGif';
import FavoriteMemes from '@app/features/expressions/state/FavoriteMemes';
import * as GifSlugUtils from '@app/features/expressions/utils/GifSlugUtils';
import {FAVORITES_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
@@ -57,6 +58,7 @@ export const GifPickerGrid = observer(
const favoriteGifs = FavoriteGif.favoriteGifs;
const favoriteGifsVersion = favoriteGifs.length;
const useSavedMediaForGifFavorites = FavoriteGif.saveGifFavoritesAsSavedMedia;
const attachmentUrlRevision = AttachmentUrlRefresher.revision;
const data: Array<GifPickerGridItemData> = useMemo(() => {
return buildGifPickerGridData({
surface: store.isShowingFavorites ? 'favorites' : store.isShowingFeatured ? 'featured' : 'results',
@@ -88,6 +90,7 @@ export const GifPickerGrid = observer(
selectGif,
store.featuredFavoritePreviewSeed,
i18n.locale,
attachmentUrlRevision,
]);
const itemKeys = useMemo(() => data.filter((item) => item.type !== 'skeleton').map((item) => item.key), [data]);
const itemByKey = useMemo(() => new Map(data.map((item) => [item.key, item])), [data]);
@@ -8,6 +8,7 @@ import {
import type {GifPickerGridItemData} from '@app/features/channel/components/pickers/gif/GifPickerTypes';
import type {Gif, GifFeatured} from '@app/features/expressions/commands/GifCommands';
import * as GifSlugUtils from '@app/features/expressions/utils/GifSlugUtils';
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
const CATEGORY_TILE_WIDTH = 200;
const CATEGORY_TILE_HEIGHT = 96;
@@ -34,6 +35,10 @@ export interface BuildGifPickerGridDataInput {
trendingTitle: string;
}
function freshStoredUrl(url: string): string {
return AttachmentUrlRefresher.fresh(url, {refreshUnsigned: true});
}
export function buildSkeletonGifPickerItems(count: number): Array<GifPickerGridItemData> {
return Array.from({length: count}, (_, i) => ({
type: 'skeleton',
@@ -68,9 +73,9 @@ function buildFavoriteGifItems(
for (let index = favoriteGifs.length - 1; index >= 0; index -= 1) {
const entry = favoriteGifs[index];
const best = pickBestPreviewFormat(entry.media);
const fallbackSrc = GifSlugUtils.isUsableMediaSource(entry.proxy_url) ? entry.proxy_url : '';
const previewSrc = best?.format.src ?? fallbackSrc;
const previewProxySrc = best?.format.proxy_src ?? fallbackSrc;
const fallbackSrc = GifSlugUtils.isUsableMediaSource(entry.proxy_url) ? freshStoredUrl(entry.proxy_url) : '';
const previewSrc = best ? freshStoredUrl(best.format.src) : fallbackSrc;
const previewProxySrc = best ? freshStoredUrl(best.format.proxy_src) : fallbackSrc;
const previewWidth = best?.format.width ?? entry.width;
const previewHeight = best?.format.height ?? entry.height;
const previewContentType = best ? inferFormatContentType(best.key) : entry.content_type;
@@ -107,14 +112,21 @@ function buildFeaturedItems(input: BuildGifPickerGridDataInput): Array<GifPicker
const favoriteGifPreview = pickBestPreviewFormat(favoriteGifPreviewEntry?.media);
const favoriteMemeCandidate =
gifvMemes.length > 0 ? (gifvMemes[Math.floor(input.featuredFavoritePreviewSeed * gifvMemes.length)] ?? null) : null;
const favoriteMemePreview = favoriteMemeCandidate?.url ?? '';
const favoriteMemePreview = favoriteMemeCandidate ? freshStoredUrl(favoriteMemeCandidate.url) : '';
const usesFavoriteMemePreview = input.useSavedMediaForGifFavorites && input.favoriteGifs.length === 0;
const favoriteTilePreview = usesFavoriteMemePreview
? favoriteMemePreview
: favoriteGifPreview?.format.src || favoriteGifPreviewEntry?.proxy_url || favoriteGifPreviewEntry?.url || '';
: freshStoredUrl(
favoriteGifPreview?.format.src || favoriteGifPreviewEntry?.proxy_url || favoriteGifPreviewEntry?.url || '',
);
const favoriteTileProxyPreview = usesFavoriteMemePreview
? favoriteTilePreview
: favoriteGifPreview?.format.proxy_src || favoriteGifPreviewEntry?.proxy_url || favoriteGifPreviewEntry?.url || '';
: freshStoredUrl(
favoriteGifPreview?.format.proxy_src ||
favoriteGifPreviewEntry?.proxy_url ||
favoriteGifPreviewEntry?.url ||
'',
);
const favoriteTileContentType = (() => {
if (usesFavoriteMemePreview) return favoriteMemeCandidate?.contentType ?? '';
if (favoriteGifPreview) return inferFormatContentType(favoriteGifPreview.key);
@@ -13,6 +13,7 @@ import * as FavoriteMemeCommands from '@app/features/expressions/commands/Favori
import {EditFavoriteMemeModal} from '@app/features/expressions/components/modals/EditFavoriteMemeModal';
import type {FavoriteMeme} from '@app/features/expressions/models/FavoriteMeme';
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
import {buildMediaProxyURL, buildStaticGifPreviewURL} from '@app/features/messaging/utils/MediaProxyUtils';
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
@@ -113,10 +114,14 @@ export const MemeGridItem = observer(
isHovering: isVideoPreviewActive,
});
const shouldRenderVideoPreview = !isAudio && isVideo && isVideoPreviewActive && shouldAnimateGif;
const thumbnailSrc = isGifImage && !shouldAnimateGif ? buildStaticGifPreviewURL(meme.url) : meme.url;
const memeUrl = AttachmentUrlRefresher.fresh(meme.url, {refreshUnsigned: true});
const thumbnailSrc =
isGifImage && !shouldAnimateGif
? AttachmentUrlRefresher.fresh(buildStaticGifPreviewURL(meme.url), {refreshUnsigned: true})
: memeUrl;
const videoPreviewStartTime = meme.duration && meme.duration > 0 ? meme.duration / 2 : null;
usePooledVideo({
src: shouldRenderVideoPreview ? meme.url : null,
src: shouldRenderVideoPreview ? memeUrl : null,
containerRef: videoContainerRef,
videoPool,
autoPlay: shouldRenderVideoPreview,
@@ -219,7 +224,7 @@ export const MemeGridItem = observer(
data-flx="channel.pickers.memes.meme-grid-item.div--2"
>
<VideoPoster
src={meme.url}
src={memeUrl}
placeholder={meme.placeholder}
data-flx="channel.pickers.memes.meme-grid-item.video-poster"
/>