about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/flavours/glitch/actions/accounts.js7
-rw-r--r--app/javascript/flavours/glitch/components/account.js12
-rw-r--r--app/javascript/flavours/glitch/components/avatar.js26
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js39
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/components/header.js10
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js35
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js5
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js22
-rw-r--r--app/javascript/flavours/glitch/features/blocks/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js18
-rw-r--r--app/javascript/flavours/glitch/features/following/index.js18
-rw-r--r--app/javascript/flavours/glitch/features/mutes/index.js2
-rw-r--r--app/javascript/flavours/glitch/reducers/accounts.js7
-rw-r--r--app/javascript/flavours/glitch/reducers/identity_proofs.js25
-rw-r--r--app/javascript/flavours/glitch/reducers/index.js2
-rw-r--r--app/javascript/flavours/glitch/selectors/index.js8
-rw-r--r--app/javascript/flavours/glitch/styles/components/columns.scss9
-rw-r--r--app/javascript/flavours/glitch/styles/forms.scss62
-rw-r--r--app/javascript/mastodon/actions/accounts.js7
-rw-r--r--app/javascript/mastodon/components/account.js9
-rw-r--r--app/javascript/mastodon/components/avatar.js28
-rw-r--r--app/javascript/mastodon/features/account/components/header.js57
-rw-r--r--app/javascript/mastodon/features/account_timeline/components/header.js8
-rw-r--r--app/javascript/mastodon/features/account_timeline/components/limited_account_hint.js35
-rw-r--r--app/javascript/mastodon/features/account_timeline/containers/header_container.js3
-rw-r--r--app/javascript/mastodon/features/account_timeline/index.js16
-rw-r--r--app/javascript/mastodon/features/blocks/index.js2
-rw-r--r--app/javascript/mastodon/features/followers/index.js20
-rw-r--r--app/javascript/mastodon/features/following/index.js20
-rw-r--r--app/javascript/mastodon/features/mutes/index.js2
-rw-r--r--app/javascript/mastodon/reducers/accounts.js7
-rw-r--r--app/javascript/mastodon/selectors/index.js8
-rw-r--r--app/javascript/styles/mastodon/components.scss9
33 files changed, 329 insertions, 211 deletions
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js
index 0cf64e076..f5871beb3 100644
--- a/app/javascript/flavours/glitch/actions/accounts.js
+++ b/app/javascript/flavours/glitch/actions/accounts.js
@@ -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());
diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js
index 396a36ea0..489f60736 100644
--- a/app/javascript/flavours/glitch/components/account.js
+++ b/app/javascript/flavours/glitch/components/account.js
@@ -16,8 +16,10 @@ const messages = defineMessages({
   requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
   unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
   unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
-  mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'You are not currently muting notifications from @{name}. Click to mute notifications' },
-  unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' },
+  mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
+  unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
+  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
+  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
 });
 
 export default @injectIntl
@@ -34,6 +36,7 @@ class Account extends ImmutablePureComponent {
     small: PropTypes.bool,
     actionIcon: PropTypes.string,
     actionTitle: PropTypes.string,
+    defaultAction: PropTypes.string,
     onActionClick: PropTypes.func,
   };
 
@@ -70,6 +73,7 @@ class Account extends ImmutablePureComponent {
       onActionClick,
       actionIcon,
       actionTitle,
+      defaultAction,
     } = this.props;
 
     if (!account) {
@@ -114,6 +118,10 @@ class Account extends ImmutablePureComponent {
             {hidingNotificationsButton}
           </Fragment>
         );
+      } else if (defaultAction === 'mute') {
+        buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />;
+      } else if (defaultAction === 'block') {
+        buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get('username') })} onClick={this.handleBlock} />;
       } else if (!account.get('moved') || following) {
         buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
       }
