From 03b20bc0a4b12940602325a52fbcd50274b1118c Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 28 May 2026 03:19:52 -0400 Subject: [PATCH 01/34] Use `rescue_from` to handle missing status scenario in `NotificationMailer` (#38155) --- app/mailers/notification_mailer.rb | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index ecb3750968..db9218619a 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -19,17 +19,15 @@ class NotificationMailer < ApplicationMailer default to: -> { email_address_with_name(@user.email, @me.username) } + rescue_from(ActiveRecord::RecordNotFound) { false } + layout 'mailer' def mention - return if @status.blank? - mail subject: default_i18n_subject(name: @status.account.acct) end def quote - return if @status.blank? - mail subject: default_i18n_subject(name: @status.account.acct) end @@ -38,14 +36,10 @@ class NotificationMailer < ApplicationMailer end def favourite - return if @status.blank? - mail subject: default_i18n_subject(name: @account.acct) end def reblog - return if @status.blank? - mail subject: default_i18n_subject(name: @account.acct) end @@ -64,7 +58,7 @@ class NotificationMailer < ApplicationMailer end def set_status - @status = @notification.target_status + @status = @notification.target_status || raise(ActiveRecord::RecordNotFound) end def set_account From 4ba9421201a66be65d73efdc3df44dd42e1e1fd2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 12:30:01 +0200 Subject: [PATCH 02/34] Update dependency ws to v8.21.0 (#39151) 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 0246f9ce39..abdf900a38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16337,8 +16337,8 @@ __metadata: linkType: hard "ws@npm:^8.12.1, ws@npm:^8.18.0, ws@npm:^8.19.0, ws@npm:^8.20.0": - version: 8.20.1 - resolution: "ws@npm:8.20.1" + version: 8.21.0 + resolution: "ws@npm:8.21.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -16347,7 +16347,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10c0/ce162433218399cdedeb76fd33363d4d86a7d910058d4e3c679dce08cea65d6da6b39f11baa4d7808d024cf46ed88f6a05c17611621aaad8fc5e62edacc30c5d + checksum: 10c0/ef4a243476283fc49bc7550966c4af4aa0eef56273837211e700de3b664e08604a760cdddcb5ba43c049140e74ccfec5b0ee0bb439e08c2adf9138902fdde5f9 languageName: node linkType: hard From 080894529d19e3dc8842ca7933be9474134303c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 10:30:43 +0000 Subject: [PATCH 03/34] Update dependency ioredis to v5.11.0 (#39168) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 62 +++++++++++++++++++++---------------------------------- 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/yarn.lock b/yarn.lock index abdf900a38..3add265cd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2786,10 +2786,10 @@ __metadata: languageName: node linkType: hard -"@ioredis/commands@npm:1.5.1": - version: 1.5.1 - resolution: "@ioredis/commands@npm:1.5.1" - checksum: 10c0/cb8f6d13cff0753e3e7ef001fb895491985d9a623248192538f13bc2fd9bfdfde3c18cf2ba6f20ec8ceaa681b0771070d3a09b82eed044c798bcfef5e3ae54b3 +"@ioredis/commands@npm:1.10.0": + version: 1.10.0 + resolution: "@ioredis/commands@npm:1.10.0" + checksum: 10c0/baf91e62d0e64ef2b5f7ca4413dc2456fe250e87483beac4a1c8ef1fe5ad0d2fcdeb9b89d4556d8ef6c7455c64a964359d729601fdb06b2f4c76c35dd59afa99 languageName: node linkType: hard @@ -6900,10 +6900,10 @@ __metadata: languageName: node linkType: hard -"cluster-key-slot@npm:^1.1.0": - version: 1.1.2 - resolution: "cluster-key-slot@npm:1.1.2" - checksum: 10c0/d7d39ca28a8786e9e801eeb8c770e3c3236a566625d7299a47bb71113fb2298ce1039596acb82590e598c52dbc9b1f088c8f587803e697cb58e1867a95ff94d3 +"cluster-key-slot@npm:1.1.1": + version: 1.1.1 + resolution: "cluster-key-slot@npm:1.1.1" + checksum: 10c0/079b1ae86b20e2d53308a877b08de5e830722a45c07810569d0dab4955bed569da33ac9f79998289d014adf02cca7223a0647cb0ee6548a12ab3c4f9beac1377 languageName: node linkType: hard @@ -7319,7 +7319,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": +"debug@npm:4, debug@npm:4.4.3, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -7437,7 +7437,7 @@ __metadata: languageName: node linkType: hard -"denque@npm:^2.1.0": +"denque@npm:2.1.0": version: 2.1.0 resolution: "denque@npm:2.1.0" checksum: 10c0/f9ef81aa0af9c6c614a727cb3bd13c5d7db2af1abf9e6352045b86e85873e629690f6222f4edd49d10e4ccf8f078bbeec0794fafaf61b659c0589d0c511ec363 @@ -9546,19 +9546,17 @@ __metadata: linkType: hard "ioredis@npm:^5.3.2": - version: 5.10.1 - resolution: "ioredis@npm:5.10.1" + version: 5.11.0 + resolution: "ioredis@npm:5.11.0" dependencies: - "@ioredis/commands": "npm:1.5.1" - cluster-key-slot: "npm:^1.1.0" - debug: "npm:^4.3.4" - denque: "npm:^2.1.0" - lodash.defaults: "npm:^4.2.0" - lodash.isarguments: "npm:^3.1.0" - redis-errors: "npm:^1.2.0" - redis-parser: "npm:^3.0.0" - standard-as-callback: "npm:^2.1.0" - checksum: 10c0/d0507b52520d3bdd5dacaa33aed9dd3133794d8633b43a6b7fc3199a5e73f92cb77409f6904abe68e3221a95a630d97073b8c1c9e2c0c7613124db67e97c0eb0 + "@ioredis/commands": "npm:1.10.0" + cluster-key-slot: "npm:1.1.1" + debug: "npm:4.4.3" + denque: "npm:2.1.0" + redis-errors: "npm:1.2.0" + redis-parser: "npm:3.0.0" + standard-as-callback: "npm:2.1.0" + checksum: 10c0/6bba1eda256bafabf581089ec24c98bccc5af614b108f13fca6672ea707c36d67e7021c4f0965cbe0294e7a3964b6dbd897a95ed7f8fe82a175531219e91b84f languageName: node linkType: hard @@ -10552,20 +10550,6 @@ __metadata: languageName: node linkType: hard -"lodash.defaults@npm:^4.2.0": - version: 4.2.0 - resolution: "lodash.defaults@npm:4.2.0" - checksum: 10c0/d5b77aeb702caa69b17be1358faece33a84497bcca814897383c58b28a2f8dfc381b1d9edbec239f8b425126a3bbe4916223da2a576bb0411c2cefd67df80707 - languageName: node - linkType: hard - -"lodash.isarguments@npm:^3.1.0": - version: 3.1.0 - resolution: "lodash.isarguments@npm:3.1.0" - checksum: 10c0/5e8f95ba10975900a3920fb039a3f89a5a79359a1b5565e4e5b4310ed6ebe64011e31d402e34f577eca983a1fc01ff86c926e3cbe602e1ddfc858fdd353e62d8 - languageName: node - linkType: hard - "lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" @@ -13219,14 +13203,14 @@ __metadata: languageName: node linkType: hard -"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": +"redis-errors@npm:1.2.0, redis-errors@npm:^1.0.0": version: 1.2.0 resolution: "redis-errors@npm:1.2.0" checksum: 10c0/5b316736e9f532d91a35bff631335137a4f974927bb2fb42bf8c2f18879173a211787db8ac4c3fde8f75ed6233eb0888e55d52510b5620e30d69d7d719c8b8a7 languageName: node linkType: hard -"redis-parser@npm:^3.0.0": +"redis-parser@npm:3.0.0": version: 3.0.0 resolution: "redis-parser@npm:3.0.0" dependencies: @@ -14305,7 +14289,7 @@ __metadata: languageName: node linkType: hard -"standard-as-callback@npm:^2.1.0": +"standard-as-callback@npm:2.1.0": version: 2.1.0 resolution: "standard-as-callback@npm:2.1.0" checksum: 10c0/012677236e3d3fdc5689d29e64ea8a599331c4babe86956bf92fc5e127d53f85411c5536ee0079c52c43beb0026b5ce7aa1d834dd35dd026e82a15d1bcaead1f From 725b1964d2fff27244c3015824747dcce482994f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 10:32:34 +0000 Subject: [PATCH 04/34] Update dependency pg-connection-string to v2.13.0 (#39068) 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 3add265cd9..3882158e70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11921,9 +11921,9 @@ __metadata: linkType: hard "pg-connection-string@npm:^2.12.0, pg-connection-string@npm:^2.6.0": - version: 2.12.0 - resolution: "pg-connection-string@npm:2.12.0" - checksum: 10c0/3a26c62884a9f0464718f652bd5d6bce276ebda830c0fef4de4f88ae73c2507d70cae1d45c2f5b49bebd76187fb4c94f889d07c53fca6acd06b2eecbebcdc336 + version: 2.13.0 + resolution: "pg-connection-string@npm:2.13.0" + checksum: 10c0/870f83a8fca06d0340fc522653471d9c7081efbadf25c7f5801fcfb58104ef527138bb5d0546b21498ff4df75a742469622f657911a3b74034a1e94e59f34e31 languageName: node linkType: hard From ad821c8b74b041663aa5617bbcc70b03ff91ee90 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 10:36:32 +0000 Subject: [PATCH 05/34] Update dependency sidekiq to v8.1.6 (#39189) 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 333294b836..205dbed567 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -816,7 +816,7 @@ GEM securerandom (0.4.1) shoulda-matchers (7.0.1) activesupport (>= 7.1) - sidekiq (8.1.5) + sidekiq (8.1.6) connection_pool (>= 3.0.0) json (>= 2.16.0) logger (>= 1.7.0) From dabf28a421d34c4c11009b40cbdad2b2a0c5f451 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 10:40:27 +0000 Subject: [PATCH 06/34] Update dependency sidekiq-unique-jobs to v8.1.0 (#38468) 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 205dbed567..c82a3f443d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -827,7 +827,7 @@ GEM sidekiq-scheduler (6.0.2) rufus-scheduler (~> 3.2) sidekiq (>= 7.3, < 9) - sidekiq-unique-jobs (8.0.13) + sidekiq-unique-jobs (8.1.0) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 7.0.0, < 9.0.0) thor (>= 1.0, < 3.0) From 51fb5abeba976cc8fb6385df7358878184bdfcb6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 10:42:48 +0000 Subject: [PATCH 07/34] Update dependency net-http to '~> 0.9.0' (#36881) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index d4c1008a4f..d345cb1f4a 100644 --- a/Gemfile +++ b/Gemfile @@ -223,7 +223,7 @@ gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' -gem 'net-http', '~> 0.6.0' +gem 'net-http', '~> 0.9.0' gem 'rubyzip', '~> 3.0' gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index c82a3f443d..9c8e6f0abb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -455,8 +455,8 @@ GEM msgpack (1.8.0) multi_json (1.20.1) mutex_m (0.3.0) - net-http (0.6.0) - uri + net-http (0.9.1) + uri (>= 0.11.1) net-imap (0.6.4) date net-protocol @@ -1012,7 +1012,7 @@ DEPENDENCIES memory_profiler mime-types (~> 3.7.0) mutex_m - net-http (~> 0.6.0) + net-http (~> 0.9.0) net-ldap (~> 0.18) nokogiri (~> 1.15) omniauth (~> 2.0) From 161cea90c77b68a57b6f70909841e27c0352dde2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 11:46:57 +0000 Subject: [PATCH 08/34] Update Node.js to 24.16 (#39130) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index a2e33f6e2c..7858245567 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.15 +24.16 From 554b6cf35ef83d32469e1e35e915f3baf06c1b56 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 28 May 2026 15:35:23 -0400 Subject: [PATCH 09/34] Update playwright to version 1.60.0 (#39199) --- Gemfile | 2 +- Gemfile.lock | 4 ++-- package.json | 2 +- yarn.lock | 20 ++++++++++---------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index d345cb1f4a..8576937d2d 100644 --- a/Gemfile +++ b/Gemfile @@ -135,7 +135,7 @@ group :test do # Browser integration testing gem 'capybara', '~> 3.39' gem 'capybara-playwright-driver' - gem 'playwright-ruby-client', '1.59.1', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package + gem 'playwright-ruby-client', '1.60.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package # Used to reset the database between system tests gem 'database_cleaner-active_record' diff --git a/Gemfile.lock b/Gemfile.lock index 9c8e6f0abb..635d855569 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -598,7 +598,7 @@ GEM pg (1.6.3) pghero (3.8.0) activerecord (>= 7.2) - playwright-ruby-client (1.59.1) + playwright-ruby-client (1.60.0) base64 concurrent-ruby (>= 1.1.6) mime-types (>= 3.0) @@ -1040,7 +1040,7 @@ DEPENDENCIES parslet pg (~> 1.5) pghero - playwright-ruby-client (= 1.59.1) + playwright-ruby-client (= 1.60.0) premailer-rails prometheus_exporter (~> 2.2) propshaft diff --git a/package.json b/package.json index 76f4ee1c7a..6cebc2948a 100644 --- a/package.json +++ b/package.json @@ -180,7 +180,7 @@ "msw": "^2.12.1", "msw-storybook-addon": "^2.0.6", "oxfmt": "^0.47.0", - "playwright": "^1.57.0", + "playwright": "^1.60.0", "react-test-renderer": "^18.2.0", "storybook": "^10.3.0", "stylelint": "^17.0.0", diff --git a/yarn.lock b/yarn.lock index 3882158e70..eec215751e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3007,7 +3007,7 @@ __metadata: msw-storybook-addon: "npm:^2.0.6" oxfmt: "npm:^0.47.0" path-complete-extname: "npm:^1.0.0" - playwright: "npm:^1.57.0" + playwright: "npm:^1.60.0" postcss-preset-env: "npm:^11.0.0" prop-types: "npm:^15.8.1" punycode: "npm:^2.3.0" @@ -12105,27 +12105,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.59.1": - version: 1.59.1 - resolution: "playwright-core@npm:1.59.1" +"playwright-core@npm:1.60.0": + version: 1.60.0 + resolution: "playwright-core@npm:1.60.0" bin: playwright-core: cli.js - checksum: 10c0/d41a74d9681ce3beb3d5239e9ed577710b4ad099a6ca2476219c6599d51e9cb4b80bd72ed82c528da6a5d929c18ae3b872cf02bb83f78fa1c2cb9199c501abee + checksum: 10c0/99ccd43923b6e9355e0723b7fe221e6326efd4687f8dafff951313662aea11db51f542a9c2122c704c445fb9baae1c9ec9fa6f895126bbddd9fe92313f6942c9 languageName: node linkType: hard -"playwright@npm:^1.57.0": - version: 1.59.1 - resolution: "playwright@npm:1.59.1" +"playwright@npm:^1.60.0": + version: 1.60.0 + resolution: "playwright@npm:1.60.0" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.59.1" + playwright-core: "npm:1.60.0" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/dfe38396e616e5c4f98825ce90037bb96e477c5a2bd9258a24854f8ce72a8a41427b19098863866f85aa0216e70287dd537c4438d761aca93995e31ae099c533 + checksum: 10c0/714ad76d85b4865d7e43c0012f9039800c1485373388973ed39d79339cee5ad467052d1e2f1eaeca107a1cb6e65342186a8578a4c3504853d84c3a691250d5db languageName: node linkType: hard From faa5944d4627f8d843ec63c7f95769b2fd1a2787 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Thu, 28 May 2026 21:52:49 +0200 Subject: [PATCH 10/34] Hydrate user-specific feature approval policy (#39194) --- app/lib/status_cache_hydrator.rb | 72 +++++++++++++++----------- spec/lib/status_cache_hydrator_spec.rb | 2 +- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb index 1f1184d42f..d5920530c6 100644 --- a/app/lib/status_cache_hydrator.rb +++ b/app/lib/status_cache_hydrator.rb @@ -5,42 +5,45 @@ class StatusCacheHydrator @status = status end - def hydrate(account_id, nested: false) + def hydrate(account_or_id, nested: false) + account = account_or_id.is_a?(Account) ? account_or_id : Account.find(account_or_id) + # The cache of the serialized hash is generated by the fan-out-on-write service payload = Rails.cache.fetch("fan-out/#{@status.id}") { InlineRenderer.render(@status, nil, :status) } # If we're delivering to the author who disabled the display of the application used to create the # status, we need to hydrate the application, since it was not rendered for the basic payload - payload[:application] = payload_application if payload[:application].nil? && @status.account_id == account_id + payload[:application] = payload_application if payload[:application].nil? && @status.account_id == account.id # We take advantage of the fact that some relationships can only occur with an original status, not # the reblog that wraps it, so we can assume that some values are always false if payload[:reblog] - hydrate_reblog_payload(payload, account_id, nested:) + hydrate_reblog_payload(payload, account, nested:) else - hydrate_non_reblog_payload(payload, account_id, nested:) + hydrate_non_reblog_payload(payload, account, nested:) end end private - def hydrate_non_reblog_payload(empty_payload, account_id, nested: false) + def hydrate_non_reblog_payload(empty_payload, account, nested: false) empty_payload.tap do |payload| - fill_status_payload(payload, @status, account_id, fresh: !nested, nested:) + fill_status_payload(payload, @status, account, fresh: !nested, nested:) end end - def hydrate_reblog_payload(empty_payload, account_id, nested: false) + def hydrate_reblog_payload(empty_payload, account, nested: false) empty_payload.tap do |payload| payload[:muted] = false payload[:bookmarked] = false - payload[:pinned] = false if @status.account_id == account_id + payload[:pinned] = false if @status.account_id == account.id # If the reblogged status is being delivered to the author who disabled the display of the application # used to create the status, we need to hydrate it here too - payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id + payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account.id - fill_status_payload(payload[:reblog], @status.reblog, account_id, fresh: false, nested:) + hydrate_account(payload[:account], account) + fill_status_payload(payload[:reblog], @status.reblog, account, fresh: false, nested:) payload[:filtered] = payload[:reblog][:filtered] payload[:favourited] = payload[:reblog][:favourited] @@ -49,36 +52,37 @@ class StatusCacheHydrator end end - def fill_status_payload(payload, status, account_id, nested: false, fresh: true) - payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: status.id) - payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: status.id) - payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: status.conversation_id) - payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id) - payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id - payload[:filtered] = mapped_applied_custom_filter(account_id, status) - # TODO: performance optimization by not loading `Account` twice - payload[:quote_approval][:current_user] = status.quote_policy_for_account(Account.find_by(id: account_id)) if payload[:quote_approval] - payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id, nested:) if payload[:quote] + def fill_status_payload(payload, status, account, nested: false, fresh: true) + payload[:favourited] = Favourite.exists?(account_id: account.id, status_id: status.id) + payload[:reblogged] = Status.exists?(account_id: account.id, reblog_of_id: status.id) + payload[:muted] = ConversationMute.exists?(account_id: account.id, conversation_id: status.conversation_id) + payload[:bookmarked] = Bookmark.exists?(account_id: account.id, status_id: status.id) + payload[:pinned] = StatusPin.exists?(account_id: account.id, status_id: status.id) if status.account_id == account.id + payload[:filtered] = mapped_applied_custom_filter(account, status) + payload[:quote_approval][:current_user] = status.quote_policy_for_account(account) if payload[:quote_approval] + payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account, nested:) if payload[:quote] if payload[:poll] if fresh # If the status is brand new, we don't need to look up votes in database - payload[:poll][:voted] = status.account_id == account_id + payload[:poll][:voted] = status.account_id == account.id payload[:poll][:own_votes] = [] - elsif status.account_id == account_id + elsif status.account_id == account.id payload[:poll][:voted] = true payload[:poll][:own_votes] = [] else - own_votes = PollVote.where(poll_id: status.poll_id, account_id: account_id).pluck(:choice) + own_votes = PollVote.where(poll_id: status.poll_id, account_id: account.id).pluck(:choice) payload[:poll][:voted] = !own_votes.empty? payload[:poll][:own_votes] = own_votes end end - payload[:card][:missing_attribution] = status.preview_card.unverified_author_account_id == account_id if payload[:card] + payload[:card][:missing_attribution] = status.preview_card.unverified_author_account_id == account.id if payload[:card] # Nested statuses are more likely to have a stale cache fill_status_stats(payload, status) if nested + + hydrate_account(payload[:account], account) end def fill_status_stats(payload, status) @@ -88,7 +92,7 @@ class StatusCacheHydrator payload[:quotes_count] = status.quotes_count end - def hydrate_quote_payload(empty_payload, quote, account_id, nested: false) + def hydrate_quote_payload(empty_payload, quote, account, nested: false) return unless quote&.acceptable? empty_payload.tap do |payload| @@ -100,14 +104,14 @@ class StatusCacheHydrator payload[nested ? :quoted_status_id : :quoted_status] = nil payload[:state] = 'deleted' else - filter_state = StatusFilter.new(quote.quoted_status, Account.find_by(id: account_id)).filter_state_for_quote + filter_state = StatusFilter.new(quote.quoted_status, account).filter_state_for_quote payload[:state] = filter_state || 'accepted' if filter_state == 'unauthorized' payload[nested ? :quoted_status_id : :quoted_status] = nil elsif nested payload[:quoted_status_id] = quote.quoted_status_id&.to_s else - payload[:quoted_status] = StatusCacheHydrator.new(quote.quoted_status).hydrate(account_id, nested: true) + payload[:quoted_status] = StatusCacheHydrator.new(quote.quoted_status).hydrate(account, nested: true) end end else @@ -116,9 +120,19 @@ class StatusCacheHydrator end end - def mapped_applied_custom_filter(account_id, status) + def hydrate_account(payload, account) + return unless Mastodon::Feature.collections_enabled? + return unless payload[:id] + + stale_account = Account.find_by(id: payload[:id]) + return if stale_account.nil? + + payload[:feature_approval][:current_user] = stale_account.feature_policy_for_account(account) + end + + def mapped_applied_custom_filter(account, status) CustomFilter - .apply_cached_filters(CustomFilter.cached_filters_for(account_id), status) + .apply_cached_filters(CustomFilter.cached_filters_for(account), status) .map { |filter| serialized_filter(filter) } end diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index 3eb781dfba..03e453c046 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -6,7 +6,7 @@ RSpec.describe StatusCacheHydrator do let(:status) { Fabricate(:status) } let(:account) { Fabricate(:account) } - describe '#hydrate' do + describe '#hydrate', feature: :collections do let(:compare_to_hash) { InlineRenderer.render(status, account, :status) } shared_examples 'shared behavior' do From 4101f567c5da9f388f48cfd59ea805646dd6159a Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 28 May 2026 22:13:56 +0200 Subject: [PATCH 11/34] Collection notification filtering (#39198) --- .../mastodon/actions/notification_groups.ts | 15 +- .../mastodon/actions/notifications.js | 3 + .../components/column_settings.jsx | 6 + .../components/column_settings_group.tsx | 162 ++++++++++++++++++ .../features/notifications_v2/filter_bar.tsx | 13 ++ app/javascript/mastodon/locales/en.json | 2 + app/javascript/mastodon/reducers/settings.js | 3 + .../mastodon/selectors/notifications.ts | 5 +- app/javascript/mastodon/selectors/settings.ts | 17 +- 9 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 app/javascript/mastodon/features/notifications/components/column_settings_group.tsx diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 2d03ef080f..eddd6a9300 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -36,9 +36,18 @@ function notificationTypeForFilter(type: NotificationType) { } function notificationTypeForQuickFilter(type: NotificationType) { - if (type === 'quoted_update') return 'update'; - else if (type === 'quote') return 'mention'; - else return type; + switch (type) { + case 'quoted_update': + return 'update'; + case 'quote': + return 'mention'; + case 'collection_update': + return 'collection'; + case 'added_to_collection': + return 'collection'; + default: + return type; + } } function excludeAllTypesExcept(filter: string) { diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index da0c5f1102..cb4cd1251c 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -86,6 +86,9 @@ export function setupBrowserNotifications() { }; } +/** + * @param {(NotificationPermission) => void} callback + */ export function requestBrowserPermission(callback = noOp) { return dispatch => { requestNotificationPermission((permission) => { diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index b1f4e59818..588af106b5 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -12,6 +12,7 @@ import ClearColumnButton from './clear_column_button'; import GrantPermissionButton from './grant_permission_button'; import { PolicyControls } from './policy_controls'; import SettingToggle from './setting_toggle'; +import { ColumnSettingsGroup } from './column_settings_group'; class ColumnSettings extends PureComponent { static propTypes = { @@ -187,6 +188,11 @@ class ColumnSettings extends PureComponent { + } + /> + {((this.props.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (

diff --git a/app/javascript/mastodon/features/notifications/components/column_settings_group.tsx b/app/javascript/mastodon/features/notifications/components/column_settings_group.tsx new file mode 100644 index 0000000000..f888488be3 --- /dev/null +++ b/app/javascript/mastodon/features/notifications/components/column_settings_group.tsx @@ -0,0 +1,162 @@ +import { useCallback } from 'react'; +import type { FC, ReactNode } from 'react'; + +import { defineMessages, FormattedMessage } from 'react-intl'; + +import { showAlert } from '@/mastodon/actions/alerts'; +import { requestBrowserPermission } from '@/mastodon/actions/notifications'; +import { changeAlerts } from '@/mastodon/actions/push_notifications'; +import { changeSetting } from '@/mastodon/actions/settings'; +import { + createAppSelector, + useAppDispatch, + useAppSelector, +} from '@/mastodon/store'; + +import SettingToggle from './setting_toggle'; + +const selectNotificationSettings = createAppSelector( + [ + (state) => + (state.settings as Immutable.Map).get( + 'notifications', + ) as Immutable.Map | boolean>, + (state) => state.notifications.get('browserPermission') as string, + (state) => state.push_notifications, + ], + (settings, browserPermission, pushSettings) => ({ + settings, + browserPermission: browserPermission !== 'denied', + pushSettings, + showPushSettings: + pushSettings.get('supported') && pushSettings.get('enabled'), + }), +); + +const messages = defineMessages({ + permissionDenied: { + id: 'notifications.permission_denied_alert', + defaultMessage: + "Desktop notifications can't be enabled, as browser permission has been denied before", + }, +}); + +type SettingsType = 'alerts' | 'shows' | 'sounds'; + +export const ColumnSettingsGroup: FC<{ label: ReactNode; type: string }> = ({ + label, + type, +}) => { + const { settings, browserPermission, pushSettings, showPushSettings } = + useAppSelector(selectNotificationSettings); + + const dispatch = useAppDispatch(); + const handleChange = useCallback( + (path: [SettingsType, string], checked: boolean) => { + if ( + path[0] === 'alerts' && + checked && + typeof window.Notification !== 'undefined' && + Notification.permission !== 'granted' + ) { + dispatch( + requestBrowserPermission((permission) => { + if (permission === 'granted') { + dispatch(changeSetting(['notifications', ...path], checked)); + } else { + dispatch(showAlert({ message: messages.permissionDenied })); + } + }), + ); + } else { + dispatch(changeSetting(['notifications', ...path], checked)); + } + }, + [dispatch], + ); + const handlePushChange = useCallback( + (path: string[], checked: boolean) => { + if ( + checked && + typeof window.Notification !== 'undefined' && + Notification.permission !== 'granted' + ) { + dispatch( + requestBrowserPermission((permission: NotificationPermission) => { + if (permission === 'granted') { + dispatch(changeAlerts(path, checked)); + } else { + dispatch(showAlert({ message: messages.permissionDenied })); + } + }), + ); + } else { + dispatch(changeAlerts(path, checked)); + } + }, + [dispatch], + ); + + return ( +
+

{label}

+ +
+ + } + /> + + {showPushSettings && ( + + } + /> + )} + + + } + /> + + + } + /> +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/notifications_v2/filter_bar.tsx b/app/javascript/mastodon/features/notifications_v2/filter_bar.tsx index 56067afa93..f955276182 100644 --- a/app/javascript/mastodon/features/notifications_v2/filter_bar.tsx +++ b/app/javascript/mastodon/features/notifications_v2/filter_bar.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import CollectionsIcon from '@/material-icons/400-24px/category.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; @@ -26,6 +27,10 @@ const tooltips = defineMessages({ boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, + collections: { + id: 'notifications.filter.collections', + defaultMessage: 'Collections', + }, statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow', @@ -124,6 +129,14 @@ export const FilterBar: React.FC = () => { > + + + ); else diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index b97260173b..f111e436b8 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "New reports:", "notifications.column_settings.admin.sign_up": "New sign-ups:", "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.collections": "Collections:", "notifications.column_settings.favourite": "Favorites:", "notifications.column_settings.filter_bar.advanced": "Display all categories", "notifications.column_settings.filter_bar.category": "Quick filter bar", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Edits:", "notifications.filter.all": "All", "notifications.filter.boosts": "Boosts", + "notifications.filter.collections": "Collections", "notifications.filter.favourites": "Favorites", "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 775996a48c..c10d350600 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -41,6 +41,7 @@ const initialState = ImmutableMap({ poll: false, status: false, update: false, + collections: false, 'admin.sign_up': false, 'admin.report': false, }), @@ -65,6 +66,7 @@ const initialState = ImmutableMap({ poll: true, status: true, update: true, + collections: true, 'admin.sign_up': true, 'admin.report': true, }), @@ -79,6 +81,7 @@ const initialState = ImmutableMap({ poll: true, status: true, update: true, + collections: true, 'admin.sign_up': true, 'admin.report': true, }), diff --git a/app/javascript/mastodon/selectors/notifications.ts b/app/javascript/mastodon/selectors/notifications.ts index 8c808a2dff..14111176ac 100644 --- a/app/javascript/mastodon/selectors/notifications.ts +++ b/app/javascript/mastodon/selectors/notifications.ts @@ -29,7 +29,10 @@ const filterNotificationsByAllowedTypes = ( (item) => item.type === 'gap' || allowedType === item.type || - (allowedType === 'mention' && item.type === 'quote'), + (allowedType === 'mention' && item.type === 'quote') || + (allowedType === 'collection' && + (item.type === 'collection_update' || + item.type === 'added_to_collection')), ); }; diff --git a/app/javascript/mastodon/selectors/settings.ts b/app/javascript/mastodon/selectors/settings.ts index ca34374167..49e0df0c40 100644 --- a/app/javascript/mastodon/selectors/settings.ts +++ b/app/javascript/mastodon/selectors/settings.ts @@ -17,10 +17,19 @@ export const selectSettingsNotificationsShows = createSelector( export const selectSettingsNotificationsExcludedTypes = createSelector( [selectSettingsNotificationsShows], - (shows) => - Object.entries(shows) - .filter(([_type, enabled]) => !enabled) - .map(([type, _enabled]) => type), + (shows) => { + const excludedTypes: string[] = []; + for (const key in shows) { + if (!shows[key]) { + if (key === 'collections') { + excludedTypes.push('collection_update', 'added_to_collection'); + } else { + excludedTypes.push(key); + } + } + } + return excludedTypes; + }, ); export const selectSettingsNotificationsQuickFilterShow = (state: RootState) => From 89a32c36654ab6a05a67b976f526d601885230f6 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 28 May 2026 16:14:12 -0400 Subject: [PATCH 12/34] Update jwt to version 2.10.3 (#39187) --- .bundler-audit.yml | 3 --- Gemfile.lock | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 .bundler-audit.yml diff --git a/.bundler-audit.yml b/.bundler-audit.yml deleted file mode 100644 index 4dae946991..0000000000 --- a/.bundler-audit.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -ignore: - - CVE-2026-45363 diff --git a/Gemfile.lock b/Gemfile.lock index 635d855569..e7c077a975 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -378,7 +378,7 @@ GEM addressable (~> 2.8) bigdecimal (>= 3.1, < 5) jsonapi-renderer (0.2.2) - jwt (2.10.2) + jwt (2.10.3) base64 kaminari (1.2.2) activesupport (>= 4.1.0) From 8a9ea06dee2d3a7f94e4f4c7094ec3594c83442a Mon Sep 17 00:00:00 2001 From: "Pia B." Date: Thu, 28 May 2026 22:14:31 +0200 Subject: [PATCH 13/34] fixes bug Admin Mailer trends mail not displayed correctly (#39122) --- app/mailers/admin_mailer.rb | 10 ++++-- config/sidekiq.yml | 2 +- spec/mailers/admin_mailer_spec.rb | 55 +++++++++++++++++++++++++++---- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index fe2325b6f3..dcf0ef5c7f 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -34,9 +34,13 @@ class AdminMailer < ApplicationMailer end def new_trends(links, tags, statuses) - @links = links - @tags = tags - @statuses = statuses + ActiveRecord::Associations::Preloader.new(records: [*links, *tags, *statuses], associations: [:trend]).call + + @links = links.filter { |link| link.trend.present? } + @tags = tags.filter { |tag| tag.trend.present? } + @statuses = statuses.filter { |status| status.trend.present? } + + return unless @links.any? || @tags.any? || @statuses.any? mail subject: default_i18n_subject(instance: @instance) end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 5beb95a3f8..ae30a5c011 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -17,7 +17,7 @@ class: Scheduler::ScheduledStatusesScheduler queue: scheduler trends_refresh_scheduler: - every: '5m' + every: ['5m', first_in: '4m'] class: Scheduler::Trends::RefreshScheduler queue: scheduler trends_review_notifications_scheduler: diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb index e71a8308bf..34ed2d4dfa 100644 --- a/spec/mailers/admin_mailer_spec.rb +++ b/spec/mailers/admin_mailer_spec.rb @@ -71,16 +71,22 @@ RSpec.describe AdminMailer do describe '.new_trends' do let(:recipient) { Fabricate(:account, username: 'Snurf') } - let(:link) { Fabricate(:preview_card, trendable: true, language: 'en') } - let(:status) { Fabricate(:status) } - let(:tag) { Fabricate(:tag) } - let(:mail) { described_class.with(recipient: recipient).new_trends([link], [tag], [status]) } + let!(:link) { Fabricate(:preview_card, trendable: true, language: 'en') } + let!(:status) { Fabricate(:status) } + let!(:tag) { Fabricate(:tag, display_name: 'Test Tag') } + let!(:other_tag) { Fabricate(:tag, display_name: 'Test Tag') } + let!(:another_tag) { Fabricate(:tag, display_name: 'Test Tag') } + let(:mail) { described_class.with(recipient: recipient).new_trends([link], [tag, other_tag, another_tag], [status]) } + let(:status_trend) { Fabricate(:status_trend, status: status, account: Fabricate(:account)) } + let(:tag_trend) { Fabricate(:tag_trend, tag: tag) } + let(:other_tag_trend) { Fabricate(:tag_trend, tag: other_tag) } + let(:preview_card_trend) { Fabricate(:preview_card_trend, preview_card: link) } before do - PreviewCardTrend.create!(preview_card: link) - StatusTrend.create!(status: status, account: Fabricate(:account)) - TagTrend.create!(tag: tag) recipient.user.update(locale: :en) + status_trend + tag_trend + preview_card_trend end it 'renders the email' do @@ -96,6 +102,41 @@ RSpec.describe AdminMailer do .and match(link.title) .and match(tag.display_name) end + + context 'when between queueing and sending trends gets deleted' do + let(:queue_mail) { described_class.with(recipient: recipient).new_trends([link], [tag, other_tag], [status]).deliver_later! } + + before do + recipient.user.update(locale: :en) + end + + it 'sends the email when all but one trends were deleted without the respective tag or status or link' do + other_tag_trend + expect(queue_mail.successfully_enqueued?).to be(true) + + TagTrend.delete_all + StatusTrend.delete_all + expect { queue_mail.perform_now }.to send_email( + to: recipient.user_email, + from: 'notifications@localhost', + subject: I18n.t('admin_mailer.new_trends.subject', instance: Rails.configuration.x.local_domain) + ) + expect(mail.body).to have_text(/The following items need a review before they can be displayed publicly/) + .and match(link.title) + expect(mail.body).to_not match(ActivityPub::TagManager.instance.url_for(status)) + expect(mail.body).to_not match(tag.display_name) + end + + it 'returns nil when no trends are present' do + expect(queue_mail.successfully_enqueued?).to be(true) + + TagTrend.delete_all + StatusTrend.delete_all + PreviewCardTrend.delete_all + + expect { queue_mail.perform_now }.to_not send_email + end + end end describe '.new_software_updates' do From 3d84865edb17884fb313db9337b4dc3500e8884d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 20:14:36 +0000 Subject: [PATCH 14/34] Update dependency vite to v8.0.14 (#39121) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 175 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 91 insertions(+), 84 deletions(-) diff --git a/yarn.lock b/yarn.lock index eec215751e..251cc538c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3684,10 +3684,10 @@ __metadata: languageName: node linkType: hard -"@oxc-project/types@npm:=0.130.0, @oxc-project/types@npm:^0.130.0": - version: 0.130.0 - resolution: "@oxc-project/types@npm:0.130.0" - checksum: 10c0/7ec8c03407b0bcb235b930c62859e6efcb3fe5cbaa5db98770d760df5c3e6b3e28a0ad22c2e35d1addede8065b40000c3822c5235dde2959af226639eb870000 +"@oxc-project/types@npm:=0.132.0": + version: 0.132.0 + resolution: "@oxc-project/types@npm:0.132.0" + checksum: 10c0/d0ca5e98be0b873d69e4f0f743eb35026833603dac11db9d55f2b5438251b381b886dc556fe3175a17b673f8e2073c49bde88d7e6e702aa09298c22b8b5504e1 languageName: node linkType: hard @@ -3705,6 +3705,13 @@ __metadata: languageName: node linkType: hard +"@oxc-project/types@npm:^0.130.0": + version: 0.130.0 + resolution: "@oxc-project/types@npm:0.130.0" + checksum: 10c0/7ec8c03407b0bcb235b930c62859e6efcb3fe5cbaa5db98770d760df5c3e6b3e28a0ad22c2e35d1addede8065b40000c3822c5235dde2959af226639eb870000 + languageName: node + linkType: hard + "@oxfmt/binding-android-arm-eabi@npm:0.47.0": version: 0.47.0 resolution: "@oxfmt/binding-android-arm-eabi@npm:0.47.0" @@ -4109,93 +4116,93 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-android-arm64@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-android-arm64@npm:1.0.1" +"@rolldown/binding-android-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-android-arm64@npm:1.0.2" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-darwin-arm64@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.1" +"@rolldown/binding-darwin-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-darwin-x64@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.1" +"@rolldown/binding-darwin-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rolldown/binding-freebsd-x64@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.1" +"@rolldown/binding-freebsd-x64@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.1" +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@rolldown/binding-linux-arm64-gnu@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.1" +"@rolldown/binding-linux-arm64-gnu@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.2" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-arm64-musl@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.1" +"@rolldown/binding-linux-arm64-musl@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.2" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.1" +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.2" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-s390x-gnu@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.1" +"@rolldown/binding-linux-s390x-gnu@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.2" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-x64-gnu@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.1" +"@rolldown/binding-linux-x64-gnu@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.2" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-x64-musl@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.1" +"@rolldown/binding-linux-x64-musl@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.2" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rolldown/binding-openharmony-arm64@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.1" +"@rolldown/binding-openharmony-arm64@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.2" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-wasm32-wasi@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.1" +"@rolldown/binding-wasm32-wasi@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.2" dependencies: "@emnapi/core": "npm:1.10.0" "@emnapi/runtime": "npm:1.10.0" @@ -4204,16 +4211,16 @@ __metadata: languageName: node linkType: hard -"@rolldown/binding-win32-arm64-msvc@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.1" +"@rolldown/binding-win32-arm64-msvc@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-win32-x64-msvc@npm:1.0.1": - version: 1.0.1 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.1" +"@rolldown/binding-win32-x64-msvc@npm:1.0.2": + version: 1.0.2 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -11074,12 +11081,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.11": - version: 3.3.11 - resolution: "nanoid@npm:3.3.11" +"nanoid@npm:^3.3.12": + version: 3.3.12 + resolution: "nanoid@npm:3.3.12" bin: nanoid: bin/nanoid.cjs - checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b + checksum: 10c0/ba142b7b39e11e80c16dd74b0365d407880c87c1cf7e1480956981ae940ee36060fa5b6f092cd1e315184dd19244c657bd017d03327bd3c62247d691c5e8edfb languageName: node linkType: hard @@ -12618,14 +12625,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.0.0, postcss@npm:^8.4.35, postcss@npm:^8.5.13, postcss@npm:^8.5.14": - version: 8.5.14 - resolution: "postcss@npm:8.5.14" +"postcss@npm:^8.0.0, postcss@npm:^8.4.35, postcss@npm:^8.5.13, postcss@npm:^8.5.15": + version: 8.5.15 + resolution: "postcss@npm:8.5.15" dependencies: - nanoid: "npm:^3.3.11" + nanoid: "npm:^3.3.12" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10c0/48138207cf5ef5581be1bfe2cb65ccfe0ac75e43888ba045afc8ed6043d7b56aeb3b9a9fe5b353ff554be943cd0cc15d826ccb991525159175971e5ee8ab0237 + checksum: 10c0/7f2e63ae22fbe43aace1bf652bd99da4e90737c64194d49e51ddc9cd0f9e51ff2861a7d734379b494deffa03a880a5c65eec70bc29ee9ebaa7136dde3eee8f31 languageName: node linkType: hard @@ -13507,26 +13514,26 @@ __metadata: languageName: node linkType: hard -"rolldown@npm:1.0.1": - version: 1.0.1 - resolution: "rolldown@npm:1.0.1" +"rolldown@npm:1.0.2": + version: 1.0.2 + resolution: "rolldown@npm:1.0.2" dependencies: - "@oxc-project/types": "npm:=0.130.0" - "@rolldown/binding-android-arm64": "npm:1.0.1" - "@rolldown/binding-darwin-arm64": "npm:1.0.1" - "@rolldown/binding-darwin-x64": "npm:1.0.1" - "@rolldown/binding-freebsd-x64": "npm:1.0.1" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.1" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.1" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.1" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.1" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.1" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.1" - "@rolldown/binding-linux-x64-musl": "npm:1.0.1" - "@rolldown/binding-openharmony-arm64": "npm:1.0.1" - "@rolldown/binding-wasm32-wasi": "npm:1.0.1" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.1" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.1" + "@oxc-project/types": "npm:=0.132.0" + "@rolldown/binding-android-arm64": "npm:1.0.2" + "@rolldown/binding-darwin-arm64": "npm:1.0.2" + "@rolldown/binding-darwin-x64": "npm:1.0.2" + "@rolldown/binding-freebsd-x64": "npm:1.0.2" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.2" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.2" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.2" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.2" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.2" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.2" + "@rolldown/binding-linux-x64-musl": "npm:1.0.2" + "@rolldown/binding-openharmony-arm64": "npm:1.0.2" + "@rolldown/binding-wasm32-wasi": "npm:1.0.2" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.2" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.2" "@rolldown/pluginutils": "npm:^1.0.0" dependenciesMeta: "@rolldown/binding-android-arm64": @@ -13560,8 +13567,8 @@ __metadata: "@rolldown/binding-win32-x64-msvc": optional: true bin: - rolldown: bin/cli.mjs - checksum: 10c0/0631c071874e1471c33923905061fa514fce2bd43c2e741adcddcaa4d9beaa2ba7a5d14af130d53753d838823e15b59f5acef7d24fb83ffb7aef15933b78e7d3 + rolldown: ./bin/cli.mjs + checksum: 10c0/628327a6e3122c0b62880f1c87d54095394e5138a6af2e6e7b2f67ef4c4b11f1421db68c9a5bb4e1be161465a863ab4f68f15076ce895cd4bb3d0ba18a3b20b1 languageName: node linkType: hard @@ -15730,14 +15737,14 @@ __metadata: linkType: hard "vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.0": - version: 8.0.13 - resolution: "vite@npm:8.0.13" + version: 8.0.14 + resolution: "vite@npm:8.0.14" dependencies: fsevents: "npm:~2.3.3" lightningcss: "npm:^1.32.0" picomatch: "npm:^4.0.4" - postcss: "npm:^8.5.14" - rolldown: "npm:1.0.1" + postcss: "npm:^8.5.15" + rolldown: "npm:1.0.2" tinyglobby: "npm:^0.2.16" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 @@ -15782,7 +15789,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/8f4d6fd30c3be710f76dba8ee7cd156902200e649884911cfa8e6e5f7ad4dd5b6933bdd4f0c46c0169c49ddce9ce1bfab6d395df9d176c0d959e3ba0e5ee54e4 + checksum: 10c0/1ff99b4daadc64aed5f9e40387ecf39fd3bca45c1a5c4fa4aa82197de901930f0507af8d75c54715e2744c99575913947efb625653a78ef6df3997c5613970bd languageName: node linkType: hard From cf6bf8eae5245cf5f657942c1e6fde7b14790019 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 20:37:32 +0000 Subject: [PATCH 15/34] Update dependency opentelemetry-instrumentation-rack to v0.31.1 (#39139) 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 e7c077a975..693228a4d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -539,7 +539,7 @@ GEM opentelemetry-instrumentation-active_support (~> 0.10) opentelemetry-instrumentation-active_support (0.12.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-base (0.26.0) + opentelemetry-instrumentation-base (0.26.1) opentelemetry-api (~> 1.7) opentelemetry-common (~> 0.21) opentelemetry-registry (~> 0.1) @@ -559,7 +559,7 @@ GEM opentelemetry-helpers-sql opentelemetry-helpers-sql-processor opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-rack (0.31.0) + opentelemetry-instrumentation-rack (0.31.1) opentelemetry-instrumentation-base (~> 0.25) opentelemetry-instrumentation-rails (0.42.0) opentelemetry-instrumentation-action_mailer (~> 0.7) From 2dde0179c5cb58b6910797c1083bce1a823b8dee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 20:37:42 +0000 Subject: [PATCH 16/34] Update dependency sass to v1.100.0 (#39141) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index 251cc538c2..ea4ef7f6eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6812,12 +6812,12 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^4.0.0": - version: 4.0.0 - resolution: "chokidar@npm:4.0.0" +"chokidar@npm:^5.0.0": + version: 5.0.0 + resolution: "chokidar@npm:5.0.0" dependencies: - readdirp: "npm:^4.0.1" - checksum: 10c0/42d03c53b0ad200689e4fae7763133561480561cab8ba5304e8f2298ff45ff84bf0f6065c3f02b9e557b74b156813734439a1a2ff19a1ea6b35692395cd92738 + readdirp: "npm:^5.0.0" + checksum: 10c0/42fc907cb2a7ff5c9e220f84dae75380a77997f851c2a5e7865a2cf9ae45dd407a23557208cdcdbf3ac8c93341135a1748e4c48c31855f3bfa095e5159b6bdec languageName: node linkType: hard @@ -13173,10 +13173,10 @@ __metadata: languageName: node linkType: hard -"readdirp@npm:^4.0.1": - version: 4.0.1 - resolution: "readdirp@npm:4.0.1" - checksum: 10c0/e5a0b547015f68ecc918f115b62b75b2b840611480a9240cb3317090a0ddac01bb9b40315a8fa08acdf52a43eea17b808c89b645263cba3ab64dc557d7f801f1 +"readdirp@npm:^5.0.0": + version: 5.0.0 + resolution: "readdirp@npm:5.0.0" + checksum: 10c0/faf1ec57cff2020f473128da3f8d2a57813cc3a08a36c38cae1c9af32c1579906cc50ba75578043b35bade77e945c098233665797cf9730ba3613a62d6e79219 languageName: node linkType: hard @@ -13781,11 +13781,11 @@ __metadata: linkType: hard "sass@npm:^1.62.1, sass@npm:^1.70.0": - version: 1.99.0 - resolution: "sass@npm:1.99.0" + version: 1.100.0 + resolution: "sass@npm:1.100.0" dependencies: "@parcel/watcher": "npm:^2.4.1" - chokidar: "npm:^4.0.0" + chokidar: "npm:^5.0.0" immutable: "npm:^5.1.5" source-map-js: "npm:>=0.6.2 <2.0.0" dependenciesMeta: @@ -13793,7 +13793,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: 10c0/83c54a8c6decb79fff50dd9500d7932cf1cb7c5d9be4bc42bd3d537402c37bbee062aea6efdbdf9fb0b8697b18177d60c72bf101872336b93b1c27a2dc3621e1 + checksum: 10c0/e2aab47c87b69d2d4f8e48fa665138548069f56a7fd0fc4e15c9bde888b715798e49d33436e873918a8849ca3cc6c141a68618f58e2f3b2e6ec179cc309ca622 languageName: node linkType: hard From fd92d33d712b73721e8bc198e4e47d6286e120de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 22:38:03 +0200 Subject: [PATCH 17/34] Update dependency react-redux to v9.3.0 (#39045) 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 ea4ef7f6eb..a7c35f775a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13018,8 +13018,8 @@ __metadata: linkType: hard "react-redux@npm:^9.0.4": - version: 9.2.0 - resolution: "react-redux@npm:9.2.0" + version: 9.3.0 + resolution: "react-redux@npm:9.3.0" dependencies: "@types/use-sync-external-store": "npm:^0.0.6" use-sync-external-store: "npm:^1.4.0" @@ -13032,7 +13032,7 @@ __metadata: optional: true redux: optional: true - checksum: 10c0/00d485f9d9219ca1507b4d30dde5f6ff8fb68ba642458f742e0ec83af052f89e65cd668249b99299e1053cc6ad3d2d8ac6cb89e2f70d2ac5585ae0d7fa0ef259 + checksum: 10c0/b9f4efcfbfbc90cac9d1709ab3affb1e18a9dc9bd3cceda43bd2e1d9d2394ee0c29df36ec2202d52b566db774888b594c8c5aa86b64f27ef34fca607c687c9e3 languageName: node linkType: hard From f436be941034206ad68dd6e58389c4fcf328a629 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 07:30:42 +0000 Subject: [PATCH 18/34] New Crowdin Translations (automated) (#39193) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/be.json | 2 + app/javascript/mastodon/locales/da.json | 2 + app/javascript/mastodon/locales/de.json | 2 + app/javascript/mastodon/locales/el.json | 2 + app/javascript/mastodon/locales/es-AR.json | 2 + app/javascript/mastodon/locales/es-MX.json | 6 +++ app/javascript/mastodon/locales/es.json | 6 +++ app/javascript/mastodon/locales/et.json | 26 +++++++---- app/javascript/mastodon/locales/ga.json | 6 +++ app/javascript/mastodon/locales/gl.json | 2 + app/javascript/mastodon/locales/it.json | 2 + app/javascript/mastodon/locales/nl.json | 2 + app/javascript/mastodon/locales/pt-BR.json | 54 +++++++++++----------- app/javascript/mastodon/locales/vi.json | 2 + app/javascript/mastodon/locales/zh-CN.json | 10 +++- app/javascript/mastodon/locales/zh-TW.json | 2 + config/locales/de.yml | 7 +++ config/locales/es-MX.yml | 11 +++++ config/locales/es.yml | 11 +++++ config/locales/et.yml | 17 +++++-- config/locales/ga.yml | 11 +++++ config/locales/lt.yml | 3 ++ config/locales/simple_form.lt.yml | 3 ++ config/locales/tr.yml | 7 +++ config/locales/zh-CN.yml | 11 +++++ 25 files changed, 170 insertions(+), 39 deletions(-) diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 1fff2a1fc7..dc2733bde7 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "Новыя скаргі:", "notifications.column_settings.admin.sign_up": "Новыя рэгістрацыі:", "notifications.column_settings.alert": "Апавяшчэнні на працоўным стале", + "notifications.column_settings.collections": "Калекцыі:", "notifications.column_settings.favourite": "Упадабанае:", "notifications.column_settings.filter_bar.advanced": "Паказаць усе катэгорыі", "notifications.column_settings.filter_bar.category": "Панэль хуткай фільтрацыі", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Праўкі:", "notifications.filter.all": "Усе", "notifications.filter.boosts": "Пашырэнні", + "notifications.filter.collections": "Калекцыі", "notifications.filter.favourites": "Упадабанае", "notifications.filter.follows": "Падпісаны на", "notifications.filter.mentions": "Згадванні", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index c9e86a3ae3..0b3604ef68 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "Nye rapporteringer:", "notifications.column_settings.admin.sign_up": "Nye tilmeldinger:", "notifications.column_settings.alert": "Computernotifikationer", + "notifications.column_settings.collections": "Samlinger:", "notifications.column_settings.favourite": "Favoritter:", "notifications.column_settings.filter_bar.advanced": "Vis alle kategorier", "notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Redigeringer:", "notifications.filter.all": "Alle", "notifications.filter.boosts": "Fremhævelser", + "notifications.filter.collections": "Samlinger", "notifications.filter.favourites": "Favoritter", "notifications.filter.follows": "Følger", "notifications.filter.mentions": "Omtaler", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 76767a271e..1637ae9df3 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "Neue Meldungen:", "notifications.column_settings.admin.sign_up": "Neue Registrierungen:", "notifications.column_settings.alert": "Desktop-Benachrichtigungen", + "notifications.column_settings.collections": "Sammlungen:", "notifications.column_settings.favourite": "Favoriten:", "notifications.column_settings.filter_bar.advanced": "Alle Filterkategorien anzeigen", "notifications.column_settings.filter_bar.category": "Filterleiste", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Überarbeitete Beiträge:", "notifications.filter.all": "Alles", "notifications.filter.boosts": "Geteilte Beiträge", + "notifications.filter.collections": "Sammlungen", "notifications.filter.favourites": "Favoriten", "notifications.filter.follows": "Neue Follower", "notifications.filter.mentions": "Erwähnungen", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 745e23c335..aac2f5b6d0 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "Νέες αναφορές:", "notifications.column_settings.admin.sign_up": "Νέες εγγραφές:", "notifications.column_settings.alert": "Ειδοποιήσεις για υπολογιστή", + "notifications.column_settings.collections": "Συλλογές:", "notifications.column_settings.favourite": "Αγαπημένα:", "notifications.column_settings.filter_bar.advanced": "Εμφάνιση όλων των κατηγοριών", "notifications.column_settings.filter_bar.category": "Μπάρα γρήγορου φίλτρου", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Επεξεργασίες:", "notifications.filter.all": "Όλες", "notifications.filter.boosts": "Προωθήσεις", + "notifications.filter.collections": "Συλλογές", "notifications.filter.favourites": "Αγαπημένα", "notifications.filter.follows": "Ακολουθείς", "notifications.filter.mentions": "Επισημάνσεις", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 4cf1086871..a3de4c997b 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "Nuevas denuncias:", "notifications.column_settings.admin.sign_up": "Nuevos registros:", "notifications.column_settings.alert": "Notificaciones de escritorio", + "notifications.column_settings.collections": "Colecciones:", "notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías", "notifications.column_settings.filter_bar.category": "Barra de filtrado rápido", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Ediciones:", "notifications.filter.all": "Todas", "notifications.filter.boosts": "Adhesiones", + "notifications.filter.collections": "Colecciones", "notifications.filter.favourites": "Favoritos", "notifications.filter.follows": "Seguidores", "notifications.filter.mentions": "Menciones", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 2d9d8493d6..e8b8e2deec 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -584,6 +584,12 @@ "copy_icon_button.copy_this_text": "Copiar enlace al portapapeles", "copypaste.copied": "Copiado", "copypaste.copy_to_clipboard": "Copiar al portapapeles", + "custom_homepage.about": "Acerca de", + "custom_homepage.about_this_server": "Acerca de este servidor", + "custom_homepage.administered_by": "Administrado por", + "custom_homepage.contact": "Contacto:", + "custom_homepage.latest_activity": "Actividad más reciente", + "custom_homepage.these_are_the_latest_posts": "Estas son las últimas 40 publicaciones de las cuentas de este servidor.", "directory.federated": "Desde el fediverso conocido", "directory.local": "Sólo de {domain}", "directory.new_arrivals": "Recién llegados", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index e685ccf1c8..50142cb2fd 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -584,6 +584,12 @@ "copy_icon_button.copy_this_text": "Copiar enlace al portapapeles", "copypaste.copied": "Copiado", "copypaste.copy_to_clipboard": "Copiar al portapapeles", + "custom_homepage.about": "Acerca de", + "custom_homepage.about_this_server": "Acerca de este servidor", + "custom_homepage.administered_by": "Administrado por", + "custom_homepage.contact": "Contacto:", + "custom_homepage.latest_activity": "Actividad más reciente", + "custom_homepage.these_are_the_latest_posts": "Estas son las últimas 40 publicaciones de las cuentas de este servidor.", "directory.federated": "Desde el fediverso conocido", "directory.local": "Solo desde {domain}", "directory.new_arrivals": "Recién llegados", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 959cdcba59..c545e3a2b5 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -584,6 +584,12 @@ "copy_icon_button.copy_this_text": "Kopeeri link lõikelauale", "copypaste.copied": "Kopeeritud", "copypaste.copy_to_clipboard": "Kopeeri vahemällu", + "custom_homepage.about": "Teave", + "custom_homepage.about_this_server": "Teave koduserveri kohta", + "custom_homepage.administered_by": "Seda haldab", + "custom_homepage.contact": "Kontakt:", + "custom_homepage.latest_activity": "Viimane tegevus", + "custom_homepage.these_are_the_latest_posts": "Need on selle serveri kasutajate viimased 40 postitust.", "directory.federated": "Tuntud födiversumist", "directory.local": "Ainult domeenilt {domain}", "directory.new_arrivals": "Uustulijad", @@ -775,17 +781,17 @@ "home.pending_critical_update.link": "Vaata uuendusi", "home.pending_critical_update.title": "Saadaval kriitiline turvauuendus!", "home.show_announcements": "Kuva teadaandeid", - "ignore_notifications_modal.disclaimer": "Mastodon ei saa teavitada kasutajaid, et ignoreerisid nende teavitusi. Teavituste ignoreerimine ei peata sõnumite endi saatmist.", + "ignore_notifications_modal.disclaimer": "Mastodon ei saa teavitada kasutajaid, et eirasid nende teavitusi. Teavituste eiramine ei peata sõnumite endi saatmist.", "ignore_notifications_modal.filter_instead": "Selle asemel filtreeri", "ignore_notifications_modal.filter_to_act_users": "Saad endiselt kasutajaid vastu võtta, tagasi lükata või neist teatada", "ignore_notifications_modal.filter_to_avoid_confusion": "Filtreerimine aitab vältida võimalikke segaminiajamisi", "ignore_notifications_modal.filter_to_review_separately": "Saad filtreeritud teateid eraldi vaadata", "ignore_notifications_modal.ignore": "Ignoreeri teavitusi", - "ignore_notifications_modal.limited_accounts_title": "Ignoreeri modereeritud kontode teavitusi?", - "ignore_notifications_modal.new_accounts_title": "Ignoreeri uute kontode teavitusi?", - "ignore_notifications_modal.not_followers_title": "Ignoreeri inimeste teavitusi, kes sind ei jälgi?", - "ignore_notifications_modal.not_following_title": "Ignoreeri inimeste teavitusi, keda sa ei jälgi?", - "ignore_notifications_modal.private_mentions_title": "Ignoreeri soovimatute eraviisiliste mainimiste teateid?", + "ignore_notifications_modal.limited_accounts_title": "Kas eirad modereeritud kontode teavitusi?", + "ignore_notifications_modal.new_accounts_title": "Kas eirad uute kontode teavitusi?", + "ignore_notifications_modal.not_followers_title": "Kas eirad teavitusi kasutajatelt, kes sind ei jälgi?", + "ignore_notifications_modal.not_following_title": "Kas eirad teavitusi kasutajatelt, keda sa ei jälgi?", + "ignore_notifications_modal.private_mentions_title": "Kas eirad soovimatute eraviisiliste mainimiste teavitusi?", "info_button.label": "Abi", "info_button.what_is_alt_text": "

Mis on alt-tekst?

Alt-tekst pakub pildi kirjeldust nägemispuudega inimeste jaoks või neile, kel on aeglane internet või neile, kes otsivad lisaselgitust

Saad parandada ligipääsetavust ja mõistmist kõigi jaoks, kirjutades selge, lühida ja objektiivse alt-teksti.

  • Lisa tähtsad elemendid
  • Tee pildil olevast tekstist kokkuvõte
  • Kasuta reeglipärast lausestruktuuri
  • Väldi ebaolulist infot
  • Keskendu keerukate vaadete puhul (näiteks diagrammid ja kaardid) puhul trendidele ja põhiseostele
", "interaction_modal.action": "Suhestumaks kasutaja {name} postitusega palun logi sisse oma Mastodoni kasutajakontoga sõltumata serverist, mida kasutad.", @@ -879,7 +885,7 @@ "loading_indicator.label": "Laadimine…", "media_gallery.hide": "Peida", "moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.", - "mute_modal.hide_from_notifications": "Peida teavituste hulgast", + "mute_modal.hide_from_notifications": "Peida teavituste seast", "mute_modal.hide_options": "Peida valikud", "mute_modal.indefinite": "Kuni eemaldan neilt summutamise", "mute_modal.show_options": "Kuva valikud", @@ -908,6 +914,7 @@ "navigation_bar.live_feed_local": "Ajajoon reaalajas (sinu server)", "navigation_bar.live_feed_public": "Ajajoon reaalajas (Födiversum)", "navigation_bar.logout": "Logi välja", + "navigation_bar.main": "Esileht", "navigation_bar.moderation": "Modereerimine", "navigation_bar.more": "Lisavalikud", "navigation_bar.mutes": "Summutatud kasutajad", @@ -980,8 +987,8 @@ "notification_requests.dismiss_multiple": "{count, plural, one {Loobuda # taotlusest…} other {Loobuda # taotlusest…}}", "notification_requests.edit_selection": "Muuda", "notification_requests.exit_selection": "Valmis", - "notification_requests.explainer_for_limited_account": "Sellelt kontolt tulevad teavitused on filtreeritud, sest moderaator on seda kontot piiranud.", - "notification_requests.explainer_for_limited_remote_account": "Sellelt kontolt tulevad teavitused on filtreeritud, sest moderaator on seda kontot või serverit piiranud.", + "notification_requests.explainer_for_limited_account": "Selle kasutajakontoga seotud teavitused on filtreeritud, sest moderaator on selle konto tegevust piiranud.", + "notification_requests.explainer_for_limited_remote_account": "Selle kasutajakontoga seotud teavitused on filtreeritud, sest moderaator on selle konto tegevust või suhtlust andtu koduserveriga piiranud.", "notification_requests.maximize": "Maksimeeri", "notification_requests.minimize_banner": "Minimeeri filtreeritud teavituste bänner", "notification_requests.notifications_from": "Teavitus kasutajalt {name}", @@ -1305,6 +1312,7 @@ "tabs_bar.menu": "Menüü", "tabs_bar.notifications": "Teated", "tabs_bar.publish": "Uus postitus", + "tabs_bar.quick_links": "Kiirlingid", "tabs_bar.search": "Otsing", "tag.remove": "Eemalda", "terms_of_service.effective_as_of": "Kehtib alates {date}", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index ae4ec98b68..a4f3b08f3c 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -584,6 +584,12 @@ "copy_icon_button.copy_this_text": "Cóipeáil nasc chuig an ghearrthaisce", "copypaste.copied": "Cóipeáilte", "copypaste.copy_to_clipboard": "Cóipeáil chuig an ngearrthaisce", + "custom_homepage.about": "Maidir", + "custom_homepage.about_this_server": "Maidir leis an bhfreastalaí seo", + "custom_homepage.administered_by": "Arna riar ag", + "custom_homepage.contact": "Déan teagmháil le:", + "custom_homepage.latest_activity": "An ghníomhaíocht is déanaí", + "custom_homepage.these_are_the_latest_posts": "Seo iad na 40 postáil is déanaí ó chuntais ar an bhfreastalaí seo.", "directory.federated": "Ó chomhchruinne aitheanta", "directory.local": "Ó {domain} amháin", "directory.new_arrivals": "Daoine atá tar éis teacht", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 9cbf31fe1f..ec102c2b68 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "Novas denuncias:", "notifications.column_settings.admin.sign_up": "Novas usuarias:", "notifications.column_settings.alert": "Notificacións de escritorio", + "notifications.column_settings.collections": "Coleccións:", "notifications.column_settings.favourite": "Favoritas:", "notifications.column_settings.filter_bar.advanced": "Mostrar todas as categorías", "notifications.column_settings.filter_bar.category": "Barra de filtrado rápido", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Edicións:", "notifications.filter.all": "Todo", "notifications.filter.boosts": "Compartidos", + "notifications.filter.collections": "Coleccións", "notifications.filter.favourites": "Favoritas", "notifications.filter.follows": "Seguimentos", "notifications.filter.mentions": "Mencións", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 080e25da4e..e723f68c12 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "Nuove segnalazioni:", "notifications.column_settings.admin.sign_up": "Nuove iscrizioni:", "notifications.column_settings.alert": "Notifiche desktop", + "notifications.column_settings.collections": "Collezioni:", "notifications.column_settings.favourite": "Preferiti:", "notifications.column_settings.filter_bar.advanced": "Mostra tutte le categorie", "notifications.column_settings.filter_bar.category": "Barra del filtro veloce", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Modifiche:", "notifications.filter.all": "Tutti", "notifications.filter.boosts": "Condivisioni", + "notifications.filter.collections": "Collezioni", "notifications.filter.favourites": "Preferiti", "notifications.filter.follows": "Seguaci", "notifications.filter.mentions": "Menzioni", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index e298d75842..696b927044 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "Nieuwe rapportages:", "notifications.column_settings.admin.sign_up": "Nieuwe registraties:", "notifications.column_settings.alert": "Desktopmeldingen", + "notifications.column_settings.collections": "Verzamelingen:", "notifications.column_settings.favourite": "Favorieten:", "notifications.column_settings.filter_bar.advanced": "Alle categorieën tonen", "notifications.column_settings.filter_bar.category": "Snelle filterbalk", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Bewerkingen:", "notifications.filter.all": "Alles", "notifications.filter.boosts": "Boosts", + "notifications.filter.collections": "Verzamelingen", "notifications.filter.favourites": "Favorieten", "notifications.filter.follows": "Nieuwe volgers", "notifications.filter.mentions": "Vermeldingen", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 778740e274..51dc45c340 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -715,22 +715,22 @@ "follow_suggestions.curated_suggestion": "Escolha da equipe", "follow_suggestions.dismiss": "Não mostrar novamente", "follow_suggestions.featured_longer": "Escolhido à mão pela equipe de {domain}", - "follow_suggestions.friends_of_friends_longer": "Popular entre as pessoas que você segue", - "follow_suggestions.hints.featured": "Este perfil foi escolhido a dedo pela equipe {domain}.", - "follow_suggestions.hints.friends_of_friends": "Este perfil é popular entre as pessoas que você segue.", + "follow_suggestions.friends_of_friends_longer": "Em alta entre as pessoas que você segue", + "follow_suggestions.hints.featured": "Este perfil foi escolhido à mão pela equipe de {domain}.", + "follow_suggestions.hints.friends_of_friends": "Este perfil está em alta entre as pessoas que você segue.", "follow_suggestions.hints.most_followed": "Este perfil é um dos mais seguidos em {domain}.", - "follow_suggestions.hints.most_interactions": "Este perfil tem recebido recentemente muita atenção em {domain}.", + "follow_suggestions.hints.most_interactions": "Este perfil recentemente ganhou bastante atenção em {domain}.", "follow_suggestions.hints.similar_to_recently_followed": "Este perfil é semelhante aos perfis que você seguiu recentemente.", "follow_suggestions.personalized_suggestion": "Sugestão personalizada", - "follow_suggestions.popular_suggestion": "Sugestão popular", - "follow_suggestions.popular_suggestion_longer": "Popular em {domain}", - "follow_suggestions.similar_to_recently_followed_longer": "Similar a perfis que você seguiu recentemente", - "follow_suggestions.view_all": "Visualizar tudo", + "follow_suggestions.popular_suggestion": "Sugestão em alta", + "follow_suggestions.popular_suggestion_longer": "Em alta em {domain}", + "follow_suggestions.similar_to_recently_followed_longer": "Semelhante aos perfis que você seguiu recentemente", + "follow_suggestions.view_all": "Ver tudo", "follow_suggestions.who_to_follow": "Quem seguir", "followed_tags": "Hashtags seguidas", - "followers.hide_other_followers": "Este usuário escolheu não deixar visíveis seus seguidores", - "followers.title": "Seguinte {name}", - "following.hide_other_following": "Este usuário escolheu não deixar visíveis aqueles a quem segue", + "followers.hide_other_followers": "O usuário decidiu não deixar seus seguidores visíveis", + "followers.title": "Seguindo {name}", + "following.hide_other_following": "O usuário decidiu não deixar quem eles seguem visível", "following.title": "Seguido por {name}", "footer.about": "Sobre", "footer.about_mastodon": "Sobre o Mastodon", @@ -740,13 +740,13 @@ "footer.get_app": "Baixe o app", "footer.keyboard_shortcuts": "Atalhos de teclado", "footer.privacy_policy": "Política de privacidade", - "footer.source_code": "Exibir código-fonte", + "footer.source_code": "Ver código-fonte", "footer.status": "Status", "footer.terms_of_service": "Termos de serviço", - "form_error.blank": "O espaço não pode estar em branco.", + "form_error.blank": "O campo não pode estar vazio.", "form_field.optional": "(opcional)", "getting_started.heading": "Primeiros passos", - "hashtag.admin_moderation": "Abrir interface de moderação para #{name}", + "hashtag.admin_moderation": "Abrir menu de moderação para #{name}", "hashtag.browse": "Buscar publicações em #{hashtag}", "hashtag.browse_from_account": "Buscar publicações de @{name} em #{hashtag}", "hashtag.column_header.tag_mode.all": "e {additional}", @@ -764,27 +764,27 @@ "hashtag.feature": "Destacar no perfil", "hashtag.follow": "Seguir hashtag", "hashtag.mute": "Silenciar #{hashtag}", - "hashtag.unfeature": "Não destacar no perfil", + "hashtag.unfeature": "Remover destaque", "hashtag.unfollow": "Parar de seguir hashtag", - "hashtags.and_other": "…e {count, plural, one {}other {outros #}}", - "hints.profiles.followers_may_be_missing": "Pode haver seguidores deste perfil faltando.", - "hints.profiles.follows_may_be_missing": "Pode haver seguidos por este perfil faltando.", - "hints.profiles.posts_may_be_missing": "É possível que algumas publicações deste perfil estejam faltando.", - "hints.profiles.see_more_followers": "Ver mais seguidores no {domain}", - "hints.profiles.see_more_follows": "Ver mais seguidos no {domain}", + "hashtags.and_other": "…e {count, plural, other {mais #}}", + "hints.profiles.followers_may_be_missing": "Alguns seguidores deste perfil podem estar faltando.", + "hints.profiles.follows_may_be_missing": "Alguns seguidos deste perfil podem estar faltando.", + "hints.profiles.posts_may_be_missing": "Algumas publicações deste perfil podem estar faltando.", + "hints.profiles.see_more_followers": "Ver mais seguidores em {domain}", + "hints.profiles.see_more_follows": "Ver mais perfis seguidos em {domain}", "hints.profiles.see_more_posts": "Ver mais publicações em {domain}", - "home.column_settings.show_quotes": "Mostrar citações", + "home.column_settings.show_quotes": "Exibir citações", "home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_replies": "Mostrar respostas", "home.hide_announcements": "Ocultar anúncios", - "home.pending_critical_update.body": "Por favor, atualize o seu servidor Mastodon o mais rápido possível!", + "home.pending_critical_update.body": "Por favor, atualize o servidor do Mastodon o mais rápido possível!", "home.pending_critical_update.link": "Ver atualizações", "home.pending_critical_update.title": "Atualização de segurança crítica disponível!", "home.show_announcements": "Mostrar anúncios", - "ignore_notifications_modal.disclaimer": "O Mastodon não pode informar aos usuários que você ignorou suas notificações. Ignorar notificações não impedirá que as próprias mensagens sejam enviadas.", + "ignore_notifications_modal.disclaimer": "O Mastodon não informa os usuários se você ignorar as notificações deles. Ignorar notificações não impedirá as mensagens de serem enviadas.", "ignore_notifications_modal.filter_instead": "Filtrar em vez disso", - "ignore_notifications_modal.filter_to_act_users": "Você ainda conseguirá aceitar, rejeitar ou denunciar usuários", - "ignore_notifications_modal.filter_to_avoid_confusion": "A filtragem ajuda a evitar confusão potencial", + "ignore_notifications_modal.filter_to_act_users": "Você ainda poderá aceitar, rejeitar ou denunciar", + "ignore_notifications_modal.filter_to_avoid_confusion": "Os filtros ajudam a evitar potenciais confusões", "ignore_notifications_modal.filter_to_review_separately": "Você pode rever notificações filtradas separadamente", "ignore_notifications_modal.ignore": "Ignorar notificações", "ignore_notifications_modal.limited_accounts_title": "Ignorar notificações de contas moderadas?", @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "Novas denúncias:", "notifications.column_settings.admin.sign_up": "Novas inscrições:", "notifications.column_settings.alert": "Notificações no computador", + "notifications.column_settings.collections": "Coleções:", "notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.filter_bar.advanced": "Exibir todas as categorias", "notifications.column_settings.filter_bar.category": "Barra de filtro rápido", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Editar:", "notifications.filter.all": "Tudo", "notifications.filter.boosts": "Impulsos", + "notifications.filter.collections": "Coleções", "notifications.filter.favourites": "Favoritos", "notifications.filter.follows": "Seguidores", "notifications.filter.mentions": "Menções", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 547ebd7a5f..ffc2c6edff 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -999,6 +999,7 @@ "notifications.column_settings.admin.report": "Báo cáo mới:", "notifications.column_settings.admin.sign_up": "Người mới tham gia:", "notifications.column_settings.alert": "Báo trên máy tính", + "notifications.column_settings.collections": "Gói khởi đầu:", "notifications.column_settings.favourite": "Lượt thích:", "notifications.column_settings.filter_bar.advanced": "Xếp theo từng loại thông báo", "notifications.column_settings.filter_bar.category": "Phân loại thông báo", @@ -1018,6 +1019,7 @@ "notifications.column_settings.update": "Sửa tút:", "notifications.filter.all": "Tất cả", "notifications.filter.boosts": "Đăng lại", + "notifications.filter.collections": "Gói khởi đầu", "notifications.filter.favourites": "Lượt thích", "notifications.filter.follows": "Người theo dõi mới", "notifications.filter.mentions": "Lượt nhắc đến", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 96f8ffbe95..7a45db8814 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -584,6 +584,12 @@ "copy_icon_button.copy_this_text": "复制链接到剪贴板", "copypaste.copied": "已复制", "copypaste.copy_to_clipboard": "复制到剪贴板", + "custom_homepage.about": "关于", + "custom_homepage.about_this_server": "关于此服务器", + "custom_homepage.administered_by": "管理者为", + "custom_homepage.contact": "联系方式:", + "custom_homepage.latest_activity": "最新动态", + "custom_homepage.these_are_the_latest_posts": "以下是来自此服务器账号的最新 40 条嘟文。", "directory.federated": "来自已知联邦宇宙", "directory.local": "仅来自 {domain}", "directory.new_arrivals": "新来者", @@ -908,7 +914,7 @@ "navigation_bar.live_feed_local": "实时动态(本站)", "navigation_bar.live_feed_public": "实时动态(公开)", "navigation_bar.logout": "退出登录", - "navigation_bar.main": "首页", + "navigation_bar.main": "主要", "navigation_bar.moderation": "审核", "navigation_bar.more": "更多", "navigation_bar.mutes": "已隐藏的用户", @@ -994,6 +1000,7 @@ "notifications.column_settings.admin.report": "新举报:", "notifications.column_settings.admin.sign_up": "新注册:", "notifications.column_settings.alert": "桌面通知", + "notifications.column_settings.collections": "收藏列表:", "notifications.column_settings.favourite": "喜欢:", "notifications.column_settings.filter_bar.advanced": "显示全部类别", "notifications.column_settings.filter_bar.category": "快速筛选栏", @@ -1013,6 +1020,7 @@ "notifications.column_settings.update": "编辑:", "notifications.filter.all": "全部", "notifications.filter.boosts": "转嘟", + "notifications.filter.collections": "收藏列表", "notifications.filter.favourites": "喜欢", "notifications.filter.follows": "关注", "notifications.filter.mentions": "提及", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 0793407f65..eecf0c51a6 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "新檢舉報告:", "notifications.column_settings.admin.sign_up": "新註冊帳號:", "notifications.column_settings.alert": "桌面通知", + "notifications.column_settings.collections": "收藏名單:", "notifications.column_settings.favourite": "最愛:", "notifications.column_settings.filter_bar.advanced": "顯示所有分類", "notifications.column_settings.filter_bar.category": "快速過濾器", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "編輯:", "notifications.filter.all": "全部", "notifications.filter.boosts": "轉嘟", + "notifications.filter.collections": "收藏名單", "notifications.filter.favourites": "最愛", "notifications.filter.follows": "跟隨的使用者", "notifications.filter.mentions": "提及", diff --git a/config/locales/de.yml b/config/locales/de.yml index 129a42708e..6ba465395d 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -956,7 +956,14 @@ de: disabled: Bestimmte Rolle erforderlich public: Alle landing_page: + hints: + about_html: Eine Seite mit der Beschreibung, den Kontaktinformationen, den Regeln und weiteren Informationen zu diesem Server. + local_feed_html: Ein Live-Feed mit den neuesten Beiträgen der Nutzer auf diesem Server. + overview_html: Eine Seite, die die Beschreibung deines Servers zusammen mit den neuesten lokalen Beiträgen der Nutzer zeigt. + trends_html: Eine Seite mit dem, was auf diesem Server gerade im Trend liegt. values: + about: Über-Seite + local_feed: Lokaler Live-Feed overview: Übersicht trends: Trendet registrations: diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index d5b34b7814..358e0e3291 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -955,6 +955,17 @@ es-MX: authenticated: Solo usuarios registrados disabled: Requerir un rol de usuario específico public: Todos + landing_page: + hints: + about_html: Una página con la descripción, la información de contacto, las normas y otros datos sobre este servidor. + local_feed_html: Una cronología en tiempo real con las publicaciones más recientes de los usuarios de este servidor. + overview_html: Una página que muestra la descripción de tu servidor junto con las publicaciones locales más recientes de los usuarios de este servidor. + trends_html: Una página que muestra lo que es popular en este servidor en este momento. + values: + about: Página sobre el servidor + local_feed: Cronología local + overview: Resumen + trends: En tendencia registrations: moderation_recommandation: "¡Por favor, asegúrate de contar con un equipo de moderación adecuado y activo antes de abrir el registro al público!" preamble: Controla quién puede crear una cuenta en tu servidor. diff --git a/config/locales/es.yml b/config/locales/es.yml index 44b7d0517a..2d96e9ce36 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -955,6 +955,17 @@ es: authenticated: Solo usuarios autenticados disabled: Requerir un rol de usuario específico public: Todos + landing_page: + hints: + about_html: Una página con la descripción, información de contacto, reglas y otra información relevante de este servidor. + local_feed_html: Una cronología con los mensajes más recientes de los usuarios en este servidor. + overview_html: Una página que muestra la descripción de tu servidor junto a las publicaciones locales más recientes de los usuarios en este servidor. + trends_html: Una página destacando lo que es popular en este servidor en este momento. + values: + about: Página sobre el servidor + local_feed: Cronología local + overview: Resumen + trends: En tendencia registrations: moderation_recommandation: Por favor, ¡asegúrate de tener un equipo de moderación adecuado y reactivo antes de abrir los registros a todo el mundo! preamble: Controla quién puede crear una cuenta en tu servidor. diff --git a/config/locales/et.yml b/config/locales/et.yml index 34227ceb48..1120d4db2e 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -279,8 +279,8 @@ et: destroy_unavailable_domain_html: "%{name} taastas edastamise domeeni %{target}" destroy_user_role_html: "%{name} kustutas %{target} rolli" destroy_username_block_html: "%{name} eemaldas kasutajanime reegli, milles sisaldub %{target}" - disable_2fa_user_html: "%{name} eemaldas kasutaja %{target} kahe etapise nõude" - disable_custom_emoji_html: "%{name} keelas emotikooni %{target}" + disable_2fa_user_html: "%{name} eemaldas kasutajalt %{target} kahefaktorilise autentimise nõude" + disable_custom_emoji_html: "%{name} keelas emoji %{target}" disable_relay_html: "%{name} eemaldas sõnumivahendusserveri kasutuselt: %{target}" disable_sign_in_token_auth_user_html: "%{name} keelas e-posti võtme abil autentimise %{target} jaoks" disable_user_html: "%{name} keelas %{target} sisenemise" @@ -295,7 +295,7 @@ et: reject_user_html: "%{name} lükkas %{target} liitumissoovi tagasi" remove_avatar_user_html: "%{name} eemaldas %{target} avatari" reopen_report_html: "%{name} taasavas raporti %{target}" - resend_user_html: "%{name} lähtestas %{target} kinnituskirja e-posti" + resend_user_html: "%{name} saatis kasutajale %{target} e-postiga uue kinnituskirja" reset_password_user_html: "%{name} lähtestas %{target} kasutaja salasõna" resolve_report_html: "%{name} lahendas raporti %{target}" sensitive_account_html: "%{name} märkis %{target} meedia kui tundlik sisu" @@ -955,6 +955,17 @@ et: authenticated: Vaid autenditud kasutajad disabled: Eelda konkreetse kasutajarolli olemasolu public: Kõik + landing_page: + hints: + about_html: Leht kirjeldusega, kontaktiteabega, reeglitega muu olulisega selle koduserveri kohta. + local_feed_html: Sisuvoog viimaste selle serveri kasutajate postitustega. + overview_html: Leht, kus näidatakse selle serveri kirjeldust koos serveri kasutate viimaste postitustega. + trends_html: Leht, mis näitab selles serveris hetkel populaarseid teemasid. + values: + about: Serveri teabe leht + local_feed: Kohalik postituste voog + overview: Ülevaade + trends: Populaarsust koguv registrations: moderation_recommandation: Enne kõigi jaoks registreerimise avamist veendu, et oleks olemas adekvaatne ja reageerimisvalmis modereerijaskond! preamble: Kes saab serveril konto luua. diff --git a/config/locales/ga.yml b/config/locales/ga.yml index e0accf79cf..ceb9257108 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -1003,6 +1003,17 @@ ga: authenticated: Úsáideoirí fíordheimhnithe amháin disabled: Éiligh ról úsáideora sonrach public: Gach duine + landing_page: + hints: + about_html: Leathanach leis an gcur síos, eolas teagmhála, rialacha agus faisnéis eile maidir leis an bhfreastalaí seo. + local_feed_html: Fotha beo ina bhfuil na postálacha is déanaí ó úsáideoirí ar an bhfreastalaí seo. + overview_html: Leathanach a thaispeánann cur síos ar do fhreastalaí in éineacht leis na postálacha áitiúla is déanaí ó úsáideoirí ar an bhfreastalaí seo. + trends_html: Leathanach ar a bhfuil an-tóir ar an bhfreastalaí seo faoi láthair. + values: + about: Maidir le leathanach + local_feed: Beatha beo áitiúil + overview: Forbhreathnú + trends: Treocht registrations: moderation_recommandation: Cinntigh le do thoil go bhfuil foireann mhodhnóireachta imoibríoch leordhóthanach agat sula n-osclaíonn tú clárúcháin do gach duine! preamble: Rialú cé atá in ann cuntas a chruthú ar do fhreastalaí. diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 0f08842c90..ea6ccfbd5c 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -961,6 +961,9 @@ lt: your_appeal_rejected: Tavo apeliacija buvo atmesta edit_profile: other: Kita + email_subscription_mailer: + confirmation: + action: Patvirtinkite el. pašto adresą emoji_styles: auto: Automatinis native: Vietiniai diff --git a/config/locales/simple_form.lt.yml b/config/locales/simple_form.lt.yml index a027191c3f..4d5b72c7b0 100644 --- a/config/locales/simple_form.lt.yml +++ b/config/locales/simple_form.lt.yml @@ -148,6 +148,7 @@ lt: avatar: Profilio nuotrauka bot: Tai automatinė paskyra chosen_languages: Filtruoti kalbas + confirm_password: Patvirtink slaptažodį display_name: Rodomas vardas email: El. pašto adresas expires_in: Nustoja galioti po @@ -261,7 +262,9 @@ lt: jurisdiction: Teisinis teismingumas min_age: Mažiausias amžius user: + date_of_birth_1i: Metai date_of_birth_2i: Mėnuo + date_of_birth_3i: Diena role: Vaidmuo time_zone: Laiko juosta user_role: diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 7ec25d873e..81837e5b6c 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -956,9 +956,16 @@ tr: disabled: Belirli kullanıcı rolü gerekir public: Herkes landing_page: + hints: + about_html: Bu sunucuya ilişkin açıklama, iletişim bilgileri, kurallar ve diğer bilgilerin yer aldığı bir sayfa. + local_feed_html: Bu sunucudaki kullanıcıların en son paylaşımlarını içeren bir canlı akış. + overview_html: Sunucunuzun açıklamasını ve bu sunucudaki kullanıcıların en son yerel gönderilerini gösteren bir sayfa. + trends_html: Bu sunucuda şu anda popüler olanları gösteren bir sayfa. values: about: Hakkında sayfası + local_feed: Yerel canlı akış overview: Genel Bakış + trends: Öne çıkanlar registrations: moderation_recommandation: Lütfen kayıtları herkese açmadan önce yeterli ve duyarlı bir denetleyici ekibine sahip olduğunuzdan emin olun! preamble: Sunucunuzda kimin hesap oluşturabileceğini denetleyin. diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 8ecfbdc16f..e768ff4d9b 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -939,6 +939,17 @@ zh-CN: authenticated: 仅已登录用户 disabled: 需要特定的用户角色 public: 每个人 + landing_page: + hints: + about_html: 关于此服务器的描述、联系信息、规则及其他信息的页面。 + local_feed_html: 展示此服务器用户最新嘟文的实时动态。 + overview_html: 展示你的服务器描述及本站用户最新嘟文的页面。 + trends_html: 展示此服务器当前热门内容的页面。 + values: + about: 关于本站 + local_feed: 本站实时动态 + overview: 概览 + trends: 当前热门 registrations: moderation_recommandation: 在向每个人开放注册之前,请确保你拥有一个人手足够且反应迅速的管理团队! preamble: 控制谁可以在你的服务器上创建账号。 From f89ba969c2b585ca1f9c978b5c338f865f4d95d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 10:23:48 +0200 Subject: [PATCH 19/34] Update dependency aws-sdk-core to v3.250.0 (#39204) 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 693228a4d2..2a659eb3c2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,8 +99,8 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1253.0) - aws-sdk-core (3.249.0) + aws-partitions (1.1254.0) + aws-sdk-core (3.250.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) From 6d3182a6eb2de6998a832669f5a81c31512455ed Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 29 May 2026 10:39:51 +0200 Subject: [PATCH 20/34] Move Collections API to `v1` namespace (#39210) --- .../collection_items_controller.rb | 2 +- .../collections_controller.rb | 6 ++-- .../in_collections_controller.rb | 6 ++-- config/routes/api.rb | 18 ++++++++++-- lib/mastodon/version.rb | 2 +- .../{v1_alpha => v1}/collection_items_spec.rb | 0 .../api/{v1_alpha => v1}/collections_spec.rb | 28 +++++++++---------- .../{v1_alpha => v1}/in_collections_spec.rb | 12 ++++---- 8 files changed, 44 insertions(+), 30 deletions(-) rename app/controllers/api/{v1_alpha => v1}/collection_items_controller.rb (95%) rename app/controllers/api/{v1_alpha => v1}/collections_controller.rb (90%) rename app/controllers/api/{v1_alpha => v1}/in_collections_controller.rb (79%) rename spec/requests/api/{v1_alpha => v1}/collection_items_spec.rb (100%) rename spec/requests/api/{v1_alpha => v1}/collections_spec.rb (87%) rename spec/requests/api/{v1_alpha => v1}/in_collections_spec.rb (75%) diff --git a/app/controllers/api/v1_alpha/collection_items_controller.rb b/app/controllers/api/v1/collection_items_controller.rb similarity index 95% rename from app/controllers/api/v1_alpha/collection_items_controller.rb rename to app/controllers/api/v1/collection_items_controller.rb index 2c46cc4f9f..eaa46a4430 100644 --- a/app/controllers/api/v1_alpha/collection_items_controller.rb +++ b/app/controllers/api/v1/collection_items_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Api::V1Alpha::CollectionItemsController < Api::BaseController +class Api::V1::CollectionItemsController < Api::BaseController include Authorization before_action :check_feature_enabled diff --git a/app/controllers/api/v1_alpha/collections_controller.rb b/app/controllers/api/v1/collections_controller.rb similarity index 90% rename from app/controllers/api/v1_alpha/collections_controller.rb rename to app/controllers/api/v1/collections_controller.rb index 1ca1cd6923..3c1841237d 100644 --- a/app/controllers/api/v1_alpha/collections_controller.rb +++ b/app/controllers/api/v1/collections_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Api::V1Alpha::CollectionsController < Api::BaseController +class Api::V1::CollectionsController < Api::BaseController include Authorization DEFAULT_COLLECTIONS_LIMIT = 40 @@ -98,13 +98,13 @@ class Api::V1Alpha::CollectionsController < Api::BaseController def next_path return unless records_continue? - api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT))) + api_v1_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT))) end def prev_path return if offset_param.zero? - api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT))) + api_v1_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT))) end def records_continue? diff --git a/app/controllers/api/v1_alpha/in_collections_controller.rb b/app/controllers/api/v1/in_collections_controller.rb similarity index 79% rename from app/controllers/api/v1_alpha/in_collections_controller.rb rename to app/controllers/api/v1/in_collections_controller.rb index 087464989e..54a1334e3c 100644 --- a/app/controllers/api/v1_alpha/in_collections_controller.rb +++ b/app/controllers/api/v1/in_collections_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Api::V1Alpha::InCollectionsController < Api::BaseController +class Api::V1::InCollectionsController < Api::BaseController include Authorization DEFAULT_COLLECTIONS_LIMIT = 40 @@ -44,13 +44,13 @@ class Api::V1Alpha::InCollectionsController < Api::BaseController def next_path return unless records_continue? - api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT))) + api_v1_account_in_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT))) end def prev_path return if offset_param.zero? - api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT))) + api_v1_account_in_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT))) end def records_continue? diff --git a/config/routes/api.rb b/config/routes/api.rb index 2546b5517a..a212685eb0 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -6,13 +6,16 @@ namespace :api, format: false do # Experimental JSON / REST API namespace :v1_alpha do + resources :async_refreshes, only: :show + end + + # TODO: Remove once apps switch over to v1 + scope :v1_alpha, as: :v1_alpha, module: :v1 do resources :accounts, only: [] do resources :collections, only: [:index] resources :in_collections, only: [:index] end - resources :async_refreshes, only: :show - resources :collections, only: [:show, :create, :update, :destroy] do resources :items, only: [:create, :destroy], controller: 'collection_items' do member do @@ -221,6 +224,9 @@ namespace :api, format: false do resources :email_subscriptions, only: :create end + resources :collections, only: [:index] + resources :in_collections, only: [:index] + member do post :follow post :unfollow @@ -327,6 +333,14 @@ namespace :api, format: false do resources :tags, only: [:index, :show, :update] end + + resources :collections, only: [:show, :create, :update, :destroy] do + resources :items, only: [:create, :destroy], controller: 'collection_items' do + member do + post :revoke + end + end + end end namespace :v2 do diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 750c83b718..6d5353eefc 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -45,7 +45,7 @@ module Mastodon def api_versions { - mastodon: 9, + mastodon: 10, } end diff --git a/spec/requests/api/v1_alpha/collection_items_spec.rb b/spec/requests/api/v1/collection_items_spec.rb similarity index 100% rename from spec/requests/api/v1_alpha/collection_items_spec.rb rename to spec/requests/api/v1/collection_items_spec.rb diff --git a/spec/requests/api/v1_alpha/collections_spec.rb b/spec/requests/api/v1/collections_spec.rb similarity index 87% rename from spec/requests/api/v1_alpha/collections_spec.rb rename to spec/requests/api/v1/collections_spec.rb index f448659bf5..4bf296f293 100644 --- a/spec/requests/api/v1_alpha/collections_spec.rb +++ b/spec/requests/api/v1/collections_spec.rb @@ -2,12 +2,12 @@ require 'rails_helper' -RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do +RSpec.describe 'Api::V1::Collections', feature: :collections do include_context 'with API authentication', oauth_scopes: 'read:collections write:collections' - describe 'GET /api/v1_alpha/accounts/:account_id/collections' do + describe 'GET /api/v1/accounts/:account_id/collections' do subject do - get "/api/v1_alpha/accounts/#{account.id}/collections", headers: headers, params: params + get "/api/v1/accounts/#{account.id}/collections", headers: headers, params: params end let(:params) { {} } @@ -34,7 +34,7 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do expect(response) .to include_pagination_headers( - next: api_v1_alpha_account_collections_url(account, limit: 1, offset: 1) + next: api_v1_account_collections_url(account, limit: 1, offset: 1) ) end end @@ -50,8 +50,8 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do expect(response) .to include_pagination_headers( - prev: api_v1_alpha_account_collections_url(account, limit: 1, offset: 0), - next: api_v1_alpha_account_collections_url(account, limit: 1, offset: 2) + prev: api_v1_account_collections_url(account, limit: 1, offset: 0), + next: api_v1_account_collections_url(account, limit: 1, offset: 2) ) end end @@ -96,9 +96,9 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do end end - describe 'GET /api/v1_alpha/collections/:id' do + describe 'GET /api/v1/collections/:id' do subject do - get "/api/v1_alpha/collections/#{collection.id}", headers: headers + get "/api/v1/collections/#{collection.id}", headers: headers end let(:collection) { Fabricate(:collection) } @@ -140,9 +140,9 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do end end - describe 'POST /api/v1_alpha/collections' do + describe 'POST /api/v1/collections' do subject do - post '/api/v1_alpha/collections', headers: headers, params: params + post '/api/v1/collections', headers: headers, params: params end let(:params) { {} } @@ -187,9 +187,9 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do end end - describe 'PATCH /api/v1_alpha/collections/:id' do + describe 'PATCH /api/v1/collections/:id' do subject do - patch "/api/v1_alpha/collections/#{collection.id}", headers: headers, params: params + patch "/api/v1/collections/#{collection.id}", headers: headers, params: params end let(:collection) { Fabricate(:collection) } @@ -256,9 +256,9 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do end end - describe 'DELETE /api/v1_alpha/collections/:id' do + describe 'DELETE /api/v1/collections/:id' do subject do - delete "/api/v1_alpha/collections/#{collection.id}", headers: headers + delete "/api/v1/collections/#{collection.id}", headers: headers end let(:collection) { Fabricate(:collection) } diff --git a/spec/requests/api/v1_alpha/in_collections_spec.rb b/spec/requests/api/v1/in_collections_spec.rb similarity index 75% rename from spec/requests/api/v1_alpha/in_collections_spec.rb rename to spec/requests/api/v1/in_collections_spec.rb index a4bd3110be..3c41feb2e7 100644 --- a/spec/requests/api/v1_alpha/in_collections_spec.rb +++ b/spec/requests/api/v1/in_collections_spec.rb @@ -2,12 +2,12 @@ require 'rails_helper' -RSpec.describe 'Api::V1Alpha::InCollections', feature: :collections do +RSpec.describe 'Api::V1::InCollections', feature: :collections do include_context 'with API authentication', oauth_scopes: 'read:collections write:collections' - describe 'GET /api/v1_alpha/in_collections' do + describe 'GET /api/v1/in_collections' do subject do - get "/api/v1_alpha/accounts/#{account.id}/in_collections", headers: headers, params: params + get "/api/v1/accounts/#{account.id}/in_collections", headers: headers, params: params end let(:params) { {} } @@ -33,7 +33,7 @@ RSpec.describe 'Api::V1Alpha::InCollections', feature: :collections do expect(response) .to include_pagination_headers( - next: api_v1_alpha_account_in_collections_url(account, limit: 1, offset: 1) + next: api_v1_account_in_collections_url(account, limit: 1, offset: 1) ) end end @@ -49,8 +49,8 @@ RSpec.describe 'Api::V1Alpha::InCollections', feature: :collections do expect(response) .to include_pagination_headers( - prev: api_v1_alpha_account_in_collections_url(account, limit: 1, offset: 0), - next: api_v1_alpha_account_in_collections_url(account, limit: 1, offset: 2) + prev: api_v1_account_in_collections_url(account, limit: 1, offset: 0), + next: api_v1_account_in_collections_url(account, limit: 1, offset: 2) ) end end From e2754b0bc2872a17f4cca57c25f0c34b4e60fe33 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 29 May 2026 05:16:19 -0400 Subject: [PATCH 21/34] Unify `queue_mail` and `mail` in admin mailer new trends spec (#39207) --- spec/mailers/admin_mailer_spec.rb | 43 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb index 34ed2d4dfa..75efe3a462 100644 --- a/spec/mailers/admin_mailer_spec.rb +++ b/spec/mailers/admin_mailer_spec.rb @@ -74,19 +74,15 @@ RSpec.describe AdminMailer do let!(:link) { Fabricate(:preview_card, trendable: true, language: 'en') } let!(:status) { Fabricate(:status) } let!(:tag) { Fabricate(:tag, display_name: 'Test Tag') } - let!(:other_tag) { Fabricate(:tag, display_name: 'Test Tag') } - let!(:another_tag) { Fabricate(:tag, display_name: 'Test Tag') } - let(:mail) { described_class.with(recipient: recipient).new_trends([link], [tag, other_tag, another_tag], [status]) } - let(:status_trend) { Fabricate(:status_trend, status: status, account: Fabricate(:account)) } - let(:tag_trend) { Fabricate(:tag_trend, tag: tag) } + let!(:other_tag) { Fabricate(:tag, display_name: 'Other Test Tag') } + let(:mail) { described_class.with(recipient: recipient).new_trends([link], [tag, other_tag], [status]) } let(:other_tag_trend) { Fabricate(:tag_trend, tag: other_tag) } - let(:preview_card_trend) { Fabricate(:preview_card_trend, preview_card: link) } before do recipient.user.update(locale: :en) - status_trend - tag_trend - preview_card_trend + Fabricate(:status_trend, status: status, account: Fabricate(:account)) + Fabricate(:tag_trend, tag: tag) + Fabricate(:preview_card_trend, preview_card: link) end it 'renders the email' do @@ -104,37 +100,40 @@ RSpec.describe AdminMailer do end context 'when between queueing and sending trends gets deleted' do - let(:queue_mail) { described_class.with(recipient: recipient).new_trends([link], [tag, other_tag], [status]).deliver_later! } - before do recipient.user.update(locale: :en) end it 'sends the email when all but one trends were deleted without the respective tag or status or link' do other_tag_trend - expect(queue_mail.successfully_enqueued?).to be(true) + expect(mail.deliver_later!) + .to be_successfully_enqueued TagTrend.delete_all StatusTrend.delete_all - expect { queue_mail.perform_now }.to send_email( - to: recipient.user_email, - from: 'notifications@localhost', - subject: I18n.t('admin_mailer.new_trends.subject', instance: Rails.configuration.x.local_domain) - ) - expect(mail.body).to have_text(/The following items need a review before they can be displayed publicly/) + expect { mail.deliver } + .to send_email( + to: recipient.user_email, + from: 'notifications@localhost', + subject: I18n.t('admin_mailer.new_trends.subject', instance: Rails.configuration.x.local_domain) + ) + expect(mail.body) + .to have_text(/The following items need a review before they can be displayed publicly/) .and match(link.title) - expect(mail.body).to_not match(ActivityPub::TagManager.instance.url_for(status)) - expect(mail.body).to_not match(tag.display_name) + .and not_include(ActivityPub::TagManager.instance.url_for(status)) + .and not_include(tag.display_name) end it 'returns nil when no trends are present' do - expect(queue_mail.successfully_enqueued?).to be(true) + expect(mail.deliver_later!) + .to be_successfully_enqueued TagTrend.delete_all StatusTrend.delete_all PreviewCardTrend.delete_all - expect { queue_mail.perform_now }.to_not send_email + expect { mail.deliver } + .to_not send_email end end end From 572612fde9f764297cc6e47c22372c7b1dcba65b Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 29 May 2026 11:37:42 +0200 Subject: [PATCH 22/34] Remove `collections` feature flag (#39211) --- .../featured_collections_controller.rb | 5 - .../admin/report_notes_controller.rb | 2 + .../api/v1/collection_items_controller.rb | 6 - .../api/v1/collections_controller.rb | 6 - .../api/v1/in_collections_controller.rb | 6 - app/controllers/api/v1/reports_controller.rb | 6 +- app/controllers/api/v1/statuses_controller.rb | 2 +- .../collection_items_controller.rb | 5 - app/controllers/collections_controller.rb | 5 - app/lib/activitypub/activity/accept.rb | 2 +- app/lib/activitypub/activity/add.rb | 4 +- app/lib/activitypub/activity/delete.rb | 2 +- .../activitypub/activity/feature_request.rb | 1 - app/lib/activitypub/activity/update.rb | 2 +- app/lib/status_cache_hydrator.rb | 1 - .../activitypub/actor_serializer.rb | 6 +- app/serializers/rest/account_serializer.rb | 2 +- app/serializers/rest/role_serializer.rb | 2 +- .../activitypub/process_account_service.rb | 4 +- app/services/resolve_url_service.rb | 2 +- app/views/admin/reports/index.html.haml | 7 +- app/views/admin/reports/show.html.haml | 39 +++--- app/views/admin/roles/_form.html.haml | 10 +- spec/lib/activitypub/activity/accept_spec.rb | 2 +- spec/lib/activitypub/activity/add_spec.rb | 4 +- spec/lib/activitypub/activity/delete_spec.rb | 2 +- .../activity/feature_request_spec.rb | 2 +- spec/lib/activitypub/activity/update_spec.rb | 2 +- spec/lib/status_cache_hydrator_spec.rb | 2 +- spec/models/admin/moderation_action_spec.rb | 6 +- .../activitypub/featured_collections_spec.rb | 2 +- spec/requests/admin/reports_spec.rb | 4 +- spec/requests/api/v1/collection_items_spec.rb | 2 +- spec/requests/api/v1/collections_spec.rb | 2 +- spec/requests/api/v1/in_collections_spec.rb | 2 +- spec/requests/api/v1/reports_spec.rb | 2 +- spec/requests/collection_items_spec.rb | 2 +- spec/requests/collections_spec.rb | 4 +- .../activitypub/actor_serializer_spec.rb | 63 ++++----- .../activitypub/flag_serializer_spec.rb | 2 +- .../rest/account_serializer_spec.rb | 121 ++++++++---------- .../rest/account_warning_serializer_spec.rb | 3 +- .../rest/admin/account_serializer_spec.rb | 4 +- .../rest/admin/report_serializer_spec.rb | 2 +- .../rest/report_serializer_spec.rb | 8 +- spec/serializers/rest/role_serializer_spec.rb | 2 +- .../rest/suggestion_serializer_spec.rb | 3 +- .../process_account_service_spec.rb | 13 +- spec/services/resolve_url_service_spec.rb | 4 +- 49 files changed, 166 insertions(+), 226 deletions(-) diff --git a/app/controllers/activitypub/featured_collections_controller.rb b/app/controllers/activitypub/featured_collections_controller.rb index 09de5583cc..12c0648fae 100644 --- a/app/controllers/activitypub/featured_collections_controller.rb +++ b/app/controllers/activitypub/featured_collections_controller.rb @@ -9,7 +9,6 @@ class ActivityPub::FeaturedCollectionsController < ApplicationController vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } - before_action :check_feature_enabled before_action :require_account_signature!, if: -> { authorized_fetch_mode? } before_action :set_collections @@ -72,8 +71,4 @@ class ActivityPub::FeaturedCollectionsController < ApplicationController ) end end - - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end end diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index 10dbe846e4..03234b0bde 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -25,6 +25,8 @@ module Admin @action_logs = @report.history.includes(:target) @form = Admin::StatusBatchAction.new @statuses = @report.statuses.with_includes + @collections = @report.collections + @collection_form = Admin::CollectionBatchAction.new render 'admin/reports/show' end diff --git a/app/controllers/api/v1/collection_items_controller.rb b/app/controllers/api/v1/collection_items_controller.rb index eaa46a4430..3ec5e18ed9 100644 --- a/app/controllers/api/v1/collection_items_controller.rb +++ b/app/controllers/api/v1/collection_items_controller.rb @@ -3,8 +3,6 @@ class Api::V1::CollectionItemsController < Api::BaseController include Authorization - before_action :check_feature_enabled - before_action -> { doorkeeper_authorize! :write, :'write:collections' } before_action :require_user! @@ -55,8 +53,4 @@ class Api::V1::CollectionItemsController < Api::BaseController def set_collection_item @collection_item = @collection.collection_items.find(params[:id]) end - - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end end diff --git a/app/controllers/api/v1/collections_controller.rb b/app/controllers/api/v1/collections_controller.rb index 3c1841237d..9acd535f46 100644 --- a/app/controllers/api/v1/collections_controller.rb +++ b/app/controllers/api/v1/collections_controller.rb @@ -9,8 +9,6 @@ class Api::V1::CollectionsController < Api::BaseController render json: { error: ValidationErrorFormatter.new(e).as_json }, status: 422 end - before_action :check_feature_enabled - before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create, :update, :destroy] @@ -91,10 +89,6 @@ class Api::V1::CollectionsController < Api::BaseController params.permit(:name, :description, :language, :sensitive, :discoverable, :tag_name) end - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end - def next_path return unless records_continue? diff --git a/app/controllers/api/v1/in_collections_controller.rb b/app/controllers/api/v1/in_collections_controller.rb index 54a1334e3c..c34845e463 100644 --- a/app/controllers/api/v1/in_collections_controller.rb +++ b/app/controllers/api/v1/in_collections_controller.rb @@ -5,8 +5,6 @@ class Api::V1::InCollectionsController < Api::BaseController DEFAULT_COLLECTIONS_LIMIT = 40 - before_action :check_feature_enabled - before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index] before_action :require_user! @@ -37,10 +35,6 @@ class Api::V1::InCollectionsController < Api::BaseController .limit(limit_param(DEFAULT_COLLECTIONS_LIMIT)) end - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end - def next_path return unless records_continue? diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 8e341aa48e..a8653631c2 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -23,10 +23,6 @@ class Api::V1::ReportsController < Api::BaseController end def report_params - if Mastodon::Feature.collections_enabled? - params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], collection_ids: [], rule_ids: []) - else - params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], rule_ids: []) - end + params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], collection_ids: [], rule_ids: []) end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 78b237357c..d3f742d62a 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -170,6 +170,6 @@ class Api::V1::StatusesController < Api::BaseController end def serialized_accounts(accounts) - ActiveModel::Serializer::CollectionSerializer.new(accounts, serializer: REST::AccountSerializer) + ActiveModel::Serializer::CollectionSerializer.new(accounts, serializer: REST::AccountSerializer, scope_name: :current_user, scope: current_user) end end diff --git a/app/controllers/collection_items_controller.rb b/app/controllers/collection_items_controller.rb index 09c1e0e192..51044b5965 100644 --- a/app/controllers/collection_items_controller.rb +++ b/app/controllers/collection_items_controller.rb @@ -7,7 +7,6 @@ class CollectionItemsController < ApplicationController vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } - before_action :check_feature_enabled before_action :require_account_signature!, if: -> { authorized_fetch_mode? } before_action :set_collection_item @@ -35,8 +34,4 @@ class CollectionItemsController < ApplicationController rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end - - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end end diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index 70541433f0..628418557c 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -8,7 +8,6 @@ class CollectionsController < ApplicationController vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } - before_action :check_feature_enabled before_action :require_account_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_collection @@ -51,8 +50,4 @@ class CollectionsController < ApplicationController recently_updated = @collection.updated_at > 15.minutes.ago recently_updated ? 30.seconds : 5.minutes end - - def check_feature_enabled - raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? - end end diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb index 4dc6977b8e..a76b79a6d8 100644 --- a/app/lib/activitypub/activity/accept.rb +++ b/app/lib/activitypub/activity/accept.rb @@ -5,7 +5,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity return accept_follow_for_relay if relay_follow? return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil? return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil? - return accept_feature_request! if Mastodon::Feature.collections_enabled? && feature_request_from_object.present? + return accept_feature_request! if feature_request_from_object.present? case @object['type'] when 'Follow' diff --git a/app/lib/activitypub/activity/add.rb b/app/lib/activitypub/activity/add.rb index 0d22910a9a..957471a5c6 100644 --- a/app/lib/activitypub/activity/add.rb +++ b/app/lib/activitypub/activity/add.rb @@ -13,12 +13,10 @@ class ActivityPub::Activity::Add < ActivityPub::Activity add_featured end when @account.collections_url - return unless Mastodon::Feature.collections_enabled? - add_collection else @collection = @account.collections.find_by(uri: value_or_id(@json['target'])) - add_collection_item if @collection && Mastodon::Feature.collections_enabled? + add_collection_item if @collection end end diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 1239462a11..1e3cd1a647 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -3,7 +3,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def perform return delete_person if @account.uri == object_uri - return delete_feature_authorization! unless !Mastodon::Feature.collections_enabled? || feature_authorization_from_object.nil? + return delete_feature_authorization! unless feature_authorization_from_object.nil? delete_object end diff --git a/app/lib/activitypub/activity/feature_request.rb b/app/lib/activitypub/activity/feature_request.rb index 16a3860a0e..2738618767 100644 --- a/app/lib/activitypub/activity/feature_request.rb +++ b/app/lib/activitypub/activity/feature_request.rb @@ -4,7 +4,6 @@ class ActivityPub::Activity::FeatureRequest < ActivityPub::Activity include Payloadable def perform - return unless Mastodon::Feature.collections_enabled? return if non_matching_uri_hosts?(@account.uri, @json['id']) @collection = find_or_fetch_collection diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb index 8eb2427a84..87d5a5fad5 100644 --- a/app/lib/activitypub/activity/update.rb +++ b/app/lib/activitypub/activity/update.rb @@ -13,7 +13,7 @@ class ActivityPub::Activity::Update < ActivityPub::Activity update_account elsif supported_object_type? || converted_object_type? update_status - elsif equals_or_includes_any?(@object['type'], ['FeaturedCollection']) && Mastodon::Feature.collections_enabled? + elsif equals_or_includes_any?(@object['type'], ['FeaturedCollection']) update_collection end end diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb index d5920530c6..ece88c04c1 100644 --- a/app/lib/status_cache_hydrator.rb +++ b/app/lib/status_cache_hydrator.rb @@ -121,7 +121,6 @@ class StatusCacheHydrator end def hydrate_account(payload, account) - return unless Mastodon::Feature.collections_enabled? return unless payload[:id] stale_account = Account.find_by(id: payload[:id]) diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 8212b607f6..38b0878b1a 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -10,7 +10,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer :moved_to, :property_value, :discoverable, :suspended, :memorial, :indexable, :attribution_domains, :profile_settings - context_extensions :interaction_policies if Mastodon::Feature.collections_enabled? + context_extensions :interaction_policies attributes :id, :webfinger, :type, :following, :followers, :inbox, :outbox, :featured, :featured_tags, @@ -21,8 +21,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer attribute :show_media_replies, key: :show_replies_in_media - attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? } - attribute :featured_collections, if: -> { Mastodon::Feature.collections_enabled? } + attribute :interaction_policy + attribute :featured_collections has_one :public_key, serializer: ActivityPub::PublicKeySerializer diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 8afe3f3b67..e24cdfab85 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -21,7 +21,7 @@ class REST::AccountSerializer < ActiveModel::Serializer attribute :memorial, if: :memorial? - attribute :feature_approval, if: -> { Mastodon::Feature.collections_enabled? } + attribute :feature_approval attribute :email_subscriptions, if: -> { Rails.application.config.x.email_subscriptions && Setting.email_subscriptions } class AccountDecorator < SimpleDelegator diff --git a/app/serializers/rest/role_serializer.rb b/app/serializers/rest/role_serializer.rb index a3d8af64b2..424b3401da 100644 --- a/app/serializers/rest/role_serializer.rb +++ b/app/serializers/rest/role_serializer.rb @@ -3,7 +3,7 @@ class REST::RoleSerializer < ActiveModel::Serializer attributes :id, :name, :permissions, :color, :highlighted - attribute :collection_limit, if: -> { Mastodon::Feature.collections_enabled? } + attribute :collection_limit def id object.id.to_s diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 6254539327..e616400c93 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -72,7 +72,7 @@ class ActivityPub::ProcessAccountService < BaseService unless @options[:only_key] || @account.suspended? check_featured_collection! if @json['featured'].present? check_featured_tags_collection! if @json['featuredTags'].present? - check_featured_collections_collection! if @json['featuredCollections'].present? && Mastodon::Feature.collections_enabled? + check_featured_collections_collection! if @json['featuredCollections'].present? check_links! if @account.fields.any?(&:requires_verification?) end @@ -121,7 +121,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.uri = @uri @account.actor_type = actor_type @account.created_at = @json['published'] if @json['published'].present? - @account.feature_approval_policy = feature_approval_policy if Mastodon::Feature.collections_enabled? + @account.feature_approval_policy = feature_approval_policy end def valid_collection_uri(uri) diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index 9a136439b0..5c27121acd 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -28,7 +28,7 @@ class ResolveURLService < BaseService status = FetchRemoteStatusService.new.call(resource_url, prefetched_body: body) authorize_with @on_behalf_of, status, :show? unless status.nil? status - elsif type == 'FeaturedCollection' && Mastodon::Feature.collections_enabled? + elsif type == 'FeaturedCollection' collection = ActivityPub::FetchRemoteFeaturedCollectionService.new.call(resource_url, prefetched_body: body) authorize_with @on_behalf_of, collection, :show? unless collection.nil? collection diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 070d8c8518..b6b25c190e 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -67,10 +67,9 @@ = material_symbol('photo_camera') = report.media_attachments_count - - if Mastodon::Feature.collections_enabled? - %span.report-card__summary__item__content__icon{ title: t('admin.accounts.collections') } - = material_symbol('category') - = report.collections.size + %span.report-card__summary__item__content__icon{ title: t('admin.accounts.collections') } + = material_symbol('category') + = report.collections.size - if report.forwarded? · diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index bae69cca04..d23edee3c4 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -62,27 +62,26 @@ - else = render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f } -- if Mastodon::Feature.collections_enabled? - %details{ open: @collections.any? } - %summary - = t 'admin.reports.collections', count: @collections.size +%details{ open: @collections.any? } + %summary + = t 'admin.reports.collections', count: @collections.size - = form_with model: @collection_form, url: batch_admin_account_collections_path(@report.target_account_id, report_id: @report.id) do |f| - .batch-table - .batch-table__toolbar - %label.batch-table__toolbar__select.batch-checkbox-all - = check_box_tag :batch_checkbox_all, nil, false - .batch-table__toolbar__actions - = link_to safe_join([material_symbol('add'), t('admin.reports.add_to_report')]), - admin_account_collections_path(@report.target_account_id, report_id: @report.id), - class: 'table-action-link' - - if !@collections.empty? && @report.unresolved? - = f.button safe_join([material_symbol('close'), t('admin.collections.batch.remove_from_report')]), name: :remove_from_report, class: 'table-action-link', type: :submit - .batch-table__body - - if @collections.empty? - = nothing_here 'nothing-here--under-tabs' - - else - = render partial: 'admin/shared/collection_batch_row', collection: @collections, as: :collection, locals: { f: f } + = form_with model: @collection_form, url: batch_admin_account_collections_path(@report.target_account_id, report_id: @report.id) do |f| + .batch-table + .batch-table__toolbar + %label.batch-table__toolbar__select.batch-checkbox-all + = check_box_tag :batch_checkbox_all, nil, false + .batch-table__toolbar__actions + = link_to safe_join([material_symbol('add'), t('admin.reports.add_to_report')]), + admin_account_collections_path(@report.target_account_id, report_id: @report.id), + class: 'table-action-link' + - if !@collections.empty? && @report.unresolved? + = f.button safe_join([material_symbol('close'), t('admin.collections.batch.remove_from_report')]), name: :remove_from_report, class: 'table-action-link', type: :submit + .batch-table__body + - if @collections.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'admin/shared/collection_batch_row', collection: @collections, as: :collection, locals: { f: f } - if @report.unresolved? %hr.spacer/ diff --git a/app/views/admin/roles/_form.html.haml b/app/views/admin/roles/_form.html.haml index 51114beac1..7246357e67 100644 --- a/app/views/admin/roles/_form.html.haml +++ b/app/views/admin/roles/_form.html.haml @@ -32,13 +32,11 @@ %hr.spacer/ -- if Mastodon::Feature.collections_enabled? +.fields-group + = form.input :collection_limit, + wrapper: :with_label - .fields-group - = form.input :collection_limit, - wrapper: :with_label - - %hr.spacer/ +%hr.spacer/ - unless current_user.role == form.object diff --git a/spec/lib/activitypub/activity/accept_spec.rb b/spec/lib/activitypub/activity/accept_spec.rb index 732f01cc6d..7775143e88 100644 --- a/spec/lib/activitypub/activity/accept_spec.rb +++ b/spec/lib/activitypub/activity/accept_spec.rb @@ -172,7 +172,7 @@ RSpec.describe ActivityPub::Activity::Accept do end end - context 'with a FeatureRequest', feature: :collections do + context 'with a FeatureRequest' do let(:collection) { Fabricate(:collection, account: recipient) } let(:collection_item) { Fabricate(:collection_item, collection:, account: sender, state: :pending) } let(:object) { collection_item.activity_uri } diff --git a/spec/lib/activitypub/activity/add_spec.rb b/spec/lib/activitypub/activity/add_spec.rb index b444f38a3d..201953087f 100644 --- a/spec/lib/activitypub/activity/add_spec.rb +++ b/spec/lib/activitypub/activity/add_spec.rb @@ -80,7 +80,7 @@ RSpec.describe ActivityPub::Activity::Add do end end - context 'when the target is the `featuredCollections` collection', feature: :collections do + context 'when the target is the `featuredCollections` collection' do subject { described_class.new(activity_json, account) } let(:account) { Fabricate(:remote_account, collections_url: 'https://example.com/actor/1/featured_collections') } @@ -122,7 +122,7 @@ RSpec.describe ActivityPub::Activity::Add do end end - context 'when the target is a collection', feature: :collections do + context 'when the target is a collection' do subject { described_class.new(activity_json, collection.account) } let(:collection) { Fabricate(:remote_collection) } diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb index c6d74b4b5b..7e5d5f8574 100644 --- a/spec/lib/activitypub/activity/delete_spec.rb +++ b/spec/lib/activitypub/activity/delete_spec.rb @@ -120,7 +120,7 @@ RSpec.describe ActivityPub::Activity::Delete do end end - context 'with a FeatureAuthorization', feature: :collections do + context 'with a FeatureAuthorization' do let(:recipient) { Fabricate(:account) } let(:approval_uri) { 'https://example.com/authorizations/1' } let(:collection) { Fabricate(:collection, account: recipient) } diff --git a/spec/lib/activitypub/activity/feature_request_spec.rb b/spec/lib/activitypub/activity/feature_request_spec.rb index 5d134b6ceb..3efdf33b35 100644 --- a/spec/lib/activitypub/activity/feature_request_spec.rb +++ b/spec/lib/activitypub/activity/feature_request_spec.rb @@ -20,7 +20,7 @@ RSpec.describe ActivityPub::Activity::FeatureRequest do } end - describe '#perform', feature: :collections do + describe '#perform' do subject { described_class.new(json, sender) } context 'when recipient is discoverable' do diff --git a/spec/lib/activitypub/activity/update_spec.rb b/spec/lib/activitypub/activity/update_spec.rb index 701a2ff1f5..c281d03a94 100644 --- a/spec/lib/activitypub/activity/update_spec.rb +++ b/spec/lib/activitypub/activity/update_spec.rb @@ -257,7 +257,7 @@ RSpec.describe ActivityPub::Activity::Update do end end - context 'with a `FeaturedCollection` object', feature: :collections do + context 'with a `FeaturedCollection` object' do let(:collection) { Fabricate(:remote_collection, account: sender, name: 'old name', discoverable: false) } let(:account) { Fabricate(:account) } let!(:collection_item) { Fabricate(:collection_item, account:, collection:, uri: 'https://example.com/featured_stamps/1') } diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index 03e453c046..3eb781dfba 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -6,7 +6,7 @@ RSpec.describe StatusCacheHydrator do let(:status) { Fabricate(:status) } let(:account) { Fabricate(:account) } - describe '#hydrate', feature: :collections do + describe '#hydrate' do let(:compare_to_hash) { InlineRenderer.render(status, account, :status) } shared_examples 'shared behavior' do diff --git a/spec/models/admin/moderation_action_spec.rb b/spec/models/admin/moderation_action_spec.rb index fd2e7a4c4c..c49e7c9a30 100644 --- a/spec/models/admin/moderation_action_spec.rb +++ b/spec/models/admin/moderation_action_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Admin::ModerationAction do expect(report.reload).to be_action_taken end - context 'with attached collections', feature: :collections do + context 'with attached collections' do let(:status_ids) { [] } let(:collections) { Fabricate.times(2, :collection, account: target_account) } @@ -47,7 +47,7 @@ RSpec.describe Admin::ModerationAction do end end - context 'with a remote collection', feature: :collections do + context 'with a remote collection' do let(:status_ids) { [] } let(:collection) { Fabricate(:remote_collection) } let(:target_account) { collection.account } @@ -83,7 +83,7 @@ RSpec.describe Admin::ModerationAction do expect(report.reload).to be_action_taken end - context 'with attached collections', feature: :collections do + context 'with attached collections' do let(:status_ids) { [] } let(:collections) { Fabricate.times(2, :collection, account: target_account) } diff --git a/spec/requests/activitypub/featured_collections_spec.rb b/spec/requests/activitypub/featured_collections_spec.rb index 09a17c53be..c4601df69b 100644 --- a/spec/requests/activitypub/featured_collections_spec.rb +++ b/spec/requests/activitypub/featured_collections_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe 'Collections' do - describe 'GET /ap/users/@:account_id/featured_collections', feature: :collections do + describe 'GET /ap/users/@:account_id/featured_collections' do subject { get ap_account_featured_collections_path(account.id, format: :json) } let(:collection) { Fabricate(:collection) } diff --git a/spec/requests/admin/reports_spec.rb b/spec/requests/admin/reports_spec.rb index d44db63795..2e361e43df 100644 --- a/spec/requests/admin/reports_spec.rb +++ b/spec/requests/admin/reports_spec.rb @@ -47,7 +47,7 @@ RSpec.describe 'Admin Reports' do it_behaves_like 'successful return' end - context 'with a reported collection', feature: :collections do + context 'with a reported collection' do before do report.collections << Fabricate(:collection, account: report.target_account) end @@ -55,7 +55,7 @@ RSpec.describe 'Admin Reports' do it_behaves_like 'successful return' end - context 'with both status and collection', feature: :collections do + context 'with both status and collection' do before do status = Fabricate(:status, account: report.target_account) report.update(status_ids: [status.id]) diff --git a/spec/requests/api/v1/collection_items_spec.rb b/spec/requests/api/v1/collection_items_spec.rb index e7ee854e67..93d4f70ad5 100644 --- a/spec/requests/api/v1/collection_items_spec.rb +++ b/spec/requests/api/v1/collection_items_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'Api::V1Alpha::CollectionItems', feature: :collections do +RSpec.describe 'Api::V1Alpha::CollectionItems' do include_context 'with API authentication', oauth_scopes: 'read:collections write:collections' describe 'POST /api/v1_alpha/collections/:collection_id/items' do diff --git a/spec/requests/api/v1/collections_spec.rb b/spec/requests/api/v1/collections_spec.rb index 4bf296f293..b6bb17319d 100644 --- a/spec/requests/api/v1/collections_spec.rb +++ b/spec/requests/api/v1/collections_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'Api::V1::Collections', feature: :collections do +RSpec.describe 'Api::V1::Collections' do include_context 'with API authentication', oauth_scopes: 'read:collections write:collections' describe 'GET /api/v1/accounts/:account_id/collections' do diff --git a/spec/requests/api/v1/in_collections_spec.rb b/spec/requests/api/v1/in_collections_spec.rb index 3c41feb2e7..74902fe09e 100644 --- a/spec/requests/api/v1/in_collections_spec.rb +++ b/spec/requests/api/v1/in_collections_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'Api::V1::InCollections', feature: :collections do +RSpec.describe 'Api::V1::InCollections' do include_context 'with API authentication', oauth_scopes: 'read:collections write:collections' describe 'GET /api/v1/in_collections' do diff --git a/spec/requests/api/v1/reports_spec.rb b/spec/requests/api/v1/reports_spec.rb index 247ff979c5..6b344eaac8 100644 --- a/spec/requests/api/v1/reports_spec.rb +++ b/spec/requests/api/v1/reports_spec.rb @@ -113,7 +113,7 @@ RSpec.describe 'Reports' do end end - context 'with attached collection', feature: :collections do + context 'with attached collection' do let(:collection) { Fabricate(:collection, account: target_account) } let(:collection_ids) { [collection.id] } diff --git a/spec/requests/collection_items_spec.rb b/spec/requests/collection_items_spec.rb index f0802fc3f3..703e6bdba3 100644 --- a/spec/requests/collection_items_spec.rb +++ b/spec/requests/collection_items_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe 'CollectionItems' do - describe 'GET /ap/users/@:account_id/collection_items/:id', feature: :collections do + describe 'GET /ap/users/@:account_id/collection_items/:id' do subject { get ap_account_collection_item_path(account.id, collection_item, format: :json) } let(:collection_item) { Fabricate(:collection_item) } diff --git a/spec/requests/collections_spec.rb b/spec/requests/collections_spec.rb index bdebb03f60..dbdf1c16a2 100644 --- a/spec/requests/collections_spec.rb +++ b/spec/requests/collections_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe 'Collections' do - describe 'GET /collections/:id', feature: :collections do + describe 'GET /collections/:id' do subject { get collection_path(collection) } let(:collection) { Fabricate(:collection) } @@ -15,7 +15,7 @@ RSpec.describe 'Collections' do end end - describe 'GET /ap/:account_id/collections/:id', feature: :collections do + describe 'GET /ap/:account_id/collections/:id' do subject { get ap_account_collection_path(account.id, collection, format: :json) } let(:collection) { Fabricate(:collection) } diff --git a/spec/serializers/activitypub/actor_serializer_spec.rb b/spec/serializers/activitypub/actor_serializer_spec.rb index 73702c979c..661890f33b 100644 --- a/spec/serializers/activitypub/actor_serializer_spec.rb +++ b/spec/serializers/activitypub/actor_serializer_spec.rb @@ -40,46 +40,37 @@ RSpec.describe ActivityPub::ActorSerializer do describe '#interactionPolicy' do let(:record) { Fabricate(:account) } - # TODO: Remove when feature flag is removed - context 'when collections feature is disabled?' do - it 'is not present' do - expect(subject).to_not have_key('interactionPolicy') + context 'when actor is discoverable' do + it 'includes an automatic policy allowing everyone' do + expect(subject).to include('interactionPolicy' => { + 'canFeature' => { + 'automaticApproval' => ['https://www.w3.org/ns/activitystreams#Public'], + }, + }) + end + + context 'when actor is locked' do + let(:record) { Fabricate(:account, locked: true) } + + it 'includes an automatic policy allowing followers' do + expect(subject).to include('interactionPolicy' => { + 'canFeature' => { + 'automaticApproval' => [ActivityPub::TagManager.instance.followers_uri_for(record)], + }, + }) + end end end - context 'when collections feature is enabled', feature: :collections do - context 'when actor is discoverable' do - it 'includes an automatic policy allowing everyone' do - expect(subject).to include('interactionPolicy' => { - 'canFeature' => { - 'automaticApproval' => ['https://www.w3.org/ns/activitystreams#Public'], - }, - }) - end + context 'when actor is not discoverable' do + let(:record) { Fabricate(:account, discoverable: false) } - context 'when actor is locked' do - let(:record) { Fabricate(:account, locked: true) } - - it 'includes an automatic policy allowing followers' do - expect(subject).to include('interactionPolicy' => { - 'canFeature' => { - 'automaticApproval' => [ActivityPub::TagManager.instance.followers_uri_for(record)], - }, - }) - end - end - end - - context 'when actor is not discoverable' do - let(:record) { Fabricate(:account, discoverable: false) } - - it 'includes an automatic policy limited to the actor itself' do - expect(subject).to include('interactionPolicy' => { - 'canFeature' => { - 'automaticApproval' => [ActivityPub::TagManager.instance.uri_for(record)], - }, - }) - end + it 'includes an automatic policy limited to the actor itself' do + expect(subject).to include('interactionPolicy' => { + 'canFeature' => { + 'automaticApproval' => [ActivityPub::TagManager.instance.uri_for(record)], + }, + }) end end end diff --git a/spec/serializers/activitypub/flag_serializer_spec.rb b/spec/serializers/activitypub/flag_serializer_spec.rb index a66a49bc87..cebf0d2ea7 100644 --- a/spec/serializers/activitypub/flag_serializer_spec.rb +++ b/spec/serializers/activitypub/flag_serializer_spec.rb @@ -38,7 +38,7 @@ RSpec.describe ActivityPub::FlagSerializer do end end - context 'with collection', feature: :collections do + context 'with collection' do let(:target_account) { Fabricate(:account) } let(:collection) { Fabricate(:collection, account: target_account) } let(:report) { Fabricate(:report, target_account:, collections: [collection]) } diff --git a/spec/serializers/rest/account_serializer_spec.rb b/spec/serializers/rest/account_serializer_spec.rb index e8e437cbd3..68e62f7e07 100644 --- a/spec/serializers/rest/account_serializer_spec.rb +++ b/spec/serializers/rest/account_serializer_spec.rb @@ -76,75 +76,66 @@ RSpec.describe REST::AccountSerializer do end describe '#feature_approval' do - # TODO: Remove when feature flag is removed - context 'when collections feature is disabled' do - it 'does not include the approval policy' do - expect(subject).to_not have_key('feature_approval') - end - end - - context 'when collections feature is enabled', feature: :collections do - context 'when account is local' do - context 'when account is discoverable' do - it 'includes a policy that allows featuring' do - expect(subject['feature_approval']).to include({ - 'automatic' => ['public'], - 'manual' => [], - 'current_user' => 'automatic', - }) - end - - context 'when account is locked' do - let(:account) { Fabricate(:account, locked: true) } - - context 'when the current account does not follow the user' do - it 'includes a policy that allows featuring for followers and has "denied" for the current user' do - expect(subject['feature_approval']).to include({ - 'automatic' => ['followers'], - 'manual' => [], - 'current_user' => 'denied', - }) - end - end - - context 'when the current account follows the user' do - before { current_user.account.follow!(account) } - - it 'includes a policy that allows featuring for followers and has "automatic" for the current user' do - expect(subject['feature_approval']).to include({ - 'automatic' => ['followers'], - 'manual' => [], - 'current_user' => 'automatic', - }) - end - end - end - end - - context 'when account is not discoverable' do - let(:account) { Fabricate(:account, discoverable: false) } - - it 'includes a policy that disallows featuring' do - expect(subject['feature_approval']).to include({ - 'automatic' => [], - 'manual' => [], - 'current_user' => 'denied', - }) - end - end - end - - context 'when account is remote' do - let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy: 0b11000000000000000010) } - - it 'includes the matching policy' do + context 'when account is local' do + context 'when account is discoverable' do + it 'includes a policy that allows featuring' do expect(subject['feature_approval']).to include({ - 'automatic' => ['followers', 'following'], - 'manual' => ['public'], - 'current_user' => 'manual', + 'automatic' => ['public'], + 'manual' => [], + 'current_user' => 'automatic', + }) + end + + context 'when account is locked' do + let(:account) { Fabricate(:account, locked: true) } + + context 'when the current account does not follow the user' do + it 'includes a policy that allows featuring for followers and has "denied" for the current user' do + expect(subject['feature_approval']).to include({ + 'automatic' => ['followers'], + 'manual' => [], + 'current_user' => 'denied', + }) + end + end + + context 'when the current account follows the user' do + before { current_user.account.follow!(account) } + + it 'includes a policy that allows featuring for followers and has "automatic" for the current user' do + expect(subject['feature_approval']).to include({ + 'automatic' => ['followers'], + 'manual' => [], + 'current_user' => 'automatic', + }) + end + end + end + end + + context 'when account is not discoverable' do + let(:account) { Fabricate(:account, discoverable: false) } + + it 'includes a policy that disallows featuring' do + expect(subject['feature_approval']).to include({ + 'automatic' => [], + 'manual' => [], + 'current_user' => 'denied', }) end end end + + context 'when account is remote' do + let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy: 0b11000000000000000010) } + + it 'includes the matching policy' do + expect(subject['feature_approval']).to include({ + 'automatic' => ['followers', 'following'], + 'manual' => ['public'], + 'current_user' => 'manual', + }) + end + end end end diff --git a/spec/serializers/rest/account_warning_serializer_spec.rb b/spec/serializers/rest/account_warning_serializer_spec.rb index a7a9dc5f63..ebbfebe76a 100644 --- a/spec/serializers/rest/account_warning_serializer_spec.rb +++ b/spec/serializers/rest/account_warning_serializer_spec.rb @@ -3,8 +3,9 @@ require 'rails_helper' RSpec.describe REST::AccountWarningSerializer do - subject { serialized_record_json(record, described_class) } + subject { serialized_record_json(record, described_class, options: { scope: current_user, scope_name: :current_user }) } + let(:current_user) { Fabricate(:moderator_user) } let(:record) { Fabricate :account_warning, id: 123, status_ids: [456, 789] } describe 'serialization' do diff --git a/spec/serializers/rest/admin/account_serializer_spec.rb b/spec/serializers/rest/admin/account_serializer_spec.rb index 5f617207a7..d16b23f112 100644 --- a/spec/serializers/rest/admin/account_serializer_spec.rb +++ b/spec/serializers/rest/admin/account_serializer_spec.rb @@ -3,7 +3,9 @@ require 'rails_helper' RSpec.describe REST::Admin::AccountSerializer do - subject { serialized_record_json(record, described_class) } + subject { serialized_record_json(record, described_class, options: { scope: current_user, scope_name: :current_user }) } + + let(:current_user) { Fabricate(:admin_user) } context 'when created_at is populated' do let(:record) { Fabricate :account, user: Fabricate(:user) } diff --git a/spec/serializers/rest/admin/report_serializer_spec.rb b/spec/serializers/rest/admin/report_serializer_spec.rb index 78d7d4f10a..8be7c9410b 100644 --- a/spec/serializers/rest/admin/report_serializer_spec.rb +++ b/spec/serializers/rest/admin/report_serializer_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe REST::Admin::ReportSerializer do - subject { serialized_record_json(report, described_class) } + subject { serialized_record_json(report, described_class, options: { scope_name: :current_user, scope: nil }) } context 'with timestamps' do let(:report) { Fabricate(:report, action_taken_at: 3.days.ago) } diff --git a/spec/serializers/rest/report_serializer_spec.rb b/spec/serializers/rest/report_serializer_spec.rb index 180cdbdb68..5ffff83220 100644 --- a/spec/serializers/rest/report_serializer_spec.rb +++ b/spec/serializers/rest/report_serializer_spec.rb @@ -6,10 +6,16 @@ RSpec.describe REST::ReportSerializer do subject do serialized_record_json( report, - described_class + described_class, + options: { + scope: current_user, + scope_name: :current_user, + } ) end + let(:current_user) { Fabricate(:moderator_user) } + context 'with timestamps' do let(:report) { Fabricate(:report, action_taken_at: 3.days.ago) } diff --git a/spec/serializers/rest/role_serializer_spec.rb b/spec/serializers/rest/role_serializer_spec.rb index 5b38047587..cb8fd14fc6 100644 --- a/spec/serializers/rest/role_serializer_spec.rb +++ b/spec/serializers/rest/role_serializer_spec.rb @@ -27,7 +27,7 @@ RSpec.describe REST::RoleSerializer do }) end - context 'when collections are enabled', feature: :collections do + context 'when collections are enabled' do it 'includes the relevant attributes' do expect(subject) .to include({ diff --git a/spec/serializers/rest/suggestion_serializer_spec.rb b/spec/serializers/rest/suggestion_serializer_spec.rb index 288d1daa23..a30076ed48 100644 --- a/spec/serializers/rest/suggestion_serializer_spec.rb +++ b/spec/serializers/rest/suggestion_serializer_spec.rb @@ -3,7 +3,8 @@ require 'rails_helper' RSpec.describe REST::SuggestionSerializer do - let(:serialization) { serialized_record_json(record, described_class) } + let(:serialization) { serialized_record_json(record, described_class, options: { scope: current_user, scope_name: :current_user }) } + let(:current_user) { Fabricate(:user) } let(:record) do AccountSuggestions::Suggestion.new( account: account, diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index 96923a816e..56f117ca82 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -63,7 +63,7 @@ RSpec.describe ActivityPub::ProcessAccountService do end end - context 'with collection URIs', feature: :collections do + context 'with collection URIs' do let(:payload) do { 'id' => 'https://foo.test', @@ -562,16 +562,7 @@ RSpec.describe ActivityPub::ProcessAccountService do .to_return(status: 200, body: '', headers: {}) end - # TODO: Remove when feature flag is removed - context 'when collections feature is disabled' do - it 'does not set the interaction policy' do - account = subject.call('user1', 'foo.test', payload) - - expect(account.feature_approval_policy).to be_zero - end - end - - context 'when collections feature is enabled', feature: :collections do + context 'when collections feature is enabled' do it 'sets the interaction policy to the correct value' do account = subject.call('user1', 'foo.test', payload) diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index 6174d8cac9..d5ac5b55b5 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -30,7 +30,7 @@ RSpec.describe ResolveURLService do expect(subject.call(url)).to eq known_account end - context 'when searching for a remote collection', feature: :collections do + context 'when searching for a remote collection' do let(:account) { Fabricate(:account) } let(:collection_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) } @@ -63,7 +63,7 @@ RSpec.describe ResolveURLService do end end - context 'when searching for a local collection', feature: :collections do + context 'when searching for a local collection' do let(:account) { Fabricate(:account) } let(:collection) { Fabricate(:collection) } From 796f77136229cfb400e0ea403ef6ed9ed3e06f07 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 09:48:52 +0000 Subject: [PATCH 23/34] Update dependency pg to v8.21.0 (#39067) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/yarn.lock b/yarn.lock index a7c35f775a..cac35d1e98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11920,14 +11920,14 @@ __metadata: languageName: node linkType: hard -"pg-cloudflare@npm:^1.3.0": - version: 1.3.0 - resolution: "pg-cloudflare@npm:1.3.0" - checksum: 10c0/b0866c88af8e54c7b3ed510719d92df37714b3af5e3a3a10d9f761fcec99483e222f5b78a1f2de590368127648087c45c01aaf66fadbe46edb25673eedc4f8fc +"pg-cloudflare@npm:^1.4.0": + version: 1.4.0 + resolution: "pg-cloudflare@npm:1.4.0" + checksum: 10c0/553764d00055052648393cda53c1feb065991d6f9fbfdeb56cf8396c5b33377ab2897aaf5dc9cd3933d09023a1f01e8b1ca755431dcf5fd71c92ea277e2888f1 languageName: node linkType: hard -"pg-connection-string@npm:^2.12.0, pg-connection-string@npm:^2.6.0": +"pg-connection-string@npm:^2.13.0, pg-connection-string@npm:^2.6.0": version: 2.13.0 resolution: "pg-connection-string@npm:2.13.0" checksum: 10c0/870f83a8fca06d0340fc522653471d9c7081efbadf25c7f5801fcfb58104ef527138bb5d0546b21498ff4df75a742469622f657911a3b74034a1e94e59f34e31 @@ -11941,19 +11941,19 @@ __metadata: languageName: node linkType: hard -"pg-pool@npm:^3.13.0": - version: 3.13.0 - resolution: "pg-pool@npm:3.13.0" +"pg-pool@npm:^3.14.0": + version: 3.14.0 + resolution: "pg-pool@npm:3.14.0" peerDependencies: pg: ">=8.0" - checksum: 10c0/2756f79cda14e3834356f2ca035deab806bca2172a38a488b62ada54bd3e65d33f583661bbe96da0c0e75e6bc59807ada733c37efca6e24ae2893429936a1549 + checksum: 10c0/3dd706e67e3b317e29409d9eb3bd44e960eabd86db2a9711a9391bbd43881f8ce7a5f7054a341509558ee05e9bf0a3cc7aef037039da0790ce1e16762ade3ba6 languageName: node linkType: hard -"pg-protocol@npm:*, pg-protocol@npm:^1.13.0": - version: 1.13.0 - resolution: "pg-protocol@npm:1.13.0" - checksum: 10c0/a4e851e6bb8ff404ca19d561cf49b6b0caf45163bd3f289889edaf6c4e9fb25b08fb57f50d37a8cc86007efcf2cbb3dd2372c97a353a546f45eb49ddebc84fa9 +"pg-protocol@npm:*, pg-protocol@npm:^1.14.0": + version: 1.14.0 + resolution: "pg-protocol@npm:1.14.0" + checksum: 10c0/dccb29b30f5cee8f2ca7dfd17da9eb957174f7a1a25e987e0bfc9fe7640f53dc9fd05c7f3635e7db0c5eefcd41716fffe625f3c1ea9789634d438851b9ce90ae languageName: node linkType: hard @@ -11971,13 +11971,13 @@ __metadata: linkType: hard "pg@npm:^8.5.0": - version: 8.20.0 - resolution: "pg@npm:8.20.0" + version: 8.21.0 + resolution: "pg@npm:8.21.0" dependencies: - pg-cloudflare: "npm:^1.3.0" - pg-connection-string: "npm:^2.12.0" - pg-pool: "npm:^3.13.0" - pg-protocol: "npm:^1.13.0" + pg-cloudflare: "npm:^1.4.0" + pg-connection-string: "npm:^2.13.0" + pg-pool: "npm:^3.14.0" + pg-protocol: "npm:^1.14.0" pg-types: "npm:2.2.0" pgpass: "npm:1.0.5" peerDependencies: @@ -11988,7 +11988,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 10c0/e21d44b9fb3ec188e67778d7abd32d945a546f2da5128b6c8c16da8ae1e42fdc953c0d6f0a2ee65d11f31808c1dffaf908cb9c880cd2e8f0ae05525e4b8bc832 + checksum: 10c0/6b46ae867a3838bf3bb720ef5a3d877bd85de19d90c6f3422e772f56443fc04a4f5b1fa44c9e8544a0f44454971e653d98f4040096e92c378a5aa5a7b07fa0f1 languageName: node linkType: hard From fa1e16ed9fe5633d9e86468d57b2040608677c37 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 29 May 2026 13:53:03 +0200 Subject: [PATCH 24/34] Collections: Add default recommendations (#39202) --- .../components/form_fields/combobox_field.tsx | 17 +++ .../features/collections/editor/accounts.tsx | 2 + .../mastodon/features/collections/utils.ts | 6 +- .../mastodon/hooks/useSearchAccounts.ts | 117 ++++++++++++++---- 4 files changed, 112 insertions(+), 30 deletions(-) diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.tsx index f3e7b45476..a0e0a36f79 100644 --- a/app/javascript/mastodon/components/form_fields/combobox_field.tsx +++ b/app/javascript/mastodon/components/form_fields/combobox_field.tsx @@ -100,6 +100,10 @@ interface ComboboxProps< * Icon to be displayed in the text input */ icon?: TextInputProps['icon'] | null; + /** + * Set to true to open as soon as there is focus + */ + openOnFocus?: boolean; /** * Set to false to keep the menu open when an item is selected */ @@ -217,8 +221,10 @@ const ComboboxWithRef = ( renderGroupTitle, renderItem, onSelectItem, + onFocus, onChange, onKeyDown, + openOnFocus = false, closeOnSelect = true, suppressMenu = false, icon = SearchIcon, @@ -288,6 +294,16 @@ const ComboboxWithRef = ( } }, []); + const handleFocus: React.FocusEventHandler = useCallback( + (e) => { + if (openOnFocus) { + setShouldMenuOpen(true); + } + onFocus?.(e); + }, + [onFocus, openOnFocus], + ); + const handleInputChange = useCallback( (e: React.ChangeEvent) => { onChange(e); @@ -487,6 +503,7 @@ const ComboboxWithRef = ( autoComplete='off' spellCheck='false' value={value} + onFocus={handleFocus} onChange={handleInputChange} onKeyDown={handleInputKeyDown} icon={icon ?? undefined} diff --git a/app/javascript/mastodon/features/collections/editor/accounts.tsx b/app/javascript/mastodon/features/collections/editor/accounts.tsx index c9423217f5..c5d23bdf3e 100644 --- a/app/javascript/mastodon/features/collections/editor/accounts.tsx +++ b/app/javascript/mastodon/features/collections/editor/accounts.tsx @@ -215,6 +215,7 @@ export const CollectionAccounts: React.FC<{ resetAccounts, } = useSearchAccounts({ withRelationships: true, + withDefaultFollows: searchValue === '', // Don't suggest accounts that were already added filterResults: (account) => !editorItems.find((item) => item.account_id === account.id), @@ -363,6 +364,7 @@ export const CollectionAccounts: React.FC<{ )} {hasPendingItems && } `/collections/${id}`; -export const canAccountBeAdded = (account: ApiMutedAccountJSON | Account) => +export const canAccountBeAdded = (account: ApiAccountJSON | Account) => ['automatic', 'manual'].includes(account.feature_approval.current_user); export const canAccountBeAddedByFollowers = ( - account: ApiMutedAccountJSON | Account, + account: ApiAccountJSON | Account, ) => account.feature_approval.automatic.includes('followers') || account.feature_approval.manual.includes('followers'); diff --git a/app/javascript/mastodon/hooks/useSearchAccounts.ts b/app/javascript/mastodon/hooks/useSearchAccounts.ts index c19f08e734..b7e6ab9f52 100644 --- a/app/javascript/mastodon/hooks/useSearchAccounts.ts +++ b/app/javascript/mastodon/hooks/useSearchAccounts.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useDebouncedCallback } from 'use-debounce'; @@ -8,16 +8,20 @@ import { apiRequest } from 'mastodon/api'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import { useAppDispatch } from 'mastodon/store'; +import { useCurrentAccountId } from './useAccountId'; + export function useSearchAccounts({ onSettled, filterResults, resetOnInputClear = true, withRelationships = false, + withDefaultFollows = false, }: { onSettled?: (value: string) => void; filterResults?: (account: ApiAccountJSON) => boolean; resetOnInputClear?: boolean; withRelationships?: boolean; + withDefaultFollows?: boolean; } = {}) { const dispatch = useAppDispatch(); @@ -29,7 +33,7 @@ export function useSearchAccounts({ const searchRequestRef = useRef(null); const searchAccounts = useDebouncedCallback( - (value: string) => { + async (value: string) => { if (searchRequestRef.current) { searchRequestRef.current.abort(); } @@ -46,41 +50,100 @@ export function useSearchAccounts({ searchRequestRef.current = new AbortController(); - void apiRequest('GET', 'v1/accounts/search', { - signal: searchRequestRef.current.signal, - params: { - q: value, - resolve: true, - }, - }) - .then((data) => { - const accounts = filterResults ? data.filter(filterResults) : data; - const accountIds = accounts.map((a) => a.id); - dispatch(importFetchedAccounts(accounts)); - if (withRelationships) { - dispatch(fetchRelationships(accountIds)); - } - setAccounts(accounts); - setLoadingState('idle'); - onSettled?.(value); - }) - .catch(() => { - setLoadingState('error'); - onSettled?.(value); - }); + try { + const data = await apiRequest( + 'GET', + 'v1/accounts/search', + { + signal: searchRequestRef.current.signal, + params: { + q: value, + resolve: true, + }, + }, + ); + const accounts = filterResults ? data.filter(filterResults) : data; + const accountIds = accounts.map((a) => a.id); + dispatch(importFetchedAccounts(accounts)); + if (withRelationships) { + dispatch(fetchRelationships(accountIds)); + } + setAccounts(accounts); + setLoadingState('idle'); + onSettled?.(value); + } catch { + setLoadingState('error'); + onSettled?.(value); + } }, 500, { leading: true, trailing: true }, ); + const startSearch = useCallback( + (value: string) => { + void searchAccounts(value); + }, + [searchAccounts], + ); + const resetAccounts = useCallback(() => { setAccounts([]); }, []); - return { - searchAccounts, - resetAccounts, + const currentUserId = useCurrentAccountId(); + const [defaultAccounts, setDefaultAccounts] = useState< + ApiAccountJSON[] | null + >(null); + useEffect(() => { + if ( + !currentUserId || + loadingState !== 'idle' || + defaultAccounts !== null || + !withDefaultFollows + ) { + return; + } + + async function doRequest() { + setLoadingState('loading'); + try { + const data = await apiRequest( + 'GET', + `v1/accounts/${currentUserId}/following`, + { params: { limit: 40 } }, + ); + const accounts = filterResults ? data.filter(filterResults) : data; + const accountIds = accounts.map((a) => a.id); + dispatch(importFetchedAccounts(accounts)); + if (withRelationships) { + dispatch(fetchRelationships(accountIds)); + } + setDefaultAccounts(accounts); + setLoadingState('idle'); + } catch { + setLoadingState('error'); + } + } + void doRequest(); + }, [ + currentUserId, accounts, + dispatch, + filterResults, + loadingState, + withRelationships, + defaultAccounts, + withDefaultFollows, + ]); + + return { + searchAccounts: startSearch, + resetAccounts, + accounts: + accounts.length === 0 && withDefaultFollows + ? (defaultAccounts ?? []) + : accounts, isLoading: loadingState === 'loading', isError: loadingState === 'error', }; From a86f3a4000bac9d502f5a7613f2fa544755a8321 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 29 May 2026 14:48:12 +0200 Subject: [PATCH 25/34] Use new Collections endpoint version (#39214) --- app/javascript/mastodon/api/collections.ts | 26 ++++++++-------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/app/javascript/mastodon/api/collections.ts b/app/javascript/mastodon/api/collections.ts index 7f39651f02..badd3b4397 100644 --- a/app/javascript/mastodon/api/collections.ts +++ b/app/javascript/mastodon/api/collections.ts @@ -15,48 +15,40 @@ import type { } from '../api_types/collections'; export const apiCreateCollection = (collection: ApiCreateCollectionPayload) => - apiRequestPost('v1_alpha/collections', collection); + apiRequestPost('v1/collections', collection); export const apiUpdateCollection = ({ id, ...collection }: ApiUpdateCollectionPayload) => - apiRequestPut( - `v1_alpha/collections/${id}`, - collection, - ); + apiRequestPut(`v1/collections/${id}`, collection); export const apiDeleteCollection = (collectionId: string) => - apiRequestDelete(`v1_alpha/collections/${collectionId}`); + apiRequestDelete(`v1/collections/${collectionId}`); export const apiGetCollection = (collectionId: string) => apiRequestGet( - `v1_alpha/collections/${collectionId}`, + `v1/collections/${collectionId}`, ); export const apiGetCollectionsCreatedByAccount = (accountId: string) => - apiRequestGet( - `v1_alpha/accounts/${accountId}/collections`, - ); + apiRequestGet(`v1/accounts/${accountId}/collections`); export const apiGetCollectionsFeaturingAccount = (accountId: string) => - apiRequestGet( - `v1_alpha/accounts/${accountId}/in_collections`, - ); + apiRequestGet(`v1/accounts/${accountId}/in_collections`); export const apiAddCollectionItem = (collectionId: string, accountId: string) => apiRequestPost( - `v1_alpha/collections/${collectionId}/items`, + `v1/collections/${collectionId}/items`, { account_id: accountId }, ); export const apiRemoveCollectionItem = (collectionId: string, itemId: string) => apiRequestDelete( - `v1_alpha/collections/${collectionId}/items/${itemId}`, + `v1/collections/${collectionId}/items/${itemId}`, ); export const apiRevokeCollectionInclusion = ( collectionId: string, itemId: string, -) => - apiRequestPost(`v1_alpha/collections/${collectionId}/items/${itemId}/revoke`); +) => apiRequestPost(`v1/collections/${collectionId}/items/${itemId}/revoke`); From 7fba458d9227bea224a6c779f895c4945005f074 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 29 May 2026 15:07:00 +0200 Subject: [PATCH 26/34] Remove frontend check for collections feature flag (#39215) --- .../components/empty_message.tsx | 78 +++++-------- .../features/account_featured/index.tsx | 106 ++++++++---------- .../collections/overview/created_by_you.tsx | 3 +- .../collections/overview/featuring_you.tsx | 3 +- .../mastodon/features/collections/utils.ts | 5 - .../features/navigation_panel/index.tsx | 23 ++-- .../features/ui/components/block_modal.jsx | 11 +- app/javascript/mastodon/features/ui/index.jsx | 11 +- app/javascript/mastodon/locales/en.json | 2 - app/javascript/mastodon/utils/environment.ts | 2 +- 10 files changed, 98 insertions(+), 146 deletions(-) diff --git a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx index aa1dc4078e..ad903d5a5d 100644 --- a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx +++ b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx @@ -9,7 +9,6 @@ import { Button } from '@/mastodon/components/button'; import { DisplayName } from '@/mastodon/components/display_name'; import { EmptyState } from '@/mastodon/components/empty_state'; import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint'; -import { areCollectionsEnabled } from '@/mastodon/features/collections/utils'; import { useAccount } from '@/mastodon/hooks/useAccount'; import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId'; import { useAppDispatch } from '@/mastodon/store'; @@ -50,56 +49,39 @@ export const EmptyMessage: React.FC = ({ let title: React.ReactNode = null; let message: React.ReactNode = null; - const hasCollections = areCollectionsEnabled(); - if (me === accountId) { - if (hasCollections) { - // Return only here to insert the "Create a collection" button as the action for the empty state. - return ( - + } + message={ + + } + > + {!withoutAddCollectionButton && ( + - } - message={ - - } - > - {!withoutAddCollectionButton && ( - - - - )} - - - ); - } else { - title = ( - - ); - message = ( - - ); - } + + )} + + + ); } else if (suspended) { title = ( = ({ multiColumn, }) => { @@ -98,14 +95,11 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ ); const hasCollections = - collectionsEnabled && - collectionsLoadStatus === 'idle' && - listedCollections.length > 0; + collectionsLoadStatus === 'idle' && listedCollections.length > 0; const hasFeaturedAccounts = !featuredAccountIds.isEmpty(); - const isLoading = - !accountId || (collectionsEnabled && collectionsLoadStatus !== 'idle'); + const isLoading = !accountId || collectionsLoadStatus !== 'idle'; if (accountId === null) { return ; @@ -165,57 +159,53 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ )} - {collectionsEnabled && ( - <> - -

- -

- {accountId === me && ( - - - - )} -
- {hasCollections ? ( - - - ), - subtitle: ( - - ), - }} - renderListItem={renderListItem} - /> - - ) : ( -
+ } + /> + {((this.props.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (

diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings_group.tsx b/app/javascript/flavours/glitch/features/notifications/components/column_settings_group.tsx new file mode 100644 index 0000000000..bbafac7de7 --- /dev/null +++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings_group.tsx @@ -0,0 +1,162 @@ +import { useCallback } from 'react'; +import type { FC, ReactNode } from 'react'; + +import { defineMessages, FormattedMessage } from 'react-intl'; + +import { showAlert } from '@/flavours/glitch/actions/alerts'; +import { requestBrowserPermission } from '@/flavours/glitch/actions/notifications'; +import { changeAlerts } from '@/flavours/glitch/actions/push_notifications'; +import { changeSetting } from '@/flavours/glitch/actions/settings'; +import { + createAppSelector, + useAppDispatch, + useAppSelector, +} from '@/flavours/glitch/store'; + +import PillBarButton from './pill_bar_button'; + +const selectNotificationSettings = createAppSelector( + [ + (state) => + (state.settings as Immutable.Map).get( + 'notifications', + ) as Immutable.Map | boolean>, + (state) => state.notifications.get('browserPermission') as string, + (state) => state.push_notifications, + ], + (settings, browserPermission, pushSettings) => ({ + settings, + browserPermission: browserPermission !== 'denied', + pushSettings, + showPushSettings: + pushSettings.get('supported') && pushSettings.get('enabled'), + }), +); + +const messages = defineMessages({ + permissionDenied: { + id: 'notifications.permission_denied_alert', + defaultMessage: + "Desktop notifications can't be enabled, as browser permission has been denied before", + }, +}); + +type SettingsType = 'alerts' | 'shows' | 'sounds'; + +export const ColumnSettingsGroup: FC<{ label: ReactNode; type: string }> = ({ + label, + type, +}) => { + const { settings, browserPermission, pushSettings, showPushSettings } = + useAppSelector(selectNotificationSettings); + + const dispatch = useAppDispatch(); + const handleChange = useCallback( + (path: [SettingsType, string], checked: boolean) => { + if ( + path[0] === 'alerts' && + checked && + typeof window.Notification !== 'undefined' && + Notification.permission !== 'granted' + ) { + dispatch( + requestBrowserPermission((permission) => { + if (permission === 'granted') { + dispatch(changeSetting(['notifications', ...path], checked)); + } else { + dispatch(showAlert({ message: messages.permissionDenied })); + } + }), + ); + } else { + dispatch(changeSetting(['notifications', ...path], checked)); + } + }, + [dispatch], + ); + const handlePushChange = useCallback( + (path: string[], checked: boolean) => { + if ( + checked && + typeof window.Notification !== 'undefined' && + Notification.permission !== 'granted' + ) { + dispatch( + requestBrowserPermission((permission: NotificationPermission) => { + if (permission === 'granted') { + dispatch(changeAlerts(path, checked)); + } else { + dispatch(showAlert({ message: messages.permissionDenied })); + } + }), + ); + } else { + dispatch(changeAlerts(path, checked)); + } + }, + [dispatch], + ); + + return ( +
+

{label}

+ +
+ + } + /> + + {showPushSettings && ( + + } + /> + )} + + + } + /> + + + } + /> +
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx b/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx index 84aebef79f..1ddcf0bf74 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import CollectionsIcon from '@/material-icons/400-24px/category.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; @@ -27,6 +28,10 @@ const tooltips = defineMessages({ boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, + collections: { + id: 'notifications.filter.collections', + defaultMessage: 'Collections', + }, statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow', @@ -130,6 +135,14 @@ export const FilterBar: React.FC = () => { > + + + ); else diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js index a9d2953c85..ca52994e1f 100644 --- a/app/javascript/flavours/glitch/reducers/settings.js +++ b/app/javascript/flavours/glitch/reducers/settings.js @@ -42,6 +42,7 @@ const initialState = ImmutableMap({ poll: false, status: false, update: false, + collections: false, 'admin.sign_up': false, 'admin.report': false, }), @@ -66,6 +67,7 @@ const initialState = ImmutableMap({ poll: true, status: true, update: true, + collections: true, 'admin.sign_up': true, 'admin.report': true, }), @@ -80,6 +82,7 @@ const initialState = ImmutableMap({ poll: true, status: true, update: true, + collections: true, 'admin.sign_up': true, 'admin.report': true, }), diff --git a/app/javascript/flavours/glitch/selectors/notifications.ts b/app/javascript/flavours/glitch/selectors/notifications.ts index a27876d6c3..df31fe2ce8 100644 --- a/app/javascript/flavours/glitch/selectors/notifications.ts +++ b/app/javascript/flavours/glitch/selectors/notifications.ts @@ -29,7 +29,10 @@ const filterNotificationsByAllowedTypes = ( (item) => item.type === 'gap' || allowedType === item.type || - (allowedType === 'mention' && item.type === 'quote'), + (allowedType === 'mention' && item.type === 'quote') || + (allowedType === 'collection' && + (item.type === 'collection_update' || + item.type === 'added_to_collection')), ); }; diff --git a/app/javascript/flavours/glitch/selectors/settings.ts b/app/javascript/flavours/glitch/selectors/settings.ts index 5229b15cf0..067ac34a76 100644 --- a/app/javascript/flavours/glitch/selectors/settings.ts +++ b/app/javascript/flavours/glitch/selectors/settings.ts @@ -17,10 +17,19 @@ export const selectSettingsNotificationsShows = createSelector( export const selectSettingsNotificationsExcludedTypes = createSelector( [selectSettingsNotificationsShows], - (shows) => - Object.entries(shows) - .filter(([_type, enabled]) => !enabled) - .map(([type, _enabled]) => type), + (shows) => { + const excludedTypes: string[] = []; + for (const key in shows) { + if (!shows[key]) { + if (key === 'collections') { + excludedTypes.push('collection_update', 'added_to_collection'); + } else { + excludedTypes.push(key); + } + } + } + return excludedTypes; + }, ); export const selectSettingsNotificationsQuickFilterShow = (state: RootState) => From 121839f262c639e3f0d869a167de9eb15bf7532c Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 29 May 2026 13:53:03 +0200 Subject: [PATCH 30/34] [Glitch] Collections: Add default recommendations Port fa1e16ed9fe5633d9e86468d57b2040608677c37 to glitch-soc Signed-off-by: Claire --- .../components/form_fields/combobox_field.tsx | 17 +++ .../features/collections/editor/accounts.tsx | 2 + .../glitch/features/collections/utils.ts | 6 +- .../glitch/hooks/useSearchAccounts.ts | 117 ++++++++++++++---- 4 files changed, 112 insertions(+), 30 deletions(-) diff --git a/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx b/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx index 654507fe03..b856c4b32d 100644 --- a/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx @@ -100,6 +100,10 @@ interface ComboboxProps< * Icon to be displayed in the text input */ icon?: TextInputProps['icon'] | null; + /** + * Set to true to open as soon as there is focus + */ + openOnFocus?: boolean; /** * Set to false to keep the menu open when an item is selected */ @@ -217,8 +221,10 @@ const ComboboxWithRef = ( renderGroupTitle, renderItem, onSelectItem, + onFocus, onChange, onKeyDown, + openOnFocus = false, closeOnSelect = true, suppressMenu = false, icon = SearchIcon, @@ -288,6 +294,16 @@ const ComboboxWithRef = ( } }, []); + const handleFocus: React.FocusEventHandler = useCallback( + (e) => { + if (openOnFocus) { + setShouldMenuOpen(true); + } + onFocus?.(e); + }, + [onFocus, openOnFocus], + ); + const handleInputChange = useCallback( (e: React.ChangeEvent) => { onChange(e); @@ -487,6 +503,7 @@ const ComboboxWithRef = ( autoComplete='off' spellCheck='false' value={value} + onFocus={handleFocus} onChange={handleInputChange} onKeyDown={handleInputKeyDown} icon={icon ?? undefined} diff --git a/app/javascript/flavours/glitch/features/collections/editor/accounts.tsx b/app/javascript/flavours/glitch/features/collections/editor/accounts.tsx index 50c7bcc31f..1bdb6e84f0 100644 --- a/app/javascript/flavours/glitch/features/collections/editor/accounts.tsx +++ b/app/javascript/flavours/glitch/features/collections/editor/accounts.tsx @@ -218,6 +218,7 @@ export const CollectionAccounts: React.FC<{ resetAccounts, } = useSearchAccounts({ withRelationships: true, + withDefaultFollows: searchValue === '', // Don't suggest accounts that were already added filterResults: (account) => !editorItems.find((item) => item.account_id === account.id), @@ -366,6 +367,7 @@ export const CollectionAccounts: React.FC<{ )} {hasPendingItems && } `/collections/${id}`; -export const canAccountBeAdded = (account: ApiMutedAccountJSON | Account) => +export const canAccountBeAdded = (account: ApiAccountJSON | Account) => ['automatic', 'manual'].includes(account.feature_approval.current_user); export const canAccountBeAddedByFollowers = ( - account: ApiMutedAccountJSON | Account, + account: ApiAccountJSON | Account, ) => account.feature_approval.automatic.includes('followers') || account.feature_approval.manual.includes('followers'); diff --git a/app/javascript/flavours/glitch/hooks/useSearchAccounts.ts b/app/javascript/flavours/glitch/hooks/useSearchAccounts.ts index f9af2a1eb6..d7f1eff1b3 100644 --- a/app/javascript/flavours/glitch/hooks/useSearchAccounts.ts +++ b/app/javascript/flavours/glitch/hooks/useSearchAccounts.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useDebouncedCallback } from 'use-debounce'; @@ -8,16 +8,20 @@ import { apiRequest } from 'flavours/glitch/api'; import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts'; import { useAppDispatch } from 'flavours/glitch/store'; +import { useCurrentAccountId } from './useAccountId'; + export function useSearchAccounts({ onSettled, filterResults, resetOnInputClear = true, withRelationships = false, + withDefaultFollows = false, }: { onSettled?: (value: string) => void; filterResults?: (account: ApiAccountJSON) => boolean; resetOnInputClear?: boolean; withRelationships?: boolean; + withDefaultFollows?: boolean; } = {}) { const dispatch = useAppDispatch(); @@ -29,7 +33,7 @@ export function useSearchAccounts({ const searchRequestRef = useRef(null); const searchAccounts = useDebouncedCallback( - (value: string) => { + async (value: string) => { if (searchRequestRef.current) { searchRequestRef.current.abort(); } @@ -46,41 +50,100 @@ export function useSearchAccounts({ searchRequestRef.current = new AbortController(); - void apiRequest('GET', 'v1/accounts/search', { - signal: searchRequestRef.current.signal, - params: { - q: value, - resolve: true, - }, - }) - .then((data) => { - const accounts = filterResults ? data.filter(filterResults) : data; - const accountIds = accounts.map((a) => a.id); - dispatch(importFetchedAccounts(accounts)); - if (withRelationships) { - dispatch(fetchRelationships(accountIds)); - } - setAccounts(accounts); - setLoadingState('idle'); - onSettled?.(value); - }) - .catch(() => { - setLoadingState('error'); - onSettled?.(value); - }); + try { + const data = await apiRequest( + 'GET', + 'v1/accounts/search', + { + signal: searchRequestRef.current.signal, + params: { + q: value, + resolve: true, + }, + }, + ); + const accounts = filterResults ? data.filter(filterResults) : data; + const accountIds = accounts.map((a) => a.id); + dispatch(importFetchedAccounts(accounts)); + if (withRelationships) { + dispatch(fetchRelationships(accountIds)); + } + setAccounts(accounts); + setLoadingState('idle'); + onSettled?.(value); + } catch { + setLoadingState('error'); + onSettled?.(value); + } }, 500, { leading: true, trailing: true }, ); + const startSearch = useCallback( + (value: string) => { + void searchAccounts(value); + }, + [searchAccounts], + ); + const resetAccounts = useCallback(() => { setAccounts([]); }, []); - return { - searchAccounts, - resetAccounts, + const currentUserId = useCurrentAccountId(); + const [defaultAccounts, setDefaultAccounts] = useState< + ApiAccountJSON[] | null + >(null); + useEffect(() => { + if ( + !currentUserId || + loadingState !== 'idle' || + defaultAccounts !== null || + !withDefaultFollows + ) { + return; + } + + async function doRequest() { + setLoadingState('loading'); + try { + const data = await apiRequest( + 'GET', + `v1/accounts/${currentUserId}/following`, + { params: { limit: 40 } }, + ); + const accounts = filterResults ? data.filter(filterResults) : data; + const accountIds = accounts.map((a) => a.id); + dispatch(importFetchedAccounts(accounts)); + if (withRelationships) { + dispatch(fetchRelationships(accountIds)); + } + setDefaultAccounts(accounts); + setLoadingState('idle'); + } catch { + setLoadingState('error'); + } + } + void doRequest(); + }, [ + currentUserId, accounts, + dispatch, + filterResults, + loadingState, + withRelationships, + defaultAccounts, + withDefaultFollows, + ]); + + return { + searchAccounts: startSearch, + resetAccounts, + accounts: + accounts.length === 0 && withDefaultFollows + ? (defaultAccounts ?? []) + : accounts, isLoading: loadingState === 'loading', isError: loadingState === 'error', }; From bc9732c26cb2b79ce3c0642eba65b6f4c6b701ca Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 29 May 2026 14:48:12 +0200 Subject: [PATCH 31/34] [Glitch] Use new Collections endpoint version Port a86f3a4000bac9d502f5a7613f2fa544755a8321 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/api/collections.ts | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/app/javascript/flavours/glitch/api/collections.ts b/app/javascript/flavours/glitch/api/collections.ts index 76a29f2d66..521f5bf25a 100644 --- a/app/javascript/flavours/glitch/api/collections.ts +++ b/app/javascript/flavours/glitch/api/collections.ts @@ -15,48 +15,40 @@ import type { } from '../api_types/collections'; export const apiCreateCollection = (collection: ApiCreateCollectionPayload) => - apiRequestPost('v1_alpha/collections', collection); + apiRequestPost('v1/collections', collection); export const apiUpdateCollection = ({ id, ...collection }: ApiUpdateCollectionPayload) => - apiRequestPut( - `v1_alpha/collections/${id}`, - collection, - ); + apiRequestPut(`v1/collections/${id}`, collection); export const apiDeleteCollection = (collectionId: string) => - apiRequestDelete(`v1_alpha/collections/${collectionId}`); + apiRequestDelete(`v1/collections/${collectionId}`); export const apiGetCollection = (collectionId: string) => apiRequestGet( - `v1_alpha/collections/${collectionId}`, + `v1/collections/${collectionId}`, ); export const apiGetCollectionsCreatedByAccount = (accountId: string) => - apiRequestGet( - `v1_alpha/accounts/${accountId}/collections`, - ); + apiRequestGet(`v1/accounts/${accountId}/collections`); export const apiGetCollectionsFeaturingAccount = (accountId: string) => - apiRequestGet( - `v1_alpha/accounts/${accountId}/in_collections`, - ); + apiRequestGet(`v1/accounts/${accountId}/in_collections`); export const apiAddCollectionItem = (collectionId: string, accountId: string) => apiRequestPost( - `v1_alpha/collections/${collectionId}/items`, + `v1/collections/${collectionId}/items`, { account_id: accountId }, ); export const apiRemoveCollectionItem = (collectionId: string, itemId: string) => apiRequestDelete( - `v1_alpha/collections/${collectionId}/items/${itemId}`, + `v1/collections/${collectionId}/items/${itemId}`, ); export const apiRevokeCollectionInclusion = ( collectionId: string, itemId: string, -) => - apiRequestPost(`v1_alpha/collections/${collectionId}/items/${itemId}/revoke`); +) => apiRequestPost(`v1/collections/${collectionId}/items/${itemId}/revoke`); From ac0bacbcc06994b9b9eb5cd3c4112e4c2accb234 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 29 May 2026 15:07:00 +0200 Subject: [PATCH 32/34] [Glitch] Remove frontend check for collections feature flag Port 7fba458d9227bea224a6c779f895c4945005f074 to glitch-soc Signed-off-by: Claire --- .../components/empty_message.tsx | 78 +++++-------- .../features/account_featured/index.tsx | 106 ++++++++---------- .../collections/overview/created_by_you.tsx | 3 +- .../collections/overview/featuring_you.tsx | 3 +- .../glitch/features/collections/utils.ts | 5 - .../features/navigation_panel/index.tsx | 23 ++-- .../features/ui/components/block_modal.jsx | 11 +- .../flavours/glitch/features/ui/index.jsx | 11 +- .../flavours/glitch/utils/environment.ts | 2 +- 9 files changed, 98 insertions(+), 144 deletions(-) diff --git a/app/javascript/flavours/glitch/features/account_featured/components/empty_message.tsx b/app/javascript/flavours/glitch/features/account_featured/components/empty_message.tsx index 2b29bf4238..4eb367fb14 100644 --- a/app/javascript/flavours/glitch/features/account_featured/components/empty_message.tsx +++ b/app/javascript/flavours/glitch/features/account_featured/components/empty_message.tsx @@ -9,7 +9,6 @@ import { Button } from '@/flavours/glitch/components/button'; import { DisplayName } from '@/flavours/glitch/components/display_name'; import { EmptyState } from '@/flavours/glitch/components/empty_state'; import { LimitedAccountHint } from '@/flavours/glitch/components/limited_account_hint'; -import { areCollectionsEnabled } from '@/flavours/glitch/features/collections/utils'; import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId'; import { useAppDispatch } from '@/flavours/glitch/store'; @@ -50,56 +49,39 @@ export const EmptyMessage: React.FC = ({ let title: React.ReactNode = null; let message: React.ReactNode = null; - const hasCollections = areCollectionsEnabled(); - if (me === accountId) { - if (hasCollections) { - // Return only here to insert the "Create a collection" button as the action for the empty state. - return ( - + } + message={ + + } + > + {!withoutAddCollectionButton && ( + - } - message={ - - } - > - {!withoutAddCollectionButton && ( - - - - )} - - - ); - } else { - title = ( - - ); - message = ( - - ); - } + + )} + + + ); } else if (suspended) { title = ( = ({ multiColumn, }) => { @@ -98,14 +95,11 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ ); const hasCollections = - collectionsEnabled && - collectionsLoadStatus === 'idle' && - listedCollections.length > 0; + collectionsLoadStatus === 'idle' && listedCollections.length > 0; const hasFeaturedAccounts = !featuredAccountIds.isEmpty(); - const isLoading = - !accountId || (collectionsEnabled && collectionsLoadStatus !== 'idle'); + const isLoading = !accountId || collectionsLoadStatus !== 'idle'; if (accountId === null) { return ; @@ -165,57 +159,53 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ )} - {collectionsEnabled && ( - <> - -

- -

- {accountId === me && ( - - - - )} -
- {hasCollections ? ( - - - ), - subtitle: ( - - ), - }} - renderListItem={renderListItem} - /> - - ) : ( -