Browse Source

Add table of contents to about page (#11885)

Move public domain blocks information to about page
pull/4/head
Eugen Rochko 4 years ago
committed by GitHub
parent
commit
d930eb88b6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 322 additions and 209 deletions
  1. +15
    -28
      app/controllers/about_controller.rb
  2. +61
    -77
      app/javascript/styles/mastodon/about.scss
  3. +62
    -0
      app/javascript/styles/mastodon/containers.scss
  4. +60
    -23
      app/javascript/styles/mastodon/widgets.scss
  5. +69
    -0
      app/lib/toc_generator.rb
  6. +1
    -0
      app/models/domain_block.rb
  7. +0
    -48
      app/views/about/blocks.html.haml
  8. +47
    -12
      app/views/about/more.html.haml
  9. +7
    -20
      config/locales/en.yml
  10. +0
    -1
      config/routes.rb

+ 15
- 28
app/controllers/about_controller.rb View File

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

+ 61
- 77
app/javascript/styles/mastodon/about.scss View File

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

+ 62
- 0
app/javascript/styles/mastodon/containers.scss View File

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

+ 60
- 23
app/javascript/styles/mastodon/widgets.scss View File

@ -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%);
}
}

+ 69
- 0
app/lib/toc_generator.rb 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

+ 1
- 0
app/models/domain_block.rb View File

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

+ 0
- 48
app/views/about/blocks.html.haml 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'

+ 47
- 12
app/views/about/more.html.haml View File

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

+ 7
- 20
config/locales/en.yml View File

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

+ 0
- 1
config/routes.rb View File

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

Loading…
Cancel
Save