about summary refs log tree commit diff
path: root/app/views
diff options
context:
space:
mode:
Diffstat (limited to 'app/views')
-rw-r--r--app/views/about/_login.html.haml29
-rw-r--r--app/views/about/more.html.haml4
-rw-r--r--app/views/about/show.html.haml4
-rw-r--r--app/views/accounts/_bio.html.haml10
-rw-r--r--app/views/accounts/_header.html.haml10
-rw-r--r--app/views/accounts/show.html.haml2
-rw-r--r--app/views/admin/accounts/_account.html.haml59
-rw-r--r--app/views/admin/accounts/index.html.haml53
-rw-r--r--app/views/admin/accounts/show.html.haml50
-rw-r--r--app/views/admin/action_logs/index.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml184
-rw-r--r--app/views/admin/follow_recommendations/_account.html.haml4
-rw-r--r--app/views/admin/instances/_instance.html.haml2
-rw-r--r--app/views/admin/instances/show.html.haml4
-rw-r--r--app/views/admin/ip_blocks/_ip_block.html.haml6
-rw-r--r--app/views/admin/pending_accounts/_account.html.haml16
-rw-r--r--app/views/admin/pending_accounts/index.html.haml30
-rw-r--r--app/views/admin/report_notes/_report_note.html.haml23
-rw-r--r--app/views/admin/reports/_action_log.html.haml6
-rw-r--r--app/views/admin/reports/_status.html.haml6
-rw-r--r--app/views/admin/reports/index.html.haml6
-rw-r--r--app/views/admin/reports/show.html.haml273
-rw-r--r--app/views/admin/settings/edit.html.haml8
-rw-r--r--app/views/admin/statuses/index.html.haml33
-rw-r--r--app/views/admin/statuses/show.html.haml27
-rw-r--r--app/views/admin/tags/_tag.html.haml19
-rw-r--r--app/views/admin/tags/index.html.haml71
-rw-r--r--app/views/admin/tags/show.html.haml65
-rw-r--r--app/views/admin/trends/links/_preview_card.html.haml30
-rw-r--r--app/views/admin/trends/links/index.html.haml38
-rw-r--r--app/views/admin/trends/links/preview_card_providers/_preview_card_provider.html.haml16
-rw-r--r--app/views/admin/trends/links/preview_card_providers/index.html.haml40
-rw-r--r--app/views/admin/trends/tags/_tag.html.haml24
-rw-r--r--app/views/admin/trends/tags/index.html.haml35
-rw-r--r--app/views/admin_mailer/new_pending_account.text.erb4
-rw-r--r--app/views/admin_mailer/new_trending_links.text.erb16
-rw-r--r--app/views/admin_mailer/new_trending_tag.text.erb5
-rw-r--r--app/views/admin_mailer/new_trending_tags.text.erb16
-rw-r--r--app/views/application/_sidebar.html.haml2
-rw-r--r--app/views/auth/confirmations/captcha.html.haml14
-rw-r--r--app/views/auth/sessions/new.html.haml25
-rw-r--r--app/views/auth/shared/_links.html.haml2
-rw-r--r--app/views/directories/index.html.haml4
-rw-r--r--app/views/layouts/_theme.html.haml7
-rw-r--r--app/views/layouts/public.html.haml2
-rw-r--r--app/views/notification_mailer/_status.html.haml2
-rw-r--r--app/views/notification_mailer/_status.text.erb8
-rw-r--r--app/views/relationships/_account.html.haml4
-rw-r--r--app/views/settings/featured_tags/index.html.haml2
-rw-r--r--app/views/settings/identity_proofs/_proof.html.haml21
-rw-r--r--app/views/settings/identity_proofs/index.html.haml17
-rw-r--r--app/views/settings/identity_proofs/new.html.haml36
-rw-r--r--app/views/settings/profiles/show.html.haml5
-rw-r--r--app/views/statuses/_detailed_status.html.haml11
-rw-r--r--app/views/statuses/_simple_status.html.haml3
-rw-r--r--app/views/statuses/_status.html.haml2
-rw-r--r--app/views/statuses_cleanup/show.html.haml45
-rw-r--r--app/views/user_mailer/warning.html.haml16
-rw-r--r--app/views/user_mailer/warning.text.erb17
59 files changed, 811 insertions, 664 deletions
diff --git a/app/views/about/_login.html.haml b/app/views/about/_login.html.haml
index fa58f04d7..0f19e8164 100644
--- a/app/views/about/_login.html.haml
+++ b/app/views/about/_login.html.haml
@@ -1,13 +1,22 @@
-= simple_form_for(new_user, url: user_session_path, namespace: 'login') do |f|
-  .fields-group
-    - if use_seamless_external_login?
-      = f.input :email, placeholder: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
-    - else
-      = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
+- unless omniauth_only?
+  = simple_form_for(new_user, url: user_session_path, namespace: 'login') do |f|
+    .fields-group
+      - if use_seamless_external_login?
+        = f.input :email, placeholder: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
+      - else
+        = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
 
-    = f.input :password, placeholder: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }, hint: false
+      = f.input :password, placeholder: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }, hint: false
 
-  .actions
-    = f.button :button, t('auth.login'), type: :submit, class: 'button button-primary'
+    .actions
+      = f.button :button, t('auth.login'), type: :submit, class: 'button button-primary'
 
-  %p.hint.subtle-hint= link_to t('auth.trouble_logging_in'), new_user_password_path
+    %p.hint.subtle-hint= link_to t('auth.trouble_logging_in'), new_user_password_path
+
+- if Devise.mappings[:user].omniauthable? and User.omniauth_providers.any?
+  .simple_form.alternative-login
+    %h4= omniauth_only? ? t('auth.log_in_with') : t('auth.or_log_in_with')
+
+    .actions
+      - User.omniauth_providers.each do |provider|
+        = provider_sign_in_link(provider)
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index 1cf194522..a4a79c4e7 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -16,11 +16,11 @@
         .row__information-board
           .information-board__section
             %span= t 'about.user_count_before'
-            %strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
+            %strong= friendly_number_to_human @instance_presenter.user_count
             %span= t 'about.user_count_after', count: @instance_presenter.user_count
           .information-board__section
             %span= t 'about.status_count_before'
-            %strong= number_to_human @instance_presenter.status_count, strip_insignificant_zeros: true
+            %strong= friendly_number_to_human @instance_presenter.status_count
             %span= t 'about.status_count_after', count: @instance_presenter.status_count
         .row__mascot
           .landing-page__mascot
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 565c4ed59..6ae9e6ae0 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -70,10 +70,10 @@
 
             .hero-widget__counters__wrapper
               .hero-widget__counter
-                %strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
+                %strong= friendly_number_to_human @instance_presenter.user_count
                 %span= t 'about.user_count_after', count: @instance_presenter.user_count
               .hero-widget__counter
-                %strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true
+                %strong= friendly_number_to_human @instance_presenter.active_user_count
                 %span
                   = t 'about.active_count_after'
                   %abbr{ title: t('about.active_footnote') } *
diff --git a/app/views/accounts/_bio.html.haml b/app/views/accounts/_bio.html.haml
index efc26d136..e8a49a1aa 100644
--- a/app/views/accounts/_bio.html.haml
+++ b/app/views/accounts/_bio.html.haml
@@ -1,16 +1,8 @@
-- proofs = account.identity_proofs.active
 - fields = account.fields
 
 .public-account-bio
-  - unless fields.empty? && proofs.empty?
+  - unless fields.empty?
     .account__header__fields
-      - proofs.each do |proof|
-        %dl
-          %dt= proof.provider.capitalize
-          %dd.verified
-            = link_to fa_icon('check'), proof.badge.proof_url, class: 'verified__mark', title: t('accounts.link_verified_on', date: l(proof.updated_at))
-            = link_to proof.provider_username, proof.badge.profile_url
-
       - fields.each do |field|
         %dl
           %dt.emojify{ title: field.name }= Formatter.instance.format_field(account, field.name, custom_emojify: true)
diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml
index 76dec18b1..d583edbd2 100644
--- a/app/views/accounts/_header.html.haml
+++ b/app/views/accounts/_header.html.haml
@@ -15,17 +15,17 @@
         .details-counters
           .counter{ class: active_nav_class(short_account_url(account), short_account_with_replies_url(account), short_account_media_url(account)) }
             = link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do
-              %span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true
+              %span.counter-number= friendly_number_to_human account.statuses_count
               %span.counter-label= t('accounts.posts', count: account.statuses_count)
 
           .counter{ class: active_nav_class(account_following_index_url(account)) }
             = link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do
-              %span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true
+              %span.counter-number= friendly_number_to_human account.following_count
               %span.counter-label= t('accounts.following', count: account.following_count)
 
           .counter{ class: active_nav_class(account_followers_url(account)) }
             = link_to account_followers_url(account), title: hide_followers_count?(account) ? nil : number_with_delimiter(account.followers_count) do
-              %span.counter-number= hide_followers_count?(account) ? '-' : (number_to_human account.followers_count, strip_insignificant_zeros: true)
+              %span.counter-number= hide_followers_count?(account) ? '-' : (friendly_number_to_human account.followers_count)
               %span.counter-label= t('accounts.followers', count: account.followers_count)
         .spacer
         .public-account-header__tabs__tabs__buttons
@@ -36,8 +36,8 @@
 
       .public-account-header__extra__links
         = link_to account_following_index_url(account) do
-          %strong= number_to_human account.following_count, strip_insignificant_zeros: true
+          %strong= friendly_number_to_human account.following_count
           = t('accounts.following', count: account.following_count)
         = link_to account_followers_url(account) do
-          %strong= hide_followers_count?(account) ? '-' : (number_to_human account.followers_count, strip_insignificant_zeros: true)
+          %strong= hide_followers_count?(account) ? '-' : (friendly_number_to_human account.followers_count)
           = t('accounts.followers', count: account.followers_count)
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 1a81b96f6..72e9c6611 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -81,6 +81,6 @@
                   = t('accounts.nothing_here')
                 - else
                   %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
-            .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
+            .trends__item__current= friendly_number_to_human featured_tag.statuses_count
 
     = render 'application/sidebar'
diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml
index c9bd8c686..2df91301e 100644
--- a/app/views/admin/accounts/_account.html.haml
+++ b/app/views/admin/accounts/_account.html.haml
@@ -1,24 +1,35 @@
-%tr
-  %td
-    = admin_account_link_to(account)
-  %td
-    %div.account-badges= account_badge(account, all: true)
-  %td
-    - if account.user_current_sign_in_ip
-      %samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip
-    - else
-      \-
-  %td
-    - if account.user_current_sign_in_at
-      %time.time-ago{ datetime: account.user_current_sign_in_at.iso8601, title: l(account.user_current_sign_in_at) }= l account.user_current_sign_in_at
-    - elsif 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
-      \-
-  %td
-    - if account.local? && account.user_pending?
-      = table_link_to 'check', t('admin.accounts.approve'), approve_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:approve, account.user)
-      = table_link_to 'times', t('admin.accounts.reject'), reject_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:reject, account.user)
-    - else
-      = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}")
-      = table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account)
+.batch-table__row{ class: [!account.suspended? && account.user_pending? && 'batch-table__row--attention', account.suspended? && 'batch-table__row--muted'] }
+  %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, path: admin_account_path(account.id)
+          %td.accounts-table__count.optional
+            - if account.suspended? || account.user_pending?
+              \-
+            - else
+              = friendly_number_to_human account.statuses_count
+            %small= t('accounts.posts', count: account.statuses_count).downcase
+          %td.accounts-table__count.optional
+            - if account.suspended? || account.user_pending?
+              \-
+            - else
+              = friendly_number_to_human account.followers_count
+            %small= t('accounts.followers', count: account.followers_count).downcase
+          %td.accounts-table__count
+            = relevant_account_timestamp(account)
+            %small= t('accounts.last_active')
+          %td.accounts-table__extra
+            - if account.local?
+              - if account.user_email
+                = link_to account.user_email.split('@').last, admin_accounts_path(email: "%@#{account.user_email.split('@').last}"), title: account.user_email
+              - else
+                \-
+              %br/
+              %samp.ellipsized-ip= relevant_account_ip(account, params[:ip])
+    - if !account.suspended? && account.user_pending? && account.user&.invite_request&.text&.present?
+      .batch-table__row__content__quote
+        %p= account.user&.invite_request&.text
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 398ab4bb4..fc667b376 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -5,30 +5,30 @@
   .filter-subset
     %strong= t('admin.accounts.location.title')
     %ul
-      %li= filter_link_to t('admin.accounts.location.local'), remote: nil
-      %li= filter_link_to t('admin.accounts.location.remote'), remote: '1'
+      %li= filter_link_to t('generic.all'), origin: nil
+      %li= filter_link_to t('admin.accounts.location.local'), origin: 'local'
+      %li= filter_link_to t('admin.accounts.location.remote'), origin: 'remote'
   .filter-subset
     %strong= t('admin.accounts.moderation.title')
     %ul
-      %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
+      %li= filter_link_to t('generic.all'), status: nil
+      %li= filter_link_to t('admin.accounts.moderation.active'), status: 'active'
+      %li= filter_link_to t('admin.accounts.moderation.suspended'), status: 'suspended'
+      %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), status: 'pending'
   .filter-subset
     %strong= t('admin.accounts.role')
     %ul
-      %li= filter_link_to t('admin.accounts.moderation.all'), staff: nil
-      %li= filter_link_to t('admin.accounts.roles.staff'), staff: '1'
+      %li= filter_link_to t('admin.accounts.moderation.all'), permissions: nil
+      %li= filter_link_to t('admin.accounts.roles.staff'), permissions: 'staff'
   .filter-subset
     %strong= t 'generic.order_by'
     %ul
       %li= filter_link_to t('relationships.most_recent'), order: nil
-      %li= filter_link_to t('admin.accounts.username'), order: 'alphabetic'
       %li= filter_link_to t('relationships.last_active'), order: 'active'
 
 = form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do
   .fields-group
-    - AccountFilter::KEYS.each do |key|
+    - (AccountFilter::KEYS - %i(origin status permissions)).each do |key|
       - if params[key].present?
         = hidden_field_tag key, params[key]
 
@@ -41,16 +41,27 @@
       %button.button= t('admin.accounts.search')
       = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
 
-.table-wrapper
-  %table.table
-    %thead
-      %tr
-        %th= t('admin.accounts.username')
-        %th= t('admin.accounts.role')
-        %th= t('admin.accounts.most_recent_ip')
-        %th= t('admin.accounts.most_recent_activity')
-        %th
-    %tbody
-      = render partial: 'account', collection: @accounts
+= form_for(@form, url: batch_admin_accounts_path) do |f|
+  = hidden_field_tag :page, params[:page] || 1
+
+  - AccountFilter::KEYS.each do |key|
+    = hidden_field_tag key, params[key] if params[key].present?
+
+  .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
+        - if @accounts.any? { |account| account.user_pending? }
+          = 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') }
+
+        = f.button safe_join([fa_icon('lock'), t('admin.accounts.perform_full_suspension')]), name: :suspend, 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
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 27e1f80a7..3867d1b19 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -8,20 +8,12 @@
 = render 'application/card', account: @account
 
 - account = @account
-- proofs = account.identity_proofs.active
 - fields = account.fields
-- unless fields.empty? && proofs.empty? && account.note.blank?
+- unless fields.empty? && account.note.blank?
   .admin-account-bio
-    - unless fields.empty? && proofs.empty?
+    - unless fields.empty?
       %div
         .account__header__fields
-          - proofs.each do |proof|
-            %dl
-              %dt= proof.provider.capitalize
-              %dd.verified
-                = link_to fa_icon('check'), proof.badge.proof_url, class: 'verified__mark', title: t('accounts.link_verified_on', date: l(proof.updated_at))
-                = link_to proof.provider_username, proof.badge.profile_url
-
           - fields.each do |field|
             %dl
               %dt.emojify{ title: field.name }= Formatter.instance.format_field(account, field.name, custom_emojify: true)
@@ -79,7 +71,9 @@
           = t('admin.accounts.no_limits_imposed')
       .dashboard__counters__label= t 'admin.accounts.login_status'
 
-- unless @account.local? && @account.user.nil?
+- if @account.local? && @account.user.nil?
+  = link_to t('admin.accounts.unblock_email'), unblock_email_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unblock_email, @account) && CanonicalEmailBlock.where(reference_account_id: @account.id).exists?
+- else
   .table-wrapper
     %table.table.inline-table
       %tbody
@@ -129,6 +123,27 @@
               - else
                 = t('admin.accounts.confirming')
             %td= table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user)
+          %tr
+            %th{ rowspan: can?(:reset_password, @account.user) ? 2 : 1 }= t('admin.accounts.security')
+            %td{ rowspan: can?(:reset_password, @account.user) ? 2 : 1 }
+              - if @account.user&.two_factor_enabled?
+                = t 'admin.accounts.security_measures.password_and_2fa'
+              - elsif @account.user&.skip_sign_in_token?
+                = t 'admin.accounts.security_measures.only_password'
+              - else
+                = t 'admin.accounts.security_measures.password_and_sign_in_token'
+            %td
+              - if @account.user&.two_factor_enabled?
+                = table_link_to 'unlock', t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete if can?(:disable_2fa, @account.user)
+              - elsif @account.user&.skip_sign_in_token?
+                = table_link_to 'lock', t('admin.accounts.enable_sign_in_token_auth'), admin_user_sign_in_token_authentication_path(@account.user.id), method: :post if can?(:enable_sign_in_token_auth, @account.user)
+              - else
+                = table_link_to 'unlock', t('admin.accounts.disable_sign_in_token_auth'), admin_user_sign_in_token_authentication_path(@account.user.id), method: :delete if can?(:disable_sign_in_token_auth, @account.user)
+
+          - if can?(:reset_password, @account.user)
+            %tr
+              %td
+                = table_link_to 'key', t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, data: { confirm: t('admin.accounts.are_you_sure') }
 
           %tr
             %th= t('simple_form.labels.defaults.locale')
@@ -141,12 +156,14 @@
               %time.formatted{ datetime: @account.created_at.iso8601, title: l(@account.created_at) }= l @account.created_at
             %td
 
-          - @account.user.recent_ips.each_with_index do |(_, ip), i|
+          - recent_ips = @account.user.ips.order(used_at: :desc).to_a
+
+          - recent_ips.each_with_index do |recent_ip, i|
             %tr
               - if i.zero?
-                %th{ rowspan: @account.user.recent_ips.size }= t('admin.accounts.most_recent_ip')
-              %td= ip
-              %td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: ip)
+                %th{ rowspan: recent_ips.size }= t('admin.accounts.most_recent_ip')
+              %td= recent_ip.ip
+              %td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: recent_ip.ip)
 
           %tr
             %th= t('admin.accounts.most_recent_activity')
@@ -221,9 +238,6 @@
 
       %div
         - if @account.local?
-          = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
-          - if @account.user&.otp_required_for_login?
-            = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
           - if !@account.memorial? && @account.user_approved?
             = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
         - else
diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml
index 347eca166..03d5bffb9 100644
--- a/app/views/admin/action_logs/index.html.haml
+++ b/app/views/admin/action_logs/index.html.haml
@@ -19,7 +19,7 @@
   %div.muted-hint.center-text
     = t 'admin.action_logs.empty'
 - else
-  .announcements-list
+  .report-notes
     = render partial: 'action_log', collection: @action_logs
 
 = paginate @action_logs
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e25b80846..2ee13b9e2 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -1,6 +1,11 @@
 - content_for :page_title do
   = t('admin.dashboard.title')
 
+- content_for :heading_actions do
+  = l(@time_period.first)
+  = ' - '
+  = l(@time_period.last)
+
 - unless @system_checks.empty?
   .flash-message-stack
     - @system_checks.each do |message|
@@ -9,133 +14,52 @@
         - if message.action
           = link_to t("admin.system_checks.#{message.key}.action"), message.action
 
