about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/javascript/mastodon/features/ui/components/image_loader.js116
-rw-r--r--app/javascript/styles/components.scss30
2 files changed, 113 insertions, 33 deletions
diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js
index 5c3879970..52c3a898b 100644
--- a/app/javascript/mastodon/features/ui/components/image_loader.js
+++ b/app/javascript/mastodon/features/ui/components/image_loader.js
@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import classNames from 'classnames';
 
 export default class ImageLoader extends React.PureComponent {
 
@@ -20,46 +21,121 @@ export default class ImageLoader extends React.PureComponent {
     error: false,
   }
 
-  componentWillMount() {
-    this._loadImage(this.props.src);
+  removers = [];
+
+  get canvasContext() {
+    if (!this.canvas) {
+      return null;
+    }
+    this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
+    return this._canvasContext;
+  }
+
+  componentDidMount () {
+    this.loadImage(this.props);
+  }
+
+  componentWillReceiveProps (nextProps) {
+    if (this.props.src !== nextProps.src) {
+      this.loadImage(nextProps);
+    }
   }
 
-  componentWillReceiveProps(props) {
-    this._loadImage(props.src);
+  loadImage (props) {
+    this.removeEventListeners();
+    this.setState({ loading: true, error: false });
+    Promise.all([
+      this.loadPreviewCanvas(props),
+      this.loadOriginalImage(props),
+    ])
+      .then(() => {
+        this.setState({ loading: false, error: false });
+        this.clearPreviewCanvas();
+      })
+      .catch(() => this.setState({ loading: false, error: true }));
   }
 
-  _loadImage(src) {
+  loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => {
     const image = new Image();
+    const removeEventListeners = () => {
+      image.removeEventListener('error', handleError);
+      image.removeEventListener('load', handleLoad);
+    };
+    const handleError = () => {
+      removeEventListeners();
+      reject();
+    };
+    const handleLoad = () => {
+      removeEventListeners();
+      this.canvasContext.drawImage(image, 0, 0, width, height);
+      resolve();
+    };
+    image.addEventListener('error', handleError);
+    image.addEventListener('load', handleLoad);
+    image.src = previewSrc;
+    this.removers.push(removeEventListeners);
+  })
 
-    image.onerror = () => this.setState({ loading: false, error: true });
-    image.onload  = () => this.setState({ loading: false, error: false });
+  clearPreviewCanvas () {
+    const { width, height } = this.canvas;
+    this.canvasContext.clearRect(0, 0, width, height);
+  }
 
+  loadOriginalImage = ({ src }) => new Promise((resolve, reject) => {
+    const image = new Image();
+    const removeEventListeners = () => {
+      image.removeEventListener('error', handleError);
+      image.removeEventListener('load', handleLoad);
+    };
+    const handleError = () => {
+      removeEventListeners();
+      reject();
+    };
+    const handleLoad = () => {
+      removeEventListeners();
+      resolve();
+    };
+    image.addEventListener('error', handleError);
+    image.addEventListener('load', handleLoad);
     image.src = src;
+    this.removers.push(removeEventListeners);
+  });
 
-    this.setState({ loading: true });
+  removeEventListeners () {
+    this.removers.forEach(listeners => listeners());
+    this.removers = [];
   }
 
-  render() {
-    const { alt, src, previewSrc, width, height } = this.props;
+  setCanvasRef = c => {
+    this.canvas = c;
+  }
+
+  render () {
+    const { alt, src, width, height } = this.props;
     const { loading } = this.state;
 
+    const className = classNames('image-loader', {
+      'image-loader--loading': loading,
+    });
+
     return (
-      <div className='image-loader'>
-        <img
-          alt={alt}
-          className='image-loader__img'
-          src={src}
+      <div className={className}>
+        <canvas
+          className='image-loader__preview-canvas'
           width={width}
           height={height}
+          ref={this.setCanvasRef}
         />
 
-        {loading &&
+        {!loading && (
           <img
-            alt=''
-            src={previewSrc}
-            className='image-loader__preview-img'
+            alt={alt}
+            className='image-loader__img'
+            src={src}
+            width={width}
+            height={height}
           />
-        }
+        )}
       </div>
     );
   }
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index bb9723f5a..91ebd91fd 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -1099,20 +1099,22 @@
 
 .image-loader {
   position: relative;
-}
 
-.image-loader__preview-img {
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  filter: blur(2px);
-}
+  &.image-loader--loading {
+    .image-loader__preview-canvas {
+      filter: blur(2px);
+    }
+  }
 
-.media-modal img.image-loader__preview-img {
-  width: 100%;
-  height: 100%;
+  .image-loader__img {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    width: 100%;
+    height: 100%;
+    background-image: none;
+  }
 }
 
 .navigation-bar {
@@ -2933,6 +2935,7 @@ button.icon-button.active i.fa-retweet {
   position: relative;
 
   img,
+  canvas,
   video {
     max-width: 80vw;
     max-height: 80vh;
@@ -2940,7 +2943,8 @@ button.icon-button.active i.fa-retweet {
     height: auto;
   }
 
-  img {
+  img,
+  canvas {
     display: block;
     background: url('../images/void.png') repeat;
   }