From 1960aac90b16fce1ec620ac990aa931efcf04700 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 21 Mar 2025 11:23:49 +0100
Subject: [PATCH] Fix display of failed-to-load image attachments in web UI
 (#34217)

---
 .../mastodon/components/media_gallery.jsx           |  8 +++++++-
 .../account_gallery/components/media_item.tsx       | 13 ++++++++++++-
 app/javascript/styles/mastodon/components.scss      |  4 ++++
 3 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx
index 5132316600..e52e14b1ed 100644
--- a/app/javascript/mastodon/components/media_gallery.jsx
+++ b/app/javascript/mastodon/components/media_gallery.jsx
@@ -38,6 +38,7 @@ class Item extends PureComponent {
 
   state = {
     loaded: false,
+    error: false,
   };
 
   handleMouseEnter = (e) => {
@@ -81,6 +82,10 @@ class Item extends PureComponent {
     this.setState({ loaded: true });
   };
 
+  handleImageError = () => {
+    this.setState({ error: true });
+  };
+
   render () {
     const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;
 
@@ -148,6 +153,7 @@ class Item extends PureComponent {
             lang={lang}
             style={{ objectPosition: `${x}% ${y}%` }}
             onLoad={this.handleImageLoad}
+            onError={this.handleImageError}
           />
         </a>
       );
@@ -183,7 +189,7 @@ class Item extends PureComponent {
     }
 
     return (
-      <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
+      <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--error': this.state.error, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
         <Blurhash
           hash={attachment.get('blurhash')}
           dummy={!useBlurhash}
diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.tsx b/app/javascript/mastodon/features/account_gallery/components/media_item.tsx
index 3de2a29b18..0d251ff99f 100644
--- a/app/javascript/mastodon/features/account_gallery/components/media_item.tsx
+++ b/app/javascript/mastodon/features/account_gallery/components/media_item.tsx
@@ -26,11 +26,16 @@ export const MediaItem: React.FC<{
       displayMedia === 'show_all',
   );
   const [loaded, setLoaded] = useState(false);
+  const [error, setError] = useState(false);
 
   const handleImageLoad = useCallback(() => {
     setLoaded(true);
   }, [setLoaded]);
 
+  const handleImageError = useCallback(() => {
+    setError(true);
+  }, [setError]);
+
   const handleMouseEnter = useCallback(
     (e: React.MouseEvent<HTMLVideoElement>) => {
       if (e.target instanceof HTMLVideoElement) {
@@ -98,6 +103,7 @@ export const MediaItem: React.FC<{
           alt={description}
           lang={lang}
           onLoad={handleImageLoad}
+          onError={handleImageError}
         />
 
         <div className='media-gallery__item__overlay media-gallery__item__overlay--corner'>
@@ -118,6 +124,7 @@ export const MediaItem: React.FC<{
         lang={lang}
         style={{ objectPosition: `${x}% ${y}%` }}
         onLoad={handleImageLoad}
+        onError={handleImageError}
       />
     );
   } else if (['video', 'gifv'].includes(type)) {
@@ -173,7 +180,11 @@ export const MediaItem: React.FC<{
   }
 
   return (
-    <div className='media-gallery__item media-gallery__item--square'>
+    <div
+      className={classNames('media-gallery__item media-gallery__item--square', {
+        'media-gallery__item--error': error,
+      })}
+    >
       <Blurhash
         hash={blurhash}
         className={classNames('media-gallery__preview', {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 011f8263f4..097a2fa20e 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -6902,6 +6902,10 @@ a.status-card {
       filter: var(--overlay-icon-shadow);
     }
   }
+
+  &--error img {
+    visibility: hidden;
+  }
 }
 
 .media-gallery__item-thumbnail {