-.dashboard__counters
-  %div
-    = link_to admin_accounts_url(local: 1, recent: 1) do
-      .dashboard__counters__num{ title: number_with_delimiter(@users_count, strip_insignificant_zeros: true) }
-        = number_to_human @users_count, strip_insignificant_zeros: true
-      .dashboard__counters__label= t 'admin.dashboard.total_users'
-  %div
-    %div
-      .dashboard__counters__num{ title: number_with_delimiter(@registrations_week, strip_insignificant_zeros: true) }
-        = number_to_human @registrations_week, strip_insignificant_zeros: true
-      .dashboard__counters__label= t 'admin.dashboard.week_users_new'
-  %div
-    %div
-      .dashboard__counters__num{ title: number_with_delimiter(@logins_week, strip_insignificant_zeros: true) }
-        = number_to_human @logins_week, strip_insignificant_zeros: true
-      .dashboard__counters__label= t 'admin.dashboard.week_users_active'
-  %div
-    = link_to admin_pending_accounts_path do
-      .dashboard__counters__num{ title: number_with_delimiter(@pending_users_count, strip_insignificant_zeros: true) }
-        = number_to_human @pending_users_count, strip_insignificant_zeros: true
-      .dashboard__counters__label= t 'admin.dashboard.pending_users'
-  %div
-    = link_to admin_reports_url do
-      .dashboard__counters__num{ title: number_with_delimiter(@reports_count, strip_insignificant_zeros: true) }
-        = number_to_human @reports_count, strip_insignificant_zeros: true
-      .dashboard__counters__label= t 'admin.dashboard.open_reports'
-  %div
-    = link_to admin_tags_path(pending_review: '1') do
-      .dashboard__counters__num{ title: number_with_delimiter(@pending_tags_count, strip_insignificant_zeros: true) }
-        = number_to_human @pending_tags_count, strip_insignificant_zeros: true
-      .dashboard__counters__label= t 'admin.dashboard.pending_tags'
-  %div
-    %div
-      .dashboard__counters__num{ title: number_with_delimiter(@interactions_week, strip_insignificant_zeros: true) }
-        = number_to_human @interactions_week, strip_insignificant_zeros: true
-      .dashboard__counters__label= t 'admin.dashboard.week_interactions'
-  %div
-    = link_to sidekiq_url do
-      .dashboard__counters__num{ title: number_with_delimiter(@queue_backlog, strip_insignificant_zeros: true) }
-        = number_to_human @queue_backlog, strip_insignificant_zeros: true
-      .dashboard__counters__label= t 'admin.dashboard.backlog'
-
-.dashboard__widgets
-  .dashboard__widgets__users
-    %div
-      %h4= t 'admin.dashboard.recent_users'
-      %ul
-        - @recent_users.each do |user|
-          %li= admin_account_link_to(user.account)
-
-  .dashboard__widgets__features
-    %div
-      %h4= t 'admin.dashboard.features'
-      %ul
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_registrations'), edit_admin_settings_path), @registrations_enabled)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_invites'), edit_admin_settings_path), @invites_enabled)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_deletions'), edit_admin_settings_path), @deletions_enabled)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_profile_directory'), edit_admin_settings_path), @profile_directory)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_timeline_preview'), edit_admin_settings_path), @timeline_preview)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.keybase'), edit_admin_settings_path), @keybase_integration)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.trends'), edit_admin_settings_path), @trends_enabled)
-        %li
-          = feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled)
-
-  .dashboard__widgets__versions
-    %div
-      %h4= t 'admin.dashboard.software'
-      %ul
-        %li
-          Mastodon
-          %span.pull-right= @version
-        %li
-          Ruby
-          %span.pull-right= "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}"
-        %li
-          PostgreSQL
-          %span.pull-right= @database_version
-        %li
-          Redis
-          %span.pull-right= @redis_version
-
-  .dashboard__widgets__space
-    %div
-      %h4= t 'admin.dashboard.space'
-      %ul
-        %li
-          PostgreSQL
-          %span.pull-right= number_to_human_size @database_size
-        %li
-          Redis
-          %span.pull-right= number_to_human_size @redis_size
-
-  .dashboard__widgets__config
-    %div
-      %h4= t 'admin.dashboard.config'
-      %ul
-        %li
-          = feature_hint(t('admin.dashboard.search'), @search_enabled)
-        %li
-          = feature_hint(t('admin.dashboard.single_user_mode'), @single_user_mode)
-        %li
-          = feature_hint(t('admin.dashboard.authorized_fetch_mode'), @authorized_fetch)
-        %li
-          = feature_hint(t('admin.dashboard.whitelist_mode'), @whitelist_enabled)
-        %li
-          = feature_hint('LDAP', @ldap_enabled)
-        %li
-          = feature_hint('CAS', @cas_enabled)
-        %li
-          = feature_hint('SAML', @saml_enabled)
-        %li
-          = feature_hint('PAM', @pam_enabled)
-        %li
-          = feature_hint(t('admin.dashboard.hidden_service'), @hidden_service)
-
-  .dashboard__widgets__trends
-    %div
-      %h4= t 'admin.dashboard.trends'
-      %ul
-        - @trending_hashtags.each do |tag|
-          %li
-            = link_to content_tag(:span, "##{tag.name}", class: !tag.trendable? && !tag.reviewed? ? 'warning-hint' : (!tag.trendable? ? 'negative-hint' : nil)), admin_tag_path(tag.id)
-            %span.pull-right= number_with_delimiter(tag.history[0][:accounts].to_i)
+.dashboard
+  .dashboard__item
+    = react_admin_component :counter, measure: 'new_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.new_users'), href: admin_accounts_path
+
+  .dashboard__item
+    = react_admin_component :counter, measure: 'active_users', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.active_users'), href: admin_accounts_path
+
+  .dashboard__item
+    = react_admin_component :counter, measure: 'interactions', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.interactions')
+
+  .dashboard__item
+    = react_admin_component :counter, measure: 'opened_reports', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.opened_reports'), href: admin_reports_path
+
+  .dashboard__item
+    = react_admin_component :counter, measure: 'resolved_reports', start_at: @time_period.first, end_at: @time_period.last, label: t('admin.dashboard.resolved_reports'), href: admin_reports_path(resolved: '1')
+
+  .dashboard__item
+    = link_to admin_reports_path, class: 'dashboard__quick-access' do
+      %span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count)
+      = fa_icon 'chevron-right fw'
+
+    = link_to admin_accounts_path(status: 'pending'), class: 'dashboard__quick-access' do
+      %span= t('admin.dashboard.pending_users_html', count: @pending_users_count)
+      = fa_icon 'chevron-right fw'
+
+    = link_to admin_trends_tags_path(status: 'pending_review'), class: 'dashboard__quick-access' do
+      %span= t('admin.dashboard.pending_tags_html', count: @pending_tags_count)
+      = fa_icon 'chevron-right fw'
+
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'sources', start_at: @time_period.first, end_at: @time_period.last, limit: 8, label: t('admin.dashboard.sources')
+
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'languages', start_at: @time_period.first, end_at: @time_period.last, limit: 8, label: t('admin.dashboard.top_languages')
+
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'servers', start_at: @time_period.first, end_at: @time_period.last, limit: 8, label: t('admin.dashboard.top_servers')
+
+  .dashboard__item.dashboard__item--span-double-column
+    = react_admin_component :retention, start_at: @time_period.last - 6.months,   end_at: @time_period.last, frequency: 'month'
+
+  .dashboard__item.dashboard__item--span-double-row
+    = react_admin_component :trends, limit: 7
+
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'software_versions', start_at: @time_period.first, end_at: @time_period.last, limit: 4, label: t('admin.dashboard.software')
+
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'space_usage', start_at: @time_period.first, end_at: @time_period.last, limit: 3, label: t('admin.dashboard.space')
diff --git a/app/views/admin/follow_recommendations/_account.html.haml b/app/views/admin/follow_recommendations/_account.html.haml
index af5a4aaf7..00196dd01 100644
--- a/app/views/admin/follow_recommendations/_account.html.haml
+++ b/app/views/admin/follow_recommendations/_account.html.haml
@@ -7,10 +7,10 @@
         %tr
           %td= account_link_to account
           %td.accounts-table__count.optional
-            = number_to_human account.statuses_count, strip_insignificant_zeros: true
+            = friendly_number_to_human account.statuses_count
             %small= t('accounts.posts', count: account.statuses_count).downcase
           %td.accounts-table__count.optional
-            = number_to_human account.followers_count, strip_insignificant_zeros: true
+            = friendly_number_to_human account.followers_count
             %small= t('accounts.followers', count: account.followers_count).downcase
           %td.accounts-table__count
             - if account.last_status_at.present?
diff --git a/app/views/admin/instances/_instance.html.haml b/app/views/admin/instances/_instance.html.haml
index 990cf9ec8..dc81007ac 100644
--- a/app/views/admin/instances/_instance.html.haml
+++ b/app/views/admin/instances/_instance.html.haml
@@ -30,4 +30,4 @@
           = ' / '
           %span.negative-hint
             = t('admin.instances.delivery.unavailable_message')
-    .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true
+    .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= friendly_number_to_human instance.accounts_count
diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml
index 462529338..e520bca0c 100644
--- a/app/views/admin/instances/show.html.haml
+++ b/app/views/admin/instances/show.html.haml
@@ -15,7 +15,7 @@
 
 .dashboard__counters
   %div
-    = link_to admin_accounts_path(remote: '1', by_domain: @instance.domain) do
+    = link_to admin_accounts_path(origin: 'remote', by_domain: @instance.domain) do
       .dashboard__counters__num= number_with_delimiter @instance.accounts_count
       .dashboard__counters__label= t 'admin.accounts.title'
   %div
@@ -84,3 +84,5 @@
       = link_to t('admin.instances.delivery.stop'), stop_delivery_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }, class: 'button'
     - else
       = link_to t('admin.instances.delivery.restart'), restart_delivery_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }, class: 'button'
+    - unless @instance.delivery_failure_tracker.available? && @instance.accounts_count > 0
+      = link_to t('admin.instances.purge'), admin_instance_path(@instance), data: { confirm: t('admin.instances.confirm_purge'), method: :delete }, class: 'button'
diff --git a/app/views/admin/ip_blocks/_ip_block.html.haml b/app/views/admin/ip_blocks/_ip_block.html.haml
index e07e2b444..b8d3ac0e8 100644
--- a/app/views/admin/ip_blocks/_ip_block.html.haml
+++ b/app/views/admin/ip_blocks/_ip_block.html.haml
@@ -1,9 +1,9 @@
 .batch-table__row
   %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
     = f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id
-  .batch-table__row__content
-    .batch-table__row__content__text
-      %samp= "#{ip_block.ip}/#{ip_block.ip.prefix}"
+  .batch-table__row__content.pending-account
+    .pending-account__header
+      %samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}")
       - if ip_block.comment.present?

         = ip_block.comment
diff --git a/app/views/admin/pending_accounts/_account.html.haml b/app/views/admin/pending_accounts/_account.html.haml
deleted file mode 100644
index 5b475b59a..000000000
--- a/app/views/admin/pending_accounts/_account.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-.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.pending-account
-    .pending-account__header
-      = link_to admin_account_path(account.id) do
-        %strong= account.user_email
-        = "(@#{account.username})"
-      %br/
-      %samp= account.user_current_sign_in_ip
-      •
-      = t 'admin.accounts.time_in_queue', time: time_ago_in_words(account.user&.created_at)
-
-    - if account.user&.invite_request&.text&.present?
-      .pending-account__body
-        %p= account.user&.invite_request&.text
diff --git a/app/views/admin/pending_accounts/index.html.haml b/app/views/admin/pending_accounts/index.html.haml
deleted file mode 100644
index 8101d7f99..000000000
--- a/app/views/admin/pending_accounts/index.html.haml
+++ /dev/null
@@ -1,30 +0,0 @@
-- content_for :page_title do
-  = t('admin.pending_accounts.title', count: User.pending.count)
-
-= form_for(@form, url: batch_admin_pending_accounts_path) 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.action-buttons
-  %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'
-
-  %div
-    = 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'
diff --git a/app/views/admin/report_notes/_report_note.html.haml b/app/views/admin/report_notes/_report_note.html.haml
index d34dc3d15..428b6cf59 100644
--- a/app/views/admin/report_notes/_report_note.html.haml
+++ b/app/views/admin/report_notes/_report_note.html.haml
@@ -1,7 +1,18 @@
-.speech-bubble
-  .speech-bubble__bubble
+.report-notes__item
+  = image_tag report_note.account.avatar.url, class: 'report-notes__item__avatar'
+
+  .report-notes__item__header
+    %span.username
+      = link_to display_name(report_note.account), admin_account_path(report_note.account_id)
+    %time{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) }
+      - if report_note.created_at.today?
+        = t('admin.report_notes.today_at', time: l(report_note.created_at, format: :time))
+      - else
+        = l report_note.created_at.to_date
+
+  .report-notes__item__content
     = simple_format(h(report_note.content))
-  .speech-bubble__owner
-    = admin_account_link_to report_note.account
-    %time.formatted{ datetime: report_note.created_at.iso8601 }= l report_note.created_at
-    = table_link_to 'trash', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete if can?(:destroy, report_note)
+
+  - if can?(:destroy, report_note)
+    .report-notes__item__actions
+      = table_link_to 'trash', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete
diff --git a/app/views/admin/reports/_action_log.html.haml b/app/views/admin/reports/_action_log.html.haml
deleted file mode 100644
index 0f7d05867..000000000
--- a/app/views/admin/reports/_action_log.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.speech-bubble.positive
-  .speech-bubble__bubble
-    = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}_html", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target'))
-  .speech-bubble__owner
-    = admin_account_link_to(action_log.account)
-    %time.formatted{ datetime: action_log.created_at.iso8601 }= l action_log.created_at
diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml
index ada6dd2bc..4e06d4bbf 100644
--- a/app/views/admin/reports/_status.html.haml
+++ b/app/views/admin/reports/_status.html.haml
@@ -22,8 +22,14 @@
         = react_component :media_gallery, height: 343, sensitive: status.proper.sensitive?, visible: false, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
 
     .detailed-status__meta
