From 3f81e7dcc8afbb86e48c36773fe2bc7018b44379 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 24 Jun 2024 12:16:24 +0200
Subject: [PATCH 01/84] chore(deps): update dependency @types/http-link-header
 to v1.0.6 (#30814)

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 ff4e1110d..b6117d821 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3622,11 +3622,11 @@ __metadata:
   linkType: hard
 
 "@types/http-link-header@npm:^1.0.3":
-  version: 1.0.5
-  resolution: "@types/http-link-header@npm:1.0.5"
+  version: 1.0.6
+  resolution: "@types/http-link-header@npm:1.0.6"
   dependencies:
     "@types/node": "npm:*"
-  checksum: 10c0/adeb13381b38c3625478149820772924c154b4a7250dca62c346810a8378f8968fc7f3a9a4f55ec61de5d06083637540f862c8a920f6a710310c9645d19a077d
+  checksum: 10c0/63f3f7ab5ff6312280727ba8cf836abf5d1b76f9dc5eefc8cd4389db29d57a72fb0e028db99735ada5ccfd3c2cc6607e096b5cc142fc53c2bb5688b6295f61af
   languageName: node
   linkType: hard
 

From 6d14cfbf298eec6e2ffcd4ad69a118cdd168b3a1 Mon Sep 17 00:00:00 2001
From: Nick Schonning <nschonni@gmail.com>
Date: Mon, 24 Jun 2024 06:18:36 -0400
Subject: [PATCH 02/84] Unset Rails/UnusedIgnoredColumns (#30800)

---
 .rubocop/rails.yml | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/.rubocop/rails.yml b/.rubocop/rails.yml
index 4e08f1ab9..68c90143a 100644
--- a/.rubocop/rails.yml
+++ b/.rubocop/rails.yml
@@ -18,6 +18,3 @@ Rails/RakeEnvironment:
 
 Rails/SkipsModelValidations:
   Enabled: false
-
-Rails/UnusedIgnoredColumns:
-  Enabled: false # Preserve ability to migrate from arbitrary old versions

From b3710098a8edcb5cc317a280758b2a772ea722ef Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 24 Jun 2024 12:18:51 +0200
Subject: [PATCH 03/84] chore(deps): update dependency
 opentelemetry-instrumentation-faraday to v0.24.5 (#30797)

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 0fe1c03b2..abd31f49a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -532,7 +532,7 @@ GEM
     opentelemetry-instrumentation-excon (0.22.3)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.22.1)
-    opentelemetry-instrumentation-faraday (0.24.4)
+    opentelemetry-instrumentation-faraday (0.24.5)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.22.1)
     opentelemetry-instrumentation-http (0.23.3)

From 54cc204473302eda7e6e7e75b343a14859524ab1 Mon Sep 17 00:00:00 2001
From: Essem <smswessem@gmail.com>
Date: Mon, 24 Jun 2024 05:29:00 -0500
Subject: [PATCH 04/84] Use WebSocketServer instead of WebSocket.Server in
 streaming (#30788)

---
 streaming/index.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/streaming/index.js b/streaming/index.js
index 154ecbc02..65a63bb11 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -12,7 +12,7 @@ import { Redis } from 'ioredis';
 import { JSDOM } from 'jsdom';
 import pg from 'pg';
 import pgConnectionString from 'pg-connection-string';
-import WebSocket from 'ws';
+import { WebSocketServer } from 'ws';
 
 import { AuthenticationError, RequestError, extractStatusAndMessage as extractErrorStatusAndMessage } from './errors.js';
 import { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } from './logging.js';
@@ -289,7 +289,7 @@ const CHANNEL_NAMES = [
 const startServer = async () => {
   const pgPool = new pg.Pool(pgConfigFromEnv(process.env));
   const server = http.createServer();
-  const wss = new WebSocket.Server({ noServer: true });
+  const wss = new WebSocketServer({ noServer: true });
 
   // Set the X-Request-Id header on WebSockets:
   wss.on("headers", function onHeaders(headers, req) {

From 1af6313ced5b39041cb8c480b1f0c65155429238 Mon Sep 17 00:00:00 2001
From: Essem <smswessem@gmail.com>
Date: Mon, 24 Jun 2024 05:36:26 -0500
Subject: [PATCH 05/84] Fix CMD syntax in streaming Dockerfile (#30795)

---
 streaming/Dockerfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/streaming/Dockerfile b/streaming/Dockerfile
index 319d5b7fd..d9f7615fa 100644
--- a/streaming/Dockerfile
+++ b/streaming/Dockerfile
@@ -110,4 +110,4 @@ USER mastodon
 # Expose default Streaming ports
 EXPOSE 4000
 # Run streaming when started
-CMD [ node ./streaming/index.js ]
+CMD [ "node", "./streaming/index.js" ]

From 61722b1b1fb36ff5c3d6c470981f5fc6648e2d14 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 24 Jun 2024 12:46:53 +0200
Subject: [PATCH 06/84] New Crowdin Translations (automated) (#30808)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/fil.json   | 7 +++++++
 app/javascript/mastodon/locales/pt-BR.json | 4 ++++
 app/javascript/mastodon/locales/zh-TW.json | 2 +-
 config/locales/doorkeeper.pt-BR.yml        | 2 ++
 config/locales/pt-BR.yml                   | 1 +
 config/locales/ru.yml                      | 4 ++++
 6 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/locales/fil.json b/app/javascript/mastodon/locales/fil.json
index 9e459f767..b8a2987ef 100644
--- a/app/javascript/mastodon/locales/fil.json
+++ b/app/javascript/mastodon/locales/fil.json
@@ -118,6 +118,7 @@
   "confirmations.delete_list.confirm": "Tanggalin",
   "confirmations.delete_list.message": "Sigurado ka bang gusto mong burahin ang listahang ito?",
   "confirmations.discard_edit_media.confirm": "Ipagpaliban",
+  "confirmations.domain_block.confirm": "Harangan ang serbiro",
   "confirmations.edit.confirm": "Baguhin",
   "confirmations.reply.confirm": "Tumugon",
   "conversation.mark_as_read": "Markahan bilang nabasa na",
@@ -186,6 +187,7 @@
   "follow_request.authorize": "Tanggapin",
   "follow_request.reject": "Tanggihan",
   "follow_suggestions.dismiss": "Huwag nang ipakita muli",
+  "follow_suggestions.popular_suggestion_longer": "Sikat sa {domain}",
   "follow_suggestions.view_all": "Tingnan lahat",
   "follow_suggestions.who_to_follow": "Sinong maaaring sundan",
   "footer.about": "Tungkol dito",
@@ -220,6 +222,7 @@
   "link_preview.author": "Ni/ng {name}",
   "lists.account.add": "Idagdag sa talaan",
   "lists.account.remove": "Tanggalin mula sa talaan",
+  "lists.delete": "Burahin ang talaan",
   "lists.new.create": "Idagdag sa talaan",
   "lists.new.title_placeholder": "Bagong pangalan ng talaan",
   "lists.replies_policy.title": "Ipakita ang mga tugon sa:",
@@ -287,9 +290,13 @@
   "reply_indicator.cancel": "Ipagpaliban",
   "report.block": "Harangan",
   "report.categories.other": "Iba pa",
+  "report.categories.violation": "Lumalabag ang nilalaman sa isa o higit pang mga patakaran ng serbiro",
+  "report.category.subtitle": "Piliin ang pinakamahusay na tugma",
   "report.category.title": "Sabihin mo sa amin kung anong nangyari sa {type} na ito",
   "report.close": "Tapos na",
   "report.next": "Sunod",
+  "report.placeholder": "Mga Karagdagang Puna",
+  "report.reasons.dislike": "Hindi ko gusto ito",
   "report.reasons.violation": "Lumalabag ito sa mga panuntunan ng serbiro",
   "report.reasons.violation_description": "Alam mo na lumalabag ito sa mga partikular na panuntunan",
   "report.rules.title": "Aling mga patakaran ang nilabag?",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index afe549054..4d3bd2d28 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -415,6 +415,7 @@
   "limited_account_hint.title": "Este perfil foi ocultado pelos moderadores do {domain}.",
   "link_preview.author": "Por {name}",
   "link_preview.more_from_author": "Mais de {name}",
+  "link_preview.shares": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
   "lists.account.add": "Adicionar à lista",
   "lists.account.remove": "Remover da lista",
   "lists.delete": "Excluir lista",
@@ -695,8 +696,11 @@
   "server_banner.about_active_users": "Pessoas usando este servidor durante os últimos 30 dias (Usuários ativos mensalmente)",
   "server_banner.active_users": "usuários ativos",
   "server_banner.administered_by": "Administrado por:",
+  "server_banner.is_one_of_many": "{domain} é um dos muitos servidores Mastodon independentes que você pode usar para participar do fediverso.",
   "server_banner.server_stats": "Estatísticas do servidor:",
   "sign_in_banner.create_account": "Criar conta",
+  "sign_in_banner.follow_anyone": "Siga alguém pelo fediverso e veja tudo em ordem cronológica. Sem algoritmos, anúncios ou clickbait à vista.",
+  "sign_in_banner.mastodon_is": "O Mastodon é a melhor maneira de acompanhar o que está acontecendo.",
   "sign_in_banner.sign_in": "Entrar",
   "sign_in_banner.sso_redirect": "Entrar ou Registrar-se",
   "status.admin_account": "Abrir interface de moderação para @{name}",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index e6cd62162..4ab22daba 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -62,7 +62,7 @@
   "account.requested": "正在等候審核。按一下以取消跟隨請求",
   "account.requested_follow": "{name} 要求跟隨您",
   "account.share": "分享 @{name} 的個人檔案",
-  "account.show_reblogs": "顯示來自 @{name} 的嘟文",
+  "account.show_reblogs": "顯示來自 @{name} 的轉嘟",
   "account.statuses_counter": "{count, plural,one {{counter} 則}other {{counter} 則}}嘟文",
   "account.unblock": "解除封鎖 @{name}",
   "account.unblock_domain": "解除封鎖網域 {domain}",
diff --git a/config/locales/doorkeeper.pt-BR.yml b/config/locales/doorkeeper.pt-BR.yml
index d7e9353b5..6b076e908 100644
--- a/config/locales/doorkeeper.pt-BR.yml
+++ b/config/locales/doorkeeper.pt-BR.yml
@@ -135,6 +135,7 @@ pt-BR:
         media: Mídias anexadas
         mutes: Silenciados
         notifications: Notificações
+        profile: Seu perfil do Mastodon
         push: Notificações push
         reports: Denúncias
         search: Buscar
@@ -165,6 +166,7 @@ pt-BR:
       admin:write:reports: executar ações de moderação em denúncias
       crypto: usar criptografia de ponta-a-ponta
       follow: alterar o relacionamento das contas
+      profile: ler somente as informações do perfil da sua conta
       push: receber notificações push
       read: ler todos os dados da sua conta
       read:accounts: ver informações das contas
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 8d3b53f77..584a9253b 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -293,6 +293,7 @@ pt-BR:
       filter_by_action: Filtrar por ação
       filter_by_user: Filtrar por usuário
       title: Auditar histórico
+      unavailable_instance: "(nome de domínio indisponível)"
     announcements:
       destroyed_msg: Anúncio excluído!
       edit:
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 6dff92bb6..5f5b3676f 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -1699,6 +1699,7 @@ ru:
     import: Импорт
     import_and_export: Импорт и экспорт
     migrate: Миграция учётной записи
+    notifications: Уведомления по электронной почте
     preferences: Настройки
     profile: Профиль
     relationships: Подписки и подписчики
@@ -1706,6 +1707,9 @@ ru:
     strikes: Замечания модерации
     two_factor_authentication: Подтверждение входа
     webauthn_authentication: Ключи безопасности
+  severed_relationships:
+    event_type:
+      user_domain_block: Вы заблокировали %{target_name}
   statuses:
     attached:
       audio:

From 8827cd597e695c0368dfdce582755eda7f667272 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 24 Jun 2024 15:11:10 +0200
Subject: [PATCH 07/84] Fix `/admin/accounts/:account_id/statuses/:id` for
 edited posts with media attachments (#30819)

---
 app/models/status_edit.rb                          | 2 +-
 spec/controllers/admin/statuses_controller_spec.rb | 5 +++++
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/app/models/status_edit.rb b/app/models/status_edit.rb
index 50dabb91f..089c42fb9 100644
--- a/app/models/status_edit.rb
+++ b/app/models/status_edit.rb
@@ -42,7 +42,7 @@ class StatusEdit < ApplicationRecord
   scope :ordered, -> { order(id: :asc) }
 
   delegate :local?, :application, :edited?, :edited_at,
-           :discarded?, :visibility, to: :status
+           :discarded?, :visibility, :language, to: :status
 
   def emojis
     return @emojis if defined?(@emojis)
diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb
index 4e8bf9ead..4144d97d6 100644
--- a/spec/controllers/admin/statuses_controller_spec.rb
+++ b/spec/controllers/admin/statuses_controller_spec.rb
@@ -44,6 +44,11 @@ describe Admin::StatusesController do
 
   describe 'GET #show' do
     before do
+      status.media_attachments << Fabricate(:media_attachment, type: :image, account: status.account)
+      status.save!
+      status.snapshot!(at_time: status.created_at, rate_limit: false)
+      status.update!(text: 'Hello, this is an edited post')
+      status.snapshot!(rate_limit: false)
       get :show, params: { account_id: account.id, id: status.id }
     end
 

From f6e466058a5a70eba5001c7ad3a80d86c07eb25d Mon Sep 17 00:00:00 2001
From: Tim Rogers <rogers.timothy.john@gmail.com>
Date: Mon, 24 Jun 2024 09:41:04 -0500
Subject: [PATCH 08/84] Added check for STATSD_ADDR setting to emit a warning
 and proceed rather than crashing if the address is unreachable (#30691)

---
 config/initializers/statsd.rb | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/config/initializers/statsd.rb b/config/initializers/statsd.rb
index a655c1071..f1628a9d1 100644
--- a/config/initializers/statsd.rb
+++ b/config/initializers/statsd.rb
@@ -3,13 +3,17 @@
 if ENV['STATSD_ADDR'].present?
   host, port = ENV['STATSD_ADDR'].split(':')
 
-  statsd = Statsd.new(host, port)
-  statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') }
+  begin
+    statsd = Statsd.new(host, port)
+    statsd.namespace = ENV.fetch('STATSD_NAMESPACE') { ['Mastodon', Rails.env].join('.') }
 
-  NSA.inform_statsd(statsd) do |informant|
-    informant.collect(:action_controller, :web)
-    informant.collect(:active_record, :db)
-    informant.collect(:active_support_cache, :cache)
-    informant.collect(:sidekiq, :sidekiq) if ENV['STATSD_SIDEKIQ'] == 'true'
+    NSA.inform_statsd(statsd) do |informant|
+      informant.collect(:action_controller, :web)
+      informant.collect(:active_record, :db)
+      informant.collect(:active_support_cache, :cache)
+      informant.collect(:sidekiq, :sidekiq) if ENV['STATSD_SIDEKIQ'] == 'true'
+    end
+  rescue
+    Rails.logger.warn("statsd address #{ENV['STATSD_ADDR']} not reachable, proceeding without statsd")
   end
 end

From 39d80e84be660e6d3eb121ee8f1067bd87cc3783 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 24 Jun 2024 10:47:14 -0400
Subject: [PATCH 09/84] Remove `lockfileMaintenance` setting (#30799)

---
 .github/renovate.json5 | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 03787dfac..2cf7bec8e 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -14,9 +14,6 @@
   // to `null` after any other rule set it to something.
   dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).',
   postUpdateOptions: ['yarnDedupeHighest'],
-  lockFileMaintenance: {
-    enabled: true,
-  },
   packageRules: [
     {
       // Require Dependency Dashboard Approval for major version bumps of these node packages

From 6527d5039141fe4a80645147b581d76952a64f39 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 24 Jun 2024 10:50:37 -0400
Subject: [PATCH 10/84] Disable `Rails/BulkChangeTable` cop (#30820)

---
 .rubocop/rails.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.rubocop/rails.yml b/.rubocop/rails.yml
index 68c90143a..ae31c1f26 100644
--- a/.rubocop/rails.yml
+++ b/.rubocop/rails.yml
@@ -1,4 +1,7 @@
 ---
+Rails/BulkChangeTable:
+  Enabled: false # Conflicts with strong_migrations features
+
 Rails/FilePath:
   EnforcedStyle: arguments
 

From 052c90b8de2164b8003c29f445f165c7e802fd25 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 25 Jun 2024 09:45:58 +0200
Subject: [PATCH 11/84] New Crowdin Translations (automated) (#30825)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/lt.json | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index b365d6458..bb69b7339 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -1,18 +1,18 @@
 {
   "about.blocks": "Prižiūrimi serveriai",
   "about.contact": "Kontaktai:",
-  "about.disclaimer": "Mastodon – tai nemokama atvirojo kodo programinė įranga ir Mastodon gGmbH prekės ženklas.",
+  "about.disclaimer": "„Mastodon“ – tai nemokama atvirojo kodo programinė įranga ir „Mastodon“ gGmbH prekės ženklas.",
   "about.domain_blocks.no_reason_available": "Priežastis nepateikta",
-  "about.domain_blocks.preamble": "Mastodon paprastai leidžia peržiūrėti turinį ir bendrauti su naudotojais iš bet kurio kito fediverse esančio serverio. Šios yra išimtys, kurios buvo padarytos šiame konkrečiame serveryje.",
+  "about.domain_blocks.preamble": "„Mastodon“ paprastai leidžia peržiūrėti turinį ir bendrauti su naudotojais iš bet kurio kito fediverse esančio serverio. Šios yra išimtys, kurios buvo padarytos šiame konkrečiame serveryje.",
   "about.domain_blocks.silenced.explanation": "Paprastai nematysi profilių ir turinio iš šio serverio, nebent jį aiškiai ieškosi arba pasirinksi jį sekdamas (-a).",
   "about.domain_blocks.silenced.title": "Ribota",
   "about.domain_blocks.suspended.explanation": "Jokie duomenys iš šio serverio nebus apdorojami, saugomi ar keičiami, todėl bet kokia sąveika ar bendravimas su šio serverio naudotojais bus neįmanomas.",
-  "about.domain_blocks.suspended.title": "Uždrausta",
+  "about.domain_blocks.suspended.title": "Pristabdyta",
   "about.not_available": "Ši informacija nebuvo pateikta šiame serveryje.",
-  "about.powered_by": "Decentralizuota socialinė medija, kurią valdo {mastodon}",
+  "about.powered_by": "Decentralizuota socialinė medija, veikianti pagal „{mastodon}“",
   "about.rules": "Serverio taisyklės",
   "account.account_note_header": "Pastaba",
-  "account.add_or_remove_from_list": "Pridėti arba ištrinti iš sąrašų",
+  "account.add_or_remove_from_list": "Pridėti arba pašalinti iš sąrašų",
   "account.badges.bot": "Automatizuotas",
   "account.badges.group": "Grupė",
   "account.block": "Blokuoti @{name}",

From 30ae5952d228b31af58534d76a8d78bf27a171f9 Mon Sep 17 00:00:00 2001
From: Emelia Smith <ThisIsMissEm@users.noreply.github.com>
Date: Tue, 25 Jun 2024 09:46:53 +0200
Subject: [PATCH 12/84] Fix: Ensure "With Media" is highlighted from Admin
 Accounts page (#30812)

---
 app/views/admin/statuses/index.html.haml           | 2 +-
 spec/controllers/admin/statuses_controller_spec.rb | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml
index 770d972d9..b03b8ac51 100644
--- a/app/views/admin/statuses/index.html.haml
+++ b/app/views/admin/statuses/index.html.haml
@@ -8,7 +8,7 @@
     %strong= t('admin.statuses.media.title')
     %ul
       %li= filter_link_to t('generic.all'), media: nil, id: nil
-      %li= filter_link_to t('admin.statuses.with_media'), media: '1'
+      %li= filter_link_to t('admin.statuses.with_media'), media: true
   .back-link
     - if params[:report_id]
       = link_to admin_report_path(params[:report_id].to_i) do
diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb
index 4144d97d6..4ab6d109e 100644
--- a/spec/controllers/admin/statuses_controller_spec.rb
+++ b/spec/controllers/admin/statuses_controller_spec.rb
@@ -33,7 +33,7 @@ describe Admin::StatusesController do
 
     context 'when filtering by media' do
       before do
-        get :index, params: { account_id: account.id, media: '1' }
+        get :index, params: { account_id: account.id, media: true }
       end
 
       it 'returns http success' do

From 309274839dd446d334ae2ea5c6e033debe56e4ed Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 25 Jun 2024 09:56:38 +0200
Subject: [PATCH 13/84] chore(deps): update dependency aws-sdk-s3 to v1.153.0
 (#30824)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index abd31f49a..5d735eb75 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -100,17 +100,17 @@ GEM
     attr_required (1.0.2)
     awrence (1.2.1)
     aws-eventstream (1.3.0)
-    aws-partitions (1.940.0)
-    aws-sdk-core (3.197.0)
+    aws-partitions (1.947.0)
+    aws-sdk-core (3.198.0)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.8)
       jmespath (~> 1, >= 1.6.1)
-    aws-sdk-kms (1.83.0)
-      aws-sdk-core (~> 3, >= 3.197.0)
+    aws-sdk-kms (1.86.0)
+      aws-sdk-core (~> 3, >= 3.198.0)
       aws-sigv4 (~> 1.1)
-    aws-sdk-s3 (1.152.3)
-      aws-sdk-core (~> 3, >= 3.197.0)
+    aws-sdk-s3 (1.153.0)
+      aws-sdk-core (~> 3, >= 3.198.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.8)
     aws-sigv4 (1.8.0)

From 547e97945df0abc68dc473ed60d2faeb2feb2b06 Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Tue, 25 Jun 2024 15:45:41 +0200
Subject: [PATCH 14/84] Change `apiRequest` to accept both `params` and `data`
 (#30818)

---
 app/javascript/mastodon/api.ts                | 37 ++++++++++++++++++-
 app/javascript/mastodon/api/accounts.ts       |  8 ++--
 app/javascript/mastodon/api/interactions.ts   | 10 +++--
 .../mastodon/api/notification_policies.ts     |  8 ++--
 4 files changed, 51 insertions(+), 12 deletions(-)

diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts
index e133125a2..24672290c 100644
--- a/app/javascript/mastodon/api.ts
+++ b/app/javascript/mastodon/api.ts
@@ -59,16 +59,49 @@ export default function api(withAuthorization = true) {
   });
 }
 
+type RequestParamsOrData = Record<string, unknown>;
+
 export async function apiRequest<ApiResponse = unknown>(
   method: Method,
   url: string,
-  params?: Record<string, unknown>,
+  args: {
+    params?: RequestParamsOrData;
+    data?: RequestParamsOrData;
+  } = {},
 ) {
   const { data } = await api().request<ApiResponse>({
     method,
     url: '/api/' + url,
-    data: params,
+    ...args,
   });
 
   return data;
 }
+
+export async function apiRequestGet<ApiResponse = unknown>(
+  url: string,
+  params?: RequestParamsOrData,
+) {
+  return apiRequest<ApiResponse>('GET', url, { params });
+}
+
+export async function apiRequestPost<ApiResponse = unknown>(
+  url: string,
+  data?: RequestParamsOrData,
+) {
+  return apiRequest<ApiResponse>('POST', url, { data });
+}
+
+export async function apiRequestPut<ApiResponse = unknown>(
+  url: string,
+  data?: RequestParamsOrData,
+) {
+  return apiRequest<ApiResponse>('PUT', url, { data });
+}
+
+export async function apiRequestDelete<ApiResponse = unknown>(
+  url: string,
+  params?: RequestParamsOrData,
+) {
+  return apiRequest<ApiResponse>('DELETE', url, { params });
+}
diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts
index 3d89e44b2..e58608785 100644
--- a/app/javascript/mastodon/api/accounts.ts
+++ b/app/javascript/mastodon/api/accounts.ts
@@ -1,7 +1,9 @@
-import { apiRequest } from 'mastodon/api';
+import { apiRequestPost } from 'mastodon/api';
 import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
 
 export const apiSubmitAccountNote = (id: string, value: string) =>
-  apiRequest<ApiRelationshipJSON>('post', `v1/accounts/${id}/note`, {
-    comment: value,
+  apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
+    data: {
+      comment: value,
+    },
   });
diff --git a/app/javascript/mastodon/api/interactions.ts b/app/javascript/mastodon/api/interactions.ts
index 4c466a1b4..0bdaffbda 100644
--- a/app/javascript/mastodon/api/interactions.ts
+++ b/app/javascript/mastodon/api/interactions.ts
@@ -1,10 +1,12 @@
-import { apiRequest } from 'mastodon/api';
+import { apiRequestPost } from 'mastodon/api';
 import type { Status, StatusVisibility } from 'mastodon/models/status';
 
 export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
-  apiRequest<{ reblog: Status }>('post', `v1/statuses/${statusId}/reblog`, {
-    visibility,
+  apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, {
+    data: {
+      visibility,
+    },
   });
 
 export const apiUnreblog = (statusId: string) =>
-  apiRequest<Status>('post', `v1/statuses/${statusId}/unreblog`);
+  apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);
diff --git a/app/javascript/mastodon/api/notification_policies.ts b/app/javascript/mastodon/api/notification_policies.ts
index b2a1e5ac3..5c1ef9c1d 100644
--- a/app/javascript/mastodon/api/notification_policies.ts
+++ b/app/javascript/mastodon/api/notification_policies.ts
@@ -1,10 +1,12 @@
-import { apiRequest } from 'mastodon/api';
+import { apiRequestGet, apiRequestPut } from 'mastodon/api';
 import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies';
 
 export const apiGetNotificationPolicy = () =>
-  apiRequest<NotificationPolicyJSON>('GET', '/v1/notifications/policy');
+  apiRequestGet<NotificationPolicyJSON>('/v1/notifications/policy');
 
 export const apiUpdateNotificationsPolicy = (
   policy: Partial<NotificationPolicyJSON>,
 ) =>
-  apiRequest<NotificationPolicyJSON>('PUT', '/v1/notifications/policy', policy);
+  apiRequestPut<NotificationPolicyJSON>('/v1/notifications/policy', {
+    data: policy,
+  });

From 8ef59729a10fd77121507dcf9ef5138ff9037d39 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 25 Jun 2024 09:57:40 -0400
Subject: [PATCH 15/84] Ignore intermittent chrome/manifest/icon interaction
 failure (#30793)

---
 spec/support/javascript_errors.rb | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/spec/support/javascript_errors.rb b/spec/support/javascript_errors.rb
index 28a43b3b8..764528536 100644
--- a/spec/support/javascript_errors.rb
+++ b/spec/support/javascript_errors.rb
@@ -2,7 +2,14 @@
 
 RSpec.configure do |config|
   config.after(:each, :js, type: :system) do
-    errors = page.driver.browser.logs.get(:browser)
+    # Classes of intermittent ignorable errors
+    ignored_errors = [
+      /Error while trying to use the following icon from the Manifest/, # https://github.com/mastodon/mastodon/pull/30793
+    ]
+    errors = page.driver.browser.logs.get(:browser).reject do |error|
+      ignored_errors.any? { |pattern| pattern.match(error.message) }
+    end
+
     if errors.present?
       aggregate_failures 'javascript errrors' do
         errors.each do |error|

From 845fe1c6936a7b386fd74ae567c19600a88e795a Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Tue, 25 Jun 2024 16:05:24 +0200
Subject: [PATCH 16/84] Add the Interlingua locale (#30828)

---
 config/initializers/i18n.rb | 1 +
 1 file changed, 1 insertion(+)

diff --git a/config/initializers/i18n.rb b/config/initializers/i18n.rb
index 8643060fa..5e8d3a545 100644
--- a/config/initializers/i18n.rb
+++ b/config/initializers/i18n.rb
@@ -41,6 +41,7 @@ Rails.application.configure do
     :hr,
     :hu,
     :hy,
+    :ia,
     :id,
     :ie,
     :ig,

From 2c7eed1fa1e7af72dd03a041a60f2cfd42e913e0 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Tue, 25 Jun 2024 18:53:03 +0200
Subject: [PATCH 17/84] Fix API requests after #30818 (#30837)

---
 app/javascript/mastodon/api/accounts.ts              | 4 +---
 app/javascript/mastodon/api/interactions.ts          | 4 +---
 app/javascript/mastodon/api/notification_policies.ts | 5 +----
 3 files changed, 3 insertions(+), 10 deletions(-)

diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts
index e58608785..bd1757e82 100644
--- a/app/javascript/mastodon/api/accounts.ts
+++ b/app/javascript/mastodon/api/accounts.ts
@@ -3,7 +3,5 @@ import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
 
 export const apiSubmitAccountNote = (id: string, value: string) =>
   apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
-    data: {
-      comment: value,
-    },
+    comment: value,
   });
diff --git a/app/javascript/mastodon/api/interactions.ts b/app/javascript/mastodon/api/interactions.ts
index 0bdaffbda..118b5f06d 100644
--- a/app/javascript/mastodon/api/interactions.ts
+++ b/app/javascript/mastodon/api/interactions.ts
@@ -3,9 +3,7 @@ import type { Status, StatusVisibility } from 'mastodon/models/status';
 
 export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
   apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, {
-    data: {
-      visibility,
-    },
+    visibility,
   });
 
 export const apiUnreblog = (statusId: string) =>
diff --git a/app/javascript/mastodon/api/notification_policies.ts b/app/javascript/mastodon/api/notification_policies.ts
index 5c1ef9c1d..4032134fb 100644
--- a/app/javascript/mastodon/api/notification_policies.ts
+++ b/app/javascript/mastodon/api/notification_policies.ts
@@ -6,7 +6,4 @@ export const apiGetNotificationPolicy = () =>
 
 export const apiUpdateNotificationsPolicy = (
   policy: Partial<NotificationPolicyJSON>,
-) =>
-  apiRequestPut<NotificationPolicyJSON>('/v1/notifications/policy', {
-    data: policy,
-  });
+) => apiRequestPut<NotificationPolicyJSON>('/v1/notifications/policy', policy);

From 07d222665b9974a97c78d2a500a55555ebedd640 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 25 Jun 2024 17:25:11 +0000
Subject: [PATCH 18/84] chore(deps): update dependency typescript to v5.5.2
 (#30815)

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

diff --git a/yarn.lock b/yarn.lock
index b6117d821..dc73fb93b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17151,22 +17151,22 @@ __metadata:
   linkType: hard
 
 "typescript@npm:5, typescript@npm:^5.0.4":
-  version: 5.4.5
-  resolution: "typescript@npm:5.4.5"
+  version: 5.5.2
+  resolution: "typescript@npm:5.5.2"
   bin:
     tsc: bin/tsc
     tsserver: bin/tsserver
-  checksum: 10c0/2954022ada340fd3d6a9e2b8e534f65d57c92d5f3989a263754a78aba549f7e6529acc1921913560a4b816c46dce7df4a4d29f9f11a3dc0d4213bb76d043251e
+  checksum: 10c0/8ca39b27b5f9bd7f32db795045933ab5247897660627251e8254180b792a395bf061ea7231947d5d7ffa5cb4cc771970fd4ef543275f9b559f08c9325cccfce3
   languageName: node
   linkType: hard
 
 "typescript@patch:typescript@npm%3A5#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.0.4#optional!builtin<compat/typescript>":
-  version: 5.4.5
-  resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin<compat/typescript>::version=5.4.5&hash=5adc0c"
+  version: 5.5.2
+  resolution: "typescript@patch:typescript@npm%3A5.5.2#optional!builtin<compat/typescript>::version=5.5.2&hash=379a07"
   bin:
     tsc: bin/tsc
     tsserver: bin/tsserver
-  checksum: 10c0/db2ad2a16ca829f50427eeb1da155e7a45e598eec7b086d8b4e8ba44e5a235f758e606d681c66992230d3fc3b8995865e5fd0b22a2c95486d0b3200f83072ec9
+  checksum: 10c0/a7b7ede75dc7fc32a76d0d0af6b91f5fbd8620890d84c906f663d8783bf3de6d7bd50f0430b8bb55eac88a38934af847ff709e7156e5138b95ae94cbd5f73e5b
   languageName: node
   linkType: hard
 

From a40831b3636fbca1e6d6b096e3d3cdd66569baf9 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Tue, 25 Jun 2024 22:37:48 +0200
Subject: [PATCH 19/84] Fix account search results (#30803)

---
 app/services/account_search_service.rb | 52 ++++++++++----------------
 1 file changed, 20 insertions(+), 32 deletions(-)

diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index b86c9b9e7..dab5f748b 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -28,9 +28,7 @@ class AccountSearchService < BaseService
               },
 
               functions: [
-                reputation_score_function,
                 followers_score_function,
-                time_distance_function,
               ],
             },
           },
@@ -81,36 +79,12 @@ class AccountSearchService < BaseService
       }
     end
 
-    # This function deranks accounts that follow more people than follow them
-    def reputation_score_function
-      {
-        script_score: {
-          script: {
-            source: "(Math.max(doc['followers_count'].value, 0) + 0.0) / (Math.max(doc['followers_count'].value, 0) + Math.max(doc['following_count'].value, 0) + 1)",
-          },
-        },
-      }
-    end
-
     # This function promotes accounts that have more followers
     def followers_score_function
       {
         script_score: {
           script: {
-            source: "(Math.max(doc['followers_count'].value, 0) / (Math.max(doc['followers_count'].value, 0) + 1))",
-          },
-        },
-      }
-    end
-
-    # This function deranks accounts that haven't posted in a long time
-    def time_distance_function
-      {
-        gauss: {
-          last_status_at: {
-            scale: '30d',
-            offset: '30d',
-            decay: 0.3,
+            source: "Math.log10((Math.max(doc['followers_count'].value, 0) + 1))",
           },
         },
       }
@@ -126,10 +100,24 @@ class AccountSearchService < BaseService
 
     def core_query
       {
-        multi_match: {
-          query: @query,
-          type: 'bool_prefix',
-          fields: %w(username^2 username.*^2 display_name display_name.*),
+        dis_max: {
+          queries: [
+            {
+              multi_match: {
+                query: @query,
+                type: 'most_fields',
+                fields: %w(username username.*),
+              },
+            },
+
+            {
+              multi_match: {
+                query: @query,
+                type: 'most_fields',
+                fields: %w(display_name display_name.*),
+              },
+            },
+          ],
         },
       }
     end
@@ -142,7 +130,7 @@ class AccountSearchService < BaseService
       {
         multi_match: {
           query: @query,
-          type: 'most_fields',
+          type: 'best_fields',
           fields: %w(username^2 display_name^2 text text.*),
           operator: 'and',
         },

From 8c0ff6498e090a2919e8f8104339796ed2d3d212 Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Tue, 25 Jun 2024 23:57:22 +0200
Subject: [PATCH 20/84] Change light mode to apply CSS variables to the body
 (#30839)

---
 app/javascript/styles/mastodon-light/variables.scss | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss
index 09a75a834..3cdbd9bf6 100644
--- a/app/javascript/styles/mastodon-light/variables.scss
+++ b/app/javascript/styles/mastodon-light/variables.scss
@@ -56,11 +56,11 @@ $account-background-color: $white !default;
 
 $emojis-requiring-inversion: 'chains';
 
-.theme-mastodon-light {
+body {
   --dropdown-border-color: #d9e1e8;
   --dropdown-background-color: #fff;
   --background-border-color: #d9e1e8;
   --background-color: #fff;
-  --background-color-tint: rgba(255, 255, 255, 90%);
+  --background-color-tint: rgba(255, 255, 255, 80%);
   --background-filter: blur(10px);
 }

From 2b43c05a6a1c38a7480119a0bd12fefa8b5589c6 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 26 Jun 2024 15:42:25 +0200
Subject: [PATCH 21/84] chore(deps): update dependency aws-sdk-s3 to v1.154.0
 (#30838)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index 5d735eb75..e8b54ed56 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -101,16 +101,16 @@ GEM
     awrence (1.2.1)
     aws-eventstream (1.3.0)
     aws-partitions (1.947.0)
-    aws-sdk-core (3.198.0)
+    aws-sdk-core (3.199.0)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.8)
       jmespath (~> 1, >= 1.6.1)
-    aws-sdk-kms (1.86.0)
-      aws-sdk-core (~> 3, >= 3.198.0)
+    aws-sdk-kms (1.87.0)
+      aws-sdk-core (~> 3, >= 3.199.0)
       aws-sigv4 (~> 1.1)
-    aws-sdk-s3 (1.153.0)
-      aws-sdk-core (~> 3, >= 3.198.0)
+    aws-sdk-s3 (1.154.0)
+      aws-sdk-core (~> 3, >= 3.199.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.8)
     aws-sigv4 (1.8.0)

From 7a84b76bb1716b131c35d9569bfcef3b32d73e60 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 26 Jun 2024 15:44:08 +0200
Subject: [PATCH 22/84] Drop favicon.ico generation (#30375)

---
 app/models/site_upload.rb               |  11 ++---------
 app/views/layouts/application.html.haml |   2 --
 config/imagemagick/policy.xml           |   2 +-
 lib/tasks/branding.rake                 |   3 ---
 public/favicon.ico                      | Bin 15086 -> 0 bytes
 spec/requests/account_show_page_spec.rb |   2 +-
 6 files changed, 4 insertions(+), 16 deletions(-)
 delete mode 100644 public/favicon.ico

diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb
index 6431d1007..273dd6de9 100644
--- a/app/models/site_upload.rb
+++ b/app/models/site_upload.rb
@@ -31,17 +31,10 @@ class SiteUpload < ApplicationRecord
         [:"#{size}", { format: 'png', geometry: "#{size}x#{size}#", file_geometry_parser: FastGeometryParser }]
       end.freeze,
 
-    favicon: {
-      ico: {
-        format: 'ico',
-        geometry: '48x48#',
-        file_geometry_parser: FastGeometryParser,
-      }.freeze,
-    }.merge(
+    favicon:
       FAVICON_SIZES.to_h do |size|
         [:"#{size}", { format: 'png', geometry: "#{size}x#{size}#", file_geometry_parser: FastGeometryParser }]
-      end
-    ).freeze,
+      end.freeze,
 
     thumbnail: {
       '@1x': {
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index e7f1a595e..0c0512e81 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -11,8 +11,6 @@
     - if storage_host?
       %link{ rel: 'dns-prefetch', href: storage_host }/
 
-    %link{ rel: 'icon', href: favicon_path('ico') || '/favicon.ico', type: 'image/x-icon' }/
-
     - SiteUpload::FAVICON_SIZES.each do |size|
       %link{ rel: 'icon', sizes: "#{size}x#{size}", href: favicon_path(size.to_i) || frontend_asset_path("icons/favicon-#{size}x#{size}.png"), type: 'image/png' }/
 
diff --git a/config/imagemagick/policy.xml b/config/imagemagick/policy.xml
index 2730a9f84..e2aa202f2 100644
--- a/config/imagemagick/policy.xml
+++ b/config/imagemagick/policy.xml
@@ -23,5 +23,5 @@
   <!-- Disallow any coder by default, and only enable ones required by Mastodon -->
   <policy domain="coder" rights="none" pattern="*" />
   <policy domain="coder" rights="read | write" pattern="{JPEG,PNG,GIF,WEBP,HEIC,AVIF}" />
-  <policy domain="coder" rights="write" pattern="{HISTOGRAM,RGB,INFO,ICO}" />
+  <policy domain="coder" rights="write" pattern="{HISTOGRAM,RGB,INFO}" />
 </policymap>
diff --git a/lib/tasks/branding.rake b/lib/tasks/branding.rake
index 608fb3af9..be72454ce 100644
--- a/lib/tasks/branding.rake
+++ b/lib/tasks/branding.rake
@@ -42,7 +42,6 @@ namespace :branding do
     output_dest     = Rails.root.join('app', 'javascript', 'icons')
 
     rsvg_convert = Terrapin::CommandLine.new('rsvg-convert', '-w :size -h :size --keep-aspect-ratio :input -o :output')
-    convert = Terrapin::CommandLine.new('convert', ':input :output', environment: { 'MAGICK_CONFIGURE_PATH' => nil })
 
     favicon_sizes      = [16, 32, 48]
     apple_icon_sizes   = [57, 60, 72, 76, 114, 120, 144, 152, 167, 180, 1024]
@@ -56,8 +55,6 @@ namespace :branding do
       rsvg_convert.run(size: size, input: favicon_source, output: output_path)
     end
 
-    convert.run(input: favicons, output: Rails.public_path.join('favicon.ico'))
-
     apple_icon_sizes.each do |size|
       rsvg_convert.run(size: size, input: app_icon_source, output: output_dest.join("apple-touch-icon-#{size}x#{size}.png"))
     end
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index b09a98bb9b0649cb67305b6663bd56b3cfb17222..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 15086
zcmdU0d303O8Gn{m+S9sq@gI+sdfIc^b8PjTdaRg0MwkFW3;~?w&Ax;HN^k)eL`<|&
z1*?|DswkUO)VePfAuIt!5wnnlY=nf607(d0XL~dAX1;#+P41gFZ{8awAyE3x`Q^L!
ze(Sx-yf^RrzA(&X%oyhCs~M!@nE5|snA;eJ88<FAe+Sn$;@T~@49$PXFjIcTFuz9`
zWT8fE9SOsrT&k;TqV6~48T#kTGjtUd8G0`wSdn39t;o=QP?@32PNiF&rJq|h(ZC^q
zpn9TV;Bc1VFagdx5zT7)wRak39=X#PMF2tVB;(d^#_N6}m2b*1Un!kS#|@L&%KFJH
zP}FHn+PcYXcKsxlt=F0g>a|7_j_<D5n*Z38lQUMTTdUFi?C2Cz6ai^s0OeL!O|kxz
z*lC$+x~*lJ35E~$6QY0OE|c!WU1lKgY~Kyuy<ZDsx+bNb&r86!|67sYp3DB})J%)>
zWUd7WLM<nN@9GxDWZvN{gbw%p;yl>Y0(?)eIB(B2&+W{!6?WuVfgpT5rcdXgW5<C%
z(=G7GS_t(cs89JFvuqEanQdQsW{wRAsJ=LFj_Hf@V?@7Ckd3Wbm-XoEPxk2TKoCZG
zVxP|WQymZ;9E{ITv_Yi%tT-pSg9gXL14cU#gueX_AR2|hmbW1AuXg~#VTf`ZaPNN%
zf!8+y@H~WTY6V#$-*0d{<}*2Gdd&_X@IO%uQNJJP72NhNaO*b+Hifld5CSV!3bI7r
zXR;SSVd2<-)!B&v0&dZR5L)vx1eX;Fb7AeVWx^Ww;1Y2S*Ezq<b{+W}2)kUFA%|-K
z0cpYR3WpqS7ty5`psk48{d^=pzZ~gV#2^BO(1)}KQHR(BZa2Aw2v_S33{#4D>Nvv`
zlTXHFLx{0ee-@)$>6qAkW*kas5aj+OT;#?x%!_<3%kR)l!u!t(y#MSd%g{IC*oWwi
z!BAg*hyEkHCp}$phi*K?A|0leWbuQ4F<)7cX?(OI)6j{4bWxdUXs(!GyeHXW`YRO~
z##;|hFti|GgxHn7w?xUBiAHyImLY@y6-01-AL2E{N<ynNhIjBjzZ1tN5ab>|BLCGI
zHm@ej7^%@1K^cZG@xJ`q#E27Tl^Vy@H4_bgNL)SltbVfX7qxd9dk~-uLuIW_clo(3
zjZC6eYbwTP6Q}{7VT(s*IX$fgEn9;CRb-oV_{^M&&(ufiwe0$aZ1z2*B`E)WgVuD2
zj4{R!k-xQZGW++&Y~y{6*{12WTHUYD6#;pMAI)YX2)Gbv%4QoIr?5-OGhHbb<YzVE
zbN(Wry-91nT^>X8RP+31{H}13;Axp^xmg-RbB=jsOO6>X8m5~MLvisBXbi2>&D#(l
zMOV@D(7W*+0o^OtCGezjYU8dao{Q_h@Jh0NjyZ?=Z<}HM0s&HZcYj46_rO)xCGezj
zYGcoTN<{tO(dH!m_PZ^wQUC2TEC+B7Qut*4rMz^;o-cMK_)pS7pVa@UT=S7r_`OgH
z-yX^SYw8ksM6T=JN8s%8X9b*jXn7LfEAbz~{Y!1f#@06K|8$=91OlY+OZ*4Xzm!MD
zz8!nw`>Sn8=5LShKh)Hm#6LaL+Clwy&a!qPK#IWL`2LB1DUXc(GXF_@nSVJS=Vz(^
zuGzLh1V|CU@B8!$9;r{@5jp?Py_5~1x`t%_m%HP9{C+OAJv1Jq{=4t71rZ<xR~q*p
zLjO`883#%d{U`Bv#QlfSznqWrAobs)v+)R!Lh3(UTc5xqatVC={YrHO{!WR1e7}(M
zaUP}qd-Zl60a9?gO2zZz+fF9%h+Me7QRGMZ2cXyBNRSC0{9fb}S*h)zvAF+!qdkBC
zDFW|(B<hoQ7tf>1MZV|p;`qDAkd$xDD<XfOpb(-ypQs-?a8Tq)e0b>o@tvn>fOUWr
z-lv`w^~r#||AfB&MtGMZ?@q$npa&v&Ho_H&K)9>|Li@ggXkTA!ZRls<jm-(Rhy3(V
z{~nXQ#A9+m0?ba}k2WRPkx#hw8^Qn7ilnoU%0=<L(d(L*Xq)&lJH*ew-|SfBBkyf8
z_!ljPXh(<CoH!1b>=xyNZ@#4%Lln;^u>2`ePHG?D6Zg^m`>d{8{rHZk2=_b)ZG8*)
zvT}$VsRbU-H@NDBBw4QLX$bGy1AKK2@Rd~%djCTR+*_C=C;G6t9;W^Y<!sIm5O9%j
z227@Fr2c^|_+`-UYC*sS2G7}Dxl-SB3{L0uVW+z?>~z5e0>?X@7V1xllDq-Fi$4)@
zxq1;WT8O$_yTRp3_}-`FpRR$!@ng&f<Y6o;qi*-7h$D#RXny`lq`e3)0uYg*Iaxb_
zXhzhcTnXkJwm^RVVzhG;wJWE<j0K(<1013s(K^-xrB?xz-hg13qGE=*3NaQj265T>
zpcM5HV_MN(KicIPCX3`lF-6Ka5MrJp<r^jn4&(Z_PB*4J!*F}qc-><a<Mo>>#_RT$
zjn}te&Z1X>GdM0QyF<SXdCO6LHs(2hJ`6XbDq8mEf*)ZXLnq`_GWGqKTY(XPN1c6`
zlUbnl-ZiR0qcL*UhcgXM%+<7E{4jdZcU4T#&l&A_rS_^bO*d9fFzg#)zUQ2IqN)kT
z^-_PMIj+*M(=d<UPxAX03>PT=8jaq9IV)bsTdC)~<higT)f)Y}sw~63nENuKt}d20
zGd@_QF&1Mk>_x;UxZZ+@#%z!N`J>$&%#($cb7jdnv}%p9qE^FZ4(E5@kdb>ubrw4w
z_nDNp7>*se?BPke>#(NNO=~;nq#AaQy!Iovc`@flCb4fT=Lr?_hg{tx6K2ydjjK*$
zxT$Uu8%01`m~8y`(uh131?Cz5Mr(R$Q7v1j7C*^7)=V*8UpEEM1aVYry0Knk9EbPg
zYxZh1=ctUTSxvo;InzqjJgagJ)=6@y4cVqg8@22<)U8JJBBE*7;ZUcO<Y-a$-3G1k
zIXts@SgV{UuX(19I>v3s`fFo08%Dq-0Dq&__}Nh{I|uODpHBQF|C(k!H{H7LD9N!k
zjoIeO=i0-usiuWZQ!!V4X(DoTn(2vDHMnCrrnSc~*L^9YIfs2x?f1|!&HVZBYx*PU
z12`|8DAp3DEA8QUj=2s2{|9Iw?;-L%V7}k0mh(U9xF0$ne>MT97Cf4q`<K-k)a=RE
z+bs(dY!hFn?^~*B|M+wZD~+Gj52bw@@^RmWZDM|j4!kA%#J&!>{BY+zt6!&PMe6^y
zOx^wr)BDo+kIk}P+cv|3wG3q_#=mR7s=S&VwROe#50<5}@0He>+Gm)4msAIl)Lr^F
zDx5#YpGs#qyxuhm=a2EHvT=HP{Ci_tuJu;h)1yTDHzmjK!}yhCN3zztHaY$P#-GyG
zjQBI{)J&Te`;tHn14{Al-mfa(HSZzlU9~0w-Ajs8<@;3PSBr<NpUku9>1W_{o+S_K
zMxcg)O>c{zD?0FF{A%+3uWX`XBK7Y*pf11m4N;B`0td@g<psaF7Bh{%Gta_`Jz9CO
z-Y)5UaI<p!QW@!ZKq>yx1MxmEnUDT;$?<dFm8;4NzGm6%G=7{r6zXVVZB8wIHJyQ1
zH_>AhX#nF_m48_|el_`_^|>_ut~r(kU9+uNi&KVYOY->#_Uu=cAAC(A{(T43<yGQW
zi-)YAnPt6?#@{{1{vhqyQ=(p#_?2YIn&-96;<?j70OMDaAN=PADkf6yP`Ro+vC}=r
z_7IK#tj_kBLLE}9O?ux{IRCx-MZR3uvpzX~4&#^0(YZ$<{vgIr`AU?;9FO0+4CkN*
zpF;c`#;+#t-SDP(j&u;jTA`Y}=auC1C-JMvll7i^Y(??-?aOEnrxI0)Klsge@xIQX
zeo(6G|L8Lswn)QR3#WWJ^=;fL%FzLdUoMw8cNEk3dv*3@eHeope6Mb#W<?rqY*LjE
zeS1ihqXW|C+K07dB_G_UUr;fT21~zEl#T1h<L}qom-ZV-f2%Tlt6mcAiUaJ89V}du
zq%$z@exZj}l#>j+E1pl1?>AvBtFucY6&rG&e4Z>TiLcKniQiyffVFC|Pgg!3oWCf^
zK3U+g{_a_{MAY*vUJ5++1gKq6tmpflSsANYqrXAdd<d4r`%R>J{ukGZcIEMqIiB@0
z`uF2u?Q;gP&YlYVv1Y0HgfXcnlX|xNA~pCBe$M4r)T1m<<wzah<Gwd#JOd`j{WN~B
z#dUK+jb2`}$9Z7&%hY^&6nh)|e|-+T1q*@1JDe-@<<8`~zAUCe*6F12Lr&pXth<Lu
zk9I1A4`RP~npbH14$&6(k9CTD07|CFXTgWH{Zw%fp3m%@LE|UXZ*lDQS#UiS9QnYf
zv4`;fufS(>CD;mX!Fwe3$EUg?ZEfJke)g0x#Nvy3tk!F#@%t>!1!50^TF=AKdhn&y
z5UDwm7!My2dK?H(%?`J$2>7oL00aW@ZQ$Qo<oI!+m%@+dqoylcKO~Kvj7eP_d-U5$
z|9;wVxP|_IuJ8c};CZL5hd#LZ3xRt8|3AQ0Ff2X6hs9-+$4}+~tHXMc`aI63pQHi`
zlJh@wKiDI(<pTGKoKGK$Kjd(bwgS})^VOGQkI9E={a;7A|Nk*Ma(vK{KU+0$34{E8
zc$aGp?jw4E`elaKhl4#cVTXH~B*e6ayxV;j{;y3v?gjpb_ze4VZcHm+1tC)Z;PdlY
z?7i86Jv%&t{*TXvqyhBvHug<rDTFlQB|L{K5O+ep>w(Bz_uA-O_vg`k*CE{7QKVhb
zeC$7pfkO%sL?t{Ta{_g-7YTLq-P<Gi?#+m2U@m^?al13fy*%9@u}NtxU||DV5rh_F
za}uFwfKbv*N+|Xe36x`Q;K{t7$--w4p+y31MQt)~W%w~MiVaCPFGW43XozMF(W_*1
z?9d!5U4lH)VxmVW{_mAAzXWJ0i3{Tps3hK2vW|1?zrrr8A&UFt$oz)bIWo9^Aandd
J&YPUie*xt08p;3w

diff --git a/spec/requests/account_show_page_spec.rb b/spec/requests/account_show_page_spec.rb
index 81e965e6e..830d77860 100644
--- a/spec/requests/account_show_page_spec.rb
+++ b/spec/requests/account_show_page_spec.rb
@@ -9,7 +9,7 @@ describe 'The account show page' do
 
     get '/@alice'
 
-    expect(head_link_icons.size).to eq(4) # One general favicon and three with sizes
+    expect(head_link_icons.size).to eq(3) # Three favicons with sizes
 
     expect(head_meta_content('og:title')).to match alice.display_name
     expect(head_meta_content('og:type')).to eq 'profile'

From 528a7f57fa8a31d1a901c247cc55b214e2dc1af5 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 26 Jun 2024 09:51:11 -0400
Subject: [PATCH 23/84] Fix `Rails/ReversibleMigration` cop for `change_column`
 (#30835)

---
 db/migrate/20160223164502_make_uris_nullable_in_statuses.rb | 6 +++++-
 ...170322143850_change_primary_key_to_bigint_on_statuses.rb | 6 +++++-
 .../20170609145826_remove_default_language_from_statuses.rb | 6 +++++-
 3 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/db/migrate/20160223164502_make_uris_nullable_in_statuses.rb b/db/migrate/20160223164502_make_uris_nullable_in_statuses.rb
index fff07093c..ebb572bd6 100644
--- a/db/migrate/20160223164502_make_uris_nullable_in_statuses.rb
+++ b/db/migrate/20160223164502_make_uris_nullable_in_statuses.rb
@@ -1,7 +1,11 @@
 # frozen_string_literal: true
 
 class MakeUrisNullableInStatuses < ActiveRecord::Migration[4.2]
-  def change
+  def up
     change_column :statuses, :uri, :string, null: true, default: nil
   end
+
+  def down
+    raise ActiveRecord::IrreversibleMigration
+  end
 end
diff --git a/db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb b/db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb
index b98fffab8..e7fcb75a4 100644
--- a/db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb
+++ b/db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ChangePrimaryKeyToBigintOnStatuses < ActiveRecord::Migration[5.0]
-  def change
+  def up
     change_table(:statuses, bulk: true) do |t|
       t.change :id, :bigint
       t.change :reblog_of_id, :bigint
@@ -16,4 +16,8 @@ class ChangePrimaryKeyToBigintOnStatuses < ActiveRecord::Migration[5.0]
     change_column :statuses_tags, :status_id, :bigint
     change_column :stream_entries, :activity_id, :bigint
   end
+
+  def down
+    raise ActiveRecord::IrreversibleMigration
+  end
 end
diff --git a/db/migrate/20170609145826_remove_default_language_from_statuses.rb b/db/migrate/20170609145826_remove_default_language_from_statuses.rb
index 28b4172a8..122c32228 100644
--- a/db/migrate/20170609145826_remove_default_language_from_statuses.rb
+++ b/db/migrate/20170609145826_remove_default_language_from_statuses.rb
@@ -1,7 +1,11 @@
 # frozen_string_literal: true
 
 class RemoveDefaultLanguageFromStatuses < ActiveRecord::Migration[5.1]
-  def change
+  def up
     change_column :statuses, :language, :string, default: nil, null: true
   end
+
+  def down
+    raise ActiveRecord::IrreversibleMigration
+  end
 end

From 51f581e03e1b2611eceddf5010ef736680e1f62b Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 26 Jun 2024 09:51:44 -0400
Subject: [PATCH 24/84] Fix `Rails/ReversibleMigration` cop for `remove`
 (#30833)

---
 db/migrate/20170520145338_change_language_filter_to_opt_out.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/db/migrate/20170520145338_change_language_filter_to_opt_out.rb b/db/migrate/20170520145338_change_language_filter_to_opt_out.rb
index f0a95819c..0e91c4680 100644
--- a/db/migrate/20170520145338_change_language_filter_to_opt_out.rb
+++ b/db/migrate/20170520145338_change_language_filter_to_opt_out.rb
@@ -5,7 +5,7 @@ class ChangeLanguageFilterToOptOut < ActiveRecord::Migration[5.0]
     remove_index :users, :allowed_languages
 
     change_table(:users, bulk: true) do |t|
-      t.remove :allowed_languages
+      t.remove :allowed_languages, type: :string, array: true, default: [], null: false
       t.column :filtered_languages, :string, array: true, default: [], null: false
     end
 

From 863c470a2bc4e13a5b8df4d66a1322f4b84e2db2 Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Wed, 26 Jun 2024 20:04:50 +0200
Subject: [PATCH 25/84] Convert `<Directory>` to Typescript / function
 component (#30829)

---
 app/javascript/mastodon/actions/directory.js  |  62 ----
 app/javascript/mastodon/actions/directory.ts  |  37 +++
 app/javascript/mastodon/api/directory.ts      |  15 +
 .../directory/components/account_card.jsx     | 234 ---------------
 .../directory/components/account_card.tsx     | 269 ++++++++++++++++++
 .../mastodon/features/directory/index.jsx     | 181 ------------
 .../mastodon/features/directory/index.tsx     | 217 ++++++++++++++
 .../mastodon/reducers/user_lists.js           |  32 +--
 8 files changed, 553 insertions(+), 494 deletions(-)
 delete mode 100644 app/javascript/mastodon/actions/directory.js
 create mode 100644 app/javascript/mastodon/actions/directory.ts
 create mode 100644 app/javascript/mastodon/api/directory.ts
 delete mode 100644 app/javascript/mastodon/features/directory/components/account_card.jsx
 create mode 100644 app/javascript/mastodon/features/directory/components/account_card.tsx
 delete mode 100644 app/javascript/mastodon/features/directory/index.jsx
 create mode 100644 app/javascript/mastodon/features/directory/index.tsx

diff --git a/app/javascript/mastodon/actions/directory.js b/app/javascript/mastodon/actions/directory.js
deleted file mode 100644
index 7a0748029..000000000
--- a/app/javascript/mastodon/actions/directory.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import api from '../api';
-
-import { fetchRelationships } from './accounts';
-import { importFetchedAccounts } from './importer';
-
-export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
-export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
-export const DIRECTORY_FETCH_FAIL    = 'DIRECTORY_FETCH_FAIL';
-
-export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
-export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
-export const DIRECTORY_EXPAND_FAIL    = 'DIRECTORY_EXPAND_FAIL';
-
-export const fetchDirectory = params => (dispatch) => {
-  dispatch(fetchDirectoryRequest());
-
-  api().get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
-    dispatch(importFetchedAccounts(data));
-    dispatch(fetchDirectorySuccess(data));
-    dispatch(fetchRelationships(data.map(x => x.id)));
-  }).catch(error => dispatch(fetchDirectoryFail(error)));
-};
-
-export const fetchDirectoryRequest = () => ({
-  type: DIRECTORY_FETCH_REQUEST,
-});
-
-export const fetchDirectorySuccess = accounts => ({
-  type: DIRECTORY_FETCH_SUCCESS,
-  accounts,
-});
-
-export const fetchDirectoryFail = error => ({
-  type: DIRECTORY_FETCH_FAIL,
-  error,
-});
-
-export const expandDirectory = params => (dispatch, getState) => {
-  dispatch(expandDirectoryRequest());
-
-  const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
-
-  api().get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
-    dispatch(importFetchedAccounts(data));
-    dispatch(expandDirectorySuccess(data));
-    dispatch(fetchRelationships(data.map(x => x.id)));
-  }).catch(error => dispatch(expandDirectoryFail(error)));
-};
-
-export const expandDirectoryRequest = () => ({
-  type: DIRECTORY_EXPAND_REQUEST,
-});
-
-export const expandDirectorySuccess = accounts => ({
-  type: DIRECTORY_EXPAND_SUCCESS,
-  accounts,
-});
-
-export const expandDirectoryFail = error => ({
-  type: DIRECTORY_EXPAND_FAIL,
-  error,
-});
diff --git a/app/javascript/mastodon/actions/directory.ts b/app/javascript/mastodon/actions/directory.ts
new file mode 100644
index 000000000..34ac309c6
--- /dev/null
+++ b/app/javascript/mastodon/actions/directory.ts
@@ -0,0 +1,37 @@
+import type { List as ImmutableList } from 'immutable';
+
+import { apiGetDirectory } from 'mastodon/api/directory';
+import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
+
+import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
+
+export const fetchDirectory = createDataLoadingThunk(
+  'directory/fetch',
+  async (params: Parameters<typeof apiGetDirectory>[0]) =>
+    apiGetDirectory(params),
+  (data, { dispatch }) => {
+    dispatch(importFetchedAccounts(data));
+    dispatch(fetchRelationships(data.map((x) => x.id)));
+
+    return { accounts: data };
+  },
+);
+
+export const expandDirectory = createDataLoadingThunk(
+  'directory/expand',
+  async (params: Parameters<typeof apiGetDirectory>[0], { getState }) => {
+    const loadedItems = getState().user_lists.getIn([
+      'directory',
+      'items',
+    ]) as ImmutableList<unknown>;
+
+    return apiGetDirectory({ ...params, offset: loadedItems.size }, 20);
+  },
+  (data, { dispatch }) => {
+    dispatch(importFetchedAccounts(data));
+    dispatch(fetchRelationships(data.map((x) => x.id)));
+
+    return { accounts: data };
+  },
+);
diff --git a/app/javascript/mastodon/api/directory.ts b/app/javascript/mastodon/api/directory.ts
new file mode 100644
index 000000000..cd39f8f26
--- /dev/null
+++ b/app/javascript/mastodon/api/directory.ts
@@ -0,0 +1,15 @@
+import { apiRequestGet } from 'mastodon/api';
+import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
+
+export const apiGetDirectory = (
+  params: {
+    order: string;
+    local: boolean;
+    offset?: number;
+  },
+  limit = 20,
+) =>
+  apiRequestGet<ApiAccountJSON[]>('v1/directory', {
+    ...params,
+    limit,
+  });
diff --git a/app/javascript/mastodon/features/directory/components/account_card.jsx b/app/javascript/mastodon/features/directory/components/account_card.jsx
deleted file mode 100644
index 9c5e68812..000000000
--- a/app/javascript/mastodon/features/directory/components/account_card.jsx
+++ /dev/null
@@ -1,234 +0,0 @@
-import PropTypes from 'prop-types';
-
-import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
-
-import classNames from 'classnames';
-import { Link } from 'react-router-dom';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import {
-  followAccount,
-  unfollowAccount,
-  unblockAccount,
-  unmuteAccount,
-} from 'mastodon/actions/accounts';
-import { openModal } from 'mastodon/actions/modal';
-import { Avatar } from 'mastodon/components/avatar';
-import { Button } from 'mastodon/components/button';
-import { DisplayName } from 'mastodon/components/display_name';
-import { ShortNumber } from 'mastodon/components/short_number';
-import { autoPlayGif, me } from 'mastodon/initial_state';
-import { makeGetAccount } from 'mastodon/selectors';
-
-const messages = defineMessages({
-  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
-  follow: { id: 'account.follow', defaultMessage: 'Follow' },
-  cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
-  cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
-  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
-  unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
-  unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
-  unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
-  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
-});
-
-const makeMapStateToProps = () => {
-  const getAccount = makeGetAccount();
-
-  const mapStateToProps = (state, { id }) => ({
-    account: getAccount(state, id),
-  });
-
-  return mapStateToProps;
-};
-
-const mapDispatchToProps = (dispatch, { intl }) => ({
-  onFollow(account) {
-    if (account.getIn(['relationship', 'following'])) {
-      dispatch(
-        openModal({
-          modalType: 'CONFIRM',
-          modalProps: {
-            message: (
-              <FormattedMessage
-                id='confirmations.unfollow.message'
-                defaultMessage='Are you sure you want to unfollow {name}?'
-                values={{ name: <strong>@{account.get('acct')}</strong> }}
-              />
-            ),
-            confirm: intl.formatMessage(messages.unfollowConfirm),
-            onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
-          } }),
-      );
-    } else if (account.getIn(['relationship', 'requested'])) {
-      dispatch(openModal({
-        modalType: 'CONFIRM',
-        modalProps: {
-          message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-          confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
-          onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
-        },
-      }));
-    } else {
-      dispatch(followAccount(account.get('id')));
-    }
-  },
-
-  onBlock(account) {
-    if (account.getIn(['relationship', 'blocking'])) {
-      dispatch(unblockAccount(account.get('id')));
-    }
-  },
-
-  onMute(account) {
-    if (account.getIn(['relationship', 'muting'])) {
-      dispatch(unmuteAccount(account.get('id')));
-    }
-  },
-
-});
-
-class AccountCard extends ImmutablePureComponent {
-
-  static propTypes = {
-    account: ImmutablePropTypes.record.isRequired,
-    intl: PropTypes.object.isRequired,
-    onFollow: PropTypes.func.isRequired,
-    onBlock: PropTypes.func.isRequired,
-    onMute: PropTypes.func.isRequired,
-  };
-
-  handleMouseEnter = ({ currentTarget }) => {
-    if (autoPlayGif) {
-      return;
-    }
-
-    const emojis = currentTarget.querySelectorAll('.custom-emoji');
-
-    for (var i = 0; i < emojis.length; i++) {
-      let emoji = emojis[i];
-      emoji.src = emoji.getAttribute('data-original');
-    }
-  };
-
-  handleMouseLeave = ({ currentTarget }) => {
-    if (autoPlayGif) {
-      return;
-    }
-
-    const emojis = currentTarget.querySelectorAll('.custom-emoji');
-
-    for (var i = 0; i < emojis.length; i++) {
-      let emoji = emojis[i];
-      emoji.src = emoji.getAttribute('data-static');
-    }
-  };
-
-  handleFollow = () => {
-    this.props.onFollow(this.props.account);
-  };
-
-  handleBlock = () => {
-    this.props.onBlock(this.props.account);
-  };
-
-  handleMute = () => {
-    this.props.onMute(this.props.account);
-  };
-
-  handleEditProfile = () => {
-    window.open('/settings/profile', '_blank');
-  };
-
-  render() {
-    const { account, intl } = this.props;
-
-    let actionBtn;
-
-    if (me !== account.get('id')) {
-      if (!account.get('relationship')) { // Wait until the relationship is loaded
-        actionBtn = '';
-      } else if (account.getIn(['relationship', 'requested'])) {
-        actionBtn = <Button  text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
-      } else if (account.getIn(['relationship', 'muting'])) {
-        actionBtn = <Button  text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
-      } else if (!account.getIn(['relationship', 'blocking'])) {
-        actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
-      } else if (account.getIn(['relationship', 'blocking'])) {
-        actionBtn = <Button  text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
-      }
-    } else {
-      actionBtn = <Button  text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
-    }
-
-    return (
-      <div className='account-card'>
-        <Link to={`/@${account.get('acct')}`} className='account-card__permalink'>
-          <div className='account-card__header'>
-            <img
-              src={
-                autoPlayGif ? account.get('header') : account.get('header_static')
-              }
-              alt=''
-            />
-          </div>
-
-          <div className='account-card__title'>
-            <div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
-            <DisplayName account={account} />
-          </div>
-        </Link>
-
-        {account.get('note').length > 0 && (
-          <div
-            className='account-card__bio translate'
-            onMouseEnter={this.handleMouseEnter}
-            onMouseLeave={this.handleMouseLeave}
-            dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
-          />
-        )}
-
-        <div className='account-card__actions'>
-          <div className='account-card__counters'>
-            <div className='account-card__counters__item'>
-              <ShortNumber value={account.get('statuses_count')} />
-              <small>
-                <FormattedMessage id='account.posts' defaultMessage='Posts' />
-              </small>
-            </div>
-
-            <div className='account-card__counters__item'>
-              <ShortNumber value={account.get('followers_count')} />{' '}
-              <small>
-                <FormattedMessage
-                  id='account.followers'
-                  defaultMessage='Followers'
-                />
-              </small>
-            </div>
-
-            <div className='account-card__counters__item'>
-              <ShortNumber value={account.get('following_count')} />{' '}
-              <small>
-                <FormattedMessage
-                  id='account.following'
-                  defaultMessage='Following'
-                />
-              </small>
-            </div>
-          </div>
-
-          <div className='account-card__actions__button'>
-            {actionBtn}
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(AccountCard));
diff --git a/app/javascript/mastodon/features/directory/components/account_card.tsx b/app/javascript/mastodon/features/directory/components/account_card.tsx
new file mode 100644
index 000000000..7201f6135
--- /dev/null
+++ b/app/javascript/mastodon/features/directory/components/account_card.tsx
@@ -0,0 +1,269 @@
+import type { MouseEventHandler } from 'react';
+import { useCallback } from 'react';
+
+import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
+
+import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
+import {
+  followAccount,
+  unfollowAccount,
+  unblockAccount,
+  unmuteAccount,
+} from 'mastodon/actions/accounts';
+import { openModal } from 'mastodon/actions/modal';
+import { Avatar } from 'mastodon/components/avatar';
+import { Button } from 'mastodon/components/button';
+import { DisplayName } from 'mastodon/components/display_name';
+import { ShortNumber } from 'mastodon/components/short_number';
+import { autoPlayGif, me } from 'mastodon/initial_state';
+import type { Account } from 'mastodon/models/account';
+import { makeGetAccount } from 'mastodon/selectors';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+const messages = defineMessages({
+  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
+  follow: { id: 'account.follow', defaultMessage: 'Follow' },
+  cancel_follow_request: {
+    id: 'account.cancel_follow_request',
+    defaultMessage: 'Withdraw follow request',
+  },
+  cancelFollowRequestConfirm: {
+    id: 'confirmations.cancel_follow_request.confirm',
+    defaultMessage: 'Withdraw request',
+  },
+  requested: {
+    id: 'account.requested',
+    defaultMessage: 'Awaiting approval. Click to cancel follow request',
+  },
+  unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
+  unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
+  unfollowConfirm: {
+    id: 'confirmations.unfollow.confirm',
+    defaultMessage: 'Unfollow',
+  },
+  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+});
+
+const getAccount = makeGetAccount();
+
+export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
+  const intl = useIntl();
+  const account = useAppSelector((s) => getAccount(s, accountId));
+  const dispatch = useAppDispatch();
+
+  const handleMouseEnter = useCallback<MouseEventHandler>(
+    ({ currentTarget }) => {
+      if (autoPlayGif) {
+        return;
+      }
+      const emojis =
+        currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
+
+      emojis.forEach((emoji) => {
+        const original = emoji.getAttribute('data-original');
+        if (original) emoji.src = original;
+      });
+    },
+    [],
+  );
+
+  const handleMouseLeave = useCallback<MouseEventHandler>(
+    ({ currentTarget }) => {
+      if (autoPlayGif) {
+        return;
+      }
+
+      const emojis =
+        currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
+
+      emojis.forEach((emoji) => {
+        const staticUrl = emoji.getAttribute('data-static');
+        if (staticUrl) emoji.src = staticUrl;
+      });
+    },
+    [],
+  );
+
+  const handleFollow = useCallback(() => {
+    if (!account) return;
+
+    if (account.getIn(['relationship', 'following'])) {
+      dispatch(
+        openModal({
+          modalType: 'CONFIRM',
+          modalProps: {
+            message: (
+              <FormattedMessage
+                id='confirmations.unfollow.message'
+                defaultMessage='Are you sure you want to unfollow {name}?'
+                values={{ name: <strong>@{account.get('acct')}</strong> }}
+              />
+            ),
+            confirm: intl.formatMessage(messages.unfollowConfirm),
+            onConfirm: () => {
+              dispatch(unfollowAccount(account.get('id')));
+            },
+          },
+        }),
+      );
+    } else if (account.getIn(['relationship', 'requested'])) {
+      dispatch(
+        openModal({
+          modalType: 'CONFIRM',
+          modalProps: {
+            message: (
+              <FormattedMessage
+                id='confirmations.cancel_follow_request.message'
+                defaultMessage='Are you sure you want to withdraw your request to follow {name}?'
+                values={{ name: <strong>@{account.get('acct')}</strong> }}
+              />
+            ),
+            confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
+            onConfirm: () => {
+              dispatch(unfollowAccount(account.get('id')));
+            },
+          },
+        }),
+      );
+    } else {
+      dispatch(followAccount(account.get('id')));
+    }
+  }, [account, dispatch, intl]);
+
+  const handleBlock = useCallback(() => {
+    if (account?.relationship?.blocking) {
+      dispatch(unblockAccount(account.get('id')));
+    }
+  }, [account, dispatch]);
+
+  const handleMute = useCallback(() => {
+    if (account?.relationship?.muting) {
+      dispatch(unmuteAccount(account.get('id')));
+    }
+  }, [account, dispatch]);
+
+  const handleEditProfile = useCallback(() => {
+    window.open('/settings/profile', '_blank');
+  }, []);
+
+  if (!account) return null;
+
+  let actionBtn;
+
+  if (me !== account.get('id')) {
+    if (!account.get('relationship')) {
+      // Wait until the relationship is loaded
+      actionBtn = '';
+    } else if (account.getIn(['relationship', 'requested'])) {
+      actionBtn = (
+        <Button
+          text={intl.formatMessage(messages.cancel_follow_request)}
+          title={intl.formatMessage(messages.requested)}
+          onClick={handleFollow}
+        />
+      );
+    } else if (account.getIn(['relationship', 'muting'])) {
+      actionBtn = (
+        <Button
+          text={intl.formatMessage(messages.unmute)}
+          onClick={handleMute}
+        />
+      );
+    } else if (!account.getIn(['relationship', 'blocking'])) {
+      actionBtn = (
+        <Button
+          disabled={account.relationship?.blocked_by}
+          className={classNames({
+            'button--destructive': account.getIn(['relationship', 'following']),
+          })}
+          text={intl.formatMessage(
+            account.getIn(['relationship', 'following'])
+              ? messages.unfollow
+              : messages.follow,
+          )}
+          onClick={handleFollow}
+        />
+      );
+    } else if (account.getIn(['relationship', 'blocking'])) {
+      actionBtn = (
+        <Button
+          text={intl.formatMessage(messages.unblock)}
+          onClick={handleBlock}
+        />
+      );
+    }
+  } else {
+    actionBtn = (
+      <Button
+        text={intl.formatMessage(messages.edit_profile)}
+        onClick={handleEditProfile}
+      />
+    );
+  }
+
+  return (
+    <div className='account-card'>
+      <Link to={`/@${account.get('acct')}`} className='account-card__permalink'>
+        <div className='account-card__header'>
+          <img
+            src={
+              autoPlayGif ? account.get('header') : account.get('header_static')
+            }
+            alt=''
+          />
+        </div>
+
+        <div className='account-card__title'>
+          <div className='account-card__title__avatar'>
+            <Avatar account={account as Account} size={56} />
+          </div>
+          <DisplayName account={account as Account} />
+        </div>
+      </Link>
+
+      {account.get('note').length > 0 && (
+        <div
+          className='account-card__bio translate'
+          onMouseEnter={handleMouseEnter}
+          onMouseLeave={handleMouseLeave}
+          dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
+        />
+      )}
+
+      <div className='account-card__actions'>
+        <div className='account-card__counters'>
+          <div className='account-card__counters__item'>
+            <ShortNumber value={account.get('statuses_count')} />
+            <small>
+              <FormattedMessage id='account.posts' defaultMessage='Posts' />
+            </small>
+          </div>
+
+          <div className='account-card__counters__item'>
+            <ShortNumber value={account.get('followers_count')} />{' '}
+            <small>
+              <FormattedMessage
+                id='account.followers'
+                defaultMessage='Followers'
+              />
+            </small>
+          </div>
+
+          <div className='account-card__counters__item'>
+            <ShortNumber value={account.get('following_count')} />{' '}
+            <small>
+              <FormattedMessage
+                id='account.following'
+                defaultMessage='Following'
+              />
+            </small>
+          </div>
+        </div>
+
+        <div className='account-card__actions__button'>{actionBtn}</div>
+      </div>
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/features/directory/index.jsx b/app/javascript/mastodon/features/directory/index.jsx
deleted file mode 100644
index 0d3408146..000000000
--- a/app/javascript/mastodon/features/directory/index.jsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { defineMessages, injectIntl } from 'react-intl';
-
-import { Helmet } from 'react-helmet';
-
-import { List as ImmutableList } from 'immutable';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
-
-import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
-import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodon/actions/columns';
-import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
-import Column from 'mastodon/components/column';
-import ColumnHeader from 'mastodon/components/column_header';
-import { LoadMore } from 'mastodon/components/load_more';
-import { LoadingIndicator } from 'mastodon/components/loading_indicator';
-import { RadioButton } from 'mastodon/components/radio_button';
-import ScrollContainer from 'mastodon/containers/scroll_container';
-
-import AccountCard from './components/account_card';
-
-const messages = defineMessages({
-  title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
-  recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
-  newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
-  local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
-  federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
-});
-
-const mapStateToProps = state => ({
-  accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
-  isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
-  domain: state.getIn(['meta', 'domain']),
-});
-
-class Directory extends PureComponent {
-
-  static propTypes = {
-    isLoading: PropTypes.bool,
-    accountIds: ImmutablePropTypes.list.isRequired,
-    dispatch: PropTypes.func.isRequired,
-    columnId: PropTypes.string,
-    intl: PropTypes.object.isRequired,
-    multiColumn: PropTypes.bool,
-    domain: PropTypes.string.isRequired,
-    params: PropTypes.shape({
-      order: PropTypes.string,
-      local: PropTypes.bool,
-    }),
-  };
-
-  state = {
-    order: null,
-    local: null,
-  };
-
-  handlePin = () => {
-    const { columnId, dispatch } = this.props;
-
-    if (columnId) {
-      dispatch(removeColumn(columnId));
-    } else {
-      dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state)));
-    }
-  };
-
-  getParams = (props, state) => ({
-    order: state.order === null ? (props.params.order || 'active') : state.order,
-    local: state.local === null ? (props.params.local || false) : state.local,
-  });
-
-  handleMove = dir => {
-    const { columnId, dispatch } = this.props;
-    dispatch(moveColumn(columnId, dir));
-  };
-
-  handleHeaderClick = () => {
-    this.column.scrollTop();
-  };
-
-  componentDidMount () {
-    const { dispatch } = this.props;
-    dispatch(fetchDirectory(this.getParams(this.props, this.state)));
-  }
-
-  componentDidUpdate (prevProps, prevState) {
-    const { dispatch } = this.props;
-    const paramsOld = this.getParams(prevProps, prevState);
-    const paramsNew = this.getParams(this.props, this.state);
-
-    if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
-      dispatch(fetchDirectory(paramsNew));
-    }
-  }
-
-  setRef = c => {
-    this.column = c;
-  };
-
-  handleChangeOrder = e => {
-    const { dispatch, columnId } = this.props;
-
-    if (columnId) {
-      dispatch(changeColumnParams(columnId, ['order'], e.target.value));
-    } else {
-      this.setState({ order: e.target.value });
-    }
-  };
-
-  handleChangeLocal = e => {
-    const { dispatch, columnId } = this.props;
-
-    if (columnId) {
-      dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1'));
-    } else {
-      this.setState({ local: e.target.value === '1' });
-    }
-  };
-
-  handleLoadMore = () => {
-    const { dispatch } = this.props;
-    dispatch(expandDirectory(this.getParams(this.props, this.state)));
-  };
-
-  render () {
-    const { isLoading, accountIds, intl, columnId, multiColumn, domain } = this.props;
-    const { order, local }  = this.getParams(this.props, this.state);
-    const pinned = !!columnId;
-
-    const scrollableArea = (
-      <div className='scrollable'>
-        <div className='filter-form'>
-          <div className='filter-form__column' role='group'>
-            <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
-            <RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} />
-          </div>
-
-          <div className='filter-form__column' role='group'>
-            <RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain })} checked={local} onChange={this.handleChangeLocal} />
-            <RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} />
-          </div>
-        </div>
-
-        <div className='directory__list'>
-          {isLoading ? <LoadingIndicator /> : accountIds.map(accountId => (
-            <AccountCard id={accountId} key={accountId} />
-          ))}
-        </div>
-
-        <LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
-      </div>
-    );
-
-    return (
-      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
-        <ColumnHeader
-          icon='address-book-o'
-          iconComponent={PeopleIcon}
-          title={intl.formatMessage(messages.title)}
-          onPin={this.handlePin}
-          onMove={this.handleMove}
-          onClick={this.handleHeaderClick}
-          pinned={pinned}
-          multiColumn={multiColumn}
-        />
-
-        {multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
-
-        <Helmet>
-          <title>{intl.formatMessage(messages.title)}</title>
-          <meta name='robots' content='noindex' />
-        </Helmet>
-      </Column>
-    );
-  }
-
-}
-
-export default connect(mapStateToProps)(injectIntl(Directory));
diff --git a/app/javascript/mastodon/features/directory/index.tsx b/app/javascript/mastodon/features/directory/index.tsx
new file mode 100644
index 000000000..482c6858f
--- /dev/null
+++ b/app/javascript/mastodon/features/directory/index.tsx
@@ -0,0 +1,217 @@
+import type { ChangeEventHandler } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import { List as ImmutableList } from 'immutable';
+
+import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
+import {
+  addColumn,
+  removeColumn,
+  moveColumn,
+  changeColumnParams,
+} from 'mastodon/actions/columns';
+import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
+import Column from 'mastodon/components/column';
+import ColumnHeader from 'mastodon/components/column_header';
+import { LoadMore } from 'mastodon/components/load_more';
+import { LoadingIndicator } from 'mastodon/components/loading_indicator';
+import { RadioButton } from 'mastodon/components/radio_button';
+import ScrollContainer from 'mastodon/containers/scroll_container';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+import { AccountCard } from './components/account_card';
+
+const messages = defineMessages({
+  title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
+  recentlyActive: {
+    id: 'directory.recently_active',
+    defaultMessage: 'Recently active',
+  },
+  newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
+  local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
+  federated: {
+    id: 'directory.federated',
+    defaultMessage: 'From known fediverse',
+  },
+});
+
+export const Directory: React.FC<{
+  columnId?: string;
+  multiColumn?: boolean;
+  params?: { order: string; local?: boolean };
+}> = ({ columnId, multiColumn, params }) => {
+  const intl = useIntl();
+  const dispatch = useAppDispatch();
+
+  const [state, setState] = useState<{
+    order: string | null;
+    local: boolean | null;
+  }>({
+    order: null,
+    local: null,
+  });
+
+  const column = useRef<Column>(null);
+
+  const order = state.order ?? params?.order ?? 'active';
+  const local = state.local ?? params?.local ?? false;
+
+  const handlePin = useCallback(() => {
+    if (columnId) {
+      dispatch(removeColumn(columnId));
+    } else {
+      dispatch(addColumn('DIRECTORY', { order, local }));
+    }
+  }, [dispatch, columnId, order, local]);
+
+  const domain = useAppSelector((s) => s.meta.get('domain') as string);
+  const accountIds = useAppSelector(
+    (state) =>
+      state.user_lists.getIn(
+        ['directory', 'items'],
+        ImmutableList(),
+      ) as ImmutableList<string>,
+  );
+  const isLoading = useAppSelector(
+    (state) =>
+      state.user_lists.getIn(['directory', 'isLoading'], true) as boolean,
+  );
+
+  useEffect(() => {
+    void dispatch(fetchDirectory({ order, local }));
+  }, [dispatch, order, local]);
+
+  const handleMove = useCallback(
+    (dir: string) => {
+      dispatch(moveColumn(columnId, dir));
+    },
+    [dispatch, columnId],
+  );
+
+  const handleHeaderClick = useCallback(() => {
+    column.current?.scrollTop();
+  }, []);
+
+  const handleChangeOrder = useCallback<ChangeEventHandler<HTMLInputElement>>(
+    (e) => {
+      if (columnId) {
+        dispatch(changeColumnParams(columnId, ['order'], e.target.value));
+      } else {
+        setState((s) => ({ order: e.target.value, local: s.local }));
+      }
+    },
+    [dispatch, columnId],
+  );
+
+  const handleChangeLocal = useCallback<ChangeEventHandler<HTMLInputElement>>(
+    (e) => {
+      if (columnId) {
+        dispatch(
+          changeColumnParams(columnId, ['local'], e.target.value === '1'),
+        );
+      } else {
+        setState((s) => ({ local: e.target.value === '1', order: s.order }));
+      }
+    },
+    [dispatch, columnId],
+  );
+
+  const handleLoadMore = useCallback(() => {
+    void dispatch(expandDirectory({ order, local }));
+  }, [dispatch, order, local]);
+
+  const pinned = !!columnId;
+
+  const scrollableArea = (
+    <div className='scrollable'>
+      <div className='filter-form'>
+        <div className='filter-form__column' role='group'>
+          <RadioButton
+            name='order'
+            value='active'
+            label={intl.formatMessage(messages.recentlyActive)}
+            checked={order === 'active'}
+            onChange={handleChangeOrder}
+          />
+          <RadioButton
+            name='order'
+            value='new'
+            label={intl.formatMessage(messages.newArrivals)}
+            checked={order === 'new'}
+            onChange={handleChangeOrder}
+          />
+        </div>
+
+        <div className='filter-form__column' role='group'>
+          <RadioButton
+            name='local'
+            value='1'
+            label={intl.formatMessage(messages.local, { domain })}
+            checked={local}
+            onChange={handleChangeLocal}
+          />
+          <RadioButton
+            name='local'
+            value='0'
+            label={intl.formatMessage(messages.federated)}
+            checked={!local}
+            onChange={handleChangeLocal}
+          />
+        </div>
+      </div>
+
+      <div className='directory__list'>
+        {isLoading ? (
+          <LoadingIndicator />
+        ) : (
+          accountIds.map((accountId) => (
+            <AccountCard accountId={accountId} key={accountId} />
+          ))
+        )}
+      </div>
+
+      <LoadMore onClick={handleLoadMore} visible={!isLoading} />
+    </div>
+  );
+
+  return (
+    <Column
+      bindToDocument={!multiColumn}
+      ref={column}
+      label={intl.formatMessage(messages.title)}
+    >
+      <ColumnHeader
+        // @ts-expect-error ColumnHeader is not properly typed yet
+        icon='address-book-o'
+        iconComponent={PeopleIcon}
+        title={intl.formatMessage(messages.title)}
+        onPin={handlePin}
+        onMove={handleMove}
+        onClick={handleHeaderClick}
+        pinned={pinned}
+        multiColumn={multiColumn}
+      />
+
+      {multiColumn && !pinned ? (
+        // @ts-expect-error ScrollContainer is not properly typed yet
+        <ScrollContainer scrollKey='directory'>
+          {scrollableArea}
+        </ScrollContainer>
+      ) : (
+        scrollableArea
+      )}
+
+      <Helmet>
+        <title>{intl.formatMessage(messages.title)}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export -- Needed because this is called as an async components
+export default Directory;
diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js
index 2f17fed5f..7a4c04c5c 100644
--- a/app/javascript/mastodon/reducers/user_lists.js
+++ b/app/javascript/mastodon/reducers/user_lists.js
@@ -1,12 +1,8 @@
 import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 
 import {
-  DIRECTORY_FETCH_REQUEST,
-  DIRECTORY_FETCH_SUCCESS,
-  DIRECTORY_FETCH_FAIL,
-  DIRECTORY_EXPAND_REQUEST,
-  DIRECTORY_EXPAND_SUCCESS,
-  DIRECTORY_EXPAND_FAIL,
+  expandDirectory,
+  fetchDirectory
 } from 'mastodon/actions/directory';
 import {
   FEATURED_TAGS_FETCH_REQUEST,
@@ -117,6 +113,7 @@ const normalizeFeaturedTags = (state, path, featuredTags, accountId) => {
   }));
 };
 
+/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
 export default function userLists(state = initialState, action) {
   switch(action.type) {
   case FOLLOWERS_FETCH_SUCCESS:
@@ -194,16 +191,6 @@ export default function userLists(state = initialState, action) {
   case MUTES_FETCH_FAIL:
   case MUTES_EXPAND_FAIL:
     return state.setIn(['mutes', 'isLoading'], false);
-  case DIRECTORY_FETCH_SUCCESS:
-    return normalizeList(state, ['directory'], action.accounts, action.next);
-  case DIRECTORY_EXPAND_SUCCESS:
-    return appendToList(state, ['directory'], action.accounts, action.next);
-  case DIRECTORY_FETCH_REQUEST:
-  case DIRECTORY_EXPAND_REQUEST:
-    return state.setIn(['directory', 'isLoading'], true);
-  case DIRECTORY_FETCH_FAIL:
-  case DIRECTORY_EXPAND_FAIL:
-    return state.setIn(['directory', 'isLoading'], false);
   case FEATURED_TAGS_FETCH_SUCCESS:
     return normalizeFeaturedTags(state, ['featured_tags', action.id], action.tags, action.id);
   case FEATURED_TAGS_FETCH_REQUEST:
@@ -211,6 +198,17 @@ export default function userLists(state = initialState, action) {
   case FEATURED_TAGS_FETCH_FAIL:
     return state.setIn(['featured_tags', action.id, 'isLoading'], false);
   default:
-    return state;
+    if(fetchDirectory.fulfilled.match(action))
+      return normalizeList(state, ['directory'], action.payload.accounts, undefined);
+    else if( expandDirectory.fulfilled.match(action))
+      return appendToList(state, ['directory'], action.payload.accounts, undefined);
+    else if(fetchDirectory.pending.match(action) ||
+     expandDirectory.pending.match(action))
+      return state.setIn(['directory', 'isLoading'], true);
+    else if(fetchDirectory.rejected.match(action) ||
+     expandDirectory.rejected.match(action))
+      return state.setIn(['directory', 'isLoading'], false);
+    else
+      return state;
   }
 }

From e89317d4c1da991b728b6d4a21671ed33f057cc4 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Wed, 26 Jun 2024 21:33:38 +0200
Subject: [PATCH 26/84] Add hover cards in web UI (#30754)

Co-authored-by: Renaud Chaput <renchap@gmail.com>
---
 app/javascript/hooks/useLinks.ts              |  61 ++++++
 app/javascript/hooks/useTimeout.ts            |  29 +++
 .../mastodon/components/account_bio.tsx       |  20 ++
 .../mastodon/components/account_fields.tsx    |  42 +++++
 .../mastodon/components/follow_button.tsx     |  93 +++++++++
 .../components/hover_card_account.tsx         |  74 ++++++++
 .../components/hover_card_controller.tsx      | 117 ++++++++++++
 app/javascript/mastodon/components/status.jsx |   6 +-
 .../mastodon/components/status_content.jsx    |   3 +-
 .../explore/components/author_link.jsx        |   2 +-
 .../features/explore/components/card.jsx      |  17 +-
 .../components/inline_follow_suggestions.jsx  |  15 +-
 .../notifications/components/notification.jsx |   4 +-
 .../status/components/detailed_status.jsx     |   2 +-
 app/javascript/mastodon/features/ui/index.jsx |   2 +
 app/javascript/mastodon/locales/en.json       |   6 +-
 .../styles/mastodon-light/variables.scss      |   2 +
 .../styles/mastodon/components.scss           | 178 +++++++++++++++++-
 18 files changed, 631 insertions(+), 42 deletions(-)
 create mode 100644 app/javascript/hooks/useLinks.ts
 create mode 100644 app/javascript/hooks/useTimeout.ts
 create mode 100644 app/javascript/mastodon/components/account_bio.tsx
 create mode 100644 app/javascript/mastodon/components/account_fields.tsx
 create mode 100644 app/javascript/mastodon/components/follow_button.tsx
 create mode 100644 app/javascript/mastodon/components/hover_card_account.tsx
 create mode 100644 app/javascript/mastodon/components/hover_card_controller.tsx

diff --git a/app/javascript/hooks/useLinks.ts b/app/javascript/hooks/useLinks.ts
new file mode 100644
index 000000000..f08b9500d
--- /dev/null
+++ b/app/javascript/hooks/useLinks.ts
@@ -0,0 +1,61 @@
+import { useCallback } from 'react';
+
+import { useHistory } from 'react-router-dom';
+
+import { openURL } from 'mastodon/actions/search';
+import { useAppDispatch } from 'mastodon/store';
+
+const isMentionClick = (element: HTMLAnchorElement) =>
+  element.classList.contains('mention');
+
+const isHashtagClick = (element: HTMLAnchorElement) =>
+  element.textContent?.[0] === '#' ||
+  element.previousSibling?.textContent?.endsWith('#');
+
+export const useLinks = () => {
+  const history = useHistory();
+  const dispatch = useAppDispatch();
+
+  const handleHashtagClick = useCallback(
+    (element: HTMLAnchorElement) => {
+      const { textContent } = element;
+
+      if (!textContent) return;
+
+      history.push(`/tags/${textContent.replace(/^#/, '')}`);
+    },
+    [history],
+  );
+
+  const handleMentionClick = useCallback(
+    (element: HTMLAnchorElement) => {
+      dispatch(
+        openURL(element.href, history, () => {
+          window.location.href = element.href;
+        }),
+      );
+    },
+    [dispatch, history],
+  );
+
+  const handleClick = useCallback(
+    (e: React.MouseEvent) => {
+      const target = (e.target as HTMLElement).closest('a');
+
+      if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) {
+        return;
+      }
+
+      if (isMentionClick(target)) {
+        e.preventDefault();
+        handleMentionClick(target);
+      } else if (isHashtagClick(target)) {
+        e.preventDefault();
+        handleHashtagClick(target);
+      }
+    },
+    [handleMentionClick, handleHashtagClick],
+  );
+
+  return handleClick;
+};
diff --git a/app/javascript/hooks/useTimeout.ts b/app/javascript/hooks/useTimeout.ts
new file mode 100644
index 000000000..f1814ae8e
--- /dev/null
+++ b/app/javascript/hooks/useTimeout.ts
@@ -0,0 +1,29 @@
+import { useRef, useCallback, useEffect } from 'react';
+
+export const useTimeout = () => {
+  const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
+
+  const set = useCallback((callback: () => void, delay: number) => {
+    if (timeoutRef.current) {
+      clearTimeout(timeoutRef.current);
+    }
+
+    timeoutRef.current = setTimeout(callback, delay);
+  }, []);
+
+  const cancel = useCallback(() => {
+    if (timeoutRef.current) {
+      clearTimeout(timeoutRef.current);
+      timeoutRef.current = undefined;
+    }
+  }, []);
+
+  useEffect(
+    () => () => {
+      cancel();
+    },
+    [cancel],
+  );
+
+  return [set, cancel] as const;
+};
diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx
new file mode 100644
index 000000000..9d523c740
--- /dev/null
+++ b/app/javascript/mastodon/components/account_bio.tsx
@@ -0,0 +1,20 @@
+import { useLinks } from 'mastodon/../hooks/useLinks';
+
+export const AccountBio: React.FC<{
+  note: string;
+  className: string;
+}> = ({ note, className }) => {
+  const handleClick = useLinks();
+
+  if (note.length === 0 || note === '<p></p>') {
+    return null;
+  }
+
+  return (
+    <div
+      className={`${className} translate`}
+      dangerouslySetInnerHTML={{ __html: note }}
+      onClickCapture={handleClick}
+    />
+  );
+};
diff --git a/app/javascript/mastodon/components/account_fields.tsx b/app/javascript/mastodon/components/account_fields.tsx
new file mode 100644
index 000000000..e297f99e3
--- /dev/null
+++ b/app/javascript/mastodon/components/account_fields.tsx
@@ -0,0 +1,42 @@
+import classNames from 'classnames';
+
+import CheckIcon from '@/material-icons/400-24px/check.svg?react';
+import { useLinks } from 'mastodon/../hooks/useLinks';
+import { Icon } from 'mastodon/components/icon';
+import type { Account } from 'mastodon/models/account';
+
+export const AccountFields: React.FC<{
+  fields: Account['fields'];
+  limit: number;
+}> = ({ fields, limit = -1 }) => {
+  const handleClick = useLinks();
+
+  if (fields.size === 0) {
+    return null;
+  }
+
+  return (
+    <div className='account-fields' onClickCapture={handleClick}>
+      {fields.take(limit).map((pair, i) => (
+        <dl
+          key={i}
+          className={classNames({ verified: pair.get('verified_at') })}
+        >
+          <dt
+            dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }}
+            className='translate'
+          />
+
+          <dd className='translate' title={pair.get('value_plain') ?? ''}>
+            {pair.get('verified_at') && (
+              <Icon id='check' icon={CheckIcon} className='verified__mark' />
+            )}
+            <span
+              dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }}
+            />
+          </dd>
+        </dl>
+      ))}
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx
new file mode 100644
index 000000000..4b4d27831
--- /dev/null
+++ b/app/javascript/mastodon/components/follow_button.tsx
@@ -0,0 +1,93 @@
+import { useCallback, useEffect } from 'react';
+
+import { useIntl, defineMessages } from 'react-intl';
+
+import {
+  fetchRelationships,
+  followAccount,
+  unfollowAccount,
+} from 'mastodon/actions/accounts';
+import { Button } from 'mastodon/components/button';
+import { LoadingIndicator } from 'mastodon/components/loading_indicator';
+import { me } from 'mastodon/initial_state';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+const messages = defineMessages({
+  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
+  follow: { id: 'account.follow', defaultMessage: 'Follow' },
+  followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
+  mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
+  cancel_follow_request: {
+    id: 'account.cancel_follow_request',
+    defaultMessage: 'Withdraw follow request',
+  },
+  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+});
+
+export const FollowButton: React.FC<{
+  accountId: string;
+}> = ({ accountId }) => {
+  const intl = useIntl();
+  const dispatch = useAppDispatch();
+  const relationship = useAppSelector((state) =>
+    state.relationships.get(accountId),
+  );
+  const following = relationship?.following || relationship?.requested;
+
+  useEffect(() => {
+    dispatch(fetchRelationships([accountId]));
+  }, [dispatch, accountId]);
+
+  const handleClick = useCallback(() => {
+    if (!relationship) return;
+    if (accountId === me) {
+      return;
+    } else if (relationship.following || relationship.requested) {
+      dispatch(unfollowAccount(accountId));
+    } else {
+      dispatch(followAccount(accountId));
+    }
+  }, [dispatch, accountId, relationship]);
+
+  let label;
+
+  if (accountId === me) {
+    label = intl.formatMessage(messages.edit_profile);
+  } else if (!relationship) {
+    label = <LoadingIndicator />;
+  } else if (relationship.requested) {
+    label = intl.formatMessage(messages.cancel_follow_request);
+  } else if (relationship.following && relationship.followed_by) {
+    label = intl.formatMessage(messages.mutual);
+  } else if (!relationship.following && relationship.followed_by) {
+    label = intl.formatMessage(messages.followBack);
+  } else if (relationship.following) {
+    label = intl.formatMessage(messages.unfollow);
+  } else {
+    label = intl.formatMessage(messages.follow);
+  }
+
+  if (accountId === me) {
+    return (
+      <a
+        href='/settings/profile'
+        target='_blank'
+        rel='noreferrer noopener'
+        className='button button-secondary'
+      >
+        {label}
+      </a>
+    );
+  }
+
+  return (
+    <Button
+      onClick={handleClick}
+      disabled={relationship?.blocked_by || relationship?.blocking}
+      secondary={following}
+      className={following ? 'button--destructive' : undefined}
+    >
+      {label}
+    </Button>
+  );
+};
diff --git a/app/javascript/mastodon/components/hover_card_account.tsx b/app/javascript/mastodon/components/hover_card_account.tsx
new file mode 100644
index 000000000..59f957783
--- /dev/null
+++ b/app/javascript/mastodon/components/hover_card_account.tsx
@@ -0,0 +1,74 @@
+import { useEffect, forwardRef } from 'react';
+
+import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
+import { fetchAccount } from 'mastodon/actions/accounts';
+import { AccountBio } from 'mastodon/components/account_bio';
+import { AccountFields } from 'mastodon/components/account_fields';
+import { Avatar } from 'mastodon/components/avatar';
+import { FollowersCounter } from 'mastodon/components/counters';
+import { DisplayName } from 'mastodon/components/display_name';
+import { FollowButton } from 'mastodon/components/follow_button';
+import { LoadingIndicator } from 'mastodon/components/loading_indicator';
+import { ShortNumber } from 'mastodon/components/short_number';
+import { domain } from 'mastodon/initial_state';
+import { useAppSelector, useAppDispatch } from 'mastodon/store';
+
+export const HoverCardAccount = forwardRef<
+  HTMLDivElement,
+  { accountId: string }
+>(({ accountId }, ref) => {
+  const dispatch = useAppDispatch();
+
+  const account = useAppSelector((state) =>
+    accountId ? state.accounts.get(accountId) : undefined,
+  );
+
+  useEffect(() => {
+    if (accountId && !account) {
+      dispatch(fetchAccount(accountId));
+    }
+  }, [dispatch, accountId, account]);
+
+  return (
+    <div
+      ref={ref}
+      id='hover-card'
+      role='tooltip'
+      className={classNames('hover-card dropdown-animation', {
+        'hover-card--loading': !account,
+      })}
+    >
+      {account ? (
+        <>
+          <Link to={`/@${account.acct}`} className='hover-card__name'>
+            <Avatar account={account} size={46} />
+            <DisplayName account={account} localDomain={domain} />
+          </Link>
+
+          <div className='hover-card__text-row'>
+            <AccountBio
+              note={account.note_emojified}
+              className='hover-card__bio'
+            />
+            <AccountFields fields={account.fields} limit={2} />
+          </div>
+
+          <div className='hover-card__number'>
+            <ShortNumber
+              value={account.followers_count}
+              renderer={FollowersCounter}
+            />
+          </div>
+
+          <FollowButton accountId={accountId} />
+        </>
+      ) : (
+        <LoadingIndicator />
+      )}
+    </div>
+  );
+});
+
+HoverCardAccount.displayName = 'HoverCardAccount';
diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx
new file mode 100644
index 000000000..0130390ef
--- /dev/null
+++ b/app/javascript/mastodon/components/hover_card_controller.tsx
@@ -0,0 +1,117 @@
+import { useEffect, useRef, useState, useCallback } from 'react';
+
+import { useLocation } from 'react-router-dom';
+
+import Overlay from 'react-overlays/Overlay';
+import type {
+  OffsetValue,
+  UsePopperOptions,
+} from 'react-overlays/esm/usePopper';
+
+import { useTimeout } from 'mastodon/../hooks/useTimeout';
+import { HoverCardAccount } from 'mastodon/components/hover_card_account';
+
+const offset = [-12, 4] as OffsetValue;
+const enterDelay = 650;
+const leaveDelay = 250;
+const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
+
+const isHoverCardAnchor = (element: HTMLElement) =>
+  element.matches('[data-hover-card-account]');
+
+export const HoverCardController: React.FC = () => {
+  const [open, setOpen] = useState(false);
+  const [accountId, setAccountId] = useState<string | undefined>();
+  const [anchor, setAnchor] = useState<HTMLElement | null>(null);
+  const cardRef = useRef<HTMLDivElement>(null);
+  const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
+  const [setEnterTimeout, cancelEnterTimeout] = useTimeout();
+  const location = useLocation();
+
+  const handleAnchorMouseEnter = useCallback(
+    (e: MouseEvent) => {
+      const { target } = e;
+
+      if (target instanceof HTMLElement && isHoverCardAnchor(target)) {
+        cancelLeaveTimeout();
+
+        setEnterTimeout(() => {
+          target.setAttribute('aria-describedby', 'hover-card');
+          setAnchor(target);
+          setOpen(true);
+          setAccountId(
+            target.getAttribute('data-hover-card-account') ?? undefined,
+          );
+        }, enterDelay);
+      }
+
+      if (target === cardRef.current?.parentNode) {
+        cancelLeaveTimeout();
+      }
+    },
+    [cancelLeaveTimeout, setEnterTimeout, setOpen, setAccountId, setAnchor],
+  );
+
+  const handleAnchorMouseLeave = useCallback(
+    (e: MouseEvent) => {
+      if (e.target === anchor || e.target === cardRef.current?.parentNode) {
+        cancelEnterTimeout();
+
+        setLeaveTimeout(() => {
+          anchor?.removeAttribute('aria-describedby');
+          setOpen(false);
+          setAnchor(null);
+        }, leaveDelay);
+      }
+    },
+    [cancelEnterTimeout, setLeaveTimeout, setOpen, setAnchor, anchor],
+  );
+
+  const handleClose = useCallback(() => {
+    cancelEnterTimeout();
+    cancelLeaveTimeout();
+    setOpen(false);
+    setAnchor(null);
+  }, [cancelEnterTimeout, cancelLeaveTimeout, setOpen, setAnchor]);
+
+  useEffect(() => {
+    handleClose();
+  }, [handleClose, location]);
+
+  useEffect(() => {
+    document.body.addEventListener('mouseenter', handleAnchorMouseEnter, {
+      passive: true,
+      capture: true,
+    });
+    document.body.addEventListener('mouseleave', handleAnchorMouseLeave, {
+      passive: true,
+      capture: true,
+    });
+
+    return () => {
+      document.body.removeEventListener('mouseenter', handleAnchorMouseEnter);
+      document.body.removeEventListener('mouseleave', handleAnchorMouseLeave);
+    };
+  }, [handleAnchorMouseEnter, handleAnchorMouseLeave]);
+
+  if (!accountId) return null;
+
+  return (
+    <Overlay
+      rootClose
+      onHide={handleClose}
+      show={open}
+      target={anchor}
+      placement='bottom-start'
+      flip
+      offset={offset}
+      popperConfig={popperConfig}
+    >
+      {({ props }) => (
+        <div {...props} className='hover-card-controller'>
+          <HoverCardAccount accountId={accountId} ref={cardRef} />
+        </div>
+      )}
+    </Overlay>
+  );
+};
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index 7b97e4576..dce48d703 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -425,7 +425,7 @@ class Status extends ImmutablePureComponent {
       prepend = (
         <div className='status__prepend'>
           <div className='status__prepend-icon-wrapper'><Icon id='retweet' icon={RepeatIcon} className='status__prepend-icon' /></div>
-          <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
+          <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
         </div>
       );
 
@@ -446,7 +446,7 @@ class Status extends ImmutablePureComponent {
       prepend = (
         <div className='status__prepend'>
           <div className='status__prepend-icon-wrapper'><Icon id='reply' icon={ReplyIcon} className='status__prepend-icon' /></div>
-          <FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
+          <FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
         </div>
       );
     }
@@ -562,7 +562,7 @@ class Status extends ImmutablePureComponent {
                 <RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
               </a>
 
-              <a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
+              <a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} data-hover-card-account={status.getIn(['account', 'id'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
                 <div className='status__avatar'>
                   {statusAvatar}
                 </div>
diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx
index 24483cf51..82135b85c 100644
--- a/app/javascript/mastodon/components/status_content.jsx
+++ b/app/javascript/mastodon/components/status_content.jsx
@@ -116,8 +116,9 @@ class StatusContent extends PureComponent {
 
       if (mention) {
         link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
-        link.setAttribute('title', `@${mention.get('acct')}`);
+        link.removeAttribute('title');
         link.setAttribute('href', `/@${mention.get('acct')}`);
+        link.setAttribute('data-hover-card-account', mention.get('id'));
       } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
         link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
         link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
diff --git a/app/javascript/mastodon/features/explore/components/author_link.jsx b/app/javascript/mastodon/features/explore/components/author_link.jsx
index b9dec3367..8dd9b0dab 100644
--- a/app/javascript/mastodon/features/explore/components/author_link.jsx
+++ b/app/javascript/mastodon/features/explore/components/author_link.jsx
@@ -9,7 +9,7 @@ export const AuthorLink = ({ accountId }) => {
   const account = useAppSelector(state => state.getIn(['accounts', accountId]));
 
   return (
-    <Link to={`/@${account.get('acct')}`} className='story__details__shared__author-link'>
+    <Link to={`/@${account.get('acct')}`} className='story__details__shared__author-link' data-hover-card-account={accountId}>
       <Avatar account={account} size={16} />
       <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} />
     </Link>
diff --git a/app/javascript/mastodon/features/explore/components/card.jsx b/app/javascript/mastodon/features/explore/components/card.jsx
index 316203060..190864851 100644
--- a/app/javascript/mastodon/features/explore/components/card.jsx
+++ b/app/javascript/mastodon/features/explore/components/card.jsx
@@ -8,34 +8,21 @@ import { Link } from 'react-router-dom';
 import { useDispatch, useSelector } from 'react-redux';
 
 import CloseIcon from '@/material-icons/400-24px/close.svg?react';
-import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
 import { dismissSuggestion } from 'mastodon/actions/suggestions';
 import { Avatar } from 'mastodon/components/avatar';
-import { Button } from 'mastodon/components/button';
 import { DisplayName } from 'mastodon/components/display_name';
+import { FollowButton } from 'mastodon/components/follow_button';
 import { IconButton } from 'mastodon/components/icon_button';
 import { domain } from 'mastodon/initial_state';
 
 const messages = defineMessages({
-  follow: { id: 'account.follow', defaultMessage: 'Follow' },
-  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
   dismiss: { id: 'follow_suggestions.dismiss', defaultMessage: "Don't show again" },
 });
 
 export const Card = ({ id, source }) => {
   const intl = useIntl();
   const account = useSelector(state => state.getIn(['accounts', id]));
-  const relationship = useSelector(state => state.getIn(['relationships', id]));
   const dispatch = useDispatch();
-  const following = relationship?.get('following') ?? relationship?.get('requested');
-
-  const handleFollow = useCallback(() => {
-    if (following) {
-      dispatch(unfollowAccount(id));
-    } else {
-      dispatch(followAccount(id));
-    }
-  }, [id, following, dispatch]);
 
   const handleDismiss = useCallback(() => {
     dispatch(dismissSuggestion(id));
@@ -74,7 +61,7 @@ export const Card = ({ id, source }) => {
           <div className='explore__suggestions__card__body__main__name-button'>
             <Link className='explore__suggestions__card__body__main__name-button__name' to={`/@${account.get('acct')}`}><DisplayName account={account} /></Link>
             <IconButton iconComponent={CloseIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
-            <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} secondary={following} onClick={handleFollow} />
+            <FollowButton accountId={account.get('id')} />
           </div>
         </div>
       </div>
diff --git a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx
index c39b43bad..1b8040e55 100644
--- a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx
+++ b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx
@@ -12,12 +12,11 @@ import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
 import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
 import CloseIcon from '@/material-icons/400-24px/close.svg?react';
 import InfoIcon from '@/material-icons/400-24px/info.svg?react';
-import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
 import { changeSetting } from 'mastodon/actions/settings';
 import { fetchSuggestions, dismissSuggestion } from 'mastodon/actions/suggestions';
 import { Avatar } from 'mastodon/components/avatar';
-import { Button } from 'mastodon/components/button';
 import { DisplayName } from 'mastodon/components/display_name';
+import { FollowButton } from 'mastodon/components/follow_button';
 import { Icon } from 'mastodon/components/icon';
 import { IconButton } from 'mastodon/components/icon_button';
 import { VerifiedBadge } from 'mastodon/components/verified_badge';
@@ -79,18 +78,8 @@ Source.propTypes = {
 const Card = ({ id, sources }) => {
   const intl = useIntl();
   const account = useSelector(state => state.getIn(['accounts', id]));
-  const relationship = useSelector(state => state.getIn(['relationships', id]));
   const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
   const dispatch = useDispatch();
-  const following = relationship?.get('following') ?? relationship?.get('requested');
-
-  const handleFollow = useCallback(() => {
-    if (following) {
-      dispatch(unfollowAccount(id));
-    } else {
-      dispatch(followAccount(id));
-    }
-  }, [id, following, dispatch]);
 
   const handleDismiss = useCallback(() => {
     dispatch(dismissSuggestion(id));
@@ -109,7 +98,7 @@ const Card = ({ id, sources }) => {
         {firstVerifiedField ? <VerifiedBadge link={firstVerifiedField.get('value')} /> : <Source id={sources.get(0)} />}
       </div>
 
-      <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} secondary={following} onClick={handleFollow} />
+      <FollowButton accountId={id} />
     </div>
   );
 };
diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx
index 69084c211..272893042 100644
--- a/app/javascript/mastodon/features/notifications/components/notification.jsx
+++ b/app/javascript/mastodon/features/notifications/components/notification.jsx
@@ -435,7 +435,7 @@ class Notification extends ImmutablePureComponent {
 
     const targetAccount = report.get('target_account');
     const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') };
-    const targetLink = <bdi><Link className='notification__display-name' title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
+    const targetLink = <bdi><Link className='notification__display-name' data-hover-card-account={targetAccount.get('id')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
 
     return (
       <HotKeys handlers={this.getHandlers()}>
@@ -458,7 +458,7 @@ class Notification extends ImmutablePureComponent {
     const { notification } = this.props;
     const account          = notification.get('account');
     const displayNameHtml  = { __html: account.get('display_name_html') };
-    const link             = <bdi><Link className='notification__display-name' href={`/@${account.get('acct')}`} title={account.get('acct')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
+    const link             = <bdi><Link className='notification__display-name' href={`/@${account.get('acct')}`} data-hover-card-account={account.get('id')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
 
     switch(notification.get('type')) {
     case 'follow':
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx
index 8843619bc..bc81fd2df 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.jsx
+++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx
@@ -272,7 +272,7 @@ class DetailedStatus extends ImmutablePureComponent {
               <FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
             </div>
           )}
-          <a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='detailed-status__display-name'>
+          <a href={`/@${status.getIn(['account', 'acct'])}`} data-hover-card-account={status.getIn(['account', 'id'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
             <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
             <DisplayName account={status.get('account')} localDomain={this.props.domain} />
           </a>
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 7742f6486..b58e191ed 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -14,6 +14,7 @@ import { HotKeys } from 'react-hotkeys';
 import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
 import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
 import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
+import { HoverCardController } from 'mastodon/components/hover_card_controller';
 import { PictureInPicture } from 'mastodon/features/picture_in_picture';
 import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
 import { layoutFromWindow } from 'mastodon/is_mobile';
@@ -585,6 +586,7 @@ class UI extends PureComponent {
 
           {layout !== 'mobile' && <PictureInPicture />}
           <NotificationsContainer />
+          <HoverCardController />
           <LoadingBarContainer className='loading-bar' />
           <ModalContainer />
           <UploadArea active={draggingOver} onClose={this.closeUploadModal} />
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index f0c27ad70..13296e1d2 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -35,9 +35,9 @@
   "account.follow_back": "Follow back",
   "account.followers": "Followers",
   "account.followers.empty": "No one follows this user yet.",
-  "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
+  "account.followers_counter": "{count, plural, one {{counter} follower} other {{counter} followers}}",
   "account.following": "Following",
-  "account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
+  "account.following_counter": "{count, plural, one {{counter} following} other {{counter} following}}",
   "account.follows.empty": "This user doesn't follow anyone yet.",
   "account.go_to_profile": "Go to profile",
   "account.hide_reblogs": "Hide boosts from @{name}",
@@ -63,7 +63,7 @@
   "account.requested_follow": "{name} has requested to follow you",
   "account.share": "Share @{name}'s profile",
   "account.show_reblogs": "Show boosts from @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Posts}}",
+  "account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} posts}}",
   "account.unblock": "Unblock @{name}",
   "account.unblock_domain": "Unblock domain {domain}",
   "account.unblock_short": "Unblock",
diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss
index 3cdbd9bf6..9f571b3f2 100644
--- a/app/javascript/styles/mastodon-light/variables.scss
+++ b/app/javascript/styles/mastodon-light/variables.scss
@@ -59,6 +59,8 @@ $emojis-requiring-inversion: 'chains';
 body {
   --dropdown-border-color: #d9e1e8;
   --dropdown-background-color: #fff;
+  --modal-border-color: #d9e1e8;
+  --modal-background-color: var(--background-color-tint);
   --background-border-color: #d9e1e8;
   --background-color: #fff;
   --background-color-tint: rgba(255, 255, 255, 80%);
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 73d0e6220..cbf9314ff 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -120,8 +120,27 @@
       text-decoration: none;
     }
 
-    &:disabled {
-      opacity: 0.5;
+    &.button--destructive {
+      &:active,
+      &:focus,
+      &:hover {
+        border-color: $ui-button-destructive-focus-background-color;
+        color: $ui-button-destructive-focus-background-color;
+      }
+    }
+
+    &:disabled,
+    &.disabled {
+      opacity: 0.7;
+      border-color: $ui-primary-color;
+      color: $ui-primary-color;
+
+      &:active,
+      &:focus,
+      &:hover {
+        border-color: $ui-primary-color;
+        color: $ui-primary-color;
+      }
     }
   }
 
@@ -2420,7 +2439,7 @@ a.account__display-name {
 }
 
 .dropdown-animation {
-  animation: dropdown 150ms cubic-bezier(0.1, 0.7, 0.1, 1);
+  animation: dropdown 250ms cubic-bezier(0.1, 0.7, 0.1, 1);
 
   @keyframes dropdown {
     from {
@@ -10325,3 +10344,156 @@ noscript {
     }
   }
 }
+
+.hover-card-controller[data-popper-reference-hidden='true'] {
+  opacity: 0;
+  pointer-events: none;
+}
+
+.hover-card {
+  box-shadow: var(--dropdown-shadow);
+  background: var(--modal-background-color);
+  backdrop-filter: var(--background-filter);
+  border: 1px solid var(--modal-border-color);
+  border-radius: 8px;
+  padding: 16px;
+  width: 270px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+
+  &--loading {
+    position: relative;
+    min-height: 100px;
+  }
+
+  &__name {
+    display: flex;
+    gap: 12px;
+    text-decoration: none;
+    color: inherit;
+  }
+
+  &__number {
+    font-size: 15px;
+    line-height: 22px;
+    color: $secondary-text-color;
+
+    strong {
+      font-weight: 700;
+    }
+  }
+
+  &__text-row {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+  }
+
+  &__bio {
+    color: $secondary-text-color;
+    font-size: 14px;
+    line-height: 20px;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    max-height: 2 * 20px;
+    overflow: hidden;
+
+    p {
+      margin-bottom: 0;
+    }
+
+    a {
+      color: inherit;
+      text-decoration: underline;
+
+      &:hover,
+      &:focus,
+      &:active {
+        text-decoration: none;
+      }
+    }
+  }
+
+  .display-name {
+    font-size: 15px;
+    line-height: 22px;
+
+    bdi {
+      font-weight: 500;
+      color: $primary-text-color;
+    }
+
+    &__account {
+      display: block;
+      color: $dark-text-color;
+    }
+  }
+
+  .account-fields {
+    color: $secondary-text-color;
+    font-size: 14px;
+    line-height: 20px;
+
+    a {
+      color: inherit;
+      text-decoration: none;
+
+      &:focus,
+      &:hover,
+      &:active {
+        text-decoration: underline;
+      }
+    }
+
+    dl {
+      display: flex;
+      align-items: center;
+      gap: 4px;
+
+      dt {
+        flex: 0 0 auto;
+        color: $dark-text-color;
+        min-width: 0;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+
+      dd {
+        flex: 1 1 auto;
+        font-weight: 500;
+        min-width: 0;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+
+      &.verified {
+        dd {
+          display: flex;
+          align-items: center;
+          gap: 4px;
+          overflow: hidden;
+          white-space: nowrap;
+          color: $valid-value-color;
+
+          & > span {
+            overflow: hidden;
+            text-overflow: ellipsis;
+          }
+
+          a {
+            font-weight: 500;
+          }
+
+          .icon {
+            width: 16px;
+            height: 16px;
+          }
+        }
+      }
+    }
+  }
+}

From 3939352e92f4be13b773ee243bbb6ad54d6b5bd1 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Wed, 26 Jun 2024 21:46:28 +0200
Subject: [PATCH 27/84] Convert `<ColumnHeader>` to Typescript (#30849)

---
 .../mastodon/components/column_header.jsx     | 233 --------------
 .../mastodon/components/column_header.tsx     | 301 ++++++++++++++++++
 .../mastodon/features/directory/index.tsx     |   5 +-
 .../features/ui/components/column_loading.tsx |  11 +-
 4 files changed, 307 insertions(+), 243 deletions(-)
 delete mode 100644 app/javascript/mastodon/components/column_header.jsx
 create mode 100644 app/javascript/mastodon/components/column_header.tsx

diff --git a/app/javascript/mastodon/components/column_header.jsx b/app/javascript/mastodon/components/column_header.jsx
deleted file mode 100644
index 42183f336..000000000
--- a/app/javascript/mastodon/components/column_header.jsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent, useCallback } from 'react';
-
-import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl';
-
-import classNames from 'classnames';
-import { withRouter } from 'react-router-dom';
-
-import AddIcon from '@/material-icons/400-24px/add.svg?react';
-import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
-import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
-import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
-import CloseIcon from '@/material-icons/400-24px/close.svg?react';
-import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
-import { Icon }  from 'mastodon/components/icon';
-import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
-import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
-import { WithRouterPropTypes } from 'mastodon/utils/react_router';
-
-
-import { useAppHistory } from './router';
-
-const messages = defineMessages({
-  show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
-  hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
-  moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
-  moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
-  back: { id: 'column_back_button.label', defaultMessage: 'Back' },
-});
-
-const BackButton = ({ onlyIcon }) => {
-  const history = useAppHistory();
-  const intl = useIntl();
-
-  const handleBackClick = useCallback(() => {
-    if (history.location?.state?.fromMastodon) {
-      history.goBack();
-    } else {
-      history.push('/');
-    }
-  }, [history]);
-
-  return (
-    <button onClick={handleBackClick} className={classNames('column-header__back-button', { 'compact': onlyIcon })} aria-label={intl.formatMessage(messages.back)}>
-      <Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
-      {!onlyIcon && <FormattedMessage id='column_back_button.label' defaultMessage='Back' />}
-    </button>
-  );
-};
-
-BackButton.propTypes = {
-  onlyIcon: PropTypes.bool,
-};
-
-class ColumnHeader extends PureComponent {
-  static propTypes = {
-    identity: identityContextPropShape,
-    intl: PropTypes.object.isRequired,
-    title: PropTypes.node,
-    icon: PropTypes.string,
-    iconComponent: PropTypes.func,
-    active: PropTypes.bool,
-    multiColumn: PropTypes.bool,
-    extraButton: PropTypes.node,
-    showBackButton: PropTypes.bool,
-    children: PropTypes.node,
-    pinned: PropTypes.bool,
-    placeholder: PropTypes.bool,
-    onPin: PropTypes.func,
-    onMove: PropTypes.func,
-    onClick: PropTypes.func,
-    appendContent: PropTypes.node,
-    collapseIssues: PropTypes.bool,
-    ...WithRouterPropTypes,
-  };
-
-  state = {
-    collapsed: true,
-    animating: false,
-  };
-
-  handleToggleClick = (e) => {
-    e.stopPropagation();
-    this.setState({ collapsed: !this.state.collapsed, animating: true });
-  };
-
-  handleTitleClick = () => {
-    this.props.onClick?.();
-  };
-
-  handleMoveLeft = () => {
-    this.props.onMove(-1);
-  };
-
-  handleMoveRight = () => {
-    this.props.onMove(1);
-  };
-
-  handleTransitionEnd = () => {
-    this.setState({ animating: false });
-  };
-
-  handlePin = () => {
-    if (!this.props.pinned) {
-      this.props.history.replace('/');
-    }
-
-    this.props.onPin();
-  };
-
-  render () {
-    const { title, icon, iconComponent, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props;
-    const { collapsed, animating } = this.state;
-
-    const wrapperClassName = classNames('column-header__wrapper', {
-      'active': active,
-    });
-
-    const buttonClassName = classNames('column-header', {
-      'active': active,
-    });
-
-    const collapsibleClassName = classNames('column-header__collapsible', {
-      'collapsed': collapsed,
-      'animating': animating,
-    });
-
-    const collapsibleButtonClassName = classNames('column-header__button', {
-      'active': !collapsed,
-    });
-
-    let extraContent, pinButton, moveButtons, backButton, collapseButton;
-
-    if (children) {
-      extraContent = (
-        <div key='extra-content' className='column-header__collapsible__extra'>
-          {children}
-        </div>
-      );
-    }
-
-    if (multiColumn && pinned) {
-      pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
-
-      moveButtons = (
-        <div className='column-header__setting-arrows'>
-          <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
-          <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
-        </div>
-      );
-    } else if (multiColumn && this.props.onPin) {
-      pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
-    }
-
-    if (history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) {
-      backButton = <BackButton onlyIcon={!!title} />;
-    }
-
-    const collapsedContent = [
-      extraContent,
-    ];
-
-    if (multiColumn) {
-      collapsedContent.push(
-        <div key='buttons' className='column-header__advanced-buttons'>
-          {pinButton}
-          {moveButtons}
-        </div>
-      );
-    }
-
-    if (this.props.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
-      collapseButton = (
-        <button
-          className={collapsibleButtonClassName}
-          title={formatMessage(collapsed ? messages.show : messages.hide)}
-          aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
-          onClick={this.handleToggleClick}
-        >
-          <i className='icon-with-badge'>
-            <Icon id='sliders' icon={SettingsIcon} />
-            {collapseIssues && <i className='icon-with-badge__issue-badge' />}
-          </i>
-        </button>
-      );
-    }
-
-    const hasTitle = (icon || iconComponent) && title;
-
-    const component = (
-      <div className={wrapperClassName}>
-        <h1 className={buttonClassName}>
-          {hasTitle && (
-            <>
-              {backButton}
-
-              <button onClick={this.handleTitleClick} className='column-header__title'>
-                {!backButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
-                {title}
-              </button>
-            </>
-          )}
-
-          {!hasTitle && backButton}
-
-          <div className='column-header__buttons'>
-            {extraButton}
-            {collapseButton}
-          </div>
-        </h1>
-
-        <div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
-          <div className='column-header__collapsible-inner'>
-            {(!collapsed || animating) && collapsedContent}
-          </div>
-        </div>
-
-        {appendContent}
-      </div>
-    );
-
-    if (placeholder) {
-      return component;
-    } else {
-      return (<ButtonInTabsBar>
-        {component}
-      </ButtonInTabsBar>);
-    }
-  }
-
-}
-
-export default injectIntl(withIdentity(withRouter(ColumnHeader)));
diff --git a/app/javascript/mastodon/components/column_header.tsx b/app/javascript/mastodon/components/column_header.tsx
new file mode 100644
index 000000000..ec946cab3
--- /dev/null
+++ b/app/javascript/mastodon/components/column_header.tsx
@@ -0,0 +1,301 @@
+import { useCallback, useState } from 'react';
+
+import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import AddIcon from '@/material-icons/400-24px/add.svg?react';
+import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
+import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
+import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
+import CloseIcon from '@/material-icons/400-24px/close.svg?react';
+import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
+import type { IconProp } from 'mastodon/components/icon';
+import { Icon } from 'mastodon/components/icon';
+import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
+import { useIdentity } from 'mastodon/identity_context';
+
+import { useAppHistory } from './router';
+
+const messages = defineMessages({
+  show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
+  hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
+  moveLeft: {
+    id: 'column_header.moveLeft_settings',
+    defaultMessage: 'Move column to the left',
+  },
+  moveRight: {
+    id: 'column_header.moveRight_settings',
+    defaultMessage: 'Move column to the right',
+  },
+  back: { id: 'column_back_button.label', defaultMessage: 'Back' },
+});
+
+const BackButton: React.FC<{
+  onlyIcon: boolean;
+}> = ({ onlyIcon }) => {
+  const history = useAppHistory();
+  const intl = useIntl();
+
+  const handleBackClick = useCallback(() => {
+    if (history.location.state?.fromMastodon) {
+      history.goBack();
+    } else {
+      history.push('/');
+    }
+  }, [history]);
+
+  return (
+    <button
+      onClick={handleBackClick}
+      className={classNames('column-header__back-button', {
+        compact: onlyIcon,
+      })}
+      aria-label={intl.formatMessage(messages.back)}
+    >
+      <Icon
+        id='chevron-left'
+        icon={ArrowBackIcon}
+        className='column-back-button__icon'
+      />
+      {!onlyIcon && (
+        <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
+      )}
+    </button>
+  );
+};
+
+export interface Props {
+  title?: string;
+  icon?: string;
+  iconComponent?: IconProp;
+  active?: boolean;
+  children?: React.ReactNode;
+  pinned?: boolean;
+  multiColumn?: boolean;
+  extraButton?: React.ReactNode;
+  showBackButton?: boolean;
+  placeholder?: boolean;
+  appendContent?: React.ReactNode;
+  collapseIssues?: boolean;
+  onClick?: () => void;
+  onMove?: (arg0: number) => void;
+  onPin?: () => void;
+}
+
+export const ColumnHeader: React.FC<Props> = ({
+  title,
+  icon,
+  iconComponent,
+  active,
+  children,
+  pinned,
+  multiColumn,
+  extraButton,
+  showBackButton,
+  placeholder,
+  appendContent,
+  collapseIssues,
+  onClick,
+  onMove,
+  onPin,
+}) => {
+  const intl = useIntl();
+  const { signedIn } = useIdentity();
+  const history = useAppHistory();
+  const [collapsed, setCollapsed] = useState(true);
+  const [animating, setAnimating] = useState(false);
+
+  const handleToggleClick = useCallback(
+    (e: React.MouseEvent) => {
+      e.stopPropagation();
+      setCollapsed((value) => !value);
+      setAnimating(true);
+    },
+    [setCollapsed, setAnimating],
+  );
+
+  const handleTitleClick = useCallback(() => {
+    onClick?.();
+  }, [onClick]);
+
+  const handleMoveLeft = useCallback(() => {
+    onMove?.(-1);
+  }, [onMove]);
+
+  const handleMoveRight = useCallback(() => {
+    onMove?.(1);
+  }, [onMove]);
+
+  const handleTransitionEnd = useCallback(() => {
+    setAnimating(false);
+  }, [setAnimating]);
+
+  const handlePin = useCallback(() => {
+    if (!pinned) {
+      history.replace('/');
+    }
+
+    onPin?.();
+  }, [history, pinned, onPin]);
+
+  const wrapperClassName = classNames('column-header__wrapper', {
+    active,
+  });
+
+  const buttonClassName = classNames('column-header', {
+    active,
+  });
+
+  const collapsibleClassName = classNames('column-header__collapsible', {
+    collapsed,
+    animating,
+  });
+
+  const collapsibleButtonClassName = classNames('column-header__button', {
+    active: !collapsed,
+  });
+
+  let extraContent, pinButton, moveButtons, backButton, collapseButton;
+
+  if (children) {
+    extraContent = (
+      <div key='extra-content' className='column-header__collapsible__extra'>
+        {children}
+      </div>
+    );
+  }
+
+  if (multiColumn && pinned) {
+    pinButton = (
+      <button
+        className='text-btn column-header__setting-btn'
+        onClick={handlePin}
+      >
+        <Icon id='times' icon={CloseIcon} />{' '}
+        <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' />
+      </button>
+    );
+
+    moveButtons = (
+      <div className='column-header__setting-arrows'>
+        <button
+          title={intl.formatMessage(messages.moveLeft)}
+          aria-label={intl.formatMessage(messages.moveLeft)}
+          className='icon-button column-header__setting-btn'
+          onClick={handleMoveLeft}
+        >
+          <Icon id='chevron-left' icon={ChevronLeftIcon} />
+        </button>
+        <button
+          title={intl.formatMessage(messages.moveRight)}
+          aria-label={intl.formatMessage(messages.moveRight)}
+          className='icon-button column-header__setting-btn'
+          onClick={handleMoveRight}
+        >
+          <Icon id='chevron-right' icon={ChevronRightIcon} />
+        </button>
+      </div>
+    );
+  } else if (multiColumn && onPin) {
+    pinButton = (
+      <button
+        className='text-btn column-header__setting-btn'
+        onClick={handlePin}
+      >
+        <Icon id='plus' icon={AddIcon} />{' '}
+        <FormattedMessage id='column_header.pin' defaultMessage='Pin' />
+      </button>
+    );
+  }
+
+  if (
+    !pinned &&
+    ((multiColumn && history.location.state?.fromMastodon) || showBackButton)
+  ) {
+    backButton = <BackButton onlyIcon={!!title} />;
+  }
+
+  const collapsedContent = [extraContent];
+
+  if (multiColumn) {
+    collapsedContent.push(
+      <div key='buttons' className='column-header__advanced-buttons'>
+        {pinButton}
+        {moveButtons}
+      </div>,
+    );
+  }
+
+  if (signedIn && (children || (multiColumn && onPin))) {
+    collapseButton = (
+      <button
+        className={collapsibleButtonClassName}
+        title={intl.formatMessage(collapsed ? messages.show : messages.hide)}
+        aria-label={intl.formatMessage(
+          collapsed ? messages.show : messages.hide,
+        )}
+        onClick={handleToggleClick}
+      >
+        <i className='icon-with-badge'>
+          <Icon id='sliders' icon={SettingsIcon} />
+          {collapseIssues && <i className='icon-with-badge__issue-badge' />}
+        </i>
+      </button>
+    );
+  }
+
+  const hasIcon = icon && iconComponent;
+  const hasTitle = hasIcon && title;
+
+  const component = (
+    <div className={wrapperClassName}>
+      <h1 className={buttonClassName}>
+        {hasTitle && (
+          <>
+            {backButton}
+
+            <button onClick={handleTitleClick} className='column-header__title'>
+              {!backButton && (
+                <Icon
+                  id={icon}
+                  icon={iconComponent}
+                  className='column-header__icon'
+                />
+              )}
+              {title}
+            </button>
+          </>
+        )}
+
+        {!hasTitle && backButton}
+
+        <div className='column-header__buttons'>
+          {extraButton}
+          {collapseButton}
+        </div>
+      </h1>
+
+      <div
+        className={collapsibleClassName}
+        tabIndex={collapsed ? -1 : undefined}
+        onTransitionEnd={handleTransitionEnd}
+      >
+        <div className='column-header__collapsible-inner'>
+          {(!collapsed || animating) && collapsedContent}
+        </div>
+      </div>
+
+      {appendContent}
+    </div>
+  );
+
+  if (placeholder) {
+    return component;
+  } else {
+    return <ButtonInTabsBar>{component}</ButtonInTabsBar>;
+  }
+};
+
+// eslint-disable-next-line import/no-default-export
+export default ColumnHeader;
diff --git a/app/javascript/mastodon/features/directory/index.tsx b/app/javascript/mastodon/features/directory/index.tsx
index 482c6858f..51d283a48 100644
--- a/app/javascript/mastodon/features/directory/index.tsx
+++ b/app/javascript/mastodon/features/directory/index.tsx
@@ -16,7 +16,7 @@ import {
 } from 'mastodon/actions/columns';
 import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
 import Column from 'mastodon/components/column';
-import ColumnHeader from 'mastodon/components/column_header';
+import { ColumnHeader } from 'mastodon/components/column_header';
 import { LoadMore } from 'mastodon/components/load_more';
 import { LoadingIndicator } from 'mastodon/components/loading_indicator';
 import { RadioButton } from 'mastodon/components/radio_button';
@@ -86,7 +86,7 @@ export const Directory: React.FC<{
   }, [dispatch, order, local]);
 
   const handleMove = useCallback(
-    (dir: string) => {
+    (dir: number) => {
       dispatch(moveColumn(columnId, dir));
     },
     [dispatch, columnId],
@@ -185,7 +185,6 @@ export const Directory: React.FC<{
       label={intl.formatMessage(messages.title)}
     >
       <ColumnHeader
-        // @ts-expect-error ColumnHeader is not properly typed yet
         icon='address-book-o'
         iconComponent={PeopleIcon}
         title={intl.formatMessage(messages.title)}
diff --git a/app/javascript/mastodon/features/ui/components/column_loading.tsx b/app/javascript/mastodon/features/ui/components/column_loading.tsx
index 42174838c..d9563dda7 100644
--- a/app/javascript/mastodon/features/ui/components/column_loading.tsx
+++ b/app/javascript/mastodon/features/ui/components/column_loading.tsx
@@ -1,11 +1,8 @@
-import Column from '../../../components/column';
-import ColumnHeader from '../../../components/column_header';
+import Column from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import type { Props as ColumnHeaderProps } from 'mastodon/components/column_header';
 
-interface Props {
-  multiColumn?: boolean;
-}
-
-export const ColumnLoading: React.FC<Props> = (otherProps) => (
+export const ColumnLoading: React.FC<ColumnHeaderProps> = (otherProps) => (
   <Column>
     <ColumnHeader {...otherProps} />
     <div className='scrollable' />

From ad53b0ab65cd9eba6d3078a3980c467428e79371 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 27 Jun 2024 03:16:59 -0400
Subject: [PATCH 28/84] Rely on built-in ruby private IP detection (#30848)

---
 Gemfile                          |  2 --
 Gemfile.lock                     |  2 --
 app/lib/private_address_check.rb | 33 ++++++++++++++++++++++++++++++++
 3 files changed, 33 insertions(+), 4 deletions(-)
 create mode 100644 app/lib/private_address_check.rb

diff --git a/Gemfile b/Gemfile
index f2d7d098d..aa3ca8f79 100644
--- a/Gemfile
+++ b/Gemfile
@@ -100,8 +100,6 @@ gem 'json-ld'
 gem 'json-ld-preloaded', '~> 3.2'
 gem 'rdf-normalize', '~> 0.5'
 
-gem 'private_address_check', '~> 0.5'
-
 gem 'opentelemetry-api', '~> 1.2.5'
 
 group :opentelemetry do
diff --git a/Gemfile.lock b/Gemfile.lock
index e8b54ed56..3b1b4b112 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -595,7 +595,6 @@ GEM
       actionmailer (>= 3)
       net-smtp
       premailer (~> 1.7, >= 1.7.9)
-    private_address_check (0.5.0)
     propshaft (0.9.0)
       actionpack (>= 7.0.0)
       activesupport (>= 7.0.0)
@@ -994,7 +993,6 @@ DEPENDENCIES
   pg (~> 1.5)
   pghero
   premailer-rails
-  private_address_check (~> 0.5)
   propshaft
   public_suffix (~> 6.0)
   puma (~> 6.3)
diff --git a/app/lib/private_address_check.rb b/app/lib/private_address_check.rb
new file mode 100644
index 000000000..d00b16e66
--- /dev/null
+++ b/app/lib/private_address_check.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module PrivateAddressCheck
+  module_function
+
+  CIDR_LIST = [
+    IPAddr.new('0.0.0.0/8'),       # Current network (only valid as source address)
+    IPAddr.new('100.64.0.0/10'),   # Shared Address Space
+    IPAddr.new('172.16.0.0/12'),   # Private network
+    IPAddr.new('192.0.0.0/24'),    # IETF Protocol Assignments
+    IPAddr.new('192.0.2.0/24'),    # TEST-NET-1, documentation and examples
+    IPAddr.new('192.88.99.0/24'),  # IPv6 to IPv4 relay (includes 2002::/16)
+    IPAddr.new('198.18.0.0/15'),   # Network benchmark tests
+    IPAddr.new('198.51.100.0/24'), # TEST-NET-2, documentation and examples
+    IPAddr.new('203.0.113.0/24'),  # TEST-NET-3, documentation and examples
+    IPAddr.new('224.0.0.0/4'),     # IP multicast (former Class D network)
+    IPAddr.new('240.0.0.0/4'),     # Reserved (former Class E network)
+    IPAddr.new('255.255.255.255'), # Broadcast
+    IPAddr.new('64:ff9b::/96'),    # IPv4/IPv6 translation (RFC 6052)
+    IPAddr.new('100::/64'),        # Discard prefix (RFC 6666)
+    IPAddr.new('2001::/32'),       # Teredo tunneling
+    IPAddr.new('2001:10::/28'),    # Deprecated (previously ORCHID)
+    IPAddr.new('2001:20::/28'),    # ORCHIDv2
+    IPAddr.new('2001:db8::/32'),   # Addresses used in documentation and example source code
+    IPAddr.new('2002::/16'),       # 6to4
+    IPAddr.new('fc00::/7'),        # Unique local address
+    IPAddr.new('ff00::/8'),        # Multicast
+  ].freeze
+
+  def private_address?(address)
+    address.private? || address.loopback? || address.link_local? || CIDR_LIST.any? { |cidr| cidr.include?(address) }
+  end
+end

From f6390c3326b016e1a52057573839a5608ba317ea Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 27 Jun 2024 03:42:57 -0400
Subject: [PATCH 29/84] Use flatware to parallelize CI specs (#30284)

---
 .github/workflows/test-ruby.yml |  8 +++++---
 Gemfile                         |  3 +++
 Gemfile.lock                    | 10 ++++++++++
 bin/flatware                    | 27 +++++++++++++++++++++++++++
 spec/flatware_helper.rb         | 12 ++++++++++++
 5 files changed, 57 insertions(+), 3 deletions(-)
 create mode 100755 bin/flatware
 create mode 100644 spec/flatware_helper.rb

diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml
index dd71fd253..ef898968d 100644
--- a/.github/workflows/test-ruby.yml
+++ b/.github/workflows/test-ruby.yml
@@ -132,15 +132,17 @@ jobs:
           additional-system-dependencies: ffmpeg libpam-dev
 
       - name: Load database schema
-        run: './bin/rails db:create db:schema:load db:seed'
+        run: |
+          bin/rails db:setup
+          bin/flatware fan bin/rails db:test:prepare
 
-      - run: bin/rspec
+      - run: bin/flatware rspec -r ./spec/flatware_helper.rb
 
       - name: Upload coverage reports to Codecov
         if: matrix.ruby-version == '.ruby-version'
         uses: codecov/codecov-action@v4
         with:
-          files: coverage/lcov/mastodon.lcov
+          files: coverage/lcov/*.lcov
         env:
           CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
 
diff --git a/Gemfile b/Gemfile
index aa3ca8f79..be3f9e6f9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -121,6 +121,9 @@ group :opentelemetry do
 end
 
 group :test do
+  # Enable usage of all available CPUs/cores during spec runs
+  gem 'flatware-rspec'
+
   # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
   gem 'rspec-github', '~> 2.4', require: false
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 3b1b4b112..dd112d018 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -264,6 +264,11 @@ GEM
     ffi-compiler (1.3.2)
       ffi (>= 1.15.5)
       rake
+    flatware (2.3.2)
+      thor (< 2.0)
+    flatware-rspec (2.3.2)
+      flatware (= 2.3.2)
+      rspec (>= 3.6)
     fog-core (2.4.0)
       builder
       excon (~> 0.71)
@@ -700,6 +705,10 @@ GEM
       chunky_png (~> 1.0)
       rqrcode_core (~> 1.0)
     rqrcode_core (1.2.0)
+    rspec (3.13.0)
+      rspec-core (~> 3.13.0)
+      rspec-expectations (~> 3.13.0)
+      rspec-mocks (~> 3.13.0)
     rspec-core (3.13.0)
       rspec-support (~> 3.13.0)
     rspec-expectations (3.13.1)
@@ -932,6 +941,7 @@ DEPENDENCIES
   faker (~> 3.2)
   fast_blank (~> 1.0)
   fastimage
+  flatware-rspec
   fog-core (<= 2.4.0)
   fog-openstack (~> 1.0)
   fuubar (~> 2.5)
diff --git a/bin/flatware b/bin/flatware
new file mode 100755
index 000000000..337ce9277
--- /dev/null
+++ b/bin/flatware
@@ -0,0 +1,27 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'flatware' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+bundle_binstub = File.expand_path("bundle", __dir__)
+
+if File.file?(bundle_binstub)
+  if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
+    load(bundle_binstub)
+  else
+    abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
+Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
+  end
+end
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("flatware", "flatware")
diff --git a/spec/flatware_helper.rb b/spec/flatware_helper.rb
new file mode 100644
index 000000000..57a7c1f56
--- /dev/null
+++ b/spec/flatware_helper.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+if defined?(Flatware)
+  Flatware.configure do |config|
+    config.after_fork do |test_env_number|
+      unless ENV.fetch('DISABLE_SIMPLECOV', nil) == 'true'
+        require 'simplecov'
+        SimpleCov.at_fork.call(test_env_number) # Combines parallel coverage results
+      end
+    end
+  end
+end

From 6d1c1fd684d0f20691cba552b209b15d0107dfe4 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 27 Jun 2024 10:00:04 +0200
Subject: [PATCH 30/84] New Crowdin Translations (automated) (#30851)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/af.json      | 1 -
 app/javascript/mastodon/locales/an.json      | 3 ---
 app/javascript/mastodon/locales/ar.json      | 3 ---
 app/javascript/mastodon/locales/ast.json     | 2 --
 app/javascript/mastodon/locales/be.json      | 3 ---
 app/javascript/mastodon/locales/bg.json      | 3 ---
 app/javascript/mastodon/locales/bn.json      | 3 ---
 app/javascript/mastodon/locales/br.json      | 3 ---
 app/javascript/mastodon/locales/ca.json      | 6 +++---
 app/javascript/mastodon/locales/ckb.json     | 3 ---
 app/javascript/mastodon/locales/co.json      | 3 ---
 app/javascript/mastodon/locales/cs.json      | 3 ---
 app/javascript/mastodon/locales/cy.json      | 3 ---
 app/javascript/mastodon/locales/da.json      | 4 +---
 app/javascript/mastodon/locales/el.json      | 3 ---
 app/javascript/mastodon/locales/en-GB.json   | 3 ---
 app/javascript/mastodon/locales/eo.json      | 3 ---
 app/javascript/mastodon/locales/es-AR.json   | 2 +-
 app/javascript/mastodon/locales/es-MX.json   | 6 +++---
 app/javascript/mastodon/locales/es.json      | 6 +++---
 app/javascript/mastodon/locales/et.json      | 3 ---
 app/javascript/mastodon/locales/eu.json      | 3 ---
 app/javascript/mastodon/locales/fa.json      | 3 ---
 app/javascript/mastodon/locales/fi.json      | 2 +-
 app/javascript/mastodon/locales/fo.json      | 3 ---
 app/javascript/mastodon/locales/fr-CA.json   | 3 ---
 app/javascript/mastodon/locales/fr.json      | 3 ---
 app/javascript/mastodon/locales/fy.json      | 3 ---
 app/javascript/mastodon/locales/ga.json      | 3 ---
 app/javascript/mastodon/locales/gd.json      | 3 ---
 app/javascript/mastodon/locales/gl.json      | 3 ---
 app/javascript/mastodon/locales/he.json      | 3 ---
 app/javascript/mastodon/locales/hi.json      | 3 ---
 app/javascript/mastodon/locales/hr.json      | 3 ---
 app/javascript/mastodon/locales/hu.json      | 3 ---
 app/javascript/mastodon/locales/hy.json      | 3 ---
 app/javascript/mastodon/locales/ia.json      | 1 +
 app/javascript/mastodon/locales/id.json      | 3 ---
 app/javascript/mastodon/locales/ie.json      | 3 ---
 app/javascript/mastodon/locales/io.json      | 3 ---
 app/javascript/mastodon/locales/is.json      | 2 +-
 app/javascript/mastodon/locales/it.json      | 6 +++---
 app/javascript/mastodon/locales/ja.json      | 3 ---
 app/javascript/mastodon/locales/ka.json      | 1 -
 app/javascript/mastodon/locales/kab.json     | 3 ---
 app/javascript/mastodon/locales/kk.json      | 3 ---
 app/javascript/mastodon/locales/kn.json      | 1 -
 app/javascript/mastodon/locales/ko.json      | 3 ---
 app/javascript/mastodon/locales/ku.json      | 3 ---
 app/javascript/mastodon/locales/kw.json      | 3 ---
 app/javascript/mastodon/locales/la.json      | 3 ---
 app/javascript/mastodon/locales/lad.json     | 3 ---
 app/javascript/mastodon/locales/lt.json      | 3 ---
 app/javascript/mastodon/locales/lv.json      | 3 ---
 app/javascript/mastodon/locales/mk.json      | 1 -
 app/javascript/mastodon/locales/ml.json      | 3 ---
 app/javascript/mastodon/locales/mr.json      | 3 ---
 app/javascript/mastodon/locales/ms.json      | 3 ---
 app/javascript/mastodon/locales/my.json      | 3 ---
 app/javascript/mastodon/locales/ne.json      | 1 -
 app/javascript/mastodon/locales/nn.json      | 3 ---
 app/javascript/mastodon/locales/no.json      | 3 ---
 app/javascript/mastodon/locales/oc.json      | 3 ---
 app/javascript/mastodon/locales/pa.json      | 3 ---
 app/javascript/mastodon/locales/pl.json      | 3 ---
 app/javascript/mastodon/locales/pt-BR.json   | 3 ---
 app/javascript/mastodon/locales/pt-PT.json   | 3 ---
 app/javascript/mastodon/locales/ro.json      | 3 ---
 app/javascript/mastodon/locales/ru.json      | 3 ---
 app/javascript/mastodon/locales/sa.json      | 3 ---
 app/javascript/mastodon/locales/sc.json      | 3 ---
 app/javascript/mastodon/locales/sco.json     | 3 ---
 app/javascript/mastodon/locales/si.json      | 3 ---
 app/javascript/mastodon/locales/sk.json      | 3 ---
 app/javascript/mastodon/locales/sl.json      | 6 +++---
 app/javascript/mastodon/locales/sq.json      | 3 ---
 app/javascript/mastodon/locales/sr-Latn.json | 6 +++---
 app/javascript/mastodon/locales/sr.json      | 6 +++---
 app/javascript/mastodon/locales/sv.json      | 3 ---
 app/javascript/mastodon/locales/szl.json     | 1 -
 app/javascript/mastodon/locales/ta.json      | 3 ---
 app/javascript/mastodon/locales/tai.json     | 1 -
 app/javascript/mastodon/locales/te.json      | 1 -
 app/javascript/mastodon/locales/th.json      | 3 ---
 app/javascript/mastodon/locales/tr.json      | 6 +++---
 app/javascript/mastodon/locales/tt.json      | 3 ---
 app/javascript/mastodon/locales/ug.json      | 1 -
 app/javascript/mastodon/locales/uk.json      | 3 ---
 app/javascript/mastodon/locales/ur.json      | 3 ---
 app/javascript/mastodon/locales/uz.json      | 3 ---
 app/javascript/mastodon/locales/vi.json      | 3 ---
 app/javascript/mastodon/locales/zgh.json     | 1 -
 app/javascript/mastodon/locales/zh-CN.json   | 7 ++++---
 app/javascript/mastodon/locales/zh-HK.json   | 3 ---
 app/javascript/mastodon/locales/zh-TW.json   | 6 +++---
 config/locales/fi.yml                        | 2 +-
 config/locales/sr-Latn.yml                   | 2 +-
 config/locales/sr.yml                        | 2 +-
 config/locales/zh-TW.yml                     | 2 +-
 99 files changed, 40 insertions(+), 259 deletions(-)

diff --git a/app/javascript/mastodon/locales/af.json b/app/javascript/mastodon/locales/af.json
index e4f7f12b0..77e15eb2c 100644
--- a/app/javascript/mastodon/locales/af.json
+++ b/app/javascript/mastodon/locales/af.json
@@ -50,7 +50,6 @@
   "account.requested_follow": "{name} het versoek om jou te volg",
   "account.share": "Deel @{name} se profiel",
   "account.show_reblogs": "Wys aangestuurde plasings van @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Plaas} other {{counter} Plasings}}",
   "account.unblock": "Deblokkeer @{name}",
   "account.unblock_domain": "Deblokkeer domein {domain}",
   "account.unblock_short": "Deblokkeer",
diff --git a/app/javascript/mastodon/locales/an.json b/app/javascript/mastodon/locales/an.json
index af5f8426d..752b6c356 100644
--- a/app/javascript/mastodon/locales/an.json
+++ b/app/javascript/mastodon/locales/an.json
@@ -31,9 +31,7 @@
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
   "account.followers.empty": "Encara no sigue dengún a este usuario.",
-  "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidores}}",
   "account.following": "Seguindo",
-  "account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Seguindo}}",
   "account.follows.empty": "Este usuario encara no sigue a dengún.",
   "account.go_to_profile": "Ir ta lo perfil",
   "account.hide_reblogs": "Amagar retutz de @{name}",
@@ -54,7 +52,6 @@
   "account.requested_follow": "{name} ha demandau seguir-te",
   "account.share": "Compartir lo perfil de @{name}",
   "account.show_reblogs": "Amostrar retutz de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Publicación} other {{counter} Publicaciones}}",
   "account.unblock": "Desblocar a @{name}",
   "account.unblock_domain": "Amostrar a {domain}",
   "account.unblock_short": "Desblocar",
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index b5ce0ae86..a9e1bdb27 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -35,9 +35,7 @@
   "account.follow_back": "رد المتابعة",
   "account.followers": "مُتابِعون",
   "account.followers.empty": "لا أحدَ يُتابع هذا المُستخدم إلى حد الآن.",
-  "account.followers_counter": "{count, plural, zero{لا مُتابع} one {مُتابعٌ واحِد} two {مُتابعانِ اِثنان} few {{counter} مُتابِعين} many {{counter} مُتابِعًا} other {{counter} مُتابع}}",
   "account.following": "الاشتراكات",
-  "account.following_counter": "{count, plural, zero{لا يُتابِع أحدًا} one {يُتابِعُ واحد} two{يُتابِعُ اِثنان} few{يُتابِعُ {counter}} many{يُتابِعُ {counter}} other {يُتابِعُ {counter}}}",
   "account.follows.empty": "لا يُتابع هذا المُستخدمُ أيَّ أحدٍ حتى الآن.",
   "account.go_to_profile": "اذهب إلى الملف الشخصي",
   "account.hide_reblogs": "إخفاء المعاد نشرها مِن @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "لقد طلب {name} متابعتك",
   "account.share": "شارِك الملف التعريفي لـ @{name}",
   "account.show_reblogs": "اعرض إعادات نشر @{name}",
-  "account.statuses_counter": "{count, plural, zero {لَا منشورات} one {منشور واحد} two {منشوران إثنان} few {{counter} منشورات} many {{counter} منشورًا} other {{counter} منشور}}",
   "account.unblock": "إلغاء الحَظر عن @{name}",
   "account.unblock_domain": "إلغاء الحَظر عن النِّطاق {domain}",
   "account.unblock_short": "ألغ الحجب",
diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json
index 80e0aa6cb..3f32a8bf1 100644
--- a/app/javascript/mastodon/locales/ast.json
+++ b/app/javascript/mastodon/locales/ast.json
@@ -32,7 +32,6 @@
   "account.followers": "Siguidores",
   "account.followers.empty": "Naide sigue a esti perfil.",
   "account.following": "Siguiendo",
-  "account.following_counter": "{count, plural,one {Sigue a {counter}} other {Sigue a {counter}}}",
   "account.follows.empty": "Esti perfil nun sigue a naide.",
   "account.go_to_profile": "Dir al perfil",
   "account.hide_reblogs": "Anubrir los artículos compartíos de @{name}",
@@ -49,7 +48,6 @@
   "account.report": "Informar de: @{name}",
   "account.requested_follow": "{name} solicitó siguite",
   "account.show_reblogs": "Amosar los artículos compartíos de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} artículu} other {{counter} artículos}}",
   "account.unblock": "Desbloquiar a @{name}",
   "account.unblock_domain": "Desbloquiar el dominiu «{domain}»",
   "account.unblock_short": "Desbloquiar",
diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json
index 03164c429..643725270 100644
--- a/app/javascript/mastodon/locales/be.json
+++ b/app/javascript/mastodon/locales/be.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Падпісацца ў адказ",
   "account.followers": "Падпісчыкі",
   "account.followers.empty": "Ніхто пакуль не падпісаны на гэтага карыстальніка.",
-  "account.followers_counter": "{count, plural, one {{counter} падпісчык} few {{counter} падпісчыкі} many {{counter} падпісчыкаў} other {{counter} падпісчыка}}",
   "account.following": "Падпіскі",
-  "account.following_counter": "{count, plural, one {{counter} падпіска} few {{counter} падпіскі} many {{counter} падпісак} other {{counter} падпіскі}}",
   "account.follows.empty": "Карыстальнік ні на каго не падпісаны.",
   "account.go_to_profile": "Перайсці да профілю",
   "account.hide_reblogs": "Схаваць пашырэнні ад @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} адправіў запыт на падпіску",
   "account.share": "Абагуліць профіль @{name}",
   "account.show_reblogs": "Паказаць падштурхоўванні ад @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} допіс} few {{counter} допісы} many {{counter} допісаў} other {{counter} допісу}}",
   "account.unblock": "Разблакіраваць @{name}",
   "account.unblock_domain": "Разблакіраваць дамен {domain}",
   "account.unblock_short": "Разблакіраваць",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 98e84c45d..323890ba2 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Последване взаимно",
   "account.followers": "Последователи",
   "account.followers.empty": "Още никой не следва потребителя.",
-  "account.followers_counter": "{count, plural, one {{counter} последовател} other {{counter} последователи}}",
   "account.following": "Последвано",
-  "account.following_counter": "{count, plural, one {{counter} последван} other {{counter} последвани}}",
   "account.follows.empty": "Потребителят още никого не следва.",
   "account.go_to_profile": "Към профила",
   "account.hide_reblogs": "Скриване на подсилвания от @{name}",
@@ -63,7 +61,6 @@
   "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": "Отблокиране",
diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json
index 4c4138bcf..a203c43f0 100644
--- a/app/javascript/mastodon/locales/bn.json
+++ b/app/javascript/mastodon/locales/bn.json
@@ -33,9 +33,7 @@
   "account.follow": "অনুসরণ",
   "account.followers": "অনুসরণকারী",
   "account.followers.empty": "এই ব্যক্তিকে এখনো কেউ অনুসরণ করে না.",
-  "account.followers_counter": "{count, plural,one {{counter} জন অনুসরণকারী } other {{counter} জন অনুসরণকারী}}",
   "account.following": "অনুসরণ করা হচ্ছে",
-  "account.following_counter": "{count, plural,one {{counter} জনকে অনুসরণ} other {{counter} জনকে অনুসরণ}}",
   "account.follows.empty": "এই সদস্য কাউকে এখনো ফলো করেন না.",
   "account.go_to_profile": "প্রোফাইলে যান",
   "account.hide_reblogs": "@{name}'র সমর্থনগুলি লুকিয়ে ফেলুন",
@@ -60,7 +58,6 @@
   "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": "আনব্লক করুন",
diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json
index 7cd49ba59..c919d2e9d 100644
--- a/app/javascript/mastodon/locales/br.json
+++ b/app/javascript/mastodon/locales/br.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Heuliañ d'ho tro",
   "account.followers": "Tud koumanantet",
   "account.followers.empty": "Den na heul an implijer·ez-mañ c'hoazh.",
-  "account.followers_counter": "{count, plural, other{{counter} Heulier·ez}}",
   "account.following": "Koumanantoù",
-  "account.following_counter": "{count, plural, one{{counter} C'houmanant} two{{counter} Goumanant} other {{counter} a Goumanant}}",
   "account.follows.empty": "An implijer·ez-mañ na heul den ebet.",
   "account.go_to_profile": "Gwelet ar profil",
   "account.hide_reblogs": "Kuzh skignadennoù gant @{name}",
@@ -62,7 +60,6 @@
   "account.requested_follow": "Gant {name} eo bet goulennet ho heuliañ",
   "account.share": "Skignañ profil @{name}",
   "account.show_reblogs": "Diskouez skignadennoù @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} C'hannad} two {{counter} Gannad} other {{counter} a Gannadoù}}",
   "account.unblock": "Diverzañ @{name}",
   "account.unblock_domain": "Diverzañ an domani {domain}",
   "account.unblock_short": "Distankañ",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 88dd34aff..3123e29d8 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -35,9 +35,9 @@
   "account.follow_back": "Segueix tu també",
   "account.followers": "Seguidors",
   "account.followers.empty": "A aquest usuari encara no el segueix ningú.",
-  "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} Seguidors}}",
+  "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidors}}",
   "account.following": "Seguint",
-  "account.following_counter": "{count, plural, other {{counter} Seguint-ne}}",
+  "account.following_counter": "{count, plural, other {Seguint-ne {counter}}}",
   "account.follows.empty": "Aquest usuari encara no segueix ningú.",
   "account.go_to_profile": "Vés al perfil",
   "account.hide_reblogs": "Amaga els impulsos de @{name}",
@@ -63,7 +63,7 @@
   "account.requested_follow": "{name} ha demanat de seguir-te",
   "account.share": "Comparteix el perfil de @{name}",
   "account.show_reblogs": "Mostra els impulsos de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Tut} other {{counter} Tuts}}",
+  "account.statuses_counter": "{count, plural, one {{counter} publicació} other {{counter} publicacions}}",
   "account.unblock": "Desbloca @{name}",
   "account.unblock_domain": "Desbloca el domini {domain}",
   "account.unblock_short": "Desbloca",
diff --git a/app/javascript/mastodon/locales/ckb.json b/app/javascript/mastodon/locales/ckb.json
index c212b53a8..3ebf9391d 100644
--- a/app/javascript/mastodon/locales/ckb.json
+++ b/app/javascript/mastodon/locales/ckb.json
@@ -35,9 +35,7 @@
   "account.follow_back": "فۆڵۆو بکەنەوە",
   "account.followers": "شوێنکەوتووان",
   "account.followers.empty": "کەسێک شوێن ئەم بەکارهێنەرە نەکەوتووە",
-  "account.followers_counter": "{count, plural, one {{counter} شوێنکەوتوو} other {{counter} شوێنکەوتوو}}",
   "account.following": "بەدوادا",
-  "account.following_counter": "{count, plural, one {{counter} شوێنکەوتوو} other {{counter} شوێنکەوتوو}}",
   "account.follows.empty": "ئەم بەکارهێنەرە تا ئێستا شوێن کەس نەکەوتووە.",
   "account.go_to_profile": "بڕۆ بۆ پڕۆفایلی",
   "account.hide_reblogs": "داشاردنی بووستەکان لە @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} داوای کردووە شوێنت بکەوێت",
   "account.share": "پرۆفایلی @{name} هاوبەش بکە",
   "account.show_reblogs": "پیشاندانی بەرزکردنەوەکان لە @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
   "account.unblock": "@{name} لاببە",
   "account.unblock_domain": "کردنەوەی دۆمەینی {domain}",
   "account.unblock_short": "لابردنی بەربەست",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index be4cce269..78f8e6fd7 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -16,8 +16,6 @@
   "account.follow": "Siguità",
   "account.followers": "Abbunati",
   "account.followers.empty": "Nisunu hè abbunatu à st'utilizatore.",
-  "account.followers_counter": "{count, plural, one {{counter} Abbunatu} other {{counter} Abbunati}}",
-  "account.following_counter": "{count, plural, one {{counter} Abbunamentu} other {{counter} Abbunamenti}}",
   "account.follows.empty": "St'utilizatore ùn seguita nisunu.",
   "account.hide_reblogs": "Piattà spartere da @{name}",
   "account.link_verified_on": "A prupietà di stu ligame hè stata verificata u {date}",
@@ -32,7 +30,6 @@
   "account.requested": "In attesa d'apprubazione. Cliccate per annullà a dumanda",
   "account.share": "Sparte u prufile di @{name}",
   "account.show_reblogs": "Vede spartere da @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Statutu} other {{counter} Statuti}}",
   "account.unblock": "Sbluccà @{name}",
   "account.unblock_domain": "Ùn piattà più {domain}",
   "account.unendorse": "Ùn fà figurà nant'à u prufilu",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index d8d83ae5f..66aa1fe0a 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Také sledovat",
   "account.followers": "Sledující",
   "account.followers.empty": "Tohoto uživatele zatím nikdo nesleduje.",
-  "account.followers_counter": "{count, plural, one {{counter} Sledující} few {{counter} Sledující} many {{counter} Sledujících} other {{counter} Sledujících}}",
   "account.following": "Sledujete",
-  "account.following_counter": "{count, plural, one {{counter} Sledovaný} few {{counter} Sledovaní} many {{counter} Sledovaných} other {{counter} Sledovaných}}",
   "account.follows.empty": "Tento uživatel zatím nikoho nesleduje.",
   "account.go_to_profile": "Přejít na profil",
   "account.hide_reblogs": "Skrýt boosty od @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} tě požádal o sledování",
   "account.share": "Sdílet profil @{name}",
   "account.show_reblogs": "Zobrazit boosty od @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Příspěvek} few {{counter} Příspěvky} many {{counter} Příspěvků} other {{counter} Příspěvků}}",
   "account.unblock": "Odblokovat @{name}",
   "account.unblock_domain": "Odblokovat doménu {domain}",
   "account.unblock_short": "Odblokovat",
diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index 96476b143..1c7e61832 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Dilyn yn ôl",
   "account.followers": "Dilynwyr",
   "account.followers.empty": "Does neb yn dilyn y defnyddiwr hwn eto.",
-  "account.followers_counter": "{count, plural, one {Dilynwr: {counter}} other {Dilynwyr: {counter}}}",
   "account.following": "Yn dilyn",
-  "account.following_counter": "{count, plural, one {Yn dilyn: {counter}} other {Yn dilyn: {counter}}}",
   "account.follows.empty": "Nid yw'r defnyddiwr hwn yn dilyn unrhyw un eto.",
   "account.go_to_profile": "Mynd i'r proffil",
   "account.hide_reblogs": "Cuddio hybiau gan @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "Mae {name} wedi gwneud cais i'ch dilyn",
   "account.share": "Rhannwch broffil @{name}",
   "account.show_reblogs": "Dangos hybiau gan @{name}",
-  "account.statuses_counter": "{count, plural, one {Postiad: {counter}} other {Postiad: {counter}}}",
   "account.unblock": "Dadflocio @{name}",
   "account.unblock_domain": "Dadflocio parth {domain}",
   "account.unblock_short": "Dadflocio",
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index 5ac7128a3..d8c178d29 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -35,9 +35,8 @@
   "account.follow_back": "Følg tilbage",
   "account.followers": "Følgere",
   "account.followers.empty": "Ingen følger denne bruger endnu.",
-  "account.followers_counter": "{count, plural, one {{counter} Følger} other {{counter} Følgere}}",
+  "account.followers_counter": "{count, plural, one {{counter} følger} other {{counter} følgere}}",
   "account.following": "Følger",
-  "account.following_counter": "{count, plural, one {{counter} Følges} other {{counter} Følges}}",
   "account.follows.empty": "Denne bruger følger ikke nogen endnu.",
   "account.go_to_profile": "Gå til profil",
   "account.hide_reblogs": "Skjul boosts fra @{name}",
@@ -63,7 +62,6 @@
   "account.requested_follow": "{name} har anmodet om at følge dig",
   "account.share": "Del @{name}s profil",
   "account.show_reblogs": "Vis fremhævelser fra @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Indlæg} other {{counter} Indlæg}}",
   "account.unblock": "Afblokér @{name}",
   "account.unblock_domain": "Afblokér domænet {domain}",
   "account.unblock_short": "Afblokér",
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index 47a8df620..5442624b3 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Ακολούθησε και εσύ",
   "account.followers": "Ακόλουθοι",
   "account.followers.empty": "Κανείς δεν ακολουθεί αυτόν τον χρήστη ακόμα.",
-  "account.followers_counter": "{count, plural, one {{counter} Ακόλουθος} other {{counter} Ακόλουθοι}}",
   "account.following": "Ακολουθείτε",
-  "account.following_counter": "{count, plural, one {{counter} Ακολουθεί} other {{counter} Ακολουθούν}}",
   "account.follows.empty": "Αυτός ο χρήστης δεν ακολουθεί κανέναν ακόμα.",
   "account.go_to_profile": "Μετάβαση στο προφίλ",
   "account.hide_reblogs": "Απόκρυψη ενισχύσεων από @{name}",
@@ -63,7 +61,6 @@
   "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": "Άρση αποκλεισμού",
diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json
index 108880cc9..c4f401d86 100644
--- a/app/javascript/mastodon/locales/en-GB.json
+++ b/app/javascript/mastodon/locales/en-GB.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Follow back",
   "account.followers": "Followers",
   "account.followers.empty": "No one follows this user yet.",
-  "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
   "account.following": "Following",
-  "account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
   "account.follows.empty": "This user doesn't follow anyone yet.",
   "account.go_to_profile": "Go to profile",
   "account.hide_reblogs": "Hide boosts from @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} has requested to follow you",
   "account.share": "Share @{name}'s profile",
   "account.show_reblogs": "Show boosts from @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Posts}}",
   "account.unblock": "Unblock @{name}",
   "account.unblock_domain": "Unblock domain {domain}",
   "account.unblock_short": "Unblock",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index bab277b48..e7cfc0346 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Sekvu reen",
   "account.followers": "Sekvantoj",
   "account.followers.empty": "Ankoraŭ neniu sekvas ĉi tiun uzanton.",
-  "account.followers_counter": "{count, plural, one{{counter} Sekvanto} other {{counter} Sekvantoj}}",
   "account.following": "Sekvatoj",
-  "account.following_counter": "{count, plural, one {{counter} Sekvato} other {{counter} Sekvatoj}}",
   "account.follows.empty": "La uzanto ankoraŭ ne sekvas iun ajn.",
   "account.go_to_profile": "Iri al profilo",
   "account.hide_reblogs": "Kaŝi diskonigojn de @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} petis sekvi vin",
   "account.share": "Diskonigi la profilon de @{name}",
   "account.show_reblogs": "Montri diskonigojn de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Afiŝo} other {{counter} Afiŝoj}}",
   "account.unblock": "Malbloki @{name}",
   "account.unblock_domain": "Malbloki la domajnon {domain}",
   "account.unblock_short": "Malbloki",
diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json
index 7da39b88c..28e8de923 100644
--- a/app/javascript/mastodon/locales/es-AR.json
+++ b/app/javascript/mastodon/locales/es-AR.json
@@ -37,7 +37,7 @@
   "account.followers.empty": "Todavía nadie sigue a este usuario.",
   "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}",
   "account.following": "Siguiendo",
-  "account.following_counter": "{count, plural, other {Siguiendo a {counter}}}",
+  "account.following_counter": "{count, plural, one {siguiendo a {counter}} other {siguiendo a {counter}}}",
   "account.follows.empty": "Todavía este usuario no sigue a nadie.",
   "account.go_to_profile": "Ir al perfil",
   "account.hide_reblogs": "Ocultar adhesiones de @{name}",
diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json
index d3e02cd6e..c10a16101 100644
--- a/app/javascript/mastodon/locales/es-MX.json
+++ b/app/javascript/mastodon/locales/es-MX.json
@@ -35,9 +35,9 @@
   "account.follow_back": "Seguir también",
   "account.followers": "Seguidores",
   "account.followers.empty": "Todavía nadie sigue a este usuario.",
-  "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidores}}",
+  "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}",
   "account.following": "Siguiendo",
-  "account.following_counter": "{count, plural, other {{counter} Siguiendo}}",
+  "account.following_counter": "{count, plural, one {{counter} siguiendo} other {{counter} siguiendo}}",
   "account.follows.empty": "Este usuario todavía no sigue a nadie.",
   "account.go_to_profile": "Ir al perfil",
   "account.hide_reblogs": "Ocultar retoots de @{name}",
@@ -63,7 +63,7 @@
   "account.requested_follow": "{name} ha solicitado seguirte",
   "account.share": "Compartir el perfil de @{name}",
   "account.show_reblogs": "Mostrar retoots de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
+  "account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
   "account.unblock": "Desbloquear a @{name}",
   "account.unblock_domain": "Mostrar a {domain}",
   "account.unblock_short": "Desbloquear",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 849e0fa27..259fc1795 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -35,9 +35,9 @@
   "account.follow_back": "Seguir también",
   "account.followers": "Seguidores",
   "account.followers.empty": "Todavía nadie sigue a este usuario.",
-  "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidores}}",
+  "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}",
   "account.following": "Siguiendo",
-  "account.following_counter": "{count, plural, other {Siguiendo a {counter}}}",
+  "account.following_counter": "{count, plural, one {{counter} siguiendo} other {{counter} siguiendo}}",
   "account.follows.empty": "Este usuario todavía no sigue a nadie.",
   "account.go_to_profile": "Ir al perfil",
   "account.hide_reblogs": "Ocultar impulsos de @{name}",
@@ -63,7 +63,7 @@
   "account.requested_follow": "{name} ha solicitado seguirte",
   "account.share": "Compartir el perfil de @{name}",
   "account.show_reblogs": "Mostrar impulsos de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Publicación} other {{counter} Publicaciones}}",
+  "account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
   "account.unblock": "Desbloquear a @{name}",
   "account.unblock_domain": "Desbloquear dominio {domain}",
   "account.unblock_short": "Desbloquear",
diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json
index 547a0fe61..94f5ef5d9 100644
--- a/app/javascript/mastodon/locales/et.json
+++ b/app/javascript/mastodon/locales/et.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Jälgi vastu",
   "account.followers": "Jälgijad",
   "account.followers.empty": "Keegi ei jälgi veel seda kasutajat.",
-  "account.followers_counter": "{count, plural, one {{counter} jälgija} other {{counter} jälgijat}}",
   "account.following": "Jälgib",
-  "account.following_counter": "{count, plural, one {{counter} jälgitav} other {{counter} jälgitavat}}",
   "account.follows.empty": "See kasutaja ei jälgi veel kedagi.",
   "account.go_to_profile": "Mine profiilile",
   "account.hide_reblogs": "Peida @{name} jagamised",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} on taodelnud sinu jälgimist",
   "account.share": "Jaga @{name} profiili",
   "account.show_reblogs": "Näita @{name} jagamisi",
-  "account.statuses_counter": "{count, plural, one {{counter} postitus} other {{counter} postitust}}",
   "account.unblock": "Eemalda blokeering @{name}",
   "account.unblock_domain": "Tee {domain} nähtavaks",
   "account.unblock_short": "Eemalda blokeering",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index 5fbac270c..97c4250d2 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Jarraitu bueltan",
   "account.followers": "Jarraitzaileak",
   "account.followers.empty": "Ez du inork erabiltzaile hau jarraitzen oraindik.",
-  "account.followers_counter": "{count, plural, one {Jarraitzaile {counter}} other {{counter} jarraitzaile}}",
   "account.following": "Jarraitzen",
-  "account.following_counter": "{count, plural, one {{counter} jarraitzen} other {{counter} jarraitzen}}",
   "account.follows.empty": "Erabiltzaile honek ez du inor jarraitzen oraindik.",
   "account.go_to_profile": "Joan profilera",
   "account.hide_reblogs": "Ezkutatu @{name} erabiltzailearen bultzadak",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name}-(e)k zu jarraitzeko eskaera egin du",
   "account.share": "Partekatu @{name} erabiltzailearen profila",
   "account.show_reblogs": "Erakutsi @{name} erabiltzailearen bultzadak",
-  "account.statuses_counter": "{count, plural, one {Bidalketa {counter}} other {{counter} bidalketa}}",
   "account.unblock": "Desblokeatu @{name}",
   "account.unblock_domain": "Berriz erakutsi {domain}",
   "account.unblock_short": "Desblokeatu",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 072a67421..18f6466d4 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -35,9 +35,7 @@
   "account.follow_back": "دنبال کردن متقابل",
   "account.followers": "پی‌گیرندگان",
   "account.followers.empty": "هنوز کسی پی‌گیر این کاربر نیست.",
-  "account.followers_counter": "{count, plural, one {{counter} پی‌گیرنده} other {{counter} پی‌گیرنده}}",
   "account.following": "پی می‌گیرید",
-  "account.following_counter": "{count, plural, one {{counter} پی‌گرفته} other {{counter} پی‌گرفته}}",
   "account.follows.empty": "این کاربر هنوز پی‌گیر کسی نیست.",
   "account.go_to_profile": "رفتن به نمایه",
   "account.hide_reblogs": "نهفتن تقویت‌های ‎@{name}",
@@ -63,7 +61,6 @@
   "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": "رفع مسدودیت",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 67e2b72b8..0767dd5e3 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -699,7 +699,7 @@
   "server_banner.is_one_of_many": "{domain} on yksi monista itsenäisistä Mastodon-palvelimista, joiden välityksellä voit toimia fediversumissa.",
   "server_banner.server_stats": "Palvelimen tilastot:",
   "sign_in_banner.create_account": "Luo tili",
-  "sign_in_banner.follow_anyone": "Seuraa kenen tahansa julkaisuja fediversumissa ja näe ne kaikki aikajärjestyksessä. Ei algoritmejä, mainoksia tai klikkikalastelua.",
+  "sign_in_banner.follow_anyone": "Seuraa kenen tahansa julkaisuja fediversumissa ja näe ne kaikki aikajärjestyksessä. Ei algoritmeja, mainoksia tai klikkausten kalastelua.",
   "sign_in_banner.mastodon_is": "Mastodon on paras tapa pysyä ajan tasalla siitä, mitä ympärillä tapahtuu.",
   "sign_in_banner.sign_in": "Kirjaudu",
   "sign_in_banner.sso_redirect": "Kirjaudu tai rekisteröidy",
diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json
index 7a317820b..e7786f388 100644
--- a/app/javascript/mastodon/locales/fo.json
+++ b/app/javascript/mastodon/locales/fo.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Fylg aftur",
   "account.followers": "Fylgjarar",
   "account.followers.empty": "Ongar fylgjarar enn.",
-  "account.followers_counter": "{count, plural, one {{counter} Fylgjari} other {{counter} Fylgjarar}}",
   "account.following": "Fylgir",
-  "account.following_counter": "{count, plural, one {{counter} fylgir} other {{counter} fylgja}}",
   "account.follows.empty": "Hesin brúkari fylgir ongum enn.",
   "account.go_to_profile": "Far til vanga",
   "account.hide_reblogs": "Fjal lyft frá @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} hevur biðið um at fylgja tær",
   "account.share": "Deil vanga @{name}'s",
   "account.show_reblogs": "Vís lyft frá @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} postur} other {{counter} postar}}",
   "account.unblock": "Banna ikki @{name}",
   "account.unblock_domain": "Banna ikki økisnavnið {domain}",
   "account.unblock_short": "Banna ikki",
diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json
index 50b7dcf90..432485500 100644
--- a/app/javascript/mastodon/locales/fr-CA.json
+++ b/app/javascript/mastodon/locales/fr-CA.json
@@ -35,9 +35,7 @@
   "account.follow_back": "S'abonner en retour",
   "account.followers": "abonné·e·s",
   "account.followers.empty": "Personne ne suit ce compte pour l'instant.",
-  "account.followers_counter": "{count, plural, one {{counter} Abonné·e} other {{counter} Abonné·e·s}}",
   "account.following": "Abonné·e",
-  "account.following_counter": "{count, plural, one {{counter} Abonnement} other {{counter} Abonnements}}",
   "account.follows.empty": "Ce compte ne suit personne présentement.",
   "account.go_to_profile": "Voir ce profil",
   "account.hide_reblogs": "Masquer les boosts de @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} a demandé à vous suivre",
   "account.share": "Partager le profil de @{name}",
   "account.show_reblogs": "Afficher les boosts de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Publication} other {{counter} Publications}}",
   "account.unblock": "Débloquer @{name}",
   "account.unblock_domain": "Débloquer le domaine {domain}",
   "account.unblock_short": "Débloquer",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 2e565c200..cd67cda53 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -35,9 +35,7 @@
   "account.follow_back": "S'abonner en retour",
   "account.followers": "Abonné·e·s",
   "account.followers.empty": "Personne ne suit cet·te utilisateur·rice pour l’instant.",
-  "account.followers_counter": "{count, plural, one {{counter} Abonné·e} other {{counter} Abonné·e·s}}",
   "account.following": "Abonnements",
-  "account.following_counter": "{count, plural, one {{counter} Abonnement} other {{counter} Abonnements}}",
   "account.follows.empty": "Cet·te utilisateur·rice ne suit personne pour l’instant.",
   "account.go_to_profile": "Aller au profil",
   "account.hide_reblogs": "Masquer les partages de @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} a demandé à vous suivre",
   "account.share": "Partager le profil de @{name}",
   "account.show_reblogs": "Afficher les partages de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Message} other {{counter} Messages}}",
   "account.unblock": "Débloquer @{name}",
   "account.unblock_domain": "Débloquer le domaine {domain}",
   "account.unblock_short": "Débloquer",
diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json
index 11b11ff81..d787c16bf 100644
--- a/app/javascript/mastodon/locales/fy.json
+++ b/app/javascript/mastodon/locales/fy.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Weromfolgje",
   "account.followers": "Folgers",
   "account.followers.empty": "Noch net ien folget dizze brûker.",
-  "account.followers_counter": "{count, plural, one {{counter} folger} other {{counter} folgers}}",
   "account.following": "Folgjend",
-  "account.following_counter": "{count, plural, one {{counter} folgjend} other {{counter} folgjend}}",
   "account.follows.empty": "Dizze brûker folget noch net ien.",
   "account.go_to_profile": "Gean nei profyl",
   "account.hide_reblogs": "Boosts fan @{name} ferstopje",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} hat dy in folchfersyk stjoerd",
   "account.share": "Profyl fan @{name} diele",
   "account.show_reblogs": "Boosts fan @{name} toane",
-  "account.statuses_counter": "{count, plural, one {{counter} berjocht} other {{counter} berjochten}}",
   "account.unblock": "@{name} deblokkearje",
   "account.unblock_domain": "Domein {domain} deblokkearje",
   "account.unblock_short": "Deblokkearje",
diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json
index 97dcc752b..edf761814 100644
--- a/app/javascript/mastodon/locales/ga.json
+++ b/app/javascript/mastodon/locales/ga.json
@@ -31,9 +31,7 @@
   "account.follow": "Lean",
   "account.followers": "Leantóirí",
   "account.followers.empty": "Ní leanann éinne an t-úsáideoir seo fós.",
-  "account.followers_counter": "{count, plural, one {Leantóir amháin} other {{counter} Leantóir}}",
   "account.following": "Ag leanúint",
-  "account.following_counter": "{count, plural, one {Ag leanúint cúntas amháin} other {Ag leanúint {counter} cúntas}}",
   "account.follows.empty": "Ní leanann an t-úsáideoir seo duine ar bith fós.",
   "account.go_to_profile": "Téigh go dtí próifíl",
   "account.hide_reblogs": "Folaigh moltaí ó @{name}",
@@ -55,7 +53,6 @@
   "account.requested_follow": "D'iarr {name} ort do chuntas a leanúint",
   "account.share": "Roinn próifíl @{name}",
   "account.show_reblogs": "Taispeáin moltaí ó @{name}",
-  "account.statuses_counter": "{count, plural, one {Postáil amháin} other {{counter} Postáil}}",
   "account.unblock": "Bain bac de @{name}",
   "account.unblock_domain": "Bain bac den ainm fearainn {domain}",
   "account.unblock_short": "Bain bac de",
diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json
index 714fa6e36..fec025045 100644
--- a/app/javascript/mastodon/locales/gd.json
+++ b/app/javascript/mastodon/locales/gd.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Lean air ais",
   "account.followers": "Luchd-leantainn",
   "account.followers.empty": "Chan eil neach sam bith a’ leantainn air a’ chleachdaiche seo fhathast.",
-  "account.followers_counter": "{count, plural, one {{counter} neach-leantainn} two {{counter} neach-leantainn} few {{counter} luchd-leantainn} other {{counter} luchd-leantainn}}",
   "account.following": "A’ leantainn",
-  "account.following_counter": "{count, plural, one {A’ leantainn {counter}} two {A’ leantainn {counter}} few {A’ leantainn {counter}} other {A’ leantainn {counter}}}",
   "account.follows.empty": "Chan eil an cleachdaiche seo a’ leantainn neach sam bith fhathast.",
   "account.go_to_profile": "Tadhail air a’ phròifil",
   "account.hide_reblogs": "Falaich na brosnachaidhean o @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "Dh’iarr {name} ’gad leantainn",
   "account.share": "Co-roinn a’ phròifil aig @{name}",
   "account.show_reblogs": "Seall na brosnachaidhean o @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} phost} two {{counter} phost} few {{counter} postaichean} other {{counter} post}}",
   "account.unblock": "Dì-bhac @{name}",
   "account.unblock_domain": "Dì-bhac an àrainn {domain}",
   "account.unblock_short": "Dì-bhac",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 7b77f9803..156fe3ee8 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Seguir tamén",
   "account.followers": "Seguidoras",
   "account.followers.empty": "Aínda ninguén segue esta usuaria.",
-  "account.followers_counter": "{count, plural, one {{counter} Seguidora} other {{counter} Seguidoras}}",
   "account.following": "Seguindo",
-  "account.following_counter": "{count, plural, one {{counter} Seguindo} other {{counter} Seguindo}}",
   "account.follows.empty": "Esta usuaria aínda non segue a ninguén.",
   "account.go_to_profile": "Ir ao perfil",
   "account.hide_reblogs": "Agochar promocións de @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} solicitou seguirte",
   "account.share": "Compartir o perfil de @{name}",
   "account.show_reblogs": "Amosar compartidos de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Publicación} other {{counter} Publicacións}}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Amosar {domain}",
   "account.unblock_short": "Desbloquear",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 1c50ba8e1..e022eac11 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -35,9 +35,7 @@
   "account.follow_back": "לעקוב בחזרה",
   "account.followers": "עוקבים",
   "account.followers.empty": "אף אחד לא עוקב אחר המשתמש הזה עדיין.",
-  "account.followers_counter": "{count, plural,one {עוקב אחד} other {{counter} עוקבים}}",
   "account.following": "נעקבים",
-  "account.following_counter": "{count, plural,one {עוקב אחרי {counter}}other {עוקב אחרי {counter}}}",
   "account.follows.empty": "משתמש זה עדיין לא עוקב אחרי אף אחד.",
   "account.go_to_profile": "מעבר לפרופיל",
   "account.hide_reblogs": "להסתיר הידהודים מאת @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} ביקשו לעקוב אחריך",
   "account.share": "שתף את הפרופיל של @{name}",
   "account.show_reblogs": "הצג הדהודים מאת @{name}",
-  "account.statuses_counter": "{count, plural, one {הודעה} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}",
   "account.unblock": "להסיר חסימה ל- @{name}",
   "account.unblock_domain": "הסירי את החסימה של קהילת {domain}",
   "account.unblock_short": "הסר חסימה",
diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json
index a2da55da8..89c71207f 100644
--- a/app/javascript/mastodon/locales/hi.json
+++ b/app/javascript/mastodon/locales/hi.json
@@ -35,9 +35,7 @@
   "account.follow_back": "फॉलो करें",
   "account.followers": "फॉलोवर",
   "account.followers.empty": "कोई भी इस यूज़र् को फ़ॉलो नहीं करता है",
-  "account.followers_counter": "{count, plural, one {{counter} अनुगामी} other {{counter} समर्थक}}",
   "account.following": "फॉलोइंग",
-  "account.following_counter": "{count, plural, one {{counter} निम्नलिखित} other {{counter} निम्नलिखित}}",
   "account.follows.empty": "यह यूज़र् अभी तक किसी को फॉलो नहीं करता है।",
   "account.go_to_profile": "प्रोफाइल में जाएँ",
   "account.hide_reblogs": "@{name} के बूस्ट छुपाएं",
@@ -63,7 +61,6 @@
   "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": "अनब्लॉक",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index d952945c4..c8f6f0186 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Slijedi natrag",
   "account.followers": "Pratitelji",
   "account.followers.empty": "Nitko još ne prati korisnika/cu.",
-  "account.followers_counter": "{count, plural, one {{counter} pratitelj} other {{counter} pratitelja}}",
   "account.following": "Pratim",
-  "account.following_counter": "{count, plural, one {{counter} praćeni} few{{counter} praćena} other {{counter} praćenih}}",
   "account.follows.empty": "Korisnik/ca još ne prati nikoga.",
   "account.go_to_profile": "Idi na profil",
   "account.hide_reblogs": "Sakrij boostove od @{name}",
@@ -62,7 +60,6 @@
   "account.requested_follow": "{name} zatražio/la je praćenje",
   "account.share": "Podijeli profil @{name}",
   "account.show_reblogs": "Prikaži boostove od @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} toot} other {{counter} toota}}",
   "account.unblock": "Deblokiraj @{name}",
   "account.unblock_domain": "Deblokiraj domenu {domain}",
   "account.unblock_short": "Deblokiraj",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 6164335da..627e3cab5 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Viszontkövetés",
   "account.followers": "Követő",
   "account.followers.empty": "Ezt a felhasználót még senki sem követi.",
-  "account.followers_counter": "{count, plural, one {{counter} Követő} other {{counter} Követő}}",
   "account.following": "Követve",
-  "account.following_counter": "{count, plural, one {{counter} Követett} other {{counter} Követett}}",
   "account.follows.empty": "Ez a felhasználó még senkit sem követ.",
   "account.go_to_profile": "Ugrás a profilhoz",
   "account.hide_reblogs": "@{name} megtolásainak elrejtése",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} kérte, hogy követhessen",
   "account.share": "@{name} profiljának megosztása",
   "account.show_reblogs": "@{name} megtolásainak mutatása",
-  "account.statuses_counter": "{count, plural, one {{counter} Bejegyzés} other {{counter} Bejegyzés}}",
   "account.unblock": "@{name} letiltásának feloldása",
   "account.unblock_domain": "{domain} domain tiltásának feloldása",
   "account.unblock_short": "Tiltás feloldása",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index cd29f441d..b4abe9bf0 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -28,9 +28,7 @@
   "account.follow": "Հետեւել",
   "account.followers": "Հետեւողներ",
   "account.followers.empty": "Այս օգտատիրոջը դեռ ոչ մէկ չի հետեւում։",
-  "account.followers_counter": "{count, plural, one {{counter} Հետեւորդ} other {{counter} Հետեւորդ}}",
   "account.following": "Հետեւած",
-  "account.following_counter": "{count, plural, one {{counter} Հետեւած} other {{counter} Հետեւած}}",
   "account.follows.empty": "Այս օգտատէրը դեռ ոչ մէկի չի հետեւում։",
   "account.go_to_profile": "Գնալ անձնական հաշիւ",
   "account.hide_reblogs": "Թաքցնել @{name}֊ի տարածածները",
@@ -52,7 +50,6 @@
   "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": "Արգելաբացել",
diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json
index 53cc93859..a2e64c10f 100644
--- a/app/javascript/mastodon/locales/ia.json
+++ b/app/javascript/mastodon/locales/ia.json
@@ -699,6 +699,7 @@
   "server_banner.is_one_of_many": "{domain} es un de multe servitores independente de Mastodon que tu pote usar pro participar in le fediverso.",
   "server_banner.server_stats": "Statos del servitor:",
   "sign_in_banner.create_account": "Crear un conto",
+  "sign_in_banner.follow_anyone": "Seque quicunque in le fediverso, e tu videra toto in ordine chronologic. Sin algorithmo, sin publicitate, sin titulos de esca.",
   "sign_in_banner.mastodon_is": "Mastodon es le melior maniera de sequer lo que passa.",
   "sign_in_banner.sign_in": "Aperir session",
   "sign_in_banner.sso_redirect": "Aperir session o crear conto",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index d86b5854f..f4e5e1bae 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Ikuti balik",
   "account.followers": "Pengikut",
   "account.followers.empty": "Pengguna ini belum ada pengikut.",
-  "account.followers_counter": "{count, plural, other {{counter} Pengikut}}",
   "account.following": "Mengikuti",
-  "account.following_counter": "{count, plural, other {{counter} Mengikuti}}",
   "account.follows.empty": "Pengguna ini belum mengikuti siapa pun.",
   "account.go_to_profile": "Buka profil",
   "account.hide_reblogs": "Sembunyikan boosts dari @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} ingin mengikuti Anda",
   "account.share": "Bagikan profil @{name}",
   "account.show_reblogs": "Tampilkan boost dari @{name}",
-  "account.statuses_counter": "{count, plural, other {{counter} Kiriman}}",
   "account.unblock": "Buka blokir @{name}",
   "account.unblock_domain": "Buka blokir domain {domain}",
   "account.unblock_short": "Buka blokir",
diff --git a/app/javascript/mastodon/locales/ie.json b/app/javascript/mastodon/locales/ie.json
index f15b98288..c75788c43 100644
--- a/app/javascript/mastodon/locales/ie.json
+++ b/app/javascript/mastodon/locales/ie.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Sequer reciprocmen",
   "account.followers": "Sequitores",
   "account.followers.empty": "Ancor nequi seque ti-ci usator.",
-  "account.followers_counter": "{count, plural, one {{counter} Sequitor} other {{counter} Sequitor}}",
   "account.following": "Sequent",
-  "account.following_counter": "{count, plural, one {{counter} Sequent} other {{counter} Sequent}}",
   "account.follows.empty": "Ti-ci usator ancor ne seque quemcunc.",
   "account.go_to_profile": "Ear a profil",
   "account.hide_reblogs": "Celar boosts de @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} ha petit sequer te",
   "account.share": "Distribuer li profil de @{name}",
   "account.show_reblogs": "Monstrar boosts de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Posta} other {{counter} Postas}}",
   "account.unblock": "Desbloccar @{name}",
   "account.unblock_domain": "Desbloccar dominia {domain}",
   "account.unblock_short": "Desbloccar",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 016a111c4..6aa954ae5 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -33,9 +33,7 @@
   "account.follow": "Sequar",
   "account.followers": "Sequanti",
   "account.followers.empty": "Nulu sequas ca uzanto til nun.",
-  "account.followers_counter": "{count, plural, one {{counter} Sequanto} other {{counter} Sequanti}}",
   "account.following": "Sequata",
-  "account.following_counter": "{count, plural, one {{counter} Sequas} other {{counter} Sequanti}}",
   "account.follows.empty": "Ca uzanto ne sequa irgu til nun.",
   "account.go_to_profile": "Irez al profilo",
   "account.hide_reblogs": "Celez repeti de @{name}",
@@ -60,7 +58,6 @@
   "account.requested_follow": "{name} demandis sequar tu",
   "account.share": "Partigez profilo di @{name}",
   "account.show_reblogs": "Montrez repeti de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Posto} other {{counter} Posti}}",
   "account.unblock": "Desblokusar @{name}",
   "account.unblock_domain": "Desblokusar {domain}",
   "account.unblock_short": "Desblokusar",
diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json
index 08605f523..1a38591b8 100644
--- a/app/javascript/mastodon/locales/is.json
+++ b/app/javascript/mastodon/locales/is.json
@@ -63,7 +63,7 @@
   "account.requested_follow": "{name} hefur beðið um að fylgjast með þér",
   "account.share": "Deila notandasniði fyrir @{name}",
   "account.show_reblogs": "Sýna endurbirtingar frá @{name}",
-  "account.statuses_counter": "{count, plural, one {Færsla: {counter}} other {Færslur: {counter}}}",
+  "account.statuses_counter": "{count, plural, one {{counter} færsla} other {{counter} færslur}}",
   "account.unblock": "Aflétta útilokun af @{name}",
   "account.unblock_domain": "Aflétta útilokun lénsins {domain}",
   "account.unblock_short": "Hætta að loka á",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 3672b5fd7..73c4f9ba6 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -35,9 +35,9 @@
   "account.follow_back": "Segui a tua volta",
   "account.followers": "Follower",
   "account.followers.empty": "Ancora nessuno segue questo utente.",
-  "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}",
+  "account.followers_counter": "{count, plural, one {{counter} seguace} other {{counter} seguaci}}",
   "account.following": "Seguiti",
-  "account.following_counter": "{count, plural, one {{counter} Seguiti} other {{counter} Seguiti}}",
+  "account.following_counter": "{count, plural, one {{counter} segui} other {{counter} segui}}",
   "account.follows.empty": "Questo utente non segue ancora nessuno.",
   "account.go_to_profile": "Vai al profilo",
   "account.hide_reblogs": "Nascondi potenziamenti da @{name}",
@@ -63,7 +63,7 @@
   "account.requested_follow": "{name} ha richiesto di seguirti",
   "account.share": "Condividi il profilo di @{name}",
   "account.show_reblogs": "Mostra potenziamenti da @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Post}}",
+  "account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} post}}",
   "account.unblock": "Sblocca @{name}",
   "account.unblock_domain": "Sblocca il dominio {domain}",
   "account.unblock_short": "Sblocca",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 90a46edd5..575c68de0 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -35,9 +35,7 @@
   "account.follow_back": "フォローバック",
   "account.followers": "フォロワー",
   "account.followers.empty": "まだ誰もフォローしていません。",
-  "account.followers_counter": "{counter} フォロワー",
   "account.following": "フォロー中",
-  "account.following_counter": "{counter} フォロー",
   "account.follows.empty": "まだ誰もフォローしていません。",
   "account.go_to_profile": "プロフィールページへ",
   "account.hide_reblogs": "@{name}さんからのブーストを非表示",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name}さんがあなたにフォローリクエストしました",
   "account.share": "@{name}さんのプロフィールを共有する",
   "account.show_reblogs": "@{name}さんからのブーストを表示",
-  "account.statuses_counter": "{counter} 投稿",
   "account.unblock": "@{name}さんのブロックを解除",
   "account.unblock_domain": "{domain}のブロックを解除",
   "account.unblock_short": "ブロック解除",
diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json
index 7af4dccd8..b2e67e143 100644
--- a/app/javascript/mastodon/locales/ka.json
+++ b/app/javascript/mastodon/locales/ka.json
@@ -26,7 +26,6 @@
   "account.requested": "დამტკიცების მოლოდინში. დააწკაპუნეთ რომ უარყოთ დადევნების მოთხონვა",
   "account.share": "გააზიარე @{name}-ის პროფილი",
   "account.show_reblogs": "აჩვენე ბუსტები @{name}-სგან",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock": "განბლოკე @{name}",
   "account.unblock_domain": "გამოაჩინე {domain}",
   "account.unendorse": "არ გამოირჩეს პროფილზე",
diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json
index 5aa46bafd..de866cc1b 100644
--- a/app/javascript/mastodon/locales/kab.json
+++ b/app/javascript/mastodon/locales/kab.json
@@ -26,9 +26,7 @@
   "account.follow": "Ḍfer",
   "account.followers": "Imeḍfaren",
   "account.followers.empty": "Ar tura, ulac yiwen i yeṭṭafaṛen amseqdac-agi.",
-  "account.followers_counter": "{count, plural, one {{count} n umeḍfar} other {{count} n imeḍfaren}}",
   "account.following": "Yeṭṭafaṛ",
-  "account.following_counter": "{count, plural, one {{counter} yettwaḍfaren} other {{counter} yettwaḍfaren}}",
   "account.follows.empty": "Ar tura, amseqdac-agi ur yeṭṭafaṛ yiwen.",
   "account.go_to_profile": "Ddu ɣer umaɣnu",
   "account.hide_reblogs": "Ffer ayen i ibeṭṭu @{name}",
@@ -51,7 +49,6 @@
   "account.requested_follow": "{name} yessuter ad k·m-yeḍfer",
   "account.share": "Bḍu amaɣnu n @{name}",
   "account.show_reblogs": "Ssken-d inebḍa n @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} n tsuffeɣt} other {{counter} n tsuffaɣ}}",
   "account.unblock": "Serreḥ i @{name}",
   "account.unblock_domain": "Ssken-d {domain}",
   "account.unblock_short": "Serreḥ",
diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json
index bd0a806cd..efeee16c6 100644
--- a/app/javascript/mastodon/locales/kk.json
+++ b/app/javascript/mastodon/locales/kk.json
@@ -31,9 +31,7 @@
   "account.follow": "Жазылу",
   "account.followers": "Жазылушы",
   "account.followers.empty": "Бұл қолданушыға әлі ешкім жазылмаған.",
-  "account.followers_counter": "{count, plural, one {{counter} жазылушы} other {{counter} жазылушы}}",
   "account.following": "Жазылым",
-  "account.following_counter": "{count, plural, one {{counter} жазылым} other {{counter} жазылым}}",
   "account.follows.empty": "Бұл қолданушы әлі ешкімге жазылмаған.",
   "account.go_to_profile": "Профиліне өту",
   "account.hide_reblogs": "@{name} бустарын жасыру",
@@ -52,7 +50,6 @@
   "account.requested": "Растауын күтіңіз. Жазылудан бас тарту үшін басыңыз",
   "account.share": "@{name} профилін бөлісу\"",
   "account.show_reblogs": "@{name} бөліскендерін көрсету",
-  "account.statuses_counter": "{count, plural, one {{counter} Пост} other {{counter} Пост}}",
   "account.unblock": "Бұғаттан шығару @{name}",
   "account.unblock_domain": "Бұғаттан шығару {domain}",
   "account.unendorse": "Профильде рекомендемеу",
diff --git a/app/javascript/mastodon/locales/kn.json b/app/javascript/mastodon/locales/kn.json
index ceb0f8b9b..24592e37f 100644
--- a/app/javascript/mastodon/locales/kn.json
+++ b/app/javascript/mastodon/locales/kn.json
@@ -16,7 +16,6 @@
   "account.posts": "ಟೂಟ್‌ಗಳು",
   "account.posts_with_replies": "Toots and replies",
   "account.requested": "Awaiting approval",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock_domain": "Unhide {domain}",
   "account_note.placeholder": "Click to add a note",
   "alert.unexpected.title": "ಅಯ್ಯೋ!",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index c4c084d98..90755666b 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -35,9 +35,7 @@
   "account.follow_back": "맞팔로우 하기",
   "account.followers": "팔로워",
   "account.followers.empty": "아직 아무도 이 사용자를 팔로우하고 있지 않습니다.",
-  "account.followers_counter": "{counter} 팔로워",
   "account.following": "팔로잉",
-  "account.following_counter": "{counter} 팔로잉",
   "account.follows.empty": "이 사용자는 아직 아무도 팔로우하고 있지 않습니다.",
   "account.go_to_profile": "프로필로 이동",
   "account.hide_reblogs": "@{name}의 부스트를 숨기기",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} 님이 팔로우 요청을 보냈습니다",
   "account.share": "@{name}의 프로필 공유",
   "account.show_reblogs": "@{name}의 부스트 보기",
-  "account.statuses_counter": "{counter} 게시물",
   "account.unblock": "차단 해제",
   "account.unblock_domain": "도메인 {domain} 차단 해제",
   "account.unblock_short": "차단 해제",
diff --git a/app/javascript/mastodon/locales/ku.json b/app/javascript/mastodon/locales/ku.json
index 83fcef26f..5248cdfa5 100644
--- a/app/javascript/mastodon/locales/ku.json
+++ b/app/javascript/mastodon/locales/ku.json
@@ -32,9 +32,7 @@
   "account.follow": "Bişopîne",
   "account.followers": "Şopîner",
   "account.followers.empty": "Kesekî hin ev bikarhêner neşopandiye.",
-  "account.followers_counter": "{count, plural, one {{counter} Şopîner} other {{counter} Şopîner}}",
   "account.following": "Dişopîne",
-  "account.following_counter": "{count, plural, one {{counter} Dişopîne} other {{counter} Dişopîne}}",
   "account.follows.empty": "Ev bikarhêner hin kesekî heya niha neşopandiye.",
   "account.go_to_profile": "Biçe bo profîlê",
   "account.hide_reblogs": "Bilindkirinên ji @{name} veşêre",
@@ -56,7 +54,6 @@
   "account.requested_follow": "{name} dixwaze te bişopîne",
   "account.share": "Profîla @{name} parve bike",
   "account.show_reblogs": "Bilindkirinên ji @{name} nîşan bike",
-  "account.statuses_counter": "{count, plural,one {{counter} Şandî}other {{counter} Şandî}}",
   "account.unblock": "Astengê li ser @{name} rake",
   "account.unblock_domain": "Astengê li ser navperê {domain} rake",
   "account.unblock_short": "Astengiyê rake",
diff --git a/app/javascript/mastodon/locales/kw.json b/app/javascript/mastodon/locales/kw.json
index 794cbd9ed..1afcf645c 100644
--- a/app/javascript/mastodon/locales/kw.json
+++ b/app/javascript/mastodon/locales/kw.json
@@ -17,8 +17,6 @@
   "account.follow": "Holya",
   "account.followers": "Holyoryon",
   "account.followers.empty": "Ny wra nagonan holya'n devnydhyer ma hwath.",
-  "account.followers_counter": "{count, plural, one {{counter} Holyer} other {{counter} Holyer}}",
-  "account.following_counter": "{count, plural, one {Ow holya {counter}} other {Ow holya {counter}}}",
   "account.follows.empty": "Ny wra'n devnydhyer ma holya nagonan hwath.",
   "account.hide_reblogs": "Kudha kenerthow a @{name}",
   "account.link_verified_on": "Perghenogeth an kolm ma a veu checkys dhe {date}",
@@ -33,7 +31,6 @@
   "account.requested": "Ow kortos komendyans. Klyckyewgh dhe hedhi govyn holya",
   "account.share": "Kevrenna profil @{name}",
   "account.show_reblogs": "Diskwedhes kenerthow a @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Tout} other {{counter} Tout}}",
   "account.unblock": "Anlettya @{name}",
   "account.unblock_domain": "Anlettya gorfarth {domain}",
   "account.unendorse": "Na wra diskwedhes yn profil",
diff --git a/app/javascript/mastodon/locales/la.json b/app/javascript/mastodon/locales/la.json
index d867034f0..aa209fcc0 100644
--- a/app/javascript/mastodon/locales/la.json
+++ b/app/javascript/mastodon/locales/la.json
@@ -14,12 +14,9 @@
   "account.edit_profile": "Recolere notionem",
   "account.featured_tags.last_status_never": "Nulla contributa",
   "account.featured_tags.title": "Hashtag notātī {name}",
-  "account.followers_counter": "{count, plural, one {{counter} Sectator} other {{counter} Sectatores}}",
-  "account.following_counter": "{count, plural, one {{counter} Sequens} other {{counter} Sequentes}}",
   "account.moved_to": "{name} significavit eum suam rationem novam nunc esse:",
   "account.muted": "Confutatus",
   "account.requested_follow": "{name} postulavit ut te sequeretur",
-  "account.statuses_counter": "{count, plural, one {{counter} Nuntius} other {{counter} Nuntii}}",
   "account.unblock_short": "Solvere impedimentum",
   "account_note.placeholder": "Click to add a note",
   "admin.dashboard.retention.average": "Mediocritas",
diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json
index bf676a602..292f00818 100644
--- a/app/javascript/mastodon/locales/lad.json
+++ b/app/javascript/mastodon/locales/lad.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Sige tamyen",
   "account.followers": "Suivantes",
   "account.followers.empty": "Por agora dingun no sige a este utilizador.",
-  "account.followers_counter": "{count, plural, one {{counter} suivante} other {{counter} suivantes}}",
   "account.following": "Sigiendo",
-  "account.following_counter": "{count, plural, other {Sigiendo a {counter}}}",
   "account.follows.empty": "Este utilizador ainda no sige a dingun.",
   "account.go_to_profile": "Va al profil",
   "account.hide_reblogs": "Eskonde repartajasyones de @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} tiene solisitado segirte",
   "account.share": "Partaja el profil de @{name}",
   "account.show_reblogs": "Amostra repartajasyones de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} publikasyon} other {{counter} publikasyones}}",
   "account.unblock": "Dezbloka a @{name}",
   "account.unblock_domain": "Dezbloka domeno {domain}",
   "account.unblock_short": "Dezbloka",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index bb69b7339..6bf8f94bd 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Sekti atgal",
   "account.followers": "Sekėjai",
   "account.followers.empty": "Šio naudotojo dar niekas neseka.",
-  "account.followers_counter": "{count, plural, one {{counter} sekėjas} few {{counter} sekėjai} many {{counter} sekėjo} other {{counter} sekėjų}}",
   "account.following": "Sekama",
-  "account.following_counter": "{count, plural, one {{counter} sekimas} few {{counter} sekimai} many {{counter} sekimo} other {{counter} sekimų}}",
   "account.follows.empty": "Šis naudotojas dar nieko neseka.",
   "account.go_to_profile": "Eiti į profilį",
   "account.hide_reblogs": "Slėpti pakėlimus iš @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} paprašė tave sekti",
   "account.share": "Bendrinti @{name} profilį",
   "account.show_reblogs": "Rodyti pakėlimus iš @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} įrašas} few {{counter} įrašai} many {{counter} įrašo} other {{counter} įrašų}}",
   "account.unblock": "Atblokuoti @{name}",
   "account.unblock_domain": "Atblokuoti domeną {domain}",
   "account.unblock_short": "Atblokuoti",
diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
index 13ceec21c..041072c6a 100644
--- a/app/javascript/mastodon/locales/lv.json
+++ b/app/javascript/mastodon/locales/lv.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Sekot atpakaļ",
   "account.followers": "Sekotāji",
   "account.followers.empty": "Šim lietotājam vēl nav sekotāju.",
-  "account.followers_counter": "{count, plural, zero {{counter} sekotāju} one {{counter} sekotājs} other {{counter} sekotāji}}",
   "account.following": "Seko",
-  "account.following_counter": "{count, plural, zero{{counter} sekojamo} one {{counter} sekojamais} other {{counter} sekojamie}}",
   "account.follows.empty": "Šis lietotājs pagaidām nevienam neseko.",
   "account.go_to_profile": "Doties uz profilu",
   "account.hide_reblogs": "Paslēpt @{name} pastiprinātos ierakstus",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} nosūtīja Tev sekošanas pieprasījumu",
   "account.share": "Dalīties ar @{name} profilu",
   "account.show_reblogs": "Parādīt @{name} pastiprinātos ierakstus",
-  "account.statuses_counter": "{count, plural, zero {{counter} ierakstu} one {{counter} ieraksts} other {{counter} ieraksti}}",
   "account.unblock": "Atbloķēt @{name}",
   "account.unblock_domain": "Atbloķēt domēnu {domain}",
   "account.unblock_short": "Atbloķēt",
diff --git a/app/javascript/mastodon/locales/mk.json b/app/javascript/mastodon/locales/mk.json
index d8a470ed4..a09ad98eb 100644
--- a/app/javascript/mastodon/locales/mk.json
+++ b/app/javascript/mastodon/locales/mk.json
@@ -38,7 +38,6 @@
   "account.requested": "Се чека одобрување. Кликни за да одкажиш барање за следење",
   "account.share": "Сподели @{name} профил",
   "account.show_reblogs": "Прикажи бустови од @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock": "Одблокирај @{name}",
   "account.unblock_domain": "Прикажи {domain}",
   "account.unendorse": "Не прикажувај на профил",
diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json
index 8fb4e818d..d9caccef3 100644
--- a/app/javascript/mastodon/locales/ml.json
+++ b/app/javascript/mastodon/locales/ml.json
@@ -22,9 +22,7 @@
   "account.follow": "പിന്തുടരുക",
   "account.followers": "പിന്തുടരുന്നവർ",
   "account.followers.empty": "ഈ ഉപയോക്താവിനെ ആരും ഇതുവരെ പിന്തുടരുന്നില്ല.",
-  "account.followers_counter": "{count, plural, one {{counter} പിന്തുടരുന്നവർ} other {{counter} പിന്തുടരുന്നവർ}}",
   "account.following": "പിന്തുടരുന്നു",
-  "account.following_counter": "{count, plural, one {{counter} പിന്തുടരുന്നു} other {{counter} പിന്തുടരുന്നു}}",
   "account.follows.empty": "ഈ ഉപയോക്താവ് ആരേയും ഇതുവരെ പിന്തുടരുന്നില്ല.",
   "account.go_to_profile": "പ്രൊഫൈലിലേക്ക് പോകാം",
   "account.hide_reblogs": "@{name} ബൂസ്റ്റ് ചെയ്തവ മറയ്കുക",
@@ -42,7 +40,6 @@
   "account.requested": "അനുവാദത്തിനായി കാത്തിരിക്കുന്നു. പിന്തുടരാനുള്ള അപേക്ഷ റദ്ദാക്കുവാൻ ഞെക്കുക",
   "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": "അൺബ്ലോക്കു ചെയ്യുക",
diff --git a/app/javascript/mastodon/locales/mr.json b/app/javascript/mastodon/locales/mr.json
index c07294d90..2757b96f9 100644
--- a/app/javascript/mastodon/locales/mr.json
+++ b/app/javascript/mastodon/locales/mr.json
@@ -35,9 +35,7 @@
   "account.follow_back": "आपणही अनुसरण करा",
   "account.followers": "अनुयायी",
   "account.followers.empty": "ह्या वापरकर्त्याचा आतापर्यंत कोणी अनुयायी नाही.",
-  "account.followers_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.following": "अनुसरण",
-  "account.following_counter": "{count, plural, one {{counter} following} other {{counter} following}}",
   "account.follows.empty": "हा वापरकर्ता अजूनपर्यंत कोणाचा अनुयायी नाही.",
   "account.go_to_profile": "प्रोफाइल वर जा",
   "account.hide_reblogs": "@{name} पासून सर्व बूस्ट लपवा",
@@ -59,7 +57,6 @@
   "account.requested_follow": "{name} ने आपल्याला फॉलो करण्याची रिक्वेस्ट केली आहे",
   "account.share": "@{name} चे प्रोफाइल शेअर करा",
   "account.show_reblogs": "{name}चे सर्व बुस्ट्स दाखवा",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock": "@{name} ला ब्लॉक करा",
   "account.unblock_domain": "उघड करा {domain}",
   "account.unblock_short": "अनब्लॉक करा",
diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json
index 3d7992faf..88c093bde 100644
--- a/app/javascript/mastodon/locales/ms.json
+++ b/app/javascript/mastodon/locales/ms.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Ikut balik",
   "account.followers": "Pengikut",
   "account.followers.empty": "Belum ada yang mengikuti pengguna ini.",
-  "account.followers_counter": "{count, plural, one {{counter} Pengikut} other {{counter} Pengikut}}",
   "account.following": "Mengikuti",
-  "account.following_counter": "{count, plural, one {{counter} Diikuti} other {{counter} Diikuti}}",
   "account.follows.empty": "Pengguna ini belum mengikuti sesiapa.",
   "account.go_to_profile": "Pergi ke profil",
   "account.hide_reblogs": "Sembunyikan galakan daripada @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} has requested to follow you",
   "account.share": "Kongsi profil @{name}",
   "account.show_reblogs": "Tunjukkan galakan daripada @{name}",
-  "account.statuses_counter": "{count, plural, other {{counter} kiriman}}",
   "account.unblock": "Nyahsekat @{name}",
   "account.unblock_domain": "Nyahsekat domain {domain}",
   "account.unblock_short": "Nyahsekat",
diff --git a/app/javascript/mastodon/locales/my.json b/app/javascript/mastodon/locales/my.json
index e3287f3f3..46c8d1806 100644
--- a/app/javascript/mastodon/locales/my.json
+++ b/app/javascript/mastodon/locales/my.json
@@ -34,9 +34,7 @@
   "account.follow": "စောင့်ကြည့်",
   "account.followers": "စောင့်ကြည့်သူများ",
   "account.followers.empty": "ဤသူကို စောင့်ကြည့်သူ မရှိသေးပါ။",
-  "account.followers_counter": "{count, plural, one {စောင့်ကြည့်သူ {counter}} other {စောင့်ကြည့်သူများ {counter}}}",
   "account.following": "စောင့်ကြည့်နေသည်",
-  "account.following_counter": "{count, plural, one {စောင့်ကြည့်ထားသူ {counter}} other {စောင့်ကြည့်ထားသူများ {counter}}}",
   "account.follows.empty": "ဤသူသည် မည်သူ့ကိုမျှ စောင့်ကြည့်ခြင်း မရှိသေးပါ။",
   "account.go_to_profile": "ပရိုဖိုင်းသို့ သွားရန်",
   "account.hide_reblogs": "@{name} ၏ မျှဝေမှုကို ဝှက်ထားရန်",
@@ -61,7 +59,6 @@
   "account.requested_follow": "{name} က သင့်ကို စောင့်ကြည့်ရန် တောင်းဆိုထားသည်",
   "account.share": "{name}၏ပရိုဖိုင်ကိုမျှဝေပါ",
   "account.show_reblogs": "@{name} မှ မျှ၀ေမှုများကို ပြပါ\n",
-  "account.statuses_counter": "{count, plural, one {{counter} ပိုစ့်များ} other {{counter} ပိုစ့်များ}}",
   "account.unblock": "{name} ကို ဘလော့ဖြုတ်မည်",
   "account.unblock_domain": " {domain} ဒိုမိန်းကိုပြန်ဖွင့်မည်",
   "account.unblock_short": "ဘလော့ဖြုတ်ရန်",
diff --git a/app/javascript/mastodon/locales/ne.json b/app/javascript/mastodon/locales/ne.json
index 500261a34..ca23a1f78 100644
--- a/app/javascript/mastodon/locales/ne.json
+++ b/app/javascript/mastodon/locales/ne.json
@@ -39,7 +39,6 @@
   "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": "अनब्लक गर्नुहोस्",
diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json
index 93b44f29a..7eca29659 100644
--- a/app/javascript/mastodon/locales/nn.json
+++ b/app/javascript/mastodon/locales/nn.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Fylg tilbake",
   "account.followers": "Fylgjarar",
   "account.followers.empty": "Ingen fylgjer denne brukaren enno.",
-  "account.followers_counter": "{count, plural, one {{counter} fylgjar} other {{counter} fylgjarar}}",
   "account.following": "Fylgjer",
-  "account.following_counter": "{count, plural, one {Fylgjer {counter}} other {Fylgjer {counter}}}",
   "account.follows.empty": "Denne brukaren fylgjer ikkje nokon enno.",
   "account.go_to_profile": "Gå til profil",
   "account.hide_reblogs": "Gøym framhevingar frå @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} har bedt om å få fylgja deg",
   "account.share": "Del @{name} sin profil",
   "account.show_reblogs": "Vis framhevingar frå @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} tut} other {{counter} tut}}",
   "account.unblock": "Stopp blokkering av @{name}",
   "account.unblock_domain": "Stopp blokkering av domenet {domain}",
   "account.unblock_short": "Stopp blokkering",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 213ba8af1..2bda37340 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Følg tilbake",
   "account.followers": "Følgere",
   "account.followers.empty": "Ingen følger denne brukeren ennå.",
-  "account.followers_counter": "{count, plural, one {{counter} følger} other {{counter} følgere}}",
   "account.following": "Følger",
-  "account.following_counter": "{count, plural, one {{counter} som følges} other {{counter} som følges}}",
   "account.follows.empty": "Denne brukeren følger ikke noen enda.",
   "account.go_to_profile": "Gå til profil",
   "account.hide_reblogs": "Skjul fremhevinger fra @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} har bedt om å få følge deg",
   "account.share": "Del @{name} sin profil",
   "account.show_reblogs": "Vis fremhevinger fra @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} innlegg} other {{counter} innlegg}}",
   "account.unblock": "Opphev blokkering av @{name}",
   "account.unblock_domain": "Opphev blokkering av {domain}",
   "account.unblock_short": "Opphev blokkering",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index d8e114158..d977eed4a 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -32,9 +32,7 @@
   "account.follow_back": "Sègre en retorn",
   "account.followers": "Seguidors",
   "account.followers.empty": "Degun sèc pas aqueste utilizaire pel moment.",
-  "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidors}}",
   "account.following": "Abonat",
-  "account.following_counter": "{count, plural, one {{counter} Abonaments} other {{counter} Abonaments}}",
   "account.follows.empty": "Aqueste utilizaire sèc pas degun pel moment.",
   "account.go_to_profile": "Anar al perfil",
   "account.hide_reblogs": "Rescondre los partatges de @{name}",
@@ -60,7 +58,6 @@
   "account.requested_follow": "{name} a demandat a vos sègre",
   "account.share": "Partejar lo perfil a @{name}",
   "account.show_reblogs": "Mostrar los partatges de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Tut} other {{counter} Tuts}}",
   "account.unblock": "Desblocar @{name}",
   "account.unblock_domain": "Desblocar {domain}",
   "account.unblock_short": "Desblocat",
diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json
index 46924d737..3828ff887 100644
--- a/app/javascript/mastodon/locales/pa.json
+++ b/app/javascript/mastodon/locales/pa.json
@@ -25,9 +25,7 @@
   "account.follow_back": "ਵਾਪਸ ਫਾਲ਼ੋ ਕਰੋ",
   "account.followers": "ਫ਼ਾਲੋਅਰ",
   "account.followers.empty": "ਇਸ ਵਰਤੋਂਕਾਰ ਨੂੰ ਹਾਲੇ ਕੋਈ ਫ਼ਾਲੋ ਨਹੀਂ ਕਰਦਾ ਹੈ।",
-  "account.followers_counter": "{count, plural, one {{counter} ਫ਼ਾਲੋਅਰ} other {{counter} ਫ਼ਾਲੋਅਰ}}",
   "account.following": "ਫ਼ਾਲੋ ਕੀਤਾ",
-  "account.following_counter": "{count, plural, one {{counter} ਨੂੰ ਫ਼ਾਲੋ} other {{counter} ਨੂੰ ਫ਼ਾਲੋ}}",
   "account.follows.empty": "ਇਹ ਵਰਤੋਂਕਾਰ ਹਾਲੇ ਕਿਸੇ ਨੂੰ ਫ਼ਾਲੋ ਨਹੀਂ ਕਰਦਾ ਹੈ।",
   "account.go_to_profile": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਜਾਓ",
   "account.media": "ਮੀਡੀਆ",
@@ -41,7 +39,6 @@
   "account.requested": "ਮਨਜ਼ੂਰੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ। ਫ਼ਾਲੋ ਬੇਨਤੀਆਂ ਨੂੰ ਰੱਦ ਕਰਨ ਲਈ ਕਲਿੱਕ ਕਰੋ",
   "account.requested_follow": "{name} ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਨ ਦੀ ਬੇਨਤੀ ਕੀਤੀ ਹੈ",
   "account.share": "{name} ਦਾ ਪਰੋਫ਼ਾਇਲ ਸਾਂਝਾ ਕਰੋ",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock": "@{name} ਤੋਂ ਪਾਬੰਦੀ ਹਟਾਓ",
   "account.unblock_domain": "{domain} ਡੋਮੇਨ ਤੋਂ ਪਾਬੰਦੀ ਹਟਾਓ",
   "account.unblock_short": "ਪਾਬੰਦੀ ਹਟਾਓ",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index ddfe1d4fb..a3690e734 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Obserwuj wzajemnie",
   "account.followers": "Obserwujący",
   "account.followers.empty": "Nikt jeszcze nie obserwuje tego użytkownika.",
-  "account.followers_counter": "{count, plural, one {{counter} obserwujący} few {{counter} obserwujących} many {{counter} obserwujących} other {{counter} obserwujących}}",
   "account.following": "Obserwowani",
-  "account.following_counter": "{count, plural, one {{counter} obserwowany} few {{counter} obserwowanych} many {{counter} obserwowanych} other {{counter} obserwowanych}}",
   "account.follows.empty": "Ten użytkownik nie obserwuje jeszcze nikogo.",
   "account.go_to_profile": "Przejdź do profilu",
   "account.hide_reblogs": "Ukryj podbicia od @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} chce zaobserwować twój profil",
   "account.share": "Udostępnij profil @{name}",
   "account.show_reblogs": "Pokazuj podbicia od @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}}",
   "account.unblock": "Odblokuj @{name}",
   "account.unblock_domain": "Odblokuj domenę {domain}",
   "account.unblock_short": "Odblokuj",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 4d3bd2d28..34d0ba36e 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Seguir de volta",
   "account.followers": "Seguidores",
   "account.followers.empty": "Nada aqui.",
-  "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}",
   "account.following": "Seguindo",
-  "account.following_counter": "{count, plural, one {segue {counter}} other {segue {counter}}}",
   "account.follows.empty": "Nada aqui.",
   "account.go_to_profile": "Ir ao perfil",
   "account.hide_reblogs": "Ocultar boosts de @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} quer te seguir",
   "account.share": "Compartilhar perfil de @{name}",
   "account.show_reblogs": "Mostrar boosts de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Desbloquear domínio {domain}",
   "account.unblock_short": "Desbloquear",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index 9446d5ee2..41112c2ca 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Seguir de volta",
   "account.followers": "Seguidores",
   "account.followers.empty": "Ainda ninguém segue este utilizador.",
-  "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}",
   "account.following": "A seguir",
-  "account.following_counter": "{count, plural, other {A seguir {counter}}}",
   "account.follows.empty": "Este utilizador ainda não segue ninguém.",
   "account.go_to_profile": "Ir para o perfil",
   "account.hide_reblogs": "Esconder partilhas de @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} pediu para segui-lo",
   "account.share": "Partilhar o perfil @{name}",
   "account.show_reblogs": "Mostrar partilhas de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Desbloquear o domínio {domain}",
   "account.unblock_short": "Desbloquear",
diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json
index 3a2fab905..35abf1b02 100644
--- a/app/javascript/mastodon/locales/ro.json
+++ b/app/javascript/mastodon/locales/ro.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Urmăreşte înapoi",
   "account.followers": "Urmăritori",
   "account.followers.empty": "Acest utilizator nu are încă urmăritori.",
-  "account.followers_counter": "{count, plural, one {Un abonat} few {{counter} abonați} other {{counter} de abonați}}",
   "account.following": "Urmăriți",
-  "account.following_counter": "{count, plural, one {Un abonament} few {{counter} abonamente} other {{counter} de abonamente}}",
   "account.follows.empty": "Momentan acest utilizator nu are niciun abonament.",
   "account.go_to_profile": "Mergi la profil",
   "account.hide_reblogs": "Ascunde distribuirile de la @{name}",
@@ -62,7 +60,6 @@
   "account.requested_follow": "{name} A cerut să vă urmărească",
   "account.share": "Distribuie profilul lui @{name}",
   "account.show_reblogs": "Afișează distribuirile de la @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock": "Deblochează pe @{name}",
   "account.unblock_domain": "Deblochează domeniul {domain}",
   "account.unblock_short": "Deblochează",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 40ca84814..97a1f0b09 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Подписаться в ответ",
   "account.followers": "Подписчики",
   "account.followers.empty": "На этого пользователя пока никто не подписан.",
-  "account.followers_counter": "{count, plural, one {{counter} подписчик} many {{counter} подписчиков} other {{counter} подписчика}}",
   "account.following": "Подписки",
-  "account.following_counter": "{count, plural, one {{counter} подписка} many {{counter} подписок} other {{counter} подписки}}",
   "account.follows.empty": "Этот пользователь пока ни на кого не подписался.",
   "account.go_to_profile": "Перейти к профилю",
   "account.hide_reblogs": "Скрыть продвижения от @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} отправил(а) вам запрос на подписку",
   "account.share": "Поделиться профилем @{name}",
   "account.show_reblogs": "Показывать продвижения от @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} пост} many {{counter} постов} other {{counter} поста}}",
   "account.unblock": "Разблокировать @{name}",
   "account.unblock_domain": "Разблокировать {domain}",
   "account.unblock_short": "Разблокировать",
diff --git a/app/javascript/mastodon/locales/sa.json b/app/javascript/mastodon/locales/sa.json
index 58654deb0..c3880a6b0 100644
--- a/app/javascript/mastodon/locales/sa.json
+++ b/app/javascript/mastodon/locales/sa.json
@@ -32,9 +32,7 @@
   "account.follow": "अनुस्रियताम्",
   "account.followers": "अनुसर्तारः",
   "account.followers.empty": "नाऽनुसर्तारो वर्तन्ते",
-  "account.followers_counter": "{count, plural, one {{counter} अनुसर्ता} two {{counter} अनुसर्तारौ} other {{counter} अनुसर्तारः}}",
   "account.following": "अनुसरति",
-  "account.following_counter": "{count, plural, one {{counter} अनुसृतः} two {{counter} अनुसृतौ} other {{counter} अनुसृताः}}",
   "account.follows.empty": "न कोऽप्यनुसृतो वर्तते",
   "account.go_to_profile": "प्रोफायिलं गच्छ",
   "account.hide_reblogs": "@{name} मित्रस्य प्रकाशनानि छिद्यन्ताम्",
@@ -56,7 +54,6 @@
   "account.requested_follow": "{name} त्वामनुसर्तुमयाचीत्",
   "account.share": "@{name} मित्रस्य विवरणं विभाज्यताम्",
   "account.show_reblogs": "@{name} मित्रस्य प्रकाशनानि दृश्यन्ताम्",
-  "account.statuses_counter": "{count, plural, one {{counter} पत्रम्}  two{{counter} पत्रे} other {{counter} पत्राणि}}",
   "account.unblock": "निषेधता नश्यताम् @{name}",
   "account.unblock_domain": "प्रदेशनिषेधता नश्यताम् {domain}",
   "account.unblock_short": "अनवरुन्धि",
diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json
index a0b5b3271..895557373 100644
--- a/app/javascript/mastodon/locales/sc.json
+++ b/app/javascript/mastodon/locales/sc.json
@@ -26,9 +26,7 @@
   "account.follow": "Sighi",
   "account.followers": "Sighiduras",
   "account.followers.empty": "Nemos sighit ancora custa persone.",
-  "account.followers_counter": "{count, plural, one {{counter} sighidura} other {{counter} sighiduras}}",
   "account.following": "Sighende",
-  "account.following_counter": "{count, plural, one {Sighende a {counter}} other {Sighende a {counter}}}",
   "account.follows.empty": "Custa persone non sighit ancora a nemos.",
   "account.hide_reblogs": "Cua is cumpartziduras de @{name}",
   "account.in_memoriam": "In memoriam.",
@@ -47,7 +45,6 @@
   "account.requested_follow": "{name} at dimandadu de ti sighire",
   "account.share": "Cumpartzi su profilu de @{name}",
   "account.show_reblogs": "Ammustra is cumpartziduras de @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
   "account.unblock": "Isbloca a @{name}",
   "account.unblock_domain": "Isbloca su domìniu {domain}",
   "account.unendorse": "Non cussiges in su profilu",
diff --git a/app/javascript/mastodon/locales/sco.json b/app/javascript/mastodon/locales/sco.json
index 53501a593..397f63fed 100644
--- a/app/javascript/mastodon/locales/sco.json
+++ b/app/javascript/mastodon/locales/sco.json
@@ -31,9 +31,7 @@
   "account.follow": "Follae",
   "account.followers": "Follaers",
   "account.followers.empty": "Naebody follaes this uiser yit.",
-  "account.followers_counter": "{count, plural, one {{counter} Follaer} other {{counter} Follaers}}",
   "account.following": "Follaein",
-  "account.following_counter": "{count, plural, one {{counter} Follaein} other {{counter} Follaein}}",
   "account.follows.empty": "This uiser disnae follae oniebody yit.",
   "account.go_to_profile": "Gang tae profile",
   "account.hide_reblogs": "Dinnae shaw heezes fae @{name}",
@@ -53,7 +51,6 @@
   "account.requested": "Haudin fir approval. Chap tae cancel follae request",
   "account.share": "Share @{name}'s profile",
   "account.show_reblogs": "Shaw heezes frae @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Posts}}",
   "account.unblock": "Undingie @{name}",
   "account.unblock_domain": "Undingie domain {domain}",
   "account.unblock_short": "Undingie",
diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json
index 22320daef..fbfdfaa65 100644
--- a/app/javascript/mastodon/locales/si.json
+++ b/app/javascript/mastodon/locales/si.json
@@ -23,9 +23,7 @@
   "account.follow": "අනුගමනය",
   "account.followers": "අනුගාමිකයින්",
   "account.followers.empty": "කිසිවෙක් අනුගමනය කර නැත.",
-  "account.followers_counter": "{count, plural, one {අනුගාමිකයින් {counter}} other {අනුගාමිකයින් {counter}}}",
   "account.following": "අනුගමන",
-  "account.following_counter": "{count, plural, one {අනුගමන {counter}} other {අනුගමන {counter}}}",
   "account.follows.empty": "තවමත් කිසිවෙක් අනුගමනය නොකරයි.",
   "account.go_to_profile": "පැතිකඩට යන්න",
   "account.joined_short": "එක් වූ දිනය",
@@ -39,7 +37,6 @@
   "account.posts_with_replies": "ලිපි සහ පිළිතුරු",
   "account.report": "@{name} වාර්තා කරන්න",
   "account.share": "@{name} ගේ පැතිකඩ බෙදාගන්න",
-  "account.statuses_counter": "{count, plural, one {ලිපි {counter}} other {ලිපි {counter}}}",
   "account.unblock": "@{name} අනවහිර කරන්න",
   "account.unblock_domain": "{domain} වසම අනවහිර කරන්න",
   "account.unblock_short": "අනවහිර",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 4c152a214..ed9c0de60 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Sledovať späť",
   "account.followers": "Sledovatelia",
   "account.followers.empty": "Tento účet ešte nikto nesleduje.",
-  "account.followers_counter": "{count, plural, one {{counter} sledujúci účet} few {{counter} sledujúce účty} many {{counter} sledujúcich účtov} other {{counter} sledujúcich účtov}}",
   "account.following": "Sledovaný účet",
-  "account.following_counter": "{count, plural, one {{counter} sledovaný účet} few {{counter} sledované účty} many {{counter} sledovaných účtov} other {{counter} sledovaných účtov}}",
   "account.follows.empty": "Tento účet ešte nikoho nesleduje.",
   "account.go_to_profile": "Prejsť na profil",
   "account.hide_reblogs": "Skryť zdieľania od @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} vás chce sledovať",
   "account.share": "Zdieľaj profil @{name}",
   "account.show_reblogs": "Zobrazovať zdieľania od @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} príspevok} few {{counter} príspevky} many {{counter} príspevkov} other {{counter} príspevkov}}",
   "account.unblock": "Odblokovať @{name}",
   "account.unblock_domain": "Odblokovať doménu {domain}",
   "account.unblock_short": "Odblokovať",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index 195797143..2a3d74a80 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -35,9 +35,9 @@
   "account.follow_back": "Sledi nazaj",
   "account.followers": "Sledilci",
   "account.followers.empty": "Nihče ne sledi temu uporabniku.",
-  "account.followers_counter": "{count, plural, one {ima {counter} sledilca} two {ima {counter} sledilca} few {ima {counter} sledilce} other {ima {counter} sledilcev}}",
+  "account.followers_counter": "{count, plural, one {{counter} sledilec} two {{counter} sledilca} few {{counter} sledilci} other {{counter} sledilcev}}",
   "account.following": "Sledim",
-  "account.following_counter": "{count, plural, one {sledi {count} osebi} two {sledi {count} osebama} few {sledi {count} osebam} other {sledi {count} osebam}}",
+  "account.following_counter": "{count, plural, one {{counter} sleden} two {{counter} sledena} few {{counter} sledeni} other {{counter} sledenih}}",
   "account.follows.empty": "Ta uporabnik še ne sledi nikomur.",
   "account.go_to_profile": "Pojdi na profil",
   "account.hide_reblogs": "Skrij izpostavitve od @{name}",
@@ -63,7 +63,7 @@
   "account.requested_follow": "{name} vam želi slediti",
   "account.share": "Deli profil osebe @{name}",
   "account.show_reblogs": "Pokaži izpostavitve osebe @{name}",
-  "account.statuses_counter": "{count, plural, one {{count} objava} two {{count} objavi} few {{count} objave} other {{count} objav}}",
+  "account.statuses_counter": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objave} other {{counter} objav}}",
   "account.unblock": "Odblokiraj @{name}",
   "account.unblock_domain": "Odblokiraj domeno {domain}",
   "account.unblock_short": "Odblokiraj",
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 6903bceff..6ac038c9f 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Ndiqe gjithashtu",
   "account.followers": "Ndjekës",
   "account.followers.empty": "Këtë përdorues ende s’e ndjek kush.",
-  "account.followers_counter": "{count, plural, one {{counter} Ndjekës} other {{counter} Ndjekës}}",
   "account.following": "Ndjekje",
-  "account.following_counter": "{count, plural, one {{counter} i Ndjekur} other {{counter} të Ndjekur}}",
   "account.follows.empty": "Ky përdorues ende s’ndjek kënd.",
   "account.go_to_profile": "Kalo te profili",
   "account.hide_reblogs": "Fshih përforcime nga @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} ka kërkuar t’ju ndjekë",
   "account.share": "Ndajeni profilin e @{name} me të tjerët",
   "account.show_reblogs": "Shfaq përforcime nga @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Mesazh} other {{counter} Mesazhe}}",
   "account.unblock": "Zhbllokoje @{name}",
   "account.unblock_domain": "Zhblloko përkatësinë {domain}",
   "account.unblock_short": "Zhbllokoje",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index 63b2e03c9..93c3b8fe2 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Uzvrati praćenje",
   "account.followers": "Pratioci",
   "account.followers.empty": "Još uvek niko ne prati ovog korisnika.",
-  "account.followers_counter": "{count, plural, one {{counter} pratilac} few {{counter} pratioca} other {{counter} pratilaca}}",
   "account.following": "Prati",
-  "account.following_counter": "{count, plural, one {{counter} prati} few {{counter} prati} other {{counter} prati}}",
   "account.follows.empty": "Ovaj korisnik još uvek nikog ne prati.",
   "account.go_to_profile": "Idi na profil",
   "account.hide_reblogs": "Sakrij podržavanja @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} je zatražio da vas prati",
   "account.share": "Podeli profil korisnika @{name}",
   "account.show_reblogs": "Prikaži podržavanja od korisnika @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} objavio} few {{counter} objavio} other {{counter} objavio}}",
   "account.unblock": "Odblokiraj korisnika @{name}",
   "account.unblock_domain": "Odblokiraj domen {domain}",
   "account.unblock_short": "Odblokiraj",
@@ -696,8 +693,11 @@
   "server_banner.about_active_users": "Ljudi koji su koristili ovaj server u prethodnih 30 dana (mesečno aktivnih korisnika)",
   "server_banner.active_users": "aktivnih korisnika",
   "server_banner.administered_by": "Administrira:",
+  "server_banner.is_one_of_many": "{domain} je jedan od mnogih nezavisnih Mastodon servera koje možete koristiti za učešće u fediverzumu.",
   "server_banner.server_stats": "Statistike servera:",
   "sign_in_banner.create_account": "Napravite nalog",
+  "sign_in_banner.follow_anyone": "Pratite bilo koga širom fediverzuma i pogledajte sve hronološkim redom. Nema algoritama, reklama ili mamaca za klikove na vidiku.",
+  "sign_in_banner.mastodon_is": "Mastodon je najbolji način da budete u toku sa onim što se dešava.",
   "sign_in_banner.sign_in": "Prijavite se",
   "sign_in_banner.sso_redirect": "Prijavite se ili se registrujte",
   "status.admin_account": "Otvori moderatorsko okruženje za @{name}",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index c6b969e98..0273002b3 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Узврати праћење",
   "account.followers": "Пратиоци",
   "account.followers.empty": "Још увек нико не прати овог корисника.",
-  "account.followers_counter": "{count, plural, one {{counter} пратилац} few {{counter} пратиоца} other {{counter} пратилаца}}",
   "account.following": "Прати",
-  "account.following_counter": "{count, plural, one {{counter} прати} few {{counter} прати} other {{counter} прати}}",
   "account.follows.empty": "Овај корисник још увек никог не прати.",
   "account.go_to_profile": "Иди на профил",
   "account.hide_reblogs": "Сакриј подржавања од @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} је затражио да вас прати",
   "account.share": "Подели профил корисника @{name}",
   "account.show_reblogs": "Прикажи подржавања од корисника @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} објавио} few {{counter} објавио} other {{counter} објавио}}",
   "account.unblock": "Одблокирај корисника @{name}",
   "account.unblock_domain": "Одблокирај домен {domain}",
   "account.unblock_short": "Одблокирај",
@@ -696,8 +693,11 @@
   "server_banner.about_active_users": "Људи који су користили овај сервер у претходних 30 дана (месечно активних корисника)",
   "server_banner.active_users": "активних корисника",
   "server_banner.administered_by": "Администрира:",
+  "server_banner.is_one_of_many": "{domain} је један од многих независних Mastodon сервера које можете користити за учешће у федиверзуму.",
   "server_banner.server_stats": "Статистике сервера:",
   "sign_in_banner.create_account": "Направите налог",
+  "sign_in_banner.follow_anyone": "Пратите било кога широм федиверзума и погледајте све хронолошким редом. Нема алгоритама, реклама или мамаца за кликове на видику.",
+  "sign_in_banner.mastodon_is": "Mastodon је најбољи начин да будете у току са оним што се дешава.",
   "sign_in_banner.sign_in": "Пријавите се",
   "sign_in_banner.sso_redirect": "Пријавите се или се региструјте",
   "status.admin_account": "Отвори модераторско окружење за @{name}",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index ced6c3605..7445b77ba 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Följ tillbaka",
   "account.followers": "Följare",
   "account.followers.empty": "Ingen följer denna användare än.",
-  "account.followers_counter": "{count, plural, one {{counter} följare} other {{counter} följare}}",
   "account.following": "Följer",
-  "account.following_counter": "{count, plural, one {{counter} följd} other {{counter} följda}}",
   "account.follows.empty": "Denna användare följer inte någon än.",
   "account.go_to_profile": "Gå till profilen",
   "account.hide_reblogs": "Dölj boostar från @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} har begärt att följa dig",
   "account.share": "Dela @{name}s profil",
   "account.show_reblogs": "Visa boostar från @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} inlägg} other {{counter} inlägg}}",
   "account.unblock": "Avblockera @{name}",
   "account.unblock_domain": "Avblockera {domain}",
   "account.unblock_short": "Avblockera",
diff --git a/app/javascript/mastodon/locales/szl.json b/app/javascript/mastodon/locales/szl.json
index 43cfc78d5..34d086eb4 100644
--- a/app/javascript/mastodon/locales/szl.json
+++ b/app/javascript/mastodon/locales/szl.json
@@ -23,7 +23,6 @@
   "account.posts": "Toots",
   "account.posts_with_replies": "Toots and replies",
   "account.requested": "Awaiting approval",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account_note.placeholder": "Click to add a note",
   "column.pins": "Pinned toot",
   "community.column_settings.media_only": "Media only",
diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json
index ac0984293..d44ac424f 100644
--- a/app/javascript/mastodon/locales/ta.json
+++ b/app/javascript/mastodon/locales/ta.json
@@ -24,9 +24,7 @@
   "account.follow_back": "பின்தொடரு",
   "account.followers": "பின்தொடர்பவர்கள்",
   "account.followers.empty": "இதுவரை யாரும் இந்த பயனரைப் பின்தொடரவில்லை.",
-  "account.followers_counter": "{count, plural, one {{counter} வாசகர்} other {{counter} வாசகர்கள்}}",
   "account.following": "பின்தொடரும்",
-  "account.following_counter": "{count, plural,one {{counter} சந்தா} other {{counter} சந்தாக்கள்}}",
   "account.follows.empty": "இந்த பயனர் இதுவரை யாரையும் பின்தொடரவில்லை.",
   "account.go_to_profile": "சுயவிவரத்திற்குச் செல்லவும்",
   "account.hide_reblogs": "இருந்து ஊக்கியாக மறை @{name}",
@@ -45,7 +43,6 @@
   "account.requested": "ஒப்புதலுக்காகக் காத்திருக்கிறது. பின்தொடரும் கோரிக்கையை நீக்க அழுத்தவும்",
   "account.share": "@{name} உடைய விவரத்தை பகிர்",
   "account.show_reblogs": "காட்டு boosts இருந்து @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} டூட்} other {{counter} டூட்டுகள்}}",
   "account.unblock": "@{name} மீது தடை நீக்குக",
   "account.unblock_domain": "{domain} ஐ காண்பி",
   "account.unblock_short": "தடையை நீக்கு",
diff --git a/app/javascript/mastodon/locales/tai.json b/app/javascript/mastodon/locales/tai.json
index 825cfb93b..cad6e8eaa 100644
--- a/app/javascript/mastodon/locales/tai.json
+++ b/app/javascript/mastodon/locales/tai.json
@@ -9,7 +9,6 @@
   "account.posts": "Huah-siann",
   "account.posts_with_replies": "Huah-siann kah huê-ìng",
   "account.requested": "Tán-thāi phue-tsún",
-  "account.statuses_counter": "{count, plural, one {{counter} Huah-siann} other {{counter} Huah-siann}}",
   "account_note.placeholder": "Tiám tsi̍t-ē ka-thiam pī-tsù",
   "column.pins": "Tah thâu-tsîng ê huah-siann",
   "community.column_settings.media_only": "Kan-na muî-thé",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index 284102c38..c06472561 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -25,7 +25,6 @@
   "account.requested": "ఆమోదం కోసం వేచి ఉంది. అభ్యర్థనను రద్దు చేయడానికి క్లిక్ చేయండి",
   "account.share": "@{name} యొక్క ప్రొఫైల్ను పంచుకోండి",
   "account.show_reblogs": "@{name}నుంచి బూస్ట్ లను చూపించు",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock": "@{name}పై బ్లాక్ ను తొలగించు",
   "account.unblock_domain": "{domain}ను దాచవద్దు",
   "account.unendorse": "ప్రొఫైల్లో చూపించవద్దు",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 64abb394b..e1d556ebf 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -35,9 +35,7 @@
   "account.follow_back": "ติดตามกลับ",
   "account.followers": "ผู้ติดตาม",
   "account.followers.empty": "ยังไม่มีใครติดตามผู้ใช้นี้",
-  "account.followers_counter": "{count, plural, other {{counter} ผู้ติดตาม}}",
   "account.following": "กำลังติดตาม",
-  "account.following_counter": "{count, plural, other {{counter} กำลังติดตาม}}",
   "account.follows.empty": "ผู้ใช้นี้ยังไม่ได้ติดตามใคร",
   "account.go_to_profile": "ไปยังโปรไฟล์",
   "account.hide_reblogs": "ซ่อนการดันจาก @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} ได้ขอติดตามคุณ",
   "account.share": "แชร์โปรไฟล์ของ @{name}",
   "account.show_reblogs": "แสดงการดันจาก @{name}",
-  "account.statuses_counter": "{count, plural, other {{counter} โพสต์}}",
   "account.unblock": "เลิกปิดกั้น @{name}",
   "account.unblock_domain": "เลิกปิดกั้นโดเมน {domain}",
   "account.unblock_short": "เลิกปิดกั้น",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 0bb2a0e4a..ac39a3fd7 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -35,9 +35,9 @@
   "account.follow_back": "Geri takip et",
   "account.followers": "Takipçi",
   "account.followers.empty": "Henüz kimse bu kullanıcıyı takip etmiyor.",
-  "account.followers_counter": "{count, plural, one {{counter} Takipçi} other {{counter} Takipçi}}",
+  "account.followers_counter": "{count, plural, one {{counter} takipçi} other {{counter} takipçi}}",
   "account.following": "Takip Ediliyor",
-  "account.following_counter": "{count, plural, one {{counter} Takip Edilen} other {{counter} Takip Edilen}}",
+  "account.following_counter": "{count, plural, one {{counter} takip edilen} other {{counter} takip edilen}}",
   "account.follows.empty": "Bu kullanıcı henüz kimseyi takip etmiyor.",
   "account.go_to_profile": "Profile git",
   "account.hide_reblogs": "@{name} kişisinin boostlarını gizle",
@@ -63,7 +63,7 @@
   "account.requested_follow": "{name} size takip isteği gönderdi",
   "account.share": "@{name} adlı kişinin profilini paylaş",
   "account.show_reblogs": "@{name} kişisinin yeniden paylaşımlarını göster",
-  "account.statuses_counter": "{count, plural, one {{counter} Gönderi} other {{counter} Gönderi}}",
+  "account.statuses_counter": "{count, plural, one {{counter} gönderi} other {{counter} gönderi}}",
   "account.unblock": "@{name} adlı kişinin engelini kaldır",
   "account.unblock_domain": "{domain} alan adının engelini kaldır",
   "account.unblock_short": "Engeli kaldır",
diff --git a/app/javascript/mastodon/locales/tt.json b/app/javascript/mastodon/locales/tt.json
index 273c1a6de..baba3190d 100644
--- a/app/javascript/mastodon/locales/tt.json
+++ b/app/javascript/mastodon/locales/tt.json
@@ -31,9 +31,7 @@
   "account.follow": "Язылу",
   "account.followers": "Язылучы",
   "account.followers.empty": "Әле беркем дә язылмаган.",
-  "account.followers_counter": "{count, plural,one {{counter} язылучы} other {{counter} язылучы}}",
   "account.following": "Язылулар",
-  "account.following_counter": "{count, plural, one {{counter} язылу} other {{counter} язылу}}",
   "account.follows.empty": "Беркемгә дә язылмаган әле.",
   "account.go_to_profile": "Профильгә күчү",
   "account.hide_reblogs": "Скрывать көчен нче @{name}",
@@ -55,7 +53,6 @@
   "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": "Бикләүне чыгу",
diff --git a/app/javascript/mastodon/locales/ug.json b/app/javascript/mastodon/locales/ug.json
index 4120d4483..e3dd0e6b1 100644
--- a/app/javascript/mastodon/locales/ug.json
+++ b/app/javascript/mastodon/locales/ug.json
@@ -6,7 +6,6 @@
   "account.posts": "Toots",
   "account.posts_with_replies": "Toots and replies",
   "account.requested": "Awaiting approval",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account_note.placeholder": "Click to add a note",
   "column.pins": "Pinned toot",
   "community.column_settings.media_only": "Media only",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 22cd15bd2..338b65061 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Підписатися взаємно",
   "account.followers": "Підписники",
   "account.followers.empty": "Ніхто ще не підписаний на цього користувача.",
-  "account.followers_counter": "{count, plural, one {{counter} підписник} few {{counter} підписники} many {{counter} підписників} other {{counter} підписники}}",
   "account.following": "Ви стежите",
-  "account.following_counter": "{count, plural, one {{counter} підписка} few {{counter} підписки} many {{counter} підписок} other {{counter} підписки}}",
   "account.follows.empty": "Цей користувач ще ні на кого не підписався.",
   "account.go_to_profile": "Перейти до профілю",
   "account.hide_reblogs": "Сховати поширення від @{name}",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} надсилає запит на стеження",
   "account.share": "Поділитися профілем @{name}",
   "account.show_reblogs": "Показати поширення від @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} дописи}}",
   "account.unblock": "Розблокувати @{name}",
   "account.unblock_domain": "Розблокувати {domain}",
   "account.unblock_short": "Розблокувати",
diff --git a/app/javascript/mastodon/locales/ur.json b/app/javascript/mastodon/locales/ur.json
index 1b9f8d969..cf53eb6fe 100644
--- a/app/javascript/mastodon/locales/ur.json
+++ b/app/javascript/mastodon/locales/ur.json
@@ -29,9 +29,7 @@
   "account.follow_back": "اکاؤنٹ کو فالو بیک ",
   "account.followers": "پیروکار",
   "account.followers.empty": "ہنوز اس صارف کی کوئی پیروی نہیں کرتا.",
-  "account.followers_counter": "{count, plural,one {{counter} پیروکار} other {{counter} پیروکار}}",
   "account.following": "فالو کر رہے ہیں",
-  "account.following_counter": "{count, plural, one {{counter} پیروی کر رہے ہیں} other {{counter} پیروی کر رہے ہیں}}",
   "account.follows.empty": "\"یہ صارف ہنوز کسی کی پیروی نہیں کرتا ہے\".",
   "account.go_to_profile": "پروفائل پر جائیں",
   "account.hide_reblogs": "@{name} سے فروغ چھپائیں",
@@ -57,7 +55,6 @@
   "account.requested_follow": "{name} آپ کو فالو کرنا چھاتا ہے۔",
   "account.share": "@{name} کے مشخص کو بانٹیں",
   "account.show_reblogs": "@{name} کی افزائشات کو دکھائیں",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock": "@{name} کو بحال کریں",
   "account.unblock_domain": "{domain} کو نہ چھپائیں",
   "account.unblock_short": "بلاک ختم کریں",
diff --git a/app/javascript/mastodon/locales/uz.json b/app/javascript/mastodon/locales/uz.json
index 77892914a..4824b1d33 100644
--- a/app/javascript/mastodon/locales/uz.json
+++ b/app/javascript/mastodon/locales/uz.json
@@ -31,9 +31,7 @@
   "account.follow": "Obuna bo‘lish",
   "account.followers": "Obunachilar",
   "account.followers.empty": "Bu foydalanuvchini hali hech kim kuzatmaydi.",
-  "account.followers_counter": "{count, plural, one {{counter} Muxlis} other {{counter} Muxlislar}}",
   "account.following": "Kuzatish",
-  "account.following_counter": "{count, plural, one {{counter} ga Muxlis} other {{counter} larga muxlis}}",
   "account.follows.empty": "Bu foydalanuvchi hali hech kimni kuzatmagan.",
   "account.go_to_profile": "Profilga o'tish",
   "account.hide_reblogs": "@{name} dan boostlarni yashirish",
@@ -54,7 +52,6 @@
   "account.requested_follow": "{name} sizni kuzatishni soʻradi",
   "account.share": "@{name} profilini ulashing",
   "account.show_reblogs": "@{name} dan bootlarni ko'rsatish",
-  "account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Postlar}}",
   "account.unblock": "@{name} ni blokdan chiqarish",
   "account.unblock_domain": "{domain} domenini blokdan chiqarish",
   "account.unblock_short": "Blokdan chiqarish",
diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json
index 18f0fec3c..bbfecf2c8 100644
--- a/app/javascript/mastodon/locales/vi.json
+++ b/app/javascript/mastodon/locales/vi.json
@@ -35,9 +35,7 @@
   "account.follow_back": "Theo dõi lại",
   "account.followers": "Người theo dõi",
   "account.followers.empty": "Chưa có người theo dõi nào.",
-  "account.followers_counter": "{count, plural, one {{counter} Người theo dõi} other {{counter} Người theo dõi}}",
   "account.following": "Đang theo dõi",
-  "account.following_counter": "{count, plural, one {{counter} Theo dõi} other {{counter} Theo dõi}}",
   "account.follows.empty": "Người này chưa theo dõi ai.",
   "account.go_to_profile": "Xem hồ sơ",
   "account.hide_reblogs": "Ẩn tút @{name} đăng lại",
@@ -63,7 +61,6 @@
   "account.requested_follow": "{name} yêu cầu theo dõi bạn",
   "account.share": "Chia sẻ @{name}",
   "account.show_reblogs": "Hiện tút do @{name} đăng lại",
-  "account.statuses_counter": "{count, plural, one {{counter} Tút} other {{counter} Tút}}",
   "account.unblock": "Bỏ chặn @{name}",
   "account.unblock_domain": "Bỏ ẩn {domain}",
   "account.unblock_short": "Bỏ chặn",
diff --git a/app/javascript/mastodon/locales/zgh.json b/app/javascript/mastodon/locales/zgh.json
index 1d3a22108..b42bb7589 100644
--- a/app/javascript/mastodon/locales/zgh.json
+++ b/app/javascript/mastodon/locales/zgh.json
@@ -19,7 +19,6 @@
   "account.posts_with_replies": "Toots and replies",
   "account.requested": "Awaiting approval",
   "account.share": "ⴱⴹⵓ ⵉⴼⵔⵙ ⵏ @{name}",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unfollow": "ⴽⴽⵙ ⴰⴹⴼⴼⵓⵕ",
   "account_note.placeholder": "Click to add a note",
   "bundle_column_error.retry": "ⴰⵍⵙ ⴰⵔⵎ",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 3456f99d2..f2accae0d 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -35,9 +35,9 @@
   "account.follow_back": "回关",
   "account.followers": "关注者",
   "account.followers.empty": "目前无人关注此用户。",
-  "account.followers_counter": "被 {counter} 人关注",
+  "account.followers_counter": "{count, plural, other {{counter} 关注者}}",
   "account.following": "正在关注",
-  "account.following_counter": "正在关注 {counter} 人",
+  "account.following_counter": "{count, plural, other {{counter} 关注}}",
   "account.follows.empty": "此用户目前未关注任何人。",
   "account.go_to_profile": "前往个人资料页",
   "account.hide_reblogs": "隐藏来自 @{name} 的转嘟",
@@ -63,7 +63,7 @@
   "account.requested_follow": "{name} 已经向你发送了关注请求",
   "account.share": "分享 @{name} 的个人资料页",
   "account.show_reblogs": "显示来自 @{name} 的转嘟",
-  "account.statuses_counter": "{counter} 条嘟文",
+  "account.statuses_counter": "{count, plural, other {{counter} 嘟文}}",
   "account.unblock": "取消屏蔽 @{name}",
   "account.unblock_domain": "取消屏蔽 {domain} 域名",
   "account.unblock_short": "取消屏蔽",
@@ -699,6 +699,7 @@
   "server_banner.is_one_of_many": "{domain} 是可用于参与联邦宇宙的众多独立 Mastodon 服务器之一。",
   "server_banner.server_stats": "服务器统计数据:",
   "sign_in_banner.create_account": "创建账户",
+  "sign_in_banner.follow_anyone": "关注联邦宇宙中的任何人,并按时间顺序查看所有内容。没有算法、广告或诱导链接。",
   "sign_in_banner.mastodon_is": "Mastodon 是了解最新动态的最佳途径。",
   "sign_in_banner.sign_in": "登录",
   "sign_in_banner.sso_redirect": "登录或注册",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 5dff46620..09a497e88 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -35,9 +35,7 @@
   "account.follow_back": "追蹤對方",
   "account.followers": "追蹤者",
   "account.followers.empty": "尚未有人追蹤這位使用者。",
-  "account.followers_counter": "有 {count, plural,one {{counter} 個} other {{counter} 個}}追蹤者",
   "account.following": "正在追蹤",
-  "account.following_counter": "正在追蹤 {count, plural,one {{counter}}other {{counter} 人}}",
   "account.follows.empty": "這位使用者尚未追蹤任何人。",
   "account.go_to_profile": "前往個人檔案",
   "account.hide_reblogs": "隱藏 @{name} 的轉推",
@@ -63,7 +61,6 @@
   "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": "解除封鎖",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 4ab22daba..04469a971 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -35,9 +35,9 @@
   "account.follow_back": "跟隨回去",
   "account.followers": "跟隨者",
   "account.followers.empty": "尚未有人跟隨這位使用者。",
-  "account.followers_counter": "被 {count, plural, other {{counter} 人}}跟隨",
+  "account.followers_counter": "被 {count, plural, other {{count} 人}}跟隨",
   "account.following": "跟隨中",
-  "account.following_counter": "正在跟隨 {count,plural,other {{counter} 人}}",
+  "account.following_counter": "正在跟隨 {count,plural,other {{count} 人}}",
   "account.follows.empty": "這位使用者尚未跟隨任何人。",
   "account.go_to_profile": "前往個人檔案",
   "account.hide_reblogs": "隱藏來自 @{name} 的轉嘟",
@@ -63,7 +63,7 @@
   "account.requested_follow": "{name} 要求跟隨您",
   "account.share": "分享 @{name} 的個人檔案",
   "account.show_reblogs": "顯示來自 @{name} 的轉嘟",
-  "account.statuses_counter": "{count, plural,one {{counter} 則}other {{counter} 則}}嘟文",
+  "account.statuses_counter": "{count, plural, other {{count} 則嘟文}}",
   "account.unblock": "解除封鎖 @{name}",
   "account.unblock_domain": "解除封鎖網域 {domain}",
   "account.unblock_short": "解除封鎖",
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index f108718e5..be87258da 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -293,7 +293,7 @@ fi:
       filter_by_action: Suodata tapahtuman mukaan
       filter_by_user: Suodata käyttäjän mukaan
       title: Auditointiloki
-      unavailable_instance: "(verkkotunnus ei ole saatavilla)"
+      unavailable_instance: "(verkkotunnus ei saatavilla)"
     announcements:
       destroyed_msg: Tiedote poistettu onnistuneesti!
       edit:
diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml
index 776e473ee..f6e6d4d2a 100644
--- a/config/locales/sr-Latn.yml
+++ b/config/locales/sr-Latn.yml
@@ -577,7 +577,7 @@ sr-Latn:
     relays:
       add_new: Dodaj novi relej
       delete: Obriši
-      description_html: "<strong>Federalni relej</strong> je posrednički server koji razmenjuje velike količine javnih truba između servera na koji je pretplaćen i na koji objavljuje.<strong>Može pomoći malim i srednjim serverima da otkriju sadržaj iz fediversa</strong>, koji inače zahteva od lokalnih korisnika da ručno pratiti ostale ljude na udaljenim serverima."
+      description_html: "<strong>Federalni relej</strong> je posrednički server koji razmenjuje velike količine javnih objava između servera na koji je pretplaćen i na koji objavljuje.<strong>Može pomoći malim i srednjim serverima da otkriju sadržaj iz fediverzuma</strong>, koji inače zahteva od lokalnih korisnika da ručno pratiti ostale ljude na udaljenim serverima."
       disable: Isključi
       disabled: Isključen
       enable: Uključi
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 365b358d5..9bfefde83 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -577,7 +577,7 @@ sr:
     relays:
       add_new: Додај нови релеј
       delete: Обриши
-      description_html: "<strong>Федерални релеј</strong> је посреднички сервер који размењује велике количине јавних труба између сервера на који је претплаћен и на који објављује.<strong>Може помоћи малим и средњим серверима да открију садржај из федиверса</strong>, који иначе захтева од локалних корисника да ручно пратити остале људе на удаљеним серверима."
+      description_html: "<strong>Федерални релеј</strong> је посреднички сервер који размењује велике количине јавних објава између сервера на који је претплаћен и на који објављује.<strong>Може помоћи малим и средњим серверима да открију садржај из федиверзума</strong>, који иначе захтева од локалних корисника да ручно пратити остале људе на удаљеним серверима."
       disable: Искључи
       disabled: Искључен
       enable: Укључи
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 93de27c0b..1317d5f70 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -525,7 +525,7 @@ zh-TW:
       total_followed_by_us: 被我們跟隨
       total_reported: 關於他們的檢舉報告
       total_storage: 多媒體附加檔案
-      totals_time_period_hint_html: 以下顯示之總和包含所有時間的資料。
+      totals_time_period_hint_html: 以下顯示之統計包含所有時間的資料。
       unknown_instance: 此伺服器目前沒有這個網域的紀錄。
     invites:
       deactivate_all: 全部停用

From 096057b845f27fd6090915b22c8688a6eeb22e28 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 27 Jun 2024 15:17:18 +0200
Subject: [PATCH 31/84] Change `author_account` to be `authors` in REST API
 (#30846)

---
 .../mastodon/actions/importer/index.js           |  4 ++--
 .../mastodon/actions/importer/normalizer.js      | 11 +++++++++--
 app/javascript/mastodon/actions/trends.js        |  2 +-
 app/javascript/mastodon/api_types/statuses.ts    |  7 +++++++
 .../features/explore/components/author_link.jsx  |  4 ++++
 .../mastodon/features/explore/links.jsx          |  2 +-
 .../mastodon/features/status/components/card.jsx |  4 ++--
 app/models/preview_card.rb                       | 16 ++++++++++++++++
 app/serializers/rest/preview_card_serializer.rb  |  7 ++++++-
 9 files changed, 48 insertions(+), 9 deletions(-)

diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js
index d906bdfb1..516a7a797 100644
--- a/app/javascript/mastodon/actions/importer/index.js
+++ b/app/javascript/mastodon/actions/importer/index.js
@@ -76,8 +76,8 @@ export function importFetchedStatuses(statuses) {
         pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
       }
 
-      if (status.card?.author_account) {
-        pushUnique(accounts, status.card.author_account);
+      if (status.card) {
+        status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account));
       }
     }
 
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index be76b0f39..c09a3f442 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -36,8 +36,15 @@ export function normalizeStatus(status, normalOldStatus) {
     normalStatus.poll = status.poll.id;
   }
 
-  if (status.card?.author_account) {
-    normalStatus.card = { ...status.card, author_account: status.card.author_account.id };
+  if (status.card) {
+    normalStatus.card = {
+      ...status.card,
+      authors: status.card.authors.map(author => ({
+        ...author,
+        accountId: author.account?.id,
+        account: undefined,
+      })),
+    };
   }
 
   if (status.filtered) {
diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js
index 01089fccb..0bdf17a5d 100644
--- a/app/javascript/mastodon/actions/trends.js
+++ b/app/javascript/mastodon/actions/trends.js
@@ -51,7 +51,7 @@ export const fetchTrendingLinks = () => (dispatch) => {
   api()
     .get('/api/v1/trends/links', { params: { limit: 20 } })
     .then(({ data }) => {
-      dispatch(importFetchedAccounts(data.map(link => link.author_account).filter(account => !!account)));
+      dispatch(importFetchedAccounts(data.flatMap(link => link.authors.map(author => author.account)).filter(account => !!account)));
       dispatch(fetchTrendingLinksSuccess(data));
     })
     .catch(err => dispatch(fetchTrendingLinksFail(err)));
diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts
index c7dd33b5d..db4e20506 100644
--- a/app/javascript/mastodon/api_types/statuses.ts
+++ b/app/javascript/mastodon/api_types/statuses.ts
@@ -30,6 +30,12 @@ export interface ApiMentionJSON {
   acct: string;
 }
 
+export interface ApiPreviewCardAuthorJSON {
+  name: string;
+  url: string;
+  account?: ApiAccountJSON;
+}
+
 export interface ApiPreviewCardJSON {
   url: string;
   title: string;
@@ -48,6 +54,7 @@ export interface ApiPreviewCardJSON {
   embed_url: string;
   blurhash: string;
   published_at: string;
+  authors: ApiPreviewCardAuthorJSON[];
 }
 
 export interface ApiStatusJSON {
diff --git a/app/javascript/mastodon/features/explore/components/author_link.jsx b/app/javascript/mastodon/features/explore/components/author_link.jsx
index 8dd9b0dab..764ae7534 100644
--- a/app/javascript/mastodon/features/explore/components/author_link.jsx
+++ b/app/javascript/mastodon/features/explore/components/author_link.jsx
@@ -8,6 +8,10 @@ import { useAppSelector } from 'mastodon/store';
 export const AuthorLink = ({ accountId }) => {
   const account = useAppSelector(state => state.getIn(['accounts', accountId]));
 
+  if (!account) {
+    return null;
+  }
+
   return (
     <Link to={`/@${account.get('acct')}`} className='story__details__shared__author-link' data-hover-card-account={accountId}>
       <Avatar account={account} size={16} />
diff --git a/app/javascript/mastodon/features/explore/links.jsx b/app/javascript/mastodon/features/explore/links.jsx
index 93fd1fb6d..035e5aaad 100644
--- a/app/javascript/mastodon/features/explore/links.jsx
+++ b/app/javascript/mastodon/features/explore/links.jsx
@@ -75,7 +75,7 @@ class Links extends PureComponent {
             publisher={link.get('provider_name')}
             publishedAt={link.get('published_at')}
             author={link.get('author_name')}
-            authorAccount={link.getIn(['author_account', 'id'])}
+            authorAccount={link.getIn(['authors', 0, 'account', 'id'])}
             sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
             thumbnail={link.get('image')}
             thumbnailDescription={link.get('image_description')}
diff --git a/app/javascript/mastodon/features/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx
index f562e53f0..f0ae40cbc 100644
--- a/app/javascript/mastodon/features/status/components/card.jsx
+++ b/app/javascript/mastodon/features/status/components/card.jsx
@@ -138,7 +138,7 @@ export default class Card extends PureComponent {
     const interactive = card.get('type') === 'video';
     const language    = card.get('language') || '';
     const largeImage  = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive;
-    const showAuthor  = !!card.get('author_account');
+    const showAuthor  = !!card.getIn(['authors', 0, 'accountId']);
 
     const description = (
       <div className='status-card__content'>
@@ -244,7 +244,7 @@ export default class Card extends PureComponent {
           {description}
         </a>
 
-        {showAuthor && <MoreFromAuthor accountId={card.get('author_account')} />}
+        {showAuthor && <MoreFromAuthor accountId={card.getIn(['authors', 0, 'accountId'])} />}
       </>
     );
   }
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index cbfc39378..eac02ac14 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -128,6 +128,22 @@ class PreviewCard < ApplicationRecord
     @history ||= Trends::History.new('links', id)
   end
 
+  def authors
+    @authors ||= [PreviewCard::Author.new(self)]
+  end
+
+  class Author < ActiveModelSerializers::Model
+    attributes :name, :url, :account
+
+    def initialize(preview_card)
+      super(
+        name: preview_card.author_name,
+        url: preview_card.author_url,
+        account: preview_card.author_account,
+      )
+    end
+  end
+
   class << self
     private
 
diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb
index 7d4c99c2d..f73a051ac 100644
--- a/app/serializers/rest/preview_card_serializer.rb
+++ b/app/serializers/rest/preview_card_serializer.rb
@@ -1,6 +1,11 @@
 # frozen_string_literal: true
 
 class REST::PreviewCardSerializer < ActiveModel::Serializer
+  class AuthorSerializer < ActiveModel::Serializer
+    attributes :name, :url
+    has_one :account, serializer: REST::AccountSerializer
+  end
+
   include RoutingHelper
 
   attributes :url, :title, :description, :language, :type,
@@ -8,7 +13,7 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
              :provider_url, :html, :width, :height,
              :image, :image_description, :embed_url, :blurhash, :published_at
 
-  has_one :author_account, serializer: REST::AccountSerializer, if: -> { object.author_account.present? }
+  has_many :authors, serializer: AuthorSerializer
 
   def url
     object.original_url.presence || object.url

From 42adb6eaee3250e3557403ce52513e3d31b3ab80 Mon Sep 17 00:00:00 2001
From: David Roetzel <david@roetzel.de>
Date: Thu, 27 Jun 2024 16:40:19 +0200
Subject: [PATCH 32/84] Add size limit for link preview URLs (#30854)

---
 app/services/fetch_link_card_service.rb       |  5 ++++-
 spec/services/fetch_link_card_service_spec.rb | 13 +++++++++++++
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 900cb9863..9692dd21b 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -15,6 +15,9 @@ class FetchLinkCardService < BaseService
     )
   }iox
 
+  # URL size limit to safely store in PosgreSQL's unique indexes
+  BYTESIZE_LIMIT = 2692
+
   def call(status)
     @status       = status
     @original_url = parse_urls
@@ -85,7 +88,7 @@ class FetchLinkCardService < BaseService
 
   def bad_url?(uri)
     # Avoid local instance URLs and invalid URLs
-    uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme)
+    uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme) || uri.to_s.bytesize > BYTESIZE_LIMIT
   end
 
   def mention_link?(anchor)
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index 239f84fde..4f02aba54 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -193,6 +193,19 @@ RSpec.describe FetchLinkCardService do
       end
     end
 
+    context 'with an URL too long for PostgreSQL unique indexes' do
+      let(:url) { "http://example.com/#{'a' * 2674}" }
+      let(:status) { Fabricate(:status, text: url) }
+
+      it 'does not fetch the URL' do
+        expect(a_request(:get, url)).to_not have_been_made
+      end
+
+      it 'does not create a preview card' do
+        expect(status.preview_card).to be_nil
+      end
+    end
+
     context 'with a URL of a page with oEmbed support' do
       let(:html) { '<!doctype html><title>Hello world</title><link rel="alternate" type="application/json+oembed" href="http://example.com/oembed?url=http://example.com/html">' }
       let(:status) { Fabricate(:status, text: 'http://example.com/html') }

From ff08d99d4dceebd720e16e4a40118cbd6941b0ca Mon Sep 17 00:00:00 2001
From: David Roetzel <david@roetzel.de>
Date: Thu, 27 Jun 2024 16:41:03 +0200
Subject: [PATCH 33/84] Catch encoding errors when creating link previews.
 (#30853)

---
 app/services/fetch_link_card_service.rb           | 2 +-
 spec/fixtures/requests/redirect_with_utf8_url.txt | 5 +++++
 spec/services/fetch_link_card_service_spec.rb     | 9 +++++++++
 3 files changed, 15 insertions(+), 1 deletion(-)
 create mode 100644 spec/fixtures/requests/redirect_with_utf8_url.txt

diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 9692dd21b..8bc9f912c 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -32,7 +32,7 @@ class FetchLinkCardService < BaseService
     end
 
     attach_card if @card&.persisted?
-  rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e
+  rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError, Encoding::UndefinedConversionError => e
     Rails.logger.debug { "Error fetching link #{@original_url}: #{e}" }
     nil
   end
diff --git a/spec/fixtures/requests/redirect_with_utf8_url.txt b/spec/fixtures/requests/redirect_with_utf8_url.txt
new file mode 100644
index 000000000..08f99ee2a
--- /dev/null
+++ b/spec/fixtures/requests/redirect_with_utf8_url.txt
@@ -0,0 +1,5 @@
+HTTP/1.1 301 Moved Permanently
+server: nginx
+date: Thu, 27 Jun 2024 11:04:53 GMT
+content-type: text/html; charset=UTF-8
+location: http://example.com/ärgerliche-umlaute.html
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index 4f02aba54..d83a52751 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -27,6 +27,7 @@ RSpec.describe FetchLinkCardService do
     stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
     stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt'))
     stub_request(:get, 'http://example.com/low_confidence_latin1').to_return(request_fixture('low_confidence_latin1.txt'))
+    stub_request(:get, 'http://example.com/aergerliche-umlaute').to_return(request_fixture('redirect_with_utf8_url.txt'))
 
     Rails.cache.write('oembed_endpoint:example.com', oembed_cache) if oembed_cache
 
@@ -101,6 +102,14 @@ RSpec.describe FetchLinkCardService do
       end
     end
 
+    context 'with a redirect URL with faulty encoding' do
+      let(:status) { Fabricate(:status, text: 'http://example.com/aergerliche-umlaute') }
+
+      it 'does not create a preview card' do
+        expect(status.preview_card).to be_nil
+      end
+    end
+
     context 'with a 404 URL' do
       let(:status) { Fabricate(:status, text: 'http://example.com/not-found') }
 

From b15a3614dc7466e983e1394a12a16874a812e672 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 27 Jun 2024 17:25:27 +0200
Subject: [PATCH 34/84] Stub `Vips::Error` when not using libvips (#30857)

---
 config/initializers/vips.rb | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/config/initializers/vips.rb b/config/initializers/vips.rb
index 25a17b2a1..8d5d5cdc9 100644
--- a/config/initializers/vips.rb
+++ b/config/initializers/vips.rb
@@ -25,3 +25,11 @@ if Rails.configuration.x.use_vips
 
   Vips.block_untrusted(true)
 end
+
+# In some places of the code, we rescue this exception, but we don't always
+# load libvips, so it may be an undefined constant:
+unless defined?(Vips)
+  module Vips
+    class Error < StandardError; end
+  end
+end

From 836c0477ac63ea61b955bfb900cf52ec29554e40 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 27 Jun 2024 12:03:26 -0400
Subject: [PATCH 35/84] Use vips setting instead of env var in media processing
 spec (#30859)

---
 spec/models/media_attachment_spec.rb | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb
index 221645ac5..a8f1ce774 100644
--- a/spec/models/media_attachment_spec.rb
+++ b/spec/models/media_attachment_spec.rb
@@ -210,10 +210,14 @@ RSpec.describe MediaAttachment, :paperclip_processing do
       expect(media.file.meta['original']['duration']).to be_within(0.05).of(0.235102)
       expect(media.thumbnail.present?).to be true
 
-      # NOTE: Our libvips and ImageMagick implementations currently have different results
-      expect(media.file.meta['colors']['background']).to eq(ENV['MASTODON_USE_LIBVIPS'] ? '#268cd9' : '#3088d4')
+      expect(media.file.meta['colors']['background']).to eq(expected_background_color)
       expect(media.file_file_name).to_not eq 'boop.ogg'
     end
+
+    def expected_background_color
+      # The libvips and ImageMagick implementations produce different results
+      Rails.configuration.x.use_vips ? '#268cd9' : '#3088d4'
+    end
   end
 
   describe 'mp3 with large cover art' do

From 03bbb74b0ccc5f56bb3856efb1892ae8e0872a38 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 27 Jun 2024 16:27:32 +0000
Subject: [PATCH 36/84] fix(deps): update dependency prom-client to v15.1.3
 (#30852)

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 dc73fb93b..f9eaef458 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -14099,12 +14099,12 @@ __metadata:
   linkType: hard
 
 "prom-client@npm:^15.0.0":
-  version: 15.1.2
-  resolution: "prom-client@npm:15.1.2"
+  version: 15.1.3
+  resolution: "prom-client@npm:15.1.3"
   dependencies:
     "@opentelemetry/api": "npm:^1.4.0"
     tdigest: "npm:^0.1.1"
-  checksum: 10c0/a221db148fa64e29dfd4c6cdcaaae14635495a4272b68917e2b44fcfd988bc57027d275b04489ceeea4d0c4d64d058af842c1300966d2c1ffa255f1fa6af1277
+  checksum: 10c0/816525572e5799a2d1d45af78512fb47d073c842dc899c446e94d17cfc343d04282a1627c488c7ca1bcd47f766446d3e49365ab7249f6d9c22c7664a5bce7021
   languageName: node
   linkType: hard
 

From bc3737f0c3c4cc0af500413db53fb5443ad02b69 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 27 Jun 2024 12:27:42 -0400
Subject: [PATCH 37/84] Add detail about running version on vips error failure
 (#30858)

---
 config/initializers/vips.rb | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/config/initializers/vips.rb b/config/initializers/vips.rb
index 8d5d5cdc9..a539d7035 100644
--- a/config/initializers/vips.rb
+++ b/config/initializers/vips.rb
@@ -5,7 +5,11 @@ if Rails.configuration.x.use_vips
 
   require 'vips'
 
-  abort('Incompatible libvips version, please install libvips >= 8.13') unless Vips.at_least_libvips?(8, 13)
+  unless Vips.at_least_libvips?(8, 13)
+    abort <<~ERROR.squish
+      Incompatible libvips version (#{Vips.version_string}), please install libvips >= 8.13
+    ERROR
+  end
 
   Vips.block('VipsForeign', true)
 

From 3225954865c49ce7a336b73b2bc0c5ff2b707746 Mon Sep 17 00:00:00 2001
From: Michael Stanclift <mx@vmstan.com>
Date: Thu, 27 Jun 2024 11:46:20 -0500
Subject: [PATCH 38/84] Fix browser window color on light theme (#30861)

---
 app/lib/themes.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/lib/themes.rb b/app/lib/themes.rb
index b6da98073..183258d62 100644
--- a/app/lib/themes.rb
+++ b/app/lib/themes.rb
@@ -8,7 +8,7 @@ class Themes
 
   THEME_COLORS = {
     dark: '#191b22',
-    light: '#f3f5f7',
+    light: '#ffffff',
   }.freeze
 
   def initialize

From 0f3fef6fda5819824457b9c1cdc41cb3d5ca976e Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 27 Jun 2024 23:34:34 +0200
Subject: [PATCH 39/84] Change search modifiers to be case-insensitive (#30865)

---
 app/lib/search_query_transformer.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index 927495eac..606819ed4 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -225,7 +225,7 @@ class SearchQueryTransformer < Parslet::Transform
   end
 
   rule(clause: subtree(:clause)) do
-    prefix   = clause[:prefix][:term].to_s if clause[:prefix]
+    prefix   = clause[:prefix][:term].to_s.downcase if clause[:prefix]
     operator = clause[:operator]&.to_s
     term     = clause[:phrase] ? clause[:phrase].map { |term| term[:term].to_s }.join(' ') : clause[:term].to_s
 

From ea6c455e81f9f01a64adf123d0a4f820c6e67f97 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 28 Jun 2024 00:01:40 +0200
Subject: [PATCH 40/84] Fix follow button in hover cards not working when
 signed out in web UI (#30864)

---
 .../mastodon/components/follow_button.tsx     | 32 ++++++++++++++++---
 1 file changed, 28 insertions(+), 4 deletions(-)

diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx
index 4b4d27831..db5994288 100644
--- a/app/javascript/mastodon/components/follow_button.tsx
+++ b/app/javascript/mastodon/components/follow_button.tsx
@@ -2,11 +2,13 @@ import { useCallback, useEffect } from 'react';
 
 import { useIntl, defineMessages } from 'react-intl';
 
+import { useIdentity } from '@/mastodon/identity_context';
 import {
   fetchRelationships,
   followAccount,
   unfollowAccount,
 } from 'mastodon/actions/accounts';
+import { openModal } from 'mastodon/actions/modal';
 import { Button } from 'mastodon/components/button';
 import { LoadingIndicator } from 'mastodon/components/loading_indicator';
 import { me } from 'mastodon/initial_state';
@@ -29,17 +31,37 @@ export const FollowButton: React.FC<{
 }> = ({ accountId }) => {
   const intl = useIntl();
   const dispatch = useAppDispatch();
+  const { signedIn } = useIdentity();
+  const account = useAppSelector((state) =>
+    accountId ? state.accounts.get(accountId) : undefined,
+  );
   const relationship = useAppSelector((state) =>
     state.relationships.get(accountId),
   );
   const following = relationship?.following || relationship?.requested;
 
   useEffect(() => {
-    dispatch(fetchRelationships([accountId]));
-  }, [dispatch, accountId]);
+    if (accountId && signedIn) {
+      dispatch(fetchRelationships([accountId]));
+    }
+  }, [dispatch, accountId, signedIn]);
 
   const handleClick = useCallback(() => {
+    if (!signedIn) {
+      dispatch(
+        openModal({
+          modalType: 'INTERACTION',
+          modalProps: {
+            type: 'follow',
+            accountId: accountId,
+            url: account?.url,
+          },
+        }),
+      );
+    }
+
     if (!relationship) return;
+
     if (accountId === me) {
       return;
     } else if (relationship.following || relationship.requested) {
@@ -47,11 +69,13 @@ export const FollowButton: React.FC<{
     } else {
       dispatch(followAccount(accountId));
     }
-  }, [dispatch, accountId, relationship]);
+  }, [dispatch, accountId, relationship, account, signedIn]);
 
   let label;
 
-  if (accountId === me) {
+  if (!signedIn) {
+    label = intl.formatMessage(messages.follow);
+  } else if (accountId === me) {
     label = intl.formatMessage(messages.edit_profile);
   } else if (!relationship) {
     label = <LoadingIndicator />;

From a5134f2695659ccb1c40b28b1f691e2dc932a5fb Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 28 Jun 2024 10:33:57 +0200
Subject: [PATCH 41/84] New Crowdin Translations (automated) (#30867)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/de.json    | 2 +-
 app/javascript/mastodon/locales/gl.json    | 3 +++
 app/javascript/mastodon/locales/hu.json    | 3 +++
 app/javascript/mastodon/locales/lt.json    | 3 +++
 app/javascript/mastodon/locales/nn.json    | 6 ++++++
 app/javascript/mastodon/locales/pl.json    | 3 +++
 app/javascript/mastodon/locales/pt-PT.json | 3 +++
 app/javascript/mastodon/locales/sq.json    | 3 +++
 app/javascript/mastodon/locales/sv.json    | 2 ++
 config/locales/nn.yml                      | 1 +
 10 files changed, 28 insertions(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 86438757a..4a5b666d3 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -37,7 +37,7 @@
   "account.followers.empty": "Diesem Profil folgt noch niemand.",
   "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}",
   "account.following": "Folge ich",
-  "account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}",
+  "account.following_counter": "{count, plural, one {{counter} folge ich} other {{counter} folge ich}}",
   "account.follows.empty": "Dieses Profil folgt noch niemandem.",
   "account.go_to_profile": "Profil aufrufen",
   "account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 156fe3ee8..fae48ed06 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Seguir tamén",
   "account.followers": "Seguidoras",
   "account.followers.empty": "Aínda ninguén segue esta usuaria.",
+  "account.followers_counter": "{count, plural, one {{counter} seguidora} other {{counter} seguidoras}}",
   "account.following": "Seguindo",
+  "account.following_counter": "{count, plural, one {{counter} seguimento} other {{counter} seguimentos}}",
   "account.follows.empty": "Esta usuaria aínda non segue a ninguén.",
   "account.go_to_profile": "Ir ao perfil",
   "account.hide_reblogs": "Agochar promocións de @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} solicitou seguirte",
   "account.share": "Compartir o perfil de @{name}",
   "account.show_reblogs": "Amosar compartidos de @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicacións}}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Amosar {domain}",
   "account.unblock_short": "Desbloquear",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 627e3cab5..1fcadc8f9 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Viszontkövetés",
   "account.followers": "Követő",
   "account.followers.empty": "Ezt a felhasználót még senki sem követi.",
+  "account.followers_counter": "{count, plural, one {{counter} követő} other {{counter} követő}}",
   "account.following": "Követve",
+  "account.following_counter": "{count, plural, one {{counter} követett} other {{counter} követett}}",
   "account.follows.empty": "Ez a felhasználó még senkit sem követ.",
   "account.go_to_profile": "Ugrás a profilhoz",
   "account.hide_reblogs": "@{name} megtolásainak elrejtése",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} kérte, hogy követhessen",
   "account.share": "@{name} profiljának megosztása",
   "account.show_reblogs": "@{name} megtolásainak mutatása",
+  "account.statuses_counter": "{count, plural, one {{counter} bejegyzés} other {{counter} bejegyzés}}",
   "account.unblock": "@{name} letiltásának feloldása",
   "account.unblock_domain": "{domain} domain tiltásának feloldása",
   "account.unblock_short": "Tiltás feloldása",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index 6bf8f94bd..bb69b7339 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Sekti atgal",
   "account.followers": "Sekėjai",
   "account.followers.empty": "Šio naudotojo dar niekas neseka.",
+  "account.followers_counter": "{count, plural, one {{counter} sekėjas} few {{counter} sekėjai} many {{counter} sekėjo} other {{counter} sekėjų}}",
   "account.following": "Sekama",
+  "account.following_counter": "{count, plural, one {{counter} sekimas} few {{counter} sekimai} many {{counter} sekimo} other {{counter} sekimų}}",
   "account.follows.empty": "Šis naudotojas dar nieko neseka.",
   "account.go_to_profile": "Eiti į profilį",
   "account.hide_reblogs": "Slėpti pakėlimus iš @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} paprašė tave sekti",
   "account.share": "Bendrinti @{name} profilį",
   "account.show_reblogs": "Rodyti pakėlimus iš @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} įrašas} few {{counter} įrašai} many {{counter} įrašo} other {{counter} įrašų}}",
   "account.unblock": "Atblokuoti @{name}",
   "account.unblock_domain": "Atblokuoti domeną {domain}",
   "account.unblock_short": "Atblokuoti",
diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json
index 7eca29659..0fb0edf0a 100644
--- a/app/javascript/mastodon/locales/nn.json
+++ b/app/javascript/mastodon/locales/nn.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Fylg tilbake",
   "account.followers": "Fylgjarar",
   "account.followers.empty": "Ingen fylgjer denne brukaren enno.",
+  "account.followers_counter": "{count, plural, one {{counter} følgjar} other {{counter} følgjarar}}",
   "account.following": "Fylgjer",
+  "account.following_counter": "{count, plural, one {{counter} følgjer} other {{counter} følgjer}}",
   "account.follows.empty": "Denne brukaren fylgjer ikkje nokon enno.",
   "account.go_to_profile": "Gå til profil",
   "account.hide_reblogs": "Gøym framhevingar frå @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} har bedt om å få fylgja deg",
   "account.share": "Del @{name} sin profil",
   "account.show_reblogs": "Vis framhevingar frå @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} innlegg} other {{counter} innlegg}}",
   "account.unblock": "Stopp blokkering av @{name}",
   "account.unblock_domain": "Stopp blokkering av domenet {domain}",
   "account.unblock_short": "Stopp blokkering",
@@ -693,8 +696,11 @@
   "server_banner.about_active_users": "Personar som har brukt denne tenaren dei siste 30 dagane (Månadlege Aktive Brukarar)",
   "server_banner.active_users": "aktive brukarar",
   "server_banner.administered_by": "Administrert av:",
+  "server_banner.is_one_of_many": "{domain} er ein av dei mange uavhengige Mastodon-serverane du kan bruke til å delta i Fødiverset.",
   "server_banner.server_stats": "Tenarstatistikk:",
   "sign_in_banner.create_account": "Opprett konto",
+  "sign_in_banner.follow_anyone": "Følg kven som helst på tvers av Fødiverset og sjå alt i kronologisk rekkjefølgje. Ingen algoritmar, reklamar eller clickbait i sikte.",
+  "sign_in_banner.mastodon_is": "Mastodon er den beste måten å følgje med på det som skjer.",
   "sign_in_banner.sign_in": "Logg inn",
   "sign_in_banner.sso_redirect": "Logg inn eller registrer deg",
   "status.admin_account": "Opne moderasjonsgrensesnitt for @{name}",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index a3690e734..ddfe1d4fb 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Obserwuj wzajemnie",
   "account.followers": "Obserwujący",
   "account.followers.empty": "Nikt jeszcze nie obserwuje tego użytkownika.",
+  "account.followers_counter": "{count, plural, one {{counter} obserwujący} few {{counter} obserwujących} many {{counter} obserwujących} other {{counter} obserwujących}}",
   "account.following": "Obserwowani",
+  "account.following_counter": "{count, plural, one {{counter} obserwowany} few {{counter} obserwowanych} many {{counter} obserwowanych} other {{counter} obserwowanych}}",
   "account.follows.empty": "Ten użytkownik nie obserwuje jeszcze nikogo.",
   "account.go_to_profile": "Przejdź do profilu",
   "account.hide_reblogs": "Ukryj podbicia od @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} chce zaobserwować twój profil",
   "account.share": "Udostępnij profil @{name}",
   "account.show_reblogs": "Pokazuj podbicia od @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}}",
   "account.unblock": "Odblokuj @{name}",
   "account.unblock_domain": "Odblokuj domenę {domain}",
   "account.unblock_short": "Odblokuj",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index 41112c2ca..6a6feca30 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Seguir de volta",
   "account.followers": "Seguidores",
   "account.followers.empty": "Ainda ninguém segue este utilizador.",
+  "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}",
   "account.following": "A seguir",
+  "account.following_counter": "{count, plural, one {A seguir {counter}} other {A seguir {counter}}}",
   "account.follows.empty": "Este utilizador ainda não segue ninguém.",
   "account.go_to_profile": "Ir para o perfil",
   "account.hide_reblogs": "Esconder partilhas de @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} pediu para segui-lo",
   "account.share": "Partilhar o perfil @{name}",
   "account.show_reblogs": "Mostrar partilhas de @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Desbloquear o domínio {domain}",
   "account.unblock_short": "Desbloquear",
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 6ac038c9f..96b7b3fef 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Ndiqe gjithashtu",
   "account.followers": "Ndjekës",
   "account.followers.empty": "Këtë përdorues ende s’e ndjek kush.",
+  "account.followers_counter": "{count, plural, one {{counter} ndjekës} other {{counter} ndjekës}}",
   "account.following": "Ndjekje",
+  "account.following_counter": "{count, plural, one {{counter} i ndjekur} other {{counter} të ndjekur}}",
   "account.follows.empty": "Ky përdorues ende s’ndjek kënd.",
   "account.go_to_profile": "Kalo te profili",
   "account.hide_reblogs": "Fshih përforcime nga @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} ka kërkuar t’ju ndjekë",
   "account.share": "Ndajeni profilin e @{name} me të tjerët",
   "account.show_reblogs": "Shfaq përforcime nga @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} postim} other {{counter} postime}}",
   "account.unblock": "Zhbllokoje @{name}",
   "account.unblock_domain": "Zhblloko përkatësinë {domain}",
   "account.unblock_short": "Zhbllokoje",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 7445b77ba..1833a2cfd 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -35,6 +35,7 @@
   "account.follow_back": "Följ tillbaka",
   "account.followers": "Följare",
   "account.followers.empty": "Ingen följer denna användare än.",
+  "account.followers_counter": "{count, plural, one {{counter} följare} other {{counter} följare}}",
   "account.following": "Följer",
   "account.follows.empty": "Denna användare följer inte någon än.",
   "account.go_to_profile": "Gå till profilen",
@@ -61,6 +62,7 @@
   "account.requested_follow": "{name} har begärt att följa dig",
   "account.share": "Dela @{name}s profil",
   "account.show_reblogs": "Visa boostar från @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} inlägg} other {{counter} inlägg}}",
   "account.unblock": "Avblockera @{name}",
   "account.unblock_domain": "Avblockera {domain}",
   "account.unblock_short": "Avblockera",
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index d82c92c26..2da30e662 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -293,6 +293,7 @@ nn:
       filter_by_action: Sorter etter handling
       filter_by_user: Sorter etter brukar
       title: Revisionslogg
+      unavailable_instance: "(domenenamn er utilgjengeleg)"
     announcements:
       destroyed_msg: Kunngjøringen er slettet!
       edit:

From 1bccba14082f380232a363d036b6b2e92104ffc6 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 28 Jun 2024 13:39:32 +0200
Subject: [PATCH 42/84] chore(deps): update dependency @testing-library/react
 to v16 (#30533)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
---
 app/javascript/mastodon/test_helpers.tsx |  7 ++++-
 package.json                             |  3 ++-
 yarn.lock                                | 33 +++++++++++++-----------
 3 files changed, 26 insertions(+), 17 deletions(-)

diff --git a/app/javascript/mastodon/test_helpers.tsx b/app/javascript/mastodon/test_helpers.tsx
index 93b5a8453..f40509073 100644
--- a/app/javascript/mastodon/test_helpers.tsx
+++ b/app/javascript/mastodon/test_helpers.tsx
@@ -2,6 +2,7 @@ import { IntlProvider } from 'react-intl';
 
 import { MemoryRouter } from 'react-router';
 
+import type { RenderOptions } from '@testing-library/react';
 // eslint-disable-next-line import/no-extraneous-dependencies
 import { render as rtlRender } from '@testing-library/react';
 
@@ -9,7 +10,11 @@ import { IdentityContext } from './identity_context';
 
 function render(
   ui: React.ReactElement,
-  { locale = 'en', signedIn = true, ...renderOptions } = {},
+  {
+    locale = 'en',
+    signedIn = true,
+    ...renderOptions
+  }: RenderOptions & { locale?: string; signedIn?: boolean } = {},
 ) {
   const fakeIdentity = {
     signedIn: signedIn,
diff --git a/package.json b/package.json
index 729482f85..3eb1ebad7 100644
--- a/package.json
+++ b/package.json
@@ -138,8 +138,9 @@
   },
   "devDependencies": {
     "@formatjs/cli": "^6.1.1",
+    "@testing-library/dom": "^10.2.0",
     "@testing-library/jest-dom": "^6.0.0",
-    "@testing-library/react": "^15.0.0",
+    "@testing-library/react": "^16.0.0",
     "@types/babel__core": "^7.20.1",
     "@types/emoji-mart": "^3.0.9",
     "@types/escape-html": "^1.0.2",
diff --git a/yarn.lock b/yarn.lock
index f9eaef458..0e7337328 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2768,8 +2768,9 @@ __metadata:
     "@rails/ujs": "npm:7.1.3"
     "@reduxjs/toolkit": "npm:^2.0.1"
     "@svgr/webpack": "npm:^5.5.0"
+    "@testing-library/dom": "npm:^10.2.0"
     "@testing-library/jest-dom": "npm:^6.0.0"
-    "@testing-library/react": "npm:^15.0.0"
+    "@testing-library/react": "npm:^16.0.0"
     "@types/babel__core": "npm:^7.20.1"
     "@types/emoji-mart": "npm:^3.0.9"
     "@types/escape-html": "npm:^1.0.2"
@@ -3348,9 +3349,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@testing-library/dom@npm:^10.0.0":
-  version: 10.0.0
-  resolution: "@testing-library/dom@npm:10.0.0"
+"@testing-library/dom@npm:^10.2.0":
+  version: 10.2.0
+  resolution: "@testing-library/dom@npm:10.2.0"
   dependencies:
     "@babel/code-frame": "npm:^7.10.4"
     "@babel/runtime": "npm:^7.12.5"
@@ -3360,7 +3361,7 @@ __metadata:
     dom-accessibility-api: "npm:^0.5.9"
     lz-string: "npm:^1.5.0"
     pretty-format: "npm:^27.0.2"
-  checksum: 10c0/2d12d2a6018a6f1d15e91834180bc068932c699ff1fcbfb80aa21aba519a4f5329c861dfa852e06ee5615bcb92ef2a0f0e755e32684ea3dada63bc34248382ab
+  checksum: 10c0/de582dfbeb632436547a0ca5851b5a714a4a17f8e96ab3dc4fb4e454eef52c912b648b7cb6e9fdf477f3eeef97e698f3250f0ce50846f39d04677a44169209f2
   languageName: node
   linkType: hard
 
@@ -3397,21 +3398,23 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@testing-library/react@npm:^15.0.0":
-  version: 15.0.7
-  resolution: "@testing-library/react@npm:15.0.7"
+"@testing-library/react@npm:^16.0.0":
+  version: 16.0.0
+  resolution: "@testing-library/react@npm:16.0.0"
   dependencies:
     "@babel/runtime": "npm:^7.12.5"
-    "@testing-library/dom": "npm:^10.0.0"
-    "@types/react-dom": "npm:^18.0.0"
   peerDependencies:
+    "@testing-library/dom": ^10.0.0
     "@types/react": ^18.0.0
+    "@types/react-dom": ^18.0.0
     react: ^18.0.0
     react-dom: ^18.0.0
   peerDependenciesMeta:
     "@types/react":
       optional: true
-  checksum: 10c0/ac8ee8968e81949ecb35f7ee34741c2c043f73dd7fee2247d56f6de6a30de4742af94f25264356863974e54387485b46c9448ecf3f6ca41cf4339011c369f2d4
+    "@types/react-dom":
+      optional: true
+  checksum: 10c0/297f97bf4722dad05f11d9cafd47d387dbdb096fea4b79b876c7466460f0f2e345b55b81b3e37fc81ed8185c528cb53dd8455ca1b6b019b229edf6c796f11c9f
   languageName: node
   linkType: hard
 
@@ -3430,9 +3433,9 @@ __metadata:
   linkType: hard
 
 "@types/aria-query@npm:^5.0.1":
-  version: 5.0.1
-  resolution: "@types/aria-query@npm:5.0.1"
-  checksum: 10c0/bc9e40ce37bd3a1654948778c7829bd55aea1bc5f2cd06fcf6cd650b07bb388995799e9aab6e2d93a6cf55dcba3b85c155f7ba93adefcc7c2e152fc6057061b5
+  version: 5.0.4
+  resolution: "@types/aria-query@npm:5.0.4"
+  checksum: 10c0/dc667bc6a3acc7bba2bccf8c23d56cb1f2f4defaa704cfef595437107efaa972d3b3db9ec1d66bc2711bfc35086821edd32c302bffab36f2e79b97f312069f08
   languageName: node
   linkType: hard
 
@@ -3829,7 +3832,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.2.4":
+"@types/react-dom@npm:^18.2.4":
   version: 18.3.0
   resolution: "@types/react-dom@npm:18.3.0"
   dependencies:

From ba6a558a7088b811b63a90d20b82069eee7dfaa1 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Sat, 29 Jun 2024 00:41:27 +0200
Subject: [PATCH 43/84] Simplify color extraction code using `bandunfold`
 (#30869)

---
 lib/paperclip/color_extractor.rb | 35 +++++++++++---------------------
 1 file changed, 12 insertions(+), 23 deletions(-)

diff --git a/lib/paperclip/color_extractor.rb b/lib/paperclip/color_extractor.rb
index 378af0961..fba32ba4c 100644
--- a/lib/paperclip/color_extractor.rb
+++ b/lib/paperclip/color_extractor.rb
@@ -116,34 +116,23 @@ module Paperclip
       # The number of occurrences of a color (r, g, b) is thus encoded in band `b` at pixel position `(r, g)`
       histogram = image.hist_find_ndim(bins: BINS)
 
-      # `histogram.max` returns an array of maxima with their pixel positions, but we don't know in which
-      # band they are
+      # With `bandunfold`, we get back to a (BINS*BINS)×BINS 2D image with a single band.
+      # The number of occurrences of a color (r, g, b) is thus encoded at pixel position `(r * BINS + b, g)`
+      histogram = histogram.bandunfold
+
       _, colors = histogram.max(size: 10, out_array: true, x_array: true, y_array: true)
 
-      colors['out_array'].zip(colors['x_array'], colors['y_array']).map do |v, x, y|
-        rgb_from_xyv(histogram, x, y, v)
-      end.flatten.reverse.uniq
+      colors['x_array'].zip(colors['y_array']).map do |x, y|
+        rgb_from_hist_xy(x, y)
+      end.flatten.reverse
     end
 
     # rubocop:disable Naming/MethodParameterName
-    def rgb_from_xyv(image, x, y, v)
-      pixel = image.getpoint(x, y)
-
-      # As we only have the first 2 dimensions for this maximum, we
-      # can't distinguish with different maxima with the same `r` and `g`
-      # values but different `b` values.
-      #
-      # Therefore, we return an array of maxima, which is always non-empty,
-      # but may contain multiple colors with the same values.
-
-      pixel.filter_map.with_index do |pv, z|
-        next if pv != v
-
-        r = (x + 0.5) * 256 / BINS
-        g = (y + 0.5) * 256 / BINS
-        b = (z + 0.5) * 256 / BINS
-        ColorDiff::Color::RGB.new(r, g, b)
-      end
+    def rgb_from_hist_xy(x, y)
+      r = ((x / BINS) + 0.5) * 256 / BINS
+      g = (y + 0.5) * 256 / BINS
+      b = ((x % BINS) + 0.5) * 256 / BINS
+      ColorDiff::Color::RGB.new(r, g, b)
     end
 
     def w3c_contrast(color1, color2)

From 5d4dbbcc67c98007d417cbe67b5a2261889304dc Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Sat, 29 Jun 2024 22:51:46 +0200
Subject: [PATCH 44/84] chore(deps): update dependency charlock_holmes to
 v0.7.8 (#30870)

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 dd112d018..02437eab6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -159,7 +159,7 @@ GEM
     case_transform (0.2)
       activesupport
     cbor (0.5.9.8)
-    charlock_holmes (0.7.7)
+    charlock_holmes (0.7.8)
     chewy (7.6.0)
       activesupport (>= 5.2)
       elasticsearch (>= 7.14.0, < 8)

From 8142c7aa3eb2e0705c32e862fd7db000b898bc38 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 1 Jul 2024 10:21:15 +0200
Subject: [PATCH 45/84] chore(deps): update definitelytyped types (non-major)
 (#30882)

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

diff --git a/yarn.lock b/yarn.lock
index 0e7337328..30141a6cc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3625,11 +3625,11 @@ __metadata:
   linkType: hard
 
 "@types/http-link-header@npm:^1.0.3":
-  version: 1.0.6
-  resolution: "@types/http-link-header@npm:1.0.6"
+  version: 1.0.7
+  resolution: "@types/http-link-header@npm:1.0.7"
   dependencies:
     "@types/node": "npm:*"
-  checksum: 10c0/63f3f7ab5ff6312280727ba8cf836abf5d1b76f9dc5eefc8cd4389db29d57a72fb0e028db99735ada5ccfd3c2cc6607e096b5cc142fc53c2bb5688b6295f61af
+  checksum: 10c0/ffde4514a286ee62fab86bd5164958f5c9ad9d8012eaeb4f5536efa6157e6cf9f93121d5a39bf160c3712554d945300a223a9f8b1f3fb08cc70b73f539767338
   languageName: node
   linkType: hard
 
@@ -3715,9 +3715,9 @@ __metadata:
   linkType: hard
 
 "@types/lodash@npm:^4.14.195":
-  version: 4.17.5
-  resolution: "@types/lodash@npm:4.17.5"
-  checksum: 10c0/55924803ed853e72261512bd3eaf2c5b16558c3817feb0a3125ef757afe46e54b86f33d1960e40b7606c0ddab91a96f47966bf5e6006b7abfd8994c13b04b19b
+  version: 4.17.6
+  resolution: "@types/lodash@npm:4.17.6"
+  checksum: 10c0/3b197ac47af9443fee8c4719c5ffde527d7febc018b827d44a6bc2523c728c7adfdd25196fdcfe3eed827993e0c41a917d0da6e78938b18b2be94164789f1117
   languageName: node
   linkType: hard
 

From 9c7d56db9a30e35d54974ea9257c2b48545b5217 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 1 Jul 2024 10:21:40 +0200
Subject: [PATCH 46/84] fix(deps): update dependency postcss-preset-env to
 v9.5.15 (#30878)

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

diff --git a/yarn.lock b/yarn.lock
index 30141a6cc..b650d5f49 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1558,69 +1558,69 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/cascade-layer-name-parser@npm:^1.0.11":
-  version: 1.0.11
-  resolution: "@csstools/cascade-layer-name-parser@npm:1.0.11"
+"@csstools/cascade-layer-name-parser@npm:^1.0.12":
+  version: 1.0.12
+  resolution: "@csstools/cascade-layer-name-parser@npm:1.0.12"
   peerDependencies:
-    "@csstools/css-parser-algorithms": ^2.6.3
-    "@csstools/css-tokenizer": ^2.3.1
-  checksum: 10c0/52ac8369877c8072ff5c111f656bd87e9a2a4b9e44e48fe005c26faeb6cffd83bfe2f463f4f385a2ae5cfe1f82bbf95d26ddaabca18b66c6b657c4fe1520fb43
+    "@csstools/css-parser-algorithms": ^2.7.0
+    "@csstools/css-tokenizer": ^2.3.2
+  checksum: 10c0/5f92aefcbb3f4b660cf7b0db54f6a4ba21a32fa1b64ea4f050a6370233152d4f561ecf5c8e98ca231e73c16e0d9f75b20b0a65153e18b14957658c81e0f68213
   languageName: node
   linkType: hard
 
-"@csstools/color-helpers@npm:^4.2.0":
-  version: 4.2.0
-  resolution: "@csstools/color-helpers@npm:4.2.0"
-  checksum: 10c0/3f1feac43c2ef35f38b3b06fe74e0acc130283d7efb6874f6624e45e178c1a7b3c7e39816c7421cddbffc2666430906aa6f0d3dd7c7209db1369c0afd4a29b1b
+"@csstools/color-helpers@npm:^4.2.1":
+  version: 4.2.1
+  resolution: "@csstools/color-helpers@npm:4.2.1"
+  checksum: 10c0/72e11b186ad0f6019a9b4b3752e620fa798c2a40cf47e8cad565dff46e572c9342eb8cf804542d7886344a1e540555d77f20119ace6b2d8a45b6e5ef8a41685c
   languageName: node
   linkType: hard
 
-"@csstools/css-calc@npm:^1.2.2":
-  version: 1.2.2
-  resolution: "@csstools/css-calc@npm:1.2.2"
+"@csstools/css-calc@npm:^1.2.3":
+  version: 1.2.3
+  resolution: "@csstools/css-calc@npm:1.2.3"
   peerDependencies:
-    "@csstools/css-parser-algorithms": ^2.6.3
-    "@csstools/css-tokenizer": ^2.3.1
-  checksum: 10c0/6032b482764a11c1b882d7502928950ab11760044fa7a2c23ecee802002902f6ea8fca045ee2919302af5a5c399e7baa9f68dff001ac6246ac7fef48fb3f6df7
+    "@csstools/css-parser-algorithms": ^2.7.0
+    "@csstools/css-tokenizer": ^2.3.2
+  checksum: 10c0/fb34767ea9638b837167bcecaf945bcc0c5e8f0d811067c4e8c7a57bc8f0955f61107b1ed5e017b95c54acacc8088473e5497a9986bee95b37ec92999e792871
   languageName: node
   linkType: hard
 
-"@csstools/css-color-parser@npm:^2.0.2":
-  version: 2.0.2
-  resolution: "@csstools/css-color-parser@npm:2.0.2"
+"@csstools/css-color-parser@npm:^2.0.3":
+  version: 2.0.3
+  resolution: "@csstools/css-color-parser@npm:2.0.3"
   dependencies:
-    "@csstools/color-helpers": "npm:^4.2.0"
-    "@csstools/css-calc": "npm:^1.2.2"
+    "@csstools/color-helpers": "npm:^4.2.1"
+    "@csstools/css-calc": "npm:^1.2.3"
   peerDependencies:
-    "@csstools/css-parser-algorithms": ^2.6.3
-    "@csstools/css-tokenizer": ^2.3.1
-  checksum: 10c0/c5ae4ad78745e425dce56da9f1ab053fb4f7963399735df3303305b32123bed0b2237689c2e7e99da2c62387e3226c12ea85e70e275c4027c7507e4ac929bffa
+    "@csstools/css-parser-algorithms": ^2.7.0
+    "@csstools/css-tokenizer": ^2.3.2
+  checksum: 10c0/d8860e6b9c65de4f90d4c21e4d66471fd858434cf63af80f812a900371343b753b86a256627e8bd024cb8903a6a0181d2d9c0c65ab5d78cf29d084a761e2adba
   languageName: node
   linkType: hard
 
-"@csstools/css-parser-algorithms@npm:^2.6.3":
-  version: 2.6.3
-  resolution: "@csstools/css-parser-algorithms@npm:2.6.3"
+"@csstools/css-parser-algorithms@npm:^2.6.3, @csstools/css-parser-algorithms@npm:^2.7.0":
+  version: 2.7.0
+  resolution: "@csstools/css-parser-algorithms@npm:2.7.0"
   peerDependencies:
-    "@csstools/css-tokenizer": ^2.3.1
-  checksum: 10c0/6648fda75a1c08096320fb5c04fd13656a0168de13584d2795547fecfb26c2c7d8b3b1fb79ba7aa758714851e98bfbec20d89e28697f999f41f91133eafe4207
+    "@csstools/css-tokenizer": ^2.3.2
+  checksum: 10c0/fb84fefdf37c41d170f81b687bf1ee1847a970e51cc1fe3a320e3eaf225383ae9a3c4eb6208b83357dfe18c5114353d780e0c65f05d86d6435e5a9ad9334c834
   languageName: node
   linkType: hard
 
-"@csstools/css-tokenizer@npm:^2.3.1":
-  version: 2.3.1
-  resolution: "@csstools/css-tokenizer@npm:2.3.1"
-  checksum: 10c0/fed6619fb5108e109d4dd10b0e967035a92793bae8fb84544e1342058b6df4e306d9d075623e2201fe88831b1ada797aea3546a8d12229d2d81cd7a5dfee4444
+"@csstools/css-tokenizer@npm:^2.3.1, @csstools/css-tokenizer@npm:^2.3.2":
+  version: 2.3.2
+  resolution: "@csstools/css-tokenizer@npm:2.3.2"
+  checksum: 10c0/f7d0d8b3e9e2dcdc6547a387253a09dbbacaaffb5c8718bcd7f15dddeefdd441b73fc5f9fad3f03fabef3b37ec4b62be7ff79caab366427fa90eaf54cd8fc452
   languageName: node
   linkType: hard
 
-"@csstools/media-query-list-parser@npm:^2.1.11":
-  version: 2.1.11
-  resolution: "@csstools/media-query-list-parser@npm:2.1.11"
+"@csstools/media-query-list-parser@npm:^2.1.11, @csstools/media-query-list-parser@npm:^2.1.12":
+  version: 2.1.12
+  resolution: "@csstools/media-query-list-parser@npm:2.1.12"
   peerDependencies:
-    "@csstools/css-parser-algorithms": ^2.6.3
-    "@csstools/css-tokenizer": ^2.3.1
-  checksum: 10c0/9bcd99f7d28ae3cdaba73fbbfef571b0393dd4e841f522cc796fe5161744f17e327ba1713dad3c481626fade1357c55890e3d365177abed50e857b69130a9be5
+    "@csstools/css-parser-algorithms": ^2.7.0
+    "@csstools/css-tokenizer": ^2.3.2
+  checksum: 10c0/7395cc710d8f54670c1e7a418a88dcf1ae726316272294ec645f6d79a8e931f5d390ba7ed5d0141d29ad7280cd447b8773143dc7676659413de79228130e1a65
   languageName: node
   linkType: hard
 
@@ -1636,46 +1636,46 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-color-function@npm:^3.0.16":
-  version: 3.0.16
-  resolution: "@csstools/postcss-color-function@npm:3.0.16"
+"@csstools/postcss-color-function@npm:^3.0.17":
+  version: 3.0.17
+  resolution: "@csstools/postcss-color-function@npm:3.0.17"
   dependencies:
-    "@csstools/css-color-parser": "npm:^2.0.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-color-parser": "npm:^2.0.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/postcss-progressive-custom-properties": "npm:^3.2.0"
     "@csstools/utilities": "npm:^1.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/41756a4601a3f1086290dab6ca92b54e201bd94637b54b439c66a04fd628a14e2a0bd1452ad294d2981e2f4bb306758fa5f44639b1c4332320435050749aa487
+  checksum: 10c0/6d347fc9fe65cb897c275c129103576e551b74a7c47a1a4dc8160da2fad7752bf51e3cfbff339f86b39c723efac33643168d2dfaac4d3624d072875d18a65a4b
   languageName: node
   linkType: hard
 
-"@csstools/postcss-color-mix-function@npm:^2.0.16":
-  version: 2.0.16
-  resolution: "@csstools/postcss-color-mix-function@npm:2.0.16"
+"@csstools/postcss-color-mix-function@npm:^2.0.17":
+  version: 2.0.17
+  resolution: "@csstools/postcss-color-mix-function@npm:2.0.17"
   dependencies:
-    "@csstools/css-color-parser": "npm:^2.0.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-color-parser": "npm:^2.0.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/postcss-progressive-custom-properties": "npm:^3.2.0"
     "@csstools/utilities": "npm:^1.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/70cd5b291dd615e20e4475517bf0027c90c433241397a66866f89acedb12cb91f45552a162bdd1000636ec56f7d6a099b65e44fe100fd03228fc65f17cfae285
+  checksum: 10c0/ba9a406ebe4caba6709878ee26debb06780be5cbf4e6ab7e902d79ca6e21ec6a8409b9dc0a5ef36fc4bf54bf2bd8f9642b72da8d7939145f99dc40fedd2be9d2
   languageName: node
   linkType: hard
 
-"@csstools/postcss-exponential-functions@npm:^1.0.7":
-  version: 1.0.7
-  resolution: "@csstools/postcss-exponential-functions@npm:1.0.7"
+"@csstools/postcss-exponential-functions@npm:^1.0.8":
+  version: 1.0.8
+  resolution: "@csstools/postcss-exponential-functions@npm:1.0.8"
   dependencies:
-    "@csstools/css-calc": "npm:^1.2.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-calc": "npm:^1.2.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/2079c81c3437686ef432d88502fa3a13bf8a27b7af105b4c6c2eb8e779f14adc8967a5a3ed03271ab919eeaf999fc4489fe4b37d32a8f61ab3212439517bddcc
+  checksum: 10c0/6b049801fc1275b34f43ffbb915f447a54cbff7bf48ab0705c3ad1ffde055cb876c4dc24e7a9162cd65e219457328e298a673f6176446493db17cf7af6f90dc0
   languageName: node
   linkType: hard
 
@@ -1691,46 +1691,46 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-gamut-mapping@npm:^1.0.9":
-  version: 1.0.9
-  resolution: "@csstools/postcss-gamut-mapping@npm:1.0.9"
+"@csstools/postcss-gamut-mapping@npm:^1.0.10":
+  version: 1.0.10
+  resolution: "@csstools/postcss-gamut-mapping@npm:1.0.10"
   dependencies:
-    "@csstools/css-color-parser": "npm:^2.0.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-color-parser": "npm:^2.0.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/412ae1410f3fce240401576441637c2c4e71d1a54153ac9b7a991b3de7519c253d03e10db78b09872eb10b0776d7f960b442779efabc11332b5be6672163c836
+  checksum: 10c0/6c2dab6a84f81904bed89cb584bd9bc6a904b49a4fa315b17be65c7d68baefe592561ee439660d5602b7481bac3be9a93189dc45404764524495400f34c6b6e6
   languageName: node
   linkType: hard
 
-"@csstools/postcss-gradients-interpolation-method@npm:^4.0.17":
-  version: 4.0.17
-  resolution: "@csstools/postcss-gradients-interpolation-method@npm:4.0.17"
+"@csstools/postcss-gradients-interpolation-method@npm:^4.0.18":
+  version: 4.0.18
+  resolution: "@csstools/postcss-gradients-interpolation-method@npm:4.0.18"
   dependencies:
-    "@csstools/css-color-parser": "npm:^2.0.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-color-parser": "npm:^2.0.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/postcss-progressive-custom-properties": "npm:^3.2.0"
     "@csstools/utilities": "npm:^1.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/465ac42856ca1a57aa2b9ea41ede31d9e2bcf2fe84345dbc182ae41f463069a0cfd41041b834b5133108c702cd85ecb8636b51b0b88fff8a221628639b59f386
+  checksum: 10c0/23c431068ac205392b4953dbce411e208e79e221ba8030c5e23c0b82e8fd53bc3bc4f2cdc47050f5d91a4ac69cb80f4f1853b213aa8072fa60a6cb6ff0621e04
   languageName: node
   linkType: hard
 
-"@csstools/postcss-hwb-function@npm:^3.0.15":
-  version: 3.0.15
-  resolution: "@csstools/postcss-hwb-function@npm:3.0.15"
+"@csstools/postcss-hwb-function@npm:^3.0.16":
+  version: 3.0.16
+  resolution: "@csstools/postcss-hwb-function@npm:3.0.16"
   dependencies:
-    "@csstools/css-color-parser": "npm:^2.0.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-color-parser": "npm:^2.0.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/postcss-progressive-custom-properties": "npm:^3.2.0"
     "@csstools/utilities": "npm:^1.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/fdfaeefbab1008ab1e4a98a2b45cc3db002b2724c404fa0600954b411a68b1fa4028286250bf9898eed10fa80c44e4d6b4e55f1aca073c3dfce8198a0aaedf3f
+  checksum: 10c0/4deca8831a69038aff719a77df92c53578bb28e23cc61dc4ea7b1d912b1b685683a9c6232396c2616948ac2e8488ad1e2009c9c8ed30c493d97ba8ad37b6418d
   languageName: node
   linkType: hard
 
@@ -1768,17 +1768,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-light-dark-function@npm:^1.0.5":
-  version: 1.0.5
-  resolution: "@csstools/postcss-light-dark-function@npm:1.0.5"
+"@csstools/postcss-light-dark-function@npm:^1.0.6":
+  version: 1.0.6
+  resolution: "@csstools/postcss-light-dark-function@npm:1.0.6"
   dependencies:
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/postcss-progressive-custom-properties": "npm:^3.2.0"
     "@csstools/utilities": "npm:^1.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/4fbeda98372d0da25d3ed87da09903c9a0a5d0b8c13cc9de82a98acce4a8f8367e5ba33bfc25c2534d10f2b1db9d5b4278df4ebab755e27ef2b03a95e0ebe264
+  checksum: 10c0/6b2c64860d789cd3e3ce875c01259333911f6e32a751a7475604de8022c13abcb578e5cb941b51bd3a2022bee883df3f6b64800c6e3559b06da283d968aeb615
   languageName: node
   linkType: hard
 
@@ -1820,42 +1820,42 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-logical-viewport-units@npm:^2.0.9":
-  version: 2.0.9
-  resolution: "@csstools/postcss-logical-viewport-units@npm:2.0.9"
+"@csstools/postcss-logical-viewport-units@npm:^2.0.10":
+  version: 2.0.10
+  resolution: "@csstools/postcss-logical-viewport-units@npm:2.0.10"
   dependencies:
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/utilities": "npm:^1.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/25b01e36b08c571806d09046be63582dbebf97a4612df59be405fa8a92e6eebcd4e768ad7fbe53b0b8739d6ab04d56957964fb04d6a3ea129fc5f72e6d0adf95
+  checksum: 10c0/fe142b11e0e8ccab4667cc5db90b45e67b7d11eaf5c038e91d867e1b18a315ef0859114185aeb48fdc1ce05986be8b644d6157fe9e19da7281f7023c99eb8877
   languageName: node
   linkType: hard
 
-"@csstools/postcss-media-minmax@npm:^1.1.6":
-  version: 1.1.6
-  resolution: "@csstools/postcss-media-minmax@npm:1.1.6"
+"@csstools/postcss-media-minmax@npm:^1.1.7":
+  version: 1.1.7
+  resolution: "@csstools/postcss-media-minmax@npm:1.1.7"
   dependencies:
-    "@csstools/css-calc": "npm:^1.2.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
-    "@csstools/media-query-list-parser": "npm:^2.1.11"
+    "@csstools/css-calc": "npm:^1.2.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
+    "@csstools/media-query-list-parser": "npm:^2.1.12"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/2cbfb3728a232c655d82f63d5ac7da36876d14e5fee5d62a0738efed40c58f20ef11f600395ade24d5063d750e8e093251dd93cc361f782b5a6c0e0f80288f51
+  checksum: 10c0/a02943a17b540cbd909b55bbb1f8c9331badc51b613279bbdb7127c9921a5d0675bb41675a3b4d0f15e9586120e5a96d9b9786b63b2c594fbb3a238e860c6ad8
   languageName: node
   linkType: hard
 
-"@csstools/postcss-media-queries-aspect-ratio-number-values@npm:^2.0.9":
-  version: 2.0.9
-  resolution: "@csstools/postcss-media-queries-aspect-ratio-number-values@npm:2.0.9"
+"@csstools/postcss-media-queries-aspect-ratio-number-values@npm:^2.0.10":
+  version: 2.0.10
+  resolution: "@csstools/postcss-media-queries-aspect-ratio-number-values@npm:2.0.10"
   dependencies:
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
-    "@csstools/media-query-list-parser": "npm:^2.1.11"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
+    "@csstools/media-query-list-parser": "npm:^2.1.12"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/d431d2900a7177c938d9dc2d5bdf3c1930758adc214cc72f94b34e6bbd02fd917c200dc81482db515519c97d4f1e766ba3200f3ec9b55081887f2f8111f68e20
+  checksum: 10c0/d7879e72df98d9fe2e5d85a64837e7a73c2df1aea8659d65516f0acb070317edd353531882f0bdfd81510703d1da30d6da861052a0bda85fde1f9eab94b1e467
   languageName: node
   linkType: hard
 
@@ -1882,18 +1882,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-oklab-function@npm:^3.0.16":
-  version: 3.0.16
-  resolution: "@csstools/postcss-oklab-function@npm:3.0.16"
+"@csstools/postcss-oklab-function@npm:^3.0.17":
+  version: 3.0.17
+  resolution: "@csstools/postcss-oklab-function@npm:3.0.17"
   dependencies:
-    "@csstools/css-color-parser": "npm:^2.0.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-color-parser": "npm:^2.0.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/postcss-progressive-custom-properties": "npm:^3.2.0"
     "@csstools/utilities": "npm:^1.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/9c67ee5f51116df16ab6baffa1b3c6c7aa93d53b836f421125ae8824075bd3cfaa1a93594466de0ac935c89c4fc8171e80974e1a15bafa23ea864e4cf1f1c1f2
+  checksum: 10c0/ff27a4b6fd8490439aa0f3c91ffa2a42a8cf539d7306d9329cef7ca59f28317cee40253f402d19a18c196471fd39a05842d2974d92f1b131dc748074d91ac4ee
   languageName: node
   linkType: hard
 
@@ -1908,18 +1908,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-relative-color-syntax@npm:^2.0.16":
-  version: 2.0.16
-  resolution: "@csstools/postcss-relative-color-syntax@npm:2.0.16"
+"@csstools/postcss-relative-color-syntax@npm:^2.0.17":
+  version: 2.0.17
+  resolution: "@csstools/postcss-relative-color-syntax@npm:2.0.17"
   dependencies:
-    "@csstools/css-color-parser": "npm:^2.0.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-color-parser": "npm:^2.0.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/postcss-progressive-custom-properties": "npm:^3.2.0"
     "@csstools/utilities": "npm:^1.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/cdc965706212dcbc03394f55c79a0ad043d1e0174059c4d0d90e4267fe8e6fd9eef7cfed4f5bbc1f8e89c225c1c042ae792e115bba198eb2daae763d65f44679
+  checksum: 10c0/46226351b3825323e3496dcee44ff354cd3ccc9241d837659e1311f428f0b4dc878d9bb762cbb8f63243b7af346728ab7a46c311f9dc38bb609147523c698eab
   languageName: node
   linkType: hard
 
@@ -1934,41 +1934,41 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/postcss-stepped-value-functions@npm:^3.0.8":
-  version: 3.0.8
-  resolution: "@csstools/postcss-stepped-value-functions@npm:3.0.8"
+"@csstools/postcss-stepped-value-functions@npm:^3.0.9":
+  version: 3.0.9
+  resolution: "@csstools/postcss-stepped-value-functions@npm:3.0.9"
   dependencies:
-    "@csstools/css-calc": "npm:^1.2.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-calc": "npm:^1.2.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/2be66aa769808245137be8ff14308aa17c3a0d75433f6fd6789114966a78c365dbf173d087e7ff5bc80118c75be2ff740baab83ed39fc0671980f6217779956b
+  checksum: 10c0/bafe80947abc8613903f1f3f1939ece9780696774f15960aef229733e40e483dc2830145426d49c4f6d0b1dabb35f812c8a2dda0d0dcddc930321e36b5c6ca0b
   languageName: node
   linkType: hard
 
-"@csstools/postcss-text-decoration-shorthand@npm:^3.0.6":
-  version: 3.0.6
-  resolution: "@csstools/postcss-text-decoration-shorthand@npm:3.0.6"
+"@csstools/postcss-text-decoration-shorthand@npm:^3.0.7":
+  version: 3.0.7
+  resolution: "@csstools/postcss-text-decoration-shorthand@npm:3.0.7"
   dependencies:
-    "@csstools/color-helpers": "npm:^4.2.0"
+    "@csstools/color-helpers": "npm:^4.2.1"
     postcss-value-parser: "npm:^4.2.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/5abdc4fad1c3f15e9d47c7af3995dec9cdf4e6f87c5857eb2e149764779b8389f4f4b21d11e6f2509c57c554a0dc5c11f68f212acd04bbc47defa15911ac3eb9
+  checksum: 10c0/072b9893ca2409aa16e53e84747d7b7e13071ce19738a0800a139bf71b535e439958d9093df2b85f83eee2e0c44bc22a14bf3a39b5a7508bca9e747a12273d02
   languageName: node
   linkType: hard
 
-"@csstools/postcss-trigonometric-functions@npm:^3.0.8":
-  version: 3.0.8
-  resolution: "@csstools/postcss-trigonometric-functions@npm:3.0.8"
+"@csstools/postcss-trigonometric-functions@npm:^3.0.9":
+  version: 3.0.9
+  resolution: "@csstools/postcss-trigonometric-functions@npm:3.0.9"
   dependencies:
-    "@csstools/css-calc": "npm:^1.2.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-calc": "npm:^1.2.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/aeed8d1026f4a5cb7afafbadd739af84291d5bfcbcdef2f79b77174f003d0cd0c7f9deb3fe0b9377efab37ce9bb17a2499efd4af8211f5ff9eb01b878b0b62b3
+  checksum: 10c0/7a439d31a63d35986dab634d9e415f7ce7c32a2d3d382052b5b730a259a12e44c5f1b14e318d79086253e3d5d4f7d942d0e7317d92eb3421dd08824eebec45fb
   languageName: node
   linkType: hard
 
@@ -5646,7 +5646,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"browserslist@npm:^4.0.0, browserslist@npm:^4.22.2, browserslist@npm:^4.22.3, browserslist@npm:^4.23.0, browserslist@npm:^4.23.1":
+"browserslist@npm:^4.0.0, browserslist@npm:^4.22.2, browserslist@npm:^4.23.0, browserslist@npm:^4.23.1":
   version: 4.23.1
   resolution: "browserslist@npm:4.23.1"
   dependencies:
@@ -13208,18 +13208,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-color-functional-notation@npm:^6.0.11":
-  version: 6.0.11
-  resolution: "postcss-color-functional-notation@npm:6.0.11"
+"postcss-color-functional-notation@npm:^6.0.12":
+  version: 6.0.12
+  resolution: "postcss-color-functional-notation@npm:6.0.12"
   dependencies:
-    "@csstools/css-color-parser": "npm:^2.0.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-color-parser": "npm:^2.0.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/postcss-progressive-custom-properties": "npm:^3.2.0"
     "@csstools/utilities": "npm:^1.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/7fd75e6881cf62f536f79dfc0ae1b709ea0b8b84833cce1671372711f6019ab4360c6a17089b654b2d376b87e7f9455b94f0d13b45ab0ab767e547b604709b3d
+  checksum: 10c0/2e8faecd2609e1b4eb8c1cab21ecca5e746916795df20e6997d66eb61c29fbb01d3e75fef3e0b3e1c181918a2186570441b81779b1fc429d6d8823fbfa164231
   languageName: node
   linkType: hard
 
@@ -13273,46 +13273,46 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-custom-media@npm:^10.0.6":
-  version: 10.0.6
-  resolution: "postcss-custom-media@npm:10.0.6"
+"postcss-custom-media@npm:^10.0.7":
+  version: 10.0.7
+  resolution: "postcss-custom-media@npm:10.0.7"
   dependencies:
-    "@csstools/cascade-layer-name-parser": "npm:^1.0.11"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
-    "@csstools/media-query-list-parser": "npm:^2.1.11"
+    "@csstools/cascade-layer-name-parser": "npm:^1.0.12"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
+    "@csstools/media-query-list-parser": "npm:^2.1.12"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/98a524bc46b780a86094bbe8007f1e577137da5490823631a683d4b3df4a13e40c5e1ab52380275a54f7011abfd98bb597c6293d964c14f9f22ec6cf9d75c550
+  checksum: 10c0/4171385ab9370806861dcf7597e53fd6aa1862e77b475c9c565c95bfcc2b950f920f8da26a6dbec42e257388bca97c274635662b5e81fe3905b5e37babe06569
   languageName: node
   linkType: hard
 
-"postcss-custom-properties@npm:^13.3.10":
-  version: 13.3.10
-  resolution: "postcss-custom-properties@npm:13.3.10"
+"postcss-custom-properties@npm:^13.3.11":
+  version: 13.3.11
+  resolution: "postcss-custom-properties@npm:13.3.11"
   dependencies:
-    "@csstools/cascade-layer-name-parser": "npm:^1.0.11"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/cascade-layer-name-parser": "npm:^1.0.12"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/utilities": "npm:^1.0.0"
     postcss-value-parser: "npm:^4.2.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/52688fd0aaadccfdf4a3d86d3a2ab988163e8108088c5e33fc9145d261f75b92b8321c044a8161345abda10df5715d674330309dcc0c17f2980db5515f6a76d6
+  checksum: 10c0/4aa95628aa5d5b6df4dfeedbc3891b9666db88d75930cadc14d2fbba0a1b72f4e3cc3d83b5a0c0b8ce44f85b4fda6ebd7fe7792a1abc0a14d7d63b9f170d299c
   languageName: node
   linkType: hard
 
-"postcss-custom-selectors@npm:^7.1.10":
-  version: 7.1.10
-  resolution: "postcss-custom-selectors@npm:7.1.10"
+"postcss-custom-selectors@npm:^7.1.11":
+  version: 7.1.11
+  resolution: "postcss-custom-selectors@npm:7.1.11"
   dependencies:
-    "@csstools/cascade-layer-name-parser": "npm:^1.0.11"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
-    postcss-selector-parser: "npm:^6.0.13"
+    "@csstools/cascade-layer-name-parser": "npm:^1.0.12"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
+    postcss-selector-parser: "npm:^6.1.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/11311ae6f306420223c6bf926fb1798738f3aa525a267de204de8e8ee9de467bf63b580d9ad5dbb0fff4bd9266770a3fa7e27a24af08a2e0a4115d0727d1d043
+  checksum: 10c0/f37d2e34239e868b35b7970ec97a7a8f657a9f92ed2b221af44f19949f7c3aedcecd0abb5fa1acb120c5ceffdf7a20869338956a37d7bfc37a83d8088f5d3dd2
   languageName: node
   linkType: hard
 
@@ -13430,18 +13430,18 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-lab-function@npm:^6.0.16":
-  version: 6.0.16
-  resolution: "postcss-lab-function@npm:6.0.16"
+"postcss-lab-function@npm:^6.0.17":
+  version: 6.0.17
+  resolution: "postcss-lab-function@npm:6.0.17"
   dependencies:
-    "@csstools/css-color-parser": "npm:^2.0.2"
-    "@csstools/css-parser-algorithms": "npm:^2.6.3"
-    "@csstools/css-tokenizer": "npm:^2.3.1"
+    "@csstools/css-color-parser": "npm:^2.0.3"
+    "@csstools/css-parser-algorithms": "npm:^2.7.0"
+    "@csstools/css-tokenizer": "npm:^2.3.2"
     "@csstools/postcss-progressive-custom-properties": "npm:^3.2.0"
     "@csstools/utilities": "npm:^1.0.0"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/ba8717cd8a197ec17acaac1b61631cd4403f07bd406b0c92f2e430a55e3f786cd6c338b626c3326e9178a0f3e58ff838ebaded19f480f39197a9cb17349ecdcd
+  checksum: 10c0/a331f188b02cc8beb315150232b6b58bc5793e8d61585973d352a9b4d370b908ff354ccf9ea1ba20a956fd37ea4ada918ea975c8d4f69e850d26edf0106436e8
   languageName: node
   linkType: hard
 
@@ -13762,52 +13762,52 @@ __metadata:
   linkType: hard
 
 "postcss-preset-env@npm:^9.5.2":
-  version: 9.5.14
-  resolution: "postcss-preset-env@npm:9.5.14"
+  version: 9.5.15
+  resolution: "postcss-preset-env@npm:9.5.15"
   dependencies:
     "@csstools/postcss-cascade-layers": "npm:^4.0.6"
-    "@csstools/postcss-color-function": "npm:^3.0.16"
-    "@csstools/postcss-color-mix-function": "npm:^2.0.16"
-    "@csstools/postcss-exponential-functions": "npm:^1.0.7"
+    "@csstools/postcss-color-function": "npm:^3.0.17"
+    "@csstools/postcss-color-mix-function": "npm:^2.0.17"
+    "@csstools/postcss-exponential-functions": "npm:^1.0.8"
     "@csstools/postcss-font-format-keywords": "npm:^3.0.2"
-    "@csstools/postcss-gamut-mapping": "npm:^1.0.9"
-    "@csstools/postcss-gradients-interpolation-method": "npm:^4.0.17"
-    "@csstools/postcss-hwb-function": "npm:^3.0.15"
+    "@csstools/postcss-gamut-mapping": "npm:^1.0.10"
+    "@csstools/postcss-gradients-interpolation-method": "npm:^4.0.18"
+    "@csstools/postcss-hwb-function": "npm:^3.0.16"
     "@csstools/postcss-ic-unit": "npm:^3.0.6"
     "@csstools/postcss-initial": "npm:^1.0.1"
     "@csstools/postcss-is-pseudo-class": "npm:^4.0.8"
-    "@csstools/postcss-light-dark-function": "npm:^1.0.5"
+    "@csstools/postcss-light-dark-function": "npm:^1.0.6"
     "@csstools/postcss-logical-float-and-clear": "npm:^2.0.1"
     "@csstools/postcss-logical-overflow": "npm:^1.0.1"
     "@csstools/postcss-logical-overscroll-behavior": "npm:^1.0.1"
     "@csstools/postcss-logical-resize": "npm:^2.0.1"
-    "@csstools/postcss-logical-viewport-units": "npm:^2.0.9"
-    "@csstools/postcss-media-minmax": "npm:^1.1.6"
-    "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^2.0.9"
+    "@csstools/postcss-logical-viewport-units": "npm:^2.0.10"
+    "@csstools/postcss-media-minmax": "npm:^1.1.7"
+    "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^2.0.10"
     "@csstools/postcss-nested-calc": "npm:^3.0.2"
     "@csstools/postcss-normalize-display-values": "npm:^3.0.2"
-    "@csstools/postcss-oklab-function": "npm:^3.0.16"
+    "@csstools/postcss-oklab-function": "npm:^3.0.17"
     "@csstools/postcss-progressive-custom-properties": "npm:^3.2.0"
-    "@csstools/postcss-relative-color-syntax": "npm:^2.0.16"
+    "@csstools/postcss-relative-color-syntax": "npm:^2.0.17"
     "@csstools/postcss-scope-pseudo-class": "npm:^3.0.1"
-    "@csstools/postcss-stepped-value-functions": "npm:^3.0.8"
-    "@csstools/postcss-text-decoration-shorthand": "npm:^3.0.6"
-    "@csstools/postcss-trigonometric-functions": "npm:^3.0.8"
+    "@csstools/postcss-stepped-value-functions": "npm:^3.0.9"
+    "@csstools/postcss-text-decoration-shorthand": "npm:^3.0.7"
+    "@csstools/postcss-trigonometric-functions": "npm:^3.0.9"
     "@csstools/postcss-unset-value": "npm:^3.0.1"
     autoprefixer: "npm:^10.4.19"
-    browserslist: "npm:^4.22.3"
+    browserslist: "npm:^4.23.1"
     css-blank-pseudo: "npm:^6.0.2"
     css-has-pseudo: "npm:^6.0.5"
     css-prefers-color-scheme: "npm:^9.0.1"
     cssdb: "npm:^8.0.0"
     postcss-attribute-case-insensitive: "npm:^6.0.3"
     postcss-clamp: "npm:^4.1.0"
-    postcss-color-functional-notation: "npm:^6.0.11"
+    postcss-color-functional-notation: "npm:^6.0.12"
     postcss-color-hex-alpha: "npm:^9.0.4"
     postcss-color-rebeccapurple: "npm:^9.0.3"
-    postcss-custom-media: "npm:^10.0.6"
-    postcss-custom-properties: "npm:^13.3.10"
-    postcss-custom-selectors: "npm:^7.1.10"
+    postcss-custom-media: "npm:^10.0.7"
+    postcss-custom-properties: "npm:^13.3.11"
+    postcss-custom-selectors: "npm:^7.1.11"
     postcss-dir-pseudo-class: "npm:^8.0.1"
     postcss-double-position-gradients: "npm:^5.0.6"
     postcss-focus-visible: "npm:^9.0.1"
@@ -13815,7 +13815,7 @@ __metadata:
     postcss-font-variant: "npm:^5.0.0"
     postcss-gap-properties: "npm:^5.0.1"
     postcss-image-set-function: "npm:^6.0.3"
-    postcss-lab-function: "npm:^6.0.16"
+    postcss-lab-function: "npm:^6.0.17"
     postcss-logical: "npm:^7.0.1"
     postcss-nesting: "npm:^12.1.5"
     postcss-opacity-percentage: "npm:^2.0.0"
@@ -13827,7 +13827,7 @@ __metadata:
     postcss-selector-not: "npm:^7.0.2"
   peerDependencies:
     postcss: ^8.4
-  checksum: 10c0/8e0c8f5c2e7b8385a770c13185986dc50d7a73b10b98c65c2f86bb4cd2860de722caef8172b1676962dafbbc044d6be1955f2a092e951976a30d4ee33b0d7571
+  checksum: 10c0/e2ee0b5d7dbaddb82ff6d51b5882120862d6be184973ae3d55642923183ab441d421d5f9810fe02e680a70dbc85b20b1c2eb02c68f167dcaf3ef80a71dd40e78
   languageName: node
   linkType: hard
 

From 3c1e1685c73fa69dec719cb0bf2847de6df31dd1 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 1 Jul 2024 10:21:55 +0200
Subject: [PATCH 47/84] fix(deps): update dependency postcss to v8.4.39
 (#30877)

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

diff --git a/yarn.lock b/yarn.lock
index b650d5f49..31b812df6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13951,13 +13951,13 @@ __metadata:
   linkType: hard
 
 "postcss@npm:^8.2.15, postcss@npm:^8.4.24, postcss@npm:^8.4.38":
-  version: 8.4.38
-  resolution: "postcss@npm:8.4.38"
+  version: 8.4.39
+  resolution: "postcss@npm:8.4.39"
   dependencies:
     nanoid: "npm:^3.3.7"
-    picocolors: "npm:^1.0.0"
+    picocolors: "npm:^1.0.1"
     source-map-js: "npm:^1.2.0"
-  checksum: 10c0/955407b8f70cf0c14acf35dab3615899a2a60a26718a63c848cf3c29f2467b0533991b985a2b994430d890bd7ec2b1963e36352b0774a19143b5f591540f7c06
+  checksum: 10c0/16f5ac3c4e32ee76d1582b3c0dcf1a1fdb91334a45ad755eeb881ccc50318fb8d64047de4f1601ac96e30061df203f0f2e2edbdc0bfc49b9c57bc9fb9bedaea3
   languageName: node
   linkType: hard
 

From 2f1df842f8ba3ec064a560685f51489aee3a5ecf Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 1 Jul 2024 10:25:52 +0200
Subject: [PATCH 48/84] chore(deps): update dependency test-prof to v1.3.3.1
 (#30872)

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 02437eab6..a1340537c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -833,7 +833,7 @@ GEM
       unicode-display_width (>= 1.1.1, < 3)
     terrapin (1.0.1)
       climate_control
-    test-prof (1.3.3)
+    test-prof (1.3.3.1)
     thor (1.3.1)
     tilt (2.3.0)
     timeout (0.4.1)

From 4c701463b4717acfdf37d5992bf1d729c2221011 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 1 Jul 2024 08:26:08 +0000
Subject: [PATCH 49/84] chore(deps): update dependency aws-sdk-s3 to v1.155.0
 (#30871)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index a1340537c..1d73a8ef5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -100,8 +100,8 @@ GEM
     attr_required (1.0.2)
     awrence (1.2.1)
     aws-eventstream (1.3.0)
-    aws-partitions (1.947.0)
-    aws-sdk-core (3.199.0)
+    aws-partitions (1.949.0)
+    aws-sdk-core (3.200.0)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.8)
@@ -109,7 +109,7 @@ GEM
     aws-sdk-kms (1.87.0)
       aws-sdk-core (~> 3, >= 3.199.0)
       aws-sigv4 (~> 1.1)
-    aws-sdk-s3 (1.154.0)
+    aws-sdk-s3 (1.155.0)
       aws-sdk-core (~> 3, >= 3.199.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.8)

From ed15ae075c00b72388d06f1d62b87a6ed2028e57 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 1 Jul 2024 08:27:03 +0000
Subject: [PATCH 50/84] fix(deps): update dependency @reduxjs/toolkit to v2.2.6
 (#30875)

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 31b812df6..5b4f3c22b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3058,8 +3058,8 @@ __metadata:
   linkType: hard
 
 "@reduxjs/toolkit@npm:^2.0.1":
-  version: 2.2.5
-  resolution: "@reduxjs/toolkit@npm:2.2.5"
+  version: 2.2.6
+  resolution: "@reduxjs/toolkit@npm:2.2.6"
   dependencies:
     immer: "npm:^10.0.3"
     redux: "npm:^5.0.1"
@@ -3073,7 +3073,7 @@ __metadata:
       optional: true
     react-redux:
       optional: true
-  checksum: 10c0/be0593bf26852482fb8716b9248531466c6e8782a3114b823ae680fce90267d8c5512a3231cfecc30b17eff81a4604112772b49ad7ca6a3366ddd4f2a838e53c
+  checksum: 10c0/60af753e6d02c8acd3c5bc843c846d60b19821d93ff9f4415fa7011ebf17a85301ed42132fabc1aaee8523d8f61418b5ba164a11c31ab29937e485842d3744a0
   languageName: node
   linkType: hard
 

From 7756db65519136d92b04b8709681f62a971836f7 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 1 Jul 2024 08:34:38 +0000
Subject: [PATCH 51/84] New Crowdin Translations (automated) (#30873)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/br.json |  2 ++
 app/javascript/mastodon/locales/cs.json | 18 +++++++++++++-----
 app/javascript/mastodon/locales/de.json |  2 +-
 app/javascript/mastodon/locales/fo.json |  3 +++
 app/javascript/mastodon/locales/ko.json |  3 +++
 app/javascript/mastodon/locales/th.json |  3 +++
 app/javascript/mastodon/locales/vi.json |  3 +++
 config/locales/cs.yml                   |  3 +++
 config/locales/doorkeeper.cs.yml        |  1 +
 9 files changed, 32 insertions(+), 6 deletions(-)

diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json
index c919d2e9d..a150fb490 100644
--- a/app/javascript/mastodon/locales/br.json
+++ b/app/javascript/mastodon/locales/br.json
@@ -35,6 +35,7 @@
   "account.follow_back": "Heuliañ d'ho tro",
   "account.followers": "Tud koumanantet",
   "account.followers.empty": "Den na heul an implijer·ez-mañ c'hoazh.",
+  "account.followers_counter": "{count, plural, one {{counter} heulier} two {{counter} heulier} few {{counter} heulier} many {{counter} heulier} other {{counter} heulier}}",
   "account.following": "Koumanantoù",
   "account.follows.empty": "An implijer·ez-mañ na heul den ebet.",
   "account.go_to_profile": "Gwelet ar profil",
@@ -60,6 +61,7 @@
   "account.requested_follow": "Gant {name} eo bet goulennet ho heuliañ",
   "account.share": "Skignañ profil @{name}",
   "account.show_reblogs": "Diskouez skignadennoù @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} embannadur} two {{counter} embannadur} few {{counter} embannadur} many {{counter} embannadur} other {{counter} embannadur}}",
   "account.unblock": "Diverzañ @{name}",
   "account.unblock_domain": "Diverzañ an domani {domain}",
   "account.unblock_short": "Distankañ",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index 66aa1fe0a..e96e28397 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -20,7 +20,7 @@
   "account.block_short": "Zablokovat",
   "account.blocked": "Blokovaný",
   "account.browse_more_on_origin_server": "Více na původním profilu",
-  "account.cancel_follow_request": "Zrušit žádost o sledování",
+  "account.cancel_follow_request": "Zrušit sledování",
   "account.copy": "Kopírovat odkaz na profil",
   "account.direct": "Soukromě zmínit @{name}",
   "account.disable_notifications": "Přestat mě upozorňovat, když @{name} zveřejní příspěvek",
@@ -35,7 +35,9 @@
   "account.follow_back": "Také sledovat",
   "account.followers": "Sledující",
   "account.followers.empty": "Tohoto uživatele zatím nikdo nesleduje.",
+  "account.followers_counter": "{count, plural, one {{counter} sledující} few {{counter} sledující} many {{counter} sledujících} other {{counter} sledujících}}",
   "account.following": "Sledujete",
+  "account.following_counter": "{count, plural, one {{counter} sledovaný} few {{counter} sledovaní} many {{counter} sledovaných} other {{counter} sledovaných}}",
   "account.follows.empty": "Tento uživatel zatím nikoho nesleduje.",
   "account.go_to_profile": "Přejít na profil",
   "account.hide_reblogs": "Skrýt boosty od @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} tě požádal o sledování",
   "account.share": "Sdílet profil @{name}",
   "account.show_reblogs": "Zobrazit boosty od @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",
   "account.unblock": "Odblokovat @{name}",
   "account.unblock_domain": "Odblokovat doménu {domain}",
   "account.unblock_short": "Odblokovat",
@@ -75,9 +78,9 @@
   "admin.dashboard.retention.average": "Průměr",
   "admin.dashboard.retention.cohort": "Měsíc registrace",
   "admin.dashboard.retention.cohort_size": "Noví uživatelé",
-  "admin.impact_report.instance_accounts": "Profily účtů, které by odstranily",
-  "admin.impact_report.instance_followers": "Sledovatelé, o které by naši uživatelé přišli",
-  "admin.impact_report.instance_follows": "Následovníci jejich uživatelé by ztratili",
+  "admin.impact_report.instance_accounts": "Profily účtů, které by byli odstaněny",
+  "admin.impact_report.instance_followers": "Sledující, o které by naši uživatelé přišli",
+  "admin.impact_report.instance_follows": "Sledující, o které by naši uživatelé přišli",
   "admin.impact_report.title": "Shrnutí dopadu",
   "alert.rate_limited.message": "Zkuste to prosím znovu po {retry_time, time, medium}.",
   "alert.rate_limited.title": "Spojení omezena",
@@ -86,7 +89,7 @@
   "announcement.announcement": "Oznámení",
   "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 být stále 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 stále být 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.",
@@ -411,6 +414,8 @@
   "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.account.add": "Přidat do seznamu",
   "lists.account.remove": "Odebrat ze seznamu",
   "lists.delete": "Smazat seznam",
@@ -691,8 +696,11 @@
   "server_banner.about_active_users": "Lidé používající tento server během posledních 30 dní (měsíční aktivní uživatelé)",
   "server_banner.active_users": "aktivní uživatelé",
   "server_banner.administered_by": "Spravováno:",
+  "server_banner.is_one_of_many": "{domain} je jedním z mnoha Mastodon serverů, které můžete použít k účasti na fediversu.",
   "server_banner.server_stats": "Statistiky serveru:",
   "sign_in_banner.create_account": "Vytvořit účet",
+  "sign_in_banner.follow_anyone": "Sledujte kohokoli napříč fediversem a uvidíte vše v chronologickém pořadí. Bez algoritmů, reklam a clickbaitu.",
+  "sign_in_banner.mastodon_is": "Mastodon je ten nejlepší způsob, jak udržet krok s tím, co se právě děje.",
   "sign_in_banner.sign_in": "Přihlásit se",
   "sign_in_banner.sso_redirect": "Přihlášení nebo Registrace",
   "status.admin_account": "Otevřít moderátorské rozhraní pro @{name}",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 4a5b666d3..86438757a 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -37,7 +37,7 @@
   "account.followers.empty": "Diesem Profil folgt noch niemand.",
   "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}",
   "account.following": "Folge ich",
-  "account.following_counter": "{count, plural, one {{counter} folge ich} other {{counter} folge ich}}",
+  "account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}",
   "account.follows.empty": "Dieses Profil folgt noch niemandem.",
   "account.go_to_profile": "Profil aufrufen",
   "account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden",
diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json
index e7786f388..c27ffe0aa 100644
--- a/app/javascript/mastodon/locales/fo.json
+++ b/app/javascript/mastodon/locales/fo.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Fylg aftur",
   "account.followers": "Fylgjarar",
   "account.followers.empty": "Ongar fylgjarar enn.",
+  "account.followers_counter": "{count, plural, one {{counter} fylgjari} other {{counter} fylgjarar}}",
   "account.following": "Fylgir",
+  "account.following_counter": "{count, plural, one {{counter} fylgir} other {{counter} fylgja}}",
   "account.follows.empty": "Hesin brúkari fylgir ongum enn.",
   "account.go_to_profile": "Far til vanga",
   "account.hide_reblogs": "Fjal lyft frá @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} hevur biðið um at fylgja tær",
   "account.share": "Deil vanga @{name}'s",
   "account.show_reblogs": "Vís lyft frá @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} postur} other {{counter} postar}}",
   "account.unblock": "Banna ikki @{name}",
   "account.unblock_domain": "Banna ikki økisnavnið {domain}",
   "account.unblock_short": "Banna ikki",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 90755666b..fe3582c1d 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -35,7 +35,9 @@
   "account.follow_back": "맞팔로우 하기",
   "account.followers": "팔로워",
   "account.followers.empty": "아직 아무도 이 사용자를 팔로우하고 있지 않습니다.",
+  "account.followers_counter": "{count, plural, other {{counter} 팔로워}}",
   "account.following": "팔로잉",
+  "account.following_counter": "{count, plural, other {{counter} 팔로잉}}",
   "account.follows.empty": "이 사용자는 아직 아무도 팔로우하고 있지 않습니다.",
   "account.go_to_profile": "프로필로 이동",
   "account.hide_reblogs": "@{name}의 부스트를 숨기기",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} 님이 팔로우 요청을 보냈습니다",
   "account.share": "@{name}의 프로필 공유",
   "account.show_reblogs": "@{name}의 부스트 보기",
+  "account.statuses_counter": "{count, plural, other {{counter} 게시물}}",
   "account.unblock": "차단 해제",
   "account.unblock_domain": "도메인 {domain} 차단 해제",
   "account.unblock_short": "차단 해제",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index e1d556ebf..64abb394b 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -35,7 +35,9 @@
   "account.follow_back": "ติดตามกลับ",
   "account.followers": "ผู้ติดตาม",
   "account.followers.empty": "ยังไม่มีใครติดตามผู้ใช้นี้",
+  "account.followers_counter": "{count, plural, other {{counter} ผู้ติดตาม}}",
   "account.following": "กำลังติดตาม",
+  "account.following_counter": "{count, plural, other {{counter} กำลังติดตาม}}",
   "account.follows.empty": "ผู้ใช้นี้ยังไม่ได้ติดตามใคร",
   "account.go_to_profile": "ไปยังโปรไฟล์",
   "account.hide_reblogs": "ซ่อนการดันจาก @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} ได้ขอติดตามคุณ",
   "account.share": "แชร์โปรไฟล์ของ @{name}",
   "account.show_reblogs": "แสดงการดันจาก @{name}",
+  "account.statuses_counter": "{count, plural, other {{counter} โพสต์}}",
   "account.unblock": "เลิกปิดกั้น @{name}",
   "account.unblock_domain": "เลิกปิดกั้นโดเมน {domain}",
   "account.unblock_short": "เลิกปิดกั้น",
diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json
index bbfecf2c8..70932d10b 100644
--- a/app/javascript/mastodon/locales/vi.json
+++ b/app/javascript/mastodon/locales/vi.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Theo dõi lại",
   "account.followers": "Người theo dõi",
   "account.followers.empty": "Chưa có người theo dõi nào.",
+  "account.followers_counter": "{count, plural, other {{counter} người theo dõi}}",
   "account.following": "Đang theo dõi",
+  "account.following_counter": "{count, plural, other {{counter} đang theo dõi}}",
   "account.follows.empty": "Người này chưa theo dõi ai.",
   "account.go_to_profile": "Xem hồ sơ",
   "account.hide_reblogs": "Ẩn tút @{name} đăng lại",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} yêu cầu theo dõi bạn",
   "account.share": "Chia sẻ @{name}",
   "account.show_reblogs": "Hiện tút do @{name} đăng lại",
+  "account.statuses_counter": "{count, plural, other {{counter} tút}}",
   "account.unblock": "Bỏ chặn @{name}",
   "account.unblock_domain": "Bỏ ẩn {domain}",
   "account.unblock_short": "Bỏ chặn",
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 17c743f1d..f3b8f27d8 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -291,6 +291,7 @@ cs:
         update_custom_emoji_html: Uživatel %{name} aktualizoval emoji %{target}
         update_domain_block_html: "%{name} aktualizoval blokaci domény %{target}"
         update_ip_block_html: "%{name} změnil pravidlo pro IP %{target}"
+        update_report_html: "%{name} aktualizoval hlášení %{target}"
         update_status_html: Uživatel %{name} aktualizoval příspěvek uživatele %{target}
         update_user_role_html: "%{name} změnil %{target} roli"
       deleted_account: smazaný účet
@@ -298,6 +299,7 @@ cs:
       filter_by_action: Filtrovat podle akce
       filter_by_user: Filtrovat podle uživatele
       title: Protokol auditu
+      unavailable_instance: "(doména není k dispozici)"
     announcements:
       destroyed_msg: Oznámení bylo úspěšně odstraněno!
       edit:
@@ -984,6 +986,7 @@ cs:
       delete: Smazat
       edit_preset: Upravit předlohu pro varování
       empty: Zatím jste nedefinovali žádné předlohy varování.
+      title: Předvolby varování
     webhooks:
       add_new: Přidat koncový bod
       delete: Smazat
diff --git a/config/locales/doorkeeper.cs.yml b/config/locales/doorkeeper.cs.yml
index 332383468..3101779ed 100644
--- a/config/locales/doorkeeper.cs.yml
+++ b/config/locales/doorkeeper.cs.yml
@@ -166,6 +166,7 @@ cs:
       admin:write:reports: provádět moderátorské akce s hlášeními
       crypto: používat end-to-end šifrování
       follow: upravovat vztahy mezi profily
+      profile: číst pouze základní informace o vašem účtu
       push: přijímat vaše push oznámení
       read: vidět všechna data vašeho účtu
       read:accounts: vidět informace o účtech

From aefb4c027b035bc3022343157d31117e7ae531b4 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 1 Jul 2024 08:35:19 +0000
Subject: [PATCH 52/84] chore(deps): update dependency rubocop-rails to v2.25.1
 (#30876)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index 1d73a8ef5..42cc0e198 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -696,7 +696,7 @@ GEM
     responders (3.1.1)
       actionpack (>= 5.2)
       railties (>= 5.2)
-    rexml (3.3.0)
+    rexml (3.3.1)
       strscan
     rotp (6.3.0)
     rouge (4.2.1)
@@ -751,7 +751,7 @@ GEM
     rubocop-performance (1.21.1)
       rubocop (>= 1.48.1, < 2.0)
       rubocop-ast (>= 1.31.1, < 2.0)
-    rubocop-rails (2.25.0)
+    rubocop-rails (2.25.1)
       activesupport (>= 4.2.0)
       rack (>= 1.1)
       rubocop (>= 1.33.0, < 2.0)

From aeefe5b2bea6e0fb511f029c2aacdefa7bd41eb8 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 1 Jul 2024 09:03:38 +0000
Subject: [PATCH 53/84] chore(deps): update eslint (non-major) (#30883)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 package.json |   4 +-
 yarn.lock    | 357 ++++++++++++++++++++++++++++++++++-----------------
 2 files changed, 241 insertions(+), 120 deletions(-)

diff --git a/package.json b/package.json
index 3eb1ebad7..0379c7a5f 100644
--- a/package.json
+++ b/package.json
@@ -178,8 +178,8 @@
     "eslint-plugin-formatjs": "^4.10.1",
     "eslint-plugin-import": "~2.29.0",
     "eslint-plugin-jsdoc": "^48.0.0",
-    "eslint-plugin-jsx-a11y": "~6.8.0",
-    "eslint-plugin-promise": "~6.2.0",
+    "eslint-plugin-jsx-a11y": "~6.9.0",
+    "eslint-plugin-promise": "~6.4.0",
     "eslint-plugin-react": "^7.33.2",
     "eslint-plugin-react-hooks": "^4.6.0",
     "husky": "^9.0.11",
diff --git a/yarn.lock b/yarn.lock
index 5b4f3c22b..9d19cb837 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1502,7 +1502,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.3, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
+"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.3, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
   version: 7.24.7
   resolution: "@babel/runtime@npm:7.24.7"
   dependencies:
@@ -2830,8 +2830,8 @@ __metadata:
     eslint-plugin-formatjs: "npm:^4.10.1"
     eslint-plugin-import: "npm:~2.29.0"
     eslint-plugin-jsdoc: "npm:^48.0.0"
-    eslint-plugin-jsx-a11y: "npm:~6.8.0"
-    eslint-plugin-promise: "npm:~6.2.0"
+    eslint-plugin-jsx-a11y: "npm:~6.9.0"
+    eslint-plugin-promise: "npm:~6.4.0"
     eslint-plugin-react: "npm:^7.33.2"
     eslint-plugin-react-hooks: "npm:^4.6.0"
     file-loader: "npm:^6.2.0"
@@ -3036,6 +3036,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@pkgr/core@npm:^0.1.0":
+  version: 0.1.1
+  resolution: "@pkgr/core@npm:0.1.1"
+  checksum: 10c0/3f7536bc7f57320ab2cf96f8973664bef624710c403357429fbf680a5c3b4843c1dbd389bb43daa6b1f6f1f007bb082f5abcb76bb2b5dc9f421647743b71d3d8
+  languageName: node
+  linkType: hard
+
 "@polka/url@npm:^1.0.0-next.20":
   version: 1.0.0-next.21
   resolution: "@polka/url@npm:1.0.0-next.21"
@@ -4120,14 +4127,14 @@ __metadata:
   linkType: hard
 
 "@typescript-eslint/eslint-plugin@npm:^7.0.0":
-  version: 7.11.0
-  resolution: "@typescript-eslint/eslint-plugin@npm:7.11.0"
+  version: 7.14.1
+  resolution: "@typescript-eslint/eslint-plugin@npm:7.14.1"
   dependencies:
     "@eslint-community/regexpp": "npm:^4.10.0"
-    "@typescript-eslint/scope-manager": "npm:7.11.0"
-    "@typescript-eslint/type-utils": "npm:7.11.0"
-    "@typescript-eslint/utils": "npm:7.11.0"
-    "@typescript-eslint/visitor-keys": "npm:7.11.0"
+    "@typescript-eslint/scope-manager": "npm:7.14.1"
+    "@typescript-eslint/type-utils": "npm:7.14.1"
+    "@typescript-eslint/utils": "npm:7.14.1"
+    "@typescript-eslint/visitor-keys": "npm:7.14.1"
     graphemer: "npm:^1.4.0"
     ignore: "npm:^5.3.1"
     natural-compare: "npm:^1.4.0"
@@ -4138,25 +4145,25 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 10c0/50fedf832e4de9546569106eab1d10716204ceebc5cc7d62299112c881212270d0f7857e3d6452c07db031d40b58cf27c4d1b1a36043e8e700fc3496e377b54a
+  checksum: 10c0/7c2b9b98a38d78326b0ff7348fe001203eda10817ca7834a7a01f492ae7c2508469bbafaa933208d6459f8ff6685277685983cf6f6843e556a6ab2aa5c05080c
   languageName: node
   linkType: hard
 
 "@typescript-eslint/parser@npm:^7.0.0":
-  version: 7.11.0
-  resolution: "@typescript-eslint/parser@npm:7.11.0"
+  version: 7.14.1
+  resolution: "@typescript-eslint/parser@npm:7.14.1"
   dependencies:
-    "@typescript-eslint/scope-manager": "npm:7.11.0"
-    "@typescript-eslint/types": "npm:7.11.0"
-    "@typescript-eslint/typescript-estree": "npm:7.11.0"
-    "@typescript-eslint/visitor-keys": "npm:7.11.0"
+    "@typescript-eslint/scope-manager": "npm:7.14.1"
+    "@typescript-eslint/types": "npm:7.14.1"
+    "@typescript-eslint/typescript-estree": "npm:7.14.1"
+    "@typescript-eslint/visitor-keys": "npm:7.14.1"
     debug: "npm:^4.3.4"
   peerDependencies:
     eslint: ^8.56.0
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 10c0/f5d1343fae90ccd91aea8adf194e22ed3eb4b2ea79d03d8a9ca6e7b669a6db306e93138ec64f7020c5b3128619d50304dea1f06043eaff6b015071822cad4972
+  checksum: 10c0/db3169d4852685cfb27db741c557f58a3e52104bfacc7621beb7c94ec36ac2a08d4e410ac86745db52f482fbfc87e99fa0a26c1d7a10d37a215cce85e1661f0e
   languageName: node
   linkType: hard
 
@@ -4170,22 +4177,22 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/scope-manager@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@typescript-eslint/scope-manager@npm:7.11.0"
+"@typescript-eslint/scope-manager@npm:7.14.1":
+  version: 7.14.1
+  resolution: "@typescript-eslint/scope-manager@npm:7.14.1"
   dependencies:
-    "@typescript-eslint/types": "npm:7.11.0"
-    "@typescript-eslint/visitor-keys": "npm:7.11.0"
-  checksum: 10c0/35f9d88f38f2366017b15c9ee752f2605afa8009fa1eaf81c8b2b71fc22ddd2a33fff794a02015c8991a5fa99f315c3d6d76a5957d3fad1ccbb4cd46735c98b5
+    "@typescript-eslint/types": "npm:7.14.1"
+    "@typescript-eslint/visitor-keys": "npm:7.14.1"
+  checksum: 10c0/f8c05a0d6f8de4cc19b90a4da308817c66e53f36f7ec48f6cc23e93c7399bc418643d8135933aaf5fc013199cbef0e1ea4223f5147db5ca401b239eaf087011e
   languageName: node
   linkType: hard
 
-"@typescript-eslint/type-utils@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@typescript-eslint/type-utils@npm:7.11.0"
+"@typescript-eslint/type-utils@npm:7.14.1":
+  version: 7.14.1
+  resolution: "@typescript-eslint/type-utils@npm:7.14.1"
   dependencies:
-    "@typescript-eslint/typescript-estree": "npm:7.11.0"
-    "@typescript-eslint/utils": "npm:7.11.0"
+    "@typescript-eslint/typescript-estree": "npm:7.14.1"
+    "@typescript-eslint/utils": "npm:7.14.1"
     debug: "npm:^4.3.4"
     ts-api-utils: "npm:^1.3.0"
   peerDependencies:
@@ -4193,7 +4200,7 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 10c0/637395cb0f4c424c610e751906a31dcfedcdbd8c479012da6e81f9be6b930f32317bfe170ccb758d93a411b2bd9c4e7e5d18892094466099c6f9c3dceda81a72
+  checksum: 10c0/bd1c4a8db6273e24156fb10da2cbeb52b4eb03f819da193d4b6bd5a95db3b5524c6fe00d088308d8855b9ae60a3b82afa3a06e89982a09a8573561da960758fd
   languageName: node
   linkType: hard
 
@@ -4204,10 +4211,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/types@npm:7.11.0, @typescript-eslint/types@npm:^7.2.0":
-  version: 7.11.0
-  resolution: "@typescript-eslint/types@npm:7.11.0"
-  checksum: 10c0/c5d6c517124017eb44aa180c8ea1fad26ec8e47502f92fd12245ba3141560e69d7f7e35b8aa160ddd5df63a2952af407e2f62cc58b663c86e1f778ffb5b01789
+"@typescript-eslint/types@npm:7.14.1, @typescript-eslint/types@npm:^7.2.0":
+  version: 7.14.1
+  resolution: "@typescript-eslint/types@npm:7.14.1"
+  checksum: 10c0/5b7bda83c47a9b386482e63447c6b0ed7bd4e82eb43f11a180c6e2f3d2e7a2828f57bcbed82196ad761c49e363cccf4c81a89f1fc976e9f5f0a79dcc928fa2d2
   languageName: node
   linkType: hard
 
@@ -4230,12 +4237,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/typescript-estree@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@typescript-eslint/typescript-estree@npm:7.11.0"
+"@typescript-eslint/typescript-estree@npm:7.14.1":
+  version: 7.14.1
+  resolution: "@typescript-eslint/typescript-estree@npm:7.14.1"
   dependencies:
-    "@typescript-eslint/types": "npm:7.11.0"
-    "@typescript-eslint/visitor-keys": "npm:7.11.0"
+    "@typescript-eslint/types": "npm:7.14.1"
+    "@typescript-eslint/visitor-keys": "npm:7.14.1"
     debug: "npm:^4.3.4"
     globby: "npm:^11.1.0"
     is-glob: "npm:^4.0.3"
@@ -4245,21 +4252,21 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 10c0/a4eda43f352d20edebae0c1c221c4fd9de0673a94988cf1ae3f5e4917ef9cdb9ead8d3673ea8dd6e80d9cf3523a47c295be1326a3fae017b277233f4c4b4026b
+  checksum: 10c0/a8da9bcc4de3334a225424946abd99374de05c42098455419224bc0f46bb1b66115f8bd6ae268461294b90943ed4a407bcd255c0fa60eb76ba4cdc5fc7c20855
   languageName: node
   linkType: hard
 
-"@typescript-eslint/utils@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@typescript-eslint/utils@npm:7.11.0"
+"@typescript-eslint/utils@npm:7.14.1":
+  version: 7.14.1
+  resolution: "@typescript-eslint/utils@npm:7.14.1"
   dependencies:
     "@eslint-community/eslint-utils": "npm:^4.4.0"
-    "@typescript-eslint/scope-manager": "npm:7.11.0"
-    "@typescript-eslint/types": "npm:7.11.0"
-    "@typescript-eslint/typescript-estree": "npm:7.11.0"
+    "@typescript-eslint/scope-manager": "npm:7.14.1"
+    "@typescript-eslint/types": "npm:7.14.1"
+    "@typescript-eslint/typescript-estree": "npm:7.14.1"
   peerDependencies:
     eslint: ^8.56.0
-  checksum: 10c0/539a7ff8b825ad810fc59a80269094748df1a397a42cdbb212c493fc2486711c7d8fd6d75d4cd8a067822b8e6a11f42c50441977d51c183eec47992506d1cdf8
+  checksum: 10c0/c7f635a3c2c6c085e1d51a52088e55cad9d7e1257b1f60378e5eeb6eb0871db027d42747e9ef60a2f557cf9dd68b2ce014d488d795db8f771506290b164b0e5a
   languageName: node
   linkType: hard
 
@@ -4290,13 +4297,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/visitor-keys@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@typescript-eslint/visitor-keys@npm:7.11.0"
+"@typescript-eslint/visitor-keys@npm:7.14.1":
+  version: 7.14.1
+  resolution: "@typescript-eslint/visitor-keys@npm:7.14.1"
   dependencies:
-    "@typescript-eslint/types": "npm:7.11.0"
+    "@typescript-eslint/types": "npm:7.14.1"
     eslint-visitor-keys: "npm:^3.4.3"
-  checksum: 10c0/664e558d9645896484b7ffc9381837f0d52443bf8d121a5586d02d42ca4d17dc35faf526768c4b1beb52c57c43fae555898eb087651eb1c7a3d60f1085effea1
+  checksum: 10c0/39ac489990fcfdcee442f27658431a0eb44ccf694f701a45df2a108c47cea9582e0955bff0d449047549149385f72895a5d7e6c1622ece1fe32594b7cecb85f3
   languageName: node
   linkType: hard
 
@@ -4786,7 +4793,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"aria-query@npm:5.3.0, aria-query@npm:^5.0.0, aria-query@npm:^5.3.0":
+"aria-query@npm:5.3.0, aria-query@npm:^5.0.0":
   version: 5.3.0
   resolution: "aria-query@npm:5.3.0"
   dependencies:
@@ -4795,6 +4802,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"aria-query@npm:~5.1.3":
+  version: 5.1.3
+  resolution: "aria-query@npm:5.1.3"
+  dependencies:
+    deep-equal: "npm:^2.0.5"
+  checksum: 10c0/edcbc8044c4663d6f88f785e983e6784f98cb62b4ba1e9dd8d61b725d0203e4cfca38d676aee984c31f354103461102a3d583aa4fbe4fd0a89b679744f4e5faf
+  languageName: node
+  linkType: hard
+
 "arr-diff@npm:^4.0.0":
   version: 4.0.0
   resolution: "arr-diff@npm:4.0.0"
@@ -4816,7 +4832,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"array-buffer-byte-length@npm:^1.0.1":
+"array-buffer-byte-length@npm:^1.0.0, array-buffer-byte-length@npm:^1.0.1":
   version: 1.0.1
   resolution: "array-buffer-byte-length@npm:1.0.1"
   dependencies:
@@ -4960,16 +4976,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"array.prototype.tosorted@npm:^1.1.3":
-  version: 1.1.3
-  resolution: "array.prototype.tosorted@npm:1.1.3"
+"array.prototype.tosorted@npm:^1.1.4":
+  version: 1.1.4
+  resolution: "array.prototype.tosorted@npm:1.1.4"
   dependencies:
-    call-bind: "npm:^1.0.5"
+    call-bind: "npm:^1.0.7"
     define-properties: "npm:^1.2.1"
-    es-abstract: "npm:^1.22.3"
-    es-errors: "npm:^1.1.0"
+    es-abstract: "npm:^1.23.3"
+    es-errors: "npm:^1.3.0"
     es-shim-unscopables: "npm:^1.0.2"
-  checksum: 10c0/a27e1ca51168ecacf6042901f5ef021e43c8fa04b6c6b6f2a30bac3645cd2b519cecbe0bc45db1b85b843f64dc3207f0268f700b4b9fbdec076d12d432cf0865
+  checksum: 10c0/eb3c4c4fc0381b0bf6dba2ea4d48d367c2827a0d4236a5718d97caaccc6b78f11f4cadf090736e86301d295a6aa4967ed45568f92ced51be8cbbacd9ca410943
   languageName: node
   linkType: hard
 
@@ -5135,10 +5151,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"axe-core@npm:=4.7.0":
-  version: 4.7.0
-  resolution: "axe-core@npm:4.7.0"
-  checksum: 10c0/89ac5712b5932ac7d23398b4cb5ba081c394a086e343acc68ba49c83472706e18e0799804e8388c779dcdacc465377deb29f2714241d3fbb389cf3a6b275c9ba
+"axe-core@npm:^4.9.1":
+  version: 4.9.1
+  resolution: "axe-core@npm:4.9.1"
+  checksum: 10c0/ac9e5a0c6fa115a43ebffc32a1d2189e1ca6431b5a78e88cdcf94a72a25c5964185682edd94fe6bdb1cb4266c0d06301b022866e0e50dcdf6e3cefe556470110
   languageName: node
   linkType: hard
 
@@ -5153,12 +5169,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"axobject-query@npm:^3.2.1":
-  version: 3.2.1
-  resolution: "axobject-query@npm:3.2.1"
+"axobject-query@npm:~3.1.1":
+  version: 3.1.1
+  resolution: "axobject-query@npm:3.1.1"
   dependencies:
-    dequal: "npm:^2.0.3"
-  checksum: 10c0/f7debc2012e456139b57d888c223f6d3cb4b61eb104164a85e3d346273dd6ef0bc9a04b6660ca9407704a14a8e05fa6b6eb9d55f44f348c7210de7ffb350c3a7
+    deep-equal: "npm:^2.0.5"
+  checksum: 10c0/fff3175a22fd1f41fceb7ae0cd25f6594a0d7fba28c2335dd904538b80eb4e1040432564a3c643025cd2bb748f68d35aaabffb780b794da97ecfc748810b25ad
   languageName: node
   linkType: hard
 
@@ -6996,6 +7012,32 @@ __metadata:
   languageName: node
   linkType: hard
 
+"deep-equal@npm:^2.0.5":
+  version: 2.2.3
+  resolution: "deep-equal@npm:2.2.3"
+  dependencies:
+    array-buffer-byte-length: "npm:^1.0.0"
+    call-bind: "npm:^1.0.5"
+    es-get-iterator: "npm:^1.1.3"
+    get-intrinsic: "npm:^1.2.2"
+    is-arguments: "npm:^1.1.1"
+    is-array-buffer: "npm:^3.0.2"
+    is-date-object: "npm:^1.0.5"
+    is-regex: "npm:^1.1.4"
+    is-shared-array-buffer: "npm:^1.0.2"
+    isarray: "npm:^2.0.5"
+    object-is: "npm:^1.1.5"
+    object-keys: "npm:^1.1.1"
+    object.assign: "npm:^4.1.4"
+    regexp.prototype.flags: "npm:^1.5.1"
+    side-channel: "npm:^1.0.4"
+    which-boxed-primitive: "npm:^1.0.2"
+    which-collection: "npm:^1.0.1"
+    which-typed-array: "npm:^1.1.13"
+  checksum: 10c0/a48244f90fa989f63ff5ef0cc6de1e4916b48ea0220a9c89a378561960814794a5800c600254482a2c8fd2e49d6c2e196131dc983976adb024c94a42dfe4949f
+  languageName: node
+  linkType: hard
+
 "deep-is@npm:^0.1.3":
   version: 0.1.4
   resolution: "deep-is@npm:0.1.4"
@@ -7578,7 +7620,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"es-abstract@npm:^1.17.2, es-abstract@npm:^1.20.4, es-abstract@npm:^1.21.2, es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3":
+"es-abstract@npm:^1.17.2, es-abstract@npm:^1.17.5, es-abstract@npm:^1.20.4, es-abstract@npm:^1.21.2, es-abstract@npm:^1.22.1, es-abstract@npm:^1.22.3, es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3":
   version: 1.23.3
   resolution: "es-abstract@npm:1.23.3"
   dependencies:
@@ -7648,14 +7690,31 @@ __metadata:
   languageName: node
   linkType: hard
 
-"es-errors@npm:^1.1.0, es-errors@npm:^1.2.1, es-errors@npm:^1.3.0":
+"es-errors@npm:^1.2.1, es-errors@npm:^1.3.0":
   version: 1.3.0
   resolution: "es-errors@npm:1.3.0"
   checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85
   languageName: node
   linkType: hard
 
-"es-iterator-helpers@npm:^1.0.15, es-iterator-helpers@npm:^1.0.19":
+"es-get-iterator@npm:^1.1.3":
+  version: 1.1.3
+  resolution: "es-get-iterator@npm:1.1.3"
+  dependencies:
+    call-bind: "npm:^1.0.2"
+    get-intrinsic: "npm:^1.1.3"
+    has-symbols: "npm:^1.0.3"
+    is-arguments: "npm:^1.1.1"
+    is-map: "npm:^2.0.2"
+    is-set: "npm:^2.0.2"
+    is-string: "npm:^1.0.7"
+    isarray: "npm:^2.0.5"
+    stop-iteration-iterator: "npm:^1.0.0"
+  checksum: 10c0/ebd11effa79851ea75d7f079405f9d0dc185559fd65d986c6afea59a0ff2d46c2ed8675f19f03dce7429d7f6c14ff9aede8d121fbab78d75cfda6a263030bac0
+  languageName: node
+  linkType: hard
+
+"es-iterator-helpers@npm:^1.0.19":
   version: 1.0.19
   resolution: "es-iterator-helpers@npm:1.0.19"
   dependencies:
@@ -7677,6 +7736,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"es-module-lexer@npm:^1.5.3":
+  version: 1.5.4
+  resolution: "es-module-lexer@npm:1.5.4"
+  checksum: 10c0/300a469488c2f22081df1e4c8398c78db92358496e639b0df7f89ac6455462aaf5d8893939087c1a1cbcbf20eed4610c70e0bcb8f3e4b0d80a5d2611c539408c
+  languageName: node
+  linkType: hard
+
 "es-object-atoms@npm:^1.0.0":
   version: 1.0.0
   resolution: "es-object-atoms@npm:1.0.0"
@@ -7867,8 +7933,8 @@ __metadata:
   linkType: hard
 
 "eslint-plugin-jsdoc@npm:^48.0.0":
-  version: 48.2.7
-  resolution: "eslint-plugin-jsdoc@npm:48.2.7"
+  version: 48.5.0
+  resolution: "eslint-plugin-jsdoc@npm:48.5.0"
   dependencies:
     "@es-joy/jsdoccomment": "npm:~0.43.1"
     are-docs-informative: "npm:^0.0.2"
@@ -7876,46 +7942,48 @@ __metadata:
     debug: "npm:^4.3.4"
     escape-string-regexp: "npm:^4.0.0"
     esquery: "npm:^1.5.0"
+    parse-imports: "npm:^2.1.0"
     semver: "npm:^7.6.2"
     spdx-expression-parse: "npm:^4.0.0"
+    synckit: "npm:^0.9.0"
   peerDependencies:
     eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
-  checksum: 10c0/74d0f95b3d880dd4221dbc0b9341266a6cce3b8ca8d3e30032223af3552364643d6b82ad733d9bc06a20f0d640f21e4d8f5a4b00901d1771572625178b8c40c3
+  checksum: 10c0/1c5eb83df06cb228e44ad2c9da5b31987347a45b99d9e7a68957d178487a81603ad3c4c7db1ecba7e8a62d7ae20d9de1aec18a8cf2aa0e9169731cec54f78ab7
   languageName: node
   linkType: hard
 
-"eslint-plugin-jsx-a11y@npm:~6.8.0":
-  version: 6.8.0
-  resolution: "eslint-plugin-jsx-a11y@npm:6.8.0"
+"eslint-plugin-jsx-a11y@npm:~6.9.0":
+  version: 6.9.0
+  resolution: "eslint-plugin-jsx-a11y@npm:6.9.0"
   dependencies:
-    "@babel/runtime": "npm:^7.23.2"
-    aria-query: "npm:^5.3.0"
-    array-includes: "npm:^3.1.7"
+    aria-query: "npm:~5.1.3"
+    array-includes: "npm:^3.1.8"
     array.prototype.flatmap: "npm:^1.3.2"
     ast-types-flow: "npm:^0.0.8"
-    axe-core: "npm:=4.7.0"
-    axobject-query: "npm:^3.2.1"
+    axe-core: "npm:^4.9.1"
+    axobject-query: "npm:~3.1.1"
     damerau-levenshtein: "npm:^1.0.8"
     emoji-regex: "npm:^9.2.2"
-    es-iterator-helpers: "npm:^1.0.15"
-    hasown: "npm:^2.0.0"
+    es-iterator-helpers: "npm:^1.0.19"
+    hasown: "npm:^2.0.2"
     jsx-ast-utils: "npm:^3.3.5"
     language-tags: "npm:^1.0.9"
     minimatch: "npm:^3.1.2"
-    object.entries: "npm:^1.1.7"
-    object.fromentries: "npm:^2.0.7"
+    object.fromentries: "npm:^2.0.8"
+    safe-regex-test: "npm:^1.0.3"
+    string.prototype.includes: "npm:^2.0.0"
   peerDependencies:
     eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
-  checksum: 10c0/199b883e526e6f9d7c54cb3f094abc54f11a1ec816db5fb6cae3b938eb0e503acc10ccba91ca7451633a9d0b9abc0ea03601844a8aba5fe88c5e8897c9ac8f49
+  checksum: 10c0/72ac719ca90b6149c8f3c708ac5b1177f6757668b6e174d72a78512d4ac10329331b9c666c21e9561237a96a45d7f147f6a5d270dadbb99eb4ee093f127792c3
   languageName: node
   linkType: hard
 
-"eslint-plugin-promise@npm:~6.2.0":
-  version: 6.2.0
-  resolution: "eslint-plugin-promise@npm:6.2.0"
+"eslint-plugin-promise@npm:~6.4.0":
+  version: 6.4.0
+  resolution: "eslint-plugin-promise@npm:6.4.0"
   peerDependencies:
     eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
-  checksum: 10c0/5f42ee774023c089453ecb792076c64c6d0739ea6e9d6cdc9d6a63da5ba928c776e349d01cc110548f2c67045ec55343136aa7eb8b486e4ab145ac016c06a492
+  checksum: 10c0/5d07be976504f92d1d91756b0b0588a4c65e379af2520dd77c8655203085c0ab43e24d4698d1ac4b50926430cd8eb81cd1cc4c3653aae8386c805577bdf57b6c
   languageName: node
   linkType: hard
 
@@ -7929,14 +7997,14 @@ __metadata:
   linkType: hard
 
 "eslint-plugin-react@npm:^7.33.2":
-  version: 7.34.2
-  resolution: "eslint-plugin-react@npm:7.34.2"
+  version: 7.34.3
+  resolution: "eslint-plugin-react@npm:7.34.3"
   dependencies:
     array-includes: "npm:^3.1.8"
     array.prototype.findlast: "npm:^1.2.5"
     array.prototype.flatmap: "npm:^1.3.2"
     array.prototype.toreversed: "npm:^1.1.2"
-    array.prototype.tosorted: "npm:^1.1.3"
+    array.prototype.tosorted: "npm:^1.1.4"
     doctrine: "npm:^2.1.0"
     es-iterator-helpers: "npm:^1.0.19"
     estraverse: "npm:^5.3.0"
@@ -7952,7 +8020,7 @@ __metadata:
     string.prototype.matchall: "npm:^4.0.11"
   peerDependencies:
     eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
-  checksum: 10c0/37dc04424da8626f20a071466e7238d53ed111c53e5e5398d813ac2cf76a2078f00d91f7833fe5b2f0fc98f2688a75b36e78e9ada9f1068705d23c7031094316
+  checksum: 10c0/60717e32c9948e2b4ddc53dac7c4b62c68fc7129c3249079191c941c08ebe7d1f4793d65182922d19427c2a6634e05231a7b74ceee34169afdfd0e43d4a43d26
   languageName: node
   linkType: hard
 
@@ -8821,7 +8889,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4":
+"get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.3, get-intrinsic@npm:^1.2.4":
   version: 1.2.4
   resolution: "get-intrinsic@npm:1.2.4"
   dependencies:
@@ -9661,7 +9729,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"internal-slot@npm:^1.0.7":
+"internal-slot@npm:^1.0.4, internal-slot@npm:^1.0.7":
   version: 1.0.7
   resolution: "internal-slot@npm:1.0.7"
   dependencies:
@@ -9777,7 +9845,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-arguments@npm:^1.0.4":
+"is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1":
   version: 1.1.1
   resolution: "is-arguments@npm:1.1.1"
   dependencies:
@@ -9787,7 +9855,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-array-buffer@npm:^3.0.4":
+"is-array-buffer@npm:^3.0.2, is-array-buffer@npm:^3.0.4":
   version: 3.0.4
   resolution: "is-array-buffer@npm:3.0.4"
   dependencies:
@@ -10043,10 +10111,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-map@npm:^2.0.1":
-  version: 2.0.2
-  resolution: "is-map@npm:2.0.2"
-  checksum: 10c0/119ff9137a37fd131a72fab3f4ab8c9d6a24b0a1ee26b4eff14dc625900d8675a97785eea5f4174265e2006ed076cc24e89f6e57ebd080a48338d914ec9168a5
+"is-map@npm:^2.0.1, is-map@npm:^2.0.2":
+  version: 2.0.3
+  resolution: "is-map@npm:2.0.3"
+  checksum: 10c0/2c4d431b74e00fdda7162cd8e4b763d6f6f217edf97d4f8538b94b8702b150610e2c64961340015fe8df5b1fcee33ccd2e9b62619c4a8a3a155f8de6d6d355fc
   languageName: node
   linkType: hard
 
@@ -10168,10 +10236,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-set@npm:^2.0.1":
-  version: 2.0.2
-  resolution: "is-set@npm:2.0.2"
-  checksum: 10c0/5f8bd1880df8c0004ce694e315e6e1e47a3452014be792880bb274a3b2cdb952fdb60789636ca6e084c7947ca8b7ae03ccaf54c93a7fcfed228af810559e5432
+"is-set@npm:^2.0.1, is-set@npm:^2.0.2":
+  version: 2.0.3
+  resolution: "is-set@npm:2.0.3"
+  checksum: 10c0/f73732e13f099b2dc879c2a12341cfc22ccaca8dd504e6edae26484bd5707a35d503fba5b4daad530a9b088ced1ae6c9d8200fd92e09b428fe14ea79ce8080b7
   languageName: node
   linkType: hard
 
@@ -12366,13 +12434,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"object-is@npm:^1.0.1":
-  version: 1.1.5
-  resolution: "object-is@npm:1.1.5"
+"object-is@npm:^1.0.1, object-is@npm:^1.1.5":
+  version: 1.1.6
+  resolution: "object-is@npm:1.1.6"
   dependencies:
-    call-bind: "npm:^1.0.2"
-    define-properties: "npm:^1.1.3"
-  checksum: 10c0/8c263fb03fc28f1ffb54b44b9147235c5e233dc1ca23768e7d2569740b5d860154d7cc29a30220fe28ed6d8008e2422aefdebfe987c103e1c5d190cf02d9d886
+    call-bind: "npm:^1.0.7"
+    define-properties: "npm:^1.2.1"
+  checksum: 10c0/506af444c4dce7f8e31f34fc549e2fb8152d6b9c4a30c6e62852badd7f520b579c679af433e7a072f9d78eb7808d230dc12e1cf58da9154dfbf8813099ea0fe0
   languageName: node
   linkType: hard
 
@@ -12404,7 +12472,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"object.entries@npm:^1.1.7, object.entries@npm:^1.1.8":
+"object.entries@npm:^1.1.8":
   version: 1.1.8
   resolution: "object.entries@npm:1.1.8"
   dependencies:
@@ -12708,6 +12776,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"parse-imports@npm:^2.1.0":
+  version: 2.1.0
+  resolution: "parse-imports@npm:2.1.0"
+  dependencies:
+    es-module-lexer: "npm:^1.5.3"
+    slashes: "npm:^3.0.12"
+  checksum: 10c0/18ef58008868d2d09e472bb540d63efc7cc27f2c33607e5d09c256ece7a30062cac292bda96d820438e94f3dbf558c85e4b084c10d238baa858796794e6cf628
+  languageName: node
+  linkType: hard
+
 "parse-json@npm:^5.0.0, parse-json@npm:^5.2.0":
   version: 5.2.0
   resolution: "parse-json@npm:5.2.0"
@@ -14940,7 +15018,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"regexp.prototype.flags@npm:^1.2.0, regexp.prototype.flags@npm:^1.5.2":
+"regexp.prototype.flags@npm:^1.2.0, regexp.prototype.flags@npm:^1.5.1, regexp.prototype.flags@npm:^1.5.2":
   version: 1.5.2
   resolution: "regexp.prototype.flags@npm:1.5.2"
   dependencies:
@@ -15742,6 +15820,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"slashes@npm:^3.0.12":
+  version: 3.0.12
+  resolution: "slashes@npm:3.0.12"
+  checksum: 10c0/71ca2a1fcd1ab6814b0fdb8cf9c33a3d54321deec2aa8d173510f0086880201446021a9b9e6a18561f7c472b69a2145977c6a8fb9c53a8ff7be31778f203d175
+  languageName: node
+  linkType: hard
+
 "slice-ansi@npm:^4.0.0":
   version: 4.0.0
   resolution: "slice-ansi@npm:4.0.0"
@@ -16151,6 +16236,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"stop-iteration-iterator@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "stop-iteration-iterator@npm:1.0.0"
+  dependencies:
+    internal-slot: "npm:^1.0.4"
+  checksum: 10c0/c4158d6188aac510d9e92925b58709207bd94699e9c31186a040c80932a687f84a51356b5895e6dc72710aad83addb9411c22171832c9ae0e6e11b7d61b0dfb9
+  languageName: node
+  linkType: hard
+
 "stream-browserify@npm:^2.0.1":
   version: 2.0.2
   resolution: "stream-browserify@npm:2.0.2"
@@ -16235,6 +16329,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"string.prototype.includes@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "string.prototype.includes@npm:2.0.0"
+  dependencies:
+    define-properties: "npm:^1.1.3"
+    es-abstract: "npm:^1.17.5"
+  checksum: 10c0/32dff118c9e9dcc87e240b05462fa8ee7248d9e335c0015c1442fe18152261508a2146d9bb87ddae56abab69148a83c61dfaea33f53853812a6a2db737689ed2
+  languageName: node
+  linkType: hard
+
 "string.prototype.matchall@npm:^4.0.11, string.prototype.matchall@npm:^4.0.6":
   version: 4.0.11
   resolution: "string.prototype.matchall@npm:4.0.11"
@@ -16678,6 +16782,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"synckit@npm:^0.9.0":
+  version: 0.9.0
+  resolution: "synckit@npm:0.9.0"
+  dependencies:
+    "@pkgr/core": "npm:^0.1.0"
+    tslib: "npm:^2.6.2"
+  checksum: 10c0/b5c1e7c03fefe3d36a9ab4e71dd21859cb32be4138712c31a893382a568fd00efc59ede8f521dd7e53d43a2fea92bdf717e987ea9ed6ad94f97ef28d71d0ba2f
+  languageName: node
+  linkType: hard
+
 "table@npm:^6.8.2":
   version: 6.8.2
   resolution: "table@npm:6.8.2"
@@ -17014,13 +17128,20 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tslib@npm:2.6.2, tslib@npm:^2.4.0":
+"tslib@npm:2.6.2":
   version: 2.6.2
   resolution: "tslib@npm:2.6.2"
   checksum: 10c0/e03a8a4271152c8b26604ed45535954c0a45296e32445b4b87f8a5abdb2421f40b59b4ca437c4346af0f28179780d604094eb64546bee2019d903d01c6c19bdb
   languageName: node
   linkType: hard
 
+"tslib@npm:^2.4.0, tslib@npm:^2.6.2":
+  version: 2.6.3
+  resolution: "tslib@npm:2.6.3"
+  checksum: 10c0/2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a
+  languageName: node
+  linkType: hard
+
 "tty-browserify@npm:0.0.0":
   version: 0.0.0
   resolution: "tty-browserify@npm:0.0.0"
@@ -18009,7 +18130,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15, which-typed-array@npm:^1.1.9":
+"which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15, which-typed-array@npm:^1.1.9":
   version: 1.1.15
   resolution: "which-typed-array@npm:1.1.15"
   dependencies:

From 20fa9ce4845f1b6c8a7223f08409091808fa9bc0 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 1 Jul 2024 16:45:48 +0200
Subject: [PATCH 54/84] Add timeline of public posts about a trending link in
 web UI (#30840)

---
 app/javascript/mastodon/actions/timelines.js  |  1 +
 app/javascript/mastodon/api_types/statuses.ts |  1 +
 .../mastodon/components/status_list.jsx       |  1 +
 .../features/explore/components/story.jsx     |  4 +-
 .../mastodon/features/link_timeline/index.tsx | 76 +++++++++++++++++++
 app/javascript/mastodon/features/ui/index.jsx |  2 +
 .../features/ui/util/async-components.js      |  4 +
 app/javascript/mastodon/models/status.ts      |  8 ++
 config/routes.rb                              |  1 +
 9 files changed, 97 insertions(+), 1 deletion(-)
 create mode 100644 app/javascript/mastodon/features/link_timeline/index.tsx

diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 4ca3c3a15..f0ea46118 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -158,6 +158,7 @@ export const expandAccountTimeline         = (accountId, { maxId, withReplies, t
 export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
 export const expandAccountMediaTimeline    = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
 export const expandListTimeline            = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
+export const expandLinkTimeline            = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done);
 export const expandHashtagTimeline         = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
   return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
     max_id: maxId,
diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts
index db4e20506..a934faeb7 100644
--- a/app/javascript/mastodon/api_types/statuses.ts
+++ b/app/javascript/mastodon/api_types/statuses.ts
@@ -44,6 +44,7 @@ export interface ApiPreviewCardJSON {
   type: string;
   author_name: string;
   author_url: string;
+  author_account?: ApiAccountJSON;
   provider_name: string;
   provider_url: string;
   html: string;
diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx
index 3ed20f65e..fee6675fa 100644
--- a/app/javascript/mastodon/components/status_list.jsx
+++ b/app/javascript/mastodon/components/status_list.jsx
@@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent {
     withCounters: PropTypes.bool,
     timelineId: PropTypes.string,
     lastId: PropTypes.string,
+    bindToDocument: PropTypes.bool,
   };
 
   static defaultProps = {
diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx
index a2cae942d..125df412a 100644
--- a/app/javascript/mastodon/features/explore/components/story.jsx
+++ b/app/javascript/mastodon/features/explore/components/story.jsx
@@ -4,6 +4,8 @@ import { useState, useCallback } from 'react';
 import { FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
 
 
 import { Blurhash } from 'mastodon/components/blurhash';
@@ -57,7 +59,7 @@ export const Story = ({
 
         <div className='story__details__shared'>
           {author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />}
-          {typeof sharedTimes === 'number' ? <span className='story__details__shared__pill'><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></span> : <Skeleton width='10ch' />}
+          {typeof sharedTimes === 'number' ? <Link className='story__details__shared__pill' to={`/links/${encodeURIComponent(url)}`}><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></Link> : <Skeleton width='10ch' />}
         </div>
       </div>
 
diff --git a/app/javascript/mastodon/features/link_timeline/index.tsx b/app/javascript/mastodon/features/link_timeline/index.tsx
new file mode 100644
index 000000000..dd726dac1
--- /dev/null
+++ b/app/javascript/mastodon/features/link_timeline/index.tsx
@@ -0,0 +1,76 @@
+import { useRef, useEffect, useCallback } from 'react';
+
+import { Helmet } from 'react-helmet';
+import { useParams } from 'react-router-dom';
+
+import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
+import { expandLinkTimeline } from 'mastodon/actions/timelines';
+import Column from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
+import type { Card } from 'mastodon/models/status';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+export const LinkTimeline: React.FC<{
+  multiColumn: boolean;
+}> = ({ multiColumn }) => {
+  const { url } = useParams<{ url: string }>();
+  const decodedUrl = url ? decodeURIComponent(url) : undefined;
+  const dispatch = useAppDispatch();
+  const columnRef = useRef<Column>(null);
+  const firstStatusId = useAppSelector((state) =>
+    decodedUrl
+      ? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
+        (state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string)
+      : undefined,
+  );
+  const story = useAppSelector((state) =>
+    firstStatusId
+      ? (state.statuses.getIn([firstStatusId, 'card']) as Card)
+      : undefined,
+  );
+
+  const handleHeaderClick = useCallback(() => {
+    columnRef.current?.scrollTop();
+  }, []);
+
+  const handleLoadMore = useCallback(
+    (maxId: string) => {
+      dispatch(expandLinkTimeline(decodedUrl, { maxId }));
+    },
+    [dispatch, decodedUrl],
+  );
+
+  useEffect(() => {
+    dispatch(expandLinkTimeline(decodedUrl));
+  }, [dispatch, decodedUrl]);
+
+  return (
+    <Column bindToDocument={!multiColumn} ref={columnRef} label={story?.title}>
+      <ColumnHeader
+        icon='explore'
+        iconComponent={ExploreIcon}
+        title={story?.title}
+        onClick={handleHeaderClick}
+        multiColumn={multiColumn}
+        showBackButton
+      />
+
+      <StatusListContainer
+        timelineId={`link:${decodedUrl}`}
+        onLoadMore={handleLoadMore}
+        trackScroll
+        scrollKey={`link_timeline-${decodedUrl}`}
+        bindToDocument={!multiColumn}
+      />
+
+      <Helmet>
+        <title>{story?.title}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default LinkTimeline;
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index b58e191ed..d41132f9c 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -56,6 +56,7 @@ import {
   FavouritedStatuses,
   BookmarkedStatuses,
   FollowedTags,
+  LinkTimeline,
   ListTimeline,
   Blocks,
   DomainBlocks,
@@ -202,6 +203,7 @@ class SwitchingColumnsArea extends PureComponent {
             <WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
             <WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
             <WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
+            <WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
             <WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
             <WrappedRoute path='/notifications' component={Notifications} content={children} exact />
             <WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index e1f5bfdaf..b8a2359d9 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -201,3 +201,7 @@ export function NotificationRequests () {
 export function NotificationRequest () {
   return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request');
 }
+
+export function LinkTimeline () {
+  return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline');
+}
diff --git a/app/javascript/mastodon/models/status.ts b/app/javascript/mastodon/models/status.ts
index 7907fc34f..3900df4e3 100644
--- a/app/javascript/mastodon/models/status.ts
+++ b/app/javascript/mastodon/models/status.ts
@@ -1,4 +1,12 @@
+import type { RecordOf } from 'immutable';
+
+import type { ApiPreviewCardJSON } from 'mastodon/api_types/statuses';
+
 export type { StatusVisibility } from 'mastodon/api_types/statuses';
 
 // Temporary until we type it correctly
 export type Status = Immutable.Map<string, unknown>;
+
+type CardShape = Required<ApiPreviewCardJSON>;
+
+export type Card = RecordOf<CardShape>;
diff --git a/config/routes.rb b/config/routes.rb
index f4662dd5d..4b3bd4f18 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -27,6 +27,7 @@ Rails.application.routes.draw do
     /public/remote
     /conversations
     /lists/(*any)
+    /links/(*any)
     /notifications/(*any)
     /favourites
     /bookmarks

From b728c0e8ce9ac3a74f116bedff85e36dd7cc6a1e Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 1 Jul 2024 17:52:01 +0200
Subject: [PATCH 55/84] Change hover cards to not appear until the mouse stops
 in web UI (#30850)

---
 app/javascript/hooks/useTimeout.ts            |  17 +-
 .../mastodon/components/follow_button.tsx     |   4 +-
 .../components/hover_card_account.tsx         |   2 +-
 .../components/hover_card_controller.tsx      | 159 ++++++++++++------
 .../components/conversation.jsx               |   2 +-
 .../styles/mastodon/components.scss           |   2 +
 6 files changed, 131 insertions(+), 55 deletions(-)

diff --git a/app/javascript/hooks/useTimeout.ts b/app/javascript/hooks/useTimeout.ts
index f1814ae8e..bb1e8848d 100644
--- a/app/javascript/hooks/useTimeout.ts
+++ b/app/javascript/hooks/useTimeout.ts
@@ -2,19 +2,34 @@ import { useRef, useCallback, useEffect } from 'react';
 
 export const useTimeout = () => {
   const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
+  const callbackRef = useRef<() => void>();
 
   const set = useCallback((callback: () => void, delay: number) => {
     if (timeoutRef.current) {
       clearTimeout(timeoutRef.current);
     }
 
+    callbackRef.current = callback;
     timeoutRef.current = setTimeout(callback, delay);
   }, []);
 
+  const delay = useCallback((delay: number) => {
+    if (timeoutRef.current) {
+      clearTimeout(timeoutRef.current);
+    }
+
+    if (!callbackRef.current) {
+      return;
+    }
+
+    timeoutRef.current = setTimeout(callbackRef.current, delay);
+  }, []);
+
   const cancel = useCallback(() => {
     if (timeoutRef.current) {
       clearTimeout(timeoutRef.current);
       timeoutRef.current = undefined;
+      callbackRef.current = undefined;
     }
   }, []);
 
@@ -25,5 +40,5 @@ export const useTimeout = () => {
     [cancel],
   );
 
-  return [set, cancel] as const;
+  return [set, cancel, delay] as const;
 };
diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx
index db5994288..62771c254 100644
--- a/app/javascript/mastodon/components/follow_button.tsx
+++ b/app/javascript/mastodon/components/follow_button.tsx
@@ -27,7 +27,7 @@ const messages = defineMessages({
 });
 
 export const FollowButton: React.FC<{
-  accountId: string;
+  accountId?: string;
 }> = ({ accountId }) => {
   const intl = useIntl();
   const dispatch = useAppDispatch();
@@ -36,7 +36,7 @@ export const FollowButton: React.FC<{
     accountId ? state.accounts.get(accountId) : undefined,
   );
   const relationship = useAppSelector((state) =>
-    state.relationships.get(accountId),
+    accountId ? state.relationships.get(accountId) : undefined,
   );
   const following = relationship?.following || relationship?.requested;
 
diff --git a/app/javascript/mastodon/components/hover_card_account.tsx b/app/javascript/mastodon/components/hover_card_account.tsx
index 59f957783..8933e14a9 100644
--- a/app/javascript/mastodon/components/hover_card_account.tsx
+++ b/app/javascript/mastodon/components/hover_card_account.tsx
@@ -17,7 +17,7 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store';
 
 export const HoverCardAccount = forwardRef<
   HTMLDivElement,
-  { accountId: string }
+  { accountId?: string }
 >(({ accountId }, ref) => {
   const dispatch = useAppDispatch();
 
diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx
index 0130390ef..5ca55ebde 100644
--- a/app/javascript/mastodon/components/hover_card_controller.tsx
+++ b/app/javascript/mastodon/components/hover_card_controller.tsx
@@ -12,8 +12,8 @@ import { useTimeout } from 'mastodon/../hooks/useTimeout';
 import { HoverCardAccount } from 'mastodon/components/hover_card_account';
 
 const offset = [-12, 4] as OffsetValue;
-const enterDelay = 650;
-const leaveDelay = 250;
+const enterDelay = 750;
+const leaveDelay = 150;
 const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
 
 const isHoverCardAnchor = (element: HTMLElement) =>
@@ -23,50 +23,12 @@ export const HoverCardController: React.FC = () => {
   const [open, setOpen] = useState(false);
   const [accountId, setAccountId] = useState<string | undefined>();
   const [anchor, setAnchor] = useState<HTMLElement | null>(null);
-  const cardRef = useRef<HTMLDivElement>(null);
+  const cardRef = useRef<HTMLDivElement | null>(null);
   const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
-  const [setEnterTimeout, cancelEnterTimeout] = useTimeout();
+  const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
+  const [setScrollTimeout] = useTimeout();
   const location = useLocation();
 
-  const handleAnchorMouseEnter = useCallback(
-    (e: MouseEvent) => {
-      const { target } = e;
-
-      if (target instanceof HTMLElement && isHoverCardAnchor(target)) {
-        cancelLeaveTimeout();
-
-        setEnterTimeout(() => {
-          target.setAttribute('aria-describedby', 'hover-card');
-          setAnchor(target);
-          setOpen(true);
-          setAccountId(
-            target.getAttribute('data-hover-card-account') ?? undefined,
-          );
-        }, enterDelay);
-      }
-
-      if (target === cardRef.current?.parentNode) {
-        cancelLeaveTimeout();
-      }
-    },
-    [cancelLeaveTimeout, setEnterTimeout, setOpen, setAccountId, setAnchor],
-  );
-
-  const handleAnchorMouseLeave = useCallback(
-    (e: MouseEvent) => {
-      if (e.target === anchor || e.target === cardRef.current?.parentNode) {
-        cancelEnterTimeout();
-
-        setLeaveTimeout(() => {
-          anchor?.removeAttribute('aria-describedby');
-          setOpen(false);
-          setAnchor(null);
-        }, leaveDelay);
-      }
-    },
-    [cancelEnterTimeout, setLeaveTimeout, setOpen, setAnchor, anchor],
-  );
-
   const handleClose = useCallback(() => {
     cancelEnterTimeout();
     cancelLeaveTimeout();
@@ -79,22 +41,119 @@ export const HoverCardController: React.FC = () => {
   }, [handleClose, location]);
 
   useEffect(() => {
-    document.body.addEventListener('mouseenter', handleAnchorMouseEnter, {
+    let isScrolling = false;
+    let currentAnchor: HTMLElement | null = null;
+
+    const open = (target: HTMLElement) => {
+      target.setAttribute('aria-describedby', 'hover-card');
+      setOpen(true);
+      setAnchor(target);
+      setAccountId(target.getAttribute('data-hover-card-account') ?? undefined);
+    };
+
+    const close = () => {
+      currentAnchor?.removeAttribute('aria-describedby');
+      currentAnchor = null;
+      setOpen(false);
+      setAnchor(null);
+      setAccountId(undefined);
+    };
+
+    const handleMouseEnter = (e: MouseEvent) => {
+      const { target } = e;
+
+      // We've exited the window
+      if (!(target instanceof HTMLElement)) {
+        close();
+        return;
+      }
+
+      // We've entered an anchor
+      if (!isScrolling && isHoverCardAnchor(target)) {
+        cancelLeaveTimeout();
+
+        currentAnchor?.removeAttribute('aria-describedby');
+        currentAnchor = target;
+
+        setEnterTimeout(() => {
+          open(target);
+        }, enterDelay);
+      }
+
+      // We've entered the hover card
+      if (
+        !isScrolling &&
+        (target === currentAnchor || target === cardRef.current)
+      ) {
+        cancelLeaveTimeout();
+      }
+    };
+
+    const handleMouseLeave = (e: MouseEvent) => {
+      if (!currentAnchor) {
+        return;
+      }
+
+      if (e.target === currentAnchor || e.target === cardRef.current) {
+        cancelEnterTimeout();
+
+        setLeaveTimeout(() => {
+          close();
+        }, leaveDelay);
+      }
+    };
+
+    const handleScrollEnd = () => {
+      isScrolling = false;
+    };
+
+    const handleScroll = () => {
+      isScrolling = true;
+      cancelEnterTimeout();
+      setScrollTimeout(handleScrollEnd, 100);
+    };
+
+    const handleMouseMove = () => {
+      delayEnterTimeout(enterDelay);
+    };
+
+    document.body.addEventListener('mouseenter', handleMouseEnter, {
       passive: true,
       capture: true,
     });
-    document.body.addEventListener('mouseleave', handleAnchorMouseLeave, {
+
+    document.body.addEventListener('mousemove', handleMouseMove, {
+      passive: true,
+      capture: false,
+    });
+
+    document.body.addEventListener('mouseleave', handleMouseLeave, {
+      passive: true,
+      capture: true,
+    });
+
+    document.addEventListener('scroll', handleScroll, {
       passive: true,
       capture: true,
     });
 
     return () => {
-      document.body.removeEventListener('mouseenter', handleAnchorMouseEnter);
-      document.body.removeEventListener('mouseleave', handleAnchorMouseLeave);
+      document.body.removeEventListener('mouseenter', handleMouseEnter);
+      document.body.removeEventListener('mousemove', handleMouseMove);
+      document.body.removeEventListener('mouseleave', handleMouseLeave);
+      document.removeEventListener('scroll', handleScroll);
     };
-  }, [handleAnchorMouseEnter, handleAnchorMouseLeave]);
-
-  if (!accountId) return null;
+  }, [
+    setEnterTimeout,
+    setLeaveTimeout,
+    setScrollTimeout,
+    cancelEnterTimeout,
+    cancelLeaveTimeout,
+    delayEnterTimeout,
+    setOpen,
+    setAccountId,
+    setAnchor,
+  ]);
 
   return (
     <Overlay
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
index 3af89f997..a2b72f716 100644
--- a/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
+++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.jsx
@@ -163,7 +163,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
   menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
 
   const names = accounts.map(a => (
-    <Link to={`/@${a.get('acct')}`} key={a.get('id')} title={a.get('acct')}>
+    <Link to={`/@${a.get('acct')}`} key={a.get('id')} data-hover-card-account={a.get('id')}>
       <bdi>
         <strong
           className='display-name__html'
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index cbf9314ff..12eac79b9 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -10468,12 +10468,14 @@ noscript {
         overflow: hidden;
         white-space: nowrap;
         text-overflow: ellipsis;
+        text-align: end;
       }
 
       &.verified {
         dd {
           display: flex;
           align-items: center;
+          justify-content: flex-end;
           gap: 4px;
           overflow: hidden;
           white-space: nowrap;

From d3f504245cab5a9a0e89262e0a1398d035dffac9 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 1 Jul 2024 20:10:22 +0200
Subject: [PATCH 56/84] Fix missing confirmation when unfollowing from hover
 card in web UI (#30879)

---
 .../mastodon/components/follow_button.tsx     | 31 +++++++++++++------
 .../features/account/components/header.jsx    |  6 ++--
 .../containers/header_container.jsx           | 12 +------
 3 files changed, 24 insertions(+), 25 deletions(-)

diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx
index 62771c254..ecc4e1ee1 100644
--- a/app/javascript/mastodon/components/follow_button.tsx
+++ b/app/javascript/mastodon/components/follow_button.tsx
@@ -1,6 +1,6 @@
 import { useCallback, useEffect } from 'react';
 
-import { useIntl, defineMessages } from 'react-intl';
+import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
 
 import { useIdentity } from '@/mastodon/identity_context';
 import {
@@ -19,10 +19,6 @@ const messages = defineMessages({
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
   followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
   mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
-  cancel_follow_request: {
-    id: 'account.cancel_follow_request',
-    defaultMessage: 'Withdraw follow request',
-  },
   edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
 });
 
@@ -65,11 +61,28 @@ export const FollowButton: React.FC<{
     if (accountId === me) {
       return;
     } else if (relationship.following || relationship.requested) {
-      dispatch(unfollowAccount(accountId));
+      dispatch(
+        openModal({
+          modalType: 'CONFIRM',
+          modalProps: {
+            message: (
+              <FormattedMessage
+                id='confirmations.unfollow.message'
+                defaultMessage='Are you sure you want to unfollow {name}?'
+                values={{ name: <strong>@{account?.acct}</strong> }}
+              />
+            ),
+            confirm: intl.formatMessage(messages.unfollow),
+            onConfirm: () => {
+              dispatch(unfollowAccount(accountId));
+            },
+          },
+        }),
+      );
     } else {
       dispatch(followAccount(accountId));
     }
-  }, [dispatch, accountId, relationship, account, signedIn]);
+  }, [dispatch, intl, accountId, relationship, account, signedIn]);
 
   let label;
 
@@ -79,13 +92,11 @@ export const FollowButton: React.FC<{
     label = intl.formatMessage(messages.edit_profile);
   } else if (!relationship) {
     label = <LoadingIndicator />;
-  } else if (relationship.requested) {
-    label = intl.formatMessage(messages.cancel_follow_request);
   } else if (relationship.following && relationship.followed_by) {
     label = intl.formatMessage(messages.mutual);
   } else if (!relationship.following && relationship.followed_by) {
     label = intl.formatMessage(messages.followBack);
-  } else if (relationship.following) {
+  } else if (relationship.following || relationship.requested) {
     label = intl.formatMessage(messages.unfollow);
   } else {
     label = intl.formatMessage(messages.follow);
diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx
index b10ef6ef7..1326874e5 100644
--- a/app/javascript/mastodon/features/account/components/header.jsx
+++ b/app/javascript/mastodon/features/account/components/header.jsx
@@ -94,7 +94,7 @@ const messageForFollowButton = relationship => {
     return messages.mutual;
   } else if (!relationship.get('following') && relationship.get('followed_by')) {
     return messages.followBack;
-  } else if (relationship.get('following')) {
+  } else if (relationship.get('following') || relationship.get('requested')) {
     return messages.unfollow;
   } else {
     return messages.follow;
@@ -291,10 +291,8 @@ class Header extends ImmutablePureComponent {
     if (me !== account.get('id')) {
       if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
         actionBtn = <Button disabled><LoadingIndicator /></Button>;
-      } else if (account.getIn(['relationship', 'requested'])) {
-        actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
       } else if (!account.getIn(['relationship', 'blocking'])) {
-        actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(messageForFollowButton(account.get('relationship')))} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
+        actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) })} text={intl.formatMessage(messageForFollowButton(account.get('relationship')))} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
       } else if (account.getIn(['relationship', 'blocking'])) {
         actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
       }
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx
index 73fd62841..d55d8c58e 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx
@@ -25,7 +25,6 @@ import { makeGetAccount, getAccountHidden } from '../../../selectors';
 import Header from '../components/header';
 
 const messages = defineMessages({
-  cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
   blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
 });
@@ -45,7 +44,7 @@ const makeMapStateToProps = () => {
 const mapDispatchToProps = (dispatch, { intl }) => ({
 
   onFollow (account) {
-    if (account.getIn(['relationship', 'following'])) {
+    if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
       dispatch(openModal({
         modalType: 'CONFIRM',
         modalProps: {
@@ -54,15 +53,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
           onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
         },
       }));
-    } else if (account.getIn(['relationship', 'requested'])) {
-      dispatch(openModal({
-        modalType: 'CONFIRM',
-        modalProps: {
-          message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
-          confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
-          onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
-        },
-      }));
     } else {
       dispatch(followAccount(account.get('id')));
     }

From 1fc14e324bf103e379cf73ffceebbf46472cccde Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 2 Jul 2024 10:41:54 +0200
Subject: [PATCH 57/84] New Crowdin Translations (automated) (#30890)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/en-GB.json | 8 ++++++++
 app/javascript/mastodon/locales/sk.json    | 3 +++
 config/locales/doorkeeper.en-GB.yml        | 2 ++
 config/locales/en-GB.yml                   | 3 +++
 4 files changed, 16 insertions(+)

diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json
index c4f401d86..94d7defc7 100644
--- a/app/javascript/mastodon/locales/en-GB.json
+++ b/app/javascript/mastodon/locales/en-GB.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Follow back",
   "account.followers": "Followers",
   "account.followers.empty": "No one follows this user yet.",
+  "account.followers_counter": "{count, plural, one {{counter} follower} other {{counter} followers}}",
   "account.following": "Following",
+  "account.following_counter": "{count, plural, one {{counter} following} other {{counter} following}}",
   "account.follows.empty": "This user doesn't follow anyone yet.",
   "account.go_to_profile": "Go to profile",
   "account.hide_reblogs": "Hide boosts from @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} has requested to follow you",
   "account.share": "Share @{name}'s profile",
   "account.show_reblogs": "Show boosts from @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} posts}}",
   "account.unblock": "Unblock @{name}",
   "account.unblock_domain": "Unblock domain {domain}",
   "account.unblock_short": "Unblock",
@@ -411,6 +414,8 @@
   "limited_account_hint.action": "Show profile anyway",
   "limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.",
   "link_preview.author": "By {name}",
+  "link_preview.more_from_author": "More from {name}",
+  "link_preview.shares": "{count, plural, one {{counter} post} other {{counter} posts}}",
   "lists.account.add": "Add to list",
   "lists.account.remove": "Remove from list",
   "lists.delete": "Delete list",
@@ -691,8 +696,11 @@
   "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)",
   "server_banner.active_users": "active users",
   "server_banner.administered_by": "Administered by:",
+  "server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.",
   "server_banner.server_stats": "Server stats:",
   "sign_in_banner.create_account": "Create account",
+  "sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.",
+  "sign_in_banner.mastodon_is": "Mastodon is the best way to keep up with what's happening.",
   "sign_in_banner.sign_in": "Sign in",
   "sign_in_banner.sso_redirect": "Login or Register",
   "status.admin_account": "Open moderation interface for @{name}",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index ed9c0de60..ed877f766 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -88,7 +88,10 @@
   "audio.hide": "Skryť zvuk",
   "block_modal.show_less": "Zobraziť menej",
   "block_modal.show_more": "Zobraziť viac",
+  "block_modal.they_cant_mention": "Nemôžu ťa spomenúť, alebo nasledovať.",
+  "block_modal.they_will_know": "Môžu vidieť, že sú zablokovaní/ý.",
   "block_modal.title": "Blokovať užívateľa?",
+  "block_modal.you_wont_see_mentions": "Neuvidíš príspevky, ktoré ich spomínajú.",
   "boost_modal.combo": "Nabudúce môžete preskočiť stlačením {combo}",
   "bundle_column_error.copy_stacktrace": "Kopírovať chybovú hlášku",
   "bundle_column_error.error.body": "Požadovanú stránku nebolo možné vykresliť. Môže to byť spôsobené chybou v našom kóde alebo problémom s kompatibilitou prehliadača.",
diff --git a/config/locales/doorkeeper.en-GB.yml b/config/locales/doorkeeper.en-GB.yml
index b3ceffb13..f254825b1 100644
--- a/config/locales/doorkeeper.en-GB.yml
+++ b/config/locales/doorkeeper.en-GB.yml
@@ -135,6 +135,7 @@ en-GB:
         media: Media attachments
         mutes: Mutes
         notifications: Notifications
+        profile: Your Mastodon profile
         push: Push notifications
         reports: Reports
         search: Search
@@ -165,6 +166,7 @@ en-GB:
       admin:write:reports: perform moderation actions on reports
       crypto: use end-to-end encryption
       follow: modify account relationships
+      profile: read only your account's profile information
       push: receive your push notifications
       read: read all your account's data
       read:accounts: see accounts information
diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml
index 07eb84ebb..928823b99 100644
--- a/config/locales/en-GB.yml
+++ b/config/locales/en-GB.yml
@@ -285,6 +285,7 @@ en-GB:
         update_custom_emoji_html: "%{name} updated emoji %{target}"
         update_domain_block_html: "%{name} updated domain block for %{target}"
         update_ip_block_html: "%{name} changed rule for IP %{target}"
+        update_report_html: "%{name} updated report %{target}"
         update_status_html: "%{name} updated post by %{target}"
         update_user_role_html: "%{name} changed %{target} role"
       deleted_account: deleted account
@@ -292,6 +293,7 @@ en-GB:
       filter_by_action: Filter by action
       filter_by_user: Filter by user
       title: Audit log
+      unavailable_instance: "(domain name unavailable)"
     announcements:
       destroyed_msg: Announcement successfully deleted!
       edit:
@@ -950,6 +952,7 @@ en-GB:
       delete: Delete
       edit_preset: Edit warning preset
       empty: You haven't defined any warning presets yet.
+      title: Warning presets
     webhooks:
       add_new: Add endpoint
       delete: Delete

From ebd8e1bbb6465c78f6542fe7f09938fdb768dbb7 Mon Sep 17 00:00:00 2001
From: David Roetzel <david@roetzel.de>
Date: Wed, 3 Jul 2024 09:19:54 +0200
Subject: [PATCH 58/84] Add system check for missing database indexes (#30888)

---
 Gemfile                                       |  1 +
 Gemfile.lock                                  |  2 +
 app/lib/admin/db/schema_parser.rb             | 92 +++++++++++++++++++
 app/lib/admin/system_check.rb                 |  1 +
 .../system_check/missing_indexes_check.rb     | 36 ++++++++
 config/locales/en.yml                         |  2 +
 spec/lib/admin/db/schema_parser_spec.rb       | 49 ++++++++++
 .../missing_indexes_check_spec.rb             | 65 +++++++++++++
 8 files changed, 248 insertions(+)
 create mode 100644 app/lib/admin/db/schema_parser.rb
 create mode 100644 app/lib/admin/system_check/missing_indexes_check.rb
 create mode 100644 spec/lib/admin/db/schema_parser_spec.rb
 create mode 100644 spec/lib/admin/system_check/missing_indexes_check_spec.rb

diff --git a/Gemfile b/Gemfile
index be3f9e6f9..0d6cc4c4e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -229,3 +229,4 @@ gem 'rubyzip', '~> 2.3'
 gem 'hcaptcha', '~> 7.1'
 
 gem 'mail', '~> 2.8'
+gem 'prism'
diff --git a/Gemfile.lock b/Gemfile.lock
index 42cc0e198..969d18493 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -600,6 +600,7 @@ GEM
       actionmailer (>= 3)
       net-smtp
       premailer (~> 1.7, >= 1.7.9)
+    prism (0.30.0)
     propshaft (0.9.0)
       actionpack (>= 7.0.0)
       activesupport (>= 7.0.0)
@@ -1003,6 +1004,7 @@ DEPENDENCIES
   pg (~> 1.5)
   pghero
   premailer-rails
+  prism
   propshaft
   public_suffix (~> 6.0)
   puma (~> 6.3)
diff --git a/app/lib/admin/db/schema_parser.rb b/app/lib/admin/db/schema_parser.rb
new file mode 100644
index 000000000..e61a2281e
--- /dev/null
+++ b/app/lib/admin/db/schema_parser.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+class Admin::Db::SchemaParser
+  class Index
+    attr_reader :name, :table_name, :columns, :options
+
+    def initialize(name:, table_name:, columns:, options:)
+      @name = name
+      @table_name = table_name
+      @columns = columns
+      @options = options
+    end
+  end
+
+  attr_reader :indexes_by_table
+
+  def initialize(source)
+    parse(source)
+  end
+
+  private
+
+  def parse(source)
+    @indexes_by_table = {}
+    queue = [Prism.parse(source).value]
+    while (node = queue.shift)
+      if node.type == :call_node && node.name == :create_table
+        parse_create_table(node)
+      elsif node.type == :call_node && node.name == :add_index
+        parse_add_index(node)
+      else
+        queue.concat(node.compact_child_nodes)
+      end
+    end
+  end
+
+  def parse_create_table(node)
+    table_name = parse_arguments(node).first
+    queue = node.compact_child_nodes
+    while (node = queue.shift)
+      if node.type == :call_node && node.name == :index
+        parse_index(node, table_name:)
+      else
+        queue.concat(node.compact_child_nodes)
+      end
+    end
+  end
+
+  def parse_index(node, table_name:)
+    arguments = parse_arguments(node)
+    save_index(
+      name: arguments.last[:name],
+      table_name: table_name,
+      columns: arguments.first,
+      options: arguments.last
+    )
+  end
+
+  def parse_add_index(node)
+    arguments = parse_arguments(node)
+    save_index(
+      name: arguments.last[:name],
+      table_name: arguments.first,
+      columns: arguments[1],
+      options: arguments.last
+    )
+  end
+
+  def parse_arguments(node)
+    node.arguments.arguments.map { |a| parse_argument(a) }
+  end
+
+  def parse_argument(argument)
+    case argument
+    when Prism::StringNode
+      argument.unescaped
+    when Prism::SymbolNode
+      argument.unescaped.to_sym
+    when Prism::ArrayNode
+      argument.elements.map { |e| parse_argument(e) }
+    when Prism::KeywordHashNode
+      argument.elements.to_h do |element|
+        [element.key.unescaped.to_sym, parse_argument(element.value)]
+      end
+    end
+  end
+
+  def save_index(name:, table_name:, columns:, options:)
+    @indexes_by_table[table_name] ||= []
+    @indexes_by_table[table_name] << Index.new(name:, table_name:, columns:, options:)
+  end
+end
diff --git a/app/lib/admin/system_check.rb b/app/lib/admin/system_check.rb
index 25c88341a..453011f7a 100644
--- a/app/lib/admin/system_check.rb
+++ b/app/lib/admin/system_check.rb
@@ -8,6 +8,7 @@ class Admin::SystemCheck
     Admin::SystemCheck::SidekiqProcessCheck,
     Admin::SystemCheck::RulesCheck,
     Admin::SystemCheck::ElasticsearchCheck,
+    Admin::SystemCheck::MissingIndexesCheck,
   ].freeze
 
   def self.perform(current_user)
diff --git a/app/lib/admin/system_check/missing_indexes_check.rb b/app/lib/admin/system_check/missing_indexes_check.rb
new file mode 100644
index 000000000..b7eecbb06
--- /dev/null
+++ b/app/lib/admin/system_check/missing_indexes_check.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class Admin::SystemCheck::MissingIndexesCheck < Admin::SystemCheck::BaseCheck
+  def skip?
+    !current_user.can?(:view_devops)
+  end
+
+  def pass?
+    missing_indexes.none?
+  end
+
+  def message
+    Admin::SystemCheck::Message.new(:missing_indexes_check, missing_indexes.join(', '))
+  end
+
+  private
+
+  def missing_indexes
+    @missing_indexes ||= begin
+      expected_indexes_by_table.flat_map do |table, indexes|
+        expected_indexes = indexes.map(&:name)
+        expected_indexes - existing_indexes_for(table)
+      end
+    end
+  end
+
+  def expected_indexes_by_table
+    schema_rb = Rails.root.join('db', 'schema.rb').read
+    schema_parser = Admin::Db::SchemaParser.new(schema_rb)
+    schema_parser.indexes_by_table
+  end
+
+  def existing_indexes_for(table)
+    ActiveRecord::Base.connection.indexes(table).map(&:name)
+  end
+end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 20df80c27..270382dd1 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -862,6 +862,8 @@ en:
       elasticsearch_version_check:
         message_html: 'Incompatible Elasticsearch version: %{value}'
         version_comparison: Elasticsearch %{running_version} is running while %{required_version} is required
+      missing_indexes_check:
+        message_html: 'The following indexes are missing from the database and should be recreated: <code>%{value}</code>.<br> Missing indexes may lead to severely reduced performance and data inconsistencies.'
       rules_check:
         action: Manage server rules
         message_html: You haven't defined any server rules.
diff --git a/spec/lib/admin/db/schema_parser_spec.rb b/spec/lib/admin/db/schema_parser_spec.rb
new file mode 100644
index 000000000..e28d5c1f9
--- /dev/null
+++ b/spec/lib/admin/db/schema_parser_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::Db::SchemaParser do
+  let(:dummy_schema) do
+    <<~SCHEMA
+      # Comment
+      ActiveRecord::Schema[7.1].define(version: 23) do
+        create_table "people", force: :cascade do |t|
+          t.string "name"
+        end
+
+        create_table "posts", force: :cascade do |t|
+          t.string "title", null: false
+          t.bigint "size", null: false
+          t.string "description"
+          # t.index ["size", "title"], name: "index_posts_on_size_and_title"
+          t.index ["title"], name: "index_posts_on_title", unique: true
+          t.index ["size"], name: "index_posts_on_size"
+        end
+
+        # add_index "people", ["name"], name: "commented_out_index"
+        add_index "people", ["name"], name: "index_people_on_name"
+      end
+    SCHEMA
+  end
+  let(:schema_parser) { described_class.new(dummy_schema) }
+
+  describe '#indexes_by_table' do
+    subject { schema_parser.indexes_by_table }
+
+    it 'returns index info for all affected tables' do
+      expect(subject.keys).to match_array(%w(people posts))
+    end
+
+    it 'returns all index information for the `people` table' do
+      people_info = subject['people']
+      expect(people_info.map(&:name)).to contain_exactly('index_people_on_name')
+    end
+
+    it 'returns all index information for the `posts` table' do
+      posts_info = subject['posts']
+      expect(posts_info.map(&:name)).to contain_exactly(
+        'index_posts_on_title', 'index_posts_on_size'
+      )
+    end
+  end
+end
diff --git a/spec/lib/admin/system_check/missing_indexes_check_spec.rb b/spec/lib/admin/system_check/missing_indexes_check_spec.rb
new file mode 100644
index 000000000..e183be562
--- /dev/null
+++ b/spec/lib/admin/system_check/missing_indexes_check_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::SystemCheck::MissingIndexesCheck do
+  subject(:check) { described_class.new(user) }
+
+  let(:user) { Fabricate(:user) }
+  let(:schema_parser) do
+    instance_double(Admin::Db::SchemaParser, indexes_by_table: index_info)
+  end
+  let(:index_info) do
+    {
+      'users' => [instance_double(Admin::Db::SchemaParser::Index, name: 'index_users_on_profile_id')],
+      'posts' => [instance_double(Admin::Db::SchemaParser::Index, name: 'index_posts_on_user_id')],
+    }
+  end
+  let(:posts_indexes) { [] }
+  let(:users_indexes) { [] }
+
+  before do
+    allow(Admin::Db::SchemaParser).to receive(:new).and_return(schema_parser)
+    allow(ActiveRecord::Base.connection).to receive(:indexes).with('posts').and_return(posts_indexes)
+    allow(ActiveRecord::Base.connection).to receive(:indexes).with('users').and_return(users_indexes)
+  end
+
+  it_behaves_like 'a check available to devops users'
+
+  describe '#pass?' do
+    context 'when indexes are missing' do
+      let(:posts_indexes) do
+        [instance_double(ActiveRecord::ConnectionAdapters::IndexDefinition, name: 'index_posts_on_user_id')]
+      end
+
+      it 'returns false' do
+        expect(check.pass?).to be false
+      end
+    end
+
+    context 'when all expected indexes are present' do
+      let(:posts_indexes) do
+        [instance_double(ActiveRecord::ConnectionAdapters::IndexDefinition, name: 'index_posts_on_user_id')]
+      end
+      let(:users_indexes) do
+        [instance_double(ActiveRecord::ConnectionAdapters::IndexDefinition, name: 'index_users_on_profile_id')]
+      end
+
+      it 'returns true' do
+        expect(check.pass?).to be true
+      end
+    end
+  end
+
+  describe '#message' do
+    subject { check.message }
+
+    it 'sets the class name as the message key' do
+      expect(subject.key).to eq(:missing_indexes_check)
+    end
+
+    it 'sets a list of missing indexes as message value' do
+      expect(subject.value).to eq('index_users_on_profile_id, index_posts_on_user_id')
+    end
+  end
+end

From 2e295bd5e7d3e7fefd4d4f03279f0dd0f3e5427d Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Jul 2024 07:20:48 +0000
Subject: [PATCH 59/84] chore(deps): update dependency aws-sdk-s3 to v1.156.0
 (#30899)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index 969d18493..d855c05f4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -100,19 +100,19 @@ GEM
     attr_required (1.0.2)
     awrence (1.2.1)
     aws-eventstream (1.3.0)
-    aws-partitions (1.949.0)
-    aws-sdk-core (3.200.0)
+    aws-partitions (1.950.0)
+    aws-sdk-core (3.201.0)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.8)
       jmespath (~> 1, >= 1.6.1)
-    aws-sdk-kms (1.87.0)
-      aws-sdk-core (~> 3, >= 3.199.0)
-      aws-sigv4 (~> 1.1)
-    aws-sdk-s3 (1.155.0)
-      aws-sdk-core (~> 3, >= 3.199.0)
+    aws-sdk-kms (1.88.0)
+      aws-sdk-core (~> 3, >= 3.201.0)
+      aws-sigv4 (~> 1.5)
+    aws-sdk-s3 (1.156.0)
+      aws-sdk-core (~> 3, >= 3.201.0)
       aws-sdk-kms (~> 1)
-      aws-sigv4 (~> 1.8)
+      aws-sigv4 (~> 1.5)
     aws-sigv4 (1.8.0)
       aws-eventstream (~> 1, >= 1.0.2)
     azure-storage-blob (2.0.3)

From ba7e7a6368f4e6097a7c6ee7145c5ff43e906517 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Jul 2024 09:21:05 +0200
Subject: [PATCH 60/84] chore(deps): update opentelemetry-ruby (non-major)
 (#30898)

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

diff --git a/Gemfile b/Gemfile
index 0d6cc4c4e..d1a8c2c5a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -114,7 +114,7 @@ group :opentelemetry do
   gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
   gem 'opentelemetry-instrumentation-pg', '~> 0.27.1', require: false
   gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false
-  gem 'opentelemetry-instrumentation-rails', '~> 0.30.0', require: false
+  gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false
   gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
   gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
   gem 'opentelemetry-sdk', '~> 1.4', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index d855c05f4..e66077ff0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -525,7 +525,7 @@ GEM
     opentelemetry-instrumentation-active_record (0.7.2)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.22.1)
-    opentelemetry-instrumentation-active_support (0.5.1)
+    opentelemetry-instrumentation-active_support (0.6.0)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.22.1)
     opentelemetry-instrumentation-base (0.22.3)
@@ -556,14 +556,14 @@ GEM
     opentelemetry-instrumentation-rack (0.24.5)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.22.1)
-    opentelemetry-instrumentation-rails (0.30.2)
+    opentelemetry-instrumentation-rails (0.31.0)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-action_mailer (~> 0.1.0)
       opentelemetry-instrumentation-action_pack (~> 0.9.0)
       opentelemetry-instrumentation-action_view (~> 0.7.0)
       opentelemetry-instrumentation-active_job (~> 0.7.0)
       opentelemetry-instrumentation-active_record (~> 0.7.0)
-      opentelemetry-instrumentation-active_support (~> 0.5.0)
+      opentelemetry-instrumentation-active_support (~> 0.6.0)
       opentelemetry-instrumentation-base (~> 0.22.1)
     opentelemetry-instrumentation-redis (0.25.6)
       opentelemetry-api (~> 1.0)
@@ -995,7 +995,7 @@ DEPENDENCIES
   opentelemetry-instrumentation-net_http (~> 0.22.4)
   opentelemetry-instrumentation-pg (~> 0.27.1)
   opentelemetry-instrumentation-rack (~> 0.24.1)
-  opentelemetry-instrumentation-rails (~> 0.30.0)
+  opentelemetry-instrumentation-rails (~> 0.31.0)
   opentelemetry-instrumentation-redis (~> 0.25.3)
   opentelemetry-instrumentation-sidekiq (~> 0.25.2)
   opentelemetry-sdk (~> 1.4)

From dd85e3bcc5e04a11775cbdd9cb1f7d3577ddd9d7 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 3 Jul 2024 07:30:30 +0000
Subject: [PATCH 61/84] New Crowdin Translations (automated) (#30901)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/uk.json | 5 +++++
 config/locales/cs.yml                   | 2 +-
 config/locales/simple_form.cs.yml       | 2 +-
 3 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 338b65061..150b808f8 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Підписатися взаємно",
   "account.followers": "Підписники",
   "account.followers.empty": "Ніхто ще не підписаний на цього користувача.",
+  "account.followers_counter": "{count, plural, one {{counter} підписник} few {{counter} підписники} many {{counter} підписників} other {{counter} підписники}}",
   "account.following": "Ви стежите",
+  "account.following_counter": "{count, plural, one {{counter} підписка} few {{counter} підписки} many {{counter} підписок} other {{counter} підписки}}",
   "account.follows.empty": "Цей користувач ще ні на кого не підписався.",
   "account.go_to_profile": "Перейти до профілю",
   "account.hide_reblogs": "Сховати поширення від @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} надсилає запит на стеження",
   "account.share": "Поділитися профілем @{name}",
   "account.show_reblogs": "Показати поширення від @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}}",
   "account.unblock": "Розблокувати @{name}",
   "account.unblock_domain": "Розблокувати {domain}",
   "account.unblock_short": "Розблокувати",
@@ -412,6 +415,7 @@
   "limited_account_hint.title": "Цей профіль сховали модератори {domain}.",
   "link_preview.author": "Від {name}",
   "link_preview.more_from_author": "Більше від {name}",
+  "link_preview.shares": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}}",
   "lists.account.add": "Додати до списку",
   "lists.account.remove": "Вилучити зі списку",
   "lists.delete": "Видалити список",
@@ -695,6 +699,7 @@
   "server_banner.is_one_of_many": "{domain} - один з багатьох незалежних серверів Mastodon, які ви можете використати, щоб брати участь у федівері.",
   "server_banner.server_stats": "Статистика сервера:",
   "sign_in_banner.create_account": "Створити обліковий запис",
+  "sign_in_banner.follow_anyone": "Слідкуйте за ким завгодно у всьому fediverse і дивіться все це в хронологічному порядку. Немає алгоритмів, реклами чи наживок для натискань при перегляді.",
   "sign_in_banner.mastodon_is": "Мастодон - найкращий спосіб продовжувати свою справу.",
   "sign_in_banner.sign_in": "Увійти",
   "sign_in_banner.sso_redirect": "Увійдіть або зареєструйтесь",
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index f3b8f27d8..20e7e4d46 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -81,7 +81,7 @@ cs:
       invite_request_text: Důvody založení
       invited_by: Pozván uživatelem
       ip: IP adresa
-      joined: Uživatel založen
+      joined: Uživatelem od
       location:
         all: Všechny
         local: Místní
diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml
index 0b1a34e1b..f8422102f 100644
--- a/config/locales/simple_form.cs.yml
+++ b/config/locales/simple_form.cs.yml
@@ -59,7 +59,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_use_blurhash: Gradienty jsou založeny na barvách skryté grafiky, ale zakrývají jakékoliv detaily
+        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
         whole_word: Je-li klíčové slovo či fráze pouze alfanumerická, bude aplikován pouze, pokud se shoduje s celým slovem

From 5651c16d11a3bb35e44c260b0c18afcd451fd5aa Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 3 Jul 2024 03:47:40 -0400
Subject: [PATCH 62/84] Limit `browser` version to enforce ruby 3.1 support
 (#30766)

---
 Gemfile      | 2 +-
 Gemfile.lock | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Gemfile b/Gemfile
index d1a8c2c5a..769f834f4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -25,7 +25,7 @@ gem 'ruby-vips', '~> 2.2', require: false
 gem 'active_model_serializers', '~> 0.10'
 gem 'addressable', '~> 2.8'
 gem 'bootsnap', '~> 1.18.0', require: false
-gem 'browser'
+gem 'browser', '< 6' # https://github.com/fnando/browser/issues/543
 gem 'charlock_holmes', '~> 0.7.7'
 gem 'chewy', '~> 7.3'
 gem 'devise', '~> 4.9'
diff --git a/Gemfile.lock b/Gemfile.lock
index e66077ff0..aafad69ed 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -917,7 +917,7 @@ DEPENDENCIES
   blurhash (~> 0.1)
   bootsnap (~> 1.18.0)
   brakeman (~> 6.0)
-  browser
+  browser (< 6)
   bundler-audit (~> 0.9)
   capybara (~> 3.39)
   charlock_holmes (~> 0.7.7)

From 1dbffc30f12d20f3c5f038c72b06967480f82129 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Jul 2024 08:04:38 +0000
Subject: [PATCH 63/84] chore(deps): update opentelemetry-ruby (non-major)
 (#30903)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index aafad69ed..30835adef 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -516,7 +516,7 @@ GEM
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-active_support (~> 0.1)
       opentelemetry-instrumentation-base (~> 0.22.1)
-    opentelemetry-instrumentation-active_job (0.7.1)
+    opentelemetry-instrumentation-active_job (0.7.2)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.22.1)
     opentelemetry-instrumentation-active_model_serializers (0.20.1)
@@ -568,7 +568,7 @@ GEM
     opentelemetry-instrumentation-redis (0.25.6)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.22.1)
-    opentelemetry-instrumentation-sidekiq (0.25.5)
+    opentelemetry-instrumentation-sidekiq (0.25.6)
       opentelemetry-api (~> 1.0)
       opentelemetry-instrumentation-base (~> 0.22.1)
     opentelemetry-registry (0.3.1)

From 6270281037c1f0991bfbf8d195280be3f5733bf3 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Jul 2024 08:05:19 +0000
Subject: [PATCH 64/84] chore(deps): update dependency doorkeeper to v5.7.1
 (#30053)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index 30835adef..6444e4687 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -208,7 +208,7 @@ GEM
       activerecord (>= 4.2, < 8)
     docile (1.4.0)
     domain_name (0.6.20240107)
-    doorkeeper (5.6.9)
+    doorkeeper (5.7.1)
       railties (>= 5)
     dotenv (3.1.2)
     drb (2.2.1)
@@ -431,7 +431,7 @@ GEM
     mime-types-data (3.2024.0604)
     mini_mime (1.1.5)
     mini_portile2 (2.8.7)
-    minitest (5.23.1)
+    minitest (5.24.1)
     msgpack (1.7.2)
     multi_json (1.15.0)
     multipart-post (2.4.0)

From f99159d1eb671d72138edc42d11cd58472c33aec Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Jul 2024 10:46:51 +0200
Subject: [PATCH 65/84] fix(deps): update dependency webpack-merge to v6
 (#30891)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 package.json |  2 +-
 yarn.lock    | 14 +++++++-------
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/package.json b/package.json
index 0379c7a5f..404c4f486 100644
--- a/package.json
+++ b/package.json
@@ -127,7 +127,7 @@
     "webpack-assets-manifest": "^4.0.6",
     "webpack-bundle-analyzer": "^4.8.0",
     "webpack-cli": "^3.3.12",
-    "webpack-merge": "^5.9.0",
+    "webpack-merge": "^6.0.0",
     "wicg-inert": "^3.1.2",
     "workbox-expiration": "^7.0.0",
     "workbox-precaching": "^7.0.0",
diff --git a/yarn.lock b/yarn.lock
index 9d19cb837..8e72138a1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2901,7 +2901,7 @@ __metadata:
     webpack-bundle-analyzer: "npm:^4.8.0"
     webpack-cli: "npm:^3.3.12"
     webpack-dev-server: "npm:^3.11.3"
-    webpack-merge: "npm:^5.9.0"
+    webpack-merge: "npm:^6.0.0"
     wicg-inert: "npm:^3.1.2"
     workbox-expiration: "npm:^7.0.0"
     workbox-precaching: "npm:^7.0.0"
@@ -17928,14 +17928,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"webpack-merge@npm:^5.9.0":
-  version: 5.10.0
-  resolution: "webpack-merge@npm:5.10.0"
+"webpack-merge@npm:^6.0.0":
+  version: 6.0.1
+  resolution: "webpack-merge@npm:6.0.1"
   dependencies:
     clone-deep: "npm:^4.0.1"
     flat: "npm:^5.0.2"
-    wildcard: "npm:^2.0.0"
-  checksum: 10c0/b607c84cabaf74689f965420051a55a08722d897bdd6c29cb0b2263b451c090f962d41ecf8c9bf56b0ab3de56e65476ace0a8ecda4f4a4663684243d90e0512b
+    wildcard: "npm:^2.0.1"
+  checksum: 10c0/bf1429567858b353641801b8a2696ca0aac270fc8c55d4de8a7b586fe07d27fdcfc83099a98ab47e6162383db8dd63bb8cc25b1beb2ec82150422eec843b0dc0
   languageName: node
   linkType: hard
 
@@ -18183,7 +18183,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"wildcard@npm:^2.0.0":
+"wildcard@npm:^2.0.1":
   version: 2.0.1
   resolution: "wildcard@npm:2.0.1"
   checksum: 10c0/08f70cd97dd9a20aea280847a1fe8148e17cae7d231640e41eb26d2388697cbe65b67fd9e68715251c39b080c5ae4f76d71a9a69fa101d897273efdfb1b58bf7

From 20c749bd45286fbf0aca5610562e5dc6f2c616dd Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 3 Jul 2024 11:14:15 +0200
Subject: [PATCH 66/84] chore(deps): update dependency rubocop-rspec to v3.0.2
 (#30902)

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 6444e4687..ca36c8cf9 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -757,7 +757,7 @@ GEM
       rack (>= 1.1)
       rubocop (>= 1.33.0, < 2.0)
       rubocop-ast (>= 1.31.1, < 2.0)
-    rubocop-rspec (3.0.1)
+    rubocop-rspec (3.0.2)
       rubocop (~> 1.61)
     rubocop-rspec_rails (2.30.0)
       rubocop (~> 1.61)

From 9be77fc0dbb01c1a8a54cd3da97e16c7941df367 Mon Sep 17 00:00:00 2001
From: David Roetzel <david@roetzel.de>
Date: Wed, 3 Jul 2024 15:36:42 +0200
Subject: [PATCH 67/84] Revert "Add system check for missing database indexes"
 (#30909)

---
 Gemfile                                       |  1 -
 Gemfile.lock                                  |  2 -
 app/lib/admin/db/schema_parser.rb             | 92 -------------------
 app/lib/admin/system_check.rb                 |  1 -
 .../system_check/missing_indexes_check.rb     | 36 --------
 config/locales/en.yml                         |  2 -
 spec/lib/admin/db/schema_parser_spec.rb       | 49 ----------
 .../missing_indexes_check_spec.rb             | 65 -------------
 8 files changed, 248 deletions(-)
 delete mode 100644 app/lib/admin/db/schema_parser.rb
 delete mode 100644 app/lib/admin/system_check/missing_indexes_check.rb
 delete mode 100644 spec/lib/admin/db/schema_parser_spec.rb
 delete mode 100644 spec/lib/admin/system_check/missing_indexes_check_spec.rb

diff --git a/Gemfile b/Gemfile
index 769f834f4..ef52d50ca 100644
--- a/Gemfile
+++ b/Gemfile
@@ -229,4 +229,3 @@ gem 'rubyzip', '~> 2.3'
 gem 'hcaptcha', '~> 7.1'
 
 gem 'mail', '~> 2.8'
-gem 'prism'
diff --git a/Gemfile.lock b/Gemfile.lock
index ca36c8cf9..eb6720e45 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -600,7 +600,6 @@ GEM
       actionmailer (>= 3)
       net-smtp
       premailer (~> 1.7, >= 1.7.9)
-    prism (0.30.0)
     propshaft (0.9.0)
       actionpack (>= 7.0.0)
       activesupport (>= 7.0.0)
@@ -1004,7 +1003,6 @@ DEPENDENCIES
   pg (~> 1.5)
   pghero
   premailer-rails
-  prism
   propshaft
   public_suffix (~> 6.0)
   puma (~> 6.3)
diff --git a/app/lib/admin/db/schema_parser.rb b/app/lib/admin/db/schema_parser.rb
deleted file mode 100644
index e61a2281e..000000000
--- a/app/lib/admin/db/schema_parser.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# frozen_string_literal: true
-
-class Admin::Db::SchemaParser
-  class Index
-    attr_reader :name, :table_name, :columns, :options
-
-    def initialize(name:, table_name:, columns:, options:)
-      @name = name
-      @table_name = table_name
-      @columns = columns
-      @options = options
-    end
-  end
-
-  attr_reader :indexes_by_table
-
-  def initialize(source)
-    parse(source)
-  end
-
-  private
-
-  def parse(source)
-    @indexes_by_table = {}
-    queue = [Prism.parse(source).value]
-    while (node = queue.shift)
-      if node.type == :call_node && node.name == :create_table
-        parse_create_table(node)
-      elsif node.type == :call_node && node.name == :add_index
-        parse_add_index(node)
-      else
-        queue.concat(node.compact_child_nodes)
-      end
-    end
-  end
-
-  def parse_create_table(node)
-    table_name = parse_arguments(node).first
-    queue = node.compact_child_nodes
-    while (node = queue.shift)
-      if node.type == :call_node && node.name == :index
-        parse_index(node, table_name:)
-      else
-        queue.concat(node.compact_child_nodes)
-      end
-    end
-  end
-
-  def parse_index(node, table_name:)
-    arguments = parse_arguments(node)
-    save_index(
-      name: arguments.last[:name],
-      table_name: table_name,
-      columns: arguments.first,
-      options: arguments.last
-    )
-  end
-
-  def parse_add_index(node)
-    arguments = parse_arguments(node)
-    save_index(
-      name: arguments.last[:name],
-      table_name: arguments.first,
-      columns: arguments[1],
-      options: arguments.last
-    )
-  end
-
-  def parse_arguments(node)
-    node.arguments.arguments.map { |a| parse_argument(a) }
-  end
-
-  def parse_argument(argument)
-    case argument
-    when Prism::StringNode
-      argument.unescaped
-    when Prism::SymbolNode
-      argument.unescaped.to_sym
-    when Prism::ArrayNode
-      argument.elements.map { |e| parse_argument(e) }
-    when Prism::KeywordHashNode
-      argument.elements.to_h do |element|
-        [element.key.unescaped.to_sym, parse_argument(element.value)]
-      end
-    end
-  end
-
-  def save_index(name:, table_name:, columns:, options:)
-    @indexes_by_table[table_name] ||= []
-    @indexes_by_table[table_name] << Index.new(name:, table_name:, columns:, options:)
-  end
-end
diff --git a/app/lib/admin/system_check.rb b/app/lib/admin/system_check.rb
index 453011f7a..25c88341a 100644
--- a/app/lib/admin/system_check.rb
+++ b/app/lib/admin/system_check.rb
@@ -8,7 +8,6 @@ class Admin::SystemCheck
     Admin::SystemCheck::SidekiqProcessCheck,
     Admin::SystemCheck::RulesCheck,
     Admin::SystemCheck::ElasticsearchCheck,
-    Admin::SystemCheck::MissingIndexesCheck,
   ].freeze
 
   def self.perform(current_user)
diff --git a/app/lib/admin/system_check/missing_indexes_check.rb b/app/lib/admin/system_check/missing_indexes_check.rb
deleted file mode 100644
index b7eecbb06..000000000
--- a/app/lib/admin/system_check/missing_indexes_check.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-class Admin::SystemCheck::MissingIndexesCheck < Admin::SystemCheck::BaseCheck
-  def skip?
-    !current_user.can?(:view_devops)
-  end
-
-  def pass?
-    missing_indexes.none?
-  end
-
-  def message
-    Admin::SystemCheck::Message.new(:missing_indexes_check, missing_indexes.join(', '))
-  end
-
-  private
-
-  def missing_indexes
-    @missing_indexes ||= begin
-      expected_indexes_by_table.flat_map do |table, indexes|
-        expected_indexes = indexes.map(&:name)
-        expected_indexes - existing_indexes_for(table)
-      end
-    end
-  end
-
-  def expected_indexes_by_table
-    schema_rb = Rails.root.join('db', 'schema.rb').read
-    schema_parser = Admin::Db::SchemaParser.new(schema_rb)
-    schema_parser.indexes_by_table
-  end
-
-  def existing_indexes_for(table)
-    ActiveRecord::Base.connection.indexes(table).map(&:name)
-  end
-end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 270382dd1..20df80c27 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -862,8 +862,6 @@ en:
       elasticsearch_version_check:
         message_html: 'Incompatible Elasticsearch version: %{value}'
         version_comparison: Elasticsearch %{running_version} is running while %{required_version} is required
-      missing_indexes_check:
-        message_html: 'The following indexes are missing from the database and should be recreated: <code>%{value}</code>.<br> Missing indexes may lead to severely reduced performance and data inconsistencies.'
       rules_check:
         action: Manage server rules
         message_html: You haven't defined any server rules.
diff --git a/spec/lib/admin/db/schema_parser_spec.rb b/spec/lib/admin/db/schema_parser_spec.rb
deleted file mode 100644
index e28d5c1f9..000000000
--- a/spec/lib/admin/db/schema_parser_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Admin::Db::SchemaParser do
-  let(:dummy_schema) do
-    <<~SCHEMA
-      # Comment
-      ActiveRecord::Schema[7.1].define(version: 23) do
-        create_table "people", force: :cascade do |t|
-          t.string "name"
-        end
-
-        create_table "posts", force: :cascade do |t|
-          t.string "title", null: false
-          t.bigint "size", null: false
-          t.string "description"
-          # t.index ["size", "title"], name: "index_posts_on_size_and_title"
-          t.index ["title"], name: "index_posts_on_title", unique: true
-          t.index ["size"], name: "index_posts_on_size"
-        end
-
-        # add_index "people", ["name"], name: "commented_out_index"
-        add_index "people", ["name"], name: "index_people_on_name"
-      end
-    SCHEMA
-  end
-  let(:schema_parser) { described_class.new(dummy_schema) }
-
-  describe '#indexes_by_table' do
-    subject { schema_parser.indexes_by_table }
-
-    it 'returns index info for all affected tables' do
-      expect(subject.keys).to match_array(%w(people posts))
-    end
-
-    it 'returns all index information for the `people` table' do
-      people_info = subject['people']
-      expect(people_info.map(&:name)).to contain_exactly('index_people_on_name')
-    end
-
-    it 'returns all index information for the `posts` table' do
-      posts_info = subject['posts']
-      expect(posts_info.map(&:name)).to contain_exactly(
-        'index_posts_on_title', 'index_posts_on_size'
-      )
-    end
-  end
-end
diff --git a/spec/lib/admin/system_check/missing_indexes_check_spec.rb b/spec/lib/admin/system_check/missing_indexes_check_spec.rb
deleted file mode 100644
index e183be562..000000000
--- a/spec/lib/admin/system_check/missing_indexes_check_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Admin::SystemCheck::MissingIndexesCheck do
-  subject(:check) { described_class.new(user) }
-
-  let(:user) { Fabricate(:user) }
-  let(:schema_parser) do
-    instance_double(Admin::Db::SchemaParser, indexes_by_table: index_info)
-  end
-  let(:index_info) do
-    {
-      'users' => [instance_double(Admin::Db::SchemaParser::Index, name: 'index_users_on_profile_id')],
-      'posts' => [instance_double(Admin::Db::SchemaParser::Index, name: 'index_posts_on_user_id')],
-    }
-  end
-  let(:posts_indexes) { [] }
-  let(:users_indexes) { [] }
-
-  before do
-    allow(Admin::Db::SchemaParser).to receive(:new).and_return(schema_parser)
-    allow(ActiveRecord::Base.connection).to receive(:indexes).with('posts').and_return(posts_indexes)
-    allow(ActiveRecord::Base.connection).to receive(:indexes).with('users').and_return(users_indexes)
-  end
-
-  it_behaves_like 'a check available to devops users'
-
-  describe '#pass?' do
-    context 'when indexes are missing' do
-      let(:posts_indexes) do
-        [instance_double(ActiveRecord::ConnectionAdapters::IndexDefinition, name: 'index_posts_on_user_id')]
-      end
-
-      it 'returns false' do
-        expect(check.pass?).to be false
-      end
-    end
-
-    context 'when all expected indexes are present' do
-      let(:posts_indexes) do
-        [instance_double(ActiveRecord::ConnectionAdapters::IndexDefinition, name: 'index_posts_on_user_id')]
-      end
-      let(:users_indexes) do
-        [instance_double(ActiveRecord::ConnectionAdapters::IndexDefinition, name: 'index_users_on_profile_id')]
-      end
-
-      it 'returns true' do
-        expect(check.pass?).to be true
-      end
-    end
-  end
-
-  describe '#message' do
-    subject { check.message }
-
-    it 'sets the class name as the message key' do
-      expect(subject.key).to eq(:missing_indexes_check)
-    end
-
-    it 'sets a list of missing indexes as message value' do
-      expect(subject.value).to eq('index_users_on_profile_id, index_posts_on_user_id')
-    end
-  end
-end

From 47f0faebc9cb197ea7b4cf8d191df0a1ec926f59 Mon Sep 17 00:00:00 2001
From: Emelia Smith <ThisIsMissEm@users.noreply.github.com>
Date: Wed, 3 Jul 2024 22:05:59 +0200
Subject: [PATCH 68/84] Implement HTML ruby tags for east-asian languages
 (#30897)

---
 lib/sanitize_ext/sanitize_config.rb   | 2 +-
 spec/lib/html_aware_formatter_spec.rb | 8 ++++++++
 spec/lib/plain_text_formatter_spec.rb | 8 ++++++++
 spec/lib/sanitize/config_spec.rb      | 4 ++++
 4 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb
index 70efe7c1a..3379823cb 100644
--- a/lib/sanitize_ext/sanitize_config.rb
+++ b/lib/sanitize_ext/sanitize_config.rb
@@ -65,7 +65,7 @@ class Sanitize
     end
 
     MASTODON_STRICT = freeze_config(
-      elements: %w(p br span a del pre blockquote code b strong u i em ul ol li),
+      elements: %w(p br span a del pre blockquote code b strong u i em ul ol li ruby rt rp),
 
       attributes: {
         'a' => %w(href rel class translate),
diff --git a/spec/lib/html_aware_formatter_spec.rb b/spec/lib/html_aware_formatter_spec.rb
index a20902d4f..b75ccb06e 100644
--- a/spec/lib/html_aware_formatter_spec.rb
+++ b/spec/lib/html_aware_formatter_spec.rb
@@ -41,6 +41,14 @@ RSpec.describe HtmlAwareFormatter do
           expect(subject).to_not include 'status__content__spoiler-link'
         end
       end
+
+      context 'when given text containing ruby tags for east-asian languages' do
+        let(:text) { '<ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby>' }
+
+        it 'keeps the ruby tags' do
+          expect(subject).to eq '<ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby>'
+        end
+      end
     end
   end
 end
diff --git a/spec/lib/plain_text_formatter_spec.rb b/spec/lib/plain_text_formatter_spec.rb
index 80b3c331a..b22f473d0 100644
--- a/spec/lib/plain_text_formatter_spec.rb
+++ b/spec/lib/plain_text_formatter_spec.rb
@@ -72,6 +72,14 @@ RSpec.describe PlainTextFormatter do
           expect(subject).to eq 'Lorem ipsum'
         end
       end
+
+      context 'when text contains HTML ruby tags' do
+        let(:status) { Fabricate(:status, account: remote_account, text: '<p>Lorem <ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby> ipsum</p>') }
+
+        it 'strips the comment' do
+          expect(subject).to eq 'Lorem 明日 (Ashita) ipsum'
+        end
+      end
     end
   end
 end
diff --git a/spec/lib/sanitize/config_spec.rb b/spec/lib/sanitize/config_spec.rb
index 2d8dc2f63..fe0b272c0 100644
--- a/spec/lib/sanitize/config_spec.rb
+++ b/spec/lib/sanitize/config_spec.rb
@@ -18,6 +18,10 @@ describe Sanitize::Config do
       expect(Sanitize.fragment('<p>Check out:</p><ol start="3" reversed=""><li>Foo</li><li>Bar</li></ol>', subject)).to eq '<p>Check out:</p><ol start="3" reversed=""><li>Foo</li><li>Bar</li></ol>'
     end
 
+    it 'keeps ruby tags' do
+      expect(Sanitize.fragment('<p><ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby></p>', subject)).to eq '<p><ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby></p>'
+    end
+
     it 'removes a without href' do
       expect(Sanitize.fragment('<a>Test</a>', subject)).to eq 'Test'
     end

From 8331f9e379220847020aed9cd0a8a1d6ab0e7d43 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 4 Jul 2024 10:46:27 +0200
Subject: [PATCH 69/84] New Crowdin Translations (automated) (#30916)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/gl.json      | 2 +-
 app/javascript/mastodon/locales/he.json      | 8 +++++++-
 app/javascript/mastodon/locales/ia.json      | 4 ++--
 app/javascript/mastodon/locales/sr-Latn.json | 3 +++
 app/javascript/mastodon/locales/sr.json      | 3 +++
 config/locales/ia.yml                        | 8 ++++----
 config/locales/simple_form.ja.yml            | 2 +-
 7 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index fae48ed06..03287c7e5 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -224,7 +224,7 @@
   "domain_pill.their_server": "O seu fogar dixital, onde están as súas publicacións.",
   "domain_pill.their_username": "O seu identificador único no seu servidor. É posible atopar usuarias co mesmo nome de usuaria en diferentes servidores.",
   "domain_pill.username": "Nome de usuaria",
-  "domain_pill.whats_in_a_handle": "Que é o alcume?",
+  "domain_pill.whats_in_a_handle": "As partes do alcume?",
   "domain_pill.who_they_are": "O alcume dinos quen é esa persoa e onde está, para que poidas interactuar con ela en toda a web social de <button>plataformas ActivityPub</button>.",
   "domain_pill.who_you_are": "Como o teu alcume informa de quen es e onde estás, as persoas poden interactuar contigo desde toda a web social de <button>plataformas ActivityPub</button>.",
   "domain_pill.your_handle": "O teu alcume:",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index e022eac11..8111a56e8 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -35,7 +35,9 @@
   "account.follow_back": "לעקוב בחזרה",
   "account.followers": "עוקבים",
   "account.followers.empty": "אף אחד לא עוקב אחר המשתמש הזה עדיין.",
+  "account.followers_counter": "{count, plural,one {עוקב אחד} other {{count} עוקבים}}",
   "account.following": "נעקבים",
+  "account.following_counter": "{count, plural,one {עוקב אחרי {count}}other {עוקב אחרי {count}}}",
   "account.follows.empty": "משתמש זה עדיין לא עוקב אחרי אף אחד.",
   "account.go_to_profile": "מעבר לפרופיל",
   "account.hide_reblogs": "להסתיר הידהודים מאת @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} ביקשו לעקוב אחריך",
   "account.share": "שתף את הפרופיל של @{name}",
   "account.show_reblogs": "הצג הדהודים מאת @{name}",
+  "account.statuses_counter": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}",
   "account.unblock": "להסיר חסימה ל- @{name}",
   "account.unblock_domain": "הסירי את החסימה של קהילת {domain}",
   "account.unblock_short": "הסר חסימה",
@@ -693,8 +696,11 @@
   "server_banner.about_active_users": "משתמשים פעילים בשרת ב־30 הימים האחרונים (משתמשים פעילים חודשיים)",
   "server_banner.active_users": "משתמשים פעילים",
   "server_banner.administered_by": "מנוהל ע\"י:",
+  "server_banner.is_one_of_many": "{domain} הוא שרת אחד משרתי מסטודון עצמאיים רבים שדרגם תוכלו להשתתף בפדיוורס (רשת חברתית מבוזרת).",
   "server_banner.server_stats": "סטטיסטיקות שרת:",
   "sign_in_banner.create_account": "יצירת חשבון",
+  "sign_in_banner.follow_anyone": "תוכלו לעקוב אחרי כל משמתמש בפדיוורס ולקרוא הכל לפי סדר הפרסום בציר הזמן. אין אלגוריתמים, פרסומות, או קליקבייט מטעם בעלי הרשת.",
+  "sign_in_banner.mastodon_is": "מסטודון הוא הדרך הטובה ביותר לעקוב אחרי מה שקורה.",
   "sign_in_banner.sign_in": "התחברות",
   "sign_in_banner.sso_redirect": "התחברות/הרשמה",
   "status.admin_account": "פתח/י ממשק ניהול עבור @{name}",
@@ -771,7 +777,7 @@
   "timeline_hint.resources.followers": "עוקבים",
   "timeline_hint.resources.follows": "נעקבים",
   "timeline_hint.resources.statuses": "הודעות ישנות יותר",
-  "trends.counter_by_accounts": "{count, plural, one {אדם {count}} other {{count} א.נשים}} {days, plural, one {מאז אתמול} two {ביומיים האחרונים} other {במשך {days} הימים האחרונים}}",
+  "trends.counter_by_accounts": "{count, plural, one {אדם אחד} other {{count} א.נשים}} {days, plural, one {מאז אתמול} two {ביומיים האחרונים} other {במשך {days} הימים האחרונים}}",
   "trends.trending_now": "נושאים חמים",
   "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.",
   "units.short.billion": "{count} מליארד",
diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json
index a2e64c10f..ace6402ee 100644
--- a/app/javascript/mastodon/locales/ia.json
+++ b/app/javascript/mastodon/locales/ia.json
@@ -354,7 +354,7 @@
   "home.pending_critical_update.link": "Vider actualisationes",
   "home.pending_critical_update.title": "Actualisation de securitate critic disponibile!",
   "home.show_announcements": "Monstrar annuncios",
-  "interaction_modal.description.favourite": "Con un conto sur Mastodon, tu pote marcar iste message como favorite pro informar le autor que tu lo apprecia e salveguarda pro plus tarde.",
+  "interaction_modal.description.favourite": "Con un conto sur Mastodon, tu pote marcar iste message como favorite pro informar le autor que tu lo apprecia e lo salva pro plus tarde.",
   "interaction_modal.description.follow": "Con un conto sur Mastodon, tu pote sequer {name} e reciper su messages in tu fluxo de initio.",
   "interaction_modal.description.reblog": "Con un conto sur Mastodon, tu pote impulsar iste message pro condivider lo con tu proprie sequitores.",
   "interaction_modal.description.reply": "Con un conto sur Mastodon, tu pote responder a iste message.",
@@ -764,7 +764,7 @@
   "status.unmute_conversation": "Non plus silentiar conversation",
   "status.unpin": "Disfixar del profilo",
   "subscribed_languages.lead": "Solmente le messages in le linguas seligite apparera in tu chronologias de initio e de listas post le cambiamento. Selige necun pro reciper messages in tote le linguas.",
-  "subscribed_languages.save": "Salveguardar le cambiamentos",
+  "subscribed_languages.save": "Salvar le cambiamentos",
   "subscribed_languages.target": "Cambiar le linguas subscribite pro {target}",
   "tabs_bar.home": "Initio",
   "tabs_bar.notifications": "Notificationes",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index 93c3b8fe2..71b69d428 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Uzvrati praćenje",
   "account.followers": "Pratioci",
   "account.followers.empty": "Još uvek niko ne prati ovog korisnika.",
+  "account.followers_counter": "{count, plural, one {{counter} pratilac} few {{counter} pratioca} other {{counter} pratilaca}}",
   "account.following": "Prati",
+  "account.following_counter": "{count, plural, one {{counter} prati} few {{counter} prati} other {{counter} prati}}",
   "account.follows.empty": "Ovaj korisnik još uvek nikog ne prati.",
   "account.go_to_profile": "Idi na profil",
   "account.hide_reblogs": "Sakrij podržavanja @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} je zatražio da vas prati",
   "account.share": "Podeli profil korisnika @{name}",
   "account.show_reblogs": "Prikaži podržavanja od korisnika @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} objava} few {{counter} objave} other {{counter} objava}}",
   "account.unblock": "Odblokiraj korisnika @{name}",
   "account.unblock_domain": "Odblokiraj domen {domain}",
   "account.unblock_short": "Odblokiraj",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index 0273002b3..2c4649f9d 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -35,7 +35,9 @@
   "account.follow_back": "Узврати праћење",
   "account.followers": "Пратиоци",
   "account.followers.empty": "Још увек нико не прати овог корисника.",
+  "account.followers_counter": "{count, plural, one {{counter} пратилац} few {{counter} пратиоца} other {{counter} пратилаца}}",
   "account.following": "Прати",
+  "account.following_counter": "{count, plural, one {{counter} прати} few {{counter} прати} other {{counter} прати}}",
   "account.follows.empty": "Овај корисник још увек никог не прати.",
   "account.go_to_profile": "Иди на профил",
   "account.hide_reblogs": "Сакриј подржавања од @{name}",
@@ -61,6 +63,7 @@
   "account.requested_follow": "{name} је затражио да вас прати",
   "account.share": "Подели профил корисника @{name}",
   "account.show_reblogs": "Прикажи подржавања од корисника @{name}",
+  "account.statuses_counter": "{count, plural, one {{counter} објава} few {{counter} објаве} other {{counter} објава}}",
   "account.unblock": "Одблокирај корисника @{name}",
   "account.unblock_domain": "Одблокирај домен {domain}",
   "account.unblock_short": "Одблокирај",
diff --git a/config/locales/ia.yml b/config/locales/ia.yml
index 4932d42ac..7350ffcee 100644
--- a/config/locales/ia.yml
+++ b/config/locales/ia.yml
@@ -574,7 +574,7 @@ ia:
       enabled: Activate
       inbox_url: URL del repetitor
       pending: Attende le approbation del repetitor
-      save_and_enable: Salveguardar e activar
+      save_and_enable: Salvar e activar
       setup: Crear un connexion con un repetitor
       signatures_not_enabled: Le repetitores pote non functionar correctemente durante que le modo secur o le modo de federation limitate es activate
       status: Stato
@@ -1276,7 +1276,7 @@ ia:
         other: "%{count} messages individual celate"
       title: Filtros
     new:
-      save: Salveguardar nove filtro
+      save: Salvar nove filtro
       title: Adder nove filtro
     statuses:
       back_to_filter: Retro al filtro
@@ -1294,14 +1294,14 @@ ia:
       one: "<strong>%{count}</strong> elemento correspondente al recerca es seligite."
       other: Tote le <strong>%{count}</strong> elementos correspondente al recerca es seligite.
     cancel: Cancellar
-    changes_saved_msg: Cambios salveguardate con successo!
+    changes_saved_msg: Le cambiamentos ha essite salvate!
     confirm: Confirmar
     copy: Copiar
     delete: Deler
     deselect: Deseliger toto
     none: Necun
     order_by: Ordinar per
-    save_changes: Salvar le cambios
+    save_changes: Salvar le cambiamentos
     select_all_matching_items:
       one: Selige %{count} elemento correspondente a tu recerca.
       other: Selige %{count} elementos correspondente a tu recerca.
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index c0698c3f7..664082dab 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -81,7 +81,7 @@ ja:
         backups_retention_period: ユーザーには、後でダウンロードするために投稿のアーカイブを生成する機能があります。正の値に設定すると、これらのアーカイブは指定された日数後に自動的にストレージから削除されます。
         bootstrap_timeline_accounts: これらのアカウントは、新しいユーザー向けのおすすめユーザーの一番上にピン留めされます。
         closed_registrations_message: アカウント作成を停止している時に表示されます
-        content_cache_retention_period: 他のサーバーからのすべての投稿(ブーストや返信を含む)は、指定された日数が経過すると、ローカルユーザーとのやりとりに関係なく削除されます。これには、ローカルユーザーがブックマークやお気に入りとして登録した投稿も含まれます。異なるサーバーのユーザー間の非公開な変身も失われ、復元することは不可能です。この設定の使用は特別な目的のインスタンスのためのものであり、一般的な目的のサーバーで使用するした場合、多くのユーザーの期待を裏切ることになります。
+        content_cache_retention_period: 他のサーバーからのすべての投稿(ブーストや返信を含む)は、指定された日数が経過すると、ローカルユーザーとのやりとりに関係なく削除されます。これには、ローカルユーザーがブックマークやお気に入りとして登録した投稿も含まれます。異なるサーバーのユーザー間の非公開な返信も失われ、復元することは不可能です。この設定の使用は特別な目的のインスタンスのためのものであり、一般的な目的のサーバーで使用した場合、多くのユーザーの期待を裏切ることになります。
         custom_css: ウェブ版のMastodonでカスタムスタイルを適用できます。
         favicon: デフォルトのMastodonのブックマークアイコンを独自のアイコンで上書きします。WEBP、PNG、GIF、JPGが利用可能です。
         mascot: 上級者向けWebインターフェースのイラストを上書きします。

From 528661a091bbef5b5359bfacd1478231a1cb7d10 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 4 Jul 2024 15:46:08 +0200
Subject: [PATCH 70/84] fix(deps): update dependency pino-http to v10.2.0
 (#30913)

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 8e72138a1..52a8fff72 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13139,14 +13139,14 @@ __metadata:
   linkType: hard
 
 "pino-http@npm:^10.0.0":
-  version: 10.1.0
-  resolution: "pino-http@npm:10.1.0"
+  version: 10.2.0
+  resolution: "pino-http@npm:10.2.0"
   dependencies:
     get-caller-file: "npm:^2.0.5"
     pino: "npm:^9.0.0"
     pino-std-serializers: "npm:^7.0.0"
     process-warning: "npm:^3.0.0"
-  checksum: 10c0/d97691f2ee248b0aca0e49169d0c7ca0d4c604ee57b63ae264a6f9914fc7277cace74686d5088a876f8152a8d5b8211af904b2d24a516728a662de0e9cc79e9f
+  checksum: 10c0/0b79cd3602531ee5043693e2a3ccf9d955bd93759e80c0b3a458b95b241f36ca8ebc72c8050b395e9d8fcb9581ebc18ecd6b7dc136526bebe924bc5c5079374d
   languageName: node
   linkType: hard
 

From b73014761807f3171b7ecb46052e9aa618b9c029 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 4 Jul 2024 15:46:14 +0200
Subject: [PATCH 71/84] fix(deps): update dependency ws to v8.18.0 (#30914)

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 52a8fff72..d61ef4163 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -18491,8 +18491,8 @@ __metadata:
   linkType: hard
 
 "ws@npm:^8.11.0, ws@npm:^8.12.1, ws@npm:^8.17.0":
-  version: 8.17.1
-  resolution: "ws@npm:8.17.1"
+  version: 8.18.0
+  resolution: "ws@npm:8.18.0"
   peerDependencies:
     bufferutil: ^4.0.1
     utf-8-validate: ">=5.0.2"
@@ -18501,7 +18501,7 @@ __metadata:
       optional: true
     utf-8-validate:
       optional: true
-  checksum: 10c0/f4a49064afae4500be772abdc2211c8518f39e1c959640457dcee15d4488628620625c783902a52af2dd02f68558da2868fd06e6fd0e67ebcd09e6881b1b5bfe
+  checksum: 10c0/25eb33aff17edcb90721ed6b0eb250976328533ad3cd1a28a274bd263682e7296a6591ff1436d6cbc50fa67463158b062f9d1122013b361cec99a05f84680e06
   languageName: node
   linkType: hard
 

From 395f17ca17b27f9e722425a4d76ef1b02bcebea8 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 4 Jul 2024 16:11:28 +0200
Subject: [PATCH 72/84] Merge pull request from GHSA-vp5r-5pgw-jwqx

* Fix streaming sessions not being closed when revoking access to an app

* Add tests for GHSA-7w3c-p9j8-mq3x
---
 .../oauth/authorized_applications_controller.rb        |  1 +
 app/lib/application_extension.rb                       |  8 +++++---
 .../oauth/authorized_applications_controller_spec.rb   |  6 ++++++
 .../settings/applications_controller_spec.rb           | 10 +++++++++-
 4 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index 8440df6b7..7bb22453c 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -17,6 +17,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
 
   def destroy
     Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
+    Doorkeeper::Application.find_by(id: params[:id])&.close_streaming_sessions(current_resource_owner)
     super
   end
 
diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb
index 2fea1057c..d7aaeba5b 100644
--- a/app/lib/application_extension.rb
+++ b/app/lib/application_extension.rb
@@ -16,7 +16,7 @@ module ApplicationExtension
     # dependent: delete_all, which means the ActiveRecord callback in
     # AccessTokenExtension is not run, so instead we manually announce to
     # streaming that these tokens are being deleted.
-    before_destroy :push_to_streaming_api, prepend: true
+    before_destroy :close_streaming_sessions, prepend: true
   end
 
   def confirmation_redirect_uri
@@ -29,10 +29,12 @@ module ApplicationExtension
     redirect_uri.split
   end
 
-  def push_to_streaming_api
+  def close_streaming_sessions(resource_owner = nil)
     # TODO: #28793 Combine into a single topic
     payload = Oj.dump(event: :kill)
-    access_tokens.in_batches do |tokens|
+    scope = access_tokens
+    scope = scope.where(resource_owner_id: resource_owner.id) unless resource_owner.nil?
+    scope.in_batches do |tokens|
       redis.pipelined do |pipeline|
         tokens.ids.each do |id|
           pipeline.publish("timeline:access_token:#{id}", payload)
diff --git a/spec/controllers/oauth/authorized_applications_controller_spec.rb b/spec/controllers/oauth/authorized_applications_controller_spec.rb
index b46b944d0..3fd9f9499 100644
--- a/spec/controllers/oauth/authorized_applications_controller_spec.rb
+++ b/spec/controllers/oauth/authorized_applications_controller_spec.rb
@@ -50,9 +50,11 @@ describe Oauth::AuthorizedApplicationsController do
     let!(:application) { Fabricate(:application) }
     let!(:access_token) { Fabricate(:accessible_access_token, application: application, resource_owner_id: user.id) }
     let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
+    let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) }
 
     before do
       sign_in user, scope: :user
+      allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub)
       post :destroy, params: { id: application.id }
     end
 
@@ -67,5 +69,9 @@ describe Oauth::AuthorizedApplicationsController do
     it 'removes the web_push_subscription' do
       expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
     end
+
+    it 'sends a session kill payload to the streaming server' do
+      expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}')
+    end
   end
 end
diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb
index ccbb63491..ce2e0749a 100644
--- a/spec/controllers/settings/applications_controller_spec.rb
+++ b/spec/controllers/settings/applications_controller_spec.rb
@@ -147,14 +147,22 @@ describe Settings::ApplicationsController do
   end
 
   describe 'destroy' do
+    let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) }
+    let!(:access_token) { Fabricate(:accessible_access_token, application: app) }
+
     before do
+      allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub)
       post :destroy, params: { id: app.id }
     end
 
-    it 'redirects back to applications page and removes the app' do
+    it 'redirects back to applications page removes the app' do
       expect(response).to redirect_to(settings_applications_path)
       expect(Doorkeeper::Application.find_by(id: app.id)).to be_nil
     end
+
+    it 'sends a session kill payload to the streaming server' do
+      expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}')
+    end
   end
 
   describe 'regenerate' do

From 502cf75b160c76f963a179d46498e3ea1a9fefca Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 4 Jul 2024 16:26:49 +0200
Subject: [PATCH 73/84] Merge pull request from GHSA-58x8-3qxw-6hm7

* Fix insufficient permission checking for public timeline endpoints

Note that this changes unauthenticated access failure code from 401 to 422

* Add more tests for public timelines

* Require user token in `/api/v1/statuses/:id/translate` and `/api/v1/scheduled_statuses`
---
 .../api/v1/scheduled_statuses_controller.rb   |  1 +
 .../v1/statuses/translations_controller.rb    |  1 +
 .../api/v1/timelines/base_controller.rb       |  6 +++++
 .../api/v1/timelines/link_controller.rb       |  6 +----
 .../api/v1/timelines/public_controller.rb     |  6 +----
 .../api/v1/timelines/tag_controller.rb        |  2 +-
 spec/requests/api/v1/scheduled_status_spec.rb | 11 ++++++++
 .../api/v1/statuses/translations_spec.rb      | 16 ++++++++++++
 spec/requests/api/v1/timelines/link_spec.rb   | 20 +++++++++++---
 spec/requests/api/v1/timelines/public_spec.rb | 26 ++++++++++++++-----
 spec/requests/api/v1/timelines/tag_spec.rb    | 10 ++++---
 11 files changed, 82 insertions(+), 23 deletions(-)

diff --git a/app/controllers/api/v1/scheduled_statuses_controller.rb b/app/controllers/api/v1/scheduled_statuses_controller.rb
index 45ee58651..c62305d71 100644
--- a/app/controllers/api/v1/scheduled_statuses_controller.rb
+++ b/app/controllers/api/v1/scheduled_statuses_controller.rb
@@ -6,6 +6,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
   before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
 
+  before_action :require_user!
   before_action :set_statuses, only: :index
   before_action :set_status, except: :index
 
diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb
index 7d406b0a3..8cf495f78 100644
--- a/app/controllers/api/v1/statuses/translations_controller.rb
+++ b/app/controllers/api/v1/statuses/translations_controller.rb
@@ -2,6 +2,7 @@
 
 class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
+  before_action :require_user!
   before_action :set_translation
 
   rescue_from TranslationService::NotConfiguredError, with: :not_found
diff --git a/app/controllers/api/v1/timelines/base_controller.rb b/app/controllers/api/v1/timelines/base_controller.rb
index e79eba79e..1dba4a5bb 100644
--- a/app/controllers/api/v1/timelines/base_controller.rb
+++ b/app/controllers/api/v1/timelines/base_controller.rb
@@ -3,8 +3,14 @@
 class Api::V1::Timelines::BaseController < Api::BaseController
   after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
 
+  before_action :require_user!, if: :require_auth?
+
   private
 
+  def require_auth?
+    !Setting.timeline_preview
+  end
+
   def pagination_collection
     @statuses
   end
diff --git a/app/controllers/api/v1/timelines/link_controller.rb b/app/controllers/api/v1/timelines/link_controller.rb
index af962c430..37ed084f0 100644
--- a/app/controllers/api/v1/timelines/link_controller.rb
+++ b/app/controllers/api/v1/timelines/link_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController
-  before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
+  before_action -> { authorize_if_got_token! :read, :'read:statuses' }
   before_action :set_preview_card
   before_action :set_statuses
 
@@ -17,10 +17,6 @@ class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController
 
   private
 
-  def require_auth?
-    !Setting.timeline_preview
-  end
-
   def set_preview_card
     @preview_card = PreviewCard.joins(:trend).merge(PreviewCardTrend.allowed).find_by!(url: params[:url])
   end
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index d164854d6..029e8fc2c 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
-  before_action :require_user!, only: [:show], if: :require_auth?
+  before_action -> { authorize_if_got_token! :read, :'read:statuses' }
 
   PERMITTED_PARAMS = %i(local remote limit only_media).freeze
 
@@ -13,10 +13,6 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
 
   private
 
-  def require_auth?
-    !Setting.timeline_preview
-  end
-
   def load_statuses
     preloaded_public_statuses_page
   end
diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index 3bf8f374e..2b097aab0 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
-  before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
+  before_action -> { authorize_if_got_token! :read, :'read:statuses' }
   before_action :load_tag
 
   PERMITTED_PARAMS = %i(local limit only_media).freeze
diff --git a/spec/requests/api/v1/scheduled_status_spec.rb b/spec/requests/api/v1/scheduled_status_spec.rb
index 49ccde275..f4612410b 100644
--- a/spec/requests/api/v1/scheduled_status_spec.rb
+++ b/spec/requests/api/v1/scheduled_status_spec.rb
@@ -25,6 +25,17 @@ describe 'Scheduled Statuses' do
       it_behaves_like 'forbidden for wrong scope', 'write write:statuses'
     end
 
+    context 'with an application token' do
+      let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read:statuses') }
+
+      it 'returns http unprocessable entity' do
+        get api_v1_scheduled_statuses_path, headers: headers
+
+        expect(response)
+          .to have_http_status(422)
+      end
+    end
+
     context 'with correct scope' do
       let(:scopes) { 'read:statuses' }
 
diff --git a/spec/requests/api/v1/statuses/translations_spec.rb b/spec/requests/api/v1/statuses/translations_spec.rb
index 5b0a99456..e2ab5d0b8 100644
--- a/spec/requests/api/v1/statuses/translations_spec.rb
+++ b/spec/requests/api/v1/statuses/translations_spec.rb
@@ -8,6 +8,22 @@ describe 'API V1 Statuses Translations' do
   let(:scopes)  { 'read:statuses' }
   let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
 
+  context 'with an application token' do
+    let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) }
+
+    describe 'POST /api/v1/statuses/:status_id/translate' do
+      let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') }
+
+      before do
+        post "/api/v1/statuses/#{status.id}/translate", headers: headers
+      end
+
+      it 'returns http unprocessable entity' do
+        expect(response).to have_http_status(422)
+      end
+    end
+  end
+
   context 'with an oauth token' do
     describe 'POST /api/v1/statuses/:status_id/translate' do
       let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') }
diff --git a/spec/requests/api/v1/timelines/link_spec.rb b/spec/requests/api/v1/timelines/link_spec.rb
index a219c9bcd..e1c914ab8 100644
--- a/spec/requests/api/v1/timelines/link_spec.rb
+++ b/spec/requests/api/v1/timelines/link_spec.rb
@@ -41,6 +41,8 @@ describe 'Link' do
       end
     end
 
+    it_behaves_like 'forbidden for wrong scope', 'profile'
+
     context 'when there is no preview card' do
       let(:preview_card) { nil }
 
@@ -80,13 +82,25 @@ describe 'Link' do
         Form::AdminSettings.new(timeline_preview: false).save
       end
 
-      context 'when the user is not authenticated' do
+      it_behaves_like 'forbidden for wrong scope', 'profile'
+
+      context 'without an authentication token' do
         let(:headers) { {} }
 
-        it 'returns http unauthorized' do
+        it 'returns http unprocessable entity' do
           subject
 
-          expect(response).to have_http_status(401)
+          expect(response).to have_http_status(422)
+        end
+      end
+
+      context 'with an application access token, not bound to a user' do
+        let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) }
+
+        it 'returns http unprocessable entity' do
+          subject
+
+          expect(response).to have_http_status(422)
         end
       end
 
diff --git a/spec/requests/api/v1/timelines/public_spec.rb b/spec/requests/api/v1/timelines/public_spec.rb
index 364e48d3d..100f6c1bf 100644
--- a/spec/requests/api/v1/timelines/public_spec.rb
+++ b/spec/requests/api/v1/timelines/public_spec.rb
@@ -34,6 +34,8 @@ describe 'Public' do
     context 'when the instance allows public preview' do
       let(:expected_statuses) { [local_status, remote_status, media_status] }
 
+      it_behaves_like 'forbidden for wrong scope', 'profile'
+
       context 'with an authorized user' do
         it_behaves_like 'a successful request to the public timeline'
       end
@@ -99,13 +101,9 @@ describe 'Public' do
         Form::AdminSettings.new(timeline_preview: false).save
       end
 
-      context 'with an authenticated user' do
-        let(:expected_statuses) { [local_status, remote_status, media_status] }
+      it_behaves_like 'forbidden for wrong scope', 'profile'
 
-        it_behaves_like 'a successful request to the public timeline'
-      end
-
-      context 'with an unauthenticated user' do
+      context 'without an authentication token' do
         let(:headers) { {} }
 
         it 'returns http unprocessable entity' do
@@ -114,6 +112,22 @@ describe 'Public' do
           expect(response).to have_http_status(422)
         end
       end
+
+      context 'with an application access token, not bound to a user' do
+        let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) }
+
+        it 'returns http unprocessable entity' do
+          subject
+
+          expect(response).to have_http_status(422)
+        end
+      end
+
+      context 'with an authenticated user' do
+        let(:expected_statuses) { [local_status, remote_status, media_status] }
+
+        it_behaves_like 'a successful request to the public timeline'
+      end
     end
   end
 end
diff --git a/spec/requests/api/v1/timelines/tag_spec.rb b/spec/requests/api/v1/timelines/tag_spec.rb
index 861134170..5e1415bb1 100644
--- a/spec/requests/api/v1/timelines/tag_spec.rb
+++ b/spec/requests/api/v1/timelines/tag_spec.rb
@@ -30,6 +30,8 @@ RSpec.describe 'Tag' do
     let(:params)          { {} }
     let(:hashtag)         { 'life' }
 
+    it_behaves_like 'forbidden for wrong scope', 'profile'
+
     context 'when given only one hashtag' do
       let(:expected_statuses) { [life_status] }
 
@@ -93,13 +95,15 @@ RSpec.describe 'Tag' do
         Form::AdminSettings.new(timeline_preview: false).save
       end
 
-      context 'when the user is not authenticated' do
+      it_behaves_like 'forbidden for wrong scope', 'profile'
+
+      context 'without an authentication token' do
         let(:headers) { {} }
 
-        it 'returns http unauthorized' do
+        it 'returns http unprocessable entity' do
           subject
 
-          expect(response).to have_http_status(401)
+          expect(response).to have_http_status(422)
         end
       end
 

From d3a056adfd0eca4fff57dde65ee9d95ce7c9bb3e Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 4 Jul 2024 16:45:52 +0200
Subject: [PATCH 74/84] Merge pull request from GHSA-xjvf-fm67-4qc3

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

diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 7ec7e84bd..5d700b496 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -104,7 +104,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   def find_existing_status
     status   = status_from_uri(object_uri)
     status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
-    status
+    status if status&.account_id == @account.id
   end
 
   def process_status_params

From df9e26158d9787859b24bdc276af478abf05e1af Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 4 Jul 2024 16:59:54 +0200
Subject: [PATCH 75/84] Bump version to v4.3.0-alpha.5 (#30920)

---
 CHANGELOG.md            | 31 +++++++++++++++++++++++++++++++
 docker-compose.yml      |  6 +++---
 lib/mastodon/version.rb |  2 +-
 3 files changed, 35 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9b24d6f1..7c3d96ba4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,37 @@
 
 All notable changes to this project will be documented in this file.
 
+## [4.2.10] - 2024-07-04
+
+### Security
+
+- Fix incorrect permission checking on multiple API endpoints ([GHSA-58x8-3qxw-6hm7](https://github.com/mastodon/mastodon/security/advisories/GHSA-58x8-3qxw-6hm7))
+- Fix incorrect authorship checking when processing some activities (CVE-2024-37903, [GHSA-xjvf-fm67-4qc3](https://github.com/mastodon/mastodon/security/advisories/GHSA-xjvf-fm67-4qc3))
+- Fix ongoing streaming sessions not being invalidated when application tokens get revoked ([GHSA-vp5r-5pgw-jwqx](https://github.com/mastodon/mastodon/security/advisories/GHSA-vp5r-5pgw-jwqx))
+- Update dependencies
+
+### Added
+
+- Add yarn version specification to avoid confusion with Yarn 3 and Yarn 4
+
+### Changed
+
+- Change preview cards generation to skip unusually long URLs ([oneiros](https://github.com/mastodon/mastodon/pull/30854))
+- Change search modifiers to be case-insensitive ([Gargron](https://github.com/mastodon/mastodon/pull/30865))
+- Change `STATSD_ADDR` handling to emit a warning rather than crashing if the address is unreachable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30691))
+- Change PWA start URL from `/home` to `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27377))
+
+### Removed
+
+- Removed dependency on `posix-spawn` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18559))
+
+### Fixed
+
+- Fix scheduled statuses scheduled in less than 5 minutes being immediately published ([danielmbrasil](https://github.com/mastodon/mastodon/pull/30584))
+- Fix encoding detection for link cards ([oneiros](https://github.com/mastodon/mastodon/pull/30780))
+- Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30819))
+- Fix duplicate `@context` attribute in user archive export ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30653))
+
 ## [4.2.9] - 2024-05-30
 
 ### Security
diff --git a/docker-compose.yml b/docker-compose.yml
index 7089b0d14..7a6f9be50 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -58,7 +58,7 @@ services:
 
   web:
     build: .
-    image: ghcr.io/mastodon/mastodon:v4.2.9
+    image: ghcr.io/mastodon/mastodon:v4.2.10
     restart: always
     env_file: .env.production
     command: bundle exec puma -C config/puma.rb
@@ -79,7 +79,7 @@ services:
 
   streaming:
     build: .
-    image: ghcr.io/mastodon/mastodon:v4.2.9
+    image: ghcr.io/mastodon/mastodon:v4.2.10
     restart: always
     env_file: .env.production
     command: node ./streaming
@@ -97,7 +97,7 @@ services:
 
   sidekiq:
     build: .
-    image: ghcr.io/mastodon/mastodon:v4.2.9
+    image: ghcr.io/mastodon/mastodon:v4.2.10
     restart: always
     env_file: .env.production
     command: bundle exec sidekiq
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 03972ba93..96ad40928 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -17,7 +17,7 @@ module Mastodon
     end
 
     def default_prerelease
-      'alpha.4'
+      'alpha.5'
     end
 
     def prerelease

From 8de5df225edb89ef13f5ea2697876018953a97d9 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 5 Jul 2024 10:54:45 +0200
Subject: [PATCH 76/84] Change instructions to use `bundle exec rails` instead
 of `rake` (#30917)

---
 .env.production.sample       | 6 +++---
 config/initializers/vapid.rb | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/.env.production.sample b/.env.production.sample
index 0bf01bdc3..0b458a1aa 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -1,5 +1,5 @@
 # This is a sample configuration file. You can generate your configuration
-# with the `rake mastodon:setup` interactive setup wizard, but to customize
+# with the `bundle exec rails mastodon:setup` interactive setup wizard, but to customize
 # your setup even further, you'll need to edit it manually. This sample does
 # not demonstrate all available configuration options. Please look at
 # https://docs.joinmastodon.org/admin/config/ for the full documentation.
@@ -40,14 +40,14 @@ ES_PASS=password
 
 # Secrets
 # -------
-# Make sure to use `rake secret` to generate secrets
+# Make sure to use `bundle exec rails secret` to generate secrets
 # -------
 SECRET_KEY_BASE=
 OTP_SECRET=
 
 # Web Push
 # --------
-# Generate with `rake mastodon:webpush:generate_vapid_key`
+# Generate with `bundle exec rails mastodon:webpush:generate_vapid_key`
 # --------
 VAPID_PRIVATE_KEY=
 VAPID_PUBLIC_KEY=
diff --git a/config/initializers/vapid.rb b/config/initializers/vapid.rb
index 7dd870c8b..551ede34f 100644
--- a/config/initializers/vapid.rb
+++ b/config/initializers/vapid.rb
@@ -5,7 +5,7 @@ Rails.application.configure do
   # You should only generate this once per instance. If you later decide to change it, all push subscription will
   # be invalidated, requiring the users to access the website again to resubscribe.
   #
-  # Generate with `rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
+  # Generate with `bundle exec rails mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web bundle exec rails mastodon:webpush:generate_vapid_key` if you use docker compose)
   #
   # For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
 

From 8c375d8a5c899b735950a7a88670a1bb2b7adcd2 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 5 Jul 2024 04:57:54 -0400
Subject: [PATCH 77/84] Use `scope module: ...` for settings/2FA routes
 (#30919)

---
 config/routes/settings.rb | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/config/routes/settings.rb b/config/routes/settings.rb
index 888fa9ecb..b14606656 100644
--- a/config/routes/settings.rb
+++ b/config/routes/settings.rb
@@ -37,13 +37,13 @@ namespace :settings do
     end
   end
 
-  resource :otp_authentication, only: [:show, :create], controller: 'two_factor_authentication/otp_authentication'
+  scope module: :two_factor_authentication do
+    resource :otp_authentication, only: [:show, :create], controller: :otp_authentication
 
-  resources :webauthn_credentials, only: [:index, :new, :create, :destroy],
-                                   path: 'security_keys',
-                                   controller: 'two_factor_authentication/webauthn_credentials' do
-    collection do
-      get :options
+    resources :webauthn_credentials, only: [:index, :new, :create, :destroy], path: 'security_keys' do
+      collection do
+        get :options
+      end
     end
   end
 

From 81547845ac73f860df2c86594fe0bf346745ce0e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 5 Jul 2024 11:09:40 +0200
Subject: [PATCH 78/84] New Crowdin Translations (automated) (#30925)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/ja.json |  2 ++
 app/javascript/mastodon/locales/uk.json |  2 +-
 config/locales/gl.yml                   | 42 ++++++++++++-------------
 config/locales/simple_form.gl.yml       |  2 +-
 4 files changed, 25 insertions(+), 23 deletions(-)

diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 575c68de0..60788baff 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -411,6 +411,7 @@
   "limited_account_hint.action": "構わず表示する",
   "limited_account_hint.title": "このプロフィールは{domain}のモデレーターによって非表示にされています。",
   "link_preview.author": "{name}",
+  "link_preview.more_from_author": "{name}さんの投稿をもっと読む",
   "lists.account.add": "リストに追加",
   "lists.account.remove": "リストから外す",
   "lists.delete": "リストを削除",
@@ -693,6 +694,7 @@
   "server_banner.administered_by": "管理者",
   "server_banner.server_stats": "サーバーの情報",
   "sign_in_banner.create_account": "アカウント作成",
+  "sign_in_banner.follow_anyone": "連合内の誰でもフォローして投稿を時系列で見ることができます。アルゴリズム、広告、クリックベイトはありません。",
   "sign_in_banner.sign_in": "ログイン",
   "sign_in_banner.sso_redirect": "ログインまたは登録",
   "status.admin_account": "@{name}さんのモデレーション画面を開く",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 150b808f8..5d7d040fd 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -32,7 +32,7 @@
   "account.featured_tags.last_status_never": "Немає дописів",
   "account.featured_tags.title": "{name} виділяє хештеґи",
   "account.follow": "Підписатися",
-  "account.follow_back": "Підписатися взаємно",
+  "account.follow_back": "Стежити також",
   "account.followers": "Підписники",
   "account.followers.empty": "Ніхто ще не підписаний на цього користувача.",
   "account.followers_counter": "{count, plural, one {{counter} підписник} few {{counter} підписники} many {{counter} підписників} other {{counter} підписники}}",
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index ad4744e15..c9f08dcad 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -31,18 +31,18 @@ gl:
       created_msg: Nota de moderación creada correctamente!
       destroyed_msg: Nota de moderación eliminada de xeito correcto!
     accounts:
-      add_email_domain_block: Bloquear o dominio do email
+      add_email_domain_block: Bloquear o dominio do enderezo
       approve: Aprobar
       approved_msg: Aprobada a solicitude de rexistro de %{username}
       are_you_sure: Está segura?
       avatar: Imaxe de perfil
       by_domain: Dominio
       change_email:
-        changed_msg: Email mudado de xeito correcto!
-        current_email: Email actual
-        label: Mudar email
-        new_email: Novo email
-        submit: Mudar email
+        changed_msg: Correo cambiado de xeito correcto!
+        current_email: Enderezo actual
+        label: Cambiar de enderezo
+        new_email: Novo enderezo
+        submit: Cambiar de enderezo
         title: Mudar email de %{username}
       change_role:
         changed_msg: Rol mudado correctamente!
@@ -64,10 +64,10 @@ gl:
       display_name: Nome a amosar
       domain: Dominio
       edit: Editar
-      email: Email
-      email_status: Estado do email
+      email: Enderezo de correo
+      email_status: Estado do correo
       enable: Activar
-      enable_sign_in_token_auth: Activar autenticación cun token no email
+      enable_sign_in_token_auth: Activar autenticación cun token no correo
       enabled: Activado
       enabled_msg: Desbloqueada a conta de %{username}
       followers: Seguidoras
@@ -132,7 +132,7 @@ gl:
       resubscribe: Resubscribir
       role: Rol
       search: Procurar
-      search_same_email_domain: Outras usuarias co mesmo dominio de email
+      search_same_email_domain: Outras usuarias co mesmo dominio de correo
       search_same_ip: Outras usuarias co mesmo IP
       security: Seguridade
       security_measures:
@@ -154,9 +154,9 @@ gl:
       suspension_irreversible: Elimináronse de xeito irreversible os datos desta conta. Podes reactivar a conta para facela usable novamente pero non recuperará os datos eliminados.
       suspension_reversible_hint_html: Esta conta foi suspendida, e os datos serán totalmente eliminados o %{date}. Ata entón, a conta pode ser restaurada sen danos. Se desexas eliminar agora mesmo todos os datos da conta, podes facelo aquí embaixo.
       title: Contas
-      unblock_email: Desbloquear enderezo de email
-      unblocked_email_msg: Enderezo de email de %{username} desbloqueado
-      unconfirmed_email: Email non confirmado
+      unblock_email: Desbloquear enderezo de correo
+      unblocked_email_msg: Enderezo de correo de %{username} desbloqueado
+      unconfirmed_email: Enderezo de correo sen confirmar
       undo_sensitized: Desmarcar como sensible
       undo_silenced: Desfacer acalar
       undo_suspension: Desfacer suspensión
@@ -173,12 +173,12 @@ gl:
         approve_appeal: Aprobar apelación
         approve_user: Aprobar Usuaria
         assigned_to_self_report: Asignar denuncia
-        change_email_user: Editar email da usuaria
+        change_email_user: Editar correo electrónico da usuaria
         change_role_user: Cambiar Rol da Usuaria
         confirm_user: Confirmar usuaria
         create_account_warning: Crear aviso
         create_announcement: Crear anuncio
-        create_canonical_email_block: Crear Bloqueo de email
+        create_canonical_email_block: Crear Bloqueo de correo electrónico
         create_custom_emoji: Crear emoticonas personalizadas
         create_domain_allow: Crear Dominio Permitido
         create_domain_block: Crear bloquedo do Dominio
@@ -188,7 +188,7 @@ gl:
         create_user_role: Crear Rol
         demote_user: Degradar usuaria
         destroy_announcement: Eliminar anuncio
-        destroy_canonical_email_block: Eliminar Bloqueo de email
+        destroy_canonical_email_block: Eliminar Bloqueo de correo electrónico
         destroy_custom_emoji: Eliminar emoticona personalizada
         destroy_domain_allow: Eliminar Dominio permitido
         destroy_domain_block: Eliminar bloqueo do Dominio
@@ -200,7 +200,7 @@ gl:
         destroy_user_role: Eliminar Rol
         disable_2fa_user: Desactivar 2FA
         disable_custom_emoji: Desactivar emoticona personalizada
-        disable_sign_in_token_auth_user: Desactivar Autenticación por token no email para Usuaria
+        disable_sign_in_token_auth_user: Desactivar Autenticación con token no correo para Usuaria
         disable_user: Desactivar usuaria
         enable_custom_emoji: Activar emoticona personalizada
         enable_sign_in_token_auth_user: Activar Autenticación con token no email para Usuaria
@@ -211,14 +211,14 @@ gl:
         reject_user: Rexeitar Usuaria
         remove_avatar_user: Eliminar avatar
         reopen_report: Reabrir denuncia
-        resend_user: Reenviar o email de confirmación
+        resend_user: Reenviar o correo de confirmación
         reset_password_user: Restabelecer contrasinal
         resolve_report: Resolver denuncia
         sensitive_account: Marca o multimedia da túa conta como sensible
         silence_account: Silenciar conta
         suspend_account: Suspender conta
         unassigned_report: Desasignar denuncia
-        unblock_email_account: Desbloquear enderezo de email
+        unblock_email_account: Desbloquear enderezo de correo
         unsensitive_account: Retira a marca de sensible do multimedia da conta
         unsilence_account: Deixar de silenciar conta
         unsuspend_account: Retirar suspensión de conta
@@ -660,7 +660,7 @@ gl:
         delete_data_html: Eliminar o perfil e contidos de <strong>@%{acct}</strong> para os próximos 30 días a non ser que sexa suspendida nese período
         preview_preamble_html: "<strong>@%{acct}</strong> vai recibir un aviso co seguinte contido:"
         record_strike_html: Anotar un aviso contra <strong>@%{acct}</strong> para axudarche a xestionar futuros problemas con esta conta
-        send_email_html: Enviar un email de aviso a <strong>@%{acct}</strong>
+        send_email_html: Enviar un correo de aviso a <strong>@%{acct}</strong>
         warning_placeholder: Razóns adicionais optativas para a acción de moderación.
       target_origin: Orixe da conta denunciada
       title: Denuncias
@@ -1060,7 +1060,7 @@ gl:
       redirect_to_app_html: Ímoste redirixir á app <strong>%{app_name}</strong>. Se iso non acontece, proba %{clicking_this_link} ou volve ti manualmente á app.
       registration_complete: Completouse a creación da conta en %{domain}!
       welcome_title: Benvida, %{name}!
-      wrong_email_hint: Se o enderezo de email non é correcto, podes cambialo nos axustes da conta.
+      wrong_email_hint: Se o enderezo de correo non é correcto, podes cambialo nos axustes da conta.
     delete_account: Eliminar conta
     delete_account_html: Se queres eliminar a túa conta, podes <a href="%{path}">facelo aquí</a>. Deberás confirmar a acción.
     description:
diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml
index 0411c45bc..e46ccb873 100644
--- a/config/locales/simple_form.gl.yml
+++ b/config/locales/simple_form.gl.yml
@@ -255,7 +255,7 @@ gl:
         require_invite_text: Pedir unha razón para unirse
         show_domain_blocks: Amosar dominios bloqueados
         show_domain_blocks_rationale: Explicar porque están bloqueados os dominios
-        site_contact_email: Email de contacto
+        site_contact_email: Correo de contacto
         site_contact_username: Nome do contacto
         site_extended_description: Descrición ampla
         site_short_description: Descrición do servidor

From a16c2c99b576b6815c9fefede9acc0be0d60d54d Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 5 Jul 2024 11:51:55 +0200
Subject: [PATCH 79/84] fix(deps): update dependency cssnano to v7.0.4 (#30927)

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

diff --git a/yarn.lock b/yarn.lock
index d61ef4163..4c1a79220 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6742,16 +6742,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"cssnano-preset-default@npm:^7.0.3":
-  version: 7.0.3
-  resolution: "cssnano-preset-default@npm:7.0.3"
+"cssnano-preset-default@npm:^7.0.4":
+  version: 7.0.4
+  resolution: "cssnano-preset-default@npm:7.0.4"
   dependencies:
     browserslist: "npm:^4.23.1"
     css-declaration-sorter: "npm:^7.2.0"
     cssnano-utils: "npm:^5.0.0"
     postcss-calc: "npm:^10.0.0"
     postcss-colormin: "npm:^7.0.1"
-    postcss-convert-values: "npm:^7.0.1"
+    postcss-convert-values: "npm:^7.0.2"
     postcss-discard-comments: "npm:^7.0.1"
     postcss-discard-duplicates: "npm:^7.0.0"
     postcss-discard-empty: "npm:^7.0.0"
@@ -6778,7 +6778,7 @@ __metadata:
     postcss-unique-selectors: "npm:^7.0.1"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/ab3e51003efed6542a12d43c10ca693ab26138a1d035697b9be8f07e084e37a78617cbb8028b0a7e7841302ec151f4ecf35cbd763efe291846b62c35ea4c0bb4
+  checksum: 10c0/0083821e778bdf7b8aa9589408a01a717be730f73584e7b81756a6fcf87af05b8f17342025e666572a8d573cc30783f2d817b0f7ad63670398bc3135b017ccad
   languageName: node
   linkType: hard
 
@@ -6792,14 +6792,14 @@ __metadata:
   linkType: hard
 
 "cssnano@npm:^7.0.0":
-  version: 7.0.3
-  resolution: "cssnano@npm:7.0.3"
+  version: 7.0.4
+  resolution: "cssnano@npm:7.0.4"
   dependencies:
-    cssnano-preset-default: "npm:^7.0.3"
+    cssnano-preset-default: "npm:^7.0.4"
     lilconfig: "npm:^3.1.2"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/4cbcd1e0ebe0bd83196cc5b16b3a60d3ebc98326c79b2f71df597bb73c8e3ee1f42b89159d7a038acc398251184d648d9dd516f4194e46746f3af6fa74b4aec7
+  checksum: 10c0/3939a0b37b11cb4bae92f7916517c7ba21257551f92517b49a640d5df32e855fb7e73321f4be44d2c2de578309c05d711cdcb1976e95607b1b7f92bd4cbd1350
   languageName: node
   linkType: hard
 
@@ -13339,15 +13339,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-convert-values@npm:^7.0.1":
-  version: 7.0.1
-  resolution: "postcss-convert-values@npm:7.0.1"
+"postcss-convert-values@npm:^7.0.2":
+  version: 7.0.2
+  resolution: "postcss-convert-values@npm:7.0.2"
   dependencies:
     browserslist: "npm:^4.23.1"
     postcss-value-parser: "npm:^4.2.0"
   peerDependencies:
     postcss: ^8.4.31
-  checksum: 10c0/612f025f179f0f2ad7365db8c0b423614dcb8e1e4061875a4691a39dede0bca758d1a8f9f5c8b08e12af053e9e884f65ca5626ccc723d5b3f420650d67fe3046
+  checksum: 10c0/beb59faf6aae97e6d3c233c5e6ed06cc60d65c49eec576036e3d0da1a831a1e827e3d41f5e81d016440b4f0bdf1406268ae069c4d5b38a6667b310c3da079d22
   languageName: node
   linkType: hard
 

From 05f0d510052fb56478986c474192abc7cea3e775 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 5 Jul 2024 13:32:29 +0200
Subject: [PATCH 80/84] chore(deps): update dependency sidekiq-scheduler to
 v5.0.5 (#30918)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index eb6720e45..c0fa8a603 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -793,10 +793,10 @@ GEM
       redis (>= 4.5.0, < 5)
     sidekiq-bulk (0.2.0)
       sidekiq
-    sidekiq-scheduler (5.0.3)
+    sidekiq-scheduler (5.0.5)
       rufus-scheduler (~> 3.2)
       sidekiq (>= 6, < 8)
-      tilt (>= 1.4.0)
+      tilt (>= 1.4.0, < 3)
     sidekiq-unique-jobs (7.1.33)
       brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
       concurrent-ruby (~> 1.0, >= 1.0.5)

From 016c1e4e788890f0c81a47640f76de136a0a8f32 Mon Sep 17 00:00:00 2001
From: David Roetzel <david@roetzel.de>
Date: Fri, 5 Jul 2024 13:54:38 +0200
Subject: [PATCH 81/84] Improve handling of encoding problems when creating
 link previews (#30929)

---
 app/lib/link_details_extractor.rb             | 22 +++++++++++----
 app/services/fetch_link_card_service.rb       |  2 +-
 .../requests/latin1_posing_as_utf8_broken.txt | 17 +++++++++++
 .../latin1_posing_as_utf8_recoverable.txt     | 17 +++++++++++
 spec/services/fetch_link_card_service_spec.rb | 28 +++++++++++++++++--
 5 files changed, 76 insertions(+), 10 deletions(-)
 create mode 100644 spec/fixtures/requests/latin1_posing_as_utf8_broken.txt
 create mode 100644 spec/fixtures/requests/latin1_posing_as_utf8_recoverable.txt

diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb
index dbfdd33fc..9436d20b5 100644
--- a/app/lib/link_details_extractor.rb
+++ b/app/lib/link_details_extractor.rb
@@ -156,11 +156,11 @@ class LinkDetailsExtractor
   end
 
   def title
-    html_entities.decode(structured_data&.headline || opengraph_tag('og:title') || document.xpath('//title').map(&:content).first).strip
+    html_entities_decode(structured_data&.headline || opengraph_tag('og:title') || document.xpath('//title').map(&:content).first).strip
   end
 
   def description
-    html_entities.decode(structured_data&.description || opengraph_tag('og:description') || meta_tag('description'))
+    html_entities_decode(structured_data&.description || opengraph_tag('og:description') || meta_tag('description'))
   end
 
   def published_at
@@ -180,7 +180,7 @@ class LinkDetailsExtractor
   end
 
   def provider_name
-    html_entities.decode(structured_data&.publisher_name || opengraph_tag('og:site_name'))
+    html_entities_decode(structured_data&.publisher_name || opengraph_tag('og:site_name'))
   end
 
   def provider_url
@@ -188,7 +188,7 @@ class LinkDetailsExtractor
   end
 
   def author_name
-    html_entities.decode(structured_data&.author_name || opengraph_tag('og:author') || opengraph_tag('og:author:username'))
+    html_entities_decode(structured_data&.author_name || opengraph_tag('og:author') || opengraph_tag('og:author:username'))
   end
 
   def author_url
@@ -257,7 +257,7 @@ class LinkDetailsExtractor
 
       next if json_ld.blank?
 
-      structured_data = StructuredData.new(html_entities.decode(json_ld))
+      structured_data = StructuredData.new(html_entities_decode(json_ld))
 
       next unless structured_data.valid?
 
@@ -273,10 +273,11 @@ class LinkDetailsExtractor
   end
 
   def detect_encoding_and_parse_document
-    [detect_encoding, nil, @html_charset, 'UTF-8'].uniq.each do |encoding|
+    [detect_encoding, nil, @html_charset].uniq.each do |encoding|
       document = Nokogiri::HTML(@html, nil, encoding)
       return document if document.to_s.valid_encoding?
     end
+    Nokogiri::HTML(@html, nil, 'UTF-8')
   end
 
   def detect_encoding
@@ -290,6 +291,15 @@ class LinkDetailsExtractor
     end
   end
 
+  def html_entities_decode(string)
+    return if string.nil?
+
+    unicode_string = string.encode('UTF-8')
+    raise EncodingError, 'cannot convert string to valid UTF-8' unless unicode_string.valid_encoding?
+
+    html_entities.decode(unicode_string)
+  end
+
   def html_entities
     @html_entities ||= HTMLEntities.new(:expanded)
   end
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 8bc9f912c..436e024c9 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -32,7 +32,7 @@ class FetchLinkCardService < BaseService
     end
 
     attach_card if @card&.persisted?
-  rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError, Encoding::UndefinedConversionError => e
+  rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError, EncodingError => e
     Rails.logger.debug { "Error fetching link #{@original_url}: #{e}" }
     nil
   end
diff --git a/spec/fixtures/requests/latin1_posing_as_utf8_broken.txt b/spec/fixtures/requests/latin1_posing_as_utf8_broken.txt
new file mode 100644
index 000000000..ed8a4716a
--- /dev/null
+++ b/spec/fixtures/requests/latin1_posing_as_utf8_broken.txt
@@ -0,0 +1,17 @@
+HTTP/1.1 200 OK
+server: nginx
+date: Thu, 13 Jun 2024 14:33:13 GMT
+content-type: text/html; charset=utf-8
+content-length: 158
+accept-ranges: bytes
+
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Tofu � l'orange</title>
+</head>
+<body>
+  <h2>Tofu � l'orange</h2>
+</body>
+</html>
diff --git a/spec/fixtures/requests/latin1_posing_as_utf8_recoverable.txt b/spec/fixtures/requests/latin1_posing_as_utf8_recoverable.txt
new file mode 100644
index 000000000..a24985832
--- /dev/null
+++ b/spec/fixtures/requests/latin1_posing_as_utf8_recoverable.txt
@@ -0,0 +1,17 @@
+HTTP/1.1 200 OK
+server: nginx
+date: Thu, 13 Jun 2024 14:33:13 GMT
+content-type: text/html; charset=utf-8
+content-length: 158
+accept-ranges: bytes
+
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Tofu with orange sauce</title>
+</head>
+<body>
+  <h2>Tofu � l'orange</h2>
+</body>
+</html>
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index d83a52751..547737b61 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -27,6 +27,8 @@ RSpec.describe FetchLinkCardService do
     stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
     stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt'))
     stub_request(:get, 'http://example.com/low_confidence_latin1').to_return(request_fixture('low_confidence_latin1.txt'))
+    stub_request(:get, 'http://example.com/latin1_posing_as_utf8_broken').to_return(request_fixture('latin1_posing_as_utf8_broken.txt'))
+    stub_request(:get, 'http://example.com/latin1_posing_as_utf8_recoverable').to_return(request_fixture('latin1_posing_as_utf8_recoverable.txt'))
     stub_request(:get, 'http://example.com/aergerliche-umlaute').to_return(request_fixture('redirect_with_utf8_url.txt'))
 
     Rails.cache.write('oembed_endpoint:example.com', oembed_cache) if oembed_cache
@@ -159,10 +161,30 @@ RSpec.describe FetchLinkCardService do
     end
 
     context 'with a URL of a page in ISO-8859-1 encoding, that charlock_holmes cannot detect' do
-      let(:status) { Fabricate(:status, text: 'Check out http://example.com/low_confidence_latin1') }
+      context 'when encoding in http header is correct' do
+        let(:status) { Fabricate(:status, text: 'Check out http://example.com/low_confidence_latin1') }
 
-      it 'decodes the HTML' do
-        expect(status.preview_card.title).to eq("Tofu á l'orange")
+        it 'decodes the HTML' do
+          expect(status.preview_card.title).to eq("Tofu á l'orange")
+        end
+      end
+
+      context 'when encoding in http header is incorrect' do
+        context 'when encoding problems appear in unrelated tags' do
+          let(:status) { Fabricate(:status, text: 'Check out http://example.com/latin1_posing_as_utf8_recoverable') }
+
+          it 'decodes the HTML' do
+            expect(status.preview_card.title).to eq('Tofu with orange sauce')
+          end
+        end
+
+        context 'when encoding problems appear in title tag' do
+          let(:status) { Fabricate(:status, text: 'Check out http://example.com/latin1_posing_as_utf8_broken') }
+
+          it 'does not create a preview card' do
+            expect(status.preview_card).to be_nil
+          end
+        end
       end
     end
 

From 97eddb5906640a79a1cba20823ce10e986f7f317 Mon Sep 17 00:00:00 2001
From: David Roetzel <david@roetzel.de>
Date: Fri, 5 Jul 2024 15:28:52 +0200
Subject: [PATCH 82/84] Fix details extraction when no title exists. (#30933)

---
 app/lib/link_details_extractor.rb             |  2 +-
 spec/fixtures/requests/page_without_title.txt | 17 +++++++++++++++++
 spec/services/fetch_link_card_service_spec.rb |  9 +++++++++
 3 files changed, 27 insertions(+), 1 deletion(-)
 create mode 100644 spec/fixtures/requests/page_without_title.txt

diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb
index 9436d20b5..a62ede2bb 100644
--- a/app/lib/link_details_extractor.rb
+++ b/app/lib/link_details_extractor.rb
@@ -156,7 +156,7 @@ class LinkDetailsExtractor
   end
 
   def title
-    html_entities_decode(structured_data&.headline || opengraph_tag('og:title') || document.xpath('//title').map(&:content).first).strip
+    html_entities_decode(structured_data&.headline || opengraph_tag('og:title') || document.xpath('//title').map(&:content).first)&.strip
   end
 
   def description
diff --git a/spec/fixtures/requests/page_without_title.txt b/spec/fixtures/requests/page_without_title.txt
new file mode 100644
index 000000000..0054aa3b7
--- /dev/null
+++ b/spec/fixtures/requests/page_without_title.txt
@@ -0,0 +1,17 @@
+HTTP/1.1 200 OK
+server: nginx
+date: Thu, 13 Jun 2024 14:33:13 GMT
+content-type: text/html; charset=utf-8
+content-length: 171
+accept-ranges: bytes
+
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+</head>
+<body>
+  <h2>I am not a valid page</h2>
+  <p>Thankfully, browsers do not care</p>
+</body>
+</html>
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index 547737b61..b2cd99cea 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe FetchLinkCardService do
     stub_request(:get, 'http://example.com/latin1_posing_as_utf8_broken').to_return(request_fixture('latin1_posing_as_utf8_broken.txt'))
     stub_request(:get, 'http://example.com/latin1_posing_as_utf8_recoverable').to_return(request_fixture('latin1_posing_as_utf8_recoverable.txt'))
     stub_request(:get, 'http://example.com/aergerliche-umlaute').to_return(request_fixture('redirect_with_utf8_url.txt'))
+    stub_request(:get, 'http://example.com/page_without_title').to_return(request_fixture('page_without_title.txt'))
 
     Rails.cache.write('oembed_endpoint:example.com', oembed_cache) if oembed_cache
 
@@ -112,6 +113,14 @@ RSpec.describe FetchLinkCardService do
       end
     end
 
+    context 'with a page that has no title' do
+      let(:status) { Fabricate(:status, text: 'http://example.com/page_without_title') }
+
+      it 'does not create a preview card' do
+        expect(status.preview_card).to be_nil
+      end
+    end
+
     context 'with a 404 URL' do
       let(:status) { Fabricate(:status, text: 'http://example.com/not-found') }
 

From 8f5694d79e531b94784697d92bed24d003d77353 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 5 Jul 2024 15:40:53 +0200
Subject: [PATCH 83/84] Fix right-to-left text in preview cards (#30930)

---
 app/javascript/mastodon/features/status/components/card.jsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/features/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx
index f0ae40cbc..ee1fbe0f8 100644
--- a/app/javascript/mastodon/features/status/components/card.jsx
+++ b/app/javascript/mastodon/features/status/components/card.jsx
@@ -141,7 +141,7 @@ export default class Card extends PureComponent {
     const showAuthor  = !!card.getIn(['authors', 0, 'accountId']);
 
     const description = (
-      <div className='status-card__content'>
+      <div className='status-card__content' dir='auto'>
         <span className='status-card__host'>
           <span lang={language}>{provider}</span>
           {card.get('published_at') && <> · <RelativeTimestamp timestamp={card.get('published_at')} /></>}

From 63ba69810eca80fc2d10114a79f2988c1b75892f Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Sat, 6 Jul 2024 09:22:24 +0200
Subject: [PATCH 84/84] Fix overflow behavior on profile fields in hover cards
 (#30928)

---
 app/javascript/styles/mastodon/components.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 12eac79b9..c114885d1 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -10453,7 +10453,7 @@ noscript {
       gap: 4px;
 
       dt {
-        flex: 0 0 auto;
+        flex: 0 1 auto;
         color: $dark-text-color;
         min-width: 0;
         overflow: hidden;