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.

306 lines
8.3 KiB

7 years ago
  1. import React from 'react';
  2. import ImmutablePropTypes from 'react-immutable-proptypes';
  3. import PropTypes from 'prop-types';
  4. import { is } from 'immutable';
  5. import IconButton from './icon_button';
  6. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  7. import { isIOS } from 'flavours/glitch/util/is_mobile';
  8. import classNames from 'classnames';
  9. import { autoPlayGif, displaySensitiveMedia } from 'flavours/glitch/util/initial_state';
  10. const messages = defineMessages({
  11. hidden: {
  12. defaultMessage: 'Media hidden',
  13. id: 'status.media_hidden',
  14. },
  15. sensitive: {
  16. defaultMessage: 'Sensitive',
  17. id: 'media_gallery.sensitive',
  18. },
  19. toggle: {
  20. defaultMessage: 'Click to view',
  21. id: 'status.sensitive_toggle',
  22. },
  23. toggle_visible: {
  24. defaultMessage: 'Toggle visibility',
  25. id: 'media_gallery.toggle_visible',
  26. },
  27. warning: {
  28. defaultMessage: 'Sensitive content',
  29. id: 'status.sensitive_warning',
  30. },
  31. });
  32. class Item extends React.PureComponent {
  33. static propTypes = {
  34. attachment: ImmutablePropTypes.map.isRequired,
  35. standalone: PropTypes.bool,
  36. index: PropTypes.number.isRequired,
  37. size: PropTypes.number.isRequired,
  38. letterbox: PropTypes.bool,
  39. onClick: PropTypes.func.isRequired,
  40. };
  41. static defaultProps = {
  42. standalone: false,
  43. index: 0,
  44. size: 1,
  45. };
  46. handleMouseEnter = (e) => {
  47. if (this.hoverToPlay()) {
  48. e.target.play();
  49. }
  50. }
  51. handleMouseLeave = (e) => {
  52. if (this.hoverToPlay()) {
  53. e.target.pause();
  54. e.target.currentTime = 0;
  55. }
  56. }
  57. hoverToPlay () {
  58. const { attachment } = this.props;
  59. return !autoPlayGif && attachment.get('type') === 'gifv';
  60. }
  61. handleClick = (e) => {
  62. const { index, onClick } = this.props;
  63. if (e.button === 0) {
  64. e.preventDefault();
  65. onClick(index);
  66. }
  67. e.stopPropagation();
  68. }
  69. render () {
  70. const { attachment, index, size, standalone, letterbox } = this.props;
  71. let width = 50;
  72. let height = 100;
  73. let top = 'auto';
  74. let left = 'auto';
  75. let bottom = 'auto';
  76. let right = 'auto';
  77. if (size === 1) {
  78. width = 100;
  79. }
  80. if (size === 4 || (size === 3 && index > 0)) {
  81. height = 50;
  82. }
  83. if (size === 2) {
  84. if (index === 0) {
  85. right = '2px';
  86. } else {
  87. left = '2px';
  88. }
  89. } else if (size === 3) {
  90. if (index === 0) {
  91. right = '2px';
  92. } else if (index > 0) {
  93. left = '2px';
  94. }
  95. if (index === 1) {
  96. bottom = '2px';
  97. } else if (index > 1) {
  98. top = '2px';
  99. }
  100. } else if (size === 4) {
  101. if (index === 0 || index === 2) {
  102. right = '2px';
  103. }
  104. if (index === 1 || index === 3) {
  105. left = '2px';
  106. }
  107. if (index < 2) {
  108. bottom = '2px';
  109. } else {
  110. top = '2px';
  111. }
  112. }
  113. let thumbnail = '';
  114. if (attachment.get('type') === 'image') {
  115. const previewUrl = attachment.get('preview_url');
  116. const previewWidth = attachment.getIn(['meta', 'small', 'width']);
  117. const originalUrl = attachment.get('url');
  118. const originalWidth = attachment.getIn(['meta', 'original', 'width']);
  119. const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
  120. const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
  121. const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
  122. const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
  123. const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
  124. const x = ((focusX / 2) + .5) * 100;
  125. const y = ((focusY / -2) + .5) * 100;
  126. thumbnail = (
  127. <a
  128. className='media-gallery__item-thumbnail'
  129. href={attachment.get('remote_url') || originalUrl}
  130. onClick={this.handleClick}
  131. target='_blank'
  132. >
  133. <img
  134. className={letterbox ? 'letterbox' : null}
  135. src={previewUrl}
  136. srcSet={srcSet}
  137. sizes={sizes}
  138. alt={attachment.get('description')}
  139. title={attachment.get('description')}
  140. style={{ objectPosition: `${x}% ${y}%` }} />
  141. </a>
  142. );
  143. } else if (attachment.get('type') === 'gifv') {
  144. const autoPlay = !isIOS() && autoPlayGif;
  145. thumbnail = (
  146. <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
  147. <video
  148. className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
  149. aria-label={attachment.get('description')}
  150. role='application'
  151. src={attachment.get('url')}
  152. onClick={this.handleClick}
  153. onMouseEnter={this.handleMouseEnter}
  154. onMouseLeave={this.handleMouseLeave}
  155. autoPlay={autoPlay}
  156. loop
  157. muted
  158. />
  159. <span className='media-gallery__gifv__label'>GIF</span>
  160. </div>
  161. );
  162. }
  163. return (
  164. <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
  165. {thumbnail}
  166. </div>
  167. );
  168. }
  169. }
  170. @injectIntl
  171. export default class MediaGallery extends React.PureComponent {
  172. static propTypes = {
  173. sensitive: PropTypes.bool,
  174. revealed: PropTypes.bool,
  175. standalone: PropTypes.bool,
  176. letterbox: PropTypes.bool,
  177. fullwidth: PropTypes.bool,
  178. media: ImmutablePropTypes.list.isRequired,
  179. size: PropTypes.object,
  180. onOpenMedia: PropTypes.func.isRequired,
  181. intl: PropTypes.object.isRequired,
  182. };
  183. static defaultProps = {
  184. standalone: false,
  185. };
  186. state = {
  187. visible: this.props.revealed === undefined ? (!this.props.sensitive || displaySensitiveMedia) : this.props.revealed,
  188. };
  189. componentWillReceiveProps (nextProps) {
  190. if (!is(nextProps.media, this.props.media)) {
  191. this.setState({ visible: !nextProps.sensitive });
  192. }
  193. }
  194. handleOpen = () => {
  195. this.setState({ visible: !this.state.visible });
  196. }
  197. handleClick = (index) => {
  198. this.props.onOpenMedia(this.props.media, index);
  199. }
  200. handleRef = (node) => {
  201. if (node && this.isStandaloneEligible()) {
  202. // offsetWidth triggers a layout, so only calculate when we need to
  203. this.setState({
  204. width: node.offsetWidth,
  205. });
  206. }
  207. }
  208. isStandaloneEligible() {
  209. const { media, standalone } = this.props;
  210. return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
  211. }
  212. render () {
  213. const { media, intl, sensitive, letterbox, fullwidth } = this.props;
  214. const { width, visible } = this.state;
  215. const size = media.take(4).size;
  216. let children;
  217. const style = {};
  218. if (this.isStandaloneEligible() && width) {
  219. style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
  220. }
  221. if (!visible) {
  222. let warning = <FormattedMessage {...(sensitive ? messages.warning : messages.hidden)} />;
  223. children = (
  224. <button className='media-spoiler' type='button' onClick={this.handleOpen}>
  225. <span className='media-spoiler__warning'>{warning}</span>
  226. <span className='media-spoiler__trigger'><FormattedMessage {...messages.toggle} /></span>
  227. </button>
  228. );
  229. } else {
  230. if (this.isStandaloneEligible()) {
  231. children = <Item standalone attachment={media.get(0)} onClick={this.handleClick} />;
  232. } else {
  233. children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} />);
  234. }
  235. }
  236. const computedClass = classNames('media-gallery', `size-${size}`, { 'full-width': fullwidth });
  237. return (
  238. <div className={computedClass} style={style} ref={this.handleRef}>
  239. {visible ? (
  240. <div className='sensitive-info'>
  241. <IconButton
  242. icon='eye'
  243. onClick={this.handleOpen}
  244. overlay
  245. title={intl.formatMessage(messages.toggle_visible)}
  246. />
  247. {sensitive ? (
  248. <span className='sensitive-marker'>
  249. <FormattedMessage {...messages.sensitive} />
  250. </span>
  251. ) : null}
  252. </div>
  253. ) : null}
  254. {children}
  255. </div>
  256. );
  257. }
  258. }