fix(app): pick favorite GIF preview kind from content type (#2696)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-11 21:06:00 +02:00
1 parent 1b1d48b05e
commit 569146c5bc
6 files changed
+175 -40

No files matched your search

@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import {describe, expect, it} from 'vitest'; import {describe, expect, it} from 'vitest';
import type {FavoriteGifEntry} from './FavoriteGifTypes';
import {buildGifPickerGridData} from './GifPickerGridData'; import {buildGifPickerGridData} from './GifPickerGridData';
import type {FavoriteAwareGif} from './GifPickerTypes'; import type {FavoriteAwareGif} from './GifPickerTypes';
@@ -19,7 +20,84 @@ function gif(id: string, width = 200, height = 120): FavoriteAwareGif {
}; };
} }
const OPAQUE_PROXY_URL =
'https://media.test/external/sig/v2/aHR0cHM6Ly9zdGF0aWMua2xpcHkuY29tL2lpLzhjLzZhL2E4LzVsU2U1SFBCLndlYm0';
function slimFavorite(overrides: Partial<FavoriteGifEntry> = {}): FavoriteGifEntry {
return {
url: 'https://klipy.com/gifs/doc-brown-bttf',
proxy_url: OPAQUE_PROXY_URL,
width: 640,
height: 360,
media: {},
content_type: 'video/webm',
placeholder: null,
...overrides,
};
}
describe('GifPickerGridData', () => { describe('GifPickerGridData', () => {
it('carries the stored content type of a slimmed favorite into its tile', () => {
const data = buildGifPickerGridData({
surface: 'favorites',
loading: false,
columns: 3,
provider: 'klipy',
featured: {gifs: [], categories: []},
gifs: [],
favoriteGifs: [slimFavorite()],
favoriteMemes: [],
useSavedMediaForGifFavorites: false,
featuredFavoritePreviewSeed: 0,
favoriteTitle: 'Favorites',
trendingTitle: 'Trending',
});
expect(data[0]).toMatchObject({
type: 'gif',
gif: {proxy_src: OPAQUE_PROXY_URL, contentType: 'video/webm'},
});
});
it('carries the stored content type of a slimmed favorite into the featured favorites tile', () => {
const data = buildGifPickerGridData({
surface: 'featured',
loading: false,
columns: 3,
provider: 'klipy',
featured: {gifs: [gif('featured')], categories: []},
gifs: [],
favoriteGifs: [slimFavorite()],
favoriteMemes: [],
useSavedMediaForGifFavorites: false,
featuredFavoritePreviewSeed: 0,
favoriteTitle: 'Favorites',
trendingTitle: 'Trending',
});
expect(data[0]).toMatchObject({
type: 'category',
previewProxySrc: OPAQUE_PROXY_URL,
previewContentType: 'video/webm',
});
});
it('takes the content type of the saved media standing in for an empty favorites tile', () => {
const data = buildGifPickerGridData({
surface: 'featured',
loading: false,
columns: 3,
provider: 'klipy',
featured: {gifs: [gif('featured')], categories: []},
gifs: [],
favoriteGifs: [],
favoriteMemes: [{contentType: 'video/mp4', url: 'meme-0'}],
useSavedMediaForGifFavorites: true,
featuredFavoritePreviewSeed: 0,
favoriteTitle: 'Favorites',
trendingTitle: 'Trending',
});
expect(data[0]).toMatchObject({type: 'category', previewProxySrc: 'meme-0', previewContentType: 'video/mp4'});
});
it('keeps featured data deterministic while choosing preview tiles', () => { it('keeps featured data deterministic while choosing preview tiles', () => {
const data = buildGifPickerGridData({ const data = buildGifPickerGridData({
surface: 'featured', surface: 'featured',
@@ -2,6 +2,7 @@
import { import {
type FavoriteGifEntry, type FavoriteGifEntry,
inferFormatContentType,
pickBestPreviewFormat, pickBestPreviewFormat,
} from '@app/features/channel/components/pickers/gif/FavoriteGifTypes'; } from '@app/features/channel/components/pickers/gif/FavoriteGifTypes';
import type {GifPickerGridItemData} from '@app/features/channel/components/pickers/gif/GifPickerTypes'; import type {GifPickerGridItemData} from '@app/features/channel/components/pickers/gif/GifPickerTypes';
@@ -72,6 +73,7 @@ function buildFavoriteGifItems(
const previewProxySrc = best?.format.proxy_src ?? fallbackSrc; const previewProxySrc = best?.format.proxy_src ?? fallbackSrc;
const previewWidth = best?.format.width ?? entry.width; const previewWidth = best?.format.width ?? entry.width;
const previewHeight = best?.format.height ?? entry.height; const previewHeight = best?.format.height ?? entry.height;
const previewContentType = best ? inferFormatContentType(best.key) : entry.content_type;
items.push({ items.push({
type: 'gif', type: 'gif',
key: entry.url, key: entry.url,
@@ -86,6 +88,7 @@ function buildFavoriteGifItems(
width: previewWidth > 0 ? previewWidth : DEFAULT_GIF_SIZE, width: previewWidth > 0 ? previewWidth : DEFAULT_GIF_SIZE,
height: previewHeight > 0 ? previewHeight : DEFAULT_GIF_SIZE, height: previewHeight > 0 ? previewHeight : DEFAULT_GIF_SIZE,
media: entry.media, media: entry.media,
contentType: previewContentType,
favoriteGifLookup: {url: entry.url}, favoriteGifLookup: {url: entry.url},
}, },
}); });
@@ -102,21 +105,21 @@ function buildFeaturedItems(input: BuildGifPickerGridDataInput): Array<GifPicker
const favoriteGifPreviewEntry = const favoriteGifPreviewEntry =
favoriteGifPreviewIndex >= 0 ? input.favoriteGifs[favoriteGifPreviewIndex] : undefined; favoriteGifPreviewIndex >= 0 ? input.favoriteGifs[favoriteGifPreviewIndex] : undefined;
const favoriteGifPreview = pickBestPreviewFormat(favoriteGifPreviewEntry?.media); const favoriteGifPreview = pickBestPreviewFormat(favoriteGifPreviewEntry?.media);
const favoriteMemePreview = const favoriteMemeCandidate =
gifvMemes.length > 0 gifvMemes.length > 0 ? (gifvMemes[Math.floor(input.featuredFavoritePreviewSeed * gifvMemes.length)] ?? null) : null;
? (gifvMemes[Math.floor(input.featuredFavoritePreviewSeed * gifvMemes.length)]?.url ?? '') const favoriteMemePreview = favoriteMemeCandidate?.url ?? '';
: ''; const usesFavoriteMemePreview = input.useSavedMediaForGifFavorites && input.favoriteGifs.length === 0;
const favoriteTilePreview = const favoriteTilePreview = usesFavoriteMemePreview
input.useSavedMediaForGifFavorites && input.favoriteGifs.length === 0 ? favoriteMemePreview
? favoriteMemePreview : favoriteGifPreview?.format.src || favoriteGifPreviewEntry?.proxy_url || favoriteGifPreviewEntry?.url || '';
: favoriteGifPreview?.format.src || favoriteGifPreviewEntry?.proxy_url || favoriteGifPreviewEntry?.url || ''; const favoriteTileProxyPreview = usesFavoriteMemePreview
const favoriteTileProxyPreview = ? favoriteTilePreview
input.useSavedMediaForGifFavorites && input.favoriteGifs.length === 0 : favoriteGifPreview?.format.proxy_src || favoriteGifPreviewEntry?.proxy_url || favoriteGifPreviewEntry?.url || '';
? favoriteTilePreview const favoriteTileContentType = (() => {
: favoriteGifPreview?.format.proxy_src || if (usesFavoriteMemePreview) return favoriteMemeCandidate?.contentType ?? '';
favoriteGifPreviewEntry?.proxy_url || if (favoriteGifPreview) return inferFormatContentType(favoriteGifPreview.key);
favoriteGifPreviewEntry?.url || return favoriteGifPreviewEntry?.proxy_url ? favoriteGifPreviewEntry.content_type : '';
''; })();
const favoritesTile: Array<GifPickerGridItemData> = const favoritesTile: Array<GifPickerGridItemData> =
(input.includeFavoritesTile ?? true) (input.includeFavoritesTile ?? true)
? [ ? [
@@ -128,6 +131,7 @@ function buildFeaturedItems(input: BuildGifPickerGridDataInput): Array<GifPicker
title: input.favoriteTitle, title: input.favoriteTitle,
previewUrl: favoriteTilePreview, previewUrl: favoriteTilePreview,
previewProxySrc: favoriteTileProxyPreview, previewProxySrc: favoriteTileProxyPreview,
previewContentType: favoriteTileContentType,
width: CATEGORY_TILE_WIDTH, width: CATEGORY_TILE_WIDTH,
height: CATEGORY_TILE_HEIGHT, height: CATEGORY_TILE_HEIGHT,
}, },
@@ -3,6 +3,7 @@
import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import styles from '@app/features/channel/components/GifPicker.module.css'; import styles from '@app/features/channel/components/GifPicker.module.css';
import {safePause, safePlay, useGifVideoPool} from '@app/features/channel/components/GifVideoPool'; import {safePause, safePlay, useGifVideoPool} from '@app/features/channel/components/GifVideoPool';
import {resolvesToVideo} from '@app/features/channel/components/pickers/gif/GifPickerMediaKind';
import type {GifPickerGridItemData} from '@app/features/channel/components/pickers/gif/GifPickerTypes'; import type {GifPickerGridItemData} from '@app/features/channel/components/pickers/gif/GifPickerTypes';
import {PickerThumbnail} from '@app/features/channel/components/pickers/shared/PickerThumbnail'; import {PickerThumbnail} from '@app/features/channel/components/pickers/shared/PickerThumbnail';
import {usePooledVideo} from '@app/features/channel/components/pickers/shared/usePooledVideo'; import {usePooledVideo} from '@app/features/channel/components/pickers/shared/usePooledVideo';
@@ -52,30 +53,6 @@ const SELECT_GIF_DESCRIPTOR = msg({
comment: comment:
'Button label in the gif picker grid item when picking a GIF for an avatar, banner, or video background. Keep it concise. Preserve {title}; it is inserted by code.', 'Button label in the gif picker grid item when picking a GIF for an avatar, banner, or video background. Keep it concise. Preserve {title}; it is inserted by code.',
}); });
const VIDEO_FILE_EXTENSION_REGEX = /\.(mp4|webm|mov|m4v)(?:$|\?)/iu;
const IMAGE_FILE_EXTENSION_REGEX = /\.(gif|webp|png|jpe?g|avif)(?:$|\?)/iu;
function testSourcePath(value: string, pattern: RegExp): boolean {
try {
const url = new URL(value);
return pattern.test(url.pathname);
} catch {
return pattern.test(value);
}
}
function isVideoSourceUrl(value: string): boolean {
return testSourcePath(value, VIDEO_FILE_EXTENSION_REGEX);
}
function statesItsMediaKind(value: string): boolean {
return testSourcePath(value, VIDEO_FILE_EXTENSION_REGEX) || testSourcePath(value, IMAGE_FILE_EXTENSION_REGEX);
}
function resolvesToVideo(proxySrc: string, mediaSourceUrl: string | null): boolean {
if (statesItsMediaKind(proxySrc)) return isVideoSourceUrl(proxySrc);
return mediaSourceUrl !== null && isVideoSourceUrl(mediaSourceUrl);
}
export const GifPickerGridItem = observer(function GifPickerGridItem({ export const GifPickerGridItem = observer(function GifPickerGridItem({
item, item,
@@ -126,6 +103,11 @@ export const GifPickerGridItem = observer(function GifPickerGridItem({
if (item.type === 'category') return item.previewUrl; if (item.type === 'category') return item.previewUrl;
return null; return null;
})(); })();
const mediaContentType = (() => {
if (item.type === 'gif') return item.gif.contentType ?? '';
if (item.type === 'category') return item.previewContentType ?? '';
return '';
})();
const thumbnailPlaceholder = (() => { const thumbnailPlaceholder = (() => {
if (item.type !== 'gif') return null; if (item.type !== 'gif') return null;
if (item.gif.placeholder) return item.gif.placeholder; if (item.gif.placeholder) return item.gif.placeholder;
@@ -133,7 +115,10 @@ export const GifPickerGridItem = observer(function GifPickerGridItem({
return FavoriteGif.findByUrl(lookupUrl)?.placeholder ?? null; return FavoriteGif.findByUrl(lookupUrl)?.placeholder ?? null;
})(); })();
const usesVideoElement = const usesVideoElement =
!isSkeleton && proxySrc !== null && proxySrc.length > 0 && resolvesToVideo(proxySrc, mediaSourceUrl); !isSkeleton &&
proxySrc !== null &&
proxySrc.length > 0 &&
resolvesToVideo(mediaContentType, proxySrc, mediaSourceUrl);
const videoThumbHashURL = decodeThumbHashDataURL(usesVideoElement ? thumbnailPlaceholder : null); const videoThumbHashURL = decodeThumbHashDataURL(usesVideoElement ? thumbnailPlaceholder : null);
const hasThumbnailContent = (proxySrc !== null && proxySrc.length > 0) || thumbnailPlaceholder !== null; const hasThumbnailContent = (proxySrc !== null && proxySrc.length > 0) || thumbnailPlaceholder !== null;
const videoRef = usePooledVideo({ const videoRef = usePooledVideo({
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {describe, expect, it} from 'vitest';
import {resolvesToVideo} from './GifPickerMediaKind';
const OPAQUE_PROXY_SRC =
'https://media.test/external/sig/v2/aHR0cHM6Ly9zdGF0aWMua2xpcHkuY29tL2lpLzhjLzZhL2E4LzVsU2U1SFBCLndlYm0';
const PLAIN_PROXY_SRC = 'https://media.test/external/sig/https/static.klipy.com/ii/8c/6a/a8/5lSe5HPB.webm';
describe('resolvesToVideo', () => {
it('trusts a stored video content type when no url states its kind', () => {
expect(resolvesToVideo('video/webm', OPAQUE_PROXY_SRC, OPAQUE_PROXY_SRC)).toBe(true);
});
it('trusts a stored image content type when no url states its kind', () => {
expect(resolvesToVideo('image/gif', OPAQUE_PROXY_SRC, OPAQUE_PROXY_SRC)).toBe(false);
});
it('falls back to an image when nothing states the kind', () => {
expect(resolvesToVideo('', OPAQUE_PROXY_SRC, OPAQUE_PROXY_SRC)).toBe(false);
});
it('reads the extension off a plain proxy path', () => {
expect(resolvesToVideo('', PLAIN_PROXY_SRC, null)).toBe(true);
});
it('reads the extension off the media source when the proxy path is opaque', () => {
expect(resolvesToVideo('', OPAQUE_PROXY_SRC, 'https://static.klipy.com/ii/8c/6a/a8/5lSe5HPB.webm')).toBe(true);
});
it('keeps images as images when the proxy path states its kind', () => {
expect(resolvesToVideo('', 'https://media.test/external/sig/https/cdn.test/a.gif', null)).toBe(false);
});
it('ignores a query string when reading the extension', () => {
expect(resolvesToVideo('', 'https://media.test/external/sig/https/cdn.test/a.mp4?width=200', null)).toBe(true);
});
});
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const VIDEO_FILE_EXTENSION_REGEX = /\.(mp4|webm|mov|m4v)(?:$|\?)/iu;
const IMAGE_FILE_EXTENSION_REGEX = /\.(gif|webp|png|jpe?g|avif)(?:$|\?)/iu;
function testSourcePath(value: string, pattern: RegExp): boolean {
try {
const url = new URL(value);
return pattern.test(url.pathname);
} catch {
return pattern.test(value);
}
}
function isVideoSourceUrl(value: string): boolean {
return testSourcePath(value, VIDEO_FILE_EXTENSION_REGEX);
}
function statesItsMediaKind(value: string): boolean {
return testSourcePath(value, VIDEO_FILE_EXTENSION_REGEX) || testSourcePath(value, IMAGE_FILE_EXTENSION_REGEX);
}
export function resolvesToVideo(contentType: string, proxySrc: string, mediaSourceUrl: string | null): boolean {
if (contentType.startsWith('video/')) return true;
if (contentType.startsWith('image/')) return false;
if (statesItsMediaKind(proxySrc)) return isVideoSourceUrl(proxySrc);
return mediaSourceUrl !== null && isVideoSourceUrl(mediaSourceUrl);
}
@@ -10,6 +10,7 @@ export interface FavoriteGifLookup {
export type FavoriteAwareGif = Gif & { export type FavoriteAwareGif = Gif & {
favoriteGifLookup?: FavoriteGifLookup; favoriteGifLookup?: FavoriteGifLookup;
contentType?: string;
}; };
export type GifPickerGridItemData = export type GifPickerGridItemData =
| { | {
@@ -20,6 +21,7 @@ export type GifPickerGridItemData =
categoryKind: 'favorites' | 'trending' | 'category'; categoryKind: 'favorites' | 'trending' | 'category';
previewUrl: string; previewUrl: string;
previewProxySrc: string; previewProxySrc: string;
previewContentType?: string;
width: number; width: number;
height: number; height: number;
} }