Fix infinite scroll not working on profile media tab in web UI (#33860)
This commit is contained in:
		@@ -81,6 +81,7 @@ class ScrollableList extends PureComponent {
 | 
			
		||||
    bindToDocument: PropTypes.bool,
 | 
			
		||||
    preventScroll: PropTypes.bool,
 | 
			
		||||
    footer: PropTypes.node,
 | 
			
		||||
    className: PropTypes.string,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
@@ -325,7 +326,7 @@ class ScrollableList extends PureComponent {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
 | 
			
		||||
    const { children, scrollKey, className, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
 | 
			
		||||
    const { fullscreen } = this.state;
 | 
			
		||||
    const childrenCount = Children.count(children);
 | 
			
		||||
 | 
			
		||||
@@ -336,9 +337,9 @@ class ScrollableList extends PureComponent {
 | 
			
		||||
    if (showLoading) {
 | 
			
		||||
      scrollableArea = (
 | 
			
		||||
        <div className='scrollable scrollable--flex' ref={this.setRef}>
 | 
			
		||||
          <div role='feed' className='item-list'>
 | 
			
		||||
            {prepend}
 | 
			
		||||
          </div>
 | 
			
		||||
          {prepend}
 | 
			
		||||
 | 
			
		||||
          <div role='feed' className='item-list' />
 | 
			
		||||
 | 
			
		||||
          <div className='scrollable__append'>
 | 
			
		||||
            <LoadingIndicator />
 | 
			
		||||
@@ -350,9 +351,9 @@ class ScrollableList extends PureComponent {
 | 
			
		||||
    } else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
 | 
			
		||||
      scrollableArea = (
 | 
			
		||||
        <div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
 | 
			
		||||
          <div role='feed' className='item-list'>
 | 
			
		||||
            {prepend}
 | 
			
		||||
          {prepend}
 | 
			
		||||
 | 
			
		||||
          <div role='feed' className={classNames('item-list', className)}>
 | 
			
		||||
            {loadPending}
 | 
			
		||||
 | 
			
		||||
            {Children.map(this.props.children, (child, index) => (
 | 
			
		||||
 
 | 
			
		||||
@@ -11,11 +11,15 @@ import { Icon } from 'mastodon/components/icon';
 | 
			
		||||
import { formatTime } from 'mastodon/features/video';
 | 
			
		||||
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
 | 
			
		||||
import type { Status, MediaAttachment } from 'mastodon/models/status';
 | 
			
		||||
import { useAppSelector } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
export const MediaItem: React.FC<{
 | 
			
		||||
  attachment: MediaAttachment;
 | 
			
		||||
  onOpenMedia: (arg0: MediaAttachment) => void;
 | 
			
		||||
}> = ({ attachment, onOpenMedia }) => {
 | 
			
		||||
  const account = useAppSelector((state) =>
 | 
			
		||||
    state.accounts.get(attachment.getIn(['status', 'account']) as string),
 | 
			
		||||
  );
 | 
			
		||||
  const [visible, setVisible] = useState(
 | 
			
		||||
    (displayMedia !== 'hide_all' &&
 | 
			
		||||
      !attachment.getIn(['status', 'sensitive'])) ||
 | 
			
		||||
@@ -70,7 +74,6 @@ export const MediaItem: React.FC<{
 | 
			
		||||
  const lang = status.get('language') as string;
 | 
			
		||||
  const blurhash = attachment.get('blurhash') as string;
 | 
			
		||||
  const statusId = status.get('id') as string;
 | 
			
		||||
  const acct = status.getIn(['account', 'acct']) as string;
 | 
			
		||||
  const type = attachment.get('type') as string;
 | 
			
		||||
 | 
			
		||||
  let thumbnail;
 | 
			
		||||
@@ -181,7 +184,7 @@ export const MediaItem: React.FC<{
 | 
			
		||||
 | 
			
		||||
      <a
 | 
			
		||||
        className='media-gallery__item-thumbnail'
 | 
			
		||||
        href={`/@${acct}/${statusId}`}
 | 
			
		||||
        href={`/@${account?.acct}/${statusId}`}
 | 
			
		||||
        onClick={handleClick}
 | 
			
		||||
        target='_blank'
 | 
			
		||||
        rel='noopener noreferrer'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,241 +0,0 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
 | 
			
		||||
import { LoadMore } from 'mastodon/components/load_more';
 | 
			
		||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
 | 
			
		||||
import ScrollContainer from 'mastodon/containers/scroll_container';
 | 
			
		||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 | 
			
		||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
 | 
			
		||||
import { getAccountGallery } from 'mastodon/selectors';
 | 
			
		||||
 | 
			
		||||
import { expandAccountMediaTimeline } from '../../actions/timelines';
 | 
			
		||||
import { AccountHeader } from '../account_timeline/components/account_header';
 | 
			
		||||
import Column from '../ui/components/column';
 | 
			
		||||
 | 
			
		||||
import { MediaItem } from './components/media_item';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, { params: { acct, id } }) => {
 | 
			
		||||
  const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
 | 
			
		||||
 | 
			
		||||
  if (!accountId) {
 | 
			
		||||
    return {
 | 
			
		||||
      isLoading: true,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    accountId,
 | 
			
		||||
    isAccount: !!state.getIn(['accounts', accountId]),
 | 
			
		||||
    attachments: getAccountGallery(state, accountId),
 | 
			
		||||
    isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
 | 
			
		||||
    hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
 | 
			
		||||
    suspended: state.getIn(['accounts', accountId, 'suspended'], false),
 | 
			
		||||
    blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class LoadMoreMedia extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    maxId: PropTypes.string,
 | 
			
		||||
    onLoadMore: PropTypes.func.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleLoadMore = () => {
 | 
			
		||||
    this.props.onLoadMore(this.props.maxId);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    return (
 | 
			
		||||
      <LoadMore
 | 
			
		||||
        disabled={this.props.disabled}
 | 
			
		||||
        onClick={this.handleLoadMore}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class AccountGallery extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    params: PropTypes.shape({
 | 
			
		||||
      acct: PropTypes.string,
 | 
			
		||||
      id: PropTypes.string,
 | 
			
		||||
    }).isRequired,
 | 
			
		||||
    accountId: PropTypes.string,
 | 
			
		||||
    dispatch: PropTypes.func.isRequired,
 | 
			
		||||
    attachments: ImmutablePropTypes.list.isRequired,
 | 
			
		||||
    isLoading: PropTypes.bool,
 | 
			
		||||
    hasMore: PropTypes.bool,
 | 
			
		||||
    isAccount: PropTypes.bool,
 | 
			
		||||
    blockedBy: PropTypes.bool,
 | 
			
		||||
    suspended: PropTypes.bool,
 | 
			
		||||
    multiColumn: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    width: 323,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  _load () {
 | 
			
		||||
    const { accountId, isAccount, dispatch } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!isAccount) dispatch(fetchAccount(accountId));
 | 
			
		||||
    dispatch(expandAccountMediaTimeline(accountId));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    const { params: { acct }, accountId, dispatch } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (accountId) {
 | 
			
		||||
      this._load();
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(lookupAccount(acct));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate (prevProps) {
 | 
			
		||||
    const { params: { acct }, accountId, dispatch } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (prevProps.accountId !== accountId && accountId) {
 | 
			
		||||
      this._load();
 | 
			
		||||
    } else if (prevProps.params.acct !== acct) {
 | 
			
		||||
      dispatch(lookupAccount(acct));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleScrollToBottom = () => {
 | 
			
		||||
    if (this.props.hasMore) {
 | 
			
		||||
      this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleScroll = e => {
 | 
			
		||||
    const { scrollTop, scrollHeight, clientHeight } = e.target;
 | 
			
		||||
    const offset = scrollHeight - scrollTop - clientHeight;
 | 
			
		||||
 | 
			
		||||
    if (150 > offset && !this.props.isLoading) {
 | 
			
		||||
      this.handleScrollToBottom();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleLoadMore = maxId => {
 | 
			
		||||
    this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleLoadOlder = e => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    this.handleScrollToBottom();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleOpenMedia = attachment => {
 | 
			
		||||
    const { dispatch } = this.props;
 | 
			
		||||
    const statusId = attachment.getIn(['status', 'id']);
 | 
			
		||||
    const lang = attachment.getIn(['status', 'language']);
 | 
			
		||||
 | 
			
		||||
    if (attachment.get('type') === 'video') {
 | 
			
		||||
      dispatch(openModal({
 | 
			
		||||
        modalType: 'VIDEO',
 | 
			
		||||
        modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
 | 
			
		||||
      }));
 | 
			
		||||
    } else if (attachment.get('type') === 'audio') {
 | 
			
		||||
      dispatch(openModal({
 | 
			
		||||
        modalType: 'AUDIO',
 | 
			
		||||
        modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
 | 
			
		||||
      }));
 | 
			
		||||
    } else {
 | 
			
		||||
      const media = attachment.getIn(['status', 'media_attachments']);
 | 
			
		||||
      const index = media.findIndex(x => x.get('id') === attachment.get('id'));
 | 
			
		||||
 | 
			
		||||
      dispatch(openModal({
 | 
			
		||||
        modalType: 'MEDIA',
 | 
			
		||||
        modalProps: { media, index, statusId, lang },
 | 
			
		||||
      }));
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleRef = c => {
 | 
			
		||||
    if (c) {
 | 
			
		||||
      this.setState({ width: c.offsetWidth });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
 | 
			
		||||
    const { width } = this.state;
 | 
			
		||||
 | 
			
		||||
    if (!isAccount) {
 | 
			
		||||
      return (
 | 
			
		||||
        <BundleColumnError multiColumn={multiColumn} errorType='routing' />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!attachments && isLoading) {
 | 
			
		||||
      return (
 | 
			
		||||
        <Column>
 | 
			
		||||
          <LoadingIndicator />
 | 
			
		||||
        </Column>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let loadOlder = null;
 | 
			
		||||
 | 
			
		||||
    if (hasMore && !(isLoading && attachments.size === 0)) {
 | 
			
		||||
      loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let emptyMessage;
 | 
			
		||||
 | 
			
		||||
    if (suspended) {
 | 
			
		||||
      emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
 | 
			
		||||
    } else if (blockedBy) {
 | 
			
		||||
      emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Column>
 | 
			
		||||
        <ColumnBackButton />
 | 
			
		||||
 | 
			
		||||
        <ScrollContainer scrollKey='account_gallery'>
 | 
			
		||||
          <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
 | 
			
		||||
            <AccountHeader accountId={this.props.accountId} />
 | 
			
		||||
 | 
			
		||||
            {(suspended || blockedBy) ? (
 | 
			
		||||
              <div className='empty-column-indicator'>
 | 
			
		||||
                {emptyMessage}
 | 
			
		||||
              </div>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <div role='feed' className='account-gallery__container' ref={this.handleRef}>
 | 
			
		||||
                {attachments.map((attachment, index) => attachment === null ? (
 | 
			
		||||
                  <LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
 | 
			
		||||
                ))}
 | 
			
		||||
 | 
			
		||||
                {loadOlder}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            {isLoading && attachments.size === 0 && (
 | 
			
		||||
              <div className='scrollable__append'>
 | 
			
		||||
                <LoadingIndicator />
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        </ScrollContainer>
 | 
			
		||||
      </Column>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps)(AccountGallery);
 | 
			
		||||
							
								
								
									
										283
									
								
								app/javascript/mastodon/features/account_gallery/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								app/javascript/mastodon/features/account_gallery/index.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,283 @@
 | 
			
		||||
import { useEffect, useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { useParams } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import { createSelector } from '@reduxjs/toolkit';
 | 
			
		||||
import type { Map as ImmutableMap } from 'immutable';
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
 | 
			
		||||
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
 | 
			
		||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
 | 
			
		||||
import ScrollableList from 'mastodon/components/scrollable_list';
 | 
			
		||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
 | 
			
		||||
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
 | 
			
		||||
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
 | 
			
		||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 | 
			
		||||
import Column from 'mastodon/features/ui/components/column';
 | 
			
		||||
import type { MediaAttachment } from 'mastodon/models/media_attachment';
 | 
			
		||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
 | 
			
		||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
 | 
			
		||||
import type { RootState } from 'mastodon/store';
 | 
			
		||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
import { MediaItem } from './components/media_item';
 | 
			
		||||
 | 
			
		||||
const getAccountGallery = createSelector(
 | 
			
		||||
  [
 | 
			
		||||
    (state: RootState, accountId: string) =>
 | 
			
		||||
      (state.timelines as ImmutableMap<string, unknown>).getIn(
 | 
			
		||||
        [`account:${accountId}:media`, 'items'],
 | 
			
		||||
        ImmutableList(),
 | 
			
		||||
      ) as ImmutableList<string>,
 | 
			
		||||
    (state: RootState) => state.statuses,
 | 
			
		||||
  ],
 | 
			
		||||
  (statusIds, statuses) => {
 | 
			
		||||
    let items = ImmutableList<MediaAttachment>();
 | 
			
		||||
 | 
			
		||||
    statusIds.forEach((statusId) => {
 | 
			
		||||
      const status = statuses.get(statusId) as
 | 
			
		||||
        | ImmutableMap<string, unknown>
 | 
			
		||||
        | undefined;
 | 
			
		||||
 | 
			
		||||
      if (status) {
 | 
			
		||||
        items = items.concat(
 | 
			
		||||
          (
 | 
			
		||||
            status.get('media_attachments') as ImmutableList<MediaAttachment>
 | 
			
		||||
          ).map((media) => media.set('status', status)),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return items;
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
interface Params {
 | 
			
		||||
  acct?: string;
 | 
			
		||||
  id?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const RemoteHint: React.FC<{
 | 
			
		||||
  accountId: string;
 | 
			
		||||
}> = ({ accountId }) => {
 | 
			
		||||
  const account = useAppSelector((state) => state.accounts.get(accountId));
 | 
			
		||||
  const acct = account?.acct;
 | 
			
		||||
  const url = account?.url;
 | 
			
		||||
  const domain = acct ? acct.split('@')[1] : undefined;
 | 
			
		||||
 | 
			
		||||
  if (!url) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <TimelineHint
 | 
			
		||||
      url={url}
 | 
			
		||||
      message={
 | 
			
		||||
        <FormattedMessage
 | 
			
		||||
          id='hints.profiles.posts_may_be_missing'
 | 
			
		||||
          defaultMessage='Some posts from this profile may be missing.'
 | 
			
		||||
        />
 | 
			
		||||
      }
 | 
			
		||||
      label={
 | 
			
		||||
        <FormattedMessage
 | 
			
		||||
          id='hints.profiles.see_more_posts'
 | 
			
		||||
          defaultMessage='See more posts on {domain}'
 | 
			
		||||
          values={{ domain: <strong>{domain}</strong> }}
 | 
			
		||||
        />
 | 
			
		||||
      }
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const AccountGallery: React.FC<{
 | 
			
		||||
  multiColumn: boolean;
 | 
			
		||||
}> = ({ multiColumn }) => {
 | 
			
		||||
  const { acct, id } = useParams<Params>();
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
  const accountId = useAppSelector(
 | 
			
		||||
    (state) =>
 | 
			
		||||
      id ??
 | 
			
		||||
      (state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
 | 
			
		||||
  );
 | 
			
		||||
  const attachments = useAppSelector((state) =>
 | 
			
		||||
    accountId
 | 
			
		||||
      ? getAccountGallery(state, accountId)
 | 
			
		||||
      : ImmutableList<MediaAttachment>(),
 | 
			
		||||
  );
 | 
			
		||||
  const isLoading = useAppSelector((state) =>
 | 
			
		||||
    (state.timelines as ImmutableMap<string, unknown>).getIn([
 | 
			
		||||
      `account:${accountId}:media`,
 | 
			
		||||
      'isLoading',
 | 
			
		||||
    ]),
 | 
			
		||||
  );
 | 
			
		||||
  const hasMore = useAppSelector((state) =>
 | 
			
		||||
    (state.timelines as ImmutableMap<string, unknown>).getIn([
 | 
			
		||||
      `account:${accountId}:media`,
 | 
			
		||||
      'hasMore',
 | 
			
		||||
    ]),
 | 
			
		||||
  );
 | 
			
		||||
  const account = useAppSelector((state) =>
 | 
			
		||||
    accountId ? state.accounts.get(accountId) : undefined,
 | 
			
		||||
  );
 | 
			
		||||
  const blockedBy = useAppSelector(
 | 
			
		||||
    (state) =>
 | 
			
		||||
      state.relationships.getIn([accountId, 'blocked_by'], false) as boolean,
 | 
			
		||||
  );
 | 
			
		||||
  const suspended = useAppSelector(
 | 
			
		||||
    (state) => state.accounts.getIn([accountId, 'suspended'], false) as boolean,
 | 
			
		||||
  );
 | 
			
		||||
  const isAccount = !!account;
 | 
			
		||||
  const remote = account?.acct !== account?.username;
 | 
			
		||||
  const hidden = useAppSelector((state) =>
 | 
			
		||||
    accountId ? getAccountHidden(state, accountId) : false,
 | 
			
		||||
  );
 | 
			
		||||
  const maxId = attachments.last()?.getIn(['status', 'id']) as
 | 
			
		||||
    | string
 | 
			
		||||
    | undefined;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!accountId) {
 | 
			
		||||
      dispatch(lookupAccount(acct));
 | 
			
		||||
    }
 | 
			
		||||
  }, [dispatch, accountId, acct]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (accountId && !isAccount) {
 | 
			
		||||
      dispatch(fetchAccount(accountId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (accountId && isAccount) {
 | 
			
		||||
      void dispatch(expandAccountMediaTimeline(accountId));
 | 
			
		||||
    }
 | 
			
		||||
  }, [dispatch, accountId, isAccount]);
 | 
			
		||||
 | 
			
		||||
  const handleLoadMore = useCallback(() => {
 | 
			
		||||
    if (maxId) {
 | 
			
		||||
      void dispatch(expandAccountMediaTimeline(accountId, { maxId }));
 | 
			
		||||
    }
 | 
			
		||||
  }, [dispatch, accountId, maxId]);
 | 
			
		||||
 | 
			
		||||
  const handleOpenMedia = useCallback(
 | 
			
		||||
    (attachment: MediaAttachment) => {
 | 
			
		||||
      const statusId = attachment.getIn(['status', 'id']);
 | 
			
		||||
      const lang = attachment.getIn(['status', 'language']);
 | 
			
		||||
 | 
			
		||||
      if (attachment.get('type') === 'video') {
 | 
			
		||||
        dispatch(
 | 
			
		||||
          openModal({
 | 
			
		||||
            modalType: 'VIDEO',
 | 
			
		||||
            modalProps: {
 | 
			
		||||
              media: attachment,
 | 
			
		||||
              statusId,
 | 
			
		||||
              lang,
 | 
			
		||||
              options: { autoPlay: true },
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
      } else if (attachment.get('type') === 'audio') {
 | 
			
		||||
        dispatch(
 | 
			
		||||
          openModal({
 | 
			
		||||
            modalType: 'AUDIO',
 | 
			
		||||
            modalProps: {
 | 
			
		||||
              media: attachment,
 | 
			
		||||
              statusId,
 | 
			
		||||
              lang,
 | 
			
		||||
              options: { autoPlay: true },
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        const media = attachment.getIn([
 | 
			
		||||
          'status',
 | 
			
		||||
          'media_attachments',
 | 
			
		||||
        ]) as ImmutableList<MediaAttachment>;
 | 
			
		||||
        const index = media.findIndex(
 | 
			
		||||
          (x) => x.get('id') === attachment.get('id'),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        dispatch(
 | 
			
		||||
          openModal({
 | 
			
		||||
            modalType: 'MEDIA',
 | 
			
		||||
            modalProps: { media, index, statusId, lang },
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [dispatch],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (accountId && !isAccount) {
 | 
			
		||||
    return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let emptyMessage;
 | 
			
		||||
 | 
			
		||||
  if (accountId) {
 | 
			
		||||
    if (suspended) {
 | 
			
		||||
      emptyMessage = (
 | 
			
		||||
        <FormattedMessage
 | 
			
		||||
          id='empty_column.account_suspended'
 | 
			
		||||
          defaultMessage='Account suspended'
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else if (hidden) {
 | 
			
		||||
      emptyMessage = <LimitedAccountHint accountId={accountId} />;
 | 
			
		||||
    } else if (blockedBy) {
 | 
			
		||||
      emptyMessage = (
 | 
			
		||||
        <FormattedMessage
 | 
			
		||||
          id='empty_column.account_unavailable'
 | 
			
		||||
          defaultMessage='Profile unavailable'
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else if (remote && attachments.isEmpty()) {
 | 
			
		||||
      emptyMessage = <RemoteHint accountId={accountId} />;
 | 
			
		||||
    } else {
 | 
			
		||||
      emptyMessage = (
 | 
			
		||||
        <FormattedMessage
 | 
			
		||||
          id='empty_column.account_timeline'
 | 
			
		||||
          defaultMessage='No posts found'
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const forceEmptyState = suspended || blockedBy || hidden;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Column>
 | 
			
		||||
      <ColumnBackButton />
 | 
			
		||||
 | 
			
		||||
      <ScrollableList
 | 
			
		||||
        className='account-gallery__container'
 | 
			
		||||
        prepend={
 | 
			
		||||
          accountId && (
 | 
			
		||||
            <AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
        alwaysPrepend
 | 
			
		||||
        append={remote && accountId && <RemoteHint accountId={accountId} />}
 | 
			
		||||
        scrollKey='account_gallery'
 | 
			
		||||
        isLoading={isLoading}
 | 
			
		||||
        hasMore={!forceEmptyState && hasMore}
 | 
			
		||||
        onLoadMore={handleLoadMore}
 | 
			
		||||
        emptyMessage={emptyMessage}
 | 
			
		||||
        bindToDocument={!multiColumn}
 | 
			
		||||
      >
 | 
			
		||||
        {attachments.map((attachment) => (
 | 
			
		||||
          <MediaItem
 | 
			
		||||
            key={attachment.get('id') as string}
 | 
			
		||||
            attachment={attachment}
 | 
			
		||||
            onOpenMedia={handleOpenMedia}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
      </ScrollableList>
 | 
			
		||||
    </Column>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
export default AccountGallery;
 | 
			
		||||
@@ -91,25 +91,6 @@ export const makeGetReport = () => createSelector([
 | 
			
		||||
  (state, _, targetAccountId) => state.getIn(['accounts', targetAccountId]),
 | 
			
		||||
], (base, targetAccount) => base.set('target_account', targetAccount));
 | 
			
		||||
 | 
			
		||||
export const getAccountGallery = createSelector([
 | 
			
		||||
  (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
 | 
			
		||||
  state  => state.get('statuses'),
 | 
			
		||||
  (state, id) => state.getIn(['accounts', id]),
 | 
			
		||||
], (statusIds, statuses, account) => {
 | 
			
		||||
  let medias = ImmutableList();
 | 
			
		||||
 | 
			
		||||
  statusIds.forEach(statusId => {
 | 
			
		||||
    let status = statuses.get(statusId);
 | 
			
		||||
 | 
			
		||||
    if (status) {
 | 
			
		||||
      status = status.set('account', account);
 | 
			
		||||
      medias = medias.concat(status.get('media_attachments').map(media => media.set('status', status)));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return medias;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const getStatusList = createSelector([
 | 
			
		||||
  (state, type) => state.getIn(['status_lists', type, 'items']),
 | 
			
		||||
], (items) => items.toList());
 | 
			
		||||
 
 | 
			
		||||
@@ -7398,7 +7398,8 @@ a.status-card {
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .load-more {
 | 
			
		||||
  .load-more,
 | 
			
		||||
  .timeline-hint {
 | 
			
		||||
    grid-column: span 3;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user