From d930eb88b671fa6e5573fe7342bcdda87501bdb7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 19 Sep 2019 11:09:05 +0200 Subject: 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 | 138 +++++++++++-------------- app/javascript/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 ++++++++--- 8 files changed, 315 insertions(+), 188 deletions(-) create mode 100644 app/lib/toc_generator.rb delete mode 100644 app/views/about/blocks.html.haml (limited to 'app') 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%; } @@ -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 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' -- cgit