mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
feat(media-proxy): sign attachment URLs and gate origins (#2830)
This commit is contained in:
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);
|
||||
|
||||
+5
-57
@@ -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;
|
||||
|
||||
+84
@@ -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"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user