about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/api/v1/preferences_controller.rb12
-rw-r--r--app/controllers/api/v1/statuses/reblogs_controller.rb6
-rw-r--r--app/controllers/relationships_controller.rb103
-rw-r--r--app/controllers/settings/follower_domains_controller.rb24
-rw-r--r--app/helpers/admin/action_logs_helper.rb72
-rw-r--r--app/helpers/admin/filter_helper.rb3
-rw-r--r--app/javascript/flavours/glitch/styles/tables.scss19
-rw-r--r--app/javascript/flavours/glitch/theme.yml6
-rw-r--r--app/javascript/flavours/vanilla/theme.yml6
-rw-r--r--app/javascript/mastodon/components/error_boundary.js39
-rw-r--r--app/javascript/mastodon/containers/mastodon.js5
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle.js5
-rw-r--r--app/javascript/mastodon/locales/cs.json11
-rw-r--r--app/javascript/styles/mastodon/tables.scss19
-rw-r--r--app/lib/language_detector.rb31
-rw-r--r--app/models/form/account_batch.rb60
-rw-r--r--app/models/status.rb4
-rw-r--r--app/serializers/rest/instance_serializer.rb2
-rw-r--r--app/serializers/rest/preferences_serializer.rb30
-rw-r--r--app/serializers/rss/account_serializer.rb2
-rw-r--r--app/serializers/rss/tag_serializer.rb2
-rw-r--r--app/services/post_status_service.rb18
-rw-r--r--app/services/reblog_service.rb7
-rw-r--r--app/views/about/more.html.haml4
-rw-r--r--app/views/about/show.html.haml4
-rw-r--r--app/views/admin/invites/_invite.html.haml11
-rw-r--r--app/views/application/_sidebar.html.haml2
-rw-r--r--app/views/home/index.html.haml2
-rw-r--r--app/views/invites/_invite.html.haml10
-rw-r--r--app/views/layouts/admin.html.haml2
-rw-r--r--app/views/layouts/auth.html.haml2
-rw-r--r--app/views/layouts/mailer.html.haml4
-rw-r--r--app/views/layouts/public.html.haml2
-rw-r--r--app/views/notification_mailer/favourite.html.haml2
-rw-r--r--app/views/notification_mailer/follow.html.haml2
-rw-r--r--app/views/notification_mailer/follow_request.html.haml2
-rw-r--r--app/views/notification_mailer/mention.html.haml2
-rw-r--r--app/views/notification_mailer/reblog.html.haml2
-rw-r--r--app/views/relationships/_account.html.haml20
-rw-r--r--app/views/relationships/show.html.haml40
-rw-r--r--app/views/settings/flavours/show.html.haml2
-rw-r--r--app/views/settings/follower_domains/show.html.haml34
-rw-r--r--app/views/shared/_og.html.haml2
-rw-r--r--app/views/user_mailer/backup_ready.html.haml2
-rw-r--r--app/views/user_mailer/confirmation_instructions.html.haml2
-rw-r--r--app/views/user_mailer/email_changed.html.haml2
-rw-r--r--app/views/user_mailer/password_change.html.haml2
-rw-r--r--app/views/user_mailer/reconfirmation_instructions.html.haml2
-rw-r--r--app/views/user_mailer/reset_password_instructions.html.haml2
-rw-r--r--app/views/user_mailer/warning.html.haml2
-rw-r--r--app/views/user_mailer/welcome.html.haml2
51 files changed, 489 insertions, 164 deletions
diff --git a/app/controllers/api/v1/preferences_controller.rb b/app/controllers/api/v1/preferences_controller.rb
new file mode 100644
index 000000000..077d39f5d
--- /dev/null
+++ b/app/controllers/api/v1/preferences_controller.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class Api::V1::PreferencesController < Api::BaseController
+  before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
+  before_action :require_user!
+
+  respond_to :json
+
+  def index
+    render json: current_account, serializer: REST::PreferencesSerializer
+  end
+end
diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb
index 04847a6b7..ed4f55100 100644
--- a/app/controllers/api/v1/statuses/reblogs_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogs_controller.rb
@@ -9,7 +9,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
   respond_to :json
 
   def create
-    @status = ReblogService.new.call(current_user.account, status_for_reblog)
+    @status = ReblogService.new.call(current_user.account, status_for_reblog, reblog_params)
     render json: @status, serializer: REST::StatusSerializer
   end
 
@@ -32,4 +32,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
   def status_for_destroy
     current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
   end
+
+  def reblog_params
+    params.permit(:visibility)
+  end
 end
diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb
new file mode 100644
index 000000000..af91cb658
--- /dev/null
+++ b/app/controllers/relationships_controller.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+class RelationshipsController < ApplicationController
+  layout 'admin'
+
+  before_action :authenticate_user!
+  before_action :set_accounts, only: :show
+  before_action :set_pack
+  before_action :set_body_classes
+
+  helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?
+
+  def show
+    @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 relationships_path(current_params)
+  end
+
+  private
+
+  def set_accounts
+    @accounts = relationships_scope.page(params[:page]).per(40)
+  end
+
+  def relationships_scope
+    scope = begin
+      if following_relationship?
+        current_account.following.includes(:account_stat)
+      else
+        current_account.followers.includes(:account_stat)
+      end
+    end
+
+    scope.merge!(Follow.recent)
+    scope.merge!(mutual_relationship_scope) if mutual_relationship?
+    scope.merge!(abandoned_account_scope)   if params[:status] == 'abandoned'
+    scope.merge!(active_account_scope)      if params[:status] == 'active'
+    scope.merge!(by_domain_scope)           if params[:by_domain].present?
+
+    scope
+  end
+
+  def mutual_relationship_scope
+    Account.where(id: current_account.following)
+  end
+
+  def abandoned_account_scope
+    Account.where.not(moved_to_account_id: nil)
+  end
+
+  def active_account_scope
+    Account.where(moved_to_account_id: nil)
+  end
+
+  def by_domain_scope
+    Account.where(domain: params[:by_domain])
+  end
+
+  def form_account_batch_params
+    params.require(:form_account_batch).permit(:action, account_ids: [])
+  end
+
+  def following_relationship?
+    params[:relationship].blank? || params[:relationship] == 'following'
+  end
+
+  def mutual_relationship?
+    params[:relationship] == 'mutual'
+  end
+
+  def followed_by_relationship?
+    params[:relationship] == 'followed_by'
+  end
+
+  def current_params
+    params.slice(:page, :status, :relationship, :by_domain).permit(:page, :status, :relationship, :by_domain)
+  end
+
+  def action_from_button
+    if params[:unfollow]
+      'unfollow'
+    elsif params[:remove_from_followers]
+      'remove_from_followers'
+    elsif params[:block_domains]
+      'block_domains'
+    end
+  end
+
+  def set_body_classes
+    @body_classes = 'admin'
+  end
+
+  def set_pack
+    use_pack 'admin'
+  end
+end
diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb
deleted file mode 100644
index 8aae379aa..000000000
--- a/app/controllers/settings/follower_domains_controller.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-class Settings::FollowerDomainsController < Settings::BaseController
-  def show
-    @account = current_account
-    @domains = current_account.followers.reorder(Arel.sql('MIN(follows.id) DESC')).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
-  end
-
-  def update
-    domains = bulk_params[:select] || []
-
-    AfterAccountDomainBlockWorker.push_bulk(domains) do |domain|
-      [current_account.id, domain]
-    end
-
-    redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size)
-  end
-
-  private
-
-  def bulk_params
-    params.permit(select: [])
-  end
-end
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index 359d60b60..e5fbb1500 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -9,42 +9,6 @@ module Admin::ActionLogsHelper
     end
   end
 
-  def linkable_log_target(record)
-    case record.class.name
-    when 'Account'
-      link_to record.acct, admin_account_path(record.id)
-    when 'User'
-      link_to record.account.acct, admin_account_path(record.account_id)
-    when 'CustomEmoji'
-      record.shortcode
-    when 'Report'
-      link_to "##{record.id}", admin_report_path(record)
-    when 'DomainBlock', 'EmailDomainBlock'
-      link_to record.domain, "https://#{record.domain}"
-    when 'Status'
-      link_to record.account.acct, TagManager.instance.url_for(record)
-    when 'AccountWarning'
-      link_to record.target_account.acct, admin_account_path(record.target_account_id)
-    end
-  end
-
-  def log_target_from_history(type, attributes)
-    case type
-    when 'CustomEmoji'
-      attributes['shortcode']
-    when 'DomainBlock', 'EmailDomainBlock'
-      link_to attributes['domain'], "https://#{attributes['domain']}"
-    when 'Status'
-      tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
-
-      if tmp_status.account
-        link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
-      else
-        I18n.t('admin.action_logs.deleted_status')
-      end
-    end
-  end
-
   def relevant_log_changes(log)
     if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
       log.recorded_changes.slice('domain')
@@ -111,4 +75,40 @@ module Admin::ActionLogsHelper
   def opposite_verbs?(log)
     %w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
   end
+
+  def linkable_log_target(record)
+    case record.class.name
+    when 'Account'
+      link_to record.acct, admin_account_path(record.id)
+    when 'User'
+      link_to record.account.acct, admin_account_path(record.account_id)
+    when 'CustomEmoji'
+      record.shortcode
+    when 'Report'
+      link_to "##{record.id}", admin_report_path(record)
+    when 'DomainBlock', 'EmailDomainBlock'
+      link_to record.domain, "https://#{record.domain}"
+    when 'Status'
+      link_to record.account.acct, TagManager.instance.url_for(record)
+    when 'AccountWarning'
+      link_to record.target_account.acct, admin_account_path(record.target_account_id)
+    end
+  end
+
+  def log_target_from_history(type, attributes)
+    case type
+    when 'CustomEmoji'
+      attributes['shortcode']
+    when 'DomainBlock', 'EmailDomainBlock'
+      link_to attributes['domain'], "https://#{attributes['domain']}"
+    when 'Status'
+      tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
+
+      if tmp_status.account
+        link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
+      else
+        I18n.t('admin.action_logs.deleted_status')
+      end
+    end
+  end
 end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 8f78bf5f8..09a356296 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -7,8 +7,9 @@ module Admin::FilterHelper
   CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
   TAGS_FILTERS         = %i(hidden).freeze
   INSTANCES_FILTERS    = %i(limited by_domain).freeze
+  FOLLOWERS_FILTERS    = %i(relationship status by_domain).freeze
 
-  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS
+  FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS
 
   def filter_link_to(text, link_to_params, link_class_params = link_to_params)
     new_url = filtered_url_for(link_to_params)
diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index 296182ff5..11845fb17 100644
--- a/app/javascript/flavours/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
@@ -140,6 +140,15 @@ a.table-action-link {
       input {
         margin-top: 8px;
       }
+
+      &--aligned {
+        display: flex;
+        align-items: center;
+
+        input {
+          margin-top: 0;
+        }
+      }
     }
 
     &__actions,
@@ -183,6 +192,10 @@ a.table-action-link {
     &__content {
       padding-top: 12px;
       padding-bottom: 16px;
+
+      &--unpadded {
+        padding: 0;
+      }
     }
   }
 
@@ -193,4 +206,10 @@ a.table-action-link {
       font-weight: 700;
     }
   }
+
+  .nothing-here {
+    border: 1px solid darken($ui-base-color, 8%);
+    border-top: 0;
+    box-shadow: none;
+  }
 }
diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml
index d8f313381..587cc0f1e 100644
--- a/app/javascript/flavours/glitch/theme.yml
+++ b/app/javascript/flavours/glitch/theme.yml
@@ -28,10 +28,8 @@ pack:
 locales: locales
 
 #  (OPTIONAL) A file to use as the preview screenshot for the flavour,
-#  or an array thereof. These filenames must be unique across all
-#  images (regardless of path), so it's a good idea to namespace them
-#  to your theme. It's up to you to let webpack know to compile them.
-screenshot: glitch-preview.jpg
+#  or an array thereof. These are the full path from `app/javascript/`.
+screenshot: flavours/glitch/images/glitch-preview.jpg
 
 #  (OPTIONAL) The directory which contains the pack files.
 #  Defaults to the theme directory (`app/javascript/themes/[theme]`),
diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml
index a215b2625..42e26daea 100644
--- a/app/javascript/flavours/vanilla/theme.yml
+++ b/app/javascript/flavours/vanilla/theme.yml
@@ -26,10 +26,8 @@ pack:
 locales: ../../mastodon/locales
 
 #  (OPTIONAL) A file to use as the preview screenshot for the flavour,
-#  or an array thereof. These filenames must be unique across all
-#  images (regardless of path), so it's a good idea to namespace them
-#  to your theme. It's up to you to let webpack know to compile them.
-screenshot: screenshot.jpg
+#  or an array thereof. These are the full path from `app/javascript/`.
+screenshot: images/screenshot.jpg
 
 #  (OPTIONAL) The directory which contains the pack files.
 #  Defaults to this directory (`app/javascript/flavour/[flavour]`),
