From adf8a3601de7869572bdd47857a65830124aaa0f Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 10 Dec 2025 17:59:21 +0100 Subject: [PATCH 01/20] Add service to add item to a collection (#37192) --- app/models/account.rb | 4 +++ app/models/collection_item.rb | 10 ++++++ app/policies/account_policy.rb | 4 +++ .../add_account_to_collection_service.rb | 23 ++++++++++++ config/locales/en.yml | 2 ++ spec/models/account_spec.rb | 33 +++++++++++++++++ spec/models/collection_item_spec.rb | 19 ++++++++++ spec/policies/account_policy_spec.rb | 32 +++++++++++++++++ .../add_account_to_collection_service_spec.rb | 35 +++++++++++++++++++ 9 files changed, 162 insertions(+) create mode 100644 app/services/add_account_to_collection_service.rb create mode 100644 spec/services/add_account_to_collection_service_spec.rb diff --git a/app/models/account.rb b/app/models/account.rb index 5f4caf9eaa..562d33508d 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -455,6 +455,10 @@ class Account < ApplicationRecord save! end + def featureable? + local? && discoverable? + end + private def prepare_contents diff --git a/app/models/collection_item.rb b/app/models/collection_item.rb index 093005fd3e..5c624165e6 100644 --- a/app/models/collection_item.rb +++ b/app/models/collection_item.rb @@ -32,6 +32,8 @@ class CollectionItem < ApplicationRecord validates :account, presence: true, if: :accepted? validates :object_uri, presence: true, if: -> { account.nil? } + before_validation :set_position, on: :create + scope :ordered, -> { order(position: :asc) } scope :with_accounts, -> { includes(account: [:account_stat, :user]) } scope :not_blocked_by, ->(account) { where.not(accounts: { id: account.blocking }) } @@ -39,4 +41,12 @@ class CollectionItem < ApplicationRecord def local_item_with_remote_account? local? && account&.remote? end + + private + + def set_position + return if position_changed? + + self.position = self.class.where(collection_id:).maximum(:position).to_i + 1 + end end diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index a744af81de..50fa9b4d5c 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -64,4 +64,8 @@ class AccountPolicy < ApplicationPolicy def review? role.can?(:manage_taxonomies) end + + def feature? + record.featureable? && !current_account.blocking?(record) && !record.blocking?(current_account) + end end diff --git a/app/services/add_account_to_collection_service.rb b/app/services/add_account_to_collection_service.rb new file mode 100644 index 0000000000..4477384dd0 --- /dev/null +++ b/app/services/add_account_to_collection_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class AddAccountToCollectionService + def call(collection, account) + raise ArgumentError unless collection.local? + + @collection = collection + @account = account + + raise Mastodon::NotPermittedError, I18n.t('accounts.errors.cannot_be_added_to_collections') unless AccountPolicy.new(@collection.account, @account).feature? + + create_collection_item + end + + private + + def create_collection_item + @collection.collection_items.create!( + account: @account, + state: :accepted + ) + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index f2b6de3a77..abce180d57 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -7,6 +7,8 @@ en: hosted_on: Mastodon hosted on %{domain} title: About accounts: + errors: + cannot_be_added_to_collections: This account cannot be added to collections. followers: one: Follower other: Followers diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index be400fecd4..89a2f78c2e 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -781,4 +781,37 @@ RSpec.describe Account do expect(subject.reload.followers_count).to eq 15 end end + + describe '#featureable?' do + subject { Fabricate.build(:account, domain: (local ? nil : 'example.com'), discoverable:) } + + context 'when account is local' do + let(:local) { true } + + context 'when account is discoverable' do + let(:discoverable) { true } + + it 'returns `true`' do + expect(subject.featureable?).to be true + end + end + + context 'when account is not discoverable' do + let(:discoverable) { false } + + it 'returns `false`' do + expect(subject.featureable?).to be false + end + end + end + + context 'when account is remote' do + let(:local) { false } + let(:discoverable) { true } + + it 'returns `false`' do + expect(subject.featureable?).to be false + end + end + end end diff --git a/spec/models/collection_item_spec.rb b/spec/models/collection_item_spec.rb index 39464b7a34..3592c75e66 100644 --- a/spec/models/collection_item_spec.rb +++ b/spec/models/collection_item_spec.rb @@ -38,4 +38,23 @@ RSpec.describe CollectionItem do it { is_expected.to validate_presence_of(:object_uri) } end end + + describe 'Creation' do + let(:collection) { Fabricate(:collection) } + let(:other_collection) { Fabricate(:collection) } + let(:account) { Fabricate(:account) } + let(:other_account) { Fabricate(:account) } + + it 'automatically sets the `position` if absent' do + first_item = collection.collection_items.create(account:) + second_item = collection.collection_items.create(account: other_account) + unrelated_item = other_collection.collection_items.create(account:) + custom_item = other_collection.collection_items.create(account: other_account, position: 7) + + expect(first_item.position).to eq 1 + expect(second_item.position).to eq 2 + expect(unrelated_item.position).to eq 1 + expect(custom_item.position).to eq 7 + end + end end diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb index 8b2edb15b0..f877bded25 100644 --- a/spec/policies/account_policy_spec.rb +++ b/spec/policies/account_policy_spec.rb @@ -156,4 +156,36 @@ RSpec.describe AccountPolicy do end end end + + permissions :feature? do + context 'when account is featureable?' do + it 'permits' do + expect(subject).to permit(alice, john) + end + end + + context 'when account is not featureable' do + before { allow(alice).to receive(:featureable?).and_return(false) } + + it 'denies' do + expect(subject).to_not permit(john, alice) + end + end + + context 'when account is blocked' do + before { alice.block!(john) } + + it 'denies' do + expect(subject).to_not permit(alice, john) + end + end + + context 'when account is blocking' do + before { john.block!(alice) } + + it 'denies' do + expect(subject).to_not permit(alice, john) + end + end + end end diff --git a/spec/services/add_account_to_collection_service_spec.rb b/spec/services/add_account_to_collection_service_spec.rb new file mode 100644 index 0000000000..98d88d0b8f --- /dev/null +++ b/spec/services/add_account_to_collection_service_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AddAccountToCollectionService do + subject { described_class.new } + + let(:collection) { Fabricate.create(:collection) } + + describe '#call' do + context 'when given a featurable account' do + let(:account) { Fabricate(:account) } + + it 'creates a new CollectionItem in the `accepted` state' do + expect do + subject.call(collection, account) + end.to change(collection.collection_items, :count).by(1) + + new_item = collection.collection_items.last + expect(new_item.state).to eq 'accepted' + expect(new_item.account).to eq account + end + end + + context 'when given an account that is not featureable' do + let(:account) { Fabricate(:account, discoverable: false) } + + it 'raises an error' do + expect do + subject.call(collection, account) + end.to raise_error(Mastodon::NotPermittedError) + end + end + end +end From da2b75bdcdeeefd91e03502789fac8b9a4e624c9 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 10 Dec 2025 18:01:25 +0100 Subject: [PATCH 02/20] Change `build-releases` workflow to tag images `latest` based on latest `stable-x.y` branch (#37179) Co-authored-by: emilweth <7402764+emilweth@users.noreply.github.com> --- .github/workflows/build-releases.yml | 42 ++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml index 245b25a934..d2136a2de7 100644 --- a/.github/workflows/build-releases.yml +++ b/.github/workflows/build-releases.yml @@ -9,7 +9,44 @@ permissions: packages: write jobs: + check-latest-stable: + runs-on: ubuntu-latest + outputs: + latest: ${{ steps.check.outputs.is_latest_stable }} + steps: + # Repository needs to be cloned to list branches + - name: Clone repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check latest stable + shell: bash + id: check + run: | + ref="${GITHUB_REF#refs/tags/}" + + if [[ "$ref" =~ ^v([0-9]+)\.([0-9]+)(\.[0-9]+)?$ ]]; then + current="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" + else + echo "tag $ref is not semver" + echo "is_latest_stable=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + latest=$(git for-each-ref --format='%(refname:short)' "refs/remotes/origin/stable-*.*" \ + | sed -E 's#^origin/stable-##' \ + | sort -Vr \ + | head -n1) + + if [[ "$current" == "$latest" ]]; then + echo "is_latest_stable=true" >> "$GITHUB_OUTPUT" + else + echo "is_latest_stable=false" >> "$GITHUB_OUTPUT" + fi + build-image: + needs: check-latest-stable uses: ./.github/workflows/build-container-image.yml with: file_to_build: Dockerfile @@ -21,13 +58,14 @@ jobs: # Only tag with latest when ran against the latest stable branch # This needs to be updated after each minor version release flavor: | - latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }} + latest=${{ needs.check-latest-stable.outputs.latest }} tags: | type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} secrets: inherit build-image-streaming: + needs: check-latest-stable uses: ./.github/workflows/build-container-image.yml with: file_to_build: streaming/Dockerfile @@ -39,7 +77,7 @@ jobs: # Only tag with latest when ran against the latest stable branch # This needs to be updated after each minor version release flavor: | - latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }} + latest=${{ needs.check-latest-stable.outputs.latest }} tags: | type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} From d1f690f50c8ab2dcab17b6a32c2c8f78c05b03d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:07:25 +0100 Subject: [PATCH 03/20] Update dependency stoplight to v5.7.0 (#37151) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8a94dd6075..e1b46ec36d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -839,7 +839,8 @@ GEM stackprof (0.2.27) starry (0.2.0) base64 - stoplight (5.6.0) + stoplight (5.7.0) + concurrent-ruby zeitwerk stringio (3.1.8) strong_migrations (2.5.1) From da1505a49575a2824c31c1ce37fe3332f493740e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:07:30 +0100 Subject: [PATCH 04/20] Update dependency @vitejs/plugin-react to v5.1.2 (#37155) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 753b25a660..aaf675e942 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3339,10 +3339,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-beta.47": - version: 1.0.0-beta.47 - resolution: "@rolldown/pluginutils@npm:1.0.0-beta.47" - checksum: 10c0/eb0cfa7334d66f090c47eaac612174936b05f26e789352428cb6e03575b590f355de30d26b42576ea4e613d8887b587119d19b2e4b3a8909ceb232ca1cf746c8 +"@rolldown/pluginutils@npm:1.0.0-beta.53": + version: 1.0.0-beta.53 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.53" + checksum: 10c0/e8b0a7eb76be22f6f103171f28072de821525a4e400454850516da91a7381957932ff0ce495f227bcb168e86815788b0c1d249ca9e34dca366a82c8825b714ce languageName: node linkType: hard @@ -4828,18 +4828,18 @@ __metadata: linkType: hard "@vitejs/plugin-react@npm:^5.0.0": - version: 5.1.1 - resolution: "@vitejs/plugin-react@npm:5.1.1" + version: 5.1.2 + resolution: "@vitejs/plugin-react@npm:5.1.2" dependencies: "@babel/core": "npm:^7.28.5" "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" - "@rolldown/pluginutils": "npm:1.0.0-beta.47" + "@rolldown/pluginutils": "npm:1.0.0-beta.53" "@types/babel__core": "npm:^7.20.5" react-refresh: "npm:^0.18.0" peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - checksum: 10c0/e590efaea1eabfbb1beb6e8c9fac0742fd299808e3368e63b2825ce24740adb8a28fcb2668b14b7ca1bdb42890cfefe94d02dd358dcbbf8a27ddf377b9a82abf + checksum: 10c0/d788f269cdf7474425071ba7c4ea7013f174ddaef12b758defe809a551a03ac62a4a80cd858872deb618e7936ccc7cffe178bc12b62e9c836a467e13f15b9390 languageName: node linkType: hard From 15c90887610f97b8eaf043113677ed0b0a3649f4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:07:38 +0100 Subject: [PATCH 05/20] Update dependency vite to v7.2.7 (#37156) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index aaf675e942..3d45bfc8e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14061,8 +14061,8 @@ __metadata: linkType: hard "vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.1.1": - version: 7.2.6 - resolution: "vite@npm:7.2.6" + version: 7.2.7 + resolution: "vite@npm:7.2.7" dependencies: esbuild: "npm:^0.25.0" fdir: "npm:^6.5.0" @@ -14111,7 +14111,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/d444a159ab8f0f854d596d1938f201b449d59ed4d336e587be9dc89005467214d85848c212c2495f76a8421372ffe4d061d023d659600f1aaa3ba5ac13e804f7 + checksum: 10c0/0c502d9eb898d9c05061dbd8fd199f280b524bbb4c12ab5f88c7b12779947386684a269e4dd0aa424aa35bcd857f1aa44aadb9ea764702a5043af433052455b5 languageName: node linkType: hard From d25f672c507ab2a56d37686f4f7eeb36f59a6099 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:07:52 +0100 Subject: [PATCH 06/20] Update dependency active_model_serializers to v0.10.16 (#37167) 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 e1b46ec36d..2486a0ab02 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,7 +53,7 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - active_model_serializers (0.10.15) + active_model_serializers (0.10.16) actionpack (>= 4.1) activemodel (>= 4.1) case_transform (>= 0.2) From 37d309bcaf6b72ddf6edd6be085bd6859855f6e9 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 11 Dec 2025 11:33:15 +0100 Subject: [PATCH 07/20] Fix Wrapstodon font loading by disabling inlining of fonts in Vite (#37198) --- vite.config.mts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vite.config.mts b/vite.config.mts index 3250e8f786..f27941e5a4 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -120,6 +120,8 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { manifest: true, outDir, assetsDir: 'assets', + assetsInlineLimit: (filePath, _) => + /\.woff2?$/.exec(filePath) ? false : undefined, rollupOptions: { input: await findEntrypoints(), output: { From fed26a41fa928e29e9ae531f4c8563dd667a238d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:33:47 +0100 Subject: [PATCH 08/20] Update dependency jsdom to v27.3.0 (#37165) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3d45bfc8e5..d884731937 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,10 +12,10 @@ __metadata: languageName: node linkType: hard -"@acemir/cssom@npm:^0.9.23": - version: 0.9.24 - resolution: "@acemir/cssom@npm:0.9.24" - checksum: 10c0/1c7bf8a61a74d9ecbc3b12fba697384461b3234441ed5a10f5c34aef91fdf4f1e3322fcd6659a8eaddd591eddc2259efd278212236100d90a6e16f77794d98bd +"@acemir/cssom@npm:^0.9.28": + version: 0.9.28 + resolution: "@acemir/cssom@npm:0.9.28" + checksum: 10c0/1e192d216c4236171d9930b42b9a965052d4578b23c6ddaa17c7c3d0820ffb872258544a83af163ae2d41b3bdccd6b6c4c14b2d32eb9f8b8b63972d74f46bd83 languageName: node linkType: hard @@ -46,29 +46,29 @@ __metadata: languageName: node linkType: hard -"@asamuzakjp/css-color@npm:^4.0.3": - version: 4.0.4 - resolution: "@asamuzakjp/css-color@npm:4.0.4" +"@asamuzakjp/css-color@npm:^4.1.0": + version: 4.1.0 + resolution: "@asamuzakjp/css-color@npm:4.1.0" dependencies: "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-color-parser": "npm:^3.0.10" + "@csstools/css-color-parser": "npm:^3.1.0" "@csstools/css-parser-algorithms": "npm:^3.0.5" "@csstools/css-tokenizer": "npm:^3.0.4" - lru-cache: "npm:^11.1.0" - checksum: 10c0/5a4eb3c8594f58f3df06c867a6cda4a33f702f5cd682d6afa5074813f16fd05e732653ac79bd6fc66390554e158ac478103ad5e885fd9cf154b69bb67639e82f + lru-cache: "npm:^11.2.2" + checksum: 10c0/097b9270a5befb765885dda43d6914ccbaa575565525d307e8ba3ba07f98e466062f4a77b9657e65160db9110c41c59cf5a6937479647f73637aeddf5c421579 languageName: node linkType: hard -"@asamuzakjp/dom-selector@npm:^6.7.4": - version: 6.7.5 - resolution: "@asamuzakjp/dom-selector@npm:6.7.5" +"@asamuzakjp/dom-selector@npm:^6.7.6": + version: 6.7.6 + resolution: "@asamuzakjp/dom-selector@npm:6.7.6" dependencies: "@asamuzakjp/nwsapi": "npm:^2.3.9" bidi-js: "npm:^1.0.3" css-tree: "npm:^3.1.0" is-potential-custom-element-name: "npm:^1.0.1" - lru-cache: "npm:^11.2.2" - checksum: 10c0/72ac4dc45aac9165222345aacc1db51a84094f159e9e2fa71bc89befd2d78fd00a76c7ff9a8a1ceb60e7ce198a4ec0275a4d878bea67b756cadbf3d9680162c4 + lru-cache: "npm:^11.2.4" + checksum: 10c0/1715faae0787f0c8430b3a0ff3db8576a5b9a4f964408d0808fc2060ab01e0c2f5d8e26409de54b8641433c891dab8b561b196e58798811146084c561a4954ce languageName: node linkType: hard @@ -1275,7 +1275,7 @@ __metadata: languageName: node linkType: hard -"@csstools/css-color-parser@npm:^3.0.10, @csstools/css-color-parser@npm:^3.1.0": +"@csstools/css-color-parser@npm:^3.1.0": version: 3.1.0 resolution: "@csstools/css-color-parser@npm:3.1.0" dependencies: @@ -1297,7 +1297,7 @@ __metadata: languageName: node linkType: hard -"@csstools/css-syntax-patches-for-csstree@npm:^1.0.14": +"@csstools/css-syntax-patches-for-csstree@npm:1.0.14": version: 1.0.14 resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.14" peerDependencies: @@ -6244,14 +6244,14 @@ __metadata: languageName: node linkType: hard -"cssstyle@npm:^5.3.3": - version: 5.3.3 - resolution: "cssstyle@npm:5.3.3" +"cssstyle@npm:^5.3.4": + version: 5.3.4 + resolution: "cssstyle@npm:5.3.4" dependencies: - "@asamuzakjp/css-color": "npm:^4.0.3" - "@csstools/css-syntax-patches-for-csstree": "npm:^1.0.14" + "@asamuzakjp/css-color": "npm:^4.1.0" + "@csstools/css-syntax-patches-for-csstree": "npm:1.0.14" css-tree: "npm:^3.1.0" - checksum: 10c0/0e082992851a1ded3662bda420f86dc1c90510a21cf237ddf573a1e121a722a3f78bb8f6eb46b33f267da25162e8e1fe968f7002114c9ab1d0d4e11dad9c5ee8 + checksum: 10c0/7499ea8cbc2f759ded275428e0811d147baa6a964a44577711cee5edabee2230cf76b6bd20a556603f99ebc6fff80afdcba6c00bcbb1d41ae50cd09cd9fe9a2d languageName: node linkType: hard @@ -8943,12 +8943,12 @@ __metadata: linkType: hard "jsdom@npm:^27.0.0": - version: 27.2.0 - resolution: "jsdom@npm:27.2.0" + version: 27.3.0 + resolution: "jsdom@npm:27.3.0" dependencies: - "@acemir/cssom": "npm:^0.9.23" - "@asamuzakjp/dom-selector": "npm:^6.7.4" - cssstyle: "npm:^5.3.3" + "@acemir/cssom": "npm:^0.9.28" + "@asamuzakjp/dom-selector": "npm:^6.7.6" + cssstyle: "npm:^5.3.4" data-urls: "npm:^6.0.0" decimal.js: "npm:^10.6.0" html-encoding-sniffer: "npm:^4.0.0" @@ -8971,7 +8971,7 @@ __metadata: peerDependenciesMeta: canvas: optional: true - checksum: 10c0/52d847e1aef099071d66d1d9aedcdd2f15e7ea781da9cfb41dc0d4caf741c5870c346396f8d1182d611427ae47a53f69a6f16410c698950e5809d3fed5a1672d + checksum: 10c0/b022ed8f6ce175afd97fbd42eb65b03b2be3b23df86cf87f018b6d2e757682fe8348e719a14780d6fa3fe8a65e531ba71b38db80f312818a32b77f01e31f267e languageName: node linkType: hard @@ -9378,10 +9378,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.2": - version: 11.2.2 - resolution: "lru-cache@npm:11.2.2" - checksum: 10c0/72d7831bbebc85e2bdefe01047ee5584db69d641c48d7a509e86f66f6ee111b30af7ec3bd68a967d47b69a4b1fa8bbf3872630bd06a63b6735e6f0a5f1c8e83d +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.2.2, lru-cache@npm:^11.2.4": + version: 11.2.4 + resolution: "lru-cache@npm:11.2.4" + checksum: 10c0/4a24f9b17537619f9144d7b8e42cd5a225efdfd7076ebe7b5e7dc02b860a818455201e67fbf000765233fe7e339d3c8229fc815e9b58ee6ede511e07608c19b2 languageName: node linkType: hard From d1b996b7e3b9611612dec68bb9335c37ef3e0353 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 12:12:07 +0100 Subject: [PATCH 09/20] Update dependency omniauth-rails_csrf_protection to v2.0.1 (#37199) 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 2486a0ab02..6b02122c97 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -481,7 +481,7 @@ GEM addressable (~> 2.8) nokogiri (~> 1.12) omniauth (~> 2.1) - omniauth-rails_csrf_protection (2.0.0) + omniauth-rails_csrf_protection (2.0.1) actionpack (>= 4.2) omniauth (~> 2.0) omniauth-saml (2.2.4) From 5651900b890ba5f1c36add6080b26640c5948086 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Thu, 11 Dec 2025 12:40:53 +0100 Subject: [PATCH 10/20] Wrapstodon design QA tweaks (#37201) --- .../status/intercept_status_clicks.tsx | 45 +++++++++++++ .../annual_report/highlighted_post.tsx | 26 +++++++- .../features/annual_report/index.module.scss | 64 ++++++++++++------- .../mastodon/features/annual_report/index.tsx | 7 +- .../features/annual_report/share_button.tsx | 23 +++++-- ...age.module.css => shared_page.module.scss} | 10 ++- .../features/annual_report/shared_page.tsx | 2 +- app/javascript/mastodon/locales/en.json | 1 + 8 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 app/javascript/mastodon/components/status/intercept_status_clicks.tsx rename app/javascript/mastodon/features/annual_report/{shared_page.module.css => shared_page.module.scss} (62%) diff --git a/app/javascript/mastodon/components/status/intercept_status_clicks.tsx b/app/javascript/mastodon/components/status/intercept_status_clicks.tsx new file mode 100644 index 0000000000..b0dbc3c693 --- /dev/null +++ b/app/javascript/mastodon/components/status/intercept_status_clicks.tsx @@ -0,0 +1,45 @@ +import { useCallback, useRef } from 'react'; + +export const InterceptStatusClicks: React.FC<{ + onPreventedClick: ( + clickedArea: 'account' | 'post', + event: React.MouseEvent, + ) => void; + children: React.ReactNode; +}> = ({ onPreventedClick, children }) => { + const wrapperRef = useRef(null); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + const clickTarget = e.target as Element; + const allowedElementsSelector = + '.video-player, .audio-player, .media-gallery, .content-warning'; + const allowedElements = wrapperRef.current?.querySelectorAll( + allowedElementsSelector, + ); + const isTargetClickAllowed = + allowedElements && + Array.from(allowedElements).some((element) => { + return clickTarget === element || element.contains(clickTarget); + }); + + if (!isTargetClickAllowed) { + e.preventDefault(); + e.stopPropagation(); + + const wasAccountAreaClicked = !!clickTarget.closest( + 'a.status__display-name', + ); + + onPreventedClick(wasAccountAreaClicked ? 'account' : 'post', e); + } + }, + [onPreventedClick], + ); + + return ( +
+ {children} +
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx index 2ff8597aa2..d6ca3d3f49 100644 --- a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx +++ b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx @@ -4,10 +4,14 @@ @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ +import type { ComponentPropsWithoutRef } from 'react'; +import { useCallback } from 'react'; + import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; +import { InterceptStatusClicks } from 'mastodon/components/status/intercept_status_clicks'; import { StatusQuoteManager } from 'mastodon/components/status_quoted'; import type { TopStatuses } from 'mastodon/models/annual_report'; import { makeGetStatus } from 'mastodon/selectors'; @@ -29,6 +33,24 @@ export const HighlightedPost: React.FC<{ statusId ? getStatus(state, { id: statusId }) : undefined, ); + const handleClick = useCallback< + ComponentPropsWithoutRef['onPreventedClick'] + >( + (clickedArea) => { + const link: string = + clickedArea === 'account' + ? status.getIn(['account', 'url']) + : status.get('url'); + + if (context === 'standalone') { + window.location.href = link; + } else { + window.open(link, '_blank'); + } + }, + [status, context], + ); + if (!status) { return
; } @@ -72,7 +94,9 @@ export const HighlightedPost: React.FC<{ {context === 'modal' &&

{label}

}
- + + + ); }; diff --git a/app/javascript/mastodon/features/annual_report/index.module.scss b/app/javascript/mastodon/features/annual_report/index.module.scss index 95ebb72729..7471e6282a 100644 --- a/app/javascript/mastodon/features/annual_report/index.module.scss +++ b/app/javascript/mastodon/features/annual_report/index.module.scss @@ -21,7 +21,8 @@ $mobile-breakpoint: 540px; scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary); @media (width < $mobile-breakpoint) { - padding-inline: 10px; + padding-top: 0; + padding-inline: 0; } .loading-indicator .circular-progress { @@ -50,37 +51,51 @@ $mobile-breakpoint: 540px; } .wrapper { + --gradient-strength: 0.4; + + box-sizing: border-box; position: relative; max-width: 600px; padding: 24px; + padding-top: 40px; contain: layout; flex: 0 0 auto; - pointer-events: auto; + pointer-events: all; color: var(--color-text-primary); background: var(--color-bg-primary); background: - radial-gradient(at 40% 87%, #240c9a99 0, transparent 50%), - radial-gradient(at 19% 10%, #6b0c9a99 0, transparent 50%), - radial-gradient(at 90% 27%, #9a0c8299 0, transparent 50%), - radial-gradient(at 16% 95%, #1e948299 0, transparent 50%), - radial-gradient(at 80% 91%, #16dae499 0, transparent 50%) + radial-gradient( + at 10% 27%, + rgba(83, 12, 154, var(--gradient-strength)) 0, + transparent 50% + ), + radial-gradient( + at 91% 10%, + rgba(30, 24, 223, var(--gradient-strength)) 0, + transparent 25% + ), + radial-gradient( + at 10% 91%, + rgba(22, 218, 228, var(--gradient-strength)) 0, + transparent 40% + ), + radial-gradient( + at 75% 87%, + rgba(37, 31, 217, var(--gradient-strength)) 0, + transparent 20% + ), + radial-gradient( + at 84% 60%, + rgba(95, 30, 148, var(--gradient-strength)) 0, + transparent 40% + ) var(--color-bg-primary); border-radius: 40px; @media (width < $mobile-breakpoint) { padding-inline: 12px; padding-bottom: 12px; - border-radius: 28px; - } - - &::after { - content: ''; - position: absolute; - inset: 0; - z-index: -1; - background: inherit; - border-radius: inherit; - filter: blur(20px); + border-radius: 0; } } @@ -92,7 +107,7 @@ $mobile-breakpoint: 540px; font-family: silkscreen-wrapstodon, monospace; font-size: 28px; line-height: 1; - margin-bottom: 8px; + margin-bottom: 4px; padding-inline: 40px; // Prevent overlap with close button @media (width < $mobile-breakpoint) { @@ -116,7 +131,7 @@ $mobile-breakpoint: 540px; .box { position: relative; - padding: 16px; + padding: 24px; border-radius: 16px; background: rgb(from var(--color-bg-primary) r g b / 60%); box-shadow: inset 0 0 0 1px rgb(from var(--color-text-primary) r g b / 40%); @@ -150,7 +165,6 @@ $mobile-breakpoint: 540px; flex-direction: column; justify-content: center; gap: 8px; - padding: 16px; font-size: 14px; text-align: center; text-wrap: balance; @@ -164,6 +178,10 @@ $mobile-breakpoint: 540px; text-transform: uppercase; color: #c2c8ff; font-weight: 500; + + &:last-child { + margin-bottom: -3px; + } } .statLarge { @@ -185,7 +203,7 @@ $mobile-breakpoint: 540px; .mostBoostedPost { padding: 0; - padding-top: 8px; + padding-top: 24px; overflow: hidden; } @@ -260,7 +278,7 @@ $mobile-breakpoint: 540px; display: flex; flex-direction: column; align-items: center; - gap: 12px; + gap: 16px; p { max-width: 460px; diff --git a/app/javascript/mastodon/features/annual_report/index.tsx b/app/javascript/mastodon/features/annual_report/index.tsx index 91fd02c7a7..7995caae87 100644 --- a/app/javascript/mastodon/features/annual_report/index.tsx +++ b/app/javascript/mastodon/features/annual_report/index.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import type { FC } from 'react'; -import { defineMessage, useIntl } from 'react-intl'; +import { useIntl } from 'react-intl'; import { useLocation } from 'react-router'; @@ -23,11 +23,6 @@ import { NewPosts } from './new_posts'; const moduleClassNames = classNames.bind(styles); -export const shareMessage = defineMessage({ - id: 'annual_report.summary.share_message', - defaultMessage: 'I got the {archetype} archetype!', -}); - export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({ context = 'standalone', }) => { diff --git a/app/javascript/mastodon/features/annual_report/share_button.tsx b/app/javascript/mastodon/features/annual_report/share_button.tsx index f9ad1d4e9c..80c2809adf 100644 --- a/app/javascript/mastodon/features/annual_report/share_button.tsx +++ b/app/javascript/mastodon/features/annual_report/share_button.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import type { FC } from 'react'; -import { useIntl } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import { resetCompose, focusCompose } from '@/mastodon/actions/compose'; import { closeModal } from '@/mastodon/actions/modal'; @@ -9,9 +9,19 @@ import { Button } from '@/mastodon/components/button'; import type { AnnualReport as AnnualReportData } from '@/mastodon/models/annual_report'; import { useAppDispatch } from '@/mastodon/store'; -import { shareMessage } from '.'; import { archetypeNames } from './archetype'; +const messages = defineMessages({ + share_message: { + id: 'annual_report.summary.share_message', + defaultMessage: 'I got the {archetype} archetype!', + }, + share_on_mastodon: { + id: 'annual_report.summary.share_on_mastodon', + defaultMessage: 'Share on Mastodon', + }, +}); + export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => { const intl = useIntl(); const dispatch = useAppDispatch(); @@ -21,7 +31,7 @@ export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => { archetypeNames[report.data.archetype], ); const shareLines = [ - intl.formatMessage(shareMessage, { + intl.formatMessage(messages.share_message, { archetype: archetypeName, }), ]; @@ -37,5 +47,10 @@ export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => { dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false })); }, [report, intl, dispatch]); - return