forked from treehouse/mastodon
[Glitch] Add `limited` attribute to accounts in REST API and a warning in web UI
Port b4d373a3df
to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
rebase/4.0.0rc2
parent
bb1a3e902d
commit
6fb837aa1d
|
@ -88,6 +88,8 @@ export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR
|
|||
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]));
|
||||
|
@ -798,6 +800,11 @@ export function unpinAccountFail(error) {
|
|||
};
|
||||
};
|
||||
|
||||
export const revealAccount = id => ({
|
||||
type: ACCOUNT_REVEAL,
|
||||
id,
|
||||
});
|
||||
|
||||
export function fetchPinnedAccounts() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchPinnedAccountsRequest());
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
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,
|
||||
|
@ -45,11 +45,6 @@ export default class Avatar extends React.PureComponent {
|
|||
} = 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`,
|
||||
|
@ -57,19 +52,24 @@ export default class Avatar extends React.PureComponent {
|
|||
backgroundSize: `${size}px ${size}px`,
|
||||
};
|
||||
|
||||
if (hovering || animate) {
|
||||
style.backgroundImage = `url(${src})`;
|
||||
} else {
|
||||
style.backgroundImage = `url(${staticSrc})`;
|
||||
if (account) {
|
||||
const src = account.get('avatar');
|
||||
const staticSrc = account.get('avatar_static');
|
||||
|
||||
if (hovering || animate) {
|
||||
style.backgroundImage = `url(${src})`;
|
||||
} else {
|
||||
style.backgroundImage = `url(${staticSrc})`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={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')}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ class Header extends ImmutablePureComponent {
|
|||
onEditAccountNote: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
domain: PropTypes.string.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
openEditProfile = () => {
|
||||
|
@ -115,7 +116,7 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { account, intl, domain } = this.props;
|
||||
const { account, hidden, intl, domain } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
|
@ -270,23 +271,29 @@ class Header extends ImmutablePureComponent {
|
|||
{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'>
|
||||
|
@ -298,7 +305,7 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<AccountNoteContainer account={account} />
|
||||
|
||||
{!suspended && (
|
||||
{!(suspended || hidden) && (
|
||||
<div className='account__header__extra'>
|
||||
<div className='account__header__bio'>
|
||||
{ fields.size > 0 && (
|
||||
|
|
|
@ -25,6 +25,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
onAddToList: PropTypes.func.isRequired,
|
||||
hideTabs: PropTypes.bool,
|
||||
domain: PropTypes.string.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -92,7 +93,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { account, hideTabs } = this.props;
|
||||
const { account, hidden, hideTabs } = this.props;
|
||||
|
||||
if (account === null) {
|
||||
return null;
|
||||
|
@ -100,7 +101,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='account-timeline__header'>
|
||||
{account.get('moved') && <MovedNote from={account} to={account.get('moved')} />}
|
||||
{(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
|
||||
|
||||
<InnerHeader
|
||||
account={account}
|
||||
|
@ -118,13 +119,14 @@ export default class Header extends ImmutablePureComponent {
|
|||
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>
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
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,
|
||||
|
@ -34,6 +34,7 @@ const makeMapStateToProps = () => {
|
|||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
hidden: getAccountHidden(state, accountId),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
|
|
@ -15,6 +15,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
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();
|
||||
|
||||
|
@ -39,6 +41,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) =
|
|||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
hidden: getAccountHidden(state, accountId),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -67,6 +70,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
withReplies: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
remote: PropTypes.bool,
|
||||
remoteUrl: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
|
@ -130,7 +134,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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 (
|
||||
|
@ -151,8 +155,12 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
|
||||
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 {
|
||||
|
@ -166,14 +174,14 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
<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}
|
||||
|
|
|
@ -19,6 +19,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
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]);
|
||||
|
@ -37,6 +39,8 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
|
|||
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
hidden: getAccountHidden(state, accountId),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -62,6 +66,8 @@ class Followers extends ImmutablePureComponent {
|
|||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
remote: PropTypes.bool,
|
||||
remoteUrl: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
|
@ -107,7 +113,7 @@ class Followers extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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 (
|
||||
|
@ -127,7 +133,13 @@ class Followers extends ImmutablePureComponent {
|
|||
|
||||
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.' />;
|
||||
|
@ -141,7 +153,7 @@ class Followers extends ImmutablePureComponent {
|
|||
|
||||
<ScrollableList
|
||||
scrollKey='followers'
|
||||
hasMore={hasMore}
|
||||
hasMore={!forceEmptyState && hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
||||
|
|
|
@ -19,6 +19,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
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]);
|
||||
|
@ -37,6 +39,8 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
|
|||
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
hidden: getAccountHidden(state, accountId),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -62,6 +66,8 @@ class Following extends ImmutablePureComponent {
|
|||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
remote: PropTypes.bool,
|
||||
remoteUrl: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
|
@ -107,7 +113,7 @@ class Following extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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 (
|
||||
|
@ -127,7 +133,13 @@ class Following extends ImmutablePureComponent {
|
|||
|
||||
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." />;
|
||||
|
@ -141,7 +153,7 @@ class Following extends ImmutablePureComponent {
|
|||
|
||||
<ScrollableList
|
||||
scrollKey='following'
|
||||
hasMore={hasMore}
|
||||
hasMore={!forceEmptyState && hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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();
|
||||
|
@ -10,6 +11,8 @@ const normalizeAccount = (state, account) => {
|
|||
delete account.following_count;
|
||||
delete account.statuses_count;
|
||||
|
||||
account.hidden = state.getIn([account.id, 'hidden']) === false ? false : account.limited;
|
||||
|
||||
return state.set(account.id, fromJS(account));
|
||||
};
|
||||
|
||||
|
@ -27,6 +30,8 @@ export default function accounts(state = initialState, action) {
|
|||
return normalizeAccount(state, action.account);
|
||||
case ACCOUNTS_IMPORT:
|
||||
return normalizeAccounts(state, action.accounts);
|
||||
case ACCOUNT_REVEAL:
|
||||
return state.setIn([action.id, 'hidden'], false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -194,3 +194,11 @@ export const getAccountGallery = createSelector([
|
|||
|
||||
return medias;
|
||||
});
|
||||
|
||||
export const getAccountHidden = createSelector([
|
||||
(state, id) => state.getIn(['accounts', id, 'hidden']),
|
||||
(state, id) => state.getIn(['relationships', id, 'following']) || state.getIn(['relationships', id, 'requested']),
|
||||
(state, id) => id === me,
|
||||
], (hidden, followingOrRequested, isSelf) => {
|
||||
return hidden && !(isSelf || followingOrRequested);
|
||||
});
|
||||
|
|
|
@ -560,6 +560,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
|
Loading…
Reference in New Issue