about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2019-09-19 11:09:05 +0200
committerGitHub <noreply@github.com>2019-09-19 11:09:05 +0200
commitd930eb88b671fa6e5573fe7342bcdda87501bdb7 (patch)
treea67ab76528da0060e7def3d13f5802904d215939
parente1066cd4319a220d5be16e51ffaf5236a2f6e866 (diff)
Add table of contents to about page (#11885)
Move public domain blocks information to about page
-rw-r--r--app/controllers/about_controller.rb43
-rw-r--r--app/javascript/styles/mastodon/about.scss138
-rw-r--r--app/javascript/styles/mastodon/containers.scss62
-rw-r--r--app/javascript/styles/mastodon/widgets.scss83
-rw-r--r--app/lib/toc_generator.rb69
-rw-r--r--app/models/domain_block.rb1
-rw-r--r--app/views/about/blocks.html.haml48
-rw-r--r--app/views/about/more.html.haml59
-rw-r--r--config/locales/en.yml27
-rw-r--r--config/routes.rb1
10 files changed, 322 insertions, 209 deletions
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 5e942e5c0..abd1ec0cb 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -3,9 +3,7 @@
 class AboutController < ApplicationController
   layout 'public'
 
-  before_action :require_open_federation!, only: [:show, :more, :blocks]
-  before_action :check_blocklist_enabled, only: [:blocks]
-  before_action :authenticate_user!, only: [:blocks], if: :blocklist_account_required?
+  before_action :require_open_federation!, only: [:show, :more]
   before_action :set_body_classes, only: :show
   before_action :set_instance_presenter
   before_action :set_expires_in, only: [:show, :more, :terms]
@@ -16,15 +14,20 @@ class AboutController < ApplicationController
 
   def more
     flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
+
+    toc_generator = TOCGenerator.new(@instance_presenter.site_extended_description)
+
+    @contents          = toc_generator.html
+    @table_of_contents = toc_generator.toc
+    @blocks            = DomainBlock.with_user_facing_limitations.by_severity if display_blocks?
   end
 
   def terms; end
 
-  def blocks
-    @show_rationale = Setting.show_domain_blocks_rationale == 'all'
-    @show_rationale |= Setting.show_domain_blocks_rationale == 'users' && !current_user.nil? && current_user.functional?
-    @blocks = DomainBlock.with_user_facing_limitations.order('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain').to_a
-  end
+  helper_method :display_blocks?
+  helper_method :display_blocks_rationale?
+  helper_method :public_fetch_mode?
+  helper_method :new_user
 
   private
 
@@ -32,28 +35,14 @@ class AboutController < ApplicationController
     not_found if whitelist_mode?
   end
 
-  def check_blocklist_enabled
-    not_found if Setting.show_domain_blocks == 'disabled'
-  end
-
-  def blocklist_account_required?
-    Setting.show_domain_blocks == 'users'
+  def display_blocks?
+    Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
   end
 
-  def block_severity_text(block)
-    if block.severity == 'suspend'
-      I18n.t('domain_blocks.suspension')
-    else
-      limitations = []
-      limitations << I18n.t('domain_blocks.media_block') if block.reject_media?
-      limitations << I18n.t('domain_blocks.silence') if block.severity == 'silence'
-      limitations.join(', ')
-    end
+  def display_blocks_rationale?
+    Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
   end
 
-  helper_method :block_severity_text
-  helper_method :public_fetch_mode?
-
   def new_user
     User.new.tap do |user|
       user.build_account
@@ -61,8 +50,6 @@ class AboutController < ApplicationController
     end
   end
 
-  helper_method :new_user
-
   def set_instance_presenter
     @instance_presenter = InstancePresenter.new
   end
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 61637ce96..c056ef85d 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -17,109 +17,102 @@ $small-breakpoint: 960px;
 
 .rich-formatting {
   font-family: $font-sans-serif, sans-serif;
-  font-size: 16px;
+  font-size: 14px;
   font-weight: 400;
-  font-size: 16px;
-  line-height: 30px;
+  line-height: 1.7;
+  word-wrap: break-word;
   color: $darker-text-color;
-  padding-right: 10px;
 
   a {
     color: $highlight-text-color;
     text-decoration: underline;
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: none;
+    }
   }
 
   p,
   li {
-    font-family: $font-sans-serif, sans-serif;
-    font-size: 16px;
-    font-weight: 400;
-    font-size: 16px;
-    line-height: 30px;
-    margin-bottom: 12px;
     color: $darker-text-color;
+  }
 
-    a {
-      color: $highlight-text-color;
-      text-decoration: underline;
-    }
+  p {
+    margin-top: 0;
+    margin-bottom: .85em;
 
     &:last-child {
       margin-bottom: 0;
     }
   }
 
-  strong,
-  em {
+  strong {
     font-weight: 700;
-    color: lighten($darker-text-color, 10%);
+    color: $secondary-text-color;
   }
 
-  h1 {
-    font-family: $font-display, sans-serif;
-    font-size: 26px;
-    line-height: 30px;
-    font-weight: 500;
-    margin-bottom: 20px;
+  em {
+    font-style: italic;
     color: $secondary-text-color;
+  }
 
-    small {
-      font-family: $font-sans-serif, sans-serif;
-      display: block;
-      font-size: 18px;
-      font-weight: 400;
-      color: lighten($darker-text-color, 10%);
-    }
+  code {
+    font-size: 0.85em;
+    background: darken($ui-base-color, 8%);
+    border-radius: 4px;
+    padding: 0.2em 0.3em;
   }
 
-  h2 {
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
     font-family: $font-display, sans-serif;
-    font-size: 22px;
-    line-height: 26px;
+    margin-top: 1.275em;
+    margin-bottom: .85em;
     font-weight: 500;
-    margin-bottom: 20px;
     color: $secondary-text-color;
   }
 
+  h1 {
+    font-size: 2em;
+  }
+
+  h2 {
+    font-size: 1.75em;
+  }
+
   h3 {
-    font-family: $font-display, sans-serif;
-    font-size: 18px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+    font-size: 1.5em;
   }
 
   h4 {
-    font-family: $font-display, sans-serif;
-    font-size: 16px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+    font-size: 1.25em;
   }
 
-  h5 {
-    font-family: $font-display, sans-serif;
-    font-size: 14px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+  h5,
+  h6 {
+    font-size: 1em;
   }
 
-  h6 {
-    font-family: $font-display, sans-serif;
-    font-size: 12px;
-    line-height: 24px;
-    font-weight: 500;
-    margin-bottom: 20px;
-    color: $secondary-text-color;
+  ul {
+    list-style: disc;
+  }
+
+  ol {
+    list-style: decimal;
   }
 
   ul,
   ol {
-    margin-left: 20px;
+    margin: 0;
+    padding: 0;
+    padding-left: 2em;
+    margin-bottom: 0.85em;
 
     &[type='a'] {
       list-style-type: lower-alpha;
@@ -130,31 +123,22 @@ $small-breakpoint: 960px;
     }
   }
 
-  ul {
-    list-style: disc;
-  }
-
-  ol {
-    list-style: decimal;
-  }
-
-  li > ol,
-  li > ul {
-    margin-top: 6px;
-  }
-
   hr {
     width: 100%;
     height: 0;
     border: 0;
-    border-bottom: 1px solid rgba($ui-base-lighter-color, .6);
-    margin: 20px 0;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+    margin: 1.7em 0;
 
     &.spacer {
       height: 1px;
       border: 0;
     }
   }
+
+  & > :first-child {
+    margin-top: 0;
+  }
 }
 
 .information-board {
@@ -416,7 +400,7 @@ $small-breakpoint: 960px;
   }
 
   &__call-to-action {
-    background: darken($ui-base-color, 4%);
+    background: $ui-base-color;
     border-radius: 4px;
     padding: 25px 40px;
     overflow: hidden;
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index aa45c0174..24bbf8211 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -141,6 +141,63 @@
     grid-row: 3;
   }
 
+  @media screen and (max-width: $no-gap-breakpoint) {
+    grid-gap: 0;
+    grid-template-columns: minmax(0, 100%);
+
+    .column-0 {
+      grid-column: 1;
+    }
+
+    .column-1 {
+      grid-column: 1;
+      grid-row: 3;
+    }
+
+    .column-2 {
+      grid-column: 1;
+      grid-row: 2;
+    }
+
+    .column-3 {
+      grid-column: 1;
+      grid-row: 4;
+    }
+  }
+}
+
+.grid-4 {
+  display: grid;
+  grid-gap: 10px;
+  grid-template-columns: 1fr 1fr 1fr 1fr;
+  grid-auto-columns: 25%;
+  grid-auto-rows: max-content;
+
+  .column-0 {
+    grid-column: 1 / 5;
+    grid-row: 1;
+  }
+
+  .column-1 {
+    grid-column: 1 / 4;
+    grid-row: 2;
+  }
+
+  .column-2 {
+    grid-column: 4;
+    grid-row: 2;
+  }
+
+  .column-3 {
+    grid-column: 2 / 5;
+    grid-row: 3;
+  }
+
+  .column-4 {
+    grid-column: 1;
+    grid-row: 3;
+  }
+
   .landing-page__call-to-action {
     min-height: 100%;
   }
@@ -190,6 +247,11 @@
 
     .column-3 {
       grid-column: 1;
+      grid-row: 5;
+    }
+
+    .column-4 {
+      grid-column: 1;
       grid-row: 4;
     }
   }
diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss
index 04beb869c..ca050a8d9 100644
--- a/app/javascript/styles/mastodon/widgets.scss
+++ b/app/javascript/styles/mastodon/widgets.scss
@@ -128,41 +128,43 @@
   margin-bottom: 10px;
 }
 
-.contact-widget,
-.landing-page__information.contact-widget {
-  box-sizing: border-box;
-  padding: 20px;
-  min-height: 100%;
-  border-radius: 4px;
-  background: $ui-base-color;
-  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
-}
-
 .contact-widget {
+  min-height: 100%;
   font-size: 15px;
   color: $darker-text-color;
   line-height: 20px;
   word-wrap: break-word;
   font-weight: 400;
+  padding: 0;
 
-  strong {
-    font-weight: 500;
+  h4 {
+    padding: 10px;
+    text-transform: uppercase;
+    font-weight: 700;
+    font-size: 13px;
+    color: $darker-text-color;
   }
 
-  p {
-    margin-bottom: 10px;
-
-    &:last-child {
-      margin-bottom: 0;
-    }
+  .account {
+    border-bottom: 0;
+    padding: 10px 0;
+    padding-top: 5px;
   }
 
-  &__mail {
-    margin-top: 10px;
+  & > a {
+    display: inline-block;
+    padding: 10px;
+    padding-top: 0;
+    color: $darker-text-color;
+    text-decoration: none;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
 
-    a {
-      color: $primary-text-color;
-      text-decoration: none;
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
     }
   }
 }
@@ -562,3 +564,38 @@ $fluid-breakpoint: $maximum-width + 20px;
     }
   }
 }
+
+.table-of-contents {
+  background: darken($ui-base-color, 4%);
+  min-height: 100%;
+  font-size: 14px;
+  border-radius: 4px;
+
+  li a {
+    display: block;
+    font-weight: 500;
+    padding: 15px;
+    overflow: hidden;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    text-decoration: none;
+    color: $primary-text-color;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+    &:hover,
+    &:focus,
+    &:active {
+      text-decoration: underline;
+    }
+  }
+
+  li:last-child a {
+    border-bottom: 0;
+  }
+
+  li ul {
+    padding-left: 20px;
+    border-bottom: 1px solid lighten($ui-base-color, 4%);
+  }
+}
diff --git a/app/lib/toc_generator.rb b/app/lib/toc_generator.rb
new file mode 100644
index 000000000..c6e179557
--- /dev/null
+++ b/app/lib/toc_generator.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+class TOCGenerator
+  TARGET_ELEMENTS = %w(h1 h2 h3 h4 h5 h6).freeze
+  LISTED_ELEMENTS = %w(h2 h3).freeze
+
+  class Section
+    attr_accessor :depth, :title, :children, :anchor
+
+    def initialize(depth, title, anchor)
+      @depth    = depth
+      @title    = title
+      @children = []
+      @anchor   = anchor
+    end
+
+    delegate :<<, to: :children
+  end
+
+  def initialize(source_html)
+    @source_html = source_html
+    @processed   = false
+    @target_html = ''
+    @headers     = []
+    @slugs       = Hash.new { |h, k| h[k] = 0 }
+  end
+
+  def html
+    parse_and_transform unless @processed
+    @target_html
+  end
+
+  def toc
+    parse_and_transform unless @processed
+    @headers
+  end
+
+  private
+
+  def parse_and_transform
+    return if @source_html.blank?
+
+    parsed_html = Nokogiri::HTML.fragment(@source_html)
+
+    parsed_html.traverse do |node|
+      next unless TARGET_ELEMENTS.include?(node.name)
+
+      anchor = node.text.parameterize
+      @slugs[anchor] += 1
+      anchor = "#{anchor}-#{@slugs[anchor]}" if @slugs[anchor] > 1
+
+      node['id'] = anchor
+
+      next unless LISTED_ELEMENTS.include?(node.name)
+
+      depth          = node.name[1..-1]
+      latest_section = @headers.last
+
+      if latest_section.nil? || latest_section.depth >= depth
+        @headers << Section.new(depth, node.text, anchor)
+      else
+        latest_section << Section.new(depth, node.text, anchor)
+      end
+    end
+
+    @target_html = parsed_html.to_s
+    @processed   = true
+  end
+end
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 4383cbd05..4e865b850 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -26,6 +26,7 @@ class DomainBlock < ApplicationRecord
 
   scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
   scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
+  scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain')) }
 
   class << self
     def suspend?(domain)
diff --git a/app/views/about/blocks.html.haml b/app/views/about/blocks.html.haml
deleted file mode 100644
index a81a4d1eb..000000000
--- a/app/views/about/blocks.html.haml
+++ /dev/null
@@ -1,48 +0,0 @@
-- content_for :page_title do
-  = t('domain_blocks.title', instance: site_hostname)
-
-.grid
-  .column-0
-    .box-widget.rich-formatting
-      %h2= t('domain_blocks.blocked_domains')
-      %p= t('domain_blocks.description', instance: site_hostname)
-      .table-wrapper
-        %table.blocks-table
-          %thead
-            %tr
-              %th= t('domain_blocks.domain')
-              %th.severity-column= t('domain_blocks.severity')
-              - if @show_rationale
-                %th.button-column
-          %tbody
-            - if @blocks.empty?
-              %tr
-                %td{ colspan: @show_rationale ? 3 : 2 }= t('domain_blocks.no_domain_blocks')
-            - else
-              - @blocks.each_with_index do |block, i|
-                %tr{ class: i % 2 == 0 ? 'even': nil }
-                  %td{ title: block.domain }= block.domain
-                  %td= block_severity_text(block)
-                  - if @show_rationale
-                    %td
-                      - if block.public_comment.present?
-                        %button.icon-button{ title: t('domain_blocks.show_rationale'), 'aria-label' => t('domain_blocks.show_rationale') }
-                          = fa_icon 'chevron-down fw', 'aria-hidden' => true
-                - if @show_rationale
-                  - if block.public_comment.present?
-                    %tr.rationale.hidden
-                      %td{ colspan: 3 }= block.public_comment.presence
-      %h2= t('domain_blocks.severity_legend.title')
-      - if @blocks.any? { |block| block.reject_media? }
-        %h3= t('domain_blocks.media_block')
-        %p= t('domain_blocks.severity_legend.media_block')
-      - if @blocks.any? { |block| block.severity == 'silence' }
-        %h3= t('domain_blocks.silence')
-        %p= t('domain_blocks.severity_legend.silence')
-      - if @blocks.any? { |block| block.severity == 'suspend' }
-        %h3= t('domain_blocks.suspension')
-        %p= t('domain_blocks.severity_legend.suspension')
-        - if public_fetch_mode?
-          %p= t('domain_blocks.severity_legend.suspension_disclaimer')
-  .column-1
-    = render 'application/sidebar'
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index 21431ef8e..4b3035ee8 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -5,7 +5,7 @@
   = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
   = render partial: 'shared/og'
 
-.grid-3
+.grid-4
   .column-0
     .public-account-header.public-account-header--no-bar
       .public-account-header__image
@@ -28,22 +28,57 @@
             = image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'), alt: ''
 
   .column-2
-    .landing-page__information.contact-widget
-      %p
-        %strong= t 'about.administered_by'
+    .contact-widget
+      %h4= t 'about.administered_by'
 
       = account_link_to(@instance_presenter.contact_account)
 
       - if @instance_presenter.site_contact_email.present?
-        %p.contact-widget__mail
-          %strong
-            = succeed ':' do
-              = t 'about.contact'
-          %br/
-          = mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email
+        %h4
+          = succeed ':' do
+            = t 'about.contact'
+
+        = mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email
 
   .column-3
     = render 'application/flashes'
 
-    .box-widget
-      .rich-formatting= @instance_presenter.site_extended_description.html_safe.presence || t('about.extended_description_html')
+    - if @contents.blank? && (!display_blocks? || @blocks&.empty?)
+      = nothing_here
+    - else
+      .box-widget
+        .rich-formatting
+          = @contents.html_safe
+
+          - if display_blocks? && !@blocks.empty?
+            %h2#unavailable-content= t('about.unavailable_content')
+
+            %p= t('about.unavailable_content_html')
+
+            - @blocks.each do |domain_block|
+              %p
+                %strong= "#{domain_block.domain}:"
+
+                - if domain_block.suspend?
+                  = t('about.unavailable_content_description.suspended')
+                - else
+                  = t('about.unavailable_content_description.silenced') if domain_block.silence?
+                  = t('about.unavailable_content_description.rejecting_media') if domain_block.reject_media?
+
+                - if display_blocks_rationale?
+                  %strong= t('about.unavailable_content_description.reason')
+                  = domain_block.public_comment
+
+  .column-4
+    %ul.table-of-contents
+      - @table_of_contents.each do |item|
+        %li
+          = link_to item.title, "##{item.anchor}"
+
+          - unless item.children.empty?
+            %ul
+              - item.children.each do |sub_item|
+                %li= link_to sub_item.title, "##{sub_item.anchor}"
+
+      - if display_blocks? && !@blocks.empty?
+        %li= link_to t('about.unavailable_content'), '#unavailable-content'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index da06b0e51..dabb679e7 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -17,9 +17,6 @@ en:
     contact_unavailable: N/A
     discover_users: Discover users
     documentation: Documentation
-    extended_description_html: |
-      <h3>A good place for rules</h3>
-      <p>The extended description has not been set up yet.</p>
     federation_hint_html: With an account on %{instance} you'll be able to follow people on any Mastodon server and beyond.
     generic_description: "%{domain} is one server in the network"
     get_apps: Try a mobile app
@@ -38,6 +35,13 @@ en:
     status_count_before: Who authored
     tagline: Follow friends and discover new ones
     terms: Terms of service
+    unavailable_content: Unavailable content
+    unavailable_content_description:
+      reason: 'Reason:'
+      rejecting_media: Media files from this server will not be processed and and no thumbnails will be displayed, requiring manual click-through to the other server.
+      silenced: Posts from this server will not show up anywhere except your home feed if you follow the author.
+      suspended: You won't be able to follow anyone from this server, and no data from it will be processed or stored, and no data exchanged.
+    unavailable_content_html: Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.
     user_count_after:
       one: user
       other: users
@@ -661,23 +665,6 @@ en:
     directory: Profile directory
     explanation: Discover users based on their interests
     explore_mastodon: Explore %{title}
-  domain_blocks:
-    blocked_domains: List of limited and blocked domains
-    description: This is the list of servers that %{instance} limits or reject federation with.
-    domain: Domain
-    media_block: Media block
-    no_domain_blocks: "(No domain blocks)"
-    severity: Severity
-    severity_legend:
-      media_block: Media files coming from the server are neither fetched, stored, or displayed to the user.
-      silence: Accounts from silenced servers can be found, followed and interacted with, but their toots will not appear in the public timelines, and notifications from them will not reach local users who are not following them.
-      suspension: No content from suspended servers is stored or displayed, nor is any content sent to them. Interactions from suspended servers are ignored.
-      suspension_disclaimer: Suspended servers may occasionally retrieve public content from this server.
-      title: Severities
-    show_rationale: Show rationale
-    silence: Silence
-    suspension: Suspension
-    title: "%{instance} List of blocked instances"
   domain_validator:
     invalid_domain: is not a valid domain name
   errors:
diff --git a/config/routes.rb b/config/routes.rb
index 9ad1ea65d..dcfa079a0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -441,7 +441,6 @@ Rails.application.routes.draw do
 
   get '/about',        to: 'about#show'
   get '/about/more',   to: 'about#more'
-  get '/about/blocks', to: 'about#blocks'
   get '/terms',        to: 'about#terms'
 
   match '/', via: [:post, :put, :patch, :delete], to: 'application#raise_not_found', format: false