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.

149 lines
4.4 KiB

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