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]));
error,
};
};
+
+export const revealAccount = id => ({
+ type: ACCOUNT_REVEAL,
+ id,
+});
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,
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`,
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}
onEditAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
+ hidden: PropTypes.bool,
};
openEditProfile = () => {
}
render () {
- const { account, intl, domain } = this.props;
+ const { account, hidden, intl, domain } = this.props;
if (!account) {
return null;
{!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>
</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
/>
</NavLink>
</div>
- )}
- </div>
+ </div>
+ )}
</div>
</div>
);
onAddToList: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
+ hidden: PropTypes.bool,
};
static contextTypes = {
}
render () {
- const { account, hideTabs } = this.props;
+ const { account, hidden, hideTabs } = this.props;
if (account === null) {
return null;
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}
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>
--- /dev/null
+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>
+ );
+ }
+
+}
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,
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
domain: state.getIn(['meta', 'domain']),
+ hidden: getAccountHidden(state, accountId),
});
return mapStateToProps;
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();
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),
};
};
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
suspended: PropTypes.bool,
+ hidden: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
}
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 (
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()) {
<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}
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]);
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),
};
};
isLoading: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
+ suspended: PropTypes.bool,
+ hidden: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
}, 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 (
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} />;
<ScrollableList
scrollKey='followers'
- hasMore={hasMore}
+ hasMore={!forceEmptyState && hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
- {blockedBy ? [] : accountIds.map(id =>
+ {forceEmptyState ? [] : accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
)}
</ScrollableList>
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]);
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),
};
};
isLoading: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
+ suspended: PropTypes.bool,
+ hidden: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
}, 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 (
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} />;
<ScrollableList
scrollKey='following'
- hasMore={hasMore}
+ hasMore={!forceEmptyState && hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
- {blockedBy ? [] : accountIds.map(id =>
+ {forceEmptyState ? [] : accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
)}
</ScrollableList>
-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();
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));
};
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;
}
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);
+});
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 {
has_many :emojis, serializer: REST::CustomEmojiSerializer
attribute :suspended, if: :suspended?
+ attribute :silenced, key: :limited, if: :silenced?
class FieldSerializer < ActiveModel::Serializer
include FormattingHelper
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?