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.

332 lines
9.2 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, displayMedia } 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. displayWidth: PropTypes.number,
  41. };
  42. static defaultProps = {
  43. standalone: false,
  44. index: 0,
  45. size: 1,
  46. };
  47. handleMouseEnter = (e) => {
  48. if (this.hoverToPlay()) {
  49. e.target.play();
  50. }
  51. }
  52. handleMouseLeave = (e) => {
  53. if (this.hoverToPlay()) {
  54. e.target.pause();
  55. e.target.currentTime = 0;
  56. }
  57. }
  58. hoverToPlay () {
  59. const { attachment } = this.props;
  60. return !autoPlayGif && attachment.get('type') === 'gifv';
  61. }
  62. handleClick = (e) => {
  63. const { index, onClick } = this.props;
  64. if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
  65. if (this.hoverToPlay()) {
  66. e.target.pause();
  67. e.target.currentTime = 0;
  68. }
  69. e.preventDefault();
  70. onClick(index);
  71. }
  72. e.stopPropagation();
  73. }
  74. handleMouseDown = (e) => {
  75. e.preventDefault();
  76. e.stopPropagation();
  77. }
  78. render () {
  79. const { attachment, index, size, standalone, letterbox, displayWidth } = this.props;
  80. let width = 50;
  81. let height = 100;
  82. let top = 'auto';
  83. let left = 'auto';
  84. let bottom = 'auto';
  85. let right = 'auto';
  86. if (size === 1) {
  87. width = 100;
  88. }
  89. if (size === 4 || (size === 3 && index > 0)) {
  90. height = 50;
  91. }
  92. if (size === 2) {
  93. if (index === 0) {
  94. right = '2px';
  95. } else {
  96. left = '2px';
  97. }
  98. } else if (size === 3) {
  99. if (index === 0) {
  100. right = '2px';
  101. } else if (index > 0) {
  102. left = '2px';
  103. }
  104. if (index === 1) {
  105. bottom = '2px';
  106. } else if (index > 1) {
  107. top = '2px';
  108. }
  109. } else if (size === 4) {
  110. if (index === 0 || index === 2) {
  111. right = '2px';
  112. }
  113. if (index === 1 || index === 3) {
  114. left = '2px';
  115. }
  116. if (index < 2) {
  117. bottom = '2px';
  118. } else {
  119. top = '2px';
  120. }
  121. }
  122. let thumbnail = '';
  123. if (attachment.get('type') === 'image') {
  124. const previewUrl = attachment.get('preview_url');
  125. const previewWidth = attachment.getIn(['meta', 'small', 'width']);
  126. const originalUrl = attachment.get('url');
  127. const originalWidth = attachment.getIn(['meta', 'original', 'width']);
  128. const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
  129. const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
  130. const sizes = hasSize && (displayWidth > 0) ? `${displayWidth * (width / 100)}px` : null;
  131. const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
  132. const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
  133. const x = ((focusX / 2) + .5) * 100;
  134. const y = ((focusY / -2) + .5) * 100;
  135. thumbnail = (
  136. <a
  137. className='media-gallery__item-thumbnail'
  138. href={attachment.get('remote_url') || originalUrl}
  139. onClick={this.handleClick}
  140. target='_blank'
  141. >
  142. <img
  143. className={letterbox ? 'letterbox' : null}
  144. src={previewUrl}
  145. srcSet={srcSet}
  146. sizes={sizes}
  147. alt={attachment.get('description')}
  148. title={attachment.get('description')}
  149. style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
  150. />
  151. </a>
  152. );
  153. } else if (attachment.get('type') === 'gifv') {
  154. const autoPlay = !isIOS() && autoPlayGif;
  155. thumbnail = (
  156. <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
  157. <video
  158. className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
  159. aria-label={attachment.get('description')}
  160. title={attachment.get('description')}
  161. role='application'
  162. src={attachment.get('url')}
  163. onClick={this.handleClick}
  164. onMouseEnter={this.handleMouseEnter}
  165. onMouseLeave={this.handleMouseLeave}
  166. onMouseDown={this.handleMouseDown}
  167. autoPlay={autoPlay}
  168. loop
  169. muted
  170. />
  171. <span className='media-gallery__gifv__label'>GIF</span>
  172. </div>
  173. );
  174. }
  175. return (
  176. <div className={classNames('media-gallery__item', { standalone, letterbox })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
  177. {thumbnail}
  178. </div>
  179. );
  180. }
  181. }
  182. @injectIntl
  183. export default class MediaGallery extends React.PureComponent {
  184. static propTypes = {
  185. sensitive: PropTypes.bool,
  186. revealed: PropTypes.bool,
  187. standalone: PropTypes.bool,
  188. letterbox: PropTypes.bool,
  189. fullwidth: PropTypes.bool,
  190. hidden: PropTypes.bool,
  191. media: ImmutablePropTypes.list.isRequired,
  192. size: PropTypes.object,
  193. onOpenMedia: PropTypes.func.isRequired,
  194. intl: PropTypes.object.isRequired,
  195. };
  196. static defaultProps = {
  197. standalone: false,
  198. };
  199. state = {
  200. visible: this.props.revealed === undefined ? (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all') : this.props.revealed,
  201. };
  202. componentWillReceiveProps (nextProps) {
  203. if (!is(nextProps.media, this.props.media)) {
  204. this.setState({ visible: nextProps.revealed === undefined ? (displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all') : nextProps.revealed });
  205. }
  206. }
  207. componentDidUpdate (prevProps) {
  208. if (this.node && this.node.offsetWidth && this.node.offsetWidth != this.state.width) {
  209. this.setState({
  210. width: this.node.offsetWidth,
  211. });
  212. }
  213. }
  214. handleOpen = () => {
  215. this.setState({ visible: !this.state.visible });
  216. }
  217. handleClick = (index) => {
  218. this.props.onOpenMedia(this.props.media, index);
  219. }
  220. handleRef = (node) => {
  221. this.node = node;
  222. if (node && node.offsetWidth && node.offsetWidth != this.state.width) {
  223. this.setState({
  224. width: node.offsetWidth,
  225. });
  226. }
  227. }
  228. isStandaloneEligible() {
  229. const { media, standalone } = this.props;
  230. return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
  231. }
  232. render () {
  233. const { media, intl, sensitive, letterbox, fullwidth } = this.props;
  234. const { width, visible } = this.state;
  235. const size = media.take(4).size;
  236. let children;
  237. const style = {};
  238. const computedClass = classNames('media-gallery', { 'full-width': fullwidth });
  239. if (this.isStandaloneEligible() && width) {
  240. style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
  241. } else if (width) {
  242. style.height = width / (16/9);
  243. } else {
  244. return (<div className={computedClass} ref={this.handleRef}></div>);
  245. }
  246. if (!visible) {
  247. let warning = <FormattedMessage {...(sensitive ? messages.warning : messages.hidden)} />;
  248. children = (
  249. <button className='media-spoiler' type='button' onClick={this.handleOpen}>
  250. <span className='media-spoiler__warning'>{warning}</span>
  251. <span className='media-spoiler__trigger'><FormattedMessage {...messages.toggle} /></span>
  252. </button>
  253. );
  254. } else {
  255. if (this.isStandaloneEligible()) {
  256. children = <Item standalone attachment={media.get(0)} onClick={this.handleClick} displayWidth={width} />;
  257. } else {
  258. children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} />);
  259. }
  260. }
  261. return (
  262. <div className={computedClass} style={style} ref={this.handleRef}>
  263. {visible ? (
  264. <div className='sensitive-info'>
  265. <IconButton
  266. icon='eye'
  267. onClick={this.handleOpen}
  268. overlay
  269. title={intl.formatMessage(messages.toggle_visible)}
  270. />
  271. {sensitive ? (
  272. <span className='sensitive-marker'>
  273. <FormattedMessage {...messages.sensitive} />
  274. </span>
  275. ) : null}
  276. </div>
  277. ) : null}
  278. {children}
  279. </div>
  280. );
  281. }
  282. }