You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

163 lines
4.8 KiB

  1. import { decode } from 'blurhash';
  2. import classNames from 'classnames';
  3. import Icon from 'mastodon/components/icon';
  4. import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
  5. import { isIOS } from 'mastodon/is_mobile';
  6. import PropTypes from 'prop-types';
  7. import React from 'react';
  8. import ImmutablePropTypes from 'react-immutable-proptypes';
  9. import ImmutablePureComponent from 'react-immutable-pure-component';
  10. export default class MediaItem extends ImmutablePureComponent {
  11. static propTypes = {
  12. attachment: ImmutablePropTypes.map.isRequired,
  13. displayWidth: PropTypes.number.isRequired,
  14. onOpenMedia: PropTypes.func.isRequired,
  15. };
  16. state = {
  17. visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
  18. loaded: false,
  19. };
  20. componentDidMount () {
  21. if (this.props.attachment.get('blurhash')) {
  22. this._decode();
  23. }
  24. }
  25. componentDidUpdate (prevProps) {
  26. if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
  27. this._decode();
  28. }
  29. }
  30. _decode () {
  31. const hash = this.props.attachment.get('blurhash');
  32. const pixels = decode(hash, 32, 32);
  33. if (pixels) {
  34. const ctx = this.canvas.getContext('2d');
  35. const imageData = new ImageData(pixels, 32, 32);
  36. ctx.putImageData(imageData, 0, 0);
  37. }
  38. }
  39. setCanvasRef = c => {
  40. this.canvas = c;
  41. }
  42. handleImageLoad = () => {
  43. this.setState({ loaded: true });
  44. }
  45. handleMouseEnter = e => {
  46. if (this.hoverToPlay()) {
  47. e.target.play();
  48. }
  49. }
  50. handleMouseLeave = e => {
  51. if (this.hoverToPlay()) {
  52. e.target.pause();
  53. e.target.currentTime = 0;
  54. }
  55. }
  56. hoverToPlay () {
  57. return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
  58. }
  59. handleClick = e => {
  60. if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
  61. e.preventDefault();
  62. if (this.state.visible) {
  63. this.props.onOpenMedia(this.props.attachment);
  64. } else {
  65. this.setState({ visible: true });
  66. }
  67. }
  68. }
  69. render () {
  70. const { attachment, displayWidth } = this.props;
  71. const { visible, loaded } = this.state;
  72. const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
  73. const height = width;
  74. const status = attachment.get('status');
  75. const title = status.get('spoiler_text') || attachment.get('description');
  76. let thumbnail = '';
  77. let icon;
  78. if (attachment.get('type') === 'unknown') {
  79. // Skip
  80. } else if (attachment.get('type') === 'audio') {
  81. thumbnail = (
  82. <span className='account-gallery__item__icons'>
  83. <Icon id='music' />
  84. </span>
  85. );
  86. } else if (attachment.get('type') === 'image') {
  87. const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
  88. const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
  89. const x = ((focusX / 2) + .5) * 100;
  90. const y = ((focusY / -2) + .5) * 100;
  91. thumbnail = (
  92. <img
  93. src={attachment.get('preview_url')}
  94. alt={attachment.get('description')}
  95. title={attachment.get('description')}
  96. style={{ objectPosition: `${x}% ${y}%` }}
  97. onLoad={this.handleImageLoad}
  98. />
  99. );
  100. } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) {
  101. const autoPlay = !isIOS() && autoPlayGif;
  102. const label = attachment.get('type') === 'video' ? <Icon id='play' /> : 'GIF';
  103. thumbnail = (
  104. <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
  105. <video
  106. className='media-gallery__item-gifv-thumbnail'
  107. aria-label={attachment.get('description')}
  108. title={attachment.get('description')}
  109. role='application'
  110. src={attachment.get('url')}
  111. onMouseEnter={this.handleMouseEnter}
  112. onMouseLeave={this.handleMouseLeave}
  113. autoPlay={autoPlay}
  114. loop
  115. muted
  116. />
  117. <span className='media-gallery__gifv__label'>{label}</span>
  118. </div>
  119. );
  120. }
  121. if (!visible) {
  122. icon = (
  123. <span className='account-gallery__item__icons'>
  124. <Icon id='eye-slash' />
  125. </span>
  126. );
  127. }
  128. return (
  129. <div className='account-gallery__item' style={{ width, height }}>
  130. <a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
  131. <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
  132. {visible && thumbnail}
  133. {!visible && icon}
  134. </a>
  135. </div>
  136. );
  137. }
  138. }