+      - if status.application
+        = status.application.name
+        ·
       = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do
         %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
+      - if status.edited?
+        ·
+        = t('statuses.edited_at', date: l(status.edited_at))
       - if status.discarded?
         ·
         %span.negative-hint= t('admin.statuses.deleted')
diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml
index 721c55f71..619173373 100644
--- a/app/views/admin/reports/index.html.haml
+++ b/app/views/admin/reports/index.html.haml
@@ -7,6 +7,12 @@
     %ul
       %li= filter_link_to t('admin.reports.unresolved'), resolved: nil
       %li= filter_link_to t('admin.reports.resolved'), resolved: '1'
+  .filter-subset
+    %strong= t('admin.reports.target_origin')
+    %ul
+      %li= filter_link_to t('admin.accounts.location.all'), target_origin: nil
+      %li= filter_link_to t('admin.accounts.location.local'), target_origin: 'local'
+      %li= filter_link_to t('admin.accounts.location.remote'), target_origin: 'remote'
 
 = form_tag admin_reports_url, method: 'GET', class: 'simple_form' do
   .fields-group
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index 167e96c03..e03c1220c 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -7,122 +7,199 @@
   - else
     = link_to t('admin.reports.mark_as_unresolved'), reopen_admin_report_path(@report), method: :post, class: 'button'
 
-.table-wrapper
-  %table.table.inline-table
-    %tbody
-      %tr
-        %th= t('admin.reports.reported_account')
-        %td= admin_account_link_to @report.target_account
-        %td= table_link_to 'flag', t('admin.reports.account.reports', count: @report.target_account.targeted_reports.count), admin_reports_path(target_account_id: @report.target_account.id)
-        %td= table_link_to 'file', t('admin.reports.account.notes', count: @report.target_account.targeted_moderation_notes.count), admin_reports_path(target_account_id: @report.target_account.id)
-      %tr
-        %th= t('admin.reports.reported_by')
+.report-header
+  .report-header__card
+    .account-card
+      .account-card__header
+        = image_tag @report.target_account.header.url, alt: ''
+      .account-card__title
+        .account-card__title__avatar
+          = image_tag @report.target_account.avatar.url, alt: ''
+        .display-name
+          %bdi
+            %strong.emojify.p-name= display_name(@report.target_account, custom_emojify: true)
+          %span
+            = acct(@report.target_account)
+            = fa_icon('lock') if @report.target_account.locked?
+      - if @report.target_account.note.present?
+        .account-card__bio.emojify
+          = Formatter.instance.simplified_format(@report.target_account, custom_emojify: true)
+      .account-card__actions
+        .account-card__counters
+          .account-card__counters__item
+            = friendly_number_to_human @report.target_account.statuses_count
+            %small= t('accounts.posts', count: @report.target_account.statuses_count).downcase
+          .account-card__counters__item
+            = friendly_number_to_human @report.target_account.followers_count
+            %small= t('accounts.followers', count: @report.target_account.followers_count).downcase
+          .account-card__counters__item
+            = friendly_number_to_human @report.target_account.following_count
+            %small= t('accounts.following', count: @report.target_account.following_count).downcase
+        .account-card__actions__button
+          = link_to t('admin.reports.view_profile'), admin_account_path(@report.target_account_id), class: 'button'
+    .report-header__details.report-header__details--horizontal
+      .report-header__details__item
+        .report-header__details__item__header
+          %strong= t('admin.accounts.joined')
+        .report-header__details__item__content
+          %time.time-ago{ datetime: @report.target_account.created_at.iso8601, title: l(@report.target_account.created_at) }= l @report.target_account.created_at
+      .report-header__details__item
+        .report-header__details__item__header
+          %strong= t('accounts.last_active')
+        .report-header__details__item__content
+          - if @report.target_account.last_status_at.present?
+            %time.time-ago{ datetime: @report.target_account.last_status_at.to_date.iso8601, title: l(@report.target_account.last_status_at.to_date) }= l @report.target_account.last_status_at
+      .report-header__details__item
+        .report-header__details__item__header
+          %strong= t('admin.accounts.strikes')
+        .report-header__details__item__content
+          = @report.target_account.strikes.count
+
+  .report-header__details
+    .report-header__details__item
+      .report-header__details__item__header
+        %strong= t('admin.reports.created_at')
+      .report-header__details__item__content
+        %time.formatted{ datetime: @report.created_at.iso8601 }
+    .report-header__details__item
+      .report-header__details__item__header
+        %strong= t('admin.reports.reported_by')
+      .report-header__details__item__content
         - if @report.account.instance_actor?
-          %td{ colspan: 3 }= site_hostname
+          = site_hostname
         - elsif @report.account.local?
-          %td= admin_account_link_to @report.account
-          %td= table_link_to 'flag', t('admin.reports.account.reports', count: @report.account.targeted_reports.count), admin_reports_path(target_account_id: @report.account.id)
-          %td= table_link_to 'file', t('admin.reports.account.notes', count: @report.account.targeted_moderation_notes.count), admin_reports_path(target_account_id: @report.account.id)
+          = admin_account_link_to @report.account
+        - else
+          = @report.account.domain
+    .report-header__details__item
+      .report-header__details__item__header
+        %strong= t('admin.reports.status')
+      .report-header__details__item__content
+        - if @report.action_taken?
+          = t('admin.reports.resolved')
         - else
-          %td{ colspan: 3 }= @report.account.domain
-      %tr
-        %th= t('admin.reports.created_at')
-        %td{ colspan: 3 }
-          %time.formatted{ datetime: @report.created_at.iso8601 }
-      %tr
-        %th= t('admin.reports.updated_at')
-        %td{ colspan: 3 }
-          %time.formatted{ datetime: @report.updated_at.iso8601 }
-      %tr
-        %th= t('admin.reports.status')
-        %td
-          - if @report.action_taken?
-            = t('admin.reports.resolved')
+          = t('admin.reports.unresolved')
+    - unless @report.target_account.local?
+      .report-header__details__item
+        .report-header__details__item__header
+          %strong= t('admin.reports.forwarded')
+        .report-header__details__item__content
+          - if @report.forwarded?
+            = t('simple_form.yes')
           - else
-            = t('admin.reports.unresolved')
-        %td{ colspan: 2 }
-          - if @report.action_taken?
-            = table_link_to 'envelope-open', t('admin.reports.reopen'), admin_report_path(@report, outcome: 'reopen'), method: :put
-      - unless @report.target_account.local?
-        %tr
-          %th= t('admin.reports.forwarded')
-          %td{ colspan: 3 }
-            - if @report.forwarded.nil?
-              \-
-            - elsif @report.forwarded?
-              = t('simple_form.yes')
-            - else
-              = t('simple_form.no')
-      - if !@report.action_taken_by_account.nil?
-        %tr
-          %th= t('admin.reports.action_taken_by')
-          %td{ colspan: 3 }
-            = admin_account_link_to @report.action_taken_by_account
-      - else
-        %tr
-          %th= t('admin.reports.assigned')
-          %td
-            - if @report.assigned_account.nil?
-              \-
-            - else
-              = admin_account_link_to @report.assigned_account
-          %td
-            - if @report.assigned_account != current_user.account
-              = table_link_to 'user', t('admin.reports.assign_to_self'), assign_to_self_admin_report_path(@report), method: :post
-          %td
-            - if !@report.assigned_account.nil?
-              = table_link_to 'trash', t('admin.reports.unassign'), unassign_admin_report_path(@report), method: :post
+            = t('simple_form.no')
+    - if !@report.action_taken_by_account.nil?
+      .report-header__details__item
+        .report-header__details__item__header
+          %strong= t('admin.reports.action_taken_by')
+        .report-header__details__item__content
+          = admin_account_link_to @report.action_taken_by_account
+    - else
+      .report-header__details__item
+        .report-header__details__item__header
+          %strong= t('admin.reports.assigned')
+        .report-header__details__item__content
+          - if @report.assigned_account.nil?
+            = t 'admin.reports.no_one_assigned'
+          - else
+            = admin_account_link_to @report.assigned_account
+          —
+          - if @report.assigned_account != current_user.account
+            = table_link_to 'user', t('admin.reports.assign_to_self'), assign_to_self_admin_report_path(@report), method: :post
+          - elsif !@report.assigned_account.nil?
+            = table_link_to 'trash', t('admin.reports.unassign'), unassign_admin_report_path(@report), method: :post
 
 %hr.spacer
 
-%div.action-buttons
-  %div
+%h3= t 'admin.reports.category'
 
-  - if @report.unresolved?
-    %div
-      - if @report.target_account.local?
-        = link_to t('admin.accounts.warn'), new_admin_account_action_path(@report.target_account_id, type: 'none', report_id: @report.id), class: 'button'
-        = link_to t('admin.accounts.disable'), new_admin_account_action_path(@report.target_account_id, type: 'disable', report_id: @report.id), class: 'button button--destructive'
-      = link_to t('admin.accounts.silence'), new_admin_account_action_path(@report.target_account_id, type: 'silence', report_id: @report.id), class: 'button button--destructive'
-      = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@report.target_account_id, type: 'suspend', report_id: @report.id), class: 'button button--destructive'
+%p= t 'admin.reports.category_description_html'
 
-%hr.spacer
+= react_admin_component :report_reason_selector, id: @report.id, category: @report.category, rule_ids: @report.rule_ids&.map(&:to_s), disabled: @report.action_taken?
 
-.speech-bubble
-  .speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none'))
-  .speech-bubble__owner
-    - if @report.account.local?
-      = admin_account_link_to @report.account
-    - else
-      = @report.account.domain
-      %br/
-    %time.formatted{ datetime: @report.created_at.iso8601 }
+- if @report.comment.present?
+  %p= t('admin.reports.comment_description_html', name: content_tag(:strong, @report.account.username, class: 'username'))
+
+  .report-notes__item
+    = image_tag @report.account.avatar.url, class: 'report-notes__item__avatar'
+
+    .report-notes__item__header
+      %span.username
+        = link_to display_name(@report.account), admin_account_path(@report.account_id)
+      %time{ datetime: @report.created_at.iso8601, title: l(@report.created_at) }
+        - if @report.created_at.today?
+          = t('admin.report_notes.today_at', time: l(@report.created_at, format: :time))
+        - else
+          = l @report.created_at.to_date
+
+    .report-notes__item__content
+      = simple_format(h(@report.comment))
+
+%hr.spacer/
 
