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/_forms.html.haml15
-rw-r--r--app/views/about/_links.html.haml16
-rw-r--r--app/views/about/_registration.html.haml2
-rw-r--r--app/views/about/more.html.haml33
-rw-r--r--app/views/about/show.html.haml192
-rw-r--r--app/views/about/terms.html.haml16
-rw-r--r--app/views/accounts/_follow_button.html.haml23
-rw-r--r--app/views/accounts/_grid_card.html.haml7
-rw-r--r--app/views/accounts/_header.html.haml19
-rw-r--r--app/views/accounts/_og.html.haml2
-rw-r--r--app/views/accounts/show.html.haml16
-rw-r--r--app/views/admin/accounts/show.html.haml5
-rw-r--r--app/views/admin/settings/edit.html.haml4
-rw-r--r--app/views/auth/confirmations/finish_signup.html.haml14
-rw-r--r--app/views/auth/passwords/edit.html.haml14
-rw-r--r--app/views/auth/registrations/_sessions.html.haml2
-rw-r--r--app/views/auth/registrations/edit.html.haml23
-rw-r--r--app/views/auth/sessions/new.html.haml14
-rw-r--r--app/views/authorize_follows/success.html.haml2
-rw-r--r--app/views/follower_accounts/index.html.haml4
-rw-r--r--app/views/following_accounts/index.html.haml4
-rwxr-xr-xapp/views/layouts/application.html.haml8
-rw-r--r--app/views/layouts/auth.html.haml2
-rw-r--r--app/views/layouts/mailer.html.haml2
-rw-r--r--app/views/layouts/modal.html.haml2
-rw-r--r--app/views/layouts/public.html.haml2
-rw-r--r--app/views/media/player.html.haml2
-rw-r--r--app/views/notification_mailer/_status.html.haml7
-rw-r--r--app/views/settings/exports/show.html.haml23
-rw-r--r--app/views/settings/preferences/show.html.haml4
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml4
-rw-r--r--app/views/stream_entries/_og_description.html.haml5
-rw-r--r--app/views/stream_entries/_og_image.html.haml29
-rw-r--r--app/views/stream_entries/_simple_status.html.haml5
-rw-r--r--app/views/stream_entries/show.html.haml2
-rw-r--r--app/views/tags/_features.html.haml25
-rw-r--r--app/views/tags/show.html.haml61
-rw-r--r--app/views/user_mailer/backup_ready.html.haml59
-rw-r--r--app/views/user_mailer/backup_ready.text.erb7
39 files changed, 447 insertions, 229 deletions
diff --git a/app/views/about/_forms.html.haml b/app/views/about/_forms.html.haml
new file mode 100644
index 000000000..81f7173f7
--- /dev/null
+++ b/app/views/about/_forms.html.haml
@@ -0,0 +1,15 @@
+- if @instance_presenter.open_registrations
+  = render 'registration'
+- else
+  = link_to t('auth.register_elsewhere'), 'https://joinmastodon.org', class: 'button button-primary'
+
+  .closed-registrations-message
+    - if @instance_presenter.closed_registrations_message.blank?
+      %p= t('about.closed_registrations')
+    - else
+      = @instance_presenter.closed_registrations_message.html_safe
+
+.separator-or
+  %span= t('auth.or')
+
+= link_to t('auth.login'), new_user_session_path, class: 'button button-alternative-2 webapp-btn'
diff --git a/app/views/about/_links.html.haml b/app/views/about/_links.html.haml
new file mode 100644
index 000000000..f79c37e65
--- /dev/null
+++ b/app/views/about/_links.html.haml
@@ -0,0 +1,16 @@
+.container-alt.links
+  .brand
+    = link_to root_url do
+      = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+
+  %ul.nav
+    %li
+      - if user_signed_in?
+        = link_to t('settings.back'), root_url, class: 'webapp-btn'
+      - else
+        = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
+    %li= link_to t('about.about_this'), about_more_path
+    %li
+      = link_to 'https://joinmastodon.org/' do
+        = "#{t('about.other_instances')}"
+        %i.fa.fa-external-link{ style: 'padding-left: 5px;' }
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
index 7a28f9738..6ca1d7129 100644
--- a/app/views/about/_registration.html.haml
+++ b/app/views/about/_registration.html.haml
@@ -10,6 +10,6 @@
   = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
 
   .actions
-    = f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative'
+    = f.button :button, t('auth.register'), type: :submit, class: 'button button-primary'
 
   %p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path)
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index d92362bd7..f86051fbf 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -7,51 +7,36 @@
 .landing-page
   .header-wrapper.compact
     .header
-      .container.links
-        .brand
-          = link_to root_url do
-            = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+      = render 'links'
 
-        %ul.nav
-          %li
-            - if user_signed_in?
-              = link_to t('settings.back'), root_url, class: 'webapp-btn'
-            - else
-              = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-          %li= link_to t('about.about_this'), about_more_path
-          %li
-            = link_to 'https://joinmastodon.org/' do
-              = "#{t('about.other_instances')}"
-              %i.fa.fa-external-link{ style: 'padding-left: 5px;' }
-
-      .container.hero
+      .container-alt.hero
         .heading
           %h3= t('about.description_headline', domain: site_hostname)
           %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
 
   .information-board
