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_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,
|
||||||
|
});
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 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;
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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?
|
||||||
|
|
Loading…
Reference in a new issue