about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSasha Sorokin <dafri.nochiterov8@gmail.com>2020-07-09 18:01:30 +0700
committerGitHub <noreply@github.com>2020-07-09 13:01:30 +0200
commit61c07c37317f01c1ab4981826704750fe9937fe7 (patch)
treeabecace8339a034b8615c4a11eabf541c178bdb5
parent5b7a93b02cfa4916bd7a99d876b0e6b567e639c3 (diff)
Replace repetitive blurhash code with component (#14267)
This commit replaces all unnecessarily repeated code for decoding and
embedding blurhash canvases with separate component - <Blurhash>.

Under the hood Blurhash component will use effect dependent on its
props. This gives a few benefits: it will only be re-rendered whenever
the hash or width/height/dummy props update, and will not render if
canvas won't get to the final DOM, because then effect won't fire,
which prevents weird bugs like #14257.
-rw-r--r--app/javascript/mastodon/components/blurhash.js61
-rw-r--r--app/javascript/mastodon/components/media_gallery.js46
-rw-r--r--app/javascript/mastodon/features/account_gallery/components/media_item.js40
-rw-r--r--app/javascript/mastodon/features/status/components/card.js42
-rw-r--r--app/javascript/mastodon/features/video/index.js38
5 files changed, 102 insertions, 125 deletions
diff --git a/app/javascript/mastodon/components/blurhash.js b/app/javascript/mastodon/components/blurhash.js
new file mode 100644
index 000000000..172f8c2f5
--- /dev/null
+++ b/app/javascript/mastodon/components/blurhash.js
@@ -0,0 +1,61 @@
+// @ts-check
+
+import { decode } from 'blurhash';
+import React, { useRef, useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * @typedef BlurhashPropsBase
+ * @property {string} hash Hash to render
+ * @property {number} width
+ * Width of the blurred region in pixels. Defaults to 32
+ * @property {number} [height]
+ * Height of the blurred region in pixels. Defaults to width
+ * @property {boolean} [dummy]
+ * Whether dummy mode is enabled. If enabled, nothing is rendered
+ * and canvas left untouched
+ */
+
+/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */
+
+/**
+ * Component that is used to render blurred of blurhash string
+ *
+ * @param {BlurhashProps} param1 Props of the component
+ * @returns Canvas which will render blurred region element to embed
+ */
+function Blurhash({
+  hash,
+  width = 32,
+  height = width,
+  dummy = false,
+  ...canvasProps
+}) {
+  const canvasRef = /** @type {import('react').MutableRefObject<HTMLCanvasElement>} */ (useRef());
+
+  useEffect(() => {
+    const { current: canvas } = canvasRef;
+    canvas.width = canvas.width; // resets canvas
+
+    if (dummy) return;
+
+    const pixels = decode(hash, width, height);
+    const ctx = canvas.getContext('2d');
+    const imageData = new ImageData(pixels, width, height);
+
+    ctx.putImageData(imageData, 0, 0);
+  }, [dummy, hash, width, height]);
+
+  return (
+    <canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
+  );
+}
+
+Blurhash.propTypes = {
+  hash: PropTypes.string.isRequired,
+  width: PropTypes.number,
+  height: PropTypes.number,
+  dummy: PropTypes.bool,
+};
+
+export default React.memo(Blurhash);
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index 0ec866138..0a8f42585 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -7,8 +7,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { isIOS } from '../is_mobile';
 import classNames from 'classnames';
 import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
-import { decode } from 'blurhash';
 import { debounce } from 'lodash';
+import Blurhash from 'mastodon/components/blurhash';
 
 const messages = defineMessages({
   toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide {number, plural, one {image} other {images}}' },
@@ -74,36 +74,6 @@ class Item extends React.PureComponent {
     e.stopPropagation();
   }
 
-  componentDidMount () {
-    if (this.props.attachment.get('blurhash')) {
-      this._decode();
-    }
-  }
-
-  componentDidUpdate (prevProps) {
-    if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
-      this._decode();
-    }
-  }
-
-  _decode () {
-    if (!useBlurhash) return;
-
-    const hash   = this.props.attachment.get('blurhash');
-    const pixels = decode(hash, 32, 32);
-
-    if (pixels) {
-      const ctx       = this.canvas.getContext('2d');
-      const imageData = new ImageData(pixels, 32, 32);
-
-      ctx.putImageData(imageData, 0, 0);
-    }
-  }
-
-  setCanvasRef = c => {
-    this.canvas = c;
-  }
-
   handleImageLoad = () => {
     this.setState({ loaded: true });
   }
@@ -166,7 +136,11 @@ class Item extends React.PureComponent {
       return (
         <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
           <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
-            <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
+            <Blurhash
+              hash={attachment.get('blurhash')}
+              className='media-gallery__preview'
+              dummy={!useBlurhash}
+            />
           </a>
         </div>
       );
@@ -232,7 +206,13 @@ class Item extends React.PureComponent {
 
     return (
       <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
-        <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} />
+        <Blurhash
+          hash={attachment.get('blurhash')}
+          dummy={!useBlurhash}
+          className={classNames('media-gallery__preview', {
+            'media-gallery__preview--hidden': visible && this.state.loaded,
+          })}
+        />
         {visible && thumbnail}
       </div>
     );
diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js
index 617a45d16..9eb4ed0d3 100644
--- a/app/javascript/mastodon/features/account_gallery/components/media_item.js
+++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js
@@ -1,7 +1,7 @@
-import { decode } from 'blurhash';
+import Blurhash from 'mastodon/components/blurhash';
 import classNames from 'classnames';
 import Icon from 'mastodon/components/icon';
-import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
+import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
 import { isIOS } from 'mastodon/is_mobile';
 import PropTypes from 'prop-types';
 import React from 'react';
@@ -21,34 +21,6 @@ export default class MediaItem extends ImmutablePureComponent {
     loaded: false,
   };
 
-  componentDidMount () {
-    if (this.props.attachment.get('blurhash')) {
-      this._decode();
-    }
-  }
-
-  componentDidUpdate (prevProps) {
-    if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
-      this._decode();
-    }
-  }
-
-  _decode () {
-    const hash   = this.props.attachment.get('blurhash');
-    const pixels = decode(hash, 32, 32);
-
-    if (pixels) {
-      const ctx       = this.canvas.getContext('2d');
-      const imageData = new ImageData(pixels, 32, 32);
-
-      ctx.putImageData(imageData, 0, 0);
-    }
-  }
-
-  setCanvasRef = c => {
-    this.canvas = c;
-  }
-
   handleImageLoad = () => {
     this.setState({ loaded: true });
   }
@@ -152,7 +124,13 @@ export default class MediaItem extends ImmutablePureComponent {
     return (
       <div className='account-gallery__item' style={{ width, height }}>
         <a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
-          <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
+          <Blurhash
+            hash={attachment.get('blurhash')}
+            className={classNames('media-gallery__preview', {
+              'media-gallery__preview--hidden': visible && loaded,
+            })}
+            dummy={!useBlurhash}
+          />
           {visible && thumbnail}
           {!visible && icon}
         </a>
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index 971682df8..90f9ae7ae 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -7,7 +7,7 @@ import punycode from 'punycode';
 import classnames from 'classnames';
 import Icon from 'mastodon/components/icon';
 import { useBlurhash } from 'mastodon/initial_state';
-import { decode } from 'blurhash';
+import Blurhash from 'mastodon/components/blurhash';
 import { debounce } from 'lodash';
 
 const IDNA_PREFIX = 'xn--';
@@ -93,38 +93,12 @@ export default class Card extends React.PureComponent {
 
   componentDidMount () {
     window.addEventListener('resize', this.handleResize, { passive: true });
-
-    if (this.props.card && this.props.card.get('blurhash') && this.canvas) {
-      this._decode();
-    }
   }
 
   componentWillUnmount () {
     window.removeEventListener('resize', this.handleResize);
   }
 
-  componentDidUpdate (prevProps) {
-    const { card } = this.props;
-
-    if (card.get('blurhash') && (!prevProps.card || prevProps.card.get('blurhash') !== card.get('blurhash')) && this.canvas) {
-      this._decode();
-    }
-  }
-
-  _decode () {
-    if (!useBlurhash) return;
-
-    const hash   = this.props.card.get('blurhash');
-    const pixels = decode(hash, 32, 32);
-
-    if (pixels) {
-      const ctx       = this.canvas.getContext('2d');
-      const imageData = new ImageData(pixels, 32, 32);
-
-      ctx.putImageData(imageData, 0, 0);
-    }
-  }
-
   _setDimensions () {
     const width = this.node.offsetWidth;
 
@@ -182,10 +156,6 @@ export default class Card extends React.PureComponent {
     }
   }
 
-  setCanvasRef = c => {
-    this.canvas = c;
-  }
-
   handleImageLoad = () => {
     this.setState({ previewLoaded: true });
   }
@@ -238,7 +208,15 @@ export default class Card extends React.PureComponent {
     );
 
     let embed     = '';
-    let canvas = <canvas width={32} height={32} ref={this.setCanvasRef} className={classnames('status-card__image-preview', { 'status-card__image-preview--hidden' : revealed && this.state.previewLoaded })} />;
+    let canvas = (
+      <Blurhash
+        className={classnames('status-card__image-preview', {
+          'status-card__image-preview--hidden': revealed && this.state.previewLoaded,
+        })}
+        hash={card.get('blurhash')}
+        dummy={!useBlurhash}
+      />
+    );
     let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />;
     let spoilerButton = (
       <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 135200a3d..fb12567f0 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -7,7 +7,7 @@ import classNames from 'classnames';
 import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
 import { displayMedia, useBlurhash } from '../../initial_state';
 import Icon from 'mastodon/components/icon';
-import { decode } from 'blurhash';
+import Blurhash from 'mastodon/components/blurhash';
 
 const messages = defineMessages({
   play: { id: 'video.play', defaultMessage: 'Play' },
@@ -169,10 +169,6 @@ class Video extends React.PureComponent {
     this.volume = c;
   }
 
-  setCanvasRef = c => {
-    this.canvas = c;
-  }
-
   handleClickRoot = e => e.stopPropagation();
 
   handlePlay = () => {
@@ -289,10 +285,6 @@ class Video extends React.PureComponent {
 
     window.addEventListener('scroll', this.handleScroll);
     window.addEventListener('resize', this.handleResize, { passive: true });
-
-    if (this.props.blurhash) {
-      this._decode();
-    }
   }
 
   componentWillUnmount () {
@@ -315,24 +307,6 @@ class Video extends React.PureComponent {
     if (prevState.revealed && !this.state.revealed && this.video) {
       this.video.pause();
     }
-
-    if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
-      this._decode();
-    }
-  }
-
-  _decode () {
-    if (!useBlurhash) return;
-
-    const hash   = this.props.blurhash;
-    const pixels = decode(hash, 32, 32);
-
-    if (pixels) {
-      const ctx       = this.canvas.getContext('2d');
-      const imageData = new ImageData(pixels, 32, 32);
-
-      ctx.putImageData(imageData, 0, 0);
-    }
   }
 
   handleResize = debounce(() => {
@@ -438,7 +412,7 @@ class Video extends React.PureComponent {
   }
 
   render () {
-    const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable } = this.props;
+    const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable, blurhash } = this.props;
     const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
     const progress = (currentTime / duration) * 100;
     const playerStyle = {};
@@ -481,7 +455,13 @@ class Video extends React.PureComponent {
         onClick={this.handleClickRoot}
         tabIndex={0}
       >
-        <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} />
+        <Blurhash
+          hash={blurhash}
+          className={classNames('media-gallery__preview', {
+            'media-gallery__preview--hidden': revealed,
+          })}
+          dummy={!useBlurhash}
+        />
 
         {(revealed || editable) && <video
           ref={this.setVideoRef}