-    .container
-      .information-board-sections
-        .section
+    .container-alt
+      .information-board__sections
+        .information-board__section
           %span= t 'about.user_count_before'
           %strong= number_with_delimiter @instance_presenter.user_count
           %span= t 'about.user_count_after'
-        .section
+        .information-board__section
           %span= t 'about.status_count_before'
           %strong= number_with_delimiter @instance_presenter.status_count
           %span= t 'about.status_count_after'
-        .section
+        .information-board__section
           %span= t 'about.domain_count_before'
           %strong= number_with_delimiter @instance_presenter.domain_count
           %span= t 'about.domain_count_after'
       = render 'contact', contact: @instance_presenter
 
   .extended-description
-    .container
+    .container-alt
       = @instance_presenter.site_extended_description.html_safe.presence || t('about.extended_description_html')
 
   .footer-links
-    .container
+    .container-alt
       %p
         = link_to t('about.source_code'), @instance_presenter.source_url
         - if @instance_presenter.commit_hash == ""
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 4f5b53470..2f0b31a9f 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -2,80 +2,134 @@
   = site_hostname
 
 - content_for :header_tags do
+  %link{ rel: 'canonical', href: about_url }/
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
   = render partial: 'shared/og'
 
-.landing-page
-  .header-wrapper
-    .mascot-container
-      = image_tag asset_pack_path('elephant-fren.png'), alt: '', role: 'presentation', class: 'mascot'
-
-    .header
-      .container.links
+.landing-page.alternative
+  .container
+    .grid
+      .column-0
         .brand
           = link_to root_url do
             = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
 
-        %ul.nav
-          %li
-            - if user_signed_in?
-              = link_to t('settings.back'), root_url, class: 'webapp-btn'
-            - else
-              = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-          %li= link_to t('about.about_this'), about_more_path
-          %li
-            = link_to 'https://joinmastodon.org/' do
-              = "#{t('about.other_instances')}"
-              %i.fa.fa-external-link{ style: 'padding-left: 5px;' }
-
-      .container.hero
-        .floats
-          %div{ role: 'presentation', class: 'float-1' }
-          %div{ role: 'presentation', class: 'float-2' }
-          %div{ role: 'presentation', class: 'float-3' }
-        .heading
-          %h1
-            = @instance_presenter.site_title
-            %small= t 'about.hosted_on', domain: site_hostname
-        - if @instance_presenter.open_registrations
-          = render 'registration'
-        - else
-          .closed-registrations-message
-            %div
-              - if @instance_presenter.closed_registrations_message.blank?
-                %p= t('about.closed_registrations')
-              - else
-                = @instance_presenter.closed_registrations_message.html_safe
-
-            = simple_form_for(:user, html: { style: 'margin-left: -20px' }, url: session_path(:user)) do |f|
-              = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
-              = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
-
-              .actions
-                = f.button :button, t('auth.login'), type: :submit
-            = link_to t('about.find_another_instance'), 'https://joinmastodon.org/', class: 'button button-alternative button--block'
-
-  .about-short
-    .container
-      %h3= t('about.description_headline', domain: site_hostname)
-      %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
-
-  .features
-    .container
       - if Setting.timeline_preview
