about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2022-10-22 11:44:41 +0200
committerGitHub <noreply@github.com>2022-10-22 11:44:41 +0200
commit7c152acb2cc545a87610de349a94e14f45fbed5d (patch)
tree1f698c5ffb954b000cb0779de5a2bf25884779c0 /app
parentabf6c87ee8b57e09dca5f5b1fe1839a314e1aa46 (diff)
Change settings area to be separated into categories in admin UI (#19407)
And update all descriptions
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/settings/about_controller.rb9
-rw-r--r--app/controllers/admin/settings/appearance_controller.rb9
-rw-r--r--app/controllers/admin/settings/branding_controller.rb9
-rw-r--r--app/controllers/admin/settings/content_retention_controller.rb9
-rw-r--r--app/controllers/admin/settings/discovery_controller.rb9
-rw-r--r--app/controllers/admin/settings/registrations_controller.rb9
-rw-r--r--app/controllers/admin/settings_controller.rb10
-rw-r--r--app/controllers/admin/site_uploads_controller.rb2
-rw-r--r--app/helpers/admin/settings_helper.rb7
-rw-r--r--app/javascript/styles/mastodon/admin.scss82
-rw-r--r--app/javascript/styles/mastodon/components.scss5
-rw-r--r--app/javascript/styles/mastodon/forms.scss16
-rw-r--r--app/models/form/admin_settings.rb57
-rw-r--r--app/serializers/rest/extended_description_serializer.rb12
-rw-r--r--app/views/admin/settings/about/show.html.haml33
-rw-r--r--app/views/admin/settings/appearance/show.html.haml34
-rw-r--r--app/views/admin/settings/branding/show.html.haml39
-rw-r--r--app/views/admin/settings/content_retention/show.html.haml22
-rw-r--r--app/views/admin/settings/discovery/show.html.haml40
-rw-r--r--app/views/admin/settings/edit.html.haml102
-rw-r--r--app/views/admin/settings/registrations/show.html.haml27
-rw-r--r--app/views/admin/settings/shared/_links.html.haml8
-rw-r--r--app/views/layouts/admin.html.haml11
23 files changed, 401 insertions, 160 deletions
diff --git a/app/controllers/admin/settings/about_controller.rb b/app/controllers/admin/settings/about_controller.rb
new file mode 100644
index 000000000..86327fe39
--- /dev/null
+++ b/app/controllers/admin/settings/about_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::AboutController < Admin::SettingsController
+  private
+
+  def after_update_redirect_path
+    admin_settings_about_path
+  end
+end
diff --git a/app/controllers/admin/settings/appearance_controller.rb b/app/controllers/admin/settings/appearance_controller.rb
new file mode 100644
index 000000000..39b2448d8
--- /dev/null
+++ b/app/controllers/admin/settings/appearance_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::AppearanceController < Admin::SettingsController
+  private
+
+  def after_update_redirect_path
+    admin_settings_appearance_path
+  end
+end
diff --git a/app/controllers/admin/settings/branding_controller.rb b/app/controllers/admin/settings/branding_controller.rb
new file mode 100644
index 000000000..4a4d76f49
--- /dev/null
+++ b/app/controllers/admin/settings/branding_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::BrandingController < Admin::SettingsController
+  private
+
+  def after_update_redirect_path
+    admin_settings_branding_path
+  end
+end
diff --git a/app/controllers/admin/settings/content_retention_controller.rb b/app/controllers/admin/settings/content_retention_controller.rb
new file mode 100644
index 000000000..b88336a2c
--- /dev/null
+++ b/app/controllers/admin/settings/content_retention_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::ContentRetentionController < Admin::SettingsController
+  private
+
+  def after_update_redirect_path
+    admin_settings_content_retention_path
+  end
+end
diff --git a/app/controllers/admin/settings/discovery_controller.rb b/app/controllers/admin/settings/discovery_controller.rb
new file mode 100644
index 000000000..be4b57f79
--- /dev/null
+++ b/app/controllers/admin/settings/discovery_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::DiscoveryController < Admin::SettingsController
+  private
+
+  def after_update_redirect_path
+    admin_settings_discovery_path
+  end
+end
diff --git a/app/controllers/admin/settings/registrations_controller.rb b/app/controllers/admin/settings/registrations_controller.rb
new file mode 100644
index 000000000..b4a74349c
--- /dev/null
+++ b/app/controllers/admin/settings/registrations_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Admin::Settings::RegistrationsController < Admin::SettingsController
+  private
+
+  def after_update_redirect_path
+    admin_settings_registrations_path
+  end
+end
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index dc1c79b7f..338a3638c 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -2,7 +2,7 @@
 
 module Admin
   class SettingsController < BaseController
-    def edit
+    def show
       authorize :settings, :show?
 
       @admin_settings = Form::AdminSettings.new
@@ -15,14 +15,18 @@ module Admin
 
       if @admin_settings.save
         flash[:notice] = I18n.t('generic.changes_saved_msg')
-        redirect_to edit_admin_settings_path
+        redirect_to after_update_redirect_path
       else
-        render :edit
+        render :show
       end
     end
 
     private
 
+    def after_update_redirect_path
+      raise NotImplementedError
+    end
+
     def settings_params
       params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS)
     end
