--- /dev/null
+// @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);
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}}' },
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 });
}
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>
);
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>
);
-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';
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 });
}
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>
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--';
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;
}
}
- setCanvasRef = c => {
- this.canvas = c;
- }
-
handleImageLoad = () => {
this.setState({ previewLoaded: true });
}
);
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'>
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' },
this.volume = c;
}
- setCanvasRef = c => {
- this.canvas = c;
- }
-
handleClickRoot = e => e.stopPropagation();
handlePlay = () => {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize, { passive: true });
-
- if (this.props.blurhash) {
- this._decode();
- }
}
componentWillUnmount () {
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(() => {
}
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 = {};
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}