diff --git a/app/javascript/mastodon/components/error_boundary.js b/app/javascript/mastodon/components/error_boundary.js
new file mode 100644
index 000000000..d1ca5bf75
--- /dev/null
+++ b/app/javascript/mastodon/components/error_boundary.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import illustration from '../../images/elephant_ui_disappointed.svg';
+
+export default class ErrorBoundary extends React.PureComponent {
+
+  static propTypes = {
+    children: PropTypes.node,
+  };
+
+  state = {
+    hasError: false,
+    stackTrace: undefined,
+    componentStack: undefined,
+  }
+
+  componentDidCatch(error, info) {
+    this.setState({
+      hasError: true,
+      stackTrace: error.stack,
+      componentStack: info && info.componentStack,
+    });
+  }
+
+  render() {
+    const { hasError } = this.state;
+
+    if (!hasError) {
+      return this.props.children;
+    }
+
+    return (
+      <div>
+        <img src={illustration} alt='' />
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 2912540a0..542b68282 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -13,6 +13,7 @@ import { connectUserStream } from '../actions/streaming';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from '../locales';
 import initialState from '../initial_state';
+import ErrorBoundary from '../components/error_boundary';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
@@ -75,7 +76,9 @@ export default class Mastodon extends React.PureComponent {
     return (
       <IntlProvider locale={locale} messages={messages}>
         <Provider store={store}>
-          <MastodonMount />
+          <ErrorBoundary>
+            <MastodonMount />
+          </ErrorBoundary>
         </Provider>
       </IntlProvider>
     );
diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js
index e7d935251..a60ace35b 100644
--- a/app/javascript/mastodon/features/ui/components/bundle.js
+++ b/app/javascript/mastodon/features/ui/components/bundle.js
@@ -53,6 +53,11 @@ class Bundle extends React.PureComponent {
     const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
     const cachedMod = Bundle.cache.get(fetchComponent);
 
+    if (fetchComponent === undefined) {
+      this.setState({ mod: null });
+      return Promise.resolve();
+    }
+
     onFetch();
 
     if (cachedMod) {
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index a9442d803..9396d7b5d 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -73,6 +73,10 @@
   "compose_form.lock_disclaimer": "Váš účet není {locked}. Kdokoliv vás může sledovat a vidět vaše příspěvky pouze pro sledující.",
   "compose_form.lock_disclaimer.lock": "uzamčen",
   "compose_form.placeholder": "Co se vám honí hlavou?",
+  "compose_form.poll.add_option": "Přidat volbu",
+  "compose_form.poll.duration": "Délka ankety",
+  "compose_form.poll.option_placeholder": "Volba {number}",
+  "compose_form.poll.remove_option": "Odstranit tuto volbu",
   "compose_form.publish": "Tootnout",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive.marked": "Mediální obsah je označen jako citlivý",
@@ -151,6 +155,9 @@
   "home.column_settings.basic": "Základní",
   "home.column_settings.show_reblogs": "Zobrazit boosty",
   "home.column_settings.show_replies": "Zobrazit odpovědi",
+  "intervals.full.days": "{number, plural, one {# den} few {# dny} many {# dne} other {# dní}}",
+  "intervals.full.hours": "{number, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}",
+  "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minuty} other {# minut}}",
   "introduction.federation.action": "Další",
   "introduction.federation.federated.headline": "Federovaná",
   "introduction.federation.federated.text": "Veřejné příspěvky z jiných serverů na fediverse se zobrazí na federované časové ose.",
@@ -240,6 +247,7 @@
   "notification.favourite": "{name} si oblíbil/a váš toot",
   "notification.follow": "{name} vás začal/a sledovat",
   "notification.mention": "{name} vás zmínil/a",
+  "notification.poll": "Anketa, ve které jste hlasoval/a, skončila",
   "notification.reblog": "{name} boostnul/a váš toot",
   "notifications.clear": "Vymazat oznámení",
   "notifications.clear_confirmation": "Jste si jistý/á, že chcete trvale vymazat všechna vaše oznámení?",
@@ -264,6 +272,8 @@
   "poll.refresh": "Refresh",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "Vote",
+  "poll_button.add_poll": "Přidat anketu",
+  "poll_button.remove_poll": "Odstranit anketu",
   "privacy.change": "Změnit soukromí tootu",
   "privacy.direct.long": "Odeslat pouze zmíněným uživatelům",
   "privacy.direct.short": "Přímý",
@@ -356,6 +366,7 @@
   "upload_area.title": "Přetažením nahrajete",
   "upload_button.label": "Přidat média (JPEG, PNG, GIF, WebM, MP4, MOV)",
   "upload_error.limit": "Byl překročen limit nahraných souborů.",
+  "upload_error.poll": "Nahrávání souborů není povoleno u anket.",
   "upload_form.description": "Popis pro zrakově postižené",
   "upload_form.focus": "Změnit náhled",
   "upload_form.undo": "Smazat",
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index 9e8785679..d3a0ea03d 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -140,6 +140,15 @@ a.table-action-link {
       input {
         margin-top: 8px;
       }
+
+      &--aligned {
+        display: flex;
+        align-items: center;
+
+        input {
+          margin-top: 0;
+        }
+      }
     }
 
     &__actions,
@@ -183,6 +192,10 @@ a.table-action-link {
     &__content {
       padding-top: 12px;
       padding-bottom: 16px;
+
+      &--unpadded {
+        padding: 0;
+      }
     }
   }
 
@@ -197,4 +210,10 @@ a.table-action-link {
       font-weight: 700;
     }
   }
+
+  .nothing-here {
+    border: 1px solid darken($ui-base-color, 8%);
+    border-top: 0;
+    box-shadow: none;
+  }
 }
diff --git a/app/lib/language_detector.rb b/app/lib/language_detector.rb
index 58c8e2069..70a9084d1 100644
--- a/app/lib/language_detector.rb
+++ b/app/lib/language_detector.rb
@@ -3,7 +3,8 @@
 class LanguageDetector
   include Singleton
 
-  CHARACTER_THRESHOLD = 140
+  CHARACTER_THRESHOLD    = 140
+  RELIABLE_CHARACTERS_RE = /[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}\p{Han}\p{Katakana}\p{Hiragana}\p{Hangul}]+/m
 
   def initialize
     @identifier = CLD3::NNetLanguageIdentifier.new(1, 2048)
@@ -11,15 +12,14 @@ class LanguageDetector
 
   def detect(text, account)
     input_text = prepare_text(text)
+
     return if input_text.blank?
 
     detect_language_code(input_text) || default_locale(account)
   end
 
   def language_names
-    @language_names =
-      CLD3::TaskContextParams::LANGUAGE_NAMES.map { |name| iso6391(name.to_s).to_sym }
-                                             .uniq
+    @language_names = CLD3::TaskContextParams::LANGUAGE_NAMES.map { |name| iso6391(name.to_s).to_sym }.uniq
   end
 
   private
@@ -29,12 +29,29 @@ class LanguageDetector
   end
 
   def unreliable_input?(text)
-    text.size < CHARACTER_THRESHOLD
+    !reliable_input?(text)
+  end
+
+  def reliable_input?(text)
+    sufficient_text_length?(text) || language_specific_character_set?(text)
+  end
+
+  def sufficient_text_length?(text)
+    text.size >= CHARACTER_THRESHOLD
+  end
+
+  def language_specific_character_set?(text)
+    words = text.scan(RELIABLE_CHARACTERS_RE)
+
+    if words.present?
+      words.reduce(0) { |acc, elem| acc + elem.size }.to_f / text.size.to_f > 0.3
+    else
+      false
+    end
   end
 
   def detect_language_code(text)
     return if unreliable_input?(text)
-
     result = @identifier.find_language(text)
     iso6391(result.language.to_s).to_sym if result.reliable?
   end
@@ -77,6 +94,6 @@ class LanguageDetector
   end
 
   def default_locale(account)
-    return account.user_locale&.to_sym || I18n.default_locale if account.local?
+    account.user_locale&.to_sym || I18n.default_locale if account.local?
   end
 end
diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb
new file mode 100644
index 000000000..60eaaf0e2
--- /dev/null
+++ b/app/models/form/account_batch.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+class Form::AccountBatch
+  include ActiveModel::Model
+
+  attr_accessor :account_ids, :action, :current_account
+
+  def save
+    case action
+    when 'unfollow'
+      unfollow!
+    when 'remove_from_followers'
+      remove_from_followers!
+    when 'block_domains'
+      block_domains!
+    end
+  end
+
+  private
+
+  def unfollow!
+    accounts.find_each do |target_account|
+      UnfollowService.new.call(current_account, target_account)
+    end
+  end
+
+  def remove_from_followers!
+    current_account.passive_relationships.where(account_id: account_ids).find_each do |follow|
+      reject_follow!(follow)
+    end
+  end
+
+  def block_domains!
+    AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain|
+      [current_account.id, domain]
+    end
+  end
+
+  def account_domains
+    accounts.pluck(Arel.sql('distinct domain')).compact
+  end
+
+  def accounts
+    Account.where(id: account_ids)
+  end
+
+  def reject_follow!(follow)
+    follow.destroy
+
+    return unless follow.account.activitypub?
+
+    json = ActiveModelSerializers::SerializableResource.new(
+      follow,
+      serializer: ActivityPub::RejectFollowSerializer,
+      adapter: ActivityPub::Adapter
+    ).to_json
+
+    ActivityPub::DeliveryWorker.perform_async(json, current_account.id, follow.account.inbox_url)
+  end
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index f576489b4..440d7cc63 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -73,7 +73,9 @@ class Status < ApplicationRecord
   validates_with StatusLengthValidator
   validates_with DisallowedHashtagsValidator
   validates :reblog, uniqueness: { scope: :account }, if: :reblog?
-  validates_associated :owned_poll
+  validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
+
+  accepts_nested_attributes_for :owned_poll
 
   default_scope { recent }
 
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index 97fed63d1..98c53c84a 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -32,7 +32,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
   end
 
   def thumbnail
-    instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('preview.jpg')
+    instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('media/images/preview.jpg')
   end
 
   def max_toot_chars
diff --git a/app/serializers/rest/preferences_serializer.rb b/app/serializers/rest/preferences_serializer.rb
new file mode 100644
index 000000000..119f0e06d
--- /dev/null
+++ b/app/serializers/rest/preferences_serializer.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class REST::PreferencesSerializer < ActiveModel::Serializer
+  attribute :posting_default_privacy, key: 'posting:default:visibility'
+  attribute :posting_default_sensitive, key: 'posting:default:sensitive'
+  attribute :posting_default_language, key: 'posting:default:language'
+
+  attribute :reading_default_sensitive_media, key: 'reading:expand:media'
+  attribute :reading_default_sensitive_text, key: 'reading:expand:spoilers'
+
+  def posting_default_privacy
+    object.user.setting_default_privacy
+  end
+
+  def posting_default_sensitive
+    object.user.setting_default_sensitive
+  end
+
+  def posting_default_language
+    object.user.setting_default_language.presence
+  end
+
+  def reading_default_sensitive_media
+    object.user.setting_display_media
+  end
+
+  def reading_default_sensitive_text
+    object.user.setting_expand_spoilers
+  end
+end
diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb
index 712b1347a..88eca79ed 100644
--- a/app/serializers/rss/account_serializer.rb
+++ b/app/serializers/rss/account_serializer.rb
@@ -11,7 +11,7 @@ class RSS::AccountSerializer
     builder.title("#{display_name(account)} (@#{account.local_username_and_domain})")
            .description(account_description(account))
            .link(TagManager.instance.url_for(account))
-           .logo(full_asset_url(asset_pack_path('logo.svg')))
+           .logo(full_pack_url('media/images/logo.svg'))
            .accent_color('2b90d9')
 
     builder.image(full_asset_url(account.avatar.url(:original))) if account.avatar?
diff --git a/app/serializers/rss/tag_serializer.rb b/app/serializers/rss/tag_serializer.rb
index 7680a8da5..644380149 100644
--- a/app/serializers/rss/tag_serializer.rb
+++ b/app/serializers/rss/tag_serializer.rb
@@ -12,7 +12,7 @@ class RSS::TagSerializer
     builder.title("##{tag.name}")
            .description(strip_tags(I18n.t('about.about_hashtag_html', hashtag: tag.name)))
            .link(tag_url(tag))
-           .logo(full_asset_url(asset_pack_path('logo.svg')))
+           .logo(full_pack_url('media/images/logo.svg'))
            .accent_color('2b90d9')
 
     statuses.each do |status|
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index b9952369d..820c553c9 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -29,7 +29,6 @@ class PostStatusService < BaseService
     return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
 
     validate_media!
-    validate_poll!
     preprocess_attributes!
 
     if scheduled?
@@ -74,6 +73,7 @@ class PostStatusService < BaseService
 
   def schedule_status!
     status_for_validation = @account.statuses.build(status_attributes)
+
     if status_for_validation.valid?
       status_for_validation.destroy
 
@@ -110,12 +110,6 @@ class PostStatusService < BaseService
     raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?)
   end
 
-  def validate_poll!
-    return if @options[:poll].blank?
-
-    @poll = @account.polls.new(@options[:poll])
-  end
-
   def language_from_option(str)
     ISO_639.find(str)&.alpha2
   end
@@ -168,13 +162,13 @@ class PostStatusService < BaseService
       text: @text,
       media_attachments: @media || [],
       thread: @in_reply_to,
-      owned_poll: @poll,
+      owned_poll_attributes: poll_attributes,
       sensitive: (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?,
       spoiler_text: @options[:spoiler_text] || '',
       visibility: @visibility,
       language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account),
       application: @options[:application],
-    }
+    }.compact
   end
 
   def scheduled_status_attributes