-- unless @report.statuses.empty?
+%h3= t 'admin.reports.statuses'
+
+%p
+  = t 'admin.reports.statuses_description_html'
+  —
+  = link_to safe_join([fa_icon('plus'), t('admin.reports.add_to_report')]), admin_account_statuses_path(@report.target_account_id, report_id: @report.id), class: 'table-action-link'
+
+= form_for(@form, url: batch_admin_account_statuses_path(@report.target_account_id, report_id: @report.id)) do |f|
+  .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
+        - if !@statuses.empty? && @report.unresolved?
+          = f.button safe_join([fa_icon('times'), t('admin.statuses.batch.remove_from_report')]), name: :remove_from_report, class: 'table-action-link', type: :submit
+          = f.button safe_join([fa_icon('trash'), t('admin.reports.delete_and_resolve')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+        - else
+    .batch-table__body
+      - if @statuses.empty?
+        = nothing_here 'nothing-here--under-tabs'
+      - else
+        = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
+
+- if @report.unresolved?
   %hr.spacer/
 
-  = form_for(@form, url: admin_report_reported_statuses_path(@report.id)) do |f|
-    .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('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
-          = f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
-          = f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
-      .batch-table__body
-        = render partial: 'admin/reports/status', collection: @report.statuses, locals: { f: f }
+  %p= t 'admin.reports.actions_description_html'
+
+  .report-actions
+    .report-actions__item
+      .report-actions__item__button
+        = link_to t('admin.accounts.silence'), new_admin_account_action_path(@report.target_account_id, type: 'silence', report_id: @report.id), class: 'button button--destructive'
+      .report-actions__item__description
+        = t('admin.reports.actions.silence_description_html')
+    .report-actions__item
+      .report-actions__item__button
+        = link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@report.target_account_id, report_id: @report.id, type: 'suspend'), class: 'button button--destructive'
+      .report-actions__item__description
+        = t('admin.reports.actions.suspend_description_html')
+    .report-actions__item
+      .report-actions__item__button
+        = link_to t('admin.accounts.custom'), new_admin_account_action_path(@report.target_account_id, report_id: @report.id), class: 'button'
+      .report-actions__item__description
+        = t('admin.reports.actions.other_description_html')
+
+- unless @action_logs.empty?
+  %hr.spacer/
+
+  %h3= t 'admin.reports.action_log'
+
+  .report-notes
+    = render @action_logs
 
 %hr.spacer/
 
-- @report_notes.each do |item|
-  - if item.is_a?(Admin::ActionLog)
-    = render partial: 'action_log', locals: { action_log: item }
-  - else
-    = render item
+%h3= t 'admin.reports.notes.title'
+
+%p= t 'admin.reports.notes_description_html'
+
+.report-notes
+  = render @report_notes
 
 = simple_form_for @report_note, url: admin_report_notes_path do |f|
-  = render 'shared/error_messages', object: @report_note
   = f.input :report_id, as: :hidden
 
   .field-group
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 373811ea3..49b03a9e3 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -42,7 +42,10 @@
 
   .fields-group
     = f.input :require_invite_text, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.require_invite_text.title'), hint: t('admin.settings.registrations.require_invite_text.desc_html'), disabled: !approved_registrations?
-  .fields-group
+
+  - if captcha_available?
+    .fields-group
+      = f.input :captcha_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.captcha_enabled.title'), hint: t('admin.settings.captcha_enabled.desc_html')
 
   %hr.spacer/
 
@@ -90,9 +93,6 @@
     = f.input :hide_followers_count, as: :boolean, wrapper: :with_label, label: t('admin.settings.hide_followers_count.title'), hint: t('admin.settings.hide_followers_count.desc_html')
 
   .fields-group
-    = f.input :enable_keybase, as: :boolean, wrapper: :with_label, label: t('admin.settings.enable_keybase.title'), hint: t('admin.settings.enable_keybase.desc_html')
-
-  .fields-group
     = f.input :show_reblogs_in_public_timelines, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_reblogs_in_public_timelines.title'), hint: t('admin.settings.show_reblogs_in_public_timelines.desc_html')
 
   .fields-group
diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml
index 5414d69d5..865464c72 100644
--- a/app/views/admin/statuses/index.html.haml
+++ b/app/views/admin/statuses/index.html.haml
@@ -7,28 +7,37 @@
   .filter-subset
     %strong= t('admin.statuses.media.title')
     %ul
-      %li= link_to t('admin.statuses.no_media'), admin_account_statuses_path(@account.id, current_params.merge(media: nil)), class: !params[:media] && 'selected'
-      %li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected'
+      %li= filter_link_to t('generic.all'), media: nil, id: nil
+      %li= filter_link_to t('admin.statuses.with_media'), media: '1'
   .back-link
-    = link_to admin_account_path(@account.id) do
-      = fa_icon 'chevron-left fw'
-      = t('admin.statuses.back_to_account')
+    - if params[:report_id]
+      = link_to admin_report_path(params[:report_id].to_i) do
+        = fa_icon 'chevron-left fw'
+        = t('admin.statuses.back_to_report')
+    - else
+      = link_to admin_account_path(@account.id) do
+        = fa_icon 'chevron-left fw'
+        = t('admin.statuses.back_to_account')
 
 %hr.spacer/
 
-= form_for(@form, url: admin_account_statuses_path(@account.id)) do |f|
-  = hidden_field_tag :page, params[:page]
-  = hidden_field_tag :media, params[:media]
+= form_for(@status_batch_action, url: batch_admin_account_statuses_path(@account.id)) do |f|
+  = hidden_field_tag :page, params[:page] || 1
+
+  - Admin::StatusFilter::KEYS.each do |key|
+    = hidden_field_tag key, params[key] if params[key].present?
 
   .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('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
-        = f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
-        = f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+        - unless @statuses.empty?
+          = f.button safe_join([fa_icon('flag'), t('admin.statuses.batch.report')]), name: :report, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
     .batch-table__body
-      = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
+      - if @statuses.empty?
+        = nothing_here 'nothing-here--under-tabs'
+      - else
+        = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
 
 = paginate @statuses
diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml
deleted file mode 100644
index e2470198d..000000000
--- a/app/views/admin/statuses/show.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-- content_for :page_title do
-  = t('admin.statuses.title')
-  \-
-  = "@#{@account.acct}"
-
-.filters
-  .back-link
-    = link_to admin_account_path(@account.id) do
-      %i.fa.fa-chevron-left.fa-fw
-      = t('admin.statuses.back_to_account')
-
-%hr.spacer/
-
-= form_for(@form, url: admin_account_statuses_path(@account.id)) do |f|
-  = hidden_field_tag :page, params[:page]
-  = hidden_field_tag :media, params[:media]
-
-  .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('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
-        = f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
-        = f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
-    .batch-table__body
-      = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
diff --git a/app/views/admin/tags/_tag.html.haml b/app/views/admin/tags/_tag.html.haml
deleted file mode 100644
index adf4ca7b2..000000000
--- a/app/views/admin/tags/_tag.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-.batch-table__row
-  - if batch_available
-    %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
-      = f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id
-
-  .directory__tag
-    = link_to admin_tag_path(tag.id) do
-      %h4
-        = fa_icon 'hashtag'
-        = tag.name
-
-        %small
-          = t('admin.tags.unique_uses_today', count: tag.history.first[:accounts])
-
-          - if tag.trending?
-            = fa_icon 'fire fw'
-            = t('admin.tags.trending_right_now')
-
-      .trends__item__current= number_to_human tag.history.first[:uses], strip_insignificant_zeros: true
diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml
deleted file mode 100644
index e25b0ae84..000000000
--- a/app/views/admin/tags/index.html.haml
+++ /dev/null
@@ -1,71 +0,0 @@
-- content_for :page_title do
-  = t('admin.tags.title')
-
-.filters
-  .filter-subset
-    %strong= t('admin.tags.review')
-    %ul
-      %li= filter_link_to t('generic.all'), reviewed: nil, unreviewed: nil, pending_review: nil
-      %li= filter_link_to t('admin.tags.unreviewed'), unreviewed: '1', reviewed: nil, pending_review: nil
-      %li= filter_link_to t('admin.tags.reviewed'), reviewed: '1', unreviewed: nil, pending_review: nil
-      %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Tag.pending_review.count})"], ' '), pending_review: '1', reviewed: nil, unreviewed: nil
-
-  .filter-subset
-    %strong= t('generic.order_by')
-    %ul
-      %li= filter_link_to t('admin.tags.most_recent'), popular: nil, active: nil
-      %li= filter_link_to t('admin.tags.last_active'), active: '1', popular: nil
-      %li= filter_link_to t('admin.tags.most_popular'), popular: '1', active: nil
-
-
-= form_tag admin_tags_url, method: 'GET', class: 'simple_form' do
-  .fields-group
-    - TagFilter::KEYS.each do |key|
-      = hidden_field_tag key, params[key] if params[key].present?
-
-    - %i(name).each do |key|
-      .input.string.optional
-        = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.tags.#{key}")
-
-    .actions
-      %button.button= t('admin.accounts.search')
-      = link_to t('admin.accounts.reset'), admin_tags_path, class: 'button negative'
-
-%hr.spacer/
-
-= form_for(@form, url: batch_admin_tags_path) do |f|
-  = hidden_field_tag :page, params[:page] || 1
-
-  - TagFilter::KEYS.each do |key|
-    = hidden_field_tag key, params[key] if params[key].present?
-
-  .batch-table.optional
-    .batch-table__toolbar
-      - if params[:pending_review] == '1' || params[:unreviewed] == '1'
-        %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') }
-      - else
-        .batch-table__toolbar__actions
-          %span.neutral-hint= t('generic.no_batch_actions_available')
-
-    .batch-table__body
-      - if @tags.empty?
-        = nothing_here 'nothing-here--under-tabs'
-      - else
-        = render partial: 'tag', collection: @tags, locals: { f: f, batch_available: params[:pending_review] == '1' || params[:unreviewed] == '1' }
-
-= paginate @tags
-
-- if params[:pending_review] == '1' || params[:unreviewed] == '1'
-  %hr.spacer/
-
-  %div.action-buttons
-    %div
-      = link_to t('admin.accounts.approve_all'), approve_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'
-
-    %div
-      = link_to t('admin.accounts.reject_all'), reject_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'
diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml
index c4caffda1..c41ce2fc2 100644
--- a/app/views/admin/tags/show.html.haml
+++ b/app/views/admin/tags/show.html.haml
@@ -1,15 +1,47 @@
 - content_for :page_title do
   = "##{@tag.name}"
 
-.dashboard__counters
-  %div
-    = link_to tag_url(@tag), target: '_blank', rel: 'noopener noreferrer' do
-      .dashboard__counters__num= number_with_delimiter @accounts_today
-      .dashboard__counters__label= t 'admin.tags.accounts_today'
-  %div
-    %div
-      .dashboard__counters__num= number_with_delimiter @accounts_week
-      .dashboard__counters__label= t 'admin.tags.accounts_week'
+- content_for :heading_actions do
+  = l(@time_period.first)
+  = ' - '
+  = l(@time_period.last)
+
+.dashboard
+  .dashboard__item
+    = react_admin_component :counter, measure: 'tag_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_accounts_measure')
+  .dashboard__item
+    = react_admin_component :counter, measure: 'tag_uses', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_uses_measure')
+  .dashboard__item
+    = react_admin_component :counter, measure: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_servers_measure')
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_servers_dimension')
+  .dashboard__item
+    = react_admin_component :dimension, dimension: 'tag_languages', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_languages_dimension')
+  .dashboard__item
+    = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.usable? ? 'positive' : 'negative'] do
+      - if @tag.usable?
+        %span= t('admin.trends.tags.usable')
+        = fa_icon 'check fw'
+      - else
+        %span= t('admin.trends.tags.not_usable')
+        = fa_icon 'lock fw'
+
+    = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.trendable? ? 'positive' : 'negative'] do
+      - if @tag.trendable?
+        %span= t('admin.trends.tags.trendable')
+        = fa_icon 'check fw'
+      - else
+        %span= t('admin.trends.tags.not_trendable')
+        = fa_icon 'lock fw'
+
+
+    = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.listable? ? 'positive' : 'negative'] do
+      - if @tag.listable?
+        %span= t('admin.trends.tags.listable')
+        = fa_icon 'check fw'
+      - else
+        %span= t('admin.trends.tags.not_listable')
+        = fa_icon 'lock fw'
 
 %hr.spacer/
 
