fix(app): refresh favorite gif previews that point at a provider page (#1925)

This commit is contained in:
Hampus
2026-08-24 12:46:40 +02:00
committed by GitHub
parent fbd653d8e0
commit 1300e5a690
4 changed files with 94 additions and 30 deletions
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {inferFormatContentType, PREVIEW_FORMAT_PRIORITY} from '@fluxer/schema/src/domains/gif/GifMediaFormatKeys';
export interface FavoriteGifMediaFormat {
src: string;
proxy_src: string;
@@ -17,41 +19,90 @@ export interface FavoriteGifEntry {
placeholder: string | null;
}
const PREVIEW_FORMAT_PRIORITY = ['webm', 'mp4', 'tinywebm', 'tinymp4', 'webp', 'gif', 'tinygif', 'nanogif'] as const;
const VIDEO_FORMAT_KEYS = new Set(['webm', 'mp4', 'tinywebm', 'tinymp4']);
const FORMAT_CONTENT_TYPES: Record<string, string> = {
webm: 'video/webm',
tinywebm: 'video/webm',
mp4: 'video/mp4',
tinymp4: 'video/mp4',
webp: 'image/webp',
gif: 'image/gif',
tinygif: 'image/gif',
nanogif: 'image/gif',
};
export const PREVIEW_TILE_CSS_WIDTH = 200;
const PLAYABLE_CLASS = 0;
const STILL_CLASS = 1;
const UNKNOWN_CLASS = 2;
interface PreviewCandidate {
key: string;
format: FavoriteGifMediaFormat;
formatClass: number;
priority: number;
}
export interface PreviewTileBox {
cssWidth?: number;
devicePixelRatio?: number;
}
function isUsablePreviewFormat(format: FavoriteGifMediaFormat | undefined): format is FavoriteGifMediaFormat {
return Boolean(format?.src && format.proxy_src && format.width > 0 && format.height > 0);
}
function previewFormatClass(formatKey: string): number {
const contentType = inferFormatContentType(formatKey);
if (contentType.startsWith('video/')) return PLAYABLE_CLASS;
if (contentType.startsWith('image/')) return STILL_CLASS;
return UNKNOWN_CLASS;
}
function previewFormatPriority(formatKey: string): number {
const index = PREVIEW_FORMAT_PRIORITY.indexOf(formatKey as (typeof PREVIEW_FORMAT_PRIORITY)[number]);
return index === -1 ? PREVIEW_FORMAT_PRIORITY.length : index;
}
function currentDevicePixelRatio(): number {
const ratio = (globalThis as {devicePixelRatio?: number}).devicePixelRatio;
return typeof ratio === 'number' && Number.isFinite(ratio) && ratio > 0 ? ratio : 1;
}
export function previewTileDeviceWidth(tile: PreviewTileBox = {}): number {
return (tile.cssWidth ?? PREVIEW_TILE_CSS_WIDTH) * (tile.devicePixelRatio ?? currentDevicePixelRatio());
}
export function pickBestPreviewFormat(
media: Record<string, FavoriteGifMediaFormat> | null | undefined,
kind: 'any' | 'image' = 'any',
tile: PreviewTileBox = {},
): {key: string; format: FavoriteGifMediaFormat} | null {
if (!media) return null;
const deviceWidth = previewTileDeviceWidth(tile);
const candidates: Array<PreviewCandidate> = [];
for (const [key, format] of Object.entries(media)) {
if (!isUsablePreviewFormat(format)) continue;
const formatClass = previewFormatClass(key);
if (kind === 'image' && formatClass !== STILL_CLASS) continue;
candidates.push({key, format, formatClass, priority: previewFormatPriority(key)});
}
if (candidates.length === 0) return null;
const filling = candidates.filter((candidate) => candidate.format.width >= deviceWidth);
const pool = filling.length > 0 ? filling : candidates;
const widthDirection = filling.length > 0 ? 1 : -1;
pool.sort(
(a, b) =>
a.formatClass - b.formatClass ||
widthDirection * (a.format.width - b.format.width) ||
a.priority - b.priority ||
a.key.localeCompare(b.key),
);
const best = pool[0];
return {key: best.key, format: best.format};
}
export function pickCanonicalPreviewFormat(
media: Record<string, FavoriteGifMediaFormat> | null | undefined,
): {key: string; format: FavoriteGifMediaFormat} | null {
if (!media) return null;
for (const key of PREVIEW_FORMAT_PRIORITY) {
const format = media[key];
if (format?.src && format.proxy_src && format.width > 0 && format.height > 0) {
return {key, format};
}
if (isUsablePreviewFormat(format)) return {key, format};
}
for (const [key, format] of Object.entries(media)) {
if (format?.src && format.proxy_src && format.width > 0 && format.height > 0) {
return {key, format};
}
if (isUsablePreviewFormat(format)) return {key, format};
}
return null;
}
export function inferFormatContentType(formatKey: string): string {
return FORMAT_CONTENT_TYPES[formatKey] ?? '';
}
export function isVideoFormatKey(formatKey: string): boolean {
return VIDEO_FORMAT_KEYS.has(formatKey);
}
export {inferFormatContentType};
@@ -5,10 +5,11 @@ import {Endpoints} from '@app/features/app/constants/Endpoints';
import {
type FavoriteGifEntry,
inferFormatContentType,
pickBestPreviewFormat,
pickCanonicalPreviewFormat,
} from '@app/features/channel/components/pickers/gif/FavoriteGifTypes';
import FavoriteGif from '@app/features/expressions/state/FavoriteGif';
import {FAVORITE_GIF_LIMIT_REACHED_DESCRIPTOR} from '@app/features/expressions/utils/FavoriteGifMessageDescriptors';
import * as GifSlugUtils from '@app/features/expressions/utils/GifSlugUtils';
import {http} from '@app/features/platform/transport/RestTransport';
import {Logger} from '@app/features/platform/utils/AppLogger';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
@@ -88,10 +89,10 @@ export function addFavoriteGif(i18n: I18n, entry: FavoriteGifEntry): void {
export function addFavoriteGifFromMedia(i18n: I18n, params: FavoriteGifFromMediaParams): void {
const media = params.media ?? {};
const bestPreview = pickBestPreviewFormat(media);
const bestPreview = pickCanonicalPreviewFormat(media);
addFavoriteGif(i18n, {
url: params.url,
proxy_url: bestPreview?.format.proxy_src ?? params.proxyUrl ?? params.url,
proxy_url: bestPreview?.format.proxy_src ?? params.proxyUrl ?? '',
width: bestPreview?.format.width ?? params.width ?? 0,
height: bestPreview?.format.height ?? params.height ?? 0,
media,
@@ -147,7 +148,8 @@ export async function refreshFavoriteGifPreviews(): Promise<void> {
}
function needsPreviewRefresh(entry: FavoriteGifEntry): boolean {
if (pickBestPreviewFormat(entry.media)) return false;
if (pickCanonicalPreviewFormat(entry.media)) return false;
if (!entry.proxy_url || GifSlugUtils.isProviderPageUrl(entry.proxy_url)) return true;
if (entry.width <= 0 || entry.height <= 0) return true;
return !entry.content_type.startsWith('image/') && !entry.content_type.startsWith('video/');
}
@@ -27,6 +27,17 @@ export function extractSlugFromUrl(provider: string, url: string): string | null
return getAdapter(provider)?.extractSlugFromUrl(url) ?? null;
}
export function isProviderPageUrl(url: string): boolean {
if (!url) return false;
return Object.values(PROVIDER_URL_ADAPTERS).some((adapter) => adapter.extractSlugFromUrl(url) != null);
}
export function isUsableMediaSource(url: string): boolean {
if (!url) return false;
if (!/^https?:\/\//i.test(url.trim())) return false;
return !isProviderPageUrl(url);
}
function looksLikeUrl(value: string): boolean {
const trimmed = value.trim();
return /^https?:\/\//i.test(trimmed);
@@ -98,7 +98,7 @@ export function useMediaFavorite({
} else {
FavoriteGifCommands.addFavoriteGifFromMedia(i18n, {
url: embedURL!,
proxyUrl: proxyURL ?? embedURL!,
proxyUrl: proxyURL,
width: naturalWidth ?? 0,
height: naturalHeight ?? 0,
media: {},