media_modal.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import React from 'react';
  2. import ReactSwipeableViews from 'react-swipeable-views';
  3. import ImmutablePropTypes from 'react-immutable-proptypes';
  4. import PropTypes from 'prop-types';
  5. import Video from 'mastodon/features/video';
  6. import classNames from 'classnames';
  7. import { defineMessages, injectIntl } from 'react-intl';
  8. import IconButton from 'mastodon/components/icon_button';
  9. import ImmutablePureComponent from 'react-immutable-pure-component';
  10. import ImageLoader from './image_loader';
  11. import Icon from 'mastodon/components/icon';
  12. import GIFV from 'mastodon/components/gifv';
  13. import { disableSwiping } from 'mastodon/initial_state';
  14. import Footer from 'mastodon/features/picture_in_picture/components/footer';
  15. import { getAverageFromBlurhash } from 'mastodon/blurhash';
  16. const messages = defineMessages({
  17. close: { id: 'lightbox.close', defaultMessage: 'Close' },
  18. previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
  19. next: { id: 'lightbox.next', defaultMessage: 'Next' },
  20. });
  21. export default @injectIntl
  22. class MediaModal extends ImmutablePureComponent {
  23. static propTypes = {
  24. media: ImmutablePropTypes.list.isRequired,
  25. statusId: PropTypes.string,
  26. index: PropTypes.number.isRequired,
  27. onClose: PropTypes.func.isRequired,
  28. intl: PropTypes.object.isRequired,
  29. onChangeBackgroundColor: PropTypes.func.isRequired,
  30. currentTime: PropTypes.number,
  31. autoPlay: PropTypes.bool,
  32. volume: PropTypes.number,
  33. };
  34. state = {
  35. index: null,
  36. navigationHidden: false,
  37. zoomButtonHidden: false,
  38. };
  39. handleSwipe = (index) => {
  40. this.setState({ index: index % this.props.media.size });
  41. }
  42. handleTransitionEnd = () => {
  43. this.setState({
  44. zoomButtonHidden: false,
  45. });
  46. }
  47. handleNextClick = () => {
  48. this.setState({
  49. index: (this.getIndex() + 1) % this.props.media.size,
  50. zoomButtonHidden: true,
  51. });
  52. }
  53. handlePrevClick = () => {
  54. this.setState({
  55. index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size,
  56. zoomButtonHidden: true,
  57. });
  58. }
  59. handleChangeIndex = (e) => {
  60. const index = Number(e.currentTarget.getAttribute('data-index'));
  61. this.setState({
  62. index: index % this.props.media.size,
  63. zoomButtonHidden: true,
  64. });
  65. }
  66. handleKeyDown = (e) => {
  67. switch(e.key) {
  68. case 'ArrowLeft':
  69. this.handlePrevClick();
  70. e.preventDefault();
  71. e.stopPropagation();
  72. break;
  73. case 'ArrowRight':
  74. this.handleNextClick();
  75. e.preventDefault();
  76. e.stopPropagation();
  77. break;
  78. }
  79. }
  80. componentDidMount () {
  81. window.addEventListener('keydown', this.handleKeyDown, false);
  82. this._sendBackgroundColor();
  83. }
  84. componentDidUpdate (prevProps, prevState) {
  85. if (prevState.index !== this.state.index) {
  86. this._sendBackgroundColor();
  87. }
  88. }
  89. _sendBackgroundColor () {
  90. const { media, onChangeBackgroundColor } = this.props;
  91. const index = this.getIndex();
  92. const blurhash = media.getIn([index, 'blurhash']);
  93. if (blurhash) {
  94. const backgroundColor = getAverageFromBlurhash(blurhash);
  95. onChangeBackgroundColor(backgroundColor);
  96. }
  97. }
  98. componentWillUnmount () {
  99. window.removeEventListener('keydown', this.handleKeyDown);
  100. this.props.onChangeBackgroundColor(null);
  101. }
  102. getIndex () {
  103. return this.state.index !== null ? this.state.index : this.props.index;
  104. }
  105. toggleNavigation = () => {
  106. this.setState(prevState => ({
  107. navigationHidden: !prevState.navigationHidden,
  108. }));
  109. };
  110. render () {
  111. const { media, statusId, intl, onClose } = this.props;
  112. const { navigationHidden } = this.state;
  113. const index = this.getIndex();
  114. const leftNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' fixedWidth /></button>;
  115. const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' fixedWidth /></button>;
  116. const content = media.map((image) => {
  117. const width = image.getIn(['meta', 'original', 'width']) || null;
  118. const height = image.getIn(['meta', 'original', 'height']) || null;
  119. if (image.get('type') === 'image') {
  120. return (
  121. <ImageLoader
  122. previewSrc={image.get('preview_url')}
  123. src={image.get('url')}
  124. width={width}
  125. height={height}
  126. alt={image.get('description')}
  127. key={image.get('url')}
  128. onClick={this.toggleNavigation}
  129. zoomButtonHidden={this.state.zoomButtonHidden}
  130. />
  131. );
  132. } else if (image.get('type') === 'video') {
  133. const { currentTime, autoPlay, volume } = this.props;
  134. return (
  135. <Video
  136. preview={image.get('preview_url')}
  137. blurhash={image.get('blurhash')}
  138. src={image.get('url')}
  139. width={image.get('width')}
  140. height={image.get('height')}
  141. frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
  142. currentTime={currentTime || 0}
  143. autoPlay={autoPlay || false}
  144. volume={volume || 1}
  145. onCloseVideo={onClose}
  146. detailed
  147. alt={image.get('description')}
  148. key={image.get('url')}
  149. />
  150. );
  151. } else if (image.get('type') === 'gifv') {
  152. return (
  153. <GIFV
  154. src={image.get('url')}
  155. width={width}
  156. height={height}
  157. key={image.get('preview_url')}
  158. alt={image.get('description')}
  159. onClick={this.toggleNavigation}
  160. />
  161. );
  162. }
  163. return null;
  164. }).toArray();
  165. // you can't use 100vh, because the viewport height is taller
  166. // than the visible part of the document in some mobile
  167. // browsers when it's address bar is visible.
  168. // https://developers.google.com/web/updates/2016/12/url-bar-resizing
  169. const swipeableViewsStyle = {
  170. width: '100%',
  171. height: '100%',
  172. };
  173. const containerStyle = {
  174. alignItems: 'center', // center vertically
  175. };
  176. const navigationClassName = classNames('media-modal__navigation', {
  177. 'media-modal__navigation--hidden': navigationHidden,
  178. });
  179. let pagination;
  180. if (media.size > 1) {
  181. pagination = media.map((item, i) => (
  182. <button key={i} className={classNames('media-modal__page-dot', { active: i === index })} data-index={i} onClick={this.handleChangeIndex}>
  183. {i + 1}
  184. </button>
  185. ));
  186. }
  187. return (
  188. <div className='modal-root__modal media-modal'>
  189. <div className='media-modal__closer' role='presentation' onClick={onClose} >
  190. <ReactSwipeableViews
  191. style={swipeableViewsStyle}
  192. containerStyle={containerStyle}
  193. onChangeIndex={this.handleSwipe}
  194. onTransitionEnd={this.handleTransitionEnd}
  195. index={index}
  196. disabled={disableSwiping}
  197. >
  198. {content}
  199. </ReactSwipeableViews>
  200. </div>
  201. <div className={navigationClassName}>
  202. <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
  203. {leftNav}
  204. {rightNav}
  205. <div className='media-modal__overlay'>
  206. {pagination && <ul className='media-modal__pagination'>{pagination}</ul>}
  207. {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
  208. </div>
  209. </div>
  210. </div>
  211. );
  212. }
  213. }