@@ -26,18 +58,3 @@
 
   .actions
     = f.button :button, t('generic.save_changes'), type: :submit
-
-%hr.spacer/
-
-%h3= t 'admin.tags.breakdown'
-
-.table-wrapper
-  %table.table
-    %tbody
-      - total = @usage_by_domain.sum(&:last).to_f
-
-      - @usage_by_domain.each do |(domain, count)|
-        %tr
-          %th= domain || site_hostname
-          %td= number_to_percentage((count / total) * 100, precision: 1)
-          %td= number_with_delimiter count
diff --git a/app/views/admin/trends/links/_preview_card.html.haml b/app/views/admin/trends/links/_preview_card.html.haml
new file mode 100644
index 000000000..b88c1be2f
--- /dev/null
+++ b/app/views/admin/trends/links/_preview_card.html.haml
@@ -0,0 +1,30 @@
+.batch-table__row{ class: [preview_card.provider&.requires_review? && 'batch-table__row--attention', !preview_card.provider&.requires_review? && !preview_card.trendable? && 'batch-table__row--muted'] }
+  %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
+    = f.check_box :preview_card_ids, { multiple: true, include_hidden: false }, preview_card.id
+
+  .batch-table__row__content.pending-account
+    .pending-account__header
+      = link_to preview_card.title, preview_card.url
+
+      %br/
+
+      - if preview_card.provider_name.present?
+        = preview_card.provider_name
+        •
+
+      - if preview_card.language.present?
+        = human_locale(preview_card.language)
+        •
+
+      = t('admin.trends.links.shared_by_over_week', count: preview_card.history.reduce(0) { |sum, day| sum + day.accounts })
+
+      - if preview_card.trendable? && (rank = Trends.links.rank(preview_card.id))
+        •
+        %abbr{ title: t('admin.trends.tags.current_score', score: Trends.links.score(preview_card.id)) }= t('admin.trends.tags.trending_rank', rank: rank + 1)
+
+        - if preview_card.decaying?
+          •
+          = t('admin.trends.tags.peaked_on_and_decaying', date: l(preview_card.max_score_at.to_date, format: :short))
+      - elsif preview_card.provider&.requires_review?
+        •
+        = t('admin.trends.pending_review')
diff --git a/app/views/admin/trends/links/index.html.haml b/app/views/admin/trends/links/index.html.haml
new file mode 100644
index 000000000..acd2b0466
--- /dev/null
+++ b/app/views/admin/trends/links/index.html.haml
@@ -0,0 +1,38 @@
+- content_for :page_title do
+  = t('admin.trends.links.title')
+
+.filters
+  .filter-subset
+    %strong= t('admin.trends.trending')
+    %ul
+      %li= filter_link_to t('generic.all'), trending: nil
+      %li= filter_link_to t('admin.trends.only_allowed'), trending: 'allowed'
+  .back-link
+    = link_to admin_trends_links_preview_card_providers_path do
+      = t('admin.trends.preview_card_providers.title')
+      = fa_icon 'chevron-right fw'
+
+%hr.spacer/
+
+= form_for(@form, url: batch_admin_trends_links_path) do |f|
+  = hidden_field_tag :page, params[:page] || 1
+
+  - PreviewCardFilter::KEYS.each do |key|
+    = hidden_field_tag key, params[key] if params[key].present?
+
+  .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.trends.links.allow')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+        = f.button safe_join([fa_icon('check'), t('admin.trends.links.allow_provider')]), name: :approve_all, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+        = f.button safe_join([fa_icon('times'), t('admin.trends.links.disallow')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+        = f.button safe_join([fa_icon('times'), t('admin.trends.links.disallow_provider')]), name: :reject_all, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+    .batch-table__body
+      - if @preview_cards.empty?
+        = nothing_here 'nothing-here--under-tabs'
+      - else
+        = render partial: 'preview_card', collection: @preview_cards, locals: { f: f }
+
+= paginate @preview_cards
diff --git a/app/views/admin/trends/links/preview_card_providers/_preview_card_provider.html.haml b/app/views/admin/trends/links/preview_card_providers/_preview_card_provider.html.haml
new file mode 100644
index 000000000..e40e6529d
--- /dev/null
+++ b/app/views/admin/trends/links/preview_card_providers/_preview_card_provider.html.haml
@@ -0,0 +1,16 @@
+.batch-table__row{ class: [preview_card_provider.requires_review? && 'batch-table__row--attention', !preview_card_provider.requires_review? && !preview_card_provider.trendable? && 'batch-table__row--muted'] }
+  %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
+    = f.check_box :preview_card_provider_ids, { multiple: true, include_hidden: false }, preview_card_provider.id
+
+  .batch-table__row__content.pending-account
+    .pending-account__header
+      %strong= preview_card_provider.domain
+
+      %br/
+
+      - if preview_card_provider.requires_review?
+        = t('admin.trends.pending_review')
+      - elsif preview_card_provider.trendable?
+        = t('admin.trends.preview_card_providers.allowed')
+      - else
+        = t('admin.trends.preview_card_providers.rejected')
diff --git a/app/views/admin/trends/links/preview_card_providers/index.html.haml b/app/views/admin/trends/links/preview_card_providers/index.html.haml
new file mode 100644
index 000000000..df54f58ba
--- /dev/null
+++ b/app/views/admin/trends/links/preview_card_providers/index.html.haml
@@ -0,0 +1,40 @@
+- content_for :page_title do
+  = t('admin.trends.preview_card_providers.title')
+
+.filters
+  .filter-subset
+    %strong= t('admin.tags.review')
+    %ul
+      %li= filter_link_to t('generic.all'), status: nil
+      %li= filter_link_to t('admin.trends.approved'), status: 'approved'
+      %li= filter_link_to t('admin.trends.rejected'), status: 'rejected'
+      %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{PreviewCardProvider.pending_review.count})"], ' '), status: 'pending_review'
+  .back-link
+    = link_to admin_trends_links_path do
+      = fa_icon 'chevron-left fw'
+      = t('admin.trends.links.title')
+
+
+%hr.spacer/
+
+= form_for(@form, url: batch_admin_trends_links_preview_card_providers_path) do |f|
+  = hidden_field_tag :page, params[:page] || 1
+
+  - PreviewCardProviderFilter::KEYS.each do |key|
+    = hidden_field_tag key, params[key] if params[key].present?
+
+  .batch-table.optional
+    .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.trends.allow')]), 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.trends.disallow')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+
+    .batch-table__body
+      - if @preview_card_providers.empty?
+        = nothing_here 'nothing-here--under-tabs'
+      - else
+        = render partial: 'preview_card_provider', collection: @preview_card_providers, locals: { f: f }
+
+= paginate @preview_card_providers
diff --git a/app/views/admin/trends/tags/_tag.html.haml b/app/views/admin/trends/tags/_tag.html.haml
new file mode 100644
index 000000000..7bb99b158
--- /dev/null
+++ b/app/views/admin/trends/tags/_tag.html.haml
@@ -0,0 +1,24 @@
+.batch-table__row{ class: [tag.requires_review? && 'batch-table__row--attention', !tag.requires_review? && !tag.trendable? && 'batch-table__row--muted'] }
+  %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
+    = f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id
+
+  .batch-table__row__content.pending-account
+    .pending-account__header
+      = link_to admin_tag_path(tag.id) do
+        = fa_icon 'hashtag'
+        = tag.name
+
+      %br/
+
+      = t('admin.trends.tags.used_by_over_week', count: tag.history.reduce(0) { |sum, day| sum + day.accounts })
+
+      - if tag.trendable? && (rank = Trends.tags.rank(tag.id))
+        •
+        %abbr{ title: t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id)) }= t('admin.trends.tags.trending_rank', rank: rank + 1)
+
+        - if tag.decaying?
+          •
+          = t('admin.trends.tags.peaked_on_and_decaying', date: l(tag.max_score_at.to_date, format: :short))
+      - elsif tag.requires_review?
+        •
+        = t('admin.trends.pending_review')
diff --git a/app/views/admin/trends/tags/index.html.haml b/app/views/admin/trends/tags/index.html.haml
new file mode 100644
index 000000000..99ad5490f
--- /dev/null
+++ b/app/views/admin/trends/tags/index.html.haml
@@ -0,0 +1,35 @@
+- content_for :page_title do
+  = t('admin.trends.tags.title')
+
+.filters
+  .filter-subset
+    %strong= t('admin.tags.review')
+    %ul
+      %li= filter_link_to t('generic.all'), status: nil
+      %li= filter_link_to t('admin.trends.approved'), status: 'approved'
+      %li= filter_link_to t('admin.trends.rejected'), status: 'rejected'
+      %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Tag.pending_review.count})"], ' '), status: 'pending_review'
+
+%hr.spacer/
+
+= form_for(@form, url: batch_admin_trends_tags_path) do |f|
+  = hidden_field_tag :page, params[:page] || 1
+
+  - TagFilter::KEYS.each do |key|
+    = hidden_field_tag key, params[key] if params[key].present?
+
+  .batch-table.optional
+    .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.trends.allow')]), 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.trends.disallow')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+
+    .batch-table__body
+      - if @tags.empty?
+        = nothing_here 'nothing-here--under-tabs'
+      - else
+        = render partial: 'tag', collection: @tags, locals: { f: f }
+
+= paginate @tags
diff --git a/app/views/admin_mailer/new_pending_account.text.erb b/app/views/admin_mailer/new_pending_account.text.erb
index a466ee2de..a8a2a35fa 100644
--- a/app/views/admin_mailer/new_pending_account.text.erb
+++ b/app/views/admin_mailer/new_pending_account.text.erb
@@ -3,10 +3,10 @@
 <%= raw t('admin_mailer.new_pending_account.body') %>
 
 <%= @account.user_email %> (@<%= @account.username %>)
-<%= @account.user_current_sign_in_ip %>
+<%= @account.user_sign_up_ip %>
 <% if @account.user&.invite_request&.text.present? %>
 
 <%= quote_wrap(@account.user&.invite_request&.text) %>
 <% end %>
 
