From 86d4a154b3d969813673612ad4b88b0df994fe08 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 22 Sep 2026 12:22:42 +0000 Subject: [PATCH] Redesign: Update visibility settings in user settings (#40639) --- .../posting_defaults_controller.rb | 25 ++++++- app/javascript/entrypoints/public.tsx | 69 +++++++++++++++++++ .../posting_defaults/show.html.haml | 41 ++++++++--- config/locales/simple_form.en.yml | 1 + 4 files changed, 125 insertions(+), 11 deletions(-) diff --git a/app/controllers/settings/preferences/posting_defaults_controller.rb b/app/controllers/settings/preferences/posting_defaults_controller.rb index dcff94fc712..a51bcae9249 100644 --- a/app/controllers/settings/preferences/posting_defaults_controller.rb +++ b/app/controllers/settings/preferences/posting_defaults_controller.rb @@ -3,12 +3,35 @@ class Settings::Preferences::PostingDefaultsController < Settings::Preferences::BaseController private + def split_default_privacy + case current_user.setting_default_privacy + when 'public', 'unlisted' + 'public' + else + 'private' + end + end + helper_method :split_default_privacy + + def split_default_privacy_discoverable? + current_user.setting_default_privacy == 'public' + end + helper_method :split_default_privacy_discoverable? + def after_update_redirect_path settings_preferences_posting_defaults_path end def user_params - super.tap do |params| + params.expect(user: [:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys + %w(split_default_privacy split_default_privacy_discoverable)]).tap do |params| + if params[:settings_attributes][:split_default_privacy] + default_privacy = params[:settings_attributes].delete(:split_default_privacy) + discoverable = params[:settings_attributes].delete(:split_default_privacy_discoverable) + default_privacy = 'unlisted' if default_privacy == 'public' && !ActiveModel::Type::Boolean.new.cast(discoverable) + + params[:settings_attributes][:default_privacy] = default_privacy + end + params[:settings_attributes][:default_quote_policy] = 'nobody' if params[:settings_attributes][:default_privacy] == 'private' end end diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index 363ca0fec9a..a31dbb7f8ac 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -180,6 +180,10 @@ async function loaded() { document.querySelector('#user_settings_attributes_default_privacy'), ); + updateDefaultDiscoverableFromPrivacy( + document.querySelector('#user_settings_attributes_split_default_privacy'), + ); + truncateRuleHints(); applyRailsA11yPatches(); @@ -444,6 +448,71 @@ on('change', '#user_settings_attributes_default_privacy', ({ target }) => { updateDefaultQuotePrivacyFromPrivacy(target); }); +const updateDefaultDiscoverableFromPrivacy = ( + privacySelect: EventTarget | null, +) => { + if (!(privacySelect instanceof HTMLSelectElement) || !privacySelect.form) + return; + + const checkbox = privacySelect.form.querySelector( + 'input#user_settings_attributes_split_default_privacy_discoverable', + ); + if (!checkbox) return; + + if (privacySelect.value === 'private') { + checkbox.checked = false; + } + + setInputDisabled(checkbox, privacySelect.value === 'private'); +}; + +on( + 'change', + '#user_settings_attributes_split_default_privacy', + ({ target }) => { + updateDefaultDiscoverableFromPrivacy(target); + }, +); + +const updateDefaultQuotePrivacyFromSplitPrivacy = () => { + const privacySelect = document.querySelector( + 'select#user_settings_attributes_split_default_privacy', + ); + if (!(privacySelect instanceof HTMLSelectElement) || !privacySelect.form) + return; + + const discoverableCheckbox = document.querySelector( + 'input#user_settings_attributes_split_default_privacy_discoverable', + ); + if (!discoverableCheckbox) return; + + const select = privacySelect.form.querySelector( + 'select#user_settings_attributes_default_quote_policy', + ); + if (!select) return; + + let privacy = privacySelect.value; + if (privacy === 'public' && !discoverableCheckbox.checked) + privacy = 'unlisted'; + + setInputHint(select, privacy); + + if (privacySelect.value === 'private') { + select.value = 'nobody'; + setInputDisabled(select, true); + } else { + setInputDisabled(select, false); + } +}; + +on( + 'change', + '#user_settings_attributes_split_default_privacy,#user_settings_attributes_split_default_privacy_discoverable', + () => { + updateDefaultQuotePrivacyFromSplitPrivacy(); + }, +); + // Empty the honeypot fields in JS in case something like an extension // automatically filled them. on('submit', '#registration_new_user,#new_user', () => { diff --git a/app/views/settings/preferences/posting_defaults/show.html.haml b/app/views/settings/preferences/posting_defaults/show.html.haml index 9e145850132..dcb83bf13ca 100644 --- a/app/views/settings/preferences/posting_defaults/show.html.haml +++ b/app/views/settings/preferences/posting_defaults/show.html.haml @@ -13,19 +13,40 @@ = f.simple_fields_for :settings, current_user.settings do |ff| .flash-message= t('posting_defaults.explanation') - .fields-group - = ff.input :default_privacy, - collection: Status.selectable_visibilities, - selected: current_user.setting_default_privacy, - hint: false, - include_blank: false, - label_method: ->(visibility) { visibility == 'private' ? I18n.t('statuses.visibilities.private') : safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, - label: I18n.t('simple_form.labels.defaults.setting_default_privacy'), - required: false, - wrapper: :with_label + - if Mastodon::Feature.redesign_enabled? + .fields-group + = ff.input :split_default_privacy, + collection: Status.selectable_visibilities - ['unlisted'], + selected: split_default_privacy, + hint: false, + include_blank: false, + label_method: ->(visibility) { visibility == 'private' ? I18n.t('statuses.visibilities.private') : safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, + label: I18n.t('simple_form.labels.defaults.setting_default_privacy'), + required: false, + wrapper: :with_label + + .fields-group + = ff.input :split_default_privacy_discoverable, + disabled: split_default_privacy == 'private', + as: :boolean, + input_html: { checked: split_default_privacy_discoverable? }, + label: I18n.t('simple_form.labels.defaults.setting_split_default_privacy_discoverable'), + wrapper: :with_label + - else + .fields-group + = ff.input :default_privacy, + collection: Status.selectable_visibilities, + selected: current_user.setting_default_privacy, + hint: false, + include_blank: false, + label_method: ->(visibility) { visibility == 'private' ? I18n.t('statuses.visibilities.private') : safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, + label: I18n.t('simple_form.labels.defaults.setting_default_privacy'), + required: false, + wrapper: :with_label .fields-group = ff.input :default_quote_policy, + disabled: current_user.setting_default_privacy == 'private', collection: user_settings_collection('default_quote_policy'), include_blank: false, label_method: ->(policy) { I18n.t("statuses.quote_policies.#{policy}") }, diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 82c4f42bcf8..6f388e8b16a 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -263,6 +263,7 @@ en: setting_missing_alt_text_modal: Warn me before posting media without alt text setting_quick_boosting: Enable quick boosting setting_reduce_motion: Reduce motion in animations + setting_split_default_privacy_discoverable: Make my posts discoverable in public feed & search results setting_system_font_ui: Use system's default font setting_system_scrollbars_ui: Use system's default scrollbar setting_theme: Site theme