Add table of contents to about page (#11885)

Move public domain blocks information to about page
This commit is contained in:
Eugen Rochko 2019-09-19 11:09:05 +02:00 committed by GitHub
parent e1066cd431
commit d930eb88b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 328 additions and 215 deletions

View file

@ -3,9 +3,7 @@
class AboutController < ApplicationController class AboutController < ApplicationController
layout 'public' layout 'public'
before_action :require_open_federation!, only: [:show, :more, :blocks] before_action :require_open_federation!, only: [:show, :more]
before_action :check_blocklist_enabled, only: [:blocks]
before_action :authenticate_user!, only: [:blocks], if: :blocklist_account_required?
before_action :set_body_classes, only: :show before_action :set_body_classes, only: :show
before_action :set_instance_presenter before_action :set_instance_presenter
before_action :set_expires_in, only: [:show, :more, :terms] before_action :set_expires_in, only: [:show, :more, :terms]
@ -16,15 +14,20 @@ class AboutController < ApplicationController
def more def more
flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor] 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 end
def terms; end def terms; end
def blocks helper_method :display_blocks?
@show_rationale = Setting.show_domain_blocks_rationale == 'all' helper_method :display_blocks_rationale?
@show_rationale |= Setting.show_domain_blocks_rationale == 'users' && !current_user.nil? && current_user.functional? helper_method :public_fetch_mode?
@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 helper_method :new_user
end
private private
@ -32,28 +35,14 @@ class AboutController < ApplicationController
not_found if whitelist_mode? not_found if whitelist_mode?
end end
def check_blocklist_enabled def display_blocks?
not_found if Setting.show_domain_blocks == 'disabled' Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?)
end end
def blocklist_account_required? def display_blocks_rationale?
Setting.show_domain_blocks == 'users' Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)
end 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 def new_user
User.new.tap do |user| User.new.tap do |user|
user.build_account user.build_account
@ -61,8 +50,6 @@ class AboutController < ApplicationController
end end
end end
helper_method :new_user
def set_instance_presenter def set_instance_presenter
@instance_presenter = InstancePresenter.new @instance_presenter = InstancePresenter.new
end end

View file

