index.jsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import PropTypes from 'prop-types';
  2. import { FormattedMessage } from 'react-intl';
  3. import ImmutablePropTypes from 'react-immutable-proptypes';
  4. import ImmutablePureComponent from 'react-immutable-pure-component';
  5. import { connect } from 'react-redux';
  6. import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
  7. import { openModal } from 'mastodon/actions/modal';
  8. import { ColumnBackButton } from 'mastodon/components/column_back_button';
  9. import { LoadMore } from 'mastodon/components/load_more';
  10. import { LoadingIndicator } from 'mastodon/components/loading_indicator';
  11. import ScrollContainer from 'mastodon/containers/scroll_container';
  12. import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
  13. import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
  14. import { getAccountGallery } from 'mastodon/selectors';
  15. import { expandAccountMediaTimeline } from '../../actions/timelines';
  16. import HeaderContainer from '../account_timeline/containers/header_container';
  17. import Column from '../ui/components/column';
  18. import MediaItem from './components/media_item';
  19. const mapStateToProps = (state, { params: { acct, id } }) => {
  20. const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
  21. if (!accountId) {
  22. return {
  23. isLoading: true,
  24. };
  25. }
  26. return {
  27. accountId,
  28. isAccount: !!state.getIn(['accounts', accountId]),
  29. attachments: getAccountGallery(state, accountId),
  30. isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
  31. hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
  32. suspended: state.getIn(['accounts', accountId, 'suspended'], false),
  33. blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
  34. };
  35. };
  36. class LoadMoreMedia extends ImmutablePureComponent {
  37. static propTypes = {
  38. maxId: PropTypes.string,
  39. onLoadMore: PropTypes.func.isRequired,
  40. };
  41. handleLoadMore = () => {
  42. this.props.onLoadMore(this.props.maxId);
  43. };
  44. render () {
  45. return (
  46. <LoadMore
  47. disabled={this.props.disabled}
  48. onClick={this.handleLoadMore}
  49. />
  50. );
  51. }
  52. }
  53. class AccountGallery extends ImmutablePureComponent {
  54. static propTypes = {
  55. params: PropTypes.shape({
  56. acct: PropTypes.string,
  57. id: PropTypes.string,
  58. }).isRequired,
  59. accountId: PropTypes.string,
  60. dispatch: PropTypes.func.isRequired,
  61. attachments: ImmutablePropTypes.list.isRequired,
  62. isLoading: PropTypes.bool,
  63. hasMore: PropTypes.bool,
  64. isAccount: PropTypes.bool,
  65. blockedBy: PropTypes.bool,
  66. suspended: PropTypes.bool,
  67. multiColumn: PropTypes.bool,
  68. };
  69. state = {
  70. width: 323,
  71. };
  72. _load () {
  73. const { accountId, isAccount, dispatch } = this.props;
  74. if (!isAccount) dispatch(fetchAccount(accountId));
  75. dispatch(expandAccountMediaTimeline(accountId));
  76. }
  77. componentDidMount () {
  78. const { params: { acct }, accountId, dispatch } = this.props;
  79. if (accountId) {
  80. this._load();
  81. } else {
  82. dispatch(lookupAccount(acct));
  83. }
  84. }
  85. componentDidUpdate (prevProps) {
  86. const { params: { acct }, accountId, dispatch } = this.props;
  87. if (prevProps.accountId !== accountId && accountId) {
  88. this._load();
  89. } else if (prevProps.params.acct !== acct) {
  90. dispatch(lookupAccount(acct));
  91. }
  92. }
  93. handleScrollToBottom = () => {
  94. if (this.props.hasMore) {
  95. this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
  96. }
  97. };
  98. handleScroll = e => {
  99. const { scrollTop, scrollHeight, clientHeight } = e.target;
  100. const offset = scrollHeight - scrollTop - clientHeight;
  101. if (150 > offset && !this.props.isLoading) {
  102. this.handleScrollToBottom();
  103. }
  104. };
  105. handleLoadMore = maxId => {
  106. this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
  107. };
  108. handleLoadOlder = e => {
  109. e.preventDefault();
  110. this.handleScrollToBottom();
  111. };
  112. handleOpenMedia = attachment => {
  113. const { dispatch } = this.props;
  114. const statusId = attachment.getIn(['status', 'id']);
  115. const lang = attachment.getIn(['status', 'language']);
  116. if (attachment.get('type') === 'video') {
  117. dispatch(openModal({
  118. modalType: 'VIDEO',
  119. modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
  120. }));
  121. } else if (attachment.get('type') === 'audio') {
  122. dispatch(openModal({
  123. modalType: 'AUDIO',
  124. modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
  125. }));
  126. } else {
  127. const media = attachment.getIn(['status', 'media_attachments']);
  128. const index = media.findIndex(x => x.get('id') === attachment.get('id'));
  129. dispatch(openModal({
  130. modalType: 'MEDIA',
  131. modalProps: { media, index, statusId, lang },
  132. }));
  133. }
  134. };
  135. handleRef = c => {
  136. if (c) {
  137. this.setState({ width: c.offsetWidth });
  138. }
  139. };
  140. render () {
  141. const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
  142. const { width } = this.state;
  143. if (!isAccount) {
  144. return (
  145. <BundleColumnError multiColumn={multiColumn} errorType='routing' />
  146. );
  147. }
  148. if (!attachments && isLoading) {
  149. return (
  150. <Column>
  151. <LoadingIndicator />
  152. </Column>
  153. );
  154. }
  155. let loadOlder = null;
  156. if (hasMore && !(isLoading && attachments.size === 0)) {
  157. loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
  158. }
  159. let emptyMessage;
  160. if (suspended) {
  161. emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
  162. } else if (blockedBy) {
  163. emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
  164. }
  165. return (
  166. <Column>
  167. <ColumnBackButton />
  168. <ScrollContainer scrollKey='account_gallery'>
  169. <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
  170. <HeaderContainer accountId={this.props.accountId} />
  171. {(suspended || blockedBy) ? (
  172. <div className='empty-column-indicator'>
  173. {emptyMessage}
  174. </div>
  175. ) : (
  176. <div role='feed' className='account-gallery__container' ref={this.handleRef}>
  177. {attachments.map((attachment, index) => attachment === null ? (
  178. <LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
  179. ) : (
  180. <MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
  181. ))}
  182. {loadOlder}
  183. </div>
  184. )}
  185. {isLoading && attachments.size === 0 && (
  186. <div className='scrollable__append'>
  187. <LoadingIndicator />
  188. </div>
  189. )}
  190. </div>
  191. </ScrollContainer>
  192. </Column>
  193. );
  194. }
  195. }
  196. export default connect(mapStateToProps)(AccountGallery);