diff --git a/fluxer_app/src/features/channel/components/GifPicker.module.css b/fluxer_app/src/features/channel/components/GifPicker.module.css index 040f1c553..edd3f5fe1 100644 --- a/fluxer_app/src/features/channel/components/GifPicker.module.css +++ b/fluxer_app/src/features/channel/components/GifPicker.module.css @@ -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); } diff --git a/fluxer_app/src/features/channel/components/pickers/gif/GifPickerGridData.ts b/fluxer_app/src/features/channel/components/pickers/gif/GifPickerGridData.ts index 97a9a228b..2c1387abf 100644 --- a/fluxer_app/src/features/channel/components/pickers/gif/GifPickerGridData.ts +++ b/fluxer_app/src/features/channel/components/pickers/gif/GifPickerGridData.ts @@ -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 ({ - 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, + }; + }), ]; } diff --git a/fluxer_app/src/features/channel/components/pickers/gif/GifPickerGridItem.tsx b/fluxer_app/src/features/channel/components/pickers/gif/GifPickerGridItem.tsx index 12bab2344..627982e21 100644 --- a/fluxer_app/src/features/channel/components/pickers/gif/GifPickerGridItem.tsx +++ b/fluxer_app/src/features/channel/components/pickers/gif/GifPickerGridItem.tsx @@ -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) => { 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 (
{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 && ( + + )} +
) : ( - + hasThumbnailContent && ( + + ) )}