@@ -185,6 +179,12 @@ class PostStatusService < BaseService
     }
   end
 
+  def poll_attributes
+    return if @options[:poll].blank?
+
+    @options[:poll].merge(account: @account)
+  end
+
   def scheduled_options
     @options.tap do |options_hash|
       options_hash[:in_reply_to_id] = options_hash.delete(:thread)&.id
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 03db27406..deaa0549e 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -7,8 +7,9 @@ class ReblogService < BaseService
   # Reblog a status and notify its remote author
   # @param [Account] account Account to reblog from
   # @param [Status] reblogged_status Status to be reblogged
+  # @param [Hash] options
   # @return [Status]
-  def call(account, reblogged_status)
+  def call(account, reblogged_status, options = {})
     reblogged_status = reblogged_status.reblog if reblogged_status.reblog?
 
     authorize_with account, reblogged_status, :reblog?
@@ -17,7 +18,7 @@ class ReblogService < BaseService
 
     return reblog unless reblog.nil?
 
-    reblog = account.statuses.create!(reblog: reblogged_status, text: '')
+    reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: options[:visibility] || account.user&.setting_default_privacy)
 
     DistributionWorker.perform_async(reblog.id)
 
@@ -38,7 +39,7 @@ class ReblogService < BaseService
     reblogged_status = reblog.reblog
 
     if reblogged_status.account.local?
-      NotifyService.new.call(reblogged_status.account, reblog)
+      LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name)
     elsif reblogged_status.account.ostatus?
       NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), reblog.account_id, reblogged_status.account_id)
     elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account)
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index 87f1071d9..f02a7906a 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -8,7 +8,7 @@
   .column-0
     .public-account-header.public-account-header--no-bar
       .public-account-header__image
-        = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title, class: 'parallax'
+        = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.jpg'), alt: @instance_presenter.site_title, class: 'parallax'
 
   .column-1
     .landing-page__call-to-action{ dir: 'ltr' }
@@ -24,7 +24,7 @@
             %span= t 'about.status_count_after', count: @instance_presenter.status_count
         .row__mascot
           .landing-page__mascot
