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) --- app/views/admin/settings/edit.html.haml | 3 --- 1 file changed, 3 deletions(-) (limited to 'app/views/admin') diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 1dfd21643..a00cd0222 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 :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.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') -- 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 'app/views/admin') 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 45ebdb72ca1678eb30e6087f61bd019c7ea903f4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Oct 2022 16:45:40 +0200 Subject: Add support for language preferences for trending statuses and links (#18288) --- app/controllers/admin/trends/links_controller.rb | 1 + .../admin/trends/statuses_controller.rb | 1 + app/controllers/api/v1/trends/links_controller.rb | 4 +- app/mailers/admin_mailer.rb | 4 +- app/models/preview_card.rb | 1 + app/models/preview_card_trend.rb | 17 ++++ app/models/status.rb | 1 + app/models/status_trend.rb | 21 +++++ app/models/trends.rb | 6 +- app/models/trends/base.rb | 4 + app/models/trends/links.rb | 98 +++++++++++++++------- app/models/trends/preview_card_filter.rb | 25 ++++-- app/models/trends/status_filter.rb | 25 ++++-- app/models/trends/statuses.rb | 82 ++++++++++-------- .../admin/trends/links/_preview_card.html.haml | 4 +- app/views/admin/trends/links/index.html.haml | 2 +- app/views/admin/trends/statuses/_status.html.haml | 4 +- app/views/admin/trends/statuses/index.html.haml | 2 +- .../admin_mailer/_new_trending_links.text.erb | 8 +- .../admin_mailer/_new_trending_statuses.text.erb | 8 +- config/locales/en.yml | 4 - db/migrate/20220824233535_create_status_trends.rb | 12 +++ .../20221006061337_create_preview_card_trends.rb | 11 +++ db/schema.rb | 25 +++++- spec/fabricators/account_fabricator.rb | 1 + spec/mailers/previews/admin_mailer_preview.rb | 2 +- spec/models/preview_card_trend_spec.rb | 4 + spec/models/status_trend_spec.rb | 4 + spec/models/trends/statuses_spec.rb | 14 ++-- 29 files changed, 274 insertions(+), 121 deletions(-) create mode 100644 app/models/preview_card_trend.rb create mode 100644 app/models/status_trend.rb create mode 100644 db/migrate/20220824233535_create_status_trends.rb create mode 100644 db/migrate/20221006061337_create_preview_card_trends.rb create mode 100644 spec/models/preview_card_trend_spec.rb create mode 100644 spec/models/status_trend_spec.rb (limited to 'app/views/admin') diff --git a/app/controllers/admin/trends/links_controller.rb b/app/controllers/admin/trends/links_controller.rb index a497eae41..a3454f2da 100644 --- a/app/controllers/admin/trends/links_controller.rb +++ b/app/controllers/admin/trends/links_controller.rb @@ -4,6 +4,7 @@ class Admin::Trends::LinksController < Admin::BaseController def index authorize :preview_card, :review? + @locales = PreviewCardTrend.pluck('distinct language') @preview_cards = filtered_preview_cards.page(params[:page]) @form = Trends::PreviewCardBatch.new end diff --git a/app/controllers/admin/trends/statuses_controller.rb b/app/controllers/admin/trends/statuses_controller.rb index c538962f9..2b8226138 100644 --- a/app/controllers/admin/trends/statuses_controller.rb +++ b/app/controllers/admin/trends/statuses_controller.rb @@ -4,6 +4,7 @@ class Admin::Trends::StatusesController < Admin::BaseController def index authorize :status, :review? + @locales = StatusTrend.pluck('distinct language') @statuses = filtered_statuses.page(params[:page]) @form = Trends::StatusBatch.new end diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index 1a9f918f2..8ff3b364e 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -28,7 +28,9 @@ class Api::V1::Trends::LinksController < Api::BaseController end def links_from_trends - Trends.links.query.allowed.in_locale(content_locale) + scope = Trends.links.query.allowed.in_locale(content_locale) + scope = scope.filtered_for(current_account) if user_signed_in? + scope end def insert_pagination_headers diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index f416977d8..bc6d87ae6 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -4,6 +4,7 @@ class AdminMailer < ApplicationMailer layout 'plain_mailer' helper :accounts + helper :languages def new_report(recipient, report) @report = report @@ -37,11 +38,8 @@ class AdminMailer < ApplicationMailer def new_trends(recipient, links, tags, statuses) @links = links - @lowest_trending_link = Trends.links.query.allowed.limit(Trends.links.options[:review_threshold]).last @tags = tags - @lowest_trending_tag = Trends.tags.query.allowed.limit(Trends.tags.options[:review_threshold]).last @statuses = statuses - @lowest_trending_status = Trends.statuses.query.allowed.limit(Trends.statuses.options[:review_threshold]).last @me = recipient @instance = Rails.configuration.x.local_domain diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index c49c51a1b..b5d3f9c8f 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -48,6 +48,7 @@ class PreviewCard < ApplicationRecord enum link_type: [:unknown, :article] has_and_belongs_to_many :statuses + has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }, validate_media_type: false diff --git a/app/models/preview_card_trend.rb b/app/models/preview_card_trend.rb new file mode 100644 index 000000000..018400dfa --- /dev/null +++ b/app/models/preview_card_trend.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: preview_card_trends +# +# id :bigint(8) not null, primary key +# preview_card_id :bigint(8) not null +# score :float default(0.0), not null +# rank :integer default(0), not null +# allowed :boolean default(FALSE), not null +# language :string +# +class PreviewCardTrend < ApplicationRecord + belongs_to :preview_card + scope :allowed, -> { where(allowed: true) } +end diff --git a/app/models/status.rb b/app/models/status.rb index de958aaf3..4805abfea 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -75,6 +75,7 @@ class Status < ApplicationRecord has_one :notification, as: :activity, dependent: :destroy has_one :status_stat, inverse_of: :status has_one :poll, inverse_of: :status, dependent: :destroy + has_one :trend, class_name: 'StatusTrend', inverse_of: :status validates :uri, uniqueness: true, presence: true, unless: :local? validates :text, presence: true, unless: -> { with_media? || reblog? } diff --git a/app/models/status_trend.rb b/app/models/status_trend.rb new file mode 100644 index 000000000..33fd6b004 --- /dev/null +++ b/app/models/status_trend.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: status_trends +# +# id :bigint(8) not null, primary key +# status_id :bigint(8) not null +# account_id :bigint(8) not null +# score :float default(0.0), not null +# rank :integer default(0), not null +# allowed :boolean default(FALSE), not null +# language :string +# + +class StatusTrend < ApplicationRecord + belongs_to :status + belongs_to :account + + scope :allowed, -> { where(allowed: true) } +end diff --git a/app/models/trends.rb b/app/models/trends.rb index d886be89a..151d734f9 100644 --- a/app/models/trends.rb +++ b/app/models/trends.rb @@ -26,7 +26,7 @@ module Trends end def self.request_review! - return unless enabled? + return if skip_review? || !enabled? links_requiring_review = links.request_review tags_requiring_review = tags.request_review @@ -43,6 +43,10 @@ module Trends Setting.trends end + def skip_review? + Setting.trendable_by_default + end + def self.available_locales @available_locales ||= I18n.available_locales.map { |locale| locale.to_s.split(/[_-]/).first }.uniq end diff --git a/app/models/trends/base.rb b/app/models/trends/base.rb index 047111248..a189f11f2 100644 --- a/app/models/trends/base.rb +++ b/app/models/trends/base.rb @@ -98,4 +98,8 @@ class Trends::Base pipeline.rename(from_key, to_key) end end + + def skip_review? + Setting.trendable_by_default + end end diff --git a/app/models/trends/links.rb b/app/models/trends/links.rb index 604894cd6..8808b3ab6 100644 --- a/app/models/trends/links.rb +++ b/app/models/trends/links.rb @@ -11,6 +11,40 @@ class Trends::Links < Trends::Base decay_threshold: 1, } + class Query < Trends::Query + def filtered_for!(account) + @account = account + self + end + + def filtered_for(account) + clone.filtered_for!(account) + end + + def to_arel + scope = PreviewCard.joins(:trend).reorder(score: :desc) + scope = scope.reorder(language_order_clause.desc, score: :desc) if preferred_languages.present? + scope = scope.merge(PreviewCardTrend.allowed) if @allowed + scope = scope.offset(@offset) if @offset.present? + scope = scope.limit(@limit) if @limit.present? + scope + end + + private + + def language_order_clause + Arel::Nodes::Case.new.when(PreviewCardTrend.arel_table[:language].in(preferred_languages)).then(1).else(0) + end + + def preferred_languages + if @account&.chosen_languages.present? + @account.chosen_languages + else + @locale + end + end + end + def register(status, at_time = Time.now.utc) original_status = status.proper @@ -28,24 +62,33 @@ class Trends::Links < Trends::Base record_used_id(preview_card.id, at_time) end + def query + Query.new(key_prefix, klass) + end + def refresh(at_time = Time.now.utc) - preview_cards = PreviewCard.where(id: (recently_used_ids(at_time) + currently_trending_ids(false, -1)).uniq) + preview_cards = PreviewCard.where(id: (recently_used_ids(at_time) + PreviewCardTrend.pluck(:preview_card_id)).uniq) calculate_scores(preview_cards, at_time) end def request_review - preview_cards = PreviewCard.where(id: currently_trending_ids(false, -1)) + PreviewCardTrend.pluck('distinct language').flat_map do |language| + score_at_threshold = PreviewCardTrend.where(language: language, allowed: true).order(rank: :desc).where('rank <= ?', options[:review_threshold]).first&.score || 0 + preview_card_trends = PreviewCardTrend.where(language: language, allowed: false).joins(:preview_card) - preview_cards.filter_map do |preview_card| - next unless would_be_trending?(preview_card.id) && !preview_card.trendable? && preview_card.requires_review_notification? + preview_card_trends.filter_map do |trend| + preview_card = trend.preview_card - if preview_card.provider.nil? - preview_card.provider = PreviewCardProvider.create(domain: preview_card.domain, requested_review_at: Time.now.utc) - else - preview_card.provider.touch(:requested_review_at) - end + next unless trend.score > score_at_threshold && !preview_card.trendable? && preview_card.requires_review_notification? + + if preview_card.provider.nil? + preview_card.provider = PreviewCardProvider.create(domain: preview_card.domain, requested_review_at: Time.now.utc) + else + preview_card.provider.touch(:requested_review_at) + end - preview_card + preview_card + end end end @@ -62,10 +105,7 @@ class Trends::Links < Trends::Base private def calculate_scores(preview_cards, at_time) - global_items = [] - locale_items = Hash.new { |h, key| h[key] = [] } - - preview_cards.each do |preview_card| + items = preview_cards.map do |preview_card| expected = preview_card.history.get(at_time - 1.day).accounts.to_f expected = 1.0 if expected.zero? observed = preview_card.history.get(at_time).accounts.to_f @@ -89,26 +129,24 @@ class Trends::Links < Trends::Base preview_card.update_columns(max_score: max_score, max_score_at: max_time) end - decaying_score = max_score * (0.5**((at_time.to_f - max_time.to_f) / options[:max_score_halflife].to_f)) - - next unless decaying_score >= options[:decay_threshold] + decaying_score = begin + if max_score.zero? || !valid_locale?(preview_card.language) + 0 + else + max_score * (0.5**((at_time.to_f - max_time.to_f) / options[:max_score_halflife].to_f)) + end + end - global_items << { score: decaying_score, item: preview_card } - locale_items[preview_card.language] << { score: decaying_score, item: preview_card } if valid_locale?(preview_card.language) + [decaying_score, preview_card] end - replace_items('', global_items) + to_insert = items.filter { |(score, _)| score >= options[:decay_threshold] } + to_delete = items.filter { |(score, _)| score < options[:decay_threshold] } - Trends.available_locales.each do |locale| - replace_items(":#{locale}", locale_items[locale]) + PreviewCardTrend.transaction do + PreviewCardTrend.upsert_all(to_insert.map { |(score, preview_card)| { preview_card_id: preview_card.id, score: score, language: preview_card.language, allowed: preview_card.trendable? || false } }, unique_by: :preview_card_id) if to_insert.any? + PreviewCardTrend.where(preview_card_id: to_delete.map { |(_, preview_card)| preview_card.id }).delete_all if to_delete.any? + PreviewCardTrend.connection.exec_update('UPDATE preview_card_trends SET rank = t0.calculated_rank FROM (SELECT id, row_number() OVER w AS calculated_rank FROM preview_card_trends WINDOW w AS (PARTITION BY language ORDER BY score DESC)) t0 WHERE preview_card_trends.id = t0.id') end end - - def filter_for_allowed_items(items) - items.select { |item| item[:item].trendable? } - end - - def would_be_trending?(id) - score(id) > score_at_rank(options[:review_threshold] - 1) - end end diff --git a/app/models/trends/preview_card_filter.rb b/app/models/trends/preview_card_filter.rb index 25add58c8..0a81146d4 100644 --- a/app/models/trends/preview_card_filter.rb +++ b/app/models/trends/preview_card_filter.rb @@ -13,10 +13,10 @@ class Trends::PreviewCardFilter end def results - scope = PreviewCard.unscoped + scope = initial_scope params.each do |key, value| - next if %w(page locale).include?(key.to_s) + next if %w(page).include?(key.to_s) scope.merge!(scope_for(key, value.to_s.strip)) if value.present? end @@ -26,21 +26,30 @@ class Trends::PreviewCardFilter private + def initial_scope + PreviewCard.select(PreviewCard.arel_table[Arel.star]) + .joins(:trend) + .eager_load(:trend) + .reorder(score: :desc) + end + def scope_for(key, value) case key.to_s when 'trending' trending_scope(value) + when 'locale' + PreviewCardTrend.where(language: value) else raise "Unknown filter: #{key}" end end def trending_scope(value) - scope = Trends.links.query - - scope = scope.in_locale(@params[:locale].to_s) if @params[:locale].present? - scope = scope.allowed if value == 'allowed' - - scope.to_arel + case value + when 'allowed' + PreviewCardTrend.allowed + else + PreviewCardTrend.all + end end end diff --git a/app/models/trends/status_filter.rb b/app/models/trends/status_filter.rb index 7c453e339..cb0f75d67 100644 --- a/app/models/trends/status_filter.rb +++ b/app/models/trends/status_filter.rb @@ -13,10 +13,10 @@ class Trends::StatusFilter end def results - scope = Status.unscoped.kept + scope = initial_scope params.each do |key, value| - next if %w(page locale).include?(key.to_s) + next if %w(page).include?(key.to_s) scope.merge!(scope_for(key, value.to_s.strip)) if value.present? end @@ -26,21 +26,30 @@ class Trends::StatusFilter private + def initial_scope + Status.select(Status.arel_table[Arel.star]) + .joins(:trend) + .eager_load(:trend) + .reorder(score: :desc) + end + def scope_for(key, value) case key.to_s when 'trending' trending_scope(value) + when 'locale' + StatusTrend.where(language: value) else raise "Unknown filter: #{key}" end end def trending_scope(value) - scope = Trends.statuses.query - - scope = scope.in_locale(@params[:locale].to_s) if @params[:locale].present? - scope = scope.allowed if value == 'allowed' - - scope.to_arel + case value + when 'allowed' + StatusTrend.allowed + else + StatusTrend.all + end end end diff --git a/app/models/trends/statuses.rb b/app/models/trends/statuses.rb index 777065d3e..c9ef7c8f2 100644 --- a/app/models/trends/statuses.rb +++ b/app/models/trends/statuses.rb @@ -20,13 +20,27 @@ class Trends::Statuses < Trends::Base clone.filtered_for!(account) end + def to_arel + scope = Status.joins(:trend).reorder(score: :desc) + scope = scope.reorder(language_order_clause.desc, score: :desc) if preferred_languages.present? + scope = scope.merge(StatusTrend.allowed) if @allowed + scope = scope.not_excluded_by_account(@account).not_domain_blocked_by_account(@account) if @account.present? + scope = scope.offset(@offset) if @offset.present? + scope = scope.limit(@limit) if @limit.present? + scope + end + private - def apply_scopes(scope) - if @account.nil? - scope + def language_order_clause + Arel::Nodes::Case.new.when(StatusTrend.arel_table[:language].in(preferred_languages)).then(1).else(0) + end + + def preferred_languages + if @account&.chosen_languages.present? + @account.chosen_languages else - scope.not_excluded_by_account(@account).not_domain_blocked_by_account(@account) + @locale end end end @@ -36,9 +50,6 @@ class Trends::Statuses < Trends::Base end def add(status, _account_id, at_time = Time.now.utc) - # We rely on the total reblogs and favourites count, so we - # don't record which account did the what and when here - record_used_id(status.id, at_time) end @@ -47,18 +58,23 @@ class Trends::Statuses < Trends::Base end def refresh(at_time = Time.now.utc) - statuses = Status.where(id: (recently_used_ids(at_time) + currently_trending_ids(false, -1)).uniq).includes(:account, :media_attachments) + statuses = Status.where(id: (recently_used_ids(at_time) + StatusTrend.pluck(:status_id)).uniq).includes(:status_stat, :account) calculate_scores(statuses, at_time) end def request_review - statuses = Status.where(id: currently_trending_ids(false, -1)).includes(:account) + StatusTrend.pluck('distinct language').flat_map do |language| + score_at_threshold = StatusTrend.where(language: language, allowed: true).order(rank: :desc).where('rank <= ?', options[:review_threshold]).first&.score || 0 + status_trends = StatusTrend.where(language: language, allowed: false).joins(:status).includes(status: :account) - statuses.filter_map do |status| - next unless would_be_trending?(status.id) && !status.trendable? && status.requires_review_notification? + status_trends.filter_map do |trend| + status = trend.status - status.account.touch(:requested_review_at) - status + if trend.score > score_at_threshold && !status.trendable? && status.requires_review_notification? + status.account.touch(:requested_review_at) + status + end + end end end @@ -75,14 +91,11 @@ class Trends::Statuses < Trends::Base private def eligible?(status) - status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? + status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language) end def calculate_scores(statuses, at_time) - global_items = [] - locale_items = Hash.new { |h, key| h[key] = [] } - - statuses.each do |status| + items = statuses.map do |status| expected = 1.0 observed = (status.reblogs_count + status.favourites_count).to_f @@ -94,29 +107,24 @@ class Trends::Statuses < Trends::Base end end - decaying_score = score * (0.5**((at_time.to_f - status.created_at.to_f) / options[:score_halflife].to_f)) - - next unless decaying_score >= options[:decay_threshold] + decaying_score = begin + if score.zero? || !eligible?(status) + 0 + else + score * (0.5**((at_time.to_f - status.created_at.to_f) / options[:score_halflife].to_f)) + end + end - global_items << { score: decaying_score, item: status } - locale_items[status.language] << { account_id: status.account_id, score: decaying_score, item: status } if valid_locale?(status.language) + [decaying_score, status] end - replace_items('', global_items) + to_insert = items.filter { |(score, _)| score >= options[:decay_threshold] } + to_delete = items.filter { |(score, _)| score < options[:decay_threshold] } - Trends.available_locales.each do |locale| - replace_items(":#{locale}", locale_items[locale]) + StatusTrend.transaction do + StatusTrend.upsert_all(to_insert.map { |(score, status)| { status_id: status.id, account_id: status.account_id, score: score, language: status.language, allowed: status.trendable? || false } }, unique_by: :status_id) if to_insert.any? + StatusTrend.where(status_id: to_delete.map { |(_, status)| status.id }).delete_all if to_delete.any? + StatusTrend.connection.exec_update('UPDATE status_trends SET rank = t0.calculated_rank FROM (SELECT id, row_number() OVER w AS calculated_rank FROM status_trends WINDOW w AS (PARTITION BY language ORDER BY score DESC)) t0 WHERE status_trends.id = t0.id') end end - - def filter_for_allowed_items(items) - # Show only one status per account, pick the one with the highest score - # that's also eligible to trend - - items.group_by { |item| item[:account_id] }.values.filter_map { |account_items| account_items.select { |item| item[:item].trendable? && item[:item].account.discoverable? }.max_by { |item| item[:score] } } - end - - def would_be_trending?(id) - score(id) > score_at_rank(options[:review_threshold] - 1) - end end diff --git a/app/views/admin/trends/links/_preview_card.html.haml b/app/views/admin/trends/links/_preview_card.html.haml index 7d4897c7e..8812feb31 100644 --- a/app/views/admin/trends/links/_preview_card.html.haml +++ b/app/views/admin/trends/links/_preview_card.html.haml @@ -18,9 +18,9 @@ = t('admin.trends.links.shared_by_over_week', count: preview_card.history.reduce(0) { |sum, day| sum + day.accounts }) - - if preview_card.trendable? && (rank = Trends.links.rank(preview_card.id, locale: params[:locale].presence)) + - if preview_card.trend.allowed? • - %abbr{ title: t('admin.trends.tags.current_score', score: Trends.links.score(preview_card.id, locale: params[:locale].presence)) }= t('admin.trends.tags.trending_rank', rank: rank + 1) + %abbr{ title: t('admin.trends.tags.current_score', score: preview_card.trend.score) }= t('admin.trends.tags.trending_rank', rank: preview_card.trend.rank) - if preview_card.decaying? • diff --git a/app/views/admin/trends/links/index.html.haml b/app/views/admin/trends/links/index.html.haml index 49a53d979..7b5122cf1 100644 --- a/app/views/admin/trends/links/index.html.haml +++ b/app/views/admin/trends/links/index.html.haml @@ -16,7 +16,7 @@ .filter-subset.filter-subset--with-select %strong= t('admin.follow_recommendations.language') .input.select.optional - = select_tag :locale, options_for_select(Trends.available_locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), include_blank: true + = select_tag :locale, options_for_select(@locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), include_blank: true .filter-subset %strong= t('admin.trends.trending') %ul diff --git a/app/views/admin/trends/statuses/_status.html.haml b/app/views/admin/trends/statuses/_status.html.haml index e4d75bbb9..f35e13d12 100644 --- a/app/views/admin/trends/statuses/_status.html.haml +++ b/app/views/admin/trends/statuses/_status.html.haml @@ -25,9 +25,9 @@ - if status.trendable? && !status.account.discoverable? • = t('admin.trends.statuses.not_discoverable') - - if status.trendable? && (rank = Trends.statuses.rank(status.id, locale: params[:locale].presence)) + - if status.trend.allowed? • - %abbr{ title: t('admin.trends.tags.current_score', score: Trends.statuses.score(status.id, locale: params[:locale].presence)) }= t('admin.trends.tags.trending_rank', rank: rank + 1) + %abbr{ title: t('admin.trends.tags.current_score', score: status.trend.score) }= t('admin.trends.tags.trending_rank', rank: status.trend.rank) - elsif status.requires_review? • = t('admin.trends.pending_review') diff --git a/app/views/admin/trends/statuses/index.html.haml b/app/views/admin/trends/statuses/index.html.haml index b0059b20d..a42d60b00 100644 --- a/app/views/admin/trends/statuses/index.html.haml +++ b/app/views/admin/trends/statuses/index.html.haml @@ -16,7 +16,7 @@ .filter-subset.filter-subset--with-select %strong= t('admin.follow_recommendations.language') .input.select.optional - = select_tag :locale, options_for_select(Trends.available_locales.map { |key| [standard_locale_name(key), key]}, params[:locale]), include_blank: true + = select_tag :locale, options_for_select(@locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), include_blank: true .filter-subset %strong= t('admin.trends.trending') %ul diff --git a/app/views/admin_mailer/_new_trending_links.text.erb b/app/views/admin_mailer/_new_trending_links.text.erb index 405926fdd..602e12793 100644 --- a/app/views/admin_mailer/_new_trending_links.text.erb +++ b/app/views/admin_mailer/_new_trending_links.text.erb @@ -2,13 +2,7 @@ <% @links.each do |link| %> - <%= link.title %> • <%= link.url %> - <%= raw t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: Trends.links.score(link.id).round(2)) %> -<% end %> - -<% if @lowest_trending_link %> -<%= raw t('admin_mailer.new_trends.new_trending_links.requirements', lowest_link_title: @lowest_trending_link.title, lowest_link_score: Trends.links.score(@lowest_trending_link.id).round(2), rank: Trends.links.options[:review_threshold]) %> -<% else %> -<%= raw t('admin_mailer.new_trends.new_trending_links.no_approved_links') %> + <%= standard_locale_name(link.language) %> • <%= raw t('admin.trends.links.usage_comparison', today: link.history.get(Time.now.utc).accounts, yesterday: link.history.get(Time.now.utc - 1.day).accounts) %> • <%= t('admin.trends.tags.current_score', score: link.trend.score.round(2)) %> <% end %> <%= raw t('application_mailer.view')%> <%= admin_trends_links_url %> diff --git a/app/views/admin_mailer/_new_trending_statuses.text.erb b/app/views/admin_mailer/_new_trending_statuses.text.erb index 8d11a80c2..1ed3ae857 100644 --- a/app/views/admin_mailer/_new_trending_statuses.text.erb +++ b/app/views/admin_mailer/_new_trending_statuses.text.erb @@ -2,13 +2,7 @@ <% @statuses.each do |status| %> - <%= ActivityPub::TagManager.instance.url_for(status) %> - <%= raw t('admin.trends.tags.current_score', score: Trends.statuses.score(status.id).round(2)) %> -<% end %> - -<% if @lowest_trending_status %> -<%= raw t('admin_mailer.new_trends.new_trending_statuses.requirements', lowest_status_url: ActivityPub::TagManager.instance.url_for(@lowest_trending_status), lowest_status_score: Trends.statuses.score(@lowest_trending_status.id).round(2), rank: Trends.statuses.options[:review_threshold]) %> -<% else %> -<%= raw t('admin_mailer.new_trends.new_trending_statuses.no_approved_statuses') %> + <%= standard_locale_name(status.language) %> • <%= raw t('admin.trends.tags.current_score', score: status.trend.score.round(2)) %> <% end %> <%= raw t('application_mailer.view')%> <%= admin_trends_statuses_url %> diff --git a/config/locales/en.yml b/config/locales/en.yml index cdac4fb54..bd913a5ca 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -939,12 +939,8 @@ en: new_trends: body: 'The following items need a review before they can be displayed publicly:' new_trending_links: - no_approved_links: There are currently no approved trending links. - requirements: 'Any of these candidates could surpass the #%{rank} approved trending link, which is currently "%{lowest_link_title}" with a score of %{lowest_link_score}.' title: Trending links new_trending_statuses: - no_approved_statuses: There are currently no approved trending posts. - requirements: 'Any of these candidates could surpass the #%{rank} approved trending post, which is currently %{lowest_status_url} with a score of %{lowest_status_score}.' title: Trending posts new_trending_tags: no_approved_tags: There are currently no approved trending hashtags. diff --git a/db/migrate/20220824233535_create_status_trends.rb b/db/migrate/20220824233535_create_status_trends.rb new file mode 100644 index 000000000..cea0abf35 --- /dev/null +++ b/db/migrate/20220824233535_create_status_trends.rb @@ -0,0 +1,12 @@ +class CreateStatusTrends < ActiveRecord::Migration[6.1] + def change + create_table :status_trends do |t| + t.references :status, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true } + t.references :account, null: false, foreign_key: { on_delete: :cascade } + t.float :score, null: false, default: 0 + t.integer :rank, null: false, default: 0 + t.boolean :allowed, null: false, default: false + t.string :language + end + end +end diff --git a/db/migrate/20221006061337_create_preview_card_trends.rb b/db/migrate/20221006061337_create_preview_card_trends.rb new file mode 100644 index 000000000..baad9c31c --- /dev/null +++ b/db/migrate/20221006061337_create_preview_card_trends.rb @@ -0,0 +1,11 @@ +class CreatePreviewCardTrends < ActiveRecord::Migration[6.1] + def change + create_table :preview_card_trends do |t| + t.references :preview_card, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true } + t.float :score, null: false, default: 0 + t.integer :rank, null: false, default: 0 + t.boolean :allowed, null: false, default: false + t.string :language + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1a98b22db..2f9058509 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_08_29_192658) do +ActiveRecord::Schema.define(version: 2022_10_06_061337) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -735,6 +735,15 @@ ActiveRecord::Schema.define(version: 2022_08_29_192658) do t.index ["domain"], name: "index_preview_card_providers_on_domain", unique: true end + create_table "preview_card_trends", force: :cascade do |t| + t.bigint "preview_card_id", null: false + t.float "score", default: 0.0, null: false + t.integer "rank", default: 0, null: false + t.boolean "allowed", default: false, null: false + t.string "language" + t.index ["preview_card_id"], name: "index_preview_card_trends_on_preview_card_id", unique: true + end + create_table "preview_cards", force: :cascade do |t| t.string "url", default: "", null: false t.string "title", default: "", null: false @@ -894,6 +903,17 @@ ActiveRecord::Schema.define(version: 2022_08_29_192658) do t.index ["status_id"], name: "index_status_stats_on_status_id", unique: true end + create_table "status_trends", force: :cascade do |t| + t.bigint "status_id", null: false + t.bigint "account_id", null: false + t.float "score", default: 0.0, null: false + t.integer "rank", default: 0, null: false + t.boolean "allowed", default: false, null: false + t.string "language" + t.index ["account_id"], name: "index_status_trends_on_account_id" + t.index ["status_id"], name: "index_status_trends_on_status_id", unique: true + end + create_table "statuses", id: :bigint, default: -> { "timestamp_id('statuses'::text)" }, force: :cascade do |t| t.string "uri" t.text "text", default: "", null: false @@ -1171,6 +1191,7 @@ ActiveRecord::Schema.define(version: 2022_08_29_192658) do add_foreign_key "poll_votes", "polls", on_delete: :cascade add_foreign_key "polls", "accounts", on_delete: :cascade add_foreign_key "polls", "statuses", on_delete: :cascade + add_foreign_key "preview_card_trends", "preview_cards", on_delete: :cascade add_foreign_key "report_notes", "accounts", on_delete: :cascade add_foreign_key "report_notes", "reports", on_delete: :cascade add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify @@ -1185,6 +1206,8 @@ ActiveRecord::Schema.define(version: 2022_08_29_192658) do add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade add_foreign_key "status_pins", "statuses", on_delete: :cascade add_foreign_key "status_stats", "statuses", on_delete: :cascade + add_foreign_key "status_trends", "accounts", on_delete: :cascade + add_foreign_key "status_trends", "statuses", on_delete: :cascade add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", name: "fk_c7fa917661", on_delete: :nullify add_foreign_key "statuses", "accounts", name: "fk_9bda1543f7", on_delete: :cascade add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify diff --git a/spec/fabricators/account_fabricator.rb b/spec/fabricators/account_fabricator.rb index f1cce281c..205706532 100644 --- a/spec/fabricators/account_fabricator.rb +++ b/spec/fabricators/account_fabricator.rb @@ -11,4 +11,5 @@ Fabricator(:account) do suspended_at { |attrs| attrs[:suspended] ? Time.now.utc : nil } silenced_at { |attrs| attrs[:silenced] ? Time.now.utc : nil } user { |attrs| attrs[:domain].nil? ? Fabricate.build(:user, account: nil) : nil } + discoverable true end diff --git a/spec/mailers/previews/admin_mailer_preview.rb b/spec/mailers/previews/admin_mailer_preview.rb index 01436ba7a..0ec9e9882 100644 --- a/spec/mailers/previews/admin_mailer_preview.rb +++ b/spec/mailers/previews/admin_mailer_preview.rb @@ -8,7 +8,7 @@ class AdminMailerPreview < ActionMailer::Preview # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_trends def new_trends - AdminMailer.new_trends(Account.first, PreviewCard.limit(3), Tag.limit(3), Status.where(reblog_of_id: nil).limit(3)) + AdminMailer.new_trends(Account.first, PreviewCard.joins(:trend).limit(3), Tag.limit(3), Status.joins(:trend).where(reblog_of_id: nil).limit(3)) end # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_appeal diff --git a/spec/models/preview_card_trend_spec.rb b/spec/models/preview_card_trend_spec.rb new file mode 100644 index 000000000..c7ab6ed14 --- /dev/null +++ b/spec/models/preview_card_trend_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe PreviewCardTrend, type: :model do +end diff --git a/spec/models/status_trend_spec.rb b/spec/models/status_trend_spec.rb new file mode 100644 index 000000000..6b82204a6 --- /dev/null +++ b/spec/models/status_trend_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe StatusTrend, type: :model do +end diff --git a/spec/models/trends/statuses_spec.rb b/spec/models/trends/statuses_spec.rb index 9cc67acbe..5f338a65e 100644 --- a/spec/models/trends/statuses_spec.rb +++ b/spec/models/trends/statuses_spec.rb @@ -9,8 +9,8 @@ RSpec.describe Trends::Statuses do let!(:query) { subject.query } let!(:today) { at_time } - let!(:status1) { Fabricate(:status, text: 'Foo', trendable: true, created_at: today) } - let!(:status2) { Fabricate(:status, text: 'Bar', trendable: true, created_at: today) } + let!(:status1) { Fabricate(:status, text: 'Foo', language: 'en', trendable: true, created_at: today) } + let!(:status2) { Fabricate(:status, text: 'Bar', language: 'en', trendable: true, created_at: today) } before do 15.times { reblog(status1, today) } @@ -69,9 +69,9 @@ RSpec.describe Trends::Statuses do let!(:today) { at_time } let!(:yesterday) { today - 1.day } - let!(:status1) { Fabricate(:status, text: 'Foo', trendable: true, created_at: yesterday) } - let!(:status2) { Fabricate(:status, text: 'Bar', trendable: true, created_at: today) } - let!(:status3) { Fabricate(:status, text: 'Baz', trendable: true, created_at: today) } + let!(:status1) { Fabricate(:status, text: 'Foo', language: 'en', trendable: true, created_at: yesterday) } + let!(:status2) { Fabricate(:status, text: 'Bar', language: 'en', trendable: true, created_at: today) } + let!(:status3) { Fabricate(:status, text: 'Baz', language: 'en', trendable: true, created_at: today) } before do 13.times { reblog(status1, today) } @@ -95,10 +95,10 @@ RSpec.describe Trends::Statuses do it 'decays scores' do subject.refresh(today) - original_score = subject.score(status2.id) + original_score = status2.trend.score expect(original_score).to be_a Float subject.refresh(today + subject.options[:score_halflife]) - decayed_score = subject.score(status2.id) + decayed_score = status2.trend.reload.score expect(decayed_score).to be <= original_score / 2 end end -- cgit From b04633a9614609f18b39ba0f0015df301a04ab64 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 13 Oct 2022 11:29:19 +0200 Subject: Add image processing and generate blurhash for server thumbnail (#19348) Remove separate server hero setting --- app/models/form/admin_settings.rb | 2 -- app/models/site_upload.rb | 27 +++++++++++++++++++++- app/presenters/instance_presenter.rb | 4 ---- app/serializers/rest/instance_serializer.rb | 15 +++++++++++- app/serializers/rest/v1/instance_serializer.rb | 2 +- app/views/admin/settings/edit.html.haml | 2 -- app/views/application/_sidebar.html.haml | 2 +- app/views/shared/_og.html.haml | 2 +- config/locales/en.yml | 3 --- .../20221012181003_add_blurhash_to_site_uploads.rb | 5 ++++ db/schema.rb | 3 ++- spec/presenters/instance_presenter_spec.rb | 7 ------ 12 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 db/migrate/20221012181003_add_blurhash_to_site_uploads.rb (limited to 'app/views/admin') diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 7bd9e3743..b6bb3d795 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -22,7 +22,6 @@ class Form::AdminSettings custom_css profile_directory thumbnail - hero mascot trends trendable_by_default @@ -49,7 +48,6 @@ class Form::AdminSettings UPLOAD_KEYS = %i( thumbnail - hero mascot ).freeze diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb index cf10b30fc..d3b81d4d5 100644 --- a/app/models/site_upload.rb +++ b/app/models/site_upload.rb @@ -12,10 +12,35 @@ # meta :json # created_at :datetime not null # updated_at :datetime not null +# blurhash :string # class SiteUpload < ApplicationRecord - has_attached_file :file + include Attachmentable + + STYLES = { + thumbnail: { + '@1x': { + format: 'png', + geometry: '1200x630#', + file_geometry_parser: FastGeometryParser, + blurhash: { + x_comp: 4, + y_comp: 4, + }.freeze, + }, + + '@2x': { + format: 'png', + geometry: '2400x1260#', + file_geometry_parser: FastGeometryParser, + }.freeze, + }.freeze, + + mascot: {}.freeze, + }.freeze + + has_attached_file :file, styles: ->(file) { STYLES[file.instance.var.to_sym] }, convert_options: { all: '-coalesce -strip' }, processors: [:lazy_thumbnail, :blurhash_transcoder, :type_corrector] validates_attachment_content_type :file, content_type: /\Aimage\/.*\z/ validates :file, presence: true diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index c461ac55f..43594a280 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -84,10 +84,6 @@ class InstancePresenter < ActiveModelSerializers::Model @thumbnail ||= Rails.cache.fetch('site_uploads/thumbnail') { SiteUpload.find_by(var: 'thumbnail') } end - def hero - @hero ||= Rails.cache.fetch('site_uploads/hero') { SiteUpload.find_by(var: 'hero') } - end - def mascot @mascot ||= Rails.cache.fetch('site_uploads/mascot') { SiteUpload.find_by(var: 'mascot') } end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index f4ea49427..dfa8ce40a 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -17,7 +17,20 @@ class REST::InstanceSerializer < ActiveModel::Serializer has_many :rules, serializer: REST::RuleSerializer def thumbnail - object.thumbnail ? full_asset_url(object.thumbnail.file.url) : full_pack_url('media/images/preview.png') + if object.thumbnail + { + url: full_asset_url(object.thumbnail.file.url(:'@1x')), + blurhash: object.thumbnail.blurhash, + versions: { + '@1x': full_asset_url(object.thumbnail.file.url(:'@1x')), + '@2x': full_asset_url(object.thumbnail.file.url(:'@2x')), + }, + } + else + { + url: full_pack_url('media/images/preview.png'), + } + end end def usage diff --git a/app/serializers/rest/v1/instance_serializer.rb b/app/serializers/rest/v1/instance_serializer.rb index 47fa086fc..872175451 100644 --- a/app/serializers/rest/v1/instance_serializer.rb +++ b/app/serializers/rest/v1/instance_serializer.rb @@ -33,7 +33,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer end def thumbnail - instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('media/images/preview.png') + instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url(:'@1x')) : full_pack_url('media/images/preview.png') end def stats diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 79f73a60f..15b1a2498 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -34,8 +34,6 @@ .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__column.fields-row__column-6.fields-group - = f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: site_upload_delete_hint(t('admin.settings.hero.desc_html'), :hero) .fields-row .fields-row__column.fields-row__column-6.fields-group diff --git a/app/views/application/_sidebar.html.haml b/app/views/application/_sidebar.html.haml index eb2813dd0..6d18668b0 100644 --- a/app/views/application/_sidebar.html.haml +++ b/app/views/application/_sidebar.html.haml @@ -1,6 +1,6 @@ .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 + = image_tag @instance_presenter.thumbnail&.file&.url(:'@1x') || 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') diff --git a/app/views/shared/_og.html.haml b/app/views/shared/_og.html.haml index b54ab2429..2941b566e 100644 --- a/app/views/shared/_og.html.haml +++ b/app/views/shared/_og.html.haml @@ -8,7 +8,7 @@ = opengraph 'og:type', 'website' = opengraph 'og:title', @instance_presenter.title = opengraph 'og:description', description -= opengraph 'og:image', full_asset_url(thumbnail&.file&.url || asset_pack_path('media/images/preview.png', protocol: :request)) += opengraph 'og:image', full_asset_url(thumbnail&.file&.url(:'@1x') || asset_pack_path('media/images/preview.png', protocol: :request)) = opengraph 'og:image:width', thumbnail ? thumbnail.meta['width'] : '1200' = opengraph 'og:image:height', thumbnail ? thumbnail.meta['height'] : '630' = opengraph 'twitter:card', 'summary_large_image' diff --git a/config/locales/en.yml b/config/locales/en.yml index bd913a5ca..8a70bd8ca 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -735,9 +735,6 @@ en: users: To logged-in local users domain_blocks_rationale: title: Show rationale - hero: - desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to server thumbnail - title: Hero image mascot: desc_html: Displayed on multiple pages. At least 293×205px recommended. When not set, falls back to default mascot title: Mascot image diff --git a/db/migrate/20221012181003_add_blurhash_to_site_uploads.rb b/db/migrate/20221012181003_add_blurhash_to_site_uploads.rb new file mode 100644 index 000000000..e1c87712b --- /dev/null +++ b/db/migrate/20221012181003_add_blurhash_to_site_uploads.rb @@ -0,0 +1,5 @@ +class AddBlurhashToSiteUploads < ActiveRecord::Migration[6.1] + def change + add_column :site_uploads, :blurhash, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 2f9058509..3972f777a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_10_06_061337) do +ActiveRecord::Schema.define(version: 2022_10_12_181003) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -866,6 +866,7 @@ ActiveRecord::Schema.define(version: 2022_10_06_061337) do t.json "meta" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "blurhash" t.index ["var"], name: "index_site_uploads_on_var", unique: true end diff --git a/spec/presenters/instance_presenter_spec.rb b/spec/presenters/instance_presenter_spec.rb index a0a8628e8..659c2cc2f 100644 --- a/spec/presenters/instance_presenter_spec.rb +++ b/spec/presenters/instance_presenter_spec.rb @@ -99,13 +99,6 @@ describe InstancePresenter do end end - describe '#hero' do - it 'returns SiteUpload' do - hero = Fabricate(:site_upload, var: 'hero') - expect(instance_presenter.hero).to eq(hero) - end - end - describe '#mascot' do it 'returns SiteUpload' do mascot = Fabricate(:site_upload, var: 'mascot') -- 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 'app/views/admin') 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 f8ca3bb2a1dd648f41e8fea5b5eb87b53bc8d521 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 26 Oct 2022 13:42:29 +0200 Subject: Add ability to view previous edits of a status in admin UI (#19462) * Add ability to view previous edits of a status in admin UI * Change moderator access to posts to be controlled by a separate policy --- app/controllers/admin/statuses_controller.rb | 16 +++++- .../admin/trends/statuses_controller.rb | 4 +- .../mastodon/components/status_action_bar.js | 2 +- .../features/status/components/action_bar.js | 2 +- app/javascript/styles/mastodon/admin.scss | 64 ++++++++++++++++++++++ app/models/admin/status_filter.rb | 5 +- app/models/status_edit.rb | 13 ++++- app/policies/admin/status_policy.rb | 29 ++++++++++ app/policies/status_policy.rb | 12 +--- .../admin/reports/_media_attachments.html.haml | 8 +++ app/views/admin/reports/_status.html.haml | 11 +--- .../admin/status_edits/_status_edit.html.haml | 20 +++++++ app/views/admin/statuses/show.html.haml | 64 ++++++++++++++++++++++ config/locales/en.yml | 13 +++++ config/routes.rb | 2 +- spec/policies/status_policy_spec.rb | 22 -------- 16 files changed, 232 insertions(+), 55 deletions(-) create mode 100644 app/policies/admin/status_policy.rb create mode 100644 app/views/admin/reports/_media_attachments.html.haml create mode 100644 app/views/admin/status_edits/_status_edit.html.haml create mode 100644 app/views/admin/statuses/show.html.haml (limited to 'app/views/admin') diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index 084921ceb..b80cd20f5 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -3,18 +3,23 @@ module Admin class StatusesController < BaseController before_action :set_account - before_action :set_statuses + before_action :set_statuses, except: :show + before_action :set_status, only: :show PER_PAGE = 20 def index - authorize :status, :index? + authorize [:admin, :status], :index? @status_batch_action = Admin::StatusBatchAction.new end + def show + authorize [:admin, @status], :show? + end + def batch - authorize :status, :index? + authorize [:admin, :status], :index? @status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button)) @status_batch_action.save! @@ -32,6 +37,7 @@ module Admin def after_create_redirect_path report_id = @status_batch_action&.report_id || params[:report_id] + if report_id.present? admin_report_path(report_id) else @@ -43,6 +49,10 @@ module Admin @account = Account.find(params[:account_id]) end + def set_status + @status = @account.statuses.find(params[:id]) + end + def set_statuses @statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE) end diff --git a/app/controllers/admin/trends/statuses_controller.rb b/app/controllers/admin/trends/statuses_controller.rb index 004f42b0c..3d8b53ea8 100644 --- a/app/controllers/admin/trends/statuses_controller.rb +++ b/app/controllers/admin/trends/statuses_controller.rb @@ -2,7 +2,7 @@ class Admin::Trends::StatusesController < Admin::BaseController def index - authorize :status, :review? + authorize [:admin, :status], :review? @locales = StatusTrend.pluck('distinct language') @statuses = filtered_statuses.page(params[:page]) @@ -10,7 +10,7 @@ class Admin::Trends::StatusesController < Admin::BaseController end def batch - authorize :status, :review? + authorize [:admin, :status], :review? @form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button)) @form.save diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 17150524e..fe8ece0f9 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -323,7 +323,7 @@ class StatusActionBar extends ImmutablePureComponent { if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { menu.push(null); menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); - menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` }); + menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); } } diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index a0a6a7894..4bd419ca4 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -254,7 +254,7 @@ class ActionBar extends React.PureComponent { if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { menu.push(null); menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); - menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` }); + menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); } } diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index affe1c79c..f86778399 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -1752,3 +1752,67 @@ a.sparkline { } } } + +.history { + counter-reset: step 0; + font-size: 15px; + line-height: 22px; + + li { + counter-increment: step 1; + padding-left: 2.5rem; + padding-bottom: 8px; + position: relative; + margin-bottom: 8px; + + &::before { + position: absolute; + content: counter(step); + font-size: 0.625rem; + font-weight: 500; + left: 0; + display: flex; + justify-content: center; + align-items: center; + width: calc(1.375rem + 1px); + height: calc(1.375rem + 1px); + background: $ui-base-color; + border: 1px solid $highlight-text-color; + color: $highlight-text-color; + border-radius: 8px; + } + + &::after { + position: absolute; + content: ""; + width: 1px; + background: $highlight-text-color; + bottom: 0; + top: calc(1.875rem + 1px); + left: 0.6875rem; + } + + &:last-child { + margin-bottom: 0; + + &::after { + display: none; + } + } + } + + &__entry { + h5 { + font-weight: 500; + color: $primary-text-color; + line-height: 25px; + margin-bottom: 16px; + } + + .status { + border: 1px solid lighten($ui-base-color, 4%); + background: $ui-base-color; + border-radius: 4px; + } + } +} diff --git a/app/models/admin/status_filter.rb b/app/models/admin/status_filter.rb index 4fba612a6..d7a16f760 100644 --- a/app/models/admin/status_filter.rb +++ b/app/models/admin/status_filter.rb @@ -3,7 +3,6 @@ class Admin::StatusFilter KEYS = %i( media - id report_id ).freeze @@ -28,12 +27,10 @@ class Admin::StatusFilter private - def scope_for(key, value) + def scope_for(key, _value) case key.to_s when 'media' Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id).reorder('statuses.id desc') - when 'id' - Status.where(id: value) else raise "Unknown filter: #{key}" end diff --git a/app/models/status_edit.rb b/app/models/status_edit.rb index e9c8fbe98..e33470226 100644 --- a/app/models/status_edit.rb +++ b/app/models/status_edit.rb @@ -30,7 +30,7 @@ class StatusEdit < ApplicationRecord :preview_remote_url, :text_url, :meta, :blurhash, :not_processed?, :needs_redownload?, :local?, :file, :thumbnail, :thumbnail_remote_url, - :shortcode, to: :media_attachment + :shortcode, :video?, :audio?, to: :media_attachment end rate_limit by: :account, family: :statuses @@ -40,7 +40,8 @@ class StatusEdit < ApplicationRecord default_scope { order(id: :asc) } - delegate :local?, to: :status + delegate :local?, :application, :edited?, :edited_at, + :discarded?, :visibility, to: :status def emojis return @emojis if defined?(@emojis) @@ -59,4 +60,12 @@ class StatusEdit < ApplicationRecord end end end + + def proper + self + end + + def reblog? + false + end end diff --git a/app/policies/admin/status_policy.rb b/app/policies/admin/status_policy.rb new file mode 100644 index 000000000..ffaa30f13 --- /dev/null +++ b/app/policies/admin/status_policy.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Admin::StatusPolicy < ApplicationPolicy + def initialize(current_account, record, preloaded_relations = {}) + super(current_account, record) + + @preloaded_relations = preloaded_relations + end + + def index? + role.can?(:manage_reports, :manage_users) + end + + def show? + role.can?(:manage_reports, :manage_users) && (record.public_visibility? || record.unlisted_visibility? || record.reported?) + end + + def destroy? + role.can?(:manage_reports) + end + + def update? + role.can?(:manage_reports) + end + + def review? + role.can?(:manage_taxonomies) + end +end diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 2f48b5d70..f3d0ffdba 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -7,10 +7,6 @@ class StatusPolicy < ApplicationPolicy @preloaded_relations = preloaded_relations end - def index? - role.can?(:manage_reports, :manage_users) - end - def show? return false if author.suspended? @@ -32,17 +28,13 @@ class StatusPolicy < ApplicationPolicy end def destroy? - role.can?(:manage_reports) || owned? + owned? end alias unreblog? destroy? def update? - role.can?(:manage_reports) || owned? - end - - def review? - role.can?(:manage_taxonomies) + owned? end private diff --git a/app/views/admin/reports/_media_attachments.html.haml b/app/views/admin/reports/_media_attachments.html.haml new file mode 100644 index 000000000..d0b7d52c3 --- /dev/null +++ b/app/views/admin/reports/_media_attachments.html.haml @@ -0,0 +1,8 @@ +- if status.ordered_media_attachments.first.video? + - video = status.ordered_media_attachments.first + = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), frameRate: video.file.meta.dig('original', 'frame_rate'), blurhash: video.blurhash, sensitive: status.sensitive?, visible: false, width: 610, height: 343, inline: true, alt: video.description, media: [ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer)].as_json +- elsif status.ordered_media_attachments.first.audio? + - audio = status.ordered_media_attachments.first + = react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) +- else + = react_component :media_gallery, height: 343, sensitive: status.sensitive?, visible: false, media: status.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml index 392fc8f81..b2982a42b 100644 --- a/app/views/admin/reports/_status.html.haml +++ b/app/views/admin/reports/_status.html.haml @@ -12,14 +12,7 @@ = prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis) - unless status.proper.ordered_media_attachments.empty? - - if status.proper.ordered_media_attachments.first.video? - - video = status.proper.ordered_media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), frameRate: video.file.meta.dig('original', 'frame_rate'), blurhash: video.blurhash, sensitive: status.proper.sensitive?, visible: false, width: 610, height: 343, inline: true, alt: video.description, media: [ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer)].as_json - - elsif status.proper.ordered_media_attachments.first.audio? - - audio = status.proper.ordered_media_attachments.first - = react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) - - else - = react_component :media_gallery, height: 343, sensitive: status.proper.sensitive?, visible: false, media: status.proper.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } + = render partial: 'admin/reports/media_attachments', locals: { status: status.proper } .detailed-status__meta - if status.application @@ -29,7 +22,7 @@ %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) - if status.edited? · - = t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'formatted')) + = link_to t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'formatted')), admin_account_status_path(status.account_id, status), class: 'detailed-status__datetime' - if status.discarded? · %span.negative-hint= t('admin.statuses.deleted') diff --git a/app/views/admin/status_edits/_status_edit.html.haml b/app/views/admin/status_edits/_status_edit.html.haml new file mode 100644 index 000000000..19a0e063d --- /dev/null +++ b/app/views/admin/status_edits/_status_edit.html.haml @@ -0,0 +1,20 @@ +.status + .status__content>< + - if status_edit.spoiler_text.blank? + = prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis) + - else + %details< + %summary>< + %strong> Content warning: #{prerender_custom_emojis(h(status_edit.spoiler_text), status_edit.emojis)} + = prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis) + + - unless status_edit.ordered_media_attachments.empty? + = render partial: 'admin/reports/media_attachments', locals: { status: status_edit } + + .detailed-status__meta + %time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at) + + - if status_edit.sensitive? + · + = fa_icon('eye-slash fw') + = t('stream_entries.sensitive_content') diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml new file mode 100644 index 000000000..62b49de8c --- /dev/null +++ b/app/views/admin/statuses/show.html.haml @@ -0,0 +1,64 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('statuses.title', name: display_name(@account), quote: truncate(@status.spoiler_text.presence || @status.text, length: 50, omission: '…', escape: false)) + +- content_for :heading_actions do + = link_to t('admin.statuses.open'), ActivityPub::TagManager.instance.url_for(@status), class: 'button', target: '_blank' + +%h3= t('admin.statuses.metadata') + +.table-wrapper + %table.table.horizontal-table + %tbody + %tr + %th= t('admin.statuses.account') + %td= admin_account_link_to @status.account + - if @status.reply? + %tr + %th= t('admin.statuses.in_reply_to') + %td= admin_account_link_to @status.in_reply_to_account, path: admin_account_status_path(@status.thread.account_id, @status.in_reply_to_id) + %tr + %th= t('admin.statuses.application') + %td= @status.application&.name + %tr + %th= t('admin.statuses.language') + %td= standard_locale_name(@status.language) + %tr + %th= t('admin.statuses.visibility') + %td= t("statuses.visibilities.#{@status.visibility}") + - if @status.trend + %tr + %th= t('admin.statuses.trending') + %td + - if @status.trend.allowed? + %abbr{ title: t('admin.trends.tags.current_score', score: @status.trend.score) }= t('admin.trends.tags.trending_rank', rank: @status.trend.rank) + - elsif @status.trend.requires_review? + = t('admin.trends.pending_review') + - else + = t('admin.trends.not_allowed_to_trend') + %tr + %th= t('admin.statuses.reblogs') + %td= friendly_number_to_human @status.reblogs_count + %tr + %th= t('admin.statuses.favourites') + %td= friendly_number_to_human @status.favourites_count + +%hr.spacer/ + +%h3= t('admin.statuses.history') + +%ol.history + - @status.edits.includes(:account, status: [:account]).each.with_index do |status_edit, i| + %li + .history__entry + %h5 + - if i.zero? + = t('admin.statuses.original_status') + - else + = t('admin.statuses.status_changed') + · + %time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at) + + = render status_edit diff --git a/config/locales/en.yml b/config/locales/en.yml index 70850d478..fd845c3c2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -705,16 +705,29 @@ en: delete: Delete uploaded file destroyed_msg: Site upload successfully deleted! statuses: + account: Author + application: Application back_to_account: Back to account page back_to_report: Back to report page batch: remove_from_report: Remove from report report: Report deleted: Deleted + favourites: Favourites + history: Version history + in_reply_to: Replying to + language: Language media: title: Media + metadata: Metadata no_status_selected: No posts were changed as none were selected + open: Open post + original_status: Original post + reblogs: Reblogs + status_changed: Post changed title: Account posts + trending: Trending + visibility: Visibility with_media: With media strikes: actions: diff --git a/config/routes.rb b/config/routes.rb index b44479e77..12726a677 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -325,7 +325,7 @@ Rails.application.routes.draw do resource :reset, only: [:create] resource :action, only: [:new, :create], controller: 'account_actions' - resources :statuses, only: [:index] do + resources :statuses, only: [:index, :show] do collection do post :batch end diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index 205ecd720..b88521708 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -96,10 +96,6 @@ RSpec.describe StatusPolicy, type: :model do expect(subject).to permit(status.account, status) end - it 'grants access when account is admin' do - expect(subject).to permit(admin.account, status) - end - it 'denies access when account is not deleter' do expect(subject).to_not permit(bob, status) end @@ -125,27 +121,9 @@ RSpec.describe StatusPolicy, type: :model do end end - permissions :index? do - it 'grants access if staff' do - expect(subject).to permit(admin.account) - end - - it 'denies access unless staff' do - expect(subject).to_not permit(alice) - end - end - permissions :update? do - it 'grants access if staff' do - expect(subject).to permit(admin.account, status) - end - it 'grants access if owner' do expect(subject).to permit(status.account, status) end - - it 'denies access unless staff' do - expect(subject).to_not permit(bob, status) - end end end -- cgit From d9d722d74b2cb9bf30fa4cc98af6ddbeca003ebc Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 28 Oct 2022 12:56:32 +0200 Subject: Change admin announcement edition interface to use datetime-local (#18321) * Change admin announcement edition interface to use datetime-local * Dynamically set announcement stop date as required if start date is set, set minimum date for stop date * Change `all_day` to not be bound to presence of time-range * Add pattern and placeholder as minimal fallback for browsers not supporting datetime-local * Display datetime-local inputs as local time Co-authored-by: Eugen Rochko --- app/javascript/packs/admin.js | 54 ++++++++++++++++++++++++++++ app/javascript/styles/mastodon/forms.scss | 5 ++- app/models/announcement.rb | 5 --- app/views/admin/announcements/edit.html.haml | 7 ++-- app/views/admin/announcements/new.html.haml | 9 +++-- 5 files changed, 69 insertions(+), 11 deletions(-) (limited to 'app/views/admin') diff --git a/app/javascript/packs/admin.js b/app/javascript/packs/admin.js index b733d6b18..de86e0e11 100644 --- a/app/javascript/packs/admin.js +++ b/app/javascript/packs/admin.js @@ -2,6 +2,24 @@ import './public-path'; import { delegate } from '@rails/ujs'; import ready from '../mastodon/ready'; +const setAnnouncementEndsAttributes = (target) => { + const valid = target?.value && target?.validity?.valid; + const element = document.querySelector('input[type="datetime-local"]#announcement_ends_at'); + if (valid) { + element.classList.remove('optional'); + element.required = true; + element.min = target.value; + } else { + element.classList.add('optional'); + element.removeAttribute('required'); + element.removeAttribute('min'); + } +}; + +delegate(document, 'input[type="datetime-local"]#announcement_starts_at', 'change', ({ target }) => { + setAnnouncementEndsAttributes(target); +}); + const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]'; const showSelectAll = () => { @@ -141,6 +159,20 @@ const onChangeRegistrationMode = (target) => { }); }; +const convertUTCDateTimeToLocal = (value) => { + const date = new Date(value + 'Z'); + const twoChars = (x) => (x.toString().padStart(2, '0')); + return `${date.getFullYear()}-${twoChars(date.getMonth()+1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`; +}; + +const convertLocalDatetimeToUTC = (value) => { + const re = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})/; + const match = re.exec(value); + const date = new Date(match[1], match[2] - 1, match[3], match[4], match[5]); + const fullISO8601 = date.toISOString(); + return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6); +}; + delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target)); ready(() => { @@ -163,6 +195,28 @@ ready(() => { } }); + [].forEach.call(document.querySelectorAll('input[type="datetime-local"]'), element => { + if (element.value) { + element.value = convertUTCDateTimeToLocal(element.value); + } + if (element.placeholder) { + element.placeholder = convertUTCDateTimeToLocal(element.placeholder); + } + }); + + delegate(document, 'form', 'submit', ({ target }) => { + [].forEach.call(target.querySelectorAll('input[type="datetime-local"]'), element => { + if (element.value && element.validity.valid) { + element.value = convertLocalDatetimeToUTC(element.value); + } + }); + }); + + const announcementStartsAt = document.querySelector('input[type="datetime-local"]#announcement_starts_at'); + if (announcementStartsAt) { + setAnnouncementEndsAttributes(announcementStartsAt); + } + const React = require('react'); const ReactDOM = require('react-dom'); diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 25c9c9fe5..a3ddc7636 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -405,6 +405,7 @@ code { input[type="email"], input[type="password"], input[type="url"], + input[type="datetime-local"], textarea { box-sizing: border-box; font-size: 16px; @@ -445,7 +446,8 @@ code { input[type="text"], input[type="number"], input[type="email"], - input[type="password"] { + input[type="password"], + input[type="datetime-local"] { &:focus:invalid:not(:placeholder-shown), &:required:invalid:not(:placeholder-shown) { border-color: lighten($error-red, 12%); @@ -461,6 +463,7 @@ code { input[type="number"], input[type="email"], input[type="password"], + input[type="datetime-local"], textarea, select { border-color: lighten($error-red, 12%); diff --git a/app/models/announcement.rb b/app/models/announcement.rb index bedced9de..4b2cb4c6d 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -31,7 +31,6 @@ class Announcement < ApplicationRecord validates :starts_at, presence: true, if: -> { ends_at.present? } validates :ends_at, presence: true, if: -> { starts_at.present? } - before_validation :set_all_day before_validation :set_published, on: :create def to_log_human_identifier @@ -89,10 +88,6 @@ class Announcement < ApplicationRecord private - def set_all_day - self.all_day = false if starts_at.blank? || ends_at.blank? - end - def set_published return unless scheduled_at.blank? || scheduled_at.past? diff --git a/app/views/admin/announcements/edit.html.haml b/app/views/admin/announcements/edit.html.haml index 5f56db5e7..e20a839b9 100644 --- a/app/views/admin/announcements/edit.html.haml +++ b/app/views/admin/announcements/edit.html.haml @@ -1,12 +1,15 @@ - content_for :page_title do = t('.title') +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + = simple_form_for @announcement, url: admin_announcement_path(@announcement) do |f| = render 'shared/error_messages', object: @announcement .fields-group - = f.input :starts_at, include_blank: true, wrapper: :with_block_label - = f.input :ends_at, include_blank: true, wrapper: :with_block_label + = f.input :starts_at, include_blank: true, wrapper: :with_block_label, html5: true, input_html: { pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}', placeholder: Time.now.strftime('%FT%R') } + = f.input :ends_at, include_blank: true, wrapper: :with_block_label, html5: true, input_html: { pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}', placeholder: Time.now.strftime('%FT%R') } .fields-group = f.input :all_day, as: :boolean, wrapper: :with_label diff --git a/app/views/admin/announcements/new.html.haml b/app/views/admin/announcements/new.html.haml index a5298c5f6..d5881b7aa 100644 --- a/app/views/admin/announcements/new.html.haml +++ b/app/views/admin/announcements/new.html.haml @@ -1,12 +1,15 @@ - content_for :page_title do = t('.title') +- content_for :header_tags do + = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' + = simple_form_for @announcement, url: admin_announcements_path do |f| = render 'shared/error_messages', object: @announcement .fields-group - = f.input :starts_at, include_blank: true, wrapper: :with_block_label - = f.input :ends_at, include_blank: true, wrapper: :with_block_label + = f.input :starts_at, include_blank: true, wrapper: :with_block_label, html5: true, input_html: { pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}', placeholder: Time.now.strftime('%FT%R') } + = f.input :ends_at, include_blank: true, wrapper: :with_block_label, html5: true, input_html: { pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}', placeholder: Time.now.strftime('%FT%R') } .fields-group = f.input :all_day, as: :boolean, wrapper: :with_label @@ -15,7 +18,7 @@ = f.input :text, wrapper: :with_block_label .fields-group - = f.input :scheduled_at, include_blank: true, wrapper: :with_block_label + = f.input :scheduled_at, include_blank: true, wrapper: :with_block_label, html5: true, input_html: { pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}(:[0-9]{2}){1,2}', placeholder: Time.now.strftime('%FT%R') } .actions = f.button :button, t('.create'), type: :submit -- cgit