From d930eb88b671fa6e5573fe7342bcdda87501bdb7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 19 Sep 2019 11:09:05 +0200 Subject: [PATCH] Add table of contents to about page (#11885) Move public domain blocks information to about page --- app/controllers/about_controller.rb | 43 ++--- app/javascript/styles/mastodon/about.scss | 150 ++++++++---------- .../styles/mastodon/containers.scss | 62 ++++++++ app/javascript/styles/mastodon/widgets.scss | 83 +++++++--- app/lib/toc_generator.rb | 69 ++++++++ app/models/domain_block.rb | 1 + app/views/about/blocks.html.haml | 48 ------ app/views/about/more.html.haml | 59 +++++-- config/locales/en.yml | 27 +--- config/routes.rb | 1 - 10 files changed, 328 insertions(+), 215 deletions(-) create mode 100644 app/lib/toc_generator.rb delete mode 100644 app/views/about/blocks.html.haml diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 5e942e5c07..abd1ec0cb6 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' + def display_blocks? + Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?) end - def blocklist_account_required? - Setting.show_domain_blocks == 'users' + def display_blocks_rationale? + Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == '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 - 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 61637ce967..c056ef85dc 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -17,117 +17,86 @@ $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; + } + + em { + font-style: italic; + color: $secondary-text-color; + } + + code { + font-size: 0.85em; + background: darken($ui-base-color, 8%); + border-radius: 4px; + padding: 0.2em 0.3em; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + font-family: $font-display, sans-serif; + margin-top: 1.275em; + margin-bottom: .85em; + font-weight: 500; + color: $secondary-text-color; } h1 { - font-family: $font-display, sans-serif; - font-size: 26px; - line-height: 30px; - font-weight: 500; - margin-bottom: 20px; - 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%); - } + font-size: 2em; } h2 { - font-family: $font-display, sans-serif; - font-size: 22px; - line-height: 26px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; + 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; - } - - h5 { - font-family: $font-display, sans-serif; - font-size: 14px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; + font-size: 1.25em; } + h5, h6 { - font-family: $font-display, sans-serif; - font-size: 12px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; - } - - ul, - ol { - margin-left: 20px; - - &[type='a'] { - list-style-type: lower-alpha; - } - - &[type='i'] { - list-style-type: lower-roman; - } + font-size: 1em; } ul { @@ -138,23 +107,38 @@ $small-breakpoint: 960px; list-style: decimal; } - li > ol, - li > ul { - margin-top: 6px; + ul, + ol { + margin: 0; + padding: 0; + padding-left: 2em; + margin-bottom: 0.85em; + + &[type='a'] { + list-style-type: lower-alpha; + } + + &[type='i'] { + list-style-type: lower-roman; + } } 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 aa45c0174d..24bbf82119 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%; } @@ -189,6 +246,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 04beb869ca..ca050a8d99 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 0000000000..c6e1795579 --- /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 4383cbd051..4e865b850a 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 a81a4d1eba..0000000000 --- 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 21431ef8e5..4b3035ee82 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 da06b0e51d..dabb679e76 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: | -

A good place for rules

-

The extended description has not been set up yet.

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 9ad1ea65de..dcfa079a0f 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