-        #mastodon-timeline{ data: { props: Oj.dump(default_props) } }
-
-      .about-mastodon
-        %h3= t 'about.what_is_mastodon'
-        %p= t 'about.about_mastodon_html'
-        = link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-secondary'
-        = render 'features'
-  .footer-links
-    .container
-      %p
-        = link_to t('about.source_code'), @instance_presenter.source_url
-        - if @instance_presenter.commit_hash == ""
-          %strong= " (#{@instance_presenter.version_number})"
-        - else
-          %strong= " (#{@instance_presenter.version_number}, "
-          %strong= " #{@instance_presenter.commit_hash})"
+        .column-1
+          .landing-page__forms
+            .brand
+              = link_to root_url do
+                = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+
+            = render 'forms'
+
+      - else
+        .column-1.non-preview
+          .landing-page__forms
+            .brand
+              = link_to root_url do
+                = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+
+            = render 'forms'
+
+      - if Setting.timeline_preview
+        .column-2
+          .landing-page__hero
+            = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
+
+          .landing-page__information
+            .landing-page__short-description
+              .row
+                .landing-page__logo
+                  = image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon'
+
+                %h1
+                  = @instance_presenter.site_title
+                  %small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname)
+
+              %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
+
+          .landing-page__call-to-action
+            .row
+              .row__information-board
+                .information-board__section
+                  %span= t 'about.user_count_before'
+                  %strong= number_with_delimiter @instance_presenter.user_count
+                  %span= t 'about.user_count_after'
+                .information-board__section
+                  %span= t 'about.status_count_before'
+                  %strong= number_with_delimiter @instance_presenter.status_count
+                  %span= t 'about.status_count_after'
+              .row__mascot
+                .landing-page__mascot
+                  = image_tag asset_pack_path('elephant_ui_plane.svg')
+
+      - else
+        .column-2.non-preview
+          .landing-page__hero
+            = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title
+
+          .landing-page__information
+            .landing-page__short-description
+              .row
+                .landing-page__logo
+                  = image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon'
+
+                %h1
+                  = @instance_presenter.site_title
+                  %small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname)
+
+              %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
+
+          .landing-page__call-to-action
+            .row
+              .row__information-board
+                .information-board__section
+                  %span= t 'about.user_count_before'
+                  %strong= number_with_delimiter @instance_presenter.user_count
+                  %span= t 'about.user_count_after'
+                .information-board__section
+                  %span= t 'about.status_count_before'
+                  %strong= number_with_delimiter @instance_presenter.status_count
+                  %span= t 'about.status_count_after'
+              .row__mascot
+                .landing-page__mascot
+                  = image_tag asset_pack_path('elephant_ui_plane.svg')
+
+      - if Setting.timeline_preview
+        .column-3
+          #mastodon-timeline{ data: { props: Oj.dump(default_props) } }
+
+      - if Setting.timeline_preview
+        .column-4.landing-page__information
+          .landing-page__features
+            .features-list
+              %div
+                %h3= t 'about.what_is_mastodon'
+                %p= t 'about.about_mastodon_html'
+
+            = render 'features'
+
+            .landing-page__features__action
+              = link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-alternative'
+
+          .landing-page__footer
+            %p
+              = link_to t('about.source_code'), @instance_presenter.source_url
+              = " (#{@instance_presenter.version_number})"
+
+      - else
+        .column-4.non-preview.landing-page__information
+          .landing-page__features
+            %h3= t 'about.what_is_mastodon'
+            %p= t 'about.about_mastodon_html'
+
+            = render 'features'
+
+            .landing-page__features__action
+              = link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-alternative'
+
+          .landing-page__footer
+            %p
+              = link_to t('about.source_code'), @instance_presenter.source_url
+              = " (#{@instance_presenter.version_number})"
diff --git a/app/views/about/terms.html.haml b/app/views/about/terms.html.haml
index 7004cb0b1..c7d36ed47 100644
--- a/app/views/about/terms.html.haml
+++ b/app/views/about/terms.html.haml
@@ -4,20 +4,8 @@
 .landing-page
   .header-wrapper.compact
     .header
-      .container.links
-        .brand
-          = link_to root_url do
-            = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
-
-        %ul.nav
-          %li
-            - if user_signed_in?
-              = link_to t('settings.back'), root_url, class: 'webapp-btn'
-            - else
-              = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-          %li= link_to t('about.about_this'), about_more_path
-          %li= link_to t('about.other_instances'), 'https://joinmastodon.org/'
+      = render 'links'
 
   .extended-description
-    .container
+    .container-alt
       = @instance_presenter.site_terms.html_safe.presence || t('terms.body_html')
diff --git a/app/views/accounts/_follow_button.html.haml b/app/views/accounts/_follow_button.html.haml
new file mode 100644
index 000000000..e476e0aff
--- /dev/null
+++ b/app/views/accounts/_follow_button.html.haml
@@ -0,0 +1,23 @@
+- relationships ||= nil
+
+- unless account.memorial? || account.moved?
+  - if user_signed_in?
+    - requested = relationships ? relationships.requested[account.id].present? : current_account.requested?(account)
+    - following = relationships ? relationships.following[account.id].present? : current_account.following?(account)
+
+  - if user_signed_in? && current_account.id != account.id && !requested
+    .controls
+      - if following
+        = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do
+          = fa_icon 'user-times'
+          = t('accounts.unfollow')
+      - else
+        = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do
+          = fa_icon 'user-plus'
+          = t('accounts.follow')
+  - elsif !user_signed_in?
+    .controls
+      .remote-follow
+        = link_to account_remote_follow_path(account), class: 'icon-button' do
+          = fa_icon 'user-plus'
+          = t('accounts.remote_follow')
diff --git a/app/views/accounts/_grid_card.html.haml b/app/views/accounts/_grid_card.html.haml
index 305eb2c44..95acbd581 100644
--- a/app/views/accounts/_grid_card.html.haml
+++ b/app/views/accounts/_grid_card.html.haml
@@ -1,9 +1,12 @@
 .account-grid-card
   .account-grid-card__header{ style: "background-image: url(#{account.header.url(:original)})" }
+    = render 'accounts/follow_button', account: account, relationships: @relationships
   .account-grid-card__avatar
     .avatar= image_tag account.avatar.url(:original)
   .name
     = link_to TagManager.instance.url_for(account) do
       %span.display_name.emojify= display_name(account)
-      %span.username @#{account.acct}
-  %p.note.emojify= truncate(strip_tags(account.note), length: 150)
+      %span.username
+        @#{account.local? ? account.local_username_and_domain : account.acct}
+        = fa_icon('lock') if account.locked?
+  .account__header__content.p-note.emojify= Formatter.instance.simplified_format(account)
diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml
index b0062752c..74251b923 100644
--- a/app/views/accounts/_header.html.haml
+++ b/app/views/accounts/_header.html.haml
@@ -1,24 +1,7 @@
 - processed_bio = FrontmatterHandler.instance.process_bio Formatter.instance.simplified_format account
 .card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" }
   .card__illustration