-            = image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('elephant_ui_plane.svg'), alt: ''
+            = image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'), alt: ''
 
   .column-2
     .landing-page__information.contact-widget
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 15d0af64e..21dcf226d 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -8,7 +8,7 @@
 .landing
   .landing__brand
     = link_to root_url, class: 'brand' do
-      = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+      = image_pack_tag 'logo_full.svg', alt: 'Mastodon'
       %span.brand__tagline=t 'about.tagline'
 
   .landing__grid
@@ -48,7 +48,7 @@
 
       .hero-widget
         .hero-widget__img
-          = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
+          = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.jpg'), alt: @instance_presenter.site_title
 
         - if @instance_presenter.site_short_description.present?
           .hero-widget__text
diff --git a/app/views/admin/invites/_invite.html.haml b/app/views/admin/invites/_invite.html.haml
index ee0eacaf5..e6ad9de34 100644
--- a/app/views/admin/invites/_invite.html.haml
+++ b/app/views/admin/invites/_invite.html.haml
@@ -10,10 +10,7 @@
       = image_tag invite.user.account.avatar.url(:original), alt: '', width: 16, height: 16, class: 'avatar'
       %span.username= invite.user.account.username
 
-  - if invite.expired?
-    %td{ colspan: 2 }
-      = t('invites.expired')
-  - else
+  - if invite.valid_for_use?
     %td
       = fa_icon 'user fw'
       = invite.uses
@@ -24,6 +21,10 @@
       - else
         %time.formatted{ datetime: invite.expires_at.iso8601, title: l(invite.expires_at) }
           = l invite.expires_at
+  - else
+    %td{ colspan: 2 }
+      = t('invites.expired')
+
   %td
-    - if !invite.expired? && policy(invite).destroy?
+    - if invite.valid_for_use? && policy(invite).destroy?
       = table_link_to 'times', t('invites.delete'), admin_invite_path(invite), method: :delete
diff --git a/app/views/application/_sidebar.html.haml b/app/views/application/_sidebar.html.haml
index 2ff14b252..b5ce5845e 100644
--- a/app/views/application/_sidebar.html.haml
+++ b/app/views/application/_sidebar.html.haml
@@ -1,6 +1,6 @@
 .hero-widget
   .hero-widget__img
-    = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
+    = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.jpg'), alt: @instance_presenter.site_title
 
   .hero-widget__text
     %p= @instance_presenter.site_short_description.html_safe.presence || @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 5f32635e5..6c5268b61 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -9,7 +9,7 @@
 
 .app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
   %noscript
-    = image_tag asset_pack_path('logo.svg'), alt: 'Mastodon'
+    = image_pack_tag 'logo.svg', alt: 'Mastodon'
 
     %div
       = t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
diff --git a/app/views/invites/_invite.html.haml b/app/views/invites/_invite.html.haml
index 4240aa3e7..62799ca5b 100644
--- a/app/views/invites/_invite.html.haml
+++ b/app/views/invites/_invite.html.haml
@@ -5,10 +5,7 @@
         %input{ type: :text, maxlength: '999', spellcheck: 'false', readonly: 'true', value: public_invite_url(invite_code: invite.code) }
       %button{ type: :button }= t('generic.copy')
 
-  - if invite.expired?
-    %td{ colspan: 2 }
-      = t('invites.expired')
-  - else
+  - if invite.valid_for_use?
     %td
       = fa_icon 'user fw'
       = invite.uses
@@ -19,7 +16,10 @@
       - else
         %time.formatted{ datetime: invite.expires_at.iso8601, title: l(invite.expires_at) }
           = l invite.expires_at
+  - else
+    %td{ colspan: 2 }
+      = t('invites.expired')
 
   %td
-    - if !invite.expired? && policy(invite).destroy?
+    - if invite.valid_for_use? && policy(invite).destroy?
       = table_link_to 'times', t('invites.delete'), invite_path(invite), method: :delete
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index 0e52702dc..a0cb7c4fe 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -3,7 +3,7 @@
     .sidebar-wrapper
       .sidebar
         = link_to root_path do
-          = image_tag asset_pack_path('logo.svg'), class: 'logo', alt: 'Mastodon'
+          = image_pack_tag 'logo.svg', class: 'logo', alt: 'Mastodon'
 
         = render_navigation
     .content-wrapper
diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml
index ca9c13945..fcbd29fe9 100644
--- a/app/views/layouts/auth.html.haml
+++ b/app/views/layouts/auth.html.haml
@@ -3,7 +3,7 @@
     .logo-container
       %h1
         = link_to root_path do
-          = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+          = image_pack_tag 'logo_full.svg', alt: 'Mastodon'
 
     .form-container
       = render 'flashes'
diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml
index 6321fec61..26fb697bb 100644
--- a/app/views/layouts/mailer.html.haml
+++ b/app/views/layouts/mailer.html.haml
@@ -24,7 +24,7 @@
                               %tr
                                 %td.column-cell
                                   = link_to root_url do
-                                    = image_tag full_pack_url('logo_full.png'), alt: 'Mastodon', height: 34, class: 'logo'
+                                    = image_tag full_pack_url('media/images/mailer/logo_full.png'), alt: 'Mastodon', height: 34, class: 'logo'
 
     = yield
 
@@ -49,4 +49,4 @@
                                 %p= link_to t('application_mailer.notification_preferences'), settings_notifications_url
                               %td.column-cell.text-right
                                 = link_to root_url do
-                                  = image_tag full_pack_url('logo_transparent.png'), alt: 'Mastodon', height: 24
+                                  = image_tag full_pack_url('media/images/mailer/logo_transparent.png'), alt: 'Mastodon', height: 24
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index 1d3519b8a..b4a21caf1 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -5,7 +5,7 @@
         %nav.header
           .nav-left
             = link_to root_url, class: 'brand' do