@ -17,117 +17,86 @@ $small-breakpoint: 960px;
.rich-formatting { .rich-formatting {
font-family: $font-sans-serif, sans-serif; font-family: $font-sans-serif, sans-serif;
font-size: 16px; font-size: 14px;
font-weight: 400; font-weight: 400;
font-size: 16px; line-height: 1.7;
line-height: 30px; word-wrap: break-word;
color: $darker-text-color; color: $darker-text-color;
padding-right: 10px;
a { a {
color: $highlight-text-color; color: $highlight-text-color;
text-decoration: underline; text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
} }
p, p,
li { 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; color: $darker-text-color;
a {
color: $highlight-text-color;
text-decoration: underline;
} }
p {
margin-top: 0;
margin-bottom: .85em;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }
strong, strong {
em {
font-weight: 700; 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 { h1 {
font-family: $font-display, sans-serif; font-size: 2em;
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%);
}
} }
h2 { h2 {
font-family: $font-display, sans-serif; font-size: 1.75em;
font-size: 22px;
line-height: 26px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
} }
h3 { h3 {
font-family: $font-display, sans-serif; font-size: 1.5em;
font-size: 18px;
line-height: 24px;
font-weight: 500;
margin-bottom: 20px;
color: $secondary-text-color;
} }
h4 { h4 {
font-family: $font-display, sans-serif; font-size: 1.25em;
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;
} }
h5,
h6 { h6 {
font-family: $font-display, sans-serif; font-size: 1em;
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;
}
} }
ul { ul {
@ -138,23 +107,38 @@ $small-breakpoint: 960px;
list-style: decimal; list-style: decimal;
} }
li > ol, ul,
li > ul { ol {
margin-top: 6px; 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 { hr {
width: 100%; width: 100%;
height: 0; height: 0;
border: 0; border: 0;
border-bottom: 1px solid rgba($ui-base-lighter-color, .6); border-bottom: 1px solid lighten($ui-base-color, 4%);
margin: 20px 0; margin: 1.7em 0;
&.spacer { &.spacer {
height: 1px; height: 1px;
border: 0; border: 0;
} }
} }
& > :first-child {
margin-top: 0;
}
} }
.information-board { .information-board {
@ -416,7 +400,7 @@ $small-breakpoint: 960px;
} }
&__call-to-action { &__call-to-action {
background: darken($ui-base-color, 4%); background: $ui-base-color;
border-radius: 4px; border-radius: 4px;
padding: 25px 40px; padding: 25px 40px;
overflow: hidden; overflow: hidden;

View file

@ -141,6 +141,63 @@
grid-row: 3; 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 { .landing-page__call-to-action {
min-height: 100%; min-height: 100%;
} }
@ -189,6 +246,11 @@
} }
.column-3 { .column-3 {
grid-column: 1;
grid-row: 5;
}
.column-4 {
grid-column: 1; grid-column: 1;
grid-row: 4; grid-row: 4;
} }

View file

@ -128,41 +128,43 @@
margin-bottom: 10px; 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 { .contact-widget {
min-height: 100%;
font-size: 15px; font-size: 15px;
color: $darker-text-color; color: $darker-text-color;
line-height: 20px; line-height: 20px;
word-wrap: break-word; word-wrap: break-word;
font-weight: 400; font-weight: 400;
padding: 0;
strong { h4 {
font-weight: 500; padding: 10px;
text-transform: uppercase;
font-weight: 700;
font-size: 13px;
color: $darker-text-color;
} }
p { .account {
margin-bottom: 10px; border-bottom: 0;
padding: 10px 0;
&:last-child { padding-top: 5px;
margin-bottom: 0;
}
} }
&__mail { & > a {
margin-top: 10px; display: inline-block;
padding: 10px;
a { padding-top: 0;
color: $primary-text-color; color: $darker-text-color;
text-decoration: none; text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&: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%);
}
}

69
app/lib/toc_generator.rb Normal file
View file

@ -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

View file

@ -26,6 +26,7 @@ class DomainBlock < ApplicationRecord
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } 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 :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 class << self
def suspend?(domain) def suspend?(domain)

View file

@ -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'

View file

@ -5,7 +5,7 @@
= javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
= render partial: 'shared/og' = render partial: 'shared/og'
.grid-3 .grid-4
.column-0 .column-0
.public-account-header.public-account-header--no-bar .public-account-header.public-account-header--no-bar
.public-account-header__image .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: '' = image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'), alt: ''
.column-2 .column-2
.landing-page__information.contact-widget .contact-widget
%p %h4= t 'about.administered_by'
%strong= t 'about.administered_by'
= account_link_to(@instance_presenter.contact_account) = account_link_to(@instance_presenter.contact_account)
- if @instance_presenter.site_contact_email.present? - if @instance_presenter.site_contact_email.present?
%p.contact-widget__mail %h4
%strong
= succeed ':' do = succeed ':' do
= t 'about.contact' = t 'about.contact'
%br/
= mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email = mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email
.column-3 .column-3
= render 'application/flashes' = render 'application/flashes'
- if @contents.blank? && (!display_blocks? || @blocks&.empty?)
= nothing_here
- else
.box-widget .box-widget
.rich-formatting= @instance_presenter.site_extended_description.html_safe.presence || t('about.extended_description_html') .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'

View file

@ -17,9 +17,6 @@ en:
contact_unavailable: N/A contact_unavailable: N/A
discover_users: Discover users discover_users: Discover users
documentation: Documentation 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. 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" generic_description: "%{domain} is one server in the network"
get_apps: Try a mobile app get_apps: Try a mobile app
@ -38,6 +35,13 @@ en:
status_count_before: Who authored status_count_before: Who authored
tagline: Follow friends and discover new ones tagline: Follow friends and discover new ones
terms: Terms of service 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: user_count_after:
one: user one: user
other: users other: users
@ -661,23 +665,6 @@ en:
directory: Profile directory directory: Profile directory
explanation: Discover users based on their interests explanation: Discover users based on their interests
explore_mastodon: Explore %{title} 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: domain_validator:
invalid_domain: is not a valid domain name invalid_domain: is not a valid domain name
errors: errors:

View file

@ -441,7 +441,6 @@ Rails.application.routes.draw do
get '/about', to: 'about#show' get '/about', to: 'about#show'
get '/about/more', to: 'about#more' get '/about/more', to: 'about#more'
get '/about/blocks', to: 'about#blocks'
get '/terms', to: 'about#terms' get '/terms', to: 'about#terms'
match '/', via: [:post, :put, :patch, :delete], to: 'application#raise_not_found', format: false match '/', via: [:post, :put, :patch, :delete], to: 'application#raise_not_found', format: false