-    - unless account.memorial? || account.moved?
-      - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account)
-        .controls
-          - if current_account.following?(account)
-            = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do
-              = fa_icon 'user-times'
-              = t('accounts.unfollow')
-          - else
-            = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do
-              = fa_icon 'user-plus'
-              = t('accounts.follow')
-      - elsif !user_signed_in?
-        .controls
-          .remote-follow
-            = link_to account_remote_follow_path(account), class: 'icon-button' do
-              = fa_icon 'user-plus'
-              = t('accounts.remote_follow')
-
+    = render 'accounts/follow_button', account: account
     .avatar= image_tag account.avatar.url(:original), class: 'u-photo'
 
   .card__bio
diff --git a/app/views/accounts/_og.html.haml b/app/views/accounts/_og.html.haml
index 1d16be590..26424a49c 100644
--- a/app/views/accounts/_og.html.haml
+++ b/app/views/accounts/_og.html.haml
@@ -1,7 +1,7 @@
 = opengraph 'og:url', url
 = opengraph 'og:site_name', site_title
 = opengraph 'og:title', [yield(:page_title).strip.presence, site_title].compact.join(' - ')
-= opengraph 'og:description', account.note
+= opengraph 'og:description', account_description(account)
 = opengraph 'og:image', full_asset_url(account.avatar.url(:original))
 = opengraph 'og:image:width', '120'
 = opengraph 'og:image:height', '120'
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index accad5f78..c62a573b0 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -1,7 +1,9 @@
 - content_for :page_title do
-  = display_name(@account)
+  = "#{display_name(@account)} (@#{@account.username})"
 
 - content_for :header_tags do
+  %meta{ name: 'description', content: account_description(@account) }/
+
   - if @account.user&.setting_noindex
     %meta{ name: 'robots', content: 'noindex' }/
 
@@ -9,6 +11,11 @@
   %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
   %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
 
+  - if @older_url
+    %link{ rel: 'next', href: @older_url }/
+  - if @newer_url
+    %link{ rel: 'prev', href: @newer_url }/
+
   = opengraph 'og:type', 'profile'
   = render 'og', account: @account, url: short_account_url(@account, only_path: false)
 
@@ -39,6 +46,9 @@
 
       = render partial: 'stream_entries/status', collection: @statuses, as: :status
 
-  - if @statuses.size == 20
+  - if @newer_url || @older_url
     .pagination
-      = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), @next_url, class: 'next', rel: 'next'
+      - if @older_url
+        = link_to safe_join([fa_icon('chevron-left'), t('pagination.older')], ' '), @older_url, class: 'older', rel: 'next'
+      - if @newer_url
+        = link_to safe_join([t('pagination.newer'), fa_icon('chevron-right')], ' '), @newer_url, class: 'newer', rel: 'prev'
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 5f5d0995c..dbbf5fc09 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -18,7 +18,10 @@
         %tr
           %th= t('admin.accounts.role')
           %td
-            = t("admin.accounts.roles.#{@account.user&.role}")
+            - if @account.user.nil?
+              = t("admin.accounts.moderation.suspended")
+            - else
+              = t("admin.accounts.roles.#{@account.user&.role}")
             = table_link_to 'angle-double-up', t('admin.accounts.promote'), promote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:promote, @account.user)
             = table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user)
         %tr
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 4f9115ed2..08d05d738 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -12,6 +12,7 @@
 
   .fields-group
     = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html')
+    = f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: t('admin.settings.hero.desc_html')
 
   %hr/
 
@@ -19,6 +20,9 @@
     = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
 
   .fields-group
+    = f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html')
+
+  .fields-group
     = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html')
 
   .fields-group
diff --git a/app/views/auth/confirmations/finish_signup.html.haml b/app/views/auth/confirmations/finish_signup.html.haml
new file mode 100644
index 000000000..4b5161d6b
--- /dev/null
+++ b/app/views/auth/confirmations/finish_signup.html.haml
@@ -0,0 +1,14 @@
+- content_for :page_title do
+  = t('auth.confirm_email')
+
+= simple_form_for(current_user, as: 'user', url: finish_signup_path, html: { role: 'form'}) do |f|
+  - if @show_errors && current_user.errors.any?
+    #error_explanation
+      - current_user.errors.full_messages.each do |msg|
+        = msg
+        %br
+
+  = f.input :email
+
+  .actions
+    = f.submit t('auth.confirm_email'), class: 'button'
diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml
index 5ef3de976..53d1769d6 100644
--- a/app/views/auth/passwords/edit.html.haml
+++ b/app/views/auth/passwords/edit.html.haml
@@ -3,12 +3,16 @@
 
 = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f|
   = render 'shared/error_messages', object: resource
-  = f.input :reset_password_token, as: :hidden
 