-              = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+              = image_pack_tag 'logo_full.svg', alt: 'Mastodon'
 
             = link_to t('directories.directory'), explore_path, class: 'nav-link optional' if Setting.profile_directory
             = link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
diff --git a/app/views/notification_mailer/favourite.html.haml b/app/views/notification_mailer/favourite.html.haml
index 7d1b494d0..a715d615c 100644
--- a/app/views/notification_mailer/favourite.html.haml
+++ b/app/views/notification_mailer/favourite.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_grade.png'), alt:''
+                                      = image_tag full_pack_url('media/images/mailer/icon_grade.png'), alt:''
 
                               %h1= t 'notification_mailer.favourite.title'
                               %p.lead= t('notification_mailer.favourite.body', name: @account.acct)
diff --git a/app/views/notification_mailer/follow.html.haml b/app/views/notification_mailer/follow.html.haml
index 31a2b7445..cd84f7858 100644
--- a/app/views/notification_mailer/follow.html.haml
+++ b/app/views/notification_mailer/follow.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_person_add.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_person_add.png'), alt: ''
 
                               %h1= t 'notification_mailer.follow.title'
                               %p.lead= t('notification_mailer.follow.body', name: @account.acct)
diff --git a/app/views/notification_mailer/follow_request.html.haml b/app/views/notification_mailer/follow_request.html.haml
index 44f1911c4..a63e27a90 100644
--- a/app/views/notification_mailer/follow_request.html.haml
+++ b/app/views/notification_mailer/follow_request.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_person_add.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_person_add.png'), alt: ''
 
                               %h1= t 'notification_mailer.follow_request.title'
                               %p.lead= t('notification_mailer.follow_request.body', name: @account.acct)
diff --git a/app/views/notification_mailer/mention.html.haml b/app/views/notification_mailer/mention.html.haml
index 479fed41c..619873cfa 100644
--- a/app/views/notification_mailer/mention.html.haml
+++ b/app/views/notification_mailer/mention.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_reply.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_reply.png'), alt: ''
 
                               %h1= t 'notification_mailer.mention.title'
                               %p.lead= t('notification_mailer.mention.body', name: @status.account.acct)
diff --git a/app/views/notification_mailer/reblog.html.haml b/app/views/notification_mailer/reblog.html.haml
index 85b202cf9..a2811be23 100644
--- a/app/views/notification_mailer/reblog.html.haml
+++ b/app/views/notification_mailer/reblog.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_cached.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_cached.png'), alt: ''
 
                               %h1= t 'notification_mailer.reblog.title'
                               %p.lead= t('notification_mailer.reblog.body', name: @account.acct)
diff --git a/app/views/relationships/_account.html.haml b/app/views/relationships/_account.html.haml
new file mode 100644
index 000000000..6c22deb51
--- /dev/null
+++ b/app/views/relationships/_account.html.haml
@@ -0,0 +1,20 @@
+.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_link_to account
+          %td.accounts-table__count.optional
+            = number_to_human account.statuses_count, strip_insignificant_zeros: true
+            %small= t('accounts.posts', count: account.statuses_count).downcase
+          %td.accounts-table__count.optional
+            = number_to_human account.followers_count, strip_insignificant_zeros: true
+            %small= t('accounts.followers', count: account.followers_count).downcase
+          %td.accounts-table__count
+            - if account.last_status_at.present?
+              %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
+            - else
+              \-
+            %small= t('accounts.last_active')
diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml
new file mode 100644
index 000000000..94972026a
--- /dev/null
+++ b/app/views/relationships/show.html.haml
@@ -0,0 +1,40 @@
+- content_for :page_title do
+  = t('settings.relationships')
+
+.filters
+  .filter-subset
+    %strong= t 'relationships.relationship'
+    %ul
+      %li= filter_link_to t('accounts.following', count: current_account.following_count), relationship: nil
+      %li= filter_link_to t('accounts.followers', count: current_account.followers_count), relationship: 'followed_by'
+      %li= filter_link_to t('relationships.mutual'), relationship: 'mutual'
+
+  .filter-subset
+    %strong= t 'relationships.status'
+    %ul
+      %li= filter_link_to t('generic.all'), status: nil
+      %li= filter_link_to t('relationships.active'), status: 'active'
+      %li= filter_link_to t('relationships.abandoned'), status: 'abandoned'
+
+= form_for(@form, url: relationships_path, method: :patch) do |f|
+  = hidden_field_tag :page, params[:page] || 1
+  = hidden_field_tag :relationship, params[:relationship]
+  = hidden_field_tag :status, params[:status]
+
+  .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('user-times'), t('relationships.remove_selected_follows')]), name: :unfollow, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless followed_by_relationship?
+
+        = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless following_relationship?
+
+        = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
+    .batch-table__body
+      - if @accounts.empty?
+        = nothing_here 'nothing-here--under-tabs'
+      - else
+        = render partial: 'account', collection: @accounts, locals: { f: f }
+
+= paginate @accounts
diff --git a/app/views/settings/flavours/show.html.haml b/app/views/settings/flavours/show.html.haml
index a5126d9c5..c3f785aa0 100644
--- a/app/views/settings/flavours/show.html.haml
+++ b/app/views/settings/flavours/show.html.haml
@@ -5,7 +5,7 @@
   = render 'shared/error_messages', object: current_user
 
   - Themes.instance.flavour(@selected)['screenshot'].each do |screen|
-    %img.flavour-screen{ src: asset_pack_path(screen) }
+    %img.flavour-screen{ src: full_pack_url("media/#{screen}") }
 
   .flavour-description
     = t "flavours.#{@selected}.description", default: ''