diff --git a/app/javascript/flavours/glitch/components/avatar.js b/app/javascript/flavours/glitch/components/avatar.js
index c5e9072c4..6d53a5298 100644
--- a/app/javascript/flavours/glitch/components/avatar.js
+++ b/app/javascript/flavours/glitch/components/avatar.js
@@ -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')}`}
       />
     );
   }
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index 68c6bae8e..45aba53f7 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -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, identity_proofs } = 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,23 +305,11 @@ class Header extends ImmutablePureComponent {
 
           <AccountNoteContainer account={account} />
 
-          {!suspended && (
+          {!(suspended || hidden) && (
             <div className='account__header__extra'>
               <div className='account__header__bio'>
-                { (fields.size > 0 || identity_proofs.size > 0) && (
+                { fields.size > 0 && (
                   <div className='account__header__fields'>
-                    {identity_proofs.map((proof, i) => (
-                      <dl key={i}>
-                        <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} className='translate' />
-
-                        <dd className='verified'>
-                          <a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
-                            <Icon id='check' className='verified__mark' />
-                          </span></a>
-                          <a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} className='translate' /></a>
-                        </dd>
-                      </dl>
-                    ))}
                     {fields.map((pair, i) => (
                       <dl key={i}>
                         <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
index d6e607a37..645ff29ea 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
@@ -12,7 +12,6 @@ export default class Header extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map,
-    identity_proofs: ImmutablePropTypes.list,
     onFollow: PropTypes.func.isRequired,
     onBlock: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
@@ -26,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 = {
@@ -93,7 +93,7 @@ export default class Header extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, hideTabs, identity_proofs } = this.props;
+    const { account, hidden, hideTabs } = this.props;
 
     if (account === null) {
       return null;
@@ -101,11 +101,10 @@ 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}
-          identity_proofs={identity_proofs}
           onFollow={this.handleFollow}
           onBlock={this.handleBlock}
           onMention={this.handleMention}
@@ -120,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>
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js
new file mode 100644
index 000000000..e465c83b4
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js
@@ -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>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
index 90e746679..3fa7c1448 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
@@ -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,
@@ -22,7 +22,6 @@ import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_block
 import { initEditAccountNote } from 'flavours/glitch/actions/account_notes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { unfollowModal } from 'flavours/glitch/util/initial_state';
-import { List as ImmutableList } from 'immutable';
 
 const messages = defineMessages({
   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -35,7 +34,7 @@ const makeMapStateToProps = () => {
   const mapStateToProps = (state, { accountId }) => ({
     account: getAccount(state, accountId),
     domain: state.getIn(['meta', 'domain']),
-    identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()),
+    hidden: getAccountHidden(state, accountId),
   });
 
   return mapStateToProps;
diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js
index 6d2df5c6f..68d558e66 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/index.js
@@ -13,9 +13,10 @@ import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import { List as ImmutableList } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { FormattedMessage } from 'react-intl';
-import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
 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();
 
@@ -40,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),
   };
 };
 
@@ -68,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,
@@ -77,7 +80,7 @@ class AccountTimeline extends ImmutablePureComponent {
     const { accountId, withReplies, dispatch } = this.props;
 
     dispatch(fetchAccount(accountId));
-    dispatch(fetchAccountIdentityProofs(accountId));
+
     if (!withReplies) {
       dispatch(expandAccountFeaturedTimeline(accountId));
     }
@@ -109,10 +112,11 @@ class AccountTimeline extends ImmutablePureComponent {
 
     if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
       dispatch(fetchAccount(nextProps.params.accountId));
-      dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
+
       if (!nextProps.withReplies) {
         dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
       }
+
       dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
     }
   }
@@ -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}
diff --git a/app/javascript/flavours/glitch/features/blocks/index.js b/app/javascript/flavours/glitch/features/blocks/index.js
index 4d0f58239..4461bd14d 100644
--- a/app/javascript/flavours/glitch/features/blocks/index.js
+++ b/app/javascript/flavours/glitch/features/blocks/index.js
@@ -69,7 +69,7 @@ class Blocks extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} />,
+            <AccountContainer key={id} id={id} defaultAction='block' />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js
index 978436dcc..27a63b3fd 100644
--- a/app/javascript/flavours/glitch/features/followers/index.js
+++ b/app/javascript/flavours/glitch/features/followers/index.js
@@ -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 />}
diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js
index 446a19894..aa187bf95 100644
--- a/app/javascript/flavours/glitch/features/following/index.js
+++ b/app/javascript/flavours/glitch/features/following/index.js
@@ -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 />}
diff --git a/app/javascript/flavours/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js
index 9f0d5a43e..764cbef1a 100644
--- a/app/javascript/flavours/glitch/features/mutes/index.js
+++ b/app/javascript/flavours/glitch/features/mutes/index.js
@@ -69,7 +69,7 @@ class Mutes extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} />,
+            <AccountContainer key={id} id={id} defaultAction='mute' />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/flavours/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js
index 530ed8e60..e02a5592e 100644
--- a/app/javascript/flavours/glitch/reducers/accounts.js
+++ b/app/javascript/flavours/glitch/reducers/accounts.js
@@ -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;
   }
diff --git a/app/javascript/flavours/glitch/reducers/identity_proofs.js b/app/javascript/flavours/glitch/reducers/identity_proofs.js
deleted file mode 100644
index 58af0a5fa..000000000
--- a/app/javascript/flavours/glitch/reducers/identity_proofs.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Map as ImmutableMap, fromJS } from 'immutable';
-import {
-  IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
-  IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
-  IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
-} from '../actions/identity_proofs';
-
-const initialState = ImmutableMap();
-
-export default function identityProofsReducer(state = initialState, action) {
-  switch(action.type) {
-  case IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST:
-    return state.set('isLoading', true);
-  case IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL:
-    return state.set('isLoading', false);
-  case IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS:
-    return state.update(identity_proofs => identity_proofs.withMutations(map => {
-      map.set('isLoading', false);
-      map.set('loaded', true);
-      map.set(action.accountId, fromJS(action.identity_proofs));
-    }));
-  default:
-    return state;
-  }
-};
diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index 92348c0c5..b8aad9fad 100644
--- a/app/javascript/flavours/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
@@ -34,7 +34,6 @@ import conversations from './conversations';
 import suggestions from './suggestions';
 import pinnedAccountsEditor from './pinned_accounts_editor';
 import polls from './polls';
-import identity_proofs from './identity_proofs';
 import trends from './trends';
 import announcements from './announcements';
 import markers from './markers';
@@ -73,7 +72,6 @@ const reducers = {
   notifications,
   height_cache,
   custom_emojis,
-  identity_proofs,
   lists,
   listEditor,
   listAdder,
diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js
index bb9180d12..99afe5355 100644
--- a/app/javascript/flavours/glitch/selectors/index.js
+++ b/app/javascript/flavours/glitch/selectors/index.js
@@ -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);
+});
diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index 96e292d8b..d52ecf02c 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -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 {
diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index e36fab8fa..a08ca24f1 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -1022,68 +1022,6 @@ code {
   }
 }
 