-  = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
-  = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
+  - if !use_seamless_external_login? || resource.encrypted_password.present?
+    = f.input :reset_password_token, as: :hidden
 
-  .actions
-    = f.button :button, t('auth.set_new_password'), type: :submit
+    = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
+    = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
+
+    .actions
+      = f.button :button, t('auth.set_new_password'), type: :submit
+  - else
+    %p.hint= t('users.seamless_external_login')
 
 .form-footer= render 'auth/shared/links'
diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml
index 8424a8901..8586c0549 100644
--- a/app/views/auth/registrations/_sessions.html.haml
+++ b/app/views/auth/registrations/_sessions.html.haml
@@ -1,4 +1,4 @@
-%h6= t 'sessions.title'
+%h4= t 'sessions.title'
 %p.muted-hint= t 'sessions.explanation'
 
 .table-wrapper
diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml
index 145f5cd9e..05fc7df31 100644
--- a/app/views/auth/registrations/edit.html.haml
+++ b/app/views/auth/registrations/edit.html.haml
@@ -1,23 +1,24 @@
 - content_for :page_title do
-  = t('auth.change_password')
+  = t('auth.security')
 
+%h4= t('auth.change_password')
 = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f|
   = render 'shared/error_messages', object: resource
 
-  = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
-  = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
-  = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
-  = f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }
+  - if !use_seamless_external_login? || resource.encrypted_password.present?
+    = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
+    = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
+    = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
+    = f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }
 
-  .actions
-    = f.button :button, t('generic.save_changes'), type: :submit
-
-%hr/
+    .actions
+      = f.button :button, t('generic.save_changes'), type: :submit
+  - else
+    %p.hint= t('users.seamless_external_login')
 
 = render 'sessions'
 
 - if open_deletion?
-  %hr/
 
-  %h6= t('auth.delete_account')
+  %h4= t('auth.delete_account')
   %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path)
diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml
index a52b0053b..0c9f9d5fe 100644
--- a/app/views/auth/sessions/new.html.haml
+++ b/app/views/auth/sessions/new.html.haml
@@ -5,10 +5,22 @@
   = render partial: 'shared/og'
 
 = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
-  = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
+  - if use_seamless_external_login?
+    = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.username_or_email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }
+  - else
+    = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
   = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }
 
   .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')
+
+    .actions
+      - resource_class.omniauth_providers.each do |provider|
+        = link_to omniauth_authorize_path(resource_name, provider), class: "button button-#{provider}" do
+          = t("auth.providers.#{provider}", default: provider.to_s.chomp("_oauth2").capitalize)
+
 .form-footer= render 'auth/shared/links'
diff --git a/app/views/authorize_follows/success.html.haml b/app/views/authorize_follows/success.html.haml
index f0b495689..63ff3bcf1 100644
--- a/app/views/authorize_follows/success.html.haml
+++ b/app/views/authorize_follows/success.html.haml
@@ -12,5 +12,5 @@
 
   .post-follow-actions
     %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block'
-    %div= link_to t('authorize_follow.post_follow.return'), @account.url, class: 'button button--block'
+    %div= link_to t('authorize_follow.post_follow.return'), TagManager.instance.url_for(@account), class: 'button button--block'
     %div= t('authorize_follow.post_follow.close')
diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml
index 738b31638..a24e4ea20 100644
--- a/app/views/follower_accounts/index.html.haml
+++ b/app/views/follower_accounts/index.html.haml
@@ -2,9 +2,7 @@
   = t('accounts.people_who_follow', name: display_name(@account))
 
 - content_for :header_tags do
-  - if @account.user&.setting_noindex
-    %meta{ name: 'robots', content: 'noindex' }/
-
+  %meta{ name: 'robots', content: 'noindex' }/
   = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
 
 = render 'accounts/header', account: @account
diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml
index 9637c689f..67f6cfede 100644
--- a/app/views/following_accounts/index.html.haml
+++ b/app/views/following_accounts/index.html.haml
@@ -2,9 +2,7 @@
   = t('accounts.people_followed_by', name: display_name(@account))
 
 - content_for :header_tags do
-  - if @account.user&.setting_noindex
-    %meta{ name: 'robots', content: 'noindex' }/
-
+  %meta{ name: 'robots', content: 'noindex' }/
   = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false)
 
 = render 'accounts/header', account: @account
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 322d7403e..9ede598b3 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -2,7 +2,7 @@
 %html{ lang: I18n.locale }
   %head
     %meta{ charset: 'utf-8' }/
-    %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }/   
+    %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }/
     %link{ rel: 'icon', href: favicon_path, type: 'image/x-icon' }/
     %link{ rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }/
     %link{ rel: 'mask-icon', href: '/mask-icon.svg', color: '#2B90D9' }/
@@ -11,11 +11,7 @@
     %meta{ name: 'theme-color', content: '#282c37' }/
     %meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/
 
-    %title<
-      - if content_for?(:page_title)
-        = yield(:page_title)
-        = ' - '
-      = title
+    %title= content_for?(:page_title) ? safe_join([yield(:page_title).chomp.html_safe, title], ' - ') : title
 
     = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
     - if @theme
diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml
index f4812ac6a..ca9c13945 100644
--- a/app/views/layouts/auth.html.haml
+++ b/app/views/layouts/auth.html.haml
@@ -1,5 +1,5 @@
 - content_for :content do
-  .container
+  .container-alt
     .logo-container
       %h1
         = link_to root_path do
diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml
index ad15754d5..6321fec61 100644
--- a/app/views/layouts/mailer.html.haml
+++ b/app/views/layouts/mailer.html.haml
@@ -7,7 +7,7 @@
     %title/
 
     = stylesheet_pack_tag 'core/mailer'
-  %body
+  %body{ dir: locale_direction }
     %table.email-table{ cellspacing: 0, cellpadding: 0 }
       %tbody
         %tr
diff --git a/app/views/layouts/modal.html.haml b/app/views/layouts/modal.html.haml
index a5d79f5c0..e808593cd 100644
--- a/app/views/layouts/modal.html.haml
+++ b/app/views/layouts/modal.html.haml
@@ -8,7 +8,7 @@
       = link_to destroy_user_session_path, method: :delete, class: 'logout-link icon-button' do
         = fa_icon 'sign-out'
 
-  .container= yield
+  .container-alt= yield
   .modal-layout__mastodon
     %div
 
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index b3795eaad..07441a77d 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -1,5 +1,5 @@
 - content_for :content do
-  .container= yield
+  .container-alt= yield
   .footer
     - if !user_signed_in? && single_user_mode?
       %span.single-user-login
diff --git a/app/views/media/player.html.haml b/app/views/media/player.html.haml
new file mode 100644
index 000000000..ea868b3f6
--- /dev/null
+++ b/app/views/media/player.html.haml
@@ -0,0 +1,2 @@
+%video{ poster: @media_attachment.file.url(:small), preload: 'auto', autoplay: 'autoplay', muted: 'muted', loop: 'loop', controls: 'controls', style: "width: #{@media_attachment.file.meta.dig('original', 'width')}px; height: #{@media_attachment.file.meta.dig('original', 'height')}px" }
+  %source{ src: @media_attachment.file.url(:original), type: @media_attachment.file_content_type }
diff --git a/app/views/notification_mailer/_status.html.haml b/app/views/notification_mailer/_status.html.haml
index 85f9294e9..f82ada146 100644
--- a/app/views/notification_mailer/_status.html.haml
+++ b/app/views/notification_mailer/_status.html.haml
@@ -1,6 +1,6 @@
 - i ||= 0
 
-%table.email-table{ cellspacing: 0, cellpadding: 0 }
+%table.email-table{ cellspacing: 0, cellpadding: 0, dir: 'ltr' }
   %tbody
     %tr
       %td.email-body
@@ -19,12 +19,13 @@
                                 %tbody
                                   %tr
                                     %td{ align: 'left', width: 48 }
-                                      = image_tag full_asset_url(status.account.avatar), alt:''
+                                      = image_tag full_asset_url(status.account.avatar.url), alt:''
                                     %td{ align: 'left' }
                                       %bdi= display_name(status.account)
                                       = "@#{status.account.acct}"
 
-                              = Formatter.instance.format(status)
+                              %div{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
+                                = Formatter.instance.format(status)
 
                               %p.status-footer
                                 = link_to l(status.created_at), web_url("statuses/#{status.id}")
diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml
index e0df1c480..89d768d3f 100644
--- a/app/views/settings/exports/show.html.haml
+++ b/app/views/settings/exports/show.html.haml
@@ -20,3 +20,26 @@
         %th= t('exports.mutes')
         %td= @export.total_mutes
         %td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv)
+
+%p.muted-hint= t('exports.archive_takeout.hint_html')
+
+- if policy(:backup).create?
+  %p= link_to t('exports.archive_takeout.request'), settings_export_path, class: 'button', method: :post
+
+- unless @backups.empty?
+  .table-wrapper
+    %table.table
+      %thead
+        %tr
+          %th= t('exports.archive_takeout.date')
+          %th= t('exports.archive_takeout.size')
+          %th
+      %tbody
+        - @backups.each do |backup|
+          %tr
+            %td= l backup.created_at
+            - if backup.processed?
+              %td= number_to_human_size backup.dump_file_size
+              %td= table_link_to 'download', t('exports.archive_takeout.download'), backup.dump.url
+            - else
+              %td{ colspan: 2 }= t('exports.archive_takeout.in_progress')
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index d1459d93c..102e4d200 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -4,6 +4,9 @@
 = simple_form_for current_user, url: settings_preferences_path, html: { method: :put } do |f|
   = render 'shared/error_messages', object: current_user
 
+  .actions.actions--top
+    = f.button :button, t('generic.save_changes'), type: :submit
+
   %h4= t 'preferences.languages'
 
   .fields-group
@@ -33,6 +36,7 @@
 
   .fields-group
     = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label
+    = f.input :setting_display_sensitive_media, as: :boolean, wrapper: :with_label
     = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
     = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
 
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index d88ec8280..e1122d5a2 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -22,9 +22,9 @@
   - if !status.media_attachments.empty?
     - if status.media_attachments.first.video?
       - video = status.media_attachments.first
