export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET';
+export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
+
export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
};
};
+export const revealAccount = id => ({
+ type: ACCOUNT_REVEAL,
+ id,
+});
+
export function fetchPinnedAccounts() {
return (dispatch, getState) => {
dispatch(fetchPinnedAccountsRequest());
-import classNames from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
+import classNames from 'classnames';
export default class Avatar extends React.PureComponent {
static propTypes = {
- account: ImmutablePropTypes.map.isRequired,
+ account: ImmutablePropTypes.map,
className: PropTypes.string,
size: PropTypes.number.isRequired,
style: PropTypes.object,
} = this.props;
const { hovering } = this.state;
- const src = account.get('avatar');
- const staticSrc = account.get('avatar_static');
-
- const computedClass = classNames('account__avatar', { 'account__avatar-inline': inline }, className);
-
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={computedClass}
+ className={classNames('account__avatar', { 'account__avatar-inline': inline }, className)}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={style}
- data-avatar-of={`@${account.get('acct')}`}
+ data-avatar-of={account && `@${account.get('acct')}`}
/>
);
}
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;
{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' />
- <div className='account__header__tabs__buttons'>
- {actionBtn}
- {bellBtn}
+ {!suspended && (
+ <div className='account__header__tabs__buttons'>
+ {!hidden && (
+ <React.Fragment>
+ {actionBtn}
+ {bellBtn}
+ </React.Fragment>
+ )}
- <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
- </div>
+ <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
+ </div>
+ )}
</div>
<div className='account__header__tabs__name'>
<AccountNoteContainer account={account} />
- {!suspended && (
+ {!(suspended || hidden) && (
<div className='account__header__extra'>
<div className='account__header__bio'>
{ fields.size > 0 && (
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}
/>
<ActionBar
account={account}
/>
- {!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 with replies' /></NavLink>
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { revealAccount } from 'flavours/glitch/actions/accounts';
+import { FormattedMessage } from 'react-intl';
+import Button from 'flavours/glitch/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 'flavours/glitch/selectors';
+import { makeGetAccount, getAccountHidden } from 'flavours/glitch/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 { FormattedMessage } from 'react-intl';
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
import TimelineHint from 'flavours/glitch/components/timeline_hint';
+import LimitedAccountHint from './components/limited_account_hint';
+import { getAccountHidden } from 'flavours/glitch/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),
};
};
withReplies: 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, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
+ const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
if (!isAccount) {
return (
let emptyMessage;
+ const forceEmptyState = suspended || hidden;
+
if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
+ } else if (hidden) {
+ emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (remote && statusIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {
<ProfileColumnHeader onClick={this.handleHeaderClick} 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 ? emptyList : statusIds}
+ statusIds={forceEmptyState ? emptyList : statusIds}
featuredStatusIds={featuredStatusIds}
isLoading={isLoading}
- hasMore={hasMore}
+ hasMore={!forceEmptyState && hasMore}
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import TimelineHint from 'flavours/glitch/components/timeline_hint';
+import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
+import { getAccountHidden } from 'flavours/glitch/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),
};
};
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
isAccount: PropTypes.bool,
+ suspended: PropTypes.bool,
+ hidden: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
}
render () {
- const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
+ const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
if (!isAccount) {
return (
let emptyMessage;
- if (remote && accountIds.isEmpty()) {
+ const forceEmptyState = suspended || hidden;
+
+ if (suspended) {
+ emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
+ } else if (hidden) {
+ emptyMessage = <LimitedAccountHint accountId={accountId} />;
+ } else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
<ScrollableList
scrollKey='followers'
- hasMore={hasMore}
+ hasMore={!forceEmptyState && hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
import MissingIndicator from 'flavours/glitch/components/missing_indicator';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import TimelineHint from 'flavours/glitch/components/timeline_hint';
+import LimitedAccountHint from '../account_timeline/components/limited_account_hint';
+import { getAccountHidden } from 'flavours/glitch/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),
};
};
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
isAccount: PropTypes.bool,
+ suspended: PropTypes.bool,
+ hidden: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
}
render () {
- const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
+ const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
if (!isAccount) {
return (
let emptyMessage;
- if (remote && accountIds.isEmpty()) {
+ const forceEmptyState = suspended || hidden;
+
+ if (suspended) {
+ emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
+ } else if (hidden) {
+ emptyMessage = <LimitedAccountHint accountId={accountId} />;
+ } else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
<ScrollableList
scrollKey='following'
- hasMore={hasMore}
+ hasMore={!forceEmptyState && hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
-import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer';
+import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from 'flavours/glitch/actions/importer';
+import { ACCOUNT_REVEAL } from 'flavours/glitch/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);
+});
}
}
+.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 {