From 1926a1d6bb1f271d1ded6f2149b049454ae2432e Mon Sep 17 00:00:00 2001 From: matty Date: Sat, 14 Feb 2026 20:15:46 +0000 Subject: [PATCH] add better loading states to media --- .../pl-fe/src/components/media-gallery.tsx | 1 + packages/pl-fe/src/components/still-image.tsx | 34 ++++++++++++++++--- packages/pl-fe/src/components/ui/emoji.tsx | 7 +++- packages/pl-fe/src/features/emoji/emojify.tsx | 8 +++-- .../src/pages/accounts/account-gallery.tsx | 1 + 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/pl-fe/src/components/media-gallery.tsx b/packages/pl-fe/src/components/media-gallery.tsx index e6340567d..b313be0ac 100644 --- a/packages/pl-fe/src/components/media-gallery.tsx +++ b/packages/pl-fe/src/components/media-gallery.tsx @@ -190,6 +190,7 @@ const Item: React.FC = ({ alt={attachment.description} letterboxed={letterboxed} showExt + blurhash={attachment.blurhash} /> {attachment.description && ( diff --git a/packages/pl-fe/src/components/still-image.tsx b/packages/pl-fe/src/components/still-image.tsx index 49bff5b5f..fccad5239 100644 --- a/packages/pl-fe/src/components/still-image.tsx +++ b/packages/pl-fe/src/components/still-image.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx'; -import React, { useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import Blurhash from '@/components/blurhash'; import { useSettings } from '@/stores/settings'; interface IStillImage { @@ -28,20 +29,31 @@ interface IStillImage { isGif?: boolean; /** Specify that the group is defined by the parent */ noGroup?: boolean; + /** Blurhash string for placeholder preview. */ + blurhash?: string | null; + /** Loading placeholder style. */ + placeholder?: 'shimmer' | 'pulse' | 'none'; } /** Renders images on a canvas, only playing GIFs if autoPlayGif is enabled. */ const StillImage: React.FC = ({ - alt, className, innerClassName, src, staticSrc, style, letterboxed = false, showExt = false, onError, onLoad, isGif, noGroup, + alt, className, innerClassName, src, staticSrc, style, letterboxed = false, showExt = false, onError, onLoad, isGif, noGroup, blurhash, placeholder = 'pulse', }) => { const { autoPlayGif } = useSettings(); const canvas = useRef(null); const img = useRef(null); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + setLoaded(false); + }, [src]); const hoverToPlay = src && !autoPlayGif && (isGif || src.endsWith('.gif') || src.startsWith('blob:') || (src && staticSrc && src !== staticSrc)); const handleImageLoad: React.ReactEventHandler = (e) => { + setLoaded(true); + if (hoverToPlay && !staticSrc && canvas.current && img.current) { canvas.current.width = img.current.naturalWidth; canvas.current.height = img.current.naturalHeight; @@ -69,14 +81,26 @@ const StillImage: React.FC = ({ className={clsx(className, 'relative isolate', { 'group': !noGroup })} style={style} > + {!loaded && (blurhash ? ( + + ) : placeholder === 'shimmer' ? ( + + + + ) : placeholder === 'pulse' ? ( + + ) : null)} + {alt} @@ -84,7 +108,9 @@ const StillImage: React.FC = ({ {alt} e.currentTarget.classList.remove('opacity-0')} /> ) : ( = (props): JSX.Element | null => { isGif noGroup={noGroup} letterboxed + placeholder='none' {...rest} /> ); @@ -49,10 +51,13 @@ const Emoji: React.FC = (props): JSX.Element | null => { return ( {alt e.currentTarget.classList.remove('opacity-0')} /> ); }; diff --git a/packages/pl-fe/src/features/emoji/emojify.tsx b/packages/pl-fe/src/features/emoji/emojify.tsx index 7cd394b66..f573a5c7b 100644 --- a/packages/pl-fe/src/features/emoji/emojify.tsx +++ b/packages/pl-fe/src/features/emoji/emojify.tsx @@ -93,10 +93,12 @@ const Emojify: React.FC = React.memo(({ text, emojis = {}, large }) => {c} e.currentTarget.classList.remove('opacity-0')} />, ); } else if (!systemEmojiFont && unqualified in unicodeMapping) { @@ -108,10 +110,12 @@ const Emojify: React.FC = React.memo(({ text, emojis = {}, large }) => {unqualified} e.currentTarget.classList.remove('opacity-0')} />, ); } else if (!disableUserProvidedMedia && c === ':') { diff --git a/packages/pl-fe/src/pages/accounts/account-gallery.tsx b/packages/pl-fe/src/pages/accounts/account-gallery.tsx index ae05e07d3..88ae88221 100644 --- a/packages/pl-fe/src/pages/accounts/account-gallery.tsx +++ b/packages/pl-fe/src/pages/accounts/account-gallery.tsx @@ -77,6 +77,7 @@ const MediaItem: React.FC = ({ attachment, onOpenMedia, isLast }) => alt={attachment.description} style={{ objectPosition: `${x}% ${y}%` }} className={clsx('size-full overflow-hidden', { 'rounded-br-md': isLast })} + blurhash={attachment.blurhash} /> ); } else if (['gifv', 'video'].indexOf(attachment.type) !== -1) {