From 5b7a93b02cfa4916bd7a99d876b0e6b567e639c3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 9 Jul 2020 12:53:16 +0200 Subject: [PATCH 1/2] Fix videos on public pages not using custom thumbnails (#14273) --- app/views/media/player.html.haml | 2 +- app/views/statuses/_detailed_status.html.haml | 2 +- app/views/statuses/_simple_status.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/media/player.html.haml b/app/views/media/player.html.haml index 1d0374897..ae47750e9 100644 --- a/app/views/media/player.html.haml +++ b/app/views/media/player.html.haml @@ -3,7 +3,7 @@ = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - if @media_attachment.video? - = react_component :video, src: @media_attachment.file.url(:original), preview: @media_attachment.file.url(:small), blurhash: @media_attachment.blurhash, width: 670, height: 380, editable: true, detailed: true, inline: true, alt: @media_attachment.description do + = react_component :video, src: @media_attachment.file.url(:original), preview: @media_attachment.thumbnail.present? ? @media_attachment.thumbnail.url : @media_attachment.file.url(:small), blurhash: @media_attachment.blurhash, width: 670, height: 380, editable: true, detailed: true, inline: true, alt: @media_attachment.description do %video{ controls: 'controls' } %source{ src: @media_attachment.file.url(:original) } - elsif @media_attachment.gifv? diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index dce122607..85b2ceea4 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -29,7 +29,7 @@ - if !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: status.sensitive?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do + = react_component :video, src: video.file.url(:original), preview: video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small), blurhash: video.blurhash, sensitive: status.sensitive?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.media_attachments.first.audio? - audio = status.media_attachments.first diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index b29e92ddc..67c6c0fd0 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -35,7 +35,7 @@ - if !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: status.sensitive?, width: 610, height: 343, inline: true, alt: video.description do + = react_component :video, src: video.file.url(:original), preview: video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small), blurhash: video.blurhash, sensitive: status.sensitive?, width: 610, height: 343, inline: true, alt: video.description do = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.media_attachments.first.audio? - audio = status.media_attachments.first From 61c07c37317f01c1ab4981826704750fe9937fe7 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Thu, 9 Jul 2020 18:01:30 +0700 Subject: [PATCH 2/2] Replace repetitive blurhash code with component (#14267) This commit replaces all unnecessarily repeated code for decoding and embedding blurhash canvases with separate component - . 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. --- .../mastodon/components/blurhash.js | 61 +++++++++++++++++++ .../mastodon/components/media_gallery.js | 46 ++++---------- .../account_gallery/components/media_item.js | 40 +++--------- .../features/status/components/card.js | 42 +++---------- .../mastodon/features/video/index.js | 38 +++--------- 5 files changed, 102 insertions(+), 125 deletions(-) create mode 100644 app/javascript/mastodon/components/blurhash.js 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} */ (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 ( + + ); +} + +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 ( ); @@ -232,7 +206,13 @@ class Item extends React.PureComponent { return (
- + {visible && thumbnail}
); 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 (
- + {visible && thumbnail} {!visible && icon} 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 = ; + let canvas = ( + + ); let thumbnail = ; let spoilerButton = (