mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-08 03:32:27 +09:00
fix(app): pick gif picker previews by format and skip empty sources (#1927)
This commit is contained in:
@@ -225,6 +225,10 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.gifMediaContainerPlaceholder {
|
||||
background-color: var(--background-tertiary);
|
||||
}
|
||||
|
||||
.gridItemFavorites .gridItemBackdrop {
|
||||
background-color: color-mix(in srgb, var(--brand-primary) 60%, transparent);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@app/features/channel/components/pickers/gif/FavoriteGifTypes';
|
||||
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';
|
||||
|
||||
const CATEGORY_TILE_WIDTH = 200;
|
||||
const CATEGORY_TILE_HEIGHT = 96;
|
||||
@@ -66,8 +67,9 @@ function buildFavoriteGifItems(
|
||||
for (let index = favoriteGifs.length - 1; index >= 0; index -= 1) {
|
||||
const entry = favoriteGifs[index];
|
||||
const best = pickBestPreviewFormat(entry.media);
|
||||
const previewSrc = best?.format.src ?? entry.proxy_url;
|
||||
const previewProxySrc = best?.format.proxy_src ?? entry.proxy_url;
|
||||
const fallbackSrc = GifSlugUtils.isUsableMediaSource(entry.proxy_url) ? entry.proxy_url : '';
|
||||
const previewSrc = best?.format.src ?? fallbackSrc;
|
||||
const previewProxySrc = best?.format.proxy_src ?? fallbackSrc;
|
||||
const previewWidth = best?.format.width ?? entry.width;
|
||||
const previewHeight = best?.format.height ?? entry.height;
|
||||
items.push({
|
||||
@@ -131,6 +133,8 @@ function buildFeaturedItems(input: BuildGifPickerGridDataInput): Array<GifPicker
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const trendingGif = input.featured.gifs[0];
|
||||
const trendingPreview = pickBestPreviewFormat(trendingGif?.media);
|
||||
return [
|
||||
...favoritesTile,
|
||||
{
|
||||
@@ -139,21 +143,25 @@ function buildFeaturedItems(input: BuildGifPickerGridDataInput): Array<GifPicker
|
||||
key: 'trending',
|
||||
id: 'trending',
|
||||
title: input.trendingTitle,
|
||||
previewUrl: input.featured.gifs[0]?.src ?? input.featured.gifs[0]?.url ?? '',
|
||||
previewProxySrc: input.featured.gifs[0]?.proxy_src ?? input.featured.gifs[0]?.src ?? '',
|
||||
previewUrl: trendingPreview?.format.src ?? trendingGif?.src ?? trendingGif?.url ?? '',
|
||||
previewProxySrc: trendingPreview?.format.proxy_src ?? trendingGif?.proxy_src ?? trendingGif?.src ?? '',
|
||||
width: CATEGORY_TILE_WIDTH,
|
||||
height: CATEGORY_TILE_HEIGHT,
|
||||
},
|
||||
...input.featured.categories.map((category) => ({
|
||||
type: 'category' as const,
|
||||
categoryKind: 'category' as const,
|
||||
key: category.name,
|
||||
id: category.name,
|
||||
title: category.name,
|
||||
previewUrl: category.gif?.src ?? category.src,
|
||||
previewProxySrc: category.gif?.proxy_src ?? category.proxy_src ?? category.src,
|
||||
width: CATEGORY_TILE_WIDTH,
|
||||
height: CATEGORY_TILE_HEIGHT,
|
||||
})),
|
||||
...input.featured.categories.map((category) => {
|
||||
const categoryPreview = pickBestPreviewFormat(category.gif?.media);
|
||||
return {
|
||||
type: 'category' as const,
|
||||
categoryKind: 'category' as const,
|
||||
key: category.name,
|
||||
id: category.name,
|
||||
title: category.name,
|
||||
previewUrl: categoryPreview?.format.src ?? category.gif?.src ?? category.src,
|
||||
previewProxySrc:
|
||||
categoryPreview?.format.proxy_src ?? category.gif?.proxy_src ?? category.proxy_src ?? category.src,
|
||||
width: CATEGORY_TILE_WIDTH,
|
||||
height: CATEGORY_TILE_HEIGHT,
|
||||
};
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {getAnimatedMediaPlaybackAllowed} from '@app/features/app/hooks/useAnimatedMediaPlayback';
|
||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||
import styles from '@app/features/channel/components/GifPicker.module.css';
|
||||
import {safePause, safePlay, useGifVideoPool} from '@app/features/channel/components/GifVideoPool';
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
REMOVE_FROM_FAVORITES_DESCRIPTOR,
|
||||
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
||||
import {decodeThumbHashDataURL} from '@app/features/messaging/utils/ThumbHashUtils';
|
||||
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
import {modal, push} from '@app/features/ui/commands/ModalCommands';
|
||||
@@ -55,16 +55,30 @@ const SELECT_GIF_DESCRIPTOR = msg({
|
||||
'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 isVideoSourceUrl(value: string): boolean {
|
||||
function testSourcePath(value: string, pattern: RegExp): boolean {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return VIDEO_FILE_EXTENSION_REGEX.test(url.pathname);
|
||||
return pattern.test(url.pathname);
|
||||
} catch {
|
||||
return VIDEO_FILE_EXTENSION_REGEX.test(value);
|
||||
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({
|
||||
item,
|
||||
coords,
|
||||
@@ -120,7 +134,10 @@ export const GifPickerGridItem = observer(function GifPickerGridItem({
|
||||
const lookupUrl = item.gif.favoriteGifLookup?.url ?? item.gif.url;
|
||||
return FavoriteGif.findByUrl(lookupUrl)?.placeholder ?? null;
|
||||
})();
|
||||
const usesVideoElement = !isSkeleton && mediaSourceUrl !== null && isVideoSourceUrl(mediaSourceUrl);
|
||||
const usesVideoElement =
|
||||
!isSkeleton && proxySrc !== null && proxySrc.length > 0 && resolvesToVideo(proxySrc, mediaSourceUrl);
|
||||
const videoThumbHashURL = decodeThumbHashDataURL(usesVideoElement ? thumbnailPlaceholder : null);
|
||||
const hasThumbnailContent = (proxySrc !== null && proxySrc.length > 0) || thumbnailPlaceholder !== null;
|
||||
const videoRef = usePooledVideo({
|
||||
src: usesVideoElement ? proxySrc : null,
|
||||
containerRef: videoContainerRef,
|
||||
@@ -131,7 +148,6 @@ export const GifPickerGridItem = observer(function GifPickerGridItem({
|
||||
const playOnHover = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.pointerType !== 'mouse') return;
|
||||
if (!getAnimatedMediaPlaybackAllowed()) return;
|
||||
const v = videoRef.current;
|
||||
if (!v) return;
|
||||
void safePlay(v);
|
||||
@@ -214,12 +230,13 @@ export const GifPickerGridItem = observer(function GifPickerGridItem({
|
||||
},
|
||||
[handleClick],
|
||||
);
|
||||
const hoverPlaybackHandlers = gifAutoPlay
|
||||
? null
|
||||
: {
|
||||
onPointerEnter: playOnHover,
|
||||
onPointerLeave: stopOnHoverEnd,
|
||||
};
|
||||
const hoverPlaybackHandlers =
|
||||
gifAutoPlay || !usesVideoElement
|
||||
? null
|
||||
: {
|
||||
onPointerEnter: playOnHover,
|
||||
onPointerLeave: stopOnHoverEnd,
|
||||
};
|
||||
if (isSkeleton) {
|
||||
return (
|
||||
<div
|
||||
@@ -426,7 +443,7 @@ export const GifPickerGridItem = observer(function GifPickerGridItem({
|
||||
{...(hoverPlaybackHandlers ?? {})}
|
||||
>
|
||||
<div
|
||||
className={styles.gifMediaContainer}
|
||||
className={clsx(styles.gifMediaContainer, styles.gifMediaContainerPlaceholder)}
|
||||
data-flx="channel.pickers.gif.gif-picker-grid-item.gif-media-container--3"
|
||||
>
|
||||
{usesVideoElement ? (
|
||||
@@ -434,15 +451,26 @@ export const GifPickerGridItem = observer(function GifPickerGridItem({
|
||||
ref={videoContainerRef}
|
||||
className={styles.gifVideoContainer}
|
||||
data-flx="channel.pickers.gif.gif-picker-grid-item.gif-video-container--3"
|
||||
/>
|
||||
>
|
||||
{videoThumbHashURL != null && (
|
||||
<img
|
||||
src={videoThumbHashURL}
|
||||
alt=""
|
||||
aria-hidden
|
||||
data-flx="channel.pickers.gif.gif-picker-grid-item.img--2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<PickerThumbnail
|
||||
src={proxySrc ?? ''}
|
||||
alt={gif.title || ''}
|
||||
className={styles.gif}
|
||||
placeholder={thumbnailPlaceholder}
|
||||
data-flx="channel.pickers.gif.gif-picker-grid-item.gif--2"
|
||||
/>
|
||||
hasThumbnailContent && (
|
||||
<PickerThumbnail
|
||||
src={proxySrc ?? ''}
|
||||
alt={gif.title || ''}
|
||||
className={styles.gif}
|
||||
placeholder={thumbnailPlaceholder}
|
||||
data-flx="channel.pickers.gif.gif-picker-grid-item.gif--2"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user