diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap index 2f0a2de324..124b50d8c7 100644 --- a/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap +++ b/app/javascript/mastodon/components/__tests__/__snapshots__/avatar-test.jsx.snap @@ -2,7 +2,7 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = ` <div - className="account__avatar" + className="account__avatar account__avatar--loading" onMouseEnter={[Function]} onMouseLeave={[Function]} style={ @@ -14,6 +14,8 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = ` > <img alt="" + onError={[Function]} + onLoad={[Function]} src="/animated/alice.gif" /> </div> @@ -21,7 +23,7 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = ` exports[`<Avatar /> Still renders a still avatar 1`] = ` <div - className="account__avatar" + className="account__avatar account__avatar--loading" onMouseEnter={[Function]} onMouseLeave={[Function]} style={ @@ -33,6 +35,8 @@ exports[`<Avatar /> Still renders a still avatar 1`] = ` > <img alt="" + onError={[Function]} + onLoad={[Function]} src="/static/alice.jpg" /> </div> diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx index 8b16296c2c..f61d9676de 100644 --- a/app/javascript/mastodon/components/avatar.tsx +++ b/app/javascript/mastodon/components/avatar.tsx @@ -1,10 +1,11 @@ +import { useState, useCallback } from 'react'; + import classNames from 'classnames'; +import { useHovering } from 'mastodon/../hooks/useHovering'; +import { autoPlayGif } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; -import { useHovering } from '../../hooks/useHovering'; -import { autoPlayGif } from '../initial_state'; - interface Props { account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there size: number; @@ -25,6 +26,8 @@ export const Avatar: React.FC<Props> = ({ counterBorderColor, }) => { const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); const style = { ...styleFromParent, @@ -37,16 +40,28 @@ export const Avatar: React.FC<Props> = ({ ? account?.get('avatar') : account?.get('avatar_static'); + const handleLoad = useCallback(() => { + setLoading(false); + }, [setLoading]); + + const handleError = useCallback(() => { + setError(true); + }, [setError]); + return ( <div className={classNames('account__avatar', { - 'account__avatar-inline': inline, + 'account__avatar--inline': inline, + 'account__avatar--loading': loading, })} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} style={style} > - {src && <img src={src} alt='' />} + {src && !error && ( + <img src={src} alt='' onLoad={handleLoad} onError={handleError} /> + )} + {counter && ( <div className='account__avatar__counter' diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index f257368390..4ed05ead3f 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2077,7 +2077,6 @@ body > [data-popper-placement] { display: block; position: relative; border-radius: var(--avatar-border-radius); - background-color: var(--surface-background-color); img { width: 100%; @@ -2087,7 +2086,11 @@ body > [data-popper-placement] { display: inline-block; // to not show broken images } - &-inline { + &--loading { + background-color: var(--surface-background-color); + } + + &--inline { display: inline-block; vertical-align: middle; margin-inline-end: 5px;