add better loading states to media
Some checks failed
Some checks failed
This commit is contained in:
@ -190,6 +190,7 @@ const Item: React.FC<IItem> = ({
|
||||
alt={attachment.description}
|
||||
letterboxed={letterboxed}
|
||||
showExt
|
||||
blurhash={attachment.blurhash}
|
||||
/>
|
||||
</a>
|
||||
{attachment.description && (
|
||||
|
||||
@ -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<IStillImage> = ({
|
||||
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<HTMLCanvasElement>(null);
|
||||
const img = useRef<HTMLImageElement>(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<HTMLImageElement> = (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<IStillImage> = ({
|
||||
className={clsx(className, 'relative isolate', { 'group': !noGroup })}
|
||||
style={style}
|
||||
>
|
||||
{!loaded && (blurhash ? (
|
||||
<Blurhash hash={blurhash} className='absolute inset-0 z-0 size-full rounded-[inherit]' />
|
||||
) : placeholder === 'shimmer' ? (
|
||||
<span className='absolute inset-0 z-0 overflow-hidden rounded-[inherit] bg-gray-200 dark:bg-gray-700'>
|
||||
<span className='absolute inset-0 no-reduce-motion:animate-shimmer' style={{ background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent)' }} />
|
||||
</span>
|
||||
) : placeholder === 'pulse' ? (
|
||||
<span className='absolute inset-0 z-0 rounded-[inherit] bg-gray-200 no-reduce-motion:animate-pulse dark:bg-gray-700' />
|
||||
) : null)}
|
||||
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
ref={img}
|
||||
decoding='async'
|
||||
onLoad={handleImageLoad}
|
||||
onError={onError}
|
||||
className={clsx(baseClassName, {
|
||||
className={clsx(baseClassName, 'no-reduce-motion:transition-opacity no-reduce-motion:duration-200', {
|
||||
'invisible group-hover:visible': hoverToPlay,
|
||||
'opacity-0': !loaded,
|
||||
})}
|
||||
/>
|
||||
|
||||
@ -84,7 +108,9 @@ const StillImage: React.FC<IStillImage> = ({
|
||||
<img
|
||||
src={staticSrc}
|
||||
alt={alt}
|
||||
className={clsx(baseClassName, 'absolute top-0 group-hover:invisible')}
|
||||
decoding='async'
|
||||
className={clsx(baseClassName, 'absolute top-0 opacity-0 no-reduce-motion:transition-opacity no-reduce-motion:duration-200 group-hover:invisible')}
|
||||
onLoad={(e) => e.currentTarget.classList.remove('opacity-0')}
|
||||
/>
|
||||
) : (
|
||||
<canvas
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import StillImage from '@/components/still-image';
|
||||
@ -40,6 +41,7 @@ const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => {
|
||||
isGif
|
||||
noGroup={noGroup}
|
||||
letterboxed
|
||||
placeholder='none'
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
@ -49,10 +51,13 @@ const Emoji: React.FC<IEmoji> = (props): JSX.Element | null => {
|
||||
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
draggable='false'
|
||||
alt={alt || emoji}
|
||||
src={joinPublicPath(`packs/emoji/${filename}.svg`)}
|
||||
{...rest}
|
||||
decoding='async'
|
||||
className={clsx('opacity-0 no-reduce-motion:transition-opacity no-reduce-motion:duration-150', rest.className)}
|
||||
onLoad={(e) => e.currentTarget.classList.remove('opacity-0')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -93,10 +93,12 @@ const Emojify: React.FC<IEmojify> = React.memo(({ text, emojis = {}, large }) =>
|
||||
<img
|
||||
key={index}
|
||||
draggable={false}
|
||||
className='emojione transition-transform hover:scale-125'
|
||||
className='emojione opacity-0 no-reduce-motion:transition-opacity no-reduce-motion:duration-150 transition-transform hover:scale-125'
|
||||
alt={c}
|
||||
title={`:${shortcode}:`}
|
||||
src={joinPublicPath(`packs/emoji/${unified}.svg`)}
|
||||
decoding='async'
|
||||
onLoad={(e) => e.currentTarget.classList.remove('opacity-0')}
|
||||
/>,
|
||||
);
|
||||
} else if (!systemEmojiFont && unqualified in unicodeMapping) {
|
||||
@ -108,10 +110,12 @@ const Emojify: React.FC<IEmojify> = React.memo(({ text, emojis = {}, large }) =>
|
||||
<img
|
||||
key={index}
|
||||
draggable={false}
|
||||
className='emojione transition-transform hover:scale-125'
|
||||
className='emojione opacity-0 no-reduce-motion:transition-opacity no-reduce-motion:duration-150 transition-transform hover:scale-125'
|
||||
alt={unqualified}
|
||||
title={`:${shortcode}:`}
|
||||
src={joinPublicPath(`packs/emoji/${unified}.svg`)}
|
||||
decoding='async'
|
||||
onLoad={(e) => e.currentTarget.classList.remove('opacity-0')}
|
||||
/>,
|
||||
);
|
||||
} else if (!disableUserProvidedMedia && c === ':') {
|
||||
|
||||
@ -77,6 +77,7 @@ const MediaItem: React.FC<IMediaItem> = ({ 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) {
|
||||
|
||||
Reference in New Issue
Block a user