-<%= raw t('application_mailer.view')%> <%= admin_pending_accounts_url %>
+<%= raw t('application_mailer.view')%> <%= admin_accounts_url(status: 'pending') %>
diff --git a/app/views/admin_mailer/new_trending_links.text.erb b/app/views/admin_mailer/new_trending_links.text.erb
new file mode 100644
index 000000000..51789aca5
--- /dev/null
+++ b/app/views/admin_mailer/new_trending_links.text.erb
@@ -0,0 +1,16 @@
+<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
+
+<%= raw t('admin_mailer.new_trending_links.body') %>
+
+<% @links.each do |link| %>
+- <%= link.title %> • <%= link.url %>
+  <%= t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.links.score(link.id).round(2)) %>
+<% end %>
+
+<% if @lowest_trending_link %>
+<%= t('admin_mailer.new_trending_links.requirements', lowest_link_title: @lowest_trending_link.title, lowest_link_score: Trends.links.score(@lowest_trending_link.id).round(2)) %>
+<% else %>
+<%= t('admin_mailer.new_trending_links.no_approved_links') %>
+<% end %>
+
+<%= raw t('application_mailer.view')%> <%= admin_trends_links_url %>
diff --git a/app/views/admin_mailer/new_trending_tag.text.erb b/app/views/admin_mailer/new_trending_tag.text.erb
deleted file mode 100644
index e4bfdc591..000000000
--- a/app/views/admin_mailer/new_trending_tag.text.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
-
-<%= raw t('admin_mailer.new_trending_tag.body', name: @tag.name) %>
-
-<%= raw t('application_mailer.view')%> <%= admin_tags_url(pending_review: '1') %>
diff --git a/app/views/admin_mailer/new_trending_tags.text.erb b/app/views/admin_mailer/new_trending_tags.text.erb
new file mode 100644
index 000000000..5051e8a96
--- /dev/null
+++ b/app/views/admin_mailer/new_trending_tags.text.erb
@@ -0,0 +1,16 @@
+<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
+
+<%= raw t('admin_mailer.new_trending_tags.body') %>
+
+<% @tags.each do |tag| %>
+- #<%= tag.name %>
+  <%= t('admin.trends.tags.usage_comparison', today: tag.history.get(Time.now.utc).accounts, yesterday: tag.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.tags.score(tag.id).round(2)) %>
+<% end %>
+
+<% if @lowest_trending_tag %>
+<%= t('admin_mailer.new_trending_tags.requirements', lowest_tag_name: @lowest_trending_tag.name, lowest_tag_score: Trends.tags.score(@lowest_trending_tag.id).round(2)) %>
+<% else %>
+<%= t('admin_mailer.new_trending_tags.no_approved_tags') %>
+<% end %>
+
+<%= raw t('application_mailer.view')%> <%= admin_trends_tags_url(pending_review: '1') %>
diff --git a/app/views/application/_sidebar.html.haml b/app/views/application/_sidebar.html.haml
index 7ec91c06a..6826c3b58 100644
--- a/app/views/application/_sidebar.html.haml
+++ b/app/views/application/_sidebar.html.haml
@@ -6,7 +6,7 @@
     %p= @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html')
 
 - if Setting.trends && !(user_signed_in? && !current_user.setting_trends)
-  - trends = TrendingTags.get(3)
+  - trends = Trends.tags.get(true, 3)
 
   - unless trends.empty?
     .endorsements-widget.trends-widget
diff --git a/app/views/auth/confirmations/captcha.html.haml b/app/views/auth/confirmations/captcha.html.haml
new file mode 100644
index 000000000..0fae367db
--- /dev/null
+++ b/app/views/auth/confirmations/captcha.html.haml
@@ -0,0 +1,14 @@
+- content_for :page_title do
+  = t('auth.captcha_confirmation.title')
+
+= form_tag auth_captcha_confirmation_url, method: 'POST', class: 'simple_form' do
+  = hidden_field_tag :confirmation_token, params[:confirmation_token]
+
+  .field-group
+    %p.hint= t('auth.captcha_confirmation.hint_html')
+
+  .field-group
+    = render_captcha
+
+  .actions
+    %button.button= t('challenge.continue')
diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml
index 9713bdaeb..a4323d1d9 100644
--- a/app/views/auth/sessions/new.html.haml
+++ b/app/views/auth/sessions/new.html.haml
@@ -4,24 +4,25 @@
 - content_for :header_tags do
   = render partial: 'shared/og'
 
-= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
-  .fields-group
-    - if use_seamless_external_login?
-      = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
-    - else
-      = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
-  .fields-group
-    = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false
+- unless omniauth_only?
+  = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
+    .fields-group
+      - if use_seamless_external_login?
+        = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false
+      - else
+        = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false
+    .fields-group
+      = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false
 
-  .actions
-    = f.button :button, t('auth.login'), type: :submit
+    .actions
+      = f.button :button, t('auth.login'), type: :submit
 
 - if devise_mapping.omniauthable? and resource_class.omniauth_providers.any?
   .simple_form.alternative-login
-    %h4= t('auth.or_log_in_with')
+    %h4= omniauth_only? ? t('auth.log_in_with') : t('auth.or_log_in_with')
 
     .actions
       - resource_class.omniauth_providers.each do |provider|
-        = link_to t("auth.providers.#{provider}", default: provider.to_s.chomp("_oauth2").capitalize), omniauth_authorize_path(resource_name, provider), class: "button button-#{provider}", method: :post
+        = provider_sign_in_link(provider)
 
 .form-footer= render 'auth/shared/links'
diff --git a/app/views/auth/shared/_links.html.haml b/app/views/auth/shared/_links.html.haml
index 66ed5b93f..f078e2f7e 100644
--- a/app/views/auth/shared/_links.html.haml
+++ b/app/views/auth/shared/_links.html.haml
@@ -3,7 +3,7 @@
     %li= link_to t('settings.account_settings'), edit_user_registration_path
   - else
     - if controller_name != 'sessions'
-      %li= link_to t('auth.login'), new_user_session_path
+      %li= link_to_login t('auth.login')
 
     - if controller_name != 'registrations'
       %li= link_to t('auth.register'), available_sign_up_path
diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml
index febfb7d17..d5509f946 100644
--- a/app/views/directories/index.html.haml
+++ b/app/views/directories/index.html.haml
@@ -39,10 +39,10 @@
 
         .directory__card__extra
           .accounts-table__count
-            = number_to_human account.statuses_count, strip_insignificant_zeros: true
+            = friendly_number_to_human account.statuses_count
             %small= t('accounts.posts', count: account.statuses_count).downcase
           .accounts-table__count
-            = hide_followers_count?(account) ? '-' : (number_to_human account.followers_count, strip_insignificant_zeros: true)
+            = hide_followers_count?(account) ? '-' : (friendly_number_to_human account.followers_count)
             %small= t('accounts.followers', count: account.followers_count).downcase
           .accounts-table__count
             - if account.last_status_at.present?
diff --git a/app/views/layouts/_theme.html.haml b/app/views/layouts/_theme.html.haml
index 92de64b0d..5dba77621 100644
--- a/app/views/layouts/_theme.html.haml
+++ b/app/views/layouts/_theme.html.haml
@@ -2,12 +2,13 @@
   - if theme[:pack] != 'common' && theme[:common]
     = render partial: 'layouts/theme', object: theme[:common]
   - if theme[:pack]
-    = javascript_pack_tag theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}", crossorigin: 'anonymous'
+    - pack_path = theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}"
+    = javascript_pack_tag pack_path, crossorigin: 'anonymous'
     - if theme[:skin]
       - if !theme[:flavour] || theme[:skin] == 'default'
-        = stylesheet_pack_tag theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}", media: 'all', crossorigin: 'anonymous'
+        = stylesheet_pack_tag pack_path, media: 'all', crossorigin: 'anonymous'
       - else