-      %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380, detailed: true) }}<
+      %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 670, height: 380, detailed: true, inline: true) }}
     - else
-      %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}<
+      %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}
   - elsif status.preview_cards.first
     %div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}
 
diff --git a/app/views/stream_entries/_og_description.html.haml b/app/views/stream_entries/_og_description.html.haml
index d2fa99e63..9c24e0a61 100644
--- a/app/views/stream_entries/_og_description.html.haml
+++ b/app/views/stream_entries/_og_description.html.haml
@@ -1,4 +1 @@
-- if activity.is_a?(Status) && activity.spoiler_text?
-  = opengraph 'og:description', activity.spoiler_text
-- else
-  = opengraph 'og:description', activity.content
+= opengraph 'og:description', [activity.spoiler_text, activity.text].reject(&:blank?).join("\n\n")
diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/stream_entries/_og_image.html.haml
index 1056c1744..526034faa 100644
--- a/app/views/stream_entries/_og_image.html.haml
+++ b/app/views/stream_entries/_og_image.html.haml
@@ -1,23 +1,34 @@
-- if activity.is_a?(Status) && activity.non_sensitive_with_media?
+- if activity.is_a?(Status) && activity.media_attachments.any?
+  - player_card = false
   - activity.media_attachments.each do |media|
     - if media.image?
       = opengraph 'og:image', full_asset_url(media.file.url(:original))
       = opengraph 'og:image:type', media.file_content_type
       - unless media.file.meta.nil?
-        = opengraph 'og:image:width', media.file.meta['original']['width']
-        = opengraph 'og:image:height', media.file.meta['original']['height']
-    - elsif media.video?
+        = opengraph 'og:image:width', media.file.meta.dig('original', 'width')
+        = opengraph 'og:image:height', media.file.meta.dig('original', 'height')
+    - elsif media.video? || media.gifv?
+      - player_card = true
       = opengraph 'og:image', full_asset_url(media.file.url(:small))
       = opengraph 'og:image:type', 'image/png'
       - unless media.file.meta.nil?
-        = opengraph 'og:image:width', media.file.meta['small']['width']
-        = opengraph 'og:image:height', media.file.meta['small']['height']
+        = opengraph 'og:image:width', media.file.meta.dig('small', 'width')
+        = opengraph 'og:image:height', media.file.meta.dig('small', 'height')
       = opengraph 'og:video', full_asset_url(media.file.url(:original))
+      = opengraph 'og:video:secure_url', full_asset_url(media.file.url(:original))
       = opengraph 'og:video:type', media.file_content_type
+      = opengraph 'twitter:player', medium_player_url(media)
+      = opengraph 'twitter:player:stream', full_asset_url(media.file.url(:original))
+      = opengraph 'twitter:player:stream:content_type', media.file_content_type
       - unless media.file.meta.nil?
-        = opengraph 'og:video:width', media.file.meta['small']['width']
-        = opengraph 'og:video:height', media.file.meta['small']['height']
-  = opengraph 'twitter:card', 'summary_large_image'
+        = opengraph 'og:video:width', media.file.meta.dig('original', 'width')
+        = opengraph 'og:video:height', media.file.meta.dig('original', 'height')
+        = opengraph 'twitter:player:width', media.file.meta.dig('original', 'width')
+        = opengraph 'twitter:player:height', media.file.meta.dig('original', 'height')
+  - if player_card
+    = opengraph 'twitter:card', 'player'
+  - else
+    = opengraph 'twitter:card', 'summary_large_image'
 - else
   = opengraph 'og:image', full_asset_url(account.avatar.url(:original))
   = opengraph 'og:image:width', '120'
diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml
index 0b45ff308..2ad1f5120 100644
--- a/app/views/stream_entries/_simple_status.html.haml
+++ b/app/views/stream_entries/_simple_status.html.haml
@@ -20,10 +20,9 @@
         %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
     .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
       = Formatter.instance.format(status, custom_emojify: true)
-
       - unless status.media_attachments.empty?
         - if status.media_attachments.first.video?
           - video = status.media_attachments.first
-          %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 610, height: 343) }}><
+          %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343) }}
         - else
-          %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 343, sensitive: status.sensitive?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}><
+          %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 343, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index cf6671e67..a87c51952 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -1,5 +1,5 @@
 - content_for :page_title do
-  = t('statuses.title', name: display_name(@account), quote: truncate(@stream_entry.activity.spoiler_text.presence || @stream_entry.activity.text, length: 50, omission: '…'))
+  = t('statuses.title', name: display_name(@account), quote: truncate(@stream_entry.activity.spoiler_text.presence || @stream_entry.activity.text, length: 50, omission: '…', escape: false))
 
 - content_for :header_tags do
   - if @account.user&.setting_noindex