-.connection-prompt {
-  margin-bottom: 25px;
-
-  .fa-link {
-    background-color: darken($ui-base-color, 4%);
-    border-radius: 100%;
-    font-size: 24px;
-    padding: 10px;
-  }
-
-  &__column {
-    align-items: center;
-    display: flex;
-    flex: 1;
-    flex-direction: column;
-    flex-shrink: 1;
-    max-width: 50%;
-
-    &-sep {
-      align-self: center;
-      flex-grow: 0;
-      overflow: visible;
-      position: relative;
-      z-index: 1;
-    }
-
-    p {
-      word-break: break-word;
-    }
-  }
-
-  .account__avatar {
-    margin-bottom: 20px;
-  }
-
-  &__connection {
-    background-color: lighten($ui-base-color, 8%);
-    box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
-    border-radius: 4px;
-    padding: 25px 10px;
-    position: relative;
-    text-align: center;
-
-    &::after {
-      background-color: darken($ui-base-color, 4%);
-      content: '';
-      display: block;
-      height: 100%;
-      left: 50%;
-      position: absolute;
-      top: 0;
-      width: 1px;
-    }
-  }
-
-  &__row {
-    align-items: flex-start;
-    display: flex;
-    flex-direction: row;
-  }
-}
-
 .input.user_confirm_password,
 .input.user_website {
   &:not(.field_with_errors) {
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index ce7bb6d5f..eedf61dc9 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -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_FAIL    = 'FOLLOW_REQUEST_REJECT_FAIL';
 
+export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
+
 export function fetchAccount(id) {
   return (dispatch, getState) => {
     dispatch(fetchRelationships([id]));
@@ -780,3 +782,8 @@ export function unpinAccountFail(error) {
     error,
   };
 };
+
+export const revealAccount = id => ({
+  type: ACCOUNT_REVEAL,
+  id,
+});
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 62b5843a9..af9f119c8 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -18,6 +18,8 @@ const messages = defineMessages({
   unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
   mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
   unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
+  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
+  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
 });
 
 export default @injectIntl
@@ -33,6 +35,7 @@ class Account extends ImmutablePureComponent {
     hidden: PropTypes.bool,
     actionIcon: PropTypes.string,
     actionTitle: PropTypes.string,
+    defaultAction: PropTypes.string,
     onActionClick: PropTypes.func,
   };
 
@@ -61,7 +64,7 @@ class Account extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, intl, hidden, onActionClick, actionIcon, actionTitle } = this.props;
+    const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction } = this.props;
 
     if (!account) {
       return <div />;
@@ -105,6 +108,10 @@ class Account extends ImmutablePureComponent {
             {hidingNotificationsButton}
           </Fragment>
         );
+      } else if (defaultAction === 'mute') {
+        buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />;
+      } else if (defaultAction === 'block') {
+        buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get('username') })} onClick={this.handleBlock} />;
       } else if (!account.get('moved') || following) {
         buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
       }
diff --git a/app/javascript/mastodon/components/avatar.js b/app/javascript/mastodon/components/avatar.js
index 570505833..12ab7d2df 100644
--- a/app/javascript/mastodon/components/avatar.js
+++ b/app/javascript/mastodon/components/avatar.js
@@ -2,11 +2,12 @@ import React from 'react';
 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,
@@ -37,15 +38,6 @@ export default class Avatar extends React.PureComponent {
     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`,
@@ -53,15 +45,21 @@ 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={className}
+        className={classNames('account__avatar', { 'account__avatar-inline': inline })}
         onMouseEnter={this.handleMouseEnter}
         onMouseLeave={this.handleMouseLeave}
         style={style}
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 2830bee29..8e6b9f063 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -82,6 +82,7 @@ class Header extends ImmutablePureComponent {
     onEditAccountNote: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
     domain: PropTypes.string.isRequired,
+    hidden: PropTypes.bool,
   };
 
   openEditProfile = () => {
@@ -123,7 +124,7 @@ class Header extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, intl, domain } = this.props;
+    const { account, hidden, intl, domain } = this.props;
 
     if (!account) {
       return null;
@@ -267,21 +268,25 @@ class Header extends ImmutablePureComponent {
             {!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>
@@ -295,30 +300,30 @@ class Header extends ImmutablePureComponent {
             </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
@@ -341,8 +346,8 @@ class Header extends ImmutablePureComponent {
                   />
                 </NavLink>
               </div>
-            )}
-          </div>
+            </div>
+          )}
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
index 507b6c895..fab0bc597 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.js
+++ b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent {
     onAddToList: PropTypes.func.isRequired,
     hideTabs: PropTypes.bool,
     domain: PropTypes.string.isRequired,
+    hidden: PropTypes.bool,
   };
 
   static contextTypes = {
@@ -91,7 +92,7 @@ export default class Header extends ImmutablePureComponent {
   }
 
   render () {
-    const { account, hideTabs } = this.props;
+    const { account, hidden, hideTabs } = this.props;
 
     if (account === null) {
       return null;
@@ -99,7 +100,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}
@@ -117,9 +118,10 @@ export default class Header extends ImmutablePureComponent {
           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>
diff --git a/app/javascript/mastodon/features/account_timeline/components/limited_account_hint.js b/app/javascript/mastodon/features/account_timeline/components/limited_account_hint.js
new file mode 100644
index 000000000..6b025596c
--- /dev/null
+++ b/app/javascript/mastodon/features/account_timeline/components/limited_account_hint.js
@@ -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>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index b3f8521cb..371794dd7 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -1,6 +1,6 @@
 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,
@@ -33,6 +33,7 @@ const makeMapStateToProps = () => {
   const mapStateToProps = (state, { accountId }) => ({
     account: getAccount(state, accountId),
     domain: state.getIn(['meta', 'domain']),
+    hidden: getAccountHidden(state, accountId),
   });
 
   return mapStateToProps;
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index 5122aec4e..5b592c5a7 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -16,6 +16,8 @@ import MissingIndicator from 'mastodon/components/missing_indicator';
 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();
 
@@ -40,6 +42,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),
     blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
   };
 };
@@ -70,6 +73,7 @@ class AccountTimeline extends ImmutablePureComponent {
     blockedBy: PropTypes.bool,
     isAccount: PropTypes.bool,
     suspended: PropTypes.bool,
+    hidden: PropTypes.bool,
     remote: PropTypes.bool,
     remoteUrl: PropTypes.string,
     multiColumn: PropTypes.bool,
@@ -128,7 +132,7 @@ class AccountTimeline extends ImmutablePureComponent {
   }
 
   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 (
@@ -149,8 +153,12 @@ class AccountTimeline extends ImmutablePureComponent {
 
     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()) {
@@ -166,14 +174,14 @@ class AccountTimeline extends ImmutablePureComponent {
         <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}
diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js
index 7ec177434..e00f2b60e 100644
--- a/app/javascript/mastodon/features/blocks/index.js
+++ b/app/javascript/mastodon/features/blocks/index.js
@@ -69,7 +69,7 @@ class Blocks extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} />,
+            <AccountContainer key={id} id={id} defaultAction='block' />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js
index 224e74b3d..5b7f402f8 100644
--- a/app/javascript/mastodon/features/followers/index.js
+++ b/app/javascript/mastodon/features/followers/index.js
@@ -19,6 +19,8 @@ import ColumnBackButton from '../../components/column_back_button';
 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]);
@@ -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),
     blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
   };
 };
@@ -64,6 +68,8 @@ class Followers extends ImmutablePureComponent {
     isLoading: PropTypes.bool,
     blockedBy: PropTypes.bool,
     isAccount: PropTypes.bool,
+    suspended: PropTypes.bool,
+    hidden: PropTypes.bool,
     remote: PropTypes.bool,
     remoteUrl: PropTypes.string,
     multiColumn: PropTypes.bool,
@@ -101,7 +107,7 @@ class Followers extends ImmutablePureComponent {
   }, 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 (
@@ -121,7 +127,13 @@ class Followers extends ImmutablePureComponent {
 
     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} />;
@@ -137,7 +149,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 />}
@@ -146,7 +158,7 @@ class Followers extends ImmutablePureComponent {
           emptyMessage={emptyMessage}
           bindToDocument={!multiColumn}
         >
-          {blockedBy ? [] : accountIds.map(id =>
+          {forceEmptyState ? [] : accountIds.map(id =>
             <AccountContainer key={id} id={id} withNote={false} />,
           )}
         </ScrollableList>
diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js
index aadce1644..143082d76 100644
--- a/app/javascript/mastodon/features/following/index.js
+++ b/app/javascript/mastodon/features/following/index.js
@@ -19,6 +19,8 @@ import ColumnBackButton from '../../components/column_back_button';
 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]);
@@ -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),
     blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
   };
 };
@@ -64,6 +68,8 @@ class Following extends ImmutablePureComponent {
     isLoading: PropTypes.bool,
     blockedBy: PropTypes.bool,
     isAccount: PropTypes.bool,
+    suspended: PropTypes.bool,
+    hidden: PropTypes.bool,
     remote: PropTypes.bool,
     remoteUrl: PropTypes.string,
     multiColumn: PropTypes.bool,
@@ -101,7 +107,7 @@ class Following extends ImmutablePureComponent {
   }, 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 (
@@ -121,7 +127,13 @@ class Following extends ImmutablePureComponent {
 
     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} />;
@@ -137,7 +149,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 />}
@@ -146,7 +158,7 @@ class Following extends ImmutablePureComponent {
           emptyMessage={emptyMessage}
           bindToDocument={!multiColumn}
         >
-          {blockedBy ? [] : accountIds.map(id =>
+          {forceEmptyState ? [] : accountIds.map(id =>
             <AccountContainer key={id} id={id} withNote={false} />,
           )}
         </ScrollableList>
diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js
index c1d50d194..c21433cc4 100644
--- a/app/javascript/mastodon/features/mutes/index.js
+++ b/app/javascript/mastodon/features/mutes/index.js
@@ -69,7 +69,7 @@ class Mutes extends ImmutablePureComponent {
           bindToDocument={!multiColumn}
         >
           {accountIds.map(id =>
-            <AccountContainer key={id} id={id} />,
+            <AccountContainer key={id} id={id} defaultAction='mute' />,
           )}
         </ScrollableList>
       </Column>
diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js
index 530ed8e60..b5589668c 100644
--- a/app/javascript/mastodon/reducers/accounts.js
+++ b/app/javascript/mastodon/reducers/accounts.js
@@ -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';
 
 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;
   }
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index 1e19db65d..3121774b3 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -175,3 +175,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);
+});
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 4a805992e..e6133ee32 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4037,6 +4037,15 @@ a.status-card.compact:hover {
   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 {