diff --git a/app/views/settings/follower_domains/show.html.haml b/app/views/settings/follower_domains/show.html.haml
deleted file mode 100644
index f1687d4d2..000000000
--- a/app/views/settings/follower_domains/show.html.haml
+++ /dev/null
@@ -1,34 +0,0 @@
-- content_for :page_title do
-  = t('settings.followers')
-
-= form_tag settings_follower_domains_path, method: :patch, class: 'table-form' do
-  - unless @account.locked?
-    .warning
-      %strong
-        = fa_icon('warning')
-        = t('followers.unlocked_warning_title')
-      = t('followers.unlocked_warning_html', lock_link: link_to(t('followers.lock_link'), settings_profile_url))
-
-  %p= t('followers.explanation_html')
-  %p= t('followers.true_privacy_html')
-
-  .table-wrapper
-    %table.table
-      %thead
-        %tr
-          %th
-          %th= t('followers.domain')
-          %th= t('followers.followers_count')
-      %tbody
-        - @domains.each do |domain|
-          %tr
-            %td
-              = check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil?
-            %td
-              %samp= domain.domain.presence || Rails.configuration.x.local_domain
-            %td= number_with_delimiter domain.accounts_from_domain
-
-  .action-pagination
-    .actions
-      = button_tag t('followers.purge'), type: :submit, class: 'button', disabled: !@account.locked?
-    = paginate @domains
diff --git a/app/views/shared/_og.html.haml b/app/views/shared/_og.html.haml
index 802d8c41d..67238fc8b 100644
--- a/app/views/shared/_og.html.haml
+++ b/app/views/shared/_og.html.haml
@@ -8,7 +8,7 @@
 = opengraph 'og:type', 'website'
 = opengraph 'og:title', @instance_presenter.site_title
 = opengraph 'og:description', description
-= opengraph 'og:image', full_asset_url(thumbnail&.file&.url || asset_pack_path('preview.jpg', protocol: :request))
+= opengraph 'og:image', full_asset_url(thumbnail&.file&.url || asset_pack_path('media/images/preview.jpg', protocol: :request))
 = opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200'
 = opengraph 'og:image:height', thumbnail ? thumbnail.meta['height'] : '630'
 = opengraph 'twitter:card', 'summary_large_image'
diff --git a/app/views/user_mailer/backup_ready.html.haml b/app/views/user_mailer/backup_ready.html.haml
index d5a4b8b48..85140b08b 100644
--- a/app/views/user_mailer/backup_ready.html.haml
+++ b/app/views/user_mailer/backup_ready.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_file_download.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_file_download.png'), alt: ''
 
                               %h1= t 'user_mailer.backup_ready.title'
 
diff --git a/app/views/user_mailer/confirmation_instructions.html.haml b/app/views/user_mailer/confirmation_instructions.html.haml
index 70d0f5a24..39a83faff 100644
--- a/app/views/user_mailer/confirmation_instructions.html.haml
+++ b/app/views/user_mailer/confirmation_instructions.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_email.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_email.png'), alt: ''
 
                               %h1= t 'devise.mailer.confirmation_instructions.title'
 
diff --git a/app/views/user_mailer/email_changed.html.haml b/app/views/user_mailer/email_changed.html.haml
index 0802aaf96..7e91e87ad 100644
--- a/app/views/user_mailer/email_changed.html.haml
+++ b/app/views/user_mailer/email_changed.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_email.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_email.png'), alt: ''
 
                               %h1= t 'devise.mailer.email_changed.title'
                               %p.lead= t 'devise.mailer.email_changed.explanation'
diff --git a/app/views/user_mailer/password_change.html.haml b/app/views/user_mailer/password_change.html.haml
index 26314a217..559abf027 100644
--- a/app/views/user_mailer/password_change.html.haml
+++ b/app/views/user_mailer/password_change.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_lock_open.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
 
                               %h1= t 'devise.mailer.password_change.title'
                               %p.lead= t 'devise.mailer.password_change.explanation'
diff --git a/app/views/user_mailer/reconfirmation_instructions.html.haml b/app/views/user_mailer/reconfirmation_instructions.html.haml
index e3be8e295..7f10ba94f 100644
--- a/app/views/user_mailer/reconfirmation_instructions.html.haml
+++ b/app/views/user_mailer/reconfirmation_instructions.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_email.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_email.png'), alt: ''
 
                               %h1= t 'devise.mailer.reconfirmation_instructions.title'
                               %p.lead= t 'devise.mailer.reconfirmation_instructions.explanation'
diff --git a/app/views/user_mailer/reset_password_instructions.html.haml b/app/views/user_mailer/reset_password_instructions.html.haml
index 5d9ce6a75..eeed38c9e 100644
--- a/app/views/user_mailer/reset_password_instructions.html.haml
+++ b/app/views/user_mailer/reset_password_instructions.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_lock_open.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_lock_open.png'), alt: ''
 
                               %h1= t 'devise.mailer.reset_password_instructions.title'
                               %p.lead= t 'devise.mailer.reset_password_instructions.explanation'
diff --git a/app/views/user_mailer/warning.html.haml b/app/views/user_mailer/warning.html.haml
index c5e1f5a28..72ea5e5d2 100644
--- a/app/views/user_mailer/warning.html.haml
+++ b/app/views/user_mailer/warning.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_warning.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_warning.png'), alt: ''
 
                               %h1= t "user_mailer.warning.title.#{@warning.action}"
 
diff --git a/app/views/user_mailer/welcome.html.haml b/app/views/user_mailer/welcome.html.haml
index 4a5788bf6..1f75ff48a 100644
--- a/app/views/user_mailer/welcome.html.haml
+++ b/app/views/user_mailer/welcome.html.haml
@@ -17,7 +17,7 @@
                                 %tbody
                                   %tr
                                     %td
-                                      = image_tag full_pack_url('icon_done.png'), alt: ''
+                                      = image_tag full_pack_url('media/images/mailer/icon_done.png'), alt: ''
 
                               %h1= t 'user_mailer.welcome.title', name: @resource.account.username
                               %p.lead= t 'user_mailer.welcome.explanation'