Add `limited` attribute to accounts in REST API and a warning in web UI (#18344)

This commit is contained in:
Eugen Rochko 2022-05-10 09:44:35 +02:00 committed by GitHub
parent 898fe2fa8e
commit b4d373a3df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 166 additions and 59 deletions

View File

@ -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_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
export function fetchAccount(id) { export function fetchAccount(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchRelationships([id])); dispatch(fetchRelationships([id]));
@ -780,3 +782,8 @@ export function unpinAccountFail(error) {
error, error,
}; };
}; };
export const revealAccount = id => ({
type: ACCOUNT_REVEAL,
id,
});

View File

@ -2,11 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state'; import { autoPlayGif } from '../initial_state';
import classNames from 'classnames';
export default class Avatar extends React.PureComponent { export default class Avatar extends React.PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
style: PropTypes.object, style: PropTypes.object,
inline: PropTypes.bool, inline: PropTypes.bool,
@ -37,15 +38,6 @@ export default class Avatar extends React.PureComponent {
const { account, size, animate, inline } = this.props; const { account, size, animate, inline } = this.props;
const { hovering } = this.state; 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 = { const style = {
...this.props.style, ...this.props.style,
width: `${size}px`, width: `${size}px`,
@ -53,15 +45,21 @@ export default class Avatar extends React.PureComponent {
backgroundSize: `${size}px ${size}px`, backgroundSize: `${size}px ${size}px`,
}; };
if (hovering || animate) { if (account) {
style.backgroundImage = `url(${src})`; const src = account.get('avatar');
} else { const staticSrc = account.get('avatar_static');
style.backgroundImage = `url(${staticSrc})`;
if (hovering || animate) {
style.backgroundImage = `url(${src})`;
} else {
style.backgroundImage = `url(${staticSrc})`;
}
} }
return ( return (
<div <div
className={className} className={classNames('account__avatar', { 'account__avatar-inline': inline })}
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
style={style} style={style}

View File

@ -82,6 +82,7 @@ class Header extends ImmutablePureComponent {
onEditAccountNote: PropTypes.func.isRequired, onEditAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
}; };
openEditProfile = () => { openEditProfile = () => {
@ -123,7 +124,7 @@ class Header extends ImmutablePureComponent {
} }
render () { render () {
const { account, intl, domain } = this.props; const { account, hidden, intl, domain } = this.props;
if (!account) { if (!account) {
return null; return null;
@ -267,21 +268,25 @@ class Header extends ImmutablePureComponent {
{!suspended && info} {!suspended && info}
</div> </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>
<div className='account__header__bar'> <div className='account__header__bar'>
<div className='account__header__tabs'> <div className='account__header__tabs'>
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'> <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> </a>
<div className='spacer' /> <div className='spacer' />
{!suspended && ( {!suspended && (
<div className='account__header__tabs__buttons'> <div className='account__header__tabs__buttons'>
{actionBtn} {!hidden && (
{bellBtn} <React.Fragment>
{actionBtn}
{bellBtn}
</React.Fragment>
)}
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
</div> </div>
@ -295,30 +300,30 @@ class Header extends ImmutablePureComponent {
</h1> </h1>
</div> </div>
<div className='account__header__extra'> {!(suspended || hidden) && (
<div className='account__header__bio'> <div className='account__header__extra'>
{fields.size > 0 && ( <div className='account__header__bio'>
<div className='account__header__fields'> {fields.size > 0 && (
{fields.map((pair, i) => ( <div className='account__header__fields'>
<dl key={i}> {fields.map((pair, i) => (
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' /> <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')}> <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') }} /> {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> </dd>
</dl> </dl>
))} ))}
</div> </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 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>
{!suspended && (
<div className='account__header__extra__links'> <div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}> <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<ShortNumber <ShortNumber
@ -341,8 +346,8 @@ class Header extends ImmutablePureComponent {
/> />
</NavLink> </NavLink>
</div> </div>
)} </div>
</div> )}
</div> </div>
</div> </div>
); );

View File

@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent {
onAddToList: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired,
hideTabs: PropTypes.bool, hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
}; };
static contextTypes = { static contextTypes = {
@ -91,7 +92,7 @@ export default class Header extends ImmutablePureComponent {
} }
render () { render () {
const { account, hideTabs } = this.props; const { account, hidden, hideTabs } = this.props;
if (account === null) { if (account === null) {
return null; return null;
@ -99,7 +100,7 @@ export default class Header extends ImmutablePureComponent {
return ( return (
<div className='account-timeline__header'> <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 <InnerHeader
account={account} account={account}
@ -117,9 +118,10 @@ export default class Header extends ImmutablePureComponent {
onAddToList={this.handleAddToList} onAddToList={this.handleAddToList}
onEditAccountNote={this.handleEditAccountNote} onEditAccountNote={this.handleEditAccountNote}
domain={this.props.domain} domain={this.props.domain}
hidden={hidden}
/> />
{!hideTabs && ( {!(hideTabs || hidden) && (
<div className='account__section-headline'> <div className='account__section-headline'>
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink> <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> <NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>

View File

@ -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>
);
}
}

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeGetAccount } from '../../../selectors'; import { makeGetAccount, getAccountHidden } from '../../../selectors';
import Header from '../components/header'; import Header from '../components/header';
import { import {
followAccount, followAccount,
@ -33,6 +33,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, { accountId }) => ({ const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId), account: getAccount(state, accountId),
domain: state.getIn(['meta', 'domain']), domain: state.getIn(['meta', 'domain']),
hidden: getAccountHidden(state, accountId),
}); });
return mapStateToProps; return mapStateToProps;

View File

@ -16,6 +16,8 @@ import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint'; import TimelineHint from 'mastodon/components/timeline_hint';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines'; import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines';
import LimitedAccountHint from './components/limited_account_hint';
import { getAccountHidden } from 'mastodon/selectors';
const emptyList = ImmutableList(); const emptyList = ImmutableList();
@ -40,6 +42,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) =
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false), suspended: state.getIn(['accounts', accountId, 'suspended'], false),
hidden: getAccountHidden(state, accountId),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false), blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
}; };
}; };
@ -70,6 +73,7 @@ class AccountTimeline extends ImmutablePureComponent {
blockedBy: PropTypes.bool, blockedBy: PropTypes.bool,
isAccount: PropTypes.bool, isAccount: PropTypes.bool,
suspended: PropTypes.bool, suspended: PropTypes.bool,
hidden: PropTypes.bool,
remote: PropTypes.bool, remote: PropTypes.bool,
remoteUrl: PropTypes.string, remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
@ -128,7 +132,7 @@ class AccountTimeline extends ImmutablePureComponent {
} }
render () { 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) { if (!isAccount) {
return ( return (
@ -149,8 +153,12 @@ class AccountTimeline extends ImmutablePureComponent {
let emptyMessage; let emptyMessage;
const forceEmptyState = suspended || blockedBy || hidden;
if (suspended) { if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />; emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) { } else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />; emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && statusIds.isEmpty()) { } else if (remote && statusIds.isEmpty()) {
@ -166,14 +174,14 @@ class AccountTimeline extends ImmutablePureComponent {
<ColumnBackButton multiColumn={multiColumn} /> <ColumnBackButton multiColumn={multiColumn} />
<StatusList <StatusList
prepend={<HeaderContainer accountId={this.props.accountId} />} prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} />}
alwaysPrepend alwaysPrepend
append={remoteMessage} append={remoteMessage}
scrollKey='account_timeline' scrollKey='account_timeline'
statusIds={(suspended || blockedBy) ? emptyList : statusIds} statusIds={forceEmptyState ? emptyList : statusIds}
featuredStatusIds={featuredStatusIds} featuredStatusIds={featuredStatusIds}
isLoading={isLoading} isLoading={isLoading}
hasMore={hasMore} hasMore={!forceEmptyState && hasMore}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}

View File

@ -19,6 +19,8 @@ import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator'; import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint'; 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 mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', acct]); 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']), accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']), hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true), 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), blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
}; };
}; };
@ -64,6 +68,8 @@ class Followers extends ImmutablePureComponent {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
blockedBy: PropTypes.bool, blockedBy: PropTypes.bool,
isAccount: PropTypes.bool, isAccount: PropTypes.bool,
suspended: PropTypes.bool,
hidden: PropTypes.bool,
remote: PropTypes.bool, remote: PropTypes.bool,
remoteUrl: PropTypes.string, remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
@ -101,7 +107,7 @@ class Followers extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { 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) { if (!isAccount) {
return ( return (
@ -121,7 +127,13 @@ class Followers extends ImmutablePureComponent {
let emptyMessage; 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' />; emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && accountIds.isEmpty()) { } else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />; emptyMessage = <RemoteHint url={remoteUrl} />;
@ -137,7 +149,7 @@ class Followers extends ImmutablePureComponent {
<ScrollableList <ScrollableList
scrollKey='followers' scrollKey='followers'
hasMore={hasMore} hasMore={!forceEmptyState && hasMore}
isLoading={isLoading} isLoading={isLoading}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />} prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
@ -146,7 +158,7 @@ class Followers extends ImmutablePureComponent {
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{blockedBy ? [] : accountIds.map(id => {forceEmptyState ? [] : accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />, <AccountContainer key={id} id={id} withNote={false} />,
)} )}
</ScrollableList> </ScrollableList>

View File

@ -19,6 +19,8 @@ import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator'; import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint'; 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 mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', acct]); 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']), accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']), hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true), 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), blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
}; };
}; };
@ -64,6 +68,8 @@ class Following extends ImmutablePureComponent {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
blockedBy: PropTypes.bool, blockedBy: PropTypes.bool,
isAccount: PropTypes.bool, isAccount: PropTypes.bool,
suspended: PropTypes.bool,
hidden: PropTypes.bool,
remote: PropTypes.bool, remote: PropTypes.bool,
remoteUrl: PropTypes.string, remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
@ -101,7 +107,7 @@ class Following extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { 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) { if (!isAccount) {
return ( return (
@ -121,7 +127,13 @@ class Following extends ImmutablePureComponent {
let emptyMessage; 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' />; emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && accountIds.isEmpty()) { } else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />; emptyMessage = <RemoteHint url={remoteUrl} />;
@ -137,7 +149,7 @@ class Following extends ImmutablePureComponent {
<ScrollableList <ScrollableList
scrollKey='following' scrollKey='following'
hasMore={hasMore} hasMore={!forceEmptyState && hasMore}
isLoading={isLoading} isLoading={isLoading}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />} prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
@ -146,7 +158,7 @@ class Following extends ImmutablePureComponent {
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{blockedBy ? [] : accountIds.map(id => {forceEmptyState ? [] : accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />, <AccountContainer key={id} id={id} withNote={false} />,
)} )}
</ScrollableList> </ScrollableList>

View File

@ -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'; import { Map as ImmutableMap, fromJS } from 'immutable';
const initialState = ImmutableMap(); const initialState = ImmutableMap();
@ -10,6 +11,8 @@ const normalizeAccount = (state, account) => {
delete account.following_count; delete account.following_count;
delete account.statuses_count; delete account.statuses_count;
account.hidden = state.getIn([account.id, 'hidden']) === false ? false : account.limited;
return state.set(account.id, fromJS(account)); return state.set(account.id, fromJS(account));
}; };
@ -27,6 +30,8 @@ export default function accounts(state = initialState, action) {
return normalizeAccount(state, action.account); return normalizeAccount(state, action.account);
case ACCOUNTS_IMPORT: case ACCOUNTS_IMPORT:
return normalizeAccounts(state, action.accounts); return normalizeAccounts(state, action.accounts);
case ACCOUNT_REVEAL:
return state.setIn([action.id, 'hidden'], false);
default: default:
return state; return state;
} }

View File

@ -175,3 +175,11 @@ export const getAccountGallery = createSelector([
return medias; 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);
});

View File

@ -4037,6 +4037,15 @@ a.status-card.compact:hover {
vertical-align: middle; vertical-align: middle;
} }
.limited-account-hint {
p {
color: $secondary-text-color;
font-size: 15px;
font-weight: 500;
margin-bottom: 20px;
}
}
.empty-column-indicator, .empty-column-indicator,
.error-column, .error-column,
.follow_requests-unlocked_explanation { .follow_requests-unlocked_explanation {

View File

@ -13,6 +13,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
has_many :emojis, serializer: REST::CustomEmojiSerializer has_many :emojis, serializer: REST::CustomEmojiSerializer
attribute :suspended, if: :suspended? attribute :suspended, if: :suspended?
attribute :silenced, key: :limited, if: :silenced?
class FieldSerializer < ActiveModel::Serializer class FieldSerializer < ActiveModel::Serializer
include FormattingHelper include FormattingHelper
@ -98,7 +99,11 @@ class REST::AccountSerializer < ActiveModel::Serializer
object.suspended? object.suspended?
end end
delegate :suspended?, to: :object def silenced
object.silenced?
end
delegate :suspended?, :silenced?, to: :object
def moved_and_not_nested? def moved_and_not_nested?
object.moved? && object.moved_to_account.moved_to_account_id.nil? object.moved? && object.moved_to_account.moved_to_account_id.nil?