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.

258 lines
6.2 KiB

  1. import ImmutablePropTypes from 'react-immutable-proptypes';
  2. import PropTypes from 'prop-types';
  3. import IconButton from './icon_button';
  4. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  5. import { isIOS } from '../is_mobile';
  6. const messages = defineMessages({
  7. toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }
  8. });
  9. const outerStyle = {
  10. marginTop: '8px',
  11. overflow: 'hidden',
  12. width: '100%',
  13. boxSizing: 'border-box',
  14. position: 'relative'
  15. };
  16. const spoilerStyle = {
  17. textAlign: 'center',
  18. height: '100%',
  19. cursor: 'pointer',
  20. display: 'flex',
  21. alignItems: 'center',
  22. justifyContent: 'center',
  23. flexDirection: 'column'
  24. };
  25. const spoilerSpanStyle = {
  26. display: 'block',
  27. fontSize: '14px',
  28. };
  29. const spoilerSubSpanStyle = {
  30. display: 'block',
  31. fontSize: '11px',
  32. fontWeight: '500'
  33. };
  34. const spoilerButtonStyle = {
  35. position: 'absolute',
  36. top: '4px',
  37. left: '4px',
  38. zIndex: '100'
  39. };
  40. const itemStyle = {
  41. boxSizing: 'border-box',
  42. position: 'relative',
  43. float: 'left',
  44. border: 'none',
  45. display: 'block'
  46. };
  47. const thumbStyle = {
  48. display: 'block',
  49. width: '100%',
  50. height: '100%',
  51. textDecoration: 'none',
  52. backgroundSize: 'cover',
  53. cursor: 'zoom-in'
  54. };
  55. const gifvThumbStyle = {
  56. position: 'relative',
  57. zIndex: '1',
  58. width: '100%',
  59. height: '100%',
  60. objectFit: 'cover',
  61. top: '50%',
  62. transform: 'translateY(-50%)',
  63. cursor: 'zoom-in'
  64. };
  65. class Item extends React.PureComponent {
  66. constructor (props, context) {
  67. super(props, context);
  68. this.handleClick = this.handleClick.bind(this);
  69. }
  70. handleClick (e) {
  71. const { index, onClick } = this.props;
  72. if (e.button === 0) {
  73. e.preventDefault();
  74. onClick(index);
  75. }
  76. e.stopPropagation();
  77. }
  78. render () {
  79. const { attachment, index, size } = 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. thumbnail = (
  125. <a
  126. href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')}
  127. onClick={this.handleClick}
  128. target='_blank'
  129. style={{ background: `url(${attachment.get('preview_url')}) no-repeat center`, ...thumbStyle }}
  130. />
  131. );
  132. } else if (attachment.get('type') === 'gifv') {
  133. const autoPlay = !isIOS() && this.props.autoPlayGif;
  134. thumbnail = (
  135. <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }} className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
  136. <video
  137. src={attachment.get('url')}
  138. onClick={this.handleClick}
  139. autoPlay={autoPlay}
  140. loop={true}
  141. muted={true}
  142. style={gifvThumbStyle}
  143. />
  144. <span className='media-gallery__gifv__label'>GIF</span>
  145. </div>
  146. );
  147. }
  148. return (
  149. <div key={attachment.get('id')} style={{ ...itemStyle, left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
  150. {thumbnail}
  151. </div>
  152. );
  153. }
  154. }
  155. Item.propTypes = {
  156. attachment: ImmutablePropTypes.map.isRequired,
  157. index: PropTypes.number.isRequired,
  158. size: PropTypes.number.isRequired,
  159. onClick: PropTypes.func.isRequired,
  160. autoPlayGif: PropTypes.bool.isRequired
  161. };
  162. class MediaGallery extends React.PureComponent {
  163. constructor (props, context) {
  164. super(props, context);
  165. this.state = {
  166. visible: !props.sensitive
  167. };
  168. this.handleOpen = this.handleOpen.bind(this);
  169. this.handleClick = this.handleClick.bind(this);
  170. }
  171. handleOpen (e) {
  172. this.setState({ visible: !this.state.visible });
  173. }
  174. handleClick (index) {
  175. this.props.onOpenMedia(this.props.media, index);
  176. }
  177. render () {
  178. const { media, intl, sensitive } = this.props;
  179. let children;
  180. if (!this.state.visible) {
  181. let warning;
  182. if (sensitive) {
  183. warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
  184. } else {
  185. warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
  186. }
  187. children = (
  188. <div role='button' tabIndex='0' style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
  189. <span style={spoilerSpanStyle}>{warning}</span>
  190. <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
  191. </div>
  192. );
  193. } else {
  194. const size = media.take(4).size;
  195. children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />);
  196. }
  197. return (
  198. <div style={{ ...outerStyle, height: `${this.props.height}px` }}>
  199. <div style={{ ...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block' }}>
  200. <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
  201. </div>
  202. {children}
  203. </div>
  204. );
  205. }
  206. }
  207. MediaGallery.propTypes = {
  208. sensitive: PropTypes.bool,
  209. media: ImmutablePropTypes.list.isRequired,
  210. height: PropTypes.number.isRequired,
  211. onOpenMedia: PropTypes.func.isRequired,
  212. intl: PropTypes.object.isRequired,
  213. autoPlayGif: PropTypes.bool.isRequired
  214. };
  215. export default injectIntl(MediaGallery);