diff --git a/app/views/tags/_features.html.haml b/app/views/tags/_features.html.haml
new file mode 100644
index 000000000..8fbc6b760
--- /dev/null
+++ b/app/views/tags/_features.html.haml
@@ -0,0 +1,25 @@
+.features-list
+  .features-list__row
+    .text
+      %h6= t 'about.features.real_conversation_title'
+      = t 'about.features.real_conversation_body'
+    .visual
+      = fa_icon 'fw comments'
+  .features-list__row
+    .text
+      %h6= t 'about.features.not_a_product_title'
+      = t 'about.features.not_a_product_body'
+    .visual
+      = fa_icon 'fw users'
+  .features-list__row
+    .text
+      %h6= t 'about.features.within_reach_title'
+      = t 'about.features.within_reach_body'
+    .visual
+      = fa_icon 'fw mobile'
+  .features-list__row
+    .text
+      %h6= t 'about.features.humane_approach_title'
+      = t 'about.features.humane_approach_body'
+    .visual
+      = fa_icon 'fw leaf'
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 03f19e20a..000aa0c4d 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -5,48 +5,31 @@
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
   = render 'og'
 
-.landing-page.tag-page
+.landing-page.tag-page.alternative
   .features
     .container
-      #mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } }
+      .grid
+        .column-1
+          #mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } }
 
-      .about-mastodon
-        .about-hashtag
-          .brand
-            = link_to root_url do
-              = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+        .column-2
+          .about-mastodon
+            .about-hashtag.landing-page__information
+              .brand
+                = link_to root_url do
+                  = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
 
-          %p= t 'about.about_hashtag_html', hashtag: @tag.name
+              %p= t 'about.about_hashtag_html', hashtag: @tag.name
 
-          .cta
-            - if user_signed_in?
-              = link_to t('settings.back'), root_path, class: 'button button-secondary'
-            - else
-              = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary'
-            = link_to t('about.learn_more'), about_path, class: 'button button-alternative'
+              .cta
+                - if user_signed_in?
+                  = link_to t('settings.back'), root_path, class: 'button button-secondary'
+                - else
+                  = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary'
+                = link_to t('about.learn_more'), about_path, class: 'button button-alternative'
 
-        .features-list
-          .features-list__row
-            .text
-              %h6= t 'about.features.real_conversation_title'
-              = t 'about.features.real_conversation_body'
-            .visual
-              = fa_icon 'fw comments'
-          .features-list__row
-            .text
-              %h6= t 'about.features.not_a_product_title'
-              = t 'about.features.not_a_product_body'
-            .visual
-              = fa_icon 'fw users'
-          .features-list__row
-            .text
-              %h6= t 'about.features.within_reach_title'
-              = t 'about.features.within_reach_body'
-            .visual
-              = fa_icon 'fw mobile'
-          .features-list__row
-            .text
-              %h6= t 'about.features.humane_approach_title'
-              = t 'about.features.humane_approach_body'
-            .visual
-              = fa_icon 'fw leaf'
+            .landing-page__features.landing-page__information
+              %h3= t 'about.what_is_mastodon'
+              %p= t 'about.about_mastodon_html'
+
+              = render 'features'
diff --git a/app/views/user_mailer/backup_ready.html.haml b/app/views/user_mailer/backup_ready.html.haml
new file mode 100644
index 000000000..d5a4b8b48
--- /dev/null
+++ b/app/views/user_mailer/backup_ready.html.haml
@@ -0,0 +1,59 @@
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.hero
+                  .email-row
+                    .col-6
+                      %table.column{ cellspacing: 0, cellpadding: 0 }
+                        %tbody
+                          %tr
+                            %td.column-cell.text-center.padded
+                              %table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                                %tbody
+                                  %tr
+                                    %td
+                                      = image_tag full_pack_url('icon_file_download.png'), alt: ''
+
+                              %h1= t 'user_mailer.backup_ready.title'
+
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell.content-start
+                  .email-row
+                    .col-6
+                      %table.column{ cellspacing: 0, cellpadding: 0 }
+                        %tbody
+                          %tr
+                            %td.column-cell.text-center
+                              %p= t 'user_mailer.backup_ready.explanation'
+
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+  %tbody
+    %tr
+      %td.email-body
+        .email-container
+          %table.content-section{ cellspacing: 0, cellpadding: 0 }
+            %tbody
+              %tr
+                %td.content-cell
+                  %table.column{ cellspacing: 0, cellpadding: 0 }
+                    %tbody
+                      %tr
+                        %td.column-cell.button-cell
+                          %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
+                            %tbody
+                              %tr
+                                %td.button-primary
+                                  = link_to full_asset_url(@backup.dump.url) do
+                                    %span= t 'exports.archive_takeout.download'
diff --git a/app/views/user_mailer/backup_ready.text.erb b/app/views/user_mailer/backup_ready.text.erb
new file mode 100644
index 000000000..eb89e7d74
--- /dev/null
+++ b/app/views/user_mailer/backup_ready.text.erb
@@ -0,0 +1,7 @@
+<%= t 'user_mailer.backup_ready.title' %>
+
+===
+
+<%= t 'user_mailer.backup_ready.explanation' %>
+
+=> <%= full_asset_url(@backup.dump.url) %>