diff --git a/app/controllers/admin/site_uploads_controller.rb b/app/controllers/admin/site_uploads_controller.rb
index cacecedb0..a5d2cf41c 100644
--- a/app/controllers/admin/site_uploads_controller.rb
+++ b/app/controllers/admin/site_uploads_controller.rb
@@ -9,7 +9,7 @@ module Admin
 
       @site_upload.destroy!
 
-      redirect_to edit_admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
+      redirect_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
     end
 
     private
diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb
index baf14ab25..a133b4e7d 100644
--- a/app/helpers/admin/settings_helper.rb
+++ b/app/helpers/admin/settings_helper.rb
@@ -1,11 +1,4 @@
 # frozen_string_literal: true
 
 module Admin::SettingsHelper
-  def site_upload_delete_hint(hint, var)
-    upload = SiteUpload.find_by(var: var.to_s)
-    return hint unless upload
-
-    link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete }
-    safe_join([hint, link], '<br/>'.html_safe)
-  end
 end
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 1c5494cde..affe1c79c 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -188,21 +188,70 @@ $content-width: 840px;
       padding-top: 30px;
     }
 
-    &-heading {
-      display: flex;
+    &__heading {
       padding-bottom: 36px;
       border-bottom: 1px solid lighten($ui-base-color, 8%);
-      margin: -15px -15px 40px 0;
-      flex-wrap: wrap;
-      align-items: center;
-      justify-content: space-between;
+      margin-bottom: 40px;
 
-      & > * {
-        margin-top: 15px;
-        margin-right: 15px;
+      &__row {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: center;
+        justify-content: space-between;
+        margin: -15px -15px 0 0;
+
+        & > * {
+          margin-top: 15px;
+          margin-right: 15px;
+        }
       }
 
-      &-actions {
+      &__tabs {
+        margin-top: 30px;
+        margin-bottom: -31px;
+
+        & > div {
+          display: flex;
+          gap: 10px;
+        }
+
+        a {
+          font-size: 14px;
+          display: inline-flex;
+          align-items: center;
+          padding: 7px 15px;
+          border-radius: 4px;
+          color: $darker-text-color;
+          text-decoration: none;
+          position: relative;
+          font-weight: 500;
+          gap: 5px;
+          white-space: nowrap;
+
+          &.selected {
+            font-weight: 700;
+            color: $primary-text-color;
+
+            &::after {
+              content: "";
+              display: block;
+              width: 100%;
+              border-bottom: 1px solid $ui-highlight-color;
+              position: absolute;
+              bottom: -5px;
+              left: 0;
+            }
+          }
+
+          &:hover,
+          &:focus,
+          &:active {
+            background: lighten($ui-base-color, 4%);
+          }
+        }
+      }
+
+      &__actions {
         display: inline-flex;
 
         & > :not(:first-child) {
@@ -228,11 +277,7 @@ $content-width: 840px;
       color: $secondary-text-color;
       font-size: 24px;
       line-height: 36px;
-      font-weight: 400;
-
-      @media screen and (max-width: $no-columns-breakpoint) {
-        font-weight: 700;
-      }
+      font-weight: 700;
     }
 
     h3 {
@@ -437,6 +482,11 @@ body,
       }
     }
 
+    & > div {
+      display: flex;
+      gap: 5px;
+    }
+
     strong {
       font-weight: 500;
       text-transform: uppercase;
@@ -1143,7 +1193,7 @@ a.name-tag,
 
     path:first-child {
       fill: rgba($highlight-text-color, 0.25) !important;
-      fill-opacity: 1 !important;
+      fill-opacity: 100% !important;
     }
 
     path:last-child {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 587eba663..5d0ff8536 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -29,6 +29,11 @@
   background: transparent;
   padding: 0;
   cursor: pointer;
+  text-decoration: none;
+
+  &--destructive {
+    color: $error-value-color;
+  }
 
   &:hover,
   &:active {
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 69a0b22d6..25c9c9fe5 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -254,7 +254,7 @@ code {
 
     & > label {
       font-family: inherit;
-      font-size: 16px;
+      font-size: 14px;
       color: $primary-text-color;
       display: block;
       font-weight: 500;
@@ -291,6 +291,20 @@ code {
     .input:last-child {
       margin-bottom: 0;
     }
+
+    &__thumbnail {
+      display: block;
+      margin: 0;
+      margin-bottom: 10px;
+      max-width: 100%;
+      height: auto;
+      border-radius: 4px;
+      background: url("images/void.png");
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
   }
 
   .fields-row {
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index b6bb3d795..957a32b7c 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -8,7 +8,6 @@ class Form::AdminSettings
     site_contact_email
     site_title
     site_short_description
-    site_description
     site_extended_description
     site_terms
     registrations_mode
@@ -53,45 +52,55 @@ class Form::AdminSettings
 
   attr_accessor(*KEYS)
 
-  validates :site_short_description, :site_description, html: { wrap_with: :p }
-  validates :site_extended_description, :site_terms, :closed_registrations_message, html: true
-  validates :registrations_mode, inclusion: { in: %w(open approved none) }
-  validates :site_contact_email, :site_contact_username, presence: true
-  validates :site_contact_username, existing_username: true
-  validates :bootstrap_timeline_accounts, existing_username: { multiple: true }
-  validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }
-  validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }
-  validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true
+  validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) }
+  validates :site_contact_email, :site_contact_username, presence: true, if: -> { defined?(@site_contact_username) || defined?(@site_contact_email) }
+  validates :site_contact_username, existing_username: true, if: -> { defined?(@site_contact_username) }
+  validates :bootstrap_timeline_accounts, existing_username: { multiple: true }, if: -> { defined?(@bootstrap_timeline_accounts) }
+  validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) }
+  validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) }
+  validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
+  validates :site_short_description, length: { maximum: 200 }, if: -> { defined?(@site_short_description) }
 
-  def initialize(_attributes = {})
-    super
-    initialize_attributes
+  KEYS.each do |key|
+    define_method(key) do
+      return instance_variable_get("@#{key}") if instance_variable_defined?("@#{key}")
+
+      stored_value = begin
+        if UPLOAD_KEYS.include?(key)
+          SiteUpload.where(var: key).first_or_initialize(var: key)
+        else
+          Setting.public_send(key)
+        end
+      end
+
+      instance_variable_set("@#{key}", stored_value)
+    end
+  end
+
+  UPLOAD_KEYS.each do |key|
+    define_method("#{key}=") do |file|
+      value = public_send(key)
+      value.file = file
+    end
   end
 
   def save
     return false unless valid?
 
     KEYS.each do |key|
-      value = instance_variable_get("@#{key}")
+      next unless instance_variable_defined?("@#{key}")
 
-      if UPLOAD_KEYS.include?(key) && !value.nil?
-        upload = SiteUpload.where(var: key).first_or_initialize(var: key)
-        upload.update(file: value)
+      if UPLOAD_KEYS.include?(key)
+        public_send(key).save
       else
         setting = Setting.where(var: key).first_or_initialize(var: key)
-        setting.update(value: typecast_value(key, value))
+        setting.update(value: typecast_value(key, instance_variable_get("@#{key}")))
       end
     end
   end
 
   private
 
-  def initialize_attributes
-    KEYS.each do |key|
-      instance_variable_set("@#{key}", Setting.public_send(key)) if instance_variable_get("@#{key}").nil?
-    end
-  end
-
   def typecast_value(key, value)
     if BOOLEAN_KEYS.include?(key)
       value == '1'
diff --git a/app/serializers/rest/extended_description_serializer.rb b/app/serializers/rest/extended_description_serializer.rb
index 0c3649033..c0fa3450d 100644
--- a/app/serializers/rest/extended_description_serializer.rb
+++ b/app/serializers/rest/extended_description_serializer.rb
@@ -8,6 +8,16 @@ class REST::ExtendedDescriptionSerializer < ActiveModel::Serializer
   end
 
   def content
-    object.text
+    if object.text.present?
+      markdown.render(object.text)
+    else
+      ''
+    end
+  end
+
+  private
+
+  def markdown
+    @markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML)
   end
 end
diff --git a/app/views/admin/settings/about/show.html.haml b/app/views/admin/settings/about/show.html.haml
new file mode 100644
index 000000000..366d213f6
--- /dev/null
+++ b/app/views/admin/settings/about/show.html.haml
@@ -0,0 +1,33 @@
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+  = t('admin.settings.about.title')
+
+- content_for :heading do
+  %h2= t('admin.settings.title')
+  = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_about_path, html: { method: :patch } do |f|
+  = render 'shared/error_messages', object: @admin_settings
+
+  %p.lead= t('admin.settings.about.preamble')
+
+  .fields-group
+    = f.input :site_extended_description, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
+
+    %p.hint
+      = t 'admin.settings.about.rules_hint'
+      = link_to t('admin.settings.about.manage_rules'), admin_rules_path
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+
+  .fields-group
+    = f.input :site_terms, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/appearance/show.html.haml b/app/views/admin/settings/appearance/show.html.haml
new file mode 100644
index 000000000..d321c4b04
--- /dev/null
+++ b/app/views/admin/settings/appearance/show.html.haml
@@ -0,0 +1,34 @@
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+  = t('admin.settings.appearance.title')
+
+- content_for :heading do
+  %h2= t('admin.settings.title')
+  = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_appearance_path, html: { method: :patch } do |f|
+  = render 'shared/error_messages', object: @admin_settings
+
+  %p.lead= t('admin.settings.appearance.preamble')
+
+  .fields-group
+    = f.input :theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
+
+  .fields-group
+    = f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :mascot, as: :file, wrapper: :with_block_label
+
+    .fields-row__column.fields-row__column-6.fields-group
+      - if @admin_settings.mascot.persisted?
+        = image_tag @admin_settings.mascot.file.url, class: 'fields-group__thumbnail'
+        = link_to admin_site_upload_path(@admin_settings.mascot), data: { method: :delete }, class: 'link-button link-button--destructive' do
+          = fa_icon 'trash fw'
+          = t('admin.site_uploads.delete')
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/branding/show.html.haml b/app/views/admin/settings/branding/show.html.haml
new file mode 100644
index 000000000..74a6fadf9
--- /dev/null
+++ b/app/views/admin/settings/branding/show.html.haml
@@ -0,0 +1,39 @@
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+  = t('admin.settings.branding.title')
+
+- content_for :heading do
+  %h2= t('admin.settings.title')
+  = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_branding_path, html: { method: :patch } do |f|
+  = render 'shared/error_messages', object: @admin_settings
+
+  %p.lead= t('admin.settings.branding.preamble')
+
+  .fields-group
+    = f.input :site_title, wrapper: :with_label
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :site_contact_username, wrapper: :with_label
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :site_contact_email, wrapper: :with_label
+
+  .fields-group
+    = f.input :site_short_description, wrapper: :with_block_label, as: :text, input_html: { rows: 2, maxlength: 200 }
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :thumbnail, as: :file, wrapper: :with_block_label
+    .fields-row__column.fields-row__column-6.fields-group
+      - if @admin_settings.thumbnail.persisted?
+        = image_tag @admin_settings.thumbnail.file.url(:'@1x'), class: 'fields-group__thumbnail'
+        = link_to admin_site_upload_path(@admin_settings.thumbnail), data: { method: :delete }, class: 'link-button link-button--destructive' do
+          = fa_icon 'trash fw'
+          = t('admin.site_uploads.delete')
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/content_retention/show.html.haml b/app/views/admin/settings/content_retention/show.html.haml
new file mode 100644
index 000000000..36856127f
--- /dev/null
+++ b/app/views/admin/settings/content_retention/show.html.haml
@@ -0,0 +1,22 @@
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+  = t('admin.settings.content_retention.title')
+
+- content_for :heading do
+  %h2= t('admin.settings.title')
+  = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_content_retention_path, html: { method: :patch } do |f|
+  = render 'shared/error_messages', object: @admin_settings
+
+  %p.lead= t('admin.settings.content_retention.preamble')
+
+  .fields-group
+    = f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
+    = f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
+    = f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml
new file mode 100644
index 000000000..e63c853fb
--- /dev/null
+++ b/app/views/admin/settings/discovery/show.html.haml
@@ -0,0 +1,40 @@
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+  = t('admin.settings.discovery.title')
+
+- content_for :heading do
+  %h2= t('admin.settings.title')
+  = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_discovery_path, html: { method: :patch } do |f|
+  = render 'shared/error_messages', object: @admin_settings
+
+  %p.lead= t('admin.settings.discovery.preamble')
+
+  %h4= t('admin.settings.discovery.trends')
+
+  .fields-group
+    = f.input :trends, as: :boolean, wrapper: :with_label
+
+  .fields-group
+    = f.input :trendable_by_default, as: :boolean, wrapper: :with_label
+
+  %h4= t('admin.settings.discovery.public_timelines')
+
+  .fields-group
+    = f.input :timeline_preview, as: :boolean, wrapper: :with_label
+
+  %h4= t('admin.settings.discovery.follow_recommendations')
+
+  .fields-group
+    = f.input :bootstrap_timeline_accounts, wrapper: :with_block_label
+
+  %h4= t('admin.settings.discovery.profile_directory')
+
+  .fields-group
+    = f.input :profile_directory, as: :boolean, wrapper: :with_label
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
deleted file mode 100644
index 15b1a2498..000000000
--- a/app/views/admin/settings/edit.html.haml
+++ /dev/null
@@ -1,102 +0,0 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
-
-- content_for :page_title do
-  = t('admin.settings.title')
-
-  - content_for :heading_actions do
-    = button_tag t('generic.save_changes'), class: 'button', form: 'edit_admin'
-
-= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch, id: 'edit_admin' } do |f|
-  = render 'shared/error_messages', object: @admin_settings
-
-  .fields-group
-    = f.input :site_title, wrapper: :with_label, label: t('admin.settings.site_title')
-
-  .fields-row
-    .fields-row__column.fields-row__column-6.fields-group
-      = f.input :theme, collection: Themes.instance.names, label: t('simple_form.labels.defaults.setting_theme'), label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
-    .fields-row__column.fields-row__column-6.fields-group
-      = f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, label: t('admin.settings.registrations_mode.title'), include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
-
-  .fields-row
-    .fields-row__column.fields-row__column-6.fields-group
-      = f.input :site_contact_username, wrapper: :with_label, label: t('admin.settings.contact_information.username')
-    .fields-row__column.fields-row__column-6.fields-group
-      = f.input :site_contact_email, wrapper: :with_label, label: t('admin.settings.contact_information.email')
-
-  .fields-group
-    = f.input :site_short_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_short_description.title'), hint: t('admin.settings.site_short_description.desc_html'), input_html: { rows: 2 }
-
-  .fields-group
-    = f.input :site_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description.title'), hint: t('admin.settings.site_description.desc_html'), input_html: { rows: 2 }
-
-  .fields-row
-    .fields-row__column.fields-row__column-6.fields-group
-      = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: site_upload_delete_hint(t('admin.settings.thumbnail.desc_html'), :thumbnail)
-
-  .fields-row
-    .fields-row__column.fields-row__column-6.fields-group
-      = f.input :mascot, as: :file, wrapper: :with_block_label, label: t('admin.settings.mascot.title'), hint: site_upload_delete_hint(t('admin.settings.mascot.desc_html'), :mascot)
-
-  %hr.spacer/
-
-  .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?
-
-  %hr.spacer/
-
-  .fields-group
-    = f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html')
-
-  %hr.spacer/
-
-  - unless whitelist_mode?
-    .fields-group
-      = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
-
-  - unless whitelist_mode?
-    .fields-group
-      = f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html'), recommended: true
-
-    .fields-group
-      = f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html'), recommended: true
-
-    .fields-group
-      = f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html')
-
-    .fields-group
-      = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html')
-
-    .fields-group
-      = f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html')
-
-    .fields-group
-      = f.input :trendable_by_default, as: :boolean, wrapper: :with_label, label: t('admin.settings.trendable_by_default.title'), hint: t('admin.settings.trendable_by_default.desc_html'), recommended: :not_recommended
-
-    .fields-group
-      = f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html')
-
-  %hr.spacer/
-
-  .fields-row
-    .fields-row__column.fields-row__column-6.fields-group
-      = f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
-    .fields-row__column.fields-row__column-6.fields-group
-      = f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks_rationale.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
-
-  .fields-group
-    = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode?
-    = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
-    = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
-    = f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html')
-
-  %hr.spacer/
-
-  .fields-group
-    = f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
-    = f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
-    = f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' }
-
-  .actions
-    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml
new file mode 100644
index 000000000..0129332d7
--- /dev/null
+++ b/app/views/admin/settings/registrations/show.html.haml
@@ -0,0 +1,27 @@
+- content_for :header_tags do
+  = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+
+- content_for :page_title do
+  = t('admin.settings.registrations.title')
+
+- content_for :heading do
+  %h2= t('admin.settings.title')
+  = render partial: 'admin/settings/shared/links'
+
+= simple_form_for @admin_settings, url: admin_settings_branding_path, html: { method: :patch } do |f|
+  = render 'shared/error_messages', object: @admin_settings
+
+  %p.lead= t('admin.settings.registrations.preamble')
+
+  .fields-row
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
+
+    .fields-row__column.fields-row__column-6.fields-group
+      = f.input :require_invite_text, as: :boolean, wrapper: :with_label, disabled: !approved_registrations?
+
+  .fields-group
+    = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, input_html: { rows: 2 }
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/settings/shared/_links.html.haml b/app/views/admin/settings/shared/_links.html.haml
new file mode 100644
index 000000000..1294c26ce
--- /dev/null
+++ b/app/views/admin/settings/shared/_links.html.haml
@@ -0,0 +1,8 @@
+.content__heading__tabs
+  = render_navigation renderer: :links do |primary|
+    - primary.item :branding, safe_join([fa_icon('pencil fw'), t('admin.settings.branding.title')]), admin_settings_branding_path
+    - primary.item :about, safe_join([fa_icon('file-text fw'), t('admin.settings.about.title')]), admin_settings_about_path
+    - primary.item :registrations, safe_join([fa_icon('users fw'), t('admin.settings.registrations.title')]), admin_settings_registrations_path
+    - primary.item :discovery, safe_join([fa_icon('search fw'), t('admin.settings.discovery.title')]), admin_settings_discovery_path
+    - primary.item :content_retention, safe_join([fa_icon('history fw'), t('admin.settings.content_retention.title')]), admin_settings_content_retention_path
+    - primary.item :appearance, safe_join([fa_icon('desktop fw'), t('admin.settings.appearance.title')]), admin_settings_appearance_path
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index e577b9803..59021ad88 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -22,15 +22,16 @@
 
     .content-wrapper
       .content
-        .content-heading
+        .content__heading
           - if content_for?(:heading)
             = yield :heading
           - else
-            %h2= yield :page_title
+            .content__heading__row
+              %h2= yield :page_title
 
-          - if :heading_actions
-            .content-heading-actions
-              = yield :heading_actions
+              - if content_for?(:heading_actions)
+                .content__heading__actions
+                  = yield :heading_actions
 
         = render 'application/flashes'