mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(app): pick favorite GIF preview kind from content type (#2696)
This commit is contained in:
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user