From 22c1b6f3eec14062c6e0950fdb2d436c34430543 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 10 Jan 2025 15:34:18 -0500
Subject: [PATCH 01/26] Fix `Invite#code` changing value on every save (#33550)

---
 app/models/invite.rb       |  2 +-
 spec/models/invite_spec.rb | 45 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+), 1 deletion(-)

diff --git a/app/models/invite.rb b/app/models/invite.rb
index d1981f16ad..9437ebee60 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -31,7 +31,7 @@ class Invite < ApplicationRecord
 
   validates :comment, length: { maximum: COMMENT_SIZE_LIMIT }
 
-  before_validation :set_code
+  before_validation :set_code, on: :create
 
   def valid_for_use?
     (max_uses.nil? || uses < max_uses) && !expired? && user&.functional?
diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb
index e85885a8d8..6363f77a64 100644
--- a/spec/models/invite_spec.rb
+++ b/spec/models/invite_spec.rb
@@ -5,6 +5,29 @@ require 'rails_helper'
 RSpec.describe Invite do
   include_examples 'Expireable'
 
+  describe 'Associations' do
+    it { is_expected.to belong_to(:user).inverse_of(:invites) }
+    it { is_expected.to have_many(:users).inverse_of(:invite) }
+  end
+
+  describe 'Validations' do
+    it { is_expected.to validate_length_of(:comment).is_at_most(described_class::COMMENT_SIZE_LIMIT) }
+  end
+
+  describe 'Scopes' do
+    describe '.available' do
+      let!(:no_expires) { Fabricate :invite, expires_at: nil }
+      let!(:past_expires) { Fabricate :invite, expires_at: 2.days.ago }
+      let!(:future_expires) { Fabricate :invite, expires_at: 2.days.from_now }
+
+      it 'returns future and non-epiring records' do
+        expect(described_class.available)
+          .to include(no_expires, future_expires)
+          .and not_include(past_expires)
+      end
+    end
+  end
+
   describe '#valid_for_use?' do
     it 'returns true when there are no limitations' do
       invite = Fabricate(:invite, max_uses: nil, expires_at: nil)
@@ -37,4 +60,26 @@ RSpec.describe Invite do
       expect(invite.valid_for_use?).to be false
     end
   end
+
+  describe 'Callbacks' do
+    describe 'Setting the invite code' do
+      context 'when creating a new record' do
+        subject { Fabricate.build :invite }
+
+        it 'sets a code value' do
+          expect { subject.save }
+            .to change(subject, :code).from(be_blank).to(be_present)
+        end
+      end
+
+      context 'when updating a record' do
+        subject { Fabricate :invite }
+
+        it 'does not change the code value' do
+          expect { subject.update(max_uses: 123_456) }
+            .to not_change(subject, :code)
+        end
+      end
+    end
+  end
 end

From f3f6b65db4ca4d1f14b610dd7f6036f8516a6fa8 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 13 Jan 2025 09:47:43 +0100
Subject: [PATCH 02/26] Update dependency
 @babel/plugin-transform-nullish-coalescing-operator to v7.26.5 (#33553)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index 0e40cc7221..db96b75974 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -238,10 +238,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.0, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.8.0":
-  version: 7.25.9
-  resolution: "@babel/helper-plugin-utils@npm:7.25.9"
-  checksum: 10c0/483066a1ba36ff16c0116cd24f93de05de746a603a777cd695ac7a1b034928a65a4ecb35f255761ca56626435d7abdb73219eba196f9aa83b6c3c3169325599d
+"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.0, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.8.0":
+  version: 7.26.5
+  resolution: "@babel/helper-plugin-utils@npm:7.26.5"
+  checksum: 10c0/cdaba71d4b891aa6a8dfbe5bac2f94effb13e5fa4c2c487667fdbaa04eae059b78b28d85a885071f45f7205aeb56d16759e1bed9c118b94b16e4720ef1ab0f65
   languageName: node
   linkType: hard
 
@@ -935,13 +935,13 @@ __metadata:
   linkType: hard
 
 "@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.3, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.25.9":
-  version: 7.25.9
-  resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.25.9"
+  version: 7.26.5
+  resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.26.5"
   dependencies:
-    "@babel/helper-plugin-utils": "npm:^7.25.9"
+    "@babel/helper-plugin-utils": "npm:^7.26.5"
   peerDependencies:
     "@babel/core": ^7.0.0-0
-  checksum: 10c0/eb623db5be078a1c974afe7c7797b0309ba2ea9e9237c0b6831ade0f56d8248bb4ab3432ab34495ff8c877ec2fe412ff779d1e9b3c2b8139da18e1753d950bc3
+  checksum: 10c0/2e4b84745f9e8c40caf3e611641de4d6c7da6f96c2925b7fe568e3b031ed1864e325b9dffc9cda4e442fc40be43ffabb088782e980d411e0562bd5222df547ec
   languageName: node
   linkType: hard
 

From 44d9dc4bb0cf64fb1561adc6c4fab60316a6a43b Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 13 Jan 2025 09:51:46 +0100
Subject: [PATCH 03/26] Update dependency pino-http to v10.4.0 (#33560)

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 db96b75974..0b3b8b3d95 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13306,14 +13306,14 @@ __metadata:
   linkType: hard
 
 "pino-http@npm:^10.0.0":
-  version: 10.3.0
-  resolution: "pino-http@npm:10.3.0"
+  version: 10.4.0
+  resolution: "pino-http@npm:10.4.0"
   dependencies:
     get-caller-file: "npm:^2.0.5"
     pino: "npm:^9.0.0"
     pino-std-serializers: "npm:^7.0.0"
     process-warning: "npm:^4.0.0"
-  checksum: 10c0/da95d93e1176c02201f9b9bb0af53ad737105c5772acbb077dcad0f52ebce2438e0e9fc8216cd96396d1305d0ecf1f1d23142c7a50110a701ea093b2ee999ea7
+  checksum: 10c0/64144e2c94e939070f56ad82dfb012b6a98d21582e0660cf821e7cee64d4e06f7724aa40bc5bf9cd1254d58ab7cbd972dec287b7989eba647d384f6edd8d95fd
   languageName: node
   linkType: hard
 

From 99637f2deb517bc21506af18491e49a8af5e5fcb Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 13 Jan 2025 08:53:56 +0000
Subject: [PATCH 04/26] Update dependency ox to v2.14.20 (#33567)

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 cb5e2bbbae..4603530de0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -555,7 +555,7 @@ GEM
       opentelemetry-api (~> 1.0)
     orm_adapter (0.5.0)
     ostruct (0.6.1)
-    ox (2.14.19)
+    ox (2.14.20)
       bigdecimal (>= 3.0)
     parallel (1.26.3)
     parser (3.3.6.0)

From 53885b0fdb8b25bb0d531df2cd90bdb223e21dc6 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 13 Jan 2025 10:13:47 +0100
Subject: [PATCH 05/26] New Crowdin Translations (automated) (#33559)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/cs.json    |  72 +++++++-
 app/javascript/mastodon/locales/ga.json    |  12 ++
 app/javascript/mastodon/locales/ia.json    |   2 +-
 app/javascript/mastodon/locales/it.json    |   5 +-
 app/javascript/mastodon/locales/ja.json    |   8 +
 app/javascript/mastodon/locales/ne.json    |  65 ++++++-
 app/javascript/mastodon/locales/pl.json    |   2 +
 app/javascript/mastodon/locales/pt-BR.json |   8 +-
 app/javascript/mastodon/locales/pt-PT.json |  16 +-
 app/javascript/mastodon/locales/sk.json    |   2 +
 app/javascript/mastodon/locales/vi.json    |   4 +-
 config/locales/activerecord.cs.yml         |  13 ++
 config/locales/activerecord.ga.yml         |   2 +
 config/locales/activerecord.it.yml         |   2 +
 config/locales/activerecord.ne.yml         |  34 ++++
 config/locales/activerecord.pt-PT.yml      |   2 +-
 config/locales/cs.yml                      | 205 ++++++++++++++++++++-
 config/locales/eo.yml                      |   6 +
 config/locales/pl.yml                      |   3 +
 config/locales/pt-PT.yml                   |   8 +-
 config/locales/simple_form.cs.yml          |  14 ++
 config/locales/simple_form.eo.yml          |   7 +
 config/locales/simple_form.pt-PT.yml       |   2 +-
 23 files changed, 459 insertions(+), 35 deletions(-)

diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index ba58b998f1..81540c823c 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -104,10 +104,11 @@
   "annual_report.summary.most_used_hashtag.none": "Žádné",
   "annual_report.summary.new_posts.new_posts": "nové příspěvky",
   "annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do vrcholu</topLabel><percentage></percentage><bottomLabel>{domain} uživatelů.</bottomLabel>",
+  "annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka zůstane mezi námi ;).",
   "annual_report.summary.thanks": "Děkujeme, že jste součástí Mastodonu!",
   "attachments_list.unprocessed": "(nezpracováno)",
   "audio.hide": "Skrýt zvuk",
-  "block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou stále být viditelné pro nepřihlášené uživatele.",
+  "block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou být stále viditelné pro nepřihlášené uživatele.",
   "block_modal.show_less": "Zobrazit méně",
   "block_modal.show_more": "Zobrazit více",
   "block_modal.they_cant_mention": "Nemůže vás zmiňovat ani sledovat.",
@@ -180,6 +181,7 @@
   "compose_form.poll.duration": "Doba trvání ankety",
   "compose_form.poll.multiple": "Výběr z více možností",
   "compose_form.poll.option_placeholder": "Volba {number}",
+  "compose_form.poll.single": "Jediná volba",
   "compose_form.poll.switch_to_multiple": "Povolit u ankety výběr více voleb",
   "compose_form.poll.switch_to_single": "Povolit u ankety výběr pouze jedné volby",
   "compose_form.poll.type": "Styl",
@@ -204,6 +206,7 @@
   "confirmations.edit.message": "Editovat teď znamená přepsání zprávy, kterou právě tvoříte. Opravdu chcete pokračovat?",
   "confirmations.edit.title": "Přepsat příspěvek?",
   "confirmations.follow_to_list.confirm": "Sledovat a přidat do seznamu",
+  "confirmations.follow_to_list.message": "Musíte {name} sledovat, abyste je přidali do seznamu.",
   "confirmations.follow_to_list.title": "Sledovat uživatele?",
   "confirmations.logout.confirm": "Odhlásit se",
   "confirmations.logout.message": "Opravdu se chcete odhlásit?",
@@ -236,19 +239,25 @@
   "disabled_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován.",
   "dismissable_banner.community_timeline": "Toto jsou nejnovější veřejné příspěvky od lidí, jejichž účty hostuje {domain}.",
   "dismissable_banner.dismiss": "Zavřít",
+  "dismissable_banner.explore_links": "Tyto zprávy jsou dnes nejvíce sdíleny ve fediversu. Novější novinky publikované více různými lidmi jsou v pořadí vyšší.",
+  "dismissable_banner.explore_statuses": "Tyto příspěvky napříč fediversem dnes získávají na popularitě. Novější příspěvky s více boosty a oblíbenými jsou výše v pořadí.",
+  "dismissable_banner.explore_tags": "Tyto hashtagy dnes na fediversu získávají na popularitě. Hashtagy, které používá více různých lidí, jsou řazeny výše.",
+  "dismissable_banner.public_timeline": "Toto jsou nejnovější veřejné příspěvky od lidí na fediversu, které lidé na {domain} sledují.",
   "domain_block_modal.block": "Blokovat server",
   "domain_block_modal.block_account_instead": "Raději blokovat @{name}",
   "domain_block_modal.they_can_interact_with_old_posts": "Lidé z tohoto serveru mohou interagovat s vašimi starými příspěvky.",
   "domain_block_modal.they_cant_follow": "Nikdo z tohoto serveru vás nemůže sledovat.",
   "domain_block_modal.they_wont_know": "Nebude vědět, že je zablokován*a.",
   "domain_block_modal.title": "Blokovat doménu?",
+  "domain_block_modal.you_will_lose_num_followers": "Ztratíte {followersCount, plural, one {{followersCountDisplay} sledujícího} few {{followersCountDisplay} sledující} many {{followersCountDisplay} sledujících} other {{followersCountDisplay} sledujících}} a {followingCount, plural, one {{followingCountDisplay} sledovaného} few {{followingCountDisplay} sledované} many {{followingCountDisplay} sledovaných} other {{followingCountDisplay} sledovaných}}.",
+  "domain_block_modal.you_will_lose_relationships": "Ztratíte všechny sledující a lidi, které sledujete z tohoto serveru.",
   "domain_block_modal.you_wont_see_posts": "Neuvidíte příspěvky ani upozornění od uživatelů z tohoto serveru.",
   "domain_pill.activitypub_lets_connect": "Umožňuje vám spojit se a komunikovat s lidmi nejen na Mastodonu, ale i s dalšími sociálními aplikacemi.",
   "domain_pill.activitypub_like_language": "ActivityPub je jako jazyk, kterým Mastodon mluví s jinými sociálními sítěmi.",
   "domain_pill.server": "Server",
   "domain_pill.their_handle": "Handle:",
-  "domain_pill.their_server": "Digitální domov, kde žijí všechny příspěvky.",
-  "domain_pill.their_username": "Jedinečný identikátor na serveru. Je možné najít uživatele se stejným uživatelským jménem na různých serverech.",
+  "domain_pill.their_server": "Jejich digitální domov, kde žijí jejich všechny příspěvky.",
+  "domain_pill.their_username": "Jejich jedinečný identikátor na jejich serveru. Je možné najít uživatele se stejným uživatelským jménem na jiných serverech.",
   "domain_pill.username": "Uživatelské jméno",
   "domain_pill.whats_in_a_handle": "Co obsahuje handle?",
   "domain_pill.who_they_are": "Protože handle říkají kdo je kdo a také kde, je možné interagovat s lidmi napříč sociálními weby <button>platforem postavených na ActivityPub</button>.",
@@ -322,6 +331,7 @@
   "filter_modal.select_filter.title": "Filtrovat tento příspěvek",
   "filter_modal.title.status": "Filtrovat příspěvek",
   "filter_warning.matches_filter": "Odpovídá filtru “<span>{title}</span>”",
+  "filtered_notifications_banner.pending_requests": "Od {count, plural, =0 {nikoho, koho možná znáte} one {člověka, kterého možná znáte} few {#, které možná znáte} many {#, které možná znáte} other {#, které možná znáte}}",
   "filtered_notifications_banner.title": "Filtrovaná oznámení",
   "firehose.all": "Vše",
   "firehose.local": "Tento server",
@@ -329,14 +339,14 @@
   "follow_request.authorize": "Autorizovat",
   "follow_request.reject": "Zamítnout",
   "follow_requests.unlocked_explanation": "Přestože váš účet není uzamčen, personál {domain} usoudil, že byste mohli chtít tyto požadavky na sledování zkontrolovat ručně.",
-  "follow_suggestions.curated_suggestion": "Výběr personálů",
+  "follow_suggestions.curated_suggestion": "Výběr personálu",
   "follow_suggestions.dismiss": "Znovu nezobrazovat",
   "follow_suggestions.featured_longer": "Ručně vybráno týmem {domain}",
   "follow_suggestions.friends_of_friends_longer": "Populární mezi lidmi, které sledujete",
   "follow_suggestions.hints.featured": "Tento profil byl ručně vybrán týmem {domain}.",
   "follow_suggestions.hints.friends_of_friends": "Tento profil je populární mezi lidmi, které sledujete.",
-  "follow_suggestions.hints.most_followed": "Tento profil je jedním z nejvíce sledovaných na {domain}.",
-  "follow_suggestions.hints.most_interactions": "Tento profil nedávno dostalo velkou pozornost na {domain}.",
+  "follow_suggestions.hints.most_followed": "Tento profil je jedním z nejsledovanějších na {domain}.",
+  "follow_suggestions.hints.most_interactions": "Tomuto profilu se nedávno dostalo velké pozornosti na {domain}.",
   "follow_suggestions.hints.similar_to_recently_followed": "Tento profil je podobný profilům, které jste nedávno sledovali.",
   "follow_suggestions.personalized_suggestion": "Přizpůsobený návrh",
   "follow_suggestions.popular_suggestion": "Populární návrh",
@@ -355,6 +365,7 @@
   "footer.terms_of_service": "Obchodní podmínky",
   "generic.saved": "Uloženo",
   "getting_started.heading": "Začínáme",
+  "hashtag.admin_moderation": "Otevřít moderátorské rozhraní pro #{name}",
   "hashtag.column_header.tag_mode.all": "a {additional}",
   "hashtag.column_header.tag_mode.any": "nebo {additional}",
   "hashtag.column_header.tag_mode.none": "bez {additional}",
@@ -370,9 +381,13 @@
   "hashtag.follow": "Sledovat hashtag",
   "hashtag.unfollow": "Přestat sledovat hashtag",
   "hashtags.and_other": "…a {count, plural, one {# další} few {# další} other {# dalších}}",
+  "hints.profiles.followers_may_be_missing": "Sledující mohou pro tento profil chybět.",
+  "hints.profiles.follows_may_be_missing": "Sledování mohou pro tento profil chybět.",
+  "hints.profiles.posts_may_be_missing": "Některé příspěvky z tohoto profilu mohou chybět.",
   "hints.profiles.see_more_followers": "Zobrazit více sledujících na {domain}",
   "hints.profiles.see_more_follows": "Zobrazit další sledování na {domain}",
   "hints.profiles.see_more_posts": "Zobrazit další příspěvky na {domain}",
+  "hints.threads.replies_may_be_missing": "Odpovědi z jiných serverů mohou chybět.",
   "hints.threads.see_more": "Zobrazit další odpovědi na {domain}",
   "home.column_settings.show_reblogs": "Zobrazit boosty",
   "home.column_settings.show_replies": "Zobrazit odpovědi",
@@ -381,7 +396,22 @@
   "home.pending_critical_update.link": "Zobrazit aktualizace",
   "home.pending_critical_update.title": "K dispozici je kritická bezpečnostní aktualizace!",
   "home.show_announcements": "Zobrazit oznámení",
+  "ignore_notifications_modal.disclaimer": "Mastodon nemůže informovat uživatele, že jste ignorovali jejich oznámení. Ignorování oznámení nezabrání odesílání zpráv samotných.",
+  "ignore_notifications_modal.filter_instead": "Místo toho filtrovat",
+  "ignore_notifications_modal.filter_to_act_users": "Stále budete moci přijmout, odmítnout nebo nahlásit uživatele",
+  "ignore_notifications_modal.filter_to_avoid_confusion": "Filtrování pomáhá vyhnout se možným nejasnostem",
+  "ignore_notifications_modal.filter_to_review_separately": "Filtrovaná oznámení můžete zkontrolovat samostatně",
   "ignore_notifications_modal.ignore": "Ignorovat oznámení",
+  "ignore_notifications_modal.limited_accounts_title": "Ignorovat oznámení z moderovaných účtů?",
+  "ignore_notifications_modal.new_accounts_title": "Ignorovat oznámení z nových účtů?",
+  "ignore_notifications_modal.not_followers_title": "Ignorovat oznámení od lidí, kteří vás nesledují?",
+  "ignore_notifications_modal.not_following_title": "Ignorovat oznámení od lidí, které nesledujete?",
+  "ignore_notifications_modal.private_mentions_title": "Ignorovat oznámení z nevyžádaných soukromých zmínek?",
+  "interaction_modal.action.favourite": "Chcete-li pokračovat, musíte oblíbit z vašeho účtu.",
+  "interaction_modal.action.follow": "Chcete-li pokračovat, musíte sledovat z vašeho účtu.",
+  "interaction_modal.action.reblog": "Chcete-li pokračovat, musíte dát boost z vašeho účtu.",
+  "interaction_modal.action.reply": "Chcete-li pokračovat, musíte odpovědět z vašeho účtu.",
+  "interaction_modal.action.vote": "Chcete-li pokračovat, musíte hlasovat z vašeho účtu.",
   "interaction_modal.go": "Přejít",
   "interaction_modal.no_account_yet": "Ještě nemáte účet?",
   "interaction_modal.on_another_server": "Na jiném serveru",
@@ -433,20 +463,27 @@
   "lightbox.close": "Zavřít",
   "lightbox.next": "Další",
   "lightbox.previous": "Předchozí",
+  "lightbox.zoom_in": "Přiblížit na skutečnou velikost",
+  "lightbox.zoom_out": "Přizpůsobit velikost",
   "limited_account_hint.action": "Přesto profil zobrazit",
   "limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.",
   "link_preview.author": "Podle {name}",
   "link_preview.more_from_author": "Více od {name}",
   "link_preview.shares": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",
   "lists.add_member": "Přidat",
+  "lists.add_to_list": "Přidat do seznamu",
   "lists.add_to_lists": "Přidat {name} do seznamů",
   "lists.create": "Vytvořit",
+  "lists.create_a_list_to_organize": "Vytvořte nový seznam pro organizaci vašeho domovského kanálu",
   "lists.create_list": "Vytvořit seznam",
   "lists.delete": "Smazat seznam",
   "lists.done": "Hotovo",
   "lists.edit": "Upravit seznam",
+  "lists.exclusive": "Skrýt členy na domovském kanálu",
+  "lists.exclusive_hint": "Pokud je někdo na tomto seznamu, skryjte jej ve vašem domovském kanálu, abyste se vyhnuli dvojímu vidění jejich příspěvků.",
   "lists.find_users_to_add": "Najít uživatele, které chcete přidat",
   "lists.list_members": "Členové seznamu",
+  "lists.list_members_count": "{count, plural, one {# člen} few {# členové} many {# členů} other {# členů}}",
   "lists.list_name": "Název seznamu",
   "lists.new_list_name": "Název nového seznamu",
   "lists.no_lists_yet": "Zatím žádné seznamy.",
@@ -458,6 +495,7 @@
   "lists.replies_policy.none": "Nikomu",
   "lists.save": "Uložit",
   "lists.search": "Hledat",
+  "lists.show_replies_to": "Zahrnout odpovědi od členů seznamu pro",
   "load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
   "loading_indicator.label": "Načítání…",
   "media_gallery.hide": "Skrýt",
@@ -500,14 +538,25 @@
   "navigation_bar.security": "Zabezpečení",
   "not_signed_in_indicator.not_signed_in": "Pro přístup k tomuto zdroji se musíte přihlásit.",
   "notification.admin.report": "Uživatel {name} nahlásil {target}",
+  "notification.admin.report_account": "{name} nahlásil {count, plural, one {jeden příspěvek} few {# příspěvky} many {# příspěvků} other {# příspěvků}} od {target} za {category}",
+  "notification.admin.report_account_other": "{name} nahlásil {count, plural, one {jeden příspěvek} few {# příspěvky} many {# příspěvků} other {# příspěvků}} od {target}",
   "notification.admin.report_statuses": "{name} nahlásil {target} za {category}",
   "notification.admin.report_statuses_other": "{name} nahlásil {target}",
   "notification.admin.sign_up": "Uživatel {name} se zaregistroval",
+  "notification.admin.sign_up.name_and_others": "{name} a {count, plural, one {# další} few {# další} many {# dalších} other {# dalších}} se zaregistrovali",
+  "notification.annual_report.message": "Váš #Wrapstodon {year} na Vás čeká! Podívejte se, jak vypadal tento Váš rok na Mastodonu!",
+  "notification.annual_report.view": "Zobrazit #Wrapstodon",
   "notification.favourite": "Uživatel {name} si oblíbil váš příspěvek",
+  "notification.favourite.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Váš příspěvek",
+  "notification.favourite_pm": "{name} si oblíbil vaši soukromou zmínku",
+  "notification.favourite_pm.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Vaši soukromou zmínku",
   "notification.follow": "Uživatel {name} vás začal sledovat",
+  "notification.follow.name_and_others": "{name} a {count, plural, one {<a># další</a> Vás začal sledovat} few {<a># další</a> Vás začali sledovat} other {<a># dalších</a> Vás začalo sledovat}}",
   "notification.follow_request": "Uživatel {name} požádal o povolení vás sledovat",
+  "notification.follow_request.name_and_others": "{name} a {count, plural, one {# další Vám poslal žádost o sledování} few {# další Vám poslali žádost o sledování} other {# dalších Vám poslalo žádost o sledování}}",
   "notification.label.mention": "Zmínka",
   "notification.label.private_mention": "Soukromá zmínka",
+  "notification.label.private_reply": "Privátní odpověď",
   "notification.label.reply": "Odpověď",
   "notification.mention": "Zmínka",
   "notification.mentioned_you": "{name} vás zmínil",
@@ -523,6 +572,7 @@
   "notification.own_poll": "Vaše anketa skončila",
   "notification.poll": "Anketa, ve které jste hlasovali, skončila",
   "notification.reblog": "Uživatel {name} boostnul váš příspěvek",
+  "notification.reblog.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> boostnul} few {<a># další</a> boostnuli} other {<a># dalších</a> boostnulo}} Váš příspěvek",
   "notification.relationships_severance_event": "Kontakt ztracen s {name}",
   "notification.relationships_severance_event.account_suspension": "Administrátor z {from} pozastavil {target}, což znamená, že již od nich nemůžete přijímat aktualizace nebo s nimi interagovat.",
   "notification.relationships_severance_event.domain_block": "Administrátor z {from} pozastavil {target}, včetně {followersCount} z vašich sledujících a {followingCount, plural, one {# účet, který sledujete} few {# účty, které sledujete} many {# účtů, které sledujete} other {# účtů, které sledujete}}.",
@@ -531,10 +581,19 @@
   "notification.status": "Uživatel {name} právě přidal příspěvek",
   "notification.update": "Uživatel {name} upravil příspěvek",
   "notification_requests.accept": "Přijmout",
+  "notification_requests.accept_multiple": "{count, plural, one {Schválit # požadavek…} few {Schválit # požadavky…} other {Schválit # požadavků…}}",
+  "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Schválit požadavek} other {Schválit požadavky}}",
+  "notification_requests.confirm_accept_multiple.message": "Chystáte se schválit {count, plural, one {jeden požadavek} few {# požadavky} other {# požadavků}} na oznámení. Opravdu chcete pokračovat?",
   "notification_requests.confirm_accept_multiple.title": "Přijmout žádosti o oznámení?",
+  "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Zamítnout požadavek} other {Zamítnout požadavky}}",
+  "notification_requests.confirm_dismiss_multiple.message": "Chystáte se zamítnout {count, plural, one {jeden požadavek} few {# požadavky} many {# požadavků} other {# požadavků}} na oznámení. Poté k {count, plural, one {němu} other {něm}} již nebudete mít snadný přístup. Opravdu chcete pokračovat?",
+  "notification_requests.confirm_dismiss_multiple.title": "Zamítnout požadavky na oznámení?",
   "notification_requests.dismiss": "Zamítnout",
+  "notification_requests.dismiss_multiple": "Zamítnout {count, plural, one {# požadavek} few {# požadavky} many {# požadavků} other {# požadavků}}…",
   "notification_requests.edit_selection": "Upravit",
   "notification_requests.exit_selection": "Hotovo",
+  "notification_requests.explainer_for_limited_account": "Oznámení z tohoto účtu byla filtrována, protože tento účet byl omezen moderátorem.",
+  "notification_requests.explainer_for_limited_remote_account": "Oznámení z tohoto účtu byla filtrována, protože tento účet nebo jeho server byl omezen moderátorem.",
   "notification_requests.maximize": "Maximalizovat",
   "notification_requests.minimize_banner": "Minimalizovat banner filtrovaných oznámení",
   "notification_requests.notifications_from": "Oznámení od {name}",
@@ -578,6 +637,7 @@
   "notifications.policy.accept": "Přijmout",
   "notifications.policy.accept_hint": "Zobrazit v oznámeních",
   "notifications.policy.drop": "Ignorovat",
+  "notifications.policy.drop_hint": "Permanentně odstranit, aby již nikdy nespatřil světlo světa",
   "notifications.policy.filter": "Filtrovat",
   "notifications.policy.filter_hint": "Odeslat do filtrované schránky oznámení",
   "notifications.policy.filter_limited_accounts_hint": "Omezeno moderátory serveru",
diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json
index 1f3bccee15..d533e99906 100644
--- a/app/javascript/mastodon/locales/ga.json
+++ b/app/javascript/mastodon/locales/ga.json
@@ -407,6 +407,13 @@
   "ignore_notifications_modal.not_followers_title": "An dtugann tú aird ar fhógraí ó dhaoine nach leanann tú?",
   "ignore_notifications_modal.not_following_title": "An ndéanann tú neamhaird de fhógraí ó dhaoine nach leanann tú?",
   "ignore_notifications_modal.private_mentions_title": "An dtugann tú aird ar fhógraí ó Luaintí Príobháideacha gan iarraidh?",
+  "interaction_modal.action.favourite": "Chun leanúint ar aghaidh, ní mór duit an ceann is fearr leat ó do chuntas.",
+  "interaction_modal.action.follow": "Chun leanúint ar aghaidh, ní mór duit leanúint ó do chuntas.",
+  "interaction_modal.action.reblog": "Chun leanúint ar aghaidh, ní mór duit athbhlagáil ó do chuntas.",
+  "interaction_modal.action.reply": "Chun leanúint ar aghaidh, ní mór duit freagra a thabhairt ó do chuntas.",
+  "interaction_modal.action.vote": "Chun leanúint ar aghaidh, ní mór duit vótáil ó do chuntas.",
+  "interaction_modal.go": "Téigh",
+  "interaction_modal.no_account_yet": "Níl cuntas agat fós?",
   "interaction_modal.on_another_server": "Ar freastalaí eile",
   "interaction_modal.on_this_server": "Ar an freastalaí seo",
   "interaction_modal.title.favourite": "An postáil {name} is fearr leat",
@@ -414,6 +421,7 @@
   "interaction_modal.title.reblog": "Mol postáil de chuid {name}",
   "interaction_modal.title.reply": "Freagair postáil {name}",
   "interaction_modal.title.vote": "Vótáil i vótaíocht {name}",
+  "interaction_modal.username_prompt": "M.sh. {example}",
   "intervals.full.days": "{number, plural, one {# lá} other {# lá}}",
   "intervals.full.hours": "{number, plural, one {# uair} other {# uair}}",
   "intervals.full.minutes": "{number, plural, one {# nóiméad} other {# nóiméad}}",
@@ -449,6 +457,7 @@
   "keyboard_shortcuts.toggle_hidden": "Taispeáin/folaigh an téacs taobh thiar de CW",
   "keyboard_shortcuts.toggle_sensitivity": "Taispeáin / cuir i bhfolach meáin",
   "keyboard_shortcuts.toot": "Cuir tús le postáil nua",
+  "keyboard_shortcuts.translate": "post a aistriú",
   "keyboard_shortcuts.unfocus": "Unfocus cum textarea/search",
   "keyboard_shortcuts.up": "Bog suas ar an liosta",
   "lightbox.close": "Dún",
@@ -687,6 +696,8 @@
   "privacy_policy.title": "Polasaí príobháideachais",
   "recommended": "Molta",
   "refresh": "Athnuaigh",
+  "regeneration_indicator.please_stand_by": "Fan i do sheasamh, le do thoil.",
+  "regeneration_indicator.preparing_your_home_feed": "Ag ullmhú do bheatha baile…",
   "relative_time.days": "{number}l",
   "relative_time.full.days": "{number, plural, one {# lá} other {# lá}} ó shin",
   "relative_time.full.hours": "{number, plural, one {# uair} other {# uair}} ó shin",
@@ -826,6 +837,7 @@
   "status.reblogs.empty": "Níor mhol éinne an phostáil seo fós. Nuair a mholfaidh duine éigin í, taispeánfar anseo é sin.",
   "status.redraft": "Scrios ⁊ athdhréachtaigh",
   "status.remove_bookmark": "Bain leabharmharc",
+  "status.remove_favourite": "Bain ó cheanáin",
   "status.replied_in_thread": "D'fhreagair sa snáithe",
   "status.replied_to": "D'fhreagair {name}",
   "status.reply": "Freagair",
diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json
index e2f821022e..9125ce641b 100644
--- a/app/javascript/mastodon/locales/ia.json
+++ b/app/javascript/mastodon/locales/ia.json
@@ -803,7 +803,7 @@
   "status.bookmark": "Adder al marcapaginas",
   "status.cancel_reblog_private": "Disfacer impulso",
   "status.cannot_reblog": "Iste message non pote esser impulsate",
-  "status.continued_thread": "Discussion continuate",
+  "status.continued_thread": "Continuation del discussion",
   "status.copy": "Copiar ligamine a message",
   "status.delete": "Deler",
   "status.detailed_status": "Vista detaliate del conversation",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 09e6ea072a..aa58c34687 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -431,11 +431,11 @@
   "keyboard_shortcuts.column": "Focalizza alla colonna",
   "keyboard_shortcuts.compose": "Focalizza l'area di composizione testuale",
   "keyboard_shortcuts.description": "Descrizione",
-  "keyboard_shortcuts.direct": "per aprire la colonna menzioni private",
+  "keyboard_shortcuts.direct": "Apre la colonna \"menzioni private\"",
   "keyboard_shortcuts.down": "Scorri in basso nell'elenco",
   "keyboard_shortcuts.enter": "Apre il post",
   "keyboard_shortcuts.favourite": "Contrassegna il post come preferito",
-  "keyboard_shortcuts.favourites": "Apri l'elenco dei preferiti",
+  "keyboard_shortcuts.favourites": "Apre l'elenco dei preferiti",
   "keyboard_shortcuts.federated": "Apre la cronologia federata",
   "keyboard_shortcuts.heading": "Scorciatoie da tastiera",
   "keyboard_shortcuts.home": "Apre la cronologia domestica",
@@ -457,6 +457,7 @@
   "keyboard_shortcuts.toggle_hidden": "Mostra/Nasconde il testo dietro CW",
   "keyboard_shortcuts.toggle_sensitivity": "Mostra/Nasconde media",
   "keyboard_shortcuts.toot": "Crea un nuovo post",
+  "keyboard_shortcuts.translate": "Traduce un post",
   "keyboard_shortcuts.unfocus": "Rimuove il focus sull'area di composizione testuale/ricerca",
   "keyboard_shortcuts.up": "Scorre in su nell'elenco",
   "lightbox.close": "Chiudi",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 208ed91104..926781b7f6 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -407,6 +407,13 @@
   "ignore_notifications_modal.not_followers_title": "本当に「フォローされていないアカウントからの通知」を無視するようにしますか?",
   "ignore_notifications_modal.not_following_title": "本当に「フォローしていないアカウントからの通知」を無視するようにしますか?",
   "ignore_notifications_modal.private_mentions_title": "本当に「外部からの非公開の返信」を無視するようにしますか?",
+  "interaction_modal.action.favourite": "お気に入り登録はあなたのアカウントがあるサーバーで行う必要があります。",
+  "interaction_modal.action.follow": "ユーザーをフォローするには、あなたのアカウントがあるサーバーからフォローする必要があります。",
+  "interaction_modal.action.reblog": "投稿をブーストするには、あなたのアカウントがあるサーバーでブーストする必要があります。",
+  "interaction_modal.action.reply": "リプライを送るには、あなたのアカウントがあるサーバーから送る必要があります。",
+  "interaction_modal.action.vote": "票を入れるには、あなたのアカウントがあるサーバーから投票する必要があります。",
+  "interaction_modal.go": "サーバーに移動",
+  "interaction_modal.no_account_yet": "アカウントを持っていない場合は:",
   "interaction_modal.on_another_server": "別のサーバー",
   "interaction_modal.on_this_server": "このサーバー",
   "interaction_modal.title.favourite": "{name}さんの投稿をお気に入り登録",
@@ -414,6 +421,7 @@
   "interaction_modal.title.reblog": "{name}さんの投稿をブースト",
   "interaction_modal.title.reply": "{name}さんの投稿にリプライ",
   "interaction_modal.title.vote": "{name}さんのアンケートに投票",
+  "interaction_modal.username_prompt": "例: {example}",
   "intervals.full.days": "{number}日",
   "intervals.full.hours": "{number}時間",
   "intervals.full.minutes": "{number}分",
diff --git a/app/javascript/mastodon/locales/ne.json b/app/javascript/mastodon/locales/ne.json
index 9e9fe90888..6fe1330d6c 100644
--- a/app/javascript/mastodon/locales/ne.json
+++ b/app/javascript/mastodon/locales/ne.json
@@ -47,11 +47,13 @@
   "account.mutual": "आपसी",
   "account.no_bio": "कुनै विवरण प्रदान गरिएको छैन।",
   "account.posts": "पोस्टहरू",
+  "account.posts_with_replies": "पोस्ट र जवाफहरू",
   "account.report": "@{name}लाई रिपोर्ट गर्नुहोस्",
   "account.requested": "स्वीकृतिको पर्खाइमा। फलो अनुरोध रद्द गर्न क्लिक गर्नुहोस्",
   "account.requested_follow": "{name} ले तपाईंलाई फलो गर्न अनुरोध गर्नुभएको छ",
   "account.share": "@{name} को प्रोफाइल सेयर गर्नुहोस्",
   "account.show_reblogs": "@{name} को बूस्टहरू देखाउनुहोस्",
+  "account.statuses_counter": "{count, plural, one {{counter} पोस्ट} other {{counter} पोस्टहरू}}",
   "account.unblock": "@{name} लाई अनब्लक गर्नुहोस्",
   "account.unblock_domain": "{domain} डोमेनलाई अनब्लक गर्नुहोस्",
   "account.unblock_short": "अनब्लक गर्नुहोस्",
@@ -67,14 +69,18 @@
   "alert.unexpected.message": "एउटा अनपेक्षित त्रुटि भयो।",
   "announcement.announcement": "घोषणा",
   "annual_report.summary.followers.followers": "फलोअरहरु",
+  "annual_report.summary.highlighted_post.by_reblogs": "सबैभन्दा बढि बूस्ट गरिएको पोस्ट",
   "annual_report.summary.new_posts.new_posts": "नयाँ पोस्टहरू",
   "block_modal.remote_users_caveat": "हामी सर्भर {domain} लाई तपाईंको निर्णयको सम्मान गर्न सोध्नेछौं। तर, हामी अनुपालनको ग्यारेन्टी दिन सक्दैनौं किनभने केही सर्भरहरूले ब्लकहरू फरक रूपमा ह्यान्डल गर्न सक्छन्। सार्वजनिक पोस्टहरू लग इन नभएका प्रयोगकर्ताहरूले देख्न सक्छन्।",
   "block_modal.show_less": "कम देखाउनुहोस्",
   "block_modal.show_more": "थप देखाउनुहोस्",
-  "block_modal.title": "प्रयोगकर्तालाई ब्लक गर्ने हो?",
+  "block_modal.title": "प्रयोगकर्तालाई ब्लक गर्ने?",
+  "boost_modal.reblog": "पोस्ट बुस्ट गर्ने?",
+  "boost_modal.undo_reblog": "पोस्ट अनबुस्ट गर्ने?",
   "bundle_column_error.copy_stacktrace": "त्रुटि रिपोर्ट प्रतिलिपि गर्नुहोस्",
   "bundle_column_error.network.title": "नेटवर्क त्रुटि",
   "bundle_column_error.retry": "पुन: प्रयास गर्नुहोस्",
+  "bundle_column_error.routing.title": "४०४",
   "bundle_modal_error.close": "बन्द गर्नुहोस्",
   "bundle_modal_error.retry": "Try again",
   "closed_registrations.other_server_instructions": "Mastodon विकेन्द्रीकृत भएकोले, तपाइँ अर्को सर्भरमा खाता खोल्न सक्नुहुन्छ र पनि यो सर्भरसँग अन्तरक्रिया गर्न सक्नुहुन्छ।",
@@ -82,23 +88,54 @@
   "closed_registrations_modal.find_another_server": "अर्को सर्भर खोज्नुहोस्",
   "closed_registrations_modal.title": "Mastodon मा साइन अप गर्दै",
   "column.blocks": "ब्लक गरिएको प्रयोगकर्ताहरु",
+  "column.bookmarks": "बुकमार्कहरू",
+  "column.create_list": "सूची बनाउनुहोस्",
+  "column.direct": "निजी उल्लेखहरू",
   "column.directory": "प्रोफाइल ब्राउज गर्नुहोस्",
   "column.domain_blocks": "ब्लक गरिएको डोमेन",
+  "column.edit_list": "सूची सम्पादन गर्नुहोस्",
   "column.follow_requests": "फलो अनुरोधहरू",
+  "column.home": "गृहपृष्ठ",
   "column.lists": "सूचीहरू",
+  "column.mutes": "म्यूट गरिएका प्रयोगकर्ताहरू",
   "column.notifications": "सूचनाहरू",
+  "column.pins": "पिन गरिएका पोस्टहरू",
   "column_header.hide_settings": "सेटिङ्हरू लुकाउनुहोस्",
+  "column_header.pin": "पिन गर्नुहोस्",
+  "column_header.unpin": "अनपिन गर्नुहोस्",
+  "column_search.cancel": "रद्द गर्नुहोस्",
   "column_subheading.settings": "सेटिङहरू",
+  "community.column_settings.media_only": "मिडिया मात्र",
   "compose.language.change": "भाषा परिवर्तन गर्नुहोस्",
   "compose.language.search": "भाषाहरू खोज्नुहोस्...",
+  "compose.published.body": "पोस्ट प्रकाशित भयो।",
+  "compose.published.open": "खोल्नुहोस्",
+  "compose.saved.body": "पोस्ट सेभ गरियो।",
   "compose_form.direct_message_warning_learn_more": "थप जान्नुहोस्",
+  "compose_form.placeholder": "तपाईको मनमा के छ?",
+  "compose_form.publish": "पोस्ट गर्नुहोस्",
   "compose_form.publish_form": "नयाँ पोस्ट",
+  "compose_form.reply": "जवाफ दिनुहोस्",
+  "compose_form.save_changes": "अपडेट गर्नुहोस्",
+  "confirmations.delete.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाईं यो पोष्ट मेटाउन चाहनुहुन्छ?",
+  "confirmations.delete.title": "पोस्ट मेटाउने?",
+  "confirmations.delete_list.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाईं यो सूची स्थायी रूपमा मेटाउन चाहनुहुन्छ?",
+  "confirmations.delete_list.title": "सूची मेटाउने?",
+  "confirmations.edit.confirm": "सम्पादन गर्नुहोस्",
+  "confirmations.edit.message": "अहिले सम्पादन गर्नाले तपाईंले हाल लेखिरहनुभएको सन्देश अधिलेखन हुनेछ। के तपाईं अगाडि बढ्न चाहनुहुन्छ?",
+  "confirmations.edit.title": "पोस्ट अधिलेखन गर्ने?",
   "confirmations.follow_to_list.confirm": "फलो गर्नुहोस र सूचीमा थप्नुहोस्",
   "confirmations.follow_to_list.message": "सूचीमा {name}लाई थप्नको लागि तपाईंले तिनीहरूलाई फलो गरेको हुनुपर्छ।",
-  "confirmations.follow_to_list.title": "प्रयोगकर्तालाई फलो गर्ने हो?",
+  "confirmations.follow_to_list.title": "प्रयोगकर्तालाई फलो गर्ने?",
+  "confirmations.logout.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाइँ लाई लग आउट गर्न चाहनुहुन्छ?",
+  "confirmations.logout.title": "लग आउट गर्ने?",
+  "confirmations.redraft.title": "पोस्ट मेटाएर पुन: ड्राफ्ट गर्ने?",
+  "confirmations.reply.message": "अहिले जवाफ दिनाले तपाईंले हाल लेखिरहनुभएको सन्देश अधिलेखन हुनेछ। के तपाईं अगाडि बढ्न चाहनुहुन्छ?",
+  "confirmations.reply.title": "पोस्ट अधिलेखन गर्ने?",
   "confirmations.unfollow.confirm": "अनफलो गर्नुहोस्",
   "confirmations.unfollow.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाइँ {name}लाई अनफलो गर्न चाहनुहुन्छ?",
-  "confirmations.unfollow.title": "प्रयोगकर्तालाई अनफलो गर्ने हो?",
+  "confirmations.unfollow.title": "प्रयोगकर्तालाई अनफलो गर्ने?",
+  "disabled_account_banner.account_settings": "खाता सेटिङहरू",
   "empty_column.follow_requests": "तपाईंले अहिलेसम्म कुनै पनि फलो अनुरोधहरू प्राप्त गर्नुभएको छैन। तपाईंले कुनै प्राप्त गरेपछि त्यो यहाँ देखिनेछ।",
   "empty_column.followed_tags": "तपाईंले अहिलेसम्म कुनै पनि ह्यासट्यागहरू फलो गर्नुभएको छैन। तपाईंले ह्यासट्याग फलो गरेपछि तिनीहरू यहाँ देखिनेछन्।",
   "follow_suggestions.dismiss": "फेरि नदेखाउनुहोस्",
@@ -111,15 +148,35 @@
   "followed_tags": "फलो गरिएका ह्यासट्यागहरू",
   "hashtag.follow": "ह्यासट्याग फलो गर्नुहोस्",
   "hashtag.unfollow": "ह्यासट्याग अनफलो गर्नुहोस्",
+  "home.column_settings.show_reblogs": "बूस्टहरू देखाउनुहोस्",
+  "interaction_modal.no_account_yet": "अहिलेसम्म खाता छैन?",
   "interaction_modal.title.follow": "{name} लाई फलो गर्नुहोस्",
+  "interaction_modal.title.reblog": "{name} को पोस्ट बुस्ट गर्नुहोस्",
+  "keyboard_shortcuts.boost": "पोस्ट बुस्ट गर्नुहोस्",
   "mute_modal.they_wont_know": "उनीहरूलाई म्यूट गरिएको बारे थाहा हुँदैन।",
-  "mute_modal.title": "प्रयोगकर्तालाई म्युट गर्ने हो?",
+  "mute_modal.title": "प्रयोगकर्तालाई म्युट गर्ने?",
   "navigation_bar.blocks": "ब्लक गरिएको प्रयोगकर्ताहरु",
   "navigation_bar.follow_requests": "फलो अनुरोधहरू",
   "navigation_bar.followed_tags": "फलो गरिएका ह्यासट्यागहरू",
+  "notification.reblog": "{name} ले तपाईंको पोस्ट बूस्ट गर्नुभयो",
+  "notification_requests.confirm_accept_multiple.title": "सूचना अनुरोधहरू स्वीकार गर्ने?",
+  "notification_requests.confirm_dismiss_multiple.title": "सूचना अनुरोधहरू खारेज गर्ने?",
+  "notifications.clear_title": "सूचनाहरू खाली गर्ने?",
+  "notifications.column_settings.reblog": "बूस्टहरू:",
+  "notifications.filter.boosts": "बूस्टहरू",
+  "report.comment.title": "के हामीले थाहा पाउनुपर्ने अरू केही छ जस्तो लाग्छ?",
+  "report.forward_hint": "यो खाता अर्को सर्भरबाट हो। त्यहाँ पनि रिपोर्टको गुमनाम प्रतिलिपि पठाउने हो?",
+  "report.rules.title": "कुन नियमहरू उल्लङ्घन भइरहेका छन्?",
+  "report.statuses.title": "के यस रिपोर्टलाई समर्थन गर्ने कुनै पोस्टहरू छन्?",
+  "report.thanks.title": "यो हेर्न चाहनुहुन्न?",
   "report.unfollow": "@{name} लाई अनफलो गर्नुहोस्",
   "search_results.hashtags": "ह्यासट्यागहरू",
+  "status.cancel_reblog_private": "अनबुस्ट गर्नुहोस्",
+  "status.cannot_reblog": "यो पोस्टलाई बुस्ट गर्न सकिँदैन",
   "status.mute": "@{name}लाई म्यूट गर्नुहोस्",
   "status.mute_conversation": "कुराकानी म्यूट गर्नुहोस्",
+  "status.reblog": "बूस्ट गर्नुहोस्",
+  "status.reblogged_by": "{name} ले बूस्ट गर्नुभएको",
+  "status.reblogs": "{count, plural, one {बूस्ट} other {बूस्टहरू}}",
   "status.unmute_conversation": "कुराकानी अनम्यूट गर्नुहोस्"
 }
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index f9cb9743ac..b419870d7a 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -416,6 +416,7 @@
   "interaction_modal.title.reblog": "Podbij wpis {name}",
   "interaction_modal.title.reply": "Odpowiedz na post {name}",
   "interaction_modal.title.vote": "Weź udział w głosowaniu {name}",
+  "interaction_modal.username_prompt": "Np. {example}",
   "intervals.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}",
   "intervals.full.hours": "{number, plural, one {# godzina} few {# godziny} many {# godzin} other {# godzin}}",
   "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}",
@@ -828,6 +829,7 @@
   "status.reblogs.empty": "Nikt nie podbił jeszcze tego wpisu. Gdy ktoś to zrobi, pojawi się tutaj.",
   "status.redraft": "Usuń i przeredaguj",
   "status.remove_bookmark": "Usuń zakładkę",
+  "status.remove_favourite": "Usuń z ulubionych",
   "status.replied_in_thread": "Odpowiedź w wątku",
   "status.replied_to": "Odpowiedź do wpisu użytkownika {name}",
   "status.reply": "Odpowiedz",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 142ae33c58..99929e1f35 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -108,7 +108,7 @@
   "annual_report.summary.thanks": "Obrigada por fazer parte do Mastodon!",
   "attachments_list.unprocessed": "(não processado)",
   "audio.hide": "Ocultar áudio",
-  "block_modal.remote_users_caveat": "Pediremos ao servidor {domínio} que respeite sua decisão. No entanto, a conformidade não é garantida pois alguns servidores podem lidar com os blocos de maneira diferente. As postagens públicas ainda podem estar visíveis para usuários não logados.",
+  "block_modal.remote_users_caveat": "Pediremos ao servidor {domain} que respeite sua decisão. No entanto, a conformidade não é garantida, já que alguns servidores podem lidar com bloqueios de maneira diferente. As postagens públicas ainda podem estar visíveis para usuários não logados.",
   "block_modal.show_less": "Mostrar menos",
   "block_modal.show_more": "Mostrar mais",
   "block_modal.they_cant_mention": "Eles não podem mencionar ou seguir você.",
@@ -259,9 +259,9 @@
   "domain_pill.their_server": "Sua casa digital, onde ficam todas as suas postagens.",
   "domain_pill.their_username": "Seu identificador exclusivo em seu servidor. É possível encontrar usuários com o mesmo nome de usuário em servidores diferentes.",
   "domain_pill.username": "Nome de usuário",
-  "domain_pill.whats_in_a_handle": "O que há em uma alça?",
-  "domain_pill.who_they_are": "Como os identificadores indicam quem alguém é e onde está, você pode interagir com pessoas na web social de <button>plataformas alimentadas pelo ActivityPub</button>.",
-  "domain_pill.who_you_are": "Como seu identificador indica quem você é e onde está, as pessoas podem interagir com você nas redes sociais das <button>plataformas alimentadas pelo ActivityPub</button>.",
+  "domain_pill.whats_in_a_handle": "O que há em um identificador?",
+  "domain_pill.who_they_are": "Como os identificadores indicam quem alguém é e onde está, você pode interagir com pessoas na rede de <button>plataformas alimentadas pelo ActivityPub</button>.",
+  "domain_pill.who_you_are": "Como seu identificador indica quem você é e onde está, as pessoas podem interagir com você na rede de <button>plataformas alimentadas pelo ActivityPub</button>.",
   "domain_pill.your_handle": "Seu identificador:",
   "domain_pill.your_server": "Sua casa digital, onde ficam todas as suas postagens. Não gosta deste? Transfira servidores a qualquer momento e traga seus seguidores também.",
   "domain_pill.your_username": "Seu identificador exclusivo neste servidor. É possível encontrar usuários com o mesmo nome de usuário em servidores diferentes.",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index f7329ad97e..42d06ea72b 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -1,7 +1,7 @@
 {
   "about.blocks": "Servidores moderados",
   "about.contact": "Contacto:",
-  "about.disclaimer": "O Mastodon é um software livre, de código aberto e uma marca registada do Mastodon gGmbH.",
+  "about.disclaimer": "O Mastodon é um software livre, de código aberto e uma marca registada de Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "Motivo não disponível",
   "about.domain_blocks.preamble": "O Mastodon geralmente permite ver e interagir com o conteúdo de utilizadores de qualquer outra instância no fediverso. Estas são as exceções desta instância em específico.",
   "about.domain_blocks.silenced.explanation": "Normalmente não verás perfis e conteúdos deste servidor, a não ser que os procures explicitamente ou optes por segui-los.",
@@ -723,7 +723,7 @@
   "report.category.title_account": "perfil",
   "report.category.title_status": "publicação",
   "report.close": "Concluído",
-  "report.comment.title": "Há algo mais que pensa que devemos saber?",
+  "report.comment.title": "Há mais alguma coisa que devamos saber?",
   "report.forward": "Reencaminhar para {target}",
   "report.forward_hint": "A conta pertence a outro servidor. Enviar uma cópia anónima da denúncia para esse servidor também?",
   "report.mute": "Ocultar",
@@ -739,15 +739,15 @@
   "report.reasons.spam": "É spam",
   "report.reasons.spam_description": "Hiperligações maliciosas, contactos falsos ou respostas repetitivas",
   "report.reasons.violation": "Viola as regras do servidor",
-  "report.reasons.violation_description": "Está ciente de que infringe regras específicas",
-  "report.rules.subtitle": "Selecione tudo o que se aplicar",
+  "report.reasons.violation_description": "Infringe regras específicas",
+  "report.rules.subtitle": "Seleciona tudo o que se aplicar",
   "report.rules.title": "Que regras estão a ser violadas?",
-  "report.statuses.subtitle": "Selecione tudo o que se aplicar",
+  "report.statuses.subtitle": "Seleciona tudo o que se aplicar",
   "report.statuses.title": "Existe alguma publicação que suporte esta denúncia?",
   "report.submit": "Enviar",
   "report.target": "A denunciar {target}",
   "report.thanks.take_action": "Aqui estão as suas opções para controlar o que vê no Mastodon:",
-  "report.thanks.take_action_actionable": "Enquanto revemos a sua denúncia, pode tomar medidas contra @{name}:",
+  "report.thanks.take_action_actionable": "Enquanto revemos a tua denúncia, podes tomar medidas contra @{name}:",
   "report.thanks.title": "Não quer ver isto?",
   "report.thanks.title_actionable": "Obrigado por nos informares, vamos analisar a situação.",
   "report.unfollow": "Deixar de seguir @{name}",
@@ -758,7 +758,7 @@
   "report_notification.categories.other": "Outro",
   "report_notification.categories.other_sentence": "outro",
   "report_notification.categories.spam": "Spam",
-  "report_notification.categories.spam_sentence": "spam",
+  "report_notification.categories.spam_sentence": "publicidade indesejada / spam",
   "report_notification.categories.violation": "Violação de regra",
   "report_notification.categories.violation_sentence": "violação de regra",
   "report_notification.open": "Abrir denúncia",
@@ -813,7 +813,7 @@
   "status.edited": "Última edição em {date}",
   "status.edited_x_times": "Editado {count, plural,one {{count} vez} other {{count} vezes}}",
   "status.embed": "Obter código de incorporação",
-  "status.favourite": "Assinalar como favorito",
+  "status.favourite": "Adicionar aos favoritos",
   "status.favourites": "{count, plural, one {favorito} other {favoritos}}",
   "status.filter": "Filtrar esta publicação",
   "status.history.created": "{name} criado em {date}",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index f59e7b96bc..d0617a52c8 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -530,6 +530,7 @@
   "notification.status": "{name} uverejňuje niečo nové",
   "notification.update": "{name} upravuje príspevok",
   "notification_requests.accept": "Prijať",
+  "notification_requests.confirm_accept_multiple.title": "Priať požiadavku o oboznámenia?",
   "notification_requests.dismiss": "Zamietnuť",
   "notification_requests.edit_selection": "Uprav",
   "notification_requests.exit_selection": "Hotovo",
@@ -624,6 +625,7 @@
   "privacy_policy.title": "Pravidlá ochrany súkromia",
   "recommended": "Odporúčané",
   "refresh": "Obnoviť",
+  "regeneration_indicator.preparing_your_home_feed": "Pripravuje sa tvoj domáci kanál…",
   "relative_time.days": "{number} dní",
   "relative_time.full.days": "Pred {number, plural, one {# dňom} other {# dňami}}",
   "relative_time.full.hours": "Pred {number, plural, one {# hodinou} other {# hodinami}}",
diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json
index 834c74eb77..3bad8fedb7 100644
--- a/app/javascript/mastodon/locales/vi.json
+++ b/app/javascript/mastodon/locales/vi.json
@@ -550,7 +550,7 @@
   "notification.favourite.name_and_others_with_link": "{name} và <a>{count, plural, other {# người khác}}</a> đã thích tút của bạn",
   "notification.favourite_pm": "{name} đã thích lượt nhắn riêng của bạn",
   "notification.favourite_pm.name_and_others_with_link": "{name} và <a>{count, plural, other {# người khác}}</a> đã thích lượt nhắn riêng của bạn",
-  "notification.follow": "{name} theo dõi bạn",
+  "notification.follow": "{name} đã theo dõi bạn",
   "notification.follow.name_and_others": "{name} và <a>{count, plural, other {# người khác}}</a> theo dõi bạn",
   "notification.follow_request": "{name} yêu cầu theo dõi bạn",
   "notification.follow_request.name_and_others": "{name} và {count, plural, other {# người khác}} đã yêu cầu theo dõi bạn",
@@ -764,7 +764,7 @@
   "report_notification.open": "Mở báo cáo",
   "search.no_recent_searches": "Gần đây chưa tìm gì",
   "search.placeholder": "Tìm kiếm",
-  "search.quick_action.account_search": "Người có tên {x}",
+  "search.quick_action.account_search": "Người tên {x}",
   "search.quick_action.go_to_account": "Xem trang {x}",
   "search.quick_action.go_to_hashtag": "Xem hashtag {x}",
   "search.quick_action.open_url": "Mở liên kết trong Mastodon",
diff --git a/config/locales/activerecord.cs.yml b/config/locales/activerecord.cs.yml
index 6f4fe86e3f..e62c4264dd 100644
--- a/config/locales/activerecord.cs.yml
+++ b/config/locales/activerecord.cs.yml
@@ -15,9 +15,17 @@ cs:
       user/invite_request:
         text: Důvod
     errors:
+      attributes:
+        domain:
+          invalid: není platný název domény
+      messages:
+        invalid_domain_on_line: "%{value} není platný název domény"
+        too_many_lines: překročil limit %{limit} řádků
       models:
         account:
           attributes:
+            fields:
+              fields_with_values_missing_labels: obsahuje hodnoty s chybějícími popisky
             username:
               invalid: musí obsahovat pouze písmena, číslice a podtržítka
               reserved: je vyhrazeno
@@ -33,6 +41,11 @@ cs:
           attributes:
             data:
               malformed: je chybný
+        list_account:
+          attributes:
+            account_id:
+              taken: již je na seznamu
+          must_be_following: musí být sledovaný účet
         status:
           attributes:
             reblog:
diff --git a/config/locales/activerecord.ga.yml b/config/locales/activerecord.ga.yml
index 5b36d80d79..c02c537076 100644
--- a/config/locales/activerecord.ga.yml
+++ b/config/locales/activerecord.ga.yml
@@ -24,6 +24,8 @@ ga:
       models:
         account:
           attributes:
+            fields:
+              fields_with_values_missing_labels: ina bhfuil luachanna a bhfuil lipéid in easnamh orthu
             username:
               invalid: ní mór go mbeadh litreacha, uimhreacha agus pointí béime amháin
               reserved: in áirithe
diff --git a/config/locales/activerecord.it.yml b/config/locales/activerecord.it.yml
index 3e8897577a..6db643408c 100644
--- a/config/locales/activerecord.it.yml
+++ b/config/locales/activerecord.it.yml
@@ -24,6 +24,8 @@ it:
       models:
         account:
           attributes:
+            fields:
+              fields_with_values_missing_labels: contiene valori con label mancanti
             username:
               invalid: deve contenere solo lettere, numeri e trattini bassi
               reserved: è riservato
diff --git a/config/locales/activerecord.ne.yml b/config/locales/activerecord.ne.yml
index db03c5186b..12795ea20d 100644
--- a/config/locales/activerecord.ne.yml
+++ b/config/locales/activerecord.ne.yml
@@ -1 +1,35 @@
+---
 ne:
+  activerecord:
+    attributes:
+      user:
+        agreement: सेवा सम्झौता
+        email: ईमेल ठेगाना
+        password: पासवर्ड
+      user/account:
+        username: प्रयोगकर्ता नाम
+      user/invite_request:
+        text: कारण
+    errors:
+      attributes:
+        domain:
+          invalid: मान्य डोमेन नाम होइन
+      messages:
+        invalid_domain_on_line: "%{value} मान्य डोमेन नाम होइन"
+      models:
+        account:
+          attributes:
+            username:
+              invalid: अक्षर, संख्या र अन्डरस्कोर मात्र हुनु पर्छ
+        admin/webhook:
+          attributes:
+            url:
+              invalid: मान्य URL होइन
+        doorkeeper/application:
+          attributes:
+            website:
+              invalid: मान्य URL होइन
+        list_account:
+          attributes:
+            account_id:
+              taken: पहिले नै सूचीमा छ
diff --git a/config/locales/activerecord.pt-PT.yml b/config/locales/activerecord.pt-PT.yml
index 8b17ade2eb..ecbfab3dc4 100644
--- a/config/locales/activerecord.pt-PT.yml
+++ b/config/locales/activerecord.pt-PT.yml
@@ -27,7 +27,7 @@ pt-PT:
             fields:
               fields_with_values_missing_labels: contém valores com etiquetas em falta
             username:
-              invalid: deve conter apenas letras, números e traços inferiores
+              invalid: deve conter apenas letras, números e traços inferiores (_)
               reserved: está reservado
         admin/webhook:
           attributes:
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index c9deb24ee1..135975c3c8 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -78,10 +78,10 @@ cs:
       enabled: Povoleno
       enabled_msg: Účet %{username} byl úspěšně rozmrazen
       followers: Sledující
-      follows: Sledované
+      follows: Sledovaní
       header: Záhlaví
       inbox_url: URL příchozí schránky
-      invite_request_text: Důvody založení
+      invite_request_text: Důvody pro připojení
       invited_by: Pozván uživatelem
       ip: IP adresa
       joined: Uživatelem od
@@ -187,29 +187,40 @@ cs:
         confirm_user: Potvrdit uživatele
         create_account_warning: Vytvořit varování
         create_announcement: Nové oznámení
+        create_canonical_email_block: Vytvořit blok e-mailu
         create_custom_emoji: Vytvořit vlastní emoji
         create_domain_allow: Vytvořit povolení domény
         create_domain_block: Vytvořit blokaci domény
+        create_email_domain_block: Vytvořit blok e-mailové domény
         create_ip_block: Vytvořit IP pravidlo
+        create_relay: Vytvořit relay
         create_unavailable_domain: Vytvořit nedostupnou doménu
         create_user_role: Vytvořit roli
         demote_user: Snížit roli uživatele
         destroy_announcement: Odstranit oznámení
+        destroy_canonical_email_block: Odblokovat email
         destroy_custom_emoji: Odstranit vlastní emoji
         destroy_domain_allow: Odstranit povolení domény
         destroy_domain_block: Odstranit blokaci domény
+        destroy_email_domain_block: Smazat blokaci e-mailové domény
         destroy_instance: Odmazat doménu
         destroy_ip_block: Smazat IP pravidlo
+        destroy_relay: Smazat relay
         destroy_status: Odstranit Příspěvek
         destroy_unavailable_domain: Smazat nedostupnou doménu
         destroy_user_role: Zničit roli
         disable_2fa_user: Vypnout 2FA
         disable_custom_emoji: Zakázat vlastní emoji
+        disable_relay: Deaktivovat relay
+        disable_sign_in_token_auth_user: Zrušit uživatelovo ověřování e-mailovým tokenem
         disable_user: Deaktivovat uživatele
         enable_custom_emoji: Povolit vlastní emoji
+        enable_relay: Aktivovat relay
+        enable_sign_in_token_auth_user: Povolit uživatelovo ověřování e-mailovým tokenem
         enable_user: Povolit uživatele
         memorialize_account: Změna na „in memoriam“
         promote_user: Povýšit uživatele
+        publish_terms_of_service: Zveřejnit smluvní podmínky
         reject_appeal: Zamítnout odvolání
         reject_user: Odmítnout uživatele
         remove_avatar_user: Odstranit avatar
@@ -236,36 +247,50 @@ cs:
         approve_appeal_html: Uživatel %{name} schválil odvolání proti rozhodnutí moderátora %{target}
         approve_user_html: "%{name} schválil registraci od %{target}"
         assigned_to_self_report_html: Uživatel %{name} si přidělil hlášení %{target}
+        change_email_user_html: "%{name} změnil*a e-mailovou adresu %{target}"
         change_role_user_html: "%{name} změnil roli %{target}"
+        confirm_user_html: Uživatel %{name} potvrdil e-mailovou adresu uživatele %{target}
         create_account_warning_html: Uživatel %{name} poslal %{target} varování
         create_announcement_html: Uživatel %{name} vytvořil nové oznámení %{target}
+        create_canonical_email_block_html: "%{name} zablokoval e-mail s hashem %{target}"
         create_custom_emoji_html: Uživatel %{name} nahrál nové emoji %{target}
         create_domain_allow_html: Uživatel %{name} povolil federaci s doménou %{target}
         create_domain_block_html: Uživatel %{name} zablokoval doménu %{target}
+        create_email_domain_block_html: Uživatel %{name} zablokoval e-mailovou doménu %{target}
         create_ip_block_html: Uživatel %{name} vytvořil pravidlo pro IP %{target}
+        create_relay_html: "%{name} vytvořil*a relay %{target}"
         create_unavailable_domain_html: "%{name} zastavil doručování na doménu %{target}"
         create_user_role_html: "%{name} vytvořil %{target} roli"
         demote_user_html: Uživatel %{name} degradoval uživatele %{target}
         destroy_announcement_html: Uživatel %{name} odstranil oznámení %{target}
+        destroy_canonical_email_block_html: "%{name} odblokoval*a e-mail s hashem %{target}"
         destroy_custom_emoji_html: "%{name} odstranil emoji %{target}"
         destroy_domain_allow_html: Uživatel %{name} zakázal federaci s doménou %{target}
         destroy_domain_block_html: Uživatel %{name} odblokoval doménu %{target}
+        destroy_email_domain_block_html: "%{name} odblokoval*a e-mailovou doménu %{target}"
         destroy_instance_html: Uživatel %{name} odmazal doménu %{target}
         destroy_ip_block_html: Uživatel %{name} odstranil pravidlo pro IP %{target}
+        destroy_relay_html: "%{name} odstranil*a relay %{target}"
         destroy_status_html: Uživatel %{name} odstranil příspěvek uživatele %{target}
         destroy_unavailable_domain_html: "%{name} obnovil doručování na doménu %{target}"
         destroy_user_role_html: "%{name} odstranil %{target} roli"
         disable_2fa_user_html: Uživatel %{name} vypnul dvoufázové ověřování pro uživatele %{target}
         disable_custom_emoji_html: Uživatel %{name} zakázal emoji %{target}
+        disable_relay_html: "%{name} deaktivoval*a relay %{target}"
+        disable_sign_in_token_auth_user_html: "%{name} deaktivoval*a ověřování e-mailovým tokenem pro %{target}"
         disable_user_html: Uživatel %{name} zakázal přihlašování pro uživatele %{target}
         enable_custom_emoji_html: Uživatel %{name} povolil emoji %{target}
+        enable_relay_html: "%{name} aktivoval*a relay %{target}"
+        enable_sign_in_token_auth_user_html: "%{name} aktivoval*a ověřování e-mailovým tokenem pro %{target}"
         enable_user_html: Uživatel %{name} povolil přihlašování pro uživatele %{target}
         memorialize_account_html: Uživatel %{name} změnil účet %{target} na „in memoriam“ stránku
         promote_user_html: Uživatel %{name} povýšil uživatele %{target}
+        publish_terms_of_service_html: "%{name} zveřejnil*a aktualizace podmínek služby"
         reject_appeal_html: Uživatel %{name} zamítl odvolání proti rozhodnutí moderátora %{target}
         reject_user_html: "%{name} odmítl registraci od %{target}"
         remove_avatar_user_html: Uživatel %{name} odstranil avatar uživatele %{target}
         reopen_report_html: Uživatel %{name} znovu otevřel hlášení %{target}
+        resend_user_html: "%{name} znovu odeslal*a potvrzovací e-mail pro %{target}"
         reset_password_user_html: Uživatel %{name} obnovil heslo uživatele %{target}
         resolve_report_html: Uživatel %{name} vyřešil hlášení %{target}
         sensitive_account_html: "%{name} označil média účtu %{target} jako citlivá"
@@ -436,6 +461,7 @@ cs:
         many: "%{count} pokusů o registraci za poslední týden"
         one: "%{count} pokus o registraci za poslední týden"
         other: "%{count} pokusů o registraci za poslední týden"
+      created_msg: E-mailová doména úspěšně zablokována
       delete: Smazat
       dns:
         types:
@@ -445,7 +471,9 @@ cs:
         create: Přidat doménu
         resolve: Přeložit doménu
         title: Blokovat novou e-mailovou doménu
+      no_email_domain_block_selected: Žádné blokace e-mailové domény nebyly změněny, protože žádné nebyly vybrány
       not_permitted: Nepovoleno
+      resolved_dns_records_hint_html: Doménové jméno vede na následující MX domény, které mají nakonec na starost přijímání e-mailů. Blokování MX domény zablokuje registrace z jakékoliv e-mailové adresy, která používá stejnou MX doménu, i když je viditelné doménové jméno jiné. <strong>Dejte si pozor, abyste nezablokovali velké e-mailové poskytovatele.</strong>
       resolved_through_html: Přeložena přes %{domain}
       title: Blokované e-mailové domény
     export_domain_allows:
@@ -609,7 +637,9 @@ cs:
         resolve_description_html: Nebudou učiněny žádné kroky proti nahlášenému účtu, žádný prohřešek zaznamenán a hlášení bude uzavřeno.
         silence_description_html: Účet bude viditelný pouze těm, kdo jej již sledují nebo si jej ručně vyhledají, což výrazně omezí jeho dosah. Vždy lze vrátit zpět. Uzavře všechna hlášení proti tomuto účtu.
         suspend_description_html: Účet a veškerý jeho obsah se znepřístupní a bude nakonec smazán, interakce s ním nebude možná. Lze vrátit zpět do 30 dnů. Uzavře všechna hlášení proti tomuto účtu.
+      actions_description_html: Rozhodněte, který krok učinit pro vyřešení tohoto hlášení. Pokud podniknete kárný krok proti nahlášenému účtu, bude mu zasláno e-mailové oznámení, s výjimkou případu, kdy je zvolena kategorie <strong>Spam</strong>.
       actions_description_remote_html: Rozhodněte, co podniknout pro vyřešení tohoto hlášení. Toto ovlivní pouze to, jak <strong>váš</strong> server komunikuje s tímto vzdáleným účtem, a zpracuje jeho obsah.
+      actions_no_posts: Toto hlášení nemá žádné související příspěvky k odstranění
       add_to_report: Přidat do hlášení další
       already_suspended_badges:
         local: Již pozastaveno na tomto serveru
@@ -673,6 +703,7 @@ cs:
         delete_data_html: Odstranit profil a obsah <strong>@%{acct}</strong> ode dneška po 30 dní, pokud mezitím nebude zrušeno jeho pozastavení
         preview_preamble_html: "<strong>@%{acct}</strong> obdrží varování s následujícím obsahem:"
         record_strike_html: Zaznamenat prohřešek <strong>@%{acct}</strong> pro pomoc s řešením budoucích přestupků z tohoto účtu
+        send_email_html: Poslat varovný e-mail pro <strong>@%{acct}</strong>
         warning_placeholder: Volitelné další odůvodnění moderační akce.
       target_origin: Původ nahlášeného účtu
       title: Hlášení
@@ -716,6 +747,7 @@ cs:
         manage_appeals: Spravovat odvolání
         manage_appeals_description: Umožňuje uživatelům posuzovat odvolání proti moderátorským zásahům
         manage_blocks: Spravovat blokace
+        manage_blocks_description: Umožňuje uživatelům blokovat poskytovatele e-mailů a IP adresy
         manage_custom_emojis: Spravovat vlastní emoji
         manage_custom_emojis_description: Umožňuje uživatelům spravovat vlastní emoji na serveru
         manage_federation: Spravovat federaci
@@ -733,6 +765,7 @@ cs:
         manage_taxonomies: Správa taxonomií
         manage_taxonomies_description: Umožňuje uživatelům zkontrolovat populární obsah a aktualizovat nastavení hashtag
         manage_user_access: Spravovat uživatelské přístupy
+        manage_user_access_description: Umožňuje uživatelům rušit jiným uživatelům dvoufázové ověřování, měnit jejich e-mailovou adresu a obnovovat jim hesla
         manage_users: Spravovat uživatele
         manage_users_description: Umožňuje uživatelům zobrazit podrobnosti ostatních uživatelů a provádět moderování proti nim
         manage_webhooks: Spravovat webhooky
@@ -807,6 +840,7 @@ cs:
       destroyed_msg: Upload stránky byl úspěšně smazán!
     software_updates:
       critical_update: Kritické — aktualizujte, prosím, co nejdříve
+      description: Doporučuje se udržovat vaši instalaci Mastodonu aktuální, aby se využily nejnovější opravy a funkce. Kromě toho je někdy velmi důležité včas aktualizovat Mastodon, aby se předešlo bezpečnostním problémům. Z těchto důvodů Mastodon kontroluje aktualizace každých 30 minut a bude vás informovat podle nastavení vašeho e-mailového oznámení.
       documentation_link: Zjistit více
       release_notes: Poznámky k vydání
       title: Dostupné aktualizace
@@ -822,6 +856,7 @@ cs:
       back_to_account: Zpět na stránku účtu
       back_to_report: Zpět na stránku hlášení
       batch:
+        add_to_report: 'Přidat do hlášení #%{id}'
         remove_from_report: Odebrat z hlášení
         report: Nahlásit
       contents: Obsah
@@ -833,12 +868,17 @@ cs:
       media:
         title: Média
       metadata: Metadata
+      no_history: Tento příspěvek nebyl upraven
       no_status_selected: Nebyly změněny žádné příspěvky, neboť žádné nebyly vybrány
       open: Otevřít příspěvek
       original_status: Původní příspěvek
       reblogs: Boosty
+      replied_to_html: Odpověděl %{acct_link}
       status_changed: Příspěvek změněn
+      status_title: Příspěvek od @%{name}
+      title: Příspěvky na účtu - @%{name}
       trending: Populární
+      view_publicly: Zobrazit veřejně
       visibility: Viditelnost
       with_media: S médii
     strikes:
@@ -880,6 +920,9 @@ cs:
         message_html: Nedefinovali jste žádná pravidla serveru.
       sidekiq_process_check:
         message_html: Pro %{value} frontu/fronty neběží žádný Sidekiq proces. Zkontrolujte prosím svou Sidekiq konfiguraci
+      software_version_check:
+        action: Zobrazit dostupné aktualizace
+        message_html: K dispozici je aktualizace Mastodonu.
       software_version_critical_check:
         action: Zobrazit dostupné aktualizace
         message_html: K dispozici je kritická aktualizace Mastodonu, prosím aktualizujte co nejrychleji.
@@ -906,19 +949,57 @@ cs:
       name: Název
       newest: Nejnovější
       oldest: Nejstarší
+      open: Zobrazit veřejně
       reset: Resetovat
       review: Stav posouzení
       search: Hledat
       title: Hashtagy
       updated_msg: Nastavení hashtagů bylo úspěšně aktualizováno
+    terms_of_service:
+      back: Zpět na podmínky služby
+      changelog: Co se změnilo
+      create: Použít vlastní
+      current: Aktuální
+      draft: Koncept
+      generate: Použít šablonu
+      generates:
+        action: Vygenerovat
+        chance_to_review_html: "<strong>Vygenerované podmínky služby nebudou zveřejněny automaticky.</strong> Výsledky budete mít možnost zkontrolovat. Pro pokračování vyplňte potřebné podrobnosti."
+        explanation_html: Poskytovaná šablona služeb je určena pouze pro informační účely a neměla by být vykládána jako právní poradenství v jakékoli věci. Prosíme, konzultujte Vaši situaci a konkrétní právní otázky s Vaším pravním zástupcem.
+        title: Nastavení podmínek služby
+      history: Historie
+      live: Živě
+      no_history: Zatím nejsou zaznamenány žádné změny podmínek služby.
+      no_terms_of_service_html: Momentálně nemáte nakonfigurovány žádné podmínky služby. Podmínky služby jsou určeny k zajištění jasnosti a ochránit Vás před případnými právními závazky ve sporech s vašimi uživateli.
+      notified_on_html: Uživatelé upozorněni dne %{date}
+      notify_users: Upozornit uživatele
+      preview:
+        explanation_html: 'E-mail bude poslán <strong>%{display_count} uživatelům</strong>, kteří se zaregistrovali před datem %{date}. Následující text bude obsažen v onom e-mailu:'
+        send_preview: Poslat náhled na %{email}
+        send_to_all:
+          few: Poslat %{display_count} emaily
+          many: Poslat %{display_count} emailů
+          one: Poslat %{display_count} email
+          other: Poslat %{display_count} emailů
+        title: Náhled oznámení o podmínkách služby
+      publish: Zveřejnit
+      published_on_html: Zveřejněno %{date}
+      save_draft: Uložit koncept
+      title: Podmínky služby
     title: Administrace
     trends:
       allow: Povolit
       approved: Schválené
+      confirm_allow: Opravdu chcete povolit vybrané štítky?
+      confirm_disallow: Opravdu chcete zakázat vybrané štítky?
       disallow: Zakázat
       links:
         allow: Povolit odkaz
         allow_provider: Povolit vydavatele
+        confirm_allow: Jste si jist, že chcete povolit vybrané odkazy?
+        confirm_allow_provider: Opravdu chcete povolit vybrané poskytovatele?
+        confirm_disallow: Opravdu chcete zakázat vybrané odkazy?
+        confirm_disallow_provider: Opravdu chcete zakázat vybrané poskytovatele?
         description_html: Toto jsou odkazy, které jsou momentálně hojně sdíleny účty, jejichž příspěvky váš server vidí. To může pomoct vašim uživatelům zjistit, co se děje ve světě. Žádné odkazy se nezobrazují veřejně, dokud neschválíte vydavatele. Můžete také povolit nebo zamítnout jednotlivé odkazy.
         disallow: Zakázat odkaz
         disallow_provider: Zakázat vydavatele
@@ -944,6 +1025,10 @@ cs:
       statuses:
         allow: Povolit příspěvek
         allow_account: Povolit autora
+        confirm_allow: Opravdu chcete povolit vybrané tooty?
+        confirm_allow_account: Opravdu chcete povolit vybrané účty?
+        confirm_disallow: Opravdu chcete zakázat vybrané tooty?
+        confirm_disallow_account: Opravdu chcete zakázat vybrané účty?
         description_html: Toto jsou příspěvky, o kterých váš server ví, že jsou momentálně hodně sdíleny a oblibovány. To může pomoci vašim novým i vracejícím se uživatelům najít další lidi ke sledování. Žádné příspěvky se nezobrazují veřejně, dokud neschválíte autora a tento autor nepovolí navrhování svého účtu ostatním. Můžete také povolit či zamítnout jednotlivé příspěvky.
         disallow: Zakázat příspěvek
         disallow_account: Zakázat autora
@@ -980,6 +1065,7 @@ cs:
           many: Použit %{count} lidmi za poslední týden
           one: Použit jedním člověkem za poslední týden
           other: Použit %{count} lidmi za poslední týden
+      title: Doporučení & Trendy
       trending: Populární
     warning_presets:
       add_new: Přidat nové
@@ -1066,7 +1152,9 @@ cs:
       guide_link_text: Zapojit se může každý.
     sensitive_content: Citlivý obsah
   application_mailer:
+    notification_preferences: Změnit předvolby e-mailu
     salutation: "%{name},"
+    settings: 'Změnit předvolby e-mailu: %{link}'
     unsubscribe: Přestat odebírat
     view: 'Zobrazit:'
     view_profile: Zobrazit profil
@@ -1086,6 +1174,7 @@ cs:
       hint_html: Ještě jedna věc! Musíme potvrdit, že jste člověk (to proto, abychom drželi stranou spam!). Vyřešte CAPTCHA níže a klikněte na "Pokračovat".
       title: Bezpečnostní kontrola
     confirmations:
+      awaiting_review: Vaše e-mailová adresa je potvrzena! Personál %{domain} nyní kontrolují vaši registraci. Pokud váš účet schválí, obdržíte e-mail!
       awaiting_review_title: Vaše registrace se ověřuje
       clicking_this_link: kliknutím na tento odkaz
       login_link: přihlásit se
@@ -1093,6 +1182,7 @@ cs:
       redirect_to_app_html: Měli byste být přesměrováni do aplikace <strong>%{app_name}</strong>. Pokud se tak nestalo, zkuste %{clicking_this_link} nebo ručně se vrátit do aplikace.
       registration_complete: Vaše registrace na %{domain} je hotová!
       welcome_title: Vítám uživatele %{name}!
+      wrong_email_hint: Pokud není tento e-mail správný, můžete si ho změnit v nastavení účtu.
     delete_account: Odstranit účet
     delete_account_html: Chcete-li odstranit svůj účet, <a href="%{path}">pokračujte zde</a>. Budete požádáni o potvrzení.
     description:
@@ -1112,6 +1202,7 @@ cs:
     migrate_account_html: Zde můžete <a href="%{path}">nastavit přesměrování tohoto účtu na jiný</a>.
     or_log_in_with: Nebo se přihlaste pomocí
     progress:
+      confirm: Potvrdit e-mail
       details: Vaše údaje
       review: Naše hodnocení
       rules: Přijmout pravidla
@@ -1133,22 +1224,37 @@ cs:
     security: Zabezpečení
     set_new_password: Nastavit nové heslo
     setup:
+      email_below_hint_html: Zkontrolujte složku se spamem, nebo požádejte o další. Svou e-mailovou adresu si můžete opravit, pokud je špatně.
+      email_settings_hint_html: Kliknutím na odkaz, který jsme poslali do %{email}, začnete používat Mastodon. Budeme tu čekat.
       link_not_received: Nedostali jste odkaz?
+      new_confirmation_instructions_sent: Za několik minut obdržíte nový e-mail s potvrzovacím odkazem!
       title: Zkontrolujte doručenou poštu
     sign_in:
       preamble_html: Přihlaste se svými <strong>%{domain}</strong> údaji. Pokud je váš účet hostován na jiném serveru, přihlásit se zde nemůžete.
       title: Přihlásit se k %{domain}
     sign_up:
       manual_review: Registrace na %{domain} procházejí manuálním hodnocením od našich moderátorů. Abyste nám pomohli zpracovat Vaši registraci, napište trochu o sobě a proč chcete účet na %{domain}.
+      preamble: S účtem na tomto Mastodon serveru budete moci sledovat jakoukoli jinou osobu na fediversu, bez ohledu na to, kde je jejich účet hostován.
       title: Pojďme vás nastavit na %{domain}.
     status:
       account_status: Stav účtu
+      confirming: Čekáme na dokončení potvrzení e-mailu.
       functional: Váš účet je plně funkční.
+      pending: Vaše žádost čeká na posouzení naším personálem. To může nějakou dobu trvat. Pokud bude váš požadavek schválen, obdržíte e-mail.
       redirecting_to: Váš účet je neaktivní, protože je právě přesměrován na účet %{acct}.
       self_destruct: Protože %{domain} končí, budete mít k účtu jen omezený přístup.
       view_strikes: Zobrazit minulé prohřešky vašeho účtu
     too_fast: Formulář byl odeslán příliš rychle, zkuste to znovu.
     use_security_key: Použít bezpečnostní klíč
+    user_agreement_html: Přečetl jsem si a souhlasím s <a href="%{terms_of_service_path}" target="_blank">podmínkami služby</a> a <a href="%{privacy_policy_path}" target="_blank">ochranou osobních údajů</a>
+  author_attribution:
+    example_title: Ukázkový text
+    hint_html: Píšete novinové články nebo blog mimo Mastodon? Kontrolujte, jak Vám bude připisováno autorství, když jsou sdíleny na Mastodonu.
+    instructions: 'Ujistěte se, že tento kód je v HTML vašeho článku:'
+    more_from_html: Více od %{name}
+    s_blog: Blog %{name}
+    then_instructions: Poté přidejte název domény publikace do níže uvedeného pole.
+    title: Připisování autorství
   challenge:
     confirm: Pokračovat
     hint_html: "<strong>Tip:</strong> Po dobu jedné hodiny vás o heslo nebudeme znovu žádat."
@@ -1185,6 +1291,9 @@ cs:
       before: 'Před pokračováním si prosím pečlivě přečtěte tyto poznámky:'
       caches: Obsah, který byl uložen do cache jiných serverů, nemusí být smazán
       data_removal: Vaše příspěvky a další data budou trvale smazána
+      email_change_html: Můžete <a href="%{path}">změnit svou e-mailovou adresu</a> bez odstranění svého účtu
+      email_contact_html: Pokud stále nedorazí, můžete poslat e-mail <a href="mailto:%{email}">%{email}</a> pro pomoc
+      email_reconfirmation_html: Pokud neobdržíte potvrzovací e-mail, můžete si ho <a href="%{path}">vyžádat znovu</a>
       irreversible: Váš účet nebude možné obnovit ani znovu aktivovat
       more_details_html: Podrobnosti najdete v <a href="%{terms_path}">zásadách ochrany osobních údajů</a>.
       username_available: Vaše uživatelské jméno bude opět dostupné
@@ -1356,6 +1465,68 @@ cs:
       merge_long: Ponechat existující záznamy a přidat nové
       overwrite: Přepsat
       overwrite_long: Nahradit aktuální záznamy novými
+    overwrite_preambles:
+      blocking_html:
+        few: Chystáte se <strong>nahradit váš seznam bloků</strong> s <strong>%{count} účty</strong> z <strong>%{filename}</strong>.
+        many: Chystáte se <strong>nahradit váš seznam bloků</strong> s <strong>%{count} účty</strong> z <strong>%{filename}</strong>.
+        one: Chystáte se <strong>nahradit váš seznam bloků</strong> s <strong>%{count} účtem</strong> z <strong>%{filename}</strong>.
+        other: Chystáte se <strong>nahradit váš seznam bloků</strong> s <strong>%{count} účty</strong> z <strong>%{filename}</strong>.
+      bookmarks_html:
+        few: Chystáte se <strong>nahradit své záložky</strong> s <strong>%{count} příspěvky</strong> z <strong>%{filename}</strong>.
+        many: Chystáte se <strong>nahradit své záložky</strong> s <strong>%{count} příspěvky</strong> z <strong>%{filename}</strong>.
+        one: Chystáte se <strong>nahradit své záložky</strong> s <strong>%{count} příspěvkem</strong> z <strong>%{filename}</strong>.
+        other: Chystáte se <strong>nahradit své záložky</strong> s <strong>%{count} příspěvky</strong> z <strong>%{filename}</strong>.
+      domain_blocking_html:
+        few: Chystáte se <strong>nahradit Váš seznam zablokovaných domén</strong> s <strong>%{count} stránky</strong> z <strong>%{filename}</strong>.
+        many: Chystáte se <strong>nahradit Váš seznam zablokovaných domén</strong> s <strong>%{count} stránky</strong> z <strong>%{filename}</strong>.
+        one: Chystáte se <strong>nahradit Váš seznam zablokovaných domén</strong> s <strong>%{count} stránkou</strong> z <strong>%{filename}</strong>.
+        other: Chystáte se <strong>nahradit Váš seznam zablokovaných domén</strong> s <strong>%{count} stránky</strong> z <strong>%{filename}</strong>.
+      following_html:
+        few: Chystáte se <strong>sledovat</strong> až <strong>%{count} účty</strong> z <strong>%{filename}</strong> a <strong>přestanete sledovat kohokoliv jiného</strong>.
+        many: Chystáte se <strong>sledovat</strong> až <strong>%{count} účtů</strong> z <strong>%{filename}</strong> a <strong>přestanete sledovat kohokoliv jiného</strong>.
+        one: Chystáte se <strong>sledovat</strong> až <strong>%{count} účet</strong> z <strong>%{filename}</strong> a <strong>přestanete sledovat kohokoliv jiného</strong>.
+        other: Chystáte se <strong>sledovat</strong> až <strong>%{count} účtů</strong> z <strong>%{filename}</strong> a <strong>přestanete sledovat kohokoliv jiného</strong>.
+      lists_html:
+        few: Chystáte se <strong>nahradit své seznamy</strong> obsahem <strong>%{filename}</strong>. Až <strong>%{count} účty</strong> budou přidány do nových seznamů.
+        many: Chystáte se <strong>nahradit své seznamy</strong> obsahem <strong>%{filename}</strong>. Až <strong>%{count} účtů</strong> bude přidáno do nových seznamů.
+        one: Chystáte se <strong>nahradit své seznamy</strong> obsahem <strong>%{filename}</strong>. Až <strong>%{count} účet</strong> bude přidán do nových seznamů.
+        other: Chystáte se <strong>nahradit své seznamy</strong> obsahem <strong>%{filename}</strong>. Až <strong>%{count} účtů</strong> bude přidáno do nových seznamů.
+      muting_html:
+        few: Chystáte se <strong>nahradit svůj seznam ztišených účtů</strong> s <strong>%{count} účty</strong> z <strong>%{filename}</strong>.
+        many: Chystáte se <strong>nahradit svůj seznam ztišených účtů</strong> s <strong>%{count} účty</strong> z <strong>%{filename}</strong>.
+        one: Chystáte se <strong>nahradit svůj seznam ztišených účtů</strong> s <strong>%{count} účtem</strong> z <strong>%{filename}</strong>.
+        other: Chystáte se <strong>nahradit svůj seznam ztišených účtů</strong> s <strong>%{count} účty</strong> z <strong>%{filename}</strong>.
+    preambles:
+      blocking_html:
+        few: Chystáte se <strong>zablokovat</strong> až <strong>%{count} účty</strong> z <strong>%{filename}</strong>.
+        many: Chystáte se <strong>zablokovat</strong> až <strong>%{count} účtů</strong> z <strong>%{filename}</strong>.
+        one: Chystáte se <strong>zablokovat</strong> až <strong>%{count} účet</strong> z <strong>%{filename}</strong>.
+        other: Chystáte se <strong>zablokovat</strong> až <strong>%{count} účtů</strong> z <strong>%{filename}</strong>.
+      bookmarks_html:
+        few: Chystáte se přidat až <strong>%{count} příspěvky</strong> z <strong>%{filename}</strong> do vašich <strong>záložek</strong>.
+        many: Chystáte se přidat až <strong>%{count} příspěvků</strong> z <strong>%{filename}</strong> do vašich <strong>záložek</strong>.
+        one: Chystáte se přidat až <strong>%{count} příspěvek</strong> z <strong>%{filename}</strong> do vašich <strong>záložek</strong>.
+        other: Chystáte se přidat až <strong>%{count} příspěvků</strong> z <strong>%{filename}</strong> do vašich <strong>záložek</strong>.
+      domain_blocking_html:
+        few: Chystáte se <strong>zablokovat</strong> až <strong>%{count} domény</strong> z <strong>%{filename}</strong>.
+        many: Chystáte se <strong>zablokovat</strong> až <strong>%{count} domén</strong> z <strong>%{filename}</strong>.
+        one: Chystáte se <strong>zablokovat</strong> až <strong>%{count} doménu</strong> z <strong>%{filename}</strong>.
+        other: Chystáte se <strong>zablokovat</strong> až <strong>%{count} domén</strong> z <strong>%{filename}</strong>.
+      following_html:
+        few: Chystáte se <strong>sledovat</strong> až <strong>%{count} účty</strong> z <strong>%{filename}</strong>.
+        many: Chystáte se <strong>sledovat</strong> až <strong>%{count} účtů</strong> z <strong>%{filename}</strong>.
+        one: Chystáte se <strong>sledovat</strong> až <strong>%{count} účet</strong> z <strong>%{filename}</strong>.
+        other: Chystáte se <strong>sledovat</strong> až <strong>%{count} účtů</strong> z <strong>%{filename}</strong>.
+      lists_html:
+        few: Chystáte se přidat <strong>%{count} účty</strong> z <strong>%{filename}</strong> do vaších <strong>seznamů</strong>. Nové seznamy budou vytvořeny, pokud neexistuje žádný seznam, do kterého by je bylo možné přidat.
+        many: Chystáte se přidat <strong>%{count} účtů</strong> z <strong>%{filename}</strong> do vaších <strong>seznamů</strong>. Nové seznamy budou vytvořeny, pokud neexistuje žádný seznam, do kterého by je bylo možné přidat.
+        one: Chystáte se přidat <strong>%{count} účet</strong> z <strong>%{filename}</strong> do vaších <strong>seznamů</strong>. Nové seznamy budou vytvořeny, pokud neexistuje žádný seznam, do kterého by jej bylo možné přidat.
+        other: Chystáte se přidat <strong>%{count} účtů</strong> z <strong>%{filename}</strong> do vaších <strong>seznamů</strong>. Nové seznamy budou vytvořeny, pokud neexistuje žádný seznam, do kterého by je bylo možné přidat.
+      muting_html:
+        few: Chystáte se <strong>ztišit</strong> až <strong>%{count} účty</strong> z <strong>%{filename}</strong>.
+        many: Chystáte se <strong>ztišit</strong> až <strong>%{count} účtů</strong> z <strong>%{filename}</strong>.
+        one: Chystáte se <strong>ztišit</strong> až <strong>%{count} účet</strong> z <strong>%{filename}</strong>.
+        other: Chystáte se <strong>ztišit</strong> až <strong>%{count} účtů</strong> z <strong>%{filename}</strong>.
     preface: Můžete importovat data, která jste exportovali z jiného serveru, jako například seznam lidí, které sledujete či blokujete.
     recent_imports: Nedávné importy
     states:
@@ -1417,6 +1588,7 @@ cs:
     authentication_methods:
       otp: aplikací pro dvoufaktorové ověření
       password: heslem
+      sign_in_token: bezpečnostní kód e-mailu
       webauthn: bezpečnostními klíči
     description_html: Pokud vidíte aktivitu, kterou nepoznáváte, zvažte změnu hesla a zapnutí dvoufaktorového ověřování.
     empty: Není k dispozici žádná historie přihlášení
@@ -1427,10 +1599,21 @@ cs:
     unsubscribe:
       action: Ano, odeberte odběr
       complete: Odběr byl odhlášen
+      confirmation_html: Jste si jisti, že chcete odhlásit odběr %{type} pro Mastodon na %{domain} na váš e-mail %{email}? Vždy se můžete znovu přihlásit ve svém nastavení <a href="%{settings_path}">e-mailových oznámení</a>.
+      emails:
+        notification_emails:
+          favourite: e-mailové oznámení při oblíbení
+          follow: e-mailové oznámení při sledování
+          follow_request: e-mail při žádost o sledování
+          mention: e-mailové oznámení při zmínění
+          reblog: e-mailové oznámení při boostu
+      resubscribe_html: Pokud jste se odhlásili omylem, můžete se znovu přihlásit ve svých nastavení <a href="%{settings_path}">e-mailových oznámení</a>.
+      success_html: Již nebudete dostávat %{type} pro Mastodon na %{domain} na vaši e-mailovou adresu %{email}.
       title: Odhlásit odběr
   media_attachments:
     validations:
       images_and_video: K příspěvku, který již obsahuje obrázky, nelze připojit video
+      not_found: Média %{ids} nebyla nalezena nebo již byla připojena k jinému příspěvku
       not_ready: Nelze připojit soubory před jejich zpracováním. Zkuste to znovu za chvíli!
       too_many: Nelze připojit více než 4 soubory
   migrations:
@@ -1507,6 +1690,8 @@ cs:
     update:
       subject: Uživatel %{name} upravil příspěvek
   notifications:
+    administration_emails: E-mailová oznámení administrátora
+    email_events: Události pro e-mailová oznámení
     email_events_hint: 'Vyberte události, pro které chcete dostávat oznámení:'
   number:
     human:
@@ -1600,6 +1785,7 @@ cs:
   scheduled_statuses:
     over_daily_limit: Pro dnešek jste překročili limit %{limit} naplánovaných příspěvků
     over_total_limit: Překročili jste limit %{limit} naplánovaných příspěvků
+    too_soon: datum musí být v budoucnu
   self_destruct:
     lead_html: "<strong>%{domain}</strong> bohužel končí nadobro. Pokud jste tam měli účet, nebudete jej moci dále používat, ale stále si můžete vyžádat zálohu vašich dat."
     title: Tento server končí
@@ -1659,10 +1845,12 @@ cs:
     delete: Smazání účtu
     development: Vývoj
     edit_profile: Upravit profil
+    export: Export
     featured_tags: Zvýrazněné hashtagy
     import: Import
     import_and_export: Import a export
     migrate: Přesun účtu
+    notifications: Emailové oznámení
     preferences: Předvolby
     profile: Profil
     relationships: Sledovaní a sledující
@@ -1768,6 +1956,8 @@ cs:
       too_late: Na odvolání proti tomuto prohřešku už je pozdě
   tags:
     does_not_match_previous_name: se neshoduje s předchozím názvem
+  terms_of_service:
+    title: Podmínky služby
   themes:
     contrast: Mastodon (vysoký kontrast)
     default: Mastodon (tmavý)
@@ -1828,6 +2018,15 @@ cs:
       further_actions_html: Pokud jste to nebyli vy, doporučujeme pro zabezpečení vašeho účtu okamžitě %{action} a zapnout dvoufázové ověřování.
       subject: Váš účet byl použit z nové IP adresy
       title: Nové přihlášení
+    terms_of_service_changed:
+      agreement: Pokračováním v používání %{domain} souhlasíte s těmito podmínkami. Pokud nesouhlasíte s aktualizovanými podmínkami, můžete svůj souhlas s %{domain} kdykoliv ukončit odstraněním vašeho účtu.
+      changelog: 'Ve zkratce, zde je to, co tato změna znamená pro vás:'
+      description: 'Tento e-mail jste obdrželi, protože na %{domain} provádíme určité změny našich smluvních podmínek. Doporučujeme vám zkontrolovat aktualizované podmínky v plném znění zde:'
+      description_html: Tento e-mail jste obdrželi, protože na %{domain} provádíme určité změny našich smluvních podmínek. Doporučujeme Vám zkontrolovat <a href="%{path}" target="_blank">aktualizované termíny v plném znění zde</a>.
+      sign_off: Tým %{domain}
+      subject: Aktualizace našich podmínek služby
+      subtitle: Podmínky služby %{domain} se mění
+      title: Důležitá aktualizace
     warning:
       appeal: Podat odvolání
       appeal_description: Pokud se domníváte, že se jedná o chybu, můžete podat odvolání personálu %{instance}.
@@ -1908,6 +2107,7 @@ cs:
     invalid_otp_token: Neplatný kód pro dvoufázové ověřování
     otp_lost_help_html: Pokud jste ztratili přístup k oběma, spojte se s %{email}
     rate_limited: Příliš mnoho pokusů o ověření, zkuste to znovu později.
+    seamless_external_login: Jste přihlášeni přes externí službu, nastavení hesla a e-mailu proto nejsou dostupná.
     signed_in_as: 'Přihlášeni jako:'
   verification:
     extra_instructions_html: <strong>Tip:</strong> Odkaz na vaší webové stránce může být neviditelný. Důležitou součástí je <code>rel="me"</code>, která brání proti napodování vás na webových stránkách s obsahem vytvořeným uživatelem. Můžete dokonce použít <code>odkaz</code> v záhlaví stránky místo <code>a</code>, ale HTML musí být přístupné bez spuštění JavaScript.
@@ -1916,6 +2116,7 @@ cs:
     instructions_html: Zkopírujte a vložte níže uvedený kód do HTML vašeho webu. Poté přidejte adresu vašeho webu do jednoho z extra políček na vašem profilu na záložce "Upravit profil" a uložte změny.
     verification: Ověření
     verified_links: Vaše ověřené odkazy
+    website_verification: Ověření webové stránky
   webauthn_credentials:
     add: Přidat nový bezpečnostní klíč
     create:
diff --git a/config/locales/eo.yml b/config/locales/eo.yml
index c456a424dc..ee45921aaa 100644
--- a/config/locales/eo.yml
+++ b/config/locales/eo.yml
@@ -937,9 +937,13 @@ eo:
       generates:
         action: Generi
         title: Agordo de kondiĉoj de uzado
+      history: Historio
+      live: Antaŭmontro
       no_history: Ankoraŭ ne estas registritaj ŝanĝoj de la kondiĉoj de la servo.
       no_terms_of_service_html: Vi nuntempe ne havas iujn ajn kondiĉojn de la servo agordita. La kondiĉoj de la servo celas doni klarecon kaj protekti vin kontraŭ eblaj respondecoj en disputoj kun viaj uzantoj.
+      notify_users: Informu uzantojn
       preview:
+        send_preview: Sendu antaŭrigardon al %{email}
         send_to_all:
           one: Sendi %{display_count} retpoŝton
           other: Sendi %{display_count} retpoŝtojn
@@ -1926,6 +1930,8 @@ eo:
       subject: Via konto estas alirita de nova IP-adreso
       title: Nova saluto
     terms_of_service_changed:
+      description: 'Vi ricevas ĉi tiun retmesaĝon ĉar ni faras iujn ŝanĝojn al niaj servokondiĉoj ĉe %{domain}. Ni instigas vin revizii la ĝisdatigitajn kondiĉojn tute ĉi tie:'
+      description_html: Vi ricevas ĉi tiun retmesaĝon ĉar ni faras iujn ŝanĝojn al niaj servokondiĉoj ĉe %{domain}. Ni instigas vin revizii la <a href="%{path}" target="_blank">ĝisdatigitajn kondiĉojn plene ĉi tie</a>.
       sign_off: La teamo de %{domain}
       subject: Ĝisdatigoj al niaj kondiĉoj de servo
       subtitle: La kondiĉoj de la servo de %{domain} ŝanĝiĝas
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 0110fe540d..83a9674f84 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -956,6 +956,8 @@ pl:
       updated_msg: Pomyślnie uaktualniono ustawienia hashtagów
     terms_of_service:
       draft: Szkic
+      generate: Użyj szablonu
+      save_draft: Zapisz wersję roboczą
     title: Administracja
     trends:
       allow: Zezwól
@@ -1214,6 +1216,7 @@ pl:
       view_strikes: Zobacz dawne ostrzeżenia nałożone na twoje konto
     too_fast: Zbyt szybko przesłano formularz, spróbuj ponownie.
     use_security_key: Użyj klucza bezpieczeństwa
+    user_agreement_html: Przeczytałem i akceptuję <a href="%{terms_of_service_path}" target="_blank">warunki korzystania z usługi</a> oraz <a href="%{privacy_policy_path}" target="_blank">politykę prywatności</a>
   author_attribution:
     example_title: Przykładowy tekst
     hint_html: Piszesz wiadomości albo bloga poza Mastodonem? Kontroluj jak będą ci przypisywane podczas dizielenia się nimi na Mastodonie.
diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml
index bba71f165d..7dcf0977f3 100644
--- a/config/locales/pt-PT.yml
+++ b/config/locales/pt-PT.yml
@@ -352,7 +352,7 @@ pt-PT:
       not_permitted: Não estás autorizado a executar esta ação
       overwrite: Substituir
       shortcode: Código de atalho
-      shortcode_hint: Pelo menos 2 caracteres, apenas caracteres alfanuméricos e traços inferiores
+      shortcode_hint: Pelo menos 2 caracteres, apenas caracteres alfanuméricos e traços inferiores (_)
       title: Emojis personalizados
       uncategorized: Não categorizados
       unlist: Não listar
@@ -1826,8 +1826,8 @@ pt-PT:
       private_long: Mostrar só aos seguidores
       public: Público
       public_long: Todos podem ver
-      unlisted: Não inventariado
-      unlisted_long: Todos podem ver, mas não será inventariado nas cronologias públicas
+      unlisted: Não listado
+      unlisted_long: Todos podem ver, mas não aparecerá nas cronologias públicas
   statuses_cleanup:
     enabled: Eliminar publicações antigas automaticamente
     enabled_hint: Elimina automaticamente as tuas publicações assim que atingirem um certo limite de tempo, a não ser que correspondam a uma das seguintes exceções
@@ -1945,7 +1945,7 @@ pt-PT:
       appeal: Submeter uma contestação
       appeal_description: Se achas que isto é um erro, podes submeter uma contestação para a equipa de %{instance}.
       categories:
-        spam: Spam
+        spam: Publicidade indesejada / spam
         violation: O conteúdo infringe as seguintes diretrizes da comunidade
       explanation:
         delete_statuses: Algumas das tuas mensagens foram consideradas como violando uma ou mais diretrizes da comunidade e foram subsequentemente removidas pelos moderadores do %{instance}.
diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml
index 0bfeacd9cd..22b92a4434 100644
--- a/config/locales/simple_form.cs.yml
+++ b/config/locales/simple_form.cs.yml
@@ -3,12 +3,14 @@ cs:
   simple_form:
     hints:
       account:
+        attribution_domains_as_text: Jeden na řádek. Chrání před falešným připisování autorství.
         discoverable: Vaše veřejné příspěvky a profil mohou být zobrazeny nebo doporučeny v různých oblastech Mastodonu a váš profil může být navrhován ostatním uživatelům.
         display_name: Vaše celé jméno nebo přezdívka.
         fields: Vaše domovská stránka, zájmena, věk, cokoliv chcete.
         indexable: Vaše veřejné příspěvky se mohou objevit ve výsledcích vyhledávání na Mastodonu. Lidé, kteří s vašimi příspěvky interagovaly, je mohou stále vyhledávat.
         note: 'Můžete @zmínit jiné osoby nebo #hashtagy.'
         show_collections: Lidé budou moci procházet skrz sledující. Lidé, které sledujete, uvidí, že je sledujete bezohledně.
+        unlocked: Lidé vás budou moci sledovat, aniž by vás žádali o schválení. Zrušte zaškrtnutí, pokud chcete kontrolovat požadavky o sledování a vybírat si, zda přijmete nebo odmítnete nové sledující.
       account_alias:
         acct: Zadejte svůj účet, ze kterého se chcete přesunout jinam, ve formátu přezdívka@doména
       account_migration:
@@ -58,6 +60,7 @@ cs:
         setting_display_media_default: Skrývat média označená jako citlivá
         setting_display_media_hide_all: Vždy skrývat média
         setting_display_media_show_all: Vždy zobrazovat média
+        setting_system_scrollbars_ui: Platí pouze pro desktopové prohlížeče založené na Safari nebo Chrome
         setting_use_blurhash: Gradienty jsou vytvořeny na základě barvev skrytých médií, ale zakrývají veškeré detaily
         setting_use_pending_items: Aktualizovat časovou osu až po kliknutí namísto automatického rolování kanálu
         username: Pouze písmena, číslice a podtržítka
@@ -130,8 +133,17 @@ cs:
       terms_of_service:
         changelog: Může být strukturováno pomocí Markdown syntaxu.
         text: Může být strukturováno pomocí Markdown syntaxu.
+      terms_of_service_generator:
+        admin_email: Právní oznámení zahrnují protioznámení, soudní příkazy, žádosti o stáhnutí příspěvků a žádosti od právních vymahačů.
+        arbitration_address: Může být stejné jako výše uvedená fyzická adresa, nebo „N/A“, pokud používáte e-mail
+        arbitration_website: Může být webový formulář nebo „N/A“, pokud používáte e-mail
+        dmca_address: V případě provozovatelů v USA použijte adresu zapsanou v DMCA Designated Agent Directory. Na přímou žádost je možné použít namísto domovské adresy PO box, použijte DMCA Designated Agent Post Office Box Waiver Request k e-mailovaní Copyright Office a řekněte jim, že jste malým moderátorem obsahu, který se obavá pomsty nabo odplaty za Vaši práci a potřebujete použít PO box, abyste schovali Vaši domovskou adresu před širokou veřejností.
+        dmca_email: Může být stejný e-mail použitý pro „E-mail adresy pro právní upozornění“
+        domain: Jedinečná identifikace online služby, kterou poskytujete.
+        jurisdiction: Uveďte zemi, kde žije ten, kdo platí účty. Pokud je to společnost nebo jiný subjekt, uveďte zemi, v níž je zapsán do obchodního rejstříku, a město, region, území, případně stát, pokud je to povinné.
       user:
         chosen_languages: Po zaškrtnutí budou ve veřejných časových osách zobrazeny pouze příspěvky ve zvolených jazycích
+        role: Role určuje, která oprávnění uživatel má.
       user_role:
         color: Barva, která má být použita pro roli v celém UI, jako RGB v hex formátu
         highlighted: Toto roli učiní veřejně viditelnou
@@ -144,6 +156,7 @@ cs:
         url: Kam budou události odesílány
     labels:
       account:
+        attribution_domains_as_text: Webové stránky s povolením Vám připsat autorství
         discoverable: Zobrazovat profil a příspěvky ve vyhledávacích algoritmech
         fields:
           name: Označení
@@ -222,6 +235,7 @@ cs:
         setting_hide_network: Skrýt mou síť
         setting_reduce_motion: Omezit pohyb v animacích
         setting_system_font_ui: Použít výchozí písmo systému
+        setting_system_scrollbars_ui: Použít výchozí posuvník systému
         setting_theme: Vzhled stránky
         setting_trends: Zobrazit dnešní trendy
         setting_unfollow_modal: Před zrušením sledování zobrazovat potvrzovací okno
diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml
index be2ed2135e..59fad146d4 100644
--- a/config/locales/simple_form.eo.yml
+++ b/config/locales/simple_form.eo.yml
@@ -140,6 +140,7 @@ eo:
         dmca_address: Por tenantoj en Usono, uzu la adreson registritan en la DMCA Designated Agenŭ Directory. Registrolibro de poŝtskatoloj haveblas per direkta postulo, uzu la DMCA Designated Agent Post Office Box Waiver Request por retpoŝti la Ofico de Kopirajto kaj priskribu, ke vi estas hejm-trovigita administranto por enhavo kaj devas uzi Poŝtskatolon por forigi vian hejmadreson de publika vido.
         dmca_email: Povas esti la sama retpoŝtadreso uzita por "Retpoŝtadreso por legalaj sciigoj" supre
         domain: Unika identigilo de la retaj servicoj, ke vi provizas.
+        jurisdiction: Enlistigu la landon, kie loĝas kiu pagas la fakturojn. Se ĝi estas kompanio aŭ alia ento, listigu la landon, kie ĝi estas enkorpigita, kaj la urbon, regionon, teritorion aŭ ŝtaton laŭeble.
       user:
         chosen_languages: Kun tio markita nur mesaĝoj en elektitaj lingvoj aperos en publikaj tempolinioj
         role: La rolo kontrolas kiujn permesojn la uzanto havas.
@@ -333,7 +334,13 @@ eo:
         changelog: Kio ŝanĝiĝis?
         text: Kondiĉoj de uzado
       terms_of_service_generator:
+        admin_email: Retpoŝtadreso por laŭleĝaj avizoj
+        arbitration_address: Fizika adreso por arbitraciaj avizoj
+        arbitration_website: Retejo por sendi arbitraciajn avizojn
+        dmca_address: Fizika adreso por DMCA/kopirajto-avizoj
+        dmca_email: Retpoŝtadreso por DMCA/kopirajto-avizoj
         domain: Domajno
+        jurisdiction: Laŭleĝa jurisdikcio
       user:
         role: Rolo
         time_zone: Horzono
diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml
index f5a7e3a27e..207d77f38b 100644
--- a/config/locales/simple_form.pt-PT.yml
+++ b/config/locales/simple_form.pt-PT.yml
@@ -63,7 +63,7 @@ pt-PT:
         setting_system_scrollbars_ui: Aplica-se apenas a navegadores de desktop baseados no Safari e Chrome
         setting_use_blurhash: Os gradientes são baseados nas cores das imagens escondidas, mas ofuscam quaisquer pormenores
         setting_use_pending_items: Ocultar as atualizações da cronologia após um clique em vez de percorrer automaticamente a cronologia
-        username: Pode utilizar letras, números e sublinhados
+        username: Pode utilizar letras, números e traços inferiores (_)
         whole_word: Quando a palavra-chave ou expressão-chave é somente alfanumérica, ela só será aplicada se corresponder à palavra completa
       domain_allow:
         domain: Este domínio será capaz de obter dados desta instância e os dados dele recebidos serão processados e armazenados

From d517fa5ab7979bda3a316b62563b244d6572fbd9 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 13 Jan 2025 10:39:05 +0100
Subject: [PATCH 06/26] Change ActivityPub path generation to all happen in
 `ActivityPub::TagManager` (#33527)

---
 .../activitypub/collections_controller.rb     |  2 +-
 .../activitypub/outboxes_controller.rb        |  8 +---
 .../follower_accounts_controller.rb           |  2 +-
 app/lib/activitypub/tag_manager.rb            | 34 +++++++++++++--
 .../activitypub/actor_serializer.rb           | 14 +++----
 app/serializers/activitypub/add_serializer.rb |  2 +-
 .../activitypub/remove_serializer.rb          |  2 +-
 app/serializers/webfinger_serializer.rb       |  4 +-
 spec/lib/activitypub/tag_manager_spec.rb      | 41 +++++++++++++++++--
 9 files changed, 82 insertions(+), 27 deletions(-)

diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb
index ab1b98e646..c80db3500d 100644
--- a/app/controllers/activitypub/collections_controller.rb
+++ b/app/controllers/activitypub/collections_controller.rb
@@ -49,7 +49,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
 
   def collection_presenter
     ActivityPub::CollectionPresenter.new(
-      id: account_collection_url(@account, params[:id]),
+      id: ActivityPub::TagManager.instance.collection_uri_for(@account, params[:id]),
       type: @type,
       size: @size,
       items: @items
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index 0c995edbf8..a9476b806f 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -41,12 +41,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
     end
   end
 
-  def outbox_url(**)
-    if params[:account_username].present?
-      account_outbox_url(@account, **)
-    else
-      instance_actor_outbox_url(**)
-    end
+  def outbox_url(...)
+    ActivityPub::TagManager.instance.outbox_uri_for(@account, ...)
   end
 
   def next_page
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 5effd9495e..f4c7b37088 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -46,7 +46,7 @@ class FollowerAccountsController < ApplicationController
   end
 
   def page_url(page)
-    account_followers_url(@account, page: page) unless page.nil?
+    ActivityPub::TagManager.instance.followers_uri_for(@account, page: page) unless page.nil?
   end
 
   def next_page_url
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index a489928407..5cba9a8006 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -86,8 +86,34 @@ class ActivityPub::TagManager
     account_status_shares_url(target.account, target)
   end
 
-  def followers_uri_for(target)
-    target.local? ? account_followers_url(target) : target.followers_url.presence
+  def following_uri_for(target, ...)
+    raise ArgumentError, 'target must be a local account' unless target.local?
+
+    account_following_index_url(target, ...)
+  end
+
+  def followers_uri_for(target, ...)
+    return target.followers_url.presence unless target.local?
+
+    account_followers_url(target, ...)
+  end
+
+  def collection_uri_for(target, ...)
+    raise NotImplementedError unless target.local?
+
+    account_collection_url(target, ...)
+  end
+
+  def inbox_uri_for(target)
+    raise NotImplementedError unless target.local?
+
+    target.instance_actor? ? instance_actor_inbox_url : account_inbox_url(target)
+  end
+
+  def outbox_uri_for(target, ...)
+    raise NotImplementedError unless target.local?
+
+    target.instance_actor? ? instance_actor_outbox_url(...) : account_outbox_url(target, ...)
   end
 
   # Primary audience of a status
@@ -99,7 +125,7 @@ class ActivityPub::TagManager
     when 'public'
       [COLLECTIONS[:public]]
     when 'unlisted', 'private'
-      [account_followers_url(status.account)]
+      [followers_uri_for(status.account)]
     when 'direct', 'limited'
       if status.account.silenced?
         # Only notify followers if the account is locally silenced
@@ -133,7 +159,7 @@ class ActivityPub::TagManager
 
     case status.visibility
     when 'public'
-      cc << account_followers_url(status.account)
+      cc << followers_uri_for(status.account)
     when 'unlisted'
       cc << COLLECTIONS[:public]
     end
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index f698e758e8..ed90fa428f 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -44,7 +44,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
   delegate :suspended?, :instance_actor?, to: :object
 
   def id
-    object.instance_actor? ? instance_actor_url : account_url(object)
+    ActivityPub::TagManager.instance.uri_for(object)
   end
 
   def type
@@ -60,27 +60,27 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
   end
 
   def following
-    account_following_index_url(object)
+    ActivityPub::TagManager.instance.following_uri_for(object)
   end
 
   def followers
-    account_followers_url(object)
+    ActivityPub::TagManager.instance.followers_uri_for(object)
   end
 
   def inbox
-    object.instance_actor? ? instance_actor_inbox_url : account_inbox_url(object)
+    ActivityPub::TagManager.instance.inbox_uri_for(object)
   end
 
   def outbox
-    object.instance_actor? ? instance_actor_outbox_url : account_outbox_url(object)
+    ActivityPub::TagManager.instance.outbox_uri_for(object)
   end
 
   def featured
-    account_collection_url(object, :featured)
+    ActivityPub::TagManager.instance.collection_uri_for(object, :featured)
   end
 
   def featured_tags
-    account_collection_url(object, :tags)
+    ActivityPub::TagManager.instance.collection_uri_for(object, :tags)
   end
 
   def endpoints
diff --git a/app/serializers/activitypub/add_serializer.rb b/app/serializers/activitypub/add_serializer.rb
index 436b05086f..640d774272 100644
--- a/app/serializers/activitypub/add_serializer.rb
+++ b/app/serializers/activitypub/add_serializer.rb
@@ -38,6 +38,6 @@ class ActivityPub::AddSerializer < ActivityPub::Serializer
   end
 
   def target
-    account_collection_url(object.account, :featured)
+    ActivityPub::TagManager.instance.collection_uri_for(object.account, :featured)
   end
 end
diff --git a/app/serializers/activitypub/remove_serializer.rb b/app/serializers/activitypub/remove_serializer.rb
index fb224f8a99..4f78804031 100644
--- a/app/serializers/activitypub/remove_serializer.rb
+++ b/app/serializers/activitypub/remove_serializer.rb
@@ -38,6 +38,6 @@ class ActivityPub::RemoveSerializer < ActivityPub::Serializer
   end
 
   def target
-    account_collection_url(object.account, :featured)
+    ActivityPub::TagManager.instance.collection_uri_for(object.account, :featured)
   end
 end
diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb
index b67cd2771a..1cb3175c07 100644
--- a/app/serializers/webfinger_serializer.rb
+++ b/app/serializers/webfinger_serializer.rb
@@ -13,7 +13,7 @@ class WebfingerSerializer < ActiveModel::Serializer
     if object.instance_actor?
       [instance_actor_url]
     else
-      [short_account_url(object), account_url(object)]
+      [short_account_url(object), ActivityPub::TagManager.instance.uri_for(object)]
     end
   end
 
@@ -43,6 +43,6 @@ class WebfingerSerializer < ActiveModel::Serializer
   end
 
   def self_href
-    object.instance_actor? ? instance_actor_url : account_url(object)
+    ActivityPub::TagManager.instance.uri_for(object)
   end
 end
diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb
index 05d2609b0d..4dcfc69824 100644
--- a/spec/lib/activitypub/tag_manager_spec.rb
+++ b/spec/lib/activitypub/tag_manager_spec.rb
@@ -7,17 +7,50 @@ RSpec.describe ActivityPub::TagManager do
 
   subject { described_class.instance }
 
+  let(:domain) { "#{Rails.configuration.x.use_https ? 'https' : 'http'}://#{Rails.configuration.x.web_domain}" }
+
+  describe '#public_collection?' do
+    it 'returns true for the special public collection and common shorthands' do
+      expect(subject.public_collection?('https://www.w3.org/ns/activitystreams#Public')).to be true
+      expect(subject.public_collection?('as:Public')).to be true
+      expect(subject.public_collection?('Public')).to be true
+    end
+
+    it 'returns false for other URIs' do
+      expect(subject.public_collection?('https://example.com/foo/bar')).to be false
+    end
+  end
+
   describe '#url_for' do
-    it 'returns a string' do
+    it 'returns a string starting with web domain' do
       account = Fabricate(:account)
-      expect(subject.url_for(account)).to be_a String
+      expect(subject.url_for(account)).to be_a(String)
+        .and start_with(domain)
     end
   end
 
   describe '#uri_for' do
-    it 'returns a string' do
+    it 'returns a string starting with web domain' do
       account = Fabricate(:account)
-      expect(subject.uri_for(account)).to be_a String
+      expect(subject.uri_for(account)).to be_a(String)
+        .and start_with(domain)
+    end
+  end
+
+  describe '#activity_uri_for' do
+    context 'when given an account' do
+      it 'raises an exception' do
+        account = Fabricate(:account)
+        expect { subject.activity_uri_for(account) }.to raise_error(ArgumentError)
+      end
+    end
+
+    context 'when given a local activity' do
+      it 'returns a string starting with web domain' do
+        status = Fabricate(:status)
+        expect(subject.uri_for(status)).to be_a(String)
+          .and start_with(domain)
+      end
     end
   end
 

From 50449ae7ac57a12d81e0a3e2a6d6197af578e675 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 13 Jan 2025 12:48:47 +0100
Subject: [PATCH 07/26] Fix media preview height in compose form when 3 or more
 images are attached (#33571)

---
 app/javascript/styles/mastodon/components.scss | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index d21175595d..072e7cb4a2 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -6815,6 +6815,8 @@ a.status-card {
   }
 
   &--layout-3 {
+    min-height: calc(64px * 2 + 8px);
+
     & > .media-gallery__item:nth-child(1) {
       border-end-end-radius: 0;
       border-start-end-radius: 0;
@@ -6834,6 +6836,8 @@ a.status-card {
   }
 
   &--layout-4 {
+    min-height: calc(64px * 2 + 8px);
+
     & > .media-gallery__item:nth-child(1) {
       border-end-end-radius: 0;
       border-start-end-radius: 0;

From 68d818121d6b0252084e97ec4e3a7229510e620d Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 13 Jan 2025 13:46:32 +0100
Subject: [PATCH 08/26] Switch `webpush` dependency to latest version of
 Mastodon-maintained fork (#33572)

---
 Gemfile                                           | 2 +-
 Gemfile.lock                                      | 8 ++++----
 app/lib/web_push_request.rb                       | 2 +-
 app/validators/web_push_key_validator.rb          | 2 +-
 spec/workers/web/push_notification_worker_spec.rb | 2 +-
 5 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/Gemfile b/Gemfile
index 8df00d8c0a..f112e5ea5f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -94,7 +94,7 @@ gem 'twitter-text', '~> 3.1.0'
 gem 'tzinfo-data', '~> 1.2023'
 gem 'webauthn', '~> 3.0'
 gem 'webpacker', '~> 5.4'
-gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9'
+gem 'webpush', github: 'mastodon/webpush', ref: '52725def8baf67e0d645c9d1c6c0bdff69da0c60'
 
 gem 'json-ld'
 gem 'json-ld-preloaded', '~> 3.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 4603530de0..596ed7bf52 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,9 +1,9 @@
 GIT
-  remote: https://github.com/ClearlyClaire/webpush.git
-  revision: f14a4d52e201128b1b00245d11b6de80d6cfdcd9
-  ref: f14a4d52e201128b1b00245d11b6de80d6cfdcd9
+  remote: https://github.com/mastodon/webpush.git
+  revision: 52725def8baf67e0d645c9d1c6c0bdff69da0c60
+  ref: 52725def8baf67e0d645c9d1c6c0bdff69da0c60
   specs:
-    webpush (0.3.8)
+    webpush (1.1.0)
       hkdf (~> 0.2)
       jwt (~> 2.0)
 
diff --git a/app/lib/web_push_request.rb b/app/lib/web_push_request.rb
index a43e22480e..91227ed460 100644
--- a/app/lib/web_push_request.rb
+++ b/app/lib/web_push_request.rb
@@ -33,7 +33,7 @@ class WebPushRequest
   end
 
   def encrypt(payload)
-    Webpush::Encryption.encrypt(payload, key_p256dh, key_auth)
+    Webpush::Legacy::Encryption.encrypt(payload, key_p256dh, key_auth)
   end
 
   private
diff --git a/app/validators/web_push_key_validator.rb b/app/validators/web_push_key_validator.rb
index a8ad5c9c6b..25914d59eb 100644
--- a/app/validators/web_push_key_validator.rb
+++ b/app/validators/web_push_key_validator.rb
@@ -3,7 +3,7 @@
 class WebPushKeyValidator < ActiveModel::Validator
   def validate(subscription)
     begin
-      Webpush::Encryption.encrypt('validation_test', subscription.key_p256dh, subscription.key_auth)
+      Webpush::Legacy::Encryption.encrypt('validation_test', subscription.key_p256dh, subscription.key_auth)
     rescue ArgumentError, OpenSSL::PKey::EC::Point::Error
       subscription.errors.add(:base, I18n.t('crypto.errors.invalid_key'))
     end
diff --git a/spec/workers/web/push_notification_worker_spec.rb b/spec/workers/web/push_notification_worker_spec.rb
index 4993d467b3..88d88f2f3d 100644
--- a/spec/workers/web/push_notification_worker_spec.rb
+++ b/spec/workers/web/push_notification_worker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Web::PushNotificationWorker do
     before do
       Setting.site_contact_email = contact_email
 
-      allow(Webpush::Encryption).to receive(:encrypt).and_return(payload)
+      allow(Webpush::Legacy::Encryption).to receive(:encrypt).and_return(payload)
       allow(JWT).to receive(:encode).and_return('jwt.encoded.payload')
 
       stub_request(:post, endpoint).to_return(status: 201, body: '')

From 3c4a83fc6248df47d76cba944dcaac765198e6b4 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 13 Jan 2025 07:58:53 -0500
Subject: [PATCH 09/26] Remove unused `LanguagePresenter#native_name` (#33551)

---
 app/presenters/language_presenter.rb             |  6 +-----
 spec/requests/api/v1/instances/languages_spec.rb | 15 +++++++++++++--
 2 files changed, 14 insertions(+), 7 deletions(-)

diff --git a/app/presenters/language_presenter.rb b/app/presenters/language_presenter.rb
index 69ea991d54..717d70fdbc 100644
--- a/app/presenters/language_presenter.rb
+++ b/app/presenters/language_presenter.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class LanguagePresenter < ActiveModelSerializers::Model
-  attributes :code, :name, :native_name
+  attributes :code, :name
 
   def initialize(code)
     super()
@@ -13,8 +13,4 @@ class LanguagePresenter < ActiveModelSerializers::Model
   def name
     @item[0]
   end
-
-  def native_name
-    @item[1]
-  end
 end
diff --git a/spec/requests/api/v1/instances/languages_spec.rb b/spec/requests/api/v1/instances/languages_spec.rb
index 3d188d23c0..0b325bbb6d 100644
--- a/spec/requests/api/v1/instances/languages_spec.rb
+++ b/spec/requests/api/v1/instances/languages_spec.rb
@@ -9,10 +9,21 @@ RSpec.describe 'Languages' do
     end
 
     it 'returns http success and includes supported languages' do
-      expect(response).to have_http_status(200)
+      expect(response)
+        .to have_http_status(200)
       expect(response.content_type)
         .to start_with('application/json')
-      expect(response.parsed_body.pluck(:code)).to match_array LanguagesHelper::SUPPORTED_LOCALES.keys.map(&:to_s)
+      expect(response.parsed_body)
+        .to match_array(supported_locale_expectations)
+    end
+
+    def supported_locale_expectations
+      LanguagesHelper::SUPPORTED_LOCALES.map do |key, values|
+        include(
+          code: key.to_s,
+          name: values.first
+        )
+      end
     end
   end
 end

From 3a762cddf6311db95c4bd1b674a7bbaccb446152 Mon Sep 17 00:00:00 2001
From: Jonathan de Jong <jonathandejong02@gmail.com>
Date: Mon, 13 Jan 2025 14:35:16 +0100
Subject: [PATCH 10/26] Reject announce payload if object is nil (#33570)

---
 app/lib/activitypub/activity/announce.rb | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index 9dcafff3ab..b57dca9ad7 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -3,6 +3,7 @@
 class ActivityPub::Activity::Announce < ActivityPub::Activity
   def perform
     return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
+    return reject_payload! if @object.nil?
 
     with_redis_lock("announce:#{value_or_id(@object)}") do
       original_status = status_from_object

From 0db75588220719d5f812b33a3e45970118c6acb9 Mon Sep 17 00:00:00 2001
From: Daniel M Brasil <danielmbrasil@protonmail.com>
Date: Mon, 13 Jan 2025 10:50:58 -0300
Subject: [PATCH 11/26] Fix HTTP 500 on `POST /api/v1/admin/ip_blocks` (#29308)

---
 app/models/ip_block.rb                       |  2 +-
 spec/models/ip_block_spec.rb                 |  2 ++
 spec/requests/api/v1/admin/ip_blocks_spec.rb | 10 ++++++++++
 3 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/app/models/ip_block.rb b/app/models/ip_block.rb
index 416ae38382..4c95ac38de 100644
--- a/app/models/ip_block.rb
+++ b/app/models/ip_block.rb
@@ -24,7 +24,7 @@ class IpBlock < ApplicationRecord
     sign_up_requires_approval: 5000,
     sign_up_block: 5500,
     no_access: 9999,
-  }, prefix: true
+  }, prefix: true, validate: true
 
   validates :ip, :severity, presence: true
   validates :ip, uniqueness: true
diff --git a/spec/models/ip_block_spec.rb b/spec/models/ip_block_spec.rb
index 93ee72423b..856d55be9d 100644
--- a/spec/models/ip_block_spec.rb
+++ b/spec/models/ip_block_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe IpBlock do
     it { is_expected.to validate_presence_of(:severity) }
 
     it { is_expected.to validate_uniqueness_of(:ip) }
+
+    it { is_expected.to allow_values(:sign_up_requires_approval, :sign_up_block, :no_access).for(:severity) }
   end
 
   describe '#to_log_human_identifier' do
diff --git a/spec/requests/api/v1/admin/ip_blocks_spec.rb b/spec/requests/api/v1/admin/ip_blocks_spec.rb
index aa3db33915..59ef8d2966 100644
--- a/spec/requests/api/v1/admin/ip_blocks_spec.rb
+++ b/spec/requests/api/v1/admin/ip_blocks_spec.rb
@@ -187,6 +187,16 @@ RSpec.describe 'IP Blocks' do
           .to start_with('application/json')
       end
     end
+
+    context 'when the given severity is invalid' do
+      let(:params) { { ip: '151.0.32.55', severity: 'invalid' } }
+
+      it 'returns http unprocessable entity' do
+        subject
+
+        expect(response).to have_http_status(422)
+      end
+    end
   end
 
   describe 'PUT /api/v1/admin/ip_blocks/:id' do

From f9451c5614061bee2f3e08bc3e76b641f327bc70 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 13 Jan 2025 09:27:30 -0500
Subject: [PATCH 12/26] Fix issue with trending order when user has chosen
 languages (#33557)

---
 app/models/trends/links.rb          |  6 +++---
 app/models/trends/query.rb          | 11 +++++++----
 app/models/trends/statuses.rb       |  6 +++---
 app/models/trends/tags.rb           |  6 +++---
 spec/models/trends/links_spec.rb    | 14 ++++++++++++++
 spec/models/trends/statuses_spec.rb | 14 ++++++++++++++
 spec/models/trends/tags_spec.rb     | 14 ++++++++++++++
 7 files changed, 58 insertions(+), 13 deletions(-)

diff --git a/app/models/trends/links.rb b/app/models/trends/links.rb
index 57ad486631..35ccf7744c 100644
--- a/app/models/trends/links.rb
+++ b/app/models/trends/links.rb
@@ -16,7 +16,7 @@ class Trends::Links < Trends::Base
   class Query < Trends::Query
     def to_arel
       scope = PreviewCard.joins(:trend).reorder(score: :desc)
-      scope = scope.merge(language_order_clause) if preferred_languages.present?
+      scope = scope.reorder(language_order_clause, score: :desc) if preferred_languages.present?
       scope = scope.merge(PreviewCardTrend.allowed) if @allowed
       scope = scope.offset(@offset) if @offset.present?
       scope = scope.limit(@limit) if @limit.present?
@@ -25,8 +25,8 @@ class Trends::Links < Trends::Base
 
     private
 
-    def language_order_clause
-      language_order_for(PreviewCardTrend)
+    def trend_class
+      PreviewCardTrend
     end
   end
 
diff --git a/app/models/trends/query.rb b/app/models/trends/query.rb
index abed64042e..670390ae3c 100644
--- a/app/models/trends/query.rb
+++ b/app/models/trends/query.rb
@@ -94,11 +94,14 @@ class Trends::Query
     to_arel.to_a
   end
 
-  def language_order_for(trend_class)
+  def language_order_clause
+    Arel::Nodes::Case.new.when(language_is_preferred).then(1).else(0).desc
+  end
+
+  def language_is_preferred
     trend_class
-      .reorder(nil)
-      .in_order_of(:language, [preferred_languages], filter: false)
-      .order(score: :desc)
+      .arel_table[:language]
+      .in(preferred_languages)
   end
 
   def preferred_languages
diff --git a/app/models/trends/statuses.rb b/app/models/trends/statuses.rb
index 9c47dd486b..1a41eb9e9a 100644
--- a/app/models/trends/statuses.rb
+++ b/app/models/trends/statuses.rb
@@ -15,7 +15,7 @@ class Trends::Statuses < Trends::Base
   class Query < Trends::Query
     def to_arel
       scope = Status.joins(:trend).reorder(score: :desc)
-      scope = scope.merge(language_order_clause) if preferred_languages.present?
+      scope = scope.reorder(language_order_clause, score: :desc) if preferred_languages.present?
       scope = scope.merge(StatusTrend.allowed) if @allowed
       scope = scope.not_excluded_by_account(@account).not_domain_blocked_by_account(@account) if @account.present?
       scope = scope.offset(@offset) if @offset.present?
@@ -25,8 +25,8 @@ class Trends::Statuses < Trends::Base
 
     private
 
-    def language_order_clause
-      language_order_for(StatusTrend)
+    def trend_class
+      StatusTrend
     end
   end
 
diff --git a/app/models/trends/tags.rb b/app/models/trends/tags.rb
index 84e8dde11a..63897f4f93 100644
--- a/app/models/trends/tags.rb
+++ b/app/models/trends/tags.rb
@@ -16,7 +16,7 @@ class Trends::Tags < Trends::Base
   class Query < Trends::Query
     def to_arel
       scope = Tag.joins(:trend).reorder(score: :desc)
-      scope = scope.merge(language_order_clause) if preferred_languages.present?
+      scope = scope.reorder(language_order_clause, score: :desc) if preferred_languages.present?
       scope = scope.merge(TagTrend.allowed) if @allowed
       scope = scope.offset(@offset) if @offset.present?
       scope = scope.limit(@limit) if @limit.present?
@@ -25,8 +25,8 @@ class Trends::Tags < Trends::Base
 
     private
 
-    def language_order_clause
-      language_order_for(TagTrend)
+    def trend_class
+      TagTrend
     end
   end
 
diff --git a/spec/models/trends/links_spec.rb b/spec/models/trends/links_spec.rb
index b0d41d4613..81a4270c38 100644
--- a/spec/models/trends/links_spec.rb
+++ b/spec/models/trends/links_spec.rb
@@ -24,6 +24,20 @@ RSpec.describe Trends::Links do
               .to eq([lower_score.preview_card, higher_score.preview_card])
           end
         end
+
+        context 'when account has chosen languages' do
+          let!(:lang_match_higher_score) { Fabricate :preview_card_trend, score: 10, language: 'is' }
+          let!(:lang_match_lower_score) { Fabricate :preview_card_trend, score: 1, language: 'da' }
+          let(:user) { Fabricate :user, chosen_languages: %w(da is) }
+          let(:account) { Fabricate :account, user: user }
+
+          before { subject.filtered_for!(account) }
+
+          it 'returns results' do
+            expect(subject.records)
+              .to eq([lang_match_higher_score.preview_card, lang_match_lower_score.preview_card, higher_score.preview_card, lower_score.preview_card])
+          end
+        end
       end
     end
   end
diff --git a/spec/models/trends/statuses_spec.rb b/spec/models/trends/statuses_spec.rb
index abb1535d04..3983901042 100644
--- a/spec/models/trends/statuses_spec.rb
+++ b/spec/models/trends/statuses_spec.rb
@@ -66,6 +66,20 @@ RSpec.describe Trends::Statuses do
               .to eq([lower_score.status, higher_score.status])
           end
         end
+
+        context 'when account has chosen languages' do
+          let!(:lang_match_higher_score) { Fabricate :status_trend, score: 10, language: 'is' }
+          let!(:lang_match_lower_score) { Fabricate :status_trend, score: 1, language: 'da' }
+          let(:user) { Fabricate :user, chosen_languages: %w(da is) }
+          let(:account) { Fabricate :account, user: user }
+
+          before { subject.filtered_for!(account) }
+
+          it 'returns results' do
+            expect(subject.records)
+              .to eq([lang_match_higher_score.status, lang_match_lower_score.status, higher_score.status, lower_score.status])
+          end
+        end
       end
     end
   end
diff --git a/spec/models/trends/tags_spec.rb b/spec/models/trends/tags_spec.rb
index 8f36b4a50d..dacae602ac 100644
--- a/spec/models/trends/tags_spec.rb
+++ b/spec/models/trends/tags_spec.rb
@@ -50,6 +50,20 @@ RSpec.describe Trends::Tags do
               .to eq([lower_score.tag, higher_score.tag])
           end
         end
+
+        context 'when account has chosen languages' do
+          let!(:lang_match_higher_score) { Fabricate :tag_trend, score: 10, language: 'is' }
+          let!(:lang_match_lower_score) { Fabricate :tag_trend, score: 1, language: 'da' }
+          let(:user) { Fabricate :user, chosen_languages: %w(da is) }
+          let(:account) { Fabricate :account, user: user }
+
+          before { subject.filtered_for!(account) }
+
+          it 'returns results' do
+            expect(subject.records)
+              .to eq([lang_match_higher_score.tag, lang_match_lower_score.tag, higher_score.tag, lower_score.tag])
+          end
+        end
       end
     end
   end

From 77a44e61a84dedc293af5ea702f2cd8cb32240ac Mon Sep 17 00:00:00 2001
From: Wolfgang <w.wohanka@gmail.com>
Date: Mon, 13 Jan 2025 17:05:24 +0100
Subject: [PATCH 13/26] Add enum validation to `DomainBlock#severity` (#29158)

---
 app/models/domain_block.rb                       |  2 +-
 spec/requests/api/v1/admin/domain_blocks_spec.rb | 13 +++++++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 2853f6457a..8e7d7b6afc 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -21,7 +21,7 @@ class DomainBlock < ApplicationRecord
   include DomainNormalizable
   include DomainMaterializable
 
-  enum :severity, { silence: 0, suspend: 1, noop: 2 }
+  enum :severity, { silence: 0, suspend: 1, noop: 2 }, validate: true
 
   validates :domain, presence: true, uniqueness: true, domain: true
 
diff --git a/spec/requests/api/v1/admin/domain_blocks_spec.rb b/spec/requests/api/v1/admin/domain_blocks_spec.rb
index 029de72fd7..0b01d04f9a 100644
--- a/spec/requests/api/v1/admin/domain_blocks_spec.rb
+++ b/spec/requests/api/v1/admin/domain_blocks_spec.rb
@@ -217,6 +217,19 @@ RSpec.describe 'Domain Blocks' do
           .to start_with('application/json')
       end
     end
+
+    context 'when severity is invalid' do
+      let(:params) { { domain: 'bar.com', severity: :bar } }
+
+      it 'returns http unprocessable entity' do
+        subject
+
+        expect(response).to have_http_status(422)
+        expect(response.content_type)
+          .to start_with('application/json')
+        expect(response.parsed_body[:error]).to eq('Validation failed: Severity is not included in the list')
+      end
+    end
   end
 
   describe 'PUT /api/v1/admin/domain_blocks/:id' do

From 74da9e9281d549bc59453be71bf386d74946bdb7 Mon Sep 17 00:00:00 2001
From: Jeong Arm <kjwonmail@gmail.com>
Date: Tue, 14 Jan 2025 17:24:00 +0900
Subject: [PATCH 14/26] Fix custom css cache miss (#33583)

---
 app/helpers/theme_helper.rb       |  4 +++-
 spec/helpers/theme_helper_spec.rb | 25 ++++++++++++++++++++++---
 2 files changed, 25 insertions(+), 4 deletions(-)

diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb
index 5dfb4a5184..cda380b3bc 100644
--- a/app/helpers/theme_helper.rb
+++ b/app/helpers/theme_helper.rb
@@ -45,7 +45,9 @@ module ThemeHelper
   end
 
   def cached_custom_css_digest
-    Rails.cache.read(:setting_digest_custom_css)
+    Rails.cache.fetch(:setting_digest_custom_css) do
+      Setting.custom_css&.then { |content| Digest::SHA256.hexdigest(content) }
+    end
   end
 
   def theme_color_for(theme)
diff --git a/spec/helpers/theme_helper_spec.rb b/spec/helpers/theme_helper_spec.rb
index c811b7c981..51611b8211 100644
--- a/spec/helpers/theme_helper_spec.rb
+++ b/spec/helpers/theme_helper_spec.rb
@@ -80,18 +80,37 @@ RSpec.describe ThemeHelper do
   end
 
   describe '#custom_stylesheet' do
+    let(:custom_css) { 'body {}' }
+    let(:custom_digest) { Digest::SHA256.hexdigest(custom_css) }
+
+    before do
+      Setting.custom_css = custom_css
+    end
+
     context 'when custom css setting value digest is present' do
-      before { Rails.cache.write(:setting_digest_custom_css, '1a2s3d4f1a2s3d4f') }
+      before { Rails.cache.write(:setting_digest_custom_css, custom_digest) }
 
       it 'returns value from settings' do
         expect(custom_stylesheet)
-          .to match('/css/custom-1a2s3d4f.css')
+          .to match("/css/custom-#{custom_digest[...8]}.css")
       end
     end
 
-    context 'when custom css setting value digest is not present' do
+    context 'when custom css setting value digest is expired' do
       before { Rails.cache.delete(:setting_digest_custom_css) }
 
+      it 'returns value from settings' do
+        expect(custom_stylesheet)
+          .to match("/css/custom-#{custom_digest[...8]}.css")
+      end
+    end
+
+    context 'when custom css setting is not present' do
+      before do
+        Setting.custom_css = nil
+        Rails.cache.delete(:setting_digest_custom_css)
+      end
+
       it 'returns default value' do
         expect(custom_stylesheet)
           .to be_blank

From ee4edbb94f4006670c819a4db653a8dea347259e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 14 Jan 2025 09:44:58 +0100
Subject: [PATCH 15/26] New Crowdin Translations (automated) (#33582)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/lv.json  |  2 +-
 app/javascript/mastodon/locales/nan.json | 26 ++++++++++++++++++++++++
 config/locales/ca.yml                    |  3 +++
 config/locales/simple_form.ca.yml        |  3 +++
 4 files changed, 33 insertions(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
index 9244f3509c..94c48453b7 100644
--- a/app/javascript/mastodon/locales/lv.json
+++ b/app/javascript/mastodon/locales/lv.json
@@ -345,7 +345,7 @@
   "hints.profiles.see_more_followers": "Skatīt vairāk sekotāju {domain}",
   "hints.profiles.see_more_follows": "Skatīt vairāk sekojumu {domain}",
   "hints.profiles.see_more_posts": "Skatīt vairāk ierakstu {domain}",
-  "hints.threads.replies_may_be_missing": "Var trūkt atbildes no citiem serveriem.",
+  "hints.threads.replies_may_be_missing": "Var trūkt atbilžu no citiem serveriem.",
   "hints.threads.see_more": "Skatīt vairāk atbilžu {domain}",
   "home.column_settings.show_reblogs": "Rādīt pastiprinātos ierakstus",
   "home.column_settings.show_replies": "Rādīt atbildes",
diff --git a/app/javascript/mastodon/locales/nan.json b/app/javascript/mastodon/locales/nan.json
index 009f96b9a7..22a7c507fc 100644
--- a/app/javascript/mastodon/locales/nan.json
+++ b/app/javascript/mastodon/locales/nan.json
@@ -126,9 +126,35 @@
   "bundle_column_error.network.title": "網路錯誤",
   "bundle_column_error.retry": "Koh試",
   "bundle_column_error.return": "Tńg去頭頁",
+  "bundle_column_error.routing.body": "Tshuē bô所要求ê頁面。Lí kám確定地址liâu-á ê URL正確?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "關",
+  "bundle_modal_error.message": "Tī載入tsit ê畫面ê時起錯誤。",
+  "bundle_modal_error.retry": "Koh試",
+  "column.create_list": "建立列單",
+  "column.direct": "私人ê提起",
+  "column.directory": "瀏覽個人資料",
+  "column.domain_blocks": "封鎖ê域名",
+  "column.edit_list": "編輯列單",
+  "column.favourites": "Siōng kah意",
+  "column.firehose": "Tsit-má ê動態",
+  "column.follow_requests": "跟tuè請求",
+  "column.home": "頭頁",
+  "column_header.pin": "釘",
+  "column_header.show_settings": "顯示設定",
+  "column_header.unpin": "Pak掉",
+  "column_search.cancel": "取消",
+  "column_subheading.settings": "設定",
+  "community.column_settings.local_only": "Kan-ta展示本地ê",
+  "community.column_settings.media_only": "Kan-ta展示媒體",
+  "community.column_settings.remote_only": "Kan-ta展示遠距離ê",
   "compose.language.change": "換語言",
+  "compose.language.search": "Tshiau-tshuē語言……",
+  "compose.published.body": "成功PO文。",
+  "compose.published.open": "開",
+  "compose.saved.body": "PO文儲存ah。",
+  "compose_form.direct_message_warning_learn_more": "詳細資訊",
+  "compose_form.encryption_warning": "Mastodon ê PO文無點tuì點加密。M̄通用Mastodon分享任何敏感ê資訊。",
   "confirmations.follow_to_list.confirm": "跟tuè,加入kàu列單",
   "notification.favourite_pm": "{name} kah意lí ê私人提起",
   "notification.favourite_pm.name_and_others_with_link": "{name} kap<a>{count, plural, other {另外 # ê lâng}}</a>kah意lí ê私人提起",
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 66738c8689..acd103a250 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -937,6 +937,7 @@ ca:
       generates:
         action: Generar
         chance_to_review_html: "<strong>Les condicions de servei generades no es publicaran automàticament.</strong> Tindreu l'oportunitat de revisar-ne els resultats. Empleneu els detalls necessaris per a procedir."
+        explanation_html: La plantilla de condicions de servei proveïda ho és només a títol informatiu i no s'ha d'interpretar com a consell jurídic en cap cas. Consulteu el vostre propi servei legal sobre la vostra situació i les qüestions legals específiques que tingueu.
         title: Configuració de les condicions de servei
       history: Historial
       live: En ús
@@ -1892,6 +1893,8 @@ ca:
       title: Un inici de sessió nou
     terms_of_service_changed:
       sign_off: L'equip de %{domain}
+      subject: Actualitzacions de les condicions de servei
+      subtitle: Les condicions de servei de %{domain} canvien
       title: Actualització important
     warning:
       appeal: Envia una apel·lació
diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml
index 2fecc78c79..58cc0a034c 100644
--- a/config/locales/simple_form.ca.yml
+++ b/config/locales/simple_form.ca.yml
@@ -130,6 +130,9 @@ ca:
         show_application: Sempre podràs veure quina aplicació ha publicat els teus tuts.
       tag:
         name: Només pots canviar la caixa de les lletres, per exemple, per fer-la més llegible
+      terms_of_service_generator:
+        domain: Identificació única del servei en línia que oferiu.
+        jurisdiction: Indiqueu el país on resideix qui paga les factures. Si és una empresa o una altra entitat, indiqueu el país en què està registrada, així com la ciutat, regió, territori o estat, segons calqui.
       user:
         chosen_languages: Quan estigui marcat, només es mostraran els tuts de les llengües seleccionades en les línies de temps públiques
         role: El rol controla quins permisos té l'usuari.

From 4a2813158d16914450b6bc4c00e1b3e7edfa26ba Mon Sep 17 00:00:00 2001
From: S1m <31284753+p1gp1g@users.noreply.github.com>
Date: Tue, 14 Jan 2025 10:14:00 +0100
Subject: [PATCH 16/26] Add support for standard webpush (#33528)

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 Gemfile                                       |  2 +-
 Gemfile.lock                                  |  4 +-
 .../api/v1/push/subscriptions_controller.rb   |  3 +-
 .../api/web/push_subscriptions_controller.rb  |  3 +-
 app/lib/web_push_request.rb                   | 26 +++++-
 app/models/web/push_subscription.rb           |  7 +-
 .../rest/web_push_subscription_serializer.rb  |  4 +-
 app/validators/web_push_key_validator.rb      |  2 +-
 app/workers/web/push_notification_worker.rb   | 66 +++++++++++----
 ...11200_add_standard_to_push_subscription.rb |  7 ++
 db/schema.rb                                  |  5 +-
 .../web/push_notification_worker_spec.rb      | 84 ++++++++++++++++---
 12 files changed, 167 insertions(+), 46 deletions(-)
 create mode 100644 db/migrate/20250108111200_add_standard_to_push_subscription.rb

diff --git a/Gemfile b/Gemfile
index f112e5ea5f..2abdae151f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -94,7 +94,7 @@ gem 'twitter-text', '~> 3.1.0'
 gem 'tzinfo-data', '~> 1.2023'
 gem 'webauthn', '~> 3.0'
 gem 'webpacker', '~> 5.4'
-gem 'webpush', github: 'mastodon/webpush', ref: '52725def8baf67e0d645c9d1c6c0bdff69da0c60'
+gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913'
 
 gem 'json-ld'
 gem 'json-ld-preloaded', '~> 3.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 596ed7bf52..89739f9053 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,7 +1,7 @@
 GIT
   remote: https://github.com/mastodon/webpush.git
-  revision: 52725def8baf67e0d645c9d1c6c0bdff69da0c60
-  ref: 52725def8baf67e0d645c9d1c6c0bdff69da0c60
+  revision: 9631ac63045cfabddacc69fc06e919b4c13eb913
+  ref: 9631ac63045cfabddacc69fc06e919b4c13eb913
   specs:
     webpush (1.1.0)
       hkdf (~> 0.2)
diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb
index e1ad89ee3e..d74b5d958f 100644
--- a/app/controllers/api/v1/push/subscriptions_controller.rb
+++ b/app/controllers/api/v1/push/subscriptions_controller.rb
@@ -21,6 +21,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
         endpoint: subscription_params[:endpoint],
         key_p256dh: subscription_params[:keys][:p256dh],
         key_auth: subscription_params[:keys][:auth],
+        standard: subscription_params[:standard] || false,
         data: data_params,
         user_id: current_user.id,
         access_token_id: doorkeeper_token.id
@@ -55,7 +56,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
   end
 
   def subscription_params
-    params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
+    params.require(:subscription).permit(:endpoint, :standard, keys: [:auth, :p256dh])
   end
 
   def data_params
diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb
index f515961427..7eb51c6846 100644
--- a/app/controllers/api/web/push_subscriptions_controller.rb
+++ b/app/controllers/api/web/push_subscriptions_controller.rb
@@ -66,7 +66,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
   end
 
   def subscription_params
-    @subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
+    @subscription_params ||= params.require(:subscription).permit(:standard, :endpoint, keys: [:auth, :p256dh])
   end
 
   def web_push_subscription_params
@@ -76,6 +76,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
       endpoint: subscription_params[:endpoint],
       key_auth: subscription_params[:keys][:auth],
       key_p256dh: subscription_params[:keys][:p256dh],
+      standard: subscription_params[:standard] || false,
       user_id: active_session.user_id,
     }
   end
diff --git a/app/lib/web_push_request.rb b/app/lib/web_push_request.rb
index 91227ed460..85e8ab6bb5 100644
--- a/app/lib/web_push_request.rb
+++ b/app/lib/web_push_request.rb
@@ -2,7 +2,8 @@
 
 class WebPushRequest
   SIGNATURE_ALGORITHM = 'p256ecdsa'
-  AUTH_HEADER = 'WebPush'
+  LEGACY_AUTH_HEADER = 'WebPush'
+  STANDARD_AUTH_HEADER = 'vapid'
   PAYLOAD_EXPIRATION = 24.hours
   JWT_ALGORITHM = 'ES256'
   JWT_TYPE = 'JWT'
@@ -10,6 +11,7 @@ class WebPushRequest
   attr_reader :web_push_subscription
 
   delegate(
+    :standard,
     :endpoint,
     :key_auth,
     :key_p256dh,
@@ -24,20 +26,36 @@ class WebPushRequest
     @audience ||= Addressable::URI.parse(endpoint).normalized_site
   end
 
-  def authorization_header
-    [AUTH_HEADER, encoded_json_web_token].join(' ')
+  def legacy_authorization_header
+    [LEGACY_AUTH_HEADER, encoded_json_web_token].join(' ')
   end
 
   def crypto_key_header
     [SIGNATURE_ALGORITHM, vapid_key.public_key_for_push_header].join('=')
   end
 
-  def encrypt(payload)
+  def legacy_encrypt(payload)
     Webpush::Legacy::Encryption.encrypt(payload, key_p256dh, key_auth)
   end
 
+  def standard_authorization_header
+    [STANDARD_AUTH_HEADER, standard_vapid_value].join(' ')
+  end
+
+  def standard_encrypt(payload)
+    Webpush::Encryption.encrypt(payload, key_p256dh, key_auth)
+  end
+
+  def legacy
+    !standard
+  end
+
   private
 
+  def standard_vapid_value
+    "t=#{encoded_json_web_token},k=#{vapid_key.public_key_for_push_header}"
+  end
+
   def encoded_json_web_token
     JWT.encode(
       web_token_payload,
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index 656040d2ce..12d843cd09 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -5,10 +5,11 @@
 # Table name: web_push_subscriptions
 #
 #  id              :bigint(8)        not null, primary key
-#  endpoint        :string           not null
-#  key_p256dh      :string           not null
-#  key_auth        :string           not null
 #  data            :json
+#  endpoint        :string           not null
+#  key_auth        :string           not null
+#  key_p256dh      :string           not null
+#  standard        :boolean          default(FALSE), not null
 #  created_at      :datetime         not null
 #  updated_at      :datetime         not null
 #  access_token_id :bigint(8)
diff --git a/app/serializers/rest/web_push_subscription_serializer.rb b/app/serializers/rest/web_push_subscription_serializer.rb
index 674a2d5a86..4cb980bb93 100644
--- a/app/serializers/rest/web_push_subscription_serializer.rb
+++ b/app/serializers/rest/web_push_subscription_serializer.rb
@@ -1,7 +1,9 @@
 # frozen_string_literal: true
 
 class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer
-  attributes :id, :endpoint, :alerts, :server_key, :policy
+  attributes :id, :endpoint, :standard, :alerts, :server_key, :policy
+
+  delegate :standard, to: :object
 
   def alerts
     (object.data&.dig('alerts') || {}).each_with_object({}) { |(k, v), h| h[k] = ActiveModel::Type::Boolean.new.cast(v) }
diff --git a/app/validators/web_push_key_validator.rb b/app/validators/web_push_key_validator.rb
index 25914d59eb..a8ad5c9c6b 100644
--- a/app/validators/web_push_key_validator.rb
+++ b/app/validators/web_push_key_validator.rb
@@ -3,7 +3,7 @@
 class WebPushKeyValidator < ActiveModel::Validator
   def validate(subscription)
     begin
-      Webpush::Legacy::Encryption.encrypt('validation_test', subscription.key_p256dh, subscription.key_auth)
+      Webpush::Encryption.encrypt('validation_test', subscription.key_p256dh, subscription.key_auth)
     rescue ArgumentError, OpenSSL::PKey::EC::Point::Error
       subscription.errors.add(:base, I18n.t('crypto.errors.invalid_key'))
     end
diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb
index 3629904fa7..32279a9e74 100644
--- a/app/workers/web/push_notification_worker.rb
+++ b/app/workers/web/push_notification_worker.rb
@@ -19,7 +19,19 @@ class Web::PushNotificationWorker
     # in the meantime, so we have to double-check before proceeding
     return unless @notification.activity.present? && @subscription.pushable?(@notification)
 
-    payload = web_push_request.encrypt(push_notification_json)
+    if web_push_request.legacy
+      perform_legacy_request
+    else
+      perform_standard_request
+    end
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+
+  private
+
+  def perform_legacy_request
+    payload = web_push_request.legacy_encrypt(push_notification_json)
 
     request_pool.with(web_push_request.audience) do |http_client|
       request = Request.new(:post, web_push_request.endpoint, body: payload.fetch(:ciphertext), http_client: http_client)
@@ -31,28 +43,48 @@ class Web::PushNotificationWorker
         'Content-Encoding' => 'aesgcm',
         'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}",
         'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{web_push_request.crypto_key_header}",
-        'Authorization' => web_push_request.authorization_header,
+        'Authorization' => web_push_request.legacy_authorization_header,
         'Unsubscribe-URL' => subscription_url
       )
 
-      request.perform do |response|
-        # If the server responds with an error in the 4xx range
-        # that isn't about rate-limiting or timeouts, we can
-        # assume that the subscription is invalid or expired
-        # and must be removed
-
-        if (400..499).cover?(response.code) && ![408, 429].include?(response.code)
-          @subscription.destroy!
-        elsif !(200...300).cover?(response.code)
-          raise Mastodon::UnexpectedResponseError, response
-        end
-      end
+      send(request)
     end
-  rescue ActiveRecord::RecordNotFound
-    true
   end
 
-  private
+  def perform_standard_request
+    payload = web_push_request.standard_encrypt(push_notification_json)
+
+    request_pool.with(web_push_request.audience) do |http_client|
+      request = Request.new(:post, web_push_request.endpoint, body: payload, http_client: http_client)
+
+      request.add_headers(
+        'Content-Type' => 'application/octet-stream',
+        'Ttl' => TTL.to_s,
+        'Urgency' => URGENCY,
+        'Content-Encoding' => 'aes128gcm',
+        'Authorization' => web_push_request.standard_authorization_header,
+        'Unsubscribe-URL' => subscription_url,
+        'Content-Length' => payload.length.to_s
+      )
+
+      send(request)
+    end
+  end
+
+  def send(request)
+    request.perform do |response|
+      # If the server responds with an error in the 4xx range
+      # that isn't about rate-limiting or timeouts, we can
+      # assume that the subscription is invalid or expired
+      # and must be removed
+
+      if (400..499).cover?(response.code) && ![408, 429].include?(response.code)
+        @subscription.destroy!
+      elsif !(200...300).cover?(response.code)
+        raise Mastodon::UnexpectedResponseError, response
+      end
+    end
+  end
 
   def web_push_request
     @web_push_request || WebPushRequest.new(@subscription)
diff --git a/db/migrate/20250108111200_add_standard_to_push_subscription.rb b/db/migrate/20250108111200_add_standard_to_push_subscription.rb
new file mode 100644
index 0000000000..eb72f9c62e
--- /dev/null
+++ b/db/migrate/20250108111200_add_standard_to_push_subscription.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddStandardToPushSubscription < ActiveRecord::Migration[8.0]
+  def change
+    add_column :web_push_subscriptions, :standard, :boolean, null: false, default: false
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 49b10cb7bd..13abd5c0cd 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,9 +10,9 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[7.2].define(version: 2024_12_16_224825) do
+ActiveRecord::Schema[8.0].define(version: 2025_01_08_111200) do
   # These are extensions that must be enabled in order to support this database
-  enable_extension "plpgsql"
+  enable_extension "pg_catalog.plpgsql"
 
   create_table "account_aliases", force: :cascade do |t|
     t.bigint "account_id", null: false
@@ -1202,6 +1202,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_16_224825) do
     t.datetime "updated_at", precision: nil, null: false
     t.bigint "access_token_id"
     t.bigint "user_id"
+    t.boolean "standard", default: false, null: false
     t.index ["access_token_id"], name: "index_web_push_subscriptions_on_access_token_id", where: "(access_token_id IS NOT NULL)"
     t.index ["user_id"], name: "index_web_push_subscriptions_on_user_id"
   end
diff --git a/spec/workers/web/push_notification_worker_spec.rb b/spec/workers/web/push_notification_worker_spec.rb
index 88d88f2f3d..6ee8ae53f8 100644
--- a/spec/workers/web/push_notification_worker_spec.rb
+++ b/spec/workers/web/push_notification_worker_spec.rb
@@ -5,21 +5,36 @@ require 'rails_helper'
 RSpec.describe Web::PushNotificationWorker do
   subject { described_class.new }
 
-  let(:p256dh) { 'BN4GvZtEZiZuqFxSKVZfSfluwKBD7UxHNBmWkfiZfCtgDE8Bwh-_MtLXbBxTBAWH9r7IPKL0lhdcaqtL1dfxU5E=' }
-  let(:auth) { 'Q2BoAjC09xH3ywDLNJr-dA==' }
   let(:endpoint) { 'https://updates.push.services.mozilla.com/push/v1/subscription-id' }
   let(:user) { Fabricate(:user) }
   let(:notification) { Fabricate(:notification) }
-  let(:subscription) { Fabricate(:web_push_subscription, user_id: user.id, key_p256dh: p256dh, key_auth: auth, endpoint: endpoint, data: { alerts: { notification.type => true } }) }
   let(:vapid_public_key) { 'BB37UCyc8LLX4PNQSe-04vSFvpUWGrENubUaslVFM_l5TxcGVMY0C3RXPeUJAQHKYlcOM2P4vTYmkoo0VZGZTM4=' }
   let(:vapid_private_key) { 'OPrw1Sum3gRoL4-DXfSCC266r-qfFSRZrnj8MgIhRHg=' }
   let(:vapid_key) { Webpush::VapidKey.from_keys(vapid_public_key, vapid_private_key) }
   let(:contact_email) { 'sender@example.com' }
-  let(:ciphertext) { "+\xB8\xDBT}\x13\xB6\xDD.\xF9\xB0\xA7\xC8\xD2\x80\xFD\x99#\xF7\xAC\x83\xA4\xDB,\x1F\xB5\xB9w\x85>\xF7\xADr" }
-  let(:salt) { "X\x97\x953\xE4X\xF8_w\xE7T\x95\xC51q\xFE" }
-  let(:server_public_key) { "\x04\b-RK9w\xDD$\x16lFz\xF9=\xB4~\xC6\x12k\xF3\xF40t\xA9\xC1\fR\xC3\x81\x80\xAC\f\x7F\xE4\xCC\x8E\xC2\x88 n\x8BB\xF1\x9C\x14\a\xFA\x8D\xC9\x80\xA1\xDDyU\\&c\x01\x88#\x118Ua" }
-  let(:shared_secret) { "\t\xA7&\x85\t\xC5m\b\xA8\xA7\xF8B{1\xADk\xE1y'm\xEDE\xEC\xDD\xEDj\xB3$s\xA9\xDA\xF0" }
-  let(:payload) { { ciphertext: ciphertext, salt: salt, server_public_key: server_public_key, shared_secret: shared_secret } }
+
+  # Legacy values
+  let(:p256dh) { 'BN4GvZtEZiZuqFxSKVZfSfluwKBD7UxHNBmWkfiZfCtgDE8Bwh-_MtLXbBxTBAWH9r7IPKL0lhdcaqtL1dfxU5E=' }
+  let(:auth) { 'Q2BoAjC09xH3ywDLNJr-dA==' }
+  let(:legacy_subscription) { Fabricate(:web_push_subscription, user_id: user.id, key_p256dh: p256dh, key_auth: auth, endpoint: endpoint, data: { alerts: { notification.type => true } }) }
+  let(:legacy_payload) do
+    {
+      ciphertext: "+\xB8\xDBT}\x13\xB6\xDD.\xF9\xB0\xA7\xC8\xD2\x80\xFD\x99#\xF7\xAC\x83\xA4\xDB,\x1F\xB5\xB9w\x85>\xF7\xADr",
+      salt: "X\x97\x953\xE4X\xF8_w\xE7T\x95\xC51q\xFE",
+      server_public_key: "\x04\b-RK9w\xDD$\x16lFz\xF9=\xB4~\xC6\x12k\xF3\xF40t\xA9\xC1\fR\xC3\x81\x80\xAC\f\x7F\xE4\xCC\x8E\xC2\x88 n\x8BB\xF1\x9C\x14\a\xFA\x8D\xC9\x80\xA1\xDDyU\\&c\x01\x88#\x118Ua",
+      shared_secret: "\t\xA7&\x85\t\xC5m\b\xA8\xA7\xF8B{1\xADk\xE1y'm\xEDE\xEC\xDD\xEDj\xB3$s\xA9\xDA\xF0",
+    }
+  end
+
+  # Standard values, from RFC8291
+  let(:std_p256dh) { 'BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4' }
+  let(:std_auth) { 'BTBZMqHH6r4Tts7J_aSIgg' }
+  let(:std_as_public) { 'BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8' }
+  let(:std_as_private) { 'yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw' }
+  let(:std_salt) { 'DGv6ra1nlYgDCS1FRnbzlw' }
+  let(:std_subscription) { Fabricate(:web_push_subscription, user_id: user.id, key_p256dh: std_p256dh, key_auth: std_auth, endpoint: endpoint, standard: true, data: { alerts: { notification.type => true } }) }
+  let(:std_input) { 'When I grow up, I want to be a watermelon' }
+  let(:std_ciphertext) { 'DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN' }
 
   describe 'perform' do
     around do |example|
@@ -35,20 +50,40 @@ RSpec.describe Web::PushNotificationWorker do
     before do
       Setting.site_contact_email = contact_email
 
-      allow(Webpush::Legacy::Encryption).to receive(:encrypt).and_return(payload)
       allow(JWT).to receive(:encode).and_return('jwt.encoded.payload')
 
       stub_request(:post, endpoint).to_return(status: 201, body: '')
     end
 
-    it 'calls the relevant service with the correct headers' do
-      subject.perform(subscription.id, notification.id)
+    it 'Legacy push calls the relevant service with the legacy headers' do
+      allow(Webpush::Legacy::Encryption).to receive(:encrypt).and_return(legacy_payload)
 
-      expect(web_push_endpoint_request)
+      subject.perform(legacy_subscription.id, notification.id)
+
+      expect(legacy_web_push_endpoint_request)
         .to have_been_made
     end
 
-    def web_push_endpoint_request
+    # We allow subject stub to encrypt the same input than the RFC8291 example
+    # rubocop:disable RSpec/SubjectStub
+    it 'Standard push calls the relevant service with the standard headers' do
+      # Mock server keys to match RFC example
+      allow(OpenSSL::PKey::EC).to receive(:generate).and_return(std_as_keys)
+      # Mock the random salt to match RFC example
+      rand = Random.new
+      allow(Random).to receive(:new).and_return(rand)
+      allow(rand).to receive(:bytes).and_return(Webpush.decode64(std_salt))
+      # Mock input to match RFC example
+      allow(subject).to receive(:push_notification_json).and_return(std_input)
+
+      subject.perform(std_subscription.id, notification.id)
+
+      expect(standard_web_push_endpoint_request)
+        .to have_been_made
+    end
+    # rubocop:enable RSpec/SubjectStub
+
+    def legacy_web_push_endpoint_request
       a_request(
         :post,
         endpoint
@@ -66,5 +101,28 @@ RSpec.describe Web::PushNotificationWorker do
         body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr"
       )
     end
+
+    def standard_web_push_endpoint_request
+      a_request(
+        :post,
+        endpoint
+      ).with(
+        headers: {
+          'Content-Encoding' => 'aes128gcm',
+          'Content-Type' => 'application/octet-stream',
+          'Ttl' => '172800',
+          'Urgency' => 'normal',
+          'Authorization' => "vapid t=jwt.encoded.payload,k=#{vapid_public_key.delete('=')}",
+          'Unsubscribe-URL' => %r{/api/web/push_subscriptions/},
+        },
+        body: Webpush.decode64(std_ciphertext)
+      )
+    end
+
+    def std_as_keys
+      # VapidKey contains a method to retrieve EC keypair from
+      # B64 raw keys, the keypair is stored in curve field
+      Webpush::VapidKey.from_keys(std_as_public, std_as_private).curve
+    end
   end
 end

From a9a8b6b701026e41b2ee8edb897834b9c799cd2e Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 14 Jan 2025 05:27:21 -0500
Subject: [PATCH 17/26] Expand coverage of admin/trends/* areas (#33581)

---
 .../links/preview_card_providers_spec.rb      | 49 +++++++++--
 spec/system/admin/trends/links_spec.rb        | 85 +++++++++++++++++--
 spec/system/admin/trends/statuses_spec.rb     | 83 ++++++++++++++++--
 spec/system/admin/trends/tags_spec.rb         | 46 ++++++++--
 4 files changed, 232 insertions(+), 31 deletions(-)

diff --git a/spec/system/admin/trends/links/preview_card_providers_spec.rb b/spec/system/admin/trends/links/preview_card_providers_spec.rb
index 0a5b5a7581..159a5b720a 100644
--- a/spec/system/admin/trends/links/preview_card_providers_spec.rb
+++ b/spec/system/admin/trends/links/preview_card_providers_spec.rb
@@ -5,20 +5,49 @@ require 'rails_helper'
 RSpec.describe 'Admin::Trends::Links::PreviewCardProviders' do
   let(:current_user) { Fabricate(:admin_user) }
 
-  before do
-    sign_in current_user
-  end
+  before { sign_in current_user }
 
   describe 'Performing batch updates' do
-    before do
-      visit admin_trends_links_preview_card_providers_path
-    end
-
     context 'without selecting any records' do
       it 'displays a notice about selection' do
+        visit admin_trends_links_preview_card_providers_path
+
         click_on button_for_allow
 
-        expect(page).to have_content(selection_error_text)
+        expect(page)
+          .to have_content(selection_error_text)
+      end
+    end
+
+    context 'with providers that are not trendable' do
+      let!(:provider) { Fabricate :preview_card_provider, trendable: false }
+
+      it 'allows the providers' do
+        visit admin_trends_links_preview_card_providers_path
+
+        check_item
+
+        expect { click_on button_for_allow }
+          .to change { provider.reload.trendable? }.from(false).to(true)
+      end
+    end
+
+    context 'with providers that are trendable' do
+      let!(:provider) { Fabricate :preview_card_provider, trendable: true }
+
+      it 'disallows the providers' do
+        visit admin_trends_links_preview_card_providers_path
+
+        check_item
+
+        expect { click_on button_for_disallow }
+          .to change { provider.reload.trendable? }.from(true).to(false)
+      end
+    end
+
+    def check_item
+      within '.batch-table__row' do
+        find('input[type=checkbox]').check
       end
     end
 
@@ -26,6 +55,10 @@ RSpec.describe 'Admin::Trends::Links::PreviewCardProviders' do
       I18n.t('admin.trends.allow')
     end
 
+    def button_for_disallow
+      I18n.t('admin.trends.disallow')
+    end
+
     def selection_error_text
       I18n.t('admin.trends.links.publishers.no_publisher_selected')
     end
diff --git a/spec/system/admin/trends/links_spec.rb b/spec/system/admin/trends/links_spec.rb
index 15138f42d1..879bbe8ad9 100644
--- a/spec/system/admin/trends/links_spec.rb
+++ b/spec/system/admin/trends/links_spec.rb
@@ -5,20 +5,77 @@ require 'rails_helper'
 RSpec.describe 'Admin::Trends::Links' do
   let(:current_user) { Fabricate(:admin_user) }
 
-  before do
-    sign_in current_user
-  end
+  before { sign_in current_user }
 
   describe 'Performing batch updates' do
-    before do
-      visit admin_trends_links_path
-    end
-
     context 'without selecting any records' do
       it 'displays a notice about selection' do
+        visit admin_trends_links_path
+
         click_on button_for_allow
 
-        expect(page).to have_content(selection_error_text)
+        expect(page)
+          .to have_content(selection_error_text)
+      end
+    end
+
+    context 'with links that are not trendable' do
+      let!(:preview_card_trend) { Fabricate :preview_card_trend, preview_card: Fabricate(:preview_card, trendable: false) }
+
+      it 'allows the links' do
+        visit admin_trends_links_path
+
+        check_item
+
+        expect { click_on button_for_allow }
+          .to change { preview_card_trend.preview_card.reload.trendable? }.from(false).to(true)
+      end
+    end
+
+    context 'with links whose providers are not trendable' do
+      let(:preview_card_provider) { Fabricate :preview_card_provider, trendable: false }
+      let!(:preview_card_trend) { Fabricate :preview_card_trend, preview_card: Fabricate(:preview_card, url: "https://#{preview_card_provider.domain}/page") }
+
+      it 'allows the providers of the links' do
+        visit admin_trends_links_path
+
+        check_item
+
+        expect { click_on button_for_allow_providers }
+          .to change { preview_card_trend.preview_card.provider.reload.trendable? }.from(false).to(true)
+      end
+    end
+
+    context 'with links that are trendable' do
+      let!(:preview_card_trend) { Fabricate :preview_card_trend, preview_card: Fabricate(:preview_card, trendable: true) }
+
+      it 'disallows the links' do
+        visit admin_trends_links_path
+
+        check_item
+
+        expect { click_on button_for_disallow }
+          .to change { preview_card_trend.preview_card.reload.trendable? }.from(true).to(false)
+      end
+    end
+
+    context 'with links whose providers are trendable' do
+      let(:preview_card_provider) { Fabricate :preview_card_provider, trendable: true }
+      let!(:preview_card_trend) { Fabricate :preview_card_trend, preview_card: Fabricate(:preview_card, url: "https://#{preview_card_provider.domain}/page") }
+
+      it 'disallows the links' do
+        visit admin_trends_links_path
+
+        check_item
+
+        expect { click_on button_for_disallow_providers }
+          .to change { preview_card_trend.preview_card.provider.reload.trendable? }.from(true).to(false)
+      end
+    end
+
+    def check_item
+      within '.batch-table__row' do
+        find('input[type=checkbox]').check
       end
     end
 
@@ -26,6 +83,18 @@ RSpec.describe 'Admin::Trends::Links' do
       I18n.t('admin.trends.links.allow')
     end
 
+    def button_for_allow_providers
+      I18n.t('admin.trends.links.allow_provider')
+    end
+
+    def button_for_disallow
+      I18n.t('admin.trends.links.disallow')
+    end
+
+    def button_for_disallow_providers
+      I18n.t('admin.trends.links.disallow_provider')
+    end
+
     def selection_error_text
       I18n.t('admin.trends.links.no_link_selected')
     end
diff --git a/spec/system/admin/trends/statuses_spec.rb b/spec/system/admin/trends/statuses_spec.rb
index 45c048afb0..be081df989 100644
--- a/spec/system/admin/trends/statuses_spec.rb
+++ b/spec/system/admin/trends/statuses_spec.rb
@@ -5,20 +5,75 @@ require 'rails_helper'
 RSpec.describe 'Admin::Trends::Statuses' do
   let(:current_user) { Fabricate(:admin_user) }
 
-  before do
-    sign_in current_user
-  end
+  before { sign_in current_user }
 
   describe 'Performing batch updates' do
-    before do
-      visit admin_trends_statuses_path
-    end
-
     context 'without selecting any records' do
       it 'displays a notice about selection' do
+        visit admin_trends_statuses_path
+
         click_on button_for_allow
 
-        expect(page).to have_content(selection_error_text)
+        expect(page)
+          .to have_content(selection_error_text)
+      end
+    end
+
+    context 'with statuses that are not trendable' do
+      let!(:status_trend) { Fabricate :status_trend, status: Fabricate(:status, trendable: false) }
+
+      it 'allows the statuses' do
+        visit admin_trends_statuses_path
+
+        check_item
+
+        expect { click_on button_for_allow }
+          .to change { status_trend.status.reload.trendable? }.from(false).to(true)
+      end
+    end
+
+    context 'with statuses whose accounts are not trendable' do
+      let!(:status_trend) { Fabricate :status_trend, status: Fabricate(:status, account: Fabricate(:account, trendable: false)) }
+
+      it 'allows the accounts of the statuses' do
+        visit admin_trends_statuses_path
+
+        check_item
+
+        expect { click_on button_for_allow_accounts }
+          .to change { status_trend.status.account.reload.trendable? }.from(false).to(true)
+      end
+    end
+
+    context 'with statuses that are trendable' do
+      let!(:status_trend) { Fabricate :status_trend, status: Fabricate(:status, trendable: true) }
+
+      it 'disallows the statuses' do
+        visit admin_trends_statuses_path
+
+        check_item
+
+        expect { click_on button_for_disallow }
+          .to change { status_trend.status.reload.trendable? }.from(true).to(false)
+      end
+    end
+
+    context 'with statuses whose accounts are trendable' do
+      let!(:status_trend) { Fabricate :status_trend, status: Fabricate(:status, account: Fabricate(:account, trendable: true)) }
+
+      it 'disallows the statuses' do
+        visit admin_trends_statuses_path
+
+        check_item
+
+        expect { click_on button_for_disallow_accounts }
+          .to change { status_trend.status.reload.trendable? }.from(true).to(false)
+      end
+    end
+
+    def check_item
+      within '.batch-table__row' do
+        find('input[type=checkbox]').check
       end
     end
 
@@ -26,6 +81,18 @@ RSpec.describe 'Admin::Trends::Statuses' do
       I18n.t('admin.trends.statuses.allow')
     end
 
+    def button_for_allow_accounts
+      I18n.t('admin.trends.statuses.allow_account')
+    end
+
+    def button_for_disallow
+      I18n.t('admin.trends.statuses.disallow')
+    end
+
+    def button_for_disallow_accounts
+      I18n.t('admin.trends.statuses.disallow_account')
+    end
+
     def selection_error_text
       I18n.t('admin.trends.statuses.no_status_selected')
     end
diff --git a/spec/system/admin/trends/tags_spec.rb b/spec/system/admin/trends/tags_spec.rb
index 30b0850b93..a71d9ba8ca 100644
--- a/spec/system/admin/trends/tags_spec.rb
+++ b/spec/system/admin/trends/tags_spec.rb
@@ -5,27 +5,59 @@ require 'rails_helper'
 RSpec.describe 'Admin::Trends::Tags' do
   let(:current_user) { Fabricate(:admin_user) }
 
-  before do
-    sign_in current_user
-  end
+  before { sign_in current_user }
 
   describe 'Performing batch updates' do
-    before do
-      visit admin_trends_tags_path
-    end
-
     context 'without selecting any records' do
       it 'displays a notice about selection' do
+        visit admin_trends_tags_path
+
         click_on button_for_allow
 
         expect(page).to have_content(selection_error_text)
       end
     end
 
+    context 'with tags that are not trendable' do
+      let!(:tag_trend) { Fabricate :tag_trend, tag: Fabricate(:tag, trendable: false) }
+
+      it 'allows the tags' do
+        visit admin_trends_tags_path
+
+        check_item
+
+        expect { click_on button_for_allow }
+          .to change { tag_trend.tag.reload.trendable? }.from(false).to(true)
+      end
+    end
+
+    context 'with tags that are trendable' do
+      let!(:tag_trend) { Fabricate :tag_trend, tag: Fabricate(:tag, trendable: true) }
+
+      it 'disallows the tags' do
+        visit admin_trends_tags_path
+
+        check_item
+
+        expect { click_on button_for_disallow }
+          .to change { tag_trend.tag.reload.trendable? }.from(true).to(false)
+      end
+    end
+
+    def check_item
+      within '.batch-table__row' do
+        find('input[type=checkbox]').check
+      end
+    end
+
     def button_for_allow
       I18n.t('admin.trends.allow')
     end
 
+    def button_for_disallow
+      I18n.t('admin.trends.disallow')
+    end
+
     def selection_error_text
       I18n.t('admin.trends.tags.no_tag_selected')
     end

From e2f085e2b2cec08dc1f5ae825730c2a3bf62e054 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Tue, 14 Jan 2025 11:42:06 +0100
Subject: [PATCH 18/26] Use final specification for new WebPush subscriptions
 in web interface (#33587)

---
 .../mastodon/actions/push_notifications/registerer.js           | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js
index b3d3850e31..647a6bd9fb 100644
--- a/app/javascript/mastodon/actions/push_notifications/registerer.js
+++ b/app/javascript/mastodon/actions/push_notifications/registerer.js
@@ -33,7 +33,7 @@ const unsubscribe = ({ registration, subscription }) =>
   subscription ? subscription.unsubscribe().then(() => registration) : registration;
 
 const sendSubscriptionToBackend = (subscription) => {
-  const params = { subscription };
+  const params = { subscription: { ...subscription.toJSON(), standard: true } };
 
   if (me) {
     const data = pushNotificationsSetting.get(me);

From e9462960a7cf416b9e459d599a0f2cc8c5b070f0 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Tue, 14 Jan 2025 14:10:48 +0100
Subject: [PATCH 19/26] Redirect new users to onboarding (#33471)

---
 app/javascript/mastodon/api_types/accounts.ts       |  2 +-
 .../mastodon/features/onboarding/profile.tsx        |  6 +++++-
 app/javascript/mastodon/features/ui/index.jsx       | 13 +++++++++----
 3 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts
index fdbd7523fc..3f8b27497f 100644
--- a/app/javascript/mastodon/api_types/accounts.ts
+++ b/app/javascript/mastodon/api_types/accounts.ts
@@ -19,7 +19,7 @@ export interface BaseApiAccountJSON {
   avatar_static: string;
   bot: boolean;
   created_at: string;
-  discoverable: boolean;
+  discoverable?: boolean;
   indexable: boolean;
   display_name: string;
   emojis: ApiCustomEmojiJSON[];
diff --git a/app/javascript/mastodon/features/onboarding/profile.tsx b/app/javascript/mastodon/features/onboarding/profile.tsx
index 1e5e868f18..d9b394acfb 100644
--- a/app/javascript/mastodon/features/onboarding/profile.tsx
+++ b/app/javascript/mastodon/features/onboarding/profile.tsx
@@ -12,6 +12,7 @@ import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate
 import EditIcon from '@/material-icons/400-24px/edit.svg?react';
 import PersonIcon from '@/material-icons/400-24px/person.svg?react';
 import { updateAccount } from 'mastodon/actions/accounts';
+import { closeOnboarding } from 'mastodon/actions/onboarding';
 import { Button } from 'mastodon/components/button';
 import { Column } from 'mastodon/components/column';
 import { ColumnHeader } from 'mastodon/components/column_header';
@@ -58,7 +59,9 @@ export const Profile: React.FC<{
   );
   const [avatar, setAvatar] = useState<File>();
   const [header, setHeader] = useState<File>();
-  const [discoverable, setDiscoverable] = useState(true);
+  const [discoverable, setDiscoverable] = useState(
+    account?.discoverable ?? true,
+  );
   const [isSaving, setIsSaving] = useState(false);
   const [errors, setErrors] = useState<ApiAccountErrors>();
   const avatarFileRef = createRef<HTMLInputElement>();
@@ -132,6 +135,7 @@ export const Profile: React.FC<{
     )
       .then(() => {
         history.push('/start/follows');
+        dispatch(closeOnboarding());
         return '';
       })
       // eslint-disable-next-line @typescript-eslint/use-unknown-in-catch-callback-variable
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 1ecc52b6bd..b239e63ccd 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -92,6 +92,7 @@ const mapStateToProps = state => ({
   hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
   canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']),
   firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
+  newAccount: !state.getIn(['accounts', me, 'note']) && !state.getIn(['accounts', me, 'bot']) && state.getIn(['accounts', me, 'following_count'], 0) === 0 && state.getIn(['accounts', me, 'statuses_count'], 0) === 0,
   username: state.getIn(['accounts', me, 'username']),
 });
 
@@ -135,6 +136,7 @@ class SwitchingColumnsArea extends PureComponent {
     children: PropTypes.node,
     location: PropTypes.object,
     singleColumn: PropTypes.bool,
+    forceOnboarding: PropTypes.bool,
   };
 
   UNSAFE_componentWillMount () {
@@ -165,14 +167,16 @@ class SwitchingColumnsArea extends PureComponent {
   };
 
   render () {
-    const { children, singleColumn } = this.props;
+    const { children, singleColumn, forceOnboarding } = this.props;
     const { signedIn } = this.props.identity;
     const pathName = this.props.location.pathname;
 
     let redirect;
 
     if (signedIn) {
-      if (singleColumn) {
+      if (forceOnboarding) {
+        redirect = <Redirect from='/' to='/start' exact />;
+      } else if (singleColumn) {
         redirect = <Redirect from='/' to='/home' exact />;
       } else {
         redirect = <Redirect from='/' to='/deck/getting-started' exact />;
@@ -276,6 +280,7 @@ class UI extends PureComponent {
     intl: PropTypes.object.isRequired,
     layout: PropTypes.string.isRequired,
     firstLaunch: PropTypes.bool,
+    newAccount: PropTypes.bool,
     username: PropTypes.string,
     ...WithRouterPropTypes,
   };
@@ -568,7 +573,7 @@ class UI extends PureComponent {
 
   render () {
     const { draggingOver } = this.state;
-    const { children, isComposing, location, layout } = this.props;
+    const { children, isComposing, location, layout, firstLaunch, newAccount } = this.props;
 
     const handlers = {
       help: this.handleHotkeyToggleHelp,
@@ -597,7 +602,7 @@ class UI extends PureComponent {
         <div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
           <Header />
 
-          <SwitchingColumnsArea identity={this.props.identity} location={location} singleColumn={layout === 'mobile' || layout === 'single-column'}>
+          <SwitchingColumnsArea identity={this.props.identity} location={location} singleColumn={layout === 'mobile' || layout === 'single-column'} forceOnboarding={firstLaunch && newAccount}>
             {children}
           </SwitchingColumnsArea>
 

From 7b608b41f29ccb05fe9521fc83e3f81769f1e8df Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 14 Jan 2025 15:13:58 +0100
Subject: [PATCH 20/26] Update dependency
 @babel/plugin-transform-nullish-coalescing-operator to v7.26.6 (#33584)

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 0b3b8b3d95..337956163f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -935,13 +935,13 @@ __metadata:
   linkType: hard
 
 "@babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.3, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.25.9":
-  version: 7.26.5
-  resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.26.5"
+  version: 7.26.6
+  resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.26.6"
   dependencies:
     "@babel/helper-plugin-utils": "npm:^7.26.5"
   peerDependencies:
     "@babel/core": ^7.0.0-0
-  checksum: 10c0/2e4b84745f9e8c40caf3e611641de4d6c7da6f96c2925b7fe568e3b031ed1864e325b9dffc9cda4e442fc40be43ffabb088782e980d411e0562bd5222df547ec
+  checksum: 10c0/574d6db7cbc5c092db5d1dece8ce26195e642b9c40dbfeaf3082058a78ad7959c1c333471cdd45f38b784ec488850548075d527b178c5010ee9bff7aa527cc7a
   languageName: node
   linkType: hard
 

From 6356870dae9159afceac2bc3b4a3f1bfc702d5be Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 14 Jan 2025 15:14:03 +0100
Subject: [PATCH 21/26] Update dependency sass to v1.83.4 (#33585)

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 337956163f..f112e77920 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15623,8 +15623,8 @@ __metadata:
   linkType: hard
 
 "sass@npm:^1.62.1":
-  version: 1.83.1
-  resolution: "sass@npm:1.83.1"
+  version: 1.83.4
+  resolution: "sass@npm:1.83.4"
   dependencies:
     "@parcel/watcher": "npm:^2.4.1"
     chokidar: "npm:^4.0.0"
@@ -15635,7 +15635,7 @@ __metadata:
       optional: true
   bin:
     sass: sass.js
-  checksum: 10c0/9772506cd8290df7b5e800055098e91a8a65100840fd9e90c660deb74b248b3ddbbd1a274b8f7f09777d472d2c873575357bd87939a40fb5a80bdf654985486f
+  checksum: 10c0/6f27f0eebfeb50222b14baaeef548ef58a05daf8abd9797e6c499334ed7ad40541767056c8693780d06ca83d8836348ea7396a923d3be439b133507993ca78be
   languageName: node
   linkType: hard
 

From 87849d739e08036e44649643ca05e18b2723533a Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 14 Jan 2025 15:17:11 +0100
Subject: [PATCH 22/26] Update dependency rspec-github to v3 (#33589)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 Gemfile      | 2 +-
 Gemfile.lock | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/Gemfile b/Gemfile
index 2abdae151f..0efcc6562c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -125,7 +125,7 @@ group :test do
   gem 'flatware-rspec'
 
   # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
-  gem 'rspec-github', '~> 2.4', require: false
+  gem 'rspec-github', '~> 3.0', require: false
 
   # RSpec helpers for email specs
   gem 'email_spec'
diff --git a/Gemfile.lock b/Gemfile.lock
index 89739f9053..a625765faf 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -690,7 +690,7 @@ GEM
     rspec-expectations (3.13.3)
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.13.0)
-    rspec-github (2.4.0)
+    rspec-github (3.0.0)
       rspec-core (~> 3.0)
     rspec-mocks (3.13.2)
       diff-lcs (>= 1.2.0, < 2.0)
@@ -994,7 +994,7 @@ DEPENDENCIES
   redis (~> 4.5)
   redis-namespace (~> 1.10)
   rqrcode (~> 2.2)
-  rspec-github (~> 2.4)
+  rspec-github (~> 3.0)
   rspec-rails (~> 7.0)
   rspec-sidekiq (~> 5.0)
   rubocop

From 7c56517c7c2f3e284b912f7519682c3612d3af81 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 14 Jan 2025 09:32:29 -0500
Subject: [PATCH 23/26] Move mastodon version config to `config_for` yml
 (#33577)

---
 config/mastodon.yml                        |  4 ++++
 lib/mastodon/version.rb                    | 16 ++++++++++++----
 spec/presenters/instance_presenter_spec.rb |  6 ++++++
 3 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/config/mastodon.yml b/config/mastodon.yml
index e20ba0ab05..a4442e873c 100644
--- a/config/mastodon.yml
+++ b/config/mastodon.yml
@@ -2,6 +2,10 @@
 shared:
   self_destruct_value: <%= ENV.fetch('SELF_DESTRUCT', nil) %>
   software_update_url: <%= ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check') %>
+  source:
+    base_url: <%= ENV.fetch('SOURCE_BASE_URL', nil) %>
+    repository: <%= ENV.fetch('GITHUB_REPOSITORY', 'mastodon/mastodon') %>
+    tag: <%= ENV.fetch('SOURCE_TAG', nil) %>
   version:
     metadata: <%= ENV.fetch('MASTODON_VERSION_METADATA', nil) %>
     prerelease: <%= ENV.fetch('MASTODON_VERSION_PRERELEASE', nil) %>
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index ddde4a993d..19779eb609 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -50,16 +50,16 @@ module Mastodon
     end
 
     def repository
-      ENV.fetch('GITHUB_REPOSITORY', 'mastodon/mastodon')
+      source_configuration[:repository]
     end
 
     def source_base_url
-      ENV.fetch('SOURCE_BASE_URL', "https://github.com/#{repository}")
+      source_configuration[:base_url] || "https://github.com/#{repository}"
     end
 
     # specify git tag or commit hash here
     def source_tag
-      ENV.fetch('SOURCE_TAG', nil)
+      source_configuration[:tag]
     end
 
     def source_url
@@ -79,7 +79,15 @@ module Mastodon
     end
 
     def version_configuration
-      Rails.configuration.x.mastodon.version
+      mastodon_configuration.version
+    end
+
+    def source_configuration
+      mastodon_configuration.source
+    end
+
+    def mastodon_configuration
+      Rails.configuration.x.mastodon
     end
   end
 end
diff --git a/spec/presenters/instance_presenter_spec.rb b/spec/presenters/instance_presenter_spec.rb
index 42f5200f3a..cc6e0533bb 100644
--- a/spec/presenters/instance_presenter_spec.rb
+++ b/spec/presenters/instance_presenter_spec.rb
@@ -68,6 +68,7 @@ RSpec.describe InstancePresenter do
     context 'with the GITHUB_REPOSITORY env variable set' do
       around do |example|
         ClimateControl.modify GITHUB_REPOSITORY: 'other/repo' do
+          reload_configuration
           example.run
         end
       end
@@ -80,6 +81,7 @@ RSpec.describe InstancePresenter do
     context 'without the GITHUB_REPOSITORY env variable set' do
       around do |example|
         ClimateControl.modify GITHUB_REPOSITORY: nil do
+          reload_configuration
           example.run
         end
       end
@@ -88,6 +90,10 @@ RSpec.describe InstancePresenter do
         expect(instance_presenter.source_url).to eq('https://github.com/mastodon/mastodon')
       end
     end
+
+    def reload_configuration
+      Rails.configuration.x.mastodon = Rails.application.config_for(:mastodon)
+    end
   end
 
   describe '#thumbnail' do

From 50013b10a50a560ebf4432cbe1782426181dba6f Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 14 Jan 2025 09:32:57 -0500
Subject: [PATCH 24/26] Add `Status::Visibility` concern to hold visibility
 logic (#33578)

---
 app/models/concerns/status/visibility.rb      |  47 +++++
 app/models/status.rb                          |  27 +--
 spec/models/status_spec.rb                    |  32 +--
 .../models/concerns/status/visibility.rb      | 184 ++++++++++++++++++
 4 files changed, 234 insertions(+), 56 deletions(-)
 create mode 100644 app/models/concerns/status/visibility.rb
 create mode 100644 spec/support/examples/models/concerns/status/visibility.rb

diff --git a/app/models/concerns/status/visibility.rb b/app/models/concerns/status/visibility.rb
new file mode 100644
index 0000000000..e17196eb15
--- /dev/null
+++ b/app/models/concerns/status/visibility.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Status::Visibility
+  extend ActiveSupport::Concern
+
+  included do
+    enum :visibility,
+         { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4 },
+         suffix: :visibility,
+         validate: true
+
+    scope :distributable_visibility, -> { where(visibility: %i(public unlisted)) }
+    scope :list_eligible_visibility, -> { where(visibility: %i(public unlisted private)) }
+    scope :not_direct_visibility, -> { where.not(visibility: :direct) }
+
+    validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
+
+    before_validation :set_visibility, unless: :visibility?
+  end
+
+  class_methods do
+    def selectable_visibilities
+      visibilities.keys - %w(direct limited)
+    end
+  end
+
+  def hidden?
+    !distributable?
+  end
+
+  def distributable?
+    public_visibility? || unlisted_visibility?
+  end
+
+  alias sign? distributable?
+
+  private
+
+  def set_visibility
+    self.visibility ||= reblog.visibility if reblog?
+    self.visibility ||= visibility_from_account
+  end
+
+  def visibility_from_account
+    account.locked? ? :private : :public
+  end
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index 5a81b00773..c012b1ddfa 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -38,6 +38,7 @@ class Status < ApplicationRecord
   include Status::SearchConcern
   include Status::SnapshotConcern
   include Status::ThreadingConcern
+  include Status::Visibility
 
   MEDIA_ATTACHMENTS_LIMIT = 4
 
@@ -52,8 +53,6 @@ class Status < ApplicationRecord
   update_index('statuses', :proper)
   update_index('public_statuses', :proper)
 
-  enum :visibility, { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4 }, suffix: :visibility, validate: true
-
   belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
 
   belongs_to :account, inverse_of: :statuses
@@ -98,7 +97,6 @@ class Status < ApplicationRecord
   validates_with StatusLengthValidator
   validates_with DisallowedHashtagsValidator
   validates :reblog, uniqueness: { scope: :account }, if: :reblog?
-  validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
 
   accepts_nested_attributes_for :poll
 
@@ -125,9 +123,6 @@ class Status < ApplicationRecord
   scope :tagged_with_none, lambda { |tag_ids|
     where('NOT EXISTS (SELECT * FROM statuses_tags forbidden WHERE forbidden.status_id = statuses.id AND forbidden.tag_id IN (?))', tag_ids)
   }
-  scope :distributable_visibility, -> { where(visibility: %i(public unlisted)) }
-  scope :list_eligible_visibility, -> { where(visibility: %i(public unlisted private)) }
-  scope :not_direct_visibility, -> { where.not(visibility: :direct) }
 
   after_create_commit :trigger_create_webhooks
   after_update_commit :trigger_update_webhooks
@@ -140,7 +135,6 @@ class Status < ApplicationRecord
 
   before_validation :prepare_contents, if: :local?
   before_validation :set_reblog
-  before_validation :set_visibility
   before_validation :set_conversation
   before_validation :set_local
 
@@ -242,16 +236,6 @@ class Status < ApplicationRecord
     PreviewCardsStatus.where(status_id: id).delete_all
   end
 
-  def hidden?
-    !distributable?
-  end
-
-  def distributable?
-    public_visibility? || unlisted_visibility?
-  end
-
-  alias sign? distributable?
-
   def with_media?
     ordered_media_attachments.any?
   end
@@ -351,10 +335,6 @@ class Status < ApplicationRecord
   end
 
   class << self
-    def selectable_visibilities
-      visibilities.keys - %w(direct limited)
-    end
-
     def favourites_map(status_ids, account_id)
       Favourite.select(:status_id).where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
     end
@@ -436,11 +416,6 @@ class Status < ApplicationRecord
     update_column(:poll_id, poll.id) if association(:poll).loaded? && poll.present?
   end
 
-  def set_visibility
-    self.visibility = reblog.visibility if reblog? && visibility.nil?
-    self.visibility = (account.locked? ? :private : :public) if visibility.nil?
-  end
-
   def set_conversation
     self.thread = thread.reblog if thread&.reblog?
 
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index 36b13df815..a197aaf1d2 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe Status do
   let(:bob)   { Fabricate(:account, username: 'bob') }
   let(:other) { Fabricate(:status, account: bob, text: 'Skulls for the skull god! The enemy\'s gates are sideways!') }
 
+  include_examples 'Status::Visibility'
+
   describe '#local?' do
     it 'returns true when no remote URI is set' do
       expect(subject.local?).to be true
@@ -84,36 +86,6 @@ RSpec.describe Status do
     end
   end
 
-  describe '#hidden?' do
-    context 'when private_visibility?' do
-      it 'returns true' do
-        subject.visibility = :private
-        expect(subject.hidden?).to be true
-      end
-    end
-
-    context 'when direct_visibility?' do
-      it 'returns true' do
-        subject.visibility = :direct
-        expect(subject.hidden?).to be true
-      end
-    end
-
-    context 'when public_visibility?' do
-      it 'returns false' do
-        subject.visibility = :public
-        expect(subject.hidden?).to be false
-      end
-    end
-
-    context 'when unlisted_visibility?' do
-      it 'returns false' do
-        subject.visibility = :unlisted
-        expect(subject.hidden?).to be false
-      end
-    end
-  end
-
   describe '#content' do
     it 'returns the text of the status if it is not a reblog' do
       expect(subject.content).to eql subject.text
diff --git a/spec/support/examples/models/concerns/status/visibility.rb b/spec/support/examples/models/concerns/status/visibility.rb
new file mode 100644
index 0000000000..dd9e0bddf0
--- /dev/null
+++ b/spec/support/examples/models/concerns/status/visibility.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.shared_examples 'Status::Visibility' do
+  describe 'Validations' do
+    context 'when status is a reblog' do
+      subject { Fabricate.build :status, reblog: Fabricate(:status) }
+
+      it { is_expected.to allow_values('public', 'unlisted', 'private').for(:visibility) }
+      it { is_expected.to_not allow_values('direct', 'limited').for(:visibility) }
+    end
+
+    context 'when status is not reblog' do
+      subject { Fabricate.build :status, reblog_of_id: nil }
+
+      it { is_expected.to allow_values('public', 'unlisted', 'private', 'direct', 'limited').for(:visibility) }
+    end
+  end
+
+  describe 'Scopes' do
+    let!(:direct_status) { Fabricate :status, visibility: :direct }
+    let!(:limited_status) { Fabricate :status, visibility: :limited }
+    let!(:private_status) { Fabricate :status, visibility: :private }
+    let!(:public_status) { Fabricate :status, visibility: :public }
+    let!(:unlisted_status) { Fabricate :status, visibility: :unlisted }
+
+    describe '.list_eligible_visibility' do
+      it 'returns appropriate records' do
+        expect(Status.list_eligible_visibility)
+          .to include(
+            private_status,
+            public_status,
+            unlisted_status
+          )
+          .and not_include(direct_status)
+          .and not_include(limited_status)
+      end
+    end
+
+    describe '.distributable_visibility' do
+      it 'returns appropriate records' do
+        expect(Status.distributable_visibility)
+          .to include(
+            public_status,
+            unlisted_status
+          )
+          .and not_include(private_status)
+          .and not_include(direct_status)
+          .and not_include(limited_status)
+      end
+    end
+
+    describe '.not_direct_visibility' do
+      it 'returns appropriate records' do
+        expect(Status.not_direct_visibility)
+          .to include(
+            limited_status,
+            private_status,
+            public_status,
+            unlisted_status
+          )
+          .and not_include(direct_status)
+      end
+    end
+  end
+
+  describe 'Callbacks' do
+    describe 'Setting visibility in before validation' do
+      subject { Fabricate.build :status, visibility: nil }
+
+      context 'when explicit value is set' do
+        before { subject.visibility = :public }
+
+        it 'does not change' do
+          expect { subject.valid? }
+            .to_not change(subject, :visibility)
+        end
+      end
+
+      context 'when status is a reblog' do
+        before { subject.reblog = Fabricate(:status, visibility: :public) }
+
+        it 'changes to match the reblog' do
+          expect { subject.valid? }
+            .to change(subject, :visibility).to('public')
+        end
+      end
+
+      context 'when account is locked' do
+        before { subject.account = Fabricate.build(:account, locked: true) }
+
+        it 'changes to private' do
+          expect { subject.valid? }
+            .to change(subject, :visibility).to('private')
+        end
+      end
+
+      context 'when account is not locked' do
+        before { subject.account = Fabricate.build(:account, locked: false) }
+
+        it 'changes to public' do
+          expect { subject.valid? }
+            .to change(subject, :visibility).to('public')
+        end
+      end
+    end
+  end
+
+  describe '.selectable_visibilities' do
+    it 'returns options available for default privacy selection' do
+      expect(Status.selectable_visibilities)
+        .to match(%w(public unlisted private))
+    end
+  end
+
+  describe '#hidden?' do
+    subject { Status.new }
+
+    context 'when visibility is private' do
+      before { subject.visibility = :private }
+
+      it { is_expected.to be_hidden }
+    end
+
+    context 'when visibility is direct' do
+      before { subject.visibility = :direct }
+
+      it { is_expected.to be_hidden }
+    end
+
+    context 'when visibility is limited' do
+      before { subject.visibility = :limited }
+
+      it { is_expected.to be_hidden }
+    end
+
+    context 'when visibility is public' do
+      before { subject.visibility = :public }
+
+      it { is_expected.to_not be_hidden }
+    end
+
+    context 'when visibility is unlisted' do
+      before { subject.visibility = :unlisted }
+
+      it { is_expected.to_not be_hidden }
+    end
+  end
+
+  describe '#distributable?' do
+    subject { Status.new }
+
+    context 'when visibility is public' do
+      before { subject.visibility = :public }
+
+      it { is_expected.to be_distributable }
+    end
+
+    context 'when visibility is unlisted' do
+      before { subject.visibility = :unlisted }
+
+      it { is_expected.to be_distributable }
+    end
+
+    context 'when visibility is private' do
+      before { subject.visibility = :private }
+
+      it { is_expected.to_not be_distributable }
+    end
+
+    context 'when visibility is direct' do
+      before { subject.visibility = :direct }
+
+      it { is_expected.to_not be_distributable }
+    end
+
+    context 'when visibility is limited' do
+      before { subject.visibility = :limited }
+
+      it { is_expected.to_not be_distributable }
+    end
+  end
+end

From bfe73e153d425312211eb1114ff20275aa369059 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 14 Jan 2025 17:34:26 +0100
Subject: [PATCH 25/26] Update dependency postcss to v8.5.1 (#33586)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 yarn.lock | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index f112e77920..a99c1e23c9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12332,12 +12332,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"nanoid@npm:^3.3.7":
-  version: 3.3.7
-  resolution: "nanoid@npm:3.3.7"
+"nanoid@npm:^3.3.8":
+  version: 3.3.8
+  resolution: "nanoid@npm:3.3.8"
   bin:
     nanoid: bin/nanoid.cjs
-  checksum: 10c0/e3fb661aa083454f40500473bb69eedb85dc160e763150b9a2c567c7e9ff560ce028a9f833123b618a6ea742e311138b591910e795614a629029e86e180660f3
+  checksum: 10c0/4b1bb29f6cfebf3be3bc4ad1f1296fb0a10a3043a79f34fbffe75d1621b4318319211cd420549459018ea3592f0d2f159247a6f874911d6d26eaaadda2478120
   languageName: node
   linkType: hard
 
@@ -14209,13 +14209,13 @@ __metadata:
   linkType: hard
 
 "postcss@npm:^8.2.15, postcss@npm:^8.4.24, postcss@npm:^8.4.49":
-  version: 8.4.49
-  resolution: "postcss@npm:8.4.49"
+  version: 8.5.1
+  resolution: "postcss@npm:8.5.1"
   dependencies:
-    nanoid: "npm:^3.3.7"
+    nanoid: "npm:^3.3.8"
     picocolors: "npm:^1.1.1"
     source-map-js: "npm:^1.2.1"
-  checksum: 10c0/f1b3f17aaf36d136f59ec373459f18129908235e65dbdc3aee5eef8eba0756106f52de5ec4682e29a2eab53eb25170e7e871b3e4b52a8f1de3d344a514306be3
+  checksum: 10c0/c4d90c59c98e8a0c102b77d3f4cac190f883b42d63dc60e2f3ed840f16197c0c8e25a4327d2e9a847b45a985612317dc0534178feeebd0a1cf3eb0eecf75cae4
   languageName: node
   linkType: hard
 

From e1d7efadc04dd0826c6bcfe43325688566e13881 Mon Sep 17 00:00:00 2001
From: Michael Stanclift <mx@vmstan.com>
Date: Tue, 14 Jan 2025 10:35:58 -0600
Subject: [PATCH 26/26] Fix libyaml missing from Dockerfile build stage
 (#33591)

---
 Dockerfile | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Dockerfile b/Dockerfile
index 97c7a91499..deeac8b466 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -153,6 +153,7 @@ RUN \
   libpq-dev \
   libssl-dev \
   libtool \
+  libyaml-dev \
   meson \
   nasm \
   pkg-config \