Add blurhash (#10630)
* Add blurhash * Use fallback color for spoiler when blurhash missing * Federate the blurhash and accept it as long as it's at most 5x5 * Display unknown media attachments as blurhash placeholders * Improve style of embed actions and spoiler button * Change blurhash resolution from 3x3 to 4x4 * Improve dependency definitions * Fix code style issues
This commit is contained in:
		
							parent
							
								
									c008911249
								
							
						
					
					
						commit
						fba96c808d
					
				
					 22 changed files with 234 additions and 60 deletions
				
			
		| 
						 | 
				
			
			@ -7,6 +7,7 @@ import classNames from 'classnames';
 | 
			
		|||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
 | 
			
		||||
import { displayMedia } from '../../initial_state';
 | 
			
		||||
import Icon from 'mastodon/components/icon';
 | 
			
		||||
import { decode } from 'blurhash';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  play: { id: 'video.play', defaultMessage: 'Play' },
 | 
			
		||||
| 
						 | 
				
			
			@ -102,6 +103,7 @@ class Video extends React.PureComponent {
 | 
			
		|||
    inline: PropTypes.bool,
 | 
			
		||||
    cacheWidth: PropTypes.func,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    blurhash: PropTypes.string,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
| 
						 | 
				
			
			@ -139,6 +141,7 @@ class Video extends React.PureComponent {
 | 
			
		|||
 | 
			
		||||
  setVideoRef = c => {
 | 
			
		||||
    this.video = c;
 | 
			
		||||
 | 
			
		||||
    if (this.video) {
 | 
			
		||||
      this.setState({ volume: this.video.volume, muted: this.video.muted });
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +155,10 @@ class Video extends React.PureComponent {
 | 
			
		|||
    this.volume = c;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setCanvasRef = c => {
 | 
			
		||||
    this.canvas = c;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClickRoot = e => e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
  handlePlay = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -170,7 +177,6 @@ class Video extends React.PureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  handleVolumeMouseDown = e => {
 | 
			
		||||
 | 
			
		||||
    document.addEventListener('mousemove', this.handleMouseVolSlide, true);
 | 
			
		||||
    document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
 | 
			
		||||
    document.addEventListener('touchmove', this.handleMouseVolSlide, true);
 | 
			
		||||
| 
						 | 
				
			
			@ -190,7 +196,6 @@ class Video extends React.PureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  handleMouseVolSlide = throttle(e => {
 | 
			
		||||
 | 
			
		||||
    const rect = this.volume.getBoundingClientRect();
 | 
			
		||||
    const x = (e.clientX - rect.left) / this.volWidth; //x position within the element.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -261,6 +266,10 @@ class Video extends React.PureComponent {
 | 
			
		|||
    document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
 | 
			
		||||
    document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
 | 
			
		||||
    document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
 | 
			
		||||
 | 
			
		||||
    if (this.props.blurhash) {
 | 
			
		||||
      this._decode();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
| 
						 | 
				
			
			@ -270,6 +279,24 @@ class Video extends React.PureComponent {
 | 
			
		|||
    document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate (prevProps) {
 | 
			
		||||
    if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
 | 
			
		||||
      this._decode();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _decode () {
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleFullscreenChange = () => {
 | 
			
		||||
    this.setState({ fullscreen: isFullscreen() });
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -314,6 +341,7 @@ class Video extends React.PureComponent {
 | 
			
		|||
 | 
			
		||||
  handleOpenVideo = () => {
 | 
			
		||||
    const { src, preview, width, height, alt } = this.props;
 | 
			
		||||
 | 
			
		||||
    const media = fromJS({
 | 
			
		||||
      type: 'video',
 | 
			
		||||
      url: src,
 | 
			
		||||
| 
						 | 
				
			
			@ -351,6 +379,7 @@ class Video extends React.PureComponent {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    let preload;
 | 
			
		||||
 | 
			
		||||
    if (startTime || fullscreen || dragging) {
 | 
			
		||||
      preload = 'auto';
 | 
			
		||||
    } else if (detailed) {
 | 
			
		||||
| 
						 | 
				
			
			@ -360,6 +389,7 @@ class Video extends React.PureComponent {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    let warning;
 | 
			
		||||
 | 
			
		||||
    if (sensitive) {
 | 
			
		||||
      warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
 | 
			
		||||
    } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -377,7 +407,9 @@ class Video extends React.PureComponent {
 | 
			
		|||
        onClick={this.handleClickRoot}
 | 
			
		||||
        tabIndex={0}
 | 
			
		||||
      >
 | 
			
		||||
        <video
 | 
			
		||||
        <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} />
 | 
			
		||||
 | 
			
		||||
        {revealed && <video
 | 
			
		||||
          ref={this.setVideoRef}
 | 
			
		||||
          src={src}
 | 
			
		||||
          poster={preview}
 | 
			
		||||
| 
						 | 
				
			
			@ -397,12 +429,13 @@ class Video extends React.PureComponent {
 | 
			
		|||
          onLoadedData={this.handleLoadedData}
 | 
			
		||||
          onProgress={this.handleProgress}
 | 
			
		||||
          onVolumeChange={this.handleVolumeChange}
 | 
			
		||||
        />
 | 
			
		||||
        />}
 | 
			
		||||
 | 
			
		||||
        <button type='button' className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}>
 | 
			
		||||
          <span className='video-player__spoiler__title'>{warning}</span>
 | 
			
		||||
          <span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
 | 
			
		||||
        </button>
 | 
			
		||||
        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}>
 | 
			
		||||
          <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
 | 
			
		||||
            <span className='spoiler-button__overlay__label'>{warning}</span>
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className={classNames('video-player__controls', { active: paused || hovered })}>
 | 
			
		||||
          <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue