about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorThibG <thib@sitedethib.com>2019-04-08 18:17:22 +0200
committerGitHub <noreply@github.com>2019-04-08 18:17:22 +0200
commitfc2f7ee8710b498c4533e229aff6391396aa7213 (patch)
tree9576b254773166a419b932c50e8f4d354bea73c3 /app
parent30500dff7cbf27fbee6eb45e20cc24ca0b7a9e77 (diff)
parent63837a4b781ad27a4af895b0c983d4aff57d80e3 (diff)
Merge pull request #983 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/pending_accounts_controller.rb52
-rw-r--r--app/controllers/api/v1/accounts/follower_accounts_controller.rb8
-rw-r--r--app/controllers/api/v1/accounts/following_accounts_controller.rb8
-rw-r--r--app/controllers/api/v1/accounts/statuses_controller.rb10
-rw-r--r--app/controllers/api/v1/accounts_controller.rb5
-rw-r--r--app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb1
-rw-r--r--app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb2
-rw-r--r--app/controllers/home_controller.rb2
-rw-r--r--app/controllers/shares_controller.rb2
-rw-r--r--app/helpers/stream_entries_helper.rb2
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js2
-rw-r--r--app/javascript/flavours/glitch/styles/stream_entries.scss17
-rw-r--r--app/javascript/mastodon/components/status.js2
-rw-r--r--app/javascript/mastodon/features/account/components/header.js4
-rw-r--r--app/javascript/mastodon/features/account_timeline/index.js22
-rw-r--r--app/javascript/mastodon/features/followers/index.js8
-rw-r--r--app/javascript/mastodon/features/following/index.js8
-rw-r--r--app/javascript/mastodon/features/status/index.js2
-rw-r--r--app/javascript/mastodon/locales/co.json1
-rw-r--r--app/javascript/mastodon/locales/cs.json3
-rw-r--r--app/javascript/mastodon/locales/de.json1
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json52
-rw-r--r--app/javascript/mastodon/locales/en.json2
-rw-r--r--app/javascript/mastodon/locales/ja.json6
-rw-r--r--app/javascript/mastodon/locales/ko.json1
-rw-r--r--app/javascript/mastodon/locales/nl.json28
-rw-r--r--app/javascript/mastodon/locales/pl.json5
-rw-r--r--app/javascript/mastodon/locales/sk.json74
-rw-r--r--app/javascript/styles/mastodon/components.scss3
-rw-r--r--app/javascript/styles/mastodon/stream_entries.scss17
-rw-r--r--app/lib/proof_provider/keybase/config_serializer.rb2
-rw-r--r--app/models/account.rb1
-rw-r--r--app/models/concerns/account_finder_concern.rb2
-rw-r--r--app/models/export.rb6
-rw-r--r--app/models/form/account_batch.rb19
-rw-r--r--app/presenters/account_relationships_presenter.rb6
-rw-r--r--app/presenters/instance_presenter.rb2
-rw-r--r--app/serializers/rest/relationship_serializer.rb6
-rw-r--r--app/services/account_search_service.rb10
-rw-r--r--app/services/import_service.rb12
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/validators/existing_username_validator.rb10
-rw-r--r--app/views/accounts/show.html.haml4
-rw-r--r--app/views/admin/accounts/index.html.haml2
-rw-r--r--app/views/admin/pending_accounts/_account.html.haml14
-rw-r--r--app/views/admin/pending_accounts/index.html.haml30
-rw-r--r--app/views/follower_accounts/index.html.haml2
-rw-r--r--app/views/following_accounts/index.html.haml2
-rw-r--r--app/workers/import/relationship_worker.rb7
49 files changed, 324 insertions, 165 deletions
diff --git a/app/controllers/admin/pending_accounts_controller.rb b/app/controllers/admin/pending_accounts_controller.rb
new file mode 100644
index 000000000..8429d3585
--- /dev/null
+++ b/app/controllers/admin/pending_accounts_controller.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Admin
+  class PendingAccountsController < BaseController
+    before_action :set_accounts, only: :index
+
+    def index
+      @form = Form::AccountBatch.new
+    end
+
+    def update
+      @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
+      @form.save
+    rescue ActionController::ParameterMissing
+      # Do nothing
+    ensure
+      redirect_to admin_pending_accounts_path(current_params)
+    end
+
+    def approve_all
+      Form::AccountBatch.new(account_ids: User.pending.pluck(:account_id), action: 'approve').save
+      redirect_to admin_pending_accounts_path(current_params)
+    end
+
+    def reject_all
+      Form::AccountBatch.new(account_ids: User.pending.pluck(:account_id), action: 'reject').save
+      redirect_to admin_pending_accounts_path(current_params)
+    end
+
+    private
+
+    def set_accounts
+      @accounts = Account.joins(:user).merge(User.pending).page(params[:page])
+    end
+
+    def form_account_batch_params
+      params.require(:form_account_batch).permit(:action, account_ids: [])
+    end
+
+    def action_from_button
+      if params[:approve]
+        'approve'
+      elsif params[:reject]
+        'reject'
+      end
+    end
+
+    def current_params
+      params.slice(:page).permit(:page)
+    end
+  end
+end
diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
index 7a45e6dd2..2dabb8398 100644
--- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
@@ -19,13 +19,17 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
   end
 
   def load_accounts
-    return [] if @account.user_hides_network? && current_account.id != @account.id
+    return [] if hide_results?
 
     default_accounts.merge(paginated_follows).to_a
   end
 
+  def hide_results?
+    (@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
+  end
+
   def default_accounts
-    Account.without_blocking(current_account).includes(:active_relationships, :account_stat).references(:active_relationships)
+    Account.includes(:active_relationships, :account_stat).references(:active_relationships)
   end
 
   def paginated_follows
diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb
index 0369cb25e..44e89804b 100644
--- a/app/controllers/api/v1/accounts/following_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb
@@ -19,13 +19,17 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
   end
 
   def load_accounts
-    return [] if @account.user_hides_network? && current_account.id != @account.id
+    return [] if hide_results?
 
     default_accounts.merge(paginated_follows).to_a
   end
 
+  def hide_results?
+    (@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
+  end
+
   def default_accounts
-    Account.without_blocking(current_account).includes(:passive_relationships, :account_stat).references(:passive_relationships)
+    Account.includes(:passive_relationships, :account_stat).references(:passive_relationships)
   end
 
   def paginated_follows
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 7aba2d0bd..8cd8f8e79 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -3,8 +3,6 @@
 class Api::V1::Accounts::StatusesController < Api::BaseController
   before_action -> { authorize_if_got_token! :read, :'read:statuses' }
   before_action :set_account
-  before_action :check_account_suspension
-  before_action :check_account_block
   after_action :insert_pagination_headers
 
   respond_to :json
@@ -20,14 +18,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
     @account = Account.find(params[:account_id])
   end
 
-  def check_account_suspension
-    gone if @account.suspended?
-  end
-
-  def check_account_block
-    gone if current_account.present? && @account.blocking?(current_account)
-  end
-
   def load_statuses
     cached_account_statuses
   end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 685e044c3..b0c62778e 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -10,7 +10,6 @@ class Api::V1::AccountsController < Api::BaseController
   before_action :require_user!, except: [:show, :create]
   before_action :set_account, except: [:create]
   before_action :check_account_suspension, only: [:show]
-  before_action :check_account_block, only: [:show]
   before_action :check_enabled_registrations, only: [:create]
 
   respond_to :json
@@ -76,10 +75,6 @@ class Api::V1::AccountsController < Api::BaseController
     gone if @account.suspended?
   end
 
-  def check_account_block
-    gone if current_account.present? && @account.blocking?(current_account)
-  end
-
   def account_params
     params.permit(:username, :email, :password, :agreement, :locale)
   end
diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
index e00c4d708..657e57831 100644
--- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
@@ -22,7 +22,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
 
   def default_accounts
     Account
-      .without_blocking(current_account)
       .includes(:favourites, :account_stat)
       .references(:favourites)
       .where(favourites: { status_id: @status.id })
diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
index 9b2d0e59e..6851099f6 100644
--- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
@@ -21,7 +21,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
   end
 
   def default_accounts
-    Account.without_blocking(current_account).includes(:statuses, :account_stat).references(:statuses)
+    Account.includes(:statuses, :account_stat).references(:statuses)
   end
 
   def paginated_statuses
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 82e5265f5..06ca03e34 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -56,7 +56,7 @@ class HomeController < ApplicationController
       push_subscription: current_account.user.web_push_subscription(current_session),
       current_account: current_account,
       token: current_session.token,
-      admin: Account.find_local(Setting.site_contact_username),
+      admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
     }
   end
 
diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb
index 4624c29a6..ada4eec54 100644
--- a/app/controllers/shares_controller.rb
+++ b/app/controllers/shares_controller.rb
@@ -22,7 +22,7 @@ class SharesController < ApplicationController
       push_subscription: current_account.user.web_push_subscription(current_session),
       current_account: current_account,
       token: current_session.token,
-      admin: Account.find_local(Setting.site_contact_username),
+      admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
       text: text,
     }
   end
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index 5ad2c438e..6e646ab84 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -23,7 +23,7 @@ module StreamEntriesHelper
           safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.unfollow')])
         end
       elsif !(account.memorial? || account.moved?)
-        link_to account_follow_path(account), class: 'button logo-button', data: { method: :post } do
+        link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
           safe_join([render(file: Rails.root.join('app', 'javascript', 'images', 'logo.svg')), t('accounts.follow')])
         end
       end
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index 13f7741c8..43c4f0d32 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -22,8 +22,6 @@ const messages = defineMessages({
   account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
   mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
   direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
-  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
-  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
   unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
   block: { id: 'account.block', defaultMessage: 'Block @{name}' },
   mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
diff --git a/app/javascript/flavours/glitch/styles/stream_entries.scss b/app/javascript/flavours/glitch/styles/stream_entries.scss
index 6735049b9..e18696fb7 100644
--- a/app/javascript/flavours/glitch/styles/stream_entries.scss
+++ b/app/javascript/flavours/glitch/styles/stream_entries.scss
@@ -109,6 +109,23 @@
     }
   }
 
+  &:disabled,
+  &.disabled {
+    svg path:last-child {
+      fill: $ui-primary-color;
+    }
+
+    &:active,
+    &:focus,
+    &:hover {
+      background: $ui-primary-color;
+
+      svg path:last-child {
+        fill: $ui-primary-color;
+      }
+    }
+  }
+
   &.button--destructive {
     &:active,
     &:focus,
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index e10faedf8..cea9a0c2e 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -351,7 +351,7 @@ class Status extends ImmutablePureComponent {
 
     return (
       <HotKeys handlers={handlers}>
-        <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))} ref={this.handleRef}>
+        <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
           {prepend}
 
           <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 9d15bc28f..e5b60e33e 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -22,8 +22,6 @@ const messages = defineMessages({
   account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
   mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
   direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
-  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
-  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
   unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
   block: { id: 'account.block', defaultMessage: 'Block @{name}' },
   mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
@@ -111,7 +109,7 @@ class Header extends ImmutablePureComponent {
       } else if (account.getIn(['relationship', 'requested'])) {
         actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
       } else if (!account.getIn(['relationship', 'blocking'])) {
-        actionBtn = <Button className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
+        actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
       } else if (account.getIn(['relationship', 'blocking'])) {
         actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
       }
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index 883f40d77..a01f1dd9a 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -14,14 +14,17 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import { FormattedMessage } from 'react-intl';
 import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
 
+const emptyList = ImmutableList();
+
 const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
   const path = withReplies ? `${accountId}:with_replies` : accountId;
 
   return {
-    statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()),
-    featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()),
+    statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
+    featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
     isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
-    hasMore:   state.getIn(['timelines', `account:${path}`, 'hasMore']),
+    hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
+    blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
   };
 };
 
@@ -37,6 +40,7 @@ class AccountTimeline extends ImmutablePureComponent {
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
     withReplies: PropTypes.bool,
+    blockedBy: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -44,9 +48,11 @@ class AccountTimeline extends ImmutablePureComponent {
 
     this.props.dispatch(fetchAccount(accountId));
     this.props.dispatch(fetchAccountIdentityProofs(accountId));
+
     if (!withReplies) {
       this.props.dispatch(expandAccountFeaturedTimeline(accountId));
     }
+
     this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
   }
 
@@ -54,9 +60,11 @@ class AccountTimeline extends ImmutablePureComponent {
     if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
       this.props.dispatch(fetchAccount(nextProps.params.accountId));
       this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
+
       if (!nextProps.withReplies) {
         this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
       }
+
       this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
     }
   }
@@ -66,7 +74,7 @@ class AccountTimeline extends ImmutablePureComponent {
   }
 
   render () {
-    const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore } = this.props;
+    const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy } = this.props;
 
     if (!statusIds && isLoading) {
       return (
@@ -76,6 +84,8 @@ class AccountTimeline extends ImmutablePureComponent {
       );
     }
 
+    const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />;
+
     return (
       <Column>
         <ColumnBackButton />
@@ -84,13 +94,13 @@ class AccountTimeline extends ImmutablePureComponent {
           prepend={<HeaderContainer accountId={this.props.params.accountId} />}
           alwaysPrepend
           scrollKey='account_timeline'
-          statusIds={statusIds}
+          statusIds={blockedBy ? emptyList : statusIds}
           featuredStatusIds={featuredStatusIds}
           isLoading={isLoading}
           hasMore={hasMore}
           onLoadMore={this.handleLoadMore}
           shouldUpdateScroll={shouldUpdateScroll}
-          emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />}
+          emptyMessage={emptyMessage}
         />
       </Column>
     );
diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js
index ce56f270c..ce6357c4c 100644
--- a/app/javascript/mastodon/features/followers/index.js
+++ b/app/javascript/mastodon/features/followers/index.js
@@ -20,6 +20,7 @@ import ScrollableList from '../../components/scrollable_list';
 const mapStateToProps = (state, props) => ({
   accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
   hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
+  blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
 });
 
 export default @connect(mapStateToProps)
@@ -31,6 +32,7 @@ class Followers extends ImmutablePureComponent {
     shouldUpdateScroll: PropTypes.func,
     accountIds: ImmutablePropTypes.list,
     hasMore: PropTypes.bool,
+    blockedBy: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -50,7 +52,7 @@ class Followers extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { shouldUpdateScroll, accountIds, hasMore } = this.props;
+    const { shouldUpdateScroll, accountIds, hasMore, blockedBy } = this.props;
 
     if (!accountIds) {
       return (
@@ -60,7 +62,7 @@ class Followers extends ImmutablePureComponent {
       );
     }
 
-    const emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
+    const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
 
     return (
       <Column>
@@ -75,7 +77,7 @@ class Followers extends ImmutablePureComponent {
           alwaysPrepend
           emptyMessage={emptyMessage}
         >
-          {accountIds.map(id =>
+          {blockedBy ? [] : 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 bda0438a0..70e7fde06 100644
--- a/app/javascript/mastodon/features/following/index.js
+++ b/app/javascript/mastodon/features/following/index.js
@@ -20,6 +20,7 @@ import ScrollableList from '../../components/scrollable_list';
 const mapStateToProps = (state, props) => ({
   accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
   hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
+  blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
 });
 
 export default @connect(mapStateToProps)
@@ -31,6 +32,7 @@ class Following extends ImmutablePureComponent {
     shouldUpdateScroll: PropTypes.func,
     accountIds: ImmutablePropTypes.list,
     hasMore: PropTypes.bool,
+    blockedBy: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -50,7 +52,7 @@ class Following extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { shouldUpdateScroll, accountIds, hasMore } = this.props;
+    const { shouldUpdateScroll, accountIds, hasMore, blockedBy } = this.props;
 
     if (!accountIds) {
       return (
@@ -60,7 +62,7 @@ class Following extends ImmutablePureComponent {
       );
     }
 
-    const emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
+    const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
 
     return (
       <Column>
@@ -75,7 +77,7 @@ class Following extends ImmutablePureComponent {
           alwaysPrepend
           emptyMessage={emptyMessage}
         >
-          {accountIds.map(id =>
+          {blockedBy ? [] : accountIds.map(id =>
             <AccountContainer key={id} id={id} withNote={false} />
           )}
         </ScrollableList>
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index c0ea460e8..567af6be9 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -442,7 +442,7 @@ class Status extends ImmutablePureComponent {
             {ancestors}
 
             <HotKeys handlers={handlers}>
-              <div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false, !status.get('hidden'))}>
+              <div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false)}>
                 <DetailedStatus
                   status={status}
                   onOpenVideo={this.handleOpenVideo}
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index 154feab98..016be39b3 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -117,6 +117,7 @@
   "emoji_button.symbols": "Simbuli",
   "emoji_button.travel": "Lochi è Viaghju",
   "empty_column.account_timeline": "Nisun statutu quì!",
+  "empty_column.account_unavailable": "Prufile micca dispunibule",
   "empty_column.blocks": "Per avà ùn avete bluccatu manc'un utilizatore.",
   "empty_column.community": "Ùn c'hè nunda indè a linea lucale. Scrivete puru qualcosa!",
   "empty_column.direct": "Ùn avete ancu nisun missaghju direttu. S'è voi mandate o ricevete unu, u vidarete quì.",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index 9701bd695..61d865154 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -117,6 +117,7 @@
   "emoji_button.symbols": "Symboly",
   "emoji_button.travel": "Cestování a místa",
   "empty_column.account_timeline": "Tady nejsou žádné tooty!",
+  "empty_column.account_unavailable": "Profil nedostupný",
   "empty_column.blocks": "Ještě jste nezablokoval/a žádného uživatele.",
   "empty_column.community": "Místní časová osa je prázdná. Napište něco veřejně a rozhýbejte to tu!",
   "empty_column.direct": "Ještě nemáte žádné přímé zprávy. Pokud nějakou pošlete nebo dostanete, zobrazí se zde.",
@@ -327,7 +328,7 @@
   "status.more": "Více",
   "status.mute": "Skrýt uživatele @{name}",
   "status.mute_conversation": "Skrýt konverzaci",
-  "status.open": "Rozbalit tento toot",
+  "status.open": "Otevřít tento toot",
   "status.pin": "Připnout na profil",
   "status.pinned": "Připnutý toot",
   "status.read_more": "Číst více",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index a335af1bd..13b8ccafa 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -117,6 +117,7 @@
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Reisen und Orte",
   "empty_column.account_timeline": "Keine Beiträge!",
+  "empty_column.account_unavailable": "Konto nicht verfügbar",
   "empty_column.blocks": "Du hast keine Profile blockiert.",
   "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe einen öffentlichen Beitrag, um den Ball ins Rollen zu bringen!",
   "empty_column.direct": "Du hast noch keine Direktnachrichten erhalten. Wenn du eine sendest oder empfängst, wird sie hier zu sehen sein.",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 951745120..76d4351d0 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -204,26 +204,6 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "Moments remaining",
-        "id": "time_remaining.moments"
-      },
-      {
-        "defaultMessage": "{number, plural, one {# second} other {# seconds}} left",
-        "id": "time_remaining.seconds"
-      },
-      {
-        "defaultMessage": "{number, plural, one {# minute} other {# minutes}} left",
-        "id": "time_remaining.minutes"
-      },
-      {
-        "defaultMessage": "{number, plural, one {# hour} other {# hours}} left",
-        "id": "time_remaining.hours"
-      },
-      {
-        "defaultMessage": "{number, plural, one {# day} other {# days}} left",
-        "id": "time_remaining.days"
-      },
-      {
         "defaultMessage": "Closed",
         "id": "poll.closed"
       },
@@ -263,6 +243,26 @@
       {
         "defaultMessage": "{number}d",
         "id": "relative_time.days"
+      },
+      {
+        "defaultMessage": "Moments remaining",
+        "id": "time_remaining.moments"
+      },
+      {
+        "defaultMessage": "{number, plural, one {# second} other {# seconds}} left",
+        "id": "time_remaining.seconds"
+      },
+      {
+        "defaultMessage": "{number, plural, one {# minute} other {# minutes}} left",
+        "id": "time_remaining.minutes"
+      },
+      {
+        "defaultMessage": "{number, plural, one {# hour} other {# hours}} left",
+        "id": "time_remaining.hours"
+      },
+      {
+        "defaultMessage": "{number, plural, one {# day} other {# days}} left",
+        "id": "time_remaining.days"
       }
     ],
     "path": "app/javascript/mastodon/components/relative_timestamp.json"
@@ -552,8 +552,8 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "You are blocked",
-        "id": "empty_column.account_timeline_blocked"
+        "defaultMessage": "Profile unavailable",
+        "id": "empty_column.account_unavailable"
       },
       {
         "defaultMessage": "No toots here!",
@@ -1256,8 +1256,8 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "You are blocked",
-        "id": "empty_column.account_timeline_blocked"
+        "defaultMessage": "Profile unavailable",
+        "id": "empty_column.account_unavailable"
       },
       {
         "defaultMessage": "No one follows this user yet.",
@@ -1269,8 +1269,8 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "You are blocked",
-        "id": "empty_column.account_timeline_blocked"
+        "defaultMessage": "Profile unavailable",
+        "id": "empty_column.account_unavailable"
       },
       {
         "defaultMessage": "This user doesn't follow anyone yet.",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index de3c3da9d..79e0c504b 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -121,7 +121,7 @@
   "emoji_button.symbols": "Symbols",
   "emoji_button.travel": "Travel & Places",
   "empty_column.account_timeline": "No toots here!",
-  "empty_column.account_timeline_blocked": "You are blocked",
+  "empty_column.account_unavailable": "Profile unavailable",
   "empty_column.blocks": "You haven't blocked any users yet.",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 84a65c6d2..0bf9a22c4 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -77,8 +77,8 @@
   "compose_form.placeholder": "今なにしてる?",
   "compose_form.poll.add_option": "追加",
   "compose_form.poll.duration": "アンケート期間",
-  "compose_form.poll.option_placeholder": "選択肢 {number}",
-  "compose_form.poll.remove_option": "この選択肢を削除",
+  "compose_form.poll.option_placeholder": "項目 {number}",
+  "compose_form.poll.remove_option": "この項目を削除",
   "compose_form.publish": "トゥート",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "メディアに閲覧注意が設定されています",
@@ -121,7 +121,7 @@
   "emoji_button.symbols": "記号",
   "emoji_button.travel": "旅行と場所",
   "empty_column.account_timeline": "トゥートがありません!",
-  "empty_column.account_timeline_blocked": "ブロックされています",
+  "empty_column.account_unavailable": "プロフィールは利用できません",
   "empty_column.blocks": "まだ誰もブロックしていません。",
   "empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
   "empty_column.direct": "ダイレクトメッセージはまだありません。ダイレクトメッセージをやりとりすると、ここに表示されます。",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 433592ffd..a1e81a9d0 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -117,6 +117,7 @@
   "emoji_button.symbols": "기호",
   "emoji_button.travel": "여행과 장소",
   "empty_column.account_timeline": "여긴 툿이 없어요!",
+  "empty_column.account_unavailable": "프로필 사용 불가",
   "empty_column.blocks": "아직 아무도 차단하지 않았습니다.",
   "empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!",
   "empty_column.direct": "아직 다이렉트 메시지가 없습니다. 다이렉트 메시지를 보내거나 받은 경우, 여기에 표시 됩니다.",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index e5e4748d7..cac676c7b 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -71,10 +71,10 @@
   "compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en kan de toots zien die je alleen aan jouw volgers hebt gericht.",
   "compose_form.lock_disclaimer.lock": "besloten",
   "compose_form.placeholder": "Wat wil je kwijt?",
-  "compose_form.poll.add_option": "Add a choice",
-  "compose_form.poll.duration": "Poll duration",
-  "compose_form.poll.option_placeholder": "Choice {number}",
-  "compose_form.poll.remove_option": "Remove this choice",
+  "compose_form.poll.add_option": "Keuze toevoegen",
+  "compose_form.poll.duration": "Duur van de poll",
+  "compose_form.poll.option_placeholder": "Keuze {number}",
+  "compose_form.poll.remove_option": "Deze keuze verwijderen",
   "compose_form.publish": "Toot",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "Media is als gevoelig gemarkeerd",
@@ -83,7 +83,7 @@
   "compose_form.spoiler.unmarked": "Tekst is niet verborgen",
   "compose_form.spoiler_placeholder": "Waarschuwingstekst",
   "confirmation_modal.cancel": "Annuleren",
-  "confirmations.block.block_and_report": "Block & Report",
+  "confirmations.block.block_and_report": "Blokkeren en rapporteren",
   "confirmations.block.confirm": "Blokkeren",
   "confirmations.block.message": "Weet je het zeker dat je {name} wilt blokkeren?",
   "confirmations.delete.confirm": "Verwijderen",
@@ -154,9 +154,9 @@
   "home.column_settings.basic": "Algemeen",
   "home.column_settings.show_reblogs": "Boosts tonen",
   "home.column_settings.show_replies": "Reacties tonen",
-  "intervals.full.days": "{number, plural, one {# day} other {# days}}",
-  "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
-  "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
+  "intervals.full.days": "{number, plural, one {# dag} other {# dagen}}",
+  "intervals.full.hours": "{number, plural, one {# uur} other {# uur}}",
+  "intervals.full.minutes": "{number, plural, one {# minuut} other {# minuten}}",
   "introduction.federation.action": "Volgende",
   "introduction.federation.federated.headline": "Globaal",
   "introduction.federation.federated.text": "Openbare toots van mensen op andere servers in de fediverse verschijnen op de globale tijdlijn.",
@@ -246,7 +246,7 @@
   "notification.favourite": "{name} voegde jouw toot als favoriet toe",
   "notification.follow": "{name} volgt jou nu",
   "notification.mention": "{name} vermeldde jou",
-  "notification.poll": "A poll you have voted in has ended",
+  "notification.poll": "Een poll waaraan jij hebt meegedaan is beëindigd",
   "notification.reblog": "{name} boostte jouw toot",
   "notifications.clear": "Meldingen verwijderen",
   "notifications.clear_confirmation": "Weet je het zeker dat je al jouw meldingen wilt verwijderen?",
@@ -257,7 +257,7 @@
   "notifications.column_settings.filter_bar.show": "Tonen",
   "notifications.column_settings.follow": "Nieuwe volgers:",
   "notifications.column_settings.mention": "Vermeldingen:",
-  "notifications.column_settings.poll": "Poll results:",
+  "notifications.column_settings.poll": "Pollresultaten:",
   "notifications.column_settings.push": "Pushmeldingen",
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "In kolom tonen",
@@ -267,14 +267,14 @@
   "notifications.filter.favourites": "Favorieten",
   "notifications.filter.follows": "Die jij volgt",
   "notifications.filter.mentions": "Vermeldingen",
-  "notifications.filter.polls": "Poll results",
+  "notifications.filter.polls": "Pollresultaten",
   "notifications.group": "{count} meldingen",
   "poll.closed": "Gesloten",
   "poll.refresh": "Vernieuwen",
   "poll.total_votes": "{count, plural, one {# stem} other {# stemmen}}",
   "poll.vote": "Stemmen",
-  "poll_button.add_poll": "Add a poll",
-  "poll_button.remove_poll": "Remove poll",
+  "poll_button.add_poll": "Poll toevoegen",
+  "poll_button.remove_poll": "Poll verwijderen",
   "privacy.change": "Zichtbaarheid toot aanpassen",
   "privacy.direct.long": "Alleen aan vermelde gebruikers tonen",
   "privacy.direct.short": "Direct",
@@ -366,7 +366,7 @@
   "upload_area.title": "Hierin slepen om te uploaden",
   "upload_button.label": "Media toevoegen (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Uploadlimiet van bestand overschreden.",
-  "upload_error.poll": "File upload not allowed with polls.",
+  "upload_error.poll": "Het uploaden van bestanden is in polls niet toegestaan.",
   "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking",
   "upload_form.focus": "Voorvertoning aanpassen",
   "upload_form.undo": "Verwijderen",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 107ac8757..505f35286 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -121,6 +121,7 @@
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Podróże i miejsca",
   "empty_column.account_timeline": "Brak wpisów tutaj!",
+  "empty_column.account_timeline_blocked": "Jesteś zablokowany(-a)",
   "empty_column.blocks": "Nie zablokowałeś(-aś) jeszcze żadnego użytkownika.",
   "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
   "empty_column.direct": "Nie masz żadnych wiadomości bezpośrednich. Kiedy dostaniesz lub wyślesz jakąś, pojawi się ona tutaj.",
@@ -251,7 +252,7 @@
   "notification.favourite": "{name} dodał(a) Twój wpis do ulubionych",
   "notification.follow": "{name} zaczął(-ęła) Cię śledzić",
   "notification.mention": "{name} wspomniał(a) o tobie",
-  "notification.poll": "A poll you have voted in has ended",
+  "notification.poll": "Głosowanie w którym brałeś(-aś) udział zakończyła się",
   "notification.reblog": "{name} podbił(a) Twój wpis",
   "notifications.clear": "Wyczyść powiadomienia",
   "notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?",
@@ -262,7 +263,7 @@
   "notifications.column_settings.filter_bar.show": "Pokaż",
   "notifications.column_settings.follow": "Nowi śledzący:",
   "notifications.column_settings.mention": "Wspomnienia:",
-  "notifications.column_settings.poll": "Poll results:",
+  "notifications.column_settings.poll": "Wyniki głosowania:",
   "notifications.column_settings.push": "Powiadomienia push",
   "notifications.column_settings.reblog": "Podbicia:",
   "notifications.column_settings.show": "Pokaż w kolumnie",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 5542198f7..52c79c0bb 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -6,7 +6,7 @@
   "account.blocked": "Blokovaný/á",
   "account.direct": "Súkromná správa pre @{name}",
   "account.domain_blocked": "Doména ukrytá",
-  "account.edit_profile": "Upraviť profil",
+  "account.edit_profile": "Uprav profil",
   "account.endorse": "Zobrazuj na profile",
   "account.follow": "Následuj",
   "account.followers": "Sledujúci",
@@ -14,38 +14,38 @@
   "account.follows": "Následuje",
   "account.follows.empty": "Tento užívateľ ešte nikoho nenásleduje.",
   "account.follows_you": "Následuje ťa",
-  "account.hide_reblogs": "Skryť povýšenia od @{name}",
+  "account.hide_reblogs": "Skry vyzdvihnutia od @{name}",
   "account.link_verified_on": "Vlastníctvo tohto odkazu bolo skontrolované {date}",
   "account.locked_info": "Stav súkromia pre tento účet je nastavený na zamknutý. Jeho vlastník sám prehodnocuje, kto ho môže sledovať.",
   "account.media": "Médiá",
   "account.mention": "Spomeň @{name}",
   "account.moved_to": "{name} sa presunul/a na:",
   "account.mute": "Ignorovať @{name}",
-  "account.mute_notifications": "Stĺmiť oboznámenia od @{name}",
+  "account.mute_notifications": "Stĺm oboznámenia od @{name}",
   "account.muted": "Utíšený/á",
   "account.posts": "Príspevky",
   "account.posts_with_replies": "Príspevky aj s odpoveďami",
   "account.report": "Nahlás @{name}",
-  "account.requested": "Čaká na schválenie. Kliknite pre zrušenie žiadosti",
-  "account.share": "Zdieľať @{name} profil",
+  "account.requested": "Čaká na schválenie. Klikni pre zrušenie žiadosti",
+  "account.share": "Zdieľaj @{name} profil",
   "account.show_reblogs": "Ukáž vyzdvihnutia od @{name}",
   "account.unblock": "Odblokuj @{name}",
   "account.unblock_domain": "Prestaň skrývať {domain}",
   "account.unendorse": "Nezobrazuj na profile",
   "account.unfollow": "Prestaň následovať",
   "account.unmute": "Prestaň ignorovať @{name}",
-  "account.unmute_notifications": "Zrušiť stlmenie oznámení od @{name}",
+  "account.unmute_notifications": "Zruš stĺmenie oboznámení od @{name}",
   "alert.unexpected.message": "Vyskytla sa nečakaná chyba.",
   "alert.unexpected.title": "Oops!",
   "boost_modal.combo": "Nabudúce môžeš kliknúť {combo} pre preskočenie",
   "bundle_column_error.body": "Pri načítaní tohto prvku nastala nejaká chyba.",
   "bundle_column_error.retry": "Skús to znova",
   "bundle_column_error.title": "Chyba siete",
-  "bundle_modal_error.close": "Zatvoriť",
+  "bundle_modal_error.close": "Zatvor",
   "bundle_modal_error.message": "Nastala chyba pri načítaní tohto komponentu.",
   "bundle_modal_error.retry": "Skúsiť znova",
   "column.blocks": "Blokovaní užívatelia",
-  "column.community": "Lokálna časová os",
+  "column.community": "Miestna časová os",
   "column.direct": "Súkromné správy",
   "column.domain_blocks": "Skryté domény",
   "column.favourites": "Obľúbené",
@@ -64,11 +64,11 @@
   "column_header.show_settings": "Ukáž nastavenia",
   "column_header.unpin": "Odopnúť",
   "column_subheading.settings": "Nastavenia",
-  "community.column_settings.media_only": "Iba media",
+  "community.column_settings.media_only": "Iba médiá",
   "compose_form.direct_message_warning": "Tento príspevok bude videný výhradne iba spomenutými užívateľmi. Ber ale na vedomie že správci tvojej a všetkých iných zahrnutých instancií majú možnosť skontrolovať túto správu.",
   "compose_form.direct_message_warning_learn_more": "Zistiť viac",
   "compose_form.hashtag_warning": "Tento toot nebude zobrazený pod žiadným haštagom lebo nieje listovaný. Iba verejné tooty môžu byť nájdené podľa haštagu.",
-  "compose_form.lock_disclaimer": "Váš účet nie je {locked}. Ktokoľvek ťa môže nasledovať a vidieť tvoje správy pre sledujúcich.",
+  "compose_form.lock_disclaimer": "Tvoj účet nie je {locked}. Ktokoľvek ťa môže nasledovať a vidieť tvoje správy pre sledujúcich.",
   "compose_form.lock_disclaimer.lock": "zamknutý",
   "compose_form.placeholder": "Čo máš na mysli?",
   "compose_form.poll.add_option": "Pridaj voľbu",
@@ -90,29 +90,29 @@
   "confirmations.delete.message": "Si si istý/á, že chceš vymazať túto správu?",
   "confirmations.delete_list.confirm": "Vymaž",
   "confirmations.delete_list.message": "Si si istý/á, že chceš natrvalo vymazať tento zoznam?",
-  "confirmations.domain_block.confirm": "Skryť celú doménu",
-  "confirmations.domain_block.message": "Si si naozaj istý, že chceš blokovať celú {domain}? Vo väčšine prípadov stačí blokovať alebo ignorovať pár konkrétnych užívateľov, čo sa doporučuje. Neuvidíš obsah z tejto domény v žiadnej verejnej časovej osi, ani v oznámeniach. Tvoji následovníci pochádzajúci z tejto domény budú odstránení.",
+  "confirmations.domain_block.confirm": "Skry celú doménu",
+  "confirmations.domain_block.message": "Si si naozaj istý/á, že chceš blokovať celú doménu {domain}? Vo väčšine prípadov stačí blokovať alebo ignorovať pár konkrétnych užívateľov, čo sa doporučuje. Neuvidíš obsah z tejto domény v žiadnej verejnej časovej osi, ani v oznámeniach. Tvoji následovníci pochádzajúci z tejto domény budú odstránení.",
   "confirmations.mute.confirm": "Ignoruj",
-  "confirmations.mute.message": "Naozaj chcete ignorovať {name}?",
-  "confirmations.redraft.confirm": "Vyčistiť a prepísať",
+  "confirmations.mute.message": "Naozaj chceš ignorovať {name}?",
+  "confirmations.redraft.confirm": "Vyčisti a prepíš",
   "confirmations.redraft.message": "Si si istý/á, že chceš premazať a prepísať tento príspevok? Jeho nadobudnuté vyzdvihnutia a obľúbenia, ale i odpovede na pôvodný príspevok budú odlúčené.",
   "confirmations.reply.confirm": "Odpovedz",
   "confirmations.reply.message": "Odpovedaním akurát teraz prepíšeš správu, ktorú máš práve rozpísanú. Si si istý/á, že chceš pokračovať?",
-  "confirmations.unfollow.confirm": "Nesledovať",
-  "confirmations.unfollow.message": "Naozaj chcete prestať sledovať {name}?",
+  "confirmations.unfollow.confirm": "Nesleduj",
+  "confirmations.unfollow.message": "Naozaj chceš prestať sledovať {name}?",
   "embed.instructions": "Umiestni kód uvedený nižšie pre pridanie tohto statusu na tvoju web stránku.",
   "embed.preview": "Tu je ako to bude vyzerať:",
   "emoji_button.activity": "Aktivita",
   "emoji_button.custom": "Vlastné",
   "emoji_button.flags": "Vlajky",
   "emoji_button.food": "Jedlá a nápoje",
-  "emoji_button.label": "Vložiť emotikony",
+  "emoji_button.label": "Vlož emotikony",
   "emoji_button.nature": "Prírodné",
   "emoji_button.not_found": "Nie emotikony!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Predmety",
   "emoji_button.people": "Ľudia",
   "emoji_button.recent": "Často používané",
-  "emoji_button.search": "Hľadať...",
+  "emoji_button.search": "Hľadaj...",
   "emoji_button.search_results": "Nájdené",
   "emoji_button.symbols": "Symboly",
   "emoji_button.travel": "Cestovanie a miesta",
@@ -158,16 +158,16 @@
   "intervals.full.hours": "{number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodiny}}",
   "intervals.full.minutes": "{number, plural, one {# minúta} few {# minút} many {# minút} other {# minúty}}",
   "introduction.federation.action": "Ďalej",
-  "introduction.federation.federated.headline": "Federated",
+  "introduction.federation.federated.headline": "Federovaná",
   "introduction.federation.federated.text": "Verejné príspevky z ostatných serverov vo fediverse budú zobrazenie vo federovanej časovej osi.",
-  "introduction.federation.home.headline": "Home",
+  "introduction.federation.home.headline": "Domov",
   "introduction.federation.home.text": "Príspevky od ľudí ktorých následuješ sa zobrazia na tvojej domovskej nástenke. Môžeš následovať hocikoho na ktoromkoľvek serveri!",
-  "introduction.federation.local.headline": "Local",
+  "introduction.federation.local.headline": "Miestna",
   "introduction.federation.local.text": "Verejné príspevky od ľudí v rámci toho istého serveru na akom si aj ty, budú zobrazované na miestnej časovej osi.",
   "introduction.interactions.action": "Ukonči návod!",
   "introduction.interactions.favourite.headline": "Obľúbené",
   "introduction.interactions.favourite.text": "Obľúbením si môžeš príspevok uložiť na neskôr, a zároveň dať jeho autorovi vedieť, že sa ti páčil.",
-  "introduction.interactions.reblog.headline": "Povýš",
+  "introduction.interactions.reblog.headline": "Vyzdvihni",
   "introduction.interactions.reblog.text": "Môžeš zdieľať príspevky iných ľudí s vašimi následovateľmi tým, že ich povýšiš.",
   "introduction.interactions.reply.headline": "Odpovedz",
   "introduction.interactions.reply.text": "Odpovedať môžeš na príspevky iných ľudí, aj na svoje vlastné, čím sa spolu prepoja do konverzácie.",
@@ -210,7 +210,7 @@
   "lightbox.previous": "Predchádzajúci",
   "lists.account.add": "Pridať do zoznamu",
   "lists.account.remove": "Odobrať zo zoznamu",
-  "lists.delete": "Vymazať list",
+  "lists.delete": "Vymaž list",
   "lists.edit": "Uprav zoznam",
   "lists.edit.submit": "Zmeň názov",
   "lists.new.create": "Pridaj zoznam",
@@ -259,7 +259,7 @@
   "notifications.column_settings.mention": "Zmienenia:",
   "notifications.column_settings.poll": "Výsledky ankiet:",
   "notifications.column_settings.push": "Push notifikácie",
-  "notifications.column_settings.reblog": "Boosty:",
+  "notifications.column_settings.reblog": "Vyzdvihnutia:",
   "notifications.column_settings.show": "Zobraziť v stĺpci",
   "notifications.column_settings.sound": "Prehrať zvuk",
   "notifications.filter.all": "Všetky",
@@ -278,7 +278,7 @@
   "privacy.change": "Uprav súkromie príspevku",
   "privacy.direct.long": "Pošli iba spomenutým používateľom",
   "privacy.direct.short": "Súkromne",
-  "privacy.private.long": "Poslať iba následovateľom",
+  "privacy.private.long": "Pošli iba následovateľom",
   "privacy.private.short": "Iba pre sledujúcich",
   "privacy.public.long": "Poslať všetkým verejne",
   "privacy.public.short": "Verejné",
@@ -311,9 +311,9 @@
   "search_results.total": "{count, number} {count, plural, one {výsledok} many {výsledkov} other {výsledky}}",
   "status.admin_account": "Otvor moderovacie rozhranie užívateľa @{name}",
   "status.admin_status": "Otvor tento príspevok v moderovacom rozhraní",
-  "status.block": "Blokovať @{name}",
+  "status.block": "Blokuj @{name}",
   "status.cancel_reblog_private": "Nezdieľaj",
-  "status.cannot_reblog": "Tento príspevok nemôže byť re-tootnutý",
+  "status.cannot_reblog": "Tento príspevok nemôže byť zdieľaný",
   "status.copy": "Skopíruj odkaz na príspevok",
   "status.delete": "Zmazať",
   "status.detailed_status": "Podrobný náhľad celej konverzácie",
@@ -325,14 +325,14 @@
   "status.media_hidden": "Skryté médiá",
   "status.mention": "Spomeň @{name}",
   "status.more": "Viac",
-  "status.mute": "Utíšiť @{name}",
-  "status.mute_conversation": "Ignorovať konverzáciu",
+  "status.mute": "Utíš @{name}",
+  "status.mute_conversation": "Ignoruj konverzáciu",
   "status.open": "Otvoriť tento status",
   "status.pin": "Pripni na profil",
   "status.pinned": "Pripnutý príspevok",
   "status.read_more": "Čítaj ďalej",
   "status.reblog": "Povýšiť",
-  "status.reblog_private": "Povýš k pôvodnému publiku",
+  "status.reblog_private": "Vyzdvihni k pôvodnému publiku",
   "status.reblogged_by": "{name} povýšil/a",
   "status.reblogs.empty": "Nikto ešte nepovýšil tento príspevok. Keď tak niekto urobí, bude to zobrazené práve tu.",
   "status.redraft": "Vymaž a prepíš",
@@ -341,7 +341,7 @@
   "status.report": "Nahlásiť @{name}",
   "status.sensitive_toggle": "Klikni pre zobrazenie",
   "status.sensitive_warning": "Chúlostivý obsah",
-  "status.share": "Zdieľať",
+  "status.share": "Zdieľaj",
   "status.show_less": "Zobraz menej",
   "status.show_less_all": "Všetkým ukáž menej",
   "status.show_more": "Ukáž viac",
@@ -353,8 +353,8 @@
   "suggestions.header": "Mohlo by ťa zaujímať…",
   "tabs_bar.federated_timeline": "Federovaná",
   "tabs_bar.home": "Domov",
-  "tabs_bar.local_timeline": "Lokálna",
-  "tabs_bar.notifications": "Notifikácie",
+  "tabs_bar.local_timeline": "Miestna",
+  "tabs_bar.notifications": "Oboznámenia",
   "tabs_bar.search": "Hľadaj",
   "time_remaining.days": "Zostáva {number, plural, one {# deň} few {# dní} many {# dni} other {# dni}}",
   "time_remaining.hours": "Zostáva {number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodiny}}",
@@ -371,13 +371,13 @@
   "upload_form.focus": "Pozmeň náhľad",
   "upload_form.undo": "Vymaž",
   "upload_progress.label": "Nahráva sa...",
-  "video.close": "Zavrieť video",
+  "video.close": "Zavri video",
   "video.exit_fullscreen": "Vpnúť zobrazenie na celú obrazovku",
-  "video.expand": "Zväčšiť video",
+  "video.expand": "Zväčši video",
   "video.fullscreen": "Zobraziť na celú obrazovku",
   "video.hide": "Skryť video",
   "video.mute": "Vypnúť zvuk",
   "video.pause": "Pauza",
-  "video.play": "Prehrať",
-  "video.unmute": "Zapnúť zvuk"
+  "video.play": "Prehraj",
+  "video.unmute": "Zapni zvuk"
 }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 2ad93c59d..9fcaeb8dd 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -46,7 +46,8 @@
     }
   }
 
-  &:disabled {
+  &:disabled,
+  &.disabled {
     background-color: $ui-primary-color;
     cursor: default;
   }
diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss
index d8bd30377..63eeffe25 100644
--- a/app/javascript/styles/mastodon/stream_entries.scss
+++ b/app/javascript/styles/mastodon/stream_entries.scss
@@ -109,6 +109,23 @@
     }
   }
 
+  &:disabled,
+  &.disabled {
+    svg path:last-child {
+      fill: $ui-primary-color;
+    }
+
+    &:active,
+    &:focus,
+    &:hover {
+      background: $ui-primary-color;
+
+      svg path:last-child {
+        fill: $ui-primary-color;
+      }
+    }
+  }
+
   &.button--destructive {
     &:active,
     &:focus,
diff --git a/app/lib/proof_provider/keybase/config_serializer.rb b/app/lib/proof_provider/keybase/config_serializer.rb
index 5241d201f..2840f1823 100644
--- a/app/lib/proof_provider/keybase/config_serializer.rb
+++ b/app/lib/proof_provider/keybase/config_serializer.rb
@@ -34,7 +34,7 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer
   end
 
   def username
-    { min: 1, max: 30, re: '[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?' }
+    { min: 1, max: 30, re: '[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?' }
   end
 
   def prefill_url
diff --git a/app/models/account.rb b/app/models/account.rb
index 983d38e0e..a82251d2e 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -102,7 +102,6 @@ class Account < ApplicationRecord
   scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
   scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) }
   scope :popular, -> { order('account_stats.followers_count desc') }
-  scope :without_blocking, ->(account) { account.nil? ? all : where.not(id: Block.where(target_account_id: account.id).pluck(:account_id)) }
 
   delegate :email,
            :unconfirmed_email,
diff --git a/app/models/concerns/account_finder_concern.rb b/app/models/concerns/account_finder_concern.rb
index 7e3bbde09..0ac49cc12 100644
--- a/app/models/concerns/account_finder_concern.rb
+++ b/app/models/concerns/account_finder_concern.rb
@@ -13,7 +13,7 @@ module AccountFinderConcern
     end
 
     def representative
-      find_local(Setting.site_contact_username.gsub(/\A@/, '')) || Account.local.find_by(suspended: false)
+      find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) || Account.local.find_by(suspended: false)
     end
 
     def find_local(username)
diff --git a/app/models/export.rb b/app/models/export.rb
index b35632c60..cab01f11a 100644
--- a/app/models/export.rb
+++ b/app/models/export.rb
@@ -22,7 +22,11 @@ class Export
   end
 
   def to_following_accounts_csv
-    to_csv account.following.select(:username, :domain)
+    CSV.generate(headers: ['Account address', 'Show boosts'], write_headers: true) do |csv|
+      account.active_relationships.includes(:target_account).reorder(id: :desc).each do |follow|
+        csv << [acct(follow.target_account), follow.show_reblogs]
+      end
+    end
   end
 
   def to_lists_csv
diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb
index 60eaaf0e2..5bc44e809 100644
--- a/app/models/form/account_batch.rb
+++ b/app/models/form/account_batch.rb
@@ -2,6 +2,7 @@
 
 class Form::AccountBatch
   include ActiveModel::Model
+  include Authorization
 
   attr_accessor :account_ids, :action, :current_account
 
@@ -13,6 +14,10 @@ class Form::AccountBatch
       remove_from_followers!
     when 'block_domains'
       block_domains!
+    when 'approve'
+      approve!
+    when 'reject'
+      reject!
     end
   end
 
@@ -57,4 +62,18 @@ class Form::AccountBatch
 
     ActivityPub::DeliveryWorker.perform_async(json, current_account.id, follow.account.inbox_url)
   end
+
+  def approve!
+    users = accounts.includes(:user).map(&:user)
+
+    users.each { |user| authorize(user, :approve?) }
+         .each(&:approve!)
+  end
+
+  def reject!
+    records = accounts.includes(:user)
+
+    records.each { |account| authorize(account.user, :reject?) }
+           .each { |account| SuspendAccountService.new.call(account, including_user: true, destroy: true, skip_distribution: true) }
+  end
 end
diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb
index e4aaa65f6..b05673a3d 100644
--- a/app/presenters/account_relationships_presenter.rb
+++ b/app/presenters/account_relationships_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class AccountRelationshipsPresenter
-  attr_reader :following, :followed_by, :blocking,
+  attr_reader :following, :followed_by, :blocking, :blocked_by,
               :muting, :requested, :domain_blocking,
               :endorsed
 
@@ -12,6 +12,7 @@ class AccountRelationshipsPresenter
     @following       = cached[:following].merge(Account.following_map(@uncached_account_ids, @current_account_id))
     @followed_by     = cached[:followed_by].merge(Account.followed_by_map(@uncached_account_ids, @current_account_id))
     @blocking        = cached[:blocking].merge(Account.blocking_map(@uncached_account_ids, @current_account_id))
+    @blocked_by      = cached[:blocked_by].merge(Account.blocked_by_map(@uncached_account_ids, @current_account_id))
     @muting          = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id))
     @requested       = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id))
     @domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id))
@@ -22,6 +23,7 @@ class AccountRelationshipsPresenter
     @following.merge!(options[:following_map] || {})
     @followed_by.merge!(options[:followed_by_map] || {})
     @blocking.merge!(options[:blocking_map] || {})
+    @blocked_by.merge!(options[:blocked_by_map] || {})
     @muting.merge!(options[:muting_map] || {})
     @requested.merge!(options[:requested_map] || {})
     @domain_blocking.merge!(options[:domain_blocking_map] || {})
@@ -37,6 +39,7 @@ class AccountRelationshipsPresenter
       following: {},
       followed_by: {},
       blocking: {},
+      blocked_by: {},
       muting: {},
       requested: {},
       domain_blocking: {},
@@ -64,6 +67,7 @@ class AccountRelationshipsPresenter
         following:       { account_id => following[account_id] },
         followed_by:     { account_id => followed_by[account_id] },
         blocking:        { account_id => blocking[account_id] },
+        blocked_by:      { account_id => blocked_by[account_id] },
         muting:          { account_id => muting[account_id] },
         requested:       { account_id => requested[account_id] },
         domain_blocking: { account_id => domain_blocking[account_id] },
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
index d234516e0..534752932 100644
--- a/app/presenters/instance_presenter.rb
+++ b/app/presenters/instance_presenter.rb
@@ -13,7 +13,7 @@ class InstancePresenter
   )
 
   def contact_account
-    Account.find_local(Setting.site_contact_username.gsub(/\A@/, ''))
+    Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, ''))
   end
 
   def user_count
diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb
index c6c722a54..1a3fd915c 100644
--- a/app/serializers/rest/relationship_serializer.rb
+++ b/app/serializers/rest/relationship_serializer.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class REST::RelationshipSerializer < ActiveModel::Serializer
-  attributes :id, :following, :showing_reblogs, :followed_by, :blocking,
+  attributes :id, :following, :showing_reblogs, :followed_by, :blocking, :blocked_by,
              :muting, :muting_notifications, :requested, :domain_blocking,
              :endorsed
 
@@ -27,6 +27,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
     instance_options[:relationships].blocking[object.id] || false
   end
 
+  def blocked_by
+    instance_options[:relationships].blocked_by[object.id] || false
+  end
+
   def muting
     instance_options[:relationships].muting[object.id] ? true : false
   end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index c47b1c094..7bdffbbd2 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -10,15 +10,7 @@ class AccountSearchService < BaseService
     @options = options
     @account = account
 
-    results = search_service_results
-
-    unless account.nil?
-      account_ids    = results.map(&:id)
-      blocked_by_map = Account.blocked_by_map(account_ids, account.id)
-      results.reject! { |item| blocked_by_map[item.id] }
-    end
-
-    results
+    search_service_results
   end
 
   private
diff --git a/app/services/import_service.rb b/app/services/import_service.rb
index c1c88e0dd..4ee431ea3 100644
--- a/app/services/import_service.rb
+++ b/app/services/import_service.rb
@@ -25,7 +25,7 @@ class ImportService < BaseService
 
   def import_follows!
     parse_import_data!(['Account address'])
-    import_relationships!('follow', 'unfollow', @account.following, follow_limit)
+    import_relationships!('follow', 'unfollow', @account.following, follow_limit, reblogs: 'Show boosts')
   end
 
   def import_blocks!
@@ -35,7 +35,7 @@ class ImportService < BaseService
 
   def import_mutes!
     parse_import_data!(['Account address'])
-    import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT)
+    import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: 'Hide notifications')
   end
 
   def import_domain_blocks!
@@ -63,8 +63,8 @@ class ImportService < BaseService
     end
   end
 
-  def import_relationships!(action, undo_action, overwrite_scope, limit)
-    items = @data.take(limit).map { |row| [row['Account address']&.strip, row['Hide notifications']&.strip] }.reject { |(id, _)| id.blank? }
+  def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
+    items = @data.take(limit).map { |row| [row['Account address']&.strip, Hash[extra_fields.map { |key, header| [key, row[header]&.strip] }]] }.reject { |(id, _)| id.blank? }
 
     if @import.overwrite?
       presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }
@@ -73,7 +73,7 @@ class ImportService < BaseService
         if presence_hash[target_account.acct]
           items.delete(target_account.acct)
           extra = presence_hash[target_account.acct][1]
-          Import::RelationshipWorker.perform_async(@account.id, target_account.acct, action, ActiveModel::Type::Boolean.new.cast(extra))
+          Import::RelationshipWorker.perform_async(@account.id, target_account.acct, action, extra)
         else
           Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action)
         end
@@ -81,7 +81,7 @@ class ImportService < BaseService
     end
 
     Import::RelationshipWorker.push_bulk(items) do |acct, extra|
-      [@account.id, acct, action, ActiveModel::Type::Boolean.new.cast(extra)]
+      [@account.id, acct, action, extra]
     end
   end
 
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index a8442654c..e0da61dac 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -12,8 +12,6 @@ class SearchService < BaseService
     default_results.tap do |results|
       if url_query?
         results.merge!(url_resource_results) unless url_resource.nil?
-        results[:accounts].reject! { |item| item.blocking?(@account) }
-        results[:statuses].reject! { |status| StatusFilter.new(status, @account).filtered? }
       elsif @query.present?
         results[:accounts] = perform_accounts_search! if account_searchable?
         results[:statuses] = perform_statuses_search! if full_text_searchable?
diff --git a/app/validators/existing_username_validator.rb b/app/validators/existing_username_validator.rb
index 4388a0c98..b31d09827 100644
--- a/app/validators/existing_username_validator.rb
+++ b/app/validators/existing_username_validator.rb
@@ -5,16 +5,10 @@ class ExistingUsernameValidator < ActiveModel::EachValidator
     return if value.blank?
 
     if options[:multiple]
-      missing_usernames = value.split(',').map { |username| username unless Account.find_local(username) }.compact
+      missing_usernames = value.split(',').map { |username| username.strip.gsub(/\A@/, '') }.map { |username| username unless Account.find_local(username) }.compact
       record.errors.add(attribute, I18n.t('existing_username_validator.not_found_multiple', usernames: missing_usernames.join(', '))) if missing_usernames.any?
     else
-      record.errors.add(attribute, I18n.t('existing_username_validator.not_found')) unless Account.find_local(value)
+      record.errors.add(attribute, I18n.t('existing_username_validator.not_found')) unless Account.find_local(value.strip.gsub(/\A@/, ''))
     end
   end
-
-  private
-
-  def valid_html?(str)
-    Nokogiri::HTML.fragment(str).to_s == str
-  end
 end
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 0da69728f..e4223119c 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -33,7 +33,9 @@
         = active_link_to t('accounts.posts_with_replies'), short_account_with_replies_url(@account)
         = active_link_to t('accounts.media'), short_account_media_url(@account)
 
-      - if @statuses.empty?
+      - if user_signed_in? && @account.blocking?(current_account)
+        .nothing-here.nothing-here--under-tabs= t('accounts.unavailable')
+      - elsif @statuses.empty?
         = nothing_here 'nothing-here--under-tabs'
       - else
         .activity-stream
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 66808add7..7e9adb3ff 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -10,7 +10,7 @@
   .filter-subset
     %strong= t('admin.accounts.moderation.title')
     %ul
-      %li= filter_link_to t('admin.accounts.moderation.pending'), pending: '1', silenced: nil, suspended: nil
+      %li= link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), admin_pending_accounts_path
       %li= filter_link_to t('admin.accounts.moderation.active'), silenced: nil, suspended: nil, pending: nil
       %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1', suspended: nil, pending: nil
       %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1', silenced: nil, pending: nil
diff --git a/app/views/admin/pending_accounts/_account.html.haml b/app/views/admin/pending_accounts/_account.html.haml
new file mode 100644
index 000000000..c520dc065
--- /dev/null
+++ b/app/views/admin/pending_accounts/_account.html.haml
@@ -0,0 +1,14 @@
+.batch-table__row
+  %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
+    = f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id
+  .batch-table__row__content.batch-table__row__content--unpadded
+    %table.accounts-table
+      %tbody
+        %tr
+          %td
+            = account.user_email
+            = "(@#{account.username})"
+            %br/
+            = account.user_current_sign_in_ip
+          %td.accounts-table__count
+            = table_link_to 'pencil', t('admin.accounts.edit'), admin_account_path(account.id)
diff --git a/app/views/admin/pending_accounts/index.html.haml b/app/views/admin/pending_accounts/index.html.haml
new file mode 100644
index 000000000..1bfd3824f
--- /dev/null
+++ b/app/views/admin/pending_accounts/index.html.haml
@@ -0,0 +1,30 @@
+- content_for :page_title do
+  = t('admin.pending_accounts.title', count: User.pending.count)
+
+= form_for(@form, url: admin_pending_accounts_path, method: :patch) do |f|
+  = hidden_field_tag :page, params[:page] || 1
+
+  .batch-table
+    .batch-table__toolbar
+      %label.batch-table__toolbar__select.batch-checkbox-all
+        = check_box_tag :batch_checkbox_all, nil, false
+      .batch-table__toolbar__actions
+        = f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+
+        = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+    .batch-table__body
+      - if @accounts.empty?
+        = nothing_here 'nothing-here--under-tabs'
+      - else
+        = render partial: 'account', collection: @accounts, locals: { f: f }
+
+= paginate @accounts
+
+%hr.spacer/
+
+%div{ style: 'overflow: hidden' }
+  %div{ style: 'float: right' }
+    = link_to t('admin.accounts.reject_all'), reject_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
+
+  %div
+    = link_to t('admin.accounts.approve_all'), approve_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml
index 31dab68bf..645dd2de1 100644
--- a/app/views/follower_accounts/index.html.haml
+++ b/app/views/follower_accounts/index.html.haml
@@ -9,6 +9,8 @@
 
 - if @account.user_hides_network?
   .nothing-here= t('accounts.network_hidden')
+- elsif user_signed_in? && @account.blocking?(current_account)
+  .nothing-here= t('accounts.unavailable')
 - elsif @follows.empty?
   = nothing_here
 - else
diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml
index 8b49b529b..17fe79018 100644
--- a/app/views/following_accounts/index.html.haml
+++ b/app/views/following_accounts/index.html.haml
@@ -9,6 +9,8 @@
 
 - if @account.user_hides_network?
   .nothing-here= t('accounts.network_hidden')
+- elsif user_signed_in? && @account.blocking?(current_account)
+  .nothing-here= t('accounts.unavailable')
 - elsif @follows.empty?
   = nothing_here
 - else
diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb
index 43ec09ea2..616da6da9 100644
--- a/app/workers/import/relationship_worker.rb
+++ b/app/workers/import/relationship_worker.rb
@@ -5,15 +5,16 @@ class Import::RelationshipWorker
 
   sidekiq_options queue: 'pull', retry: 8, dead: false
 
-  def perform(account_id, target_account_uri, relationship, extra = nil)
+  def perform(account_id, target_account_uri, relationship, options = {})
     from_account   = Account.find(account_id)
     target_account = ResolveAccountService.new.call(target_account_uri)
+    options.symbolize_keys!
 
     return if target_account.nil?
 
     case relationship
     when 'follow'
-      FollowService.new.call(from_account, target_account)
+      FollowService.new.call(from_account, target_account, options)
     when 'unfollow'
       UnfollowService.new.call(from_account, target_account)
     when 'block'
@@ -21,7 +22,7 @@ class Import::RelationshipWorker
     when 'unblock'
       UnblockService.new.call(from_account, target_account)
     when 'mute'
-      MuteService.new.call(from_account, target_account, notifications: extra)
+      MuteService.new.call(from_account, target_account, options)
     when 'unmute'
       UnmuteService.new.call(from_account, target_account)
     end