-        = stylesheet_pack_tag "skins/#{theme[:flavour]}/#{theme[:skin]}/#{theme[:pack]}", crossorigin: 'anonymous'
+        = stylesheet_pack_tag "skins/#{theme[:flavour]}/#{theme[:skin]}/#{theme[:pack]}", media: 'all', crossorigin: 'anonymous'
     - if theme[:preload]
       - theme[:preload].each do |link|
         %link{ href: asset_pack_path("#{link}.js"), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index 57ad5aaf1..61198171d 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -21,7 +21,7 @@
             - if user_signed_in?
               = link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'
             - else
-              = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn nav-link nav-button'
+              = link_to_login t('auth.login'), class: 'webapp-btn nav-link nav-button'
               = link_to t('auth.register'), available_sign_up_path, class: 'webapp-btn nav-link nav-button'
 
     .container= yield
diff --git a/app/views/notification_mailer/_status.html.haml b/app/views/notification_mailer/_status.html.haml
index 9b7e1b65c..31460a76e 100644
--- a/app/views/notification_mailer/_status.html.haml
+++ b/app/views/notification_mailer/_status.html.haml
@@ -37,7 +37,7 @@
                                   %p
                                     - status.media_attachments.each do |a|
                                       - if status.local?
-                                        = link_to medium_url(a), medium_url(a)
+                                        = link_to full_asset_url(a.file.url(:original)), full_asset_url(a.file.url(:original))
                                       - else
                                         = link_to a.remote_url, a.remote_url
 
diff --git a/app/views/notification_mailer/_status.text.erb b/app/views/notification_mailer/_status.text.erb
index 8999a1f8e..c43f32d9f 100644
--- a/app/views/notification_mailer/_status.text.erb
+++ b/app/views/notification_mailer/_status.text.erb
@@ -1,8 +1,8 @@
 <% if status.spoiler_text? %>
-<%= raw status.spoiler_text %>
-----
-
+> <%= raw word_wrap(status.spoiler_text, break_sequence: "\n> ") %>
+> ----
+>
 <% end %>
-<%= raw Formatter.instance.plaintext(status) %>
+> <%= raw word_wrap(Formatter.instance.plaintext(status), break_sequence: "\n> ") %>
 
 <%= raw t('application_mailer.view')%> <%= web_url("statuses/#{status.id}") %>
diff --git a/app/views/relationships/_account.html.haml b/app/views/relationships/_account.html.haml
index f521aff22..0fa3cffb5 100644
--- a/app/views/relationships/_account.html.haml
+++ b/app/views/relationships/_account.html.haml
@@ -9,10 +9,10 @@
             = interrelationships_icon(@relationships, account.id)
           %td= account_link_to account
           %td.accounts-table__count.optional
-            = number_to_human account.statuses_count, strip_insignificant_zeros: true
+            = friendly_number_to_human account.statuses_count
             %small= t('accounts.posts', count: account.statuses_count).downcase
           %td.accounts-table__count.optional
-            = number_to_human account.followers_count, strip_insignificant_zeros: true
+            = friendly_number_to_human account.followers_count
             %small= t('accounts.followers', count: account.followers_count).downcase
           %td.accounts-table__count
             - if account.last_status_at.present?
diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml
index 297379893..65de7f8f3 100644
--- a/app/views/settings/featured_tags/index.html.haml
+++ b/app/views/settings/featured_tags/index.html.haml
@@ -28,4 +28,4 @@
           - else
             %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
           = table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
-      .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
+      .trends__item__current= friendly_number_to_human featured_tag.statuses_count
diff --git a/app/views/settings/identity_proofs/_proof.html.haml b/app/views/settings/identity_proofs/_proof.html.haml
deleted file mode 100644
index 14e8e91be..000000000
--- a/app/views/settings/identity_proofs/_proof.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-%tr
-  %td
-    = link_to proof.badge.profile_url, class: 'name-tag' do
-      = image_tag proof.badge.avatar_url, width: 15, height: 15, alt: '', class: 'avatar'
-      %span.username
-        = proof.provider_username
-        %span= "(#{proof.provider.capitalize})"
-
-  %td
-    - if proof.live?
-      %span.positive-hint
-        = fa_icon 'check-circle fw'
-        = t('identity_proofs.active')
-    - else
-      %span.negative-hint
-        = fa_icon 'times-circle fw'
-        = t('identity_proofs.inactive')
-
-  %td
-    = table_link_to 'external-link', t('identity_proofs.view_proof'), proof.badge.proof_url if proof.badge.proof_url
-    = table_link_to 'trash', t('identity_proofs.remove'), settings_identity_proof_path(proof), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
diff --git a/app/views/settings/identity_proofs/index.html.haml b/app/views/settings/identity_proofs/index.html.haml
deleted file mode 100644
index d0ea03ecd..000000000
--- a/app/views/settings/identity_proofs/index.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- content_for :page_title do
-  = t('settings.identity_proofs')
-
-%p= t('identity_proofs.explanation_html')
-
-- unless @proofs.empty?
-  %hr.spacer/
-
-  .table-wrapper
-    %table.table
-      %thead
-        %tr
-          %th= t('identity_proofs.identity')
-          %th= t('identity_proofs.status')
-          %th
-      %tbody
-        = render partial: 'settings/identity_proofs/proof', collection: @proofs, as: :proof
diff --git a/app/views/settings/identity_proofs/new.html.haml b/app/views/settings/identity_proofs/new.html.haml
deleted file mode 100644
index 5e4e9895d..000000000
--- a/app/views/settings/identity_proofs/new.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-- content_for :page_title do
-  = t('identity_proofs.authorize_connection_prompt')
-
-.form-container
-  .oauth-prompt
-    %h2= t('identity_proofs.authorize_connection_prompt')
-
-  = simple_form_for @proof, url: settings_identity_proofs_url, html: { method: :post } do |f|
-    = f.input :provider, as: :hidden
-    = f.input :provider_username, as: :hidden
-    = f.input :token, as: :hidden
-
-    = hidden_field_tag :user_agent, params[:user_agent]
-
-    .connection-prompt
-      .connection-prompt__row.connection-prompt__connection
-        .connection-prompt__column
-          = image_tag current_account.avatar.url(:original), size: 96, class: 'account__avatar'
-
-          %p= t('identity_proofs.i_am_html', username: content_tag(:strong,current_account.username), service: site_hostname)
-
-        .connection-prompt__column.connection-prompt__column-sep
-          = fa_icon 'link'
-
-        .connection-prompt__column
-          = image_tag @proof.badge.avatar_url, size: 96, class: 'account__avatar'
-
-          %p= t('identity_proofs.i_am_html', username: content_tag(:strong, @proof.provider_username), service: @proof.provider.capitalize)
-
-    .connection-prompt__post
-      = f.input :post_status, label: t('identity_proofs.publicize_checkbox'), as: :boolean, wrapper: :with_label, :input_html => { checked: true }
-
-      = f.input :status_text, as: :text, input_html: { value: t('identity_proofs.publicize_toot', username: @proof.provider_username, service: @proof.provider.capitalize, url: @proof.badge.proof_url), rows: 4 }
-
-    = f.button :button, t('identity_proofs.authorize'), type: :submit
-    = link_to t('simple_form.no'), settings_identity_proofs_url, class: 'button negative'
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index fb7ce6780..21948b200 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -29,9 +29,8 @@
   .fields-group
     = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot')
 
-  - if Setting.profile_directory
-    .fields-group
-      = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable')
+  .fields-group
+    = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t(Setting.profile_directory ? 'simple_form.hints.defaults.discoverable' : 'simple_form.hints.defaults.discoverable_no_directory'), recommended: true
 
   %hr.spacer/
 
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index daf164949..cd5ed52af 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -37,10 +37,15 @@
 
   .detailed-status__meta
     %data.dt-published{ value: status.created_at.to_time.iso8601 }
+    - if status.edited?
+      %data.dt-updated{ value: status.edited_at.to_time.iso8601 }
 
     = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
       %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
     ·
+    - if status.edited?
+      = t('statuses.edited_at', date: l(status.edited_at))
+      ·
     %span.detailed-status__visibility-icon
       = visibility_icon status
     ·
@@ -55,18 +60,18 @@
         = fa_icon('reply')
       - else
         = fa_icon('reply-all')
-      %span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true
+      %span.detailed-status__reblogs>= friendly_number_to_human status.replies_count
       = " "
     ·
     - if status.public_visibility? || status.unlisted_visibility?
       = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do
         = fa_icon('retweet')
-        %span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true
+        %span.detailed-status__reblogs>= friendly_number_to_human status.reblogs_count
         = " "
       ·
     = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do
       = fa_icon('star')
-      %span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true
+      %span.detailed-status__favorites>= friendly_number_to_human status.favourites_count
       = " "
 
     - if user_signed_in?
diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml
index a7c78b997..b1e79a1cc 100644
--- a/app/views/statuses/_simple_status.html.haml
+++ b/app/views/statuses/_simple_status.html.haml
@@ -7,6 +7,9 @@
       %span.status__visibility-icon><
         = visibility_icon status
       %time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
+      - if status.edited?
+        %abbr{ title: t('statuses.edited_at', date: l(status.edited_at.to_date)) }
+          *
     %data.dt-published{ value: status.created_at.to_time.iso8601 }
 
     .p-author.h-card
diff --git a/app/views/statuses/_status.html.haml b/app/views/statuses/_status.html.haml
index 9f3197d0d..3b7152753 100644
--- a/app/views/statuses/_status.html.haml
+++ b/app/views/statuses/_status.html.haml
@@ -56,6 +56,6 @@
 
 - if include_threads && !embedded_view? && !user_signed_in?
   .entry{ class: entry_classes }
-    = link_to new_user_session_path, class: 'load-more load-gap' do
+    = link_to_login class: 'load-more load-gap' do
       = fa_icon 'comments'
       = t('statuses.sign_in_to_participate')
diff --git a/app/views/statuses_cleanup/show.html.haml b/app/views/statuses_cleanup/show.html.haml
new file mode 100644
index 000000000..59de4b5aa
--- /dev/null
+++ b/app/views/statuses_cleanup/show.html.haml
@@ -0,0 +1,45 @@
+- content_for :page_title do
+  = t('settings.statuses_cleanup')
+
+- content_for :heading_actions do
+  = button_tag t('generic.save_changes'), class: 'button', form: 'edit_policy'
+
+= simple_form_for @policy, url: statuses_cleanup_path, method: :put, html: { id: 'edit_policy' } do |f|
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :enabled, as: :boolean, wrapper: :with_label, label: t('statuses_cleanup.enabled'), hint: t('statuses_cleanup.enabled_hint')
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :min_status_age, wrapper: :with_label, label: t('statuses_cleanup.min_age_label'), collection: AccountStatusesCleanupPolicy::ALLOWED_MIN_STATUS_AGE.map(&:to_i), label_method: lambda { |i| t("statuses_cleanup.min_age.#{i}") }, include_blank: false, hint: false
+
+  .flash-message= t('statuses_cleanup.explanation')
+
+  %h4= t('statuses_cleanup.exceptions')
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :keep_pinned, wrapper: :with_label, label: t('statuses_cleanup.keep_pinned'), hint: t('statuses_cleanup.keep_pinned_hint')
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :keep_direct, wrapper: :with_label, label: t('statuses_cleanup.keep_direct'), hint: t('statuses_cleanup.keep_direct_hint')
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :keep_self_fav, wrapper: :with_label, label: t('statuses_cleanup.keep_self_fav'), hint: t('statuses_cleanup.keep_self_fav_hint')
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :keep_self_bookmark, wrapper: :with_label, label: t('statuses_cleanup.keep_self_bookmark'), hint: t('statuses_cleanup.keep_self_bookmark_hint')
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :keep_polls, wrapper: :with_label, label: t('statuses_cleanup.keep_polls'), hint: t('statuses_cleanup.keep_polls_hint')
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :keep_media, wrapper: :with_label, label: t('statuses_cleanup.keep_media'), hint: t('statuses_cleanup.keep_media_hint')
+
+  %h4= t('statuses_cleanup.interaction_exceptions')
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :min_favs, wrapper: :with_label, label: t('statuses_cleanup.min_favs'), hint: t('statuses_cleanup.min_favs_hint'), input_html: { min: 1, placeholder: t('statuses_cleanup.ignore_favs') }
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :min_reblogs, wrapper: :with_label, label: t('statuses_cleanup.min_reblogs'), hint: t('statuses_cleanup.min_reblogs_hint'), input_html: { min: 1, placeholder: t('statuses_cleanup.ignore_reblogs') }
+
+  .flash-message= t('statuses_cleanup.interaction_exceptions_explanation')
diff --git a/app/views/user_mailer/warning.html.haml b/app/views/user_mailer/warning.html.haml
index 5a2911ecb..bda1fef6c 100644
--- a/app/views/user_mailer/warning.html.haml
+++ b/app/views/user_mailer/warning.html.haml
@@ -37,16 +37,26 @@
                           %tr
                             %td.column-cell.text-center
                               - unless @warning.none_action?
-                                %p= t "user_mailer.warning.explanation.#{@warning.action}"
+                                %p= t "user_mailer.warning.explanation.#{@warning.action}", instance: @instance
 
                               - unless @warning.text.blank?
                                 = Formatter.instance.linkify(@warning.text)
 
-                              - if !@statuses.nil? && !@statuses.empty?
+                              - if @warning.report && !@warning.report.other?
+                                %p
+                                  %strong= t('user_mailer.warning.reason')
+                                  = t("user_mailer.warning.categories.#{@warning.report.category}")
+
+                                - if @warning.report.violation? && @warning.report.rule_ids.present?
+                                  %ul.rules-list
+                                    - @warning.report.rules.each do |rule|
+                                      %li= rule.text
+
+                              - unless @statuses.empty?
                                 %p
                                   %strong= t('user_mailer.warning.statuses')
 
-- if !@statuses.nil? && !@statuses.empty?
+- unless @statuses.empty?
   - @statuses.each_with_index do |status, i|
     = render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
 
diff --git a/app/views/user_mailer/warning.text.erb b/app/views/user_mailer/warning.text.erb
index bb6610c79..31d7308ae 100644
--- a/app/views/user_mailer/warning.text.erb
+++ b/app/views/user_mailer/warning.text.erb
@@ -3,11 +3,24 @@
 ===
 
 <% unless @warning.none_action? %>
-<%= t "user_mailer.warning.explanation.#{@warning.action}" %>
+<%= t "user_mailer.warning.explanation.#{@warning.action}", instance: @instance %>
 
 <% end %>
+<% if @warning.text.present? %>
 <%= @warning.text %>
-<% if !@statuses.nil? && !@statuses.empty? %>
+
+<% end %>
+<% if @warning.report && !@warning.report.other? %>
+**<%= t('user_mailer.warning.reason') %>** <%= t("user_mailer.warning.categories.#{@warning.report.category}") %>
+
+<% if @warning.report.violation? && @warning.report.rule_ids.present? %>
+<% @warning.report.rules.each do |rule| %>
+- <%= rule.text %>
+<% end %>
+
+<% end %>
+<% end %>
+<% if !@statuses.empty? %>
 <%= t('user_mailer.warning.statuses') %>
 
 <% @statuses.each do |status| %>