From 02ba9cfa35c7b2285950955619ae3431391e9625 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 4 Oct 2022 20:13:46 +0200 Subject: Remove code for rendering public and hashtag timelines outside the web UI (#19257) --- spec/controllers/tags_controller_spec.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) (limited to 'spec/controllers') diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb index 69def90cf..1fd8494d6 100644 --- a/spec/controllers/tags_controller_spec.rb +++ b/spec/controllers/tags_controller_spec.rb @@ -10,14 +10,9 @@ RSpec.describe TagsController, type: :controller do let!(:late) { Fabricate(:status, tags: [tag], text: 'late #test') } context 'when tag exists' do - it 'returns http success' do + it 'redirects to web version' do get :show, params: { id: 'test', max_id: late.id } - expect(response).to have_http_status(200) - end - - it 'renders application layout' do - get :show, params: { id: 'test', max_id: late.id } - expect(response).to render_template layout: 'public' + expect(response).to redirect_to('/web/tags/test') end end -- cgit From 58d5b28cb00ffadfeb7a3e1e03f7ae0d3b0d8486 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 6 Oct 2022 02:19:45 +0200 Subject: Remove previous landing page (#19300) --- app/controllers/about_controller.rb | 5 +-- app/views/about/_logged_in.html.haml | 10 ----- app/views/about/_login.html.haml | 22 ---------- app/views/about/_registration.html.haml | 37 ----------------- app/views/about/show.html.haml | 69 ------------------------------- config/locales/en.yml | 14 ------- config/routes.rb | 2 +- spec/controllers/about_controller_spec.rb | 22 ---------- spec/requests/localization_spec.rb | 12 +++--- spec/views/about/show.html.haml_spec.rb | 25 ----------- 10 files changed, 8 insertions(+), 210 deletions(-) delete mode 100644 app/views/about/_logged_in.html.haml delete mode 100644 app/views/about/_login.html.haml delete mode 100644 app/views/about/_registration.html.haml delete mode 100644 app/views/about/show.html.haml delete mode 100644 spec/views/about/show.html.haml_spec.rb (limited to 'spec/controllers') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 8da4a19ab..eae7de8c8 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -5,16 +5,13 @@ class AboutController < ApplicationController layout 'public' - before_action :require_open_federation!, only: [:show, :more] + before_action :require_open_federation!, only: [:more] before_action :set_body_classes, only: :show before_action :set_instance_presenter before_action :set_expires_in, only: [:more] - before_action :set_registration_form_time, only: :show skip_before_action :require_functional!, only: [:more] - def show; end - def more flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor] diff --git a/app/views/about/_logged_in.html.haml b/app/views/about/_logged_in.html.haml deleted file mode 100644 index e1bcfffb3..000000000 --- a/app/views/about/_logged_in.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -.simple_form - %p.lead= t('about.logged_in_as_html', username: content_tag(:strong, current_account.username)) - - .actions - = link_to t('about.continue_to_web'), root_url, class: 'button button-primary' - -.form-footer - %ul.no-list - %li= link_to t('about.get_apps'), 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener noreferrer' - %li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete } diff --git a/app/views/about/_login.html.haml b/app/views/about/_login.html.haml deleted file mode 100644 index 0f19e8164..000000000 --- a/app/views/about/_login.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -- unless omniauth_only? - = simple_form_for(new_user, url: user_session_path, namespace: 'login') do |f| - .fields-group - - if use_seamless_external_login? - = f.input :email, placeholder: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false - - else - = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false - - = f.input :password, placeholder: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }, hint: false - - .actions - = f.button :button, t('auth.login'), type: :submit, class: 'button button-primary' - - %p.hint.subtle-hint= link_to t('auth.trouble_logging_in'), new_user_password_path - -- if Devise.mappings[:user].omniauthable? and User.omniauth_providers.any? - .simple_form.alternative-login - %h4= omniauth_only? ? t('auth.log_in_with') : t('auth.or_log_in_with') - - .actions - - User.omniauth_providers.each do |provider| - = provider_sign_in_link(provider) diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml deleted file mode 100644 index 5db620b2d..000000000 --- a/app/views/about/_registration.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -- disabled = closed_registrations? || omniauth_only? || current_account.present? -- show_message = disabled && (current_user.present? || @instance_presenter.closed_registrations_message.present?) - -.simple_form__overlay-area{ class: show_message ? 'simple_form__overlay-area__blurred' : '' } - = simple_form_for(new_user, url: user_registration_path, namespace: 'registration', html: { novalidate: false }) do |f| - %p.lead= t('about.federation_hint_html', instance: content_tag(:strong, site_hostname)) - - .fields-group - = f.simple_fields_for :account do |account_fields| - = account_fields.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false, disabled: disabled - - = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: disabled - = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'new-password', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: disabled - = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'new-password' }, hint: false, disabled: disabled - - = f.input :confirm_password, as: :string, placeholder: t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), :autocomplete => 'off' }, hint: false, disabled: disabled - = f.input :website, as: :url, placeholder: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: 'Website'), :autocomplete => 'off' }, hint: false, disabled: disabled - - - if approved_registrations? - .fields-group - = f.simple_fields_for :invite_request do |invite_request_fields| - = invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: Setting.require_invite_text - - .fields-group - = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), required: true, disabled: disabled - - .actions - = f.button :button, sign_up_message, type: :submit, class: 'button button-primary', disabled: disabled - - - if show_message - .simple_form__overlay-area__overlay - .simple_form__overlay-area__overlay__content.rich-formatting - .block-icon= fa_icon 'warning' - - if current_account.present? - = t('about.logout_before_registering') - - else - = @instance_presenter.closed_registrations_message.html_safe diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml deleted file mode 100644 index 75124d5e2..000000000 --- a/app/views/about/show.html.haml +++ /dev/null @@ -1,69 +0,0 @@ -- content_for :page_title do - = site_hostname - -- content_for :header_tags do - %link{ rel: 'canonical', href: about_url }/ - = render partial: 'shared/og' - -.landing - .landing__brand - = link_to root_url, class: 'brand' do - = logo_as_symbol(:wordmark) - %span.brand__tagline=t 'about.tagline' - - .landing__grid - .landing__grid__column.landing__grid__column-registration - .box-widget - = render 'registration' - - .directory - .directory__tag - = link_to web_path do - %h4 - = fa_icon 'globe fw' - = t('about.see_whats_happening') - %small= t('about.browse_public_posts') - - .directory__tag - = link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener noreferrer' do - %h4 - = fa_icon 'tablet fw' - = t('about.get_apps') - %small= t('about.apps_platforms') - - .landing__grid__column.landing__grid__column-login - .box-widget - - if current_user.present? - = render 'logged_in' - - else - = render 'login' - - .hero-widget - .hero-widget__img - = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title - - .hero-widget__text - %p - = @instance_presenter.description.html_safe.presence || t('about.about_mastodon_html') - = link_to about_more_path do - = t('about.learn_more') - = fa_icon 'angle-double-right' - - .hero-widget__footer - .hero-widget__footer__column - %h4= t 'about.administered_by' - - = account_link_to @instance_presenter.contact.account - - .hero-widget__footer__column - %h4= t 'about.server_stats' - - .hero-widget__counters__wrapper - .hero-widget__counter - %strong= friendly_number_to_human @instance_presenter.user_count - %span= t 'about.user_count_after', count: @instance_presenter.user_count - .hero-widget__counter - %strong= friendly_number_to_human @instance_presenter.active_user_count - %span - = t 'about.active_count_after' - %abbr{ title: t('about.active_footnote') } * diff --git a/config/locales/en.yml b/config/locales/en.yml index 0f2e08ee7..b41e4f47b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3,38 +3,25 @@ en: about: about_mastodon_html: 'The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!' about_this: About - active_count_after: active - active_footnote: Monthly Active Users (MAU) administered_by: 'Administered by:' api: API apps: Mobile apps - apps_platforms: Use Mastodon from iOS, Android and other platforms - browse_public_posts: Browse a live stream of public posts on Mastodon contact: Contact contact_missing: Not set contact_unavailable: N/A - continue_to_web: Continue to web app documentation: Documentation - federation_hint_html: With an account on %{instance} you'll be able to follow people on any Mastodon server and beyond. - get_apps: Try a mobile app hosted_on: Mastodon hosted on %{domain} instance_actor_flash: | This account is a virtual actor used to represent the server itself and not any individual user. It is used for federation purposes and should not be blocked unless you want to block the whole instance, in which case you should use a domain block. - learn_more: Learn more - logged_in_as_html: You are currently logged in as %{username}. - logout_before_registering: You are already logged in. privacy_policy: Privacy Policy rules: Server rules rules_html: 'Below is a summary of rules you need to follow if you want to have an account on this server of Mastodon:' - see_whats_happening: See what's happening - server_stats: 'Server stats:' source_code: Source code status_count_after: one: post other: posts status_count_before: Who published - tagline: Decentralized social network unavailable_content: Moderated servers unavailable_content_description: domain: Server @@ -1049,7 +1036,6 @@ en: redirecting_to: Your account is inactive because it is currently redirecting to %{acct}. view_strikes: View past strikes against your account too_fast: Form submitted too fast, try again. - trouble_logging_in: Trouble logging in? use_security_key: Use security key authorize_follow: already_following: You are already following this account diff --git a/config/routes.rb b/config/routes.rb index 188898fd0..472e6aa6b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -641,7 +641,7 @@ Rails.application.routes.draw do get '/web/(*any)', to: 'home#index', as: :web - get '/about', to: 'about#show' + get '/about', to: redirect('/') get '/about/more', to: 'about#more' get '/privacy-policy', to: 'privacy#show', as: :privacy_policy diff --git a/spec/controllers/about_controller_spec.rb b/spec/controllers/about_controller_spec.rb index 40e395a64..20069e413 100644 --- a/spec/controllers/about_controller_spec.rb +++ b/spec/controllers/about_controller_spec.rb @@ -3,20 +3,6 @@ require 'rails_helper' RSpec.describe AboutController, type: :controller do render_views - describe 'GET #show' do - before do - get :show - end - - it 'assigns @instance_presenter' do - expect(assigns(:instance_presenter)).to be_kind_of InstancePresenter - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - end - describe 'GET #more' do before do get :more @@ -30,12 +16,4 @@ RSpec.describe AboutController, type: :controller do expect(response).to have_http_status(200) end end - - describe 'helper_method :new_user' do - it 'returns a new User' do - user = @controller.view_context.new_user - expect(user).to be_kind_of User - expect(user.account).to be_kind_of Account - end - end end diff --git a/spec/requests/localization_spec.rb b/spec/requests/localization_spec.rb index 175f02ae9..0bc2786ac 100644 --- a/spec/requests/localization_spec.rb +++ b/spec/requests/localization_spec.rb @@ -10,30 +10,30 @@ describe 'Localization' do it 'uses a specific region when provided' do headers = { 'Accept-Language' => 'zh-HK' } - get "/about", headers: headers + get "/auth/sign_in", headers: headers expect(response.body).to include( - I18n.t('about.tagline', locale: 'zh-HK') + I18n.t('auth.login', locale: 'zh-HK') ) end it 'falls back to a locale when region missing' do headers = { 'Accept-Language' => 'es-FAKE' } - get "/about", headers: headers + get "/auth/sign_in", headers: headers expect(response.body).to include( - I18n.t('about.tagline', locale: 'es') + I18n.t('auth.login', locale: 'es') ) end it 'falls back to english when locale is missing' do headers = { 'Accept-Language' => '12-FAKE' } - get "/about", headers: headers + get "/auth/sign_in", headers: headers expect(response.body).to include( - I18n.t('about.tagline', locale: 'en') + I18n.t('auth.login', locale: 'en') ) end end diff --git a/spec/views/about/show.html.haml_spec.rb b/spec/views/about/show.html.haml_spec.rb deleted file mode 100644 index bf6e19d2b..000000000 --- a/spec/views/about/show.html.haml_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'about/show.html.haml', without_verify_partial_doubles: true do - before do - allow(view).to receive(:site_hostname).and_return('example.com') - allow(view).to receive(:site_title).and_return('example site') - allow(view).to receive(:new_user).and_return(User.new) - allow(view).to receive(:use_seamless_external_login?).and_return(false) - allow(view).to receive(:current_account).and_return(nil) - end - - it 'has valid open graph tags' do - assign(:instance_presenter, InstancePresenter.new) - render - - header_tags = view.content_for(:header_tags) - - expect(header_tags).to match(%r{}) - expect(header_tags).to match(%r{}) - expect(header_tags).to match(%r{}) - expect(header_tags).to match(%r{}) - end -end -- cgit From 93f340a4bf35082968118319448905b489b101a3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 6 Oct 2022 10:16:47 +0200 Subject: Remove setting that disables account deletes (#17683) --- app/controllers/settings/deletes_controller.rb | 5 ----- app/helpers/application_helper.rb | 4 ---- app/models/form/admin_settings.rb | 2 -- app/views/admin/settings/edit.html.haml | 3 --- app/views/auth/registrations/edit.html.haml | 7 +++---- app/views/settings/profiles/show.html.haml | 7 +++---- config/locales/en.yml | 3 --- spec/controllers/settings/deletes_controller_spec.rb | 14 -------------- 8 files changed, 6 insertions(+), 39 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index e0dd5edcb..bb096567a 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -4,7 +4,6 @@ class Settings::DeletesController < Settings::BaseController skip_before_action :require_functional! before_action :require_not_suspended! - before_action :check_enabled_deletion def show @confirmation = Form::DeleteConfirmation.new @@ -21,10 +20,6 @@ class Settings::DeletesController < Settings::BaseController private - def check_enabled_deletion - redirect_to root_path unless Setting.open_deletion - end - def resource_params params.require(:form_delete_confirmation).permit(:password, :username) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 14d27b148..23884fbd4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -87,10 +87,6 @@ module ApplicationHelper link_to label, omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post end - def open_deletion? - Setting.open_deletion - end - def locale_direction if RTL_LOCALES.include?(I18n.locale) 'rtl' diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index e744344c5..7bd9e3743 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -13,7 +13,6 @@ class Form::AdminSettings site_terms registrations_mode closed_registrations_message - open_deletion timeline_preview bootstrap_timeline_accounts theme @@ -37,7 +36,6 @@ class Form::AdminSettings ).freeze BOOLEAN_KEYS = %i( - open_deletion timeline_preview activity_api_enabled peers_api_enabled diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index a00cd0222..79f73a60f 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -57,9 +57,6 @@ .fields-group = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') - .fields-group - = f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html') - - unless whitelist_mode? .fields-group = f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html'), recommended: true diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index a3445b421..df929e3e8 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -41,8 +41,7 @@ %h3= t('migrations.incoming_migrations') %p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path) - - if open_deletion? - %hr.spacer/ + %hr.spacer/ - %h3= t('auth.delete_account') - %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) + %h3= t('auth.delete_account') + %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index fe9666d84..3067b3737 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -70,8 +70,7 @@ %h6= t 'migrations.incoming_migrations' %p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path) -- if open_deletion? - %hr.spacer/ +%hr.spacer/ - %h6= t('auth.delete_account') - %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) +%h6= t('auth.delete_account') +%p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) diff --git a/config/locales/en.yml b/config/locales/en.yml index b41e4f47b..505a2f9fc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -754,9 +754,6 @@ en: closed_message: desc_html: Displayed on frontpage when registrations are closed. You can use HTML tags title: Closed registration message - deletion: - desc_html: Allow anyone to delete their account - title: Open account deletion require_invite_text: desc_html: When registrations require manual approval, make the “Why do you want to join?” text input mandatory rather than optional title: Require new users to enter a reason to join diff --git a/spec/controllers/settings/deletes_controller_spec.rb b/spec/controllers/settings/deletes_controller_spec.rb index cd36ecc35..a94dc042a 100644 --- a/spec/controllers/settings/deletes_controller_spec.rb +++ b/spec/controllers/settings/deletes_controller_spec.rb @@ -81,20 +81,6 @@ describe Settings::DeletesController do expect(response).to redirect_to settings_delete_path end end - - context 'when account deletions are disabled' do - around do |example| - open_deletion = Setting.open_deletion - example.run - Setting.open_deletion = open_deletion - end - - it 'redirects' do - Setting.open_deletion = false - delete :destroy - expect(response).to redirect_to root_path - end - end end context 'when not signed in' do -- cgit From 1bd00036c284bcafb419eaf80347ba49d1b491d9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 13 Oct 2022 14:42:37 +0200 Subject: Change about page to be mounted in the web UI (#19345) --- app/controllers/about_controller.rb | 60 +- .../api/v1/instances/domain_blocks_controller.rb | 23 + .../instances/extended_descriptions_controller.rb | 18 + .../fonts/montserrat/Montserrat-Medium.ttf | Bin 192488 -> 0 bytes .../fonts/montserrat/Montserrat-Regular.ttf | Bin 191860 -> 0 bytes .../fonts/montserrat/Montserrat-Regular.woff | Bin 81244 -> 0 bytes .../fonts/montserrat/Montserrat-Regular.woff2 | Bin 61840 -> 0 bytes app/javascript/mastodon/actions/server.js | 61 ++ app/javascript/mastodon/components/image.js | 33 + .../mastodon/components/server_banner.js | 8 +- app/javascript/mastodon/components/skeleton.js | 4 +- app/javascript/mastodon/features/about/index.js | 195 ++++- .../mastodon/features/privacy_policy/index.js | 2 +- app/javascript/mastodon/features/report/rules.js | 2 +- .../mastodon/features/ui/components/link_footer.js | 2 +- app/javascript/mastodon/reducers/server.js | 46 +- app/javascript/styles/application.scss | 2 - app/javascript/styles/contrast/diff.scss | 4 - app/javascript/styles/fonts/montserrat.scss | 21 - app/javascript/styles/mastodon-light/diff.scss | 86 +- app/javascript/styles/mastodon/about.scss | 902 +-------------------- app/javascript/styles/mastodon/compact_header.scss | 34 - app/javascript/styles/mastodon/components.scss | 474 +++++++++-- app/javascript/styles/mastodon/containers.scss | 1 - app/javascript/styles/mastodon/dashboard.scss | 1 - app/javascript/styles/mastodon/forms.scss | 63 +- app/javascript/styles/mastodon/widgets.scss | 229 +----- app/models/domain_block.rb | 4 +- app/models/extended_description.rb | 15 + app/serializers/rest/domain_block_serializer.rb | 17 + .../rest/extended_description_serializer.rb | 13 + app/views/about/_domain_blocks.html.haml | 12 - app/views/about/more.html.haml | 96 --- app/views/about/show.html.haml | 4 + config/locales/en.yml | 28 +- config/routes.rb | 6 +- spec/controllers/about_controller_spec.rb | 8 +- 37 files changed, 900 insertions(+), 1574 deletions(-) create mode 100644 app/controllers/api/v1/instances/domain_blocks_controller.rb create mode 100644 app/controllers/api/v1/instances/extended_descriptions_controller.rb delete mode 100644 app/javascript/fonts/montserrat/Montserrat-Medium.ttf delete mode 100644 app/javascript/fonts/montserrat/Montserrat-Regular.ttf delete mode 100644 app/javascript/fonts/montserrat/Montserrat-Regular.woff delete mode 100644 app/javascript/fonts/montserrat/Montserrat-Regular.woff2 create mode 100644 app/javascript/mastodon/components/image.js delete mode 100644 app/javascript/styles/fonts/montserrat.scss delete mode 100644 app/javascript/styles/mastodon/compact_header.scss create mode 100644 app/models/extended_description.rb create mode 100644 app/serializers/rest/domain_block_serializer.rb create mode 100644 app/serializers/rest/extended_description_serializer.rb delete mode 100644 app/views/about/_domain_blocks.html.haml delete mode 100644 app/views/about/more.html.haml create mode 100644 app/views/about/show.html.haml (limited to 'spec/controllers') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index eae7de8c8..0fbc6a800 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -1,63 +1,11 @@ # frozen_string_literal: true class AboutController < ApplicationController - include RegistrationSpamConcern + include WebAppControllerConcern - layout 'public' + skip_before_action :require_functional! - before_action :require_open_federation!, only: [:more] - before_action :set_body_classes, only: :show - before_action :set_instance_presenter - before_action :set_expires_in, only: [:more] - - skip_before_action :require_functional!, only: [:more] - - def more - flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor] - - toc_generator = TOCGenerator.new(@instance_presenter.extended_description) - - @rules = Rule.ordered - @contents = toc_generator.html - @table_of_contents = toc_generator.toc - @blocks = DomainBlock.with_user_facing_limitations.by_severity if display_blocks? - end - - helper_method :display_blocks? - helper_method :display_blocks_rationale? - helper_method :public_fetch_mode? - helper_method :new_user - - private - - def require_open_federation! - not_found if whitelist_mode? - end - - def display_blocks? - Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?) - end - - def display_blocks_rationale? - Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?) - end - - def new_user - User.new.tap do |user| - user.build_account - user.build_invite_request - end - end - - def set_instance_presenter - @instance_presenter = InstancePresenter.new - end - - def set_body_classes - @hide_navbar = true - end - - def set_expires_in - expires_in 0, public: true + def show + expires_in 0, public: true unless user_signed_in? end end diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb new file mode 100644 index 000000000..37a6906fb --- /dev/null +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class Api::V1::Instances::DomainBlocksController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + + before_action :require_enabled_api! + before_action :set_domain_blocks + + def index + expires_in 3.minutes, public: true + render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)) + end + + private + + def require_enabled_api! + head 404 unless Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?) + end + + def set_domain_blocks + @domain_blocks = DomainBlock.with_user_facing_limitations.by_severity + end +end diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb new file mode 100644 index 000000000..c72e16cff --- /dev/null +++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + + before_action :set_extended_description + + def show + expires_in 3.minutes, public: true + render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer + end + + private + + def set_extended_description + @extended_description = ExtendedDescription.current + end +end diff --git a/app/javascript/fonts/montserrat/Montserrat-Medium.ttf b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf deleted file mode 100644 index 88d70b89c..000000000 Binary files a/app/javascript/fonts/montserrat/Montserrat-Medium.ttf and /dev/null differ diff --git a/app/javascript/fonts/montserrat/Montserrat-Regular.ttf b/app/javascript/fonts/montserrat/Montserrat-Regular.ttf deleted file mode 100644 index 29ca85d4a..000000000 Binary files a/app/javascript/fonts/montserrat/Montserrat-Regular.ttf and /dev/null differ diff --git a/app/javascript/fonts/montserrat/Montserrat-Regular.woff b/app/javascript/fonts/montserrat/Montserrat-Regular.woff deleted file mode 100644 index af3b5ec44..000000000 Binary files a/app/javascript/fonts/montserrat/Montserrat-Regular.woff and /dev/null differ diff --git a/app/javascript/fonts/montserrat/Montserrat-Regular.woff2 b/app/javascript/fonts/montserrat/Montserrat-Regular.woff2 deleted file mode 100644 index 3d75434dd..000000000 Binary files a/app/javascript/fonts/montserrat/Montserrat-Regular.woff2 and /dev/null differ diff --git a/app/javascript/mastodon/actions/server.js b/app/javascript/mastodon/actions/server.js index af8fef780..31d4aea10 100644 --- a/app/javascript/mastodon/actions/server.js +++ b/app/javascript/mastodon/actions/server.js @@ -5,6 +5,14 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST'; export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; +export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST'; +export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS'; +export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL'; + +export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST'; +export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS'; +export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL'; + export const fetchServer = () => (dispatch, getState) => { dispatch(fetchServerRequest()); @@ -28,3 +36,56 @@ const fetchServerFail = error => ({ type: SERVER_FETCH_FAIL, error, }); + +export const fetchExtendedDescription = () => (dispatch, getState) => { + dispatch(fetchExtendedDescriptionRequest()); + + api(getState) + .get('/api/v1/instance/extended_description') + .then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data))) + .catch(err => dispatch(fetchExtendedDescriptionFail(err))); +}; + +const fetchExtendedDescriptionRequest = () => ({ + type: EXTENDED_DESCRIPTION_REQUEST, +}); + +const fetchExtendedDescriptionSuccess = description => ({ + type: EXTENDED_DESCRIPTION_SUCCESS, + description, +}); + +const fetchExtendedDescriptionFail = error => ({ + type: EXTENDED_DESCRIPTION_FAIL, + error, +}); + +export const fetchDomainBlocks = () => (dispatch, getState) => { + dispatch(fetchDomainBlocksRequest()); + + api(getState) + .get('/api/v1/instance/domain_blocks') + .then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data))) + .catch(err => { + if (err.response.status === 404) { + dispatch(fetchDomainBlocksSuccess(false, [])); + } else { + dispatch(fetchDomainBlocksFail(err)); + } + }); +}; + +const fetchDomainBlocksRequest = () => ({ + type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST, +}); + +const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({ + type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS, + isAvailable, + blocks, +}); + +const fetchDomainBlocksFail = error => ({ + type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL, + error, +}); diff --git a/app/javascript/mastodon/components/image.js b/app/javascript/mastodon/components/image.js new file mode 100644 index 000000000..6e81ddf08 --- /dev/null +++ b/app/javascript/mastodon/components/image.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Blurhash from './blurhash'; +import classNames from 'classnames'; + +export default class Image extends React.PureComponent { + + static propTypes = { + src: PropTypes.string, + srcSet: PropTypes.string, + blurhash: PropTypes.string, + className: PropTypes.string, + }; + + state = { + loaded: false, + }; + + handleLoad = () => this.setState({ loaded: true }); + + render () { + const { src, srcSet, blurhash, className } = this.props; + const { loaded } = this.state; + + return ( +
+ {blurhash && } + +
+ ); + } + +} diff --git a/app/javascript/mastodon/components/server_banner.js b/app/javascript/mastodon/components/server_banner.js index ae4e200c3..c2336e43d 100644 --- a/app/javascript/mastodon/components/server_banner.js +++ b/app/javascript/mastodon/components/server_banner.js @@ -7,13 +7,15 @@ import ShortNumber from 'mastodon/components/short_number'; import Skeleton from 'mastodon/components/skeleton'; import Account from 'mastodon/containers/account_container'; import { domain } from 'mastodon/initial_state'; +import Image from 'mastodon/components/image'; +import { Link } from 'react-router-dom'; const messages = defineMessages({ aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' }, }); const mapStateToProps = state => ({ - server: state.get('server'), + server: state.getIn(['server', 'server']), }); export default @connect(mapStateToProps) @@ -41,7 +43,7 @@ class ServerBanner extends React.PureComponent { {domain}, mastodon: Mastodon }} /> - {server.get('title')} +
{isLoading ? ( @@ -83,7 +85,7 @@ class ServerBanner extends React.PureComponent {
- +
); } diff --git a/app/javascript/mastodon/components/skeleton.js b/app/javascript/mastodon/components/skeleton.js index 09093e99c..6a17ffb26 100644 --- a/app/javascript/mastodon/components/skeleton.js +++ b/app/javascript/mastodon/components/skeleton.js @@ -4,8 +4,8 @@ import PropTypes from 'prop-types'; const Skeleton = ({ width, height }) => ; Skeleton.propTypes = { - width: PropTypes.number, - height: PropTypes.number, + width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }; export default Skeleton; diff --git a/app/javascript/mastodon/features/about/index.js b/app/javascript/mastodon/features/about/index.js index bc8d3a41b..e9212565a 100644 --- a/app/javascript/mastodon/features/about/index.js +++ b/app/javascript/mastodon/features/about/index.js @@ -1,27 +1,214 @@ import React from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import Column from 'mastodon/components/column'; import LinkFooter from 'mastodon/features/ui/components/link_footer'; import { Helmet } from 'react-helmet'; +import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server'; +import Account from 'mastodon/containers/account_container'; +import Skeleton from 'mastodon/components/skeleton'; +import Icon from 'mastodon/components/icon'; +import classNames from 'classnames'; +import Image from 'mastodon/components/image'; const messages = defineMessages({ title: { id: 'column.about', defaultMessage: 'About' }, + rules: { id: 'about.rules', defaultMessage: 'Server rules' }, + blocks: { id: 'about.blocks', defaultMessage: 'Moderated servers' }, + silenced: { id: 'about.domain_blocks.silenced.title', defaultMessage: 'Limited' }, + silencedExplanation: { id: 'about.domain_blocks.silenced.explanation', defaultMessage: 'You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.' }, + suspended: { id: 'about.domain_blocks.suspended.title', defaultMessage: 'Suspended' }, + suspendedExplanation: { id: 'about.domain_blocks.suspended.explanation', defaultMessage: 'No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.' }, }); -export default @injectIntl +const severityMessages = { + silence: { + title: messages.silenced, + explanation: messages.silencedExplanation, + }, + + suspend: { + title: messages.suspended, + explanation: messages.suspendedExplanation, + }, +}; + +const mapStateToProps = state => ({ + server: state.getIn(['server', 'server']), + extendedDescription: state.getIn(['server', 'extendedDescription']), + domainBlocks: state.getIn(['server', 'domainBlocks']), +}); + +class Section extends React.PureComponent { + + static propTypes = { + title: PropTypes.string, + children: PropTypes.node, + open: PropTypes.bool, + onOpen: PropTypes.func, + }; + + state = { + collapsed: !this.props.open, + }; + + handleClick = () => { + const { onOpen } = this.props; + const { collapsed } = this.state; + + this.setState({ collapsed: !collapsed }, () => onOpen && onOpen()); + } + + render () { + const { title, children } = this.props; + const { collapsed } = this.state; + + return ( +
+
+ {title} +
+ + {!collapsed && ( +
{children}
+ )} +
+ ); + } + +} + +export default @connect(mapStateToProps) +@injectIntl class About extends React.PureComponent { static propTypes = { + server: ImmutablePropTypes.map, + extendedDescription: ImmutablePropTypes.map, + domainBlocks: ImmutablePropTypes.contains({ + isLoading: PropTypes.bool, + isAvailable: PropTypes.bool, + items: ImmutablePropTypes.list, + }), + dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchServer()); + dispatch(fetchExtendedDescription()); + } + + handleDomainBlocksOpen = () => { + const { dispatch } = this.props; + dispatch(fetchDomainBlocks()); + } + render () { - const { intl } = this.props; + const { intl, server, extendedDescription, domainBlocks } = this.props; + const isLoading = server.get('isLoading'); return ( - +
+
+ `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> +

{isLoading ? : server.get('domain')}

+

Mastodon }} />

+
+ +
+
+

+ + +
+ +
+ +
+

+ + {isLoading ? : {server.getIn(['contact', 'email'])}} +
+
+ +
+ {extendedDescription.get('isLoading') ? ( + <> + +
+ +
+ +
+ + + ) : (extendedDescription.get('content')?.length > 0 ? ( +
+ ) : ( +

+ ))} +
+ +
+ {!isLoading && (server.get('rules').isEmpty() ? ( +

+ ) : ( +
    + {server.get('rules').map(rule => ( +
  1. + {rule.get('text')} +
  2. + ))} +
+ ))} +
+ +
+ {domainBlocks.get('isLoading') ? ( + <> + +
+ + + ) : (domainBlocks.get('isAvailable') ? ( + <> +

+ + + + + + + + + + + + {domainBlocks.get('items').map(block => ( + + + + + + ))} + +
{block.get('domain')}{intl.formatMessage(severityMessages[block.get('severity')].title)}{block.get('comment')}
+ + ) : ( +

+ ))} +
+ + +
{intl.formatMessage(messages.title)} diff --git a/app/javascript/mastodon/features/privacy_policy/index.js b/app/javascript/mastodon/features/privacy_policy/index.js index 5fbe340c0..eee4255f4 100644 --- a/app/javascript/mastodon/features/privacy_policy/index.js +++ b/app/javascript/mastodon/features/privacy_policy/index.js @@ -44,7 +44,7 @@ class PrivacyPolicy extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/report/rules.js b/app/javascript/mastodon/features/report/rules.js index 2cb4a95b5..920da68d6 100644 --- a/app/javascript/mastodon/features/report/rules.js +++ b/app/javascript/mastodon/features/report/rules.js @@ -7,7 +7,7 @@ import Button from 'mastodon/components/button'; import Option from './components/option'; const mapStateToProps = state => ({ - rules: state.getIn(['server', 'rules']), + rules: state.getIn(['server', 'server', 'rules']), }); export default @connect(mapStateToProps) diff --git a/app/javascript/mastodon/features/ui/components/link_footer.js b/app/javascript/mastodon/features/ui/components/link_footer.js index c4ce2a985..cc3d83572 100644 --- a/app/javascript/mastodon/features/ui/components/link_footer.js +++ b/app/javascript/mastodon/features/ui/components/link_footer.js @@ -51,7 +51,7 @@ class LinkFooter extends React.PureComponent { const items = []; items.push(); - items.push(); + items.push(); items.push(); items.push(); items.push(); diff --git a/app/javascript/mastodon/reducers/server.js b/app/javascript/mastodon/reducers/server.js index 68131c6dd..db9f2b5e6 100644 --- a/app/javascript/mastodon/reducers/server.js +++ b/app/javascript/mastodon/reducers/server.js @@ -1,18 +1,52 @@ -import { SERVER_FETCH_REQUEST, SERVER_FETCH_SUCCESS, SERVER_FETCH_FAIL } from 'mastodon/actions/server'; -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { + SERVER_FETCH_REQUEST, + SERVER_FETCH_SUCCESS, + SERVER_FETCH_FAIL, + EXTENDED_DESCRIPTION_REQUEST, + EXTENDED_DESCRIPTION_SUCCESS, + EXTENDED_DESCRIPTION_FAIL, + SERVER_DOMAIN_BLOCKS_FETCH_REQUEST, + SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS, + SERVER_DOMAIN_BLOCKS_FETCH_FAIL, +} from 'mastodon/actions/server'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap({ - isLoading: true, + server: ImmutableMap({ + isLoading: true, + }), + + extendedDescription: ImmutableMap({ + isLoading: true, + }), + + domainBlocks: ImmutableMap({ + isLoading: true, + isAvailable: true, + items: ImmutableList(), + }), }); export default function server(state = initialState, action) { switch (action.type) { case SERVER_FETCH_REQUEST: - return state.set('isLoading', true); + return state.setIn(['server', 'isLoading'], true); case SERVER_FETCH_SUCCESS: - return fromJS(action.server).set('isLoading', false); + return state.set('server', fromJS(action.server)).setIn(['server', 'isLoading'], false); case SERVER_FETCH_FAIL: - return state.set('isLoading', false); + return state.setIn(['server', 'isLoading'], false); + case EXTENDED_DESCRIPTION_REQUEST: + return state.setIn(['extendedDescription', 'isLoading'], true); + case EXTENDED_DESCRIPTION_SUCCESS: + return state.set('extendedDescription', fromJS(action.description)).setIn(['extendedDescription', 'isLoading'], false); + case EXTENDED_DESCRIPTION_FAIL: + return state.setIn(['extendedDescription', 'isLoading'], false); + case SERVER_DOMAIN_BLOCKS_FETCH_REQUEST: + return state.setIn(['domainBlocks', 'isLoading'], true); + case SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS: + return state.setIn(['domainBlocks', 'items'], fromJS(action.blocks)).setIn(['domainBlocks', 'isLoading'], false).setIn(['domainBlocks', 'isAvailable'], action.isAvailable); + case SERVER_DOMAIN_BLOCKS_FETCH_FAIL: + return state.setIn(['domainBlocks', 'isLoading'], false); default: return state; } diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index bbea06195..e9f596e2f 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -2,7 +2,6 @@ @import 'mastodon/variables'; @import 'fonts/roboto'; @import 'fonts/roboto-mono'; -@import 'fonts/montserrat'; @import 'mastodon/reset'; @import 'mastodon/basics'; @@ -10,7 +9,6 @@ @import 'mastodon/containers'; @import 'mastodon/lists'; @import 'mastodon/footer'; -@import 'mastodon/compact_header'; @import 'mastodon/widgets'; @import 'mastodon/forms'; @import 'mastodon/accounts'; diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss index 841ed6648..22f5bcc94 100644 --- a/app/javascript/styles/contrast/diff.scss +++ b/app/javascript/styles/contrast/diff.scss @@ -13,10 +13,6 @@ } } -.rich-formatting a, -.rich-formatting p a, -.rich-formatting li a, -.landing-page__short-description p a, .status__content a, .reply-indicator__content a { color: lighten($ui-highlight-color, 12%); diff --git a/app/javascript/styles/fonts/montserrat.scss b/app/javascript/styles/fonts/montserrat.scss deleted file mode 100644 index 170fe6542..000000000 --- a/app/javascript/styles/fonts/montserrat.scss +++ /dev/null @@ -1,21 +0,0 @@ -@font-face { - font-family: mastodon-font-display; - src: - local('Montserrat'), - url('../fonts/montserrat/Montserrat-Regular.woff2') format('woff2'), - url('../fonts/montserrat/Montserrat-Regular.woff') format('woff'), - url('../fonts/montserrat/Montserrat-Regular.ttf') format('truetype'); - font-weight: 400; - font-display: swap; - font-style: normal; -} - -@font-face { - font-family: mastodon-font-display; - src: - local('Montserrat Medium'), - url('../fonts/montserrat/Montserrat-Medium.ttf') format('truetype'); - font-weight: 500; - font-display: swap; - font-style: normal; -} diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 0bc6247ef..4b27e6b4f 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -36,6 +36,20 @@ html { border-top: 0; } +.column > .scrollable.about { + border-top: 1px solid lighten($ui-base-color, 8%); +} + +.about__meta, +.about__section__title { + background: $white; + border: 1px solid lighten($ui-base-color, 8%); +} + +.rules-list li::before { + background: $ui-highlight-color; +} + .directory__card__img { background: lighten($ui-base-color, 12%); } @@ -45,10 +59,6 @@ html { border-bottom: 1px solid lighten($ui-base-color, 8%); } -.table-of-contents { - border: 1px solid lighten($ui-base-color, 8%); -} - .column-back-button, .column-header { background: $white; @@ -138,11 +148,6 @@ html { .compose-form__poll-wrapper select, .search__input, .setting-text, -.box-widget input[type="text"], -.box-widget input[type="email"], -.box-widget input[type="password"], -.box-widget textarea, -.statuses-grid .detailed-status, .report-dialog-modal__textarea, .audio-player { border: 1px solid lighten($ui-base-color, 8%); @@ -480,52 +485,16 @@ html { background: $white; } -.tabs-bar { - background: $white; - border: 1px solid lighten($ui-base-color, 8%); - border-bottom: 0; - - @media screen and (max-width: $no-gap-breakpoint) { - border-top: 0; - } - - &__link { - padding-bottom: 14px; - border-bottom-width: 1px; - border-bottom-color: lighten($ui-base-color, 8%); - - &:hover, - &:active, - &:focus { - background: $ui-base-color; - } - - &.active { - &:hover, - &:active, - &:focus { - background: transparent; - border-bottom-color: $ui-highlight-color; - } - } - } -} - // Change the default colors used on some parts of the profile pages .activity-stream-tabs { background: $account-background-color; border-bottom-color: lighten($ui-base-color, 8%); } -.box-widget, .nothing-here, .page-header, .directory__tag > a, -.directory__tag > div, -.landing-page__call-to-action, -.contact-widget, -.landing .hero-widget__text, -.landing-page__information.contact-widget { +.directory__tag > div { background: $white; border: 1px solid lighten($ui-base-color, 8%); @@ -536,11 +505,6 @@ html { } } -.landing .hero-widget__text { - border-top: 0; - border-bottom: 0; -} - .simple_form { input[type="text"], input[type="number"], @@ -553,26 +517,12 @@ html { } } -.landing .hero-widget__footer { - background: $white; - border: 1px solid lighten($ui-base-color, 8%); - border-top: 0; - - @media screen and (max-width: $no-gap-breakpoint) { - border: 0; - } -} - .picture-in-picture-placeholder { background: $white; border-color: lighten($ui-base-color, 8%); color: lighten($ui-base-color, 8%); } -.brand__tagline { - color: $ui-secondary-color; -} - .directory__tag > a { &:hover, &:active, @@ -666,8 +616,7 @@ html { } } -.simple_form, -.table-form { +.simple_form { .warning { box-shadow: none; background: rgba($error-red, 0.5); @@ -801,9 +750,6 @@ html { } .hero-widget, -.box-widget, -.contact-widget, -.landing-page__information.contact-widget, .moved-account-widget, .memoriam-widget, .activity-stream, diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index 8893e3cf0..0183c43d5 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -1,7 +1,5 @@ $maximum-width: 1235px; $fluid-breakpoint: $maximum-width + 20px; -$column-breakpoint: 700px; -$small-breakpoint: 960px; .container { box-sizing: border-box; @@ -15,892 +13,44 @@ $small-breakpoint: 960px; } } -.rich-formatting { - font-family: $font-sans-serif, sans-serif; - font-size: 14px; - font-weight: 400; - line-height: 1.7; - word-wrap: break-word; - color: $darker-text-color; - - a { - color: $highlight-text-color; - text-decoration: underline; - - &:hover, - &:focus, - &:active { - text-decoration: none; - } - } - - p, - li { - color: $darker-text-color; - } - - p { - margin-top: 0; - margin-bottom: 0.85em; - - &:last-child { - margin-bottom: 0; - } - } - - strong { - font-weight: 700; - 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: 0.85em; - font-weight: 500; - color: $secondary-text-color; - } - - h1 { - font-size: 2em; - } - - h2 { - font-size: 1.75em; - } - - h3 { - font-size: 1.5em; - } - - h4 { - font-size: 1.25em; - } - - h5, - h6 { - font-size: 1em; - } - - ul { - list-style: disc; - } - - ol { - list-style: decimal; - } - - 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 lighten($ui-base-color, 4%); - margin: 1.7em 0; - - &.spacer { - height: 1px; - border: 0; - } - } - - table { - width: 100%; - border-collapse: collapse; - break-inside: auto; - margin-top: 24px; - margin-bottom: 32px; - - thead tr, - tbody tr { - border-bottom: 1px solid lighten($ui-base-color, 4%); - font-size: 1em; - line-height: 1.625; - font-weight: 400; - text-align: left; - color: $darker-text-color; - } - - thead tr { - border-bottom-width: 2px; - line-height: 1.5; - font-weight: 500; - color: $dark-text-color; - } - - th, - td { - padding: 8px; - align-self: flex-start; - align-items: flex-start; - word-break: break-all; - - &.nowrap { - width: 25%; - position: relative; - - &::before { - content: ' '; - visibility: hidden; - } - - span { - position: absolute; - left: 8px; - right: 8px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - } - - & > :first-child { - margin-top: 0; - } +.brand { + position: relative; + text-decoration: none; } -.information-board { - background: darken($ui-base-color, 4%); - padding: 20px 0; - - .container-alt { - position: relative; - padding-right: 280px + 15px; - } - - &__sections { - display: flex; - justify-content: space-between; - flex-wrap: wrap; - } - - &__section { - flex: 1 0 0; - font-family: $font-sans-serif, sans-serif; - font-size: 16px; - line-height: 28px; - color: $primary-text-color; - text-align: right; - padding: 10px 15px; - - span, - strong { - display: block; - } - - span { - &:last-child { - color: $secondary-text-color; - } - } - - strong { - font-family: $font-display, sans-serif; - font-weight: 500; - font-size: 32px; - line-height: 48px; - } - - @media screen and (max-width: $column-breakpoint) { - text-align: center; - } - } - - .panel { - position: absolute; - width: 280px; - box-sizing: border-box; - background: darken($ui-base-color, 8%); - padding: 20px; - padding-top: 10px; - border-radius: 4px 4px 0 0; - right: 0; - bottom: -40px; - - .panel-header { - font-family: $font-display, sans-serif; - font-size: 14px; - line-height: 24px; - font-weight: 500; - color: $darker-text-color; - padding-bottom: 5px; - margin-bottom: 15px; - border-bottom: 1px solid lighten($ui-base-color, 4%); - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - - a, - span { - font-weight: 400; - color: darken($darker-text-color, 10%); - } - - a { - text-decoration: none; - } - } - } - - .owner { - text-align: center; - - .avatar { - width: 80px; - height: 80px; - margin: 0 auto; - margin-bottom: 15px; - - img { - display: block; - width: 80px; - height: 80px; - border-radius: 48px; - } - } - - .name { - font-size: 14px; - - a { - display: block; - color: $primary-text-color; - text-decoration: none; - - &:hover { - .display_name { - text-decoration: underline; - } - } - } - - .username { - display: block; - color: $darker-text-color; - } - } - } -} +.rules-list { + font-size: 15px; + line-height: 22px; + color: $primary-text-color; + counter-reset: list-counter; -.landing-page { - p, li { - font-family: $font-sans-serif, sans-serif; - font-size: 16px; - font-weight: 400; - line-height: 30px; - margin-bottom: 12px; - color: $darker-text-color; - - a { - color: $highlight-text-color; - text-decoration: underline; - } - } - - em { - display: inline; - margin: 0; - padding: 0; - font-weight: 700; - background: transparent; - font-family: inherit; - font-size: inherit; - line-height: inherit; - color: lighten($darker-text-color, 10%); - } - - 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%); - } - } - - h2 { - font-family: $font-display, sans-serif; - font-size: 22px; - line-height: 26px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; - } - - h3 { - font-family: $font-display, sans-serif; - font-size: 18px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; - } - - 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; - } - - h6 { - font-family: $font-display, sans-serif; - font-size: 12px; - line-height: 24px; + position: relative; + border-bottom: 1px solid lighten($ui-base-color, 8%); + padding: 1em 1.75em; + padding-left: 3em; 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 { - 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, 0.6); - margin: 20px 0; - - &.spacer { - height: 1px; - border: 0; - } - } - - &__information, - &__forms { - padding: 20px; - } - - &__call-to-action { - background: $ui-base-color; - border-radius: 4px; - padding: 25px 40px; - overflow: hidden; - box-sizing: border-box; - - .row { - width: 100%; - display: flex; - flex-direction: row-reverse; - flex-wrap: nowrap; - justify-content: space-between; - align-items: center; - } - - .row__information-board { - display: flex; - justify-content: flex-end; - align-items: flex-end; - - .information-board__section { - flex: 1 0 auto; - padding: 0 10px; - } - - @media screen and (max-width: $no-gap-breakpoint) { - width: 100%; - justify-content: space-between; - } - } - - .row__mascot { - flex: 1; - margin: 10px -50px 0 0; - - @media screen and (max-width: $no-gap-breakpoint) { - display: none; - } - } - } - - &__logo { - margin-right: 20px; - - img { - height: 50px; - width: auto; - mix-blend-mode: lighten; - } - } - - &__information { - padding: 45px 40px; - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } - - strong { + counter-increment: list-counter; + + &::before { + content: counter(list-counter); + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + background: $highlight-text-color; + color: $ui-base-color; + border-radius: 50%; + width: 4ch; + height: 4ch; font-weight: 500; - color: lighten($darker-text-color, 10%); - } - - .account { - border-bottom: 0; - padding: 0; - - &__display-name { - align-items: center; - display: flex; - margin-right: 5px; - } - - div.account__display-name { - &:hover { - .display-name strong { - text-decoration: none; - } - } - - .account__avatar { - cursor: default; - } - } - - &__avatar-wrapper { - margin-left: 0; - flex: 0 0 auto; - } - - .display-name { - font-size: 15px; - - &__account { - font-size: 14px; - } - } - } - - @media screen and (max-width: $small-breakpoint) { - .contact { - margin-top: 30px; - } - } - - @media screen and (max-width: $column-breakpoint) { - padding: 25px 20px; - } - } - - &__information, - &__forms, - #mastodon-timeline { - box-sizing: border-box; - background: $ui-base-color; - border-radius: 4px; - box-shadow: 0 0 6px rgba($black, 0.1); - } - - &__mascot { - height: 104px; - position: relative; - left: -40px; - bottom: 25px; - - img { - height: 190px; - width: auto; - } - } - - &__short-description { - .row { display: flex; - flex-wrap: wrap; + justify-content: center; align-items: center; - margin-bottom: 40px; - } - - @media screen and (max-width: $column-breakpoint) { - .row { - margin-bottom: 20px; - } - } - - p a { - color: $secondary-text-color; - } - - h1 { - font-weight: 500; - color: $primary-text-color; - margin-bottom: 0; - - small { - color: $darker-text-color; - - span { - color: $secondary-text-color; - } - } - } - - p:last-child { - margin-bottom: 0; - } - } - - &__hero { - margin-bottom: 10px; - - img { - display: block; - margin: 0; - max-width: 100%; - height: auto; - border-radius: 4px; - } - } - - @media screen and (max-width: 840px) { - .information-board { - .container-alt { - padding-right: 20px; - } - - .panel { - position: static; - margin-top: 20px; - width: 100%; - border-radius: 4px; - - .panel-header { - text-align: center; - } - } - } - } - - @media screen and (max-width: 675px) { - .header-wrapper { - padding-top: 0; - - &.compact { - padding-bottom: 0; - } - - &.compact .hero .heading { - text-align: initial; - } } - .header .container-alt, - .features .container-alt { - display: block; - } - } - - .cta { - margin: 20px; - } -} - -.landing { - margin-bottom: 100px; - - @media screen and (max-width: 738px) { - margin-bottom: 0; - } - - &__brand { - display: flex; - justify-content: center; - align-items: center; - padding: 50px; - - .logo { - fill: $primary-text-color; - height: 52px; - } - - @media screen and (max-width: $no-gap-breakpoint) { - padding: 0; - margin-bottom: 30px; - } - } - - .directory { - margin-top: 30px; - background: transparent; - box-shadow: none; - border-radius: 0; - } - - .hero-widget { - margin-top: 30px; - margin-bottom: 0; - - h4 { - padding: 10px; - text-transform: uppercase; - font-weight: 700; - font-size: 13px; - color: $darker-text-color; - } - - &__text { - border-radius: 0; - padding-bottom: 0; - } - - &__footer { - background: $ui-base-color; - padding: 10px; - border-radius: 0 0 4px 4px; - display: flex; - - &__column { - flex: 1 1 50%; - overflow-x: hidden; - } - } - - .account { - padding: 10px 0; - border-bottom: 0; - - .account__display-name { - display: flex; - align-items: center; - } - } - - &__counters__wrapper { - display: flex; - } - - &__counter { - padding: 10px; - width: 50%; - - strong { - font-family: $font-display, sans-serif; - font-size: 15px; - font-weight: 700; - display: block; - } - - span { - font-size: 14px; - color: $darker-text-color; - } - } - } - - .simple_form .user_agreement .label_input > label { - font-weight: 400; - color: $darker-text-color; - } - - .simple_form p.lead { - color: $darker-text-color; - font-size: 15px; - line-height: 20px; - font-weight: 400; - margin-bottom: 25px; - } - - &__grid { - max-width: 960px; - margin: 0 auto; - display: grid; - grid-template-columns: minmax(0, 50%) minmax(0, 50%); - grid-gap: 30px; - - @media screen and (max-width: 738px) { - grid-template-columns: minmax(0, 100%); - grid-gap: 10px; - - &__column-login { - grid-row: 1; - display: flex; - flex-direction: column; - - .box-widget { - order: 2; - flex: 0 0 auto; - } - - .hero-widget { - margin-top: 0; - margin-bottom: 10px; - order: 1; - flex: 0 0 auto; - } - } - - &__column-registration { - grid-row: 2; - } - - .directory { - margin-top: 10px; - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - grid-gap: 0; - - .hero-widget { - display: block; - margin-bottom: 0; - box-shadow: none; - - &__img, - &__img img, - &__footer { - border-radius: 0; - } - } - - .hero-widget, - .box-widget, - .directory__tag { - border-bottom: 1px solid lighten($ui-base-color, 8%); - } - - .directory { - margin-top: 0; - - &__tag { - margin-bottom: 0; - - & > a, - & > div { - border-radius: 0; - box-shadow: none; - } - - &:last-child { - border-bottom: 0; - } - } - } - } - } -} - -.brand { - position: relative; - text-decoration: none; -} - -.brand__tagline { - display: block; - position: absolute; - bottom: -10px; - left: 50px; - width: 300px; - color: $ui-primary-color; - text-decoration: none; - font-size: 14px; - - @media screen and (max-width: $no-gap-breakpoint) { - position: static; - width: auto; - margin-top: 20px; - color: $dark-text-color; - } -} - -.rules-list { - background: darken($ui-base-color, 2%); - border: 1px solid darken($ui-base-color, 8%); - border-radius: 4px; - padding: 0.5em 2.5em !important; - margin-top: 1.85em !important; - - li { - border-bottom: 1px solid lighten($ui-base-color, 4%); - color: $dark-text-color; - padding: 1em; - &:last-child { border-bottom: 0; } } - - &__text { - color: $primary-text-color; - } } diff --git a/app/javascript/styles/mastodon/compact_header.scss b/app/javascript/styles/mastodon/compact_header.scss deleted file mode 100644 index 4980ab5f1..000000000 --- a/app/javascript/styles/mastodon/compact_header.scss +++ /dev/null @@ -1,34 +0,0 @@ -.compact-header { - h1 { - font-size: 24px; - line-height: 28px; - color: $darker-text-color; - font-weight: 500; - margin-bottom: 20px; - padding: 0 10px; - word-wrap: break-word; - - @media screen and (max-width: 740px) { - text-align: center; - padding: 20px 10px 0; - } - - a { - color: inherit; - text-decoration: none; - } - - small { - font-weight: 400; - color: $secondary-text-color; - } - - img { - display: inline-block; - margin-bottom: -5px; - margin-right: 15px; - width: 36px; - height: 36px; - } - } -} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index a8919b9cb..d4657d180 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3261,6 +3261,7 @@ $ui-header-height: 55px; padding: 10px; padding-top: 20px; z-index: 1; + font-size: 13px; ul { margin-bottom: 10px; @@ -3272,7 +3273,6 @@ $ui-header-height: 55px; p { color: $dark-text-color; - font-size: 13px; margin-bottom: 20px; a { @@ -8266,79 +8266,202 @@ noscript { &__body { margin-top: 20px; - color: $secondary-text-color; - font-size: 15px; - line-height: 22px; + } +} - h1, - p, - ul, - ol { - margin-bottom: 20px; - } +.prose { + color: $secondary-text-color; + font-size: 15px; + line-height: 22px; - ul { - list-style: disc; - } + p, + ul, + ol { + margin-top: 1.25em; + margin-bottom: 1.25em; + } - ol { - list-style: decimal; - } + img { + margin-top: 2em; + margin-bottom: 2em; + } - ul, - ol { - padding-left: 1em; + video { + margin-top: 2em; + margin-bottom: 2em; + } + + figure { + margin-top: 2em; + margin-bottom: 2em; + + figcaption { + font-size: 0.875em; + line-height: 1.4285714; + margin-top: 0.8571429em; } + } - li { - margin-bottom: 10px; + figure > * { + margin-top: 0; + margin-bottom: 0; + } - &::marker { - color: $darker-text-color; - } + h1 { + font-size: 1.5em; + margin-top: 0; + margin-bottom: 1em; + line-height: 1.33; + } - &:last-child { - margin-bottom: 0; - } - } + h2 { + font-size: 1.25em; + margin-top: 1.6em; + margin-bottom: 0.6em; + line-height: 1.6; + } + + h3, + h4, + h5, + h6 { + margin-top: 1.5em; + margin-bottom: 0.5em; + line-height: 1.5; + } - h1 { - color: $primary-text-color; - font-size: 19px; - line-height: 24px; - font-weight: 700; - margin-top: 30px; + ol { + counter-reset: list-counter; + } - &:first-child { - margin-top: 0; - } - } + li { + margin-top: 0.5em; + margin-bottom: 0.5em; + } - strong { - font-weight: 700; - color: $primary-text-color; - } + ol > li { + counter-increment: list-counter; - em { - font-style: italic; + &::before { + content: counter(list-counter) "."; + position: absolute; + left: 0; } + } - a { - color: $highlight-text-color; - text-decoration: underline; + ul > li::before { + content: ""; + position: absolute; + background-color: $darker-text-color; + border-radius: 50%; + width: 0.375em; + height: 0.375em; + top: 0.5em; + left: 0.25em; + } - &:focus, - &:hover, - &:active { - text-decoration: none; - } - } + ul > li, + ol > li { + position: relative; + padding-left: 1.75em; + } - hr { - border: 1px solid lighten($ui-base-color, 4%); - margin: 30px 0; + & > ul > li p { + margin-top: 0.75em; + margin-bottom: 0.75em; + } + + & > ul > li > *:first-child { + margin-top: 1.25em; + } + + & > ul > li > *:last-child { + margin-bottom: 1.25em; + } + + & > ol > li > *:first-child { + margin-top: 1.25em; + } + + & > ol > li > *:last-child { + margin-bottom: 1.25em; + } + + ul ul, + ul ol, + ol ul, + ol ol { + margin-top: 0.75em; + margin-bottom: 0.75em; + } + + h1, + h2, + h3, + h4, + h5, + h6, + strong, + b { + color: $primary-text-color; + font-weight: 700; + } + + em, + i { + font-style: italic; + } + + a { + color: $highlight-text-color; + text-decoration: underline; + + &:focus, + &:hover, + &:active { + text-decoration: none; } } + + code { + font-size: 0.875em; + background: darken($ui-base-color, 8%); + border-radius: 4px; + padding: 0.2em 0.3em; + } + + hr { + border: 0; + border-top: 1px solid lighten($ui-base-color, 4%); + margin-top: 3em; + margin-bottom: 3em; + } + + hr + * { + margin-top: 0; + } + + h2 + * { + margin-top: 0; + } + + h3 + * { + margin-top: 0; + } + + h4 + *, + h5 + *, + h6 + * { + margin-top: 0; + } + + & > :first-child { + margin-top: 0; + } + + & > :last-child { + margin-bottom: 0; + } } .dismissable-banner { @@ -8365,3 +8488,242 @@ noscript { justify-content: center; } } + +.image { + position: relative; + overflow: hidden; + + &__preview { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + } + + &.loaded &__preview { + display: none; + } + + img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + border: 0; + background: transparent; + opacity: 0; + } + + &.loaded img { + opacity: 1; + } +} + +.about { + padding: 20px; + + @media screen and (min-width: $no-gap-breakpoint) { + border-radius: 4px; + } + + &__header { + margin-bottom: 30px; + + &__hero { + width: 100%; + height: auto; + aspect-ratio: 1.9; + background: lighten($ui-base-color, 4%); + border-radius: 8px; + margin-bottom: 30px; + } + + h1, + p { + text-align: center; + } + + h1 { + font-size: 24px; + line-height: 1.5; + font-weight: 700; + margin-bottom: 10px; + } + + p { + font-size: 16px; + line-height: 24px; + font-weight: 400; + color: $darker-text-color; + } + } + + &__meta { + background: lighten($ui-base-color, 4%); + border-radius: 4px; + display: flex; + margin-bottom: 30px; + font-size: 15px; + + &__column { + box-sizing: border-box; + width: 50%; + padding: 20px; + } + + &__divider { + width: 0; + border: 0; + border-style: solid; + border-color: lighten($ui-base-color, 8%); + border-left-width: 1px; + min-height: calc(100% - 60px); + flex: 0 0 auto; + } + + h4 { + font-size: 15px; + text-transform: uppercase; + color: $darker-text-color; + font-weight: 500; + margin-bottom: 20px; + } + + @media screen and (max-width: 600px) { + display: block; + + h4 { + text-align: center; + } + + &__column { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + } + + &__divider { + min-height: 0; + width: 100%; + border-left-width: 0; + border-top-width: 1px; + } + } + + .layout-multiple-columns & { + display: block; + + h4 { + text-align: center; + } + + &__column { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + } + + &__divider { + min-height: 0; + width: 100%; + border-left-width: 0; + border-top-width: 1px; + } + } + } + + &__mail { + color: $primary-text-color; + text-decoration: none; + font-weight: 500; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + + .getting-started__footer { + padding: 0; + margin-top: 60px; + text-align: center; + font-size: 15px; + line-height: 22px; + + @media screen and (min-width: $no-gap-breakpoint) { + display: none; + } + } + + .account { + padding: 0; + border: 0; + } + + .account__avatar-wrapper { + margin-left: 0; + } + + .account__relationship { + display: none; + } + + &__section { + margin-bottom: 10px; + + &__title { + font-size: 17px; + font-weight: 600; + line-height: 22px; + padding: 20px; + border-radius: 4px; + background: lighten($ui-base-color, 4%); + color: $highlight-text-color; + cursor: pointer; + } + + &.active &__title { + border-radius: 4px 4px 0 0; + } + + &__body { + border: 1px solid lighten($ui-base-color, 4%); + border-top: 0; + padding: 20px; + font-size: 15px; + line-height: 22px; + } + } + + &__domain-blocks { + margin-top: 30px; + width: 100%; + border-collapse: collapse; + break-inside: auto; + + th { + text-align: left; + font-weight: 500; + color: $darker-text-color; + } + + thead tr, + tbody tr { + border-bottom: 1px solid lighten($ui-base-color, 8%); + } + + tbody tr:last-child { + border-bottom: 0; + } + + th, + td { + padding: 8px; + } + } +} diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 01ee56219..8e5ed03f0 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -30,7 +30,6 @@ outline: 0; padding: 12px 16px; line-height: 32px; - font-family: $font-display, sans-serif; font-weight: 500; font-size: 14px; } diff --git a/app/javascript/styles/mastodon/dashboard.scss b/app/javascript/styles/mastodon/dashboard.scss index c21fc9eba..f25765d1d 100644 --- a/app/javascript/styles/mastodon/dashboard.scss +++ b/app/javascript/styles/mastodon/dashboard.scss @@ -38,7 +38,6 @@ font-weight: 500; font-size: 24px; color: $primary-text-color; - font-family: $font-display, sans-serif; margin-bottom: 20px; line-height: 30px; } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 3d67f3b56..69a0b22d6 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -139,18 +139,9 @@ code { } .rules-list { - list-style: decimal; font-size: 17px; line-height: 22px; - font-weight: 500; - background: transparent; - border: 0; - padding: 0.5em 1em !important; margin-bottom: 30px; - - li { - border-color: lighten($ui-base-color, 8%); - } } .hint { @@ -603,41 +594,6 @@ code { } } } - - &__overlay-area { - position: relative; - - &__blurred form { - filter: blur(2px); - } - - &__overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - background: rgba($ui-base-color, 0.65); - border-radius: 4px; - margin-left: -4px; - margin-top: -4px; - padding: 4px; - - &__content { - text-align: center; - - &.rich-formatting { - &, - p { - color: $primary-text-color; - } - } - } - } - } } .block-icon { @@ -908,24 +864,7 @@ code { } } -.table-form { - p { - margin-bottom: 15px; - - strong { - font-weight: 500; - - @each $lang in $cjk-langs { - &:lang(#{$lang}) { - font-weight: 700; - } - } - } - } -} - -.simple_form, -.table-form { +.simple_form { .warning { box-sizing: border-box; background: rgba($error-value-color, 0.5); diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index 43284eb48..260a97c6d 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -112,13 +112,6 @@ } } -.box-widget { - padding: 20px; - border-radius: 4px; - background: $ui-base-color; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); -} - .placeholder-widget { padding: 16px; border-radius: 4px; @@ -128,47 +121,6 @@ margin-bottom: 10px; } -.contact-widget { - min-height: 100%; - font-size: 15px; - color: $darker-text-color; - line-height: 20px; - word-wrap: break-word; - font-weight: 400; - padding: 0; - - h4 { - padding: 10px; - text-transform: uppercase; - font-weight: 700; - font-size: 13px; - color: $darker-text-color; - } - - .account { - border-bottom: 0; - padding: 10px 0; - padding-top: 5px; - } - - & > a { - display: inline-block; - padding: 10px; - padding-top: 0; - color: $darker-text-color; - text-decoration: none; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &:hover, - &:focus, - &:active { - text-decoration: underline; - } - } -} - .moved-account-widget { padding: 15px; padding-bottom: 20px; @@ -249,37 +201,6 @@ margin-bottom: 10px; } -.page-header { - background: lighten($ui-base-color, 8%); - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - border-radius: 4px; - padding: 60px 15px; - text-align: center; - margin: 10px 0; - - h1 { - color: $primary-text-color; - font-size: 36px; - line-height: 1.1; - font-weight: 700; - margin-bottom: 10px; - } - - p { - font-size: 15px; - color: $darker-text-color; - } - - @media screen and (max-width: $no-gap-breakpoint) { - margin-top: 0; - background: lighten($ui-base-color, 4%); - - h1 { - font-size: 24px; - } - } -} - .directory { background: $ui-base-color; border-radius: 4px; @@ -366,34 +287,6 @@ } } -.avatar-stack { - display: flex; - justify-content: flex-end; - - .account__avatar { - flex: 0 0 auto; - width: 36px; - height: 36px; - border-radius: 50%; - position: relative; - margin-left: -10px; - background: darken($ui-base-color, 8%); - border: 2px solid $ui-base-color; - - &:nth-child(1) { - z-index: 1; - } - - &:nth-child(2) { - z-index: 2; - } - - &:nth-child(3) { - z-index: 3; - } - } -} - .accounts-table { width: 100%; @@ -495,11 +388,7 @@ .moved-account-widget, .memoriam-widget, -.box-widget, -.contact-widget, -.landing-page__information.contact-widget, -.directory, -.page-header { +.directory { @media screen and (max-width: $no-gap-breakpoint) { margin-bottom: 0; box-shadow: none; @@ -507,88 +396,6 @@ } } -$maximum-width: 1235px; -$fluid-breakpoint: $maximum-width + 20px; - -.statuses-grid { - min-height: 600px; - - @media screen and (max-width: 640px) { - width: 100% !important; // Masonry layout is unnecessary at this width - } - - &__item { - width: math.div(960px - 20px, 3); - - @media screen and (max-width: $fluid-breakpoint) { - width: math.div(940px - 20px, 3); - } - - @media screen and (max-width: 640px) { - width: 100%; - } - - @media screen and (max-width: $no-gap-breakpoint) { - width: 100vw; - } - } - - .detailed-status { - border-radius: 4px; - - @media screen and (max-width: $no-gap-breakpoint) { - border-top: 1px solid lighten($ui-base-color, 16%); - } - - &.compact { - .detailed-status__meta { - margin-top: 15px; - } - - .status__content { - font-size: 15px; - line-height: 20px; - - .emojione { - width: 20px; - height: 20px; - margin: -3px 0 0; - } - - .status__content__spoiler-link { - line-height: 20px; - margin: 0; - } - } - - .media-gallery, - .status-card, - .video-player { - margin-top: 15px; - } - } - } -} - -.notice-widget { - margin-bottom: 10px; - color: $darker-text-color; - - p { - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } - } - - a { - font-size: 14px; - line-height: 20px; - } -} - -.notice-widget, .placeholder-widget { a { text-decoration: none; @@ -602,37 +409,3 @@ $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; - 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/models/domain_block.rb b/app/models/domain_block.rb index b08687787..ad1dc2a38 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -28,8 +28,8 @@ class DomainBlock < ApplicationRecord delegate :count, to: :accounts, prefix: true 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')) } + scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]) } + scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), domain')) } def to_log_human_identifier domain diff --git a/app/models/extended_description.rb b/app/models/extended_description.rb new file mode 100644 index 000000000..6e5c0d1b6 --- /dev/null +++ b/app/models/extended_description.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ExtendedDescription < ActiveModelSerializers::Model + attributes :updated_at, :text + + def self.current + custom = Setting.find_by(var: 'site_extended_description') + + if custom&.value.present? + new(text: custom.value, updated_at: custom.updated_at) + else + new + end + end +end diff --git a/app/serializers/rest/domain_block_serializer.rb b/app/serializers/rest/domain_block_serializer.rb new file mode 100644 index 000000000..678463e13 --- /dev/null +++ b/app/serializers/rest/domain_block_serializer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class REST::DomainBlockSerializer < ActiveModel::Serializer + attributes :domain, :digest, :severity, :comment + + def domain + object.public_domain + end + + def digest + object.domain_digest + end + + def comment + object.public_comment if instance_options[:with_comment] + end +end diff --git a/app/serializers/rest/extended_description_serializer.rb b/app/serializers/rest/extended_description_serializer.rb new file mode 100644 index 000000000..0c3649033 --- /dev/null +++ b/app/serializers/rest/extended_description_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class REST::ExtendedDescriptionSerializer < ActiveModel::Serializer + attributes :updated_at, :content + + def updated_at + object.updated_at&.iso8601 + end + + def content + object.text + end +end diff --git a/app/views/about/_domain_blocks.html.haml b/app/views/about/_domain_blocks.html.haml deleted file mode 100644 index 35a30f16e..000000000 --- a/app/views/about/_domain_blocks.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%table - %thead - %tr - %th= t('about.unavailable_content_description.domain') - %th= t('about.unavailable_content_description.reason') - %tbody - - domain_blocks.each do |domain_block| - %tr - %td.nowrap - %span{ title: "SHA-256: #{domain_block.domain_digest}" }= domain_block.public_domain - %td - = domain_block.public_comment if display_blocks_rationale? diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml deleted file mode 100644 index a5a10b620..000000000 --- a/app/views/about/more.html.haml +++ /dev/null @@ -1,96 +0,0 @@ -- content_for :page_title do - = site_hostname - -- content_for :header_tags do - = javascript_pack_tag 'public', crossorigin: 'anonymous' - = render partial: 'shared/og' - -.grid-4 - .column-0 - .public-account-header.public-account-header--no-bar - .public-account-header__image - = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title, class: 'parallax' - - .column-1 - .landing-page__call-to-action{ dir: 'ltr' } - .row - .row__information-board - .information-board__section - %span= t 'about.user_count_before' - %strong= friendly_number_to_human @instance_presenter.user_count - %span= t 'about.user_count_after', count: @instance_presenter.user_count - .information-board__section - %span= t 'about.status_count_before' - %strong= friendly_number_to_human @instance_presenter.status_count - %span= t 'about.status_count_after', count: @instance_presenter.status_count - .row__mascot - .landing-page__mascot - = image_tag @instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'), alt: '' - - .column-2 - .contact-widget - %h4= t 'about.administered_by' - - = account_link_to(@instance_presenter.contact.account) - - - if @instance_presenter.contact.email.present? - %h4 - = succeed ':' do - = t 'about.contact' - - = mail_to @instance_presenter.contact.email, nil, title: @instance_presenter.contact.email - - .column-3 - = render 'application/flashes' - - - if @contents.blank? && @rules.empty? && (!display_blocks? || @blocks&.empty?) - = nothing_here - - else - .box-widget - .rich-formatting - - unless @rules.empty? - %h2#rules= t('about.rules') - - %p= t('about.rules_html') - - %ol.rules-list - - @rules.each do |rule| - %li - .rules-list__text= rule.text - - = @contents.html_safe - - - if display_blocks? && !@blocks.empty? - %h2#unavailable-content= t('about.unavailable_content') - - %p= t('about.unavailable_content_html') - - - if (blocks = @blocks.select(&:reject_media?)) && !blocks.empty? - %h3= t('about.unavailable_content_description.rejecting_media_title') - %p= t('about.unavailable_content_description.rejecting_media') - = render partial: 'domain_blocks', locals: { domain_blocks: blocks } - - if (blocks = @blocks.select(&:silence?)) && !blocks.empty? - %h3= t('about.unavailable_content_description.silenced_title') - %p= t('about.unavailable_content_description.silenced') - = render partial: 'domain_blocks', locals: { domain_blocks: blocks } - - if (blocks = @blocks.select(&:suspend?)) && !blocks.empty? - %h3= t('about.unavailable_content_description.suspended_title') - %p= t('about.unavailable_content_description.suspended') - = render partial: 'domain_blocks', locals: { domain_blocks: blocks } - - .column-4 - %ul.table-of-contents - - unless @rules.empty? - %li= link_to t('about.rules'), '#rules' - - - @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/app/views/about/show.html.haml b/app/views/about/show.html.haml new file mode 100644 index 000000000..aff28b9a9 --- /dev/null +++ b/app/views/about/show.html.haml @@ -0,0 +1,4 @@ +- content_for :page_title do + = t('about.title') + += render partial: 'shared/web_app' diff --git a/config/locales/en.yml b/config/locales/en.yml index 8a70bd8ca..11716234e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,41 +2,15 @@ en: about: about_mastodon_html: 'The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!' - about_this: About - administered_by: 'Administered by:' api: API apps: Mobile apps - contact: Contact contact_missing: Not set contact_unavailable: N/A documentation: Documentation hosted_on: Mastodon hosted on %{domain} - instance_actor_flash: | - This account is a virtual actor used to represent the server itself and not any individual user. - It is used for federation purposes and should not be blocked unless you want to block the whole instance, in which case you should use a domain block. privacy_policy: Privacy Policy - rules: Server rules - rules_html: 'Below is a summary of rules you need to follow if you want to have an account on this server of Mastodon:' source_code: Source code - status_count_after: - one: post - other: posts - status_count_before: Who published - unavailable_content: Moderated servers - unavailable_content_description: - domain: Server - reason: Reason - rejecting_media: 'Media files from these servers will not be processed or stored, and no thumbnails will be displayed, requiring manual click-through to the original file:' - rejecting_media_title: Filtered media - silenced: 'Posts from these servers will be hidden in public timelines and conversations, and no notifications will be generated from their users interactions, unless you are following them:' - silenced_title: Limited servers - suspended: 'No data from these servers will be processed, stored or exchanged, making any interaction or communication with users from these servers impossible:' - suspended_title: Suspended servers - 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 - user_count_before: Home to + title: About what_is_mastodon: What is Mastodon? accounts: choices_html: "%{name}'s choices:" diff --git a/config/routes.rb b/config/routes.rb index e6098cd17..29ec0f8a5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -487,7 +487,9 @@ Rails.application.routes.draw do resource :instance, only: [:show] do resources :peers, only: [:index], controller: 'instances/peers' resources :rules, only: [:index], controller: 'instances/rules' + resources :domain_blocks, only: [:index], controller: 'instances/domain_blocks' resource :privacy_policy, only: [:show], controller: 'instances/privacy_policies' + resource :extended_description, only: [:show], controller: 'instances/extended_descriptions' resource :activity, only: [:show], controller: 'instances/activity' end @@ -642,8 +644,8 @@ Rails.application.routes.draw do get '/web/(*any)', to: 'home#index', as: :web - get '/about', to: redirect('/') - get '/about/more', to: 'about#more' + get '/about', to: 'about#show' + get '/about/more', to: redirect('/about') get '/privacy-policy', to: 'privacy#show', as: :privacy_policy get '/terms', to: redirect('/privacy-policy') diff --git a/spec/controllers/about_controller_spec.rb b/spec/controllers/about_controller_spec.rb index 20069e413..97143ec43 100644 --- a/spec/controllers/about_controller_spec.rb +++ b/spec/controllers/about_controller_spec.rb @@ -3,13 +3,9 @@ require 'rails_helper' RSpec.describe AboutController, type: :controller do render_views - describe 'GET #more' do + describe 'GET #show' do before do - get :more - end - - it 'assigns @instance_presenter' do - expect(assigns(:instance_presenter)).to be_kind_of InstancePresenter + get :show end it 'returns http success' do -- cgit From 839f893168ab221b08fa439012189e6c29a2721a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 20 Oct 2022 14:35:29 +0200 Subject: Change public accounts pages to mount the web UI (#19319) * Change public accounts pages to mount the web UI * Fix handling of remote usernames in routes - When logged in, serve web app - When logged out, redirect to permalink - Fix `app-body` class not being set sometimes due to name conflict * Fix missing `multiColumn` prop * Fix failing test * Use `discoverable` attribute to control indexing directives * Fix `` not using `multiColumn` * Add `noindex` to accounts in REST API * Change noindex directive to not be rendered by default before a route is mounted * Add loading indicator for detailed status in web UI * Fix missing indicator appearing while account is loading in web UI --- app/controllers/about_controller.rb | 8 + app/controllers/account_follow_controller.rb | 12 - app/controllers/account_unfollow_controller.rb | 12 - app/controllers/accounts_controller.rb | 58 -- .../concerns/account_controller_concern.rb | 3 +- .../concerns/web_app_controller_concern.rb | 13 +- app/controllers/follower_accounts_controller.rb | 5 +- app/controllers/following_accounts_controller.rb | 5 +- app/controllers/home_controller.rb | 13 +- app/controllers/privacy_controller.rb | 8 + app/controllers/remote_follow_controller.rb | 41 -- app/controllers/remote_interaction_controller.rb | 55 -- app/controllers/statuses_controller.rb | 2 +- app/controllers/tags_controller.rb | 10 +- app/helpers/accounts_helper.rb | 50 +- .../mastodon/components/error_boundary.js | 7 + .../mastodon/components/missing_indicator.js | 5 + app/javascript/mastodon/containers/mastodon.js | 2 +- app/javascript/mastodon/features/about/index.js | 6 +- .../mastodon/features/account/components/header.js | 5 +- .../mastodon/features/account_timeline/index.js | 12 +- .../mastodon/features/bookmarked_statuses/index.js | 1 + .../mastodon/features/community_timeline/index.js | 1 + app/javascript/mastodon/features/compose/index.js | 5 + .../mastodon/features/direct_timeline/index.js | 1 + .../mastodon/features/directory/index.js | 1 + .../mastodon/features/domain_blocks/index.js | 6 + app/javascript/mastodon/features/explore/index.js | 1 + .../mastodon/features/favourited_statuses/index.js | 1 + .../mastodon/features/favourites/index.js | 5 + .../features/follow_recommendations/index.js | 5 + .../mastodon/features/follow_requests/index.js | 5 + .../mastodon/features/getting_started/index.js | 1 + .../mastodon/features/hashtag_timeline/index.js | 1 + .../mastodon/features/home_timeline/index.js | 3 +- .../mastodon/features/keyboard_shortcuts/index.js | 5 + .../mastodon/features/list_timeline/index.js | 1 + app/javascript/mastodon/features/lists/index.js | 1 + app/javascript/mastodon/features/mutes/index.js | 5 + .../mastodon/features/notifications/index.js | 1 + .../mastodon/features/pinned_statuses/index.js | 4 + .../mastodon/features/privacy_policy/index.js | 6 +- .../mastodon/features/public_timeline/index.js | 1 + app/javascript/mastodon/features/reblogs/index.js | 5 + app/javascript/mastodon/features/status/index.js | 17 +- .../features/ui/components/bundle_column_error.js | 27 +- .../features/ui/components/column_loading.js | 6 +- .../features/ui/components/columns_area.js | 4 +- .../mastodon/features/ui/components/modal_root.js | 21 +- app/javascript/mastodon/features/ui/index.js | 4 +- .../mastodon/features/ui/util/async-components.js | 8 + .../features/ui/util/react_router_helpers.js | 4 +- app/javascript/mastodon/main.js | 8 - app/javascript/mastodon/reducers/statuses.js | 6 + app/javascript/mastodon/selectors/index.js | 2 +- .../service_worker/web_push_notifications.js | 26 +- app/javascript/packs/public.js | 29 - app/javascript/styles/application.scss | 1 - app/javascript/styles/contrast/diff.scss | 4 - app/javascript/styles/mastodon-light/diff.scss | 89 --- app/javascript/styles/mastodon/containers.scss | 782 --------------------- app/javascript/styles/mastodon/footer.scss | 152 ---- app/javascript/styles/mastodon/rtl.scss | 74 -- app/javascript/styles/mastodon/statuses.scss | 3 +- app/lib/permalink_redirector.rb | 36 +- app/models/account.rb | 1 + app/models/user.rb | 4 + app/serializers/rest/account_serializer.rb | 7 +- app/views/about/show.html.haml | 3 + app/views/accounts/_bio.html.haml | 21 - app/views/accounts/_header.html.haml | 43 -- app/views/accounts/_moved.html.haml | 20 - app/views/accounts/show.html.haml | 76 +- app/views/follower_accounts/index.html.haml | 18 +- app/views/following_accounts/index.html.haml | 18 +- app/views/home/index.html.haml | 3 + app/views/layouts/public.html.haml | 60 -- app/views/privacy/show.html.haml | 3 + app/views/remote_follow/new.html.haml | 20 - app/views/remote_interaction/new.html.haml | 24 - app/views/statuses/_detailed_status.html.haml | 6 +- app/views/statuses/_simple_status.html.haml | 6 +- app/views/statuses/show.html.haml | 2 +- app/views/tags/show.html.haml | 5 + config/locales/en.yml | 40 -- config/routes.rb | 57 +- package.json | 1 - spec/controllers/account_follow_controller_spec.rb | 64 -- .../account_unfollow_controller_spec.rb | 64 -- spec/controllers/accounts_controller_spec.rb | 194 ----- .../authorize_interactions_controller_spec.rb | 4 +- .../follower_accounts_controller_spec.rb | 21 - .../following_accounts_controller_spec.rb | 21 - spec/controllers/remote_follow_controller_spec.rb | 135 ---- .../remote_interaction_controller_spec.rb | 39 - spec/controllers/tags_controller_spec.rb | 7 +- spec/features/profile_spec.rb | 26 +- spec/lib/permalink_redirector_spec.rb | 31 +- spec/requests/account_show_page_spec.rb | 15 - spec/routing/accounts_routing_spec.rb | 88 ++- yarn.lock | 5 - 101 files changed, 389 insertions(+), 2464 deletions(-) delete mode 100644 app/controllers/account_follow_controller.rb delete mode 100644 app/controllers/account_unfollow_controller.rb delete mode 100644 app/controllers/remote_follow_controller.rb delete mode 100644 app/controllers/remote_interaction_controller.rb delete mode 100644 app/javascript/styles/mastodon/footer.scss delete mode 100644 app/views/accounts/_bio.html.haml delete mode 100644 app/views/accounts/_header.html.haml delete mode 100644 app/views/accounts/_moved.html.haml delete mode 100644 app/views/layouts/public.html.haml delete mode 100644 app/views/remote_follow/new.html.haml delete mode 100644 app/views/remote_interaction/new.html.haml create mode 100644 app/views/tags/show.html.haml delete mode 100644 spec/controllers/account_follow_controller_spec.rb delete mode 100644 spec/controllers/account_unfollow_controller_spec.rb delete mode 100644 spec/controllers/remote_follow_controller_spec.rb delete mode 100644 spec/controllers/remote_interaction_controller_spec.rb (limited to 'spec/controllers') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 0fbc6a800..104348614 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -5,7 +5,15 @@ class AboutController < ApplicationController skip_before_action :require_functional! + before_action :set_instance_presenter + def show expires_in 0, public: true unless user_signed_in? end + + private + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end end diff --git a/app/controllers/account_follow_controller.rb b/app/controllers/account_follow_controller.rb deleted file mode 100644 index 33394074d..000000000 --- a/app/controllers/account_follow_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -class AccountFollowController < ApplicationController - include AccountControllerConcern - - before_action :authenticate_user! - - def create - FollowService.new.call(current_user.account, @account, with_rate_limit: true) - redirect_to account_path(@account) - end -end diff --git a/app/controllers/account_unfollow_controller.rb b/app/controllers/account_unfollow_controller.rb deleted file mode 100644 index 378ec86dc..000000000 --- a/app/controllers/account_unfollow_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -class AccountUnfollowController < ApplicationController - include AccountControllerConcern - - before_action :authenticate_user! - - def create - UnfollowService.new.call(current_user.account, @account) - redirect_to account_path(@account) - end -end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index d92f91b30..5ceea5d3c 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -9,7 +9,6 @@ class AccountsController < ApplicationController before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers - before_action :set_body_classes skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -18,24 +17,6 @@ class AccountsController < ApplicationController respond_to do |format| format.html do expires_in 0, public: true unless user_signed_in? - - @pinned_statuses = [] - @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) - @featured_hashtags = @account.featured_tags.order(statuses_count: :desc) - - if current_account && @account.blocking?(current_account) - @statuses = [] - return - end - - @pinned_statuses = cached_filtered_status_pins if show_pinned_statuses? - @statuses = cached_filtered_status_page - @rss_url = rss_url - - unless @statuses.empty? - @older_url = older_url if @statuses.last.id > filtered_statuses.last.id - @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id - end end format.rss do @@ -55,18 +36,6 @@ class AccountsController < ApplicationController private - def set_body_classes - @body_classes = 'with-modals' - end - - def show_pinned_statuses? - [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? - end - - def filtered_pinned_statuses - @account.pinned_statuses.where(visibility: [:public, :unlisted]) - end - def filtered_statuses default_statuses.tap do |statuses| statuses.merge!(hashtag_scope) if tag_requested? @@ -113,26 +82,6 @@ class AccountsController < ApplicationController end end - def older_url - pagination_url(max_id: @statuses.last.id) - end - - def newer_url - pagination_url(min_id: @statuses.first.id) - end - - def pagination_url(max_id: nil, min_id: nil) - if tag_requested? - short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id) - elsif media_requested? - short_account_media_url(@account, max_id: max_id, min_id: min_id) - elsif replies_requested? - short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) - else - short_account_url(@account, max_id: max_id, min_id: min_id) - end - end - def media_requested? request.path.split('.').first.end_with?('/media') && !tag_requested? end @@ -145,13 +94,6 @@ class AccountsController < ApplicationController request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) end - def cached_filtered_status_pins - cache_collection( - filtered_pinned_statuses, - Status - ) - end - def cached_filtered_status_page cache_collection_paginated_by_id( filtered_statuses, diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 11eac0eb6..2f7d84df0 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -3,13 +3,12 @@ module AccountControllerConcern extend ActiveSupport::Concern + include WebAppControllerConcern include AccountOwnedConcern FOLLOW_PER_PAGE = 12 included do - layout 'public' - before_action :set_instance_presenter before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } end diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 8a6c73af3..c671ce785 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -4,15 +4,24 @@ module WebAppControllerConcern extend ActiveSupport::Concern included do - before_action :set_body_classes + before_action :redirect_unauthenticated_to_permalinks! + before_action :set_app_body_class before_action :set_referrer_policy_header end - def set_body_classes + def set_app_body_class @body_classes = 'app-body' end def set_referrer_policy_header response.headers['Referrer-Policy'] = 'origin' end + + def redirect_unauthenticated_to_permalinks! + return if user_signed_in? + + redirect_path = PermalinkRedirector.new(request.path).redirect_path + + redirect_to(redirect_path) if redirect_path.present? + end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index da7bb4ed2..e4d8cc495 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -3,6 +3,7 @@ class FollowerAccountsController < ApplicationController include AccountControllerConcern include SignatureVerification + include WebAppControllerConcern before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers @@ -14,10 +15,6 @@ class FollowerAccountsController < ApplicationController respond_to do |format| format.html do expires_in 0, public: true unless user_signed_in? - - next if @account.hide_collections? - - follows end format.json do diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index c37e3b68c..f84dca1e5 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -3,6 +3,7 @@ class FollowingAccountsController < ApplicationController include AccountControllerConcern include SignatureVerification + include WebAppControllerConcern before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers @@ -14,10 +15,6 @@ class FollowingAccountsController < ApplicationController respond_to do |format| format.html do expires_in 0, public: true unless user_signed_in? - - next if @account.hide_collections? - - follows end format.json do diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index b4d6578b9..d8ee82a7a 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,21 +3,14 @@ class HomeController < ApplicationController include WebAppControllerConcern - before_action :redirect_unauthenticated_to_permalinks! before_action :set_instance_presenter - def index; end + def index + expires_in 0, public: true unless user_signed_in? + end private - def redirect_unauthenticated_to_permalinks! - return if user_signed_in? - - redirect_path = PermalinkRedirector.new(request.path).redirect_path - - redirect_to(redirect_path) if redirect_path.present? - end - def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb index bc98bca51..2c98bf3bf 100644 --- a/app/controllers/privacy_controller.rb +++ b/app/controllers/privacy_controller.rb @@ -5,7 +5,15 @@ class PrivacyController < ApplicationController skip_before_action :require_functional! + before_action :set_instance_presenter + def show expires_in 0, public: true if current_account.nil? end + + private + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end end diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb deleted file mode 100644 index db1604644..000000000 --- a/app/controllers/remote_follow_controller.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -class RemoteFollowController < ApplicationController - include AccountOwnedConcern - - layout 'modal' - - before_action :set_body_classes - - skip_before_action :require_functional! - - def new - @remote_follow = RemoteFollow.new(session_params) - end - - def create - @remote_follow = RemoteFollow.new(resource_params) - - if @remote_follow.valid? - session[:remote_follow] = @remote_follow.acct - redirect_to @remote_follow.subscribe_address_for(@account) - else - render :new - end - end - - private - - def resource_params - params.require(:remote_follow).permit(:acct) - end - - def session_params - { acct: session[:remote_follow] || current_account&.username } - end - - def set_body_classes - @body_classes = 'modal-layout' - @hide_header = true - end -end diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb deleted file mode 100644 index 6c29a2b9f..000000000 --- a/app/controllers/remote_interaction_controller.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -class RemoteInteractionController < ApplicationController - include Authorization - - layout 'modal' - - before_action :authenticate_user!, if: :whitelist_mode? - before_action :set_interaction_type - before_action :set_status - before_action :set_body_classes - - skip_before_action :require_functional!, unless: :whitelist_mode? - - def new - @remote_follow = RemoteFollow.new(session_params) - end - - def create - @remote_follow = RemoteFollow.new(resource_params) - - if @remote_follow.valid? - session[:remote_follow] = @remote_follow.acct - redirect_to @remote_follow.interact_address_for(@status) - else - render :new - end - end - - private - - def resource_params - params.require(:remote_follow).permit(:acct) - end - - def session_params - { acct: session[:remote_follow] || current_account&.username } - end - - def set_status - @status = Status.find(params[:id]) - authorize @status, :show? - rescue Mastodon::NotPermittedError - not_found - end - - def set_body_classes - @body_classes = 'modal-layout' - @hide_header = true - end - - def set_interaction_type - @interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply' - end -end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 181c76c9a..bb4e5b01f 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true class StatusesController < ApplicationController + include WebAppControllerConcern include StatusControllerConcern include SignatureAuthentication include Authorization include AccountOwnedConcern - include WebAppControllerConcern before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 2890c179d..f0a099350 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -2,18 +2,16 @@ class TagsController < ApplicationController include SignatureVerification + include WebAppControllerConcern PAGE_SIZE = 20 PAGE_SIZE_MAX = 200 - layout 'public' - before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :authenticate_user!, if: :whitelist_mode? before_action :set_local before_action :set_tag before_action :set_statuses - before_action :set_body_classes before_action :set_instance_presenter skip_before_action :require_functional!, unless: :whitelist_mode? @@ -21,7 +19,7 @@ class TagsController < ApplicationController def show respond_to do |format| format.html do - redirect_to web_path("tags/#{@tag.name}") + expires_in 0, public: true unless user_signed_in? end format.rss do @@ -54,10 +52,6 @@ class TagsController < ApplicationController end end - def set_body_classes - @body_classes = 'with-modals' - end - def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 59664373d..6301919a9 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -20,54 +20,10 @@ module AccountsHelper end def account_action_button(account) - if user_signed_in? - if account.id == current_user.account_id - link_to settings_profile_url, class: 'button logo-button' do - safe_join([logo_as_symbol, t('settings.edit_profile')]) - end - elsif current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do - safe_join([logo_as_symbol, t('accounts.unfollow')]) - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do - safe_join([logo_as_symbol, t('accounts.follow')]) - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do - safe_join([logo_as_symbol, t('accounts.follow')]) - end - end - end - - def minimal_account_action_button(account) - if user_signed_in? - return if account.id == current_user.account_id - - if current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do - fa_icon('user-times fw') - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do - fa_icon('user-plus fw') - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do - fa_icon('user-plus fw') - end - end - end + return if account.memorial? || account.moved? - def account_badge(account) - if account.bot? - content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') - elsif account.group? - content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles') - elsif account.user_role&.highlighted? - content_tag(:div, content_tag(:div, account.user_role.name, class: "account-role user-role-#{account.user_role.id}"), class: 'roles') + link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do + safe_join([logo_as_symbol, t('accounts.follow')]) end end diff --git a/app/javascript/mastodon/components/error_boundary.js b/app/javascript/mastodon/components/error_boundary.js index ca4a2cfe1..02d5616d6 100644 --- a/app/javascript/mastodon/components/error_boundary.js +++ b/app/javascript/mastodon/components/error_boundary.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { version, source_url } from 'mastodon/initial_state'; import StackTrace from 'stacktrace-js'; +import { Helmet } from 'react-helmet'; export default class ErrorBoundary extends React.PureComponent { @@ -84,6 +85,7 @@ export default class ErrorBoundary extends React.PureComponent { )}

+

{ likelyBrowserAddonIssue ? ( @@ -91,8 +93,13 @@ export default class ErrorBoundary extends React.PureComponent { )}

+

Mastodon v{version} · ·

+ + + + ); } diff --git a/app/javascript/mastodon/components/missing_indicator.js b/app/javascript/mastodon/components/missing_indicator.js index 7b0101bab..05e0d653d 100644 --- a/app/javascript/mastodon/components/missing_indicator.js +++ b/app/javascript/mastodon/components/missing_indicator.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import illustration from 'mastodon/../images/elephant_ui_disappointed.svg'; import classNames from 'classnames'; +import { Helmet } from 'react-helmet'; const MissingIndicator = ({ fullPage }) => (
@@ -14,6 +15,10 @@ const MissingIndicator = ({ fullPage }) => (
+ + + + ); diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 8e5a1fa3a..730695c49 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -78,7 +78,7 @@ export default class Mastodon extends React.PureComponent { - + diff --git a/app/javascript/mastodon/features/about/index.js b/app/javascript/mastodon/features/about/index.js index e9212565a..75fed9b95 100644 --- a/app/javascript/mastodon/features/about/index.js +++ b/app/javascript/mastodon/features/about/index.js @@ -94,6 +94,7 @@ class About extends React.PureComponent { }), dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentDidMount () { @@ -108,11 +109,11 @@ class About extends React.PureComponent { } render () { - const { intl, server, extendedDescription, domainBlocks } = this.props; + const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props; const isLoading = server.get('isLoading'); return ( - +
`${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> @@ -212,6 +213,7 @@ class About extends React.PureComponent { {intl.formatMessage(messages.title)} + ); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 44c53f9ce..954cb0ee7 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -270,7 +270,9 @@ class Header extends ImmutablePureComponent { const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); - const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + const isLocal = account.get('acct').indexOf('@') === -1; + const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + const isIndexable = !account.get('noindex'); let badge; @@ -373,6 +375,7 @@ class Header extends ImmutablePureComponent { {titleFromAccount(account)} +
); diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index 51fb76f1f..437cee95c 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -142,19 +142,17 @@ class AccountTimeline extends ImmutablePureComponent { render () { const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props; - if (!isAccount) { + if (isLoading && statusIds.isEmpty()) { return ( - - + ); - } - - if (!statusIds && isLoading) { + } else if (!isLoading && !isAccount) { return ( - + + ); } diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.js b/app/javascript/mastodon/features/bookmarked_statuses/index.js index 0e466e5ed..097be17c9 100644 --- a/app/javascript/mastodon/features/bookmarked_statuses/index.js +++ b/app/javascript/mastodon/features/bookmarked_statuses/index.js @@ -99,6 +99,7 @@ class Bookmarks extends ImmutablePureComponent { {intl.formatMessage(messages.heading)} + ); diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 757521802..7b3f8845f 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -151,6 +151,7 @@ class CommunityTimeline extends React.PureComponent { {intl.formatMessage(messages.title)} + ); diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index c27556a0e..763c715de 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -18,6 +18,7 @@ import { mascot } from '../../initial_state'; import Icon from 'mastodon/components/icon'; import { logOut } from 'mastodon/utils/log_out'; import Column from 'mastodon/components/column'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -145,6 +146,10 @@ class Compose extends React.PureComponent { + + + + ); } diff --git a/app/javascript/mastodon/features/direct_timeline/index.js b/app/javascript/mastodon/features/direct_timeline/index.js index cfaa9c4c5..8dcc43e28 100644 --- a/app/javascript/mastodon/features/direct_timeline/index.js +++ b/app/javascript/mastodon/features/direct_timeline/index.js @@ -98,6 +98,7 @@ class DirectTimeline extends React.PureComponent { {intl.formatMessage(messages.title)} + ); diff --git a/app/javascript/mastodon/features/directory/index.js b/app/javascript/mastodon/features/directory/index.js index 0ce7919b6..b45faa049 100644 --- a/app/javascript/mastodon/features/directory/index.js +++ b/app/javascript/mastodon/features/directory/index.js @@ -169,6 +169,7 @@ class Directory extends React.PureComponent { {intl.formatMessage(messages.title)} + ); diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js index edb80aef4..43b275c2d 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.js +++ b/app/javascript/mastodon/features/domain_blocks/index.js @@ -11,6 +11,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import DomainContainer from '../../containers/domain_container'; import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks'; import ScrollableList from '../../components/scrollable_list'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' }, @@ -59,6 +60,7 @@ class Blocks extends ImmutablePureComponent { return ( + , )} + + + + ); } diff --git a/app/javascript/mastodon/features/explore/index.js b/app/javascript/mastodon/features/explore/index.js index 566be631e..1c7049e97 100644 --- a/app/javascript/mastodon/features/explore/index.js +++ b/app/javascript/mastodon/features/explore/index.js @@ -84,6 +84,7 @@ class Explore extends React.PureComponent { {intl.formatMessage(messages.title)} + )} diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index f1d32eff1..3741f68f6 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.js +++ b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -99,6 +99,7 @@ class Favourites extends ImmutablePureComponent { {intl.formatMessage(messages.heading)} + ); diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js index 673317f04..ad10744da 100644 --- a/app/javascript/mastodon/features/favourites/index.js +++ b/app/javascript/mastodon/features/favourites/index.js @@ -11,6 +11,7 @@ import LoadingIndicator from 'mastodon/components/loading_indicator'; import ScrollableList from 'mastodon/components/scrollable_list'; import AccountContainer from 'mastodon/containers/account_container'; import Column from 'mastodon/features/ui/components/column'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ refresh: { id: 'refresh', defaultMessage: 'Refresh' }, @@ -80,6 +81,10 @@ class Favourites extends ImmutablePureComponent { , )} + + + + ); } diff --git a/app/javascript/mastodon/features/follow_recommendations/index.js b/app/javascript/mastodon/features/follow_recommendations/index.js index 32b55eeb3..5f7baa64c 100644 --- a/app/javascript/mastodon/features/follow_recommendations/index.js +++ b/app/javascript/mastodon/features/follow_recommendations/index.js @@ -12,6 +12,7 @@ import Column from 'mastodon/features/ui/components/column'; import Account from './components/account'; import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg'; import Button from 'mastodon/components/button'; +import { Helmet } from 'react-helmet'; const mapStateToProps = state => ({ suggestions: state.getIn(['suggestions', 'items']), @@ -104,6 +105,10 @@ class FollowRecommendations extends ImmutablePureComponent { )}
+ + + +
); } diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js index 1f9b635bb..d16aa7737 100644 --- a/app/javascript/mastodon/features/follow_requests/index.js +++ b/app/javascript/mastodon/features/follow_requests/index.js @@ -12,6 +12,7 @@ import AccountAuthorizeContainer from './containers/account_authorize_container' import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts'; import ScrollableList from '../../components/scrollable_list'; import { me } from '../../initial_state'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, @@ -87,6 +88,10 @@ class FollowRequests extends ImmutablePureComponent { , )} + + + +
); } diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 42a5b581f..f002ffc77 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -138,6 +138,7 @@ class GettingStarted extends ImmutablePureComponent { {intl.formatMessage(messages.menu)} +
); diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index 0f7df5036..ec524be8f 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -228,6 +228,7 @@ class HashtagTimeline extends React.PureComponent { #{id} + ); diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index 68770b739..838ed7dd8 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -20,7 +20,7 @@ const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' }, hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' }, -}); +}); const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, @@ -167,6 +167,7 @@ class HomeTimeline extends React.PureComponent { {intl.formatMessage(messages.title)} + ); diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js index 2a32577ba..9a870478d 100644 --- a/app/javascript/mastodon/features/keyboard_shortcuts/index.js +++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js @@ -4,6 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ColumnHeader from 'mastodon/components/column_header'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' }, @@ -164,6 +165,10 @@ class KeyboardShortcuts extends ImmutablePureComponent { + + + + ); } diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js index f0a7a0c7f..fd9d33df7 100644 --- a/app/javascript/mastodon/features/list_timeline/index.js +++ b/app/javascript/mastodon/features/list_timeline/index.js @@ -212,6 +212,7 @@ class ListTimeline extends React.PureComponent { {title} + ); diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js index 389a0c5c8..017595ba0 100644 --- a/app/javascript/mastodon/features/lists/index.js +++ b/app/javascript/mastodon/features/lists/index.js @@ -80,6 +80,7 @@ class Lists extends ImmutablePureComponent { {intl.formatMessage(messages.heading)} + ); diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js index c21433cc4..65df6149f 100644 --- a/app/javascript/mastodon/features/mutes/index.js +++ b/app/javascript/mastodon/features/mutes/index.js @@ -11,6 +11,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import AccountContainer from '../../containers/account_container'; import { fetchMutes, expandMutes } from '../../actions/mutes'; import ScrollableList from '../../components/scrollable_list'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.mutes', defaultMessage: 'Muted users' }, @@ -72,6 +73,10 @@ class Mutes extends ImmutablePureComponent { , )} + + + + ); } diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 4577bcb2d..f1bc5f160 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -281,6 +281,7 @@ class Notifications extends React.PureComponent { {intl.formatMessage(messages.title)} + ); diff --git a/app/javascript/mastodon/features/pinned_statuses/index.js b/app/javascript/mastodon/features/pinned_statuses/index.js index 67b13f10a..c6790ea06 100644 --- a/app/javascript/mastodon/features/pinned_statuses/index.js +++ b/app/javascript/mastodon/features/pinned_statuses/index.js @@ -8,6 +8,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import StatusList from '../../components/status_list'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ heading: { id: 'column.pins', defaultMessage: 'Pinned post' }, @@ -54,6 +55,9 @@ class PinnedStatuses extends ImmutablePureComponent { hasMore={hasMore} bindToDocument={!multiColumn} /> + + + ); } diff --git a/app/javascript/mastodon/features/privacy_policy/index.js b/app/javascript/mastodon/features/privacy_policy/index.js index eee4255f4..3df487e8f 100644 --- a/app/javascript/mastodon/features/privacy_policy/index.js +++ b/app/javascript/mastodon/features/privacy_policy/index.js @@ -15,6 +15,7 @@ class PrivacyPolicy extends React.PureComponent { static propTypes = { intl: PropTypes.object, + multiColumn: PropTypes.bool, }; state = { @@ -32,11 +33,11 @@ class PrivacyPolicy extends React.PureComponent { } render () { - const { intl } = this.props; + const { intl, multiColumn } = this.props; const { isLoading, content, lastUpdated } = this.state; return ( - +

@@ -51,6 +52,7 @@ class PrivacyPolicy extends React.PureComponent { {intl.formatMessage(messages.title)} + ); diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index 8dbef98c0..a41be07e1 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -153,6 +153,7 @@ class PublicTimeline extends React.PureComponent { {intl.formatMessage(messages.title)} + ); diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js index 7704a049f..70d338ef1 100644 --- a/app/javascript/mastodon/features/reblogs/index.js +++ b/app/javascript/mastodon/features/reblogs/index.js @@ -11,6 +11,7 @@ import Column from '../ui/components/column'; import ScrollableList from '../../components/scrollable_list'; import Icon from 'mastodon/components/icon'; import ColumnHeader from '../../components/column_header'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ refresh: { id: 'refresh', defaultMessage: 'Refresh' }, @@ -80,6 +81,10 @@ class Reblogs extends ImmutablePureComponent { , )} + + + + ); } diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index f9a97c9b5..02f390c6a 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { createSelector } from 'reselect'; import { fetchStatus } from '../../actions/statuses'; import MissingIndicator from '../../components/missing_indicator'; +import LoadingIndicator from 'mastodon/components/loading_indicator'; import DetailedStatus from './components/detailed_status'; import ActionBar from './components/action_bar'; import Column from '../ui/components/column'; @@ -145,6 +146,7 @@ const makeMapStateToProps = () => { } return { + isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']), status, ancestorsIds, descendantsIds, @@ -187,6 +189,7 @@ class Status extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, status: ImmutablePropTypes.map, + isLoading: PropTypes.bool, ancestorsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, @@ -566,9 +569,17 @@ class Status extends ImmutablePureComponent { render () { let ancestors, descendants; - const { status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props; + const { isLoading, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props; const { fullscreen } = this.state; + if (isLoading) { + return ( + + + + ); + } + if (status === null) { return ( @@ -586,6 +597,9 @@ class Status extends ImmutablePureComponent { descendants =
{this.renderChildren(descendantsIds)}
; } + const isLocal = status.getIn(['account', 'acct'], '').indexOf('@') === -1; + const isIndexable = !status.getIn(['account', 'noindex']); + const handlers = { moveUp: this.handleHotkeyMoveUp, moveDown: this.handleHotkeyMoveDown, @@ -659,6 +673,7 @@ class Status extends ImmutablePureComponent { {titleFromStatus(status)} +
); diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.js index f39ebd900..ab6d4aa44 100644 --- a/app/javascript/mastodon/features/ui/components/bundle_column_error.js +++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.js @@ -1,11 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; - -import Column from './column'; -import ColumnHeader from './column_header'; -import ColumnBackButtonSlim from '../../../components/column_back_button_slim'; -import IconButton from '../../../components/icon_button'; +import Column from 'mastodon/components/column'; +import ColumnHeader from 'mastodon/components/column_header'; +import IconButton from 'mastodon/components/icon_button'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' }, @@ -18,6 +17,7 @@ class BundleColumnError extends React.PureComponent { static propTypes = { onRetry: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, } handleRetry = () => { @@ -25,16 +25,25 @@ class BundleColumnError extends React.PureComponent { } render () { - const { intl: { formatMessage } } = this.props; + const { multiColumn, intl: { formatMessage } } = this.props; return ( - - - + + +
{formatMessage(messages.body)}
+ + + +
); } diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js index 0cdfd05d8..e5ed22584 100644 --- a/app/javascript/mastodon/features/ui/components/column_loading.js +++ b/app/javascript/mastodon/features/ui/components/column_loading.js @@ -10,6 +10,7 @@ export default class ColumnLoading extends ImmutablePureComponent { static propTypes = { title: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), icon: PropTypes.string, + multiColumn: PropTypes.bool, }; static defaultProps = { @@ -18,10 +19,11 @@ export default class ColumnLoading extends ImmutablePureComponent { }; render() { - let { title, icon } = this.props; + let { title, icon, multiColumn } = this.props; + return ( - +
); diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index cc1bc83e0..9ee6fca43 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -139,11 +139,11 @@ class ColumnsArea extends ImmutablePureComponent { } renderLoading = columnId => () => { - return columnId === 'COMPOSE' ? : ; + return columnId === 'COMPOSE' ? : ; } renderError = (props) => { - return ; + return ; } render () { diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 5c273ffa4..2224a8207 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -11,9 +11,7 @@ import VideoModal from './video_modal'; import BoostModal from './boost_modal'; import AudioModal from './audio_modal'; import ConfirmationModal from './confirmation_modal'; -import SubscribedLanguagesModal from 'mastodon/features/subscribed_languages_modal'; import FocalPointModal from './focal_point_modal'; -import InteractionModal from 'mastodon/features/interaction_modal'; import { MuteModal, BlockModal, @@ -23,7 +21,10 @@ import { ListAdder, CompareHistoryModal, FilterModal, + InteractionModal, + SubscribedLanguagesModal, } from 'mastodon/features/ui/util/async-components'; +import { Helmet } from 'react-helmet'; const MODAL_COMPONENTS = { 'MEDIA': () => Promise.resolve({ default: MediaModal }), @@ -41,8 +42,8 @@ const MODAL_COMPONENTS = { 'LIST_ADDER': ListAdder, 'COMPARE_HISTORY': CompareHistoryModal, 'FILTER': FilterModal, - 'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }), - 'INTERACTION': () => Promise.resolve({ default: InteractionModal }), + 'SUBSCRIBED_LANGUAGES': SubscribedLanguagesModal, + 'INTERACTION': InteractionModal, }; export default class ModalRoot extends React.PureComponent { @@ -111,9 +112,15 @@ export default class ModalRoot extends React.PureComponent { return ( {visible && ( - - {(SpecificComponent) => } - + <> + + {(SpecificComponent) => } + + + + + + )} ); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 8f9f38036..003991857 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -197,8 +197,8 @@ class SwitchingColumnsArea extends React.PureComponent { - - + + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index c79dc014c..7686a69ea 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -166,6 +166,14 @@ export function FilterModal () { return import(/*webpackChunkName: "modals/filter_modal" */'../components/filter_modal'); } +export function InteractionModal () { + return import(/*webpackChunkName: "modals/interaction_modal" */'../../interaction_modal'); +} + +export function SubscribedLanguagesModal () { + return import(/*webpackChunkName: "modals/subscribed_languages_modal" */'../../subscribed_languages_modal'); +} + export function About () { return import(/*webpackChunkName: "features/about" */'../../about'); } diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js index d452b871f..a65d79def 100644 --- a/app/javascript/mastodon/features/ui/util/react_router_helpers.js +++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js @@ -53,7 +53,9 @@ export class WrappedRoute extends React.Component { } renderLoading = () => { - return ; + const { multiColumn } = this.props; + + return ; } renderError = (props) => { diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index f33375b50..d0337ce0c 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -12,14 +12,6 @@ const perf = require('mastodon/performance'); function main() { perf.start('main()'); - if (window.history && history.replaceState) { - const { pathname, search, hash } = window.location; - const path = pathname + search + hash; - if (!(/^\/web($|\/)/).test(path)) { - history.replaceState(null, document.title, `/web${path}`); - } - } - return ready(async () => { const mountNode = document.getElementById('mastodon'); const props = JSON.parse(mountNode.getAttribute('data-props')); diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 7efb49d85..c30c1e2cc 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -15,6 +15,8 @@ import { STATUS_COLLAPSE, STATUS_TRANSLATE_SUCCESS, STATUS_TRANSLATE_UNDO, + STATUS_FETCH_REQUEST, + STATUS_FETCH_FAIL, } from '../actions/statuses'; import { TIMELINE_DELETE } from '../actions/timelines'; import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; @@ -37,6 +39,10 @@ const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { switch(action.type) { + case STATUS_FETCH_REQUEST: + return state.setIn([action.id, 'isLoading'], true); + case STATUS_FETCH_FAIL: + return state.delete(action.id); case STATUS_IMPORT: return importStatus(state, action.status); case STATUSES_IMPORT: diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 3dd7f4897..bf46c810e 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -41,7 +41,7 @@ export const makeGetStatus = () => { ], (statusBase, statusReblog, accountBase, accountReblog, filters) => { - if (!statusBase) { + if (!statusBase || statusBase.get('isLoading')) { return null; } diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js index 9b75e9b9d..f12595777 100644 --- a/app/javascript/mastodon/service_worker/web_push_notifications.js +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -15,7 +15,7 @@ const notify = options => icon: '/android-chrome-192x192.png', tag: GROUP_TAG, data: { - url: (new URL('/web/notifications', self.location)).href, + url: (new URL('/notifications', self.location)).href, count: notifications.length + 1, preferred_locale: options.data.preferred_locale, }, @@ -90,7 +90,7 @@ export const handlePush = (event) => { options.tag = notification.id; options.badge = '/badge.png'; options.image = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined; - options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/@${notification.account.acct}/${notification.status.id}` : `/web/@${notification.account.acct}` }; + options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.acct}/${notification.status.id}` : `/@${notification.account.acct}` }; if (notification.status && notification.status.spoiler_text || notification.status.sensitive) { options.data.hiddenBody = htmlToPlainText(notification.status.content); @@ -115,7 +115,7 @@ export const handlePush = (event) => { tag: notification_id, timestamp: new Date(), badge: '/badge.png', - data: { access_token, preferred_locale, url: '/web/notifications' }, + data: { access_token, preferred_locale, url: '/notifications' }, }); }), ); @@ -166,24 +166,10 @@ const removeActionFromNotification = (notification, action) => { const openUrl = url => self.clients.matchAll({ type: 'window' }).then(clientList => { - if (clientList.length !== 0) { - const webClients = clientList.filter(client => /\/web\//.test(client.url)); - - if (webClients.length !== 0) { - const client = findBestClient(webClients); - const { pathname } = new URL(url, self.location); - - if (pathname.startsWith('/web/')) { - return client.focus().then(client => client.postMessage({ - type: 'navigate', - path: pathname.slice('/web/'.length - 1), - })); - } - } else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate - const client = findBestClient(clientList); + if (clientList.length !== 0 && 'navigate' in clientList[0]) { // Chrome 42-48 does not support navigate + const client = findBestClient(clientList); - return client.navigate(url).then(client => client.focus()); - } + return client.navigate(url).then(client => client.focus()); } return self.clients.openWindow(url); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index e42468e0c..5ff45fa55 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -33,7 +33,6 @@ function main() { const { messages } = getLocale(); const React = require('react'); const ReactDOM = require('react-dom'); - const Rellax = require('rellax'); const { createBrowserHistory } = require('history'); const scrollToDetailedStatus = () => { @@ -112,12 +111,6 @@ function main() { scrollToDetailedStatus(); } - const parallaxComponents = document.querySelectorAll('.parallax'); - - if (parallaxComponents.length > 0 ) { - new Rellax('.parallax', { speed: -1 }); - } - delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => { const password = document.getElementById('registration_user_password'); const confirmation = document.getElementById('registration_user_password_confirmation'); @@ -168,28 +161,6 @@ function main() { }); }); - delegate(document, '.webapp-btn', 'click', ({ target, button }) => { - if (button !== 0) { - return true; - } - window.location.href = target.href; - return false; - }); - - delegate(document, '.modal-button', 'click', e => { - e.preventDefault(); - - let href; - - if (e.target.nodeName !== 'A') { - href = e.target.parentNode.href; - } else { - href = e.target.href; - } - - window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); - }); - delegate(document, '#account_display_name', 'input', ({ target }) => { const name = document.querySelector('.card .display-name strong'); if (name) { diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index e9f596e2f..81a040108 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -8,7 +8,6 @@ @import 'mastodon/branding'; @import 'mastodon/containers'; @import 'mastodon/lists'; -@import 'mastodon/footer'; @import 'mastodon/widgets'; @import 'mastodon/forms'; @import 'mastodon/accounts'; diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss index 22f5bcc94..27eb837df 100644 --- a/app/javascript/styles/contrast/diff.scss +++ b/app/javascript/styles/contrast/diff.scss @@ -68,10 +68,6 @@ color: $darker-text-color; } -.public-layout .public-account-header__tabs__tabs .counter.active::after { - border-bottom: 4px solid $ui-highlight-color; -} - .compose-form .autosuggest-textarea__textarea::placeholder, .compose-form .spoiler-input__input::placeholder { color: $inverted-text-color; diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 4b27e6b4f..20e973b8b 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -655,95 +655,6 @@ html { } } -.public-layout { - .account__section-headline { - border: 1px solid lighten($ui-base-color, 8%); - - @media screen and (max-width: $no-gap-breakpoint) { - border-top: 0; - } - } - - .header, - .public-account-header, - .public-account-bio { - box-shadow: none; - } - - .public-account-bio, - .hero-widget__text { - background: $account-background-color; - } - - .header { - background: $ui-base-color; - border: 1px solid lighten($ui-base-color, 8%); - - @media screen and (max-width: $no-gap-breakpoint) { - border: 0; - } - - .brand { - &:hover, - &:focus, - &:active { - background: lighten($ui-base-color, 4%); - } - } - } - - .public-account-header { - &__image { - background: lighten($ui-base-color, 12%); - - &::after { - box-shadow: none; - } - } - - &__bar { - &::before { - background: $account-background-color; - border: 1px solid lighten($ui-base-color, 8%); - border-top: 0; - } - - .avatar img { - border-color: $account-background-color; - } - - @media screen and (max-width: $no-columns-breakpoint) { - background: $account-background-color; - border: 1px solid lighten($ui-base-color, 8%); - border-top: 0; - } - } - - &__tabs { - &__name { - h1, - h1 small { - color: $white; - - @media screen and (max-width: $no-columns-breakpoint) { - color: $primary-text-color; - } - } - } - } - - &__extra { - .public-account-bio { - border: 0; - } - - .public-account-bio .account__header__fields { - border-color: lighten($ui-base-color, 8%); - } - } - } -} - .notification__filter-bar button.active::after, .account__section-headline a.active::after { border-color: transparent transparent $white; diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 8e5ed03f0..b49b93984 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -104,785 +104,3 @@ margin-left: 10px; } } - -.grid-3 { - display: grid; - grid-gap: 10px; - grid-template-columns: 3fr 1fr; - grid-auto-columns: 25%; - grid-auto-rows: max-content; - - .column-0 { - grid-column: 1 / 3; - grid-row: 1; - } - - .column-1 { - grid-column: 1; - grid-row: 2; - } - - .column-2 { - grid-column: 2; - grid-row: 2; - } - - .column-3 { - grid-column: 1 / 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: repeat(4, minmax(0, 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%; - } - - .flash-message { - margin-bottom: 10px; - } - - @media screen and (max-width: 738px) { - grid-template-columns: minmax(0, 50%) minmax(0, 50%); - - .landing-page__call-to-action { - padding: 20px; - display: flex; - align-items: center; - justify-content: center; - } - - .row__information-board { - width: 100%; - justify-content: center; - align-items: center; - } - - .row__mascot { - display: none; - } - } - - @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: 5; - } - - .column-4 { - grid-column: 1; - grid-row: 4; - } - } -} - -.public-layout { - @media screen and (max-width: $no-gap-breakpoint) { - padding-top: 48px; - } - - .container { - max-width: 960px; - - @media screen and (max-width: $no-gap-breakpoint) { - padding: 0; - } - } - - .header { - background: lighten($ui-base-color, 8%); - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - border-radius: 4px; - height: 48px; - margin: 10px 0; - display: flex; - align-items: stretch; - justify-content: center; - flex-wrap: nowrap; - overflow: hidden; - - @media screen and (max-width: $no-gap-breakpoint) { - position: fixed; - width: 100%; - top: 0; - left: 0; - margin: 0; - border-radius: 0; - box-shadow: none; - z-index: 110; - } - - & > div { - flex: 1 1 33.3%; - min-height: 1px; - } - - .nav-left { - display: flex; - align-items: stretch; - justify-content: flex-start; - flex-wrap: nowrap; - } - - .nav-center { - display: flex; - align-items: stretch; - justify-content: center; - flex-wrap: nowrap; - } - - .nav-right { - display: flex; - align-items: stretch; - justify-content: flex-end; - flex-wrap: nowrap; - } - - .brand { - display: block; - padding: 15px; - - .logo { - display: block; - height: 18px; - width: auto; - position: relative; - bottom: -2px; - fill: $primary-text-color; - - @media screen and (max-width: $no-gap-breakpoint) { - height: 20px; - } - } - - &:hover, - &:focus, - &:active { - background: lighten($ui-base-color, 12%); - } - } - - .nav-link { - display: flex; - align-items: center; - padding: 0 1rem; - font-size: 12px; - font-weight: 500; - text-decoration: none; - color: $darker-text-color; - white-space: nowrap; - text-align: center; - - &:hover, - &:focus, - &:active { - text-decoration: underline; - color: $primary-text-color; - } - - @media screen and (max-width: 550px) { - &.optional { - display: none; - } - } - } - - .nav-button { - background: lighten($ui-base-color, 16%); - margin: 8px; - margin-left: 0; - border-radius: 4px; - - &:hover, - &:focus, - &:active { - text-decoration: none; - background: lighten($ui-base-color, 20%); - } - } - } - - $no-columns-breakpoint: 600px; - - .grid { - display: grid; - grid-gap: 10px; - grid-template-columns: minmax(300px, 3fr) minmax(298px, 1fr); - grid-auto-columns: 25%; - grid-auto-rows: max-content; - - .column-0 { - grid-row: 1; - grid-column: 1; - } - - .column-1 { - grid-row: 1; - grid-column: 2; - } - - @media screen and (max-width: $no-columns-breakpoint) { - grid-template-columns: 100%; - grid-gap: 0; - - .column-1 { - display: none; - } - } - } - - .page-header { - @media screen and (max-width: $no-gap-breakpoint) { - border-bottom: 0; - } - } - - .public-account-header { - overflow: hidden; - margin-bottom: 10px; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - - &.inactive { - opacity: 0.5; - - .public-account-header__image, - .avatar { - filter: grayscale(100%); - } - - .logo-button { - background-color: $secondary-text-color; - } - } - - .logo-button { - padding: 3px 15px; - } - - &__image { - border-radius: 4px 4px 0 0; - overflow: hidden; - height: 300px; - position: relative; - background: darken($ui-base-color, 12%); - - &::after { - content: ""; - display: block; - position: absolute; - width: 100%; - height: 100%; - box-shadow: inset 0 -1px 1px 1px rgba($base-shadow-color, 0.15); - top: 0; - left: 0; - } - - img { - object-fit: cover; - display: block; - width: 100%; - height: 100%; - margin: 0; - border-radius: 4px 4px 0 0; - } - - @media screen and (max-width: 600px) { - height: 200px; - } - } - - &--no-bar { - margin-bottom: 0; - - .public-account-header__image, - .public-account-header__image img { - border-radius: 4px; - - @media screen and (max-width: $no-gap-breakpoint) { - border-radius: 0; - } - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - margin-bottom: 0; - box-shadow: none; - - &__image::after { - display: none; - } - - &__image, - &__image img { - border-radius: 0; - } - } - - &__bar { - position: relative; - margin-top: -80px; - display: flex; - justify-content: flex-start; - - &::before { - content: ""; - display: block; - background: lighten($ui-base-color, 4%); - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 60px; - border-radius: 0 0 4px 4px; - z-index: -1; - } - - .avatar { - display: block; - width: 120px; - height: 120px; - padding-left: 20px - 4px; - flex: 0 0 auto; - - img { - display: block; - width: 100%; - height: 100%; - margin: 0; - border-radius: 50%; - border: 4px solid lighten($ui-base-color, 4%); - background: darken($ui-base-color, 8%); - } - } - - @media screen and (max-width: 600px) { - margin-top: 0; - background: lighten($ui-base-color, 4%); - border-radius: 0 0 4px 4px; - padding: 5px; - - &::before { - display: none; - } - - .avatar { - width: 48px; - height: 48px; - padding: 7px 0; - padding-left: 10px; - - img { - border: 0; - border-radius: 4px; - } - - @media screen and (max-width: 360px) { - display: none; - } - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - border-radius: 0; - } - - @media screen and (max-width: $no-columns-breakpoint) { - flex-wrap: wrap; - } - } - - &__tabs { - flex: 1 1 auto; - margin-left: 20px; - - &__name { - padding-top: 20px; - padding-bottom: 8px; - - h1 { - font-size: 20px; - line-height: 18px * 1.5; - color: $primary-text-color; - font-weight: 500; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - text-shadow: 1px 1px 1px $base-shadow-color; - - small { - display: block; - font-size: 14px; - color: $primary-text-color; - font-weight: 400; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - - @media screen and (max-width: 600px) { - margin-left: 15px; - display: flex; - justify-content: space-between; - align-items: center; - - &__name { - padding-top: 0; - padding-bottom: 0; - - h1 { - font-size: 16px; - line-height: 24px; - text-shadow: none; - - small { - color: $darker-text-color; - } - } - } - } - - &__tabs { - display: flex; - justify-content: flex-start; - align-items: stretch; - height: 58px; - - .details-counters { - display: flex; - flex-direction: row; - min-width: 300px; - } - - @media screen and (max-width: $no-columns-breakpoint) { - .details-counters { - display: none; - } - } - - .counter { - min-width: 33.3%; - box-sizing: border-box; - flex: 0 0 auto; - color: $darker-text-color; - padding: 10px; - border-right: 1px solid lighten($ui-base-color, 4%); - cursor: default; - text-align: center; - position: relative; - - a { - display: block; - } - - &:last-child { - border-right: 0; - } - - &::after { - display: block; - content: ""; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - border-bottom: 4px solid $ui-primary-color; - opacity: 0.5; - transition: all 400ms ease; - } - - &.active { - &::after { - border-bottom: 4px solid $highlight-text-color; - opacity: 1; - } - - &.inactive::after { - border-bottom-color: $secondary-text-color; - } - } - - &:hover { - &::after { - opacity: 1; - transition-duration: 100ms; - } - } - - a { - text-decoration: none; - color: inherit; - } - - .counter-label { - font-size: 12px; - display: block; - } - - .counter-number { - font-weight: 500; - font-size: 18px; - margin-bottom: 5px; - color: $primary-text-color; - font-family: $font-display, sans-serif; - } - } - - .spacer { - flex: 1 1 auto; - height: 1px; - } - - &__buttons { - padding: 7px 8px; - } - } - } - - &__extra { - display: none; - margin-top: 4px; - - .public-account-bio { - border-radius: 0; - box-shadow: none; - background: transparent; - margin: 0 -5px; - - .account__header__fields { - border-top: 1px solid lighten($ui-base-color, 12%); - } - - .roles { - display: none; - } - } - - &__links { - margin-top: -15px; - font-size: 14px; - color: $darker-text-color; - - a { - display: inline-block; - color: $darker-text-color; - text-decoration: none; - padding: 15px; - font-weight: 500; - - strong { - font-weight: 700; - color: $primary-text-color; - } - } - } - - @media screen and (max-width: $no-columns-breakpoint) { - display: block; - flex: 100%; - } - } - } - - .account__section-headline { - border-radius: 4px 4px 0 0; - - @media screen and (max-width: $no-gap-breakpoint) { - border-radius: 0; - } - } - - .detailed-status__meta { - margin-top: 25px; - } - - .public-account-bio { - background: lighten($ui-base-color, 8%); - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - border-radius: 4px; - overflow: hidden; - margin-bottom: 10px; - - @media screen and (max-width: $no-gap-breakpoint) { - box-shadow: none; - margin-bottom: 0; - border-radius: 0; - } - - .account__header__fields { - margin: 0; - border-top: 0; - - a { - color: $highlight-text-color; - } - - dl:first-child .verified { - border-radius: 0 4px 0 0; - } - - .verified a { - color: $valid-value-color; - } - } - - .account__header__content { - padding: 20px; - padding-bottom: 0; - color: $primary-text-color; - } - - &__extra, - .roles { - padding: 20px; - font-size: 14px; - color: $darker-text-color; - } - - .roles { - padding-bottom: 0; - } - } - - .directory__list { - display: grid; - grid-gap: 10px; - grid-template-columns: minmax(0, 50%) minmax(0, 50%); - - .account-card { - display: flex; - flex-direction: column; - } - - @media screen and (max-width: $no-gap-breakpoint) { - display: block; - - .account-card { - margin-bottom: 10px; - display: block; - } - } - } - - .card-grid { - display: flex; - flex-wrap: wrap; - min-width: 100%; - margin: 0 -5px; - - & > div { - box-sizing: border-box; - flex: 1 0 auto; - width: 300px; - padding: 0 5px; - margin-bottom: 10px; - max-width: 33.333%; - - @media screen and (max-width: 900px) { - max-width: 50%; - } - - @media screen and (max-width: 600px) { - max-width: 100%; - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - margin: 0; - border-top: 1px solid lighten($ui-base-color, 8%); - - & > div { - width: 100%; - padding: 0; - margin-bottom: 0; - border-bottom: 1px solid lighten($ui-base-color, 8%); - - &:last-child { - border-bottom: 0; - } - - .card__bar { - background: $ui-base-color; - - &:hover, - &:active, - &:focus { - background: lighten($ui-base-color, 4%); - } - } - } - } - } -} diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss deleted file mode 100644 index 0c3e42033..000000000 --- a/app/javascript/styles/mastodon/footer.scss +++ /dev/null @@ -1,152 +0,0 @@ -.public-layout { - .footer { - text-align: left; - padding-top: 20px; - padding-bottom: 60px; - font-size: 12px; - color: lighten($ui-base-color, 34%); - - @media screen and (max-width: $no-gap-breakpoint) { - padding-left: 20px; - padding-right: 20px; - } - - .grid { - display: grid; - grid-gap: 10px; - grid-template-columns: 1fr 1fr 2fr 1fr 1fr; - - .column-0 { - grid-column: 1; - grid-row: 1; - min-width: 0; - } - - .column-1 { - grid-column: 2; - grid-row: 1; - min-width: 0; - } - - .column-2 { - grid-column: 3; - grid-row: 1; - min-width: 0; - text-align: center; - - h4 a { - color: lighten($ui-base-color, 34%); - } - } - - .column-3 { - grid-column: 4; - grid-row: 1; - min-width: 0; - } - - .column-4 { - grid-column: 5; - grid-row: 1; - min-width: 0; - } - - @media screen and (max-width: 690px) { - grid-template-columns: 1fr 2fr 1fr; - - .column-0, - .column-1 { - grid-column: 1; - } - - .column-1 { - grid-row: 2; - } - - .column-2 { - grid-column: 2; - } - - .column-3, - .column-4 { - grid-column: 3; - } - - .column-4 { - grid-row: 2; - } - } - - @media screen and (max-width: 600px) { - .column-1 { - display: block; - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - .column-0, - .column-1, - .column-3, - .column-4 { - display: none; - } - - .column-2 h4 { - display: none; - } - } - } - - .legal-xs { - display: none; - text-align: center; - padding-top: 20px; - - @media screen and (max-width: $no-gap-breakpoint) { - display: block; - } - } - - h4 { - text-transform: uppercase; - font-weight: 700; - margin-bottom: 8px; - color: $darker-text-color; - - a { - color: inherit; - text-decoration: none; - } - } - - ul a, - .legal-xs a { - text-decoration: none; - color: lighten($ui-base-color, 34%); - - &:hover, - &:active, - &:focus { - text-decoration: underline; - } - } - - .brand { - .logo { - display: block; - height: 36px; - width: auto; - margin: 0 auto; - color: lighten($ui-base-color, 34%); - } - - &:hover, - &:focus, - &:active { - .logo { - color: lighten($ui-base-color, 38%); - } - } - } - } -} diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss index 98eb1511c..ccec8e95e 100644 --- a/app/javascript/styles/mastodon/rtl.scss +++ b/app/javascript/styles/mastodon/rtl.scss @@ -53,16 +53,6 @@ body.rtl { right: -26px; } - .landing-page__logo { - margin-right: 0; - margin-left: 20px; - } - - .landing-page .features-list .features-list__row .visual { - margin-left: 0; - margin-right: 15px; - } - .column-link__icon, .column-header__icon { margin-right: 0; @@ -350,44 +340,6 @@ body.rtl { margin-left: 45px; } - .landing-page .header-wrapper .mascot { - right: 60px; - left: auto; - } - - .landing-page__call-to-action .row__information-board { - direction: rtl; - } - - .landing-page .header .hero .floats .float-1 { - left: -120px; - right: auto; - } - - .landing-page .header .hero .floats .float-2 { - left: 210px; - right: auto; - } - - .landing-page .header .hero .floats .float-3 { - left: 110px; - right: auto; - } - - .landing-page .header .links .brand img { - left: 0; - } - - .landing-page .fa-external-link { - padding-right: 5px; - padding-left: 0 !important; - } - - .landing-page .features #mastodon-timeline { - margin-right: 0; - margin-left: 30px; - } - @media screen and (min-width: 631px) { .column, .drawer { @@ -415,32 +367,6 @@ body.rtl { padding-right: 0; } - .public-layout { - .header { - .nav-button { - margin-left: 8px; - margin-right: 0; - } - } - - .public-account-header__tabs { - margin-left: 0; - margin-right: 20px; - } - } - - .landing-page__information { - .account__display-name { - margin-right: 0; - margin-left: 5px; - } - - .account__avatar-wrapper { - margin-left: 12px; - margin-right: 0; - } - } - .card__bar .display-name { margin-left: 0; margin-right: 15px; diff --git a/app/javascript/styles/mastodon/statuses.scss b/app/javascript/styles/mastodon/statuses.scss index a3237a630..ce71d11e4 100644 --- a/app/javascript/styles/mastodon/statuses.scss +++ b/app/javascript/styles/mastodon/statuses.scss @@ -137,8 +137,7 @@ a.button.logo-button { justify-content: center; } -.embed, -.public-layout { +.embed { .status__content[data-spoiler="folded"] { .e-content { display: none; diff --git a/app/lib/permalink_redirector.rb b/app/lib/permalink_redirector.rb index 6d15f3963..cf1a37625 100644 --- a/app/lib/permalink_redirector.rb +++ b/app/lib/permalink_redirector.rb @@ -8,16 +8,14 @@ class PermalinkRedirector end def redirect_path - if path_segments[0] == 'web' - if path_segments[1].present? && path_segments[1].start_with?('@') && path_segments[2] =~ /\d/ - find_status_url_by_id(path_segments[2]) - elsif path_segments[1].present? && path_segments[1].start_with?('@') - find_account_url_by_name(path_segments[1]) - elsif path_segments[1] == 'statuses' && path_segments[2] =~ /\d/ - find_status_url_by_id(path_segments[2]) - elsif path_segments[1] == 'accounts' && path_segments[2] =~ /\d/ - find_account_url_by_id(path_segments[2]) - end + if path_segments[0].present? && path_segments[0].start_with?('@') && path_segments[1] =~ /\d/ + find_status_url_by_id(path_segments[1]) + elsif path_segments[0].present? && path_segments[0].start_with?('@') + find_account_url_by_name(path_segments[0]) + elsif path_segments[0] == 'statuses' && path_segments[1] =~ /\d/ + find_status_url_by_id(path_segments[1]) + elsif path_segments[0] == 'accounts' && path_segments[1] =~ /\d/ + find_account_url_by_id(path_segments[1]) end end @@ -29,18 +27,12 @@ class PermalinkRedirector def find_status_url_by_id(id) status = Status.find_by(id: id) - - return unless status&.distributable? - - ActivityPub::TagManager.instance.url_for(status) + ActivityPub::TagManager.instance.url_for(status) if status&.distributable? && !status.account.local? end def find_account_url_by_id(id) account = Account.find_by(id: id) - - return unless account - - ActivityPub::TagManager.instance.url_for(account) + ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local? end def find_account_url_by_name(name) @@ -48,12 +40,6 @@ class PermalinkRedirector domain = nil if TagManager.instance.local_domain?(domain) account = Account.find_remote(username, domain) - return unless account - - ActivityPub::TagManager.instance.url_for(account) - end - - def find_tag_url_by_name(name) - tag_path(CGI.unescape(name)) + ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local? end end diff --git a/app/models/account.rb b/app/models/account.rb index 1be7b4d12..df7fa8d50 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -134,6 +134,7 @@ class Account < ApplicationRecord :role, :locale, :shows_application?, + :prefers_noindex?, to: :user, prefix: true, allow_nil: true diff --git a/app/models/user.rb b/app/models/user.rb index 4767189a0..6d566b1c2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -281,6 +281,10 @@ class User < ApplicationRecord save! end + def prefers_noindex? + setting_noindex + end + def preferred_posting_language valid_locale_cascade(settings.default_language, locale, I18n.locale) end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index c52a89d87..e521dacaa 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -14,6 +14,7 @@ class REST::AccountSerializer < ActiveModel::Serializer attribute :suspended, if: :suspended? attribute :silenced, key: :limited, if: :silenced? + attribute :noindex, if: :local? class FieldSerializer < ActiveModel::Serializer include FormattingHelper @@ -103,7 +104,11 @@ class REST::AccountSerializer < ActiveModel::Serializer object.silenced? end - delegate :suspended?, :silenced?, to: :object + def noindex + object.user_prefers_noindex? + end + + delegate :suspended?, :silenced?, :local?, to: :object def moved_and_not_nested? object.moved? && object.moved_to_account.moved_to_account_id.nil? diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index aff28b9a9..05d8989ad 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -1,4 +1,7 @@ - content_for :page_title do = t('about.title') +- content_for :header_tags do + = render partial: 'shared/og' + = render partial: 'shared/web_app' diff --git a/app/views/accounts/_bio.html.haml b/app/views/accounts/_bio.html.haml deleted file mode 100644 index e2539b1d4..000000000 --- a/app/views/accounts/_bio.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -- fields = account.fields - -.public-account-bio - - unless fields.empty? - .account__header__fields - - fields.each do |field| - %dl - %dt.emojify{ title: field.name }= prerender_custom_emojis(h(field.name), account.emojis) - %dd{ title: field.value, class: custom_field_classes(field) } - - if field.verified? - %span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) } - = fa_icon 'check' - = prerender_custom_emojis(account_field_value_format(field), account.emojis) - - = account_badge(account) - - - if account.note.present? - .account__header__content.emojify= prerender_custom_emojis(account_bio_format(account), account.emojis) - - .public-account-bio__extra - = t 'accounts.joined', date: l(account.created_at, format: :month) diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml deleted file mode 100644 index d9966723a..000000000 --- a/app/views/accounts/_header.html.haml +++ /dev/null @@ -1,43 +0,0 @@ -.public-account-header{:class => ("inactive" if account.moved?)} - .public-account-header__image - = image_tag (prefers_autoplay? ? account.header_original_url : account.header_static_url), class: 'parallax' - .public-account-header__bar - = link_to short_account_url(account), class: 'avatar' do - = image_tag (prefers_autoplay? ? account.avatar_original_url : account.avatar_static_url), id: 'profile_page_avatar', data: { original: full_asset_url(account.avatar_original_url), static: full_asset_url(account.avatar_static_url), autoplay: prefers_autoplay? } - .public-account-header__tabs - .public-account-header__tabs__name - %h1 - = display_name(account, custom_emojify: true) - %small - = acct(account) - = fa_icon('lock') if account.locked? - .public-account-header__tabs__tabs - .details-counters - .counter{ class: active_nav_class(short_account_url(account), short_account_with_replies_url(account), short_account_media_url(account)) } - = link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do - %span.counter-number= friendly_number_to_human account.statuses_count - %span.counter-label= t('accounts.posts', count: account.statuses_count) - - .counter{ class: active_nav_class(account_following_index_url(account)) } - = link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do - %span.counter-number= friendly_number_to_human account.following_count - %span.counter-label= t('accounts.following', count: account.following_count) - - .counter{ class: active_nav_class(account_followers_url(account)) } - = link_to account_followers_url(account), title: number_with_delimiter(account.followers_count) do - %span.counter-number= friendly_number_to_human account.followers_count - %span.counter-label= t('accounts.followers', count: account.followers_count) - .spacer - .public-account-header__tabs__tabs__buttons - = account_action_button(account) - - .public-account-header__extra - = render 'accounts/bio', account: account - - .public-account-header__extra__links - = link_to account_following_index_url(account) do - %strong= friendly_number_to_human account.following_count - = t('accounts.following', count: account.following_count) - = link_to account_followers_url(account) do - %strong= friendly_number_to_human account.followers_count - = t('accounts.followers', count: account.followers_count) diff --git a/app/views/accounts/_moved.html.haml b/app/views/accounts/_moved.html.haml deleted file mode 100644 index 2f46e0dd0..000000000 --- a/app/views/accounts/_moved.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -- moved_to_account = account.moved_to_account - -.moved-account-widget - .moved-account-widget__message - = fa_icon 'suitcase' - = t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.pretty_acct)])), ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'mention')) - - .moved-account-widget__card - = link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener noreferrer' do - .detailed-status__display-avatar - .account__avatar-overlay - .account__avatar-overlay-base - = image_tag moved_to_account.avatar_static_url - .account__avatar-overlay-overlay - = image_tag account.avatar_static_url - - %span.display-name - %bdi - %strong.emojify= display_name(moved_to_account, custom_emojify: true) - %span @#{moved_to_account.pretty_acct} diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 7fa688bd3..a51dcd7be 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -2,85 +2,13 @@ = "#{display_name(@account)} (#{acct(@account)})" - content_for :header_tags do - - if @account.user&.setting_noindex + - if @account.user_prefers_noindex? %meta{ name: 'robots', content: 'noindex, noarchive' }/ %link{ rel: 'alternate', type: 'application/rss+xml', href: @rss_url }/ %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/ - - if @older_url - %link{ rel: 'next', href: @older_url }/ - - if @newer_url - %link{ rel: 'prev', href: @newer_url }/ - = opengraph 'og:type', 'profile' = render 'og', account: @account, url: short_account_url(@account, only_path: false) - -= render 'header', account: @account, with_bio: true - -.grid - .column-0 - .h-feed - %data.p-name{ value: "#{@account.username} on #{site_hostname}" }/ - - .account__section-headline - = active_link_to t('accounts.posts_tab_heading'), short_account_url(@account) - = active_link_to t('accounts.posts_with_replies'), short_account_with_replies_url(@account) - = active_link_to t('accounts.media'), short_account_media_url(@account) - - - if user_signed_in? && @account.blocking?(current_account) - .nothing-here.nothing-here--under-tabs= t('accounts.unavailable') - - elsif @statuses.empty? - = nothing_here 'nothing-here--under-tabs' - - else - .activity-stream.activity-stream--under-tabs - - if params[:page].to_i.zero? - = render partial: 'statuses/status', collection: @pinned_statuses, as: :status, locals: { pinned: true } - - - if @newer_url - .entry= link_to_newer @newer_url - - = render partial: 'statuses/status', collection: @statuses, as: :status - - - if @older_url - .entry= link_to_older @older_url - - .column-1 - - if @account.memorial? - .memoriam-widget= t('in_memoriam_html') - - elsif @account.moved? - = render 'moved', account: @account - - = render 'bio', account: @account - - - if @endorsed_accounts.empty? && @account.id == current_account&.id - .placeholder-widget= t('accounts.endorsements_hint') - - elsif !@endorsed_accounts.empty? - .endorsements-widget - %h4= t 'accounts.choices_html', name: content_tag(:bdi, display_name(@account, custom_emojify: true)) - - - @endorsed_accounts.each do |account| - = account_link_to account - - - if @featured_hashtags.empty? && @account.id == current_account&.id - .placeholder-widget - = t('accounts.featured_tags_hint') - = link_to settings_featured_tags_path do - = t('featured_tags.add_new') - = fa_icon 'chevron-right fw' - - else - - @featured_hashtags.each do |featured_tag| - .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } - = link_to short_account_tag_path(@account, featured_tag.tag) do - %h4 - = fa_icon 'hashtag' - = featured_tag.display_name - %small - - if featured_tag.last_status_at.nil? - = t('accounts.nothing_here') - - else - %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at - .trends__item__current= friendly_number_to_human featured_tag.statuses_count - - = render 'application/sidebar' += render partial: 'shared/web_app' diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml index 92de35a9f..d93540c02 100644 --- a/app/views/follower_accounts/index.html.haml +++ b/app/views/follower_accounts/index.html.haml @@ -1,20 +1,6 @@ -- content_for :page_title do - = t('accounts.people_who_follow', name: display_name(@account)) - - content_for :header_tags do %meta{ name: 'robots', content: 'noindex' }/ - = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false) -= render 'accounts/header', account: @account - -- if @account.hide_collections? - .nothing-here= t('accounts.network_hidden') -- elsif user_signed_in? && @account.blocking?(current_account) - .nothing-here= t('accounts.unavailable') -- elsif @follows.empty? - = nothing_here -- else - .card-grid - = render partial: 'application/card', collection: @follows.map(&:account), as: :account + = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false) - = paginate @follows += render 'shared/web_app' diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml index 9bb1a9edd..d93540c02 100644 --- a/app/views/following_accounts/index.html.haml +++ b/app/views/following_accounts/index.html.haml @@ -1,20 +1,6 @@ -- content_for :page_title do - = t('accounts.people_followed_by', name: display_name(@account)) - - content_for :header_tags do %meta{ name: 'robots', content: 'noindex' }/ - = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false) -= render 'accounts/header', account: @account - -- if @account.hide_collections? - .nothing-here= t('accounts.network_hidden') -- elsif user_signed_in? && @account.blocking?(current_account) - .nothing-here= t('accounts.unavailable') -- elsif @follows.empty? - = nothing_here -- else - .card-grid - = render partial: 'application/card', collection: @follows.map(&:target_account), as: :account + = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false) - = paginate @follows += render 'shared/web_app' diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 76a02e0f0..45990cd10 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,4 +1,7 @@ - content_for :header_tags do + - unless request.path == '/' + %meta{ name: 'robots', content: 'noindex' }/ + = render partial: 'shared/og' = render 'shared/web_app' diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml deleted file mode 100644 index 9b9e725e9..000000000 --- a/app/views/layouts/public.html.haml +++ /dev/null @@ -1,60 +0,0 @@ -- content_for :header_tags do - = render_initial_state - = javascript_pack_tag 'public', crossorigin: 'anonymous' - -- content_for :content do - .public-layout - - unless @hide_navbar - .container - %nav.header - .nav-left - = link_to root_url, class: 'brand' do - = logo_as_symbol(:wordmark) - - - unless whitelist_mode? - = link_to t('about.about_this'), about_more_path, class: 'nav-link optional' - = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional' - - .nav-center - - .nav-right - - if user_signed_in? - = link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn' - - else - = link_to_login t('auth.login'), class: 'webapp-btn nav-link nav-button' - = link_to t('auth.register'), available_sign_up_path, class: 'webapp-btn nav-link nav-button' - - .container= yield - - .container - .footer - .grid - .column-0 - %h4= t 'footer.resources' - %ul - %li= link_to t('about.privacy_policy'), privacy_policy_path - .column-1 - %h4= t 'footer.developers' - %ul - %li= link_to t('about.documentation'), 'https://docs.joinmastodon.org/' - %li= link_to t('about.api'), 'https://docs.joinmastodon.org/client/intro/' - .column-2 - %h4= link_to t('about.what_is_mastodon'), 'https://joinmastodon.org/' - = link_to logo_as_symbol, root_url, class: 'brand' - .column-3 - %h4= site_hostname - %ul - - unless whitelist_mode? - %li= link_to t('about.about_this'), about_more_path - %li= "v#{Mastodon::Version.to_s}" - .column-4 - %h4= t 'footer.more' - %ul - %li= link_to t('about.source_code'), Mastodon::Version.source_url - %li= link_to t('about.apps'), 'https://joinmastodon.org/apps' - .legal-xs - = link_to "v#{Mastodon::Version.to_s}", Mastodon::Version.source_url - · - = link_to t('about.privacy_policy'), privacy_policy_path - -= render template: 'layouts/application' diff --git a/app/views/privacy/show.html.haml b/app/views/privacy/show.html.haml index cfc285925..95e506641 100644 --- a/app/views/privacy/show.html.haml +++ b/app/views/privacy/show.html.haml @@ -1,4 +1,7 @@ - content_for :page_title do = t('privacy_policy.title') +- content_for :header_tags do + = render partial: 'shared/og' + = render 'shared/web_app' diff --git a/app/views/remote_follow/new.html.haml b/app/views/remote_follow/new.html.haml deleted file mode 100644 index 4e9601f6a..000000000 --- a/app/views/remote_follow/new.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -- content_for :header_tags do - %meta{ name: 'robots', content: 'noindex' }/ - -.form-container - .follow-prompt - %h2= t('remote_follow.prompt') - - = render partial: 'application/card', locals: { account: @account } - - = simple_form_for @remote_follow, as: :remote_follow, url: account_remote_follow_path(@account) do |f| - = render 'shared/error_messages', object: @remote_follow - - = f.input :acct, placeholder: t('remote_follow.acct'), input_html: { autocapitalize: 'none', autocorrect: 'off' } - - .actions - = f.button :button, t('remote_follow.proceed'), type: :submit - - %p.hint.subtle-hint - = t('remote_follow.reason_html', instance: site_hostname) - = t('remote_follow.no_account_html', sign_up_path: available_sign_up_path) diff --git a/app/views/remote_interaction/new.html.haml b/app/views/remote_interaction/new.html.haml deleted file mode 100644 index 2cc0fcb93..000000000 --- a/app/views/remote_interaction/new.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -- content_for :header_tags do - %meta{ name: 'robots', content: 'noindex' }/ - -.form-container - .follow-prompt - %h2= t("remote_interaction.#{@interaction_type}.prompt") - - .public-layout - .activity-stream.activity-stream--highlighted - = render 'statuses/status', status: @status - - = simple_form_for @remote_follow, as: :remote_follow, url: remote_interaction_path(@status) do |f| - = render 'shared/error_messages', object: @remote_follow - - = hidden_field_tag :type, @interaction_type - - = f.input :acct, placeholder: t('remote_follow.acct'), input_html: { autocapitalize: 'none', autocorrect: 'off' } - - .actions - = f.button :button, t("remote_interaction.#{@interaction_type}.proceed"), type: :submit - - %p.hint.subtle-hint - = t('remote_follow.reason_html', instance: site_hostname) - = t('remote_follow.no_account_html', sign_up_path: available_sign_up_path) diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index c67f0e4d9..37001b022 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -56,7 +56,7 @@ - else = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener noreferrer' · - = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do + %span.detailed-status__link - if status.in_reply_to_id.nil? = fa_icon('reply') - else @@ -65,12 +65,12 @@ = " " · - if status.public_visibility? || status.unlisted_visibility? - = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do + %span.detailed-status__link = fa_icon('retweet') %span.detailed-status__reblogs>= friendly_number_to_human status.reblogs_count = " " · - = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do + %span.detailed-status__link = fa_icon('star') %span.detailed-status__favorites>= friendly_number_to_human status.favourites_count = " " diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index f16d2c186..bfde3a260 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -53,18 +53,18 @@ = t 'statuses.show_thread' .status__action-bar - = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button icon-button--with-counter modal-button' do + %span.status__action-bar-button.icon-button.icon-button--with-counter - if status.in_reply_to_id.nil? = fa_icon 'reply fw' - else = fa_icon 'reply-all fw' %span.icon-button__counter= obscured_counter status.replies_count - = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do + %span.status__action-bar-button.icon-button - if status.distributable? = fa_icon 'retweet fw' - elsif status.private_visibility? || status.limited_visibility? = fa_icon 'lock fw' - else = fa_icon 'at fw' - = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button' do + %span.status__action-bar-button.icon-button = fa_icon 'star fw' diff --git a/app/views/statuses/show.html.haml b/app/views/statuses/show.html.haml index 5a3c94b84..106c41725 100644 --- a/app/views/statuses/show.html.haml +++ b/app/views/statuses/show.html.haml @@ -2,7 +2,7 @@ = t('statuses.title', name: display_name(@account), quote: truncate(@status.spoiler_text.presence || @status.text, length: 50, omission: '…', escape: false)) - content_for :header_tags do - - if @account.user&.setting_noindex + - if @account.user_prefers_noindex? %meta{ name: 'robots', content: 'noindex, noarchive' }/ %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: short_account_status_url(@account, @status), format: 'json') }/ diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml new file mode 100644 index 000000000..4b4967a8f --- /dev/null +++ b/app/views/tags/show.html.haml @@ -0,0 +1,5 @@ +- content_for :header_tags do + %meta{ name: 'robots', content: 'noindex' }/ + = render partial: 'shared/og' + += render partial: 'shared/web_app' diff --git a/config/locales/en.yml b/config/locales/en.yml index 504f1b364..412178ca3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,47 +2,26 @@ en: about: about_mastodon_html: 'The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!' - api: API - apps: Mobile apps contact_missing: Not set contact_unavailable: N/A - documentation: Documentation hosted_on: Mastodon hosted on %{domain} - privacy_policy: Privacy Policy - source_code: Source code title: About - what_is_mastodon: What is Mastodon? accounts: - choices_html: "%{name}'s choices:" - endorsements_hint: You can endorse people you follow from the web interface, and they will show up here. - featured_tags_hint: You can feature specific hashtags that will be displayed here. follow: Follow followers: one: Follower other: Followers following: Following instance_actor_flash: This account is a virtual actor used to represent the server itself and not any individual user. It is used for federation purposes and should not be suspended. - joined: Joined %{date} last_active: last active link_verified_on: Ownership of this link was checked on %{date} - media: Media - moved_html: "%{name} has moved to %{new_profile_link}:" - network_hidden: This information is not available nothing_here: There is nothing here! - people_followed_by: People whom %{name} follows - people_who_follow: People who follow %{name} pin_errors: following: You must be already following the person you want to endorse posts: one: Post other: Posts posts_tab_heading: Posts - posts_with_replies: Posts and replies - roles: - bot: Bot - group: Group - unavailable: Profile unavailable - unfollow: Unfollow admin: account_actions: action: Perform action @@ -1176,9 +1155,6 @@ en: hint: This filter applies to select individual posts regardless of other criteria. You can add more posts to this filter from the web interface. title: Filtered posts footer: - developers: Developers - more: More… - resources: Resources trending_now: Trending now generic: all: All @@ -1221,7 +1197,6 @@ en: following: Following list muting: Muting list upload: Upload - in_memoriam_html: In Memoriam. invites: delete: Deactivate expired: Expired @@ -1402,22 +1377,7 @@ en: remove_selected_follows: Unfollow selected users status: Account status remote_follow: - acct: Enter your username@domain you want to act from missing_resource: Could not find the required redirect URL for your account - no_account_html: Don't have an account? You can sign up here - proceed: Proceed to follow - prompt: 'You are going to follow:' - reason_html: "Why is this step necessary? %{instance} might not be the server where you are registered, so we need to redirect you to your home server first." - remote_interaction: - favourite: - proceed: Proceed to favourite - prompt: 'You want to favourite this post:' - reblog: - proceed: Proceed to boost - prompt: 'You want to boost this post:' - reply: - proceed: Proceed to reply - prompt: 'You want to reply to this post:' reports: errors: invalid_rules: does not reference valid rules diff --git a/config/routes.rb b/config/routes.rb index 29ec0f8a5..1ed585f19 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,31 @@ require 'sidekiq_unique_jobs/web' require 'sidekiq-scheduler/web' +# Paths of routes on the web app that to not require to be indexed or +# have alternative format representations requiring separate controllers +WEB_APP_PATHS = %w( + /getting-started + /keyboard-shortcuts + /home + /public + /public/local + /conversations + /lists/(*any) + /notifications + /favourites + /bookmarks + /pinned + /start + /directory + /explore/(*any) + /search + /publish + /follow_requests + /blocks + /domain_blocks + /mutes +).freeze + Rails.application.routes.draw do root 'home#index' @@ -59,9 +84,6 @@ Rails.application.routes.draw do get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" } resources :accounts, path: 'users', only: [:show], param: :username do - get :remote_follow, to: 'remote_follow#new' - post :remote_follow, to: 'remote_follow#create' - resources :statuses, only: [:show] do member do get :activity @@ -85,16 +107,21 @@ Rails.application.routes.draw do resource :inbox, only: [:create], module: :activitypub - get '/@:username', to: 'accounts#show', as: :short_account - get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies - get '/@:username/media', to: 'accounts#show', as: :short_account_media - get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag - get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status - get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status + constraints(username: /[^@\/.]+/) do + get '/@:username', to: 'accounts#show', as: :short_account + get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies + get '/@:username/media', to: 'accounts#show', as: :short_account_media + get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag + end - get '/interact/:id', to: 'remote_interaction#new', as: :remote_interaction - post '/interact/:id', to: 'remote_interaction#create' + constraints(account_username: /[^@\/.]+/) do + get '/@:account_username/following', to: 'following_accounts#index' + get '/@:account_username/followers', to: 'follower_accounts#index' + get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status + get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status + end + get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: /([^\/])+?/ }, format: false get '/settings', to: redirect('/settings/profile') namespace :settings do @@ -187,9 +214,6 @@ Rails.application.routes.draw do resource :relationships, only: [:show, :update] resource :statuses_cleanup, controller: :statuses_cleanup, only: [:show, :update] - get '/explore', to: redirect('/web/explore') - get '/public', to: redirect('/web/public') - get '/public/local', to: redirect('/web/public/local') get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy resource :authorize_interaction, only: [:show, :create] @@ -642,8 +666,11 @@ Rails.application.routes.draw do end end - get '/web/(*any)', to: 'home#index', as: :web + WEB_APP_PATHS.each do |path| + get path, to: 'home#index' + end + get '/web/(*any)', to: redirect('/%{any}', status: 302), as: :web get '/about', to: 'about#show' get '/about/more', to: redirect('/about') diff --git a/package.json b/package.json index 5d8f20abf..0a57336d6 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,6 @@ "redux-immutable": "^4.0.0", "redux-thunk": "^2.4.1", "regenerator-runtime": "^0.13.9", - "rellax": "^1.12.1", "requestidlecallback": "^0.3.0", "reselect": "^4.1.6", "rimraf": "^3.0.2", diff --git a/spec/controllers/account_follow_controller_spec.rb b/spec/controllers/account_follow_controller_spec.rb deleted file mode 100644 index d33cd0499..000000000 --- a/spec/controllers/account_follow_controller_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'rails_helper' - -describe AccountFollowController do - render_views - - let(:user) { Fabricate(:user) } - let(:alice) { Fabricate(:account, username: 'alice') } - - describe 'POST #create' do - let(:service) { double } - - subject { post :create, params: { account_username: alice.username } } - - before do - allow(FollowService).to receive(:new).and_return(service) - allow(service).to receive(:call) - end - - context 'when account is permanently suspended' do - before do - alice.suspend! - alice.deletion_request.destroy - subject - end - - it 'returns http gone' do - expect(response).to have_http_status(410) - end - end - - context 'when account is temporarily suspended' do - before do - alice.suspend! - subject - end - - it 'returns http forbidden' do - expect(response).to have_http_status(403) - end - end - - context 'when signed out' do - before do - subject - end - - it 'does not follow' do - expect(FollowService).not_to receive(:new) - end - end - - context 'when signed in' do - before do - sign_in(user) - subject - end - - it 'redirects to account path' do - expect(service).to have_received(:call).with(user.account, alice, with_rate_limit: true) - expect(response).to redirect_to(account_path(alice)) - end - end - end -end diff --git a/spec/controllers/account_unfollow_controller_spec.rb b/spec/controllers/account_unfollow_controller_spec.rb deleted file mode 100644 index a11f7aa68..000000000 --- a/spec/controllers/account_unfollow_controller_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'rails_helper' - -describe AccountUnfollowController do - render_views - - let(:user) { Fabricate(:user) } - let(:alice) { Fabricate(:account, username: 'alice') } - - describe 'POST #create' do - let(:service) { double } - - subject { post :create, params: { account_username: alice.username } } - - before do - allow(UnfollowService).to receive(:new).and_return(service) - allow(service).to receive(:call) - end - - context 'when account is permanently suspended' do - before do - alice.suspend! - alice.deletion_request.destroy - subject - end - - it 'returns http gone' do - expect(response).to have_http_status(410) - end - end - - context 'when account is temporarily suspended' do - before do - alice.suspend! - subject - end - - it 'returns http forbidden' do - expect(response).to have_http_status(403) - end - end - - context 'when signed out' do - before do - subject - end - - it 'does not unfollow' do - expect(UnfollowService).not_to receive(:new) - end - end - - context 'when signed in' do - before do - sign_in(user) - subject - end - - it 'redirects to account path' do - expect(service).to have_received(:call).with(user.account, alice) - expect(response).to redirect_to(account_path(alice)) - end - end - end -end diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 12266c800..defa8b2d3 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -99,100 +99,6 @@ RSpec.describe AccountsController, type: :controller do end it_behaves_like 'common response characteristics' - - it 'renders public status' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) - end - - it 'renders self-reply' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end - - it 'renders status with media' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) - end - - it 'renders reblog' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end - - it 'renders pinned status' do - expect(response.body).to include(I18n.t('stream_entries.pinned')) - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end - - it 'does not render reply to someone else' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) - end - end - - context 'when signed-in' do - let(:user) { Fabricate(:user) } - - before do - sign_in(user) - end - - context 'when user follows account' do - before do - user.account.follow!(account) - get :show, params: { username: account.username, format: format } - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - end - - context 'when user is blocked' do - before do - account.block!(user.account) - get :show, params: { username: account.username, format: format } - end - - it 'renders unavailable message' do - expect(response.body).to include(I18n.t('accounts.unavailable')) - end - - it 'does not render public status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) - end - - it 'does not render self-reply' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end - - it 'does not render status with media' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media)) - end - - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end - - it 'does not render pinned status' do - expect(response.body).to_not include(I18n.t('stream_entries.pinned')) - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end - - it 'does not render reply to someone else' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) - end - end end context 'with replies' do @@ -202,38 +108,6 @@ RSpec.describe AccountsController, type: :controller do end it_behaves_like 'common response characteristics' - - it 'renders public status' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status)) - end - - it 'renders self-reply' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end - - it 'renders status with media' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) - end - - it 'renders reblog' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end - - it 'does not render pinned status' do - expect(response.body).to_not include(I18n.t('stream_entries.pinned')) - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end - - it 'renders reply to someone else' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reply)) - end end context 'with media' do @@ -243,38 +117,6 @@ RSpec.describe AccountsController, type: :controller do end it_behaves_like 'common response characteristics' - - it 'does not render public status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) - end - - it 'does not render self-reply' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end - - it 'renders status with media' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media)) - end - - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end - - it 'does not render pinned status' do - expect(response.body).to_not include(I18n.t('stream_entries.pinned')) - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end - - it 'does not render reply to someone else' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) - end end context 'with tag' do @@ -289,42 +131,6 @@ RSpec.describe AccountsController, type: :controller do end it_behaves_like 'common response characteristics' - - it 'does not render public status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status)) - end - - it 'does not render self-reply' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply)) - end - - it 'does not render status with media' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media)) - end - - it 'does not render reblog' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog)) - end - - it 'does not render pinned status' do - expect(response.body).to_not include(I18n.t('stream_entries.pinned')) - end - - it 'does not render private status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private)) - end - - it 'does not render direct status' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct)) - end - - it 'does not render reply to someone else' do - expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply)) - end - - it 'renders status with tag' do - expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag)) - end end end diff --git a/spec/controllers/authorize_interactions_controller_spec.rb b/spec/controllers/authorize_interactions_controller_spec.rb index 99f3f6ffc..44f52df69 100644 --- a/spec/controllers/authorize_interactions_controller_spec.rb +++ b/spec/controllers/authorize_interactions_controller_spec.rb @@ -39,7 +39,7 @@ describe AuthorizeInteractionsController do end it 'sets resource from url' do - account = Account.new + account = Fabricate(:account) service = double allow(ResolveURLService).to receive(:new).and_return(service) allow(service).to receive(:call).with('http://example.com').and_return(account) @@ -51,7 +51,7 @@ describe AuthorizeInteractionsController do end it 'sets resource from acct uri' do - account = Account.new + account = Fabricate(:account) service = double allow(ResolveAccountService).to receive(:new).and_return(service) allow(service).to receive(:call).with('found@hostname').and_return(account) diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb index 4d2a6e01a..ab2e82e85 100644 --- a/spec/controllers/follower_accounts_controller_spec.rb +++ b/spec/controllers/follower_accounts_controller_spec.rb @@ -34,27 +34,6 @@ describe FollowerAccountsController do expect(response).to have_http_status(403) end end - - it 'assigns follows' do - expect(response).to have_http_status(200) - - assigned = assigns(:follows).to_a - expect(assigned.size).to eq 2 - expect(assigned[0]).to eq follow1 - expect(assigned[1]).to eq follow0 - end - - it 'does not assign blocked users' do - user = Fabricate(:user) - user.account.block!(follower0) - sign_in(user) - - expect(response).to have_http_status(200) - - assigned = assigns(:follows).to_a - expect(assigned.size).to eq 1 - expect(assigned[0]).to eq follow1 - end end context 'when format is json' do diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb index bb6d221ca..e43dbf882 100644 --- a/spec/controllers/following_accounts_controller_spec.rb +++ b/spec/controllers/following_accounts_controller_spec.rb @@ -34,27 +34,6 @@ describe FollowingAccountsController do expect(response).to have_http_status(403) end end - - it 'assigns follows' do - expect(response).to have_http_status(200) - - assigned = assigns(:follows).to_a - expect(assigned.size).to eq 2 - expect(assigned[0]).to eq follow1 - expect(assigned[1]).to eq follow0 - end - - it 'does not assign blocked users' do - user = Fabricate(:user) - user.account.block!(followee0) - sign_in(user) - - expect(response).to have_http_status(200) - - assigned = assigns(:follows).to_a - expect(assigned.size).to eq 1 - expect(assigned[0]).to eq follow1 - end end context 'when format is json' do diff --git a/spec/controllers/remote_follow_controller_spec.rb b/spec/controllers/remote_follow_controller_spec.rb deleted file mode 100644 index 01d43f48c..000000000 --- a/spec/controllers/remote_follow_controller_spec.rb +++ /dev/null @@ -1,135 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe RemoteFollowController do - render_views - - describe '#new' do - it 'returns success when session is empty' do - account = Fabricate(:account) - get :new, params: { account_username: account.to_param } - - expect(response).to have_http_status(200) - expect(response).to render_template(:new) - expect(assigns(:remote_follow).acct).to be_nil - end - - it 'populates the remote follow with session data when session exists' do - session[:remote_follow] = 'user@example.com' - account = Fabricate(:account) - get :new, params: { account_username: account.to_param } - - expect(response).to have_http_status(200) - expect(response).to render_template(:new) - expect(assigns(:remote_follow).acct).to eq 'user@example.com' - end - end - - describe '#create' do - before do - @account = Fabricate(:account, username: 'test_user') - end - - context 'with a valid acct' do - context 'when webfinger values are wrong' do - it 'renders new when redirect url is nil' do - resource_with_nil_link = double(link: nil) - allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_nil_link) - post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } - - expect(response).to render_template(:new) - expect(response.body).to include(I18n.t('remote_follow.missing_resource')) - end - - it 'renders new when template is nil' do - resource_with_link = double(link: nil) - allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link) - post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } - - expect(response).to render_template(:new) - expect(response.body).to include(I18n.t('remote_follow.missing_resource')) - end - end - - context 'when webfinger values are good' do - before do - resource_with_link = double(link: 'http://example.com/follow_me?acct={uri}') - allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link) - post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } - end - - it 'saves the session' do - expect(session[:remote_follow]).to eq 'user@example.com' - end - - it 'redirects to the remote location' do - expect(response).to redirect_to("http://example.com/follow_me?acct=https%3A%2F%2F#{Rails.configuration.x.local_domain}%2Fusers%2Ftest_user") - end - end - end - - context 'with an invalid acct' do - it 'renders new when acct is missing' do - post :create, params: { account_username: @account.to_param, remote_follow: { acct: '' } } - - expect(response).to render_template(:new) - end - - it 'renders new with error when webfinger fails' do - allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_raise(Webfinger::Error) - post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } - - expect(response).to render_template(:new) - expect(response.body).to include(I18n.t('remote_follow.missing_resource')) - end - - it 'renders new when occur HTTP::ConnectionError' do - allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@unknown').and_raise(HTTP::ConnectionError) - post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@unknown' } } - - expect(response).to render_template(:new) - expect(response.body).to include(I18n.t('remote_follow.missing_resource')) - end - end - end - - context 'with a permanently suspended account' do - before do - @account = Fabricate(:account) - @account.suspend! - @account.deletion_request.destroy - end - - it 'returns http gone on GET to #new' do - get :new, params: { account_username: @account.to_param } - - expect(response).to have_http_status(410) - end - - it 'returns http gone on POST to #create' do - post :create, params: { account_username: @account.to_param } - - expect(response).to have_http_status(410) - end - end - - context 'with a temporarily suspended account' do - before do - @account = Fabricate(:account) - @account.suspend! - end - - it 'returns http forbidden on GET to #new' do - get :new, params: { account_username: @account.to_param } - - expect(response).to have_http_status(403) - end - - it 'returns http forbidden on POST to #create' do - post :create, params: { account_username: @account.to_param } - - expect(response).to have_http_status(403) - end - end -end diff --git a/spec/controllers/remote_interaction_controller_spec.rb b/spec/controllers/remote_interaction_controller_spec.rb deleted file mode 100644 index bb0074b11..000000000 --- a/spec/controllers/remote_interaction_controller_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe RemoteInteractionController, type: :controller do - render_views - - let(:status) { Fabricate(:status) } - - describe 'GET #new' do - it 'returns 200' do - get :new, params: { id: status.id } - expect(response).to have_http_status(200) - end - end - - describe 'POST #create' do - context '@remote_follow is valid' do - it 'returns 302' do - allow_any_instance_of(RemoteFollow).to receive(:valid?) { true } - allow_any_instance_of(RemoteFollow).to receive(:addressable_template) do - Addressable::Template.new('https://hoge.com') - end - - post :create, params: { id: status.id, remote_follow: { acct: '@hoge' } } - expect(response).to have_http_status(302) - end - end - - context '@remote_follow is invalid' do - it 'returns 200' do - allow_any_instance_of(RemoteFollow).to receive(:valid?) { false } - post :create, params: { id: status.id, remote_follow: { acct: '@hoge' } } - - expect(response).to have_http_status(200) - end - end - end -end diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb index 1fd8494d6..547bcfb39 100644 --- a/spec/controllers/tags_controller_spec.rb +++ b/spec/controllers/tags_controller_spec.rb @@ -10,16 +10,15 @@ RSpec.describe TagsController, type: :controller do let!(:late) { Fabricate(:status, tags: [tag], text: 'late #test') } context 'when tag exists' do - it 'redirects to web version' do + it 'returns http success' do get :show, params: { id: 'test', max_id: late.id } - expect(response).to redirect_to('/web/tags/test') + expect(response).to have_http_status(200) end end context 'when tag does not exist' do - it 'returns http missing for non-existent tag' do + it 'returns http not found' do get :show, params: { id: 'none' } - expect(response).to have_http_status(404) end end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index b6de3e9d1..ec4f9a53f 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -18,36 +18,16 @@ feature 'Profile' do visit account_path('alice') is_expected.to have_title("alice (@alice@#{local_domain})") - - within('.public-account-header h1') do - is_expected.to have_content("alice @alice@#{local_domain}") - end - - bio_elem = first('.public-account-bio') - expect(bio_elem).to have_content(alice_bio) - # The bio has hashtags made clickable - expect(bio_elem).to have_link('cryptology') - expect(bio_elem).to have_link('science') - # Nicknames are make clickable - expect(bio_elem).to have_link('@alice') - expect(bio_elem).to have_link('@bob') - # Nicknames not on server are not clickable - expect(bio_elem).not_to have_link('@pepe') end scenario 'I can change my account' do visit settings_profile_path + fill_in 'Display name', with: 'Bob' fill_in 'Bio', with: 'Bob is silent' - first('.btn[type=submit]').click - is_expected.to have_content 'Changes successfully saved!' - # View my own public profile and see the changes - click_link "Bob @bob@#{local_domain}" + first('button[type=submit]').click - within('.public-account-header h1') do - is_expected.to have_content("Bob @bob@#{local_domain}") - end - expect(first('.public-account-bio')).to have_content('Bob is silent') + is_expected.to have_content 'Changes successfully saved!' end end diff --git a/spec/lib/permalink_redirector_spec.rb b/spec/lib/permalink_redirector_spec.rb index abda57da4..a00913656 100644 --- a/spec/lib/permalink_redirector_spec.rb +++ b/spec/lib/permalink_redirector_spec.rb @@ -3,40 +3,31 @@ require 'rails_helper' describe PermalinkRedirector do + let(:remote_account) { Fabricate(:account, username: 'alice', domain: 'example.com', url: 'https://example.com/@alice', id: 2) } + describe '#redirect_url' do before do - account = Fabricate(:account, username: 'alice', id: 1) - Fabricate(:status, account: account, id: 123) + Fabricate(:status, account: remote_account, id: 123, url: 'https://example.com/status-123') end it 'returns path for legacy account links' do - redirector = described_class.new('web/accounts/1') - expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice' + redirector = described_class.new('accounts/2') + expect(redirector.redirect_path).to eq 'https://example.com/@alice' end it 'returns path for legacy status links' do - redirector = described_class.new('web/statuses/123') - expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice/123' - end - - it 'returns path for legacy tag links' do - redirector = described_class.new('web/timelines/tag/hoge') - expect(redirector.redirect_path).to be_nil + redirector = described_class.new('statuses/123') + expect(redirector.redirect_path).to eq 'https://example.com/status-123' end it 'returns path for pretty account links' do - redirector = described_class.new('web/@alice') - expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice' + redirector = described_class.new('@alice@example.com') + expect(redirector.redirect_path).to eq 'https://example.com/@alice' end it 'returns path for pretty status links' do - redirector = described_class.new('web/@alice/123') - expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice/123' - end - - it 'returns path for pretty tag links' do - redirector = described_class.new('web/tags/hoge') - expect(redirector.redirect_path).to be_nil + redirector = described_class.new('@alice/123') + expect(redirector.redirect_path).to eq 'https://example.com/status-123' end end end diff --git a/spec/requests/account_show_page_spec.rb b/spec/requests/account_show_page_spec.rb index 4e51cf7ef..e84c46c47 100644 --- a/spec/requests/account_show_page_spec.rb +++ b/spec/requests/account_show_page_spec.rb @@ -3,17 +3,6 @@ require 'rails_helper' describe 'The account show page' do - it 'Has an h-feed with correct number of h-entry objects in it' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - _status = Fabricate(:status, account: alice, text: 'Hello World') - _status2 = Fabricate(:status, account: alice, text: 'Hello World Again') - _status3 = Fabricate(:status, account: alice, text: 'Are You Still There World?') - - get '/@alice' - - expect(h_feed_entries.size).to eq(3) - end - it 'has valid opengraph tags' do alice = Fabricate(:account, username: 'alice', display_name: 'Alice') _status = Fabricate(:status, account: alice, text: 'Hello World') @@ -33,8 +22,4 @@ describe 'The account show page' do def head_section Nokogiri::Slop(response.body).html.head end - - def h_feed_entries - Nokogiri::HTML(response.body).search('.h-feed .h-entry') - end end diff --git a/spec/routing/accounts_routing_spec.rb b/spec/routing/accounts_routing_spec.rb index d04cb27f0..3f0e9b3e9 100644 --- a/spec/routing/accounts_routing_spec.rb +++ b/spec/routing/accounts_routing_spec.rb @@ -1,31 +1,83 @@ require 'rails_helper' describe 'Routes under accounts/' do - describe 'the route for accounts who are followers of an account' do - it 'routes to the followers action with the right username' do - expect(get('/users/name/followers')). - to route_to('follower_accounts#index', account_username: 'name') + context 'with local username' do + let(:username) { 'alice' } + + it 'routes /@:username' do + expect(get("/@#{username}")).to route_to('accounts#show', username: username) end - end - describe 'the route for accounts who are followed by an account' do - it 'routes to the following action with the right username' do - expect(get('/users/name/following')). - to route_to('following_accounts#index', account_username: 'name') + it 'routes /@:username.json' do + expect(get("/@#{username}.json")).to route_to('accounts#show', username: username, format: 'json') + end + + it 'routes /@:username.rss' do + expect(get("/@#{username}.rss")).to route_to('accounts#show', username: username, format: 'rss') + end + + it 'routes /@:username/:id' do + expect(get("/@#{username}/123")).to route_to('statuses#show', account_username: username, id: '123') + end + + it 'routes /@:username/:id/embed' do + expect(get("/@#{username}/123/embed")).to route_to('statuses#embed', account_username: username, id: '123') + end + + it 'routes /@:username/following' do + expect(get("/@#{username}/following")).to route_to('following_accounts#index', account_username: username) + end + + it 'routes /@:username/followers' do + expect(get("/@#{username}/followers")).to route_to('follower_accounts#index', account_username: username) + end + + it 'routes /@:username/with_replies' do + expect(get("/@#{username}/with_replies")).to route_to('accounts#show', username: username) + end + + it 'routes /@:username/media' do + expect(get("/@#{username}/media")).to route_to('accounts#show', username: username) end - end - describe 'the route for following an account' do - it 'routes to the follow create action with the right username' do - expect(post('/users/name/follow')). - to route_to('account_follow#create', account_username: 'name') + it 'routes /@:username/tagged/:tag' do + expect(get("/@#{username}/tagged/foo")).to route_to('accounts#show', username: username, tag: 'foo') end end - describe 'the route for unfollowing an account' do - it 'routes to the unfollow create action with the right username' do - expect(post('/users/name/unfollow')). - to route_to('account_unfollow#create', account_username: 'name') + context 'with remote username' do + let(:username) { 'alice@example.com' } + + it 'routes /@:username' do + expect(get("/@#{username}")).to route_to('home#index', username_with_domain: username) + end + + it 'routes /@:username/:id' do + expect(get("/@#{username}/123")).to route_to('home#index', username_with_domain: username, any: '123') + end + + it 'routes /@:username/:id/embed' do + expect(get("/@#{username}/123/embed")).to route_to('home#index', username_with_domain: username, any: '123/embed') + end + + it 'routes /@:username/following' do + expect(get("/@#{username}/following")).to route_to('home#index', username_with_domain: username, any: 'following') + end + + it 'routes /@:username/followers' do + expect(get("/@#{username}/followers")).to route_to('home#index', username_with_domain: username, any: 'followers') + end + + it 'routes /@:username/with_replies' do + expect(get("/@#{username}/with_replies")).to route_to('home#index', username_with_domain: username, any: 'with_replies') + end + + it 'routes /@:username/media' do + expect(get("/@#{username}/media")).to route_to('home#index', username_with_domain: username, any: 'media') + end + + it 'routes /@:username/tagged/:tag' do + expect(get("/@#{username}/tagged/foo")).to route_to('home#index', username_with_domain: username, any: 'tagged/foo') end end end diff --git a/yarn.lock b/yarn.lock index 6ae965464..98666f23d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9663,11 +9663,6 @@ regjsparser@^0.8.2: dependencies: jsesc "~0.5.0" -rellax@^1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/rellax/-/rellax-1.12.1.tgz#1b433ef7ac4aa3573449a33efab391c112f6b34d" - integrity sha512-XBIi0CDpW5FLTujYjYBn1CIbK2CJL6TsAg/w409KghP2LucjjzBjsujXDAjyBLWgsfupfUcL5WzdnIPcGfK7XA== - remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" -- cgit From 7c152acb2cc545a87610de349a94e14f45fbed5d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 22 Oct 2022 11:44:41 +0200 Subject: Change settings area to be separated into categories in admin UI (#19407) And update all descriptions --- app/controllers/admin/settings/about_controller.rb | 9 ++ .../admin/settings/appearance_controller.rb | 9 ++ .../admin/settings/branding_controller.rb | 9 ++ .../admin/settings/content_retention_controller.rb | 9 ++ .../admin/settings/discovery_controller.rb | 9 ++ .../admin/settings/registrations_controller.rb | 9 ++ app/controllers/admin/settings_controller.rb | 10 +- app/controllers/admin/site_uploads_controller.rb | 2 +- app/helpers/admin/settings_helper.rb | 7 -- app/javascript/styles/mastodon/admin.scss | 82 +++++++++++++---- app/javascript/styles/mastodon/components.scss | 5 + app/javascript/styles/mastodon/forms.scss | 16 +++- app/models/form/admin_settings.rb | 57 +++++++----- .../rest/extended_description_serializer.rb | 12 ++- app/views/admin/settings/about/show.html.haml | 33 +++++++ app/views/admin/settings/appearance/show.html.haml | 34 +++++++ app/views/admin/settings/branding/show.html.haml | 39 ++++++++ .../settings/content_retention/show.html.haml | 22 +++++ app/views/admin/settings/discovery/show.html.haml | 40 ++++++++ app/views/admin/settings/edit.html.haml | 102 --------------------- .../admin/settings/registrations/show.html.haml | 27 ++++++ app/views/admin/settings/shared/_links.html.haml | 8 ++ app/views/layouts/admin.html.haml | 11 ++- config/locales/en.yml | 87 +++++------------- config/locales/simple_form.en.yml | 37 ++++++++ config/navigation.rb | 2 +- config/routes.rb | 13 ++- .../admin/settings/branding_controller_spec.rb | 53 +++++++++++ spec/controllers/admin/settings_controller_spec.rb | 71 -------------- 29 files changed, 528 insertions(+), 296 deletions(-) create mode 100644 app/controllers/admin/settings/about_controller.rb create mode 100644 app/controllers/admin/settings/appearance_controller.rb create mode 100644 app/controllers/admin/settings/branding_controller.rb create mode 100644 app/controllers/admin/settings/content_retention_controller.rb create mode 100644 app/controllers/admin/settings/discovery_controller.rb create mode 100644 app/controllers/admin/settings/registrations_controller.rb create mode 100644 app/views/admin/settings/about/show.html.haml create mode 100644 app/views/admin/settings/appearance/show.html.haml create mode 100644 app/views/admin/settings/branding/show.html.haml create mode 100644 app/views/admin/settings/content_retention/show.html.haml create mode 100644 app/views/admin/settings/discovery/show.html.haml delete mode 100644 app/views/admin/settings/edit.html.haml create mode 100644 app/views/admin/settings/registrations/show.html.haml create mode 100644 app/views/admin/settings/shared/_links.html.haml create mode 100644 spec/controllers/admin/settings/branding_controller_spec.rb delete mode 100644 spec/controllers/admin/settings_controller_spec.rb (limited to 'spec/controllers') diff --git a/app/controllers/admin/settings/about_controller.rb b/app/controllers/admin/settings/about_controller.rb new file mode 100644 index 000000000..86327fe39 --- /dev/null +++ b/app/controllers/admin/settings/about_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::AboutController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_about_path + end +end diff --git a/app/controllers/admin/settings/appearance_controller.rb b/app/controllers/admin/settings/appearance_controller.rb new file mode 100644 index 000000000..39b2448d8 --- /dev/null +++ b/app/controllers/admin/settings/appearance_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::AppearanceController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_appearance_path + end +end diff --git a/app/controllers/admin/settings/branding_controller.rb b/app/controllers/admin/settings/branding_controller.rb new file mode 100644 index 000000000..4a4d76f49 --- /dev/null +++ b/app/controllers/admin/settings/branding_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::BrandingController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_branding_path + end +end diff --git a/app/controllers/admin/settings/content_retention_controller.rb b/app/controllers/admin/settings/content_retention_controller.rb new file mode 100644 index 000000000..b88336a2c --- /dev/null +++ b/app/controllers/admin/settings/content_retention_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::ContentRetentionController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_content_retention_path + end +end diff --git a/app/controllers/admin/settings/discovery_controller.rb b/app/controllers/admin/settings/discovery_controller.rb new file mode 100644 index 000000000..be4b57f79 --- /dev/null +++ b/app/controllers/admin/settings/discovery_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::DiscoveryController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_discovery_path + end +end diff --git a/app/controllers/admin/settings/registrations_controller.rb b/app/controllers/admin/settings/registrations_controller.rb new file mode 100644 index 000000000..b4a74349c --- /dev/null +++ b/app/controllers/admin/settings/registrations_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Admin::Settings::RegistrationsController < Admin::SettingsController + private + + def after_update_redirect_path + admin_settings_registrations_path + end +end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index dc1c79b7f..338a3638c 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -2,7 +2,7 @@ module Admin class SettingsController < BaseController - def edit + def show authorize :settings, :show? @admin_settings = Form::AdminSettings.new @@ -15,14 +15,18 @@ module Admin if @admin_settings.save flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to edit_admin_settings_path + redirect_to after_update_redirect_path else - render :edit + render :show end end private + def after_update_redirect_path + raise NotImplementedError + end + def settings_params params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end diff --git a/app/controllers/admin/site_uploads_controller.rb b/app/controllers/admin/site_uploads_controller.rb index cacecedb0..a5d2cf41c 100644 --- a/app/controllers/admin/site_uploads_controller.rb +++ b/app/controllers/admin/site_uploads_controller.rb @@ -9,7 +9,7 @@ module Admin @site_upload.destroy! - redirect_to edit_admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') + redirect_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg') end private diff --git a/app/helpers/admin/settings_helper.rb b/app/helpers/admin/settings_helper.rb index baf14ab25..a133b4e7d 100644 --- a/app/helpers/admin/settings_helper.rb +++ b/app/helpers/admin/settings_helper.rb @@ -1,11 +1,4 @@ # frozen_string_literal: true module Admin::SettingsHelper - def site_upload_delete_hint(hint, var) - upload = SiteUpload.find_by(var: var.to_s) - return hint unless upload - - link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete } - safe_join([hint, link], '
'.html_safe) - end end diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 1c5494cde..affe1c79c 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -188,21 +188,70 @@ $content-width: 840px; padding-top: 30px; } - &-heading { - display: flex; + &__heading { padding-bottom: 36px; border-bottom: 1px solid lighten($ui-base-color, 8%); - margin: -15px -15px 40px 0; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; + margin-bottom: 40px; - & > * { - margin-top: 15px; - margin-right: 15px; + &__row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + margin: -15px -15px 0 0; + + & > * { + margin-top: 15px; + margin-right: 15px; + } } - &-actions { + &__tabs { + margin-top: 30px; + margin-bottom: -31px; + + & > div { + display: flex; + gap: 10px; + } + + a { + font-size: 14px; + display: inline-flex; + align-items: center; + padding: 7px 15px; + border-radius: 4px; + color: $darker-text-color; + text-decoration: none; + position: relative; + font-weight: 500; + gap: 5px; + white-space: nowrap; + + &.selected { + font-weight: 700; + color: $primary-text-color; + + &::after { + content: ""; + display: block; + width: 100%; + border-bottom: 1px solid $ui-highlight-color; + position: absolute; + bottom: -5px; + left: 0; + } + } + + &:hover, + &:focus, + &:active { + background: lighten($ui-base-color, 4%); + } + } + } + + &__actions { display: inline-flex; & > :not(:first-child) { @@ -228,11 +277,7 @@ $content-width: 840px; color: $secondary-text-color; font-size: 24px; line-height: 36px; - font-weight: 400; - - @media screen and (max-width: $no-columns-breakpoint) { - font-weight: 700; - } + font-weight: 700; } h3 { @@ -437,6 +482,11 @@ body, } } + & > div { + display: flex; + gap: 5px; + } + strong { font-weight: 500; text-transform: uppercase; @@ -1143,7 +1193,7 @@ a.name-tag, path:first-child { fill: rgba($highlight-text-color, 0.25) !important; - fill-opacity: 1 !important; + fill-opacity: 100% !important; } path:last-child { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 587eba663..5d0ff8536 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -29,6 +29,11 @@ background: transparent; padding: 0; cursor: pointer; + text-decoration: none; + + &--destructive { + color: $error-value-color; + } &:hover, &:active { diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 69a0b22d6..25c9c9fe5 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -254,7 +254,7 @@ code { & > label { font-family: inherit; - font-size: 16px; + font-size: 14px; color: $primary-text-color; display: block; font-weight: 500; @@ -291,6 +291,20 @@ code { .input:last-child { margin-bottom: 0; } + + &__thumbnail { + display: block; + margin: 0; + margin-bottom: 10px; + max-width: 100%; + height: auto; + border-radius: 4px; + background: url("images/void.png"); + + &:last-child { + margin-bottom: 0; + } + } } .fields-row { diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index b6bb3d795..957a32b7c 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -8,7 +8,6 @@ class Form::AdminSettings site_contact_email site_title site_short_description - site_description site_extended_description site_terms registrations_mode @@ -53,45 +52,55 @@ class Form::AdminSettings attr_accessor(*KEYS) - validates :site_short_description, :site_description, html: { wrap_with: :p } - validates :site_extended_description, :site_terms, :closed_registrations_message, html: true - validates :registrations_mode, inclusion: { in: %w(open approved none) } - validates :site_contact_email, :site_contact_username, presence: true - validates :site_contact_username, existing_username: true - validates :bootstrap_timeline_accounts, existing_username: { multiple: true } - validates :show_domain_blocks, inclusion: { in: %w(disabled users all) } - validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) } - validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true + validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) } + validates :site_contact_email, :site_contact_username, presence: true, if: -> { defined?(@site_contact_username) || defined?(@site_contact_email) } + validates :site_contact_username, existing_username: true, if: -> { defined?(@site_contact_username) } + validates :bootstrap_timeline_accounts, existing_username: { multiple: true }, if: -> { defined?(@bootstrap_timeline_accounts) } + validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) } + validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) } + validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } + validates :site_short_description, length: { maximum: 200 }, if: -> { defined?(@site_short_description) } - def initialize(_attributes = {}) - super - initialize_attributes + KEYS.each do |key| + define_method(key) do + return instance_variable_get("@#{key}") if instance_variable_defined?("@#{key}") + + stored_value = begin + if UPLOAD_KEYS.include?(key) + SiteUpload.where(var: key).first_or_initialize(var: key) + else + Setting.public_send(key) + end + end + + instance_variable_set("@#{key}", stored_value) + end + end + + UPLOAD_KEYS.each do |key| + define_method("#{key}=") do |file| + value = public_send(key) + value.file = file + end end def save return false unless valid? KEYS.each do |key| - value = instance_variable_get("@#{key}") + next unless instance_variable_defined?("@#{key}") - if UPLOAD_KEYS.include?(key) && !value.nil? - upload = SiteUpload.where(var: key).first_or_initialize(var: key) - upload.update(file: value) + if UPLOAD_KEYS.include?(key) + public_send(key).save else setting = Setting.where(var: key).first_or_initialize(var: key) - setting.update(value: typecast_value(key, value)) + setting.update(value: typecast_value(key, instance_variable_get("@#{key}"))) end end end private - def initialize_attributes - KEYS.each do |key| - instance_variable_set("@#{key}", Setting.public_send(key)) if instance_variable_get("@#{key}").nil? - end - end - def typecast_value(key, value) if BOOLEAN_KEYS.include?(key) value == '1' diff --git a/app/serializers/rest/extended_description_serializer.rb b/app/serializers/rest/extended_description_serializer.rb index 0c3649033..c0fa3450d 100644 --- a/app/serializers/rest/extended_description_serializer.rb +++ b/app/serializers/rest/extended_description_serializer.rb @@ -8,6 +8,16 @@ class REST::ExtendedDescriptionSerializer < ActiveModel::Serializer end def content - object.text + if object.text.present? + markdown.render(object.text) + else + '' + end + end + + private + + def markdown + @markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML) end end diff --git a/app/views/admin/settings/about/show.html.haml b/app/views/admin/settings/about/show.html.haml new file mode 100644 index 000000000..366d213f6 --- /dev/null +++ b/app/views/admin/settings/about/show.html.haml @@ -0,0 +1,33 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('admin.settings.about.title') + +- content_for :heading do + %h2= t('admin.settings.title') + = render partial: 'admin/settings/shared/links' + += simple_form_for @admin_settings, url: admin_settings_about_path, html: { method: :patch } do |f| + = render 'shared/error_messages', object: @admin_settings + + %p.lead= t('admin.settings.about.preamble') + + .fields-group + = f.input :site_extended_description, wrapper: :with_block_label, as: :text, input_html: { rows: 8 } + + %p.hint + = t 'admin.settings.about.rules_hint' + = link_to t('admin.settings.about.manage_rules'), admin_rules_path + + .fields-row + .fields-row__column.fields-row__column-6.fields-group + = f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + .fields-row__column.fields-row__column-6.fields-group + = f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + + .fields-group + = f.input :site_terms, wrapper: :with_block_label, as: :text, input_html: { rows: 8 } + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/settings/appearance/show.html.haml b/app/views/admin/settings/appearance/show.html.haml new file mode 100644 index 000000000..d321c4b04 --- /dev/null +++ b/app/views/admin/settings/appearance/show.html.haml @@ -0,0 +1,34 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('admin.settings.appearance.title') + +- content_for :heading do + %h2= t('admin.settings.title') + = render partial: 'admin/settings/shared/links' + += simple_form_for @admin_settings, url: admin_settings_appearance_path, html: { method: :patch } do |f| + = render 'shared/error_messages', object: @admin_settings + + %p.lead= t('admin.settings.appearance.preamble') + + .fields-group + = f.input :theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false + + .fields-group + = f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 } + + .fields-row + .fields-row__column.fields-row__column-6.fields-group + = f.input :mascot, as: :file, wrapper: :with_block_label + + .fields-row__column.fields-row__column-6.fields-group + - if @admin_settings.mascot.persisted? + = image_tag @admin_settings.mascot.file.url, class: 'fields-group__thumbnail' + = link_to admin_site_upload_path(@admin_settings.mascot), data: { method: :delete }, class: 'link-button link-button--destructive' do + = fa_icon 'trash fw' + = t('admin.site_uploads.delete') + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/settings/branding/show.html.haml b/app/views/admin/settings/branding/show.html.haml new file mode 100644 index 000000000..74a6fadf9 --- /dev/null +++ b/app/views/admin/settings/branding/show.html.haml @@ -0,0 +1,39 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('admin.settings.branding.title') + +- content_for :heading do + %h2= t('admin.settings.title') + = render partial: 'admin/settings/shared/links' + += simple_form_for @admin_settings, url: admin_settings_branding_path, html: { method: :patch } do |f| + = render 'shared/error_messages', object: @admin_settings + + %p.lead= t('admin.settings.branding.preamble') + + .fields-group + = f.input :site_title, wrapper: :with_label + + .fields-row + .fields-row__column.fields-row__column-6.fields-group + = f.input :site_contact_username, wrapper: :with_label + .fields-row__column.fields-row__column-6.fields-group + = f.input :site_contact_email, wrapper: :with_label + + .fields-group + = f.input :site_short_description, wrapper: :with_block_label, as: :text, input_html: { rows: 2, maxlength: 200 } + + .fields-row + .fields-row__column.fields-row__column-6.fields-group + = f.input :thumbnail, as: :file, wrapper: :with_block_label + .fields-row__column.fields-row__column-6.fields-group + - if @admin_settings.thumbnail.persisted? + = image_tag @admin_settings.thumbnail.file.url(:'@1x'), class: 'fields-group__thumbnail' + = link_to admin_site_upload_path(@admin_settings.thumbnail), data: { method: :delete }, class: 'link-button link-button--destructive' do + = fa_icon 'trash fw' + = t('admin.site_uploads.delete') + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/settings/content_retention/show.html.haml b/app/views/admin/settings/content_retention/show.html.haml new file mode 100644 index 000000000..36856127f --- /dev/null +++ b/app/views/admin/settings/content_retention/show.html.haml @@ -0,0 +1,22 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('admin.settings.content_retention.title') + +- content_for :heading do + %h2= t('admin.settings.title') + = render partial: 'admin/settings/shared/links' + += simple_form_for @admin_settings, url: admin_settings_content_retention_path, html: { method: :patch } do |f| + = render 'shared/error_messages', object: @admin_settings + + %p.lead= t('admin.settings.content_retention.preamble') + + .fields-group + = f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } + = f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } + = f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml new file mode 100644 index 000000000..e63c853fb --- /dev/null +++ b/app/views/admin/settings/discovery/show.html.haml @@ -0,0 +1,40 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('admin.settings.discovery.title') + +- content_for :heading do + %h2= t('admin.settings.title') + = render partial: 'admin/settings/shared/links' + += simple_form_for @admin_settings, url: admin_settings_discovery_path, html: { method: :patch } do |f| + = render 'shared/error_messages', object: @admin_settings + + %p.lead= t('admin.settings.discovery.preamble') + + %h4= t('admin.settings.discovery.trends') + + .fields-group + = f.input :trends, as: :boolean, wrapper: :with_label + + .fields-group + = f.input :trendable_by_default, as: :boolean, wrapper: :with_label + + %h4= t('admin.settings.discovery.public_timelines') + + .fields-group + = f.input :timeline_preview, as: :boolean, wrapper: :with_label + + %h4= t('admin.settings.discovery.follow_recommendations') + + .fields-group + = f.input :bootstrap_timeline_accounts, wrapper: :with_block_label + + %h4= t('admin.settings.discovery.profile_directory') + + .fields-group + = f.input :profile_directory, as: :boolean, wrapper: :with_label + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml deleted file mode 100644 index 15b1a2498..000000000 --- a/app/views/admin/settings/edit.html.haml +++ /dev/null @@ -1,102 +0,0 @@ -- content_for :header_tags do - = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' - -- content_for :page_title do - = t('admin.settings.title') - - - content_for :heading_actions do - = button_tag t('generic.save_changes'), class: 'button', form: 'edit_admin' - -= simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch, id: 'edit_admin' } do |f| - = render 'shared/error_messages', object: @admin_settings - - .fields-group - = f.input :site_title, wrapper: :with_label, label: t('admin.settings.site_title') - - .fields-row - .fields-row__column.fields-row__column-6.fields-group - = f.input :theme, collection: Themes.instance.names, label: t('simple_form.labels.defaults.setting_theme'), label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false - .fields-row__column.fields-row__column-6.fields-group - = f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, label: t('admin.settings.registrations_mode.title'), include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") } - - .fields-row - .fields-row__column.fields-row__column-6.fields-group - = f.input :site_contact_username, wrapper: :with_label, label: t('admin.settings.contact_information.username') - .fields-row__column.fields-row__column-6.fields-group - = f.input :site_contact_email, wrapper: :with_label, label: t('admin.settings.contact_information.email') - - .fields-group - = f.input :site_short_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_short_description.title'), hint: t('admin.settings.site_short_description.desc_html'), input_html: { rows: 2 } - - .fields-group - = f.input :site_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description.title'), hint: t('admin.settings.site_description.desc_html'), input_html: { rows: 2 } - - .fields-row - .fields-row__column.fields-row__column-6.fields-group - = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: site_upload_delete_hint(t('admin.settings.thumbnail.desc_html'), :thumbnail) - - .fields-row - .fields-row__column.fields-row__column-6.fields-group - = f.input :mascot, as: :file, wrapper: :with_block_label, label: t('admin.settings.mascot.title'), hint: site_upload_delete_hint(t('admin.settings.mascot.desc_html'), :mascot) - - %hr.spacer/ - - .fields-group - = f.input :require_invite_text, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.require_invite_text.title'), hint: t('admin.settings.registrations.require_invite_text.desc_html'), disabled: !approved_registrations? - - %hr.spacer/ - - .fields-group - = f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html') - - %hr.spacer/ - - - unless whitelist_mode? - .fields-group - = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') - - - unless whitelist_mode? - .fields-group - = f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html'), recommended: true - - .fields-group - = f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html'), recommended: true - - .fields-group - = f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html') - - .fields-group - = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html') - - .fields-group - = f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html') - - .fields-group - = f.input :trendable_by_default, as: :boolean, wrapper: :with_label, label: t('admin.settings.trendable_by_default.title'), hint: t('admin.settings.trendable_by_default.desc_html'), recommended: :not_recommended - - .fields-group - = f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html') - - %hr.spacer/ - - .fields-row - .fields-row__column.fields-row__column-6.fields-group - = f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' - .fields-row__column.fields-row__column-6.fields-group - = f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks_rationale.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' - - .fields-group - = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode? - = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 } - = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } - = f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html') - - %hr.spacer/ - - .fields-group - = f.input :media_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } - = f.input :content_cache_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } - = f.input :backups_retention_period, wrapper: :with_block_label, input_html: { pattern: '[0-9]+' } - - .actions - = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml new file mode 100644 index 000000000..0129332d7 --- /dev/null +++ b/app/views/admin/settings/registrations/show.html.haml @@ -0,0 +1,27 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('admin.settings.registrations.title') + +- content_for :heading do + %h2= t('admin.settings.title') + = render partial: 'admin/settings/shared/links' + += simple_form_for @admin_settings, url: admin_settings_branding_path, html: { method: :patch } do |f| + = render 'shared/error_messages', object: @admin_settings + + %p.lead= t('admin.settings.registrations.preamble') + + .fields-row + .fields-row__column.fields-row__column-6.fields-group + = f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") } + + .fields-row__column.fields-row__column-6.fields-group + = f.input :require_invite_text, as: :boolean, wrapper: :with_label, disabled: !approved_registrations? + + .fields-group + = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, input_html: { rows: 2 } + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/settings/shared/_links.html.haml b/app/views/admin/settings/shared/_links.html.haml new file mode 100644 index 000000000..1294c26ce --- /dev/null +++ b/app/views/admin/settings/shared/_links.html.haml @@ -0,0 +1,8 @@ +.content__heading__tabs + = render_navigation renderer: :links do |primary| + - primary.item :branding, safe_join([fa_icon('pencil fw'), t('admin.settings.branding.title')]), admin_settings_branding_path + - primary.item :about, safe_join([fa_icon('file-text fw'), t('admin.settings.about.title')]), admin_settings_about_path + - primary.item :registrations, safe_join([fa_icon('users fw'), t('admin.settings.registrations.title')]), admin_settings_registrations_path + - primary.item :discovery, safe_join([fa_icon('search fw'), t('admin.settings.discovery.title')]), admin_settings_discovery_path + - primary.item :content_retention, safe_join([fa_icon('history fw'), t('admin.settings.content_retention.title')]), admin_settings_content_retention_path + - primary.item :appearance, safe_join([fa_icon('desktop fw'), t('admin.settings.appearance.title')]), admin_settings_appearance_path diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index e577b9803..59021ad88 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -22,15 +22,16 @@ .content-wrapper .content - .content-heading + .content__heading - if content_for?(:heading) = yield :heading - else - %h2= yield :page_title + .content__heading__row + %h2= yield :page_title - - if :heading_actions - .content-heading-actions - = yield :heading_actions + - if content_for?(:heading_actions) + .content__heading__actions + = yield :heading_actions = render 'application/flashes' diff --git a/config/locales/en.yml b/config/locales/en.yml index 412178ca3..70850d478 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -667,79 +667,40 @@ en: empty: No server rules have been defined yet. title: Server rules settings: - activity_api_enabled: - desc_html: Counts of locally published posts, active users, and new registrations in weekly buckets - title: Publish aggregate statistics about user activity in the API - bootstrap_timeline_accounts: - desc_html: Separate multiple usernames by comma. These accounts will be guaranteed to be shown in follow recommendations - title: Recommend these accounts to new users - contact_information: - email: Business e-mail - username: Contact username - custom_css: - desc_html: Modify the look with CSS loaded on every page - title: Custom CSS - default_noindex: - desc_html: Affects all users who have not changed this setting themselves - title: Opt users out of search engine indexing by default + about: + manage_rules: Manage server rules + preamble: Provide in-depth information about how the server is operated, moderated, funded. + rules_hint: There is a dedicated area for rules that your users are expected to adhere to. + title: About + appearance: + preamble: Customize Mastodon's web interface. + title: Appearance + branding: + preamble: Your server's branding differentiates it from other servers in the network. This information may be displayed across a variety of environments, such as Mastodon's web interface, native applications, in link previews on other websites and within messaging apps, and so on. For this reason, it is best to keep this information clear, short and concise. + title: Branding + content_retention: + preamble: Control how user-generated content is stored in Mastodon. + title: Content retention + discovery: + follow_recommendations: Follow recommendations + preamble: Surfacing interesting content is instrumental in onboarding new users who may not know anyone Mastodon. Control how various discovery features work on your server. + profile_directory: Profile directory + public_timelines: Public timelines + title: Discovery + trends: Trends domain_blocks: all: To everyone disabled: To no one - title: Show domain blocks users: To logged-in local users - domain_blocks_rationale: - title: Show rationale - mascot: - desc_html: Displayed on multiple pages. At least 293×205px recommended. When not set, falls back to default mascot - title: Mascot image - peers_api_enabled: - desc_html: Domain names this server has encountered in the fediverse - title: Publish list of discovered servers in the API - preview_sensitive_media: - desc_html: Link previews on other websites will display a thumbnail even if the media is marked as sensitive - title: Show sensitive media in OpenGraph previews - profile_directory: - desc_html: Allow users to be discoverable - title: Enable profile directory registrations: - closed_message: - desc_html: Displayed on frontpage when registrations are closed. You can use HTML tags - title: Closed registration message - require_invite_text: - desc_html: When registrations require manual approval, make the “Why do you want to join?” text input mandatory rather than optional - title: Require new users to enter a reason to join + preamble: Control who can create an account on your server. + title: Registrations registrations_mode: modes: approved: Approval required for sign up none: Nobody can sign up open: Anyone can sign up - title: Registrations mode - site_description: - desc_html: Introductory paragraph on the API. Describe what makes this Mastodon server special and anything else important. You can use HTML tags, in particular <a> and <em>. - title: Server description - site_description_extended: - desc_html: A good place for your code of conduct, rules, guidelines and other things that set your server apart. You can use HTML tags - title: Custom extended information - site_short_description: - desc_html: Displayed in sidebar and meta tags. Describe what Mastodon is and what makes this server special in a single paragraph. - title: Short server description - site_terms: - desc_html: You can write your own privacy policy. You can use HTML tags - title: Custom privacy policy - site_title: Server name - thumbnail: - desc_html: Used for previews via OpenGraph and API. 1200x630px recommended - title: Server thumbnail - timeline_preview: - desc_html: Display link to public timeline on landing page and allow API access to the public timeline without authentication - title: Allow unauthenticated access to public timeline - title: Site settings - trendable_by_default: - desc_html: Specific trending content can still be explicitly disallowed - title: Allow trends without prior review - trends: - desc_html: Publicly display previously reviewed content that is currently trending - title: Trends + title: Server Settings site_uploads: delete: Delete uploaded file destroyed_msg: Site upload successfully deleted! diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index db5b45e41..64281d7b7 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -75,8 +75,25 @@ en: warn: Hide the filtered content behind a warning mentioning the filter's title form_admin_settings: backups_retention_period: Keep generated user archives for the specified number of days. + bootstrap_timeline_accounts: These accounts will be pinned to the top of new users' follow recommendations. + closed_registrations_message: Displayed when sign-ups are closed content_cache_retention_period: Posts from other servers will be deleted after the specified number of days when set to a positive value. This may be irreversible. + custom_css: You can apply custom styles on the web version of Mastodon. + mascot: Overrides the illustration in the advanced web interface. media_cache_retention_period: Downloaded media files will be deleted after the specified number of days when set to a positive value, and re-downloaded on demand. + profile_directory: The profile directory lists all users who have opted-in to be discoverable. + require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional + site_contact_email: How people can reach you for legal or support inquiries. + site_contact_username: How people can reach you on Mastodon. + site_extended_description: Any additional information that may be useful to visitors and your users. Can be structured with Markdown syntax. + site_short_description: A short description to help uniquely identify your server. Who is running it, who is it for? + site_terms: Use your own privacy policy or leave blank to use the default. Can be structured with Markdown syntax. + site_title: How people may refer to your server besides its domain name. + theme: Theme that logged out visitors and new users see. + thumbnail: A roughly 2:1 image displayed alongside your server information. + timeline_preview: Logged out visitors will be able to browse the most recent public posts available on the server. + trendable_by_default: Skip manual review of trending content. Individual items can still be removed from trends after the fact. + trends: Trends show which posts, hashtags and news stories are gaining traction on your server. form_challenge: current_password: You are entering a secure area imports: @@ -213,8 +230,28 @@ en: warn: Hide with a warning form_admin_settings: backups_retention_period: User archive retention period + bootstrap_timeline_accounts: Always recommend these accounts to new users + closed_registrations_message: Custom message when sign-ups are not available content_cache_retention_period: Content cache retention period + custom_css: Custom CSS + mascot: Custom mascot (legacy) media_cache_retention_period: Media cache retention period + profile_directory: Enable profile directory + registrations_mode: Who can sign-up + require_invite_text: Require a reason to join + show_domain_blocks: Show domain blocks + show_domain_blocks_rationale: Show why domains were blocked + site_contact_email: Contact e-mail + site_contact_username: Contact username + site_extended_description: Extended description + site_short_description: Server description + site_terms: Privacy Policy + site_title: Server name + theme: Default theme + thumbnail: Server thumbnail + timeline_preview: Allow unauthenticated access to public timelines + trendable_by_default: Allow trends without prior review + trends: Enable trends interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow diff --git a/config/navigation.rb b/config/navigation.rb index 706de0471..e901fb932 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -52,7 +52,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), nil, if: -> { current_user.can?(:view_dashboard, :manage_settings, :manage_rules, :manage_announcements, :manage_custom_emojis, :manage_webhooks, :manage_federation) } do |s| s.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_path, if: -> { current_user.can?(:view_dashboard) } - s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_path, if: -> { current_user.can?(:manage_settings) }, highlights_on: %r{/admin/settings} + s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), admin_settings_path, if: -> { current_user.can?(:manage_settings) }, highlights_on: %r{/admin/settings} s.item :rules, safe_join([fa_icon('gavel fw'), t('admin.rules.title')]), admin_rules_path, highlights_on: %r{/admin/rules}, if: -> { current_user.can?(:manage_rules) } s.item :roles, safe_join([fa_icon('vcard fw'), t('admin.roles.title')]), admin_roles_path, highlights_on: %r{/admin/roles}, if: -> { current_user.can?(:manage_roles) } s.item :announcements, safe_join([fa_icon('bullhorn fw'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}, if: -> { current_user.can?(:manage_announcements) } diff --git a/config/routes.rb b/config/routes.rb index 1ed585f19..b44479e77 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -241,7 +241,18 @@ Rails.application.routes.draw do end end - resource :settings, only: [:edit, :update] + get '/settings', to: redirect('/admin/settings/branding') + get '/settings/edit', to: redirect('/admin/settings/branding') + + namespace :settings do + resource :branding, only: [:show, :update], controller: 'branding' + resource :registrations, only: [:show, :update], controller: 'registrations' + resource :content_retention, only: [:show, :update], controller: 'content_retention' + resource :about, only: [:show, :update], controller: 'about' + resource :appearance, only: [:show, :update], controller: 'appearance' + resource :discovery, only: [:show, :update], controller: 'discovery' + end + resources :site_uploads, only: [:destroy] resources :invites, only: [:index, :create, :destroy] do diff --git a/spec/controllers/admin/settings/branding_controller_spec.rb b/spec/controllers/admin/settings/branding_controller_spec.rb new file mode 100644 index 000000000..ee1c441bc --- /dev/null +++ b/spec/controllers/admin/settings/branding_controller_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::Settings::BrandingController, type: :controller do + render_views + + describe 'When signed in as an admin' do + before do + sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + + expect(response).to have_http_status(200) + end + end + + describe 'PUT #update' do + before do + allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true) + end + + around do |example| + before = Setting.site_short_description + Setting.site_short_description = nil + example.run + Setting.site_short_description = before + Setting.new_setting_key = nil + end + + it 'cannot create a setting value for a non-admin key' do + expect(Setting.new_setting_key).to be_blank + + patch :update, params: { form_admin_settings: { new_setting_key: 'New key value' } } + + expect(response).to redirect_to(admin_settings_branding_path) + expect(Setting.new_setting_key).to be_nil + end + + it 'creates a settings value that didnt exist before for eligible key' do + expect(Setting.site_short_description).to be_blank + + patch :update, params: { form_admin_settings: { site_short_description: 'New key value' } } + + expect(response).to redirect_to(admin_settings_branding_path) + expect(Setting.site_short_description).to eq 'New key value' + end + end + end +end diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb deleted file mode 100644 index 46749f76c..000000000 --- a/spec/controllers/admin/settings_controller_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Admin::SettingsController, type: :controller do - render_views - - describe 'When signed in as an admin' do - before do - sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user - end - - describe 'GET #edit' do - it 'returns http success' do - get :edit - - expect(response).to have_http_status(200) - end - end - - describe 'PUT #update' do - before do - allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true) - end - - describe 'for a record that doesnt exist' do - around do |example| - before = Setting.site_extended_description - Setting.site_extended_description = nil - example.run - Setting.site_extended_description = before - Setting.new_setting_key = nil - end - - it 'cannot create a setting value for a non-admin key' do - expect(Setting.new_setting_key).to be_blank - - patch :update, params: { form_admin_settings: { new_setting_key: 'New key value' } } - - expect(response).to redirect_to(edit_admin_settings_path) - expect(Setting.new_setting_key).to be_nil - end - - it 'creates a settings value that didnt exist before for eligible key' do - expect(Setting.site_extended_description).to be_blank - - patch :update, params: { form_admin_settings: { site_extended_description: 'New key value' } } - - expect(response).to redirect_to(edit_admin_settings_path) - expect(Setting.site_extended_description).to eq 'New key value' - end - end - - context do - around do |example| - site_title = Setting.site_title - example.run - Setting.site_title = site_title - end - - it 'updates a settings value' do - Setting.site_title = 'Original' - patch :update, params: { form_admin_settings: { site_title: 'New title' } } - - expect(response).to redirect_to(edit_admin_settings_path) - expect(Setting.site_title).to eq 'New title' - end - end - end - end -end -- cgit From 1ae508bf2faae016b88d15e273b0dc01de4fd7af Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 26 Oct 2022 12:10:02 +0200 Subject: Change unauthenticated search to not support pagination in REST API (#19326) - Only exact search matches for queries with < 5 characters - Do not support queries with `offset` (pagination) - Return HTTP 401 on truthy `resolve` instead of overriding to false --- app/controllers/api/v2/search_controller.rb | 13 ++++- app/services/account_search_service.rb | 5 ++ spec/controllers/api/v2/search_controller_spec.rb | 62 ++++++++++++++++++++--- 3 files changed, 71 insertions(+), 9 deletions(-) (limited to 'spec/controllers') diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index e384ecbaf..4d20aeb10 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -6,6 +6,7 @@ class Api::V2::SearchController < Api::BaseController RESULTS_LIMIT = 20 before_action -> { authorize_if_got_token! :read, :'read:search' } + before_action :validate_search_params! def index @search = Search.new(search_results) @@ -18,12 +19,22 @@ class Api::V2::SearchController < Api::BaseController private + def validate_search_params! + params.require(:q) + + return if user_signed_in? + + return render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401 if params[:offset].present? + + render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401 if truthy_param?(:resolve) + end + def search_results SearchService.new.call( params[:q], current_account, limit_param(RESULTS_LIMIT), - search_params.merge(resolve: user_signed_in? ? truthy_param?(:resolve) : false, exclude_unreviewed: truthy_param?(:exclude_unreviewed)) + search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed)) ) end diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index 4dcae20eb..35b2e05f5 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -3,6 +3,9 @@ class AccountSearchService < BaseService attr_reader :query, :limit, :offset, :options, :account + # Min. number of characters to look for non-exact matches + MIN_QUERY_LENGTH = 5 + def call(query, account = nil, options = {}) @acct_hint = query&.start_with?('@') @query = query&.strip&.gsub(/\A@/, '') @@ -135,6 +138,8 @@ class AccountSearchService < BaseService end def limit_for_non_exact_results + return 0 if @account.nil? && query.size < MIN_QUERY_LENGTH + if exact_match? limit - 1 else diff --git a/spec/controllers/api/v2/search_controller_spec.rb b/spec/controllers/api/v2/search_controller_spec.rb index fa20e1e51..d417ea58c 100644 --- a/spec/controllers/api/v2/search_controller_spec.rb +++ b/spec/controllers/api/v2/search_controller_spec.rb @@ -5,18 +5,64 @@ require 'rails_helper' RSpec.describe Api::V2::SearchController, type: :controller do render_views - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') } + context 'with token' do + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:search') } - before do - allow(controller).to receive(:doorkeeper_token) { token } + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + before do + get :index, params: { q: 'test' } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end end - describe 'GET #index' do - it 'returns http success' do - get :index, params: { q: 'test' } + context 'without token' do + describe 'GET #index' do + let(:search_params) {} + + before do + get :index, params: search_params + end + + context 'with a `q` shorter than 5 characters' do + let(:search_params) { { q: 'test' } } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + end + + context 'with a `q` equal to or longer than 5 characters' do + let(:search_params) { { q: 'test1' } } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + context 'with truthy `resolve`' do + let(:search_params) { { q: 'test1', resolve: '1' } } + + it 'returns http unauthorized' do + expect(response).to have_http_status(401) + end + end + + context 'with `offset`' do + let(:search_params) { { q: 'test1', offset: 1 } } - expect(response).to have_http_status(200) + it 'returns http unauthorized' do + expect(response).to have_http_status(401) + end + end + end end end end -- cgit From bbdf61c9e46dda1c38388f804ebb3f437d705816 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 28 Oct 2022 20:12:04 +0200 Subject: Revert Home controller test to upstream --- spec/controllers/home_controller_spec.rb | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) (limited to 'spec/controllers') diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index 914ca6307..d845ae01d 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -7,16 +7,9 @@ RSpec.describe HomeController, type: :controller do subject { get :index } context 'when not signed in' do - context 'when requested path is tag timeline' do - it 'returns http success' do - @request.path = '/web/timelines/tag/name' - is_expected.to have_http_status(:success) - end - end - - it 'redirects to about page' do + it 'returns http success' do @request.path = '/' - is_expected.to redirect_to(about_path) + is_expected.to have_http_status(:success) end end -- cgit From 40c7f3e830538951862dc73074d1045a82395ab0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 30 Oct 2022 02:44:32 +0200 Subject: Fix account action type validation (#19476) * Fix account action type validation Fix #19143 * Fix #19145 * Fix code style issues --- app/models/admin/account_action.rb | 9 +++-- .../v1/admin/account_actions_controller_spec.rb | 44 ++++++++++++++-------- 2 files changed, 34 insertions(+), 19 deletions(-) (limited to 'spec/controllers') diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb index aed3bc0c7..bce0d6e17 100644 --- a/app/models/admin/account_action.rb +++ b/app/models/admin/account_action.rb @@ -25,6 +25,8 @@ class Admin::AccountAction alias send_email_notification? send_email_notification alias include_statuses? include_statuses + validates :type, :target_account, :current_account, presence: true + def initialize(attributes = {}) @send_email_notification = true @include_statuses = true @@ -41,13 +43,15 @@ class Admin::AccountAction end def save! + raise ActiveRecord::RecordInvalid, self unless valid? + ApplicationRecord.transaction do process_action! process_strike! + process_reports! end process_email! - process_reports! process_queue! end @@ -106,9 +110,8 @@ class Admin::AccountAction # Otherwise, we will mark all unresolved reports about # the account as resolved. - reports.each { |report| authorize(report, :update?) } - reports.each do |report| + authorize(report, :update?) log_action(:resolve, report) report.resolve!(current_account) end diff --git a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb index 199395f55..462c2cfa9 100644 --- a/spec/controllers/api/v1/admin/account_actions_controller_spec.rb +++ b/spec/controllers/api/v1/admin/account_actions_controller_spec.rb @@ -30,28 +30,40 @@ RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do end describe 'POST #create' do - before do - post :create, params: { account_id: account.id, type: 'disable' } - end + context do + before do + post :create, params: { account_id: account.id, type: 'disable' } + end - it_behaves_like 'forbidden for wrong scope', 'write:statuses' - it_behaves_like 'forbidden for wrong role', '' + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', '' - it 'returns http success' do - expect(response).to have_http_status(200) - end + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'performs action against account' do + expect(account.reload.user_disabled?).to be true + end + + it 'logs action' do + log_item = Admin::ActionLog.last - it 'performs action against account' do - expect(account.reload.user_disabled?).to be true + expect(log_item).to_not be_nil + expect(log_item.action).to eq :disable + expect(log_item.account_id).to eq user.account_id + expect(log_item.target_id).to eq account.user.id + end end - it 'logs action' do - log_item = Admin::ActionLog.last + context 'with no type' do + before do + post :create, params: { account_id: account.id } + end - expect(log_item).to_not be_nil - expect(log_item.action).to eq :disable - expect(log_item.account_id).to eq user.account_id - expect(log_item.target_id).to eq account.user.id + it 'returns http unprocessable entity' do + expect(response).to have_http_status(422) + end end end end -- cgit