From a209b8e544f496152946022396ae9d8eccd8b5b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:09:54 +0100 Subject: [PATCH 01/18] chore(deps): update dependency rubyzip to v3.2.2 (#36687) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2a0da53d4b..c1b8276ce1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -791,7 +791,7 @@ GEM ruby-vips (2.2.5) ffi (~> 1.12) logger - rubyzip (3.2.1) + rubyzip (3.2.2) rufus-scheduler (3.9.2) fugit (~> 1.1, >= 1.11.1) safety_net_attestation (0.5.0) From 93acfdd7d3ae7939a6bf45c2e889b2d2afa5b537 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:05:26 +0100 Subject: [PATCH 02/18] chore(deps): update dependency irb to v1.15.3 (#36682) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c1b8276ce1..eb1f8e93b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -224,7 +224,7 @@ GEM mail (~> 2.7) email_validator (2.2.4) activemodel - erb (5.1.1) + erb (5.1.3) erubi (1.13.1) et-orbi (1.4.0) tzinfo @@ -337,7 +337,7 @@ GEM activesupport (>= 3.0) nokogiri (>= 1.6) io-console (0.8.1) - irb (1.15.2) + irb (1.15.3) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) @@ -691,7 +691,7 @@ GEM readline (~> 0.0) rdf-normalize (0.7.0) rdf (~> 3.3) - rdoc (6.15.0) + rdoc (6.15.1) erb psych (>= 4.0.0) tsort From 6d53ca63d6fa84db527fab9b51b678c76bdf606e Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 3 Nov 2025 14:56:07 +0100 Subject: [PATCH 03/18] Disable paste-link-to-quote flow when composing Private Mentions (#36690) --- app/javascript/mastodon/actions/compose_typed.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts index 0f9bf5cfb3..8088306028 100644 --- a/app/javascript/mastodon/actions/compose_typed.ts +++ b/app/javascript/mastodon/actions/compose_typed.ts @@ -191,7 +191,8 @@ export const pasteLinkCompose = createDataLoadingThunk( composeState.get('is_submitting') || composeState.get('poll') || composeState.get('is_uploading') || - composeState.get('id') + composeState.get('id') || + composeState.get('privacy') === 'direct' ) return; From 16a54f7158eee82dcf408eaaa82b0e307bd001a4 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 3 Nov 2025 15:15:18 +0100 Subject: [PATCH 04/18] Fix issuance of quote approval for remote private statuses (#36693) --- .../activitypub/quote_authorizations_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/activitypub/quote_authorizations_controller.rb b/app/controllers/activitypub/quote_authorizations_controller.rb index f2f5313e1a..f4a1505550 100644 --- a/app/controllers/activitypub/quote_authorizations_controller.rb +++ b/app/controllers/activitypub/quote_authorizations_controller.rb @@ -9,7 +9,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController before_action :set_quote_authorization def show - expires_in 30.seconds, public: true if @quote.status.distributable? && public_fetch_mode? + expires_in 30.seconds, public: true if @quote.quoted_status.distributable? && public_fetch_mode? render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end @@ -23,7 +23,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController @quote = Quote.accepted.where(quoted_account: @account).find(params[:id]) return not_found unless @quote.status.present? && @quote.quoted_status.present? - authorize @quote.status, :show? + authorize @quote.quoted_status, :show? rescue Mastodon::NotPermittedError not_found end From 05964f571bea223ae6710e9965d997dcd1ce1d97 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 3 Nov 2025 15:16:25 +0100 Subject: [PATCH 05/18] Prevent creation of Private Mentions quoting someone who is not mentioned (#36689) --- app/services/post_status_service.rb | 12 +++++++ config/locales/en.yml | 1 + spec/requests/api/v1/statuses_spec.rb | 45 +++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index c55a54df37..3e4a715225 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -80,6 +80,7 @@ class PostStatusService < BaseService @status = @account.statuses.new(status_attributes) process_mentions_service.call(@status, save_records: false) safeguard_mentions!(@status) + safeguard_private_mention_quote!(@status) attach_quote!(@status) antispam = Antispam.new(@status) @@ -92,6 +93,16 @@ class PostStatusService < BaseService end end + def safeguard_private_mention_quote!(status) + return if @quoted_status.nil? || @visibility.to_sym != :direct + + # The mentions array test here is awkward because the relationship is not persisted at this time + return if @quoted_status.account_id == @account.id || status.mentions.to_a.any? { |mention| mention.account_id == @quoted_status.account_id && !mention.silent } + + status.errors.add(:base, I18n.t('statuses.errors.quoted_user_not_mentioned')) + raise ActiveRecord::RecordInvalid, status + end + def attach_quote!(status) return if @quoted_status.nil? @@ -114,6 +125,7 @@ class PostStatusService < BaseService def schedule_status! status_for_validation = @account.statuses.build(status_attributes) + safeguard_private_mention_quote!(status_for_validation) antispam = Antispam.new(status_for_validation) antispam.local_preflight_check! diff --git a/config/locales/en.yml b/config/locales/en.yml index b647c5ddff..949eb6d909 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1930,6 +1930,7 @@ en: errors: in_reply_not_found: The post you are trying to reply to does not appear to exist. quoted_status_not_found: The post you are trying to quote does not appear to exist. + quoted_user_not_mentioned: Cannot quote a non-mentioned user in a Private Mention post. over_character_limit: character limit of %{max} exceeded pin_errors: direct: Posts that are only visible to mentioned users cannot be pinned diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index ed41e54206..d3def6104f 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -228,7 +228,7 @@ RSpec.describe '/api/v1/statuses' do end context 'with a self-quote post' do - let(:quoted_status) { Fabricate(:status, account: user.account) } + let!(:quoted_status) { Fabricate(:status, account: user.account) } let(:params) do { status: 'Hello world, this is a self-quote', @@ -237,7 +237,48 @@ RSpec.describe '/api/v1/statuses' do end it 'returns a quote post, as well as rate limit headers', :aggregate_failures do - subject + expect { subject }.to change(user.account.statuses, :count).by(1) + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body[:quote]).to be_present + expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s + expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s + end + end + + context 'with a quote to a non-mentioned user in a Private Mention' do + let!(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) } + let(:params) do + { + status: 'Hello, this is a quote', + quoted_status_id: quoted_status.id, + visibility: :direct, + } + end + + it 'returns an error and does not create a post', :aggregate_failures do + expect { subject }.to_not change(user.account.statuses, :count) + + expect(response).to have_http_status(422) + expect(response.content_type) + .to start_with('application/json') + end + end + + context 'with a quote to a mentioned user in a Private Mention' do + let!(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) } + let(:params) do + { + status: "Hello @#{quoted_status.account.acct}, this is a quote", + quoted_status_id: quoted_status.id, + visibility: :direct, + } + end + + it 'returns a quote post, as well as rate limit headers', :aggregate_failures do + expect { subject }.to change(user.account.statuses, :count).by(1) expect(response).to have_http_status(200) expect(response.content_type) From b5a6feb3bf8883b5c5c423914c492e76ce2ee8fe Mon Sep 17 00:00:00 2001 From: Rachael Wright-Munn Date: Mon, 3 Nov 2025 12:56:17 -0500 Subject: [PATCH 06/18] Move "Privacy and reach" from "Public profile" to top-level navigation (#27294) --- app/views/settings/privacy/show.html.haml | 3 +-- app/views/settings/shared/_profile_navigation.html.haml | 1 - config/navigation.rb | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/views/settings/privacy/show.html.haml b/app/views/settings/privacy/show.html.haml index f7241cfb24..553d6e80af 100644 --- a/app/views/settings/privacy/show.html.haml +++ b/app/views/settings/privacy/show.html.haml @@ -2,8 +2,7 @@ = t('privacy.title') - content_for :heading do - %h2= t('settings.profile') - = render partial: 'settings/shared/profile_navigation' + %h2= t('privacy.title') = simple_form_for @account, url: settings_privacy_path do |f| = render 'shared/error_messages', object: @account diff --git a/app/views/settings/shared/_profile_navigation.html.haml b/app/views/settings/shared/_profile_navigation.html.haml index 2f81cb5cfd..9da8af9c2e 100644 --- a/app/views/settings/shared/_profile_navigation.html.haml +++ b/app/views/settings/shared/_profile_navigation.html.haml @@ -2,6 +2,5 @@ = render_navigation renderer: :links do |primary| :ruby primary.item :edit_profile, safe_join([material_symbol('person'), t('settings.edit_profile')]), settings_profile_path - primary.item :privacy_reach, safe_join([material_symbol('lock'), t('privacy.title')]), settings_privacy_path primary.item :verification, safe_join([material_symbol('check'), t('verification.verification')]), settings_verification_path primary.item :featured_tags, safe_join([material_symbol('tag'), t('settings.featured_tags')]), settings_featured_tags_path diff --git a/config/navigation.rb b/config/navigation.rb index 4829f9efd2..66fdfdfe58 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -12,7 +12,8 @@ SimpleNavigation::Configuration.run do |navigation| if: -> { Rails.configuration.x.mastodon.software_update_url.present? && current_user.can?(:view_devops) && SoftwareUpdate.urgent_pending? }, html: { class: 'warning' } - n.item :profile, safe_join([material_symbol('person'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? && !self_destruct }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification|/settings/privacy} + n.item :profile, safe_join([material_symbol('person'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? && !self_destruct }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification} + n.item :privacy, safe_join([material_symbol('globe'), t('privacy.title')]), settings_privacy_path, if: -> { current_user.functional? && !self_destruct }, highlights_on: %r{/settings/privacy} n.item :preferences, safe_join([material_symbol('settings'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? && !self_destruct } do |s| s.item :appearance, safe_join([material_symbol('computer'), t('settings.appearance')]), settings_preferences_appearance_path From e62baacfc11508eb1d2ac1c1bcc3c4bd615d940b Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 4 Nov 2025 09:29:27 +0100 Subject: [PATCH 07/18] Increase number of quote approval job retries (#36698) --- app/workers/activitypub/refetch_and_verify_quote_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/activitypub/refetch_and_verify_quote_worker.rb b/app/workers/activitypub/refetch_and_verify_quote_worker.rb index e2df023103..1a8dd40541 100644 --- a/app/workers/activitypub/refetch_and_verify_quote_worker.rb +++ b/app/workers/activitypub/refetch_and_verify_quote_worker.rb @@ -5,7 +5,7 @@ class ActivityPub::RefetchAndVerifyQuoteWorker include ExponentialBackoff include JsonLdHelper - sidekiq_options queue: 'pull', retry: 3 + sidekiq_options queue: 'pull', retry: 5 def perform(quote_id, quoted_uri, options = {}) quote = Quote.find(quote_id) From a7ecfc1ca5d2aab3cbbbc88c357a0b0d8042cdfd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:41:19 +0100 Subject: [PATCH 08/18] fix(deps): update dependency @rails/ujs to v7.1.600 (#36634) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index ae482f478a..cbdfe2cddf 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@gamestdio/websocket": "^0.3.2", "@github/webauthn-json": "^2.1.1", "@optimize-lodash/rollup-plugin": "^5.0.2", - "@rails/ujs": "7.1.502", + "@rails/ujs": "7.1.600", "@react-spring/web": "^9.7.5", "@reduxjs/toolkit": "^2.0.1", "@use-gesture/react": "^10.3.1", diff --git a/yarn.lock b/yarn.lock index d0f54b81cc..636e891709 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2795,7 +2795,7 @@ __metadata: "@gamestdio/websocket": "npm:^0.3.2" "@github/webauthn-json": "npm:^2.1.1" "@optimize-lodash/rollup-plugin": "npm:^5.0.2" - "@rails/ujs": "npm:7.1.502" + "@rails/ujs": "npm:7.1.600" "@react-spring/web": "npm:^9.7.5" "@reduxjs/toolkit": "npm:^2.0.1" "@storybook/addon-a11y": "npm:^9.1.1" @@ -3292,10 +3292,10 @@ __metadata: languageName: node linkType: hard -"@rails/ujs@npm:7.1.502": - version: 7.1.502 - resolution: "@rails/ujs@npm:7.1.502" - checksum: 10c0/79b46e8abd03e3fc633d93cc4e4c23838c628b775802fb38c2ce68b0e609ce287a67b81db112a93cc78c07ec82ca3b4cf87e74eb556d35148ce5f64c8be9201f +"@rails/ujs@npm:7.1.600": + version: 7.1.600 + resolution: "@rails/ujs@npm:7.1.600" + checksum: 10c0/0ccaa68a08fbc7b084ab89a1fe49520a5cba6d99f4b0feaf0cb3d00334c59d8d798932d7e49b84aa388875d039ea1e17eb115ed96a80ad157e408a13eceef53e languageName: node linkType: hard From 1baede0a7c757b9ec3c65a5250b9ec0ff4354e13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:32:38 +0100 Subject: [PATCH 09/18] chore(deps): update dependency brakeman to v7.1.1 (#35434) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index eb1f8e93b3..9e0eab42fe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -128,7 +128,7 @@ GEM blurhash (0.1.8) bootsnap (1.18.6) msgpack (~> 1.2) - brakeman (7.0.2) + brakeman (7.1.1) racc browser (6.2.0) builder (3.3.0) From 449eb03f11aca90ace859469c7212e31b0f11c4b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:41:59 +0100 Subject: [PATCH 10/18] chore(deps): update dependency sidekiq to v8.0.9 (#36699) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9e0eab42fe..417b89ffae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -621,7 +621,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.3) + rack (3.2.4) rack-attack (6.8.0) rack (>= 1.0, < 4) rack-cors (3.0.0) @@ -805,7 +805,7 @@ GEM securerandom (0.4.1) shoulda-matchers (6.5.0) activesupport (>= 5.2.0) - sidekiq (8.0.8) + sidekiq (8.0.9) connection_pool (>= 2.5.0) json (>= 2.9.0) logger (>= 1.6.2) From dd708298a8c885ba53cc5528ad0e9158b58cd9ab Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 4 Nov 2025 11:37:43 +0100 Subject: [PATCH 11/18] Remove option to disable access to local topic feeds for logged-in users (#36703) --- app/javascript/mastodon/initial_state.ts | 2 +- app/models/form/admin_settings.rb | 3 ++- app/views/admin/settings/discovery/show.html.haml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/initial_state.ts b/app/javascript/mastodon/initial_state.ts index 83c6c35e1b..86d36468f9 100644 --- a/app/javascript/mastodon/initial_state.ts +++ b/app/javascript/mastodon/initial_state.ts @@ -35,7 +35,7 @@ interface InitialStateMeta { streaming_api_base_url: string; local_live_feed_access: 'public' | 'authenticated' | 'disabled'; remote_live_feed_access: 'public' | 'authenticated' | 'disabled'; - local_topic_feed_access: 'public' | 'authenticated' | 'disabled'; + local_topic_feed_access: 'public' | 'authenticated'; remote_topic_feed_access: 'public' | 'authenticated' | 'disabled'; title: string; show_trends: boolean; diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 926995f02e..af4ad38ae9 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -87,6 +87,7 @@ class Form::AdminSettings DOMAIN_BLOCK_AUDIENCES = %w(disabled users all).freeze REGISTRATION_MODES = %w(open approved none).freeze FEED_ACCESS_MODES = %w(public authenticated disabled).freeze + ALTERNATE_FEED_ACCESS_MODES = %w(public authenticated).freeze LANDING_PAGE = %w(trends about local_feed).freeze attr_accessor(*KEYS) @@ -99,7 +100,7 @@ class Form::AdminSettings validates :show_domain_blocks_rationale, inclusion: { in: DOMAIN_BLOCK_AUDIENCES }, if: -> { defined?(@show_domain_blocks_rationale) } validates :local_live_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@local_live_feed_access) } validates :remote_live_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@remote_live_feed_access) } - validates :local_topic_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@local_topic_feed_access) } + validates :local_topic_feed_access, inclusion: { in: ALTERNATE_FEED_ACCESS_MODES }, if: -> { defined?(@local_topic_feed_access) } validates :remote_topic_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@remote_topic_feed_access) } 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 :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) } diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml index 1272419c33..04c242597d 100644 --- a/app/views/admin/settings/discovery/show.html.haml +++ b/app/views/admin/settings/discovery/show.html.haml @@ -43,7 +43,7 @@ .fields-row .fields-row__column.fields-row__column-6.fields-group = f.input :local_topic_feed_access, - collection: f.object.class::FEED_ACCESS_MODES, + collection: f.object.class::ALTERNATE_FEED_ACCESS_MODES, include_blank: false, label_method: ->(mode) { I18n.t("admin.settings.feed_access.modes.#{mode}") }, wrapper: :with_label From a978e37f4cb6813ad4af8eaf3f30e402b7f3760c Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 4 Nov 2025 12:01:25 +0100 Subject: [PATCH 12/18] Fix quote dropdown menu item in detailed status view (#36704) --- app/javascript/mastodon/features/status/index.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index bcccc11044..140413d1a9 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -299,6 +299,12 @@ class Status extends ImmutablePureComponent { dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } })); }; + handleQuote = (status) => { + const { dispatch } = this.props; + + dispatch(quoteComposeById(status.get('id'))); + }; + handleEditClick = (status) => { const { dispatch, askReplyConfirmation } = this.props; @@ -625,6 +631,7 @@ class Status extends ImmutablePureComponent { onDelete={this.handleDeleteClick} onRevokeQuote={this.handleRevokeQuoteClick} onQuotePolicyChange={this.handleQuotePolicyChange} + onQuote={this.handleQuote} onEdit={this.handleEditClick} onDirect={this.handleDirectClick} onMention={this.handleMentionClick} From 0b50789c5bfdf0225b4d716c11f0ea8977c66078 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 4 Nov 2025 16:30:10 +0100 Subject: [PATCH 13/18] Fix Skeleton placeholders being animated when setting to reduce animations is enabled (#36716) --- app/javascript/styles/mastodon/admin.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 6a5008909d..b5cb56d425 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -1309,6 +1309,10 @@ a.sparkline { line-height: 1; width: 100%; animation: skeleton 1.2s ease-in-out infinite; + + .reduce-motion & { + animation: none; + } } @keyframes skeleton { From 5253527ec4af6f9999a8db8152cd34f748fec4e7 Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 30 Oct 2025 13:30:42 +0100 Subject: [PATCH 14/18] Add CSS Module support (#36637) --- package.json | 1 + tsconfig.json | 3 +- vite.config.mts | 41 ++++++ yarn.lock | 322 ++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 356 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index cbdfe2cddf..44c60bb4a4 100644 --- a/package.json +++ b/package.json @@ -192,6 +192,7 @@ "stylelint-config-standard-scss": "^16.0.0", "typescript": "~5.9.0", "typescript-eslint": "^8.45.0", + "typescript-plugin-css-modules": "^5.2.0", "vitest": "^3.2.4" }, "resolutions": { diff --git a/tsconfig.json b/tsconfig.json index 2b981b67ab..db727baf18 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,8 @@ "mastodon/*": ["app/javascript/mastodon/*"], "images/*": ["app/javascript/images/*"], "styles/*": ["app/javascript/styles/*"] - } + }, + "plugins": [{ "name": "typescript-plugin-css-modules" }] }, "include": [ "vite.config.mts", diff --git a/vite.config.mts b/vite.config.mts index 30c0741aaa..f2a0ec0597 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -27,6 +27,8 @@ import { MastodonAssetsManifest } from './config/vite/plugin-assets-manifest'; const jsRoot = path.resolve(__dirname, 'app/javascript'); +const cssAliasClasses: ReadonlyArray = ['components', 'features']; + export const config: UserConfigFnPromise = async ({ mode, command }) => { const isProdBuild = mode === 'production' && command === 'build'; @@ -49,6 +51,45 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { }, }, css: { + modules: { + generateScopedName(name, filename) { + let prefix = ''; + + // Use the top two segments of the path as the prefix. + const [parentDirName, dirName] = path + .dirname(filename) + .split(path.sep) + .slice(-2) + .map((dir) => dir.toLowerCase()); + + // If the parent directory is in the cssAliasClasses list, use + // the first four letters of it as the prefix, otherwise use the full name. + if (parentDirName) { + if (cssAliasClasses.includes(parentDirName)) { + prefix = parentDirName.slice(0, 4); + } else { + prefix = parentDirName; + } + } + + // If we have a directory name, append it to the prefix. + if (dirName) { + prefix = `${prefix}_${dirName}`; + } + + // If the file is not styles.module.scss or style.module.scss, + // append the file base name to the prefix. + const baseName = path.basename( + filename, + `.module${path.extname(filename)}`, + ); + if (baseName !== 'styles' && baseName !== 'style') { + prefix = `${prefix}_${baseName}`; + } + + return `_${prefix}__${name}`; + }, + }, postcss: { plugins: [ postcssPresetEnv({ diff --git a/yarn.lock b/yarn.lock index 636e891709..48a748a1f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,13 @@ __metadata: languageName: node linkType: hard +"@adobe/css-tools@npm:~4.3.1": + version: 4.3.3 + resolution: "@adobe/css-tools@npm:4.3.3" + checksum: 10c0/e76e712df713964b87cdf2aca1f0477f19bebd845484d5fcba726d3ec7782366e2f26ec8cb2dcfaf47081a5c891987d8a9f5c3f30d11e1eb3c1848adc27fcb24 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -2923,6 +2930,7 @@ __metadata: twitter-text: "npm:3.1.0" typescript: "npm:~5.9.0" typescript-eslint: "npm:^8.45.0" + typescript-plugin-css-modules: "npm:^5.2.0" use-debounce: "npm:^10.0.0" vite: "npm:^7.1.1" vite-plugin-manifest-sri: "npm:^0.2.0" @@ -4344,6 +4352,24 @@ __metadata: languageName: node linkType: hard +"@types/postcss-modules-local-by-default@npm:^4.0.2": + version: 4.0.2 + resolution: "@types/postcss-modules-local-by-default@npm:4.0.2" + dependencies: + postcss: "npm:^8.0.0" + checksum: 10c0/af13e40673abf96f1427c467bc9d96986fc0fb702f65ef702de05f70e51af2212bc0bdf177bfd817e418f2238bf210fdee3541dd2d939fde9a4df5f8972ad716 + languageName: node + linkType: hard + +"@types/postcss-modules-scope@npm:^3.0.4": + version: 3.0.4 + resolution: "@types/postcss-modules-scope@npm:3.0.4" + dependencies: + postcss: "npm:^8.0.0" + checksum: 10c0/6364674e429143fd686e0238d071377cf9ae1780a77f99e99292a06adc93057609146aaf55c09310ae99526c37e56be5a8a843086c0ff95513bd34c6bc4c7480 + languageName: node + linkType: hard + "@types/prop-types@npm:*, @types/prop-types@npm:^15.7.5": version: 15.7.15 resolution: "@types/prop-types@npm:15.7.15" @@ -6107,6 +6133,15 @@ __metadata: languageName: node linkType: hard +"copy-anything@npm:^2.0.1": + version: 2.0.6 + resolution: "copy-anything@npm:2.0.6" + dependencies: + is-what: "npm:^3.14.1" + checksum: 10c0/2702998a8cc015f9917385b7f16b0d85f1f6e5e2fd34d99f14df584838f492f49aa0c390d973684c687e895c5c58d08b308a0400ac3e1e3d6fa1e5884a5402ad + languageName: node + linkType: hard + "core-js-compat@npm:^3.43.0": version: 3.44.0 resolution: "core-js-compat@npm:3.44.0" @@ -6581,6 +6616,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.4.2": + version: 16.6.1 + resolution: "dotenv@npm:16.6.1" + checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc + languageName: node + linkType: hard + "dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" @@ -6748,6 +6790,17 @@ __metadata: languageName: node linkType: hard +"errno@npm:^0.1.1": + version: 0.1.8 + resolution: "errno@npm:0.1.8" + dependencies: + prr: "npm:~1.0.1" + bin: + errno: cli.js + checksum: 10c0/83758951967ec57bf00b5f5b7dc797e6d65a6171e57ea57adcf1bd1a0b477fd9b5b35fae5be1ff18f4090ed156bce1db749fe7e317aac19d485a5d150f6a4936 + languageName: node + linkType: hard + "error-ex@npm:^1.3.1": version: 1.3.2 resolution: "error-ex@npm:1.3.2" @@ -8037,7 +8090,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -8254,7 +8307,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -8263,6 +8316,15 @@ __metadata: languageName: node linkType: hard +"icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0": + version: 5.1.0 + resolution: "icss-utils@npm:5.1.0" + peerDependencies: + postcss: ^8.1.0 + checksum: 10c0/39c92936fabd23169c8611d2b5cc39e39d10b19b0d223352f20a7579f75b39d5f786114a6b8fc62bee8c5fed59ba9e0d38f7219a4db383e324fb3061664b043d + languageName: node + linkType: hard + "idb-keyval@npm:^6.2.0": version: 6.2.1 resolution: "idb-keyval@npm:6.2.1" @@ -8298,6 +8360,15 @@ __metadata: languageName: node linkType: hard +"image-size@npm:~0.5.0": + version: 0.5.5 + resolution: "image-size@npm:0.5.5" + bin: + image-size: bin/image-size.js + checksum: 10c0/655204163af06732f483a9fe7cce9dff4a29b7b2e88f5c957a5852e8143fa750f5e54b1955a2ca83de99c5220dbd680002d0d4e09140b01433520f4d5a0b1f4c + languageName: node + linkType: hard + "immer@npm:^10.0.3": version: 10.0.3 resolution: "immer@npm:10.0.3" @@ -8775,6 +8846,13 @@ __metadata: languageName: node linkType: hard +"is-what@npm:^3.14.1": + version: 3.14.1 + resolution: "is-what@npm:3.14.1" + checksum: 10c0/4b770b85454c877b6929a84fd47c318e1f8c2ff70fd72fd625bc3fde8e0c18a6e57345b6e7aa1ee9fbd1c608d27cfe885df473036c5c2e40cd2187250804a2c7 + languageName: node + linkType: hard + "is-wsl@npm:^2.2.0": version: 2.2.0 resolution: "is-wsl@npm:2.2.0" @@ -9165,6 +9243,41 @@ __metadata: languageName: node linkType: hard +"less@npm:^4.2.0": + version: 4.4.2 + resolution: "less@npm:4.4.2" + dependencies: + copy-anything: "npm:^2.0.1" + errno: "npm:^0.1.1" + graceful-fs: "npm:^4.1.2" + image-size: "npm:~0.5.0" + make-dir: "npm:^2.1.0" + mime: "npm:^1.4.1" + needle: "npm:^3.1.0" + parse-node-version: "npm:^1.0.1" + source-map: "npm:~0.6.0" + tslib: "npm:^2.3.0" + dependenciesMeta: + errno: + optional: true + graceful-fs: + optional: true + image-size: + optional: true + make-dir: + optional: true + mime: + optional: true + needle: + optional: true + source-map: + optional: true + bin: + lessc: bin/lessc + checksum: 10c0/f8b796e45ef171adc390b5250f3018922cd046c256181dd9d4cbcbbdc5d6de7cb88c8327741c10eff7ff76421cd826fd95a664ea1b88fbf6f31742428d4a2dab + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -9182,6 +9295,13 @@ __metadata: languageName: node linkType: hard +"lilconfig@npm:^2.0.5": + version: 2.1.0 + resolution: "lilconfig@npm:2.1.0" + checksum: 10c0/64645641aa8d274c99338e130554abd6a0190533c0d9eb2ce7ebfaf2e05c7d9961f3ffe2bfa39efd3b60c521ba3dd24fa236fe2775fc38501bf82bf49d4678b8 + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -9238,6 +9358,13 @@ __metadata: languageName: node linkType: hard +"lodash.camelcase@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.camelcase@npm:4.3.0" + checksum: 10c0/fcba15d21a458076dd309fce6b1b4bf611d84a0ec252cb92447c948c533ac250b95d2e00955801ebc367e5af5ed288b996d75d37d2035260a937008e14eaf432 + languageName: node + linkType: hard + "lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -9388,6 +9515,16 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^2.1.0": + version: 2.1.0 + resolution: "make-dir@npm:2.1.0" + dependencies: + pify: "npm:^4.0.1" + semver: "npm:^5.6.0" + checksum: 10c0/ada869944d866229819735bee5548944caef560d7a8536ecbc6536edca28c72add47cc4f6fc39c54fb25d06b58da1f8994cf7d9df7dadea047064749efc085d8 + languageName: node + linkType: hard + "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -9519,7 +9656,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:1.6.0": +"mime@npm:1.6.0, mime@npm:^1.4.1": version: 1.6.0 resolution: "mime@npm:1.6.0" bin: @@ -9766,6 +9903,18 @@ __metadata: languageName: node linkType: hard +"needle@npm:^3.1.0": + version: 3.3.1 + resolution: "needle@npm:3.3.1" + dependencies: + iconv-lite: "npm:^0.6.3" + sax: "npm:^1.2.4" + bin: + needle: bin/needle + checksum: 10c0/233b9315d47b735867d03e7a018fb665ee6cacf3a83b991b19538019cf42b538a3e85ca745c840b4c5e9a0ffdca76472f941363bf7c166214ae8cbc650fd4d39 + languageName: node + linkType: hard + "negotiator@npm:0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" @@ -10140,6 +10289,13 @@ __metadata: languageName: node linkType: hard +"parse-node-version@npm:^1.0.1": + version: 1.0.1 + resolution: "parse-node-version@npm:1.0.1" + checksum: 10c0/999cd3d7da1425c2e182dce82b226c6dc842562d3ed79ec47f5c719c32a7f6c1a5352495b894fc25df164be7f2ede4224758255da9902ddef81f2b77ba46bb2c + languageName: node + linkType: hard + "parse-statements@npm:1.0.11": version: 1.0.11 resolution: "parse-statements@npm:1.0.11" @@ -10370,6 +10526,13 @@ __metadata: languageName: node linkType: hard +"pify@npm:^4.0.1": + version: 4.0.1 + resolution: "pify@npm:4.0.1" + checksum: 10c0/6f9d404b0d47a965437403c9b90eca8bb2536407f03de165940e62e72c8c8b75adda5516c6b9b23675a5877cc0bcac6bdfb0ef0e39414cd2476d5495da40e7cf + languageName: node + linkType: hard + "pino-abstract-transport@npm:^2.0.0": version: 2.0.0 resolution: "pino-abstract-transport@npm:2.0.0" @@ -10668,6 +10831,24 @@ __metadata: languageName: node linkType: hard +"postcss-load-config@npm:^3.1.4": + version: 3.1.4 + resolution: "postcss-load-config@npm:3.1.4" + dependencies: + lilconfig: "npm:^2.0.5" + yaml: "npm:^1.10.2" + peerDependencies: + postcss: ">=8.0.9" + ts-node: ">=9.0.0" + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + checksum: 10c0/7d2cc6695c2fc063e4538316d651a687fdb55e48db453ff699de916a6ee55ab68eac2b120c28a6b8ca7aa746a588888351b810a215b5cd090eabea62c5762ede + languageName: node + linkType: hard + "postcss-logical@npm:^8.1.0": version: 8.1.0 resolution: "postcss-logical@npm:8.1.0" @@ -10686,6 +10867,39 @@ __metadata: languageName: node linkType: hard +"postcss-modules-extract-imports@npm:^3.0.0": + version: 3.1.0 + resolution: "postcss-modules-extract-imports@npm:3.1.0" + peerDependencies: + postcss: ^8.1.0 + checksum: 10c0/402084bcab376083c4b1b5111b48ec92974ef86066f366f0b2d5b2ac2b647d561066705ade4db89875a13cb175b33dd6af40d16d32b2ea5eaf8bac63bd2bf219 + languageName: node + linkType: hard + +"postcss-modules-local-by-default@npm:^4.0.4": + version: 4.2.0 + resolution: "postcss-modules-local-by-default@npm:4.2.0" + dependencies: + icss-utils: "npm:^5.0.0" + postcss-selector-parser: "npm:^7.0.0" + postcss-value-parser: "npm:^4.1.0" + peerDependencies: + postcss: ^8.1.0 + checksum: 10c0/b0b83feb2a4b61f5383979d37f23116c99bc146eba1741ca3cf1acca0e4d0dbf293ac1810a6ab4eccbe1ee76440dd0a9eb2db5b3bba4f99fc1b3ded16baa6358 + languageName: node + linkType: hard + +"postcss-modules-scope@npm:^3.1.1": + version: 3.2.1 + resolution: "postcss-modules-scope@npm:3.2.1" + dependencies: + postcss-selector-parser: "npm:^7.0.0" + peerDependencies: + postcss: ^8.1.0 + checksum: 10c0/bd2d81f79e3da0ef6365b8e2c78cc91469d05b58046b4601592cdeef6c4050ed8fe1478ae000a1608042fc7e692cb51fecbd2d9bce3f4eace4d32e883ffca10b + languageName: node + linkType: hard + "postcss-nesting@npm:^13.0.2": version: 13.0.2 resolution: "postcss-nesting@npm:13.0.2" @@ -10882,14 +11096,14 @@ __metadata: languageName: node linkType: hard -"postcss-value-parser@npm:^4.2.0": +"postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0": version: 4.2.0 resolution: "postcss-value-parser@npm:4.2.0" checksum: 10c0/f4142a4f56565f77c1831168e04e3effd9ffcc5aebaf0f538eee4b2d465adfd4b85a44257bb48418202a63806a7da7fe9f56c330aebb3cac898e46b4cbf49161 languageName: node linkType: hard -"postcss@npm:^8.5.6": +"postcss@npm:^8.0.0, postcss@npm:^8.4.35, postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -11043,6 +11257,13 @@ __metadata: languageName: node linkType: hard +"prr@npm:~1.0.1": + version: 1.0.1 + resolution: "prr@npm:1.0.1" + checksum: 10c0/5b9272c602e4f4472a215e58daff88f802923b84bc39c8860376bb1c0e42aaf18c25d69ad974bd06ec6db6f544b783edecd5502cd3d184748d99080d68e4be5f + languageName: node + linkType: hard + "pump@npm:^3.0.0": version: 3.0.0 resolution: "pump@npm:3.0.0" @@ -11759,6 +11980,13 @@ __metadata: languageName: node linkType: hard +"reserved-words@npm:^0.1.2": + version: 0.1.2 + resolution: "reserved-words@npm:0.1.2" + checksum: 10c0/88360388d88f4b36c1f5d47f8d596936dbf950bddd642c04ce940f1dab1fa58ef6fec23f5fab81a1bfe5cd0f223b2b635311496fcf0ef3db93ad4dfb6d7be186 + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -12068,7 +12296,7 @@ __metadata: languageName: node linkType: hard -"sass@npm:^1.62.1": +"sass@npm:^1.62.1, sass@npm:^1.70.0": version: 1.93.2 resolution: "sass@npm:1.93.2" dependencies: @@ -12085,6 +12313,20 @@ __metadata: languageName: node linkType: hard +"sax@npm:^1.2.4": + version: 1.4.1 + resolution: "sax@npm:1.4.1" + checksum: 10c0/6bf86318a254c5d898ede6bd3ded15daf68ae08a5495a2739564eb265cd13bcc64a07ab466fb204f67ce472bb534eb8612dac587435515169593f4fffa11de7c + languageName: node + linkType: hard + +"sax@npm:~1.3.0": + version: 1.3.0 + resolution: "sax@npm:1.3.0" + checksum: 10c0/599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea + languageName: node + linkType: hard + "saxes@npm:^6.0.0": version: 6.0.0 resolution: "saxes@npm:6.0.0" @@ -12128,6 +12370,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^5.6.0": + version: 5.7.2 + resolution: "semver@npm:5.7.2" + bin: + semver: bin/semver + checksum: 10c0/e4cf10f86f168db772ae95d86ba65b3fd6c5967c94d97c708ccb463b778c2ee53b914cd7167620950fc07faf5a564e6efe903836639e512a1aa15fbc9667fa25 + languageName: node + linkType: hard + "semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -12417,7 +12668,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": +"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf @@ -12448,13 +12699,20 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:~0.6.1": +"source-map@npm:^0.6.0, source-map@npm:~0.6.0, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 languageName: node linkType: hard +"source-map@npm:^0.7.3": + version: 0.7.6 + resolution: "source-map@npm:0.7.6" + checksum: 10c0/59f6f05538539b274ba771d2e9e32f6c65451982510564438e048bc1352f019c6efcdc6dd07909b1968144941c14015c2c7d4369fb7c4d7d53ae769716dcc16c + languageName: node + linkType: hard + "source-map@npm:^0.7.4": version: 0.7.4 resolution: "source-map@npm:0.7.4" @@ -13002,6 +13260,21 @@ __metadata: languageName: node linkType: hard +"stylus@npm:^0.62.0": + version: 0.62.0 + resolution: "stylus@npm:0.62.0" + dependencies: + "@adobe/css-tools": "npm:~4.3.1" + debug: "npm:^4.3.2" + glob: "npm:^7.1.6" + sax: "npm:~1.3.0" + source-map: "npm:^0.7.3" + bin: + stylus: bin/stylus + checksum: 10c0/62afe3a6d781f66d7d283e8218dc1a15530d7d89fc2f09457a723975b2073e96e0d32c61d7f0dd1bd2686aae4ab6cc6933dc85e1b72eebab8aa30167bd16962b + languageName: node + linkType: hard + "substring-trie@npm:^1.0.2": version: 1.0.2 resolution: "substring-trie@npm:1.0.2" @@ -13377,7 +13650,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.4.0, tslib@npm:^2.8.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -13518,6 +13791,35 @@ __metadata: languageName: node linkType: hard +"typescript-plugin-css-modules@npm:^5.2.0": + version: 5.2.0 + resolution: "typescript-plugin-css-modules@npm:5.2.0" + dependencies: + "@types/postcss-modules-local-by-default": "npm:^4.0.2" + "@types/postcss-modules-scope": "npm:^3.0.4" + dotenv: "npm:^16.4.2" + icss-utils: "npm:^5.1.0" + less: "npm:^4.2.0" + lodash.camelcase: "npm:^4.3.0" + postcss: "npm:^8.4.35" + postcss-load-config: "npm:^3.1.4" + postcss-modules-extract-imports: "npm:^3.0.0" + postcss-modules-local-by-default: "npm:^4.0.4" + postcss-modules-scope: "npm:^3.1.1" + reserved-words: "npm:^0.1.2" + sass: "npm:^1.70.0" + source-map-js: "npm:^1.0.2" + stylus: "npm:^0.62.0" + tsconfig-paths: "npm:^4.2.0" + peerDependencies: + typescript: ">=4.0.0" + dependenciesMeta: + stylus: + optional: true + checksum: 10c0/7cd024f7145c0a29d9b78f2fb49c42cdf1747b50a43391f9993132ba42a727266f9b544fd868d905d5352e0a8676a19ae7a9aa56d516cc819c3ab39d66aa25e4 + languageName: node + linkType: hard + "typescript@npm:^5.6.0, typescript@npm:~5.9.0": version: 5.9.2 resolution: "typescript@npm:5.9.2" @@ -14607,7 +14909,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^1.10.0": +"yaml@npm:^1.10.0, yaml@npm:^1.10.2": version: 1.10.2 resolution: "yaml@npm:1.10.2" checksum: 10c0/5c28b9eb7adc46544f28d9a8d20c5b3cb1215a886609a2fd41f51628d8aaa5878ccd628b755dbcd29f6bb4921bd04ffbc6dcc370689bb96e594e2f9813d2605f From 5bae08d1ff9962c2ab31400c06d1c264c8154460 Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 4 Nov 2025 17:32:52 +0100 Subject: [PATCH 15/18] Quote Posts: Add notifications for DMs and private posts (#36696) --- app/javascript/mastodon/actions/compose.js | 8 -- .../mastodon/actions/compose_typed.ts | 37 +++++++- .../compose/components/compose_form.jsx | 5 +- .../compose/components/visibility_button.tsx | 6 +- .../containers/compose_form_container.js | 12 ++- .../containers/privacy_dropdown_container.js | 5 +- .../confirmation_modal.tsx | 4 + .../private_quote_notify.tsx | 88 +++++++++++++++++++ .../confirmation_modals/styles.module.css | 7 ++ .../features/ui/components/modal_root.jsx | 2 + .../ui/components/visibility_modal.tsx | 20 ++++- app/javascript/mastodon/locales/en.json | 7 ++ app/javascript/mastodon/reducers/compose.js | 34 +++++-- .../styles/mastodon/components.scss | 28 ++++++ 14 files changed, 236 insertions(+), 27 deletions(-) create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/private_quote_notify.tsx create mode 100644 app/javascript/mastodon/features/ui/components/confirmation_modals/styles.module.css diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 2d4f48c20b..232c4b1c19 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -56,7 +56,6 @@ export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; -export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE'; export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE'; @@ -794,13 +793,6 @@ export function changeComposeSpoilerText(text) { }; } -export function changeComposeVisibility(value) { - return { - type: COMPOSE_VISIBILITY_CHANGE, - value, - }; -} - export function insertEmojiCompose(position, emoji, needsSpace) { return { type: COMPOSE_EMOJI_INSERT, diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts index 8088306028..03a973ec09 100644 --- a/app/javascript/mastodon/actions/compose_typed.ts +++ b/app/javascript/mastodon/actions/compose_typed.ts @@ -13,10 +13,10 @@ import { } from 'mastodon/store/typed_functions'; import type { ApiQuotePolicy } from '../api_types/quotes'; -import type { Status } from '../models/status'; +import type { Status, StatusVisibility } from '../models/status'; import { showAlert } from './alerts'; -import { focusCompose } from './compose'; +import { changeCompose, focusCompose } from './compose'; import { importFetchedStatuses } from './importer'; import { openModal } from './modal'; @@ -67,6 +67,39 @@ const simulateModifiedApiResponse = ( return data; }; +export const changeComposeVisibility = createAppThunk( + 'compose/visibility_change', + (visibility: StatusVisibility, { dispatch, getState }) => { + if (visibility !== 'direct') { + return visibility; + } + + const state = getState(); + const quotedStatusId = state.compose.get('quoted_status_id') as + | string + | null; + if (!quotedStatusId) { + return visibility; + } + + // Remove the quoted status + dispatch(quoteComposeCancel()); + const quotedStatus = state.statuses.get(quotedStatusId) as Status | null; + if (!quotedStatus) { + return visibility; + } + + // Append the quoted status URL to the compose text + const url = quotedStatus.get('url') as string; + const text = state.compose.get('text') as string; + if (!text.includes(url)) { + const newText = text.trim() ? `${text}\n\n${url}` : url; + dispatch(changeCompose(newText)); + } + return visibility; + }, +); + export const changeUploadCompose = createDataLoadingThunk( 'compose/changeUpload', async ( diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index 299de12e7e..770f776049 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -140,7 +140,10 @@ class ComposeForm extends ImmutablePureComponent { return; } - this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct'); + this.props.onSubmit({ + missingAltText: missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct', + quoteToPrivate: this.props.quoteToPrivate, + }); if (e) { e.preventDefault(); diff --git a/app/javascript/mastodon/features/compose/components/visibility_button.tsx b/app/javascript/mastodon/features/compose/components/visibility_button.tsx index 1ea504ab1a..d939405020 100644 --- a/app/javascript/mastodon/features/compose/components/visibility_button.tsx +++ b/app/javascript/mastodon/features/compose/components/visibility_button.tsx @@ -5,8 +5,10 @@ import { defineMessages, useIntl } from 'react-intl'; import classNames from 'classnames'; -import { changeComposeVisibility } from '@/mastodon/actions/compose'; -import { setComposeQuotePolicy } from '@/mastodon/actions/compose_typed'; +import { + changeComposeVisibility, + setComposeQuotePolicy, +} from '@/mastodon/actions/compose_typed'; import { openModal } from '@/mastodon/actions/modal'; import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes'; import type { StatusVisibility } from '@/mastodon/api_types/statuses'; diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 3dad46bc52..15b1c7cc41 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -12,6 +12,7 @@ import { } from 'mastodon/actions/compose'; import { pasteLinkCompose } from 'mastodon/actions/compose_typed'; import { openModal } from 'mastodon/actions/modal'; +import { PRIVATE_QUOTE_MODAL_ID } from 'mastodon/features/ui/components/confirmation_modals/private_quote_notify'; import ComposeForm from '../components/compose_form'; @@ -32,6 +33,10 @@ const mapStateToProps = state => ({ isUploading: state.getIn(['compose', 'is_uploading']), anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, missingAltText: state.getIn(['compose', 'media_attachments']).some(media => ['image', 'gifv'].includes(media.get('type')) && (media.get('description') ?? '').length === 0), + quoteToPrivate: + !!state.getIn(['compose', 'quoted_status_id']) + && state.getIn(['compose', 'privacy']) === 'private' + && !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]), isInReply: state.getIn(['compose', 'in_reply_to']) !== null, lang: state.getIn(['compose', 'language']), maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500), @@ -43,12 +48,17 @@ const mapDispatchToProps = (dispatch, props) => ({ dispatch(changeCompose(text)); }, - onSubmit (missingAltText) { + onSubmit ({ missingAltText, quoteToPrivate }) { if (missingAltText) { dispatch(openModal({ modalType: 'CONFIRM_MISSING_ALT_TEXT', modalProps: {}, })); + } else if (quoteToPrivate) { + dispatch(openModal({ + modalType: 'CONFIRM_PRIVATE_QUOTE_NOTIFY', + modalProps: {}, + })); } else { dispatch(submitCompose((status) => { if (props.redirectOnSuccess) { diff --git a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js index 6d3eef13aa..803dcb1a4a 100644 --- a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js @@ -1,8 +1,7 @@ import { connect } from 'react-redux'; -import { changeComposeVisibility } from '../../../actions/compose'; -import { openModal, closeModal } from '../../../actions/modal'; -import { isUserTouching } from '../../../is_mobile'; +import { changeComposeVisibility } from '@/mastodon/actions/compose_typed'; + import PrivacyDropdown from '../components/privacy_dropdown'; const mapStateToProps = state => ({ diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx index 47f9fca890..cfa50855a8 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx @@ -18,6 +18,7 @@ export const ConfirmationModal: React.FC< onSecondary?: () => void; onConfirm: () => void; closeWhenConfirm?: boolean; + extraContent?: React.ReactNode; } & BaseConfirmationModalProps > = ({ title, @@ -29,6 +30,7 @@ export const ConfirmationModal: React.FC< secondary, onSecondary, closeWhenConfirm = true, + extraContent, }) => { const handleClick = useCallback(() => { if (closeWhenConfirm) { @@ -49,6 +51,8 @@ export const ConfirmationModal: React.FC<

{title}

{message &&

{message}

} + + {extraContent}
diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/private_quote_notify.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/private_quote_notify.tsx new file mode 100644 index 0000000000..ef917a1027 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/private_quote_notify.tsx @@ -0,0 +1,88 @@ +import { forwardRef, useCallback, useState } from 'react'; + +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { submitCompose } from '@/mastodon/actions/compose'; +import { changeSetting } from '@/mastodon/actions/settings'; +import { CheckBox } from '@/mastodon/components/check_box'; +import { useAppDispatch } from '@/mastodon/store'; + +import { ConfirmationModal } from './confirmation_modal'; +import type { BaseConfirmationModalProps } from './confirmation_modal'; +import classes from './styles.module.css'; + +export const PRIVATE_QUOTE_MODAL_ID = 'quote/private_notify'; + +const messages = defineMessages({ + title: { + id: 'confirmations.private_quote_notify.title', + defaultMessage: 'Share with followers and mentioned users?', + }, + message: { + id: 'confirmations.private_quote_notify.message', + defaultMessage: + 'The person you are quoting and other mentions ' + + "will be notified and will be able to view your post, even if they're not following you.", + }, + confirm: { + id: 'confirmations.private_quote_notify.confirm', + defaultMessage: 'Publish post', + }, + cancel: { + id: 'confirmations.private_quote_notify.cancel', + defaultMessage: 'Back to editing', + }, +}); + +export const PrivateQuoteNotify = forwardRef< + HTMLDivElement, + BaseConfirmationModalProps +>( + ( + { onClose }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _ref, + ) => { + const intl = useIntl(); + + const [dismiss, setDismissed] = useState(false); + const handleDismissToggle = useCallback(() => { + setDismissed((prev) => !prev); + }, []); + + const dispatch = useAppDispatch(); + const handleConfirm = useCallback(() => { + dispatch(submitCompose()); + if (dismiss) { + dispatch( + changeSetting(['dismissed_banners', PRIVATE_QUOTE_MODAL_ID], true), + ); + } + }, [dismiss, dispatch]); + + return ( + + {' '} + + + } + /> + ); + }, +); +PrivateQuoteNotify.displayName = 'PrivateQuoteNotify'; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/styles.module.css b/app/javascript/mastodon/features/ui/components/confirmation_modals/styles.module.css new file mode 100644 index 0000000000..f685c4525f --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/styles.module.css @@ -0,0 +1,7 @@ +.checkbox_wrapper { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 1rem 0; + cursor: pointer; +} diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 944feb325e..ad5f16d94d 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -47,6 +47,7 @@ import MediaModal from './media_modal'; import { ModalPlaceholder } from './modal_placeholder'; import VideoModal from './video_modal'; import { VisibilityModal } from './visibility_modal'; +import { PrivateQuoteNotify } from './confirmation_modals/private_quote_notify'; export const MODAL_COMPONENTS = { 'MEDIA': () => Promise.resolve({ default: MediaModal }), @@ -66,6 +67,7 @@ export const MODAL_COMPONENTS = { 'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }), 'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }), 'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }), + 'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }), 'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }), 'CONFIRM_QUIET_QUOTE': () => Promise.resolve({ default: QuietPostQuoteInfoModal }), 'MUTE': MuteModal, diff --git a/app/javascript/mastodon/features/ui/components/visibility_modal.tsx b/app/javascript/mastodon/features/ui/components/visibility_modal.tsx index afd9ee7ed0..7bc7e0ab97 100644 --- a/app/javascript/mastodon/features/ui/components/visibility_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/visibility_modal.tsx @@ -128,9 +128,12 @@ export const VisibilityModal: FC = forwardRef( const disableVisibility = !!statusId; const disableQuotePolicy = visibility === 'private' || visibility === 'direct'; - const disablePublicVisibilities: boolean = useAppSelector( + const disablePublicVisibilities = useAppSelector( selectDisablePublicVisibilities, ); + const isQuotePost = useAppSelector( + (state) => state.compose.get('quoted_status_id') !== null, + ); const visibilityItems = useMemo[]>(() => { const items: SelectItem[] = [ @@ -315,6 +318,21 @@ export const VisibilityModal: FC = forwardRef( id={quoteDescriptionId} /> + + {isQuotePost && visibility === 'direct' && ( +
+ + +
+ )}