Add limited attribute to accounts in REST API and a warning in web UI (#18344)
				
					
				
			This commit is contained in:
		
							parent
							
								
									898fe2fa8e
								
							
						
					
					
						commit
						b4d373a3df
					
				
					 13 changed files with 166 additions and 59 deletions
				
			
		| 
						 | 
				
			
			@ -77,6 +77,8 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
 | 
			
		|||
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
 | 
			
		||||
export const FOLLOW_REQUEST_REJECT_FAIL    = 'FOLLOW_REQUEST_REJECT_FAIL';
 | 
			
		||||
 | 
			
		||||
export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
 | 
			
		||||
 | 
			
		||||
export function fetchAccount(id) {
 | 
			
		||||
  return (dispatch, getState) => {
 | 
			
		||||
    dispatch(fetchRelationships([id]));
 | 
			
		||||
| 
						 | 
				
			
			@ -780,3 +782,8 @@ export function unpinAccountFail(error) {
 | 
			
		|||
    error,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const revealAccount = id => ({
 | 
			
		||||
  type: ACCOUNT_REVEAL,
 | 
			
		||||
  id,
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,11 +2,12 @@ import React from 'react';
 | 
			
		|||
import PropTypes from 'prop-types';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import { autoPlayGif } from '../initial_state';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
export default class Avatar extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    account: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
    account: ImmutablePropTypes.map,
 | 
			
		||||
    size: PropTypes.number.isRequired,
 | 
			
		||||
    style: PropTypes.object,
 | 
			
		||||
    inline: PropTypes.bool,
 | 
			
		||||
| 
						 | 
				
			
			@ -37,15 +38,6 @@ export default class Avatar extends React.PureComponent {
 | 
			
		|||
    const { account, size, animate, inline } = this.props;
 | 
			
		||||
    const { hovering } = this.state;
 | 
			
		||||
 | 
			
		||||
    const src = account.get('avatar');
 | 
			
		||||
    const staticSrc = account.get('avatar_static');
 | 
			
		||||
 | 
			
		||||
    let className = 'account__avatar';
 | 
			
		||||
 | 
			
		||||
    if (inline) {
 | 
			
		||||
      className = className + ' account__avatar-inline';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const style = {
 | 
			
		||||
      ...this.props.style,
 | 
			
		||||
      width: `${size}px`,
 | 
			
		||||
| 
						 | 
				
			
			@ -53,15 +45,21 @@ export default class Avatar extends React.PureComponent {
 | 
			
		|||
      backgroundSize: `${size}px ${size}px`,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (hovering || animate) {
 | 
			
		||||
      style.backgroundImage = `url(${src})`;
 | 
			
		||||
    } else {
 | 
			
		||||
      style.backgroundImage = `url(${staticSrc})`;
 | 
			
		||||
    if (account) {
 | 
			
		||||
      const src = account.get('avatar');
 | 
			
		||||
      const staticSrc = account.get('avatar_static');
 | 
			
		||||
 | 
			
		||||
      if (hovering || animate) {
 | 
			
		||||
        style.backgroundImage = `url(${src})`;
 | 
			
		||||
      } else {
 | 
			
		||||
        style.backgroundImage = `url(${staticSrc})`;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className={className}
 | 
			
		||||
        className={classNames('account__avatar', { 'account__avatar-inline': inline })}
 | 
			
		||||
        onMouseEnter={this.handleMouseEnter}
 | 
			
		||||
        onMouseLeave={this.handleMouseLeave}
 | 
			
		||||
        style={style}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,6 +82,7 @@ class Header extends ImmutablePureComponent {
 | 
			
		|||
    onEditAccountNote: PropTypes.func.isRequired,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    domain: PropTypes.string.isRequired,
 | 
			
		||||
    hidden: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  openEditProfile = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +124,7 @@ class Header extends ImmutablePureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { account, intl, domain } = this.props;
 | 
			
		||||
    const { account, hidden, intl, domain } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!account) {
 | 
			
		||||
      return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -267,21 +268,25 @@ class Header extends ImmutablePureComponent {
 | 
			
		|||
            {!suspended && info}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
 | 
			
		||||
          {!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className='account__header__bar'>
 | 
			
		||||
          <div className='account__header__tabs'>
 | 
			
		||||
            <a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
 | 
			
		||||
              <Avatar account={account} size={90} />
 | 
			
		||||
              <Avatar account={suspended || hidden ? undefined : account} size={90} />
 | 
			
		||||
            </a>
 | 
			
		||||
 | 
			
		||||
            <div className='spacer' />
 | 
			
		||||
 | 
			
		||||
            {!suspended && (
 | 
			
		||||
              <div className='account__header__tabs__buttons'>
 | 
			
		||||
                {actionBtn}
 | 
			
		||||
                {bellBtn}
 | 
			
		||||
                {!hidden && (
 | 
			
		||||
                  <React.Fragment>
 | 
			
		||||
                    {actionBtn}
 | 
			
		||||
                    {bellBtn}
 | 
			
		||||
                  </React.Fragment>
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
                <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
 | 
			
		||||
              </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -295,30 +300,30 @@ class Header extends ImmutablePureComponent {
 | 
			
		|||
            </h1>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className='account__header__extra'>
 | 
			
		||||
            <div className='account__header__bio'>
 | 
			
		||||
              {fields.size > 0 && (
 | 
			
		||||
                <div className='account__header__fields'>
 | 
			
		||||
                  {fields.map((pair, i) => (
 | 
			
		||||
                    <dl key={i}>
 | 
			
		||||
                      <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
 | 
			
		||||
          {!(suspended || hidden) && (
 | 
			
		||||
            <div className='account__header__extra'>
 | 
			
		||||
              <div className='account__header__bio'>
 | 
			
		||||
                {fields.size > 0 && (
 | 
			
		||||
                  <div className='account__header__fields'>
 | 
			
		||||
                    {fields.map((pair, i) => (
 | 
			
		||||
                      <dl key={i}>
 | 
			
		||||
                        <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
 | 
			
		||||
 | 
			
		||||
                      <dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}>
 | 
			
		||||
                        {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
 | 
			
		||||
                      </dd>
 | 
			
		||||
                    </dl>
 | 
			
		||||
                  ))}
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
                        <dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}>
 | 
			
		||||
                          {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
 | 
			
		||||
                        </dd>
 | 
			
		||||
                      </dl>
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
              {account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
 | 
			
		||||
                {account.get('id') !== me && <AccountNoteContainer account={account} />}
 | 
			
		||||
 | 
			
		||||
              {account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
 | 
			
		||||
                {account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
 | 
			
		||||
 | 
			
		||||
              <div className='account__header__joined'><FormattedMessage id='account.joined' defaultMessage='Joined {date}' values={{ date: intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' }) }} /></div>
 | 
			
		||||
            </div>
 | 
			
		||||
                <div className='account__header__joined'><FormattedMessage id='account.joined' defaultMessage='Joined {date}' values={{ date: intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' }) }} /></div>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
            {!suspended && (
 | 
			
		||||
              <div className='account__header__extra__links'>
 | 
			
		||||
                <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
 | 
			
		||||
                  <ShortNumber
 | 
			
		||||
| 
						 | 
				
			
			@ -341,8 +346,8 @@ class Header extends ImmutablePureComponent {
 | 
			
		|||
                  />
 | 
			
		||||
                </NavLink>
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent {
 | 
			
		|||
    onAddToList: PropTypes.func.isRequired,
 | 
			
		||||
    hideTabs: PropTypes.bool,
 | 
			
		||||
    domain: PropTypes.string.isRequired,
 | 
			
		||||
    hidden: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
| 
						 | 
				
			
			@ -91,7 +92,7 @@ export default class Header extends ImmutablePureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { account, hideTabs } = this.props;
 | 
			
		||||
    const { account, hidden, hideTabs } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (account === null) {
 | 
			
		||||
      return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +100,7 @@ export default class Header extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='account-timeline__header'>
 | 
			
		||||
        {account.get('moved') && <MovedNote from={account} to={account.get('moved')} />}
 | 
			
		||||
        {(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
 | 
			
		||||
 | 
			
		||||
        <InnerHeader
 | 
			
		||||
          account={account}
 | 
			
		||||
| 
						 | 
				
			
			@ -117,9 +118,10 @@ export default class Header extends ImmutablePureComponent {
 | 
			
		|||
          onAddToList={this.handleAddToList}
 | 
			
		||||
          onEditAccountNote={this.handleEditAccountNote}
 | 
			
		||||
          domain={this.props.domain}
 | 
			
		||||
          hidden={hidden}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        {!hideTabs && (
 | 
			
		||||
        {!(hideTabs || hidden) && (
 | 
			
		||||
          <div className='account__section-headline'>
 | 
			
		||||
            <NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
 | 
			
		||||
            <NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import { revealAccount } from 'mastodon/actions/accounts';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import Button from 'mastodon/components/button';
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
 | 
			
		||||
 | 
			
		||||
  reveal () {
 | 
			
		||||
    dispatch(revealAccount(accountId));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default @connect(() => {}, mapDispatchToProps)
 | 
			
		||||
class LimitedAccountHint extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    accountId: PropTypes.string.isRequired,
 | 
			
		||||
    reveal: PropTypes.func,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { reveal } = this.props;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='limited-account-hint'>
 | 
			
		||||
        <p><FormattedMessage id='limited_account_hint.title' defaultMessage='This profile has been hidden by the moderators of your server.' /></p>
 | 
			
		||||
        <Button onClick={reveal}><FormattedMessage id='limited_account_hint.action' defaultMessage='Show profile anyway' /></Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import { makeGetAccount } from '../../../selectors';
 | 
			
		||||
import { makeGetAccount, getAccountHidden } from '../../../selectors';
 | 
			
		||||
import Header from '../components/header';
 | 
			
		||||
import {
 | 
			
		||||
  followAccount,
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ const makeMapStateToProps = () => {
 | 
			
		|||
  const mapStateToProps = (state, { accountId }) => ({
 | 
			
		||||
    account: getAccount(state, accountId),
 | 
			
		||||
    domain: state.getIn(['meta', 'domain']),
 | 
			
		||||
    hidden: getAccountHidden(state, accountId),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return mapStateToProps;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,8 @@ import MissingIndicator from 'mastodon/components/missing_indicator';
 | 
			
		|||
import TimelineHint from 'mastodon/components/timeline_hint';
 | 
			
		||||
import { me } from 'mastodon/initial_state';
 | 
			
		||||
import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines';
 | 
			
		||||
import LimitedAccountHint from './components/limited_account_hint';
 | 
			
		||||
import { getAccountHidden } from 'mastodon/selectors';
 | 
			
		||||
 | 
			
		||||
const emptyList = ImmutableList();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,6 +42,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) =
 | 
			
		|||
    isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
 | 
			
		||||
    hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
 | 
			
		||||
    suspended: state.getIn(['accounts', accountId, 'suspended'], false),
 | 
			
		||||
    hidden: getAccountHidden(state, accountId),
 | 
			
		||||
    blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +73,7 @@ class AccountTimeline extends ImmutablePureComponent {
 | 
			
		|||
    blockedBy: PropTypes.bool,
 | 
			
		||||
    isAccount: PropTypes.bool,
 | 
			
		||||
    suspended: PropTypes.bool,
 | 
			
		||||
    hidden: PropTypes.bool,
 | 
			
		||||
    remote: PropTypes.bool,
 | 
			
		||||
    remoteUrl: PropTypes.string,
 | 
			
		||||
    multiColumn: PropTypes.bool,
 | 
			
		||||
| 
						 | 
				
			
			@ -128,7 +132,7 @@ class AccountTimeline extends ImmutablePureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
 | 
			
		||||
    const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!isAccount) {
 | 
			
		||||
      return (
 | 
			
		||||
| 
						 | 
				
			
			@ -149,8 +153,12 @@ class AccountTimeline extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
    let emptyMessage;
 | 
			
		||||
 | 
			
		||||
    const forceEmptyState = suspended || blockedBy || hidden;
 | 
			
		||||
 | 
			
		||||
    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 && statusIds.isEmpty()) {
 | 
			
		||||
| 
						 | 
				
			
			@ -166,14 +174,14 @@ class AccountTimeline extends ImmutablePureComponent {
 | 
			
		|||
        <ColumnBackButton multiColumn={multiColumn} />
 | 
			
		||||
 | 
			
		||||
        <StatusList
 | 
			
		||||
          prepend={<HeaderContainer accountId={this.props.accountId} />}
 | 
			
		||||
          prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} />}
 | 
			
		||||
          alwaysPrepend
 | 
			
		||||
          append={remoteMessage}
 | 
			
		||||
          scrollKey='account_timeline'
 | 
			
		||||
          statusIds={(suspended || blockedBy) ? emptyList : statusIds}
 | 
			
		||||
          statusIds={forceEmptyState ? emptyList : statusIds}
 | 
			
		||||
          featuredStatusIds={featuredStatusIds}
 | 
			
		||||
          isLoading={isLoading}
 | 
			
		||||
          hasMore={hasMore}
 | 
			
		||||
          hasMore={!forceEmptyState && hasMore}
 | 
			
		||||
          onLoadMore={this.handleLoadMore}
 | 
			
		||||
          emptyMessage={emptyMessage}
 | 
			
		||||
          bindToDocument={!multiColumn}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,8 @@ import ColumnBackButton from '../../components/column_back_button';
 | 
			
		|||
import ScrollableList from '../../components/scrollable_list';
 | 
			
		||||
import MissingIndicator from 'mastodon/components/missing_indicator';
 | 
			
		||||
import TimelineHint from 'mastodon/components/timeline_hint';
 | 
			
		||||
import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
 | 
			
		||||
import { getAccountHidden } from 'mastodon/selectors';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, { params: { acct, id } }) => {
 | 
			
		||||
  const accountId = id || state.getIn(['accounts_map', acct]);
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +39,8 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
 | 
			
		|||
    accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
 | 
			
		||||
    hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
 | 
			
		||||
    isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
 | 
			
		||||
    suspended: state.getIn(['accounts', accountId, 'suspended'], false),
 | 
			
		||||
    hidden: getAccountHidden(state, accountId),
 | 
			
		||||
    blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +68,8 @@ class Followers extends ImmutablePureComponent {
 | 
			
		|||
    isLoading: PropTypes.bool,
 | 
			
		||||
    blockedBy: PropTypes.bool,
 | 
			
		||||
    isAccount: PropTypes.bool,
 | 
			
		||||
    suspended: PropTypes.bool,
 | 
			
		||||
    hidden: PropTypes.bool,
 | 
			
		||||
    remote: PropTypes.bool,
 | 
			
		||||
    remoteUrl: PropTypes.string,
 | 
			
		||||
    multiColumn: PropTypes.bool,
 | 
			
		||||
| 
						 | 
				
			
			@ -101,7 +107,7 @@ class Followers extends ImmutablePureComponent {
 | 
			
		|||
  }, 300, { leading: true });
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
 | 
			
		||||
    const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!isAccount) {
 | 
			
		||||
      return (
 | 
			
		||||
| 
						 | 
				
			
			@ -121,7 +127,13 @@ class Followers extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
    let emptyMessage;
 | 
			
		||||
 | 
			
		||||
    if (blockedBy) {
 | 
			
		||||
    const forceEmptyState = blockedBy || suspended || hidden;
 | 
			
		||||
 | 
			
		||||
    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 && accountIds.isEmpty()) {
 | 
			
		||||
      emptyMessage = <RemoteHint url={remoteUrl} />;
 | 
			
		||||
| 
						 | 
				
			
			@ -137,7 +149,7 @@ class Followers extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
        <ScrollableList
 | 
			
		||||
          scrollKey='followers'
 | 
			
		||||
          hasMore={hasMore}
 | 
			
		||||
          hasMore={!forceEmptyState && hasMore}
 | 
			
		||||
          isLoading={isLoading}
 | 
			
		||||
          onLoadMore={this.handleLoadMore}
 | 
			
		||||
          prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
 | 
			
		||||
| 
						 | 
				
			
			@ -146,7 +158,7 @@ class Followers extends ImmutablePureComponent {
 | 
			
		|||
          emptyMessage={emptyMessage}
 | 
			
		||||
          bindToDocument={!multiColumn}
 | 
			
		||||
        >
 | 
			
		||||
          {blockedBy ? [] : accountIds.map(id =>
 | 
			
		||||
          {forceEmptyState ? [] : accountIds.map(id =>
 | 
			
		||||
            <AccountContainer key={id} id={id} withNote={false} />,
 | 
			
		||||
          )}
 | 
			
		||||
        </ScrollableList>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,8 @@ import ColumnBackButton from '../../components/column_back_button';
 | 
			
		|||
import ScrollableList from '../../components/scrollable_list';
 | 
			
		||||
import MissingIndicator from 'mastodon/components/missing_indicator';
 | 
			
		||||
import TimelineHint from 'mastodon/components/timeline_hint';
 | 
			
		||||
import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
 | 
			
		||||
import { getAccountHidden } from 'mastodon/selectors';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, { params: { acct, id } }) => {
 | 
			
		||||
  const accountId = id || state.getIn(['accounts_map', acct]);
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +39,8 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
 | 
			
		|||
    accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
 | 
			
		||||
    hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
 | 
			
		||||
    isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
 | 
			
		||||
    suspended: state.getIn(['accounts', accountId, 'suspended'], false),
 | 
			
		||||
    hidden: getAccountHidden(state, accountId),
 | 
			
		||||
    blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -64,6 +68,8 @@ class Following extends ImmutablePureComponent {
 | 
			
		|||
    isLoading: PropTypes.bool,
 | 
			
		||||
    blockedBy: PropTypes.bool,
 | 
			
		||||
    isAccount: PropTypes.bool,
 | 
			
		||||
    suspended: PropTypes.bool,
 | 
			
		||||
    hidden: PropTypes.bool,
 | 
			
		||||
    remote: PropTypes.bool,
 | 
			
		||||
    remoteUrl: PropTypes.string,
 | 
			
		||||
    multiColumn: PropTypes.bool,
 | 
			
		||||
| 
						 | 
				
			
			@ -101,7 +107,7 @@ class Following extends ImmutablePureComponent {
 | 
			
		|||
  }, 300, { leading: true });
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
 | 
			
		||||
    const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!isAccount) {
 | 
			
		||||
      return (
 | 
			
		||||
| 
						 | 
				
			
			@ -121,7 +127,13 @@ class Following extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
    let emptyMessage;
 | 
			
		||||
 | 
			
		||||
    if (blockedBy) {
 | 
			
		||||
    const forceEmptyState = blockedBy || suspended || hidden;
 | 
			
		||||
 | 
			
		||||
    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 && accountIds.isEmpty()) {
 | 
			
		||||
      emptyMessage = <RemoteHint url={remoteUrl} />;
 | 
			
		||||
| 
						 | 
				
			
			@ -137,7 +149,7 @@ class Following extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
        <ScrollableList
 | 
			
		||||
          scrollKey='following'
 | 
			
		||||
          hasMore={hasMore}
 | 
			
		||||
          hasMore={!forceEmptyState && hasMore}
 | 
			
		||||
          isLoading={isLoading}
 | 
			
		||||
          onLoadMore={this.handleLoadMore}
 | 
			
		||||
          prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
 | 
			
		||||
| 
						 | 
				
			
			@ -146,7 +158,7 @@ class Following extends ImmutablePureComponent {
 | 
			
		|||
          emptyMessage={emptyMessage}
 | 
			
		||||
          bindToDocument={!multiColumn}
 | 
			
		||||
        >
 | 
			
		||||
          {blockedBy ? [] : accountIds.map(id =>
 | 
			
		||||
          {forceEmptyState ? [] : accountIds.map(id =>
 | 
			
		||||
            <AccountContainer key={id} id={id} withNote={false} />,
 | 
			
		||||
          )}
 | 
			
		||||
        </ScrollableList>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
 | 
			
		||||
import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from 'mastodon/actions/importer';
 | 
			
		||||
import { ACCOUNT_REVEAL } from 'mastodon/actions/accounts';
 | 
			
		||||
import { Map as ImmutableMap, fromJS } from 'immutable';
 | 
			
		||||
 | 
			
		||||
const initialState = ImmutableMap();
 | 
			
		||||
| 
						 | 
				
			
			@ -10,6 +11,8 @@ const normalizeAccount = (state, account) => {
 | 
			
		|||
  delete account.following_count;
 | 
			
		||||
  delete account.statuses_count;
 | 
			
		||||
 | 
			
		||||
  account.hidden = state.getIn([account.id, 'hidden']) === false ? false : account.limited;
 | 
			
		||||
 | 
			
		||||
  return state.set(account.id, fromJS(account));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +30,8 @@ export default function accounts(state = initialState, action) {
 | 
			
		|||
    return normalizeAccount(state, action.account);
 | 
			
		||||
  case ACCOUNTS_IMPORT:
 | 
			
		||||
    return normalizeAccounts(state, action.accounts);
 | 
			
		||||
  case ACCOUNT_REVEAL:
 | 
			
		||||
    return state.setIn([action.id, 'hidden'], false);
 | 
			
		||||
  default:
 | 
			
		||||
    return state;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -175,3 +175,11 @@ export const getAccountGallery = createSelector([
 | 
			
		|||
 | 
			
		||||
  return medias;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const getAccountHidden = createSelector([
 | 
			
		||||
  (state, id) => state.getIn(['accounts', id, 'hidden']),
 | 
			
		||||
  (state, id) => state.getIn(['relationships', id, 'following']) || state.getIn(['relationships', id, 'requested']),
 | 
			
		||||
  (state, id) => id === me,
 | 
			
		||||
], (hidden, followingOrRequested, isSelf) => {
 | 
			
		||||
  return hidden && !(isSelf || followingOrRequested);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4037,6 +4037,15 @@ a.status-card.compact:hover {
 | 
			
		|||
  vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.limited-account-hint {
 | 
			
		||||
  p {
 | 
			
		||||
    color: $secondary-text-color;
 | 
			
		||||
    font-size: 15px;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    margin-bottom: 20px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.empty-column-indicator,
 | 
			
		||||
.error-column,
 | 
			
		||||
.follow_requests-unlocked_explanation {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
 | 
			
		|||
  has_many :emojis, serializer: REST::CustomEmojiSerializer
 | 
			
		||||
 | 
			
		||||
  attribute :suspended, if: :suspended?
 | 
			
		||||
  attribute :silenced, key: :limited, if: :silenced?
 | 
			
		||||
 | 
			
		||||
  class FieldSerializer < ActiveModel::Serializer
 | 
			
		||||
    include FormattingHelper
 | 
			
		||||
| 
						 | 
				
			
			@ -98,7 +99,11 @@ class REST::AccountSerializer < ActiveModel::Serializer
 | 
			
		|||
    object.suspended?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  delegate :suspended?, to: :object
 | 
			
		||||
  def silenced
 | 
			
		||||
    object.silenced?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  delegate :suspended?, :silenced?, to: :object
 | 
			
		||||
 | 
			
		||||
  def moved_and_not_nested?
 | 
			
		||||
    object.moved? && object.moved_to_account.moved_to_account_id.nil?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue