From 0d14fcebae0b9a0e5da73049c960064338dfee8e Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 13 Nov 2023 10:58:28 +0100
Subject: [PATCH 01/63] Change link previews to keep original URL from the
 status (#27312)

---
 app/chewy/public_statuses_index.rb             |  2 +-
 app/chewy/statuses_index.rb                    |  2 +-
 .../api/v1/conversations_controller.rb         |  2 +-
 app/models/admin/status_batch_action.rb        |  2 +-
 app/models/concerns/status_search_concern.rb   |  2 +-
 app/models/preview_card.rb                     |  7 ++++++-
 app/models/preview_cards_status.rb             | 18 ++++++++++++++++++
 app/models/status.rb                           | 15 ++++++++++-----
 app/models/trends/links.rb                     |  4 +---
 .../rest/preview_card_serializer.rb            |  4 ++++
 .../process_status_update_service.rb           |  2 +-
 app/services/fetch_link_card_service.rb        |  6 +++---
 app/services/update_status_service.rb          |  2 +-
 ...183200_add_url_to_preview_cards_statuses.rb |  7 +++++++
 db/schema.rb                                   |  3 ++-
 lib/tasks/tests.rake                           |  2 +-
 spec/helpers/media_component_helper_spec.rb    |  4 +++-
 spec/services/fetch_link_card_service_spec.rb  |  8 ++++----
 spec/services/update_status_service_spec.rb    |  8 ++++----
 19 files changed, 70 insertions(+), 30 deletions(-)
 create mode 100644 app/models/preview_cards_status.rb
 create mode 100644 db/migrate/20231006183200_add_url_to_preview_cards_statuses.rb

diff --git a/app/chewy/public_statuses_index.rb b/app/chewy/public_statuses_index.rb
index 4be204d4a..b5f0be5e5 100644
--- a/app/chewy/public_statuses_index.rb
+++ b/app/chewy/public_statuses_index.rb
@@ -53,7 +53,7 @@ class PublicStatusesIndex < Chewy::Index
   index_scope ::Status.unscoped
                       .kept
                       .indexable
-                      .includes(:media_attachments, :preloadable_poll, :preview_cards, :tags)
+                      .includes(:media_attachments, :preloadable_poll, :tags, preview_cards_status: :preview_card)
 
   root date_detection: false do
     field(:id, type: 'long')
diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb
index 6b25dc9df..e315a2030 100644
--- a/app/chewy/statuses_index.rb
+++ b/app/chewy/statuses_index.rb
@@ -50,7 +50,7 @@ class StatusesIndex < Chewy::Index
     },
   }
 
-  index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preview_cards, :local_mentioned, :local_favorited, :local_reblogged, :local_bookmarked, :tags, preloadable_poll: :local_voters), delete_if: ->(status) { status.searchable_by.empty? }
+  index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :local_mentioned, :local_favorited, :local_reblogged, :local_bookmarked, :tags, preview_cards_status: :preview_card, preloadable_poll: :local_voters), delete_if: ->(status) { status.searchable_by.empty? }
 
   root date_detection: false do
     field(:id, type: 'long')
diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb
index b3ca2f790..6a3567e62 100644
--- a/app/controllers/api/v1/conversations_controller.rb
+++ b/app/controllers/api/v1/conversations_controller.rb
@@ -41,10 +41,10 @@ class Api::V1::ConversationsController < Api::BaseController
                          account: :account_stat,
                          last_status: [
                            :media_attachments,
-                           :preview_cards,
                            :status_stat,
                            :tags,
                            {
+                             preview_cards_status: :preview_card,
                              active_mentions: [account: :account_stat],
                              account: :account_stat,
                            },
diff --git a/app/models/admin/status_batch_action.rb b/app/models/admin/status_batch_action.rb
index 24c3979aa..8a8e2fa37 100644
--- a/app/models/admin/status_batch_action.rb
+++ b/app/models/admin/status_batch_action.rb
@@ -74,7 +74,7 @@ class Admin::StatusBatchAction
 
     # Can't use a transaction here because UpdateStatusService queues
     # Sidekiq jobs
-    statuses.includes(:media_attachments, :preview_cards).find_each do |status|
+    statuses.includes(:media_attachments, preview_cards_status: :preview_card).find_each do |status|
       next if status.discarded? || !(status.with_media? || status.with_preview_card?)
 
       authorize([:admin, status], :update?)
diff --git a/app/models/concerns/status_search_concern.rb b/app/models/concerns/status_search_concern.rb
index 3ef45754a..7252fde73 100644
--- a/app/models/concerns/status_search_concern.rb
+++ b/app/models/concerns/status_search_concern.rb
@@ -40,7 +40,7 @@ module StatusSearchConcern
       properties << 'media' if with_media?
       properties << 'poll' if with_poll?
       properties << 'link' if with_preview_card?
-      properties << 'embed' if preview_cards.any?(&:video?)
+      properties << 'embed' if preview_card&.video?
       properties << 'sensitive' if sensitive?
       properties << 'reply' if reply?
     end
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index a1751c426..837592743 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -50,7 +50,9 @@ class PreviewCard < ApplicationRecord
   enum type: { link: 0, photo: 1, video: 2, rich: 3 }
   enum link_type: { unknown: 0, article: 1 }
 
-  has_and_belongs_to_many :statuses
+  has_many :preview_cards_statuses, dependent: :delete_all, inverse_of: :preview_card
+  has_many :statuses, through: :preview_cards_statuses
+
   has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy
 
   has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, validate_media_type: false
@@ -64,6 +66,9 @@ class PreviewCard < ApplicationRecord
 
   before_save :extract_dimensions, if: :link?
 
+  # This can be set by the status when retrieving the preview card using the join model
+  attr_accessor :original_url
+
   def appropriate_for_trends?
     link? && article? && title.present? && description.present? && image.present? && provider_name.present?
   end
diff --git a/app/models/preview_cards_status.rb b/app/models/preview_cards_status.rb
new file mode 100644
index 000000000..341771e4d
--- /dev/null
+++ b/app/models/preview_cards_status.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: preview_cards_statuses
+#
+#  preview_card_id :bigint(8)        not null
+#  status_id       :bigint(8)        not null
+#  url             :string
+#
+class PreviewCardsStatus < ApplicationRecord
+  # Composite primary keys are not properly supported in Rails. However,
+  # we shouldn't need this anyway...
+  self.primary_key = nil
+
+  belongs_to :preview_card
+  belongs_to :status
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index 1c41ef1d5..41c895029 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -79,8 +79,8 @@ class Status < ApplicationRecord
   has_many :local_bookmarked, -> { merge(Account.local) }, through: :bookmarks, source: :account
 
   has_and_belongs_to_many :tags
-  has_and_belongs_to_many :preview_cards
 
+  has_one :preview_cards_status, inverse_of: :status # Because of a composite primary key, the dependent option cannot be used
   has_one :notification, as: :activity, dependent: :destroy
   has_one :status_stat, inverse_of: :status
   has_one :poll, inverse_of: :status, dependent: :destroy
@@ -142,24 +142,25 @@ class Status < ApplicationRecord
   # The `prepend: true` option below ensures this runs before
   # the `dependent: destroy` callbacks remove relevant records
   before_destroy :unlink_from_conversations!, prepend: true
+  before_destroy :reset_preview_card!
 
   cache_associated :application,
                    :media_attachments,
                    :conversation,
                    :status_stat,
                    :tags,
-                   :preview_cards,
                    :preloadable_poll,
+                   preview_cards_status: [:preview_card],
                    account: [:account_stat, user: :role],
                    active_mentions: { account: :account_stat },
                    reblog: [
                      :application,
                      :tags,
-                     :preview_cards,
                      :media_attachments,
                      :conversation,
                      :status_stat,
                      :preloadable_poll,
+                     preview_cards_status: [:preview_card],
                      account: [:account_stat, user: :role],
                      active_mentions: { account: :account_stat },
                    ],
@@ -226,7 +227,11 @@ class Status < ApplicationRecord
   end
 
   def preview_card
-    preview_cards.first
+    preview_cards_status&.preview_card&.tap { |x| x.original_url = preview_cards_status.url }
+  end
+
+  def reset_preview_card!
+    PreviewCardsStatus.where(status_id: id).delete_all
   end
 
   def hidden?
@@ -244,7 +249,7 @@ class Status < ApplicationRecord
   end
 
   def with_preview_card?
-    preview_cards.any?
+    preview_cards_status.present?
   end
 
   def with_poll?
diff --git a/app/models/trends/links.rb b/app/models/trends/links.rb
index fcbdb1a5f..b4eae9f70 100644
--- a/app/models/trends/links.rb
+++ b/app/models/trends/links.rb
@@ -54,9 +54,7 @@ class Trends::Links < Trends::Base
                   !(original_status.account.silenced? || status.account.silenced?) &&
                   !(original_status.spoiler_text? || original_status.sensitive?)
 
-    original_status.preview_cards.each do |preview_card|
-      add(preview_card, status.account_id, at_time) if preview_card.appropriate_for_trends?
-    end
+    add(original_status.preview_card, status.account_id, at_time) if original_status.preview_card&.appropriate_for_trends?
   end
 
   def add(preview_card, account_id, at_time = Time.now.utc)
diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb
index 3e1c4bde3..039262cd5 100644
--- a/app/serializers/rest/preview_card_serializer.rb
+++ b/app/serializers/rest/preview_card_serializer.rb
@@ -8,6 +8,10 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
              :provider_url, :html, :width, :height,
              :image, :image_description, :embed_url, :blurhash, :published_at
 
+  def url
+    object.original_url.presence || object.url
+  end
+
   def image
     object.image? ? full_asset_url(object.image.url(:original)) : nil
   end
diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb
index 4ff92da01..2db0e80e7 100644
--- a/app/services/activitypub/process_status_update_service.rb
+++ b/app/services/activitypub/process_status_update_service.rb
@@ -280,7 +280,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
   end
 
   def reset_preview_card!
-    @status.preview_cards.clear
+    @status.reset_preview_card!
     LinkCrawlWorker.perform_in(rand(1..59).seconds, @status.id)
   end
 
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 13775e63c..c6b600dd7 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -19,7 +19,7 @@ class FetchLinkCardService < BaseService
     @status       = status
     @original_url = parse_urls
 
-    return if @original_url.nil? || @status.preview_cards.any?
+    return if @original_url.nil? || @status.with_preview_card?
 
     @url = @original_url.to_s
 
@@ -62,9 +62,9 @@ class FetchLinkCardService < BaseService
 
   def attach_card
     with_redis_lock("attach_card:#{@status.id}") do
-      return if @status.preview_cards.any?
+      return if @status.with_preview_card?
 
-      @status.preview_cards << @card
+      PreviewCardsStatus.create(status: @status, preview_card: @card, url: @original_url)
       Rails.cache.delete(@status)
       Trends.links.register(@status)
     end
diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb
index d1c2b990f..cdfe28365 100644
--- a/app/services/update_status_service.rb
+++ b/app/services/update_status_service.rb
@@ -123,7 +123,7 @@ class UpdateStatusService < BaseService
   def reset_preview_card!
     return unless @status.text_previously_changed?
 
-    @status.preview_cards.clear
+    @status.reset_preview_card!
     LinkCrawlWorker.perform_async(@status.id)
   end
 
diff --git a/db/migrate/20231006183200_add_url_to_preview_cards_statuses.rb b/db/migrate/20231006183200_add_url_to_preview_cards_statuses.rb
new file mode 100644
index 000000000..f7c6de462
--- /dev/null
+++ b/db/migrate/20231006183200_add_url_to_preview_cards_statuses.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddURLToPreviewCardsStatuses < ActiveRecord::Migration[7.0]
+  def change
+    add_column :preview_cards_statuses, :url, :string
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 37020c2d7..a0062c8ce 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
+ActiveRecord::Schema[7.0].define(version: 2023_10_06_183200) do
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
 
@@ -811,6 +811,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
   create_table "preview_cards_statuses", primary_key: ["status_id", "preview_card_id"], force: :cascade do |t|
     t.bigint "preview_card_id", null: false
     t.bigint "status_id", null: false
+    t.string "url"
   end
 
   create_table "relays", force: :cascade do |t|
diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake
index 7f8e72dd8..209a73efa 100644
--- a/lib/tasks/tests.rake
+++ b/lib/tasks/tests.rake
@@ -69,7 +69,7 @@ namespace :tests do
         exit(1)
       end
 
-      unless Status.find(12).preview_cards.pluck(:url) == ['https://joinmastodon.org/']
+      unless PreviewCard.where(id: PreviewCardsStatus.where(status_id: 12).select(:preview_card_id)).pluck(:url) == ['https://joinmastodon.org/']
         puts 'Preview cards not deduplicated as expected'
         exit(1)
       end
diff --git a/spec/helpers/media_component_helper_spec.rb b/spec/helpers/media_component_helper_spec.rb
index 71a9af6f3..149f6a83a 100644
--- a/spec/helpers/media_component_helper_spec.rb
+++ b/spec/helpers/media_component_helper_spec.rb
@@ -49,10 +49,12 @@ describe MediaComponentHelper do
   end
 
   describe 'render_card_component' do
-    let(:status) { Fabricate(:status, preview_cards: [Fabricate(:preview_card)]) }
+    let(:status) { Fabricate(:status) }
     let(:result) { helper.render_card_component(status) }
 
     before do
+      PreviewCardsStatus.create(status: status, preview_card: Fabricate(:preview_card))
+
       without_partial_double_verification do
         allow(helper).to receive(:current_account).and_return(status.account)
       end
diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb
index f44cbb750..d8ca310b2 100644
--- a/spec/services/fetch_link_card_service_spec.rb
+++ b/spec/services/fetch_link_card_service_spec.rb
@@ -120,7 +120,7 @@ RSpec.describe FetchLinkCardService, type: :service do
       let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis') }
 
       it 'decodes the HTML' do
-        expect(status.preview_cards.first.title).to eq('SJISのページ')
+        expect(status.preview_card.title).to eq('SJISのページ')
       end
     end
 
@@ -128,7 +128,7 @@ RSpec.describe FetchLinkCardService, type: :service do
       let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis_with_wrong_charset') }
 
       it 'decodes the HTML despite the wrong charset header' do
-        expect(status.preview_cards.first.title).to eq('SJISのページ')
+        expect(status.preview_card.title).to eq('SJISのページ')
       end
     end
 
@@ -136,7 +136,7 @@ RSpec.describe FetchLinkCardService, type: :service do
       let(:status) { Fabricate(:status, text: 'Check out http://example.com/koi8-r') }
 
       it 'decodes the HTML' do
-        expect(status.preview_cards.first.title).to eq('Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.')
+        expect(status.preview_card.title).to eq('Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.')
       end
     end
 
@@ -144,7 +144,7 @@ RSpec.describe FetchLinkCardService, type: :service do
       let(:status) { Fabricate(:status, text: 'Check out http://example.com/windows-1251') }
 
       it 'decodes the HTML' do
-        expect(status.preview_cards.first.title).to eq('сэмпл текст')
+        expect(status.preview_card.title).to eq('сэмпл текст')
       end
     end
 
diff --git a/spec/services/update_status_service_spec.rb b/spec/services/update_status_service_spec.rb
index 9c53ebb2f..eb38230b0 100644
--- a/spec/services/update_status_service_spec.rb
+++ b/spec/services/update_status_service_spec.rb
@@ -23,11 +23,11 @@ RSpec.describe UpdateStatusService, type: :service do
   end
 
   context 'when text changes' do
-    let!(:status) { Fabricate(:status, text: 'Foo') }
+    let(:status) { Fabricate(:status, text: 'Foo') }
     let(:preview_card) { Fabricate(:preview_card) }
 
     before do
-      status.preview_cards << preview_card
+      PreviewCardsStatus.create(status: status, preview_card: preview_card)
       subject.call(status, status.account_id, text: 'Bar')
     end
 
@@ -45,11 +45,11 @@ RSpec.describe UpdateStatusService, type: :service do
   end
 
   context 'when content warning changes' do
-    let!(:status) { Fabricate(:status, text: 'Foo', spoiler_text: '') }
+    let(:status) { Fabricate(:status, text: 'Foo', spoiler_text: '') }
     let(:preview_card) { Fabricate(:preview_card) }
 
     before do
-      status.preview_cards << preview_card
+      PreviewCardsStatus.create(status: status, preview_card: preview_card)
       subject.call(status, status.account_id, text: 'Foo', spoiler_text: 'Bar')
     end
 

From da4f37020b6ef15235fd5bcaf3ed7cd2fd15437f Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Mon, 13 Nov 2023 13:19:41 +0100
Subject: [PATCH 02/63] Fix Jest config (#27834)

---
 .watchmanconfig | 3 +++
 jest.config.js  | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)
 create mode 100644 .watchmanconfig

diff --git a/.watchmanconfig b/.watchmanconfig
new file mode 100644
index 000000000..29e4f231e
--- /dev/null
+++ b/.watchmanconfig
@@ -0,0 +1,3 @@
+{
+  "ignore_dirs": ["node_modules/", "public/"]
+}
diff --git a/jest.config.js b/jest.config.js
index 83eae46cb..b4a34a5ab 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -19,7 +19,7 @@ const config = {
   // Those packages are ESM, so we need them to be processed by Babel
   transformIgnorePatterns: ['/node_modules/(?!(redent|strip-indent)/)'],
   coverageDirectory: '<rootDir>/coverage',
-  moduleDirectories: ['<rootDir>/node_modules', '<rootDir>/app/javascript'],
+  moduleDirectories: ['node_modules', '<rootDir>/app/javascript'],
   moduleNameMapper: {
     '\\.svg$': '<rootDir>/app/javascript/__mocks__/svg.js',
   },

From 3b989e4d644d2c5e1b191e2740cd700cf9dc291d Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 13 Nov 2023 14:13:53 +0100
Subject: [PATCH 03/63] Update dependency rails to v7.1.2 (#27812)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index 83693812f..84ad19b80 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -39,50 +39,51 @@ GIT
 GEM
   remote: https://rubygems.org/
   specs:
-    actioncable (7.1.1)
-      actionpack (= 7.1.1)
-      activesupport (= 7.1.1)
+    actioncable (7.1.2)
+      actionpack (= 7.1.2)
+      activesupport (= 7.1.2)
       nio4r (~> 2.0)
       websocket-driver (>= 0.6.1)
       zeitwerk (~> 2.6)
-    actionmailbox (7.1.1)
-      actionpack (= 7.1.1)
-      activejob (= 7.1.1)
-      activerecord (= 7.1.1)
-      activestorage (= 7.1.1)
-      activesupport (= 7.1.1)
+    actionmailbox (7.1.2)
+      actionpack (= 7.1.2)
+      activejob (= 7.1.2)
+      activerecord (= 7.1.2)
+      activestorage (= 7.1.2)
+      activesupport (= 7.1.2)
       mail (>= 2.7.1)
       net-imap
       net-pop
       net-smtp
-    actionmailer (7.1.1)
-      actionpack (= 7.1.1)
-      actionview (= 7.1.1)
-      activejob (= 7.1.1)
-      activesupport (= 7.1.1)
+    actionmailer (7.1.2)
+      actionpack (= 7.1.2)
+      actionview (= 7.1.2)
+      activejob (= 7.1.2)
+      activesupport (= 7.1.2)
       mail (~> 2.5, >= 2.5.4)
       net-imap
       net-pop
       net-smtp
       rails-dom-testing (~> 2.2)
-    actionpack (7.1.1)
-      actionview (= 7.1.1)
-      activesupport (= 7.1.1)
+    actionpack (7.1.2)
+      actionview (= 7.1.2)
+      activesupport (= 7.1.2)
       nokogiri (>= 1.8.5)
+      racc
       rack (>= 2.2.4)
       rack-session (>= 1.0.1)
       rack-test (>= 0.6.3)
       rails-dom-testing (~> 2.2)
       rails-html-sanitizer (~> 1.6)
-    actiontext (7.1.1)
-      actionpack (= 7.1.1)
-      activerecord (= 7.1.1)
-      activestorage (= 7.1.1)
-      activesupport (= 7.1.1)
+    actiontext (7.1.2)
+      actionpack (= 7.1.2)
+      activerecord (= 7.1.2)
+      activestorage (= 7.1.2)
+      activesupport (= 7.1.2)
       globalid (>= 0.6.0)
       nokogiri (>= 1.8.5)
-    actionview (7.1.1)
-      activesupport (= 7.1.1)
+    actionview (7.1.2)
+      activesupport (= 7.1.2)
       builder (~> 3.1)
       erubi (~> 1.11)
       rails-dom-testing (~> 2.2)
@@ -92,22 +93,22 @@ GEM
       activemodel (>= 4.1)
       case_transform (>= 0.2)
       jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
-    activejob (7.1.1)
-      activesupport (= 7.1.1)
+    activejob (7.1.2)
+      activesupport (= 7.1.2)
       globalid (>= 0.3.6)
-    activemodel (7.1.1)
-      activesupport (= 7.1.1)
-    activerecord (7.1.1)
-      activemodel (= 7.1.1)
-      activesupport (= 7.1.1)
+    activemodel (7.1.2)
+      activesupport (= 7.1.2)
+    activerecord (7.1.2)
+      activemodel (= 7.1.2)
+      activesupport (= 7.1.2)
       timeout (>= 0.4.0)
-    activestorage (7.1.1)
-      actionpack (= 7.1.1)
-      activejob (= 7.1.1)
-      activerecord (= 7.1.1)
-      activesupport (= 7.1.1)
+    activestorage (7.1.2)
+      actionpack (= 7.1.2)
+      activejob (= 7.1.2)
+      activerecord (= 7.1.2)
+      activesupport (= 7.1.2)
       marcel (~> 1.0)
-    activesupport (7.1.1)
+    activesupport (7.1.2)
       base64
       bigdecimal
       concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -218,7 +219,7 @@ GEM
       activerecord (>= 5.a)
       database_cleaner-core (~> 2.0.0)
     database_cleaner-core (2.0.1)
-    date (3.3.3)
+    date (3.3.4)
     debug_inspector (1.1.0)
     devise (4.9.3)
       bcrypt (~> 3.0)
@@ -369,7 +370,7 @@ GEM
       terminal-table (>= 1.5.1)
     idn-ruby (0.1.5)
     io-console (0.6.0)
-    irb (1.8.1)
+    irb (1.8.3)
       rdoc
       reline (>= 0.3.8)
     jmespath (1.6.2)
@@ -462,13 +463,13 @@ GEM
       uri
     net-http-persistent (4.0.2)
       connection_pool (~> 2.2)
-    net-imap (0.4.1)
+    net-imap (0.4.4)
       date
       net-protocol
     net-ldap (0.18.0)
     net-pop (0.1.2)
       net-protocol
-    net-protocol (0.2.1)
+    net-protocol (0.2.2)
       timeout
     net-smtp (0.4.0)
       net-protocol
@@ -526,7 +527,7 @@ GEM
       net-smtp
       premailer (~> 1.7, >= 1.7.9)
     private_address_check (0.5.0)
-    psych (5.1.1)
+    psych (5.1.1.1)
       stringio
     public_suffix (5.0.3)
     puma (6.4.0)
@@ -557,20 +558,20 @@ GEM
     rackup (1.0.0)
       rack (< 3)
       webrick
-    rails (7.1.1)
-      actioncable (= 7.1.1)
-      actionmailbox (= 7.1.1)
-      actionmailer (= 7.1.1)
-      actionpack (= 7.1.1)
-      actiontext (= 7.1.1)
-      actionview (= 7.1.1)
-      activejob (= 7.1.1)
-      activemodel (= 7.1.1)
-      activerecord (= 7.1.1)
-      activestorage (= 7.1.1)
-      activesupport (= 7.1.1)
+    rails (7.1.2)
+      actioncable (= 7.1.2)
+      actionmailbox (= 7.1.2)
+      actionmailer (= 7.1.2)
+      actionpack (= 7.1.2)
+      actiontext (= 7.1.2)
+      actionview (= 7.1.2)
+      activejob (= 7.1.2)
+      activemodel (= 7.1.2)
+      activerecord (= 7.1.2)
+      activestorage (= 7.1.2)
+      activesupport (= 7.1.2)
       bundler (>= 1.15.0)
-      railties (= 7.1.1)
+      railties (= 7.1.2)
     rails-controller-testing (1.0.5)
       actionpack (>= 5.0.1.rc1)
       actionview (>= 5.0.1.rc1)
@@ -585,9 +586,9 @@ GEM
     rails-i18n (7.0.8)
       i18n (>= 0.7, < 2)
       railties (>= 6.0.0, < 8)
-    railties (7.1.1)
-      actionpack (= 7.1.1)
-      activesupport (= 7.1.1)
+    railties (7.1.2)
+      actionpack (= 7.1.2)
+      activesupport (= 7.1.2)
       irb
       rackup (>= 1.0.0)
       rake (>= 12.2)
@@ -737,7 +738,7 @@ GEM
     statsd-ruby (1.5.0)
     stoplight (3.0.2)
       redlock (~> 1.0)
-    stringio (3.0.8)
+    stringio (3.0.9)
     strong_migrations (1.6.4)
       activerecord (>= 5.2)
     swd (1.3.0)
@@ -753,7 +754,7 @@ GEM
     test-prof (1.2.3)
     thor (1.3.0)
     tilt (2.3.0)
-    timeout (0.4.0)
+    timeout (0.4.1)
     tpm-key_attestation (0.12.0)
       bindata (~> 2.4)
       openssl (> 2.0)

From a7117bbef635735d4f01fc2fb6eaa085568862fa Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 13 Nov 2023 14:13:57 +0100
Subject: [PATCH 04/63] Update dependency @rails/ujs to v7.1.2 (#27811)

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 4c70859d0..0058a6837 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2661,9 +2661,9 @@ __metadata:
   linkType: hard
 
 "@rails/ujs@npm:^7.1.1":
-  version: 7.1.1
-  resolution: "@rails/ujs@npm:7.1.1"
-  checksum: 79aa50400097d9254e194979cc011aaa92b456631fd0087a7bfc6b74ff47821c005b34a0fbb421361741c68133ac9bb35f1bb8f97de1c501144dad4e2c7440f3
+  version: 7.1.2
+  resolution: "@rails/ujs@npm:7.1.2"
+  checksum: 072962733c371fa0fff5e88a0aecd8e91c892f9dc2d31723b7586b45c723206d6b82ac71b0d7db26ea0a5ce60e0832430b061e4669b8f2aa813c3ea975aac98a
   languageName: node
   linkType: hard
 

From e5a7b73ef4ca8934bb967d15d17c8ee1bd1c2cf0 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 13 Nov 2023 14:21:02 +0100
Subject: [PATCH 05/63] New Crowdin Translations (automated) (#27815)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/be.json    |   1 +
 app/javascript/mastodon/locales/et.json    |   1 +
 app/javascript/mastodon/locales/fa.json    |   5 +-
 app/javascript/mastodon/locales/fi.json    |   1 +
 app/javascript/mastodon/locales/fo.json    |   1 +
 app/javascript/mastodon/locales/ig.json    |  18 +++
 app/javascript/mastodon/locales/ja.json    |   4 +-
 app/javascript/mastodon/locales/ko.json    |   1 +
 app/javascript/mastodon/locales/lt.json    |  13 ++-
 app/javascript/mastodon/locales/no.json    |   1 +
 app/javascript/mastodon/locales/pt-BR.json |   1 +
 app/javascript/mastodon/locales/pt-PT.json |   1 +
 app/javascript/mastodon/locales/vi.json    |   1 +
 config/locales/doorkeeper.lt.yml           | 128 ++++++++++++++++++++-
 config/locales/fa.yml                      |   8 +-
 config/locales/ig.yml                      |   4 +
 config/locales/ja.yml                      |   2 +-
 config/locales/ko.yml                      |   4 +-
 config/locales/nn.yml                      |   8 ++
 config/locales/no.yml                      |  17 +++
 config/locales/simple_form.he.yml          |   2 +-
 config/locales/simple_form.no.yml          |   1 +
 22 files changed, 210 insertions(+), 13 deletions(-)

diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json
index 6262d24e2..7c2d652b6 100644
--- a/app/javascript/mastodon/locales/be.json
+++ b/app/javascript/mastodon/locales/be.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Вынікі пошуку",
   "emoji_button.symbols": "Сімвалы",
   "emoji_button.travel": "Падарожжы і месцы",
+  "empty_column.account_hides_collections": "Гэты карыстальнік вырашыў схаваць гэтую інфармацыю",
   "empty_column.account_suspended": "Уліковы запіс прыпынены",
   "empty_column.account_timeline": "Тут няма допісаў!",
   "empty_column.account_unavailable": "Профіль недаступны",
diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json
index 0b0a8110d..c4182a073 100644
--- a/app/javascript/mastodon/locales/et.json
+++ b/app/javascript/mastodon/locales/et.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Otsitulemused",
   "emoji_button.symbols": "Sümbolid",
   "emoji_button.travel": "Reisimine & kohad",
+  "empty_column.account_hides_collections": "See kasutaja otsustas mitte teha seda infot saadavaks",
   "empty_column.account_suspended": "Konto kustutatud",
   "empty_column.account_timeline": "Siin postitusi ei ole!",
   "empty_column.account_unavailable": "Profiil pole saadaval",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index f4a2e09e7..246f21899 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "نتایج جست‌وجو",
   "emoji_button.symbols": "نمادها",
   "emoji_button.travel": "سفر و مکان",
+  "empty_column.account_hides_collections": "کاربر خواسته که این اطّلاعات در دسترس نباشند",
   "empty_column.account_suspended": "حساب معلق شد",
   "empty_column.account_timeline": "هیچ فرسته‌ای این‌جا نیست!",
   "empty_column.account_unavailable": "نمایهٔ موجود نیست",
@@ -358,13 +359,13 @@
   "keyboard_shortcuts.profile": "گشودن نمایهٔ نویسنده",
   "keyboard_shortcuts.reply": "پاسخ به فرسته",
   "keyboard_shortcuts.requests": "گشودن سیاههٔ درخواست‌های پی‌گیری",
-  "keyboard_shortcuts.search": "تمرکز روی جست‌وجو",
+  "keyboard_shortcuts.search": "تمرکز روی نوار جست‌وجو",
   "keyboard_shortcuts.spoilers": "نمایش/نهفتن زمینهٔ هشدار محتوا",
   "keyboard_shortcuts.start": "گشودن ستون «آغاز کنید»",
   "keyboard_shortcuts.toggle_hidden": "نمایش/نهفتن نوشتهٔ پشت هشدار محتوا",
   "keyboard_shortcuts.toggle_sensitivity": "نمایش/نهفتن رسانه",
   "keyboard_shortcuts.toot": "شروع یک فرستهٔ جدید",
-  "keyboard_shortcuts.unfocus": "برداشتن تمرکز از نوشتن/جست‌وجو",
+  "keyboard_shortcuts.unfocus": "برداشتن تمرکز از ناحیهٔ نوشتن یا جست‌وجو",
   "keyboard_shortcuts.up": "بالا بردن در سیاهه",
   "lightbox.close": "بستن",
   "lightbox.compress": "فشرده‌سازی جعبهٔ نمایش تصویر",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index cc4a9391a..9aa2e7355 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Hakutulokset",
   "emoji_button.symbols": "Symbolit",
   "emoji_button.travel": "Matkailu ja paikat",
+  "empty_column.account_hides_collections": "Käyttäjä on päättänyt olla julkaisematta näitä tietoja",
   "empty_column.account_suspended": "Tili jäädytetty",
   "empty_column.account_timeline": "Ei viestejä täällä.",
   "empty_column.account_unavailable": "Profiilia ei löydy",
diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json
index d6130deb0..42a1317db 100644
--- a/app/javascript/mastodon/locales/fo.json
+++ b/app/javascript/mastodon/locales/fo.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Leitiúrslit",
   "emoji_button.symbols": "Ímyndir",
   "emoji_button.travel": "Ferðing og støð",
+  "empty_column.account_hides_collections": "Hesin brúkarin hevur valt, at hesar upplýsingarnar ikki skulu vera tøkar",
   "empty_column.account_suspended": "Kontan gjørd óvirkin",
   "empty_column.account_timeline": "Einki uppslag her!",
   "empty_column.account_unavailable": "Vangin er ikki tøkur",
diff --git a/app/javascript/mastodon/locales/ig.json b/app/javascript/mastodon/locales/ig.json
index 3eec2158b..201bebc05 100644
--- a/app/javascript/mastodon/locales/ig.json
+++ b/app/javascript/mastodon/locales/ig.json
@@ -1,8 +1,11 @@
 {
   "account.add_or_remove_from_list": "Tinye ma ọ bụ Wepu na ndepụta",
   "account.badges.bot": "Bot",
+  "account.badges.group": "Otù",
   "account.cancel_follow_request": "Withdraw follow request",
   "account.follow": "Soro",
+  "account.followers": "Ndị na-eso",
+  "account.following": "Na-eso",
   "account.follows_you": "Na-eso gị",
   "account.mute": "Mee ogbi @{name}",
   "account.unfollow": "Kwụsị iso",
@@ -11,16 +14,20 @@
   "audio.hide": "Zoo ụda",
   "bundle_column_error.retry": "Nwaa ọzọ",
   "bundle_column_error.routing.title": "404",
+  "bundle_modal_error.close": "Mechie",
   "bundle_modal_error.retry": "Nwaa ọzọ",
   "column.about": "Maka",
   "column.blocks": "Ojiarụ egbochiri",
   "column.bookmarks": "Ebenrụtụakā",
   "column.home": "Be",
+  "column.lists": "Ndepụta",
   "column.pins": "Pinned post",
+  "column_header.pin": "Gbado na profaịlụ gị",
   "column_subheading.settings": "Mwube",
   "community.column_settings.media_only": "Media only",
   "compose.language.change": "Gbanwee asụsụ",
   "compose.language.search": "Chọọ asụsụ...",
+  "compose.published.open": "Mepe",
   "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
   "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
   "compose_form.placeholder": "What is on your mind?",
@@ -32,7 +39,10 @@
   "confirmations.delete.message": "Are you sure you want to delete this status?",
   "confirmations.delete_list.confirm": "Hichapụ",
   "confirmations.domain_block.confirm": "Hide entire domain",
+  "confirmations.edit.confirm": "Dezie",
+  "confirmations.mute.confirm": "Mee ogbi",
   "confirmations.reply.confirm": "Zaa",
+  "confirmations.unfollow.confirm": "Kwụsị iso",
   "conversation.delete": "Hichapụ nkata",
   "dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
   "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
@@ -76,6 +86,7 @@
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
   "lists.delete": "Hichapụ ndepụta",
+  "lists.edit": "Dezie ndepụta",
   "lists.subheading": "Ndepụta gị",
   "loading_indicator.label": "Na-adọnye...",
   "navigation_bar.about": "Maka",
@@ -100,20 +111,27 @@
   "privacy.change": "Adjust status privacy",
   "privacy.direct.short": "Direct",
   "privacy.private.short": "Followers-only",
+  "relative_time.full.just_now": "kịta",
   "relative_time.just_now": "kịta",
   "relative_time.today": "taa",
   "reply_indicator.cancel": "Kagbuo",
   "report.categories.other": "Ọzọ",
+  "report.categories.spam": "Nzipụ Ozièlètrọniìk Nkeāchọghị",
+  "report.mute": "Mee ogbi",
   "report.placeholder": "Type or paste additional comments",
   "report.submit": "Submit report",
   "report.target": "Report {target}",
   "report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached",
+  "report_notification.categories.other": "Ọzọ",
+  "search.placeholder": "Chọọ",
   "server_banner.active_users": "ojiarụ dị ìrè",
+  "server_banner.learn_more": "Mụtakwuo",
   "sign_in_banner.sign_in": "Sign in",
   "status.admin_status": "Open this status in the moderation interface",
   "status.bookmark": "Kee ebenrụtụakā",
   "status.copy": "Copy link to status",
   "status.delete": "Hichapụ",
+  "status.edit": "Dezie",
   "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
   "status.open": "Expand this status",
   "status.remove_bookmark": "Wepu ebenrụtụakā",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 72a57c8e3..4cd7228c8 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -586,8 +586,8 @@
   "search.no_recent_searches": "検索履歴はありません",
   "search.placeholder": "検索",
   "search.quick_action.account_search": "{x}に該当するプロフィール",
-  "search.quick_action.go_to_account": "{x}のプロフィールを見る",
-  "search.quick_action.go_to_hashtag": "{x}に該当するハッシュタグ",
+  "search.quick_action.go_to_account": "プロフィール {x} を見る",
+  "search.quick_action.go_to_hashtag": "ハッシュタグ {x} を見る",
   "search.quick_action.open_url": "MastodonでURLを開く",
   "search.quick_action.status_search": "{x}に該当する投稿",
   "search.search_or_paste": "検索またはURLを入力",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 269063b7c..574d8e211 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "검색 결과",
   "emoji_button.symbols": "기호",
   "emoji_button.travel": "여행과 장소",
+  "empty_column.account_hides_collections": "이 사용자는 이 정보를 사용할 수 없도록 설정했습니다",
   "empty_column.account_suspended": "계정 정지됨",
   "empty_column.account_timeline": "이곳에는 게시물이 없습니다!",
   "empty_column.account_unavailable": "프로필 사용 불가",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index a1ab53131..75f4a239e 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -15,7 +15,7 @@
   "account.add_or_remove_from_list": "Pridėti arba ištrinti iš sąrašų",
   "account.badges.bot": "Automatizuotas",
   "account.badges.group": "Grupė",
-  "account.block": "Užblokuoti @{name}",
+  "account.block": "Blokuoti @{name}",
   "account.block_domain": "Blokuoti domeną {domain}",
   "account.block_short": "Blokuoti",
   "account.blocked": "Užblokuota",
@@ -25,11 +25,20 @@
   "account.disable_notifications": "Nustoti man pranešti, kai @{name} paskelbia",
   "account.domain_blocked": "Užblokuotas domenas",
   "account.edit_profile": "Redaguoti profilį",
+  "account.enable_notifications": "Pranešti man, kai @{name} paskelbia",
+  "account.featured_tags.last_status_at": "Paskutinį kartą paskelbta {date}",
+  "account.featured_tags.last_status_never": "Nėra įrašų",
   "account.follow": "Sekti",
-  "account.follows_you": "Seka jus",
+  "account.followers": "Sekėjai",
+  "account.followers.empty": "Šio naudotojo dar niekas neseka.",
+  "account.followers_counter": "{count, plural, one {{counter} sekėjas (-a)} few {{counter} sekėjai} many {{counter} sekėjo} other {{counter} sekėjų}}",
+  "account.following": "Seka",
+  "account.follows.empty": "Šis naudotojas (-a) dar nieko neseka.",
+  "account.follows_you": "Seka tave",
   "account.go_to_profile": "Eiti į profilį",
   "account.in_memoriam": "Atminimui.",
   "account.joined_short": "Prisijungė",
+  "account.languages": "Keisti prenumeruojamas kalbas",
   "account.locked_info": "Šios paskyros privatumo būsena nustatyta kaip užrakinta. Savininkas (-ė) rankiniu būdu peržiūri, kas gali sekti.",
   "account.media": "Medija",
   "account.mute": "Užtildyti @{name}",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 449fdfb1a..7421c780f 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Søkeresultat",
   "emoji_button.symbols": "Symboler",
   "emoji_button.travel": "Reise & steder",
+  "empty_column.account_hides_collections": "Denne brukeren har valgt å ikke gjøre denne informasjonen tilgjengelig",
   "empty_column.account_suspended": "Kontoen er suspendert",
   "empty_column.account_timeline": "Ingen innlegg her!",
   "empty_column.account_unavailable": "Profilen er utilgjengelig",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index dfb464dbb..9c09e2d71 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Resultado da pesquisa",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viagem e Lugares",
+  "empty_column.account_hides_collections": "A pessoa optou por não disponibilizar esta informação",
   "empty_column.account_suspended": "Conta suspensa",
   "empty_column.account_timeline": "Nada aqui.",
   "empty_column.account_unavailable": "Perfil indisponível",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index f33dd2fb8..988bec9b0 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Resultados da pesquisa",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viagens & Lugares",
+  "empty_column.account_hides_collections": "Este utilizador escolheu não disponibilizar esta informação",
   "empty_column.account_suspended": "Conta suspensa",
   "empty_column.account_timeline": "Sem publicações por aqui!",
   "empty_column.account_unavailable": "Perfil indisponível",
diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json
index 3a7e75b91..aa229eceb 100644
--- a/app/javascript/mastodon/locales/vi.json
+++ b/app/javascript/mastodon/locales/vi.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Kết quả tìm kiếm",
   "emoji_button.symbols": "Biểu tượng",
   "emoji_button.travel": "Du lịch",
+  "empty_column.account_hides_collections": "Người này đã chọn ẩn thông tin",
   "empty_column.account_suspended": "Tài khoản vô hiệu hóa",
   "empty_column.account_timeline": "Chưa có tút nào!",
   "empty_column.account_unavailable": "Tài khoản bị đình chỉ",
diff --git a/config/locales/doorkeeper.lt.yml b/config/locales/doorkeeper.lt.yml
index 36f31e39e..1012160f6 100644
--- a/config/locales/doorkeeper.lt.yml
+++ b/config/locales/doorkeeper.lt.yml
@@ -1,18 +1,143 @@
 ---
 lt:
+  activerecord:
+    attributes:
+      doorkeeper/application:
+        name: Programėlės pavadinimas
+        redirect_uri: Peradresavimo URI
+        scopes: Aprėptys
+        website: Programėlės svetainė
+    errors:
+      models:
+        doorkeeper/application:
+          attributes:
+            redirect_uri:
+              fragment_present: negali turėti fragmento.
+              invalid_uri: turi būti tinkamas URI.
+              relative_uri: turi būti absoliutus URI.
+              secured_uri: turi būti HTTPS/SSL URI.
   doorkeeper:
+    applications:
+      buttons:
+        authorize: Įgalinti
+        cancel: Atšaukti
+        destroy: Sunaikinti
+        edit: Redaguoti
+        submit: Pateikti
+      confirmations:
+        destroy: Ar esi įsitikinęs (-usi)?
+      edit:
+        title: Redaguoti programėlę
+      form:
+        error: Ups! Patikrink, ar formoje nėra galimų klaidų.
+      help:
+        native_redirect_uri: Naudoti %{native_redirect_uri} vietiniams bandymams
+        redirect_uri: Naudoti po vieną eilutę kiekvienam URI
+        scopes: Atskirk aprėptis tarpais. Palik tuščią, jei nori naudoti numatytąsias aprėtis.
+      index:
+        application: Programėlė
+        callback_url: Atgalinis URL
+        delete: Ištrinti
+        empty: Neturi jokių programėlių.
+        name: Pavadinimas
+        new: Nauja programėlė
+        scopes: Aprėptys
+        show: Rodyti
+        title: Tavo programėlės
+      new:
+        title: Nauja programėlė
+      show:
+        actions: Veiksmai
+        application_id: Kliento raktas
+        callback_urls: Atgalinių URL adresų
+        scopes: Aprėptys
+        secret: Kliento paslaptis
+        title: 'Programėlė: %{name}'
     authorizations:
+      buttons:
+        authorize: Įgalinti
+        deny: Atmesti
       error:
         title: Įvyko klaida.
       new:
         prompt_html: "%{client_name} norėtų gauti leidimą prieigos prie tavo paskyros. Tai trečiosios šalies programėlė. <strong>Jei ja nepasitiki, neturėtum jai leisti.</strong>"
+        review_permissions: Peržiūrėti leidimus
+        title: Reikalingas įgaliojimas
+      show:
+        title: Nukopijuok šį įgaliojimo kodą ir įklijuok jį į programėlę.
     authorized_applications:
+      buttons:
+        revoke: Naikinti
+      confirmations:
+        revoke: Ar esi įsitikinęs (-usi)?
       index:
-        title: Tavo leidžiamos programėlės
+        authorized_at: Įgaliota %{date}
+        description_html: Tai programėlės, kurios gali pasiekti tavo paskyrą naudojant API. Jei čia yra programėlių, kurių neatpažįsti, arba jei programėlė elgiasi netinkamai, gali panaikinti jos prieigą.
+        last_used_at: Paskutinį kartą naudota %{date}
+        never_used: Niekada nenaudotas
+        scopes: Leidimai
+        superapp: Vidinis
+        title: Tavo įgaliotos programėlės
+    errors:
+      messages:
+        access_denied: Išteklių savininkas (-ė) arba įgaliojimų serveris atmetė užklausą.
+        credential_flow_not_configured: Išteklių savininko slaptažodžio kredencialų srautas nepavyko, nes Doorkeeper.configure.resource_owner_from_credentials nėra nesukonfigūruotas.
+        invalid_client: Kliento tapatybės nustatymas nepavyko dėl nežinomo kliento, neįtraukto kliento tapatybės nustatymo arba nepalaikomo tapatybės nustatymo metodo.
+        invalid_grant: Pateiktas įgaliojimas yra netinkamas, pasibaigęs, panaikintas, neatitinka įgaliojimo užklausoje naudoto nukreipimo URI arba buvo išduotas kitam klientui.
+        invalid_redirect_uri: Nukreipimo uri įtrauktas yra netinkamas.
+        invalid_request:
+          missing_param: 'Trūksta privalomo parametro: %{value}.'
+          request_not_authorized: Užklausą reikia įgalioti. Reikalingo parametro užklausai įgalioti trūksta arba jis netinkamas.
+          unknown: Užklausoje trūksta privalomo parametro, turi nepalaikomą parametro reikšmę arba yra kitaip netinkamai suformuota.
+        invalid_resource_owner: Pateikti išteklių savininko įgaliojimai yra netinkami arba išteklių savininko negalima surasti.
+        invalid_scope: Užklausos aprėptis yra netinkama, nežinoma arba netinkamai suformuota.
+        invalid_token:
+          expired: Baigėsi prieigos rakto galiojimas.
+          revoked: Prieigos raktas buvo panaikintas.
+          unknown: Prieigos raktas yra netinkamas.
+        resource_owner_authenticator_not_configured: Išteklių savininko suradimas nepavyko dėl to, kad Doorkeeper.configure.resource_owner_authenticator nėra sukonfigūruotas.
+        server_error: Įgaliojimų serveris susidūrė su netikėta sąlyga, dėl kurios negalėjo užpildyti užklausos.
+        temporarily_unavailable: Įgaliojimų serveris šiuo metu negali apdoroti užklausos dėl laikinos serverio perkrovos arba techninės priežiūros.
+        unauthorized_client: Klientas nėra įgaliotas atlikti šią užklausą šiuo metodu.
+        unsupported_grant_type: Įgaliojimų suteikimo tipas nepalaikomas įgaliojimų serveryje.
+        unsupported_response_type: Įgaliojimų serveris nepalaiko šio atsako tipo.
+    flash:
+      applications:
+        create:
+          notice: Programėlė sukurta.
+        destroy:
+          notice: Programėlė ištrinta.
+        update:
+          notice: Programėlė atnaujinta.
+      authorized_applications:
+        destroy:
+          notice: Programėlė panaikinta.
     grouped_scopes:
+      access:
+        read: Tik skaitymo prieiga
+        read/write: Skaitymo ir rašymo prieiga
+        write: Tik rašymo prieiga
       title:
+        accounts: Paskyros
+        admin/accounts: Paskyrų administravimas
+        admin/all: Visi administraciniai funkcijos
+        admin/reports: Ataskaitų administravimas
+        all: Pilna prieiga prie tavo Mastodon paskyros
         blocks: Blokavimai
+        bookmarks: Žymės
+        conversations: Pokalbiai
+        crypto: Galo iki galo užšifravimas
+        favourites: Mėgstami
+        filters: Filtrai
         follow: Sekimai, nutildymai ir blokavimai
+        follows: Sekimai
+        lists: Sąrašai
+        media: Medijos priedai
+        mutes: Užtildymai
+        notifications: Pranešimai
+        push: Stumdomieji pranešimai
+        reports: Ataskaitos
+        search: Paieška
         statuses: Įrašai
     layouts:
       admin:
@@ -37,6 +162,7 @@ lt:
       admin:write:domain_blocks: atlikti prižiūrėjimo veiksmus su domenų blokavimais
       admin:write:email_domain_blocks: atlikti prižiūrėjimo veiksmus su el. laiško domenų blokavimais
       admin:write:ip_blocks: atlikti prižiūrėjimo veiksmus su IP blokavimais
+      admin:write:reports: atlikti paskyrų prižiūrėjimo veiksmus atsakaitams
       crypto: naudoti galo iki galo šifravimą
       follow: modifikuoti paskyros santykius
       push: gauti tavo stumiamuosius pranešimus
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 8e076878a..8569d2e37 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -131,7 +131,7 @@ fa:
       reset_password: بازنشانی گذرواژه
       resubscribe: اشتراک دوباره
       role: نقش
-      search: جستجو
+      search: جست‌وجو
       search_same_email_domain: دیگر کاربران با دامنهٔ رایانامهٔ یکسان
       search_same_ip: دیگر کاربران با IP یکسان
       security: امنیت
@@ -386,6 +386,10 @@ fa:
       confirm_suspension:
         cancel: لغو
         confirm: تعلیق
+        permanent_action: برگرداندن تعلیق هیچ داده یا ارتباطی را برنخواهد گرداند.
+        preamble_html: در حال تعلیق <strong>%{domain}</strong> و همهٔ زیردامنه‌هایش هستید.
+        remove_all_data: این کار همهٔ داده‌های نمایه، محتوا و رسانه‌های حساب‌های این دامنه را از کارسازتان برمی‌دارد.
+        stop_communication: کارسازتان دیگر با این کارسازها ارتباط برقرار نخواهد کرد.
         title: تأیید انسداد دامنه برای %{domain}
       created_msg: مسدودسازی دامنه در حال پردازش است
       destroyed_msg: انسداد دامنه واگردانده شد
@@ -1219,7 +1223,7 @@ fa:
       followers: این کار همهٔ پیگیران شما را از حساب فعلی به حساب تازه منتقل خواهد کرد
       only_redirect_html: شما همچنین می‌توانید حساب خود را <a href="%{path}">به یک حساب دیگر اشاره دهید</a>.
       other_data: هیچ دادهٔ دیگری خودبه‌خود منتقل نخواهد شد
-      redirect: نمایهٔ حساب فعلی شما به حساب تازه اشاره خواهد کرد و خودش در نتیجهٔ جستجوها ظاهر نخواهد شد
+      redirect: نمایهٔ حساب کنونیتان به حساب تازه اشاره خواهد کرد و از جست‌وجوها حذف خواهد شد
   moderation:
     title: مدیریت کاربران
   move_handler:
diff --git a/config/locales/ig.yml b/config/locales/ig.yml
index 7c264f0d7..9db771fdc 100644
--- a/config/locales/ig.yml
+++ b/config/locales/ig.yml
@@ -1 +1,5 @@
+---
 ig:
+  filters:
+    contexts:
+      home: Ụlọ na ndepụta
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index ca438f53d..3318b690a 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -1023,7 +1023,7 @@ ja:
       hint_html: もう一つだけ!あなたが人間であることを確認する必要があります(スパムを防ぐためです!)。 以下のCAPTCHAを解き、「続ける」をクリックします。
       title: セキュリティチェック
     confirmations:
-      awaiting_review: メールアドレスは確認済みです。%{domain} のモデレーターによりアカウント登録の審査が完了すると、メールでお知らせします。
+      awaiting_review: メールアドレスが確認できました。%{domain} のスタッフが登録審査を行います。承認されたらメールでお知らせします!
       awaiting_review_title: 登録の審査待ちです
       clicking_this_link: このリンクを押す
       login_link: ログイン
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 530cf6a44..72eafc8fb 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -1689,8 +1689,8 @@ ko:
     keep_polls_hint: 설문을 삭제하지 않았음
     keep_self_bookmark: 북마크한 게시물 유지
     keep_self_bookmark_hint: 북마크한 본인의 게시물을 삭제하지 않습니다
-    keep_self_fav: 마음에 들어한 게시물 유지
-    keep_self_fav_hint: 내 스스로 마음에  들어한 본인의 게시물을 삭제하지 않습니다
+    keep_self_fav: 내가 좋아요한 게시물 유지
+    keep_self_fav_hint: 스스로 좋아요를 누른 본인의 게시물을 삭제하지 않습니다
     min_age:
       '1209600': 2 주
       '15778476': 6 개월
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index 551824e80..acd6b206e 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -1041,6 +1041,14 @@ nn:
       hint_html: Berre ein ting til! Vi må bekrefte at du er et menneske (så vi kan halde spam ute!). Løys CAPTCHA-en nedanfor og klikk "Fortsett".
       title: Sikkerheitssjekk
     confirmations:
+      awaiting_review: Din e-post adresse er bekreftet! %{domain} ansatte gjennomgår nå registreringen din. Du vil motta en e-post hvis de godkjenner din konto!
+      awaiting_review_title: Din registrering blir vurdert
+      clicking_this_link: klikke på denne lenken
+      login_link: logg inn
+      proceed_to_login_html: Du kan nå fortsette til %{login_link}.
+      redirect_to_app_html: Du burde bli omdirigert til <strong>%{app_name}</strong> -appen. Hvis det ikke skjedde, kan du prøve %{clicking_this_link} eller manuelt gå tilbake til appen.
+      registration_complete: Registreringen på %{domain} er nå fullført!
+      welcome_title: Velkommen, %{name}!
       wrong_email_hint: Viss epostadressa er feil, kan du endra ho i kontoinnstillingane.
     delete_account: Slett konto
     delete_account_html: Om du vil sletta kontoen din, kan du <a href="%{path}">gå hit</a>. Du vert spurd etter stadfesting.
diff --git a/config/locales/no.yml b/config/locales/no.yml
index 1abfbdb97..75085fa5a 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -772,6 +772,11 @@
           approved: Godkjenning kreves for påmelding
           none: Ingen kan melde seg inn
           open: Hvem som helst kan melde seg inn
+      security:
+        authorized_fetch: Krev autentisering fra fødererte servere
+        authorized_fetch_hint: Krav om godkjenning fra fødererte servere muliggjør strengere håndhevelse av blokker på både brukernivå og servernivå. Dette går imidlertid på bekostning av en ytelsesstraff, reduserer rekkevidden til svarene dine og kan introdusere kompatibilitetsproblemer med enkelte fødererte tjenester. I tillegg vil dette ikke hindre dedikerte aktører i å hente dine offentlige innlegg og kontoer.
+        authorized_fetch_overridden_hint: Du kan for øyeblikket ikke endre denne innstillingen fordi den overstyres av en miljøvariabel.
+        federation_authentication: Håndheving av føderasjonsautentisering
       title: Serverinnstillinger
     site_uploads:
       delete: Slett den opplastede filen
@@ -1036,6 +1041,14 @@
       hint_html: Bare en ting til! Vi må bekrefte at du er et menneske (dette er slik at vi kan holde spam ute!). Løs CAPTCHA nedenfor og klikk "Fortsett".
       title: Sikkerhetskontroll
     confirmations:
+      awaiting_review: Din e-post adresse er bekreftet! %{domain} ansatte gjennomgår nå registreringen din. Du vil motta en e-post hvis de godkjenner din konto!
+      awaiting_review_title: Din registrering blir vurdert
+      clicking_this_link: klikke på denne lenken
+      login_link: logg inn
+      proceed_to_login_html: Du kan nå fortsette til %{login_link}.
+      redirect_to_app_html: Du burde bli omdirigert til <strong>%{app_name}</strong> -appen. Hvis det ikke skjedde, kan du prøve %{clicking_this_link} eller manuelt gå tilbake til appen.
+      registration_complete: Registreringen på %{domain} er nå fullført!
+      welcome_title: Velkommen, %{name}!
       wrong_email_hint: Hvis e-postadressen ikke er riktig, kan du endre den i kontoinnstillingene.
     delete_account: Slett konto
     delete_account_html: Hvis du ønsker å slette kontoen din, kan du <a href="%{path}">gå hit</a>. Du vil bli spurt om bekreftelse.
@@ -1739,6 +1752,10 @@
       month: "%b %Y"
       time: "%H:%M"
       with_time_zone: "%-d. %b %Y, %H:%M %Z"
+  translation:
+    errors:
+      quota_exceeded: Den serveromfattende brukskvoten for oversettelsestjenesten er overskredet.
+      too_many_requests: Det har nylig vært for mange forespørsler til oversettelsestjenesten.
   two_factor_authentication:
     add: Legg til
     disable: Skru av
diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml
index 581a66807..13ea8a0c4 100644
--- a/config/locales/simple_form.he.yml
+++ b/config/locales/simple_form.he.yml
@@ -5,7 +5,7 @@ he:
       account:
         discoverable: הפוסטים והפרופיל שלך עשויים להיות מוצגים או מומלצים באזורים שונים באתר וייתכן שהפרופיל שלך יוצע למשתמשים אחרים.
         display_name: שמך המלא או שם הכיף שלך.
-        fields: עמוד הבית שלך, כינויי גוף, גיל, וכל מידע אחר לפי העדפתך האישית.
+        fields: עמוד הבית שלך, לשון הפנייה, גיל, וכל מידע אחר לפי העדפתך האישית.
         indexable: ההודעות הפומביות שלך עשויות להופיע בתוצאות חיפוש במסטודון. אחרים שהדהדו, חיבבו או ענו להודעות האלו יוכלו למצוא אותן בחיפוש בכל מקרה.
         note: 'ניתן לאזכר @אחרים או #תגיות.'
         show_collections: אנשים יוכלו לדפדף בין העוקבים והנעקבים שלך. אנשים שאת.ה עוקב.ת אחריהם יראו את המעקב אחריהם כרגיל.
diff --git a/config/locales/simple_form.no.yml b/config/locales/simple_form.no.yml
index a9d5465f3..ca2020e21 100644
--- a/config/locales/simple_form.no.yml
+++ b/config/locales/simple_form.no.yml
@@ -323,6 +323,7 @@
         url: Endepunkt lenke
     'no': Nei
     not_recommended: Ikke anbefalt
+    overridden: Overstyrt
     recommended: Anbefalt
     required:
       mark: "*"

From 5bca5c4c5b036c922144e713e776727cd06615bf Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 13 Nov 2023 14:24:14 +0100
Subject: [PATCH 06/63] Update formatjs monorepo (#27823)

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

diff --git a/yarn.lock b/yarn.lock
index 0058a6837..c1e9ec9a4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1800,13 +1800,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/ecma402-abstract@npm:1.17.3":
-  version: 1.17.3
-  resolution: "@formatjs/ecma402-abstract@npm:1.17.3"
+"@formatjs/ecma402-abstract@npm:1.17.4":
+  version: 1.17.4
+  resolution: "@formatjs/ecma402-abstract@npm:1.17.4"
   dependencies:
-    "@formatjs/intl-localematcher": "npm:0.5.0"
+    "@formatjs/intl-localematcher": "npm:0.5.1"
     tslib: "npm:^2.4.0"
-  checksum: 00eb87301272d22dcdb0f2de74df22fa96c0062a9c1b1d718da5b8d8a4d9841aa59f909da624bd7f99db97d19c8a0338e511ad57a01d6fcfc02e67c6627035ce
+  checksum: c24bf58cd3152ad64a29dfab185d1fde91e44423aabb041f332216b37a23256618efee1e252c0015e735bc688708ee279348e2a4a67a77f6cf918028848ef071
   languageName: node
   linkType: hard
 
@@ -1830,14 +1830,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/icu-messageformat-parser@npm:2.7.1":
-  version: 2.7.1
-  resolution: "@formatjs/icu-messageformat-parser@npm:2.7.1"
+"@formatjs/icu-messageformat-parser@npm:2.7.2":
+  version: 2.7.2
+  resolution: "@formatjs/icu-messageformat-parser@npm:2.7.2"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.3"
-    "@formatjs/icu-skeleton-parser": "npm:1.6.3"
+    "@formatjs/ecma402-abstract": "npm:1.17.4"
+    "@formatjs/icu-skeleton-parser": "npm:1.6.4"
     tslib: "npm:^2.4.0"
-  checksum: 4bfc01538ef385e8bcc7c86f08b20ab87ce22c355cf3ea322c8f8d941260e00484f10aa43266f5eea096879f782fa6cdb8fe649dcb13d2745868beea36316ecb
+  checksum: c7a2f7daecec9ba36acda2c5b7ef21f515883b886d4d9965d83c93bc55fc604f56c1097d4641608633c32917aaa0b9b0c65c0d162723428249dc29271270a064
   languageName: node
   linkType: hard
 
@@ -1851,35 +1851,35 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/icu-skeleton-parser@npm:1.6.3":
-  version: 1.6.3
-  resolution: "@formatjs/icu-skeleton-parser@npm:1.6.3"
+"@formatjs/icu-skeleton-parser@npm:1.6.4":
+  version: 1.6.4
+  resolution: "@formatjs/icu-skeleton-parser@npm:1.6.4"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.3"
+    "@formatjs/ecma402-abstract": "npm:1.17.4"
     tslib: "npm:^2.4.0"
-  checksum: 97a05fef16c93d9d663c5d7dadf9dcc72217d7a8afedda3a441ceedf085d45e500022dd67d3c578a1a508dd334538e4eca07129a9994c39c0c36e205e567c04b
+  checksum: 3688aad6d12fe677ef0ce3d6a3424c5bde9ed223dc49841de8dd33c547bdd2858f8bce4437fcc135048b4f92385374776ab48e39b3cc5063a45bdb1ce85ad2d4
   languageName: node
   linkType: hard
 
-"@formatjs/intl-displaynames@npm:6.6.2":
-  version: 6.6.2
-  resolution: "@formatjs/intl-displaynames@npm:6.6.2"
+"@formatjs/intl-displaynames@npm:6.6.3":
+  version: 6.6.3
+  resolution: "@formatjs/intl-displaynames@npm:6.6.3"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.3"
-    "@formatjs/intl-localematcher": "npm:0.5.0"
+    "@formatjs/ecma402-abstract": "npm:1.17.4"
+    "@formatjs/intl-localematcher": "npm:0.5.1"
     tslib: "npm:^2.4.0"
-  checksum: e0dbf749b19b93e009d487e70f4926bac4f94b0a4e60b435d4f925f686a54a869fd871902f6af322843437b8f80725981136be20cfb145c7d7c3b9826bfc43e3
+  checksum: b0520cb744a51290fbcde80860f39ed9c9df9b81beae98986e1fc089ef635f7699c750631fa42a559f3678d1dd02b14904614e70360477d18e68d3eba6592390
   languageName: node
   linkType: hard
 
-"@formatjs/intl-listformat@npm:7.5.1":
-  version: 7.5.1
-  resolution: "@formatjs/intl-listformat@npm:7.5.1"
+"@formatjs/intl-listformat@npm:7.5.2":
+  version: 7.5.2
+  resolution: "@formatjs/intl-listformat@npm:7.5.2"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.3"
-    "@formatjs/intl-localematcher": "npm:0.5.0"
+    "@formatjs/ecma402-abstract": "npm:1.17.4"
+    "@formatjs/intl-localematcher": "npm:0.5.1"
     tslib: "npm:^2.4.0"
-  checksum: bb972ad9078db197699452a2958dea74cfd62fcea84015a8786ac8e399738c9fe08c3c71dd1ea34ff46f24783daa20d26d5af4b5d1df5ee0ca9af7530788d2a5
+  checksum: 54fa03da4ea45504681d6d87d72d1cac574809ce43f965fa4b845e83be3072d92324c58cec57ad386827087fb1d6ecae438d29576f30176bf52eb212e454bce2
   languageName: node
   linkType: hard
 
@@ -1892,43 +1892,43 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/intl-localematcher@npm:0.5.0":
-  version: 0.5.0
-  resolution: "@formatjs/intl-localematcher@npm:0.5.0"
+"@formatjs/intl-localematcher@npm:0.5.1":
+  version: 0.5.1
+  resolution: "@formatjs/intl-localematcher@npm:0.5.1"
   dependencies:
     tslib: "npm:^2.4.0"
-  checksum: adc82cad4c37dfff2ba1d00216e2056ce4c91b85fd39c60474849ccd74ec1262a8203d460d0ee863b71eb399297c3e087f5ccbbf3a874d682b021e6e3fd2c943
+  checksum: 2282db3e623d3f65681b6a2a2dbffc4f948b8411789f51af1b221610105f809ebec7f58f9afd5008e72c62ed5524c8c321f85c78cab0cffb632e20c0064b701b
   languageName: node
   linkType: hard
 
 "@formatjs/intl-pluralrules@npm:^5.2.2":
-  version: 5.2.8
-  resolution: "@formatjs/intl-pluralrules@npm:5.2.8"
+  version: 5.2.9
+  resolution: "@formatjs/intl-pluralrules@npm:5.2.9"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.3"
-    "@formatjs/intl-localematcher": "npm:0.5.0"
+    "@formatjs/ecma402-abstract": "npm:1.17.4"
+    "@formatjs/intl-localematcher": "npm:0.5.1"
     tslib: "npm:^2.4.0"
-  checksum: cc5826774829a9c424b05010c398192aef93d89cca0144ebdc91df29032b808b235e7dde8def27887c5cddb695affd518993d728a908911897599d67d10e1954
+  checksum: a6ca5c498ce542facacf8ce8640d4ba068f9119b758547a23614b50611eb385a46abd386ff88fa423211355ec463cf102c2c908b74f6e23a5bc9e2a23873dc29
   languageName: node
   linkType: hard
 
-"@formatjs/intl@npm:2.9.6":
-  version: 2.9.6
-  resolution: "@formatjs/intl@npm:2.9.6"
+"@formatjs/intl@npm:2.9.8":
+  version: 2.9.8
+  resolution: "@formatjs/intl@npm:2.9.8"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.3"
+    "@formatjs/ecma402-abstract": "npm:1.17.4"
     "@formatjs/fast-memoize": "npm:2.2.0"
-    "@formatjs/icu-messageformat-parser": "npm:2.7.1"
-    "@formatjs/intl-displaynames": "npm:6.6.2"
-    "@formatjs/intl-listformat": "npm:7.5.1"
-    intl-messageformat: "npm:10.5.5"
+    "@formatjs/icu-messageformat-parser": "npm:2.7.2"
+    "@formatjs/intl-displaynames": "npm:6.6.3"
+    "@formatjs/intl-listformat": "npm:7.5.2"
+    intl-messageformat: "npm:10.5.7"
     tslib: "npm:^2.4.0"
   peerDependencies:
     typescript: 5
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 6dadbf3dfcd1534735899fb5eed3e340a547d92ca4b0c33857e7ec3a19bddbf7787d0ab2aa014f3c2d7288e81d0ed1cb3308b0ede8299fda6d12b0bd6b1ad89d
+  checksum: 6341f4bfb56a0e14373395b1232e1eeb8e64588a8c3d4614cd2b06f71d4e65dbd4a79e3a1c07e1b6c20c48e399ac2385977b01a559e1d2bd1a1d226e0eae3058
   languageName: node
   linkType: hard
 
@@ -1952,11 +1952,11 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/ts-transformer@npm:3.13.7":
-  version: 3.13.7
-  resolution: "@formatjs/ts-transformer@npm:3.13.7"
+"@formatjs/ts-transformer@npm:3.13.8":
+  version: 3.13.8
+  resolution: "@formatjs/ts-transformer@npm:3.13.8"
   dependencies:
-    "@formatjs/icu-messageformat-parser": "npm:2.7.1"
+    "@formatjs/icu-messageformat-parser": "npm:2.7.2"
     "@types/json-stable-stringify": "npm:^1.0.32"
     "@types/node": "npm:14 || 16 || 17"
     chalk: "npm:^4.0.0"
@@ -1968,7 +1968,7 @@ __metadata:
   peerDependenciesMeta:
     ts-jest:
       optional: true
-  checksum: 1ce528a01fa831a4a07890ae6c447c1b1cb63870c98b5f5dfbbd9f69c7e42dcd3cfd58d2717cc7214b41ddb4c3949f69525e52b6f6001d1b494573036d1478c8
+  checksum: 32b13b75732739ca016d9d654e5f40077cafa3ff2f924fbb5fd91155cd6af3292c5fee9be022bb224fb69d2ab60ed9cdda49ee83fbf9e1e8de470ee33ceae4f3
   languageName: node
   linkType: hard
 
@@ -4735,21 +4735,21 @@ __metadata:
   linkType: hard
 
 "babel-plugin-formatjs@npm:^10.5.1":
-  version: 10.5.8
-  resolution: "babel-plugin-formatjs@npm:10.5.8"
+  version: 10.5.9
+  resolution: "babel-plugin-formatjs@npm:10.5.9"
   dependencies:
     "@babel/core": "npm:^7.10.4"
     "@babel/helper-plugin-utils": "npm:^7.10.4"
     "@babel/plugin-syntax-jsx": "npm:7"
     "@babel/traverse": "npm:7"
     "@babel/types": "npm:^7.12.11"
-    "@formatjs/icu-messageformat-parser": "npm:2.7.1"
-    "@formatjs/ts-transformer": "npm:3.13.7"
+    "@formatjs/icu-messageformat-parser": "npm:2.7.2"
+    "@formatjs/ts-transformer": "npm:3.13.8"
     "@types/babel__core": "npm:^7.1.7"
     "@types/babel__helper-plugin-utils": "npm:^7.10.0"
     "@types/babel__traverse": "npm:^7.1.7"
     tslib: "npm:^2.4.0"
-  checksum: 837f031e46a771ac6874b7dbe852ebb0919da81af97d2a6d38e47cc798502ee0e02ce247c6f0d0665108aa336ef3b15b321070d444b6d6994499b931ab178537
+  checksum: 5e4127cf7b4b9b3306a9d0ab5b029831712d22db5e2117225ce706b55d222d09a7eba1f3720fdad7a99f61843b5cba107296fc11ae00a6f0941217d9322aa02e
   languageName: node
   linkType: hard
 
@@ -9276,15 +9276,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"intl-messageformat@npm:10.5.5, intl-messageformat@npm:^10.3.5":
-  version: 10.5.5
-  resolution: "intl-messageformat@npm:10.5.5"
+"intl-messageformat@npm:10.5.7, intl-messageformat@npm:^10.3.5":
+  version: 10.5.7
+  resolution: "intl-messageformat@npm:10.5.7"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.3"
+    "@formatjs/ecma402-abstract": "npm:1.17.4"
     "@formatjs/fast-memoize": "npm:2.2.0"
-    "@formatjs/icu-messageformat-parser": "npm:2.7.1"
+    "@formatjs/icu-messageformat-parser": "npm:2.7.2"
     tslib: "npm:^2.4.0"
-  checksum: 223f48d719585b572f07012e2432c742e0acc371950c01a6fc9d7cb3e022d8486593ab8b7ed50849444035cdce119484e49f7c32b3646a8601a4d8f312395cf2
+  checksum: 7f341b3eb5b3d402167c99ca7fb98720c7ad553bed8a490b2210bd90ea9009a09f9030939307fecb111fce1454f31b4298b4f0a346999af627c86f8164a5c547
   languageName: node
   linkType: hard
 
@@ -13663,18 +13663,18 @@ __metadata:
   linkType: hard
 
 "react-intl@npm:^6.4.2":
-  version: 6.5.2
-  resolution: "react-intl@npm:6.5.2"
+  version: 6.5.4
+  resolution: "react-intl@npm:6.5.4"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.3"
-    "@formatjs/icu-messageformat-parser": "npm:2.7.1"
-    "@formatjs/intl": "npm:2.9.6"
-    "@formatjs/intl-displaynames": "npm:6.6.2"
-    "@formatjs/intl-listformat": "npm:7.5.1"
+    "@formatjs/ecma402-abstract": "npm:1.17.4"
+    "@formatjs/icu-messageformat-parser": "npm:2.7.2"
+    "@formatjs/intl": "npm:2.9.8"
+    "@formatjs/intl-displaynames": "npm:6.6.3"
+    "@formatjs/intl-listformat": "npm:7.5.2"
     "@types/hoist-non-react-statics": "npm:^3.3.1"
     "@types/react": "npm:16 || 17 || 18"
     hoist-non-react-statics: "npm:^3.3.2"
-    intl-messageformat: "npm:10.5.5"
+    intl-messageformat: "npm:10.5.7"
     tslib: "npm:^2.4.0"
   peerDependencies:
     react: ^16.6.0 || 17 || 18
@@ -13682,7 +13682,7 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 8c2cbad85c4fc3647f9498a5dab13c11490a77936dcf14cb769b5659b361e7070c7528bf0d8708eec06a6dbca93b34cccf5cc919005d33c645739865cfccb878
+  checksum: 1117a7f866b103abf88a4087f5fe8b854d9c069c69444c592f8431e7d28c9b90423f7b50e550be0f2f173b7563e943bcc9238e80f6747181f81861275f6e2ce7
   languageName: node
   linkType: hard
 

From 07a4059901e8723cd3d986a0aefb216faa42b5fe Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 13 Nov 2023 14:27:00 +0100
Subject: [PATCH 07/63] Add support for invite codes in the registration API
 (#27805)

---
 app/controllers/api/v1/accounts_controller.rb | 18 ++--
 app/controllers/api/v1/invites_controller.rb  | 30 ++++++
 .../auth/registrations_controller.rb          | 15 +--
 app/helpers/registration_helper.rb            | 21 +++++
 app/services/app_sign_up_service.rb           | 30 ++----
 config/locales/en.yml                         |  1 +
 config/routes.rb                              |  2 +
 spec/requests/invite_spec.rb                  | 27 ++++++
 spec/services/app_sign_up_service_spec.rb     | 92 ++++++++++++-------
 9 files changed, 158 insertions(+), 78 deletions(-)
 create mode 100644 app/controllers/api/v1/invites_controller.rb
 create mode 100644 app/helpers/registration_helper.rb
 create mode 100644 spec/requests/invite_spec.rb

diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index ddb94d5ca..653529316 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class Api::V1::AccountsController < Api::BaseController
+  include RegistrationHelper
+
   before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute]
   before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers]
   before_action -> { doorkeeper_authorize! :follow, :write, :'write:mutes' }, only: [:mute, :unmute]
@@ -90,18 +92,14 @@ class Api::V1::AccountsController < Api::BaseController
   end
 
   def account_params
-    params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone)
+    params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code)
+  end
+
+  def invite
+    Invite.find_by(code: params[:invite_code]) if params[:invite_code].present?
   end
 
   def check_enabled_registrations
-    forbidden if single_user_mode? || omniauth_only? || !allowed_registrations?
-  end
-
-  def allowed_registrations?
-    Setting.registrations_mode != 'none'
-  end
-
-  def omniauth_only?
-    ENV['OMNIAUTH_ONLY'] == 'true'
+    forbidden unless allowed_registration?(request.remote_ip, invite)
   end
 end
diff --git a/app/controllers/api/v1/invites_controller.rb b/app/controllers/api/v1/invites_controller.rb
new file mode 100644
index 000000000..ea17ba740
--- /dev/null
+++ b/app/controllers/api/v1/invites_controller.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class Api::V1::InvitesController < Api::BaseController
+  include RegistrationHelper
+
+  skip_before_action :require_authenticated_user!
+  skip_around_action :set_locale
+
+  before_action :set_invite
+  before_action :check_enabled_registrations!
+
+  # Override `current_user` to avoid reading session cookies
+  def current_user; end
+
+  def show
+    render json: { invite_code: params[:invite_code], instance_api_url: api_v2_instance_url }, status: 200
+  end
+
+  private
+
+  def set_invite
+    @invite = Invite.find_by!(code: params[:invite_code])
+  end
+
+  def check_enabled_registrations!
+    return render json: { error: I18n.t('invites.invalid') }, status: 401 unless @invite.valid_for_use?
+
+    raise Mastodon::NotPermittedError unless allowed_registration?(request.remote_ip, @invite)
+  end
+end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 331484f36..8be7c5f19 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 class Auth::RegistrationsController < Devise::RegistrationsController
+  include RegistrationHelper
   include RegistrationSpamConcern
 
   layout :determine_layout
@@ -82,19 +83,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   end
 
   def check_enabled_registrations
-    redirect_to root_path if single_user_mode? || omniauth_only? || !allowed_registrations? || ip_blocked?
-  end
-
-  def allowed_registrations?
-    Setting.registrations_mode != 'none' || @invite&.valid_for_use?
-  end
-
-  def omniauth_only?
-    ENV['OMNIAUTH_ONLY'] == 'true'
-  end
-
-  def ip_blocked?
-    IpBlock.where(severity: :sign_up_block).where('ip >>= ?', request.remote_ip.to_s).exists?
+    redirect_to root_path unless allowed_registration?(request.remote_ip, @invite)
   end
 
   def invite_code
diff --git a/app/helpers/registration_helper.rb b/app/helpers/registration_helper.rb
new file mode 100644
index 000000000..ef5462ac8
--- /dev/null
+++ b/app/helpers/registration_helper.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module RegistrationHelper
+  extend ActiveSupport::Concern
+
+  def allowed_registration?(remote_ip, invite)
+    !Rails.configuration.x.single_user_mode && !omniauth_only? && (registrations_open? || invite&.valid_for_use?) && !ip_blocked?(remote_ip)
+  end
+
+  def registrations_open?
+    Setting.registrations_mode != 'none'
+  end
+
+  def omniauth_only?
+    ENV['OMNIAUTH_ONLY'] == 'true'
+  end
+
+  def ip_blocked?(remote_ip)
+    IpBlock.where(severity: :sign_up_block).exists?(['ip >>= ?', remote_ip.to_s])
+  end
+end
diff --git a/app/services/app_sign_up_service.rb b/app/services/app_sign_up_service.rb
index 94547b61b..766588011 100644
--- a/app/services/app_sign_up_service.rb
+++ b/app/services/app_sign_up_service.rb
@@ -1,12 +1,14 @@
 # frozen_string_literal: true
 
 class AppSignUpService < BaseService
+  include RegistrationHelper
+
   def call(app, remote_ip, params)
     @app       = app
     @remote_ip = remote_ip
     @params    = params
 
-    raise Mastodon::NotPermittedError unless allowed_registrations?
+    raise Mastodon::NotPermittedError unless allowed_registration?(remote_ip, invite)
 
     ApplicationRecord.transaction do
       create_user!
@@ -34,8 +36,12 @@ class AppSignUpService < BaseService
     )
   end
 
+  def invite
+    Invite.find_by(code: @params[:invite_code]) if @params[:invite_code].present?
+  end
+
   def user_params
-    @params.slice(:email, :password, :agreement, :locale, :time_zone)
+    @params.slice(:email, :password, :agreement, :locale, :time_zone, :invite_code)
   end
 
   def account_params
@@ -45,24 +51,4 @@ class AppSignUpService < BaseService
   def invite_request_params
     { text: @params[:reason] }
   end
-
-  def allowed_registrations?
-    registrations_open? && !single_user_mode? && !omniauth_only? && !ip_blocked?
-  end
-
-  def registrations_open?
-    Setting.registrations_mode != 'none'
-  end
-
-  def single_user_mode?
-    Rails.configuration.x.single_user_mode
-  end
-
-  def omniauth_only?
-    ENV['OMNIAUTH_ONLY'] == 'true'
-  end
-
-  def ip_blocked?
-    IpBlock.where(severity: :sign_up_block).where('ip >>= ?', @remote_ip.to_s).exists?
-  end
 end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index c298c47d3..7319de53d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1368,6 +1368,7 @@ en:
       '86400': 1 day
     expires_in_prompt: Never
     generate: Generate invite link
+    invalid: This invite is not valid
     invited_by: 'You were invited by:'
     max_uses:
       one: 1 use
diff --git a/config/routes.rb b/config/routes.rb
index 3adda3b82..82431f6ec 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -81,6 +81,8 @@ Rails.application.routes.draw do
     resource :outbox, only: [:show], module: :activitypub
   end
 
+  get '/invite/:invite_code', constraints: ->(req) { req.format == :json }, to: 'api/v1/invites#show'
+
   devise_scope :user do
     get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite
 
diff --git a/spec/requests/invite_spec.rb b/spec/requests/invite_spec.rb
new file mode 100644
index 000000000..c44ef2419
--- /dev/null
+++ b/spec/requests/invite_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'invites' do
+  let(:invite) { Fabricate(:invite) }
+
+  context 'when requesting a JSON document' do
+    it 'returns a JSON document with expected attributes' do
+      get "/invite/#{invite.code}", headers: { 'Accept' => 'application/activity+json' }
+
+      expect(response).to have_http_status(200)
+      expect(response.media_type).to eq 'application/json'
+
+      expect(body_as_json[:invite_code]).to eq invite.code
+    end
+  end
+
+  context 'when not requesting a JSON document' do
+    it 'returns an HTML page' do
+      get "/invite/#{invite.code}"
+
+      expect(response).to have_http_status(200)
+      expect(response.media_type).to eq 'text/html'
+    end
+  end
+end
diff --git a/spec/services/app_sign_up_service_spec.rb b/spec/services/app_sign_up_service_spec.rb
index 253230496..d5946cf9b 100644
--- a/spec/services/app_sign_up_service_spec.rb
+++ b/spec/services/app_sign_up_service_spec.rb
@@ -10,46 +10,72 @@ RSpec.describe AppSignUpService, type: :service do
   let(:remote_ip) { IPAddr.new('198.0.2.1') }
 
   describe '#call' do
-    it 'returns nil when registrations are closed' do
-      tmp = Setting.registrations_mode
-      Setting.registrations_mode = 'none'
-      expect { subject.call(app, remote_ip, good_params) }.to raise_error Mastodon::NotPermittedError
-      Setting.registrations_mode = tmp
+    let(:params) { good_params }
+
+    shared_examples 'successful registration' do
+      it 'creates an unconfirmed user with access token and the app\'s scope', :aggregate_failures do
+        access_token = subject.call(app, remote_ip, params)
+        expect(access_token).to_not be_nil
+        expect(access_token.scopes.to_s).to eq 'read write'
+
+        user = User.find_by(id: access_token.resource_owner_id)
+        expect(user).to_not be_nil
+        expect(user.confirmed?).to be false
+
+        expect(user.account).to_not be_nil
+        expect(user.invite_request).to be_nil
+      end
+    end
+
+    context 'when registrations are closed' do
+      around do |example|
+        tmp = Setting.registrations_mode
+        Setting.registrations_mode = 'none'
+
+        example.run
+
+        Setting.registrations_mode = tmp
+      end
+
+      it 'raises an error', :aggregate_failures do
+        expect { subject.call(app, remote_ip, good_params) }.to raise_error Mastodon::NotPermittedError
+      end
+
+      context 'when using a valid invite' do
+        let(:params) { good_params.merge({ invite_code: invite.code }) }
+        let(:invite) { Fabricate(:invite) }
+
+        before do
+          invite.user.approve!
+        end
+
+        it_behaves_like 'successful registration'
+      end
+
+      context 'when using an invalid invite' do
+        let(:params) { good_params.merge({ invite_code: invite.code }) }
+        let(:invite) { Fabricate(:invite, uses: 1, max_uses: 1) }
+
+        it 'raises an error', :aggregate_failures do
+          expect { subject.call(app, remote_ip, params) }.to raise_error Mastodon::NotPermittedError
+        end
+      end
     end
 
     it 'raises an error when params are missing' do
       expect { subject.call(app, remote_ip, {}) }.to raise_error ActiveRecord::RecordInvalid
     end
 
-    it 'creates an unconfirmed user with access token' do
-      access_token = subject.call(app, remote_ip, good_params)
-      expect(access_token).to_not be_nil
-      user = User.find_by(id: access_token.resource_owner_id)
-      expect(user).to_not be_nil
-      expect(user.confirmed?).to be false
-    end
+    it_behaves_like 'successful registration'
 
-    it 'creates access token with the app\'s scopes' do
-      access_token = subject.call(app, remote_ip, good_params)
-      expect(access_token).to_not be_nil
-      expect(access_token.scopes.to_s).to eq 'read write'
-    end
-
-    it 'creates an account' do
-      access_token = subject.call(app, remote_ip, good_params)
-      expect(access_token).to_not be_nil
-      user = User.find_by(id: access_token.resource_owner_id)
-      expect(user).to_not be_nil
-      expect(user.account).to_not be_nil
-      expect(user.invite_request).to be_nil
-    end
-
-    it 'creates an account with invite request text' do
-      access_token = subject.call(app, remote_ip, good_params.merge(reason: 'Foo bar'))
-      expect(access_token).to_not be_nil
-      user = User.find_by(id: access_token.resource_owner_id)
-      expect(user).to_not be_nil
-      expect(user.invite_request&.text).to eq 'Foo bar'
+    context 'when given an invite request text' do
+      it 'creates an account with invite request text' do
+        access_token = subject.call(app, remote_ip, good_params.merge(reason: 'Foo bar'))
+        expect(access_token).to_not be_nil
+        user = User.find_by(id: access_token.resource_owner_id)
+        expect(user).to_not be_nil
+        expect(user.invite_request&.text).to eq 'Foo bar'
+      end
     end
   end
 end

From ed79713f3ad20a78640f113d44454bab387a2d8c Mon Sep 17 00:00:00 2001
From: pajowu <pajowu@pajowu.de>
Date: Mon, 13 Nov 2023 14:27:50 +0100
Subject: [PATCH 08/63] Fix modal content not being selectable (#27813)

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

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index ae8e23f53..c8cfe46a8 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -5226,6 +5226,7 @@ a.status-card {
 
 .modal-root__modal {
   pointer-events: auto;
+  user-select: text;
   display: flex;
 }
 

From 0c98a9d9becec126ea9a53619045dbb1b88f555a Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 13 Nov 2023 15:01:07 +0100
Subject: [PATCH 09/63] Update devDependencies (non-major) (#25612)

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

diff --git a/yarn.lock b/yarn.lock
index c1e9ec9a4..891eb59fe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12,10 +12,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@adobe/css-tools@npm:^4.0.1":
-  version: 4.3.0
-  resolution: "@adobe/css-tools@npm:4.3.0"
-  checksum: 70401c49ab6b7d715147c3637fc6467608276253150c1a5fa933ee45d7c1472b5188e31c1760d06c8a19c5bc2b2914583daa33486d7eac1e8fc7c6dc76a57b3c
+"@adobe/css-tools@npm:^4.3.1":
+  version: 4.3.1
+  resolution: "@adobe/css-tools@npm:4.3.1"
+  checksum: 05672719b544cc0c21ae3ed0eb6349bf458e9d09457578eeeb07cf0f696469ac6417e9c9be1b129e5d6a18098a061c1db55b2275591760ef30a79822436fcbfa
   languageName: node
   linkType: hard
 
@@ -1538,29 +1538,29 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@csstools/css-parser-algorithms@npm:^2.3.0":
-  version: 2.3.1
-  resolution: "@csstools/css-parser-algorithms@npm:2.3.1"
+"@csstools/css-parser-algorithms@npm:^2.3.1":
+  version: 2.3.2
+  resolution: "@csstools/css-parser-algorithms@npm:2.3.2"
   peerDependencies:
-    "@csstools/css-tokenizer": ^2.2.0
-  checksum: 0f1688cc5de75f41af4581a0b4df994e9af90f6df2b3f962e0680c4ed8e2aa32b23fbf3ba4fdaffc09a9afcf93fcf13a9743204f179bab57a7603ce88cb635e8
+    "@csstools/css-tokenizer": ^2.2.1
+  checksum: ccae373a3ab5c10716418b69ce1f6db10a26d3a2d60b65df5fe69099afe4fb1d3192925f3c0f93c3b17c3ab1964b0f39ad2b0e97312ec4a51caa55d6b6a31672
   languageName: node
   linkType: hard
 
-"@csstools/css-tokenizer@npm:^2.1.1":
-  version: 2.2.0
-  resolution: "@csstools/css-tokenizer@npm:2.2.0"
-  checksum: 7a6178d5a148e426ea79d4b2761857daacd7cde00512b45697146228d59183b0043f9803b48be55299f5a331f07ff14477612c608f18df7550ee642467d74564
+"@csstools/css-tokenizer@npm:^2.2.0":
+  version: 2.2.1
+  resolution: "@csstools/css-tokenizer@npm:2.2.1"
+  checksum: 0c6901d291e99c567893846a47068057c2a28b3edc4219b6da589a530f55f51ddd4675f906f707b393bfe7a508ab2604bf3f75708f064db857bb277636bd5a44
   languageName: node
   linkType: hard
 
-"@csstools/media-query-list-parser@npm:^2.1.2":
-  version: 2.1.4
-  resolution: "@csstools/media-query-list-parser@npm:2.1.4"
+"@csstools/media-query-list-parser@npm:^2.1.4":
+  version: 2.1.5
+  resolution: "@csstools/media-query-list-parser@npm:2.1.5"
   peerDependencies:
-    "@csstools/css-parser-algorithms": ^2.3.1
-    "@csstools/css-tokenizer": ^2.2.0
-  checksum: a796ebc8df7d1c8bc2bb71f152b6bedd540bd6679a33a50c1d99f8c383a24d79e9cb8fa59b2ad8f39da12c9d4501fd799caa16da542c23b8dff8257b0b5a0ea7
+    "@csstools/css-parser-algorithms": ^2.3.2
+    "@csstools/css-tokenizer": ^2.2.1
+  checksum: ae0692c6f92cdc82053291c7a50028b692094dfed795f0259571c5eb40f4b3fa580182ac3701e56c2834e40a62a122ea6639299e43ae88b3a835ae4c869a1a12
   languageName: node
   linkType: hard
 
@@ -1777,16 +1777,16 @@ __metadata:
   linkType: hard
 
 "@formatjs/cli@npm:^6.1.1":
-  version: 6.1.3
-  resolution: "@formatjs/cli@npm:6.1.3"
+  version: 6.2.3
+  resolution: "@formatjs/cli@npm:6.2.3"
   peerDependencies:
-    "@vue/compiler-sfc": ^3.2.34
+    vue: ^3.3.4
   peerDependenciesMeta:
-    "@vue/compiler-sfc":
+    vue:
       optional: true
   bin:
     formatjs: bin/formatjs
-  checksum: d7f069b4813c7c1a4fd7e16906da6bc024553c5df3d3ebb0b3da12ec23bb763d184c0424c562bc7ef89d62a963dadd114dd5acb5acec8f0c9160a36bb79f05c2
+  checksum: 91eada7676333e2e647cbfbf9c0da88e4ca52e7b486dca73a7299594b0b0dea99de00e1b4110fac993633feb4bf5f26c97885b1a870dfd0ef95688d7f3234a03
   languageName: node
   linkType: hard
 
@@ -2061,50 +2061,50 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@jest/console@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "@jest/console@npm:29.6.2"
+"@jest/console@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "@jest/console@npm:29.7.0"
   dependencies:
-    "@jest/types": "npm:^29.6.1"
+    "@jest/types": "npm:^29.6.3"
     "@types/node": "npm:*"
     chalk: "npm:^4.0.0"
-    jest-message-util: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
+    jest-message-util: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
     slash: "npm:^3.0.0"
-  checksum: cdd97d89df5e3e84ba7927ba58a297eb0eae25190575299fede876f7d09a08dc120094be08e49bf01859c54053470215194c0d9a64fc56beb735c5de4de8c5a8
+  checksum: 7be408781d0a6f657e969cbec13b540c329671819c2f57acfad0dae9dbfe2c9be859f38fe99b35dba9ff1536937dc6ddc69fdcd2794812fa3c647a1619797f6c
   languageName: node
   linkType: hard
 
-"@jest/core@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "@jest/core@npm:29.6.2"
+"@jest/core@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "@jest/core@npm:29.7.0"
   dependencies:
-    "@jest/console": "npm:^29.6.2"
-    "@jest/reporters": "npm:^29.6.2"
-    "@jest/test-result": "npm:^29.6.2"
-    "@jest/transform": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/console": "npm:^29.7.0"
+    "@jest/reporters": "npm:^29.7.0"
+    "@jest/test-result": "npm:^29.7.0"
+    "@jest/transform": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     "@types/node": "npm:*"
     ansi-escapes: "npm:^4.2.1"
     chalk: "npm:^4.0.0"
     ci-info: "npm:^3.2.0"
     exit: "npm:^0.1.2"
     graceful-fs: "npm:^4.2.9"
-    jest-changed-files: "npm:^29.5.0"
-    jest-config: "npm:^29.6.2"
-    jest-haste-map: "npm:^29.6.2"
-    jest-message-util: "npm:^29.6.2"
-    jest-regex-util: "npm:^29.4.3"
-    jest-resolve: "npm:^29.6.2"
-    jest-resolve-dependencies: "npm:^29.6.2"
-    jest-runner: "npm:^29.6.2"
-    jest-runtime: "npm:^29.6.2"
-    jest-snapshot: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
-    jest-validate: "npm:^29.6.2"
-    jest-watcher: "npm:^29.6.2"
+    jest-changed-files: "npm:^29.7.0"
+    jest-config: "npm:^29.7.0"
+    jest-haste-map: "npm:^29.7.0"
+    jest-message-util: "npm:^29.7.0"
+    jest-regex-util: "npm:^29.6.3"
+    jest-resolve: "npm:^29.7.0"
+    jest-resolve-dependencies: "npm:^29.7.0"
+    jest-runner: "npm:^29.7.0"
+    jest-runtime: "npm:^29.7.0"
+    jest-snapshot: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
+    jest-validate: "npm:^29.7.0"
+    jest-watcher: "npm:^29.7.0"
     micromatch: "npm:^4.0.4"
-    pretty-format: "npm:^29.6.2"
+    pretty-format: "npm:^29.7.0"
     slash: "npm:^3.0.0"
     strip-ansi: "npm:^6.0.0"
   peerDependencies:
@@ -2112,23 +2112,23 @@ __metadata:
   peerDependenciesMeta:
     node-notifier:
       optional: true
-  checksum: 066fc9dc66bb3785c2670280f05cb4f01a776a2d88bc6106ad4224e4a1064b1dbe3752545b4d744d6e0e3203fb0a2a102e9864104f160f2266fd30e756d9d693
+  checksum: 934f7bf73190f029ac0f96662c85cd276ec460d407baf6b0dbaec2872e157db4d55a7ee0b1c43b18874602f662b37cb973dda469a4e6d88b4e4845b521adeeb2
   languageName: node
   linkType: hard
 
-"@jest/environment@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "@jest/environment@npm:29.6.2"
+"@jest/environment@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "@jest/environment@npm:29.7.0"
   dependencies:
-    "@jest/fake-timers": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/fake-timers": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     "@types/node": "npm:*"
-    jest-mock: "npm:^29.6.2"
-  checksum: 82f040b4f729e1a3ab9e61b33e009a7b4ccf572ff94fc157e6fe8ecd267c8af53c8c02853bfe7f023d0f6bf35edf06b6bc7873efc433f335a3774b6c2445662d
+    jest-mock: "npm:^29.7.0"
+  checksum: c7b1b40c618f8baf4d00609022d2afa086d9c6acc706f303a70bb4b67275868f620ad2e1a9efc5edd418906157337cce50589a627a6400bbdf117d351b91ef86
   languageName: node
   linkType: hard
 
-"@jest/expect-utils@npm:^29.6.2, @jest/expect-utils@npm:^29.7.0":
+"@jest/expect-utils@npm:^29.7.0":
   version: 29.7.0
   resolution: "@jest/expect-utils@npm:29.7.0"
   dependencies:
@@ -2137,51 +2137,51 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@jest/expect@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "@jest/expect@npm:29.6.2"
+"@jest/expect@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "@jest/expect@npm:29.7.0"
   dependencies:
-    expect: "npm:^29.6.2"
-    jest-snapshot: "npm:^29.6.2"
-  checksum: 2cd9a5613b1bae5300dd16d76c7790d1d3b43cb55654dc2b64b202d1522bb03844f88c7bc60b72e3095c8479873ade91009ab0cb8a851842dab00d4d9fc1e3cb
+    expect: "npm:^29.7.0"
+    jest-snapshot: "npm:^29.7.0"
+  checksum: b41f193fb697d3ced134349250aed6ccea075e48c4f803159db102b826a4e473397c68c31118259868fd69a5cba70e97e1c26d2c2ff716ca39dc73a2ccec037e
   languageName: node
   linkType: hard
 
-"@jest/fake-timers@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "@jest/fake-timers@npm:29.6.2"
+"@jest/fake-timers@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "@jest/fake-timers@npm:29.7.0"
   dependencies:
-    "@jest/types": "npm:^29.6.1"
+    "@jest/types": "npm:^29.6.3"
     "@sinonjs/fake-timers": "npm:^10.0.2"
     "@types/node": "npm:*"
-    jest-message-util: "npm:^29.6.2"
-    jest-mock: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
-  checksum: 4f333b7f8f6bc8e0549e3838e68c3859de1faa3e0639f8ede2786602ec1c237f4691e7bd13649b308ddfaf3fd5aa6b75067fe34f6b6dfa9c0b570773611e0e73
+    jest-message-util: "npm:^29.7.0"
+    jest-mock: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
+  checksum: cf0a8bcda801b28dc2e2b2ba36302200ee8104a45ad7a21e6c234148932f826cb3bc57c8df3b7b815aeea0861d7b6ca6f0d4778f93b9219398ef28749e03595c
   languageName: node
   linkType: hard
 
-"@jest/globals@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "@jest/globals@npm:29.6.2"
+"@jest/globals@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "@jest/globals@npm:29.7.0"
   dependencies:
-    "@jest/environment": "npm:^29.6.2"
-    "@jest/expect": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
-    jest-mock: "npm:^29.6.2"
-  checksum: 3ee73f13d51a08b9fe3bc39305a3b9c0259a7610d89f17b9579684b80bdff3e079adc81d6aec298f5ebe07b43ba0dfdb305be2747b9dc87aa7f337bddc83fedc
+    "@jest/environment": "npm:^29.7.0"
+    "@jest/expect": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
+    jest-mock: "npm:^29.7.0"
+  checksum: a385c99396878fe6e4460c43bd7bb0a5cc52befb462cc6e7f2a3810f9e7bcce7cdeb51908fd530391ee452dc856c98baa2c5f5fa8a5b30b071d31ef7f6955cea
   languageName: node
   linkType: hard
 
-"@jest/reporters@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "@jest/reporters@npm:29.6.2"
+"@jest/reporters@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "@jest/reporters@npm:29.7.0"
   dependencies:
     "@bcoe/v8-coverage": "npm:^0.2.3"
-    "@jest/console": "npm:^29.6.2"
-    "@jest/test-result": "npm:^29.6.2"
-    "@jest/transform": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/console": "npm:^29.7.0"
+    "@jest/test-result": "npm:^29.7.0"
+    "@jest/transform": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     "@jridgewell/trace-mapping": "npm:^0.3.18"
     "@types/node": "npm:*"
     chalk: "npm:^4.0.0"
@@ -2190,13 +2190,13 @@ __metadata:
     glob: "npm:^7.1.3"
     graceful-fs: "npm:^4.2.9"
     istanbul-lib-coverage: "npm:^3.0.0"
-    istanbul-lib-instrument: "npm:^5.1.0"
+    istanbul-lib-instrument: "npm:^6.0.0"
     istanbul-lib-report: "npm:^3.0.0"
     istanbul-lib-source-maps: "npm:^4.0.0"
     istanbul-reports: "npm:^3.1.3"
-    jest-message-util: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
-    jest-worker: "npm:^29.6.2"
+    jest-message-util: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
+    jest-worker: "npm:^29.7.0"
     slash: "npm:^3.0.0"
     string-length: "npm:^4.0.1"
     strip-ansi: "npm:^6.0.0"
@@ -2206,7 +2206,7 @@ __metadata:
   peerDependenciesMeta:
     node-notifier:
       optional: true
-  checksum: d4321978208fa8c64ff4e14694508ec8fa5712801b66db62a6c58456798ffc2fab5761db24b1c3596664f2ad0862fcabc69927f0ed54cc9f219689a77cc7db4a
+  checksum: a754402a799541c6e5aff2c8160562525e2a47e7d568f01ebfc4da66522de39cbb809bbb0a841c7052e4270d79214e70aec3c169e4eae42a03bc1a8a20cb9fa2
   languageName: node
   linkType: hard
 
@@ -2219,65 +2219,65 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@jest/source-map@npm:^29.6.0":
-  version: 29.6.0
-  resolution: "@jest/source-map@npm:29.6.0"
+"@jest/source-map@npm:^29.6.3":
+  version: 29.6.3
+  resolution: "@jest/source-map@npm:29.6.3"
   dependencies:
     "@jridgewell/trace-mapping": "npm:^0.3.18"
     callsites: "npm:^3.0.0"
     graceful-fs: "npm:^4.2.9"
-  checksum: afa654e3634ad74d5f8388ccffd7ecbd745bdce7f6f0860b69c07827c3ee5bb408f52b6c3136b43157ef5874c099059484e43bd3aa391232ab27d8c330399789
+  checksum: a2f177081830a2e8ad3f2e29e20b63bd40bade294880b595acf2fc09ec74b6a9dd98f126a2baa2bf4941acd89b13a4ade5351b3885c224107083a0059b60a219
   languageName: node
   linkType: hard
 
-"@jest/test-result@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "@jest/test-result@npm:29.6.2"
+"@jest/test-result@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "@jest/test-result@npm:29.7.0"
   dependencies:
-    "@jest/console": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/console": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     "@types/istanbul-lib-coverage": "npm:^2.0.0"
     collect-v8-coverage: "npm:^1.0.0"
-  checksum: 9c373db297d6cc4b23b143592b3121da047890ba3609115e4db7b94220095d5e32a11f7179ca3dfa1ab29fa30a5e51cbdbbdf58dcd8ef3216e92e86d2aa3251c
+  checksum: 7de54090e54a674ca173470b55dc1afdee994f2d70d185c80236003efd3fa2b753fff51ffcdda8e2890244c411fd2267529d42c4a50a8303755041ee493e6a04
   languageName: node
   linkType: hard
 
-"@jest/test-sequencer@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "@jest/test-sequencer@npm:29.6.2"
+"@jest/test-sequencer@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "@jest/test-sequencer@npm:29.7.0"
   dependencies:
-    "@jest/test-result": "npm:^29.6.2"
+    "@jest/test-result": "npm:^29.7.0"
     graceful-fs: "npm:^4.2.9"
-    jest-haste-map: "npm:^29.6.2"
+    jest-haste-map: "npm:^29.7.0"
     slash: "npm:^3.0.0"
-  checksum: dc6a37f0eb93a72ce76a5100f8adb97e40bb0ab9b6f49b07476e0b83b07d1366803185b3d64da2219448312fa78a687f473f54e0c1da08efc4d2e1cb2d3c1dfb
+  checksum: 593a8c4272797bb5628984486080cbf57aed09c7cfdc0a634e8c06c38c6bef329c46c0016e84555ee55d1cd1f381518cf1890990ff845524c1123720c8c1481b
   languageName: node
   linkType: hard
 
-"@jest/transform@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "@jest/transform@npm:29.6.2"
+"@jest/transform@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "@jest/transform@npm:29.7.0"
   dependencies:
     "@babel/core": "npm:^7.11.6"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/types": "npm:^29.6.3"
     "@jridgewell/trace-mapping": "npm:^0.3.18"
     babel-plugin-istanbul: "npm:^6.1.1"
     chalk: "npm:^4.0.0"
     convert-source-map: "npm:^2.0.0"
     fast-json-stable-stringify: "npm:^2.1.0"
     graceful-fs: "npm:^4.2.9"
-    jest-haste-map: "npm:^29.6.2"
-    jest-regex-util: "npm:^29.4.3"
-    jest-util: "npm:^29.6.2"
+    jest-haste-map: "npm:^29.7.0"
+    jest-regex-util: "npm:^29.6.3"
+    jest-util: "npm:^29.7.0"
     micromatch: "npm:^4.0.4"
     pirates: "npm:^4.0.4"
     slash: "npm:^3.0.0"
     write-file-atomic: "npm:^4.0.2"
-  checksum: dce3a28ca01ce78923bb0faf7ff4fa6e64f1ec77a729a89f874b05a98c8f4408df52fc41a9e39755e9490660163ecebb58d2364530391a443fc2e4bd0e4195d6
+  checksum: 7f4a7f73dcf45dfdf280c7aa283cbac7b6e5a904813c3a93ead7e55873761fc20d5c4f0191d2019004fac6f55f061c82eb3249c2901164ad80e362e7a7ede5a6
   languageName: node
   linkType: hard
 
-"@jest/types@npm:^29.6.1, @jest/types@npm:^29.6.3":
+"@jest/types@npm:^29.6.3":
   version: 29.6.3
   resolution: "@jest/types@npm:29.6.3"
   dependencies:
@@ -2941,10 +2941,10 @@ __metadata:
   linkType: hard
 
 "@testing-library/jest-dom@npm:^6.0.0":
-  version: 6.0.0
-  resolution: "@testing-library/jest-dom@npm:6.0.0"
+  version: 6.1.4
+  resolution: "@testing-library/jest-dom@npm:6.1.4"
   dependencies:
-    "@adobe/css-tools": "npm:^4.0.1"
+    "@adobe/css-tools": "npm:^4.3.1"
     "@babel/runtime": "npm:^7.9.2"
     aria-query: "npm:^5.0.0"
     chalk: "npm:^3.0.0"
@@ -2966,13 +2966,13 @@ __metadata:
       optional: true
     vitest:
       optional: true
-  checksum: dd5a7ff79dad99b08195cab8d7b818b3de0d02f6c84b5d28d5dbca265ceb867931a0ff79da2279a2d476db1cdafb81c14255d898f0bd1ace9d0b36896368cf96
+  checksum: 2e23f120613fd8ae6d5169bbc94f1a2e4c82b07182057dc94db8ec54ebf32555833442e6c43a187e59715d83704ffb5df49ba88a71f6f32d2683f3d95ba721c7
   languageName: node
   linkType: hard
 
 "@testing-library/react@npm:^14.0.0":
-  version: 14.0.0
-  resolution: "@testing-library/react@npm:14.0.0"
+  version: 14.1.0
+  resolution: "@testing-library/react@npm:14.1.0"
   dependencies:
     "@babel/runtime": "npm:^7.12.5"
     "@testing-library/dom": "npm:^9.0.0"
@@ -2980,7 +2980,7 @@ __metadata:
   peerDependencies:
     react: ^18.0.0
     react-dom: ^18.0.0
-  checksum: 81035913024faf18ba7e163418af517b2c3b85aef496fbd6334bda38f6f6dd4072678c6b76c41148b46b7fc846764f875e1156cbfc7643ffa1b62ee069d78951
+  checksum: 357ad80b11bdd4b6d10d2fb1bf86d5b39fb457cf09293033cf42bcc7a95738a86a2b12b760ae15bad326da0b9c074ca015d2bbf0baae7da38fdbc7c808925820
   languageName: node
   linkType: hard
 
@@ -4702,20 +4702,20 @@ __metadata:
   languageName: node
   linkType: hard
 
-"babel-jest@npm:^29.5.0, babel-jest@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "babel-jest@npm:29.6.2"
+"babel-jest@npm:^29.5.0, babel-jest@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "babel-jest@npm:29.7.0"
   dependencies:
-    "@jest/transform": "npm:^29.6.2"
+    "@jest/transform": "npm:^29.7.0"
     "@types/babel__core": "npm:^7.1.14"
     babel-plugin-istanbul: "npm:^6.1.1"
-    babel-preset-jest: "npm:^29.5.0"
+    babel-preset-jest: "npm:^29.6.3"
     chalk: "npm:^4.0.0"
     graceful-fs: "npm:^4.2.9"
     slash: "npm:^3.0.0"
   peerDependencies:
     "@babel/core": ^7.8.0
-  checksum: c1ebaecd1323852867765a6920ab8b5e1e4236254415090a682e0ebf6a3339a9861f65791b23acad2e3a4c4bf5bca31c9abc154306ba7cf9725c2f6e78c92444
+  checksum: 2eda9c1391e51936ca573dd1aedfee07b14c59b33dbe16ef347873ddd777bcf6e2fc739681e9e9661ab54ef84a3109a03725be2ac32cd2124c07ea4401cbe8c1
   languageName: node
   linkType: hard
 
@@ -4766,15 +4766,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"babel-plugin-jest-hoist@npm:^29.5.0":
-  version: 29.5.0
-  resolution: "babel-plugin-jest-hoist@npm:29.5.0"
+"babel-plugin-jest-hoist@npm:^29.6.3":
+  version: 29.6.3
+  resolution: "babel-plugin-jest-hoist@npm:29.6.3"
   dependencies:
     "@babel/template": "npm:^7.3.3"
     "@babel/types": "npm:^7.3.3"
     "@types/babel__core": "npm:^7.1.14"
     "@types/babel__traverse": "npm:^7.0.6"
-  checksum: 385547c4d81647848dc3e86fecf4381032be99ed97d87aee78d422631f651042600371ee31e37ec9bb6f4a0a4f296b3b5798d69c410626ea94eae76d9c64da63
+  checksum: 7e6451caaf7dce33d010b8aafb970e62f1b0c0b57f4978c37b0d457bbcf0874d75a395a102daf0bae0bd14eafb9f6e9a165ee5e899c0a4f1f3bb2e07b304ed2e
   languageName: node
   linkType: hard
 
@@ -4892,15 +4892,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"babel-preset-jest@npm:^29.5.0":
-  version: 29.5.0
-  resolution: "babel-preset-jest@npm:29.5.0"
+"babel-preset-jest@npm:^29.6.3":
+  version: 29.6.3
+  resolution: "babel-preset-jest@npm:29.6.3"
   dependencies:
-    babel-plugin-jest-hoist: "npm:^29.5.0"
+    babel-plugin-jest-hoist: "npm:^29.6.3"
     babel-preset-current-node-syntax: "npm:^1.0.0"
   peerDependencies:
     "@babel/core": ^7.0.0
-  checksum: 752b8682c8cf55bca46d870003f4ce43a4ba0fcaa1138ff7f0e02340628e221810b0c2c3e77a7d5070168dc163eb11907f6c9256f187242abe0f14219d1f6b12
+  checksum: ec5fd0276b5630b05f0c14bb97cc3815c6b31600c683ebb51372e54dcb776cff790bdeeabd5b8d01ede375a040337ccbf6a3ccd68d3a34219125945e167ad943
   languageName: node
   linkType: hard
 
@@ -6067,6 +6067,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"create-jest@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "create-jest@npm:29.7.0"
+  dependencies:
+    "@jest/types": "npm:^29.6.3"
+    chalk: "npm:^4.0.0"
+    exit: "npm:^0.1.2"
+    graceful-fs: "npm:^4.2.9"
+    jest-config: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
+    prompts: "npm:^2.0.1"
+  bin:
+    create-jest: bin/create-jest.js
+  checksum: e7e54c280692470d3398f62a6238fd396327e01c6a0757002833f06d00afc62dd7bfe04ff2b9cd145264460e6b4d1eb8386f2925b7e567f97939843b7b0e812f
+  languageName: node
+  linkType: hard
+
 "cross-env@npm:^7.0.3":
   version: 7.0.3
   resolution: "cross-env@npm:7.0.3"
@@ -6138,10 +6155,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"css-functions-list@npm:^3.2.0":
-  version: 3.2.0
-  resolution: "css-functions-list@npm:3.2.0"
-  checksum: 640a1760c6d63536def671b7ccd89c2525a7197fc845a5ae1ec9f380fedd1aa9634f547db81f02f1a3736492867e9333ee8d6bf9aa498d211e36feae0e71a672
+"css-functions-list@npm:^3.2.1":
+  version: 3.2.1
+  resolution: "css-functions-list@npm:3.2.1"
+  checksum: e6e2d9580437ad6df9f2cf18cff3f941691ec5cbbaebd4cb17a5da40d8d5dac50004807ddd05c00a121d2f21a224e2c5d339fe8e13614af21c00181d7d1c22b9
   languageName: node
   linkType: hard
 
@@ -7820,7 +7837,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"expect@npm:^29.0.0, expect@npm:^29.6.2":
+"expect@npm:^29.0.0, expect@npm:^29.7.0":
   version: 29.7.0
   resolution: "expect@npm:29.7.0"
   dependencies:
@@ -7998,6 +8015,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"file-entry-cache@npm:^7.0.0":
+  version: 7.0.1
+  resolution: "file-entry-cache@npm:7.0.1"
+  dependencies:
+    flat-cache: "npm:^3.1.1"
+  checksum: fc0e4f830777e07087f97da9a6734820fdffa2945583355433f40d9819dd97b89f16ac87c07118737a6bc3eb9cf4bd896e7b38b07f0768aefcf44da33e797363
+  languageName: node
+  linkType: hard
+
 "file-loader@npm:^6.2.0":
   version: 6.2.0
   resolution: "file-loader@npm:6.2.0"
@@ -8128,14 +8154,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"flat-cache@npm:^3.0.4":
-  version: 3.1.1
-  resolution: "flat-cache@npm:3.1.1"
+"flat-cache@npm:^3.0.4, flat-cache@npm:^3.1.1":
+  version: 3.2.0
+  resolution: "flat-cache@npm:3.2.0"
   dependencies:
     flatted: "npm:^3.2.9"
     keyv: "npm:^4.5.3"
     rimraf: "npm:^3.0.2"
-  checksum: 15f7f854830089a903ea660809b67ee25632b8b1965da6a328d3dc59d451abe2e9f16ad0b7523571ece2b5424d1e1979469ba25870f76f49ce3bbffc836072ef
+  checksum: b76f611bd5f5d68f7ae632e3ae503e678d205cf97a17c6ab5b12f6ca61188b5f1f7464503efae6dc18683ed8f0b41460beb48ac4b9ac63fe6201296a91ba2f75
   languageName: node
   linkType: hard
 
@@ -9955,7 +9981,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"istanbul-lib-instrument@npm:^5.0.4, istanbul-lib-instrument@npm:^5.1.0":
+"istanbul-lib-instrument@npm:^5.0.4":
   version: 5.2.1
   resolution: "istanbul-lib-instrument@npm:5.2.1"
   dependencies:
@@ -9968,6 +9994,19 @@ __metadata:
   languageName: node
   linkType: hard
 
+"istanbul-lib-instrument@npm:^6.0.0":
+  version: 6.0.1
+  resolution: "istanbul-lib-instrument@npm:6.0.1"
+  dependencies:
+    "@babel/core": "npm:^7.12.3"
+    "@babel/parser": "npm:^7.14.7"
+    "@istanbuljs/schema": "npm:^0.1.2"
+    istanbul-lib-coverage: "npm:^3.2.0"
+    semver: "npm:^7.5.4"
+  checksum: 313d61aca3f82a04ad9377841d05061d603ea3d4a4dd281fdda2479ec4ddbc86dc1792c73651f21c93480570d1ecadc5f63011e2df86f30ee662b62c0c00e3d8
+  languageName: node
+  linkType: hard
+
 "istanbul-lib-report@npm:^3.0.0":
   version: 3.0.1
   resolution: "istanbul-lib-report@npm:3.0.1"
@@ -10040,59 +10079,59 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jest-changed-files@npm:^29.5.0":
-  version: 29.5.0
-  resolution: "jest-changed-files@npm:29.5.0"
+"jest-changed-files@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-changed-files@npm:29.7.0"
   dependencies:
     execa: "npm:^5.0.0"
+    jest-util: "npm:^29.7.0"
     p-limit: "npm:^3.1.0"
-  checksum: 96334c78507a13c0f11f1360d893ade78fba7fd169825ca4acf7565156ceddd89b952be81c00378fa87ab642d3f44902c34a20f21b561e985e79f6e81fa7e9a8
+  checksum: e071384d9e2f6bb462231ac53f29bff86f0e12394c1b49ccafbad225ce2ab7da226279a8a94f421949920bef9be7ef574fd86aee22e8adfa149be73554ab828b
   languageName: node
   linkType: hard
 
-"jest-circus@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-circus@npm:29.6.2"
+"jest-circus@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-circus@npm:29.7.0"
   dependencies:
-    "@jest/environment": "npm:^29.6.2"
-    "@jest/expect": "npm:^29.6.2"
-    "@jest/test-result": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/environment": "npm:^29.7.0"
+    "@jest/expect": "npm:^29.7.0"
+    "@jest/test-result": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     "@types/node": "npm:*"
     chalk: "npm:^4.0.0"
     co: "npm:^4.6.0"
     dedent: "npm:^1.0.0"
     is-generator-fn: "npm:^2.0.0"
-    jest-each: "npm:^29.6.2"
-    jest-matcher-utils: "npm:^29.6.2"
-    jest-message-util: "npm:^29.6.2"
-    jest-runtime: "npm:^29.6.2"
-    jest-snapshot: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
+    jest-each: "npm:^29.7.0"
+    jest-matcher-utils: "npm:^29.7.0"
+    jest-message-util: "npm:^29.7.0"
+    jest-runtime: "npm:^29.7.0"
+    jest-snapshot: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
     p-limit: "npm:^3.1.0"
-    pretty-format: "npm:^29.6.2"
+    pretty-format: "npm:^29.7.0"
     pure-rand: "npm:^6.0.0"
     slash: "npm:^3.0.0"
     stack-utils: "npm:^2.0.3"
-  checksum: 04f3176bcc3adf0a5d5895f3ce2cb86fafa5d0d03d246cddd0a39021ec4bbc1092ef30792a9d8cdfb1cb6fcee75a277354d65aef6ca8c364fd3747d8ce67e255
+  checksum: 8d15344cf7a9f14e926f0deed64ed190c7a4fa1ed1acfcd81e4cc094d3cc5bf7902ebb7b874edc98ada4185688f90c91e1747e0dfd7ac12463b097968ae74b5e
   languageName: node
   linkType: hard
 
-"jest-cli@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-cli@npm:29.6.2"
+"jest-cli@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-cli@npm:29.7.0"
   dependencies:
-    "@jest/core": "npm:^29.6.2"
-    "@jest/test-result": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/core": "npm:^29.7.0"
+    "@jest/test-result": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     chalk: "npm:^4.0.0"
+    create-jest: "npm:^29.7.0"
     exit: "npm:^0.1.2"
-    graceful-fs: "npm:^4.2.9"
     import-local: "npm:^3.0.2"
-    jest-config: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
-    jest-validate: "npm:^29.6.2"
-    prompts: "npm:^2.0.1"
+    jest-config: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
+    jest-validate: "npm:^29.7.0"
     yargs: "npm:^17.3.1"
   peerDependencies:
     node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
@@ -10101,34 +10140,34 @@ __metadata:
       optional: true
   bin:
     jest: bin/jest.js
-  checksum: 76d359427a573821b0b4f80a8b752e54778b8da1e09e737ae1ff5c29487d762a6f0d16becd5c1d2017cd337295945be82448539f90d04d173c72ee577c6cf897
+  checksum: a658fd55050d4075d65c1066364595962ead7661711495cfa1dfeecf3d6d0a8ffec532f3dbd8afbb3e172dd5fd2fb2e813c5e10256e7cf2fea766314942fb43a
   languageName: node
   linkType: hard
 
-"jest-config@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-config@npm:29.6.2"
+"jest-config@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-config@npm:29.7.0"
   dependencies:
     "@babel/core": "npm:^7.11.6"
-    "@jest/test-sequencer": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
-    babel-jest: "npm:^29.6.2"
+    "@jest/test-sequencer": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
+    babel-jest: "npm:^29.7.0"
     chalk: "npm:^4.0.0"
     ci-info: "npm:^3.2.0"
     deepmerge: "npm:^4.2.2"
     glob: "npm:^7.1.3"
     graceful-fs: "npm:^4.2.9"
-    jest-circus: "npm:^29.6.2"
-    jest-environment-node: "npm:^29.6.2"
-    jest-get-type: "npm:^29.4.3"
-    jest-regex-util: "npm:^29.4.3"
-    jest-resolve: "npm:^29.6.2"
-    jest-runner: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
-    jest-validate: "npm:^29.6.2"
+    jest-circus: "npm:^29.7.0"
+    jest-environment-node: "npm:^29.7.0"
+    jest-get-type: "npm:^29.6.3"
+    jest-regex-util: "npm:^29.6.3"
+    jest-resolve: "npm:^29.7.0"
+    jest-runner: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
+    jest-validate: "npm:^29.7.0"
     micromatch: "npm:^4.0.4"
     parse-json: "npm:^5.2.0"
-    pretty-format: "npm:^29.6.2"
+    pretty-format: "npm:^29.7.0"
     slash: "npm:^3.0.0"
     strip-json-comments: "npm:^3.1.1"
   peerDependencies:
@@ -10139,11 +10178,11 @@ __metadata:
       optional: true
     ts-node:
       optional: true
-  checksum: 334b8cf02c9c9f5f3685fd6f673d634691a370c9a96f1a855234c7513c409a1cc842f2c8e786da9ef8734d33b6ee95d7b7b4d586c1a4f22bcae59118755d7d2a
+  checksum: bab23c2eda1fff06e0d104b00d6adfb1d1aabb7128441899c9bff2247bd26710b050a5364281ce8d52b46b499153bf7e3ee88b19831a8f3451f1477a0246a0f1
   languageName: node
   linkType: hard
 
-"jest-diff@npm:^29.6.2, jest-diff@npm:^29.7.0":
+"jest-diff@npm:^29.7.0":
   version: 29.7.0
   resolution: "jest-diff@npm:29.7.0"
   dependencies:
@@ -10155,104 +10194,104 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jest-docblock@npm:^29.4.3":
-  version: 29.4.3
-  resolution: "jest-docblock@npm:29.4.3"
+"jest-docblock@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-docblock@npm:29.7.0"
   dependencies:
     detect-newline: "npm:^3.0.0"
-  checksum: 25cdea8fe77ff09d958abd347e26dcd8766ca69d9935bc626a89d694c91d33be06d4c088b02e4b3f143f532f726a10dff0bfe1e2387a0972a95addf5d64ed407
+  checksum: d932a8272345cf6b6142bb70a2bb63e0856cc0093f082821577ea5bdf4643916a98744dfc992189d2b1417c38a11fa42466f6111526bc1fb81366f56410f3be9
   languageName: node
   linkType: hard
 
-"jest-each@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-each@npm:29.6.2"
+"jest-each@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-each@npm:29.7.0"
   dependencies:
-    "@jest/types": "npm:^29.6.1"
+    "@jest/types": "npm:^29.6.3"
     chalk: "npm:^4.0.0"
-    jest-get-type: "npm:^29.4.3"
-    jest-util: "npm:^29.6.2"
-    pretty-format: "npm:^29.6.2"
-  checksum: b586f5c811011589308f2d8e0d5e596fa26d101e1116b55c624342327b932d3644aac37ce7b6c4eb8ef018893d2a41610ed7edbabfe125c3b46cf9a2b0f03d9b
+    jest-get-type: "npm:^29.6.3"
+    jest-util: "npm:^29.7.0"
+    pretty-format: "npm:^29.7.0"
+  checksum: f7f9a90ebee80cc688e825feceb2613627826ac41ea76a366fa58e669c3b2403d364c7c0a74d862d469b103c843154f8456d3b1c02b487509a12afa8b59edbb4
   languageName: node
   linkType: hard
 
 "jest-environment-jsdom@npm:^29.5.0":
-  version: 29.6.2
-  resolution: "jest-environment-jsdom@npm:29.6.2"
+  version: 29.7.0
+  resolution: "jest-environment-jsdom@npm:29.7.0"
   dependencies:
-    "@jest/environment": "npm:^29.6.2"
-    "@jest/fake-timers": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/environment": "npm:^29.7.0"
+    "@jest/fake-timers": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     "@types/jsdom": "npm:^20.0.0"
     "@types/node": "npm:*"
-    jest-mock: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
+    jest-mock: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
     jsdom: "npm:^20.0.0"
   peerDependencies:
     canvas: ^2.5.0
   peerDependenciesMeta:
     canvas:
       optional: true
-  checksum: 7f5885f6fa42d7d14d7dbdc58186283d3ed409a0abc8289bf365a4a2e92ea84d0eeb2087cd5ce6db39394652d817e4c7505d28555e2594309a324634d2b45718
+  checksum: 139b94e2c8ec1bb5a46ce17df5211da65ce867354b3fd4e00fa6a0d1da95902df4cf7881273fc6ea937e5c325d39d6773f0d41b6c469363334de9d489d2c321f
   languageName: node
   linkType: hard
 
-"jest-environment-node@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-environment-node@npm:29.6.2"
+"jest-environment-node@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-environment-node@npm:29.7.0"
   dependencies:
-    "@jest/environment": "npm:^29.6.2"
-    "@jest/fake-timers": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/environment": "npm:^29.7.0"
+    "@jest/fake-timers": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     "@types/node": "npm:*"
-    jest-mock: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
-  checksum: fea7c71e2b6ef901679983809918f670551d0122380f60695df554ca1dc9a065ec347e14c516c9b5a184494572320cd1696bd5bc817853a3e6cdb89b44d4054e
+    jest-mock: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
+  checksum: 61f04fec077f8b1b5c1a633e3612fc0c9aa79a0ab7b05600683428f1e01a4d35346c474bde6f439f9fcc1a4aa9a2861ff852d079a43ab64b02105d1004b2592b
   languageName: node
   linkType: hard
 
-"jest-get-type@npm:^29.4.3, jest-get-type@npm:^29.6.3":
+"jest-get-type@npm:^29.6.3":
   version: 29.6.3
   resolution: "jest-get-type@npm:29.6.3"
   checksum: 552e7a97a983d3c2d4e412a44eb7de0430ff773dd99f7500962c268d6dfbfa431d7d08f919c9d960530e5f7f78eb47f267ad9b318265e5092b3ff9ede0db7c2b
   languageName: node
   linkType: hard
 
-"jest-haste-map@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-haste-map@npm:29.6.2"
+"jest-haste-map@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-haste-map@npm:29.7.0"
   dependencies:
-    "@jest/types": "npm:^29.6.1"
+    "@jest/types": "npm:^29.6.3"
     "@types/graceful-fs": "npm:^4.1.3"
     "@types/node": "npm:*"
     anymatch: "npm:^3.0.3"
     fb-watchman: "npm:^2.0.0"
     fsevents: "npm:^2.3.2"
     graceful-fs: "npm:^4.2.9"
-    jest-regex-util: "npm:^29.4.3"
-    jest-util: "npm:^29.6.2"
-    jest-worker: "npm:^29.6.2"
+    jest-regex-util: "npm:^29.6.3"
+    jest-util: "npm:^29.7.0"
+    jest-worker: "npm:^29.7.0"
     micromatch: "npm:^4.0.4"
     walker: "npm:^1.0.8"
   dependenciesMeta:
     fsevents:
       optional: true
-  checksum: 12c921ff059613b67e8b3a0730fe8f5f38e39a1aeb2050948a5c6890c4705f39decd4f7da8ebc7ede22e0eeef37fef2e9256952ac6557dd3bcd62416cab0612f
+  checksum: 2683a8f29793c75a4728787662972fedd9267704c8f7ef9d84f2beed9a977f1cf5e998c07b6f36ba5603f53cb010c911fe8cd0ac9886e073fe28ca66beefd30c
   languageName: node
   linkType: hard
 
-"jest-leak-detector@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-leak-detector@npm:29.6.2"
+"jest-leak-detector@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-leak-detector@npm:29.7.0"
   dependencies:
-    jest-get-type: "npm:^29.4.3"
-    pretty-format: "npm:^29.6.2"
-  checksum: 70eb538bb137b769908d4d1e15d9b380a06285ea31c1d2ae05bcc9269863efe6369484cb33bf6c6f3e325dd53cd252cd7e868bdbd2b31367a9b41b449eb8e4a9
+    jest-get-type: "npm:^29.6.3"
+    pretty-format: "npm:^29.7.0"
+  checksum: 71bb9f77fc489acb842a5c7be030f2b9acb18574dc9fb98b3100fc57d422b1abc55f08040884bd6e6dbf455047a62f7eaff12aa4058f7cbdc11558718ca6a395
   languageName: node
   linkType: hard
 
-"jest-matcher-utils@npm:^29.6.2, jest-matcher-utils@npm:^29.7.0":
+"jest-matcher-utils@npm:^29.7.0":
   version: 29.7.0
   resolution: "jest-matcher-utils@npm:29.7.0"
   dependencies:
@@ -10264,7 +10303,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jest-message-util@npm:^29.6.2, jest-message-util@npm:^29.7.0":
+"jest-message-util@npm:^29.7.0":
   version: 29.7.0
   resolution: "jest-message-util@npm:29.7.0"
   dependencies:
@@ -10281,14 +10320,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jest-mock@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-mock@npm:29.6.2"
+"jest-mock@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-mock@npm:29.7.0"
   dependencies:
-    "@jest/types": "npm:^29.6.1"
+    "@jest/types": "npm:^29.6.3"
     "@types/node": "npm:*"
-    jest-util: "npm:^29.6.2"
-  checksum: 34e8119876696d640db1b33b2c88f3bbd56b676f5e82ae65babdb56b0dab054d856b903785d38e1e8e3274549622b9a4556bfaa301d75fe4e2b30494cac5b8ee
+    jest-util: "npm:^29.7.0"
+  checksum: 7b9f8349ee87695a309fe15c46a74ab04c853369e5c40952d68061d9dc3159a0f0ed73e215f81b07ee97a9faaf10aebe5877a9d6255068a0977eae6a9ff1d5ac
   languageName: node
   linkType: hard
 
@@ -10304,128 +10343,128 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jest-regex-util@npm:^29.4.3":
-  version: 29.4.3
-  resolution: "jest-regex-util@npm:29.4.3"
-  checksum: a7a4508bda47c5177e7337fb6fb22e9adab414ba141f224c9992c86973da1ccf5c69040e63636090ad26ef3a123d28bec950fa99496c157444b4f847e5e5a670
+"jest-regex-util@npm:^29.6.3":
+  version: 29.6.3
+  resolution: "jest-regex-util@npm:29.6.3"
+  checksum: 4e33fb16c4f42111159cafe26397118dcfc4cf08bc178a67149fb05f45546a91928b820894572679d62559839d0992e21080a1527faad65daaae8743a5705a3b
   languageName: node
   linkType: hard
 
-"jest-resolve-dependencies@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-resolve-dependencies@npm:29.6.2"
+"jest-resolve-dependencies@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-resolve-dependencies@npm:29.7.0"
   dependencies:
-    jest-regex-util: "npm:^29.4.3"
-    jest-snapshot: "npm:^29.6.2"
-  checksum: b80172d164fe36a3cd9b19c458c3e8075e7935cdaa191f6e2e335f9b5c603faf0785efc35f9cf6c496729de34a3bd98f6cb8dd877c11fa6e17adf385d1ca85a6
+    jest-regex-util: "npm:^29.6.3"
+    jest-snapshot: "npm:^29.7.0"
+  checksum: b6e9ad8ae5b6049474118ea6441dfddd385b6d1fc471db0136f7c8fbcfe97137a9665e4f837a9f49f15a29a1deb95a14439b7aec812f3f99d08f228464930f0d
   languageName: node
   linkType: hard
 
-"jest-resolve@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-resolve@npm:29.6.2"
+"jest-resolve@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-resolve@npm:29.7.0"
   dependencies:
     chalk: "npm:^4.0.0"
     graceful-fs: "npm:^4.2.9"
-    jest-haste-map: "npm:^29.6.2"
+    jest-haste-map: "npm:^29.7.0"
     jest-pnp-resolver: "npm:^1.2.2"
-    jest-util: "npm:^29.6.2"
-    jest-validate: "npm:^29.6.2"
+    jest-util: "npm:^29.7.0"
+    jest-validate: "npm:^29.7.0"
     resolve: "npm:^1.20.0"
     resolve.exports: "npm:^2.0.0"
     slash: "npm:^3.0.0"
-  checksum: df6ace45facf1f9d8f2911fcc1eefcc871afa107748f41a2f84a3d7b707d2211be1450ba5044fe8fa1ffc497b6814309f71f376aac139683ddc7b05b263d45f9
+  checksum: 59da5c9c5b50563e959a45e09e2eace783d7f9ac0b5dcc6375dea4c0db938d2ebda97124c8161310082760e8ebbeff9f6b177c15ca2f57fb424f637a5d2adb47
   languageName: node
   linkType: hard
 
-"jest-runner@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-runner@npm:29.6.2"
+"jest-runner@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-runner@npm:29.7.0"
   dependencies:
-    "@jest/console": "npm:^29.6.2"
-    "@jest/environment": "npm:^29.6.2"
-    "@jest/test-result": "npm:^29.6.2"
-    "@jest/transform": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/console": "npm:^29.7.0"
+    "@jest/environment": "npm:^29.7.0"
+    "@jest/test-result": "npm:^29.7.0"
+    "@jest/transform": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     "@types/node": "npm:*"
     chalk: "npm:^4.0.0"
     emittery: "npm:^0.13.1"
     graceful-fs: "npm:^4.2.9"
-    jest-docblock: "npm:^29.4.3"
-    jest-environment-node: "npm:^29.6.2"
-    jest-haste-map: "npm:^29.6.2"
-    jest-leak-detector: "npm:^29.6.2"
-    jest-message-util: "npm:^29.6.2"
-    jest-resolve: "npm:^29.6.2"
-    jest-runtime: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
-    jest-watcher: "npm:^29.6.2"
-    jest-worker: "npm:^29.6.2"
+    jest-docblock: "npm:^29.7.0"
+    jest-environment-node: "npm:^29.7.0"
+    jest-haste-map: "npm:^29.7.0"
+    jest-leak-detector: "npm:^29.7.0"
+    jest-message-util: "npm:^29.7.0"
+    jest-resolve: "npm:^29.7.0"
+    jest-runtime: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
+    jest-watcher: "npm:^29.7.0"
+    jest-worker: "npm:^29.7.0"
     p-limit: "npm:^3.1.0"
     source-map-support: "npm:0.5.13"
-  checksum: d0f2fc80b01c40b28bb86ace6a1f913a346dbdd81d8ed84e689bc0e21b27f7e9d1b963e6d8ece44df1a870ba14016730ce08444b15f3fdee92a15dff0c6c1aa3
+  checksum: 2194b4531068d939f14c8d3274fe5938b77fa73126aedf9c09ec9dec57d13f22c72a3b5af01ac04f5c1cf2e28d0ac0b4a54212a61b05f10b5d6b47f2a1097bb4
   languageName: node
   linkType: hard
 
-"jest-runtime@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-runtime@npm:29.6.2"
+"jest-runtime@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-runtime@npm:29.7.0"
   dependencies:
-    "@jest/environment": "npm:^29.6.2"
-    "@jest/fake-timers": "npm:^29.6.2"
-    "@jest/globals": "npm:^29.6.2"
-    "@jest/source-map": "npm:^29.6.0"
-    "@jest/test-result": "npm:^29.6.2"
-    "@jest/transform": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/environment": "npm:^29.7.0"
+    "@jest/fake-timers": "npm:^29.7.0"
+    "@jest/globals": "npm:^29.7.0"
+    "@jest/source-map": "npm:^29.6.3"
+    "@jest/test-result": "npm:^29.7.0"
+    "@jest/transform": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     "@types/node": "npm:*"
     chalk: "npm:^4.0.0"
     cjs-module-lexer: "npm:^1.0.0"
     collect-v8-coverage: "npm:^1.0.0"
     glob: "npm:^7.1.3"
     graceful-fs: "npm:^4.2.9"
-    jest-haste-map: "npm:^29.6.2"
-    jest-message-util: "npm:^29.6.2"
-    jest-mock: "npm:^29.6.2"
-    jest-regex-util: "npm:^29.4.3"
-    jest-resolve: "npm:^29.6.2"
-    jest-snapshot: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
+    jest-haste-map: "npm:^29.7.0"
+    jest-message-util: "npm:^29.7.0"
+    jest-mock: "npm:^29.7.0"
+    jest-regex-util: "npm:^29.6.3"
+    jest-resolve: "npm:^29.7.0"
+    jest-snapshot: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
     slash: "npm:^3.0.0"
     strip-bom: "npm:^4.0.0"
-  checksum: 026a5fa33fa370561e6ab33a01b59e3e382b72f8eb7a42a85d1c9619bc9123a274ec791b823ad4bf58e20285758e9e895e53da6ae971c92124612f99fe7c7ffe
+  checksum: 7cd89a1deda0bda7d0941835434e44f9d6b7bd50b5c5d9b0fc9a6c990b2d4d2cab59685ab3cb2850ed4cc37059f6de903af5a50565d7f7f1192a77d3fd6dd2a6
   languageName: node
   linkType: hard
 
-"jest-snapshot@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-snapshot@npm:29.6.2"
+"jest-snapshot@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-snapshot@npm:29.7.0"
   dependencies:
     "@babel/core": "npm:^7.11.6"
     "@babel/generator": "npm:^7.7.2"
     "@babel/plugin-syntax-jsx": "npm:^7.7.2"
     "@babel/plugin-syntax-typescript": "npm:^7.7.2"
     "@babel/types": "npm:^7.3.3"
-    "@jest/expect-utils": "npm:^29.6.2"
-    "@jest/transform": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/expect-utils": "npm:^29.7.0"
+    "@jest/transform": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     babel-preset-current-node-syntax: "npm:^1.0.0"
     chalk: "npm:^4.0.0"
-    expect: "npm:^29.6.2"
+    expect: "npm:^29.7.0"
     graceful-fs: "npm:^4.2.9"
-    jest-diff: "npm:^29.6.2"
-    jest-get-type: "npm:^29.4.3"
-    jest-matcher-utils: "npm:^29.6.2"
-    jest-message-util: "npm:^29.6.2"
-    jest-util: "npm:^29.6.2"
+    jest-diff: "npm:^29.7.0"
+    jest-get-type: "npm:^29.6.3"
+    jest-matcher-utils: "npm:^29.7.0"
+    jest-message-util: "npm:^29.7.0"
+    jest-util: "npm:^29.7.0"
     natural-compare: "npm:^1.4.0"
-    pretty-format: "npm:^29.6.2"
+    pretty-format: "npm:^29.7.0"
     semver: "npm:^7.5.3"
-  checksum: 79f02c2becf90a1b5c5d06833b0a4c9f6e0d7a9fcd36e69f81750ab147180dd06e3565e83c1d79a1ef8b7943c5af3eb3e0119c45e92f78e1189279c4fba2e136
+  checksum: 6e9003c94ec58172b4a62864a91c0146513207bedf4e0a06e1e2ac70a4484088a2683e3a0538d8ea913bcfd53dc54a9b98a98cdfa562e7fe1d1339aeae1da570
   languageName: node
   linkType: hard
 
-"jest-util@npm:^29.6.2, jest-util@npm:^29.7.0":
+"jest-util@npm:^29.7.0":
   version: 29.7.0
   resolution: "jest-util@npm:29.7.0"
   dependencies:
@@ -10439,33 +10478,33 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jest-validate@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-validate@npm:29.6.2"
+"jest-validate@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-validate@npm:29.7.0"
   dependencies:
-    "@jest/types": "npm:^29.6.1"
+    "@jest/types": "npm:^29.6.3"
     camelcase: "npm:^6.2.0"
     chalk: "npm:^4.0.0"
-    jest-get-type: "npm:^29.4.3"
+    jest-get-type: "npm:^29.6.3"
     leven: "npm:^3.1.0"
-    pretty-format: "npm:^29.6.2"
-  checksum: 79af1153268d896deb183230fba547398fde7b8a4f45fe33f1cd5c3b6b84d317e4b87ea7988d1137348c693e7f9450cce7af4529d5b190891bf493bc93024e40
+    pretty-format: "npm:^29.7.0"
+  checksum: a20b930480c1ed68778c739f4739dce39423131bc070cd2505ddede762a5570a256212e9c2401b7ae9ba4d7b7c0803f03c5b8f1561c62348213aba18d9dbece2
   languageName: node
   linkType: hard
 
-"jest-watcher@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-watcher@npm:29.6.2"
+"jest-watcher@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-watcher@npm:29.7.0"
   dependencies:
-    "@jest/test-result": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/test-result": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     "@types/node": "npm:*"
     ansi-escapes: "npm:^4.2.1"
     chalk: "npm:^4.0.0"
     emittery: "npm:^0.13.1"
-    jest-util: "npm:^29.6.2"
+    jest-util: "npm:^29.7.0"
     string-length: "npm:^4.0.1"
-  checksum: ba567798961d52b3ca1f853169a5860111ae764de90634b86a4a5cc676848c147bee5d95cd168b5c5941533ed384f677764474d009437a03b6b6a15da6232eb3
+  checksum: ec6c75030562fc8f8c727cb8f3b94e75d831fc718785abfc196e1f2a2ebc9a2e38744a15147170039628a853d77a3b695561ce850375ede3a4ee6037a2574567
   languageName: node
   linkType: hard
 
@@ -10480,26 +10519,26 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jest-worker@npm:^29.6.2":
-  version: 29.6.2
-  resolution: "jest-worker@npm:29.6.2"
+"jest-worker@npm:^29.7.0":
+  version: 29.7.0
+  resolution: "jest-worker@npm:29.7.0"
   dependencies:
     "@types/node": "npm:*"
-    jest-util: "npm:^29.6.2"
+    jest-util: "npm:^29.7.0"
     merge-stream: "npm:^2.0.0"
     supports-color: "npm:^8.0.0"
-  checksum: 8b978cb4851222e536aef552bdc06a60db580d0f921107fe1a1b94cdc8b39ddeb076b23e5bb96b69752c2f936b803295cdff11484f7c5efaf4562952e2cc0897
+  checksum: 5570a3a005b16f46c131968b8a5b56d291f9bbb85ff4217e31c80bd8a02e7de799e59a54b95ca28d5c302f248b54cbffde2d177c2f0f52ffcee7504c6eabf660
   languageName: node
   linkType: hard
 
 "jest@npm:^29.5.0":
-  version: 29.6.2
-  resolution: "jest@npm:29.6.2"
+  version: 29.7.0
+  resolution: "jest@npm:29.7.0"
   dependencies:
-    "@jest/core": "npm:^29.6.2"
-    "@jest/types": "npm:^29.6.1"
+    "@jest/core": "npm:^29.7.0"
+    "@jest/types": "npm:^29.6.3"
     import-local: "npm:^3.0.2"
-    jest-cli: "npm:^29.6.2"
+    jest-cli: "npm:^29.7.0"
   peerDependencies:
     node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
   peerDependenciesMeta:
@@ -10507,7 +10546,7 @@ __metadata:
       optional: true
   bin:
     jest: bin/jest.js
-  checksum: fdb4df81f2bf1ad58f98f74b6d6f74d7727bd8fd5a8ddefc1e7612b8a68cbd0a4ae134060c5b987b01281f1fe93276c1abb034ef1ce06a0ea1468f288fafc7c1
+  checksum: f40eb8171cf147c617cc6ada49d062fbb03b4da666cb8d39cdbfb739a7d75eea4c3ca150fb072d0d273dce0c753db4d0467d54906ad0293f59c54f9db4a09d8b
   languageName: node
   linkType: hard
 
@@ -10824,10 +10863,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"known-css-properties@npm:^0.27.0":
-  version: 0.27.0
-  resolution: "known-css-properties@npm:0.27.0"
-  checksum: 49bf8d0a773039d07726d263c92145fa73be9a18990d54c3b3cebf472fdfb0095124a3fcfca3ccd1225231d4bdf9615c82e029b2d0d508de130f6be7467af9e4
+"known-css-properties@npm:^0.29.0":
+  version: 0.29.0
+  resolution: "known-css-properties@npm:0.29.0"
+  checksum: f66e9992097b8f54e97dbe729943d4a11b8d3ba15f68dbb3deb8bb0122cb89c22c90c9221ecb1e3f2e236838fe3c0faae319b43908c81b6e254ac43cafde2906
   languageName: node
   linkType: hard
 
@@ -10879,8 +10918,8 @@ __metadata:
   linkType: hard
 
 "lint-staged@npm:^15.0.0":
-  version: 15.0.2
-  resolution: "lint-staged@npm:15.0.2"
+  version: 15.1.0
+  resolution: "lint-staged@npm:15.1.0"
   dependencies:
     chalk: "npm:5.3.0"
     commander: "npm:11.1.0"
@@ -10891,10 +10930,10 @@ __metadata:
     micromatch: "npm:4.0.5"
     pidtree: "npm:0.6.0"
     string-argv: "npm:0.3.2"
-    yaml: "npm:2.3.3"
+    yaml: "npm:2.3.4"
   bin:
     lint-staged: bin/lint-staged.js
-  checksum: 5c8806137c8c9c63e1156e16f2a2763586a9eab8f24713b95bbfd099308c83f85af2df622fdc326a9f0e455e657718f61f6d4a81067a5c77243e65e822c8f16c
+  checksum: d427408be98df7558e918593cb765d5caaa67a5cdca89671fb54280a6c959f4e448db36d4f85e8e0bd9c2c1e996aa133916925cf47c9df573b47308d5e298d84
   languageName: node
   linkType: hard
 
@@ -13152,12 +13191,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-scss@npm:^4.0.7":
-  version: 4.0.7
-  resolution: "postcss-scss@npm:4.0.7"
+"postcss-scss@npm:^4.0.9":
+  version: 4.0.9
+  resolution: "postcss-scss@npm:4.0.9"
   peerDependencies:
-    postcss: ^8.4.19
-  checksum: 2f86938fef39bd766ada496d8ccac840bf9f2dee0d9c6006dc2903ba0fbdc9f5c2d6ead1f3e7508f8d82eee6ad25df7d77e9196c4c6bec8952ef9a7403f30efc
+    postcss: ^8.4.29
+  checksum: f917ecfd4b9113a6648e966a41f027ff7e14238393914978d44596e227a50f084667dc8818742348dc7d8b20130b30d4259aca1d4db86754a9c141202ae03714
   languageName: node
   linkType: hard
 
@@ -13201,7 +13240,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss@npm:^8.2.15, postcss@npm:^8.4.24, postcss@npm:^8.4.25":
+"postcss@npm:^8.2.15, postcss@npm:^8.4.24, postcss@npm:^8.4.28":
   version: 8.4.31
   resolution: "postcss@npm:8.4.31"
   dependencies:
@@ -13296,11 +13335,11 @@ __metadata:
   linkType: hard
 
 "prettier@npm:^3.0.0":
-  version: 3.0.1
-  resolution: "prettier@npm:3.0.1"
+  version: 3.1.0
+  resolution: "prettier@npm:3.1.0"
   bin:
     prettier: bin/prettier.cjs
-  checksum: 7231768b6e0f0f17cbaa83a4f7cdb100df0229ef1910b0b2cf72ce5ed8ee25ae7ec0d30cde20dcd898a002c6d1fcdb8a6ab0f8f5d8fc1275b7c29ea9e56305f2
+  checksum: a45ea70aa97fde162ea4c4aba3dfc7859aa6a732a1db34458d9535dc3c2c16d3bc3fb5689e6cd76aa835562555303b02d9449fd2e15af3b73c8053557e25c5b6
   languageName: node
   linkType: hard
 
@@ -13322,7 +13361,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"pretty-format@npm:^29.0.0, pretty-format@npm:^29.6.2, pretty-format@npm:^29.7.0":
+"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0":
   version: 29.7.0
   resolution: "pretty-format@npm:29.7.0"
   dependencies:
@@ -15720,20 +15759,20 @@ __metadata:
   languageName: node
   linkType: hard
 
-"stylelint-config-recommended-scss@npm:^13.0.0":
-  version: 13.0.0
-  resolution: "stylelint-config-recommended-scss@npm:13.0.0"
+"stylelint-config-recommended-scss@npm:^13.1.0":
+  version: 13.1.0
+  resolution: "stylelint-config-recommended-scss@npm:13.1.0"
   dependencies:
-    postcss-scss: "npm:^4.0.7"
+    postcss-scss: "npm:^4.0.9"
     stylelint-config-recommended: "npm:^13.0.0"
-    stylelint-scss: "npm:^5.1.0"
+    stylelint-scss: "npm:^5.3.0"
   peerDependencies:
     postcss: ^8.3.3
     stylelint: ^15.10.0
   peerDependenciesMeta:
     postcss:
       optional: true
-  checksum: 370152e20e6395800ac89517019f03153b121ce7a7dfa0865442365bf2725935da9df2eb3e8b5ee4e240d1b3a7438ee2cd1932009bdc59da8c2a790066379387
+  checksum: e07d0172c7936b4f644138e4129df2f187d297f1f96ce5865ab21ccd1c22caf94220f7caf9d6985e93e515de4c0356f6cb9c924d00df2eee5b3bc237f7e5bb48
   languageName: node
   linkType: hard
 
@@ -15747,10 +15786,10 @@ __metadata:
   linkType: hard
 
 "stylelint-config-standard-scss@npm:^11.0.0":
-  version: 11.0.0
-  resolution: "stylelint-config-standard-scss@npm:11.0.0"
+  version: 11.1.0
+  resolution: "stylelint-config-standard-scss@npm:11.1.0"
   dependencies:
-    stylelint-config-recommended-scss: "npm:^13.0.0"
+    stylelint-config-recommended-scss: "npm:^13.1.0"
     stylelint-config-standard: "npm:^34.0.0"
   peerDependencies:
     postcss: ^8.3.3
@@ -15758,7 +15797,7 @@ __metadata:
   peerDependenciesMeta:
     postcss:
       optional: true
-  checksum: 9b0766ec11f1e6f690c1e22d74e7f7103f64569bdca966c26ff43f81f55f13797fdb1503338f973672c001daf50b13682361dd4105d22497841437c148f86be5
+  checksum: 22d00e75c1eacce9883fd48c3d67b1107b0e39d7d86e9f73deaa332b11c39a9678c947ae2c34cd5159a452ec9a857694ed58b5a851087480d3c9a66dab629415
   languageName: node
   linkType: hard
 
@@ -15773,37 +15812,38 @@ __metadata:
   languageName: node
   linkType: hard
 
-"stylelint-scss@npm:^5.1.0":
-  version: 5.1.0
-  resolution: "stylelint-scss@npm:5.1.0"
+"stylelint-scss@npm:^5.3.0":
+  version: 5.3.1
+  resolution: "stylelint-scss@npm:5.3.1"
   dependencies:
+    known-css-properties: "npm:^0.29.0"
     postcss-media-query-parser: "npm:^0.2.3"
     postcss-resolve-nested-selector: "npm:^0.1.1"
     postcss-selector-parser: "npm:^6.0.13"
     postcss-value-parser: "npm:^4.2.0"
   peerDependencies:
     stylelint: ^14.5.1 || ^15.0.0
-  checksum: af176340227e77942429d2032cd345ebc8e40e4ff17a2ce69b6000252da178f21d1a97311a258a81c76c0610a96650c1e2ecdfa0d598a6fc41e31d6a7cd03847
+  checksum: 5dfed5f9ac9812cd2ac6ef0272c720dee0326aaaee2998315a23bdcd71b8f04427f29cad634793eea2b45984182e20f03e90d43501e8e4d55bc956f80e2de477
   languageName: node
   linkType: hard
 
 "stylelint@npm:^15.10.1":
-  version: 15.10.2
-  resolution: "stylelint@npm:15.10.2"
+  version: 15.11.0
+  resolution: "stylelint@npm:15.11.0"
   dependencies:
-    "@csstools/css-parser-algorithms": "npm:^2.3.0"
-    "@csstools/css-tokenizer": "npm:^2.1.1"
-    "@csstools/media-query-list-parser": "npm:^2.1.2"
+    "@csstools/css-parser-algorithms": "npm:^2.3.1"
+    "@csstools/css-tokenizer": "npm:^2.2.0"
+    "@csstools/media-query-list-parser": "npm:^2.1.4"
     "@csstools/selector-specificity": "npm:^3.0.0"
     balanced-match: "npm:^2.0.0"
     colord: "npm:^2.9.3"
     cosmiconfig: "npm:^8.2.0"
-    css-functions-list: "npm:^3.2.0"
+    css-functions-list: "npm:^3.2.1"
     css-tree: "npm:^2.3.1"
     debug: "npm:^4.3.4"
-    fast-glob: "npm:^3.3.0"
+    fast-glob: "npm:^3.3.1"
     fastest-levenshtein: "npm:^1.0.16"
-    file-entry-cache: "npm:^6.0.1"
+    file-entry-cache: "npm:^7.0.0"
     global-modules: "npm:^2.0.0"
     globby: "npm:^11.1.0"
     globjoin: "npm:^0.1.4"
@@ -15812,13 +15852,13 @@ __metadata:
     import-lazy: "npm:^4.0.0"
     imurmurhash: "npm:^0.1.4"
     is-plain-object: "npm:^5.0.0"
-    known-css-properties: "npm:^0.27.0"
+    known-css-properties: "npm:^0.29.0"
     mathml-tag-names: "npm:^2.1.3"
     meow: "npm:^10.1.5"
     micromatch: "npm:^4.0.5"
     normalize-path: "npm:^3.0.0"
     picocolors: "npm:^1.0.0"
-    postcss: "npm:^8.4.25"
+    postcss: "npm:^8.4.28"
     postcss-resolve-nested-selector: "npm:^0.1.1"
     postcss-safe-parser: "npm:^6.0.0"
     postcss-selector-parser: "npm:^6.0.13"
@@ -15833,7 +15873,7 @@ __metadata:
     write-file-atomic: "npm:^5.0.1"
   bin:
     stylelint: bin/stylelint.mjs
-  checksum: 8378ee868b09322d2b7e07b03461c524736778a391b2ce6d4ad46c636e55a90ed98723f253db10d9a762cfefec60200d0b9c5cd7798b2f373efd2d986768ebc7
+  checksum: 2d88b7293e308b7e418c14ba4130777b1a28b214304957f03b41a6dc8e00005266caf47479f718a6ec5e572cb52e903ca34aabf3febbe3a3ae32fff6b018d9fd
   languageName: node
   linkType: hard
 
@@ -17724,10 +17764,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"yaml@npm:2.3.3":
-  version: 2.3.3
-  resolution: "yaml@npm:2.3.3"
-  checksum: a0c56bf682159b0567e9cbbddf23efc2f6806f6450716d9be6ec5eb1af1b941e95c8d3dc9c47da20d1b6883a9d6c61e31cf98bb4b77ebca4396bf772657f2f00
+"yaml@npm:2.3.4":
+  version: 2.3.4
+  resolution: "yaml@npm:2.3.4"
+  checksum: cf03b68f8fef5e8516b0f0b54edaf2459f1648317fc6210391cf606d247e678b449382f4bd01f77392538429e306c7cba8ff46ff6b37cac4de9a76aff33bd9e1
   languageName: node
   linkType: hard
 

From a36b59be8ad7656b7ceab9751c9ec5b3563e3a30 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 13 Nov 2023 09:32:36 -0500
Subject: [PATCH 10/63] Spec coverage for `api/v1/trends` controllers (#27837)

---
 .../api/v1/trends/links_controller_spec.rb    | 38 +++++++++++++++--
 .../api/v1/trends/statuses_controller_spec.rb | 38 +++++++++++++++--
 .../api/v1/trends/tags_controller_spec.rb     | 41 +++++++++++++++----
 3 files changed, 101 insertions(+), 16 deletions(-)

diff --git a/spec/controllers/api/v1/trends/links_controller_spec.rb b/spec/controllers/api/v1/trends/links_controller_spec.rb
index 71a7e2e47..12d4198aa 100644
--- a/spec/controllers/api/v1/trends/links_controller_spec.rb
+++ b/spec/controllers/api/v1/trends/links_controller_spec.rb
@@ -2,14 +2,44 @@
 
 require 'rails_helper'
 
-describe Api::V1::Trends::LinksController do
+RSpec.describe Api::V1::Trends::LinksController do
   render_views
 
   describe 'GET #index' do
-    it 'returns http success' do
-      get :index
+    around do |example|
+      previous = Setting.trends
+      example.run
+      Setting.trends = previous
+    end
 
-      expect(response).to have_http_status(200)
+    context 'when trends are disabled' do
+      before { Setting.trends = false }
+
+      it 'returns http success' do
+        get :index
+
+        expect(response).to have_http_status(200)
+      end
+    end
+
+    context 'when trends are enabled' do
+      before { Setting.trends = true }
+
+      it 'returns http success' do
+        prepare_trends
+        stub_const('Api::V1::Trends::LinksController::DEFAULT_LINKS_LIMIT', 2)
+        get :index
+
+        expect(response).to have_http_status(200)
+        expect(response.headers).to include('Link')
+      end
+
+      def prepare_trends
+        Fabricate.times(3, :preview_card, trendable: true, language: 'en').each do |link|
+          2.times { |i| Trends.links.add(link, i) }
+        end
+        Trends::Links.new(threshold: 1).refresh
+      end
     end
   end
 end
diff --git a/spec/controllers/api/v1/trends/statuses_controller_spec.rb b/spec/controllers/api/v1/trends/statuses_controller_spec.rb
index e9892bb14..69fdb270d 100644
--- a/spec/controllers/api/v1/trends/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/trends/statuses_controller_spec.rb
@@ -2,14 +2,44 @@
 
 require 'rails_helper'
 
-describe Api::V1::Trends::StatusesController do
+RSpec.describe Api::V1::Trends::StatusesController do
   render_views
 
   describe 'GET #index' do
-    it 'returns http success' do
-      get :index
+    around do |example|
+      previous = Setting.trends
+      example.run
+      Setting.trends = previous
+    end
 
-      expect(response).to have_http_status(200)
+    context 'when trends are disabled' do
+      before { Setting.trends = false }
+
+      it 'returns http success' do
+        get :index
+
+        expect(response).to have_http_status(200)
+      end
+    end
+
+    context 'when trends are enabled' do
+      before { Setting.trends = true }
+
+      it 'returns http success' do
+        prepare_trends
+        stub_const('Api::BaseController::DEFAULT_STATUSES_LIMIT', 2)
+        get :index
+
+        expect(response).to have_http_status(200)
+        expect(response.headers).to include('Link')
+      end
+
+      def prepare_trends
+        Fabricate.times(3, :status, trendable: true, language: 'en').each do |status|
+          2.times { |i| Trends.statuses.add(status, i) }
+        end
+        Trends::Statuses.new(threshold: 1, decay_threshold: -1).refresh
+      end
     end
   end
 end
diff --git a/spec/controllers/api/v1/trends/tags_controller_spec.rb b/spec/controllers/api/v1/trends/tags_controller_spec.rb
index 84370d841..9311392cd 100644
--- a/spec/controllers/api/v1/trends/tags_controller_spec.rb
+++ b/spec/controllers/api/v1/trends/tags_controller_spec.rb
@@ -6,16 +6,41 @@ RSpec.describe Api::V1::Trends::TagsController do
   render_views
 
   describe 'GET #index' do
-    before do
-      Fabricate.times(10, :tag).each do |tag|
-        10.times { |i| Trends.tags.add(tag, i) }
-      end
-
-      get :index
+    around do |example|
+      previous = Setting.trends
+      example.run
+      Setting.trends = previous
     end
 
-    it 'returns http success' do
-      expect(response).to have_http_status(200)
+    context 'when trends are disabled' do
+      before { Setting.trends = false }
+
+      it 'returns http success' do
+        get :index
+
+        expect(response).to have_http_status(200)
+        expect(response.headers).to_not include('Link')
+      end
+    end
+
+    context 'when trends are enabled' do
+      before { Setting.trends = true }
+
+      it 'returns http success' do
+        prepare_trends
+        stub_const('Api::V1::Trends::TagsController::DEFAULT_TAGS_LIMIT', 2)
+        get :index
+
+        expect(response).to have_http_status(200)
+        expect(response.headers).to include('Link')
+      end
+
+      def prepare_trends
+        Fabricate.times(3, :tag, trendable: true).each do |tag|
+          2.times { |i| Trends.tags.add(tag, i) }
+        end
+        Trends::Tags.new(threshold: 1).refresh
+      end
     end
   end
 end

From 7e3c10dec676b0a560493071c5950c7e5df759b2 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 13 Nov 2023 15:39:45 +0100
Subject: [PATCH 11/63] Add icons for private and disabled boost in web UI
 (#27817)

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 .../mastodon/components/status_action_bar.jsx        | 12 ++++++++++--
 .../features/status/components/action_bar.jsx        | 12 ++++++++++--
 app/javascript/svg-icons/repeat_disabled.svg         |  5 +++++
 app/javascript/svg-icons/repeat_private.svg          |  5 +++++
 config/webpack/rules/material_icons.js               |  2 +-
 5 files changed, 31 insertions(+), 5 deletions(-)
 create mode 100755 app/javascript/svg-icons/repeat_disabled.svg
 create mode 100755 app/javascript/svg-icons/repeat_private.svg

diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx
index 240174739..25eab91fe 100644
--- a/app/javascript/mastodon/components/status_action_bar.jsx
+++ b/app/javascript/mastodon/components/status_action_bar.jsx
@@ -19,6 +19,8 @@ import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/s
 import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg';
 import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg';
 
+import { ReactComponent as RepeatDisabledIcon } from 'mastodon/../svg-icons/repeat_disabled.svg';
+import { ReactComponent as RepeatPrivateIcon } from 'mastodon/../svg-icons/repeat_private.svg';
 import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
 import { WithRouterPropTypes } from 'mastodon/utils/react_router';
 
@@ -348,6 +350,7 @@ class StatusActionBar extends ImmutablePureComponent {
     let replyIcon;
     let replyIconComponent;
     let replyTitle;
+
     if (status.get('in_reply_to_id', null) === null) {
       replyIcon = 'reply';
       replyIconComponent = ReplyIcon;
@@ -360,15 +363,20 @@ class StatusActionBar extends ImmutablePureComponent {
 
     const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
 
-    let reblogTitle = '';
+    let reblogTitle, reblogIconComponent;
+
     if (status.get('reblogged')) {
       reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
+      reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
     } else if (publicStatus) {
       reblogTitle = intl.formatMessage(messages.reblog);
+      reblogIconComponent = RepeatIcon;
     } else if (reblogPrivate) {
       reblogTitle = intl.formatMessage(messages.reblog_private);
+      reblogIconComponent = RepeatPrivateIcon;
     } else {
       reblogTitle = intl.formatMessage(messages.cannot_reblog);
+      reblogIconComponent = RepeatDisabledIcon;
     }
 
     const filterButton = this.props.onFilter && (
@@ -380,7 +388,7 @@ class StatusActionBar extends ImmutablePureComponent {
     return (
       <div className='status__action-bar'>
         <IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
-        <IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
+        <IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
         <IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
         <IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
 
diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx
index eac0bab7e..663bce743 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.jsx
+++ b/app/javascript/mastodon/features/status/components/action_bar.jsx
@@ -18,6 +18,8 @@ import { ReactComponent as ReplyAllIcon } from '@material-symbols/svg-600/outlin
 import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/star-fill.svg';
 import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg';
 
+import { ReactComponent as RepeatDisabledIcon } from 'mastodon/../svg-icons/repeat_disabled.svg';
+import { ReactComponent as RepeatPrivateIcon } from 'mastodon/../svg-icons/repeat_private.svg';
 import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
 import { WithRouterPropTypes } from 'mastodon/utils/react_router';
 
@@ -280,6 +282,7 @@ class ActionBar extends PureComponent {
 
     let replyIcon;
     let replyIconComponent;
+
     if (status.get('in_reply_to_id', null) === null) {
       replyIcon = 'reply';
       replyIconComponent = ReplyIcon;
@@ -290,21 +293,26 @@ class ActionBar extends PureComponent {
 
     const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
 
-    let reblogTitle;
+    let reblogTitle, reblogIconComponent;
+
     if (status.get('reblogged')) {
       reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
+      reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
     } else if (publicStatus) {
       reblogTitle = intl.formatMessage(messages.reblog);
+      reblogIconComponent = RepeatIcon;
     } else if (reblogPrivate) {
       reblogTitle = intl.formatMessage(messages.reblog_private);
+      reblogIconComponent = RepeatPrivateIcon;
     } else {
       reblogTitle = intl.formatMessage(messages.cannot_reblog);
+      reblogIconComponent = RepeatDisabledIcon;
     }
 
     return (
       <div className='detailed-status__action-bar'>
         <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent}  onClick={this.handleReplyClick} /></div>
-        <div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} /></div>
+        <div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
         <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
         <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>
 
diff --git a/app/javascript/svg-icons/repeat_disabled.svg b/app/javascript/svg-icons/repeat_disabled.svg
new file mode 100755
index 000000000..3157f660e
--- /dev/null
+++ b/app/javascript/svg-icons/repeat_disabled.svg
@@ -0,0 +1,5 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M19 13V17.8787L17 15.8787V13H19Z"/>
+<path d="M2.41421 2.70711L1 4.12132L5 8.12132V11H7V10.1213L13.8787 17H6.85L8.4 15.45L7 14L3 18L7 22L8.4 20.55L6.85 19H15.8787L19.3848 22.5061L20.799 21.0919L2.41421 2.70711Z"/>
+<path d="M17.15 7H8.12132L6.12132 5H17.15L15.6 3.45L17 2L21 6L17 10L15.6 8.55L17.15 7Z"/>
+</svg>
diff --git a/app/javascript/svg-icons/repeat_private.svg b/app/javascript/svg-icons/repeat_private.svg
new file mode 100755
index 000000000..a65be532a
--- /dev/null
+++ b/app/javascript/svg-icons/repeat_private.svg
@@ -0,0 +1,5 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.4 15.45L7 14L3 18L7 22L8.4 20.55L6.85 19H13.5V18C13.5 17.6567 13.5638 17.3171 13.6988 17H6.85L8.4 15.45Z"/>
+<path d="M5 5V11H7V7H17.15L15.6 8.55L17 10L21 6L17 2L15.6 3.45L17.15 5H5Z"/>
+<path d="M16 22C15.7167 22 15.475 21.9083 15.275 21.725C15.0917 21.525 15 21.2833 15 21V18C15 17.7167 15.0917 17.4833 15.275 17.3C15.475 17.1 15.7167 17 16 17V16C16 15.45 16.1917 14.9833 16.575 14.6C16.975 14.2 17.45 14 18 14C18.55 14 19.0167 14.2 19.4 14.6C19.8 14.9833 20 15.45 20 16V17C20.2833 17 20.5167 17.1 20.7 17.3C20.9 17.4833 21 17.7167 21 18V21C21 21.2833 20.9 21.525 20.7 21.725C20.5167 21.9083 20.2833 22 20 22H16ZM17 17H19V16C19 15.7167 18.9 15.4833 18.7 15.3C18.5167 15.1 18.2833 15 18 15C17.7167 15 17.475 15.1 17.275 15.3C17.0917 15.4833 17 15.7167 17 16V17Z"/>
+</svg>
diff --git a/config/webpack/rules/material_icons.js b/config/webpack/rules/material_icons.js
index 7ac1072b0..048e198ef 100644
--- a/config/webpack/rules/material_icons.js
+++ b/config/webpack/rules/material_icons.js
@@ -1,6 +1,6 @@
 module.exports = {
   test: /\.svg$/,
-  include: /node_modules\/@material-symbols/,
+  include: [/node_modules\/@material-symbols/, /svg-icons/],
   issuer: /\.[jt]sx?$/,
   use: [
     {

From a1b48460e4e22cb552b35776a5cc03961c619d9f Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 13 Nov 2023 15:40:10 +0100
Subject: [PATCH 12/63] Update DefinitelyTyped types (non-major) (#27830)

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

diff --git a/package.json b/package.json
index 96bfb1571..6f20b98bc 100644
--- a/package.json
+++ b/package.json
@@ -176,7 +176,7 @@
     "@types/react-dom": "^18.2.4",
     "@types/react-helmet": "^6.1.6",
     "@types/react-immutable-proptypes": "^2.1.0",
-    "@types/react-motion": "^0.0.36",
+    "@types/react-motion": "^0.0.37",
     "@types/react-overlays": "^3.1.0",
     "@types/react-router": "^5.1.20",
     "@types/react-router-dom": "^5.3.3",
diff --git a/yarn.lock b/yarn.lock
index 891eb59fe..b05b2bfbc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2384,7 +2384,7 @@ __metadata:
     "@types/react-dom": "npm:^18.2.4"
     "@types/react-helmet": "npm:^6.1.6"
     "@types/react-immutable-proptypes": "npm:^2.1.0"
-    "@types/react-motion": "npm:^0.0.36"
+    "@types/react-motion": "npm:^0.0.37"
     "@types/react-overlays": "npm:^3.1.0"
     "@types/react-router": "npm:^5.1.20"
     "@types/react-router-dom": "npm:^5.3.3"
@@ -3006,15 +3006,15 @@ __metadata:
   linkType: hard
 
 "@types/babel__core@npm:*, @types/babel__core@npm:^7.1.12, @types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.1.7, @types/babel__core@npm:^7.20.1":
-  version: 7.20.3
-  resolution: "@types/babel__core@npm:7.20.3"
+  version: 7.20.4
+  resolution: "@types/babel__core@npm:7.20.4"
   dependencies:
     "@babel/parser": "npm:^7.20.7"
     "@babel/types": "npm:^7.20.7"
     "@types/babel__generator": "npm:*"
     "@types/babel__template": "npm:*"
     "@types/babel__traverse": "npm:*"
-  checksum: 5b5f9de4df7f995c2f06f3fdad39b58bc30121d1f2daceb97dd423c9b5dcbd5c464959338824e0dbee0c758bf55c4e9fe46fafd13bd29c1834afad04f291c588
+  checksum: 2adc7ec49de5f922271ce087cedee000de468a3e13f92b7b6254016bd8357298cb98e6d2b3c9defc69bb6e38e0c134ffe80776a8ce4e9fb167bbffcb4d7613b7
   languageName: node
   linkType: hard
 
@@ -3075,18 +3075,18 @@ __metadata:
   linkType: hard
 
 "@types/emoji-mart@npm:^3.0.9":
-  version: 3.0.11
-  resolution: "@types/emoji-mart@npm:3.0.11"
+  version: 3.0.12
+  resolution: "@types/emoji-mart@npm:3.0.12"
   dependencies:
     "@types/react": "npm:*"
-  checksum: 1080ee6d0286248310b1f29ed7e21f15bdb587dec924dc2f714ab60f9ecc4567dde84fb73e41336d72a3e77afd2546f3025f886474391dd5efe1884a6a8ef034
+  checksum: 146abafe3ce9f4954c7f6ed3603bdc5a897b2d6b99dd9da6065ef597a9a6a59fb914e907decbda29b661216ddf3da8bb34a28d50f3d6929efce2b3c42e73b085
   languageName: node
   linkType: hard
 
 "@types/escape-html@npm:^1.0.2":
-  version: 1.0.3
-  resolution: "@types/escape-html@npm:1.0.3"
-  checksum: 980ec83f07f91389ffc9d7043da40db49dc40879a57f743a422bf30aa1d730f33c880baab4b31aacce550f2b195a866ec95d66a14c517ea1416d03e9b462df9d
+  version: 1.0.4
+  resolution: "@types/escape-html@npm:1.0.4"
+  checksum: 5cdae9d38e97b1ad61180528ef7ca66bf6be96e875cc560c3e064d6ca75ccf2adaf2fa3b7bdd4f7494013e4357a2fb0bb62f2e59ca53097d5c45c7519d0ce9c3
   languageName: node
   linkType: hard
 
@@ -3127,14 +3127,14 @@ __metadata:
   linkType: hard
 
 "@types/express@npm:^4.17.17":
-  version: 4.17.20
-  resolution: "@types/express@npm:4.17.20"
+  version: 4.17.21
+  resolution: "@types/express@npm:4.17.21"
   dependencies:
     "@types/body-parser": "npm:*"
     "@types/express-serve-static-core": "npm:^4.17.33"
     "@types/qs": "npm:*"
     "@types/serve-static": "npm:*"
-  checksum: f73f5f92bd0a0fa4697598be3122c89522caa9e3bcb14c28b5e6d58a8e47f0301027478997153ae9ee4cf3d432576fb3fb0918ea0db521cc1204f8b759828a32
+  checksum: 12e562c4571da50c7d239e117e688dc434db1bac8be55613294762f84fd77fbd0658ccd553c7d3ab02408f385bc93980992369dd30e2ecd2c68c358e6af8fabf
   languageName: node
   linkType: hard
 
@@ -3165,12 +3165,12 @@ __metadata:
   linkType: hard
 
 "@types/hoist-non-react-statics@npm:^3.3.1":
-  version: 3.3.4
-  resolution: "@types/hoist-non-react-statics@npm:3.3.4"
+  version: 3.3.5
+  resolution: "@types/hoist-non-react-statics@npm:3.3.5"
   dependencies:
     "@types/react": "npm:*"
     hoist-non-react-statics: "npm:^3.3.0"
-  checksum: 677ddcacf72f2fb79f65599189d4741fa3d815aa5e50c7fd6c3c9be92298aecf05b044e7dc445cf2bb0877652293949a7f0e364e63d9266b242cd9cb9bf53fa7
+  checksum: 2a3b64bf3d9817d7830afa60ee314493c475fb09570a64e7737084cd482d2177ebdddf888ce837350bac51741278b077683facc9541f052d4bbe8487b4e3e618
   languageName: node
   linkType: hard
 
@@ -3182,18 +3182,18 @@ __metadata:
   linkType: hard
 
 "@types/http-link-header@npm:^1.0.3":
-  version: 1.0.4
-  resolution: "@types/http-link-header@npm:1.0.4"
+  version: 1.0.5
+  resolution: "@types/http-link-header@npm:1.0.5"
   dependencies:
     "@types/node": "npm:*"
-  checksum: 65035f11a49a2c4ea75a0a8de284a8808264e0a1b03e6be5cd8df8637b283be154f723d2eef091715ad3e98b8d252bc261aea109559e43702dfecae03e26263e
+  checksum: adeb13381b38c3625478149820772924c154b4a7250dca62c346810a8378f8968fc7f3a9a4f55ec61de5d06083637540f862c8a920f6a710310c9645d19a077d
   languageName: node
   linkType: hard
 
 "@types/intl@npm:^1.2.0":
-  version: 1.2.1
-  resolution: "@types/intl@npm:1.2.1"
-  checksum: b1c81eade8a9b7ee33b7642283b7d024c263d34c5efae24a517822bd87b8a01c1a2a8c71ade948346c785abad522858ec389247031b5404307c06ba51da52385
+  version: 1.2.2
+  resolution: "@types/intl@npm:1.2.2"
+  checksum: f465c320139c01dfc9ae1382406259fd23f6a455aad31517f61b7fd79bdde493e854d6666c2198ae644d8cf6e147e78831ea810f83a787f65b765bc56834f259
   languageName: node
   linkType: hard
 
@@ -3223,19 +3223,19 @@ __metadata:
   linkType: hard
 
 "@types/jest@npm:^29.5.2":
-  version: 29.5.7
-  resolution: "@types/jest@npm:29.5.7"
+  version: 29.5.8
+  resolution: "@types/jest@npm:29.5.8"
   dependencies:
     expect: "npm:^29.0.0"
     pretty-format: "npm:^29.0.0"
-  checksum: 231c873f3d1ddac973b8f8f2ad7760677d941d85fb52d1c5dc4a311bafba4c2c1658a1040fd7054a51f4d1841f51c6ca4cabf70675ee4fa9e10fc5b8066e1de1
+  checksum: a28e7827ea7e1a2aace6a386868fa6b8402c162d6c71570aed2c29d3745ddc22ceef6899a20643071817905d3c57b670a7992fc8760bff65939351fd4dc481cf
   languageName: node
   linkType: hard
 
 "@types/js-yaml@npm:^4.0.5":
-  version: 4.0.8
-  resolution: "@types/js-yaml@npm:4.0.8"
-  checksum: 171a5c54d5b5c86a89300d14a004c49321f1a290fd2f625e2ef682e100ce78715a0eb8eac1ff09114dadaec8ccdb98251ddb5e06f1f3d6aa2ec83930e7a16039
+  version: 4.0.9
+  resolution: "@types/js-yaml@npm:4.0.9"
+  checksum: 24de857aa8d61526bbfbbaa383aa538283ad17363fcd5bb5148e2c7f604547db36646440e739d78241ed008702a8920665d1add5618687b6743858fae00da211
   languageName: node
   linkType: hard
 
@@ -3272,9 +3272,9 @@ __metadata:
   linkType: hard
 
 "@types/lodash@npm:^4.14.195":
-  version: 4.14.200
-  resolution: "@types/lodash@npm:4.14.200"
-  checksum: 7a8dac6dc866f10d1888846d6189d1faeb4f65adb139f0837a005fd1adcde62e60d7e7abb1a2733d13fc57bebb337d74182d8ad3dbd1d211dcd0c310c47e81bc
+  version: 4.14.201
+  resolution: "@types/lodash@npm:4.14.201"
+  checksum: 14dc43787296c429433d7d034ed47c5ac24b92217056f80a0e6c990449120b9c9c1058918188945fb88353c0c8333c5c36dccc40c51edbd39b05d2169ab2e0ad
   languageName: node
   linkType: hard
 
@@ -3330,18 +3330,18 @@ __metadata:
   linkType: hard
 
 "@types/npmlog@npm:^4.1.4":
-  version: 4.1.5
-  resolution: "@types/npmlog@npm:4.1.5"
+  version: 4.1.6
+  resolution: "@types/npmlog@npm:4.1.6"
   dependencies:
     "@types/node": "npm:*"
-  checksum: 84b01941e0bc9c4e1ee145ee1278731e4a1d4236a88c62f94640a741aa323df7a20ff20344432f574184fa2b219ca7f7b549f724684cab4bc8dada7b3a13941c
+  checksum: 432bfb14b29a383e095e099b2ddff4266051b43bc6c7ea242d10194acde2f1181a1e967bbb543f07979dd77743ead1954abac1128ec78cc2b86a5f7ea841ddcb
   languageName: node
   linkType: hard
 
 "@types/object-assign@npm:^4.0.30":
-  version: 4.0.32
-  resolution: "@types/object-assign@npm:4.0.32"
-  checksum: 25a75c2c1f4b20ce95443ab247b153dfb00e4bf08b4716f01efae7063f6e49a8af96f4c752dc3a44d7da381b9730249b3ee97900b6336bf5221f242c9532e8c5
+  version: 4.0.33
+  resolution: "@types/object-assign@npm:4.0.33"
+  checksum: 7fbc399aa1140beff35a152e206bfb336dd880721f4a13cc1ea01d964ab376fa4ca2c19059145cbd777c9d3eaf724008faec8cf3becff97353c69560196af086
   languageName: node
   linkType: hard
 
@@ -3353,13 +3353,13 @@ __metadata:
   linkType: hard
 
 "@types/pg@npm:^8.6.6":
-  version: 8.10.7
-  resolution: "@types/pg@npm:8.10.7"
+  version: 8.10.9
+  resolution: "@types/pg@npm:8.10.9"
   dependencies:
     "@types/node": "npm:*"
     pg-protocol: "npm:*"
     pg-types: "npm:^4.0.1"
-  checksum: beea456e9f4011d07e318b57d8c96af26379c658ad30d242a3194520d41406ca7bfa19a941405b7db36e401aa07f34cd035bdc5ac3d7681712c47a48df3cd09e
+  checksum: 6b3bec7230d09da6459636a66dfd6fb538378e466ffff0a0bcd07d67aa4ddce49c73afc7442f53adec92a49dbf9e71d8d847e0075750d7545331735dfd92d22c
   languageName: node
   linkType: hard
 
@@ -3371,16 +3371,16 @@ __metadata:
   linkType: hard
 
 "@types/prop-types@npm:*, @types/prop-types@npm:^15.7.5":
-  version: 15.7.9
-  resolution: "@types/prop-types@npm:15.7.9"
-  checksum: e2a7373b91a8eb30cb4e399ef5b3a14baa7d72eed1667ef5e3cb1e9400edfca9b60c20b845fefdcf7562773829f6ff60ba350b09f6313a8093e70c15b2b88f00
+  version: 15.7.10
+  resolution: "@types/prop-types@npm:15.7.10"
+  checksum: 964348d05cdf7399260b3179fbd1462b23d7452dc39fbccb319e54ed6ffafb0a01c0a62c3e6f7c944a635c7a4ad5c99d62c4787c9c4b74e2ec07aebaf6cfedc1
   languageName: node
   linkType: hard
 
 "@types/punycode@npm:^2.1.0":
-  version: 2.1.1
-  resolution: "@types/punycode@npm:2.1.1"
-  checksum: 3e5c3c786790111ac497d7a472c762671c68ec58edce0a88757c503e997eb500af1b8584c948b0a9927394b0f15828faa8f986130eb4ebb0fde0331c2f515630
+  version: 2.1.2
+  resolution: "@types/punycode@npm:2.1.2"
+  checksum: 4a748533bde61097f205638b3acc2c3b0e25382d2c35a2e3b60c33c904afbad158ef778ae3e1b3f3c84edd7b04b6d9fa049f2832210c308d3fee77ba7b637cb9
   languageName: node
   linkType: hard
 
@@ -3406,39 +3406,39 @@ __metadata:
   linkType: hard
 
 "@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.2.4":
-  version: 18.2.14
-  resolution: "@types/react-dom@npm:18.2.14"
+  version: 18.2.15
+  resolution: "@types/react-dom@npm:18.2.15"
   dependencies:
     "@types/react": "npm:*"
-  checksum: 1f79a7708d038cd651bdb21e01a99c594761bc9a40a565abe98958e1d27facfeb6e9824ddf6ae3504e7a56568f0f3da2380fe52ac18477b5864d2d5cf1386a9e
+  checksum: 70e86f15f69f89b8f179139ab2e8a8aa9765e742789f5dd5a46fec40d4300ada8fe3349cceda42b9964a018982d7ccb7d791b47f781966c992bfd37da909fbd3
   languageName: node
   linkType: hard
 
 "@types/react-helmet@npm:^6.1.6":
-  version: 6.1.8
-  resolution: "@types/react-helmet@npm:6.1.8"
+  version: 6.1.9
+  resolution: "@types/react-helmet@npm:6.1.9"
   dependencies:
     "@types/react": "npm:*"
-  checksum: 56efbf05594437fff87466853b813c0672b33fbed1f995b9eb68e1461875cd5276b36c7fbc171134e9b7b897316123ae59b0263742d5a81e28ead133a4962bd2
+  checksum: d1823582903d6e70f1f447c7bec9e844b6f85f5de84cbcde5c8bbeecc064db1394c786ed9b9ded30544afe5c91e57c7e8105171df1643998f64c0aeab9f7f2aa
   languageName: node
   linkType: hard
 
 "@types/react-immutable-proptypes@npm:^2.1.0":
-  version: 2.1.2
-  resolution: "@types/react-immutable-proptypes@npm:2.1.2"
+  version: 2.1.3
+  resolution: "@types/react-immutable-proptypes@npm:2.1.3"
   dependencies:
     "@types/prop-types": "npm:*"
     immutable: "npm:^3.8.2"
-  checksum: 8a944de2b4b484e9685495dee7b350a88e4fcba47dcecf97c7b4add6ae66b61fc61c18e877a9b8e8d453e7c4714c481166d5531328f92b2d20d45e11389734ee
+  checksum: 4dab74a43a2dde9bea6299a999dd600ae82f00082fe2b8865b11e5154e658f72fbb117132fa3753dd9a280dd8032a2574d8e7c94de5e268afdadd50d720086da
   languageName: node
   linkType: hard
 
-"@types/react-motion@npm:^0.0.36":
-  version: 0.0.36
-  resolution: "@types/react-motion@npm:0.0.36"
+"@types/react-motion@npm:^0.0.37":
+  version: 0.0.37
+  resolution: "@types/react-motion@npm:0.0.37"
   dependencies:
     "@types/react": "npm:*"
-  checksum: b039d9b6773a08292253f1343ee7c1c5ad2885499685aef2a0274a84a7bbe4e198b7aafa62b9c269fb37df9e076ae46ee9717fc53f0c26ca182342aaf1653139
+  checksum: 387f60636d9bdd2e765ce94db969cf762a62495e32807f88380748a74e9beeb3d8e17c3ec334dab8040244ea62e7954d5f4d4bdbdd0ecc8985eb4a6ce465b61c
   languageName: node
   linkType: hard
 
@@ -3482,29 +3482,29 @@ __metadata:
   linkType: hard
 
 "@types/react-sparklines@npm:^1.7.2":
-  version: 1.7.4
-  resolution: "@types/react-sparklines@npm:1.7.4"
+  version: 1.7.5
+  resolution: "@types/react-sparklines@npm:1.7.5"
   dependencies:
     "@types/react": "npm:*"
-  checksum: 1f50232fbd26d2b81508f68dbe1865503b1f9e053aae4ea2d610c771da219af4e6fa86e58fda542e2762f533042f83acab2d977ae2cb1b518ed94ba31aac8504
+  checksum: acb0937ebc06019921ec5254fb125356f206038f5e2f244663eb849c692b6f6413f75ce3ee84be91d8c659ae43c8f743dd5c4397cdea65749cd601a495491242
   languageName: node
   linkType: hard
 
 "@types/react-swipeable-views@npm:^0.13.1":
-  version: 0.13.4
-  resolution: "@types/react-swipeable-views@npm:0.13.4"
+  version: 0.13.5
+  resolution: "@types/react-swipeable-views@npm:0.13.5"
   dependencies:
     "@types/react": "npm:*"
-  checksum: 673e96a66443b07c5640c94b214fd0780846ba9e983c821f7cc7c8bdc0ab512e65b6a2af7e4aeb0f565ee5a63f30cf2bdf4d9da7a4d9d3dfe2e27d2d8f6c1200
+  checksum: d1dcc78d862f37d30a43d79d915fdb388e05dce0b2ac07462ca4f1b00e0eef37cb41d75997f5685dec79bcce1ffee0dfbc744f20d5266dd3090658def5b4e193
   languageName: node
   linkType: hard
 
 "@types/react-test-renderer@npm:^18.0.0":
-  version: 18.0.5
-  resolution: "@types/react-test-renderer@npm:18.0.5"
+  version: 18.0.6
+  resolution: "@types/react-test-renderer@npm:18.0.6"
   dependencies:
     "@types/react": "npm:*"
-  checksum: bd98abad08d04081bcf335fa71c450b9f0535ccba2c1395d11254427e1007a228d481d6868879d23b6239c0fede790de77ef6bf31afc735241e3edc63bb0e865
+  checksum: f490d4379e8d095f8fa91700ceb37d0fe5a96d7cc0c51e9d7127fc3d2dc4e37a382dd6215b295b300037985cb8938cb5088130102ad14b79e4622e7e520c5a3b
   languageName: node
   linkType: hard
 
@@ -3518,11 +3518,11 @@ __metadata:
   linkType: hard
 
 "@types/react-toggle@npm:^4.0.3":
-  version: 4.0.4
-  resolution: "@types/react-toggle@npm:4.0.4"
+  version: 4.0.5
+  resolution: "@types/react-toggle@npm:4.0.5"
   dependencies:
     "@types/react": "npm:*"
-  checksum: 86015101f1ddfd2c31396a8f4fff7b2b442af886ed1301b4470d864b41bec91c46046d3a4e1d97a838c3069138db2f9628f885a7d9f95199bfa27a46d9df33de
+  checksum: f557b85c96715b145bcc3beb2903f88ee3a6045ef85da0f80561c7cc2ecdc531e2d4ae121ed8ec3a1761264de25b8410653744093f37abf042201587add7ffa6
   languageName: node
   linkType: hard
 
@@ -3536,30 +3536,30 @@ __metadata:
   linkType: hard
 
 "@types/react@npm:*, @types/react@npm:16 || 17 || 18, @types/react@npm:>=16.9.11, @types/react@npm:^18.2.7":
-  version: 18.2.36
-  resolution: "@types/react@npm:18.2.36"
+  version: 18.2.37
+  resolution: "@types/react@npm:18.2.37"
   dependencies:
     "@types/prop-types": "npm:*"
     "@types/scheduler": "npm:*"
     csstype: "npm:^3.0.2"
-  checksum: 02b593041e9c25aaf519b5e4f87282aff559c0f3db214d7db68fee714d8286c09ab1fced68184fbe814e061019024bb479bbcd38b07985e3e794d98c96c49123
+  checksum: 79dd5d23da05bec54e7423ca17096e345eb8fd80a3bf8dd916bb5cdd60677d27c298523aa5b245d090fcc4ec100cfd58c1af4631fbac709d0a9d8be75f9d78a9
   languageName: node
   linkType: hard
 
 "@types/redux-immutable@npm:^4.0.3":
-  version: 4.0.5
-  resolution: "@types/redux-immutable@npm:4.0.5"
+  version: 4.0.6
+  resolution: "@types/redux-immutable@npm:4.0.6"
   dependencies:
     immutable: "npm:^4.0.0-rc.1"
     redux: "npm:^4.0.0"
-  checksum: 1fd808b86506b8d32745159397172fcc9e890766b4330a001c35254d695edeb142d2a17c7ad6592853dfccea1840e632634651bcd0af882f445699d1238813dd
+  checksum: 19972c307f2bbd31a201b28844224ce06e34917823cf2ded3c303cffcced273a2107c4186cc37af9db4f948204fc48c461e609b9f3d2719963049dedae3ebf82
   languageName: node
   linkType: hard
 
 "@types/requestidlecallback@npm:^0.3.5":
-  version: 0.3.6
-  resolution: "@types/requestidlecallback@npm:0.3.6"
-  checksum: 5a3df7a028fc6894446c14a67c93186f7978cf97d41a0da74a56d4c95ca9580593eb7baaaa385fa54d46f3c93f5789b21548a3708a5a0cc08302aa7e7f037e30
+  version: 0.3.7
+  resolution: "@types/requestidlecallback@npm:0.3.7"
+  checksum: aa5d1d981d7ddc98b9212c75c89d2ddb2e521077d6c0c5e285d944a8c6ae8baeec30d4d201aec31716d668d3435f884e80e768e28d929a5b87a55097bc21a5e1
   languageName: node
   linkType: hard
 
@@ -3659,9 +3659,9 @@ __metadata:
   linkType: hard
 
 "@types/uuid@npm:^9.0.0":
-  version: 9.0.6
-  resolution: "@types/uuid@npm:9.0.6"
-  checksum: 8fb6b3a583a035b8e917192eeadaadadfbfd29315094aafd3478e11f11a986cb118ee0f388b15035fda063d9f1a32fa62e7a791215b762fe1e2c177929ca7146
+  version: 9.0.7
+  resolution: "@types/uuid@npm:9.0.7"
+  checksum: b329ebd4f9d1d8e08d4f2cc211be4922d70d1149f73d5772630e4a3acfb5170c6d37b3d7a39a0412f1a56e86e8a844c7f297c798b082f90380608bf766688787
   languageName: node
   linkType: hard
 
@@ -3684,8 +3684,8 @@ __metadata:
   linkType: hard
 
 "@types/webpack@npm:^4.41.33":
-  version: 4.41.35
-  resolution: "@types/webpack@npm:4.41.35"
+  version: 4.41.36
+  resolution: "@types/webpack@npm:4.41.36"
   dependencies:
     "@types/node": "npm:*"
     "@types/tapable": "npm:^1"
@@ -3693,7 +3693,7 @@ __metadata:
     "@types/webpack-sources": "npm:*"
     anymatch: "npm:^3.0.0"
     source-map: "npm:^0.6.0"
-  checksum: ec6b9fda027ff6f7f9efeb4bcfb37607a15fe1e2f2f0883e3500fe474ba0a127ea62783cd19859bbef921cfad22cd680e50bcc2c1963482c6486263886877130
+  checksum: 9e9021049b8f7482ec7c45e95d7c1da3604ee04481d7550421ed58f201f66edb8eb6c26b85be94ce3f761874cdbb8b570827becdaf5acdb1897aba9b7f226252
   languageName: node
   linkType: hard
 
@@ -3705,11 +3705,11 @@ __metadata:
   linkType: hard
 
 "@types/yargs@npm:^17.0.24, @types/yargs@npm:^17.0.8":
-  version: 17.0.29
-  resolution: "@types/yargs@npm:17.0.29"
+  version: 17.0.31
+  resolution: "@types/yargs@npm:17.0.31"
   dependencies:
     "@types/yargs-parser": "npm:*"
-  checksum: d8c965c101f7ee3e2f301c02a83dfd5680e4d999d3503c788c13f336868f03ee1498f019552e7d357635a1a36912cbe6751a563e9c339075d30cd131dc361c98
+  checksum: 1e04df99bd0ad8ac8b3748b6ac0e99a9a4efe20b9cd8eab69ac9503fe87ab9bec312ad56982e969cdb0e2c0679431434ad571f6934049adb15fa35b22810c867
   languageName: node
   linkType: hard
 

From da5940752055369451e545c664aecdfb3821960b Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 13 Nov 2023 15:40:50 +0100
Subject: [PATCH 13/63] Update eslint (non-major) (#27831)

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

diff --git a/package.json b/package.json
index 6f20b98bc..2230b20ea 100644
--- a/package.json
+++ b/package.json
@@ -200,7 +200,7 @@
     "eslint-plugin-formatjs": "^4.10.1",
     "eslint-plugin-import": "~2.29.0",
     "eslint-plugin-jsdoc": "^46.1.0",
-    "eslint-plugin-jsx-a11y": "~6.7.1",
+    "eslint-plugin-jsx-a11y": "~6.8.0",
     "eslint-plugin-prettier": "^5.0.0",
     "eslint-plugin-promise": "~6.1.1",
     "eslint-plugin-react": "~7.33.0",
diff --git a/yarn.lock b/yarn.lock
index b05b2bfbc..e8fe28fb4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1482,7 +1482,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.1, @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.20.7, @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":
+"@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.1, @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":
   version: 7.23.2
   resolution: "@babel/runtime@npm:7.23.2"
   dependencies:
@@ -1697,14 +1697,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@es-joy/jsdoccomment@npm:~0.40.1":
-  version: 0.40.1
-  resolution: "@es-joy/jsdoccomment@npm:0.40.1"
+"@es-joy/jsdoccomment@npm:~0.41.0":
+  version: 0.41.0
+  resolution: "@es-joy/jsdoccomment@npm:0.41.0"
   dependencies:
-    comment-parser: "npm:1.4.0"
+    comment-parser: "npm:1.4.1"
     esquery: "npm:^1.5.0"
     jsdoc-type-pratt-parser: "npm:~4.0.0"
-  checksum: e66b861c55cf26d22c0facef911d65abbbbf633a9fc47cbf0f0faa4226e495cbce5133f4e69f555cd4c018a13dabb37f8a36d631ba768b9297913154b06a04af
+  checksum: 1fa27531eba32e4699664da53a0865aeeda1f7e83ac156fe53b7a6b09d2f3816baa94a34845ff019c10289b09572bda5519ec917e3e241088975477fa880f72d
   languageName: node
   linkType: hard
 
@@ -1790,16 +1790,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/ecma402-abstract@npm:1.17.2":
-  version: 1.17.2
-  resolution: "@formatjs/ecma402-abstract@npm:1.17.2"
-  dependencies:
-    "@formatjs/intl-localematcher": "npm:0.4.2"
-    tslib: "npm:^2.4.0"
-  checksum: 7086962b6f6fd517188e9640e8439062e125e7bd60852ae91ceadb259fceaa3f73f7403a77411a7c82b1ae60be4bf4b27f793acc4059214adc91b00682d880fe
-  languageName: node
-  linkType: hard
-
 "@formatjs/ecma402-abstract@npm:1.17.4":
   version: 1.17.4
   resolution: "@formatjs/ecma402-abstract@npm:1.17.4"
@@ -1819,17 +1809,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/icu-messageformat-parser@npm:2.7.0":
-  version: 2.7.0
-  resolution: "@formatjs/icu-messageformat-parser@npm:2.7.0"
-  dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.2"
-    "@formatjs/icu-skeleton-parser": "npm:1.6.2"
-    tslib: "npm:^2.4.0"
-  checksum: f671d3dfcfa8ada17d14388f21be4cf1c535cbad8aba9cd6a3132d3120424cb7fb090f67b27e44ffbd0c7d4bba9f20e76b74cfca87f4ab945939e7ea3acb878c
-  languageName: node
-  linkType: hard
-
 "@formatjs/icu-messageformat-parser@npm:2.7.2":
   version: 2.7.2
   resolution: "@formatjs/icu-messageformat-parser@npm:2.7.2"
@@ -1841,16 +1820,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/icu-skeleton-parser@npm:1.6.2":
-  version: 1.6.2
-  resolution: "@formatjs/icu-skeleton-parser@npm:1.6.2"
-  dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.2"
-    tslib: "npm:^2.4.0"
-  checksum: 2a2a56f49a15e8122b37237d5e09a817e01149ae353e1b5fe8721d1789dbaee85995c897d3aa9e5b400e3ee05b5fd4c0721b3ad49b1d128954dfed873a793153
-  languageName: node
-  linkType: hard
-
 "@formatjs/icu-skeleton-parser@npm:1.6.4":
   version: 1.6.4
   resolution: "@formatjs/icu-skeleton-parser@npm:1.6.4"
@@ -1883,15 +1852,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/intl-localematcher@npm:0.4.2":
-  version: 0.4.2
-  resolution: "@formatjs/intl-localematcher@npm:0.4.2"
-  dependencies:
-    tslib: "npm:^2.4.0"
-  checksum: 2521fa48a95a80e3bedc0d444fb2ef67e1215e0bf9e6d16020c4a22af6973849a71c7a29a10cb74fc67b818967e9f8672062760e808e70873132277830e0ec67
-  languageName: node
-  linkType: hard
-
 "@formatjs/intl-localematcher@npm:0.5.1":
   version: 0.5.1
   resolution: "@formatjs/intl-localematcher@npm:0.5.1"
@@ -1932,26 +1892,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/ts-transformer@npm:3.13.6":
-  version: 3.13.6
-  resolution: "@formatjs/ts-transformer@npm:3.13.6"
-  dependencies:
-    "@formatjs/icu-messageformat-parser": "npm:2.7.0"
-    "@types/json-stable-stringify": "npm:^1.0.32"
-    "@types/node": "npm:14 || 16 || 17"
-    chalk: "npm:^4.0.0"
-    json-stable-stringify: "npm:^1.0.1"
-    tslib: "npm:^2.4.0"
-    typescript: "npm:5"
-  peerDependencies:
-    ts-jest: ">=27"
-  peerDependenciesMeta:
-    ts-jest:
-      optional: true
-  checksum: 76cd99713a974bf63081d9f7b98997dba0fc998205b5bc7cb6249dc8572549408b62a42c394b6d589905368203b37862e0c9248eef5aaa94b8de0b423fa8b508
-  languageName: node
-  linkType: hard
-
 "@formatjs/ts-transformer@npm:3.13.8":
   version: 3.13.8
   resolution: "@formatjs/ts-transformer@npm:3.13.8"
@@ -2432,7 +2372,7 @@ __metadata:
     eslint-plugin-formatjs: "npm:^4.10.1"
     eslint-plugin-import: "npm:~2.29.0"
     eslint-plugin-jsdoc: "npm:^46.1.0"
-    eslint-plugin-jsx-a11y: "npm:~6.7.1"
+    eslint-plugin-jsx-a11y: "npm:~6.8.0"
     eslint-plugin-prettier: "npm:^5.0.0"
     eslint-plugin-promise: "npm:~6.1.1"
     eslint-plugin-react: "npm:~7.33.0"
@@ -3714,14 +3654,14 @@ __metadata:
   linkType: hard
 
 "@typescript-eslint/eslint-plugin@npm:^6.0.0":
-  version: 6.9.1
-  resolution: "@typescript-eslint/eslint-plugin@npm:6.9.1"
+  version: 6.10.0
+  resolution: "@typescript-eslint/eslint-plugin@npm:6.10.0"
   dependencies:
     "@eslint-community/regexpp": "npm:^4.5.1"
-    "@typescript-eslint/scope-manager": "npm:6.9.1"
-    "@typescript-eslint/type-utils": "npm:6.9.1"
-    "@typescript-eslint/utils": "npm:6.9.1"
-    "@typescript-eslint/visitor-keys": "npm:6.9.1"
+    "@typescript-eslint/scope-manager": "npm:6.10.0"
+    "@typescript-eslint/type-utils": "npm:6.10.0"
+    "@typescript-eslint/utils": "npm:6.10.0"
+    "@typescript-eslint/visitor-keys": "npm:6.10.0"
     debug: "npm:^4.3.4"
     graphemer: "npm:^1.4.0"
     ignore: "npm:^5.2.4"
@@ -3734,44 +3674,44 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: f2455fe74f8c90d82df97801ee5e17bb3d81b1a59d23eedc7cad95b6eed30180339107bef0460578d5cc587033bb388fd112bae48b6b85f504fe4521f365ac69
+  checksum: f50b17cb753afbfc99549d38585eba8558949b977eb4661dd584e73ee946b3dbe944c9e3b12a233fa06b5e1c7d101730ac88a00c7a91b0a7f1e2c37a98e13c7a
   languageName: node
   linkType: hard
 
 "@typescript-eslint/parser@npm:^6.0.0":
-  version: 6.9.1
-  resolution: "@typescript-eslint/parser@npm:6.9.1"
+  version: 6.10.0
+  resolution: "@typescript-eslint/parser@npm:6.10.0"
   dependencies:
-    "@typescript-eslint/scope-manager": "npm:6.9.1"
-    "@typescript-eslint/types": "npm:6.9.1"
-    "@typescript-eslint/typescript-estree": "npm:6.9.1"
-    "@typescript-eslint/visitor-keys": "npm:6.9.1"
+    "@typescript-eslint/scope-manager": "npm:6.10.0"
+    "@typescript-eslint/types": "npm:6.10.0"
+    "@typescript-eslint/typescript-estree": "npm:6.10.0"
+    "@typescript-eslint/visitor-keys": "npm:6.10.0"
     debug: "npm:^4.3.4"
   peerDependencies:
     eslint: ^7.0.0 || ^8.0.0
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: a6896655b2005a55e15dd3bb8b8239e1cb1bb0379037f6af2409e910c426cf9cda5490d45cd1857a3ca7fe2727acc8250d8196770147a4dc274e4c700ead9d9c
+  checksum: fd86c31dfdde03636393a3a9cf16716856bb506923069f34d87af14fac363a33578f47476a15d272e4d7a764de00fd905ee11361cc06b81b302a9fa8ebe4c23c
   languageName: node
   linkType: hard
 
-"@typescript-eslint/scope-manager@npm:6.9.1":
-  version: 6.9.1
-  resolution: "@typescript-eslint/scope-manager@npm:6.9.1"
+"@typescript-eslint/scope-manager@npm:6.10.0":
+  version: 6.10.0
+  resolution: "@typescript-eslint/scope-manager@npm:6.10.0"
   dependencies:
-    "@typescript-eslint/types": "npm:6.9.1"
-    "@typescript-eslint/visitor-keys": "npm:6.9.1"
-  checksum: 53fa7c3813d22b119e464f9b6d7d23407dfe103ee8ad2dcacf9ad6d656fda20e2bb3346df39e62b0e6b6ce71572ce5838071c5d2cca6daa4e0ce117ff22eafe5
+    "@typescript-eslint/types": "npm:6.10.0"
+    "@typescript-eslint/visitor-keys": "npm:6.10.0"
+  checksum: a5fbee770d763852a7f426b950d495529139f1629fdcb30136c93f787acd82236db4272f78dff1d05a3a10a6406472ae95ae94ab75cfb618a06d75b8cc536cbf
   languageName: node
   linkType: hard
 
-"@typescript-eslint/type-utils@npm:6.9.1":
-  version: 6.9.1
-  resolution: "@typescript-eslint/type-utils@npm:6.9.1"
+"@typescript-eslint/type-utils@npm:6.10.0":
+  version: 6.10.0
+  resolution: "@typescript-eslint/type-utils@npm:6.10.0"
   dependencies:
-    "@typescript-eslint/typescript-estree": "npm:6.9.1"
-    "@typescript-eslint/utils": "npm:6.9.1"
+    "@typescript-eslint/typescript-estree": "npm:6.10.0"
+    "@typescript-eslint/utils": "npm:6.10.0"
     debug: "npm:^4.3.4"
     ts-api-utils: "npm:^1.0.1"
   peerDependencies:
@@ -3779,23 +3719,23 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 9373c32c9bce736527e01baabc1dbee4b7f43774ebdcbbe20ee9cf61d1b01e7faab3d5df1ebbe75241308c52eabbc9500dd7826701f95caee4054ca659420304
+  checksum: f7c425d4da4d53d78b3d6630216dc1f2809f8dcaed62dc3cf12252102a53103a2aa39a160b310ca1cedebf87b8c339013be0c2360710c7c836b775374730c10e
   languageName: node
   linkType: hard
 
-"@typescript-eslint/types@npm:6.9.1":
-  version: 6.9.1
-  resolution: "@typescript-eslint/types@npm:6.9.1"
-  checksum: 4ba21ba18e256da210a4caedfbc5d4927cf8cb4f2c4d74f8ccc865576f3659b974e79119d3c94db2b68a4cec9cd687e43971d355450b7082d6d1736a5dd6db85
+"@typescript-eslint/types@npm:6.10.0":
+  version: 6.10.0
+  resolution: "@typescript-eslint/types@npm:6.10.0"
+  checksum: 30f47de625405b3729db6d26a0376d98628bd966c70ca01fab1adcef91bba810d27ce643d844e42d1cc77bb2c6277e62efe278a090da63ba748dfe5710c4757b
   languageName: node
   linkType: hard
 
-"@typescript-eslint/typescript-estree@npm:6.9.1":
-  version: 6.9.1
-  resolution: "@typescript-eslint/typescript-estree@npm:6.9.1"
+"@typescript-eslint/typescript-estree@npm:6.10.0":
+  version: 6.10.0
+  resolution: "@typescript-eslint/typescript-estree@npm:6.10.0"
   dependencies:
-    "@typescript-eslint/types": "npm:6.9.1"
-    "@typescript-eslint/visitor-keys": "npm:6.9.1"
+    "@typescript-eslint/types": "npm:6.10.0"
+    "@typescript-eslint/visitor-keys": "npm:6.10.0"
     debug: "npm:^4.3.4"
     globby: "npm:^11.1.0"
     is-glob: "npm:^4.0.3"
@@ -3804,34 +3744,34 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 850b1865a90107879186c3f2969968a2c08fc6fcc56d146483c297cf5be376e33d505ac81533ba8e8103ca4d2edfea7d21b178de9e52217f7ee2922f51a445fa
+  checksum: ca28ca5a55e2d431c649ad093e4a4302f2b37c430bbeebbe622b05c727fd14dab136aead5a96848499d3ff4d187889733f8871b8dd5205d19bed4a260ad74544
   languageName: node
   linkType: hard
 
-"@typescript-eslint/utils@npm:6.9.1, @typescript-eslint/utils@npm:^6.5.0":
-  version: 6.9.1
-  resolution: "@typescript-eslint/utils@npm:6.9.1"
+"@typescript-eslint/utils@npm:6.10.0, @typescript-eslint/utils@npm:^6.5.0":
+  version: 6.10.0
+  resolution: "@typescript-eslint/utils@npm:6.10.0"
   dependencies:
     "@eslint-community/eslint-utils": "npm:^4.4.0"
     "@types/json-schema": "npm:^7.0.12"
     "@types/semver": "npm:^7.5.0"
-    "@typescript-eslint/scope-manager": "npm:6.9.1"
-    "@typescript-eslint/types": "npm:6.9.1"
-    "@typescript-eslint/typescript-estree": "npm:6.9.1"
+    "@typescript-eslint/scope-manager": "npm:6.10.0"
+    "@typescript-eslint/types": "npm:6.10.0"
+    "@typescript-eslint/typescript-estree": "npm:6.10.0"
     semver: "npm:^7.5.4"
   peerDependencies:
     eslint: ^7.0.0 || ^8.0.0
-  checksum: 3d329d54c3d155ed29e2b456a602aef76bda1b88dfcf847145849362e4ddefabe5c95de236de750d08d5da9bedcfb2131bdfd784ce4eb87cf82728f0b6662033
+  checksum: 809a1d08b154f76ed7a99edddf872369f6ed93987cea19a18cb9f12b8390bddcff9138d9d94955545da54488d59e0001054bec13baf6d858a1761b059480b887
   languageName: node
   linkType: hard
 
-"@typescript-eslint/visitor-keys@npm:6.9.1":
-  version: 6.9.1
-  resolution: "@typescript-eslint/visitor-keys@npm:6.9.1"
+"@typescript-eslint/visitor-keys@npm:6.10.0":
+  version: 6.10.0
+  resolution: "@typescript-eslint/visitor-keys@npm:6.10.0"
   dependencies:
-    "@typescript-eslint/types": "npm:6.9.1"
+    "@typescript-eslint/types": "npm:6.10.0"
     eslint-visitor-keys: "npm:^3.4.1"
-  checksum: ac5f375a177add30489e5b63cafa8d82a196b33624bb36418422ebe0d7973b3ba550dc7e0dda36ea75a94cf9b200b4fb5f5fb4d77c027fd801201c1a269d343b
+  checksum: f9223c148655ce00bb17db8aa92ee964e62c75d15095893e0b4d653c60a4033f456329b06de3eab4b404d8df359904f0dd6e3c8c842885c6d130e28ccd95ce03
   languageName: node
   linkType: hard
 
@@ -4347,7 +4287,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"aria-query@npm:^5.0.0, aria-query@npm:^5.1.3":
+"aria-query@npm:^5.0.0, aria-query@npm:^5.3.0":
   version: 5.3.0
   resolution: "aria-query@npm:5.3.0"
   dependencies:
@@ -4565,10 +4505,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"ast-types-flow@npm:^0.0.7":
-  version: 0.0.7
-  resolution: "ast-types-flow@npm:0.0.7"
-  checksum: f381529f2da535949ba6cceddbdfaa33b4d5105842e147ec63582f560ea9ecc1a08f66457664f3109841d3053641fa8b9fa94ba607f1ea9f6c804fe5dee44a1d
+"ast-types-flow@npm:^0.0.8":
+  version: 0.0.8
+  resolution: "ast-types-flow@npm:0.0.8"
+  checksum: f2a0ba8055353b743c41431974521e5e852a9824870cd6fce2db0e538ac7bf4da406bbd018d109af29ff3f8f0993f6a730c9eddbd0abd031fbcb29ca75c1014e
   languageName: node
   linkType: hard
 
@@ -4675,10 +4615,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"axe-core@npm:^4.6.2":
-  version: 4.7.2
-  resolution: "axe-core@npm:4.7.2"
-  checksum: 8dfc61f038fbd9623ae8a264c8a475d887113a027fb440a2b377b82ffd300e71d1a0bcf042ff13b517a8d548b34c44b4159eff693725c5d7cde240d0aa68feac
+"axe-core@npm:=4.7.0":
+  version: 4.7.0
+  resolution: "axe-core@npm:4.7.0"
+  checksum: 89ac5712b5932ac7d23398b4cb5ba081c394a086e343acc68ba49c83472706e18e0799804e8388c779dcdacc465377deb29f2714241d3fbb389cf3a6b275c9ba
   languageName: node
   linkType: hard
 
@@ -4693,7 +4633,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"axobject-query@npm:^3.1.1":
+"axobject-query@npm:^3.2.1":
   version: 3.2.1
   resolution: "axobject-query@npm:3.2.1"
   dependencies:
@@ -5807,10 +5747,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"comment-parser@npm:1.4.0":
-  version: 1.4.0
-  resolution: "comment-parser@npm:1.4.0"
-  checksum: c87ba95d5ff9ae380ed7aab2aa8490303652d535c0cff5b1f16a97be0633d0827d689b5e854b0003fbb6341ce22caf000a03eb1badcdfbb142d7aea8f921c12b
+"comment-parser@npm:1.4.1":
+  version: 1.4.1
+  resolution: "comment-parser@npm:1.4.1"
+  checksum: d6c4be3f5be058f98b24f2d557f745d8fe1cc9eb75bebbdccabd404a0e1ed41563171b16285f593011f8b6a5ec81f564fb1f2121418ac5cbf0f49255bf0840dd
   languageName: node
   linkType: hard
 
@@ -6614,13 +6554,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0":
-  version: 1.2.0
-  resolution: "define-properties@npm:1.2.0"
+"define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1":
+  version: 1.2.1
+  resolution: "define-properties@npm:1.2.1"
   dependencies:
+    define-data-property: "npm:^1.0.1"
     has-property-descriptors: "npm:^1.0.0"
     object-keys: "npm:^1.1.1"
-  checksum: 34b58cae4651936a3c8c720310ce393a3227f5123640ab5402e7d6e59bb44f8295b789cb5d74e7513682b2e60ff20586d6f52b726d964d617abffa3da76344e0
+  checksum: 88a152319ffe1396ccc6ded510a3896e77efac7a1bfbaa174a7b00414a1747377e0bb525d303794a47cf30e805c2ec84e575758512c6e44a993076d29fd4e6c3
   languageName: node
   linkType: hard
 
@@ -7160,7 +7101,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.21.3, es-abstract@npm:^1.22.1":
+"es-abstract@npm:^1.17.2, es-abstract@npm:^1.20.4, es-abstract@npm:^1.21.2, es-abstract@npm:^1.22.1":
   version: 1.22.3
   resolution: "es-abstract@npm:1.22.3"
   dependencies:
@@ -7231,14 +7172,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"es-iterator-helpers@npm:^1.0.12":
-  version: 1.0.13
-  resolution: "es-iterator-helpers@npm:1.0.13"
+"es-iterator-helpers@npm:^1.0.12, es-iterator-helpers@npm:^1.0.15":
+  version: 1.0.15
+  resolution: "es-iterator-helpers@npm:1.0.15"
   dependencies:
     asynciterator.prototype: "npm:^1.0.0"
     call-bind: "npm:^1.0.2"
-    define-properties: "npm:^1.2.0"
-    es-abstract: "npm:^1.21.3"
+    define-properties: "npm:^1.2.1"
+    es-abstract: "npm:^1.22.1"
     es-set-tostringtag: "npm:^2.0.1"
     function-bind: "npm:^1.1.1"
     get-intrinsic: "npm:^1.2.1"
@@ -7247,9 +7188,9 @@ __metadata:
     has-proto: "npm:^1.0.1"
     has-symbols: "npm:^1.0.3"
     internal-slot: "npm:^1.0.5"
-    iterator.prototype: "npm:^1.1.0"
-    safe-array-concat: "npm:^1.0.0"
-  checksum: e6109017c432376294d5d6849cd0a5f8d9bcf5819eea612026e4401bb362d798c01e7a8984702b87d9d689c07b1146a31a99f17a761ca4e7e6470d9e8db9bea8
+    iterator.prototype: "npm:^1.1.2"
+    safe-array-concat: "npm:^1.0.1"
+  checksum: b4c83f94bfe624260d5238092de3173989f76f1416b1d02c388aea3b2024174e5f5f0e864057311ac99790b57e836ca3545b6e77256b26066dac944519f5e6d6
   languageName: node
   linkType: hard
 
@@ -7390,11 +7331,11 @@ __metadata:
   linkType: hard
 
 "eslint-plugin-formatjs@npm:^4.10.1":
-  version: 4.11.0
-  resolution: "eslint-plugin-formatjs@npm:4.11.0"
+  version: 4.11.2
+  resolution: "eslint-plugin-formatjs@npm:4.11.2"
   dependencies:
-    "@formatjs/icu-messageformat-parser": "npm:2.7.0"
-    "@formatjs/ts-transformer": "npm:3.13.6"
+    "@formatjs/icu-messageformat-parser": "npm:2.7.2"
+    "@formatjs/ts-transformer": "npm:3.13.8"
     "@types/eslint": "npm:7 || 8"
     "@types/picomatch": "npm:^2.3.0"
     "@typescript-eslint/utils": "npm:^6.5.0"
@@ -7406,7 +7347,7 @@ __metadata:
     unicode-emoji-utils: "npm:^1.1.1"
   peerDependencies:
     eslint: 7 || 8
-  checksum: 4b1afb99d1d46e5e2200669a3918c0238b075c89ac9790f3339d9231f86c093cdd0560b7bfa79e81f99b390b9beab68b3dc8cb8dac57285db2f9b120e7f2667a
+  checksum: f5a6bffd9c65b9ce765be74d384618e543388720036b070d69d93c00b8c2bfded543141affc7793bf402f2c9177e2bbc395a7d1e8b806a40bfde1744c282a13c
   languageName: node
   linkType: hard
 
@@ -7438,12 +7379,12 @@ __metadata:
   linkType: hard
 
 "eslint-plugin-jsdoc@npm:^46.1.0":
-  version: 46.8.2
-  resolution: "eslint-plugin-jsdoc@npm:46.8.2"
+  version: 46.9.0
+  resolution: "eslint-plugin-jsdoc@npm:46.9.0"
   dependencies:
-    "@es-joy/jsdoccomment": "npm:~0.40.1"
+    "@es-joy/jsdoccomment": "npm:~0.41.0"
     are-docs-informative: "npm:^0.0.2"
-    comment-parser: "npm:1.4.0"
+    comment-parser: "npm:1.4.1"
     debug: "npm:^4.3.4"
     escape-string-regexp: "npm:^4.0.0"
     esquery: "npm:^1.5.0"
@@ -7452,33 +7393,33 @@ __metadata:
     spdx-expression-parse: "npm:^3.0.1"
   peerDependencies:
     eslint: ^7.0.0 || ^8.0.0
-  checksum: ccf38567ddd73d7c57bf144d0fe9c1fe4a54e407353b3577bf036e9919a8ef96e7e385834ee383b64c7c2090b15a0f84a55b9dc5f50539ff399a3f7b91d26b48
+  checksum: 4566b0f9bda54b446c813cf5ea93ae6d5866cbc4d448cb957b9ce2563f934d3ed2ed4e665e5a870750860a57137a1714c38599c35c60be16dce0f8e5a75b6ff6
   languageName: node
   linkType: hard
 
-"eslint-plugin-jsx-a11y@npm:~6.7.1":
-  version: 6.7.1
-  resolution: "eslint-plugin-jsx-a11y@npm:6.7.1"
+"eslint-plugin-jsx-a11y@npm:~6.8.0":
+  version: 6.8.0
+  resolution: "eslint-plugin-jsx-a11y@npm:6.8.0"
   dependencies:
-    "@babel/runtime": "npm:^7.20.7"
-    aria-query: "npm:^5.1.3"
-    array-includes: "npm:^3.1.6"
-    array.prototype.flatmap: "npm:^1.3.1"
-    ast-types-flow: "npm:^0.0.7"
-    axe-core: "npm:^4.6.2"
-    axobject-query: "npm:^3.1.1"
+    "@babel/runtime": "npm:^7.23.2"
+    aria-query: "npm:^5.3.0"
+    array-includes: "npm:^3.1.7"
+    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"
     damerau-levenshtein: "npm:^1.0.8"
     emoji-regex: "npm:^9.2.2"
-    has: "npm:^1.0.3"
-    jsx-ast-utils: "npm:^3.3.3"
-    language-tags: "npm:=1.0.5"
+    es-iterator-helpers: "npm:^1.0.15"
+    hasown: "npm:^2.0.0"
+    jsx-ast-utils: "npm:^3.3.5"
+    language-tags: "npm:^1.0.9"
     minimatch: "npm:^3.1.2"
-    object.entries: "npm:^1.1.6"
-    object.fromentries: "npm:^2.0.6"
-    semver: "npm:^6.3.0"
+    object.entries: "npm:^1.1.7"
+    object.fromentries: "npm:^2.0.7"
   peerDependencies:
     eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
-  checksum: 41ad3d0c8036b36cd475685c1ad639157f403b16e8ac23c07f1dbe0226ccf8458f2805cbd5cc8e56856a5d8a356f3276e3139274d819476ccad80c41b9245502
+  checksum: 199b883e526e6f9d7c54cb3f094abc54f11a1ec816db5fb6cae3b938eb0e503acc10ccba91ca7451633a9d0b9abc0ea03601844a8aba5fe88c5e8897c9ac8f49
   languageName: node
   linkType: hard
 
@@ -10039,16 +9980,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"iterator.prototype@npm:^1.1.0":
-  version: 1.1.0
-  resolution: "iterator.prototype@npm:1.1.0"
+"iterator.prototype@npm:^1.1.2":
+  version: 1.1.2
+  resolution: "iterator.prototype@npm:1.1.2"
   dependencies:
-    define-properties: "npm:^1.1.4"
-    get-intrinsic: "npm:^1.1.3"
+    define-properties: "npm:^1.2.1"
+    get-intrinsic: "npm:^1.2.1"
     has-symbols: "npm:^1.0.3"
-    has-tostringtag: "npm:^1.0.0"
-    reflect.getprototypeof: "npm:^1.0.3"
-  checksum: fd641c4cc8cf85a1f99c772722589393b6b59562c7b73cae6bea26e0814b9bdd095d40818f061b85a4f386ecebee92f9a01ba79a70951d72bd3dd3e01a6c624c
+    reflect.getprototypeof: "npm:^1.0.4"
+    set-function-name: "npm:^2.0.1"
+  checksum: a32151326095e916f306990d909f6bbf23e3221999a18ba686419535dcd1749b10ded505e89334b77dc4c7a58a8508978f0eb16c2c8573e6d412eb7eb894ea79
   languageName: node
   linkType: hard
 
@@ -10807,7 +10748,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.3":
+"jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.5":
   version: 3.3.5
   resolution: "jsx-ast-utils@npm:3.3.5"
   dependencies:
@@ -10870,19 +10811,19 @@ __metadata:
   languageName: node
   linkType: hard
 
-"language-subtag-registry@npm:~0.3.2":
+"language-subtag-registry@npm:^0.3.20":
   version: 0.3.22
   resolution: "language-subtag-registry@npm:0.3.22"
   checksum: d1e09971260a7cd3b9fdeb190d33af0b6e99c8697013537d9aaa15f7856d9d83aee128ba8078e219df0a7cf4b8dd18d1a0c188f6543b500d92a2689d2d114b70
   languageName: node
   linkType: hard
 
-"language-tags@npm:=1.0.5":
-  version: 1.0.5
-  resolution: "language-tags@npm:1.0.5"
+"language-tags@npm:^1.0.9":
+  version: 1.0.9
+  resolution: "language-tags@npm:1.0.9"
   dependencies:
-    language-subtag-registry: "npm:~0.3.2"
-  checksum: 04215e821af9a8f1bc6c99ab5aa0a316c3fe1912ca3337eb28596316064bddd8edd22f2883d866069ebdf01b2002e504a760a336b2c728b6d30514e86744f76c
+    language-subtag-registry: "npm:^0.3.20"
+  checksum: 9ab911213c4bd8bd583c850201c17794e52cb0660d1ab6e32558aadc8324abebf6844e46f92b80a5d600d0fbba7eface2c207bfaf270a1c7fd539e4c3a880bff
   languageName: node
   linkType: hard
 
@@ -12133,14 +12074,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"object.entries@npm:^1.1.6":
-  version: 1.1.6
-  resolution: "object.entries@npm:1.1.6"
+"object.entries@npm:^1.1.6, object.entries@npm:^1.1.7":
+  version: 1.1.7
+  resolution: "object.entries@npm:1.1.7"
   dependencies:
     call-bind: "npm:^1.0.2"
-    define-properties: "npm:^1.1.4"
-    es-abstract: "npm:^1.20.4"
-  checksum: 8782c71db3a068ccbae9e0541e6b4ac2c25dc67c63f97b7e6ad3c88271d7820197e7398e37747f96542ed47c27f0b81148cdf14c42df15dc22f64818ae7bb5bf
+    define-properties: "npm:^1.2.0"
+    es-abstract: "npm:^1.22.1"
+  checksum: 3ad1899cc7bf14546bf28f4a9b363ae8690b90948fcfbcac4c808395435d760f26193d9cae95337ce0e3c1e5c1f4fa45f7b46b31b68d389e9e117fce38775d86
   languageName: node
   linkType: hard
 
@@ -14193,17 +14134,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"reflect.getprototypeof@npm:^1.0.3":
-  version: 1.0.3
-  resolution: "reflect.getprototypeof@npm:1.0.3"
+"reflect.getprototypeof@npm:^1.0.4":
+  version: 1.0.4
+  resolution: "reflect.getprototypeof@npm:1.0.4"
   dependencies:
     call-bind: "npm:^1.0.2"
-    define-properties: "npm:^1.1.4"
-    es-abstract: "npm:^1.20.4"
-    get-intrinsic: "npm:^1.1.1"
+    define-properties: "npm:^1.2.0"
+    es-abstract: "npm:^1.22.1"
+    get-intrinsic: "npm:^1.2.1"
     globalthis: "npm:^1.0.3"
     which-builtin-type: "npm:^1.1.3"
-  checksum: 6300460adb743c5e710f3d0b9c2f49206a4f2a8cc61640e58565d13df3659747e82a88758666f5d32ed449ac3647cfcf0bbd48b574ceed8cb2ea14f20a719580
+  checksum: 02104cdd22658b637efe6b1df73658edab539268347327c8250a72d0cb273dcdf280c284e2d94155d22601d022d16be1a816a8616d679e447cbcbde9860d15cb
   languageName: node
   linkType: hard
 
@@ -14921,7 +14862,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"set-function-name@npm:^2.0.0":
+"set-function-name@npm:^2.0.0, set-function-name@npm:^2.0.1":
   version: 2.0.1
   resolution: "set-function-name@npm:2.0.1"
   dependencies:

From 0945e25b8f4ac46f6522277a3461044f37c7139b Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 13 Nov 2023 09:53:22 -0500
Subject: [PATCH 14/63] Add `Api::V1::Statuses::BaseController` base controller
 class (#27794)

---
 .../api/v1/statuses/base_controller.rb           | 16 ++++++++++++++++
 .../api/v1/statuses/bookmarks_controller.rb      | 15 ++-------------
 .../favourited_by_accounts_controller.rb         | 12 +-----------
 .../api/v1/statuses/favourites_controller.rb     | 15 ++-------------
 .../api/v1/statuses/histories_controller.rb      | 12 +-----------
 .../api/v1/statuses/mutes_controller.rb          | 12 +-----------
 .../api/v1/statuses/pins_controller.rb           |  9 +--------
 .../statuses/reblogged_by_accounts_controller.rb | 12 +-----------
 .../api/v1/statuses/reblogs_controller.rb        |  4 ++--
 .../api/v1/statuses/sources_controller.rb        | 14 +-------------
 .../api/v1/statuses/translations_controller.rb   | 12 +-----------
 11 files changed, 29 insertions(+), 104 deletions(-)
 create mode 100644 app/controllers/api/v1/statuses/base_controller.rb

diff --git a/app/controllers/api/v1/statuses/base_controller.rb b/app/controllers/api/v1/statuses/base_controller.rb
new file mode 100644
index 000000000..3f56b68bc
--- /dev/null
+++ b/app/controllers/api/v1/statuses/base_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Api::V1::Statuses::BaseController < Api::BaseController
+  include Authorization
+
+  before_action :set_status
+
+  private
+
+  def set_status
+    @status = Status.find(params[:status_id])
+    authorize @status, :show?
+  rescue Mastodon::NotPermittedError
+    not_found
+  end
+end
diff --git a/app/controllers/api/v1/statuses/bookmarks_controller.rb b/app/controllers/api/v1/statuses/bookmarks_controller.rb
index 19963c002..109b12f46 100644
--- a/app/controllers/api/v1/statuses/bookmarks_controller.rb
+++ b/app/controllers/api/v1/statuses/bookmarks_controller.rb
@@ -1,11 +1,9 @@
 # frozen_string_literal: true
 
-class Api::V1::Statuses::BookmarksController < Api::BaseController
-  include Authorization
-
+class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
   before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' }
   before_action :require_user!
-  before_action :set_status, only: [:create]
+  skip_before_action :set_status, only: [:destroy]
 
   def create
     current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
@@ -28,13 +26,4 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
   rescue Mastodon::NotPermittedError
     not_found
   end
-
-  private
-
-  def set_status
-    @status = Status.find(params[:status_id])
-    authorize @status, :show?
-  rescue Mastodon::NotPermittedError
-    not_found
-  end
 end
diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
index 73eb11e71..3cca246ce 100644
--- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
@@ -1,10 +1,7 @@
 # frozen_string_literal: true
 
-class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
-  include Authorization
-
+class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::BaseController
   before_action -> { authorize_if_got_token! :read, :'read:accounts' }
-  before_action :set_status
   after_action :insert_pagination_headers
 
   def index
@@ -61,13 +58,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
     @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
   end
 
-  def set_status
-    @status = Status.find(params[:status_id])
-    authorize @status, :show?
-  rescue Mastodon::NotPermittedError
-    not_found
-  end
-
   def pagination_params(core_params)
     params.slice(:limit).permit(:limit).merge(core_params)
   end
diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb
index f3428e3df..dbc75a036 100644
--- a/app/controllers/api/v1/statuses/favourites_controller.rb
+++ b/app/controllers/api/v1/statuses/favourites_controller.rb
@@ -1,11 +1,9 @@
 # frozen_string_literal: true
 
-class Api::V1::Statuses::FavouritesController < Api::BaseController
-  include Authorization
-
+class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseController
   before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
   before_action :require_user!
-  before_action :set_status, only: [:create]
+  skip_before_action :set_status, only: [:destroy]
 
   def create
     FavouriteService.new.call(current_account, @status)
@@ -30,13 +28,4 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
   rescue Mastodon::NotPermittedError
     not_found
   end
-
-  private
-
-  def set_status
-    @status = Status.find(params[:status_id])
-    authorize @status, :show?
-  rescue Mastodon::NotPermittedError
-    not_found
-  end
 end
diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb
index 2913472b0..dcb21ef04 100644
--- a/app/controllers/api/v1/statuses/histories_controller.rb
+++ b/app/controllers/api/v1/statuses/histories_controller.rb
@@ -1,10 +1,7 @@
 # frozen_string_literal: true
 
-class Api::V1::Statuses::HistoriesController < Api::BaseController
-  include Authorization
-
+class Api::V1::Statuses::HistoriesController < Api::V1::Statuses::BaseController
   before_action -> { authorize_if_got_token! :read, :'read:statuses' }
-  before_action :set_status
 
   def show
     cache_if_unauthenticated!
@@ -16,11 +13,4 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
   def status_edits
     @status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
   end
-
-  def set_status
-    @status = Status.find(params[:status_id])
-    authorize @status, :show?
-  rescue Mastodon::NotPermittedError
-    not_found
-  end
 end
diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb
index 87071a2b9..26b92bb8a 100644
--- a/app/controllers/api/v1/statuses/mutes_controller.rb
+++ b/app/controllers/api/v1/statuses/mutes_controller.rb
@@ -1,11 +1,8 @@
 # frozen_string_literal: true
 
-class Api::V1::Statuses::MutesController < Api::BaseController
-  include Authorization
-
+class Api::V1::Statuses::MutesController < Api::V1::Statuses::BaseController
   before_action -> { doorkeeper_authorize! :write, :'write:mutes' }
   before_action :require_user!
-  before_action :set_status
   before_action :set_conversation
 
   def create
@@ -24,13 +21,6 @@ class Api::V1::Statuses::MutesController < Api::BaseController
 
   private
 
-  def set_status
-    @status = Status.find(params[:status_id])
-    authorize @status, :show?
-  rescue Mastodon::NotPermittedError
-    not_found
-  end
-
   def set_conversation
     @conversation = @status.conversation
     raise Mastodon::ValidationError if @conversation.nil?
diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb
index 51b1621b6..7107890af 100644
--- a/app/controllers/api/v1/statuses/pins_controller.rb
+++ b/app/controllers/api/v1/statuses/pins_controller.rb
@@ -1,11 +1,8 @@
 # frozen_string_literal: true
 
-class Api::V1::Statuses::PinsController < Api::BaseController
-  include Authorization
-
+class Api::V1::Statuses::PinsController < Api::V1::Statuses::BaseController
   before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
   before_action :require_user!
-  before_action :set_status
 
   def create
     StatusPin.create!(account: current_account, status: @status)
@@ -26,10 +23,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController
 
   private
 
-  def set_status
-    @status = Status.find(params[:status_id])
-  end
-
   def distribute_add_activity!
     json = ActiveModelSerializers::SerializableResource.new(
       @status,
diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
index 41672e753..dd3e60846 100644
--- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
@@ -1,10 +1,7 @@
 # frozen_string_literal: true
 
-class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
-  include Authorization
-
+class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::BaseController
   before_action -> { authorize_if_got_token! :read, :'read:accounts' }
-  before_action :set_status
   after_action :insert_pagination_headers
 
   def index
@@ -57,13 +54,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
     @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
   end
 
-  def set_status
-    @status = Status.find(params[:status_id])
-    authorize @status, :show?
-  rescue Mastodon::NotPermittedError
-    not_found
-  end
-
   def pagination_params(core_params)
     params.slice(:limit).permit(:limit).merge(core_params)
   end
diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb
index 3ca623117..971b054c5 100644
--- a/app/controllers/api/v1/statuses/reblogs_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogs_controller.rb
@@ -1,13 +1,13 @@
 # frozen_string_literal: true
 
-class Api::V1::Statuses::ReblogsController < Api::BaseController
-  include Authorization
+class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
   include Redisable
   include Lockable
 
   before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
   before_action :require_user!
   before_action :set_reblog, only: [:create]
+  skip_before_action :set_status
 
   override_rate_limit_headers :create, family: :statuses
 
diff --git a/app/controllers/api/v1/statuses/sources_controller.rb b/app/controllers/api/v1/statuses/sources_controller.rb
index 434086451..5ceda4c7e 100644
--- a/app/controllers/api/v1/statuses/sources_controller.rb
+++ b/app/controllers/api/v1/statuses/sources_controller.rb
@@ -1,21 +1,9 @@
 # frozen_string_literal: true
 
-class Api::V1::Statuses::SourcesController < Api::BaseController
-  include Authorization
-
+class Api::V1::Statuses::SourcesController < Api::V1::Statuses::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
-  before_action :set_status
 
   def show
     render json: @status, serializer: REST::StatusSourceSerializer
   end
-
-  private
-
-  def set_status
-    @status = Status.find(params[:status_id])
-    authorize @status, :show?
-  rescue Mastodon::NotPermittedError
-    not_found
-  end
 end
diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb
index ec5ea5b85..7d406b0a3 100644
--- a/app/controllers/api/v1/statuses/translations_controller.rb
+++ b/app/controllers/api/v1/statuses/translations_controller.rb
@@ -1,10 +1,7 @@
 # frozen_string_literal: true
 
-class Api::V1::Statuses::TranslationsController < Api::BaseController
-  include Authorization
-
+class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
-  before_action :set_status
   before_action :set_translation
 
   rescue_from TranslationService::NotConfiguredError, with: :not_found
@@ -24,13 +21,6 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
 
   private
 
-  def set_status
-    @status = Status.find(params[:status_id])
-    authorize @status, :show?
-  rescue Mastodon::NotPermittedError
-    not_found
-  end
-
   def set_translation
     @translation = TranslateStatusService.new.call(@status, content_locale)
   end

From 49ba5a9f94eb2b595f9bb04e29ba4e7e1d99889a Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 13 Nov 2023 11:01:24 -0500
Subject: [PATCH 15/63] Use `hash_including` to check `AccountFilter` setup in
 `admin/accounts` controller spec (#27838)

---
 .../controllers/admin/accounts_controller_spec.rb | 15 +++------------
 1 file changed, 3 insertions(+), 12 deletions(-)

diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index ba03ec85a..3b8fa2f71 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -20,8 +20,7 @@ RSpec.describe Admin::AccountsController do
     it 'filters with parameters' do
       account_filter = instance_double(AccountFilter, results: Account.all)
       allow(AccountFilter).to receive(:new).and_return(account_filter)
-
-      get :index, params: {
+      params = {
         origin: 'local',
         by_domain: 'domain',
         status: 'active',
@@ -31,17 +30,9 @@ RSpec.describe Admin::AccountsController do
         ip: '0.0.0.42',
       }
 
-      expect(AccountFilter).to have_received(:new) do |params|
-        h = params.to_h
+      get :index, params: params
 
-        expect(h[:origin]).to eq 'local'
-        expect(h[:by_domain]).to eq 'domain'
-        expect(h[:status]).to eq 'active'
-        expect(h[:username]).to eq 'username'
-        expect(h[:display_name]).to eq 'display name'
-        expect(h[:email]).to eq 'local-part@domain'
-        expect(h[:ip]).to eq '0.0.0.42'
-      end
+      expect(AccountFilter).to have_received(:new).with(hash_including(params))
     end
 
     it 'paginates accounts' do

From bac9e0b55d77257b9b9d8ce602ca79c3885a9d18 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 13 Nov 2023 17:17:05 +0100
Subject: [PATCH 16/63] Add variable delay before link verification of remote
 account links (#27774)

---
 app/services/activitypub/process_account_service.rb | 2 +-
 app/services/update_account_service.rb              | 6 +-----
 2 files changed, 2 insertions(+), 6 deletions(-)

diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index 1304ca824..8fc0989a3 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -180,7 +180,7 @@ class ActivityPub::ProcessAccountService < BaseService
   end
 
   def check_links!
-    VerifyAccountLinksWorker.perform_async(@account.id)
+    VerifyAccountLinksWorker.perform_in(rand(10.minutes.to_i), @account.id)
   end
 
   def process_duplicate_accounts!
diff --git a/app/services/update_account_service.rb b/app/services/update_account_service.rb
index a98f4d31e..1bbcfce3e 100644
--- a/app/services/update_account_service.rb
+++ b/app/services/update_account_service.rb
@@ -30,11 +30,7 @@ class UpdateAccountService < BaseService
   def check_links(account)
     return unless account.fields.any?(&:requires_verification?)
 
-    if account.local?
-      VerifyAccountLinksWorker.perform_async(account.id)
-    else
-      VerifyAccountLinksWorker.perform_in(rand(10.minutes.to_i), account.id)
-    end
+    VerifyAccountLinksWorker.perform_async(account.id)
   end
 
   def process_hashtags(account)

From b7807f3d84983ab46c3345d076576ffa8433dab4 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Mon, 13 Nov 2023 17:47:44 -0500
Subject: [PATCH 17/63] Use `normalizes` to prepare `Webhook#events` value
 (#27605)

---
 app/models/webhook.rb       | 16 ++++++++--------
 spec/models/webhook_spec.rb | 31 +++++++++++++++++++++++++++++++
 2 files changed, 39 insertions(+), 8 deletions(-)

diff --git a/app/models/webhook.rb b/app/models/webhook.rb
index 044097921..304b2b1f1 100644
--- a/app/models/webhook.rb
+++ b/app/models/webhook.rb
@@ -33,11 +33,11 @@ class Webhook < ApplicationRecord
   validates :secret, presence: true, length: { minimum: 12 }
   validates :events, presence: true
 
-  validate :validate_events
+  validate :events_validation_error, if: :invalid_events?
   validate :validate_permissions
   validate :validate_template
 
-  before_validation :strip_events
+  normalizes :events, with: ->(events) { events.filter_map { |event| event.strip.presence } }
   before_validation :generate_secret
 
   def rotate_secret!
@@ -69,8 +69,12 @@ class Webhook < ApplicationRecord
 
   private
 
-  def validate_events
-    errors.add(:events, :invalid) if events.any? { |e| EVENTS.exclude?(e) }
+  def events_validation_error
+    errors.add(:events, :invalid)
+  end
+
+  def invalid_events?
+    events.blank? || events.difference(EVENTS).any?
   end
 
   def validate_permissions
@@ -88,10 +92,6 @@ class Webhook < ApplicationRecord
     end
   end
 
-  def strip_events
-    self.events = events.filter_map { |str| str.strip.presence } if events.present?
-  end
-
   def generate_secret
     self.secret = SecureRandom.hex(20) if secret.blank?
   end
diff --git a/spec/models/webhook_spec.rb b/spec/models/webhook_spec.rb
index 715dd7574..effaf92e9 100644
--- a/spec/models/webhook_spec.rb
+++ b/spec/models/webhook_spec.rb
@@ -5,6 +5,37 @@ require 'rails_helper'
 RSpec.describe Webhook do
   let(:webhook) { Fabricate(:webhook) }
 
+  describe 'Validations' do
+    it 'requires presence of events' do
+      record = described_class.new(events: nil)
+      record.valid?
+
+      expect(record).to model_have_error_on_field(:events)
+    end
+
+    it 'requires non-empty events value' do
+      record = described_class.new(events: [])
+      record.valid?
+
+      expect(record).to model_have_error_on_field(:events)
+    end
+
+    it 'requires valid events value from EVENTS' do
+      record = described_class.new(events: ['account.invalid'])
+      record.valid?
+
+      expect(record).to model_have_error_on_field(:events)
+    end
+  end
+
+  describe 'Normalizations' do
+    it 'cleans up events values' do
+      record = described_class.new(events: ['account.approved', 'account.created     ', ''])
+
+      expect(record.events).to eq(%w(account.approved account.created))
+    end
+  end
+
   describe '#rotate_secret!' do
     it 'changes the secret' do
       previous_value = webhook.secret

From 4aa06cbdbfbd4a4f9508c988c012951024b561e8 Mon Sep 17 00:00:00 2001
From: Brian Holley <brian.holley@hotmail.com>
Date: Mon, 13 Nov 2023 16:39:54 -0800
Subject: [PATCH 18/63] Fix "Hide these posts from home" list setting not
 refreshing when switching lists (#27763)

---
 app/javascript/mastodon/features/list_timeline/index.jsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/features/list_timeline/index.jsx b/app/javascript/mastodon/features/list_timeline/index.jsx
index aadb6ecd5..55579c2fd 100644
--- a/app/javascript/mastodon/features/list_timeline/index.jsx
+++ b/app/javascript/mastodon/features/list_timeline/index.jsx
@@ -204,7 +204,7 @@ class ListTimeline extends PureComponent {
           </div>
 
           <div className='setting-toggle'>
-            <Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} />
+            <Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
             <label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
               <FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
             </label>

From 5e2ecc736ddf275e57c91348a7b0a485f9b2462d Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 14 Nov 2023 05:29:33 -0500
Subject: [PATCH 19/63] Remove double `subject` in
 api/v1/accounts/relationships spec (#27839)

---
 .../api/v1/accounts/relationships_spec.rb     | 24 ++++++++++++-------
 1 file changed, 16 insertions(+), 8 deletions(-)

diff --git a/spec/requests/api/v1/accounts/relationships_spec.rb b/spec/requests/api/v1/accounts/relationships_spec.rb
index bb78e3b3e..5011352c6 100644
--- a/spec/requests/api/v1/accounts/relationships_spec.rb
+++ b/spec/requests/api/v1/accounts/relationships_spec.rb
@@ -102,17 +102,25 @@ describe 'GET /api/v1/accounts/relationships' do
       end
     end
 
-    it 'returns JSON with correct data on cached requests too' do
-      subject
-      subject
+    it 'returns JSON with correct data on previously cached requests' do
+      # Initial request including multiple accounts in params
+      get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id, lewis.id] }
+      expect(body_as_json.size).to eq(2)
+
+      # Subsequent request with different id, should override cache from first request
+      get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id] }
 
       expect(response).to have_http_status(200)
 
-      json = body_as_json
-
-      expect(json).to be_a Enumerable
-      expect(json.first[:following]).to be true
-      expect(json.first[:showing_reblogs]).to be true
+      expect(body_as_json)
+        .to be_an(Enumerable)
+        .and have_attributes(
+          size: 1,
+          first: hash_including(
+            following: true,
+            showing_reblogs: true
+          )
+        )
     end
 
     it 'returns JSON with correct data after change too' do

From 373aa95dddd7119725c4f11d3af402066e2288cf Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 14 Nov 2023 11:30:39 +0100
Subject: [PATCH 20/63] Update formatjs monorepo (#27849)

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

diff --git a/yarn.lock b/yarn.lock
index e8fe28fb4..1cb5a42d5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1800,6 +1800,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@formatjs/ecma402-abstract@npm:1.18.0":
+  version: 1.18.0
+  resolution: "@formatjs/ecma402-abstract@npm:1.18.0"
+  dependencies:
+    "@formatjs/intl-localematcher": "npm:0.5.2"
+    tslib: "npm:^2.4.0"
+  checksum: bbdad0aee8e48baad6bfe6b2c27caf3befe35e658b922ee2f84417a819f0bdc7e849a8c0c782db8b53f5666bf19669d2b10a1104257c08796d198c87766bfc92
+  languageName: node
+  linkType: hard
+
 "@formatjs/fast-memoize@npm:2.2.0":
   version: 2.2.0
   resolution: "@formatjs/fast-memoize@npm:2.2.0"
@@ -1820,6 +1830,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@formatjs/icu-messageformat-parser@npm:2.7.3":
+  version: 2.7.3
+  resolution: "@formatjs/icu-messageformat-parser@npm:2.7.3"
+  dependencies:
+    "@formatjs/ecma402-abstract": "npm:1.18.0"
+    "@formatjs/icu-skeleton-parser": "npm:1.7.0"
+    tslib: "npm:^2.4.0"
+  checksum: 2a51038813e5cff7e2df767e1227373d228e907adb7268fc3744b3d82c4fa69d4aa9f6020a62de2c468cf724600e9372ac07ae43a4480ed066fe34e224e80e4a
+  languageName: node
+  linkType: hard
+
 "@formatjs/icu-skeleton-parser@npm:1.6.4":
   version: 1.6.4
   resolution: "@formatjs/icu-skeleton-parser@npm:1.6.4"
@@ -1830,25 +1851,35 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/intl-displaynames@npm:6.6.3":
-  version: 6.6.3
-  resolution: "@formatjs/intl-displaynames@npm:6.6.3"
+"@formatjs/icu-skeleton-parser@npm:1.7.0":
+  version: 1.7.0
+  resolution: "@formatjs/icu-skeleton-parser@npm:1.7.0"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.4"
-    "@formatjs/intl-localematcher": "npm:0.5.1"
+    "@formatjs/ecma402-abstract": "npm:1.18.0"
     tslib: "npm:^2.4.0"
-  checksum: b0520cb744a51290fbcde80860f39ed9c9df9b81beae98986e1fc089ef635f7699c750631fa42a559f3678d1dd02b14904614e70360477d18e68d3eba6592390
+  checksum: 2e4db815247ddb10f7990bbb501c85b854ee951ee45143673eb91b4392b11d0a8312327adb8b624c6a2fdafab12083904630d6d22475503d025f1612da4dcaee
   languageName: node
   linkType: hard
 
-"@formatjs/intl-listformat@npm:7.5.2":
-  version: 7.5.2
-  resolution: "@formatjs/intl-listformat@npm:7.5.2"
+"@formatjs/intl-displaynames@npm:6.6.4":
+  version: 6.6.4
+  resolution: "@formatjs/intl-displaynames@npm:6.6.4"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.4"
-    "@formatjs/intl-localematcher": "npm:0.5.1"
+    "@formatjs/ecma402-abstract": "npm:1.18.0"
+    "@formatjs/intl-localematcher": "npm:0.5.2"
     tslib: "npm:^2.4.0"
-  checksum: 54fa03da4ea45504681d6d87d72d1cac574809ce43f965fa4b845e83be3072d92324c58cec57ad386827087fb1d6ecae438d29576f30176bf52eb212e454bce2
+  checksum: 009e443dd0d10776b8573d0181407d4c0d6c7a2ff537a5ea1e36413d1b08db9c21dfef272eabab8efabd01a58b64f663a30e4d584fd761df3fd68a5d23fe444b
+  languageName: node
+  linkType: hard
+
+"@formatjs/intl-listformat@npm:7.5.3":
+  version: 7.5.3
+  resolution: "@formatjs/intl-listformat@npm:7.5.3"
+  dependencies:
+    "@formatjs/ecma402-abstract": "npm:1.18.0"
+    "@formatjs/intl-localematcher": "npm:0.5.2"
+    tslib: "npm:^2.4.0"
+  checksum: de741ce84b16fed57016afbfe446ebd57cd23a046859a9353f5d455f8bc9114493bf83b9e18429268c7ce8f77bc54516a9b8190baf09fbb25c9b06cfc80101d4
   languageName: node
   linkType: hard
 
@@ -1861,34 +1892,43 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/intl-pluralrules@npm:^5.2.2":
-  version: 5.2.9
-  resolution: "@formatjs/intl-pluralrules@npm:5.2.9"
+"@formatjs/intl-localematcher@npm:0.5.2":
+  version: 0.5.2
+  resolution: "@formatjs/intl-localematcher@npm:0.5.2"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.4"
-    "@formatjs/intl-localematcher": "npm:0.5.1"
     tslib: "npm:^2.4.0"
-  checksum: a6ca5c498ce542facacf8ce8640d4ba068f9119b758547a23614b50611eb385a46abd386ff88fa423211355ec463cf102c2c908b74f6e23a5bc9e2a23873dc29
+  checksum: 4b3ae75470e3e53ffa39b2d46e65a2a4c9c4becbc0aac989b0694370e10c6687643660a045512d676509bc29b257fe5726fbb028de12f889be02c2d20b6527e6
   languageName: node
   linkType: hard
 
-"@formatjs/intl@npm:2.9.8":
-  version: 2.9.8
-  resolution: "@formatjs/intl@npm:2.9.8"
+"@formatjs/intl-pluralrules@npm:^5.2.2":
+  version: 5.2.10
+  resolution: "@formatjs/intl-pluralrules@npm:5.2.10"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.4"
+    "@formatjs/ecma402-abstract": "npm:1.18.0"
+    "@formatjs/intl-localematcher": "npm:0.5.2"
+    tslib: "npm:^2.4.0"
+  checksum: 1050416613e80bff2c58546c80c8d52ed97847d13c90535a53d058e44969369b50e1cfdb464e9e9ef4802c934c84ea0e656c3f4e3b4d5ac7496b722c759da4cf
+  languageName: node
+  linkType: hard
+
+"@formatjs/intl@npm:2.9.9":
+  version: 2.9.9
+  resolution: "@formatjs/intl@npm:2.9.9"
+  dependencies:
+    "@formatjs/ecma402-abstract": "npm:1.18.0"
     "@formatjs/fast-memoize": "npm:2.2.0"
-    "@formatjs/icu-messageformat-parser": "npm:2.7.2"
-    "@formatjs/intl-displaynames": "npm:6.6.3"
-    "@formatjs/intl-listformat": "npm:7.5.2"
-    intl-messageformat: "npm:10.5.7"
+    "@formatjs/icu-messageformat-parser": "npm:2.7.3"
+    "@formatjs/intl-displaynames": "npm:6.6.4"
+    "@formatjs/intl-listformat": "npm:7.5.3"
+    intl-messageformat: "npm:10.5.8"
     tslib: "npm:^2.4.0"
   peerDependencies:
     typescript: 5
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 6341f4bfb56a0e14373395b1232e1eeb8e64588a8c3d4614cd2b06f71d4e65dbd4a79e3a1c07e1b6c20c48e399ac2385977b01a559e1d2bd1a1d226e0eae3058
+  checksum: b26904da605ab309535dfbbfbd403a3bb33d51d3c969c548b88fa04755be3aff60b1bddd1c453514a84048c7432271cef507ac66de32dcfa66b3f842a1ddb977
   languageName: node
   linkType: hard
 
@@ -1912,6 +1952,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@formatjs/ts-transformer@npm:3.13.9":
+  version: 3.13.9
+  resolution: "@formatjs/ts-transformer@npm:3.13.9"
+  dependencies:
+    "@formatjs/icu-messageformat-parser": "npm:2.7.3"
+    "@types/json-stable-stringify": "npm:^1.0.32"
+    "@types/node": "npm:14 || 16 || 17"
+    chalk: "npm:^4.0.0"
+    json-stable-stringify: "npm:^1.0.1"
+    tslib: "npm:^2.4.0"
+    typescript: "npm:5"
+  peerDependencies:
+    ts-jest: ">=27"
+  peerDependenciesMeta:
+    ts-jest:
+      optional: true
+  checksum: 4e313b967e45aae79246174c3181d31cc7cd297380d3a880a98fc0be16d76b783868712151e840ea16d22e2fbec0388b1005f688b6d4cb74ee4411b43f6d33f4
+  languageName: node
+  linkType: hard
+
 "@gamestdio/websocket@npm:^0.3.2":
   version: 0.3.2
   resolution: "@gamestdio/websocket@npm:0.3.2"
@@ -4675,21 +4735,21 @@ __metadata:
   linkType: hard
 
 "babel-plugin-formatjs@npm:^10.5.1":
-  version: 10.5.9
-  resolution: "babel-plugin-formatjs@npm:10.5.9"
+  version: 10.5.10
+  resolution: "babel-plugin-formatjs@npm:10.5.10"
   dependencies:
     "@babel/core": "npm:^7.10.4"
     "@babel/helper-plugin-utils": "npm:^7.10.4"
     "@babel/plugin-syntax-jsx": "npm:7"
     "@babel/traverse": "npm:7"
     "@babel/types": "npm:^7.12.11"
-    "@formatjs/icu-messageformat-parser": "npm:2.7.2"
-    "@formatjs/ts-transformer": "npm:3.13.8"
+    "@formatjs/icu-messageformat-parser": "npm:2.7.3"
+    "@formatjs/ts-transformer": "npm:3.13.9"
     "@types/babel__core": "npm:^7.1.7"
     "@types/babel__helper-plugin-utils": "npm:^7.10.0"
     "@types/babel__traverse": "npm:^7.1.7"
     tslib: "npm:^2.4.0"
-  checksum: 5e4127cf7b4b9b3306a9d0ab5b029831712d22db5e2117225ce706b55d222d09a7eba1f3720fdad7a99f61843b5cba107296fc11ae00a6f0941217d9322aa02e
+  checksum: bff65cd2a88a0ae00eabab1d022ffc44c4385b7e529cac42375bb1828c678c7a71a78f644512e5d1dd8cd532d418c16acdbabcef2bf6670e24404f4f164a74ce
   languageName: node
   linkType: hard
 
@@ -9243,15 +9303,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"intl-messageformat@npm:10.5.7, intl-messageformat@npm:^10.3.5":
-  version: 10.5.7
-  resolution: "intl-messageformat@npm:10.5.7"
+"intl-messageformat@npm:10.5.8, intl-messageformat@npm:^10.3.5":
+  version: 10.5.8
+  resolution: "intl-messageformat@npm:10.5.8"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.4"
+    "@formatjs/ecma402-abstract": "npm:1.18.0"
     "@formatjs/fast-memoize": "npm:2.2.0"
-    "@formatjs/icu-messageformat-parser": "npm:2.7.2"
+    "@formatjs/icu-messageformat-parser": "npm:2.7.3"
     tslib: "npm:^2.4.0"
-  checksum: 7f341b3eb5b3d402167c99ca7fb98720c7ad553bed8a490b2210bd90ea9009a09f9030939307fecb111fce1454f31b4298b4f0a346999af627c86f8164a5c547
+  checksum: 1d2854aae8471ec48165ca265760d6c5b1814eca831c88db698eb29b5ed20bee21ca8533090c9d28d9c6f1d844dda210b0bc58a2e036446158fae0845e5eed4f
   languageName: node
   linkType: hard
 
@@ -13643,18 +13703,18 @@ __metadata:
   linkType: hard
 
 "react-intl@npm:^6.4.2":
-  version: 6.5.4
-  resolution: "react-intl@npm:6.5.4"
+  version: 6.5.5
+  resolution: "react-intl@npm:6.5.5"
   dependencies:
-    "@formatjs/ecma402-abstract": "npm:1.17.4"
-    "@formatjs/icu-messageformat-parser": "npm:2.7.2"
-    "@formatjs/intl": "npm:2.9.8"
-    "@formatjs/intl-displaynames": "npm:6.6.3"
-    "@formatjs/intl-listformat": "npm:7.5.2"
+    "@formatjs/ecma402-abstract": "npm:1.18.0"
+    "@formatjs/icu-messageformat-parser": "npm:2.7.3"
+    "@formatjs/intl": "npm:2.9.9"
+    "@formatjs/intl-displaynames": "npm:6.6.4"
+    "@formatjs/intl-listformat": "npm:7.5.3"
     "@types/hoist-non-react-statics": "npm:^3.3.1"
     "@types/react": "npm:16 || 17 || 18"
     hoist-non-react-statics: "npm:^3.3.2"
-    intl-messageformat: "npm:10.5.7"
+    intl-messageformat: "npm:10.5.8"
     tslib: "npm:^2.4.0"
   peerDependencies:
     react: ^16.6.0 || 17 || 18
@@ -13662,7 +13722,7 @@ __metadata:
   peerDependenciesMeta:
     typescript:
       optional: true
-  checksum: 1117a7f866b103abf88a4087f5fe8b854d9c069c69444c592f8431e7d28c9b90423f7b50e550be0f2f173b7563e943bcc9238e80f6747181f81861275f6e2ce7
+  checksum: 9ff6200f195557804b735d618ee593aed7848e84213ac4eb9c57708f55c0d93232e0dd338c990348ba3b1d73dca071502a2051d4a2790838d962c3ccde87fa6c
   languageName: node
   linkType: hard
 

From 1f8173ac5aa2dbdab66e6c426b137f4fcc162462 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 14 Nov 2023 05:31:59 -0500
Subject: [PATCH 21/63] Extract private methods in
 api/v1/instances/domain_blocks (#27844)

---
 .../api/v1/instances/domain_blocks_controller.rb   | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb
index 566764dbf..8fb90305a 100644
--- a/app/controllers/api/v1/instances/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb
@@ -13,7 +13,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
       cache_if_unauthenticated!
     end
 
-    render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?))
+    render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: show_rationale_in_response?
   end
 
   private
@@ -25,4 +25,16 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
   def set_domain_blocks
     @domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
   end
+
+  def show_rationale_in_response?
+    always_show_rationale? || show_rationale_for_user?
+  end
+
+  def always_show_rationale?
+    Setting.show_domain_blocks_rationale == 'all'
+  end
+
+  def show_rationale_for_user?
+    Setting.show_domain_blocks_rationale == 'users' && user_signed_in?
+  end
 end

From c1e071f6343a0b5c6383e6a336fd1aaf8dc97100 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 14 Nov 2023 11:53:47 +0100
Subject: [PATCH 22/63] New Crowdin Translations (automated) (#27848)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/af.json      |  4 ++++
 app/javascript/mastodon/locales/be.json      |  6 +++---
 app/javascript/mastodon/locales/ca.json      |  1 +
 app/javascript/mastodon/locales/fy.json      |  1 +
 app/javascript/mastodon/locales/lt.json      |  9 ++++++++-
 app/javascript/mastodon/locales/sl.json      |  1 +
 app/javascript/mastodon/locales/sr-Latn.json |  1 +
 app/javascript/mastodon/locales/sr.json      |  1 +
 config/locales/activerecord.af.yml           |  4 ++++
 config/locales/af.yml                        | 17 +++++++++++++++++
 config/locales/be.yml                        |  1 +
 config/locales/ca.yml                        |  1 +
 config/locales/cy.yml                        |  1 +
 config/locales/da.yml                        |  5 +++++
 config/locales/de.yml                        |  1 +
 config/locales/devise.be.yml                 |  2 +-
 config/locales/doorkeeper.af.yml             |  1 +
 config/locales/es-AR.yml                     |  1 +
 config/locales/es-MX.yml                     |  1 +
 config/locales/es.yml                        |  1 +
 config/locales/eu.yml                        |  1 +
 config/locales/fa.yml                        |  1 +
 config/locales/fo.yml                        |  1 +
 config/locales/fr-QC.yml                     |  1 +
 config/locales/fr.yml                        |  1 +
 config/locales/fy.yml                        |  1 +
 config/locales/gl.yml                        |  1 +
 config/locales/he.yml                        |  5 +++--
 config/locales/hu.yml                        |  1 +
 config/locales/is.yml                        |  1 +
 config/locales/it.yml                        |  1 +
 config/locales/ko.yml                        |  1 +
 config/locales/lt.yml                        |  1 +
 config/locales/nl.yml                        |  1 +
 config/locales/nn.yml                        |  1 +
 config/locales/no.yml                        |  1 +
 config/locales/pl.yml                        |  1 +
 config/locales/pt-BR.yml                     |  1 +
 config/locales/pt-PT.yml                     |  1 +
 config/locales/simple_form.be.yml            |  2 +-
 config/locales/sk.yml                        |  8 ++++++++
 config/locales/sl.yml                        |  1 +
 config/locales/sq.yml                        |  1 +
 config/locales/sr-Latn.yml                   |  1 +
 config/locales/sr.yml                        |  1 +
 config/locales/sv.yml                        |  1 +
 config/locales/th.yml                        |  1 +
 config/locales/tr.yml                        |  1 +
 config/locales/uk.yml                        |  1 +
 config/locales/zh-CN.yml                     |  1 +
 config/locales/zh-HK.yml                     |  1 +
 config/locales/zh-TW.yml                     |  1 +
 52 files changed, 96 insertions(+), 8 deletions(-)

diff --git a/app/javascript/mastodon/locales/af.json b/app/javascript/mastodon/locales/af.json
index 7e842b5dd..6f7f355fc 100644
--- a/app/javascript/mastodon/locales/af.json
+++ b/app/javascript/mastodon/locales/af.json
@@ -14,6 +14,7 @@
   "account.badges.group": "Groep",
   "account.block": "Blokkeer @{name}",
   "account.block_domain": "Blokkeer domein {domain}",
+  "account.block_short": "Blokkeer",
   "account.blocked": "Geblokkeer",
   "account.browse_more_on_origin_server": "Verken die oorspronklike profiel",
   "account.cancel_follow_request": "Herroep volgversoek",
@@ -45,6 +46,7 @@
   "account.posts_with_replies": "Plasings en antwoorde",
   "account.report": "Rapporteer @{name}",
   "account.requested": "Wag op goedkeuring. Klik om volgversoek te kanselleer",
+  "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}}",
@@ -82,6 +84,7 @@
   "column.community": "Plaaslike tydlyn",
   "column.directory": "Blaai deur profiele",
   "column.domain_blocks": "Geblokkeerde domeine",
+  "column.favourites": "Gunstelinge",
   "column.follow_requests": "Volgversoeke",
   "column.home": "Tuis",
   "column.lists": "Lyste",
@@ -271,6 +274,7 @@
   "privacy.unlisted.short": "Ongelys",
   "privacy_policy.last_updated": "Laaste bywerking op {date}",
   "privacy_policy.title": "Privaatheidsbeleid",
+  "regeneration_indicator.sublabel": "Jou tuis-voer word voorberei!",
   "reply_indicator.cancel": "Kanselleer",
   "report.placeholder": "Type or paste additional comments",
   "report.submit": "Submit report",
diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json
index 7c2d652b6..e8a52ee29 100644
--- a/app/javascript/mastodon/locales/be.json
+++ b/app/javascript/mastodon/locales/be.json
@@ -201,7 +201,7 @@
   "disabled_account_banner.text": "Ваш уліковы запіс {disabledAccount} часова адключаны.",
   "dismissable_banner.community_timeline": "Гэта самыя апошнія допісы ад людзей, уліковыя запісы якіх размяшчаюцца на {domain}.",
   "dismissable_banner.dismiss": "Адхіліць",
-  "dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца прама зараз на гэтым і іншых серверах дэцэнтралізаванай сеткі.",
+  "dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца цяпер на гэтым і іншых серверах дэцэнтралізаванай сеткі.",
   "dismissable_banner.explore_statuses": "Допісы з гэтага і іншых сервераў дэцэнтралізаванай сеткі, якія набіраюць папулярнасць прама зараз.",
   "dismissable_banner.explore_tags": "Гэтыя хэштэгі зараз набіраюць папулярнасць сярод людзей на гэтым і іншых серверах дэцэнтралізаванай сеткі",
   "dismissable_banner.public_timeline": "Гэта апошнія публічныя допісы людзей з усей сеткі, за якімі сочаць карыстальнікі {domain}.",
@@ -482,7 +482,7 @@
   "onboarding.share.lead": "Дайце людзям ведаць, як яны могуць знайсці вас на Mastodon!",
   "onboarding.share.message": "Я {username} на #Mastodon! Сачыце за мной на {url}",
   "onboarding.share.next_steps": "Магчымыя наступныя крокі:",
-  "onboarding.share.title": "Падзяліцеся сваім профілем",
+  "onboarding.share.title": "Абагульце свой профіль",
   "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
   "onboarding.start.skip": "Want to skip right ahead?",
   "onboarding.start.title": "Вы зрабілі гэта!",
@@ -493,7 +493,7 @@
   "onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
   "onboarding.steps.setup_profile.title": "Customize your profile",
   "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
-  "onboarding.steps.share_profile.title": "Share your profile",
+  "onboarding.steps.share_profile.title": "Абагульць ваш профіль у Mastodon",
   "onboarding.tips.2fa": "<strong>Ці вы ведаеце?</strong> Вы можаце абараніць свой уліковы запіс, усталяваўшы двухфактарную аўтэнтыфікацыю ў наладах уліковага запісу. Яна працуе з любой праграмай TOTP на ваш выбар, нумар тэлефона не патрэбны!",
   "onboarding.tips.accounts_from_other_servers": "<strong>Ці вы ведаеце?</strong> Паколькі Mastodon дэцэнтралізаваны, некаторыя профілі, якія вам трапляюцца, будуць размяшчацца на іншых серверах, адрозных ад вашага. І ўсё ж вы можаце бесперашкодна ўзаемадзейнічаць з імі! Іх сервер пазначаны ў другой палове імя карыстальніка!",
   "onboarding.tips.migration": "<strong>Ці вы ведаеце?</strong> Калі вы адчуваеце, што {domain} не з'яўляецца для вас лепшым выбарам у будучыні, вы можаце перайсці на іншы сервер Mastodon, не губляючы сваіх падпісчыкаў. Вы нават можаце стварыць свой уласны сервер!",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 433b9b47b..99cae584b 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Resultats de la cerca",
   "emoji_button.symbols": "Símbols",
   "emoji_button.travel": "Viatges i llocs",
+  "empty_column.account_hides_collections": "Aquest usuari ha elegit no mostrar aquesta informació",
   "empty_column.account_suspended": "Compte suspès",
   "empty_column.account_timeline": "No hi ha tuts aquí!",
   "empty_column.account_unavailable": "Perfil no disponible",
diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json
index 12365c879..9d3b41606 100644
--- a/app/javascript/mastodon/locales/fy.json
+++ b/app/javascript/mastodon/locales/fy.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Sykresultaten",
   "emoji_button.symbols": "Symboalen",
   "emoji_button.travel": "Reizgje en lokaasjes",
+  "empty_column.account_hides_collections": "Dizze brûker hat derfoar keazen dizze ynformaasje net beskikber te meitsjen",
   "empty_column.account_suspended": "Account beskoattele",
   "empty_column.account_timeline": "Hjir binne gjin berjochten!",
   "empty_column.account_unavailable": "Profyl net beskikber",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index 75f4a239e..5cdc575de 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -41,6 +41,8 @@
   "account.languages": "Keisti prenumeruojamas kalbas",
   "account.locked_info": "Šios paskyros privatumo būsena nustatyta kaip užrakinta. Savininkas (-ė) rankiniu būdu peržiūri, kas gali sekti.",
   "account.media": "Medija",
+  "account.mention": "Paminėti @{name}",
+  "account.moved_to": "{name} nurodė, kad dabar jų nauja paskyra yra:",
   "account.mute": "Užtildyti @{name}",
   "account.muted": "Užtildytas",
   "account.posts": "Toots",
@@ -53,10 +55,15 @@
   "account.unfollow": "Nebesekti",
   "account.unmute_short": "Atitildyti",
   "account_note.placeholder": "Click to add a note",
-  "alert.unexpected.title": "Oi!",
+  "alert.unexpected.message": "Įvyko netikėta klaida.",
+  "alert.unexpected.title": "Ups!",
   "announcement.announcement": "Skelbimas",
+  "attachments_list.unprocessed": "(neapdorotas)",
   "audio.hide": "Slėpti garsą",
   "autosuggest_hashtag.per_week": "{count} per savaitę",
+  "boost_modal.combo": "Gali spausti {combo}, kad praleisti kitą kartą",
+  "bundle_column_error.copy_stacktrace": "Kopijuoti klaidos ataskaitą",
+  "bundle_column_error.error.body": "Užklausos puslapio nepavyko atvaizduoti. Tai gali būti dėl mūsų kodo klaidos arba naršyklės suderinamumo problemos.",
   "bundle_column_error.error.title": "O, ne!",
   "column.domain_blocks": "Hidden domains",
   "column.lists": "Sąrašai",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index 63507be79..f16a91d65 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Rezultati iskanja",
   "emoji_button.symbols": "Simboli",
   "emoji_button.travel": "Potovanja in kraji",
+  "empty_column.account_hides_collections": "Ta uporabnik se je odločil, da te informacije ne bo dal na voljo",
   "empty_column.account_suspended": "Račun je suspendiran",
   "empty_column.account_timeline": "Tukaj ni objav!",
   "empty_column.account_unavailable": "Profil ni na voljo",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index f64952f7b..aa948b1f0 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Rezultati pretrage",
   "emoji_button.symbols": "Simboli",
   "emoji_button.travel": "Putovanja i mesta",
+  "empty_column.account_hides_collections": "Ovaj korisnik je odlučio da ove informacije ne učini dostupnim",
   "empty_column.account_suspended": "Nalog je suspendovan",
   "empty_column.account_timeline": "Nema objava ovde!",
   "empty_column.account_unavailable": "Profil je nedostupan",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index ec2c76e8f..9e6716927 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Резултати претраге",
   "emoji_button.symbols": "Симболи",
   "emoji_button.travel": "Путовања и места",
+  "empty_column.account_hides_collections": "Овај корисник је одлучио да ове информације не учини доступним",
   "empty_column.account_suspended": "Налог је суспендован",
   "empty_column.account_timeline": "Нема објава овде!",
   "empty_column.account_unavailable": "Профил је недоступан",
diff --git a/config/locales/activerecord.af.yml b/config/locales/activerecord.af.yml
index c0810999d..91980644e 100644
--- a/config/locales/activerecord.af.yml
+++ b/config/locales/activerecord.af.yml
@@ -53,3 +53,7 @@ af:
             position:
               elevated: kan nie hoër as jou huidige rol wees nie
               own_role: kan nie verander word met jou huidige rol nie
+        webhook:
+          attributes:
+            events:
+              invalid_permissions: geleenthede waartoe jy nie toegang het nie mag nie ingesluit word nie
diff --git a/config/locales/af.yml b/config/locales/af.yml
index 1dbf99afe..74d349591 100644
--- a/config/locales/af.yml
+++ b/config/locales/af.yml
@@ -5,7 +5,23 @@ af:
     contact_unavailable: NVT
     hosted_on: Mastodon gehuisves op %{domain}
     title: Aangaande
+  accounts:
+    follow: Volg
+    followers:
+      one: Volgeling
+      other: Volgelinge
+    following: Volg
+    nothing_here: Daar is niks hier nie!
+    posts:
+      one: Plasing
+      other: Plasings
+    posts_tab_heading: Plasings
   admin:
+    account_actions:
+      action: Voer aksie uit
+      title: Voer modereer aksie uit op %{acct}
+    account_moderation_notes:
+      create: Los nota
     accounts:
       location:
         local: Plaaslik
@@ -102,6 +118,7 @@ af:
     types:
       bookmarks: Boekmerke
   invites:
+    invalid: Hierdie uitnodiging is nie geldig nie
     title: Nooi ander
   login_activities:
     description_html: Indien jy onbekende aktiwiteite gewaar, oorweeg dit om jou wagwoord te verander en tweefaktorverifikasie te aktiveer.
diff --git a/config/locales/be.yml b/config/locales/be.yml
index 275ef7a82..96a272012 100644
--- a/config/locales/be.yml
+++ b/config/locales/be.yml
@@ -1418,6 +1418,7 @@ be:
       '86400': 1 дзень
     expires_in_prompt: Ніколі
     generate: Стварыць запрашальную спасылку
+    invalid: Гэта запрашэнне несапраўднае
     invited_by: 'Вас запрасіў(-ла):'
     max_uses:
       few: "%{count} выкарыстанні"
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 2cdf87d8f..03b3ff3c2 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -1358,6 +1358,7 @@ ca:
       '86400': 1 dia
     expires_in_prompt: Mai
     generate: Genera
+    invalid: Aquesta invitació no és vàlida
     invited_by: 'Has estat invitat per:'
     max_uses:
       one: 1 ús
diff --git a/config/locales/cy.yml b/config/locales/cy.yml
index 846ce41de..c9d5b8828 100644
--- a/config/locales/cy.yml
+++ b/config/locales/cy.yml
@@ -1468,6 +1468,7 @@ cy:
       '86400': 1 diwrnod
     expires_in_prompt: Byth
     generate: Cynhyrchu dolen wahoddiad
+    invalid: Nid yw'r gwahoddiad hwn yn ddilys
     invited_by: 'Cawsoch eich gwahodd gan:'
     max_uses:
       few: "%{count} defnydd"
diff --git a/config/locales/da.yml b/config/locales/da.yml
index c5d639185..13010e1ad 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -1110,6 +1110,7 @@ da:
       functional: Din konto er fuldt funktionel.
       pending: Din ansøgning afventer gennemgang af vores medarbejdere. Dette kan tage noget tid. Du modtager en e-mail, hvis din ansøgning godkendes.
       redirecting_to: Din konto er inaktiv, da den pt. er omdirigerer til %{acct}.
+      self_destruct: Da %{domain} er under nedlukning, vil kontoadgangen være begrænset.
       view_strikes: Se tidligere anmeldelser af din konto
     too_fast: Formularen indsendt for hurtigt, forsøg igen.
     use_security_key: Brug sikkerhedsnøgle
@@ -1367,6 +1368,7 @@ da:
       '86400': 1 dag
     expires_in_prompt: Aldrig
     generate: Generér invitationslink
+    invalid: Denne invitation er ikke gyldig
     invited_by: 'Du blev inviteret af:'
     max_uses:
       one: 1 benyttelse
@@ -1579,6 +1581,9 @@ da:
     over_daily_limit: Den daglige grænse på %{limit} planlagte indlæg er nået
     over_total_limit: Grænsen på %{limit} planlagte indlæg er nået
     too_soon: Den planlagte dato skal være i fremtiden
+  self_destruct:
+    lead_html: Desværre lukker <strong>%{domain}</strong> permanent. Har man en konto dér, vil fortsat brug heraf ikke være mulig. Man kan dog stadig anmode om en sikkerhedskopi af sine data.
+    title: Denne server er under nedlukning
   sessions:
     activity: Seneste aktivitet
     browser: Browser
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 2ab90611f..81fa4b57f 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -1368,6 +1368,7 @@ de:
       '86400': 1 Tag
     expires_in_prompt: Nie
     generate: Einladungslink erstellen
+    invalid: Diese Einladung ist ungültig
     invited_by: 'Du wurdest eingeladen von:'
     max_uses:
       one: Eine Verwendung
diff --git a/config/locales/devise.be.yml b/config/locales/devise.be.yml
index 3bf35daed..e5e8aea91 100644
--- a/config/locales/devise.be.yml
+++ b/config/locales/devise.be.yml
@@ -18,7 +18,7 @@ be:
       unconfirmed: Вы павінны пацвердзіць свой адрас электроннай пошты, перш чым працягнуць
     mailer:
       confirmation_instructions:
-        action: Пацвердзіце адрас электроннай пошты
+        action: Пацвердзіць адрас электроннай пошты
         action_with_app: Пацвердзіць і вярнуцца да %{app}
         explanation: Вы стварылі ўліковы запіс на %{host} з гэтым адрасам электроннай пошты. Вам спатрэбіцца ўсяго адзін клік, каб пацвердзіць яго. Калі гэта былі не вы, то проста праігнаруйце гэты ліст.
         explanation_when_pending: Вы падалі заяўку на запрашэнне на %{host} з гэтым адрасам электроннай пошты. Як толькі вы пацвердзіце свой адрас электроннай пошты, мы разгледзім вашу заяўку. Вы можаце ўвайсці, каб змяніць свае дадзеныя або выдаліць свой уліковы запіс, але вы не можаце атрымаць доступ да большасці функцый, пакуль ваш уліковы запіс не будзе зацверджаны. Калі ваша заяўка будзе адхілена, вашы даныя будуць выдалены, таму ад вас не спатрэбіцца ніякіх дадатковых дзеянняў. Калі гэта былі не вы, ігнаруйце гэты ліст
diff --git a/config/locales/doorkeeper.af.yml b/config/locales/doorkeeper.af.yml
index 504c7f507..9e05f403f 100644
--- a/config/locales/doorkeeper.af.yml
+++ b/config/locales/doorkeeper.af.yml
@@ -149,6 +149,7 @@ af:
       write:blocks: blokkeer rekeninge en domeine
       write:bookmarks: laat ’n boekmerk by plasings
       write:conversations: doof en wis gesprekke uit
+      write:favourites: gunsteling plasings
       write:filters: skep filters
       write:follows: volg mense
       write:lists: skep lyste
diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml
index 127f1262a..9175a1fc1 100644
--- a/config/locales/es-AR.yml
+++ b/config/locales/es-AR.yml
@@ -1368,6 +1368,7 @@ es-AR:
       '86400': 1 día
     expires_in_prompt: Nunca
     generate: Generar enlace de invitación
+    invalid: Esta invitación no es válida
     invited_by: 'Fuiste invitado por:'
     max_uses:
       one: 1 uso
diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml
index ad2fb184e..75d329b0a 100644
--- a/config/locales/es-MX.yml
+++ b/config/locales/es-MX.yml
@@ -1368,6 +1368,7 @@ es-MX:
       '86400': 1 día
     expires_in_prompt: Nunca
     generate: Generar
+    invalid: Esta invitación no es válida
     invited_by: 'Fuiste invitado por:'
     max_uses:
       one: 1 uso
diff --git a/config/locales/es.yml b/config/locales/es.yml
index d071e19a1..34d1c85dc 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -1368,6 +1368,7 @@ es:
       '86400': 1 día
     expires_in_prompt: Nunca
     generate: Generar
+    invalid: Esta invitación no es válida
     invited_by: 'Fuiste invitado por:'
     max_uses:
       one: 1 uso
diff --git a/config/locales/eu.yml b/config/locales/eu.yml
index 3b7bba6ce..dd7575c57 100644
--- a/config/locales/eu.yml
+++ b/config/locales/eu.yml
@@ -1363,6 +1363,7 @@ eu:
       '86400': Egun 1
     expires_in_prompt: Inoiz ez
     generate: Sortu
+    invalid: Gonbidapen hau ez da baliozkoa
     invited_by: 'Honek gonbidatu zaitu:'
     max_uses:
       one: Erabilera 1
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index 8569d2e37..04fb52e75 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -1157,6 +1157,7 @@ fa:
       '86400': ۱ روز
     expires_in_prompt: هیچ وقت
     generate: ساختن
+    invalid: این دعوت‌نامه معتبر نیست
     invited_by: 'دعوت‌کنندهٔ شما:'
     max_uses:
       one: ۱ بار
diff --git a/config/locales/fo.yml b/config/locales/fo.yml
index 8e98cc864..ffa54f588 100644
--- a/config/locales/fo.yml
+++ b/config/locales/fo.yml
@@ -1368,6 +1368,7 @@ fo:
       '86400': 1 dag
     expires_in_prompt: Ongantíð
     generate: Ger innbjóðingarleinki
+    invalid: Henda innbjóðing er ikki gildug
     invited_by: 'Tú var bjóðað/ur av:'
     max_uses:
       one: 1 brúk
diff --git a/config/locales/fr-QC.yml b/config/locales/fr-QC.yml
index 41b71f569..f7425ea32 100644
--- a/config/locales/fr-QC.yml
+++ b/config/locales/fr-QC.yml
@@ -1368,6 +1368,7 @@ fr-QC:
       '86400': 1 jour
     expires_in_prompt: Jamais
     generate: Générer un lien d'invitation
+    invalid: Cette invitation n’est pas valide
     invited_by: 'Vous avez été invité·e par :'
     max_uses:
       one: 1 utilisation
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 79d8b92c2..289afb226 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -1368,6 +1368,7 @@ fr:
       '86400': 1 jour
     expires_in_prompt: Jamais
     generate: Générer un lien d'invitation
+    invalid: Cette invitation n’est pas valide
     invited_by: 'Vous avez été invité·e par :'
     max_uses:
       one: 1 utilisation
diff --git a/config/locales/fy.yml b/config/locales/fy.yml
index 011899d15..de609a14d 100644
--- a/config/locales/fy.yml
+++ b/config/locales/fy.yml
@@ -1368,6 +1368,7 @@ fy:
       '86400': 1 dei
     expires_in_prompt: Nea
     generate: Utnûgingskeppeling generearje
+    invalid: Dizze útnûging is net jildich
     invited_by: 'Jo binne útnûge troch:'
     max_uses:
       one: 1 kear
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index f86b4fc20..0075c6592 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -1368,6 +1368,7 @@ gl:
       '86400': 1 día
     expires_in_prompt: Nunca
     generate: Xerar
+    invalid: Este convite non é válido
     invited_by: 'Convidoute:'
     max_uses:
       one: 1 uso
diff --git a/config/locales/he.yml b/config/locales/he.yml
index ea2ab8f29..11e5db453 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -113,8 +113,8 @@ he:
       previous_strikes_description_html:
         many: לחשבון הזה יש <strong>%{count}</strong> פסילות.
         one: לחשבון הזה פסילה <strong>אחת</strong>.
-        other: לחשבון הזה <strong>%{count}</strong> פסילות.
-        two: לחשבון הזה <strong>%{count}</strong> פסילות.
+        other: לחשבון הזה יש <strong>%{count}</strong> פסילות.
+        two: לחשבון הזה יש <strong>שתי</strong> פסילות.
       promote: להעלות בדרגה
       protocol: פרטיכל
       public: פומבי
@@ -1418,6 +1418,7 @@ he:
       '86400': יום אחד
     expires_in_prompt: לעולם לא
     generate: יצירת קישור להזמנה
+    invalid: הזמנה זו אינה תקפה
     invited_by: הוזמנת ע"י
     max_uses:
       many: "%{count} שימושים"
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index c01f38a9a..48f9d5b9d 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -1368,6 +1368,7 @@ hu:
       '86400': 1 nap
     expires_in_prompt: Soha
     generate: Generálás
+    invalid: Ez a meghívó nem érvényes
     invited_by: 'Téged meghívott:'
     max_uses:
       one: 1 használat
diff --git a/config/locales/is.yml b/config/locales/is.yml
index e0eb95569..390ce0ac0 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -1372,6 +1372,7 @@ is:
       '86400': 1 dagur
     expires_in_prompt: Aldrei
     generate: Útbúa boðstengil
+    invalid: Þetta boð er ekki gilt
     invited_by: 'Þér var boðið af:'
     max_uses:
       one: 1 afnot
diff --git a/config/locales/it.yml b/config/locales/it.yml
index e62ea620c..f35e9e42b 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -1370,6 +1370,7 @@ it:
       '86400': 1 giorno
     expires_in_prompt: Mai
     generate: Genera
+    invalid: Questo invito non è valido
     invited_by: 'Sei stato invitato da:'
     max_uses:
       one: un uso
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 72eafc8fb..fb193c75f 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -1345,6 +1345,7 @@ ko:
       '86400': 1 일
     expires_in_prompt: 영원히
     generate: 초대 링크 생성하기
+    invalid: 이 초대는 올바르지 않습니다
     invited_by: '당신을 초대한 사람:'
     max_uses:
       other: "%{count}회"
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 2f4d813c9..529eb5a44 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -383,6 +383,7 @@ lt:
       '86400': 1 dienos
     expires_in_prompt: Niekada
     generate: Generuoti
+    invalid: Šis kvietimas negalioja.
     invited_by: 'Jus pakvietė:'
     max_uses:
       few: "%{count} panaudojimai"
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 2a8a59d76..94a1f29f7 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -1368,6 +1368,7 @@ nl:
       '86400': 1 dag
     expires_in_prompt: Nooit
     generate: Uitnodigingslink genereren
+    invalid: Deze uitnodiging is niet geldig
     invited_by: 'Jij bent uitgenodigd door:'
     max_uses:
       one: 1 keer
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index acd6b206e..4925d4463 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -1368,6 +1368,7 @@ nn:
       '86400': 1 dag
     expires_in_prompt: Aldri
     generate: Lag innbydingslenkje
+    invalid: Denne invitasjonen er ikkje gyldig
     invited_by: 'Du vart innboden av:'
     max_uses:
       one: 1 bruk
diff --git a/config/locales/no.yml b/config/locales/no.yml
index 75085fa5a..a1058bf9f 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -1368,6 +1368,7 @@
       '86400': 1 dag
     expires_in_prompt: Aldri
     generate: Generer
+    invalid: Denne invitasjonen er ikke gyldig
     invited_by: 'Du ble invitert av:'
     max_uses:
       one: 1 bruk
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index e02cad039..608599724 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -1418,6 +1418,7 @@ pl:
       '86400': dobie
     expires_in_prompt: Nigdy
     generate: Wygeneruj
+    invalid: Niepoprawne zaproszenie
     invited_by: 'Zostałeś(-aś) zaproszony(-a) przez:'
     max_uses:
       few: "%{count} użycia"
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index d99c265fe..3eb2950bd 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -1368,6 +1368,7 @@ pt-BR:
       '86400': 1 dia
     expires_in_prompt: Nunca
     generate: Gerar convite
+    invalid: Este convite não é válido
     invited_by: 'Você recebeu convite de:'
     max_uses:
       one: 1 uso
diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml
index a40ac02f4..ce7479aa8 100644
--- a/config/locales/pt-PT.yml
+++ b/config/locales/pt-PT.yml
@@ -1368,6 +1368,7 @@ pt-PT:
       '86400': 1 dia
     expires_in_prompt: Nunca
     generate: Gerar hiperligação de convite
+    invalid: Este convite não é válido
     invited_by: 'Foi convidado por:'
     max_uses:
       one: 1 uso
diff --git a/config/locales/simple_form.be.yml b/config/locales/simple_form.be.yml
index 1ed6c8084..7ad87cdd9 100644
--- a/config/locales/simple_form.be.yml
+++ b/config/locales/simple_form.be.yml
@@ -53,7 +53,7 @@ be:
         password: Не менш за 8 сімвалаў
         phrase: Параўнанне адбудзецца нягледзячы на рэгістр тэксту і папярэджанні аб змесціве допісу
         scopes: Якімі API праграм будзе дазволена карыстацца. Калі вы абярэце найвышэйшы ўзровень, не трэба абіраць асобныя.
-        setting_aggregate_reblogs: Не паказваць новыя пашырэнні для допісаў, якія нядаўна пашырылі(уплывае выключна на будучыя пашырэнні)
+        setting_aggregate_reblogs: Не паказваць новыя пашырэнні для допісаў, якія пашырылі нядаўна (закранае толькі нядаўнія пашырэнні)
         setting_always_send_emails: Звычайна лісты з апавяшчэннямі не будуць дасылацца, калі вы актыўна карыстаецеся Mastodon
         setting_default_sensitive: Далікатныя медыя прадвызначана схаваныя. Іх можна адкрыць адзіным клікам
         setting_display_media_default: Хаваць медыя пазначаныя як далікатныя
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 954ef745d..63779e5bd 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -592,6 +592,8 @@ sk:
         title: Ohľadom
       appearance:
         title: Vzhľad
+      content_retention:
+        title: Ponechanie obsahu
       discovery:
         follow_recommendations: Odporúčania pre nasledovanie
         profile_directory: Katalóg profilov
@@ -616,6 +618,7 @@ sk:
       delete: Vymaž nahratý súbor
       destroyed_msg: Nahratie bolo zo stránky úspešne vymazané!
     software_updates:
+      critical_update: Kritické — prosím aktualizuj rýchlo
       documentation_link: Zisti viac
       title: Dostupné aktualizácie
       types:
@@ -646,6 +649,10 @@ sk:
       appeal_approved: Namietnuté
       appeal_rejected: Námietka zamietnutá
     system_checks:
+      elasticsearch_preset:
+        action: Pozri dokumentáciu
+      elasticsearch_preset_single_node:
+        action: Pozri dokumentáciu
       rules_check:
         action: Spravuj serverové pravidlá
         message_html: Neurčil/a si žiadne serverové pravidlá.
@@ -925,6 +932,7 @@ sk:
       '86400': 1 deň
     expires_in_prompt: Nikdy
     generate: Vygeneruj
+    invalid: Táto pozvánka je neplatná
     invited_by: 'Bol/a si pozvaný/á užívateľom:'
     max_uses:
       few: "%{count} využití"
diff --git a/config/locales/sl.yml b/config/locales/sl.yml
index 77ddfd6e2..b31fe118a 100644
--- a/config/locales/sl.yml
+++ b/config/locales/sl.yml
@@ -1418,6 +1418,7 @@ sl:
       '86400': 1 dan
     expires_in_prompt: Nikoli
     generate: Ustvari
+    invalid: To povabilo ni veljavno
     invited_by: 'Povabil/a vas je:'
     max_uses:
       few: "%{count} uporabe"
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index e4fb811ce..bd01a8089 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -1362,6 +1362,7 @@ sq:
       '86400': 1 ditë
     expires_in_prompt: Kurrë
     generate: Prodho lidhje ftese
+    invalid: Kjo ftesë s’është e vlefshme
     invited_by: 'Qetë ftuar nga:'
     max_uses:
       one: 1 përdorim
diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml
index cba4354f0..6ccec7fd9 100644
--- a/config/locales/sr-Latn.yml
+++ b/config/locales/sr-Latn.yml
@@ -1393,6 +1393,7 @@ sr-Latn:
       '86400': 1 dan
     expires_in_prompt: Nikad
     generate: Generiši
+    invalid: Ova pozivnica nije važeća
     invited_by: 'Pozvao Vas je:'
     max_uses:
       few: "%{count} korišćenja"
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 902f13ad6..407908517 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -1393,6 +1393,7 @@ sr:
       '86400': 1 дан
     expires_in_prompt: Никад
     generate: Генериши
+    invalid: Ова позивница није важећа
     invited_by: 'Позвао Вас је:'
     max_uses:
       few: "%{count} коришћења"
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index 736d839f9..8126455f4 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -1368,6 +1368,7 @@ sv:
       '86400': 1 dag
     expires_in_prompt: Aldrig
     generate: Skapa
+    invalid: Ogiltig inbjudan
     invited_by: 'Du blev inbjuden av:'
     max_uses:
       one: 1 användning
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 33d8a898e..661899896 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -1343,6 +1343,7 @@ th:
       '86400': 1 วัน
     expires_in_prompt: ไม่เลย
     generate: สร้างลิงก์เชิญ
+    invalid: คำเชิญนี้ไม่ถูกต้อง
     invited_by: 'คุณได้รับเชิญโดย:'
     max_uses:
       other: "%{count} การใช้งาน"
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index c9adfd913..9d4d95a83 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -1368,6 +1368,7 @@ tr:
       '86400': 1 gün
     expires_in_prompt: Asla
     generate: Davet bağlantısı oluştur
+    invalid: Bu davet geçerli değil
     invited_by: 'Davet edildiniz:'
     max_uses:
       one: 1 kullanım
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index eb947a7d0..2261c647b 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -1418,6 +1418,7 @@ uk:
       '86400': 1 день
     expires_in_prompt: Ніколи
     generate: Згенерувати
+    invalid: Це запрошення не дійсне
     invited_by: 'Вас запросив:'
     max_uses:
       few: "%{count} використання"
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 7902cea4d..b98193065 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -1343,6 +1343,7 @@ zh-CN:
       '86400': 1 天后
     expires_in_prompt: 永不过期
     generate: 生成邀请链接
+    invalid: 此邀请无效
     invited_by: 你的邀请人是:
     max_uses:
       other: "%{count} 次"
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index 58aeac841..f13cedad6 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -1343,6 +1343,7 @@ zh-HK:
       '86400': 1 天後
     expires_in_prompt: 永不過期
     generate: 生成邀請連結
+    invalid: 此邀請無效
     invited_by: 你的邀請人是:
     max_uses:
       other: "%{count} 次"
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 7bcc13396..3063b7afd 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -1345,6 +1345,7 @@ zh-TW:
       '86400': 1 天後
     expires_in_prompt: 永不過期
     generate: 建立邀請連結
+    invalid: 此邀請是無效的
     invited_by: 您的邀請人是:
     max_uses:
       other: "%{count} 則"

From 4eb4e8b22c564222d8bc69cca754cbfb9a524611 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 14 Nov 2023 12:07:13 +0100
Subject: [PATCH 23/63] Update Yarn to v4.0.2 (#27851)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 2230b20ea..7c73d17c1 100644
--- a/package.json
+++ b/package.json
@@ -242,5 +242,5 @@
     "*.{js,jsx,ts,tsx}": "eslint --fix",
     "*.{css,scss}": "stylelint --fix"
   },
-  "packageManager": "yarn@4.0.1"
+  "packageManager": "yarn@4.0.2"
 }

From d562fb84596f7cf2380e7c1cc465e1d4d478d759 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 14 Nov 2023 09:34:30 -0500
Subject: [PATCH 24/63] Specs for minimal CSP policy in `Api::` controllers
 (#27845)

---
 app/controllers/api/base_controller.rb        | 21 +--------
 .../concerns/api/content_security_policy.rb   | 27 ++++++++++++
 spec/requests/api/v1/csp_spec.rb              | 43 +++++++++++++++++++
 3 files changed, 71 insertions(+), 20 deletions(-)
 create mode 100644 app/controllers/concerns/api/content_security_policy.rb
 create mode 100644 spec/requests/api/v1/csp_spec.rb

diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index c764b4510..135c57565 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -7,6 +7,7 @@ class Api::BaseController < ApplicationController
   include RateLimitHeaders
   include AccessTokenTrackingConcern
   include ApiCachingConcern
+  include Api::ContentSecurityPolicy
 
   skip_before_action :require_functional!, unless: :limited_federation_mode?
 
@@ -17,26 +18,6 @@ class Api::BaseController < ApplicationController
 
   protect_from_forgery with: :null_session
 
-  content_security_policy do |p|
-    # Set every directive that does not have a fallback
-    p.default_src :none
-    p.frame_ancestors :none
-    p.form_action :none
-
-    # Disable every directive with a fallback to cut on response size
-    p.base_uri false
-    p.font_src false
-    p.img_src false
-    p.style_src false
-    p.media_src false
-    p.frame_src false
-    p.manifest_src false
-    p.connect_src false
-    p.script_src false
-    p.child_src false
-    p.worker_src false
-  end
-
   rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
     render json: { error: e.to_s }, status: 422
   end
diff --git a/app/controllers/concerns/api/content_security_policy.rb b/app/controllers/concerns/api/content_security_policy.rb
new file mode 100644
index 000000000..8116dca57
--- /dev/null
+++ b/app/controllers/concerns/api/content_security_policy.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Api::ContentSecurityPolicy
+  extend ActiveSupport::Concern
+
+  included do
+    content_security_policy do |policy|
+      # Set every directive that does not have a fallback
+      policy.default_src :none
+      policy.frame_ancestors :none
+      policy.form_action :none
+
+      # Disable every directive with a fallback to cut on response size
+      policy.base_uri false
+      policy.font_src false
+      policy.img_src false
+      policy.style_src false
+      policy.media_src false
+      policy.frame_src false
+      policy.manifest_src false
+      policy.connect_src false
+      policy.script_src false
+      policy.child_src false
+      policy.worker_src false
+    end
+  end
+end
diff --git a/spec/requests/api/v1/csp_spec.rb b/spec/requests/api/v1/csp_spec.rb
new file mode 100644
index 000000000..2db52ac72
--- /dev/null
+++ b/spec/requests/api/v1/csp_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'API namespace minimal Content-Security-Policy' do
+  before { stub_tests_controller }
+
+  after { Rails.application.reload_routes! }
+
+  it 'returns the correct CSP headers' do
+    get '/api/v1/tests'
+
+    expect(response).to have_http_status(200)
+    expect(response.headers['Content-Security-Policy']).to eq(minimal_csp_headers)
+  end
+
+  private
+
+  def stub_tests_controller
+    stub_const('Api::V1::TestsController', api_tests_controller)
+
+    Rails.application.routes.draw do
+      get '/api/v1/tests', to: 'api/v1/tests#index'
+    end
+  end
+
+  def api_tests_controller
+    Class.new(Api::BaseController) do
+      def index
+        head 200
+      end
+
+      private
+
+      def user_signed_in? = false
+      def current_user = nil
+    end
+  end
+
+  def minimal_csp_headers
+    "default-src 'none'; frame-ancestors 'none'; form-action 'none'"
+  end
+end

From b2c5b20ef27edd948eca8d6bd2014b7a5efaec11 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 14 Nov 2023 09:52:59 -0500
Subject: [PATCH 25/63] Fix `RSpec/AnyInstance` cop (#27810)

---
 .rubocop_todo.yml                             | 17 ----------
 .../activitypub/inboxes_controller_spec.rb    |  2 +-
 .../admin/accounts_controller_spec.rb         |  3 +-
 .../admin/resets_controller_spec.rb           | 18 ++++++++---
 .../auth/sessions_controller_spec.rb          |  6 ++--
 .../confirmations_controller_spec.rb          | 31 +++++++++----------
 .../recovery_codes_controller_spec.rb         |  6 ++--
 spec/lib/request_spec.rb                      |  5 ++-
 spec/lib/status_filter_spec.rb                |  6 ++--
 spec/models/account_spec.rb                   | 14 +++++++--
 spec/models/setting_spec.rb                   |  6 ++--
 .../process_collection_service_spec.rb        |  9 ++++--
 .../activitypub/delivery_worker_spec.rb       |  3 +-
 .../web/push_notification_worker_spec.rb      |  4 +--
 14 files changed, 70 insertions(+), 60 deletions(-)

diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index f9d14fd55..167204604 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -41,23 +41,6 @@ Metrics/CyclomaticComplexity:
 Metrics/PerceivedComplexity:
   Max: 27
 
-RSpec/AnyInstance:
-  Exclude:
-    - 'spec/controllers/activitypub/inboxes_controller_spec.rb'
-    - 'spec/controllers/admin/accounts_controller_spec.rb'
-    - 'spec/controllers/admin/resets_controller_spec.rb'
-    - 'spec/controllers/auth/sessions_controller_spec.rb'
-    - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
-    - 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb'
-    - 'spec/lib/request_spec.rb'
-    - 'spec/lib/status_filter_spec.rb'
-    - 'spec/models/account_spec.rb'
-    - 'spec/models/setting_spec.rb'
-    - 'spec/services/activitypub/process_collection_service_spec.rb'
-    - 'spec/validators/follow_limit_validator_spec.rb'
-    - 'spec/workers/activitypub/delivery_worker_spec.rb'
-    - 'spec/workers/web/push_notification_worker_spec.rb'
-
 # Configuration parameters: CountAsOne.
 RSpec/ExampleLength:
   Max: 22
diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb
index 030a30326..feca543cb 100644
--- a/spec/controllers/activitypub/inboxes_controller_spec.rb
+++ b/spec/controllers/activitypub/inboxes_controller_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe ActivityPub::InboxesController do
 
       before do
         allow(ActivityPub::FollowersSynchronizationWorker).to receive(:perform_async).and_return(nil)
-        allow_any_instance_of(Account).to receive(:local_followers_hash).and_return('somehash')
+        allow(remote_account).to receive(:local_followers_hash).and_return('somehash')
 
         request.headers['Collection-Synchronization'] = synchronization_header
         post :create, body: '{}'
diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 3b8fa2f71..307e81950 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -227,7 +227,8 @@ RSpec.describe Admin::AccountsController do
     let(:account) { Fabricate(:account, domain: 'example.com') }
 
     before do
-      allow_any_instance_of(ResolveAccountService).to receive(:call)
+      service = instance_double(ResolveAccountService, call: nil)
+      allow(ResolveAccountService).to receive(:new).and_return(service)
     end
 
     context 'when user is admin' do
diff --git a/spec/controllers/admin/resets_controller_spec.rb b/spec/controllers/admin/resets_controller_spec.rb
index 16adb8a12..14826973c 100644
--- a/spec/controllers/admin/resets_controller_spec.rb
+++ b/spec/controllers/admin/resets_controller_spec.rb
@@ -13,12 +13,20 @@ describe Admin::ResetsController do
 
   describe 'POST #create' do
     it 'redirects to admin accounts page' do
-      expect_any_instance_of(User).to receive(:send_reset_password_instructions) do |value|
-        expect(value.account_id).to eq account.id
-      end
-
-      post :create, params: { account_id: account.id }
+      expect do
+        post :create, params: { account_id: account.id }
+      end.to change(Devise.mailer.deliveries, :size).by(2)
 
+      expect(Devise.mailer.deliveries).to have_attributes(
+        first: have_attributes(
+          to: include(account.user.email),
+          subject: I18n.t('devise.mailer.password_change.subject')
+        ),
+        last: have_attributes(
+          to: include(account.user.email),
+          subject: I18n.t('devise.mailer.reset_password_instructions.subject')
+        )
+      )
       expect(response).to redirect_to(admin_account_path(account.id))
     end
   end
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb
index 049190e2e..f341d75b7 100644
--- a/spec/controllers/auth/sessions_controller_spec.rb
+++ b/spec/controllers/auth/sessions_controller_spec.rb
@@ -126,7 +126,7 @@ RSpec.describe Auth::SessionsController do
         let!(:previous_login) { Fabricate(:login_activity, user: user, ip: previous_ip) }
 
         before do
-          allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(current_ip)
+          allow(controller.request).to receive(:remote_ip).and_return(current_ip)
           user.update(current_sign_in_at: 1.month.ago)
           post :create, params: { user: { email: user.email, password: user.password } }
         end
@@ -279,7 +279,9 @@ RSpec.describe Auth::SessionsController do
 
         context 'when the server has an decryption error' do
           before do
-            allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError)
+            allow(user).to receive(:validate_and_consume_otp!).with(user.current_otp).and_raise(OpenSSL::Cipher::CipherError)
+            allow(User).to receive(:find_by).with(id: user.id).and_return(user)
+
             post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
           end
 
diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
index 37381fe1f..a5a35e91d 100644
--- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
@@ -61,6 +61,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
           it 'renders page with success' do
             prepare_user_otp_generation
             prepare_user_otp_consumption
+            allow(controller).to receive(:current_user).and_return(user)
 
             expect do
               post :create,
@@ -75,30 +76,28 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
           end
 
           def prepare_user_otp_generation
-            expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
-              expect(value).to eq user
-              otp_backup_codes
-            end
+            allow(user)
+              .to receive(:generate_otp_backup_codes!)
+              .and_return(otp_backup_codes)
           end
 
           def prepare_user_otp_consumption
-            expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options|
-              expect(value).to eq user
-              expect(code).to eq '123456'
-              expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' })
-              true
-            end
+            options = { otp_secret: 'thisisasecretforthespecofnewview' }
+            allow(user)
+              .to receive(:validate_and_consume_otp!)
+              .with('123456', options)
+              .and_return(true)
           end
         end
 
         describe 'when creation fails' do
           subject do
-            expect_any_instance_of(User).to receive(:validate_and_consume_otp!) do |value, code, options|
-              expect(value).to eq user
-              expect(code).to eq '123456'
-              expect(options).to eq({ otp_secret: 'thisisasecretforthespecofnewview' })
-              false
-            end
+            options = { otp_secret: 'thisisasecretforthespecofnewview' }
+            allow(user)
+              .to receive(:validate_and_consume_otp!)
+              .with('123456', options)
+              .and_return(false)
+            allow(controller).to receive(:current_user).and_return(user)
 
             expect do
               post :create,
diff --git a/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
index 630cec428..28a40e138 100644
--- a/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb
@@ -9,10 +9,8 @@ describe Settings::TwoFactorAuthentication::RecoveryCodesController do
     it 'updates the codes and shows them on a view when signed in' do
       user = Fabricate(:user)
       otp_backup_codes = user.generate_otp_backup_codes!
-      expect_any_instance_of(User).to receive(:generate_otp_backup_codes!) do |value|
-        expect(value).to eq user
-        otp_backup_codes
-      end
+      allow(user).to receive(:generate_otp_backup_codes!).and_return(otp_backup_codes)
+      allow(controller).to receive(:current_user).and_return(user)
 
       sign_in user, scope: :user
       post :create, session: { challenge_passed_at: Time.now.utc }
diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb
index f0861376b..c7620cf9b 100644
--- a/spec/lib/request_spec.rb
+++ b/spec/lib/request_spec.rb
@@ -64,8 +64,11 @@ describe Request do
       end
 
       it 'closes underlying connection' do
-        expect_any_instance_of(HTTP::Client).to receive(:close)
+        allow(subject.send(:http_client)).to receive(:close)
+
         expect { |block| subject.perform(&block) }.to yield_control
+
+        expect(subject.send(:http_client)).to have_received(:close)
       end
 
       it 'returns response which implements body_with_limit' do
diff --git a/spec/lib/status_filter_spec.rb b/spec/lib/status_filter_spec.rb
index c994ad419..cf6f3c795 100644
--- a/spec/lib/status_filter_spec.rb
+++ b/spec/lib/status_filter_spec.rb
@@ -23,7 +23,8 @@ describe StatusFilter do
 
       context 'when status policy does not allow show' do
         it 'filters the status' do
-          allow_any_instance_of(StatusPolicy).to receive(:show?).and_return(false)
+          policy = instance_double(StatusPolicy, show?: false)
+          allow(StatusPolicy).to receive(:new).and_return(policy)
 
           expect(filter).to be_filtered
         end
@@ -74,7 +75,8 @@ describe StatusFilter do
 
       context 'when status policy does not allow show' do
         it 'filters the status' do
-          allow_any_instance_of(StatusPolicy).to receive(:show?).and_return(false)
+          policy = instance_double(StatusPolicy, show?: false)
+          allow(StatusPolicy).to receive(:new).and_return(policy)
 
           expect(filter).to be_filtered
         end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index b5d942412..f77ecb055 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -209,9 +209,13 @@ RSpec.describe Account do
         expect(account.refresh!).to be_nil
       end
 
-      it 'calls not ResolveAccountService#call' do
-        expect_any_instance_of(ResolveAccountService).to_not receive(:call).with(acct)
+      it 'does not call ResolveAccountService#call' do
+        service = instance_double(ResolveAccountService, call: nil)
+        allow(ResolveAccountService).to receive(:new).and_return(service)
+
         account.refresh!
+
+        expect(service).to_not have_received(:call).with(acct)
       end
     end
 
@@ -219,8 +223,12 @@ RSpec.describe Account do
       let(:domain) { 'example.com' }
 
       it 'calls ResolveAccountService#call' do
-        expect_any_instance_of(ResolveAccountService).to receive(:call).with(acct).once
+        service = instance_double(ResolveAccountService, call: nil)
+        allow(ResolveAccountService).to receive(:new).and_return(service)
+
         account.refresh!
+
+        expect(service).to have_received(:call).with(acct).once
       end
     end
   end
diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb
index b08136a1c..e98062810 100644
--- a/spec/models/setting_spec.rb
+++ b/spec/models/setting_spec.rb
@@ -52,7 +52,8 @@ RSpec.describe Setting do
         before do
           allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object)
           allow(described_class).to receive(:default_settings).and_return(default_settings)
-          allow_any_instance_of(Settings::ScopedSettings).to receive(:thing_scoped).and_return(records)
+          settings_double = instance_double(Settings::ScopedSettings, thing_scoped: records)
+          allow(Settings::ScopedSettings).to receive(:new).and_return(settings_double)
           Rails.cache.delete(cache_key)
         end
 
@@ -128,7 +129,8 @@ RSpec.describe Setting do
 
   describe '.all_as_records' do
     before do
-      allow_any_instance_of(Settings::ScopedSettings).to receive(:thing_scoped).and_return(records)
+      settings_double = instance_double(Settings::ScopedSettings, thing_scoped: records)
+      allow(Settings::ScopedSettings).to receive(:new).and_return(settings_double)
       allow(described_class).to receive(:default_settings).and_return(default_settings)
     end
 
diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb
index ede9f5c04..df526daf3 100644
--- a/spec/services/activitypub/process_collection_service_spec.rb
+++ b/spec/services/activitypub/process_collection_service_spec.rb
@@ -76,7 +76,8 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
       let(:forwarder) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/other_account') }
 
       it 'does not process payload if no signature exists' do
-        allow_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil)
+        signature_double = instance_double(ActivityPub::LinkedDataSignature, verify_actor!: nil)
+        allow(ActivityPub::LinkedDataSignature).to receive(:new).and_return(signature_double)
         allow(ActivityPub::Activity).to receive(:factory)
 
         subject.call(json, forwarder)
@@ -87,7 +88,8 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
       it 'processes payload with actor if valid signature exists' do
         payload['signature'] = { 'type' => 'RsaSignature2017' }
 
-        allow_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(actor)
+        signature_double = instance_double(ActivityPub::LinkedDataSignature, verify_actor!: actor)
+        allow(ActivityPub::LinkedDataSignature).to receive(:new).and_return(signature_double)
         allow(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash))
 
         subject.call(json, forwarder)
@@ -98,7 +100,8 @@ RSpec.describe ActivityPub::ProcessCollectionService, type: :service do
       it 'does not process payload if invalid signature exists' do
         payload['signature'] = { 'type' => 'RsaSignature2017' }
 
-        allow_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_actor!).and_return(nil)
+        signature_double = instance_double(ActivityPub::LinkedDataSignature, verify_actor!: nil)
+        allow(ActivityPub::LinkedDataSignature).to receive(:new).and_return(signature_double)
         allow(ActivityPub::Activity).to receive(:factory)
 
         subject.call(json, forwarder)
diff --git a/spec/workers/activitypub/delivery_worker_spec.rb b/spec/workers/activitypub/delivery_worker_spec.rb
index d39393d50..efce610ae 100644
--- a/spec/workers/activitypub/delivery_worker_spec.rb
+++ b/spec/workers/activitypub/delivery_worker_spec.rb
@@ -11,7 +11,8 @@ describe ActivityPub::DeliveryWorker do
   let(:payload) { 'test' }
 
   before do
-    allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/api').and_return('somehash')
+    allow(sender).to receive(:remote_followers_hash).with('https://example.com/api').and_return('somehash')
+    allow(Account).to receive(:find).with(sender.id).and_return(sender)
   end
 
   describe 'perform' do
diff --git a/spec/workers/web/push_notification_worker_spec.rb b/spec/workers/web/push_notification_worker_spec.rb
index 822ef5257..637206a40 100644
--- a/spec/workers/web/push_notification_worker_spec.rb
+++ b/spec/workers/web/push_notification_worker_spec.rb
@@ -23,8 +23,8 @@ describe Web::PushNotificationWorker do
 
   describe 'perform' do
     before do
-      allow_any_instance_of(subscription.class).to receive(:contact_email).and_return(contact_email)
-      allow_any_instance_of(subscription.class).to receive(:vapid_key).and_return(vapid_key)
+      allow(subscription).to receive_messages(contact_email: contact_email, vapid_key: vapid_key)
+      allow(Web::PushSubscription).to receive(:find).with(subscription.id).and_return(subscription)
       allow(Webpush::Encryption).to receive(:encrypt).and_return(payload)
       allow(JWT).to receive(:encode).and_return('jwt.encoded.payload')
 

From 7e1a77ea51e6dc4aecbf678f8928aa96698fa072 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Tue, 14 Nov 2023 09:53:31 -0500
Subject: [PATCH 26/63] Add base class for `api/v1/timelines/*` controllers
 (#27840)

---
 .../api/v1/timelines/base_controller.rb       | 33 +++++++++++++++++++
 .../api/v1/timelines/home_controller.rb       | 25 +++-----------
 .../api/v1/timelines/list_controller.rb       | 24 +++-----------
 .../api/v1/timelines/public_controller.rb     | 25 +++-----------
 .../api/v1/timelines/tag_controller.rb        | 25 +++-----------
 5 files changed, 52 insertions(+), 80 deletions(-)
 create mode 100644 app/controllers/api/v1/timelines/base_controller.rb

diff --git a/app/controllers/api/v1/timelines/base_controller.rb b/app/controllers/api/v1/timelines/base_controller.rb
new file mode 100644
index 000000000..173e173cc
--- /dev/null
+++ b/app/controllers/api/v1/timelines/base_controller.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class Api::V1::Timelines::BaseController < Api::BaseController
+  after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
+
+  private
+
+  def insert_pagination_headers
+    set_pagination_headers(next_path, prev_path)
+  end
+
+  def pagination_max_id
+    @statuses.last.id
+  end
+
+  def pagination_since_id
+    @statuses.first.id
+  end
+
+  def next_path_params
+    permitted_params.merge(max_id: pagination_max_id)
+  end
+
+  def prev_path_params
+    permitted_params.merge(min_id: pagination_since_id)
+  end
+
+  def permitted_params
+    params
+      .slice(*self.class::PERMITTED_PARAMS)
+      .permit(*self.class::PERMITTED_PARAMS)
+  end
+end
diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb
index 83b8cb4c6..36fdbea64 100644
--- a/app/controllers/api/v1/timelines/home_controller.rb
+++ b/app/controllers/api/v1/timelines/home_controller.rb
@@ -1,9 +1,10 @@
 # frozen_string_literal: true
 
-class Api::V1::Timelines::HomeController < Api::BaseController
+class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show]
   before_action :require_user!, only: [:show]
-  after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
+
+  PERMITTED_PARAMS = %i(local limit).freeze
 
   def show
     with_read_replica do
@@ -40,27 +41,11 @@ class Api::V1::Timelines::HomeController < Api::BaseController
     HomeFeed.new(current_account)
   end
 
-  def insert_pagination_headers
-    set_pagination_headers(next_path, prev_path)
-  end
-
-  def pagination_params(core_params)
-    params.slice(:local, :limit).permit(:local, :limit).merge(core_params)
-  end
-
   def next_path
-    api_v1_timelines_home_url pagination_params(max_id: pagination_max_id)
+    api_v1_timelines_home_url next_path_params
   end
 
   def prev_path
-    api_v1_timelines_home_url pagination_params(min_id: pagination_since_id)
-  end
-
-  def pagination_max_id
-    @statuses.last.id
-  end
-
-  def pagination_since_id
-    @statuses.first.id
+    api_v1_timelines_home_url prev_path_params
   end
 end
diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb
index a15eae468..14b884ecd 100644
--- a/app/controllers/api/v1/timelines/list_controller.rb
+++ b/app/controllers/api/v1/timelines/list_controller.rb
@@ -1,12 +1,12 @@
 # frozen_string_literal: true
 
-class Api::V1::Timelines::ListController < Api::BaseController
+class Api::V1::Timelines::ListController < Api::V1::Timelines::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:lists' }
   before_action :require_user!
   before_action :set_list
   before_action :set_statuses
 
-  after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
+  PERMITTED_PARAMS = %i(limit).freeze
 
   def show
     render json: @statuses,
@@ -41,27 +41,11 @@ class Api::V1::Timelines::ListController < Api::BaseController
     ListFeed.new(@list)
   end
 
-  def insert_pagination_headers
-    set_pagination_headers(next_path, prev_path)
-  end
-
-  def pagination_params(core_params)
-    params.slice(:limit).permit(:limit).merge(core_params)
-  end
-
   def next_path
-    api_v1_timelines_list_url params[:id], pagination_params(max_id: pagination_max_id)
+    api_v1_timelines_list_url params[:id], next_path_params
   end
 
   def prev_path
-    api_v1_timelines_list_url params[:id], pagination_params(min_id: pagination_since_id)
-  end
-
-  def pagination_max_id
-    @statuses.last.id
-  end
-
-  def pagination_since_id
-    @statuses.first.id
+    api_v1_timelines_list_url params[:id], prev_path_params
   end
 end
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index 5bbd92b9e..35af8dc4b 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -1,8 +1,9 @@
 # frozen_string_literal: true
 
-class Api::V1::Timelines::PublicController < Api::BaseController
+class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
   before_action :require_user!, only: [:show], if: :require_auth?
-  after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
+
+  PERMITTED_PARAMS = %i(local remote limit only_media).freeze
 
   def show
     cache_if_unauthenticated!
@@ -42,27 +43,11 @@ class Api::V1::Timelines::PublicController < Api::BaseController
     )
   end
 
-  def insert_pagination_headers
-    set_pagination_headers(next_path, prev_path)
-  end
-
-  def pagination_params(core_params)
-    params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params)
-  end
-
   def next_path
-    api_v1_timelines_public_url pagination_params(max_id: pagination_max_id)
+    api_v1_timelines_public_url next_path_params
   end
 
   def prev_path
-    api_v1_timelines_public_url pagination_params(min_id: pagination_since_id)
-  end
-
-  def pagination_max_id
-    @statuses.last.id
-  end
-
-  def pagination_since_id
-    @statuses.first.id
+    api_v1_timelines_public_url prev_path_params
   end
 end
diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index a79d65c12..4ba439dbb 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -1,9 +1,10 @@
 # frozen_string_literal: true
 
-class Api::V1::Timelines::TagController < Api::BaseController
+class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
   before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
   before_action :load_tag
-  after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
+
+  PERMITTED_PARAMS = %i(local limit only_media).freeze
 
   def show
     cache_if_unauthenticated!
@@ -51,27 +52,11 @@ class Api::V1::Timelines::TagController < Api::BaseController
     )
   end
 
-  def insert_pagination_headers
-    set_pagination_headers(next_path, prev_path)
-  end
-
-  def pagination_params(core_params)
-    params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params)
-  end
-
   def next_path
-    api_v1_timelines_tag_url params[:id], pagination_params(max_id: pagination_max_id)
+    api_v1_timelines_tag_url params[:id], next_path_params
   end
 
   def prev_path
-    api_v1_timelines_tag_url params[:id], pagination_params(min_id: pagination_since_id)
-  end
-
-  def pagination_max_id
-    @statuses.last.id
-  end
-
-  def pagination_since_id
-    @statuses.first.id
+    api_v1_timelines_tag_url params[:id], prev_path_params
   end
 end

From 2b038b4f8951845bee470e18ffb11fab29aacb51 Mon Sep 17 00:00:00 2001
From: ppnplus <54897463+ppnplus@users.noreply.github.com>
Date: Tue, 14 Nov 2023 22:33:59 +0700
Subject: [PATCH 27/63] Added Thai diacritics and tone marks in
 HASHTAG_INVALID_CHARS_RE (#26576)

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

diff --git a/app/models/tag.rb b/app/models/tag.rb
index 8fab98fb5..46e55d74f 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -37,7 +37,7 @@ class Tag < ApplicationRecord
 
   HASHTAG_RE = %r{(?<![=/)\w])#(#{HASHTAG_NAME_PAT})}i
   HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i
-  HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]#{HASHTAG_SEPARATORS}]/
+  HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]\u0E47-\u0E4E#{HASHTAG_SEPARATORS}]/
 
   validates :name, presence: true, format: { with: HASHTAG_NAME_RE }
   validates :display_name, format: { with: HASHTAG_NAME_RE }

From 35b9749b95fc8f61631a8b38f9dad37ec5f36a7a Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 14 Nov 2023 17:53:15 +0100
Subject: [PATCH 28/63] Update dependency webpack-bundle-analyzer to v4.10.0
 (#27852)

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

diff --git a/yarn.lock b/yarn.lock
index 1cb5a42d5..addbe638f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6430,6 +6430,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"debounce@npm:^1.2.1":
+  version: 1.2.1
+  resolution: "debounce@npm:1.2.1"
+  checksum: 6c9320aa0973fc42050814621a7a8a78146c1975799b5b3cc1becf1f77ba9a5aa583987884230da0842a03f385def452fad5d60db97c3d1c8b824e38a8edf500
+  languageName: node
+  linkType: hard
+
 "debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.3.3":
   version: 2.6.9
   resolution: "debug@npm:2.6.9"
@@ -8903,7 +8910,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"html-escaper@npm:^2.0.0":
+"html-escaper@npm:^2.0.0, html-escaper@npm:^2.0.2":
   version: 2.0.2
   resolution: "html-escaper@npm:2.0.2"
   checksum: 208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0
@@ -11032,20 +11039,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lodash.escape@npm:^4.0.1":
-  version: 4.0.1
-  resolution: "lodash.escape@npm:4.0.1"
-  checksum: 90ade409cec05b6869090476952fdfb84d4d87b1ff4a0e03ebd590f980d9a1248d93ba14579f10d80c6429e4d6af13ba137c28db64cae6dadb71442e54a3ad2b
-  languageName: node
-  linkType: hard
-
-"lodash.flatten@npm:^4.4.0":
-  version: 4.4.0
-  resolution: "lodash.flatten@npm:4.4.0"
-  checksum: 97e8f0d6b61fe4723c02ad0c6e67e51784c4a2c48f56ef283483e556ad01594cf9cec9c773e177bbbdbdb5d19e99b09d2487cb6b6e5dc405c2693e93b125bd3a
-  languageName: node
-  linkType: hard
-
 "lodash.get@npm:^4.0":
   version: 4.4.2
   resolution: "lodash.get@npm:4.4.2"
@@ -11060,13 +11053,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lodash.invokemap@npm:^4.6.0":
-  version: 4.6.0
-  resolution: "lodash.invokemap@npm:4.6.0"
-  checksum: 2bcc5f4b8782a316d55ff139215eb797f576f0f6d3db2755ebba7b35fd6061f8cbe81702a72a30bc6d70073a5dcc461f7570eaddcc9184c2e42ec3023645c6a1
-  languageName: node
-  linkType: hard
-
 "lodash.isarguments@npm:^3.1.0":
   version: 3.1.0
   resolution: "lodash.isarguments@npm:3.1.0"
@@ -11109,13 +11095,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lodash.pullall@npm:^4.2.0":
-  version: 4.2.0
-  resolution: "lodash.pullall@npm:4.2.0"
-  checksum: b129e8d879258c7db04a7dc1c23dd9e37c52f63a04e105faa8d2ab55e97b5a170d5e15cffbb732a36e7f48c4345c07b6fbddfe50e1f5ec301492b6f64a92040c
-  languageName: node
-  linkType: hard
-
 "lodash.sortby@npm:^4.7.0":
   version: 4.7.0
   resolution: "lodash.sortby@npm:4.7.0"
@@ -11137,13 +11116,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lodash.uniqby@npm:^4.7.0":
-  version: 4.7.0
-  resolution: "lodash.uniqby@npm:4.7.0"
-  checksum: c505c0de20ca759599a2ba38710e8fb95ff2d2028e24d86c901ef2c74be8056518571b9b754bfb75053b2818d30dd02243e4a4621a6940c206bbb3f7626db656
-  languageName: node
-  linkType: hard
-
 "lodash@npm:^4.17.10, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21":
   version: 4.17.21
   resolution: "lodash@npm:4.17.21"
@@ -17035,29 +17007,25 @@ __metadata:
   linkType: hard
 
 "webpack-bundle-analyzer@npm:^4.8.0":
-  version: 4.9.1
-  resolution: "webpack-bundle-analyzer@npm:4.9.1"
+  version: 4.10.0
+  resolution: "webpack-bundle-analyzer@npm:4.10.0"
   dependencies:
     "@discoveryjs/json-ext": "npm:0.5.7"
     acorn: "npm:^8.0.4"
     acorn-walk: "npm:^8.0.0"
     commander: "npm:^7.2.0"
+    debounce: "npm:^1.2.1"
     escape-string-regexp: "npm:^4.0.0"
     gzip-size: "npm:^6.0.0"
+    html-escaper: "npm:^2.0.2"
     is-plain-object: "npm:^5.0.0"
-    lodash.debounce: "npm:^4.0.8"
-    lodash.escape: "npm:^4.0.1"
-    lodash.flatten: "npm:^4.4.0"
-    lodash.invokemap: "npm:^4.6.0"
-    lodash.pullall: "npm:^4.2.0"
-    lodash.uniqby: "npm:^4.7.0"
     opener: "npm:^1.5.2"
     picocolors: "npm:^1.0.0"
     sirv: "npm:^2.0.3"
     ws: "npm:^7.3.1"
   bin:
     webpack-bundle-analyzer: lib/bin/analyzer.js
-  checksum: dd047c306471e6c389d6d4156ff22402e587140310a065a6191ee380f8251063f73a8ec6ac6d977c1cd634dbb717e2522b5d0b6cc9b0e847d4f15737fd9c65c9
+  checksum: f812a8d3c0198ce518baf742bff656526f3eae69fb7a64c7f0c9cff202f6fb3380cabf3baaae965b8d6ffbbb6fb802eacb373fca03a596a38b01b84cfb2e8329
   languageName: node
   linkType: hard
 

From 36d7d1781f99c66c85cbbde75015b622124b1c3e Mon Sep 17 00:00:00 2001
From: Nick Schonning <nschonni@gmail.com>
Date: Tue, 14 Nov 2023 11:53:38 -0500
Subject: [PATCH 29/63] Add CodeCov for Ruby coverage reports (#23868)

---
 .github/workflows/test-ruby.yml | 8 +++++++-
 Gemfile                         | 1 +
 Gemfile.lock                    | 2 ++
 spec/spec_helper.rb             | 8 ++++++++
 4 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml
index 07fd25fb1..101de66ac 100644
--- a/.github/workflows/test-ruby.yml
+++ b/.github/workflows/test-ruby.yml
@@ -94,7 +94,7 @@ jobs:
       DB_HOST: localhost
       DB_USER: postgres
       DB_PASS: postgres
-      DISABLE_SIMPLECOV: true
+      DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }}
       RAILS_ENV: test
       ALLOW_NOPAM: true
       PAM_ENABLED: true
@@ -137,6 +137,12 @@ jobs:
 
       - run: bin/rspec
 
+      - name: Upload coverage reports to Codecov
+        if: matrix.ruby-version == '.ruby-version'
+        uses: codecov/codecov-action@v3
+        with:
+          files: coverage/lcov/mastodon.lcov
+
   test-e2e:
     name: End to End testing
     runs-on: ubuntu-latest
diff --git a/Gemfile b/Gemfile
index 039e13675..add7b3606 100644
--- a/Gemfile
+++ b/Gemfile
@@ -139,6 +139,7 @@ group :test do
 
   # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false
   gem 'simplecov', '~> 0.22', require: false
+  gem 'simplecov-lcov', '~> 0.8', require: false
 
   # Stub web requests for specs
   gem 'webmock', '~> 3.18'
diff --git a/Gemfile.lock b/Gemfile.lock
index 84ad19b80..20c958e2e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -725,6 +725,7 @@ GEM
       simplecov-html (~> 0.11)
       simplecov_json_formatter (~> 0.1)
     simplecov-html (0.12.3)
+    simplecov-lcov (0.8.0)
     simplecov_json_formatter (0.1.4)
     smart_properties (1.17.0)
     sprockets (3.7.2)
@@ -936,6 +937,7 @@ DEPENDENCIES
   simple-navigation (~> 4.4)
   simple_form (~> 5.2)
   simplecov (~> 0.22)
+  simplecov-lcov (~> 0.8)
   sprockets (~> 3.7.2)
   sprockets-rails (~> 3.4)
   stackprof
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 7c97d8595..f5dcefc78 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -2,13 +2,21 @@
 
 if ENV['DISABLE_SIMPLECOV'] != 'true'
   require 'simplecov'
+  require 'simplecov-lcov'
+  SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
+  SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
   SimpleCov.start 'rails' do
+    enable_coverage :branch
+    enable_coverage_for_eval
+
     add_filter 'lib/linter'
     add_group 'Policies', 'app/policies'
     add_group 'Presenters', 'app/presenters'
     add_group 'Serializers', 'app/serializers'
     add_group 'Services', 'app/services'
     add_group 'Validators', 'app/validators'
+
+    add_group 'Libraries', 'lib'
   end
 end
 

From 15b2d7eec59c745b418debf63907d8bd08c4a730 Mon Sep 17 00:00:00 2001
From: Emelia Smith <ThisIsMissEm@users.noreply.github.com>
Date: Tue, 14 Nov 2023 18:43:20 +0100
Subject: [PATCH 30/63] Split streaming server from web server (#24702)

---
 Procfile.dev           |   2 +-
 package.json           |  27 ++--------
 streaming/index.js     |   6 ++-
 streaming/package.json |  39 +++++++++++++++
 yarn.lock              | 109 ++++++++++++++++++++++-------------------
 5 files changed, 108 insertions(+), 75 deletions(-)
 create mode 100644 streaming/package.json

diff --git a/Procfile.dev b/Procfile.dev
index fbb2c2de2..f81333b04 100644
--- a/Procfile.dev
+++ b/Procfile.dev
@@ -1,4 +1,4 @@
 web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
 sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
-stream: env PORT=4000 yarn run start
+stream: env PORT=4000 yarn workspace @mastodon/streaming start
 webpack: bin/webpack-dev-server
diff --git a/package.json b/package.json
index 7c73d17c1..bcd91e3fc 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,13 @@
 {
   "name": "@mastodon/mastodon",
   "license": "AGPL-3.0-or-later",
+  "packageManager": "yarn@4.0.2",
   "engines": {
     "node": ">=18"
   },
   "workspaces": [
-    "."
+    ".",
+    "streaming"
   ],
   "scripts": {
     "build:development": "cross-env RAILS_ENV=development NODE_ENV=development ./bin/webpack",
@@ -71,10 +73,8 @@
     "css-loader": "^5.2.7",
     "cssnano": "^6.0.1",
     "detect-passive-events": "^2.0.3",
-    "dotenv": "^16.0.3",
     "emoji-mart": "npm:emoji-mart-lazyload@latest",
     "escape-html": "^1.0.3",
-    "express": "^4.18.2",
     "file-loader": "^6.2.0",
     "font-awesome": "^4.7.0",
     "fuzzysort": "^2.0.4",
@@ -85,21 +85,15 @@
     "immutable": "^4.3.0",
     "imports-loader": "^1.2.0",
     "intl-messageformat": "^10.3.5",
-    "ioredis": "^5.3.2",
     "js-yaml": "^4.1.0",
-    "jsdom": "^22.1.0",
     "lodash": "^4.17.21",
     "mark-loader": "^0.1.6",
     "marky": "^1.2.5",
     "mini-css-extract-plugin": "^1.6.2",
     "mkdirp": "^3.0.1",
-    "npmlog": "^7.0.1",
     "path-complete-extname": "^1.0.0",
-    "pg": "^8.5.0",
-    "pg-connection-string": "^2.6.0",
     "postcss": "^8.4.24",
     "postcss-loader": "^4.3.0",
-    "prom-client": "^15.0.0",
     "prop-types": "^15.8.1",
     "punycode": "^2.3.0",
     "react": "^18.2.0",
@@ -138,7 +132,6 @@
     "tesseract.js": "^2.1.5",
     "tiny-queue": "^0.2.1",
     "twitter-text": "3.1.0",
-    "uuid": "^9.0.0",
     "webpack": "^4.47.0",
     "webpack-assets-manifest": "^4.0.6",
     "webpack-bundle-analyzer": "^4.8.0",
@@ -150,8 +143,7 @@
     "workbox-routing": "^7.0.0",
     "workbox-strategies": "^7.0.0",
     "workbox-webpack-plugin": "^7.0.0",
-    "workbox-window": "^7.0.0",
-    "ws": "^8.12.1"
+    "workbox-window": "^7.0.0"
   },
   "devDependencies": {
     "@formatjs/cli": "^6.1.1",
@@ -160,16 +152,13 @@
     "@types/babel__core": "^7.20.1",
     "@types/emoji-mart": "^3.0.9",
     "@types/escape-html": "^1.0.2",
-    "@types/express": "^4.17.17",
     "@types/hoist-non-react-statics": "^3.3.1",
     "@types/http-link-header": "^1.0.3",
     "@types/intl": "^1.2.0",
     "@types/jest": "^29.5.2",
     "@types/js-yaml": "^4.0.5",
     "@types/lodash": "^4.14.195",
-    "@types/npmlog": "^4.1.4",
     "@types/object-assign": "^4.0.30",
-    "@types/pg": "^8.6.6",
     "@types/prop-types": "^15.7.5",
     "@types/punycode": "^2.1.0",
     "@types/react": "^18.2.7",
@@ -188,7 +177,6 @@
     "@types/react-toggle": "^4.0.3",
     "@types/redux-immutable": "^4.0.3",
     "@types/requestidlecallback": "^0.3.5",
-    "@types/uuid": "^9.0.0",
     "@types/webpack": "^4.41.33",
     "@types/yargs": "^17.0.24",
     "@typescript-eslint/eslint-plugin": "^6.0.0",
@@ -232,15 +220,10 @@
       "optional": true
     }
   },
-  "optionalDependencies": {
-    "bufferutil": "^4.0.7",
-    "utf-8-validate": "^6.0.3"
-  },
   "lint-staged": {
     "*": "prettier --ignore-unknown --write",
     "Capfile|Gemfile|*.{rb,ruby,ru,rake}": "bundle exec rubocop --force-exclusion -a",
     "*.{js,jsx,ts,tsx}": "eslint --fix",
     "*.{css,scss}": "stylelint --fix"
-  },
-  "packageManager": "yarn@4.0.2"
+  }
 }
diff --git a/streaming/index.js b/streaming/index.js
index b3765531c..e3b63b53a 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -2,6 +2,7 @@
 
 const fs = require('fs');
 const http = require('http');
+const path = require('path');
 const url = require('url');
 
 const dotenv = require('dotenv');
@@ -17,8 +18,11 @@ const WebSocket = require('ws');
 
 const environment = process.env.NODE_ENV || 'development';
 
+// Correctly detect and load .env or .env.production file based on environment:
+const dotenvFile = environment === 'production' ? '.env.production' : '.env';
+
 dotenv.config({
-  path: environment === 'production' ? '.env.production' : '.env',
+  path: path.resolve(__dirname, path.join('..', dotenvFile))
 });
 
 log.level = process.env.LOG_LEVEL || 'verbose';
diff --git a/streaming/package.json b/streaming/package.json
new file mode 100644
index 000000000..d3f214432
--- /dev/null
+++ b/streaming/package.json
@@ -0,0 +1,39 @@
+{
+  "name": "@mastodon/streaming",
+  "license": "AGPL-3.0-or-later",
+  "packageManager": "yarn@4.0.1",
+  "engines": {
+    "node": ">=18"
+  },
+  "description": "Mastodon's Streaming Server",
+  "private": true,
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/mastodon/mastodon.git"
+  },
+  "scripts": {
+    "start": "node ./index.js"
+  },
+  "dependencies": {
+    "dotenv": "^16.0.3",
+    "express": "^4.18.2",
+    "ioredis": "^5.3.2",
+    "jsdom": "^22.1.0",
+    "npmlog": "^7.0.1",
+    "pg": "^8.5.0",
+    "pg-connection-string": "^2.6.0",
+    "prom-client": "^15.0.0",
+    "uuid": "^9.0.0",
+    "ws": "^8.12.1"
+  },
+  "devDependencies": {
+    "@types/express": "^4.17.17",
+    "@types/npmlog": "^4.1.4",
+    "@types/pg": "^8.6.6",
+    "@types/uuid": "^9.0.0"
+  },
+  "optionalDependencies": {
+    "bufferutil": "^4.0.7",
+    "utf-8-validate": "^6.0.3"
+  }
+}
diff --git a/yarn.lock b/yarn.lock
index addbe638f..c88dd49d0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2368,16 +2368,13 @@ __metadata:
     "@types/babel__core": "npm:^7.20.1"
     "@types/emoji-mart": "npm:^3.0.9"
     "@types/escape-html": "npm:^1.0.2"
-    "@types/express": "npm:^4.17.17"
     "@types/hoist-non-react-statics": "npm:^3.3.1"
     "@types/http-link-header": "npm:^1.0.3"
     "@types/intl": "npm:^1.2.0"
     "@types/jest": "npm:^29.5.2"
     "@types/js-yaml": "npm:^4.0.5"
     "@types/lodash": "npm:^4.14.195"
-    "@types/npmlog": "npm:^4.1.4"
     "@types/object-assign": "npm:^4.0.30"
-    "@types/pg": "npm:^8.6.6"
     "@types/prop-types": "npm:^15.7.5"
     "@types/punycode": "npm:^2.1.0"
     "@types/react": "npm:^18.2.7"
@@ -2396,7 +2393,6 @@ __metadata:
     "@types/react-toggle": "npm:^4.0.3"
     "@types/redux-immutable": "npm:^4.0.3"
     "@types/requestidlecallback": "npm:^0.3.5"
-    "@types/uuid": "npm:^9.0.0"
     "@types/webpack": "npm:^4.41.33"
     "@types/yargs": "npm:^17.0.24"
     "@typescript-eslint/eslint-plugin": "npm:^6.0.0"
@@ -2412,7 +2408,6 @@ __metadata:
     babel-plugin-preval: "npm:^5.1.0"
     babel-plugin-transform-react-remove-prop-types: "npm:^0.4.24"
     blurhash: "npm:^2.0.5"
-    bufferutil: "npm:^4.0.7"
     circular-dependency-plugin: "npm:^5.2.2"
     classnames: "npm:^2.3.2"
     cocoon-js-vanilla: "npm:^1.3.0"
@@ -2423,7 +2418,6 @@ __metadata:
     css-loader: "npm:^5.2.7"
     cssnano: "npm:^6.0.1"
     detect-passive-events: "npm:^2.0.3"
-    dotenv: "npm:^16.0.3"
     emoji-mart: "npm:emoji-mart-lazyload@latest"
     escape-html: "npm:^1.0.3"
     eslint: "npm:^8.41.0"
@@ -2437,7 +2431,6 @@ __metadata:
     eslint-plugin-promise: "npm:~6.1.1"
     eslint-plugin-react: "npm:~7.33.0"
     eslint-plugin-react-hooks: "npm:^4.6.0"
-    express: "npm:^4.18.2"
     file-loader: "npm:^6.2.0"
     font-awesome: "npm:^4.7.0"
     fuzzysort: "npm:^2.0.4"
@@ -2449,25 +2442,19 @@ __metadata:
     immutable: "npm:^4.3.0"
     imports-loader: "npm:^1.2.0"
     intl-messageformat: "npm:^10.3.5"
-    ioredis: "npm:^5.3.2"
     jest: "npm:^29.5.0"
     jest-environment-jsdom: "npm:^29.5.0"
     js-yaml: "npm:^4.1.0"
-    jsdom: "npm:^22.1.0"
     lint-staged: "npm:^15.0.0"
     lodash: "npm:^4.17.21"
     mark-loader: "npm:^0.1.6"
     marky: "npm:^1.2.5"
     mini-css-extract-plugin: "npm:^1.6.2"
     mkdirp: "npm:^3.0.1"
-    npmlog: "npm:^7.0.1"
     path-complete-extname: "npm:^1.0.0"
-    pg: "npm:^8.5.0"
-    pg-connection-string: "npm:^2.6.0"
     postcss: "npm:^8.4.24"
     postcss-loader: "npm:^4.3.0"
     prettier: "npm:^3.0.0"
-    prom-client: "npm:^15.0.0"
     prop-types: "npm:^15.8.1"
     punycode: "npm:^2.3.0"
     react: "npm:^18.2.0"
@@ -2510,8 +2497,6 @@ __metadata:
     tiny-queue: "npm:^0.2.1"
     twitter-text: "npm:3.1.0"
     typescript: "npm:^5.0.4"
-    utf-8-validate: "npm:^6.0.3"
-    uuid: "npm:^9.0.0"
     webpack: "npm:^4.47.0"
     webpack-assets-manifest: "npm:^4.0.6"
     webpack-bundle-analyzer: "npm:^4.8.0"
@@ -2525,13 +2510,7 @@ __metadata:
     workbox-strategies: "npm:^7.0.0"
     workbox-webpack-plugin: "npm:^7.0.0"
     workbox-window: "npm:^7.0.0"
-    ws: "npm:^8.12.1"
     yargs: "npm:^17.7.2"
-  dependenciesMeta:
-    bufferutil:
-      optional: true
-    utf-8-validate:
-      optional: true
   peerDependenciesMeta:
     react:
       optional: true
@@ -2542,6 +2521,34 @@ __metadata:
   languageName: unknown
   linkType: soft
 
+"@mastodon/streaming@workspace:streaming":
+  version: 0.0.0-use.local
+  resolution: "@mastodon/streaming@workspace:streaming"
+  dependencies:
+    "@types/express": "npm:^4.17.17"
+    "@types/npmlog": "npm:^4.1.4"
+    "@types/pg": "npm:^8.6.6"
+    "@types/uuid": "npm:^9.0.0"
+    bufferutil: "npm:^4.0.7"
+    dotenv: "npm:^16.0.3"
+    express: "npm:^4.18.2"
+    ioredis: "npm:^5.3.2"
+    jsdom: "npm:^22.1.0"
+    npmlog: "npm:^7.0.1"
+    pg: "npm:^8.5.0"
+    pg-connection-string: "npm:^2.6.0"
+    prom-client: "npm:^15.0.0"
+    utf-8-validate: "npm:^6.0.3"
+    uuid: "npm:^9.0.0"
+    ws: "npm:^8.12.1"
+  dependenciesMeta:
+    bufferutil:
+      optional: true
+    utf-8-validate:
+      optional: true
+  languageName: unknown
+  linkType: soft
+
 "@material-symbols/svg-600@npm:^0.14.0":
   version: 0.14.0
   resolution: "@material-symbols/svg-600@npm:0.14.0"
@@ -3056,21 +3063,21 @@ __metadata:
   linkType: hard
 
 "@types/body-parser@npm:*":
-  version: 1.19.4
-  resolution: "@types/body-parser@npm:1.19.4"
+  version: 1.19.5
+  resolution: "@types/body-parser@npm:1.19.5"
   dependencies:
     "@types/connect": "npm:*"
     "@types/node": "npm:*"
-  checksum: bec2b8a97861a960ee415f7ab3c2aeb7f4d779fd364d27ddee46057897ea571735f1f854f5ee41682964315d4e3699f62427998b9c21851d773398ef535f0612
+  checksum: aebeb200f25e8818d8cf39cd0209026750d77c9b85381cdd8deeb50913e4d18a1ebe4b74ca9b0b4d21952511eeaba5e9fbbf739b52731a2061e206ec60d568df
   languageName: node
   linkType: hard
 
 "@types/connect@npm:*":
-  version: 3.4.37
-  resolution: "@types/connect@npm:3.4.37"
+  version: 3.4.38
+  resolution: "@types/connect@npm:3.4.38"
   dependencies:
     "@types/node": "npm:*"
-  checksum: 79fd5c32a8bb5c9548369e6da3221b6a820f3a8c5396d50f6f642712b9f4c1c881ef86bdf48994a4a279e81998563410b8843c5a10dde5521d5ef6a8ae944c3b
+  checksum: 2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c
   languageName: node
   linkType: hard
 
@@ -3115,14 +3122,14 @@ __metadata:
   linkType: hard
 
 "@types/express-serve-static-core@npm:^4.17.33":
-  version: 4.17.39
-  resolution: "@types/express-serve-static-core@npm:4.17.39"
+  version: 4.17.41
+  resolution: "@types/express-serve-static-core@npm:4.17.41"
   dependencies:
     "@types/node": "npm:*"
     "@types/qs": "npm:*"
     "@types/range-parser": "npm:*"
     "@types/send": "npm:*"
-  checksum: b23b005fddd2ba3f7142ec9713f06b5582c7712cdf99c3419d3972364903b348a103c3264d9a761d6497140e3b89bd416454684c4bdeff206b4c59b86e96428a
+  checksum: dc166cbf4475c00a81fbcab120bf7477c527184be11ae149df7f26d9c1082114c68f8d387a2926fe80291b06477c8bbd9231ff4f5775de328e887695aefce269
   languageName: node
   linkType: hard
 
@@ -3175,9 +3182,9 @@ __metadata:
   linkType: hard
 
 "@types/http-errors@npm:*":
-  version: 2.0.3
-  resolution: "@types/http-errors@npm:2.0.3"
-  checksum: 717ce3e8f49a1facb7130fed934108fa8a51ab02089a1049c782e353e0e08e79bdfaac054c2a94db14ea400302e523276387363aa820eaf0031af8ba5d2941dc
+  version: 2.0.4
+  resolution: "@types/http-errors@npm:2.0.4"
+  checksum: 494670a57ad4062fee6c575047ad5782506dd35a6b9ed3894cea65830a94367bd84ba302eb3dde331871f6d70ca287bfedb1b2cf658e6132cd2cbd427ab56836
   languageName: node
   linkType: hard
 
@@ -3279,16 +3286,16 @@ __metadata:
   linkType: hard
 
 "@types/mime@npm:*":
-  version: 3.0.3
-  resolution: "@types/mime@npm:3.0.3"
-  checksum: cef99f8cdc42af9de698027c2a20ba5df12bc9a89dcf5513e70103ebb55e00c5f5c585d02411f4b42fde0e78488342f1b1d3e3546a59a3da42e95fdc616e01eb
+  version: 3.0.4
+  resolution: "@types/mime@npm:3.0.4"
+  checksum: db478bc0f99e40f7b3e01d356a9bdf7817060808a294978111340317bcd80ca35382855578c5b60fbc84ae449674bd9bb38427b18417e1f8f19e4f72f8b242cd
   languageName: node
   linkType: hard
 
 "@types/mime@npm:^1":
-  version: 1.3.4
-  resolution: "@types/mime@npm:1.3.4"
-  checksum: a0a16d26c0e70a1b133e26e7c46b70b3136b7e894396bdb7de1c642f4ac87fdbbba26bf56cf73f001312289d89de4f1c06ab745d9445850df45a5a802564c4d6
+  version: 1.3.5
+  resolution: "@types/mime@npm:1.3.5"
+  checksum: c2ee31cd9b993804df33a694d5aa3fa536511a49f2e06eeab0b484fef59b4483777dbb9e42a4198a0809ffbf698081fdbca1e5c2218b82b91603dfab10a10fbc
   languageName: node
   linkType: hard
 
@@ -3392,16 +3399,16 @@ __metadata:
   linkType: hard
 
 "@types/qs@npm:*":
-  version: 6.9.9
-  resolution: "@types/qs@npm:6.9.9"
-  checksum: aede2a4181a49ae8548a1354bac3f8235cb0c5aab066b10875a3e68e88a199e220f4284e7e2bb75a3c18e5d4ff6abe1a6ce0389ef31b63952cc45e0f4d885ba0
+  version: 6.9.10
+  resolution: "@types/qs@npm:6.9.10"
+  checksum: 6be12e5f062d1b41eb037d59bf9cb65bc9410cedd5e6da832dfd7c8e2b3f4c91e81c9b90b51811140770e5052c6c4e8361181bd9437ddcd4515dc128b7c00353
   languageName: node
   linkType: hard
 
 "@types/range-parser@npm:*":
-  version: 1.2.6
-  resolution: "@types/range-parser@npm:1.2.6"
-  checksum: 46e7fffc54cdacc8fb0cd576f8f9a6436453f0176205d6ec55434a460c7677e78e688673426d5db5e480501b2943ba08a16ececa3a354c222093551c7217fb8f
+  version: 1.2.7
+  resolution: "@types/range-parser@npm:1.2.7"
+  checksum: 361bb3e964ec5133fa40644a0b942279ed5df1949f21321d77de79f48b728d39253e5ce0408c9c17e4e0fd95ca7899da36841686393b9f7a1e209916e9381a3c
   languageName: node
   linkType: hard
 
@@ -3587,23 +3594,23 @@ __metadata:
   linkType: hard
 
 "@types/send@npm:*":
-  version: 0.17.3
-  resolution: "@types/send@npm:0.17.3"
+  version: 0.17.4
+  resolution: "@types/send@npm:0.17.4"
   dependencies:
     "@types/mime": "npm:^1"
     "@types/node": "npm:*"
-  checksum: 773a0cb55ea03eefbe9a0e6d42114e0f84968db30954a131aae9ba7e9ab984a4776915447ebdeab4412d7f11750126614b0b75e99413f75810045bdb3196554a
+  checksum: 7f17fa696cb83be0a104b04b424fdedc7eaba1c9a34b06027239aba513b398a0e2b7279778af521f516a397ced417c96960e5f50fcfce40c4bc4509fb1a5883c
   languageName: node
   linkType: hard
 
 "@types/serve-static@npm:*":
-  version: 1.15.4
-  resolution: "@types/serve-static@npm:1.15.4"
+  version: 1.15.5
+  resolution: "@types/serve-static@npm:1.15.5"
   dependencies:
     "@types/http-errors": "npm:*"
     "@types/mime": "npm:*"
     "@types/node": "npm:*"
-  checksum: 061b38993bf8f2b5033f57147c8ec90e1d1a0d6f734958ceb531ba7cc31192fd272c999cdbc57ede8672787e3aa171ec142dc65a467c04078e43823e7476eb49
+  checksum: 811d1a2f7e74a872195e7a013bcd87a2fb1edf07eaedcb9dcfd20c1eb4bc56ad4ea0d52141c13192c91ccda7c8aeb8a530d8a7e60b9c27f5990d7e62e0fecb03
   languageName: node
   linkType: hard
 

From 998f0684994b9be5ccad986b83308039ff395ef6 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 14 Nov 2023 18:52:34 +0100
Subject: [PATCH 31/63] Update Yarn to v4.0.2 (#27857)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 streaming/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/streaming/package.json b/streaming/package.json
index d3f214432..a474e6226 100644
--- a/streaming/package.json
+++ b/streaming/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@mastodon/streaming",
   "license": "AGPL-3.0-or-later",
-  "packageManager": "yarn@4.0.1",
+  "packageManager": "yarn@4.0.2",
   "engines": {
     "node": ">=18"
   },

From 7c72944661c100118555045a87ced7d1fa7cc417 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 15 Nov 2023 04:11:02 -0500
Subject: [PATCH 32/63] Use `Lcov` simplecov formatter on CI and `HTML`
 elsewhere (#27859)

---
 spec/spec_helper.rb | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index f5dcefc78..0bb4f88cf 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -2,9 +2,14 @@
 
 if ENV['DISABLE_SIMPLECOV'] != 'true'
   require 'simplecov'
-  require 'simplecov-lcov'
-  SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
-  SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
+
+  if ENV['CI']
+    require 'simplecov-lcov'
+    SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
+    SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
+  else
+    SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter
+  end
   SimpleCov.start 'rails' do
     enable_coverage :branch
     enable_coverage_for_eval

From b6f29106eab8c5deeb1c22051387292108bbb3c8 Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Wed, 15 Nov 2023 10:20:24 +0100
Subject: [PATCH 33/63] Improve codecov config (#27860)

---
 .github/codecov.yml | 13 +++++++++++++
 1 file changed, 13 insertions(+)
 create mode 100644 .github/codecov.yml

diff --git a/.github/codecov.yml b/.github/codecov.yml
new file mode 100644
index 000000000..5532c4961
--- /dev/null
+++ b/.github/codecov.yml
@@ -0,0 +1,13 @@
+coverage:
+  status:
+    project:
+      default:
+        # Github status check is not blocking
+        informational: true
+    patch:
+      default:
+        # Github status check is not blocking
+        informational: true
+comment:
+  # Only write a comment in PR if there are changes
+  require_changes: true

From 5d75799afaacb5b530d9abdb7464db779d3fcbe0 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 15 Nov 2023 09:21:25 +0000
Subject: [PATCH 34/63] Update dependency axios to v1.6.2 (#27861)

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 c88dd49d0..e66f58f8b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4690,13 +4690,13 @@ __metadata:
   linkType: hard
 
 "axios@npm:^1.4.0":
-  version: 1.6.1
-  resolution: "axios@npm:1.6.1"
+  version: 1.6.2
+  resolution: "axios@npm:1.6.2"
   dependencies:
     follow-redirects: "npm:^1.15.0"
     form-data: "npm:^4.0.0"
     proxy-from-env: "npm:^1.1.0"
-  checksum: ca2c6f56659a7f19e4a99082f549fe151952f6fd8aa72ed148559ab2d6a32ce37cd5dc72ce6d4d3cd91f0c1e2617c7c95c20077e5e244a79f319a6c0ce41204f
+  checksum: 9b77e030e85e4f9cbcba7bb52fbff67d6ce906c92d213e0bd932346a50140faf83733bf786f55bd58301bd92f9973885c7b87d6348023e10f7eaf286d0791a1d
   languageName: node
   linkType: hard
 

From 922f086253c8bfcead9df895f4624580d5b61a9c Mon Sep 17 00:00:00 2001
From: Jeong Arm <kjwonmail@gmail.com>
Date: Wed, 15 Nov 2023 18:29:10 +0900
Subject: [PATCH 35/63] Fix open status on media modal (#27867)

---
 .../mastodon/features/picture_in_picture/components/footer.jsx  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx
index a4ea989fb..9b26e2d75 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx
+++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx
@@ -166,7 +166,7 @@ class Footer extends ImmutablePureComponent {
       onClose();
     }
 
-    history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
+    this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
   };
 
   render () {

From d8074128f9f5f84bd9d5adc416ef67ae922f0289 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 15 Nov 2023 10:41:24 +0100
Subject: [PATCH 36/63] New Crowdin Translations (automated) (#27866)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/cs.json    |  1 +
 app/javascript/mastodon/locales/eu.json    |  2 +-
 app/javascript/mastodon/locales/it.json    |  2 +-
 app/javascript/mastodon/locales/lt.json    | 60 +++++++++++---
 app/javascript/mastodon/locales/zh-TW.json | 36 ++++-----
 config/locales/activerecord.lt.yml         | 45 +++++++++++
 config/locales/be.yml                      |  6 +-
 config/locales/cs.yml                      |  7 ++
 config/locales/devise.zh-TW.yml            | 10 +--
 config/locales/doorkeeper.zh-TW.yml        |  2 +-
 config/locales/pl.yml                      |  4 +-
 config/locales/simple_form.zh-TW.yml       | 50 ++++++------
 config/locales/vi.yml                      |  1 +
 config/locales/zh-TW.yml                   | 92 +++++++++++-----------
 14 files changed, 207 insertions(+), 111 deletions(-)

diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index 33c7c31d9..aaf50d67c 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Výsledky hledání",
   "emoji_button.symbols": "Symboly",
   "emoji_button.travel": "Cestování a místa",
+  "empty_column.account_hides_collections": "Tento uživatel se rozhodl nezveřejňovat tuto informaci",
   "empty_column.account_suspended": "Účet je pozastaven",
   "empty_column.account_timeline": "Nejsou tu žádné příspěvky!",
   "empty_column.account_unavailable": "Profil není dostupný",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index 7f109ea08..419589aba 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -389,7 +389,7 @@
   "lists.replies_policy.title": "Erakutsi erantzunak:",
   "lists.search": "Bilatu jarraitzen dituzun pertsonen artean",
   "lists.subheading": "Zure zerrendak",
-  "load_pending": "{count, plural, one {eleentuberri #} other {# elementu berri}}",
+  "load_pending": "{count, plural, one {elementu berri #} other {# elementu berri}}",
   "loading_indicator.label": "Kargatzen...",
   "media_gallery.toggle_visible": "Txandakatu ikusgaitasuna",
   "moved_to_account_banner.text": "Zure {disabledAccount} kontua desgaituta dago une honetan, {movedToAccount} kontura aldatu zinelako.",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 4a2f41ce6..8ad791bfe 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -252,7 +252,7 @@
   "explore.search_results": "Risultati della ricerca",
   "explore.suggested_follows": "Persone",
   "explore.title": "Esplora",
-  "explore.trending_links": "Novità",
+  "explore.trending_links": "Notizie",
   "explore.trending_statuses": "Post",
   "explore.trending_tags": "Hashtag",
   "filter_modal.added.context_mismatch_explanation": "La categoria di questo filtro non si applica al contesto in cui hai acceduto a questo post. Se desideri che il post sia filtrato anche in questo contesto, dovrai modificare il filtro.",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index 5cdc575de..5675799e7 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -1,7 +1,7 @@
 {
   "about.blocks": "Prižiūrimi serveriai",
   "about.contact": "Kontaktuoti:",
-  "about.disclaimer": "Mastodon – nemokama atvirojo šaltinio programa ir Mastodon gGmbH prekės ženklas.",
+  "about.disclaimer": "Mastodon – nemokama atvirojo kodo programa ir Mastodon gGmbH prekės ženklas.",
   "about.domain_blocks.no_reason_available": "Priežastis nežinoma",
   "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).",
@@ -33,28 +33,46 @@
   "account.followers.empty": "Šio naudotojo dar niekas neseka.",
   "account.followers_counter": "{count, plural, one {{counter} sekėjas (-a)} few {{counter} sekėjai} many {{counter} sekėjo} other {{counter} sekėjų}}",
   "account.following": "Seka",
-  "account.follows.empty": "Šis naudotojas (-a) dar nieko neseka.",
+  "account.follows.empty": "Šis (-i) naudotojas (-a) dar nieko neseka.",
   "account.follows_you": "Seka tave",
   "account.go_to_profile": "Eiti į profilį",
   "account.in_memoriam": "Atminimui.",
   "account.joined_short": "Prisijungė",
   "account.languages": "Keisti prenumeruojamas kalbas",
+  "account.link_verified_on": "Šios nuorodos nuosavybė buvo patikrinta {date}",
   "account.locked_info": "Šios paskyros privatumo būsena nustatyta kaip užrakinta. Savininkas (-ė) rankiniu būdu peržiūri, kas gali sekti.",
   "account.media": "Medija",
   "account.mention": "Paminėti @{name}",
   "account.moved_to": "{name} nurodė, kad dabar jų nauja paskyra yra:",
   "account.mute": "Užtildyti @{name}",
+  "account.mute_notifications_short": "Nutildyti pranešimus",
+  "account.mute_short": "Nutildyti",
   "account.muted": "Užtildytas",
-  "account.posts": "Toots",
-  "account.posts_with_replies": "Toots and replies",
-  "account.report": "Pranešti apie @{name}",
-  "account.requested": "Awaiting approval",
+  "account.no_bio": "Nėra pateikto aprašymo.",
+  "account.open_original_page": "Atidaryti originalinį tinklalapį",
+  "account.posts": "Įrašai",
+  "account.posts_with_replies": "Įrašai ir atsakymai",
+  "account.report": "Pranešti @{name}",
+  "account.requested": "Laukiama patvirtinimo. Spausk, kad atšaukti sekimo užklausą.",
+  "account.requested_follow": "{name} paprašė tave sekti",
+  "account.share": "Bendrinti @{name} profilį",
   "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unblock_short": "Atblokuoti",
   "account.unfollow": "Nebesekti",
+  "account.unmute": "Atitildyti @{name}",
+  "account.unmute_notifications_short": "Atitildyti pranešimus",
   "account.unmute_short": "Atitildyti",
-  "account_note.placeholder": "Click to add a note",
+  "account_note.placeholder": "Spausk norėdamas (-a) pridėti pastabą",
+  "admin.dashboard.retention.average": "Vidurkis",
+  "admin.dashboard.retention.cohort": "Registravimo mėnuo",
+  "admin.dashboard.retention.cohort_size": "Nauji naudotojai",
+  "admin.impact_report.instance_accounts": "Paskyrų profiliai, kuriuos tai ištrintų",
+  "admin.impact_report.instance_followers": "Sekėjai, kuriuos prarastų mūsų naudotojai",
+  "admin.impact_report.instance_follows": "Sekėjai, kuriuos prarastų jų naudotojai",
+  "admin.impact_report.title": "Poveikio apibendrinimas",
+  "alert.rate_limited.message": "Pabandyk vėliau po {retry_time, time, medium}.",
+  "alert.rate_limited.title": "Spartos ribojimas",
   "alert.unexpected.message": "Įvyko netikėta klaida.",
   "alert.unexpected.title": "Ups!",
   "announcement.announcement": "Skelbimas",
@@ -65,6 +83,14 @@
   "bundle_column_error.copy_stacktrace": "Kopijuoti klaidos ataskaitą",
   "bundle_column_error.error.body": "Užklausos puslapio nepavyko atvaizduoti. Tai gali būti dėl mūsų kodo klaidos arba naršyklės suderinamumo problemos.",
   "bundle_column_error.error.title": "O, ne!",
+  "bundle_column_error.network.body": "Bandant užkrauti šį puslapį įvyko klaida. Tai galėjo atsitikti dėl laikinos tavo interneto ryšio arba šio serverio problemos.",
+  "bundle_column_error.network.title": "Tinklo klaida",
+  "bundle_column_error.retry": "Bandyti dar kartą",
+  "bundle_column_error.return": "Grįžti į pradžią",
+  "bundle_column_error.routing.body": "Prašyto puslapio nepavyko rasti. Ar esi tikras (-a), kad adreso juostoje nurodytas URL adresas yra teisingas?",
+  "bundle_column_error.routing.title": "404",
+  "bundle_modal_error.close": "Uždaryti",
+  "closed_registrations_modal.find_another_server": "Rasti kitą serverį",
   "column.domain_blocks": "Hidden domains",
   "column.lists": "Sąrašai",
   "column.mutes": "Užtildyti vartotojai",
@@ -81,18 +107,32 @@
   "compose.published.body": "Įrašas paskelbtas.",
   "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
   "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
-  "compose_form.placeholder": "What is on your mind?",
+  "compose_form.placeholder": "Kas tavo mintyse?",
+  "compose_form.poll.add_option": "Pridėti pasirinkimą",
+  "compose_form.poll.duration": "Apklausos trukmė",
+  "compose_form.poll.option_placeholder": "Pasirinkimas {number}",
+  "compose_form.poll.remove_option": "Pašalinti šį pasirinkimą",
+  "compose_form.poll.switch_to_multiple": "Keisti apklausą, kad būtų galima pasirinkti kelis pasirinkimus",
   "compose_form.publish_form": "Publish",
   "compose_form.sensitive.hide": "{count, plural, one {Žymėti mediją kaip jautrią} few {Žymėti medijas kaip jautrias} many {Žymėti medijos kaip jautrios} other {Žymėti medijų kaip jautrių}}",
   "compose_form.sensitive.marked": "{count, plural, one {Medija pažymėta kaip jautri} few {Medijos pažymėtos kaip jautrios} many {Medijos pažymėta kaip jautrios} other {Medijų pažymėtos kaip jautrios}}",
   "compose_form.sensitive.unmarked": "{count, plural, one {Medija nepažymėta kaip jautri} few {Medijos nepažymėtos kaip jautrios} many {Medijos nepažymėta kaip jautri} other {Medijų nepažymėta kaip jautrios}}",
   "compose_form.spoiler.marked": "Text is hidden behind warning",
-  "compose_form.spoiler.unmarked": "Text is not hidden",
+  "compose_form.spoiler.unmarked": "Pridėti turinio įspėjimą",
+  "compose_form.spoiler_placeholder": "Rašyk savo įspėjimą čia",
+  "confirmation_modal.cancel": "Atšaukti",
+  "confirmations.block.block_and_report": "Blokuoti ir pranešti",
+  "confirmations.block.confirm": "Blokuoti",
+  "confirmations.block.message": "Ar tikrai nori užblokuoti {name}?",
   "confirmations.delete.confirm": "Ištrinti",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
   "confirmations.discard_edit_media.confirm": "Atmesti",
   "confirmations.discard_edit_media.message": "Turi neišsaugotų medijos aprašymo ar peržiūros pakeitimų, vis tiek juos atmesti?",
   "confirmations.domain_block.confirm": "Hide entire domain",
+  "confirmations.logout.confirm": "Atsijungti",
+  "confirmations.logout.message": "Ar tikrai nori atsijungti?",
+  "confirmations.mute.confirm": "Nutildyti",
+  "confirmations.mute.explanation": "Tai paslėps jų įrašus ir įrašus, kuriuose jie menėmi, tačiau jie vis tiek galės matyti tavo įrašus ir sekti.",
   "confirmations.reply.confirm": "Atsakyti",
   "confirmations.reply.message": "Atsakydamas (-a) dabar perrašysi šiuo metu rašomą žinutę. Ar tikrai nori tęsti?",
   "confirmations.unfollow.confirm": "Nebesekti",
@@ -219,6 +259,8 @@
   "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}",
   "upload_form.audio_description": "Describe for people with hearing loss",
   "upload_form.description": "Describe for the visually impaired",
+  "upload_form.description_missing": "Nėra pridėto aprašymo",
+  "upload_form.edit": "Redaguoti",
   "upload_form.video_description": "Describe for people with hearing loss or visual impairment",
   "upload_modal.edit_media": "Redaguoti mediją",
   "upload_progress.label": "Uploading…"
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index db4fe4eab..974096d2f 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -12,7 +12,7 @@
   "about.powered_by": "由 {mastodon} 提供的去中心化社群媒體",
   "about.rules": "伺服器規則",
   "account.account_note_header": "備註",
-  "account.add_or_remove_from_list": "從列表中新增或移除",
+  "account.add_or_remove_from_list": "自列表中新增或移除",
   "account.badges.bot": "機器人",
   "account.badges.group": "群組",
   "account.block": "封鎖 @{name}",
@@ -26,7 +26,7 @@
   "account.domain_blocked": "已封鎖網域",
   "account.edit_profile": "編輯個人檔案",
   "account.enable_notifications": "當 @{name} 嘟文時通知我",
-  "account.endorse": "在個人檔案推薦對方",
+  "account.endorse": "於個人檔案推薦對方",
   "account.featured_tags.last_status_at": "上次嘟文於 {date}",
   "account.featured_tags.last_status_never": "沒有嘟文",
   "account.featured_tags.title": "{name} 的推薦主題標籤",
@@ -65,7 +65,7 @@
   "account.unblock": "解除封鎖 @{name}",
   "account.unblock_domain": "解除封鎖網域 {domain}",
   "account.unblock_short": "解除封鎖",
-  "account.unendorse": "取消在個人檔案推薦對方",
+  "account.unendorse": "取消於個人檔案推薦對方",
   "account.unfollow": "取消跟隨",
   "account.unmute": "解除靜音 @{name}",
   "account.unmute_notifications_short": "取消靜音推播通知",
@@ -102,7 +102,7 @@
   "bundle_modal_error.message": "載入此元件時發生錯誤。",
   "bundle_modal_error.retry": "重試",
   "closed_registrations.other_server_instructions": "因為 Mastodon 是去中心化的,所以您也能於其他伺服器上建立帳號,並仍然與這個伺服器互動。",
-  "closed_registrations_modal.description": "目前無法在 {domain} 建立新帳號,但也請別忘了,您並不一定需要有 {domain} 伺服器的帳號,也能使用 Mastodon 。",
+  "closed_registrations_modal.description": "目前無法於 {domain} 建立新帳號,但也請別忘了,您並不一定需要有 {domain} 伺服器的帳號,也能使用 Mastodon 。",
   "closed_registrations_modal.find_another_server": "尋找另一個伺服器",
   "closed_registrations_modal.preamble": "Mastodon 是去中心化的,所以無論您於哪個伺服器新增帳號,都可以與此伺服器上的任何人跟隨及互動。您甚至能自行架一個自己的伺服器!",
   "closed_registrations_modal.title": "註冊 Mastodon",
@@ -171,7 +171,7 @@
   "confirmations.delete_list.confirm": "刪除",
   "confirmations.delete_list.message": "您確定要永久刪除此列表嗎?",
   "confirmations.discard_edit_media.confirm": "捨棄",
-  "confirmations.discard_edit_media.message": "您在媒體描述或預覽區塊有未儲存的變更。是否要捨棄這些變更?",
+  "confirmations.discard_edit_media.message": "您於媒體描述或預覽區塊有未儲存的變更。是否要捨棄這些變更?",
   "confirmations.domain_block.confirm": "封鎖整個網域",
   "confirmations.domain_block.message": "您真的非常確定要封鎖整個 {domain} 網域嗎?大部分情況下,封鎖或靜音少數特定的帳號就能滿足需求了。您將不能在任何公開的時間軸及通知中看到來自此網域的內容。您來自該網域的跟隨者也將被移除。",
   "confirmations.edit.confirm": "編輯",
@@ -205,7 +205,7 @@
   "dismissable_banner.explore_statuses": "這些於此伺服器以及去中心化網路中其他伺服器發出的嘟文正在被此伺服器上的人們熱烈討論著。越多不同人轉嘟及最愛排名更高。",
   "dismissable_banner.explore_tags": "這些主題標籤正在被此伺服器以及去中心化網路上的人們熱烈討論著。越多不同人所嘟出的主題標籤排名更高。",
   "dismissable_banner.public_timeline": "這些是來自 {domain} 使用者們跟隨中帳號所發表之最新公開嘟文。",
-  "embed.instructions": "要在您的網站嵌入此嘟文,請複製以下程式碼。",
+  "embed.instructions": "若您欲於您的網站嵌入此嘟文,請複製以下程式碼。",
   "embed.preview": "它將顯示成這樣:",
   "emoji_button.activity": "活動",
   "emoji_button.clear": "清除",
@@ -218,7 +218,7 @@
   "emoji_button.objects": "物件",
   "emoji_button.people": "人物",
   "emoji_button.recent": "最常使用",
-  "emoji_button.search": "搜尋…",
+  "emoji_button.search": "搜尋...",
   "emoji_button.search_results": "搜尋結果",
   "emoji_button.symbols": "符號",
   "emoji_button.travel": "旅遊與地點",
@@ -259,7 +259,7 @@
   "filter_modal.added.context_mismatch_title": "不符合情境!",
   "filter_modal.added.expired_explanation": "此過濾器類別已失效,您需要更新過期日期以套用。",
   "filter_modal.added.expired_title": "過期的過濾器!",
-  "filter_modal.added.review_and_configure": "要檢視和進一步設定此過濾器類別,請至 {settings_link}。",
+  "filter_modal.added.review_and_configure": "要檢視與進一步設定此過濾器類別,請至 {settings_link}。",
   "filter_modal.added.review_and_configure_title": "過濾器設定",
   "filter_modal.added.settings_link": "設定頁面",
   "filter_modal.added.short_explanation": "此嘟文已被新增至以下過濾器類別:{title}。",
@@ -362,7 +362,7 @@
   "keyboard_shortcuts.search": "將游標移至搜尋框",
   "keyboard_shortcuts.spoilers": "顯示或隱藏內容警告之嘟文",
   "keyboard_shortcuts.start": "開啟「開始使用」欄位",
-  "keyboard_shortcuts.toggle_hidden": "顯示或隱藏在內容警告之後的嘟文",
+  "keyboard_shortcuts.toggle_hidden": "顯示或隱藏於內容警告之後的嘟文",
   "keyboard_shortcuts.toggle_sensitivity": "顯示或隱藏媒體",
   "keyboard_shortcuts.toot": "發個新嘟文",
   "keyboard_shortcuts.unfocus": "跳離文字撰寫區塊或搜尋框",
@@ -376,7 +376,7 @@
   "limited_account_hint.title": "此個人檔案已被 {domain} 的管理員隱藏。",
   "link_preview.author": "由 {name} 提供",
   "lists.account.add": "新增至列表",
-  "lists.account.remove": "從列表中移除",
+  "lists.account.remove": "自列表中移除",
   "lists.delete": "刪除列表",
   "lists.edit": "編輯列表",
   "lists.edit.submit": "變更標題",
@@ -469,7 +469,7 @@
   "notifications.permission_denied_alert": "由於之前瀏覽器權限被拒絕,無法啟用桌面通知",
   "notifications.permission_required": "由於尚未授予所需的權限,因此無法使用桌面通知。",
   "notifications_permission_banner.enable": "啟用桌面通知",
-  "notifications_permission_banner.how_to_control": "啟用桌面通知以在 Mastodon 沒有開啟的時候接收通知。在已經啟用桌面通知的時候,您可以透過上面的 {icon} 按鈕準確的控制哪些類型的互動會產生桌面通知。",
+  "notifications_permission_banner.how_to_control": "啟用桌面通知以於 Mastodon 沒有開啟的時候接收通知。啟用桌面通知後,您可以透過上面的 {icon} 按鈕準確的控制哪些類型的互動會產生桌面通知。",
   "notifications_permission_banner.title": "不要錯過任何東西!",
   "onboarding.action.back": "返回",
   "onboarding.actions.back": "返回",
@@ -490,7 +490,7 @@
   "onboarding.steps.follow_people.title": "客製化您的首頁時間軸",
   "onboarding.steps.publish_status.body": "向新世界打聲招呼吧。",
   "onboarding.steps.publish_status.title": "撰寫您第一則嘟文",
-  "onboarding.steps.setup_profile.body": "若您完整填寫個人檔案,其他人比較願意和您互動。",
+  "onboarding.steps.setup_profile.body": "若您完整填寫個人檔案,其他人比較願意與您互動。",
   "onboarding.steps.setup_profile.title": "客製化您的個人檔案",
   "onboarding.steps.share_profile.body": "讓您的朋友們知道如何於 Mastodon 找到您!",
   "onboarding.steps.share_profile.title": "分享您的 Mastodon 個人檔案",
@@ -614,10 +614,10 @@
   "sign_in_banner.create_account": "新增帳號",
   "sign_in_banner.sign_in": "登入",
   "sign_in_banner.sso_redirect": "登入或註冊",
-  "sign_in_banner.text": "登入以跟隨個人檔案和主題標籤,或收藏、分享和回覆嘟文。您也可以使用您的帳號在其他伺服器上進行互動。",
+  "sign_in_banner.text": "登入以跟隨個人檔案與主題標籤,或收藏、分享及回覆嘟文。您也可以使用您的帳號於其他伺服器進行互動。",
   "status.admin_account": "開啟 @{name} 的管理介面",
   "status.admin_domain": "開啟 {domain} 的管理介面",
-  "status.admin_status": "在管理介面開啟此嘟文",
+  "status.admin_status": "於管理介面開啟此嘟文",
   "status.block": "封鎖 @{name}",
   "status.bookmark": "書籤",
   "status.cancel_reblog_private": "取消轉嘟",
@@ -672,8 +672,8 @@
   "status.translated_from_with": "透過 {provider} 翻譯 {lang}",
   "status.uncached_media_warning": "無法預覽",
   "status.unmute_conversation": "解除此對話的靜音",
-  "status.unpin": "從個人檔案頁面取消釘選",
-  "subscribed_languages.lead": "僅選定語言的嘟文才會出現在您的首頁上,並在變更後列出時間軸。選取「無」以接收所有語言的嘟文。",
+  "status.unpin": "自個人檔案頁面取消釘選",
+  "subscribed_languages.lead": "僅選定語言的嘟文才會出現於您的首頁上,並於變更後列出時間軸。選取「無」以接收所有語言的嘟文。",
   "subscribed_languages.save": "儲存變更",
   "subscribed_languages.target": "變更 {target} 的訂閱語言",
   "tabs_bar.home": "首頁",
@@ -696,7 +696,7 @@
   "upload_area.title": "拖放來上傳",
   "upload_button.label": "上傳圖片、影片、或者音樂檔案",
   "upload_error.limit": "已達到檔案上傳限制。",
-  "upload_error.poll": "不允許在投票中上傳檔案。",
+  "upload_error.poll": "不允許於投票時上傳檔案。",
   "upload_form.audio_description": "為聽障人士增加文字說明",
   "upload_form.description": "為視障人士增加文字說明",
   "upload_form.description_missing": "沒有任何描述",
@@ -706,7 +706,7 @@
   "upload_form.video_description": "為聽障或視障人士增加文字說明",
   "upload_modal.analyzing_picture": "正在分析圖片…",
   "upload_modal.apply": "套用",
-  "upload_modal.applying": "正在套用⋯⋯",
+  "upload_modal.applying": "正在套用...",
   "upload_modal.choose_image": "選擇圖片",
   "upload_modal.description_placeholder": "我能吞下玻璃而不傷身體",
   "upload_modal.detect_text": "從圖片中偵測文字",
diff --git a/config/locales/activerecord.lt.yml b/config/locales/activerecord.lt.yml
index f54d00471..cb6e21d8e 100644
--- a/config/locales/activerecord.lt.yml
+++ b/config/locales/activerecord.lt.yml
@@ -1,6 +1,19 @@
 ---
 lt:
   activerecord:
+    attributes:
+      poll:
+        expires_at: Galutinė data
+        options: Pasirinkimai
+      user:
+        agreement: Paslaugos sutartis
+        email: El. laiško adresas
+        locale: Lokali
+        password: Slaptažodis
+      user/account:
+        username: Naudotojo vardas
+      user/invite_request:
+        text: Priežastis
     errors:
       models:
         account:
@@ -12,3 +25,35 @@ lt:
           attributes:
             url:
               invalid: nėra tinkamas URL adresas.
+        doorkeeper/application:
+          attributes:
+            website:
+              invalid: nėra tinkamas URL adresas.
+        import:
+          attributes:
+            data:
+              malformed: yra netaisyklinga.
+        status:
+          attributes:
+            reblog:
+              taken: įrašas jau egzistuoja.
+        user:
+          attributes:
+            email:
+              blocked: naudoja neleidžiamą el. laiško paslaugų teikėją.
+              unreachable: neatrodo, kad egzistuoja.
+            role_id:
+              elevated: negali būti didesnis nei tavo dabartinis vaidmuo.
+        user_role:
+          attributes:
+            permissions_as_keys:
+              dangerous: apima leidimus, kurie nėra saugūs pagrindiniam vaidmeniui.
+              elevated: negali apimti leidimų, kurių neturi tavo dabartinis vaidmuo.
+              own_role: negali būti pakeistas tavo dabartinis vaidmuo.
+            position:
+              elevated: negali būti didesnis nei tavo dabartinis vaidmuo.
+              own_role: negali būti pakeistas tavo dabartinis vaidmuo.
+        webhook:
+          attributes:
+            events:
+              invalid_permissions: negali įtraukti įvykių, į kuriuos neturi teisių.
diff --git a/config/locales/be.yml b/config/locales/be.yml
index 96a272012..223b4d1df 100644
--- a/config/locales/be.yml
+++ b/config/locales/be.yml
@@ -1052,7 +1052,7 @@ be:
     localization:
       body: Mastodon перакладаецца добраахвотнікамі.
       guide_link: https://be.crowdin.com/project/mastodon/be
-      guide_link_text: Кожны можа зрабіць унёсак.
+      guide_link_text: Кожны і кожная можа зрабіць унёсак.
     sensitive_content: Далікатны змест
   application_mailer:
     notification_preferences: Змяніць налады эл. пошты
@@ -1575,7 +1575,7 @@ be:
       duration_too_short: гэта занадта хутка
       expired: Апытанне ўжо скончана
       invalid_choice: Абраны варыянт апытання не існуе
-      over_character_limit: не можа быць даўжэй за %{max} сімвалаў кожны
+      over_character_limit: кожны не можа быць даўжэй за %{max} сімвалаў
       self_vote: Вы не можаце галасаваць ва ўласных апытаннях
       too_few_options: павінна быць болей за адзін варыянт
       too_many_options: колькасць варыянтаў не можа перавышаць %{max}
@@ -1764,7 +1764,7 @@ be:
       public: Публічны
       public_long: Усе могуць бачыць
       unlisted: Не ў спісе
-      unlisted_long: Кожны можа ўбачыць гэты допіс, але ён не паказваецца ў публічных стужках
+      unlisted_long: Усе могуць пабачыць гэты допіс, але ён не паказваецца ў публічных стужках
   statuses_cleanup:
     enabled: Аўтаматычна выдаляць старыя допісы
     enabled_hint: Аўтаматычна выдаляць вашыя допісы, калі яны дасягаюць вызначанага тэрміну, акрамя наступных выпадкаў
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 15ca09470..a04682a44 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -1074,6 +1074,10 @@ cs:
       hint_html: Ještě jedna věc! Musíme potvrdit, že jste člověk (to proto, abychom drželi stranou spam!). Vyřešte CAPTCHA níže a klikněte na "Pokračovat".
       title: Bezpečnostní kontrola
     confirmations:
+      awaiting_review_title: Vaše registrace se ověřuje
+      clicking_this_link: kliknutím na tento odkaz
+      registration_complete: Vaše registrace na %{domain} je hotová!
+      welcome_title: Vítám uživatele %{name}!
       wrong_email_hint: Pokud není e-mail správný, můžete si ho změnit v nastavení účtu.
     delete_account: Odstranit účet
     delete_account_html: Chcete-li odstranit svůj účet, <a href="%{path}">pokračujte zde</a>. Budete požádáni o potvrzení.
@@ -1770,6 +1774,9 @@ cs:
       default: "%d. %b %Y, %H:%M"
       month: "%b %Y"
       time: "%H:%M"
+  translation:
+    errors:
+      too_many_requests: Na překladatelskou službu bylo zasláno v poslední době příliš mnoho požadavků.
   two_factor_authentication:
     add: Přidat
     disable: Vypnout 2FA
diff --git a/config/locales/devise.zh-TW.yml b/config/locales/devise.zh-TW.yml
index c01beb796..de977426f 100644
--- a/config/locales/devise.zh-TW.yml
+++ b/config/locales/devise.zh-TW.yml
@@ -9,7 +9,7 @@ zh-TW:
       already_authenticated: 您已登入。
       inactive: 您的帳號尚未啟用。
       invalid: 無效的 %{authentication_keys} 或密碼。
-      last_attempt: 在帳號鎖定前,您還有最後一次嘗試機會。
+      last_attempt: 帳號鎖定前,您還有最後一次嘗試機會。
       locked: 已鎖定您的帳號。
       not_found_in_database: 無效的 %{authentication_keys} 或密碼。
       pending: 您的帳號仍在審核中。
@@ -20,8 +20,8 @@ zh-TW:
       confirmation_instructions:
         action: 驗證電子郵件地址
         action_with_app: 確認並返回 %{app}
-        explanation: 您已經在 %{host} 上以此電子郵件地址建立了一支帳號。您距離啟用它只剩一點之遙了。若這不是您,請忽略此信件。
-        explanation_when_pending: 您使用此電子郵件地址申請了 %{host} 的邀請。當您確認電子郵件地址後我們將審核您的申請。您可以在登入後變更詳細資訊或刪除您的帳號,但直到您的帳號被核准之前,您無法操作大部分的功能。若您的申請遭拒絕,您的資料將被移除而不必做後續動作。如果這不是您本人,請忽略此郵件。
+        explanation: 您已於 %{host} 上以此電子郵件地址建立了一支帳號。您距離啟用它只剩一點之遙了。若這不是您,請忽略此信件。
+        explanation_when_pending: 您使用此電子郵件地址申請了 %{host} 的邀請。當您確認電子郵件地址後我們將審核您的申請。您能於登入後變更詳細資訊或刪除您的帳號,但直到您的帳號被核准之前,您無法操作大部分的功能。若您的申請遭拒絕,您的資料將被移除而不必做後續動作。如果這不是您本人,請忽略此郵件。
         extra_html: 同時也請看看<a href="%{terms_path}">伺服器規則</a>與<a href="%{policy_path}">服務條款</a>。
         subject: Mastodon:%{instance} 確認說明
         title: 驗證電子郵件地址
@@ -37,7 +37,7 @@ zh-TW:
         title: 密碼已變更
       reconfirmation_instructions:
         explanation: 請確認新的電子郵件地址以變更。
-        extra: 若此次變更不是由您起始的,請忽略此信件。Mastodon 帳號的電子郵件地址在您存取上面的連結前不會變更。
+        extra: 若此次變更不是由您起始的,請忽略此信件。Mastodon 帳號的電子郵件地址於您存取上面的連結前不會變更。
         subject: Mastodon:確認 %{instance} 的電子郵件地址
         title: 驗證電子郵件地址
       reset_password_instructions:
@@ -106,7 +106,7 @@ zh-TW:
   errors:
     messages:
       already_confirmed: 已經確認,請嘗試登入
-      confirmation_period_expired: 需要在 %{period} 內完成驗證。請重新申請
+      confirmation_period_expired: 您需要於 %{period} 內完成驗證。請重新申請
       expired: 已經過期,請重新請求
       not_found: 找不到
       not_locked: 並未鎖定
diff --git a/config/locales/doorkeeper.zh-TW.yml b/config/locales/doorkeeper.zh-TW.yml
index 6073096c3..c0d42ec7b 100644
--- a/config/locales/doorkeeper.zh-TW.yml
+++ b/config/locales/doorkeeper.zh-TW.yml
@@ -72,7 +72,7 @@ zh-TW:
         revoke: 您確定嗎?
       index:
         authorized_at: 於 %{date} 授權
-        description_html: 這些應用程式能透過 API 存取您的帳號。若有您不認得之應用程式,或應用程式行為異常,您可以於此註銷其存取權限。
+        description_html: 這些應用程式能透過 API 存取您的帳號。若有您不認得之應用程式,或應用程式行為異常,您能於此註銷其存取權限。
         last_used_at: 上次使用時間 %{date}
         never_used: 從未使用
         scopes: 權限
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 608599724..69b1aa0a9 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -419,8 +419,8 @@ pl:
         hint: Blokada domen nie zabroni tworzenia wpisów kont w bazie danych, ale pozwoli na automatyczną moderację kont do nich należących.
         severity:
           desc_html: "<strong>Wyciszenie</strong> uczyni wpisy użytkowników z tej domeny widoczne tylko dla osób, które go obserwują. <strong>Zawieszenie</strong> spowoduje usunięcie całej zawartości dodanej przez użytkownika. Użyj <strong>Żadne</strong>, jeżeli chcesz jedynie odrzucać zawartość multimedialną."
-          noop: Nic nie rób
-          silence: Limit
+          noop: Żadne
+          silence: Wycisz
           suspend: Zawieś
         title: Nowa blokada domen
       no_domain_block_selected: Nie zmieniono żadnych bloków domen, gdyż żadna nie została wybrana
diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml
index be21f862f..13b2ad30a 100644
--- a/config/locales/simple_form.zh-TW.yml
+++ b/config/locales/simple_form.zh-TW.yml
@@ -15,25 +15,25 @@ zh-TW:
       account_migration:
         acct: 指定要移動至的帳號的「使用者名稱@網域名稱」
       account_warning_preset:
-        text: 您可使用嘟文語法,例如網址、「#」標籤和提及功能
-        title: 可選。不會向收件者顯示
+        text: 您可使用嘟文語法,例如網址、「#」標籤與提及功能
+        title: 可選的。不會向收件者顯示
       admin_account_action:
         include_statuses: 使用者可看到導致檢舉或警告的嘟文
         send_email_notification: 使用者將收到帳號發生之事情的解釋
-        text_html: 選用。您能使用嘟文語法。您可 <a href="%{path}">新增警告預設</a> 來節省時間
+        text_html: 可選的。您能使用嘟文語法。您可 <a href="%{path}">新增警告預設</a> 來節省時間
         type_html: 設定要使用 <strong>%{acct}</strong> 做的事
         types:
           disable: 禁止該使用者使用他們的帳號,但是不刪除或隱藏他們的內容。
           none: 使用這個寄送警告給該使用者,而不進行其他動作。
           sensitive: 強制標記此使用者所有多媒體附加檔案為敏感內容。
-          silence: 禁止該使用者發公開嘟文,從無跟隨他們的帳號中隱藏嘟文和通知。關閉所有對此帳號之檢舉報告。
+          silence: 禁止該使用者發公開嘟文,從無跟隨他們的帳號中隱藏嘟文與通知。關閉所有對此帳號之檢舉報告。
           suspend: 禁止所有對該帳號任何互動,並且刪除其內容。三十天內可以撤銷此動作。關閉所有對此帳號之檢舉報告。
-        warning_preset_id: 選用。您仍可在預設的結尾新增自訂文字
+        warning_preset_id: 可選的。您仍可於預設的結尾新增自訂文字
       announcement:
         all_day: 當選取時,僅顯示出時間範圍中的日期部分
-        ends_at: 可選的,公告會於該時間點自動取消發布
+        ends_at: 可選的。公告會於該時間點自動取消發布
         scheduled_at: 空白則立即發布公告
-        starts_at: 可選的,讓公告在特定時間範圍內顯示
+        starts_at: 可選的。讓公告於特定時間範圍內顯示
         text: 您可以使用嘟文語法,但請小心別讓公告太鴨霸而佔據使用者的整個版面。
       appeal:
         text: 您只能對警示提出一次申訴
@@ -44,12 +44,12 @@ zh-TW:
         context: 此過濾器應套用於以下一項或多項情境
         current_password: 因安全因素,請輸入目前帳號的密碼
         current_username: 請輸入目前帳號的使用者名稱以確認
-        digest: 僅在您長時間未登入且在未登入期間收到私訊時傳送
+        digest: 僅於您長時間未登入且於未登入期間收到私訊時傳送
         email: 您將收到一封確認電子郵件
         header: 支援 PNG、GIF 或 JPG 圖片格式,檔案最大為 %{size},會等比例縮減至 %{dimensions} 像素
         inbox_url: 從您想要使用的中繼首頁複製網址
         irreversible: 已過濾的嘟文將會不可逆地消失,即便之後移除過濾器也一樣
-        locale: 使用者介面、電子郵件和推播通知的語言
+        locale: 使用者介面、電子郵件與推播通知的語言
         password: 使用至少 8 個字元
         phrase: 無論是嘟文的本文或是內容警告都會被過濾
         scopes: 允許讓應用程式存取的 API。 若您選擇最高階範圍,則無須選擇個別項目。
@@ -62,12 +62,12 @@ zh-TW:
         setting_use_blurhash: 彩色漸層圖樣是基於隱藏媒體內容顏色產生,所有細節將變得模糊
         setting_use_pending_items: 關閉自動捲動更新,時間軸僅於點擊後更新
         username: 您可以使用字幕、數字與底線
-        whole_word: 如果關鍵字或詞組僅有字母與數字,則其將只在符合整個單字的時候才會套用
+        whole_word: 如果關鍵字或詞組僅有字母與數字,則其將只於符合整個單字時才會套用
       domain_allow:
-        domain: 此網域將能夠攫取本站資料,而自該網域發出的資料也會於本站處理和留存。
+        domain: 此網域將能夠攫取本站資料,而自該網域發出的資料也會於本站處理及留存。
       email_domain_block:
-        domain: 這可以是顯示在電子郵件中的網域名稱,或是其使用的 MX 紀錄。其將於註冊時檢查。
-        with_dns_records: Mastodon 會嘗試解析所給網域的 DNS 記錄,解析結果一致者將一併封鎖
+        domain: 這可以是顯示於電子郵件中的網域名稱,或是其使用的 MX 紀錄。其將於註冊時檢查。
+        with_dns_records: Mastodon 會嘗試解析所提供之網域的 DNS 記錄,解析結果一致者將一併封鎖
       featured_tag:
         name: 這些是您最近使用的一些主題標籤:
       filters:
@@ -97,7 +97,7 @@ zh-TW:
         theme: 未登入之訪客或新使用者所見之佈景主題。
         thumbnail: 大約 2:1 圖片會顯示於您伺服器資訊之旁。
         timeline_preview: 未登入之訪客能夠瀏覽此伺服器上最新的公開嘟文。
-        trendable_by_default: 跳過手動審核熱門內容。仍能在登上熱門趨勢後移除個別內容。
+        trendable_by_default: 跳過手動審核熱門內容。仍能於登上熱門趨勢後移除個別內容。
         trends: 熱門趨勢將顯示於您伺服器上正在吸引大量注意力的嘟文、主題標籤、或者新聞。
         trends_as_landing_page: 顯示熱門趨勢內容給未登入使用者及訪客而不是關於此伺服器之描述。需要啟用熱門趨勢。
       form_challenge:
@@ -107,9 +107,9 @@ zh-TW:
       invite_request:
         text: 這會協助我們審核您的申請
       ip_block:
-        comment: 可選的,但請記得您為何添加這項規則。
+        comment: 可選的。但請記得您為何添加這項規則。
         expires_in: IP 位址是經常共用或轉手的有限資源,不建議無限期地封鎖特定 IP 位址。
-        ip: 請輸入 IPv4 或 IPv6 位址,亦可以用 CIDR 語法以封鎖整個 IP 區段。小心不要將自己給一併封鎖掉囉!
+        ip: 請輸入 IPv4 或 IPv6 位址,亦可以用 CIDR 語法以封鎖整個 IP 區段。小心不要將自己一併封鎖掉囉!
         severities:
           no_access: 封鎖對所有資源存取
           sign_up_block: 無法註冊新帳號
@@ -129,11 +129,11 @@ zh-TW:
         chosen_languages: 當選取時,只有選取語言之嘟文會於公開時間軸中顯示
         role: 角色控制使用者有哪些權限
       user_role:
-        color: 在整個使用者介面中用於角色的顏色,十六進位格式的 RGB
+        color: 於整個使用者介面中用於角色的顏色,十六進位格式的 RGB
         highlighted: 這會讓角色公開可見
         name: 角色的公開名稱,如果角色設定為顯示為徽章
-        permissions_as_keys: 有此角色的使用者將有權存取……
-        position: 在某些情況下,衝突的解決方式由更高階的角色決定。某些動作只能由優先程度較低的角色執行
+        permissions_as_keys: 有此角色的使用者將有權存取...
+        position: 某些情況下,衝突的解決方式由更高階的角色決定。某些動作只能由優先程度較低的角色執行
       webhook:
         events: 請選擇要傳送的事件
         template: 使用變數代換組合您自己的 JSON payload。留白以使用預設 JSON 。
@@ -155,7 +155,7 @@ zh-TW:
         text: 預設文字
         title: 標題
       admin_account_action:
-        include_statuses: 在電子郵件中加入檢舉的嘟文
+        include_statuses: 於電子郵件中加入檢舉之嘟文內容
         send_email_notification: 透過電子郵件通知使用者
         text: 自訂警告
         type: 動作
@@ -230,7 +230,7 @@ zh-TW:
         username_or_email: 使用者名稱或電子郵件地址
         whole_word: 整個詞彙
       email_domain_block:
-        with_dns_records: 包括網域的 MX 記錄和 IP 位址
+        with_dns_records: 包括網域的 MX 記錄與 IP 位址
       featured_tag:
         name: "「#」主題標籤"
       filters:
@@ -287,7 +287,7 @@ zh-TW:
         favourite: 當有使用者將您的嘟文加入最愛時,傳送電子郵件通知
         follow: 當有使用者跟隨您時,傳送電子郵件通知
         follow_request: 當有使用者請求跟隨您時,傳送電子郵件通知
-        mention: 當有使用者在嘟文提及您時,傳送電子郵件通知
+        mention: 當有使用者於嘟文提及您時,傳送電子郵件通知
         pending_account: 有新的帳號需要審核
         reblog: 當有使用者轉嘟您的嘟文時,傳送電子郵件通知
         report: 新回報已遞交
@@ -304,16 +304,16 @@ zh-TW:
         indexable: 於搜尋引擎中包含個人檔案頁面
         show_application: 顯示您發嘟文之應用程式
       tag:
-        listable: 允許此主題標籤在搜尋及個人檔案目錄中顯示
+        listable: 允許此主題標籤於搜尋及個人檔案目錄中顯示
         name: 主題標籤
-        trendable: 允許此主題標籤在熱門趨勢下顯示
+        trendable: 允許此主題標籤於熱門趨勢下顯示
         usable: 允許嘟文使用此主題標籤
       user:
         role: 角色
         time_zone: 時區
       user_role:
         color: 識別顏色
-        highlighted: 在使用者個人檔案上將角色顯示為徽章
+        highlighted: 於使用者個人檔案中顯示角色徽章
         name: 名稱
         permissions_as_keys: 權限
         position: 優先權
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index ec8f6c139..9d90d1d51 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -1343,6 +1343,7 @@ vi:
       '86400': 1 ngày
     expires_in_prompt: Không giới hạn
     generate: Tạo lời mời
+    invalid: Lời mời không hợp lệ
     invited_by: 'Bạn đã được mời bởi:'
     max_uses:
       other: "%{count} lần dùng"
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 3063b7afd..7259afdbe 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -1,10 +1,10 @@
 ---
 zh-TW:
   about:
-    about_mastodon_html: Mastodon (長毛象)是一個<em>自由、開放原始碼</em>的社群網站。它是一個分散式的服務,避免您的通訊被單一商業機構壟斷操控。請您選擇一家您信任的 Mastodon 站點,在上面建立帳號,然後您就可以和任一 Mastodon 站點上的使用者互通,享受無縫的<em>社群網路</em>交流。
+    about_mastodon_html: Mastodon (長毛象)是一個<em>自由、開放原始碼</em>的社群網站。它是一個分散式的服務,避免您的通訊被單一商業機構壟斷操控。請您選擇一家您信任的 Mastodon 站點,於其建立帳號,您就能與任一 Mastodon 站點上的使用者互通,享受無縫的<em>社群網路</em>交流。
     contact_missing: 未設定
     contact_unavailable: 未公開
-    hosted_on: 在 %{domain} 運作的 Mastodon 站點
+    hosted_on: 於 %{domain} 託管之 Mastodon 站點
     title: 關於本站
   accounts:
     follow: 跟隨
@@ -13,7 +13,7 @@ zh-TW:
     following: 正在跟隨
     instance_actor_flash: 此帳號是用來代表此伺服器的虛擬執行者,而非個別使用者。它的用途為維繫聯邦宇宙,且不應被停權。
     last_active: 上次活躍時間
-    link_verified_on: 此連結的所有權已在 %{date} 檢查過
+    link_verified_on: 此連結之所有權已於 %{date} 檢查過
     nothing_here: 暫時沒有內容可供顯示!
     pin_errors:
       following: 您只能推薦您正在跟隨的使用者。
@@ -115,7 +115,7 @@ zh-TW:
       reject: 拒絕
       rejected_msg: 已成功婉拒 %{username} 的新帳號申請
       remote_suspension_irreversible: 此帳號之資料已被不可逆地刪除。
-      remote_suspension_reversible_hint_html: 這個帳號已於此伺服器被停權,所有資料將會於 %{date} 被刪除。在此之前,遠端伺服器可以完全回復此的帳號。如果您想即時刪除這個帳號的資料,您可以在下面進行操作。
+      remote_suspension_reversible_hint_html: 這個帳號已於此伺服器被停權,所有資料將會於 %{date} 被刪除。於此之前,遠端伺服器可以完全回復此的帳號。如果您想即時刪除這個帳號的資料,您能於下面進行操作。
       remove_avatar: 取消大頭貼
       remove_header: 移除開頭
       removed_avatar_msg: 已成功刪除 %{username} 的大頭貼
@@ -149,7 +149,7 @@ zh-TW:
       suspend: 停權
       suspended: 已停權
       suspension_irreversible: 已永久刪除此帳號的資料。您可以取消這個帳號的停權狀態,但無法還原已刪除的資料。
-      suspension_reversible_hint_html: 這個帳號已被暫停,所有數據將於 %{date} 被刪除。在此之前,您可以完全回復您的帳號。如果您想即時刪除這個帳號的數據,您可以在下面進行操作。
+      suspension_reversible_hint_html: 這個帳號已被暫停,所有數據將於 %{date} 被刪除。於此之前,您可以完全回復您的帳號。如果您想即時刪除這個帳號的數據,您能於下面進行操作。
       title: 帳號
       unblock_email: 解除封鎖電子郵件地址
       unblocked_email_msg: 成功解除封鎖 %{username} 的電子郵件地址
@@ -333,7 +333,7 @@ zh-TW:
       not_permitted: 您無權執行此操作
       overwrite: 覆蓋
       shortcode: 短代碼
-      shortcode_hint: 至少 2 個字元,只能使用字母、數字和下劃線
+      shortcode_hint: 至少 2 個字元,只能使用字母、數字與下劃線
       title: 自訂表情符號
       uncategorized: 未分類
       unlist: 不公開
@@ -370,10 +370,10 @@ zh-TW:
     domain_allows:
       add_new: 將網域加入聯邦宇宙白名單
       created_msg: 網域已成功加入聯邦宇宙白名單
-      destroyed_msg: 網域已成功從聯邦宇宙白名單移除
+      destroyed_msg: 網域已成功自聯邦宇宙白名單移除
       export: 匯出
       import: 匯入
-      undo: 從聯邦宇宙白名單移除
+      undo: 自聯邦宇宙白名單移除
     domain_blocks:
       add_new: 新增網域黑名單
       confirm_suspension:
@@ -397,7 +397,7 @@ zh-TW:
         create: 新增封鎖
         hint: 站點封鎖動作並不會阻止帳號紀錄被新增至資料庫,但會自動回溯性地對那些帳號套用特定管理設定。
         severity:
-          desc_html: "「<strong>靜音</strong>」令該站點下使用者的嘟文,設定為只對跟隨者顯示,沒有跟隨的人會看不到。「<strong>停權</strong>」會刪除將該站點下使用者的嘟文、媒體檔案和個人檔案。「<strong>無</strong>」則會拒絕接收來自該站點的媒體檔案。"
+          desc_html: "「<strong>靜音</strong>」令該站點下使用者的嘟文,設定為只對跟隨者顯示,沒有跟隨的人會看不到。「<strong>停權</strong>」會刪除將該站點下使用者的嘟文、媒體檔案與個人檔案。「<strong>無</strong>」則會拒絕接收來自該站點的媒體檔案。"
           noop: 無
           silence: 靜音
           suspend: 停權
@@ -451,7 +451,7 @@ zh-TW:
         title: 匯入網域黑名單
       no_file: 尚未選擇檔案
     follow_recommendations:
-      description_html: "<strong>跟隨建議幫助新使用者們快速找到有趣的內容</strong>。當使用者沒有與其他帳號有足夠多的互動以建立個人化跟隨建議時,這些帳號將會被推薦。這些帳號將基於某選定語言之高互動和高本地跟隨者數量帳號而每日重新更新。"
+      description_html: "<strong>跟隨建議幫助新使用者們快速找到有趣的內容</strong>。當使用者沒有與其他帳號有足夠多的互動以建立個人化跟隨建議時,這些帳號將會被推薦。這些帳號將基於某選定語言之高互動與高本地跟隨者數量帳號而每日重新更新。"
       language: 對於語言
       status: 狀態
       suppress: 取消跟隨建議
@@ -461,7 +461,7 @@ zh-TW:
     instances:
       availability:
         description_html:
-          other: 若在<strong>%{count}天</strong>向某個網域遞送失敗,除非收到某個網域的遞送<em>表單</em>,否則不會繼續嘗試遞送。
+          other: 若於 <strong>%{count} 天</strong>向某個網域遞送失敗,除非收到某個網域的遞送<em>表單</em>,否則不會繼續嘗試遞送。
         failure_threshold_reached: 錯誤門檻於 %{date}。
         failures_recorded:
           other: 錯誤嘗試於 %{count} 天。
@@ -590,7 +590,7 @@ zh-TW:
       by_target_domain: 檢舉帳號之網域
       cancel: 取消
       category: 分類
-      category_description_html: 此帳號及/或被檢舉內容之原因會被引用在檢舉帳號通知中
+      category_description_html: 此帳號及/或被檢舉內容之原因將被引用於檢舉帳號通知中
       comment:
         none: 無
       comment_description_html: 提供更多資訊,%{name} 寫道:
@@ -611,7 +611,7 @@ zh-TW:
         delete: 刪除
         placeholder: 記錄已執行的動作,或其他相關的更新...
         title: 備註
-      notes_description_html: 檢視及留下些給其他管理員和未來的自己的備註
+      notes_description_html: 檢視及留下些給其他管理員與未來的自己的備註
       processed_msg: '檢舉報告 #%{id} 已被成功處理'
       quick_actions_description_html: 採取一個快速行動,或者下捲以檢視檢舉內容:
       remote_user_placeholder: 來自 %{instance} 之遠端使用者
@@ -624,7 +624,7 @@ zh-TW:
       skip_to_actions: 跳過行動
       status: 嘟文
       statuses: 被檢舉的內容
-      statuses_description_html: 侵犯性違規內容會被引用在檢舉帳號通知中
+      statuses_description_html: 侵犯性違規內容將被引用於檢舉帳號通知中
       summary:
         action_preambles:
           delete_html: 您將要 <strong>移除</strong> 某些 <strong>@%{acct}</strong> 之嘟文。此將會:
@@ -677,7 +677,7 @@ zh-TW:
         manage_announcements: 管理公告
         manage_announcements_description: 允許使用者管理伺服器上的公告
         manage_appeals: 管理解封申訴系統
-        manage_appeals_description: 允許使用者審閱針對站務動作的申訴
+        manage_appeals_description: 允許使用者審閱針對站務動作之申訴
         manage_blocks: 管理封鎖
         manage_blocks_description: 允許使用者封鎖電子郵件提供者與 IP 位置
         manage_custom_emojis: 管理自訂表情符號
@@ -741,7 +741,7 @@ zh-TW:
         title: 預設將使用者排除於搜尋引擎索引
       discovery:
         follow_recommendations: 跟隨建議
-        preamble: 呈現有趣的內容有助於 Mastodon 上一人不識的新手上路。控制各種不同的分類在您伺服器上如何被探索到。
+        preamble: 呈現有趣的內容有助於 Mastodon 上一人不識的新手上路。控制各種不同的分類於您伺服器上如何被探索到。
         profile_directory: 個人檔案目錄
         public_timelines: 公開時間軸
         publish_discovered_servers: 公開已知伺服器列表
@@ -989,11 +989,11 @@ zh-TW:
     created_msg: 成功建立別名。您可以自舊帳號開始轉移。
     deleted_msg: 成功移除別名。您將無法再由舊帳號轉移至目前的帳號。
     empty: 您目前沒有任何別名。
-    hint_html: 如果想由其他帳號轉移至此帳號,您可以於此處新增別名,稍後系統將容許您將跟隨者由舊帳號轉移至此。此項作業是<strong>無害且可復原的</strong>。 <strong>帳號的遷移程序需要在舊帳號啟動</strong>。
+    hint_html: 如果想由其他帳號轉移至此帳號,您能於此處新增別名,稍後系統將容許您將跟隨者由舊帳號轉移至此。此項作業是<strong>無害且可復原的</strong>。 <strong>帳號的遷移程序需要於舊帳號啟動</strong>。
     remove: 取消連結別名
   appearance:
     advanced_web_interface: 進階網頁介面
-    advanced_web_interface_hint: 進階網頁介面可讓您設定許多不同的欄位來善用螢幕空間,依需要同時查看許多不同的資訊如:首頁、通知、聯邦宇宙時間軸、任意數量的列表和主題標籤。
+    advanced_web_interface_hint: 進階網頁介面可讓您設定許多不同的欄位來善用螢幕空間,依需要同時查看許多不同的資訊如:首頁、通知、聯邦宇宙時間軸、任意數量的列表與主題標籤。
     animations_and_accessibility: 動畫與無障礙設定
     confirmation_dialogs: 確認對話框
     discovery: 探索
@@ -1033,13 +1033,13 @@ zh-TW:
       redirect_to_app_html: 您應被重新導向至 <strong>%{app_name}</strong> 應用程式。如尚未重新導向,請嘗試 %{clicking_this_link} 或手動回到應用程式。
       registration_complete: 您於 %{domain} 之註冊申請已完成!
       welcome_title: 歡迎,%{name}!
-      wrong_email_hint: 若電子郵件地址不正確,您可以於帳號設定中更改。
+      wrong_email_hint: 若電子郵件地址不正確,您能於帳號設定中更改。
     delete_account: 刪除帳號
     delete_account_html: 如果您欲刪除您的帳號,請<a href="%{path}">點擊這裡繼續</a>。您需要再三確認您的操作。
     description:
       prefix_invited_by_user: "@%{name} 邀請您加入這個 Mastodon 伺服器!"
       prefix_sign_up: 馬上註冊 Mastodon 帳號吧!
-      suffix: 有了帳號,就可以從任何 Mastodon 伺服器跟隨任何人、發發廢嘟,並且與任何 Mastodon 伺服器的使用者交流,以及更多!
+      suffix: 有了帳號,就可以自任何 Mastodon 伺服器跟隨任何人、發發廢嘟,並且與任何 Mastodon 伺服器的使用者交流,以及更多!
     didnt_get_confirmation: 沒有收到確認連結嗎?
     dont_have_your_security_key: 找不到您的安全金鑰?
     forgot_password: 忘記密碼?
@@ -1085,7 +1085,7 @@ zh-TW:
       preamble_html: 請使用您於 <strong>%{domain}</strong> 的帳號密碼登入。若您的帳號託管於其他伺服器,您將無法於此登入。
       title: 登入 %{domain}
     sign_up:
-      manual_review: "%{domain} 上的註冊由我們的管理員進行人工審核。為協助我們處理您的註冊,請寫一些關於您自己的資訊以及您想要在 %{domain} 上註冊帳號的原因。"
+      manual_review: "%{domain} 上的註冊由我們的管理員進行人工審核。為協助我們處理您的註冊,請寫一些關於您自己的資訊以及您欲於 %{domain} 上註冊帳號之原因。"
       preamble: 於此 Mastodon 伺服器擁有帳號的話,您將能跟隨聯邦宇宙網路中任何一份子,無論他們的帳號託管於何處。
       title: 讓我們一起設定 %{domain} 吧!
     status:
@@ -1100,7 +1100,7 @@ zh-TW:
     use_security_key: 使用安全金鑰
   challenge:
     confirm: 繼續
-    hint_html: "<strong>温馨小提醒:</strong> 我們在接下來一小時內不會再要求您輸入密碼。"
+    hint_html: "<strong>温馨小提醒:</strong> 我們於接下來一小時內不會再要求您輸入密碼。"
     invalid_password: 密碼錯誤
     prompt: 輸入密碼以繼續
   crypto:
@@ -1134,8 +1134,8 @@ zh-TW:
     warning:
       before: 在進行下一步驟之前,請詳細閱讀以下説明:
       caches: 已被其他節點快取的內容可能會殘留其中
-      data_removal: 您的嘟文和其他資料將會被永久刪除
-      email_change_html: 您可以在不刪除帳號的情況下<a href="%{path}">變更您的電子郵件地址</a>
+      data_removal: 您的嘟文與其他資料將被永久刪除
+      email_change_html: 您能於不刪除帳號的情況下<a href="%{path}">變更您的電子郵件地址</a>
       email_contact_html: 如果您仍然沒有收到郵件,請寄信至 <a href="mailto:%{email}">%{email}</a> 以獲得協助
       email_reconfirmation_html: 如果您沒有收到確認郵件,可以<a href="%{path}">請求再次發送</a>
       irreversible: 您將無法復原或重新啟用您的帳號
@@ -1176,7 +1176,7 @@ zh-TW:
     invalid_domain: 並非一個有效網域
   edit_profile:
     basic_information: 基本資訊
-    hint_html: "<strong>自訂人們可以於您個人檔案及嘟文內容。</strong>當您完成填寫個人檔案以及設定大頭貼後,其他人們比較願意跟隨您並與您互動。"
+    hint_html: "<strong>自訂人們能於您個人檔案及嘟文旁所見之內容。</strong>當您完成填寫個人檔案以及設定大頭貼後,其他人們比較願意跟隨您並與您互動。"
     other: 其他
   errors:
     '400': 您所送出的請求無效或格式不正確。
@@ -1194,13 +1194,13 @@ zh-TW:
     '503': 此頁面因伺服器暫時發生錯誤而無法提供。
     noscript_html: 使用 Mastodon 網頁版應用需要啟用 JavaScript。您也可以選擇適用於您的平台的 <a href="%{apps_path}">Mastodon 應用</a>。
   existing_username_validator:
-    not_found: 無法在本站找到這個名稱的使用者
+    not_found: 無法於本伺服器找到此使用者帳號
     not_found_multiple: 揣嘸 %{usernames}
   exports:
     archive_takeout:
       date: 日期
       download: 下載檔案
-      hint_html: 您可以下載包含您的<strong>文章和媒體</strong>的檔案。資料以 ActivityPub 格式儲存,可用於相容的軟體。每次允許存檔的間隔至少 7 天。
+      hint_html: 您可以下載包含您的<strong>嘟文與媒體</strong>的檔案。資料以 ActivityPub 格式儲存,可用於相容之軟體。每次允許存檔的間隔至少 7 天。
       in_progress: 正在準備您的存檔...
       request: 下載存檔
       size: 大小
@@ -1304,7 +1304,7 @@ zh-TW:
       following_html: 您將要 <strong>跟隨</strong> 自 <strong>%{filename}</strong> 中之 <strong>%{total_items} 個帳號</strong>。
       lists_html: 您將自 <strong>%{filename}</strong> 新增 <strong>%{total_items} 個帳號</strong>至您的<strong>列表</strong>。若不存在列表用以新增帳號,則會建立新列表。
       muting_html: 您將要 <strong>靜音</strong> 自 <strong>%{filename}</strong> 中之 <strong>%{total_items} 個帳號</strong>。
-    preface: 您能於此匯入您在其他伺服器所匯出的資料檔,包括跟隨中的使用者、封鎖的使用者名單等。
+    preface: 您能於此匯入您於其他伺服器所匯出的資料檔,包括跟隨中的使用者、封鎖的使用者名單等。
     recent_imports: 最近匯入的
     states:
       finished: 已完成
@@ -1414,12 +1414,12 @@ zh-TW:
     warning:
       backreference_required: 新的帳號必須先設定為反向參照到目前帳號
       before: 在進行下一步驟之前,請詳細閱讀以下説明:
-      cooldown: 在轉移帳號後會有一段等待時間,在等待時間內您將無法再次轉移
+      cooldown: 轉移帳號後會有一段等待時間,等待時間內您將無法再次轉移
       disabled_account: 之後您的目前帳號將完全無法使用。但您可以存取資料匯出與重新啟用。
       followers: 此動作將會將目前帳號的所有跟隨者轉移至新帳號
-      only_redirect_html: 或者,您也可以<a href="%{path}">僅在您的個人檔案中設定重新導向</a>。
+      only_redirect_html: 或者,您也可以<a href="%{path}">僅於您的個人檔案中設定重新導向</a>。
       other_data: 其他資料並不會自動轉移
-      redirect: 您目前的帳號將於個人檔案頁面新增重新導向公告,並會被排除在搜尋結果之外
+      redirect: 您目前的帳號將於個人檔案頁面新增重新導向公告,並會被排除於搜尋結果之外
   moderation:
     title: 站務
   move_handler:
@@ -1449,8 +1449,8 @@ zh-TW:
       title: 新的跟隨請求
     mention:
       action: 回覆
-      body: "%{name} 在嘟文中提及您:"
-      subject: "%{name} 在嘟文中提及您"
+      body: "%{name} 於嘟文中提及您:"
+      subject: "%{name} 於嘟文中提及您"
       title: 新的提及
     poll:
       subject: 由 %{name} 發起的投票已結束
@@ -1510,7 +1510,7 @@ zh-TW:
   privacy:
     hint_html: "<strong>自訂您希望如何讓您的個人檔案及嘟文被發現。</strong>藉由啟用一系列 Mastodon 功能以幫助您觸及更廣的受眾。煩請花些時間確認您是否欲啟用這些設定。"
     privacy: 隱私權
-    privacy_hint_html: 控制您希望向其他人揭露之內容。人們透過瀏覽其他人的跟隨者與其發嘟之應用程式發現有趣的個人檔案和酷炫的 Mastodon 應用程式,但您能選擇將其隱藏。
+    privacy_hint_html: 控制您希望向其他人揭露之內容。人們透過瀏覽其他人的跟隨者與其發嘟之應用程式發現有趣的個人檔案與酷炫的 Mastodon 應用程式,但您能選擇將其隱藏。
     reach: 觸及
     reach_hint_html: 控制您希望被新使用者探索或跟隨之方式。想讓您的嘟文出現於探索頁面嗎?想讓其他人透過他們的跟隨建議找到您嗎?想自動接受所有新跟隨者嗎?或是想逐一控制跟隨請求嗎?
     search: 搜尋
@@ -1669,7 +1669,7 @@ zh-TW:
       private_long: 只有跟隨您的人能看到
       public: 公開
       public_long: 所有人都能看到
-      unlisted: 不在公開時間軸顯示
+      unlisted: 不於公開時間軸顯示
       unlisted_long: 所有人都能看到,但不會出現在公開時間軸上
   statuses_cleanup:
     enabled: 自動刪除舊嘟文
@@ -1679,7 +1679,7 @@ zh-TW:
     ignore_favs: 忽略最愛數
     ignore_reblogs: 忽略轉嘟數
     interaction_exceptions: 基於互動的例外規則
-    interaction_exceptions_explanation: 請注意嘟文是無法保證被刪除的,如果在一次處理過後嘟文低於最愛或轉嘟的門檻。
+    interaction_exceptions_explanation: 請注意嘟文是無法保證被刪除的,如果於一次處理過後嘟文低於最愛或轉嘟的門檻。
     keep_direct: 保留私訊
     keep_direct_hint: 不會刪除任何您的私訊
     keep_media: 保留包含多媒體附加檔案之嘟文
@@ -1735,7 +1735,7 @@ zh-TW:
     enabled: 兩階段認證已啟用
     enabled_success: 已成功啟用兩階段認證
     generate_recovery_codes: 產生備用驗證碼
-    lost_recovery_codes: 讓您可以在遺失手機時,使用備用驗證碼登入。若您已遺失備用驗證碼,可於此產生一批新的,舊有的備用驗證碼將會失效。
+    lost_recovery_codes: 讓您能於遺失手機時,使用備用驗證碼登入。若您已遺失備用驗證碼,可於此產生一批新的,舊有的備用驗證碼將會失效。
     methods: 兩步驟方式
     otp: 驗證應用程式
     recovery_codes: 備份備用驗證碼
@@ -1745,12 +1745,12 @@ zh-TW:
   user_mailer:
     appeal_approved:
       action: 前往您的帳號
-      explanation: 您在 %{appeal_date} 遞交的針對您帳號的 %{strike_date} 警示的申訴已獲批准。您的帳號再次享有良好的信譽。
-      subject: 您在 %{date} 提出的申訴已獲批准
+      explanation: 您於 %{appeal_date} 遞交的針對您帳號的 %{strike_date} 警示之申訴已獲批准。您的帳號再次享有良好的信譽。
+      subject: 您於 %{date} 提出之申訴已獲批准
       title: 申訴已批准
     appeal_rejected:
-      explanation: 您在 %{appeal_date} 遞交的針對您帳號的 %{strike_date} 警示的申訴已被駁回。
-      subject: 您在 %{date} 提出的申訴已被駁回
+      explanation: 您於 %{appeal_date} 遞交的針對您帳號的 %{strike_date} 警示之申訴已被駁回。
+      subject: 您於 %{date} 提出之申訴已被駁回
       title: 申訴被駁回
     backup_ready:
       explanation: 您要求的 Mastodon 帳號完整備份檔案現已就緒,可供下載!
@@ -1772,7 +1772,7 @@ zh-TW:
       explanation:
         delete_statuses: 您的某些嘟文被發現已違反一項或多項社群準則,隨後已被 %{instance} 的管理員刪除。
         disable: 您無法繼續使用您的帳號,但您的個人頁面及其他資料內容保持不變。您可以要求一份您的資料備份,帳號異動設定,或是刪除帳號。
-        mark_statuses_as_sensitive: 您的部份嘟文已被 %{instance} 的管理員標記為敏感內容。這代表了人們必須在顯示預覽前點擊嘟文中的媒體。您可以在將來嘟文時自己將媒體標記為敏感內容。
+        mark_statuses_as_sensitive: 您的部份嘟文已被 %{instance} 的管理員標記為敏感內容。這代表了人們必須於顯示預覽前點擊嘟文中的媒體。您能於將來嘟文時自己將媒體標記為敏感內容。
         sensitive: 由此刻起,您所有上傳的媒體檔案將被標記為敏感內容,並且隱藏於點擊警告之後。
         silence: 您仍然能使用您的帳號,但僅有已跟隨您的人才能見到您於此伺服器之嘟文,您也可能會從各式探索功能中被排除。但其他人仍可手動跟隨您。
         suspend: 您將不能使用您的帳號,您的個人檔案頁面及其他資料將不再能被存取。您仍可於約 30 日內資料被完全刪除前要求下載您的資料,但我們仍會保留一部份基本資料,以防止有人規避停權處罰。
@@ -1781,9 +1781,9 @@ zh-TW:
       subject:
         delete_statuses: 您於 %{acct} 之嘟文已被移除
         disable: 您的帳號 %{acct} 已被凍結
-        mark_statuses_as_sensitive: 您在 %{acct} 上的嘟文已被標記為敏感內容
+        mark_statuses_as_sensitive: 您於 %{acct} 上的嘟文已被標記為敏感內容
         none: 對 %{acct} 的警告
-        sensitive: 從現在開始,您在 %{acct} 上的嘟文將會被標記為敏感內容
+        sensitive: 從現在開始,您於 %{acct} 上之嘟文將會被標記為敏感內容
         silence: 您的帳號 %{acct} 已被限制
         suspend: 您的帳號 %{acct} 已被停權
       title:
@@ -1796,10 +1796,10 @@ zh-TW:
         suspend: 帳號己被停權
     welcome:
       edit_profile_action: 設定個人檔案
-      edit_profile_step: 您可以設定您的個人檔案,包括上傳大頭貼、變更顯示名稱等等。您也可以選擇在新的跟隨者跟隨前,先對他們進行審核。
+      edit_profile_step: 您可以設定您的個人檔案,包括上傳大頭貼、變更顯示名稱等等。您也可以選擇於新的跟隨者跟隨前,先對他們進行審核。
       explanation: 下面是幾個小幫助,希望它們能幫到您
       final_action: 開始嘟嘟
-      final_step: '開始嘟嘟吧!即使您現在沒有跟隨者,其他人仍然能在本站時間軸、主題標籤等地方,看到您的公開嘟文。試著用 #introductions 這個主題標籤介紹一下自己吧。'
+      final_step: '開始嘟嘟吧!即使您現在沒有跟隨者,其他人仍然能於本站時間軸、主題標籤等地方,看到您的公開嘟文。試著用 #introductions 這個主題標籤介紹一下自己吧。'
       full_handle: 您的完整帳號名稱
       full_handle_hint: 您需要將這告訴您的朋友們,這樣他們就能從另一個伺服器向您發送訊息或跟隨您。
       subject: 歡迎來到 Mastodon

From d67bd44ca1542d665354e733b632c841b6b7d29b Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Wed, 15 Nov 2023 12:13:53 +0100
Subject: [PATCH 37/63] Add profile setup to onboarding in web UI (#27829)

---
 .../api/v1/accounts/credentials_controller.rb |   2 +
 app/javascript/mastodon/actions/accounts.js   |  15 ++
 app/javascript/mastodon/api_types/accounts.ts |   1 +
 .../mastodon/components/admin/Retention.jsx   |   2 +-
 .../mastodon/components/loading_indicator.tsx |  26 ++-
 .../components/progress_indicator.jsx         |  29 ---
 .../features/onboarding/components/step.jsx   |  15 +-
 .../mastodon/features/onboarding/follows.jsx  | 105 ++++------
 .../mastodon/features/onboarding/index.jsx    | 190 ++++++------------
 .../mastodon/features/onboarding/profile.jsx  | 162 +++++++++++++++
 .../mastodon/features/onboarding/share.jsx    | 100 ++++-----
 app/javascript/mastodon/features/ui/index.jsx |   2 +-
 app/javascript/mastodon/locales/en.json       |  13 +-
 app/javascript/mastodon/models/account.ts     |   1 +
 .../styles/mastodon/components.scss           | 164 +++++----------
 app/javascript/styles/mastodon/forms.scss     | 105 ++++++++--
 app/serializers/rest/account_serializer.rb    |   6 +-
 config/routes.rb                              |   2 +-
 18 files changed, 524 insertions(+), 416 deletions(-)
 delete mode 100644 app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx
 create mode 100644 app/javascript/mastodon/features/onboarding/profile.jsx

diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
index 76ba75824..8f31336b9 100644
--- a/app/controllers/api/v1/accounts/credentials_controller.rb
+++ b/app/controllers/api/v1/accounts/credentials_controller.rb
@@ -16,6 +16,8 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
     current_user.update(user_params) if user_params
     ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
     render json: @account, serializer: REST::CredentialAccountSerializer
+  rescue ActiveRecord::RecordInvalid => e
+    render json: ValidationErrorFormatter.new(e).as_json, status: 422
   end
 
   private
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index e0448f004..9f3bbba03 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -661,3 +661,18 @@ export function unpinAccountFail(error) {
     error,
   };
 }
+
+export const updateAccount = ({ displayName, note, avatar, header, discoverable, indexable }) => (dispatch, getState) => {
+  const data = new FormData();
+
+  data.append('display_name', displayName);
+  data.append('note', note);
+  if (avatar) data.append('avatar', avatar);
+  if (header) data.append('header', header);
+  data.append('discoverable', discoverable);
+  data.append('indexable', indexable);
+
+  return api(getState).patch('/api/v1/accounts/update_credentials', data).then(response => {
+    dispatch(importFetchedAccount(response.data));
+  });
+};
diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts
index 985abf946..5bf3e6428 100644
--- a/app/javascript/mastodon/api_types/accounts.ts
+++ b/app/javascript/mastodon/api_types/accounts.ts
@@ -20,6 +20,7 @@ export interface ApiAccountJSON {
   bot: boolean;
   created_at: string;
   discoverable: boolean;
+  indexable: boolean;
   display_name: string;
   emojis: ApiCustomEmojiJSON[];
   fields: ApiAccountFieldJSON[];
diff --git a/app/javascript/mastodon/components/admin/Retention.jsx b/app/javascript/mastodon/components/admin/Retention.jsx
index 2f5671068..1e8ef48b7 100644
--- a/app/javascript/mastodon/components/admin/Retention.jsx
+++ b/app/javascript/mastodon/components/admin/Retention.jsx
@@ -51,7 +51,7 @@ export default class Retention extends PureComponent {
     let content;
 
     if (loading) {
-      content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
+      content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading…' />;
     } else {
       content = (
         <table className='retention__table'>
diff --git a/app/javascript/mastodon/components/loading_indicator.tsx b/app/javascript/mastodon/components/loading_indicator.tsx
index 6bc24a0d6..fcdbe80d8 100644
--- a/app/javascript/mastodon/components/loading_indicator.tsx
+++ b/app/javascript/mastodon/components/loading_indicator.tsx
@@ -1,7 +1,23 @@
+import { useIntl, defineMessages } from 'react-intl';
+
 import { CircularProgress } from './circular_progress';
 
-export const LoadingIndicator: React.FC = () => (
-  <div className='loading-indicator'>
-    <CircularProgress size={50} strokeWidth={6} />
-  </div>
-);
+const messages = defineMessages({
+  loading: { id: 'loading_indicator.label', defaultMessage: 'Loading…' },
+});
+
+export const LoadingIndicator: React.FC = () => {
+  const intl = useIntl();
+
+  return (
+    <div
+      className='loading-indicator'
+      role='progressbar'
+      aria-busy
+      aria-live='polite'
+      aria-label={intl.formatMessage(messages.loading)}
+    >
+      <CircularProgress size={50} strokeWidth={6} />
+    </div>
+  );
+};
diff --git a/app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx b/app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx
deleted file mode 100644
index 37288a286..000000000
--- a/app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import PropTypes from 'prop-types';
-import { Fragment } from 'react';
-
-import classNames from 'classnames';
-
-import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/done.svg';
-
-import { Icon }  from 'mastodon/components/icon';
-
-const ProgressIndicator = ({ steps, completed }) => (
-  <div className='onboarding__progress-indicator'>
-    {(new Array(steps)).fill().map((_, i) => (
-      <Fragment key={i}>
-        {i > 0 && <div className={classNames('onboarding__progress-indicator__line', { active: completed > i })} />}
-
-        <div className={classNames('onboarding__progress-indicator__step', { active: completed > i })}>
-          {completed > i && <Icon icon={CheckIcon} />}
-        </div>
-      </Fragment>
-    ))}
-  </div>
-);
-
-ProgressIndicator.propTypes = {
-  steps: PropTypes.number.isRequired,
-  completed: PropTypes.number,
-};
-
-export default ProgressIndicator;
diff --git a/app/javascript/mastodon/features/onboarding/components/step.jsx b/app/javascript/mastodon/features/onboarding/components/step.jsx
index 1f42d9d49..1f83f2080 100644
--- a/app/javascript/mastodon/features/onboarding/components/step.jsx
+++ b/app/javascript/mastodon/features/onboarding/components/step.jsx
@@ -1,11 +1,13 @@
 import PropTypes from 'prop-types';
 
+import { Link } from 'react-router-dom';
+
 import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
 import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/done.svg';
 
-import { Icon }  from 'mastodon/components/icon';
+import { Icon } from 'mastodon/components/icon';
 
-const Step = ({ label, description, icon, iconComponent, completed, onClick, href }) => {
+export const Step = ({ label, description, icon, iconComponent, completed, onClick, href, to }) => {
   const content = (
     <>
       <div className='onboarding__steps__item__icon'>
@@ -29,6 +31,12 @@ const Step = ({ label, description, icon, iconComponent, completed, onClick, hre
         {content}
       </a>
     );
+  } else if (to) {
+    return (
+      <Link to={to} className='onboarding__steps__item'>
+        {content}
+      </Link>
+    );
   }
 
   return (
@@ -45,7 +53,6 @@ Step.propTypes = {
   iconComponent: PropTypes.func,
   completed: PropTypes.bool,
   href: PropTypes.string,
+  to: PropTypes.string,
   onClick: PropTypes.func,
 };
-
-export default Step;
diff --git a/app/javascript/mastodon/features/onboarding/follows.jsx b/app/javascript/mastodon/features/onboarding/follows.jsx
index e21c7c75b..e23a335c0 100644
--- a/app/javascript/mastodon/features/onboarding/follows.jsx
+++ b/app/javascript/mastodon/features/onboarding/follows.jsx
@@ -1,79 +1,62 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
+import { useEffect } from 'react';
 
 import { FormattedMessage } from 'react-intl';
 
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
+import { Link } from 'react-router-dom';
+
+import { useDispatch } from 'react-redux';
+
 
 import { fetchSuggestions } from 'mastodon/actions/suggestions';
 import { markAsPartial } from 'mastodon/actions/timelines';
-import Column from 'mastodon/components/column';
 import { ColumnBackButton } from 'mastodon/components/column_back_button';
 import { EmptyAccount } from 'mastodon/components/empty_account';
 import Account from 'mastodon/containers/account_container';
+import { useAppSelector } from 'mastodon/store';
 
-const mapStateToProps = state => ({
-  suggestions: state.getIn(['suggestions', 'items']),
-  isLoading: state.getIn(['suggestions', 'isLoading']),
-});
+export const Follows = () => {
+  const dispatch = useDispatch();
+  const isLoading = useAppSelector(state => state.getIn(['suggestions', 'isLoading']));
+  const suggestions = useAppSelector(state => state.getIn(['suggestions', 'items']));
 
-class Follows extends PureComponent {
-
-  static propTypes = {
-    onBack: PropTypes.func,
-    dispatch: PropTypes.func.isRequired,
-    suggestions: ImmutablePropTypes.list,
-    isLoading: PropTypes.bool,
-  };
-
-  componentDidMount () {
-    const { dispatch } = this.props;
+  useEffect(() => {
     dispatch(fetchSuggestions(true));
+
+    return () => {
+      dispatch(markAsPartial('home'));
+    };
+  }, [dispatch]);
+
+  let loadedContent;
+
+  if (isLoading) {
+    loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
+  } else if (suggestions.isEmpty()) {
+    loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
+  } else {
+    loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
   }
 
-  componentWillUnmount () {
-    const { dispatch } = this.props;
-    dispatch(markAsPartial('home'));
-  }
+  return (
+    <>
+      <ColumnBackButton />
 
-  render () {
-    const { onBack, isLoading, suggestions } = this.props;
-
-    let loadedContent;
-
-    if (isLoading) {
-      loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
-    } else if (suggestions.isEmpty()) {
-      loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
-    } else {
-      loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
-    }
-
-    return (
-      <Column>
-        <ColumnBackButton onClick={onBack} />
-
-        <div className='scrollable privacy-policy'>
-          <div className='column-title'>
-            <h3><FormattedMessage id='onboarding.follows.title' defaultMessage='Popular on Mastodon' /></h3>
-            <p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
-          </div>
-
-          <div className='follow-recommendations'>
-            {loadedContent}
-          </div>
-
-          <p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>
-
-          <div className='onboarding__footer'>
-            <button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></button>
-          </div>
+      <div className='scrollable privacy-policy'>
+        <div className='column-title'>
+          <h3><FormattedMessage id='onboarding.follows.title' defaultMessage='Popular on Mastodon' /></h3>
+          <p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
         </div>
-      </Column>
-    );
-  }
 
-}
+        <div className='follow-recommendations'>
+          {loadedContent}
+        </div>
 
-export default connect(mapStateToProps)(Follows);
+        <p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>
+
+        <div className='onboarding__footer'>
+          <Link className='link-button' to='/start'><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></Link>
+        </div>
+      </div>
+    </>
+  );
+};
diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx
index 51d4b71f2..51677fbc7 100644
--- a/app/javascript/mastodon/features/onboarding/index.jsx
+++ b/app/javascript/mastodon/features/onboarding/index.jsx
@@ -1,152 +1,90 @@
-import PropTypes from 'prop-types';
+import { useCallback } from 'react';
 
-import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
+import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
 
 import { Helmet } from 'react-helmet';
-import { Link, withRouter } from 'react-router-dom';
+import { Link, Switch, Route, useHistory } from 'react-router-dom';
+
+import { useDispatch } from 'react-redux';
 
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
 
 import { ReactComponent as AccountCircleIcon } from '@material-symbols/svg-600/outlined/account_circle.svg';
 import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
 import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/outlined/content_copy.svg';
 import { ReactComponent as EditNoteIcon } from '@material-symbols/svg-600/outlined/edit_note.svg';
 import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outlined/person_add.svg';
-import { debounce } from 'lodash';
 
 import illustration from 'mastodon/../images/elephant_ui_conversation.svg';
-import { fetchAccount } from 'mastodon/actions/accounts';
 import { focusCompose } from 'mastodon/actions/compose';
-import { closeOnboarding } from 'mastodon/actions/onboarding';
 import { Icon }  from 'mastodon/components/icon';
 import Column from 'mastodon/features/ui/components/column';
 import { me } from 'mastodon/initial_state';
-import { makeGetAccount } from 'mastodon/selectors';
+import { useAppSelector } from 'mastodon/store';
 import { assetHost } from 'mastodon/utils/config';
-import { WithRouterPropTypes } from 'mastodon/utils/react_router';
 
-import Step from './components/step';
-import Follows from './follows';
-import Share from './share';
+import { Step } from './components/step';
+import { Follows } from './follows';
+import { Profile } from './profile';
+import { Share } from './share';
 
 const messages = defineMessages({
   template: { id: 'onboarding.compose.template', defaultMessage: 'Hello #Mastodon!' },
 });
 
-const mapStateToProps = () => {
-  const getAccount = makeGetAccount();
+const Onboarding = () => {
+  const account = useAppSelector(state => state.getIn(['accounts', me]));
+  const dispatch = useDispatch();
+  const intl = useIntl();
+  const history = useHistory();
 
-  return state => ({
-    account: getAccount(state, me),
-  });
+  const handleComposeClick = useCallback(() => {
+    dispatch(focusCompose(history, intl.formatMessage(messages.template)));
+  }, [dispatch, intl, history]);
+
+  return (
+    <Column>
+      <Switch>
+        <Route path='/start' exact>
+          <div className='scrollable privacy-policy'>
+            <div className='column-title'>
+              <img src={illustration} alt='' className='onboarding__illustration' />
+              <h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
+              <p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
+            </div>
+
+            <div className='onboarding__steps'>
+              <Step to='/start/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
+              <Step to='/start/follows' completed={(account.get('following_count') * 1) >= 1} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
+              <Step onClick={handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
+              <Step to='/start/share' icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
+            </div>
+
+            <p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
+
+            <div className='onboarding__links'>
+              <Link to='/explore' className='onboarding__link'>
+                <FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
+                <Icon icon={ArrowRightAltIcon} />
+              </Link>
+
+              <Link to='/home' className='onboarding__link'>
+                <FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
+                <Icon icon={ArrowRightAltIcon} />
+              </Link>
+            </div>
+          </div>
+        </Route>
+
+        <Route path='/start/profile' component={Profile} />
+        <Route path='/start/follows' component={Follows} />
+        <Route path='/start/share' component={Share} />
+      </Switch>
+
+      <Helmet>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
 };
 
-class Onboarding extends ImmutablePureComponent {
-  static propTypes = {
-    dispatch: PropTypes.func.isRequired,
-    account: ImmutablePropTypes.record,
-    ...WithRouterPropTypes,
-  };
-
-  state = {
-    step: null,
-    profileClicked: false,
-    shareClicked: false,
-  };
-
-  handleClose = () => {
-    const { dispatch, history } = this.props;
-
-    dispatch(closeOnboarding());
-    history.push('/home');
-  };
-
-  handleProfileClick = () => {
-    this.setState({ profileClicked: true });
-  };
-
-  handleFollowClick = () => {
-    this.setState({ step: 'follows' });
-  };
-
-  handleComposeClick = () => {
-    const { dispatch, intl, history } = this.props;
-
-    dispatch(focusCompose(history, intl.formatMessage(messages.template)));
-  };
-
-  handleShareClick = () => {
-    this.setState({ step: 'share', shareClicked: true });
-  };
-
-  handleBackClick = () => {
-    this.setState({ step: null });
-  };
-
-  handleWindowFocus = debounce(() => {
-    const { dispatch, account } = this.props;
-    dispatch(fetchAccount(account.get('id')));
-  }, 1000, { trailing: true });
-
-  componentDidMount () {
-    window.addEventListener('focus', this.handleWindowFocus, false);
-  }
-
-  componentWillUnmount () {
-    window.removeEventListener('focus', this.handleWindowFocus);
-  }
-
-  render () {
-    const { account } = this.props;
-    const { step, shareClicked } = this.state;
-
-    switch(step) {
-    case 'follows':
-      return <Follows onBack={this.handleBackClick} />;
-    case 'share':
-      return <Share onBack={this.handleBackClick} />;
-    }
-
-    return (
-      <Column>
-        <div className='scrollable privacy-policy'>
-          <div className='column-title'>
-            <img src={illustration} alt='' className='onboarding__illustration' />
-            <h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
-            <p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
-          </div>
-
-          <div className='onboarding__steps'>
-            <Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
-            <Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
-            <Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
-            <Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
-          </div>
-
-          <p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
-
-          <div className='onboarding__links'>
-            <Link to='/explore' className='onboarding__link'>
-              <FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
-              <Icon icon={ArrowRightAltIcon} />
-            </Link>
-
-            <Link to='/home' className='onboarding__link'>
-              <FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
-              <Icon icon={ArrowRightAltIcon} />
-            </Link>
-          </div>
-        </div>
-
-        <Helmet>
-          <meta name='robots' content='noindex' />
-        </Helmet>
-      </Column>
-    );
-  }
-
-}
-
-export default withRouter(connect(mapStateToProps)(injectIntl(Onboarding)));
+export default Onboarding;
diff --git a/app/javascript/mastodon/features/onboarding/profile.jsx b/app/javascript/mastodon/features/onboarding/profile.jsx
new file mode 100644
index 000000000..19ba0bcb9
--- /dev/null
+++ b/app/javascript/mastodon/features/onboarding/profile.jsx
@@ -0,0 +1,162 @@
+import { useState, useMemo, useCallback, createRef } from 'react';
+
+import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
+
+import classNames from 'classnames';
+import { useHistory } from 'react-router-dom';
+
+import { useDispatch } from 'react-redux';
+
+
+import { ReactComponent as AddPhotoAlternateIcon } from '@material-symbols/svg-600/outlined/add_photo_alternate.svg';
+import { ReactComponent as EditIcon } from '@material-symbols/svg-600/outlined/edit.svg';
+import Toggle from 'react-toggle';
+
+import { updateAccount } from 'mastodon/actions/accounts';
+import { Button } from 'mastodon/components/button';
+import { ColumnBackButton } from 'mastodon/components/column_back_button';
+import { Icon } from 'mastodon/components/icon';
+import { LoadingIndicator } from 'mastodon/components/loading_indicator';
+import { me } from 'mastodon/initial_state';
+import { useAppSelector } from 'mastodon/store';
+import { unescapeHTML } from 'mastodon/utils/html';
+
+const messages = defineMessages({
+  uploadHeader: { id: 'onboarding.profile.upload_header', defaultMessage: 'Upload profile header' },
+  uploadAvatar: { id: 'onboarding.profile.upload_avatar', defaultMessage: 'Upload profile picture' },
+});
+
+export const Profile = () => {
+  const account = useAppSelector(state => state.getIn(['accounts', me]));
+  const [displayName, setDisplayName] = useState(account.get('display_name'));
+  const [note, setNote] = useState(unescapeHTML(account.get('note')));
+  const [avatar, setAvatar] = useState(null);
+  const [header, setHeader] = useState(null);
+  const [discoverable, setDiscoverable] = useState(account.get('discoverable'));
+  const [indexable, setIndexable] = useState(account.get('indexable'));
+  const [isSaving, setIsSaving] = useState(false);
+  const [errors, setErrors] = useState();
+  const avatarFileRef = createRef();
+  const headerFileRef = createRef();
+  const dispatch = useDispatch();
+  const intl = useIntl();
+  const history = useHistory();
+
+  const handleDisplayNameChange = useCallback(e => {
+    setDisplayName(e.target.value);
+  }, [setDisplayName]);
+
+  const handleNoteChange = useCallback(e => {
+    setNote(e.target.value);
+  }, [setNote]);
+
+  const handleDiscoverableChange = useCallback(e => {
+    setDiscoverable(e.target.checked);
+  }, [setDiscoverable]);
+
+  const handleIndexableChange = useCallback(e => {
+    setIndexable(e.target.checked);
+  }, [setIndexable]);
+
+  const handleAvatarChange = useCallback(e => {
+    setAvatar(e.target?.files?.[0]);
+  }, [setAvatar]);
+
+  const handleHeaderChange = useCallback(e => {
+    setHeader(e.target?.files?.[0]);
+  }, [setHeader]);
+
+  const avatarPreview = useMemo(() => avatar ? URL.createObjectURL(avatar) : account.get('avatar'), [avatar, account]);
+  const headerPreview = useMemo(() => header ? URL.createObjectURL(header) : account.get('header'), [header, account]);
+
+  const handleSubmit = useCallback(() => {
+    setIsSaving(true);
+
+    dispatch(updateAccount({
+      displayName,
+      note,
+      avatar,
+      header,
+      discoverable,
+      indexable,
+    })).then(() => history.push('/start/follows')).catch(err => {
+      setIsSaving(false);
+      setErrors(err.response.data.details);
+    });
+  }, [dispatch, displayName, note, avatar, header, discoverable, indexable, history]);
+
+  return (
+    <>
+      <ColumnBackButton />
+
+      <div className='scrollable privacy-policy'>
+        <div className='column-title'>
+          <h3><FormattedMessage id='onboarding.profile.title' defaultMessage='Profile setup' /></h3>
+          <p><FormattedMessage id='onboarding.profile.lead' defaultMessage='You can always complete this later in the settings, where even more customization options are available.' /></p>
+        </div>
+
+        <div className='simple_form'>
+          <div className='onboarding__profile'>
+            <label className={classNames('app-form__header-input', { selected: !!headerPreview, invalid: !!errors?.header })} title={intl.formatMessage(messages.uploadHeader)}>
+              <input
+                type='file'
+                hidden
+                ref={headerFileRef}
+                accept='image/*'
+                onChange={handleHeaderChange}
+              />
+
+              {headerPreview && <img src={headerPreview} alt='' />}
+
+              <Icon icon={headerPreview ? EditIcon : AddPhotoAlternateIcon} />
+            </label>
+
+            <label className={classNames('app-form__avatar-input', { selected: !!avatarPreview, invalid: !!errors?.avatar })} title={intl.formatMessage(messages.uploadAvatar)}>
+              <input
+                type='file'
+                hidden
+                ref={avatarFileRef}
+                accept='image/*'
+                onChange={handleAvatarChange}
+              />
+
+              {avatarPreview && <img src={avatarPreview} alt='' />}
+
+              <Icon icon={avatarPreview ? EditIcon : AddPhotoAlternateIcon} />
+            </label>
+          </div>
+
+          <div className={classNames('input with_block_label', { field_with_errors: !!errors?.display_name })}>
+            <label htmlFor='display_name'><FormattedMessage id='onboarding.profile.display_name' defaultMessage='Display name' /></label>
+            <span className='hint'><FormattedMessage id='onboarding.profile.display_name_hint' defaultMessage='Your full name or your fun name…' /></span>
+            <div className='label_input'>
+              <input id='display_name' type='text' value={displayName} onChange={handleDisplayNameChange} maxLength={30} />
+            </div>
+          </div>
+
+          <div className={classNames('input with_block_label', { field_with_errors: !!errors?.note })}>
+            <label htmlFor='note'><FormattedMessage id='onboarding.profile.note' defaultMessage='Bio' /></label>
+            <span className='hint'><FormattedMessage id='onboarding.profile.note_hint' defaultMessage='You can @mention other people or #hashtags…' /></span>
+            <div className='label_input'>
+              <textarea id='note' value={note} onChange={handleNoteChange} maxLength={500} />
+            </div>
+          </div>
+        </div>
+
+        <label className='report-dialog-modal__toggle'>
+          <Toggle checked={discoverable} onChange={handleDiscoverableChange} />
+          <FormattedMessage id='onboarding.profile.discoverable' defaultMessage='Feature profile and posts in discovery algorithms' />
+        </label>
+
+        <label className='report-dialog-modal__toggle'>
+          <Toggle checked={indexable} onChange={handleIndexableChange} />
+          <FormattedMessage id='onboarding.profile.indexable' defaultMessage='Include public posts in search results' />
+        </label>
+
+        <div className='onboarding__footer'>
+          <Button block onClick={handleSubmit} disabled={isSaving}>{isSaving ? <LoadingIndicator /> : <FormattedMessage id='onboarding.profile.save_and_continue' defaultMessage='Save and continue' />}</Button>
+        </div>
+      </div>
+    </>
+  );
+};
diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx
index 334924422..adc0f9cba 100644
--- a/app/javascript/mastodon/features/onboarding/share.jsx
+++ b/app/javascript/mastodon/features/onboarding/share.jsx
@@ -1,31 +1,25 @@
 import PropTypes from 'prop-types';
 import { PureComponent } from 'react';
 
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
 import { Link } from 'react-router-dom';
 
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
 
 import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
 import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/outlined/content_copy.svg';
 import SwipeableViews from 'react-swipeable-views';
 
-import Column from 'mastodon/components/column';
 import { ColumnBackButton } from 'mastodon/components/column_back_button';
 import { Icon }  from 'mastodon/components/icon';
 import { me, domain } from 'mastodon/initial_state';
+import { useAppSelector } from 'mastodon/store';
 
 const messages = defineMessages({
   shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
 });
 
-const mapStateToProps = state => ({
-  account: state.getIn(['accounts', me]),
-});
-
 class CopyPasteText extends PureComponent {
 
   static propTypes = {
@@ -141,59 +135,47 @@ class TipCarousel extends PureComponent {
 
 }
 
-class Share extends PureComponent {
+export const Share = () => {
+  const account = useAppSelector(state => state.getIn(['accounts', me]));
+  const intl = useIntl();
+  const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
 
-  static propTypes = {
-    onBack: PropTypes.func,
-    account: ImmutablePropTypes.record,
-    intl: PropTypes.object,
-  };
+  return (
+    <>
+      <ColumnBackButton />
 
-  render () {
-    const { onBack, account, intl } = this.props;
-
-    const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
-
-    return (
-      <Column>
-        <ColumnBackButton onClick={onBack} />
-
-        <div className='scrollable privacy-policy'>
-          <div className='column-title'>
-            <h3><FormattedMessage id='onboarding.share.title' defaultMessage='Share your profile' /></h3>
-            <p><FormattedMessage id='onboarding.share.lead' defaultMessage='Let people know how they can find you on Mastodon!' /></p>
-          </div>
-
-          <CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
-
-          <TipCarousel>
-            <div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!'  values={{ strong: chunks => <strong>{chunks}</strong> }}  /></p></div>
-            <div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain, strong: chunks => <strong>{chunks}</strong> }} /></p></div>
-            <div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!'  values={{ strong: chunks => <strong>{chunks}</strong> }}  /></p></div>
-          </TipCarousel>
-
-          <p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
-
-          <div className='onboarding__links'>
-            <Link to='/home' className='onboarding__link'>
-              <FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
-              <Icon icon={ArrowRightAltIcon} />
-            </Link>
-
-            <Link to='/explore' className='onboarding__link'>
-              <FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
-              <Icon icon={ArrowRightAltIcon} />
-            </Link>
-          </div>
-
-          <div className='onboarding__footer'>
-            <button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.action.back' defaultMessage='Take me back' /></button>
-          </div>
+      <div className='scrollable privacy-policy'>
+        <div className='column-title'>
+          <h3><FormattedMessage id='onboarding.share.title' defaultMessage='Share your profile' /></h3>
+          <p><FormattedMessage id='onboarding.share.lead' defaultMessage='Let people know how they can find you on Mastodon!' /></p>
         </div>
-      </Column>
-    );
-  }
 
-}
+        <CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
 
-export default connect(mapStateToProps)(injectIntl(Share));
+        <TipCarousel>
+          <div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!'  values={{ strong: chunks => <strong>{chunks}</strong> }}  /></p></div>
+          <div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain, strong: chunks => <strong>{chunks}</strong> }} /></p></div>
+          <div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!'  values={{ strong: chunks => <strong>{chunks}</strong> }}  /></p></div>
+        </TipCarousel>
+
+        <p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
+
+        <div className='onboarding__links'>
+          <Link to='/home' className='onboarding__link'>
+            <FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
+            <Icon icon={ArrowRightAltIcon} />
+          </Link>
+
+          <Link to='/explore' className='onboarding__link'>
+            <FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
+            <Icon icon={ArrowRightAltIcon} />
+          </Link>
+        </div>
+
+        <div className='onboarding__footer'>
+          <Link className='link-button' to='/start'><FormattedMessage id='onboarding.action.back' defaultMessage='Take me back' /></Link>
+        </div>
+      </div>
+    </>
+  );
+};
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 02c69cbba..d3fee272f 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -210,7 +210,7 @@ class SwitchingColumnsArea extends PureComponent {
             <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
             <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
 
-            <WrappedRoute path='/start' exact component={Onboarding} content={children} />
+            <WrappedRoute path='/start' component={Onboarding} content={children} />
             <WrappedRoute path='/directory' component={Directory} content={children} />
             <WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
             <WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 9cbaf9305..041446037 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -390,7 +390,7 @@
   "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
-  "loading_indicator.label": "Loading...",
+  "loading_indicator.label": "Loading…",
   "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
   "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
   "mute_modal.duration": "Duration",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
   "onboarding.follows.lead": "Your home feed is the primary way to experience Mastodon. The more people you follow, the more active and interesting it will be. To get you started, here are some suggestions:",
   "onboarding.follows.title": "Personalize your home feed",
+  "onboarding.profile.discoverable": "Feature profile and posts in discovery algorithms",
+  "onboarding.profile.display_name": "Display name",
+  "onboarding.profile.display_name_hint": "Your full name or your fun name…",
+  "onboarding.profile.indexable": "Include public posts in search results",
+  "onboarding.profile.lead": "You can always complete this later in the settings, where even more customization options are available.",
+  "onboarding.profile.note": "Bio",
+  "onboarding.profile.note_hint": "You can @mention other people or #hashtags…",
+  "onboarding.profile.save_and_continue": "Save and continue",
+  "onboarding.profile.title": "Profile setup",
+  "onboarding.profile.upload_avatar": "Upload profile picture",
+  "onboarding.profile.upload_header": "Upload profile header",
   "onboarding.share.lead": "Let people know how they can find you on Mastodon!",
   "onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}",
   "onboarding.share.next_steps": "Possible next steps:",
diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts
index 00066e284..a04ebe629 100644
--- a/app/javascript/mastodon/models/account.ts
+++ b/app/javascript/mastodon/models/account.ts
@@ -67,6 +67,7 @@ export const accountDefaultValues: AccountShape = {
   bot: false,
   created_at: '',
   discoverable: false,
+  indexable: false,
   display_name: '',
   display_name_html: '',
   emojis: List<CustomEmoji>(),
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index c8cfe46a8..9f87352f5 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2552,7 +2552,7 @@ $ui-header-height: 55px;
 
 .column-title {
   text-align: center;
-  padding-bottom: 40px;
+  padding-bottom: 32px;
 
   h3 {
     font-size: 24px;
@@ -2743,58 +2743,6 @@ $ui-header-height: 55px;
   }
 }
 
-.onboarding__progress-indicator {
-  display: flex;
-  align-items: center;
-  margin-bottom: 30px;
-  position: sticky;
-  background: $ui-base-color;
-
-  @media screen and (width >= 600) {
-    padding: 0 40px;
-  }
-
-  &__line {
-    height: 4px;
-    flex: 1 1 auto;
-    background: lighten($ui-base-color, 4%);
-  }
-
-  &__step {
-    flex: 0 0 auto;
-    width: 30px;
-    height: 30px;
-    background: lighten($ui-base-color, 4%);
-    border-radius: 50%;
-    color: $primary-text-color;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-
-    svg {
-      width: 15px;
-      height: auto;
-    }
-
-    &.active {
-      background: $valid-value-color;
-    }
-  }
-
-  &__step.active,
-  &__line.active {
-    background: $valid-value-color;
-    background-image: linear-gradient(
-      90deg,
-      $valid-value-color,
-      lighten($valid-value-color, 8%),
-      $valid-value-color
-    );
-    background-size: 200px 100%;
-    animation: skeleton 1.2s ease-in-out infinite;
-  }
-}
-
 .follow-recommendations {
   background: darken($ui-base-color, 4%);
   border-radius: 8px;
@@ -2871,6 +2819,28 @@ $ui-header-height: 55px;
   }
 }
 
+.onboarding__profile {
+  position: relative;
+  margin-bottom: 40px + 20px;
+
+  .app-form__avatar-input {
+    border: 2px solid $ui-base-color;
+    position: absolute;
+    inset-inline-start: -2px;
+    bottom: -40px;
+    z-index: 2;
+  }
+
+  .app-form__header-input {
+    margin: 0 -20px;
+    border-radius: 0;
+
+    img {
+      border-radius: 0;
+    }
+  }
+}
+
 .compose-form__highlightable {
   display: flex;
   flex-direction: column;
@@ -3145,6 +3115,7 @@ $ui-header-height: 55px;
   cursor: pointer;
   background-color: transparent;
   border: 0;
+  border-radius: 10px;
   padding: 0;
   user-select: none;
   -webkit-tap-highlight-color: rgba($base-overlay-background, 0);
@@ -3169,81 +3140,41 @@ $ui-header-height: 55px;
 }
 
 .react-toggle-track {
-  width: 50px;
-  height: 24px;
+  width: 32px;
+  height: 20px;
   padding: 0;
-  border-radius: 30px;
-  background-color: $ui-base-color;
-  transition: background-color 0.2s ease;
+  border-radius: 10px;
+  background-color: #626982;
 }
 
-.react-toggle:is(:hover, :focus-within):not(.react-toggle--disabled)
-  .react-toggle-track {
-  background-color: darken($ui-base-color, 10%);
+.react-toggle--focus {
+  outline: $ui-button-focus-outline;
 }
 
 .react-toggle--checked .react-toggle-track {
-  background-color: darken($ui-highlight-color, 2%);
-}
-
-.react-toggle--checked:is(:hover, :focus-within):not(.react-toggle--disabled)
-  .react-toggle-track {
   background-color: $ui-highlight-color;
 }
 
-.react-toggle-track-check {
-  position: absolute;
-  width: 14px;
-  height: 10px;
-  top: 0;
-  bottom: 0;
-  margin-top: auto;
-  margin-bottom: auto;
-  line-height: 0;
-  inset-inline-start: 8px;
-  opacity: 0;
-  transition: opacity 0.25s ease;
-}
-
-.react-toggle--checked .react-toggle-track-check {
-  opacity: 1;
-  transition: opacity 0.25s ease;
-}
-
+.react-toggle-track-check,
 .react-toggle-track-x {
-  position: absolute;
-  width: 10px;
-  height: 10px;
-  top: 0;
-  bottom: 0;
-  margin-top: auto;
-  margin-bottom: auto;
-  line-height: 0;
-  inset-inline-end: 10px;
-  opacity: 1;
-  transition: opacity 0.25s ease;
-}
-
-.react-toggle--checked .react-toggle-track-x {
-  opacity: 0;
+  display: none;
 }
 
 .react-toggle-thumb {
   position: absolute;
-  top: 1px;
-  inset-inline-start: 1px;
-  width: 22px;
-  height: 22px;
-  border: 1px solid $ui-base-color;
+  top: 2px;
+  inset-inline-start: 2px;
+  width: 16px;
+  height: 16px;
   border-radius: 50%;
-  background-color: darken($simple-background-color, 2%);
+  background-color: $primary-text-color;
   box-sizing: border-box;
   transition: all 0.25s ease;
   transition-property: border-color, left;
 }
 
 .react-toggle--checked .react-toggle-thumb {
-  inset-inline-start: 27px;
+  inset-inline-start: 32px - 16px - 2px;
   border-color: $ui-highlight-color;
 }
 
@@ -4066,6 +3997,17 @@ a.status-card {
   justify-content: center;
 }
 
+.button .loading-indicator {
+  position: static;
+  transform: none;
+
+  .circular-progress {
+    color: $primary-text-color;
+    width: 22px;
+    height: 22px;
+  }
+}
+
 .circular-progress {
   color: lighten($ui-base-color, 26%);
   animation: 1.4s linear 0s infinite normal none running simple-rotate;
@@ -5799,12 +5741,14 @@ a.status-card {
   &__toggle {
     display: flex;
     align-items: center;
-    margin-bottom: 10px;
+    margin-bottom: 16px;
+    gap: 8px;
 
     & > span {
-      font-size: 17px;
+      display: block;
+      font-size: 14px;
       font-weight: 500;
-      margin-inline-start: 10px;
+      line-height: 20px;
     }
   }
 
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 0f8eecee0..e72a01936 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -36,7 +36,7 @@ code {
   }
 
   .input {
-    margin-bottom: 15px;
+    margin-bottom: 16px;
     overflow: hidden;
 
     &.hidden {
@@ -266,12 +266,13 @@ code {
       font-size: 14px;
       color: $primary-text-color;
       display: block;
-      font-weight: 500;
-      padding-top: 5px;
+      font-weight: 600;
+      line-height: 20px;
     }
 
     .hint {
-      margin-bottom: 15px;
+      line-height: 16px;
+      margin-bottom: 12px;
     }
 
     ul {
@@ -427,7 +428,8 @@ code {
   input[type='datetime-local'],
   textarea {
     box-sizing: border-box;
-    font-size: 16px;
+    font-size: 14px;
+    line-height: 20px;
     color: $primary-text-color;
     display: block;
     width: 100%;
@@ -435,9 +437,9 @@ code {
     font-family: inherit;
     resize: vertical;
     background: darken($ui-base-color, 10%);
-    border: 1px solid darken($ui-base-color, 14%);
-    border-radius: 4px;
-    padding: 10px;
+    border: 1px solid darken($ui-base-color, 10%);
+    border-radius: 8px;
+    padding: 10px 16px;
 
     &::placeholder {
       color: lighten($darker-text-color, 4%);
@@ -451,14 +453,13 @@ code {
       border-color: $valid-value-color;
     }
 
-    &:hover {
-      border-color: darken($ui-base-color, 20%);
-    }
-
     &:active,
     &:focus {
       border-color: $highlight-text-color;
-      background: darken($ui-base-color, 8%);
+    }
+
+    @media screen and (width <= 600px) {
+      font-size: 16px;
     }
   }
 
@@ -524,12 +525,11 @@ code {
     border-radius: 4px;
     background: $ui-button-background-color;
     color: $ui-button-color;
-    font-size: 18px;
-    line-height: inherit;
+    font-size: 15px;
+    line-height: 22px;
     height: auto;
-    padding: 10px;
+    padding: 7px 18px;
     text-decoration: none;
-    text-transform: uppercase;
     text-align: center;
     box-sizing: border-box;
     cursor: pointer;
@@ -1220,3 +1220,74 @@ code {
     background: $highlight-text-color;
   }
 }
+
+.app-form {
+  & > * {
+    margin-bottom: 16px;
+  }
+
+  &__avatar-input,
+  &__header-input {
+    display: block;
+    border-radius: 8px;
+    background: var(--dropdown-background-color);
+    position: relative;
+    cursor: pointer;
+
+    img {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      border-radius: 8px;
+      z-index: 0;
+    }
+
+    .icon {
+      position: absolute;
+      inset-inline-start: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      color: $darker-text-color;
+      z-index: 3;
+    }
+
+    &.selected .icon {
+      color: $primary-text-color;
+      transform: none;
+      inset-inline-start: auto;
+      inset-inline-end: 8px;
+      top: auto;
+      bottom: 8px;
+    }
+
+    &.invalid img {
+      outline: 1px solid $error-value-color;
+      outline-offset: -1px;
+    }
+
+    &.invalid::before {
+      display: block;
+      content: '';
+      width: 100%;
+      height: 100%;
+      position: absolute;
+      background: rgba($error-value-color, 0.25);
+      z-index: 2;
+      border-radius: 8px;
+    }
+
+    &:hover {
+      background-color: var(--dropdown-border-color);
+    }
+  }
+
+  &__avatar-input {
+    width: 80px;
+    height: 80px;
+  }
+
+  &__header-input {
+    aspect-ratio: 580/193;
+  }
+}
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index 8c6520b30..5d1292a6b 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -6,7 +6,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
 
   # Please update `app/javascript/mastodon/api_types/accounts.ts` when making changes to the attributes
 
-  attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at,
+  attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :indexable, :group, :created_at,
              :note, :url, :uri, :avatar, :avatar_static, :header, :header_static,
              :followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections
 
@@ -112,6 +112,10 @@ class REST::AccountSerializer < ActiveModel::Serializer
     object.suspended? ? false : object.discoverable
   end
 
+  def indexable
+    object.suspended? ? false : object.indexable
+  end
+
   def moved_to_account
     object.suspended? ? nil : AccountDecorator.new(object.moved_to_account)
   end
diff --git a/config/routes.rb b/config/routes.rb
index 82431f6ec..150b26cf1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -31,7 +31,7 @@ Rails.application.routes.draw do
     /favourites
     /bookmarks
     /pinned
-    /start
+    /start/(*any)
     /directory
     /explore/(*any)
     /search

From fe58ac8d9f1b0c4347fde451f1caedac2ac605bc Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 15 Nov 2023 08:14:51 -0500
Subject: [PATCH 38/63] Improve spec coverage for `api/web/push_subscriptions`
 controller (#27858)

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 .../api/web/push_subscriptions_controller.rb  | 78 ++++++++++++-------
 .../web/push_subscriptions_controller_spec.rb | 60 ++++++++------
 2 files changed, 86 insertions(+), 52 deletions(-)

diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb
index 5167928e9..167d16fc4 100644
--- a/app/controllers/api/web/push_subscriptions_controller.rb
+++ b/app/controllers/api/web/push_subscriptions_controller.rb
@@ -3,37 +3,13 @@
 class Api::Web::PushSubscriptionsController < Api::Web::BaseController
   before_action :require_user!
   before_action :set_push_subscription, only: :update
+  before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
+  after_action :update_session_with_subscription, only: :create
 
   def create
-    active_session = current_session
+    @push_subscription = ::Web::PushSubscription.create!(web_push_subscription_params)
 
-    unless active_session.web_push_subscription.nil?
-      active_session.web_push_subscription.destroy!
-      active_session.update!(web_push_subscription: nil)
-    end
-
-    # Mobile devices do not support regular notifications, so we enable push notifications by default
-    alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet?
-
-    data = {
-      policy: 'all',
-      alerts: Notification::TYPES.index_with { alerts_enabled },
-    }
-
-    data.deep_merge!(data_params) if params[:data]
-
-    push_subscription = ::Web::PushSubscription.create!(
-      endpoint: subscription_params[:endpoint],
-      key_p256dh: subscription_params[:keys][:p256dh],
-      key_auth: subscription_params[:keys][:auth],
-      data: data,
-      user_id: active_session.user_id,
-      access_token_id: active_session.access_token_id
-    )
-
-    active_session.update!(web_push_subscription: push_subscription)
-
-    render json: push_subscription, serializer: REST::WebPushSubscriptionSerializer
+    render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
   end
 
   def update
@@ -43,6 +19,41 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
 
   private
 
+  def active_session
+    @active_session ||= current_session
+  end
+
+  def destroy_previous_subscriptions
+    active_session.web_push_subscription.destroy!
+    active_session.update!(web_push_subscription: nil)
+  end
+
+  def prior_subscriptions?
+    active_session.web_push_subscription.present?
+  end
+
+  def subscription_data
+    default_subscription_data.tap do |data|
+      data.deep_merge!(data_params) if params[:data]
+    end
+  end
+
+  def default_subscription_data
+    {
+      policy: 'all',
+      alerts: Notification::TYPES.index_with { alerts_enabled },
+    }
+  end
+
+  def alerts_enabled
+    # Mobile devices do not support regular notifications, so we enable push notifications by default
+    active_session.detection.device.mobile? || active_session.detection.device.tablet?
+  end
+
+  def update_session_with_subscription
+    active_session.update!(web_push_subscription: @push_subscription)
+  end
+
   def set_push_subscription
     @push_subscription = ::Web::PushSubscription.find(params[:id])
   end
@@ -51,6 +62,17 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
     @subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
   end
 
+  def web_push_subscription_params
+    {
+      access_token_id: active_session.access_token_id,
+      data: subscription_data,
+      endpoint: subscription_params[:endpoint],
+      key_auth: subscription_params[:keys][:auth],
+      key_p256dh: subscription_params[:keys][:p256dh],
+      user_id: active_session.user_id,
+    }
+  end
+
   def data_params
     @data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES)
   end
diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb
index 9f027ede9..58677841c 100644
--- a/spec/controllers/api/web/push_subscriptions_controller_spec.rb
+++ b/spec/controllers/api/web/push_subscriptions_controller_spec.rb
@@ -37,37 +37,49 @@ describe Api::Web::PushSubscriptionsController do
     }
   end
 
+  before do
+    sign_in(user)
+
+    stub_request(:post, create_payload[:subscription][:endpoint]).to_return(status: 200)
+  end
+
   describe 'POST #create' do
     it 'saves push subscriptions' do
-      sign_in(user)
-
-      stub_request(:post, create_payload[:subscription][:endpoint]).to_return(status: 200)
-
       post :create, format: :json, params: create_payload
 
+      expect(response).to have_http_status(200)
+
       user.reload
 
-      push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
+      expect(created_push_subscription).to have_attributes(
+        endpoint: eq(create_payload[:subscription][:endpoint]),
+        key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]),
+        key_auth: eq(create_payload[:subscription][:keys][:auth])
+      )
+      expect(user.session_activations.first.web_push_subscription).to eq(created_push_subscription)
+    end
 
-      expect(push_subscription['endpoint']).to eq(create_payload[:subscription][:endpoint])
-      expect(push_subscription['key_p256dh']).to eq(create_payload[:subscription][:keys][:p256dh])
-      expect(push_subscription['key_auth']).to eq(create_payload[:subscription][:keys][:auth])
+    context 'with a user who has a session with a prior subscription' do
+      let!(:prior_subscription) { Fabricate(:web_push_subscription, session_activation: user.session_activations.last) }
+
+      it 'destroys prior subscription when creating new one' do
+        post :create, format: :json, params: create_payload
+
+        expect(response).to have_http_status(200)
+        expect { prior_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
+      end
     end
 
     context 'with initial data' do
       it 'saves alert settings' do
-        sign_in(user)
-
-        stub_request(:post, create_payload[:subscription][:endpoint]).to_return(status: 200)
-
         post :create, format: :json, params: create_payload.merge(alerts_payload)
 
-        push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
+        expect(response).to have_http_status(200)
 
-        expect(push_subscription.data['policy']).to eq 'all'
+        expect(created_push_subscription.data['policy']).to eq 'all'
 
         %w(follow follow_request favourite reblog mention poll status).each do |type|
-          expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
+          expect(created_push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
         end
       end
     end
@@ -75,23 +87,23 @@ describe Api::Web::PushSubscriptionsController do
 
   describe 'PUT #update' do
     it 'changes alert settings' do
-      sign_in(user)
-
-      stub_request(:post, create_payload[:subscription][:endpoint]).to_return(status: 200)
-
       post :create, format: :json, params: create_payload
 
-      alerts_payload[:id] = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]).id
+      expect(response).to have_http_status(200)
+
+      alerts_payload[:id] = created_push_subscription.id
 
       put :update, format: :json, params: alerts_payload
 
-      push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
-
-      expect(push_subscription.data['policy']).to eq 'all'
+      expect(created_push_subscription.data['policy']).to eq 'all'
 
       %w(follow follow_request favourite reblog mention poll status).each do |type|
-        expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
+        expect(created_push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
       end
     end
   end
+
+  def created_push_subscription
+    Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
+  end
 end

From 12a5b7391d0298a99b94386538fe4ca625cea7e9 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 15 Nov 2023 08:21:18 -0500
Subject: [PATCH 39/63] Add spec for well known change password endpoint
 (#27856)

---
 spec/requests/well_known/change_password_spec.rb | 11 +++++++++++
 1 file changed, 11 insertions(+)
 create mode 100644 spec/requests/well_known/change_password_spec.rb

diff --git a/spec/requests/well_known/change_password_spec.rb b/spec/requests/well_known/change_password_spec.rb
new file mode 100644
index 000000000..04134b71f
--- /dev/null
+++ b/spec/requests/well_known/change_password_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'The /.well-known/change-password request' do
+  it 'redirects to the change password page' do
+    get '/.well-known/change-password'
+
+    expect(response).to redirect_to '/auth/edit'
+  end
+end

From bd575a1dd69d87ca0f69873f7badf28d38e8b9ed Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 15 Nov 2023 15:18:43 +0100
Subject: [PATCH 40/63] Add banner for forwarded reports made by remote users
 about remote content (#27549)

---
 app/views/admin/reports/_header_details.html.haml | 2 +-
 app/views/admin/reports/show.html.haml            | 3 +++
 config/locales/en.yml                             | 1 +
 3 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/app/views/admin/reports/_header_details.html.haml b/app/views/admin/reports/_header_details.html.haml
index 5878cd2ff..45790b9cd 100644
--- a/app/views/admin/reports/_header_details.html.haml
+++ b/app/views/admin/reports/_header_details.html.haml
@@ -22,7 +22,7 @@
         = t('admin.reports.resolved')
       - else
         = t('admin.reports.unresolved')
-  - unless report.target_account.local?
+  - if report.account.local? && !report.target_account.local?
     .report-header__details__item
       .report-header__details__item__header
         %strong= t('admin.reports.forwarded')
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index 13a4d4834..4376e5af4 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -7,6 +7,9 @@
   - else
     = link_to t('admin.reports.mark_as_unresolved'), reopen_admin_report_path(@report), method: :post, class: 'button'
 
+- unless @report.account.local? || @report.target_account.local?
+  .flash-message= t('admin.reports.forwarded_replies_explanation')
+
 .report-header
   = render 'admin/reports/header_card', report: @report
   = render 'admin/reports/header_details', report: @report
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 7319de53d..057f7a584 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -611,6 +611,7 @@ en:
       created_at: Reported
       delete_and_resolve: Delete posts
       forwarded: Forwarded
+      forwarded_replies_explanation: This report is from a remote user and about remote content. It has been forwarded to you because the reported content is in reply to one of your users.
       forwarded_to: Forwarded to %{domain}
       mark_as_resolved: Mark as resolved
       mark_as_sensitive: Mark as sensitive

From 3de91456132ffadf5b98848409fa2a0377a3bef6 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 15 Nov 2023 14:12:02 -0500
Subject: [PATCH 41/63] Move controller specs for `well-known` endpoints to
 request specs (#27855)

---
 .../well_known/host_meta_controller_spec.rb   |  22 --
 .../well_known/node_info_controller_spec.rb   |  41 ---
 .../well_known/webfinger_controller_spec.rb   | 235 ----------------
 spec/requests/host_meta_request_spec.rb       |  14 -
 spec/requests/webfinger_request_spec.rb       |  33 ---
 spec/requests/well_known/host_meta_spec.rb    |  27 ++
 spec/requests/well_known/node_info_spec.rb    |  58 ++++
 spec/requests/well_known/webfinger_spec.rb    | 255 ++++++++++++++++++
 8 files changed, 340 insertions(+), 345 deletions(-)
 delete mode 100644 spec/controllers/well_known/host_meta_controller_spec.rb
 delete mode 100644 spec/controllers/well_known/node_info_controller_spec.rb
 delete mode 100644 spec/controllers/well_known/webfinger_controller_spec.rb
 delete mode 100644 spec/requests/host_meta_request_spec.rb
 delete mode 100644 spec/requests/webfinger_request_spec.rb
 create mode 100644 spec/requests/well_known/host_meta_spec.rb
 create mode 100644 spec/requests/well_known/node_info_spec.rb
 create mode 100644 spec/requests/well_known/webfinger_spec.rb

diff --git a/spec/controllers/well_known/host_meta_controller_spec.rb b/spec/controllers/well_known/host_meta_controller_spec.rb
deleted file mode 100644
index 4bd161cd9..000000000
--- a/spec/controllers/well_known/host_meta_controller_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe WellKnown::HostMetaController do
-  render_views
-
-  describe 'GET #show' do
-    it 'returns http success' do
-      get :show, format: :xml
-
-      expect(response).to have_http_status(200)
-      expect(response.media_type).to eq 'application/xrd+xml'
-      expect(response.body).to eq <<~XML
-        <?xml version="1.0" encoding="UTF-8"?>
-        <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
-          <Link rel="lrdd" template="https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}"/>
-        </XRD>
-      XML
-    end
-  end
-end
diff --git a/spec/controllers/well_known/node_info_controller_spec.rb b/spec/controllers/well_known/node_info_controller_spec.rb
deleted file mode 100644
index 6ec34afd0..000000000
--- a/spec/controllers/well_known/node_info_controller_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe WellKnown::NodeInfoController do
-  render_views
-
-  describe 'GET #index' do
-    it 'returns json document pointing to node info' do
-      get :index
-
-      expect(response).to have_http_status(200)
-      expect(response.media_type).to eq 'application/json'
-
-      json = body_as_json
-
-      expect(json[:links]).to be_an Array
-      expect(json[:links][0][:rel]).to eq 'http://nodeinfo.diaspora.software/ns/schema/2.0'
-      expect(json[:links][0][:href]).to include 'nodeinfo/2.0'
-    end
-  end
-
-  describe 'GET #show' do
-    it 'returns json document with node info properties' do
-      get :show
-
-      expect(response).to have_http_status(200)
-      expect(response.media_type).to eq 'application/json'
-
-      json = body_as_json
-      foo = { 'foo' => 0 }
-
-      expect(foo).to_not match_json_schema('nodeinfo_2.0')
-      expect(json).to match_json_schema('nodeinfo_2.0')
-      expect(json[:version]).to eq '2.0'
-      expect(json[:usage]).to be_a Hash
-      expect(json[:software]).to be_a Hash
-      expect(json[:protocols]).to be_an Array
-    end
-  end
-end
diff --git a/spec/controllers/well_known/webfinger_controller_spec.rb b/spec/controllers/well_known/webfinger_controller_spec.rb
deleted file mode 100644
index 6610f4d13..000000000
--- a/spec/controllers/well_known/webfinger_controller_spec.rb
+++ /dev/null
@@ -1,235 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe WellKnown::WebfingerController do
-  include RoutingHelper
-
-  render_views
-
-  describe 'GET #show' do
-    subject(:perform_show!) do
-      get :show, params: { resource: resource }, format: :json
-    end
-
-    let(:alternate_domains) { [] }
-    let(:alice) { Fabricate(:account, username: 'alice') }
-    let(:resource) { nil }
-
-    around do |example|
-      tmp = Rails.configuration.x.alternate_domains
-      Rails.configuration.x.alternate_domains = alternate_domains
-      example.run
-      Rails.configuration.x.alternate_domains = tmp
-    end
-
-    shared_examples 'a successful response' do
-      it 'returns http success' do
-        expect(response).to have_http_status(200)
-      end
-
-      it 'does not set a Vary header' do
-        expect(response.headers['Vary']).to be_nil
-      end
-
-      it 'returns application/jrd+json' do
-        expect(response.media_type).to eq 'application/jrd+json'
-      end
-
-      it 'returns links for the account' do
-        json = body_as_json
-        expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
-        expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
-      end
-    end
-
-    context 'when an account exists' do
-      let(:resource) { alice.to_webfinger_s }
-
-      before do
-        perform_show!
-      end
-
-      it_behaves_like 'a successful response'
-    end
-
-    context 'when an account is temporarily suspended' do
-      let(:resource) { alice.to_webfinger_s }
-
-      before do
-        alice.suspend!
-        perform_show!
-      end
-
-      it_behaves_like 'a successful response'
-    end
-
-    context 'when an account is permanently suspended or deleted' do
-      let(:resource) { alice.to_webfinger_s }
-
-      before do
-        alice.suspend!
-        alice.deletion_request.destroy
-        perform_show!
-      end
-
-      it 'returns http gone' do
-        expect(response).to have_http_status(410)
-      end
-    end
-
-    context 'when an account is not found' do
-      let(:resource) { 'acct:not@existing.com' }
-
-      before do
-        perform_show!
-      end
-
-      it 'returns http not found' do
-        expect(response).to have_http_status(404)
-      end
-    end
-
-    context 'with an alternate domain' do
-      let(:alternate_domains) { ['foo.org'] }
-
-      before do
-        perform_show!
-      end
-
-      context 'when an account exists' do
-        let(:resource) do
-          username, = alice.to_webfinger_s.split('@')
-          "#{username}@foo.org"
-        end
-
-        it_behaves_like 'a successful response'
-      end
-
-      context 'when the domain is wrong' do
-        let(:resource) do
-          username, = alice.to_webfinger_s.split('@')
-          "#{username}@bar.org"
-        end
-
-        it 'returns http not found' do
-          expect(response).to have_http_status(404)
-        end
-      end
-    end
-
-    context 'when the old name scheme is used to query the instance actor' do
-      let(:resource) do
-        "#{Rails.configuration.x.local_domain}@#{Rails.configuration.x.local_domain}"
-      end
-
-      before do
-        perform_show!
-      end
-
-      it 'returns http success' do
-        expect(response).to have_http_status(200)
-      end
-
-      it 'does not set a Vary header' do
-        expect(response.headers['Vary']).to be_nil
-      end
-
-      it 'returns application/jrd+json' do
-        expect(response.media_type).to eq 'application/jrd+json'
-      end
-
-      it 'returns links for the internal account' do
-        json = body_as_json
-        expect(json[:subject]).to eq 'acct:mastodon.internal@cb6e6126.ngrok.io'
-        expect(json[:aliases]).to eq ['https://cb6e6126.ngrok.io/actor']
-      end
-    end
-
-    context 'with no resource parameter' do
-      let(:resource) { nil }
-
-      before do
-        perform_show!
-      end
-
-      it 'returns http bad request' do
-        expect(response).to have_http_status(400)
-      end
-    end
-
-    context 'with a nonsense parameter' do
-      let(:resource) { 'df/:dfkj' }
-
-      before do
-        perform_show!
-      end
-
-      it 'returns http bad request' do
-        expect(response).to have_http_status(400)
-      end
-    end
-
-    context 'when an account has an avatar' do
-      let(:alice) { Fabricate(:account, username: 'alice', avatar: attachment_fixture('attachment.jpg')) }
-      let(:resource) { alice.to_webfinger_s }
-
-      it 'returns avatar in response' do
-        perform_show!
-
-        avatar_link = get_avatar_link(body_as_json)
-        expect(avatar_link).to_not be_nil
-        expect(avatar_link[:type]).to eq alice.avatar.content_type
-        expect(avatar_link[:href]).to eq full_asset_url(alice.avatar)
-      end
-
-      context 'with limited federation mode' do
-        before do
-          allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(true)
-        end
-
-        it 'does not return avatar in response' do
-          perform_show!
-
-          avatar_link = get_avatar_link(body_as_json)
-          expect(avatar_link).to be_nil
-        end
-      end
-
-      context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do
-        around do |example|
-          ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do
-            example.run
-          end
-        end
-
-        it 'does not return avatar in response' do
-          perform_show!
-
-          avatar_link = get_avatar_link(body_as_json)
-          expect(avatar_link).to be_nil
-        end
-      end
-    end
-
-    context 'when an account does not have an avatar' do
-      let(:alice) { Fabricate(:account, username: 'alice', avatar: nil) }
-      let(:resource) { alice.to_webfinger_s }
-
-      before do
-        perform_show!
-      end
-
-      it 'does not return avatar in response' do
-        avatar_link = get_avatar_link(body_as_json)
-        expect(avatar_link).to be_nil
-      end
-    end
-  end
-
-  private
-
-  def get_avatar_link(json)
-    json[:links].find { |link| link[:rel] == 'http://webfinger.net/rel/avatar' }
-  end
-end
diff --git a/spec/requests/host_meta_request_spec.rb b/spec/requests/host_meta_request_spec.rb
deleted file mode 100644
index ec26ecba7..000000000
--- a/spec/requests/host_meta_request_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe 'The host_meta route' do
-  describe 'requested without accepts headers' do
-    it 'returns an xml response' do
-      get host_meta_url
-
-      expect(response).to have_http_status(200)
-      expect(response.media_type).to eq 'application/xrd+xml'
-    end
-  end
-end
diff --git a/spec/requests/webfinger_request_spec.rb b/spec/requests/webfinger_request_spec.rb
deleted file mode 100644
index 68a1478be..000000000
--- a/spec/requests/webfinger_request_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe 'The webfinger route' do
-  let(:alice) { Fabricate(:account, username: 'alice') }
-
-  describe 'requested with standard accepts headers' do
-    it 'returns a json response' do
-      get webfinger_url(resource: alice.to_webfinger_s)
-
-      expect(response).to have_http_status(200)
-      expect(response.media_type).to eq 'application/jrd+json'
-    end
-  end
-
-  describe 'asking for json format' do
-    it 'returns a json response for json format' do
-      get webfinger_url(resource: alice.to_webfinger_s, format: :json)
-
-      expect(response).to have_http_status(200)
-      expect(response.media_type).to eq 'application/jrd+json'
-    end
-
-    it 'returns a json response for json accept header' do
-      headers = { 'HTTP_ACCEPT' => 'application/jrd+json' }
-      get webfinger_url(resource: alice.to_webfinger_s), headers: headers
-
-      expect(response).to have_http_status(200)
-      expect(response.media_type).to eq 'application/jrd+json'
-    end
-  end
-end
diff --git a/spec/requests/well_known/host_meta_spec.rb b/spec/requests/well_known/host_meta_spec.rb
new file mode 100644
index 000000000..ca10a51a0
--- /dev/null
+++ b/spec/requests/well_known/host_meta_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'The /.well-known/host-meta request' do
+  it 'returns http success with valid XML response' do
+    get '/.well-known/host-meta'
+
+    expect(response)
+      .to have_http_status(200)
+      .and have_attributes(
+        media_type: 'application/xrd+xml',
+        body: host_meta_xml_template
+      )
+  end
+
+  private
+
+  def host_meta_xml_template
+    <<~XML
+      <?xml version="1.0" encoding="UTF-8"?>
+      <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+        <Link rel="lrdd" template="https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}"/>
+      </XRD>
+    XML
+  end
+end
diff --git a/spec/requests/well_known/node_info_spec.rb b/spec/requests/well_known/node_info_spec.rb
new file mode 100644
index 000000000..0934b0fde
--- /dev/null
+++ b/spec/requests/well_known/node_info_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'The well-known node-info endpoints' do
+  describe 'The /.well-known/node-info endpoint' do
+    it 'returns JSON document pointing to node info' do
+      get '/.well-known/nodeinfo'
+
+      expect(response)
+        .to have_http_status(200)
+        .and have_attributes(
+          media_type: 'application/json'
+        )
+
+      expect(body_as_json).to include(
+        links: be_an(Array).and(
+          contain_exactly(
+            include(
+              rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
+              href: include('nodeinfo/2.0')
+            )
+          )
+        )
+      )
+    end
+  end
+
+  describe 'The /nodeinfo/2.0 endpoint' do
+    it 'returns JSON document with node info properties' do
+      get '/nodeinfo/2.0'
+
+      expect(response)
+        .to have_http_status(200)
+        .and have_attributes(
+          media_type: 'application/json'
+        )
+
+      expect(non_matching_hash)
+        .to_not match_json_schema('nodeinfo_2.0')
+
+      expect(body_as_json)
+        .to match_json_schema('nodeinfo_2.0')
+        .and include(
+          version: '2.0',
+          usage: be_a(Hash),
+          software: be_a(Hash),
+          protocols: be_a(Array)
+        )
+    end
+
+    private
+
+    def non_matching_hash
+      { 'foo' => 0 }
+    end
+  end
+end
diff --git a/spec/requests/well_known/webfinger_spec.rb b/spec/requests/well_known/webfinger_spec.rb
new file mode 100644
index 000000000..779f1bba5
--- /dev/null
+++ b/spec/requests/well_known/webfinger_spec.rb
@@ -0,0 +1,255 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'The /.well-known/webfinger endpoint' do
+  subject(:perform_request!) { get webfinger_url(resource: resource) }
+
+  let(:alternate_domains) { [] }
+  let(:alice) { Fabricate(:account, username: 'alice') }
+  let(:resource) { nil }
+
+  around do |example|
+    tmp = Rails.configuration.x.alternate_domains
+    Rails.configuration.x.alternate_domains = alternate_domains
+    example.run
+    Rails.configuration.x.alternate_domains = tmp
+  end
+
+  shared_examples 'a successful response' do
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+
+    it 'sets only a Vary Origin header' do
+      expect(response.headers['Vary']).to eq('Origin')
+    end
+
+    it 'returns application/jrd+json' do
+      expect(response.media_type).to eq 'application/jrd+json'
+    end
+
+    it 'returns links for the account' do
+      json = body_as_json
+      expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
+      expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
+    end
+  end
+
+  context 'when an account exists' do
+    let(:resource) { alice.to_webfinger_s }
+
+    before do
+      perform_request!
+    end
+
+    it_behaves_like 'a successful response'
+  end
+
+  context 'when an account is temporarily suspended' do
+    let(:resource) { alice.to_webfinger_s }
+
+    before do
+      alice.suspend!
+      perform_request!
+    end
+
+    it_behaves_like 'a successful response'
+  end
+
+  context 'when an account is permanently suspended or deleted' do
+    let(:resource) { alice.to_webfinger_s }
+
+    before do
+      alice.suspend!
+      alice.deletion_request.destroy
+      perform_request!
+    end
+
+    it 'returns http gone' do
+      expect(response).to have_http_status(410)
+    end
+  end
+
+  context 'when an account is not found' do
+    let(:resource) { 'acct:not@existing.com' }
+
+    before do
+      perform_request!
+    end
+
+    it 'returns http not found' do
+      expect(response).to have_http_status(404)
+    end
+  end
+
+  context 'with an alternate domain' do
+    let(:alternate_domains) { ['foo.org'] }
+
+    before do
+      perform_request!
+    end
+
+    context 'when an account exists' do
+      let(:resource) do
+        username, = alice.to_webfinger_s.split('@')
+        "#{username}@foo.org"
+      end
+
+      it_behaves_like 'a successful response'
+    end
+
+    context 'when the domain is wrong' do
+      let(:resource) do
+        username, = alice.to_webfinger_s.split('@')
+        "#{username}@bar.org"
+      end
+
+      it 'returns http not found' do
+        expect(response).to have_http_status(404)
+      end
+    end
+  end
+
+  context 'when the old name scheme is used to query the instance actor' do
+    let(:resource) do
+      "#{Rails.configuration.x.local_domain}@#{Rails.configuration.x.local_domain}"
+    end
+
+    before do
+      perform_request!
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(200)
+    end
+
+    it 'sets only a Vary Origin header' do
+      expect(response.headers['Vary']).to eq('Origin')
+    end
+
+    it 'returns application/jrd+json' do
+      expect(response.media_type).to eq 'application/jrd+json'
+    end
+
+    it 'returns links for the internal account' do
+      json = body_as_json
+      expect(json[:subject]).to eq 'acct:mastodon.internal@cb6e6126.ngrok.io'
+      expect(json[:aliases]).to eq ['https://cb6e6126.ngrok.io/actor']
+    end
+  end
+
+  context 'with no resource parameter' do
+    let(:resource) { nil }
+
+    before do
+      perform_request!
+    end
+
+    it 'returns http bad request' do
+      expect(response).to have_http_status(400)
+    end
+  end
+
+  context 'with a nonsense parameter' do
+    let(:resource) { 'df/:dfkj' }
+
+    before do
+      perform_request!
+    end
+
+    it 'returns http bad request' do
+      expect(response).to have_http_status(400)
+    end
+  end
+
+  context 'when an account has an avatar' do
+    let(:alice) { Fabricate(:account, username: 'alice', avatar: attachment_fixture('attachment.jpg')) }
+    let(:resource) { alice.to_webfinger_s }
+
+    it 'returns avatar in response' do
+      perform_request!
+
+      avatar_link = get_avatar_link(body_as_json)
+      expect(avatar_link).to_not be_nil
+      expect(avatar_link[:type]).to eq alice.avatar.content_type
+      expect(avatar_link[:href]).to eq Addressable::URI.new(host: Rails.configuration.x.local_domain, path: alice.avatar.to_s, scheme: 'https').to_s
+    end
+
+    context 'with limited federation mode' do
+      before do
+        allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(true)
+      end
+
+      it 'does not return avatar in response' do
+        perform_request!
+
+        avatar_link = get_avatar_link(body_as_json)
+        expect(avatar_link).to be_nil
+      end
+    end
+
+    context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do
+      around do |example|
+        ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do
+          example.run
+        end
+      end
+
+      it 'does not return avatar in response' do
+        perform_request!
+
+        avatar_link = get_avatar_link(body_as_json)
+        expect(avatar_link).to be_nil
+      end
+    end
+  end
+
+  context 'when an account does not have an avatar' do
+    let(:alice) { Fabricate(:account, username: 'alice', avatar: nil) }
+    let(:resource) { alice.to_webfinger_s }
+
+    before do
+      perform_request!
+    end
+
+    it 'does not return avatar in response' do
+      avatar_link = get_avatar_link(body_as_json)
+      expect(avatar_link).to be_nil
+    end
+  end
+
+  context 'with different headers' do
+    describe 'requested with standard accepts headers' do
+      it 'returns a json response' do
+        get webfinger_url(resource: alice.to_webfinger_s)
+
+        expect(response).to have_http_status(200)
+        expect(response.media_type).to eq 'application/jrd+json'
+      end
+    end
+
+    describe 'asking for json format' do
+      it 'returns a json response for json format' do
+        get webfinger_url(resource: alice.to_webfinger_s, format: :json)
+
+        expect(response).to have_http_status(200)
+        expect(response.media_type).to eq 'application/jrd+json'
+      end
+
+      it 'returns a json response for json accept header' do
+        headers = { 'HTTP_ACCEPT' => 'application/jrd+json' }
+        get webfinger_url(resource: alice.to_webfinger_s), headers: headers
+
+        expect(response).to have_http_status(200)
+        expect(response.media_type).to eq 'application/jrd+json'
+      end
+    end
+  end
+
+  private
+
+  def get_avatar_link(json)
+    json[:links].find { |link| link[:rel] == 'http://webfinger.net/rel/avatar' }
+  end
+end

From 04121bd02059b8246233d0421349005d75a6b1a1 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Wed, 15 Nov 2023 18:09:31 -0500
Subject: [PATCH 42/63] Disable simplecov `enable_coverage_for_eval` option,
 move to standalone file (#27869)

---
 .simplecov          | 22 ++++++++++++++++++++++
 spec/spec_helper.rb | 25 ++-----------------------
 2 files changed, 24 insertions(+), 23 deletions(-)
 create mode 100644 .simplecov

diff --git a/.simplecov b/.simplecov
new file mode 100644
index 000000000..fbd0207be
--- /dev/null
+++ b/.simplecov
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+if ENV['CI']
+  require 'simplecov-lcov'
+  SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
+  SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
+else
+  SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter
+end
+
+SimpleCov.start 'rails' do
+  enable_coverage :branch
+
+  add_filter 'lib/linter'
+
+  add_group 'Libraries', 'lib'
+  add_group 'Policies', 'app/policies'
+  add_group 'Presenters', 'app/presenters'
+  add_group 'Serializers', 'app/serializers'
+  add_group 'Services', 'app/services'
+  add_group 'Validators', 'app/validators'
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 0bb4f88cf..dc60976d0 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,28 +1,7 @@
 # frozen_string_literal: true
 
-if ENV['DISABLE_SIMPLECOV'] != 'true'
-  require 'simplecov'
-
-  if ENV['CI']
-    require 'simplecov-lcov'
-    SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
-    SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
-  else
-    SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter
-  end
-  SimpleCov.start 'rails' do
-    enable_coverage :branch
-    enable_coverage_for_eval
-
-    add_filter 'lib/linter'
-    add_group 'Policies', 'app/policies'
-    add_group 'Presenters', 'app/presenters'
-    add_group 'Serializers', 'app/serializers'
-    add_group 'Services', 'app/services'
-    add_group 'Validators', 'app/validators'
-
-    add_group 'Libraries', 'lib'
-  end
+unless ENV['DISABLE_SIMPLECOV'] == 'true'
+  require 'simplecov' # Configuration details loaded from .simplecov
 end
 
 RSpec.configure do |config|

From 91a05f3cad55fa3932e8364db1a87becd2d6f14c Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 16 Nov 2023 10:33:54 +0100
Subject: [PATCH 43/63] Update libretranslate/libretranslate Docker tag to
 v1.5.2 (#27716)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
 .devcontainer/docker-compose.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 137bebc59..40dc72c12 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -70,7 +70,7 @@ services:
         hard: -1
 
   libretranslate:
-    image: libretranslate/libretranslate:v1.4.1
+    image: libretranslate/libretranslate:v1.5.2
     restart: unless-stopped
     volumes:
       - lt-data:/home/libretranslate/.local

From 669a7157cbc2d950988a80244a1f7cdec5aa59f5 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 16 Nov 2023 10:52:23 +0100
Subject: [PATCH 44/63] Update dependency webpack-bundle-analyzer to v4.10.1
 (#27885)

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 e66f58f8b..32c2546e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17014,8 +17014,8 @@ __metadata:
   linkType: hard
 
 "webpack-bundle-analyzer@npm:^4.8.0":
-  version: 4.10.0
-  resolution: "webpack-bundle-analyzer@npm:4.10.0"
+  version: 4.10.1
+  resolution: "webpack-bundle-analyzer@npm:4.10.1"
   dependencies:
     "@discoveryjs/json-ext": "npm:0.5.7"
     acorn: "npm:^8.0.4"
@@ -17032,7 +17032,7 @@ __metadata:
     ws: "npm:^7.3.1"
   bin:
     webpack-bundle-analyzer: lib/bin/analyzer.js
-  checksum: f812a8d3c0198ce518baf742bff656526f3eae69fb7a64c7f0c9cff202f6fb3380cabf3baaae965b8d6ffbbb6fb802eacb373fca03a596a38b01b84cfb2e8329
+  checksum: 6a94c8f6aa03296fb2eb00d6ad3b27bd5c551590fd253772bc61debf3177414d42701014079d4f85c74ba1ca685ae9f0cb4063812b58c21f294d108e9908e5cd
   languageName: node
   linkType: hard
 

From c1f93def403e2bdcf3338118a17802a038090a35 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 16 Nov 2023 04:54:51 -0500
Subject: [PATCH 45/63] Reduce expectations for `RSpec/MultipleExpectations`
 cop in `controllers/statuses` spec (#27875)

---
 spec/controllers/statuses_controller_spec.rb | 221 +++++++++++--------
 1 file changed, 134 insertions(+), 87 deletions(-)

diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb
index 8b715824b..fe40ee6de 100644
--- a/spec/controllers/statuses_controller_spec.rb
+++ b/spec/controllers/statuses_controller_spec.rb
@@ -57,11 +57,14 @@ describe StatusesController do
         let(:format) { 'html' }
 
         it 'renders status successfully', :aggregate_failures do
-          expect(response).to have_http_status(200)
-          expect(response.headers['Link'].to_s).to include 'activity+json'
-          expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-          expect(response.headers['Cache-Control']).to include 'public'
-          expect(response).to render_template(:show)
+          expect(response)
+            .to have_http_status(200)
+            .and render_template(:show)
+          expect(response.headers).to include(
+            'Vary' => 'Accept, Accept-Language, Cookie',
+            'Cache-Control' => include('public'),
+            'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+          )
           expect(response.body).to include status.text
         end
       end
@@ -72,12 +75,15 @@ describe StatusesController do
         it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
 
         it 'renders ActivityPub Note object successfully', :aggregate_failures do
-          expect(response).to have_http_status(200)
-          expect(response.headers['Link'].to_s).to include 'activity+json'
-          expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-          expect(response.headers['Content-Type']).to include 'application/activity+json'
-          json = body_as_json
-          expect(json[:content]).to include status.text
+          expect(response)
+            .to have_http_status(200)
+          expect(response.headers).to include(
+            'Vary' => 'Accept, Accept-Language, Cookie',
+            'Content-Type' => include('application/activity+json'),
+            'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+          )
+          expect(body_as_json)
+            .to include(content: include(status.text))
         end
       end
     end
@@ -157,11 +163,14 @@ describe StatusesController do
           let(:format) { 'html' }
 
           it 'renders status successfully', :aggregate_failures do
-            expect(response).to have_http_status(200)
-            expect(response.headers['Link'].to_s).to include 'activity+json'
-            expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-            expect(response.headers['Cache-Control']).to include 'private'
-            expect(response).to render_template(:show)
+            expect(response)
+              .to have_http_status(200)
+              .and render_template(:show)
+            expect(response.headers).to include(
+              'Vary' => 'Accept, Accept-Language, Cookie',
+              'Cache-Control' => include('private'),
+              'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+            )
             expect(response.body).to include status.text
           end
         end
@@ -170,13 +179,16 @@ describe StatusesController do
           let(:format) { 'json' }
 
           it 'renders ActivityPub Note object successfully', :aggregate_failures do
-            expect(response).to have_http_status(200)
-            expect(response.headers['Link'].to_s).to include 'activity+json'
-            expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-            expect(response.headers['Cache-Control']).to include 'private'
-            expect(response.headers['Content-Type']).to include 'application/activity+json'
-            json = body_as_json
-            expect(json[:content]).to include status.text
+            expect(response)
+              .to have_http_status(200)
+            expect(response.headers).to include(
+              'Vary' => 'Accept, Accept-Language, Cookie',
+              'Cache-Control' => include('private'),
+              'Content-Type' => include('application/activity+json'),
+              'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+            )
+            expect(body_as_json)
+              .to include(content: include(status.text))
           end
         end
       end
@@ -194,11 +206,15 @@ describe StatusesController do
             let(:format) { 'html' }
 
             it 'renders status successfully', :aggregate_failures do
-              expect(response).to have_http_status(200)
-              expect(response.headers['Link'].to_s).to include 'activity+json'
-              expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-              expect(response.headers['Cache-Control']).to include 'private'
-              expect(response).to render_template(:show)
+              expect(response)
+                .to have_http_status(200)
+                .and render_template(:show)
+
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+              )
               expect(response.body).to include status.text
             end
           end
@@ -207,13 +223,16 @@ describe StatusesController do
             let(:format) { 'json' }
 
             it 'renders ActivityPub Note object successfully', :aggregate_failures do
-              expect(response).to have_http_status(200)
-              expect(response.headers['Link'].to_s).to include 'activity+json'
-              expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-              expect(response.headers['Cache-Control']).to include 'private'
-              expect(response.headers['Content-Type']).to include 'application/activity+json'
-              json = body_as_json
-              expect(json[:content]).to include status.text
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Content-Type' => include('application/activity+json'),
+                'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+              )
+              expect(body_as_json)
+                .to include(content: include(status.text))
             end
           end
         end
@@ -254,11 +273,14 @@ describe StatusesController do
             let(:format) { 'html' }
 
             it 'renders status successfully', :aggregate_failures do
-              expect(response).to have_http_status(200)
-              expect(response.headers['Link'].to_s).to include 'activity+json'
-              expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-              expect(response.headers['Cache-Control']).to include 'private'
-              expect(response).to render_template(:show)
+              expect(response)
+                .to have_http_status(200)
+                .and render_template(:show)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+              )
               expect(response.body).to include status.text
             end
           end
@@ -267,13 +289,16 @@ describe StatusesController do
             let(:format) { 'json' }
 
             it 'renders ActivityPub Note object successfully' do
-              expect(response).to have_http_status(200)
-              expect(response.headers['Link'].to_s).to include 'activity+json'
-              expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-              expect(response.headers['Cache-Control']).to include 'private'
-              expect(response.headers['Content-Type']).to include 'application/activity+json'
-              json = body_as_json
-              expect(json[:content]).to include status.text
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Content-Type' => include('application/activity+json'),
+                'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+              )
+              expect(body_as_json)
+                .to include(content: include(status.text))
             end
           end
         end
@@ -340,11 +365,14 @@ describe StatusesController do
           let(:format) { 'html' }
 
           it 'renders status successfully', :aggregate_failures do
-            expect(response).to have_http_status(200)
-            expect(response.headers['Link'].to_s).to include 'activity+json'
-            expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-            expect(response.headers['Cache-Control']).to include 'private'
-            expect(response).to render_template(:show)
+            expect(response)
+              .to have_http_status(200)
+              .and render_template(:show)
+            expect(response.headers).to include(
+              'Vary' => 'Accept, Accept-Language, Cookie',
+              'Cache-Control' => include('private'),
+              'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+            )
             expect(response.body).to include status.text
           end
         end
@@ -355,12 +383,15 @@ describe StatusesController do
           it_behaves_like 'cacheable response', expects_vary: 'Accept, Accept-Language, Cookie'
 
           it 'renders ActivityPub Note object successfully', :aggregate_failures do
-            expect(response).to have_http_status(200)
-            expect(response.headers['Link'].to_s).to include 'activity+json'
-            expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-            expect(response.headers['Content-Type']).to include 'application/activity+json'
-            json = body_as_json
-            expect(json[:content]).to include status.text
+            expect(response)
+              .to have_http_status(200)
+            expect(response.headers).to include(
+              'Vary' => 'Accept, Accept-Language, Cookie',
+              'Content-Type' => include('application/activity+json'),
+              'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+            )
+            expect(body_as_json)
+              .to include(content: include(status.text))
           end
         end
       end
@@ -378,11 +409,14 @@ describe StatusesController do
             let(:format) { 'html' }
 
             it 'renders status successfully', :aggregate_failures do
-              expect(response).to have_http_status(200)
-              expect(response.headers['Link'].to_s).to include 'activity+json'
-              expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-              expect(response.headers['Cache-Control']).to include 'private'
-              expect(response).to render_template(:show)
+              expect(response)
+                .to have_http_status(200)
+                .and render_template(:show)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+              )
               expect(response.body).to include status.text
             end
           end
@@ -391,13 +425,17 @@ describe StatusesController do
             let(:format) { 'json' }
 
             it 'renders ActivityPub Note object successfully' do
-              expect(response).to have_http_status(200)
-              expect(response.headers['Link'].to_s).to include 'activity+json'
-              expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-              expect(response.headers['Cache-Control']).to include 'private'
-              expect(response.headers['Content-Type']).to include 'application/activity+json'
-              json = body_as_json
-              expect(json[:content]).to include status.text
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Content-Type' => include('application/activity+json'),
+                'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+              )
+
+              expect(body_as_json)
+                .to include(content: include(status.text))
             end
           end
         end
@@ -438,11 +476,14 @@ describe StatusesController do
             let(:format) { 'html' }
 
             it 'renders status successfully', :aggregate_failures do
-              expect(response).to have_http_status(200)
-              expect(response.headers['Link'].to_s).to include 'activity+json'
-              expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-              expect(response.headers['Cache-Control']).to include 'private'
-              expect(response).to render_template(:show)
+              expect(response)
+                .to have_http_status(200)
+                .and render_template(:show)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+              )
               expect(response.body).to include status.text
             end
           end
@@ -451,13 +492,16 @@ describe StatusesController do
             let(:format) { 'json' }
 
             it 'renders ActivityPub Note object', :aggregate_failures do
-              expect(response).to have_http_status(200)
-              expect(response.headers['Link'].to_s).to include 'activity+json'
-              expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-              expect(response.headers['Cache-Control']).to include 'private'
-              expect(response.headers['Content-Type']).to include 'application/activity+json'
-              json = body_as_json
-              expect(json[:content]).to include status.text
+              expect(response)
+                .to have_http_status(200)
+              expect(response.headers).to include(
+                'Vary' => 'Accept, Accept-Language, Cookie',
+                'Cache-Control' => include('private'),
+                'Content-Type' => include('application/activity+json'),
+                'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+              )
+              expect(body_as_json)
+                .to include(content: include(status.text))
             end
           end
         end
@@ -732,11 +776,14 @@ describe StatusesController do
       end
 
       it 'renders status successfully', :aggregate_failures do
-        expect(response).to have_http_status(200)
-        expect(response.headers['Link'].to_s).to include 'activity+json'
-        expect(response.headers['Vary']).to eq 'Accept, Accept-Language, Cookie'
-        expect(response.headers['Cache-Control']).to include 'public'
-        expect(response).to render_template(:embed)
+        expect(response)
+          .to have_http_status(200)
+          .and render_template(:embed)
+        expect(response.headers).to include(
+          'Vary' => 'Accept, Accept-Language, Cookie',
+          'Cache-Control' => include('public'),
+          'Link' => satisfy { |header| header.to_s.include?('activity+json') }
+        )
         expect(response.body).to include status.text
       end
     end

From 3f0c1566c34f4114d191e0eb94c0f1477874e091 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 16 Nov 2023 04:55:50 -0500
Subject: [PATCH 46/63] Reduce expectations for `RSpec/MultipleExpectations`
 cop in `api/v1/accounts/relationships` spec (#27879)

---
 .../api/v1/accounts/relationships_spec.rb     | 130 ++++++++++--------
 1 file changed, 76 insertions(+), 54 deletions(-)

diff --git a/spec/requests/api/v1/accounts/relationships_spec.rb b/spec/requests/api/v1/accounts/relationships_spec.rb
index 5011352c6..e06ffdfae 100644
--- a/spec/requests/api/v1/accounts/relationships_spec.rb
+++ b/spec/requests/api/v1/accounts/relationships_spec.rb
@@ -27,12 +27,16 @@ describe 'GET /api/v1/accounts/relationships' do
     it 'returns JSON with correct data', :aggregate_failures do
       subject
 
-      json = body_as_json
-
-      expect(response).to have_http_status(200)
-      expect(json).to be_a Enumerable
-      expect(json.first[:following]).to be true
-      expect(json.first[:followed_by]).to be false
+      expect(response)
+        .to have_http_status(200)
+      expect(body_as_json)
+        .to be_an(Enumerable)
+        .and have_attributes(
+          first: include(
+            following: true,
+            followed_by: false
+          )
+        )
     end
   end
 
@@ -40,18 +44,19 @@ describe 'GET /api/v1/accounts/relationships' do
     let(:params) { { id: [simon.id, lewis.id, bob.id] } }
 
     context 'when there is returned JSON data' do
-      let(:json) { body_as_json }
-
       context 'with default parameters' do
         it 'returns an enumerable json with correct elements, excluding suspended accounts', :aggregate_failures do
           subject
 
-          expect(response).to have_http_status(200)
-          expect(json).to be_a Enumerable
-          expect(json.size).to eq 2
-
-          expect_simon_item_one
-          expect_lewis_item_two
+          expect(response)
+            .to have_http_status(200)
+          expect(body_as_json)
+            .to be_an(Enumerable)
+            .and have_attributes(
+              size: 2,
+              first: include(simon_item),
+              second: include(lewis_item)
+            )
         end
       end
 
@@ -61,62 +66,75 @@ describe 'GET /api/v1/accounts/relationships' do
         it 'returns an enumerable json with correct elements, including suspended accounts', :aggregate_failures do
           subject
 
-          expect(response).to have_http_status(200)
-          expect(json).to be_a Enumerable
-          expect(json.size).to eq 3
-
-          expect_simon_item_one
-          expect_lewis_item_two
-          expect_bob_item_three
+          expect(response)
+            .to have_http_status(200)
+          expect(body_as_json)
+            .to be_an(Enumerable)
+            .and have_attributes(
+              size: 3,
+              first: include(simon_item),
+              second: include(lewis_item),
+              third: include(bob_item)
+            )
         end
       end
 
-      def expect_simon_item_one
-        expect(json.first[:id]).to eq simon.id.to_s
-        expect(json.first[:following]).to be true
-        expect(json.first[:showing_reblogs]).to be true
-        expect(json.first[:followed_by]).to be false
-        expect(json.first[:muting]).to be false
-        expect(json.first[:requested]).to be false
-        expect(json.first[:domain_blocking]).to be false
+      def simon_item
+        {
+          id: simon.id.to_s,
+          following: true,
+          showing_reblogs: true,
+          followed_by: false,
+          muting: false,
+          requested: false,
+          domain_blocking: false,
+        }
       end
 
-      def expect_lewis_item_two
-        expect(json.second[:id]).to eq lewis.id.to_s
-        expect(json.second[:following]).to be false
-        expect(json.second[:showing_reblogs]).to be false
-        expect(json.second[:followed_by]).to be true
-        expect(json.second[:muting]).to be false
-        expect(json.second[:requested]).to be false
-        expect(json.second[:domain_blocking]).to be false
+      def lewis_item
+        {
+          id: lewis.id.to_s,
+          following: false,
+          showing_reblogs: false,
+          followed_by: true,
+          muting: false,
+          requested: false,
+          domain_blocking: false,
+
+        }
       end
 
-      def expect_bob_item_three
-        expect(json.third[:id]).to eq bob.id.to_s
-        expect(json.third[:following]).to be false
-        expect(json.third[:showing_reblogs]).to be false
-        expect(json.third[:followed_by]).to be false
-        expect(json.third[:muting]).to be false
-        expect(json.third[:requested]).to be false
-        expect(json.third[:domain_blocking]).to be false
+      def bob_item
+        {
+          id: bob.id.to_s,
+          following: false,
+          showing_reblogs: false,
+          followed_by: false,
+          muting: false,
+          requested: false,
+          domain_blocking: false,
+
+        }
       end
     end
 
     it 'returns JSON with correct data on previously cached requests' do
       # Initial request including multiple accounts in params
       get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id, lewis.id] }
-      expect(body_as_json.size).to eq(2)
+      expect(body_as_json)
+        .to have_attributes(size: 2)
 
       # Subsequent request with different id, should override cache from first request
       get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id] }
 
-      expect(response).to have_http_status(200)
+      expect(response)
+        .to have_http_status(200)
 
       expect(body_as_json)
         .to be_an(Enumerable)
         .and have_attributes(
           size: 1,
-          first: hash_including(
+          first: include(
             following: true,
             showing_reblogs: true
           )
@@ -129,13 +147,17 @@ describe 'GET /api/v1/accounts/relationships' do
 
       get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id] }
 
-      expect(response).to have_http_status(200)
+      expect(response)
+        .to have_http_status(200)
 
-      json = body_as_json
-
-      expect(json).to be_a Enumerable
-      expect(json.first[:following]).to be false
-      expect(json.first[:showing_reblogs]).to be false
+      expect(body_as_json)
+        .to be_an(Enumerable)
+        .and have_attributes(
+          first: include(
+            following: false,
+            showing_reblogs: false
+          )
+        )
     end
   end
 end

From 8a285413f71eac89d87ab64c985cd9f33aabdaaa Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 16 Nov 2023 05:03:51 -0500
Subject: [PATCH 47/63] Reduce expectations for `RSpec/MultipleExpectations`
 cop in `MoveWorker` spec (#27880)

---
 spec/workers/move_worker_spec.rb | 36 +++++++++++++++++++++++---------
 1 file changed, 26 insertions(+), 10 deletions(-)

diff --git a/spec/workers/move_worker_spec.rb b/spec/workers/move_worker_spec.rb
index efad92c04..774296fda 100644
--- a/spec/workers/move_worker_spec.rb
+++ b/spec/workers/move_worker_spec.rb
@@ -35,17 +35,16 @@ describe MoveWorker do
     context 'when user notes are short enough' do
       it 'copies user note with prelude' do
         subject.perform(source_account.id, target_account.id)
-        expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(source_account.acct)
-        expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(account_note.comment)
+        expect(relevant_account_note.comment)
+          .to include(source_account.acct, account_note.comment)
       end
 
       it 'merges user notes when needed' do
         new_account_note = AccountNote.create!(account: account_note.account, target_account: target_account, comment: 'new note prior to move')
 
         subject.perform(source_account.id, target_account.id)
-        expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(source_account.acct)
-        expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(account_note.comment)
-        expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(new_account_note.comment)
+        expect(relevant_account_note.comment)
+          .to include(source_account.acct, account_note.comment, new_account_note.comment)
       end
     end
 
@@ -54,16 +53,24 @@ describe MoveWorker do
 
       it 'copies user note without prelude' do
         subject.perform(source_account.id, target_account.id)
-        expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(account_note.comment)
+        expect(relevant_account_note.comment)
+          .to include(account_note.comment)
       end
 
       it 'keeps user notes unchanged' do
         new_account_note = AccountNote.create!(account: account_note.account, target_account: target_account, comment: 'new note prior to move')
 
         subject.perform(source_account.id, target_account.id)
-        expect(AccountNote.find_by(account: account_note.account, target_account: target_account).comment).to include(new_account_note.comment)
+        expect(relevant_account_note.comment)
+          .to include(new_account_note.comment)
       end
     end
+
+    private
+
+    def relevant_account_note
+      AccountNote.find_by(account: account_note.account, target_account: target_account)
+    end
   end
 
   shared_examples 'block and mute handling' do
@@ -71,10 +78,19 @@ describe MoveWorker do
       subject.perform(source_account.id, target_account.id)
 
       expect(block_service).to have_received(:call).with(blocking_account, target_account)
-      expect(AccountNote.find_by(account: blocking_account, target_account: target_account).comment).to include(source_account.acct)
-
       expect(muting_account.muting?(target_account)).to be true
-      expect(AccountNote.find_by(account: muting_account, target_account: target_account).comment).to include(source_account.acct)
+
+      expect(
+        [note_account_comment, mute_account_comment]
+      ).to all include(source_account.acct)
+    end
+
+    def note_account_comment
+      AccountNote.find_by(account: blocking_account, target_account: target_account).comment
+    end
+
+    def mute_account_comment
+      AccountNote.find_by(account: muting_account, target_account: target_account).comment
     end
   end
 

From 7232d4750dedcde879bde81889fbd028897e15b0 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 16 Nov 2023 11:08:22 +0100
Subject: [PATCH 48/63] New Crowdin Translations (automated) (#27884)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/an.json      |  1 -
 app/javascript/mastodon/locales/ar.json      |  1 -
 app/javascript/mastodon/locales/ast.json     |  1 -
 app/javascript/mastodon/locales/be.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/bg.json      | 14 +++++++++++++-
 app/javascript/mastodon/locales/bn.json      |  1 -
 app/javascript/mastodon/locales/br.json      |  1 -
 app/javascript/mastodon/locales/ca.json      |  1 -
 app/javascript/mastodon/locales/ckb.json     |  1 -
 app/javascript/mastodon/locales/co.json      |  1 -
 app/javascript/mastodon/locales/cs.json      |  1 -
 app/javascript/mastodon/locales/cy.json      |  1 -
 app/javascript/mastodon/locales/da.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/de.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/el.json      |  1 -
 app/javascript/mastodon/locales/en-GB.json   | 13 ++++++++++++-
 app/javascript/mastodon/locales/eo.json      |  1 -
 app/javascript/mastodon/locales/es-AR.json   | 13 ++++++++++++-
 app/javascript/mastodon/locales/es-MX.json   | 11 +++++++++++
 app/javascript/mastodon/locales/es.json      |  1 +
 app/javascript/mastodon/locales/et.json      |  1 -
 app/javascript/mastodon/locales/eu.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/fa.json      |  9 ++++++++-
 app/javascript/mastodon/locales/fi.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/fo.json      |  1 -
 app/javascript/mastodon/locales/fr-QC.json   |  1 -
 app/javascript/mastodon/locales/fr.json      |  1 -
 app/javascript/mastodon/locales/fy.json      |  1 -
 app/javascript/mastodon/locales/ga.json      |  1 -
 app/javascript/mastodon/locales/gd.json      |  1 -
 app/javascript/mastodon/locales/gl.json      |  1 -
 app/javascript/mastodon/locales/he.json      | 15 +++++++++++++--
 app/javascript/mastodon/locales/hi.json      |  1 -
 app/javascript/mastodon/locales/hr.json      |  1 -
 app/javascript/mastodon/locales/hu.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/hy.json      |  1 -
 app/javascript/mastodon/locales/id.json      |  1 -
 app/javascript/mastodon/locales/ig.json      |  1 -
 app/javascript/mastodon/locales/io.json      |  1 -
 app/javascript/mastodon/locales/is.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/it.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/ja.json      |  2 +-
 app/javascript/mastodon/locales/ka.json      |  1 -
 app/javascript/mastodon/locales/kab.json     |  1 -
 app/javascript/mastodon/locales/kk.json      |  1 -
 app/javascript/mastodon/locales/ko.json      | 11 +++++++++++
 app/javascript/mastodon/locales/ku.json      |  1 -
 app/javascript/mastodon/locales/kw.json      |  1 -
 app/javascript/mastodon/locales/lt.json      | 12 ++++++++++++
 app/javascript/mastodon/locales/lv.json      |  1 -
 app/javascript/mastodon/locales/ml.json      |  1 -
 app/javascript/mastodon/locales/mr.json      |  1 -
 app/javascript/mastodon/locales/ms.json      |  1 -
 app/javascript/mastodon/locales/my.json      |  1 -
 app/javascript/mastodon/locales/nl.json      | 11 +++++++++++
 app/javascript/mastodon/locales/nn.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/no.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/oc.json      |  1 -
 app/javascript/mastodon/locales/pa.json      |  1 -
 app/javascript/mastodon/locales/pl.json      | 11 +++++++++++
 app/javascript/mastodon/locales/pt-BR.json   |  1 -
 app/javascript/mastodon/locales/pt-PT.json   | 13 ++++++++++++-
 app/javascript/mastodon/locales/ro.json      |  1 -
 app/javascript/mastodon/locales/ru.json      |  1 -
 app/javascript/mastodon/locales/sa.json      |  1 -
 app/javascript/mastodon/locales/sc.json      |  1 -
 app/javascript/mastodon/locales/sco.json     |  1 -
 app/javascript/mastodon/locales/si.json      |  1 -
 app/javascript/mastodon/locales/sk.json      |  1 -
 app/javascript/mastodon/locales/sl.json      |  1 -
 app/javascript/mastodon/locales/sq.json      | 11 +++++++++++
 app/javascript/mastodon/locales/sr-Latn.json |  1 -
 app/javascript/mastodon/locales/sr.json      |  1 -
 app/javascript/mastodon/locales/sv.json      | 10 +++++++++-
 app/javascript/mastodon/locales/ta.json      |  1 -
 app/javascript/mastodon/locales/te.json      |  1 -
 app/javascript/mastodon/locales/th.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/tr.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/tt.json      |  1 -
 app/javascript/mastodon/locales/uk.json      | 13 ++++++++++++-
 app/javascript/mastodon/locales/uz.json      |  1 -
 app/javascript/mastodon/locales/vi.json      |  1 -
 app/javascript/mastodon/locales/zgh.json     |  1 -
 app/javascript/mastodon/locales/zh-CN.json   | 13 ++++++++++++-
 app/javascript/mastodon/locales/zh-HK.json   | 13 ++++++++++++-
 app/javascript/mastodon/locales/zh-TW.json   | 13 ++++++++++++-
 config/locales/be.yml                        |  1 +
 config/locales/bg.yml                        |  1 +
 config/locales/da.yml                        |  1 +
 config/locales/de.yml                        |  1 +
 config/locales/es-AR.yml                     |  1 +
 config/locales/es-MX.yml                     |  1 +
 config/locales/fi.yml                        |  2 ++
 config/locales/he.yml                        | 11 ++++++-----
 config/locales/hu.yml                        |  1 +
 config/locales/is.yml                        |  1 +
 config/locales/it.yml                        |  1 +
 config/locales/ko.yml                        |  1 +
 config/locales/lt.yml                        |  1 +
 config/locales/nl.yml                        |  1 +
 config/locales/nn.yml                        |  1 +
 config/locales/no.yml                        |  1 +
 config/locales/pl.yml                        |  1 +
 config/locales/pt-PT.yml                     |  1 +
 config/locales/simple_form.fi.yml            |  2 +-
 config/locales/simple_form.he.yml            |  6 +++---
 config/locales/simple_form.ko.yml            |  4 ++--
 config/locales/sq.yml                        | 12 +++++++-----
 config/locales/th.yml                        |  1 +
 config/locales/uk.yml                        |  1 +
 config/locales/zh-HK.yml                     |  1 +
 config/locales/zh-TW.yml                     |  5 +++--
 112 files changed, 383 insertions(+), 98 deletions(-)

diff --git a/app/javascript/mastodon/locales/an.json b/app/javascript/mastodon/locales/an.json
index 6243f8d4c..a652272fa 100644
--- a/app/javascript/mastodon/locales/an.json
+++ b/app/javascript/mastodon/locales/an.json
@@ -334,7 +334,6 @@
   "lists.search": "Buscar entre la chent a la quala sigues",
   "lists.subheading": "Las tuyas listas",
   "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
-  "loading_indicator.label": "Cargando...",
   "media_gallery.toggle_visible": "{number, plural, one {Amaga la imachen} other {Amaga las imáchens}}",
   "moved_to_account_banner.text": "La tuya cuenta {disabledAccount} ye actualment deshabilitada perque t'has mudau a {movedToAccount}.",
   "mute_modal.duration": "Duración",
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 4a5e04764..979bc9a70 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -389,7 +389,6 @@
   "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
   "lists.subheading": "قوائمك",
   "load_pending": "{count, plural, one {# عنصر جديد} other {# عناصر جديدة}}",
-  "loading_indicator.label": "جارٍ التحميل…",
   "media_gallery.toggle_visible": "{number, plural, zero {} one {اخف الصورة} two {اخف الصورتين} few {اخف الصور} many {اخف الصور} other {اخف الصور}}",
   "moved_to_account_banner.text": "حسابك {disabledAccount} معطل حاليًا لأنك انتقلت إلى {movedToAccount}.",
   "mute_modal.duration": "المدة",
diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json
index fed324e2d..0d49d9a19 100644
--- a/app/javascript/mastodon/locales/ast.json
+++ b/app/javascript/mastodon/locales/ast.json
@@ -272,7 +272,6 @@
   "lists.search": "Buscar ente los perfiles que sigues",
   "lists.subheading": "Les tos llistes",
   "load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
-  "loading_indicator.label": "Cargando…",
   "media_gallery.toggle_visible": "{number, plural, one {Anubrir la imaxe} other {Anubrir les imáxenes}}",
   "mute_modal.duration": "Duración",
   "mute_modal.hide_notifications": "¿Quies anubrir los avisos d'esti perfil?",
diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json
index e8a52ee29..4f5b247b6 100644
--- a/app/javascript/mastodon/locales/be.json
+++ b/app/javascript/mastodon/locales/be.json
@@ -390,7 +390,7 @@
   "lists.search": "Шукайце сярод людзей, на якіх Вы падпісаны",
   "lists.subheading": "Вашыя спісы",
   "load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}",
-  "loading_indicator.label": "Загрузка...",
+  "loading_indicator.label": "Загрузка…",
   "media_gallery.toggle_visible": "{number, plural, one {Схаваць відарыс} other {Схаваць відарысы}}",
   "moved_to_account_banner.text": "Ваш уліковы запіс {disabledAccount} зараз адключаны таму што вы перанесены на {movedToAccount}.",
   "mute_modal.duration": "Працягласць",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "На жаль, зараз немагчыма паказаць вынікі. Вы можаце паспрабаваць выкарыстоўваць пошук і праглядзець старонку агляду, каб знайсці людзей, на якіх можна падпісацца, або паўтарыце спробу пазней.",
   "onboarding.follows.lead": "Вы самі ствараеце свой хатні канал. Чым больш людзей вы падпішаце, тым больш актыўна і цікавей гэта будзе. Гэтыя профілі могуць стаць добрай адпраўной кропкай — вы заўсёды можаце адмяніць падпіску на іх пазней!",
   "onboarding.follows.title": "Папулярна на Mastodon",
+  "onboarding.profile.discoverable": "Уключыць профіль і допісы ў алгарытмы рэкамендацый",
+  "onboarding.profile.display_name": "Бачнае імя",
+  "onboarding.profile.display_name_hint": "Ваша поўнае імя або ваш псеўданім…",
+  "onboarding.profile.indexable": "Індэксаваць публічныя допісы ў пошукавых сістэмах",
+  "onboarding.profile.lead": "Вы заўсёды можаце выканаць гэта пазней у Наладах, дзе даступна яшчэ больш параметраў.",
+  "onboarding.profile.note": "Біяграфія",
+  "onboarding.profile.note_hint": "Вы можаце @згадаць іншых людзей або выкарыстоўваць #хэштэгі…",
+  "onboarding.profile.save_and_continue": "Захаваць і працягнуць",
+  "onboarding.profile.title": "Налады профілю",
+  "onboarding.profile.upload_avatar": "Загрузіць фота профілю",
+  "onboarding.profile.upload_header": "Загрузіць шапку профілю",
   "onboarding.share.lead": "Дайце людзям ведаць, як яны могуць знайсці вас на Mastodon!",
   "onboarding.share.message": "Я {username} на #Mastodon! Сачыце за мной на {url}",
   "onboarding.share.next_steps": "Магчымыя наступныя крокі:",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 83bffd946..b30dfecaa 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -222,6 +222,7 @@
   "emoji_button.search_results": "Резултати от търсене",
   "emoji_button.symbols": "Символи",
   "emoji_button.travel": "Пътуване и места",
+  "empty_column.account_hides_collections": "Този потребител е избрал да не прави това сведение достъпно",
   "empty_column.account_suspended": "Спрян акаунт",
   "empty_column.account_timeline": "Тук няма публикации!",
   "empty_column.account_unavailable": "Профилът не е наличен",
@@ -389,7 +390,7 @@
   "lists.search": "Търсене измежду последваните",
   "lists.subheading": "Вашите списъци",
   "load_pending": "{count, plural, one {# нов елемент} other {# нови елемента}}",
-  "loading_indicator.label": "Зареждане...",
+  "loading_indicator.label": "Зареждане…",
   "media_gallery.toggle_visible": "Скриване на {number, plural, one {изображение} other {изображения}}",
   "moved_to_account_banner.text": "Вашият акаунт {disabledAccount} сега е изключен, защото се преместихте в {movedToAccount}.",
   "mute_modal.duration": "Времетраене",
@@ -478,6 +479,17 @@
   "onboarding.follows.empty": "За съжаление, в момента не могат да се показват резултати. Може да опитате да употребявате търсене или да прегледате страницата за изследване, за да намерите страница за последване, или да опитате пак по-късно.",
   "onboarding.follows.lead": "Може да бъдете куратор на началния си инфоканал. Последвайки повече хора, по-деен и по-интересен ще става. Тези профили може да са добра начална точка, от която винаги по-късно да спрете да следвате!",
   "onboarding.follows.title": "Популярно в Mastodon",
+  "onboarding.profile.discoverable": "Включване на профила и публикации в алгоритмите за откриване",
+  "onboarding.profile.display_name": "Името на показ",
+  "onboarding.profile.display_name_hint": "Вашето пълно име или псевдоним…",
+  "onboarding.profile.indexable": "Включване на обществени публикации в резултатите от търсене",
+  "onboarding.profile.lead": "Винаги може да завършите това по-късно в настройките, където дори има повече възможности за настройване.",
+  "onboarding.profile.note": "Биогр.",
+  "onboarding.profile.note_hint": "Може да @споменавате други хора или #хаштагове…",
+  "onboarding.profile.save_and_continue": "Запазване и продължаване",
+  "onboarding.profile.title": "Настройване на профила",
+  "onboarding.profile.upload_avatar": "Качване на снимка на профила",
+  "onboarding.profile.upload_header": "Качване на заглавка на профила",
   "onboarding.share.lead": "Позволете на хората да знаят, че могат да ви намерят в Mastodon!",
   "onboarding.share.message": "Аз съм {username} в #Mastodon! Елате да ме последвате при {url}",
   "onboarding.share.next_steps": "Възможни следващи стъпки:",
diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json
index 85d6f2474..b6e4fbb96 100644
--- a/app/javascript/mastodon/locales/bn.json
+++ b/app/javascript/mastodon/locales/bn.json
@@ -314,7 +314,6 @@
   "lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
   "lists.subheading": "আপনার তালিকা",
   "load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}",
-  "loading_indicator.label": "আসছে...",
   "media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান",
   "mute_modal.duration": "সময়কাল",
   "mute_modal.hide_notifications": "এই ব্যবহারকারীর প্রজ্ঞাপন বন্ধ করবেন ?",
diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json
index 8449762c2..39cd73241 100644
--- a/app/javascript/mastodon/locales/br.json
+++ b/app/javascript/mastodon/locales/br.json
@@ -329,7 +329,6 @@
   "lists.search": "Klask e-touez tud heuliet ganeoc'h",
   "lists.subheading": "Ho listennoù",
   "load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}",
-  "loading_indicator.label": "O kargañ...",
   "media_gallery.toggle_visible": "{number, plural, one {Kuzhat ar skeudenn} other {Kuzhat ar skeudenn}}",
   "mute_modal.duration": "Padelezh",
   "mute_modal.hide_notifications": "Kuzhat kemenadennoù eus an implijer-se ?",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 99cae584b..926343f67 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -390,7 +390,6 @@
   "lists.search": "Cerca entre les persones que segueixes",
   "lists.subheading": "Les teves llistes",
   "load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
-  "loading_indicator.label": "Es carrega...",
   "media_gallery.toggle_visible": "{number, plural, one {Amaga la imatge} other {Amaga les imatges}}",
   "moved_to_account_banner.text": "El teu compte {disabledAccount} està desactivat perquè l'has mogut a {movedToAccount}.",
   "mute_modal.duration": "Durada",
diff --git a/app/javascript/mastodon/locales/ckb.json b/app/javascript/mastodon/locales/ckb.json
index 381eaa5b1..7e9641832 100644
--- a/app/javascript/mastodon/locales/ckb.json
+++ b/app/javascript/mastodon/locales/ckb.json
@@ -342,7 +342,6 @@
   "lists.search": "بگەڕێ لەناو ئەو کەسانەی کە شوێنیان کەوتویت",
   "lists.subheading": "لیستەکانت",
   "load_pending": "{count, plural, one {# بەڕگەی نوێ} other {# بەڕگەی نوێ}}",
-  "loading_indicator.label": "بارکردن...",
   "media_gallery.toggle_visible": "شاردنەوەی {number, plural, one {image} other {images}}",
   "moved_to_account_banner.text": "ئەکاونتەکەت {disabledAccount} لە ئێستادا لەکارخراوە چونکە تۆ چوویتە {movedToAccount}.",
   "mute_modal.duration": "ماوە",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index 1d878a0cd..d4bb2f82b 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -236,7 +236,6 @@
   "lists.search": "Circà indè i vostr'abbunamenti",
   "lists.subheading": "E vo liste",
   "load_pending": "{count, plural, one {# entrata nova} other {# entrate nove}}",
-  "loading_indicator.label": "Caricamentu...",
   "media_gallery.toggle_visible": "Piattà {number, plural, one {ritrattu} other {ritratti}}",
   "mute_modal.duration": "Durata",
   "mute_modal.hide_notifications": "Piattà nutificazione da st'utilizatore?",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index aaf50d67c..20ef43733 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -386,7 +386,6 @@
   "lists.search": "Hledejte mezi lidmi, které sledujete",
   "lists.subheading": "Vaše seznamy",
   "load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
-  "loading_indicator.label": "Načítání...",
   "media_gallery.toggle_visible": "{number, plural, one {Skrýt obrázek} few {Skrýt obrázky} many {Skrýt obrázky} other {Skrýt obrázky}}",
   "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.",
   "mute_modal.duration": "Trvání",
diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index 183ab6955..dcd5406d0 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -390,7 +390,6 @@
   "lists.search": "Chwilio ymysg pobl rydych yn eu dilyn",
   "lists.subheading": "Eich rhestrau",
   "load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
-  "loading_indicator.label": "Llwytho...",
   "media_gallery.toggle_visible": "{number, plural, one {Cuddio delwedd} other {Cuddio delwedd}}",
   "moved_to_account_banner.text": "Ar hyn y bryd, mae eich cyfrif {disabledAccount} wedi ei analluogi am i chi symud i {movedToAccount}.",
   "mute_modal.duration": "Hyd",
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index bb8b72bca..33eda6f43 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -390,7 +390,7 @@
   "lists.search": "Søg blandt personer, som følges",
   "lists.subheading": "Dine lister",
   "load_pending": "{count, plural, one {# nyt emne} other {# nye emner}}",
-  "loading_indicator.label": "Indlæser...",
+  "loading_indicator.label": "Indlæser…",
   "media_gallery.toggle_visible": "{number, plural, one {Skjul billede} other {Skjul billeder}}",
   "moved_to_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret, da du flyttede til {movedToAccount}.",
   "mute_modal.duration": "Varighed",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Ingen resultater tilgængelige pt. Prøv at bruge søgning eller gennemse siden for at finde personer at følge, eller forsøg igen senere.",
   "onboarding.follows.lead": "Man kurerer sin eget hjemme-feed. Jo flere personer man følger, des mere aktiv og interessant vil det være. Disse profiler kan være et godt udgangspunkt – de kan altid fjernes senere!",
   "onboarding.follows.title": "Populært på Mastodon",
+  "onboarding.profile.discoverable": "Fremhæv profil og indlæg i detekteringsalgoritmer",
+  "onboarding.profile.display_name": "Visningsnavn",
+  "onboarding.profile.display_name_hint": "Fulde navn eller dit sjove navn…",
+  "onboarding.profile.indexable": "Medtag offentlige indlæg i søgeresultater",
+  "onboarding.profile.lead": "Dette kan altid færdiggøres senere i indstillingerne, hvor endnu flere tilpasningsmuligheder forefindes.",
+  "onboarding.profile.note": "Bio",
+  "onboarding.profile.note_hint": "Man kan @omtale andre personer eller #hashtags…",
+  "onboarding.profile.save_and_continue": "Gem og fortsæt",
+  "onboarding.profile.title": "Profilopsætning",
+  "onboarding.profile.upload_avatar": "Upload profilbillede",
+  "onboarding.profile.upload_header": "Upload profiloverskrift",
   "onboarding.share.lead": "Lad folk vide, hvordan de kan finde dig på Mastodon!",
   "onboarding.share.message": "Jeg er {username} på #Mastodon! Følg mig på {url}",
   "onboarding.share.next_steps": "Mulige næste trin:",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index e4f7fe6ce..5c1ccf46d 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -390,7 +390,7 @@
   "lists.search": "Suche nach Leuten, denen du folgst",
   "lists.subheading": "Deine Listen",
   "load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
-  "loading_indicator.label": "Wird geladen …",
+  "loading_indicator.label": "Wird geladen …",
   "media_gallery.toggle_visible": "{number, plural, one {Medium ausblenden} other {Medien ausblenden}}",
   "moved_to_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert, weil du zu {movedToAccount} umgezogen bist.",
   "mute_modal.duration": "Dauer",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Bedauerlicherweise können aktuell keine Ergebnisse angezeigt werden. Du kannst die Suche verwenden oder den Reiter „Entdecken“ auswählen, um neue Leute zum Folgen zu finden – oder du versuchst es später erneut.",
   "onboarding.follows.lead": "Deine Startseite ist der primäre Anlaufpunkt, um Mastodon zu erleben. Je mehr Profilen du folgst, umso aktiver und interessanter wird sie. Damit du direkt loslegen kannst, gibt es hier ein paar Vorschläge:",
   "onboarding.follows.title": "Personalisiere deine Startseite",
+  "onboarding.profile.discoverable": "Profil und Beiträge in Suchalgorithmen berücksichtigen",
+  "onboarding.profile.display_name": "Anzeigename",
+  "onboarding.profile.display_name_hint": "Dein richtiger Name oder dein Fantasiename …",
+  "onboarding.profile.indexable": "Öffentliche Beiträge in die Suchergebnisse einbeziehen",
+  "onboarding.profile.lead": "Du kannst das später in den Einstellungen vervollständigen, wo noch mehr Anpassungsmöglichkeiten zur Verfügung stehen.",
+  "onboarding.profile.note": "Über mich",
+  "onboarding.profile.note_hint": "Du kannst andere @Profile erwähnen oder #Hashtags verwenden …",
+  "onboarding.profile.save_and_continue": "Speichern und fortsetzen",
+  "onboarding.profile.title": "Profil einrichten",
+  "onboarding.profile.upload_avatar": "Profilbild hochladen",
+  "onboarding.profile.upload_header": "Titelbild hochladen",
   "onboarding.share.lead": "Lass die Leute wissen, wie sie dich auf Mastodon finden können!",
   "onboarding.share.message": "Ich bin {username} auf #Mastodon! Folge mir auf {url}",
   "onboarding.share.next_steps": "Mögliche nächste Schritte:",
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index d3be386b4..31806bcfb 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -345,7 +345,6 @@
   "lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς",
   "lists.subheading": "Οι λίστες σου",
   "load_pending": "{count, plural, one {# νέο στοιχείο} other {# νέα στοιχεία}}",
-  "loading_indicator.label": "Φορτώνει...",
   "media_gallery.toggle_visible": "{number, plural, one {Απόκρυψη εικόνας} other {Απόκρυψη εικόνων}}",
   "moved_to_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προσωρινά απενεργοποιημένος επειδή μεταφέρθηκες στον {movedToAccount}.",
   "mute_modal.duration": "Διάρκεια",
diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json
index 20ed8937b..d00782592 100644
--- a/app/javascript/mastodon/locales/en-GB.json
+++ b/app/javascript/mastodon/locales/en-GB.json
@@ -389,7 +389,7 @@
   "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
-  "loading_indicator.label": "Loading...",
+  "loading_indicator.label": "Loading…",
   "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
   "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
   "mute_modal.duration": "Duration",
@@ -478,6 +478,17 @@
   "onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
   "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
   "onboarding.follows.title": "Personalize your home feed",
+  "onboarding.profile.discoverable": "Feature profile and posts in discovery algorithms",
+  "onboarding.profile.display_name": "Display name",
+  "onboarding.profile.display_name_hint": "Your full name or your fun name…",
+  "onboarding.profile.indexable": "Include public posts in search results",
+  "onboarding.profile.lead": "You can always complete this later in the settings, where even more customisation options are available.",
+  "onboarding.profile.note": "Bio",
+  "onboarding.profile.note_hint": "You can @mention other people or #hashtags…",
+  "onboarding.profile.save_and_continue": "Save and continue",
+  "onboarding.profile.title": "Profile setup",
+  "onboarding.profile.upload_avatar": "Upload profile picture",
+  "onboarding.profile.upload_header": "Upload profile header",
   "onboarding.share.lead": "Let people know how they can find you on Mastodon!",
   "onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}",
   "onboarding.share.next_steps": "Possible next steps:",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 6b4258a6b..5679d5e41 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -365,7 +365,6 @@
   "lists.search": "Serĉi inter la homoj, kiujn vi sekvas",
   "lists.subheading": "Viaj listoj",
   "load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
-  "loading_indicator.label": "Ŝargado…",
   "media_gallery.toggle_visible": "{number, plural, one {Kaŝi la bildon} other {Kaŝi la bildojn}}",
   "moved_to_account_banner.text": "Via konto {disabledAccount} estas malvalidigita ĉar vi movis ĝin al {movedToAccount}.",
   "mute_modal.duration": "Daŭro",
diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json
index 71d6e3119..680336369 100644
--- a/app/javascript/mastodon/locales/es-AR.json
+++ b/app/javascript/mastodon/locales/es-AR.json
@@ -390,7 +390,7 @@
   "lists.search": "Buscar entre la gente que seguís",
   "lists.subheading": "Tus listas",
   "load_pending": "{count, plural, one {# elemento nuevo} other {# elementos nuevos}}",
-  "loading_indicator.label": "Cargando...",
+  "loading_indicator.label": "Cargando…",
   "media_gallery.toggle_visible": "Ocultar {number, plural, one {imagen} other {imágenes}}",
   "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.",
   "mute_modal.duration": "Duración",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Podés intentar usar la búsqueda o navegar por la página de exploración para encontrar cuentas a las que seguir, o intentarlo de nuevo más tarde.",
   "onboarding.follows.lead": "Tu línea temporal de inicio es la forma principal de experimentar Mastodon. Cuanta más cuentas sigás, más activa e interesante será. Para empezar, acá tenés algunas sugerencias:",
   "onboarding.follows.title": "Personalizá tu línea de tiempo principal",
+  "onboarding.profile.discoverable": "Destacar perfil y mensajes en algoritmos de descubrimiento",
+  "onboarding.profile.display_name": "Nombre para mostrar",
+  "onboarding.profile.display_name_hint": "Tu nombre completo o tu pseudónimo…",
+  "onboarding.profile.indexable": "Incluir mensajes públicos en resultados de búsqueda",
+  "onboarding.profile.lead": "Siempre podés completar esto más tarde en la configuración, donde hay disponibles más opciones de personalización.",
+  "onboarding.profile.note": "Biografía",
+  "onboarding.profile.note_hint": "Podés @mencionar otras cuentas o usar #etiquetas…",
+  "onboarding.profile.save_and_continue": "Guardar y continuar",
+  "onboarding.profile.title": "Configuración del perfil",
+  "onboarding.profile.upload_avatar": "Subir avatar",
+  "onboarding.profile.upload_header": "Subir cabecera",
   "onboarding.share.lead": "¡Decile a la gente cómo te pueden encontrar en Mastodon!",
   "onboarding.share.message": "¡En #Mastodon soy «{username}»! Podés seguirme desde {url}",
   "onboarding.share.next_steps": "Posibles próximos pasos:",
diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json
index aadc901f9..aa8a21edb 100644
--- a/app/javascript/mastodon/locales/es-MX.json
+++ b/app/javascript/mastodon/locales/es-MX.json
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Puedes intentar usar la búsqueda o navegar por la página de exploración para encontrar gente a la que seguir, o inténtalo de nuevo más tarde.",
   "onboarding.follows.lead": "Tienes que personalizar tu inicio. Cuantas más personas sigas, más activo e interesante será. Estos perfiles pueden ser un buen punto de partida, ¡pero siempre puedes dejar de seguirlos más adelante!",
   "onboarding.follows.title": "Popular en Mastodon",
+  "onboarding.profile.discoverable": "Destacar el perfil y las publicaciones en el algoritmo de descubrimiento",
+  "onboarding.profile.display_name": "Nombre a mostrar",
+  "onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…",
+  "onboarding.profile.indexable": "Incluir publicaciones públicas en los resultados de búsqueda",
+  "onboarding.profile.lead": "Siempre puedes completar esto más tarde en los ajustes, donde hay aún más opciones de personalización disponibles.",
+  "onboarding.profile.note": "Biografía",
+  "onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #hashtags…",
+  "onboarding.profile.save_and_continue": "Guardar y continuar",
+  "onboarding.profile.title": "Configuración del perfil",
+  "onboarding.profile.upload_avatar": "Subir foto de perfil",
+  "onboarding.profile.upload_header": "Subir foto de cabecera",
   "onboarding.share.lead": "¡Dile a la gente cómo te pueden encontrar en Mastodon!",
   "onboarding.share.message": "¡Soy {username} en #Mastodon! Ven a seguirme en {url}",
   "onboarding.share.next_steps": "Posibles siguientes pasos:",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index f3735d968..5d1aa004b 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -479,6 +479,7 @@
   "onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Puedes intentar usar la búsqueda o navegar por la página de exploración para encontrar personas a las que seguir, o inténtalo de nuevo más tarde.",
   "onboarding.follows.lead": "Tu línea de inicio es la forma principal de experimentar Mastodon. Cuanta más personas sigas, más activa e interesante será. Para empezar, aquí hay algunas sugerencias:",
   "onboarding.follows.title": "Personaliza tu línea de inicio",
+  "onboarding.profile.display_name": "Nombre para mostrar",
   "onboarding.share.lead": "¡Cuéntale a otras personas cómo te pueden encontrar en Mastodon!",
   "onboarding.share.message": "¡Soy {username} en #Mastodon! Ven a seguirme en {url}",
   "onboarding.share.next_steps": "Posibles siguientes pasos:",
diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json
index c4182a073..3b6d86a9d 100644
--- a/app/javascript/mastodon/locales/et.json
+++ b/app/javascript/mastodon/locales/et.json
@@ -390,7 +390,6 @@
   "lists.search": "Otsi enda jälgitavate inimeste hulgast",
   "lists.subheading": "Sinu nimekirjad",
   "load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
-  "loading_indicator.label": "Laeb..",
   "media_gallery.toggle_visible": "{number, plural, one {Varja pilt} other {Varja pildid}}",
   "moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
   "mute_modal.duration": "Kestus",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index 419589aba..88f70cffb 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -390,7 +390,7 @@
   "lists.search": "Bilatu jarraitzen dituzun pertsonen artean",
   "lists.subheading": "Zure zerrendak",
   "load_pending": "{count, plural, one {elementu berri #} other {# elementu berri}}",
-  "loading_indicator.label": "Kargatzen...",
+  "loading_indicator.label": "Kargatzen…",
   "media_gallery.toggle_visible": "Txandakatu ikusgaitasuna",
   "moved_to_account_banner.text": "Zure {disabledAccount} kontua desgaituta dago une honetan, {movedToAccount} kontura aldatu zinelako.",
   "mute_modal.duration": "Iraupena",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Zoritxarrez, ezin da emaitzik erakutsi orain. Bilaketa erabil dezakezu edo Arakatu orrian jendea bilatu jarraitzeko, edo saiatu geroago.",
   "onboarding.follows.lead": "Hasierako orria zuk pertsonalizatzen duzu. Gero eta jende gehiagori jarraitu, orduan eta aktibo eta interesgarriago izango da. Profil hauek egokiak izan daitezke hasteko, beti ere, geroago jarraitzeari utz diezazkiekezu!",
   "onboarding.follows.title": "Mastodonen pil-pilean",
+  "onboarding.profile.discoverable": "Ezagutarazi profila eta bidalketak bilaketa algoritmoetan",
+  "onboarding.profile.display_name": "Bistaratzeko izena",
+  "onboarding.profile.display_name_hint": "Zure izena edo ezizena…",
+  "onboarding.profile.indexable": "Gehitu argitalpen publikoak bilaketa-emaitzetan",
+  "onboarding.profile.lead": "Geroagoago bete daiteke konfigurazioan, non pertsonalizatzeko aukera gehiago dauden.",
+  "onboarding.profile.note": "Biografia",
+  "onboarding.profile.note_hint": "Beste pertsona batzuk @aipa ditzakezu edo #traolak erabili…",
+  "onboarding.profile.save_and_continue": "Gorde eta jarraitu",
+  "onboarding.profile.title": "Profilaren konfigurazioa",
+  "onboarding.profile.upload_avatar": "Igo profilaren irudia",
+  "onboarding.profile.upload_header": "Igo profilaren goiburua",
   "onboarding.share.lead": "Esan jendeari nola aurki zaitzaketen Mastodonen!",
   "onboarding.share.message": "{username} naiz #Mastodon-en! Jarrai nazazu hemen: {url}",
   "onboarding.share.next_steps": "Hurrengo urrats posibleak:",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 246f21899..97dae30d4 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -390,7 +390,7 @@
   "lists.search": "جست‌وجو بین کسانی که پی‌گرفته‌اید",
   "lists.subheading": "سیاهه‌هایتان",
   "load_pending": "{count, plural, one {# مورد جدید} other {# مورد جدید}}",
-  "loading_indicator.label": "بار کردن…",
+  "loading_indicator.label": "در حال بارگذاری…",
   "media_gallery.toggle_visible": "{number, plural, one {نهفتن تصویر} other {نهفتن تصاویر}}",
   "moved_to_account_banner.text": "حسابتان {disabledAccount} اکنون از کار افتاده؛ چرا که به {movedToAccount} منتقل شدید.",
   "mute_modal.duration": "مدت زمان",
@@ -479,6 +479,13 @@
   "onboarding.follows.empty": "متأسفانه هم‌اکنون نتیجه‌ای قابل نمایش نیست. می‌توانید استفاده از جست‌وجو یا مرور صفحهٔ کاوش را برای یافتن افرادی برای پی‌گیری آزموده یا دوباره تلاش کنید.",
   "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
   "onboarding.follows.title": "Popular on Mastodon",
+  "onboarding.profile.discoverable": "معرفی نمایه و فرسته‌ها در الگوریتم‌های کشف",
+  "onboarding.profile.display_name": "نام نمایشی",
+  "onboarding.profile.display_name_hint": "نام کامل یا نام باحالتان…",
+  "onboarding.profile.note": "درباره شما",
+  "onboarding.profile.note_hint": "می‌توانید افراد دیگر را @نام‌بردن یا #برچسب بزنید…",
+  "onboarding.profile.save_and_continue": "ذخیره کن و ادامه بده",
+  "onboarding.profile.title": "تنظیم نمایه",
   "onboarding.share.lead": "بگذارید افراد بدانند چگونه می‌توانند در ماستادون بیابندتان!",
   "onboarding.share.message": "من {username} روی #ماستودون هستم! مرا در {url} پی‌بگیرید",
   "onboarding.share.next_steps": "گام‌های ممکن بعدی:",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 9aa2e7355..849a7f463 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -390,7 +390,7 @@
   "lists.search": "Etsi seuraamistasi henkilöistä",
   "lists.subheading": "Omat listasi",
   "load_pending": "{count, plural, one {# uusi kohde} other {# uutta kohdetta}}",
-  "loading_indicator.label": "Ladataan...",
+  "loading_indicator.label": "Ladataan…",
   "media_gallery.toggle_visible": "{number, plural, one {Piilota kuva} other {Piilota kuvat}}",
   "moved_to_account_banner.text": "Tilisi {disabledAccount} on tällä hetkellä poissa käytöstä, koska teit siirron tiliin {movedToAccount}.",
   "mute_modal.duration": "Kesto",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Valitettavasti tuloksia ei voida näyttää juuri nyt. Voit kokeilla hakua tai selata tutustumissivua löytääksesi seurattavaa tai yrittää myöhemmin uudelleen.",
   "onboarding.follows.lead": "Kokoat oman kotisyötteesi itse. Mitä enemmän ihmisiä seuraat, sitä aktiivisempi ja kiinnostavampi syöte on. Nämä profiilit voivat olla alkuun hyvä lähtökohta — voit aina lopettaa niiden seuraamisen myöhemmin!",
   "onboarding.follows.title": "Mukauta kotisyötettäsi",
+  "onboarding.profile.discoverable": "Pidä profiilia ja julkaisuja esillä löytämisalgoritmeissa",
+  "onboarding.profile.display_name": "Näyttönimi",
+  "onboarding.profile.display_name_hint": "Koko nimesi tai lempinimesi…",
+  "onboarding.profile.indexable": "Sisällytä julkiset julkaisut hakutuloksiin",
+  "onboarding.profile.lead": "Voit viimeistellä tämän milloin tahansa asetuksissa, jossa on saatavilla vielä enemmän mukautusvalintoja.",
+  "onboarding.profile.note": "Elämäkerta",
+  "onboarding.profile.note_hint": "Voit @mainita muita käyttäjiä tai #aihetunnisteita…",
+  "onboarding.profile.save_and_continue": "Tallenna ja jatka",
+  "onboarding.profile.title": "Profiilin määritys",
+  "onboarding.profile.upload_avatar": "Lataa profiilikuva",
+  "onboarding.profile.upload_header": "Lataa profiilin otsakekuva",
   "onboarding.share.lead": "Kerro ihmisille, kuinka he voivat löytää sinut Mastodonista!",
   "onboarding.share.message": "Olen {username} #Mastodon⁠issa! Seuraa minua osoitteessa {url}",
   "onboarding.share.next_steps": "Mahdolliset seuraavat vaiheet:",
diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json
index 42a1317db..61c1287ea 100644
--- a/app/javascript/mastodon/locales/fo.json
+++ b/app/javascript/mastodon/locales/fo.json
@@ -390,7 +390,6 @@
   "lists.search": "Leita millum fólk, sum tú fylgir",
   "lists.subheading": "Tínir listar",
   "load_pending": "{count, plural, one {# nýtt evni} other {# nýggj evni}}",
-  "loading_indicator.label": "Innlesi...",
   "media_gallery.toggle_visible": "{number, plural, one {Fjal mynd} other {Fjal myndir}}",
   "moved_to_account_banner.text": "Konta tín {disabledAccount} er í løtuni óvirkin, tí tú flutti til {movedToAccount}.",
   "mute_modal.duration": "Tíðarbil",
diff --git a/app/javascript/mastodon/locales/fr-QC.json b/app/javascript/mastodon/locales/fr-QC.json
index a6dd91bec..e858882f8 100644
--- a/app/javascript/mastodon/locales/fr-QC.json
+++ b/app/javascript/mastodon/locales/fr-QC.json
@@ -390,7 +390,6 @@
   "lists.search": "Rechercher parmi les gens que vous suivez",
   "lists.subheading": "Vos listes",
   "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}",
-  "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "{number, plural, one {Cacher l’image} other {Cacher les images}}",
   "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous avez déménagé sur {movedToAccount}.",
   "mute_modal.duration": "Durée",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 0d2ba2bb9..a7bb4a12f 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -390,7 +390,6 @@
   "lists.search": "Rechercher parmi les gens que vous suivez",
   "lists.subheading": "Vos listes",
   "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}",
-  "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "{number, plural, one {Cacher l’image} other {Cacher les images}}",
   "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous l'avez déplacé à {movedToAccount}.",
   "mute_modal.duration": "Durée",
diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json
index 9d3b41606..2128a045b 100644
--- a/app/javascript/mastodon/locales/fy.json
+++ b/app/javascript/mastodon/locales/fy.json
@@ -390,7 +390,6 @@
   "lists.search": "Sykje nei minsken dy’t jo folgje",
   "lists.subheading": "Jo listen",
   "load_pending": "{count, plural, one {# nij item} other {# nije items}}",
-  "loading_indicator.label": "Lade…",
   "media_gallery.toggle_visible": "{number, plural, one {ôfbylding ferstopje} other {ôfbyldingen ferstopje}}",
   "moved_to_account_banner.text": "Omdat jo nei {movedToAccount} ferhuze binne is jo account {disabledAccount} op dit stuit útskeakele.",
   "mute_modal.duration": "Doer",
diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json
index 388a557e8..ee6b44c88 100644
--- a/app/javascript/mastodon/locales/ga.json
+++ b/app/javascript/mastodon/locales/ga.json
@@ -316,7 +316,6 @@
   "lists.replies_policy.title": "Taispeáin freagraí:",
   "lists.search": "Cuardaigh i measc daoine atá á leanúint agat",
   "lists.subheading": "Do liostaí",
-  "loading_indicator.label": "Ag lódáil...",
   "mute_modal.duration": "Tréimhse",
   "mute_modal.hide_notifications": "Cuir póstalacha ón t-úsáideoir seo i bhfolach?",
   "mute_modal.indefinite": "Gan téarma",
diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json
index 4f485cfc5..91333c1a0 100644
--- a/app/javascript/mastodon/locales/gd.json
+++ b/app/javascript/mastodon/locales/gd.json
@@ -389,7 +389,6 @@
   "lists.search": "Lorg am measg nan daoine a leanas tu",
   "lists.subheading": "Na liostaichean agad",
   "load_pending": "{count, plural, one {# nì ùr} two {# nì ùr} few {# nithean ùra} other {# nì ùr}}",
-  "loading_indicator.label": "’Ga luchdadh…",
   "media_gallery.toggle_visible": "{number, plural, 1 {Falaich an dealbh} one {Falaich na dealbhan} two {Falaich na dealbhan} few {Falaich na dealbhan} other {Falaich na dealbhan}}",
   "moved_to_account_banner.text": "Tha an cunntas {disabledAccount} agad à comas on a rinn thu imrich gu {movedToAccount}.",
   "mute_modal.duration": "Faide",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index e79e54926..7d2c6dab9 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -390,7 +390,6 @@
   "lists.search": "Procurar entre as persoas que segues",
   "lists.subheading": "As túas listaxes",
   "load_pending": "{count, plural, one {# novo elemento} other {# novos elementos}}",
-  "loading_indicator.label": "Estase a cargar...",
   "media_gallery.toggle_visible": "Agochar {number, plural, one {imaxe} other {imaxes}}",
   "moved_to_account_banner.text": "A túa conta {disabledAccount} está actualmente desactivada porque movéchela a {movedToAccount}.",
   "mute_modal.duration": "Duración",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 485e8313a..2e0009170 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -390,7 +390,7 @@
   "lists.search": "חיפוש בין אנשים שאני עוקב\\ת אחריהם",
   "lists.subheading": "הרשימות שלך",
   "load_pending": "{count, plural, one {# פריט חדש} other {# פריטים חדשים}}",
-  "loading_indicator.label": "טוען...",
+  "loading_indicator.label": "בטעינה…",
   "media_gallery.toggle_visible": "{number, plural, one {להסתיר תמונה} two {להסתיר תמונותיים} many {להסתיר תמונות} other {להסתיר תמונות}}",
   "moved_to_account_banner.text": "חשבונך {disabledAccount} אינו פעיל כרגע עקב מעבר ל{movedToAccount}.",
   "mute_modal.duration": "משך הזמן",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "למצער, תוצאות לחיפושך אינן בנמצא. ניתן להשתמש בחיפוש או בדף החקירות לשם מציאת אנשים ולעקבם. אפשר גם לנסות שוב אחר כך.",
   "onboarding.follows.lead": "אתם אוצרים את הזרם הבייתי שלכם. ככל שתעקבו אחרי יותר אנשים, הוא יהיה עשיר ופעיל יותר. הנה כמה פרופילים להתחיל בהם - תמיד ניתן להפסיק מעקב אחריהם בהמשך!",
   "onboarding.follows.title": "פופולארי על מסטודון",
+  "onboarding.profile.discoverable": "הצגת פרופיל והודעות במסך התגליות",
+  "onboarding.profile.display_name": "שם להצגה",
+  "onboarding.profile.display_name_hint": "שמך המלא או כינוי הכיף שלך…",
+  "onboarding.profile.indexable": "הכללת הודעות ציבוריות בתוצאות החיפוש",
+  "onboarding.profile.lead": "תמיד ניתן להשלים זאת אחר כך בהגדרות, שם יש אפילו עוד אפשרויות להתאמה אישית.",
+  "onboarding.profile.note": "אודות",
+  "onboarding.profile.note_hint": "ניתן @לאזכר משתמשים אחרים או #תגיות…",
+  "onboarding.profile.save_and_continue": "לשמור ולהמשיך",
+  "onboarding.profile.title": "הגדרת פרופיל",
+  "onboarding.profile.upload_avatar": "העלאת תמונת פרופיל",
+  "onboarding.profile.upload_header": "העלאת כותרת פרופיל",
   "onboarding.share.lead": "כדאי להודיע לחברים היכן למצוא אותך במסטודון!",
   "onboarding.share.message": "אני {username} ברשת #מסטודון! בואו לעקוב אחרי בכתובת {url}",
   "onboarding.share.next_steps": "לאיפה להמשיך מכאן:",
@@ -518,7 +529,7 @@
   "privacy.private.short": "לעוקבים בלבד",
   "privacy.public.long": "גלוי לכל",
   "privacy.public.short": "פומבי",
-  "privacy.unlisted.long": "גלוי לכל, אבל מוסתר מאמצעי גילוי",
+  "privacy.unlisted.long": "גלוי לכל, אבל מוסתר מאמצעי תגלית",
   "privacy.unlisted.short": "לא רשום (לא לפיד הכללי)",
   "privacy_policy.last_updated": "עודכן לאחרונה {date}",
   "privacy_policy.title": "מדיניות פרטיות",
diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json
index 71694db76..f4473b716 100644
--- a/app/javascript/mastodon/locales/hi.json
+++ b/app/javascript/mastodon/locales/hi.json
@@ -360,7 +360,6 @@
   "lists.replies_policy.none": "कोई नहीं",
   "lists.replies_policy.title": "इसके जवाब दिखाएं:",
   "lists.subheading": "आपकी सूचियाँ",
-  "loading_indicator.label": "लोड हो रहा है...",
   "mute_modal.duration": "अवधि",
   "mute_modal.hide_notifications": "इस सभ्य की ओरसे आनेवाली सूचनाए शांत करे",
   "mute_modal.indefinite": "अनिश्चितकालीन",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 16d25e784..01524e553 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -306,7 +306,6 @@
   "lists.replies_policy.none": "Nitko",
   "lists.search": "Traži među praćenim ljudima",
   "lists.subheading": "Vaše liste",
-  "loading_indicator.label": "Učitavanje...",
   "media_gallery.toggle_visible": "Sakrij {number, plural, one {sliku} other {slike}}",
   "mute_modal.duration": "Trajanje",
   "mute_modal.hide_notifications": "Sakrij obavijesti ovog korisnika?",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index ca2027161..96fc720a4 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -390,7 +390,7 @@
   "lists.search": "Keresés a követett személyek között",
   "lists.subheading": "Saját listák",
   "load_pending": "{count, plural, one {# új elem} other {# új elem}}",
-  "loading_indicator.label": "Betöltés...",
+  "loading_indicator.label": "Betöltés…",
   "media_gallery.toggle_visible": "{number, plural, one {Kép elrejtése} other {Képek elrejtése}}",
   "moved_to_account_banner.text": "A(z) {disabledAccount} fiókod jelenleg le van tiltva, mert átköltöztél ide: {movedToAccount}.",
   "mute_modal.duration": "Időtartam",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Sajnos jelenleg nem jeleníthető meg eredmény. Kipróbálhatod a keresést vagy böngészheted a felfedező oldalon a követni kívánt személyeket, vagy próbáld meg később.",
   "onboarding.follows.lead": "A saját hírfolyamod az elsődleges tapasztalás a Mastodonon. Minél több embert követsz, annál aktívabb és érdekesebb a dolog. Az induláshoz itt van néhány javaslat:",
   "onboarding.follows.title": "Népszerű a Mastodonon",
+  "onboarding.profile.discoverable": "Profil és bejegyzések szerepeltetése a felfedezési algoritmusokban",
+  "onboarding.profile.display_name": "Megjelenített név",
+  "onboarding.profile.display_name_hint": "Teljes név vagy becenév…",
+  "onboarding.profile.indexable": "Nyilvános bejegyzések is a keresési eredményekben",
+  "onboarding.profile.lead": "Ezt később bármikor elvégezhető a beállításoknál, ahol még több testreszabási lehetőség áll rendelkezésre.",
+  "onboarding.profile.note": "Biográfia",
+  "onboarding.profile.note_hint": "@említhetünk másokat vagy #hashtag elemeket…",
+  "onboarding.profile.save_and_continue": "Mentés és folytatás",
+  "onboarding.profile.title": "Profil beüzemelés",
+  "onboarding.profile.upload_avatar": "Profilkép feltöltése",
+  "onboarding.profile.upload_header": "Profil fejléc feltöltése",
   "onboarding.share.lead": "Tudassuk az emberekkel, hogyan találhatnak meg a Mastodonon!",
   "onboarding.share.message": "{username} vagyok a #Mastodon hálózaton! Kövess itt: {url}.",
   "onboarding.share.next_steps": "Lehetséges következő lépések:",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 776991b01..f2548c7d3 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -314,7 +314,6 @@
   "lists.search": "Փնտրել քո հետեւած մարդկանց մէջ",
   "lists.subheading": "Քո ցանկերը",
   "load_pending": "{count, plural, one {# նոր նիւթ} other {# նոր նիւթ}}",
-  "loading_indicator.label": "Բեռնւում է…",
   "media_gallery.toggle_visible": "Ցուցադրել/թաքցնել",
   "mute_modal.duration": "Տեւողութիւն",
   "mute_modal.hide_notifications": "Թաքցնե՞լ ծանուցումներն այս օգտատիրոջից։",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 76542cc52..8ecf36125 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -353,7 +353,6 @@
   "lists.search": "Cari di antara orang yang Anda ikuti",
   "lists.subheading": "Daftar Anda",
   "load_pending": "{count, plural, other {# item baru}}",
-  "loading_indicator.label": "Tunggu sebentar...",
   "media_gallery.toggle_visible": "Tampil/Sembunyikan",
   "moved_to_account_banner.text": "Akun {disabledAccount} Anda kini dinonaktifkan karena Anda pindah ke {movedToAccount}.",
   "mute_modal.duration": "Durasi",
diff --git a/app/javascript/mastodon/locales/ig.json b/app/javascript/mastodon/locales/ig.json
index 201bebc05..c24d28eea 100644
--- a/app/javascript/mastodon/locales/ig.json
+++ b/app/javascript/mastodon/locales/ig.json
@@ -88,7 +88,6 @@
   "lists.delete": "Hichapụ ndepụta",
   "lists.edit": "Dezie ndepụta",
   "lists.subheading": "Ndepụta gị",
-  "loading_indicator.label": "Na-adọnye...",
   "navigation_bar.about": "Maka",
   "navigation_bar.bookmarks": "Ebenrụtụakā",
   "navigation_bar.domain_blocks": "Hidden domains",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index cfe1b4344..552debdb5 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -383,7 +383,6 @@
   "lists.search": "Trovez inter personi quon vu sequas",
   "lists.subheading": "Vua listi",
   "load_pending": "{count, plural, one {# nova kozo} other {# nova kozi}}",
-  "loading_indicator.label": "Kargante...",
   "media_gallery.toggle_visible": "Chanjar videbleso",
   "moved_to_account_banner.text": "Vua konto {disabledAccount} es nune desaktiva pro ke vu movis a {movedToAccount}.",
   "mute_modal.duration": "Durado",
diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json
index 0b6a8012a..54123ae4c 100644
--- a/app/javascript/mastodon/locales/is.json
+++ b/app/javascript/mastodon/locales/is.json
@@ -390,7 +390,7 @@
   "lists.search": "Leita meðal þeirra sem þú fylgist með",
   "lists.subheading": "Listarnir þínir",
   "load_pending": "{count, plural, one {# nýtt atriði} other {# ný atriði}}",
-  "loading_indicator.label": "Hleð inn...",
+  "loading_indicator.label": "Hleð inn…",
   "media_gallery.toggle_visible": "Víxla sýnileika",
   "moved_to_account_banner.text": "Aðgangurinn þinn {disabledAccount} er óvirkur í augnablikinu vegna þess að þú fluttir þig yfir á {movedToAccount}.",
   "mute_modal.duration": "Lengd",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Því miður er ekki hægt að birta neinar niðurstöður í augnablikinu. Þú getur reynt að nota leitina eða skoðað könnunarsíðuna til að finna fólk til að fylgjast með, nú eða prófað aftur síðar.",
   "onboarding.follows.lead": "Þú ræktar heimastreymið þitt. Því fleira fólki sem þú fylgist með, því virkara og áhugaverðara verður það. Að fylgjast með þessum notendum gæti verið ágætt til að byrja með - þú getur alltaf hætt að fylgjast með þeim síðar!",
   "onboarding.follows.title": "Vinsælt á Mastodon",
+  "onboarding.profile.discoverable": "Hafa notandasnið og færslur með í reikniritum leitar",
+  "onboarding.profile.display_name": "Birtingarnafn",
+  "onboarding.profile.display_name_hint": "Fullt nafn þitt eða eitthvað til gamans…",
+  "onboarding.profile.indexable": "Hafa opinberar færslur með í leitarniðurstöðum",
+  "onboarding.profile.lead": "Þú getur alltaf klárað þetta seinna í stillingunum, þar sem enn fleiri möguleikar bjóðast á sérsníðingum.",
+  "onboarding.profile.note": "Æviágrip",
+  "onboarding.profile.note_hint": "Þú getur @minnst á annað fólk eða #myllumerki…",
+  "onboarding.profile.save_and_continue": "Vista og halda áfram",
+  "onboarding.profile.title": "Uppsetning notandasniðs",
+  "onboarding.profile.upload_avatar": "Sendu inn auðkennismynd",
+  "onboarding.profile.upload_header": "Sendu inn bakgrunnsmynd í haus notandasniðs",
   "onboarding.share.lead": "Láttu fólk vita hvernig það getur fundið þig á Mastodon!",
   "onboarding.share.message": "Ég heiti {username} á #Mastodon! Þú getur fylgst með mér á {url}",
   "onboarding.share.next_steps": "Möguleg næstu skref:",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 8ad791bfe..284d7739c 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -390,7 +390,7 @@
   "lists.search": "Cerca tra le persone che segui",
   "lists.subheading": "Le tue liste",
   "load_pending": "{count, plural, one {# nuovo oggetto} other {# nuovi oggetti}}",
-  "loading_indicator.label": "Caricamento...",
+  "loading_indicator.label": "Caricamento…",
   "media_gallery.toggle_visible": "{number, plural, one {Nascondi immagine} other {Nascondi immagini}}",
   "moved_to_account_banner.text": "Il tuo profilo {disabledAccount} è correntemente disabilitato perché ti sei spostato a {movedToAccount}.",
   "mute_modal.duration": "Durata",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Sfortunatamente, nessun risultato può essere mostrato in questo momento. Puoi provare a utilizzare la ricerca o sfogliare la pagina di esplorazione per trovare persone da seguire, oppure riprova più tardi.",
   "onboarding.follows.lead": "La cronologia della tua home è gestita da te. Più persone segui, più attiva e interessante sarà. Questi profili possono essere un buon punto di partenza; puoi sempre smettere di seguirli in seguito!",
   "onboarding.follows.title": "Popolare su Mastodon",
+  "onboarding.profile.discoverable": "Include il profilo e i post negli algoritmi di scoperta",
+  "onboarding.profile.display_name": "Nome da visualizzare",
+  "onboarding.profile.display_name_hint": "Il tuo nome completo o il tuo nome divertente…",
+  "onboarding.profile.indexable": "Includi i post pubblici nei risultati di ricerca",
+  "onboarding.profile.lead": "Puoi sempre completarlo in un secondo momento nelle impostazioni, dove sono disponibili ancora più opzioni di personalizzazione.",
+  "onboarding.profile.note": "Biografia",
+  "onboarding.profile.note_hint": "Puoi @menzionare altre persone o #hashtags…",
+  "onboarding.profile.save_and_continue": "Salva e continua",
+  "onboarding.profile.title": "Configurazione del profilo",
+  "onboarding.profile.upload_avatar": "Carica l'immagine del profilo",
+  "onboarding.profile.upload_header": "Carica l'intestazione del profilo",
   "onboarding.share.lead": "Fai sapere alle persone come possono trovarti su Mastodon!",
   "onboarding.share.message": "Sono {username} su #Mastodon! Vieni a seguirmi su {url}",
   "onboarding.share.next_steps": "Possibili passaggi successivi:",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 4cd7228c8..f8c85b774 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -390,7 +390,7 @@
   "lists.search": "フォローしている人の中から検索",
   "lists.subheading": "あなたのリスト",
   "load_pending": "{count}件の新着",
-  "loading_indicator.label": "読み込み中...",
+  "loading_indicator.label": "",
   "media_gallery.toggle_visible": "{number, plural, one {画像を閉じる} other {画像を閉じる}}",
   "moved_to_account_banner.text": "あなたのアカウント『{disabledAccount}』は『{movedToAccount}』に移動したため現在無効になっています。",
   "mute_modal.duration": "ミュートする期間",
diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json
index 256aa93f4..875ac3c19 100644
--- a/app/javascript/mastodon/locales/ka.json
+++ b/app/javascript/mastodon/locales/ka.json
@@ -165,7 +165,6 @@
   "lists.new.title_placeholder": "ახალი სიის სათაური",
   "lists.search": "ძებნა ადამიანებს შორის რომელთაც მიჰყვებით",
   "lists.subheading": "თქვენი სიები",
-  "loading_indicator.label": "იტვირთება...",
   "media_gallery.toggle_visible": "ხილვადობის ჩართვა",
   "mute_modal.hide_notifications": "დავმალოთ შეტყობინებები ამ მომხმარებლისგან?",
   "navigation_bar.blocks": "დაბლოკილი მომხმარებლები",
diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json
index 8f9576c26..e9d4b57de 100644
--- a/app/javascript/mastodon/locales/kab.json
+++ b/app/javascript/mastodon/locales/kab.json
@@ -274,7 +274,6 @@
   "lists.search": "Nadi gar yemdanen i teṭṭafaṛeḍ",
   "lists.subheading": "Tibdarin-ik·im",
   "load_pending": "{count, plural, one {# n uferdis amaynut} other {# n yiferdisen imaynuten}}",
-  "loading_indicator.label": "Yessalay-d…",
   "media_gallery.toggle_visible": "Ffer {number, plural, one {tugna} other {tugniwin}}",
   "mute_modal.duration": "Tanzagt",
   "mute_modal.hide_notifications": "Tebɣiḍ ad teffreḍ talɣutin n umseqdac-a?",
diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json
index 6a4b2161b..189d792e3 100644
--- a/app/javascript/mastodon/locales/kk.json
+++ b/app/javascript/mastodon/locales/kk.json
@@ -242,7 +242,6 @@
   "lists.search": "Сіз іздеген адамдар арасында іздеу",
   "lists.subheading": "Тізімдеріңіз",
   "load_pending": "{count, plural, one {# жаңа нәрсе} other {# жаңа нәрсе}}",
-  "loading_indicator.label": "Жүктеу...",
   "media_gallery.toggle_visible": "Көрінуді қосу",
   "mute_modal.hide_notifications": "Бұл қолданушы ескертпелерін жасырамыз ба?",
   "navigation_bar.blocks": "Бұғатталғандар",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 574d8e211..1420be8e0 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "안타깝지만 아직은 아무 것도 보여드릴 수 없습니다. 검색을 이용하거나 발견하기 페이지에서 팔로우 할 사람을 찾을 수 있습니다. 아니면 잠시 후에 다시 시도하세요.",
   "onboarding.follows.lead": "홈 피드는 마스토돈을 경험하는 주된 경로입니다. 더 많은 사람들을 팔로우 할수록 더 활발하고 흥미로워질 것입니다. 여기 시작을 위한 몇몇 추천을 드립니다:",
   "onboarding.follows.title": "내게 맞는 홈 피드 꾸미기",
+  "onboarding.profile.discoverable": "발견하기 알고리즘에 프로필과 게시물을 추천하기",
+  "onboarding.profile.display_name": "표시되는 이름",
+  "onboarding.profile.display_name_hint": "진짜 이름 또는 재미난 이름…",
+  "onboarding.profile.indexable": "공개 게시물을 검색 결과에 포함시키기",
+  "onboarding.profile.lead": "언제든지 나중에 설정 메뉴에서 마저 할 수 있고, 그곳에서 더 많은 맞춤 옵션을 고를 수 있습니다.",
+  "onboarding.profile.note": "자기소개",
+  "onboarding.profile.note_hint": "남을 @mention 하거나 #hashtag 태그를 달 수 있습니다…",
+  "onboarding.profile.save_and_continue": "저장 및 계속",
+  "onboarding.profile.title": "프로필 설정",
+  "onboarding.profile.upload_avatar": "프로필 사진 업로드",
+  "onboarding.profile.upload_header": "프로필 헤더 업로드",
   "onboarding.share.lead": "여러 사람에게 마스토돈에서 나를 찾을 수 있는 방법을 알려주세요!",
   "onboarding.share.message": "#마스토돈 이용하는 {username}입니다! {url} 에서 저를 팔로우 해보세요",
   "onboarding.share.next_steps": "할만한 다음 단계:",
diff --git a/app/javascript/mastodon/locales/ku.json b/app/javascript/mastodon/locales/ku.json
index 8c9aaf3e8..b94054267 100644
--- a/app/javascript/mastodon/locales/ku.json
+++ b/app/javascript/mastodon/locales/ku.json
@@ -339,7 +339,6 @@
   "lists.search": "Di navbera kesên ku te dişopînin bigere",
   "lists.subheading": "Lîsteyên te",
   "load_pending": "{count, plural, one {# hêmaneke nû} other {#hêmaneke nû}}",
-  "loading_indicator.label": "Tê barkirin...",
   "media_gallery.toggle_visible": "{number, plural, one {Wêneyê veşêre} other {Wêneyan veşêre}}",
   "moved_to_account_banner.text": "Ajimêrê te {disabledAccount} niha neçalak e ji ber ku te bar kir bo {movedToAccount}.",
   "mute_modal.duration": "Dem",
diff --git a/app/javascript/mastodon/locales/kw.json b/app/javascript/mastodon/locales/kw.json
index 6b46d8231..ca08ca836 100644
--- a/app/javascript/mastodon/locales/kw.json
+++ b/app/javascript/mastodon/locales/kw.json
@@ -235,7 +235,6 @@
   "lists.search": "Hwilas yn-mysk tus a holyewgh",
   "lists.subheading": "Agas rolyow",
   "load_pending": "{count, plural, one {# daklennowydh} other {# a daklennow nowydh}}",
-  "loading_indicator.label": "Ow karga...",
   "media_gallery.toggle_visible": "Hide {number, plural, one {aven} other {aven}}",
   "mute_modal.duration": "Duryans",
   "mute_modal.hide_notifications": "Kudha gwarnyansow a'n devnydhyer ma?",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index 5675799e7..21aa797b4 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -26,6 +26,7 @@
   "account.domain_blocked": "Užblokuotas domenas",
   "account.edit_profile": "Redaguoti profilį",
   "account.enable_notifications": "Pranešti man, kai @{name} paskelbia",
+  "account.endorse": "Savybė profilyje",
   "account.featured_tags.last_status_at": "Paskutinį kartą paskelbta {date}",
   "account.featured_tags.last_status_never": "Nėra įrašų",
   "account.follow": "Sekti",
@@ -191,6 +192,7 @@
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Uždaryti",
+  "loading_indicator.label": "Kraunama…",
   "media_gallery.toggle_visible": "{number, plural, one {Slėpti vaizdą} few {Slėpti vaizdus} many {Slėpti vaizdo} other {Slėpti vaizdų}}",
   "navigation_bar.compose": "Compose new toot",
   "navigation_bar.domain_blocks": "Hidden domains",
@@ -217,6 +219,16 @@
   "onboarding.actions.go_to_home": "Go to your home feed",
   "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
   "onboarding.follows.title": "Popular on Mastodon",
+  "onboarding.profile.display_name": "Rodomas vardas",
+  "onboarding.profile.display_name_hint": "Tavo pilnas vardas arba linksmas vardas…",
+  "onboarding.profile.indexable": "Įtraukti viešus įrašus į paieškos rezultatus",
+  "onboarding.profile.lead": "Gali visada tai užbaigti vėliau nustatymuose, kur yra dar daugiau pritaikymo parinkčių.",
+  "onboarding.profile.note": "Biografija",
+  "onboarding.profile.note_hint": "Gali @paminėti kitus žmones arba #saitažodžius…",
+  "onboarding.profile.save_and_continue": "Išsaugoti ir tęsti",
+  "onboarding.profile.title": "Profilio konfigūravimas",
+  "onboarding.profile.upload_avatar": "Įkelti profilio nuotrauką",
+  "onboarding.profile.upload_header": "Įkelti profilio antraštę",
   "onboarding.share.message": "Aš {username} #Mastodon! Ateik sekti manęs adresu {url}",
   "onboarding.start.lead": "Dabar esi Mastodon dalis – unikalios decentralizuotos socialinės žiniasklaidos platformos, kurioje tu, o ne algoritmas, pats nustatai savo patirtį. Pradėkime tavo kelionę šioje naujoje socialinėje erdvėje:",
   "onboarding.start.skip": "Want to skip right ahead?",
diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
index 5d681b829..63ec6275b 100644
--- a/app/javascript/mastodon/locales/lv.json
+++ b/app/javascript/mastodon/locales/lv.json
@@ -390,7 +390,6 @@
   "lists.search": "Meklēt starp cilvēkiem, kuriem tu seko",
   "lists.subheading": "Tavi saraksti",
   "load_pending": "{count, plural, one {# jauna lieta} other {# jaunas lietas}}",
-  "loading_indicator.label": "Ielādē...",
   "media_gallery.toggle_visible": "{number, plural, one {Slēpt attēlu} other {Slēpt attēlus}}",
   "moved_to_account_banner.text": "Tavs konts {disabledAccount} pašlaik ir atspējots, jo pārcēlies uz kontu {movedToAccount}.",
   "mute_modal.duration": "Ilgums",
diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json
index 6bf04ce75..b00cedc6f 100644
--- a/app/javascript/mastodon/locales/ml.json
+++ b/app/javascript/mastodon/locales/ml.json
@@ -237,7 +237,6 @@
   "lists.replies_policy.none": "ആരുമില്ല",
   "lists.replies_policy.title": "ഇതിനുള്ള മറുപടികൾ കാണിക്കുക:",
   "lists.subheading": "എന്റെ പട്ടികകൾ",
-  "loading_indicator.label": "ലോഡിംഗ്...",
   "mute_modal.duration": "കാലാവധി",
   "mute_modal.indefinite": "അനിശ്ചിതകാല",
   "navigation_bar.blocks": "തടയപ്പെട്ട ഉപയോക്താക്കൾ",
diff --git a/app/javascript/mastodon/locales/mr.json b/app/javascript/mastodon/locales/mr.json
index ef0efbdec..75b75375b 100644
--- a/app/javascript/mastodon/locales/mr.json
+++ b/app/javascript/mastodon/locales/mr.json
@@ -196,7 +196,6 @@
   "lists.search": "तुम्ही फॉलो करत असलेल्या लोकांमध्ये शोधा",
   "lists.subheading": "तुमच्या याद्या",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
-  "loading_indicator.label": "लोड करत आहे...",
   "navigation_bar.compose": "Compose new toot",
   "navigation_bar.domain_blocks": "Hidden domains",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json
index a3bbd0067..724e07ae7 100644
--- a/app/javascript/mastodon/locales/ms.json
+++ b/app/javascript/mastodon/locales/ms.json
@@ -386,7 +386,6 @@
   "lists.search": "Cari dalam kalangan orang yang anda ikuti",
   "lists.subheading": "Senarai anda",
   "load_pending": "{count, plural, one {# item baharu} other {# item baharu}}",
-  "loading_indicator.label": "Memuatkan...",
   "media_gallery.toggle_visible": "{number, plural, other {Sembunyikan imej}}",
   "moved_to_account_banner.text": "Akaun anda {disabledAccount} kini dinyahdayakan kerana anda berpindah ke {movedToAccount}.",
   "mute_modal.duration": "Tempoh",
diff --git a/app/javascript/mastodon/locales/my.json b/app/javascript/mastodon/locales/my.json
index 103f4e0f8..4078a4c06 100644
--- a/app/javascript/mastodon/locales/my.json
+++ b/app/javascript/mastodon/locales/my.json
@@ -389,7 +389,6 @@
   "lists.search": "မိမိဖောလိုးထားသူများမှရှာဖွေမည်",
   "lists.subheading": "သင့်၏စာရင်းများ",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
-  "loading_indicator.label": "လုပ်ဆောင်နေသည်…",
   "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
   "moved_to_account_banner.text": "{movedToAccount} အကောင့်သို့ပြောင်းလဲထားသဖြင့် {disabledAccount} အကောင့်မှာပိတ်ထားသည်",
   "mute_modal.duration": "ကြာချိန်",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 131d2e4aa..6f941999f 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Helaas kunnen op dit moment geen resultaten worden getoond. Je kunt proberen te zoeken of op de verkenningspagina te bladeren om mensen te vinden die je kunt volgen, of probeer het later opnieuw.",
   "onboarding.follows.lead": "Jouw starttijdlijn is de belangrijkste manier om Mastodon te ervaren. Hoe meer mensen je volgt, hoe actiever en interessanter het zal zijn. Om te beginnen, zijn hier enkele suggesties:",
   "onboarding.follows.title": "Je starttijdlijn aan jouw wensen aanpassen",
+  "onboarding.profile.discoverable": "Profiel en berichten laten uitlichten in ontdekkingsalgoritmes",
+  "onboarding.profile.display_name": "Weergavenaam",
+  "onboarding.profile.display_name_hint": "Jouw volledige naam of een leuke bijnaam…",
+  "onboarding.profile.indexable": "Openbare berichten in zoekresultaten opnemen",
+  "onboarding.profile.lead": "Je kunt dit later altijd aanvullen in de instellingen, waar nog meer aanpassingsopties beschikbaar zijn.",
+  "onboarding.profile.note": "Biografie",
+  "onboarding.profile.note_hint": "Je kunt andere mensen @vermelden of #hashtags gebruiken…",
+  "onboarding.profile.save_and_continue": "Opslaan en doorgaan",
+  "onboarding.profile.title": "Profiel instellen",
+  "onboarding.profile.upload_avatar": "Profielfoto uploaden",
+  "onboarding.profile.upload_header": "Kop voor het profiel uploaden",
   "onboarding.share.lead": "Laat mensen weten hoe ze je kunnen vinden op Mastodon!",
   "onboarding.share.message": "Ik ben {username} op #Mastodon! Volg mij op {url}",
   "onboarding.share.next_steps": "Mogelijke volgende stappen:",
diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json
index aa30aef7d..a3402d660 100644
--- a/app/javascript/mastodon/locales/nn.json
+++ b/app/javascript/mastodon/locales/nn.json
@@ -390,7 +390,7 @@
   "lists.search": "Søk blant folk du fylgjer",
   "lists.subheading": "Listene dine",
   "load_pending": "{count, plural, one {# nytt element} other {# nye element}}",
-  "loading_indicator.label": "Lastar...",
+  "loading_indicator.label": "Laster…",
   "media_gallery.toggle_visible": "{number, plural, one {Skjul bilete} other {Skjul bilete}}",
   "moved_to_account_banner.text": "Kontoen din, {disabledAccount} er for tida deaktivert fordi du har flytta til {movedToAccount}.",
   "mute_modal.duration": "Varigheit",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Me kan ikkje visa deg nokon resultat no. Du kan prøva å søkja eller bla gjennom utforsk-sida for å finna folk å fylgja, eller du kan prøva att seinare.",
   "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
   "onboarding.follows.title": "Popular on Mastodon",
+  "onboarding.profile.discoverable": "Fremhevede profiler og innlegg i oppdagelsealgoritmer",
+  "onboarding.profile.display_name": "Visningsnavn",
+  "onboarding.profile.display_name_hint": "Ditt fulle navn eller ditt morsomme navn…",
+  "onboarding.profile.indexable": "Inkluder offentlige innlegg i søkeresultatene",
+  "onboarding.profile.lead": "Du kan alltid fullføre dette senere i innstillingene, der enda flere tilpasningsalternativer er tilgjengelige.",
+  "onboarding.profile.note": "Om meg",
+  "onboarding.profile.note_hint": "Du kan @nevne andre eller #emneknagger…",
+  "onboarding.profile.save_and_continue": "Lagre og fortsett",
+  "onboarding.profile.title": "Konfigurering av profil",
+  "onboarding.profile.upload_avatar": "Last opp profilbilde",
+  "onboarding.profile.upload_header": "Last opp profiltoppbilde",
   "onboarding.share.lead": "La folk vita korleis dei kan finna deg på Mastodon!",
   "onboarding.share.message": "Eg er {username} på #Mastodon! Du kan fylgja meg på {url}",
   "onboarding.share.next_steps": "Dette kan du gjera no:",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 7421c780f..fe3979f0f 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -390,7 +390,7 @@
   "lists.search": "Søk blant personer du følger",
   "lists.subheading": "Dine lister",
   "load_pending": "{count, plural,one {# ny gjenstand} other {# nye gjenstander}}",
-  "loading_indicator.label": "Laster...",
+  "loading_indicator.label": "Laster…",
   "media_gallery.toggle_visible": "Veksle synlighet",
   "moved_to_account_banner.text": "Din konto {disabledAccount} er for øyeblikket deaktivert fordi du flyttet til {movedToAccount}.",
   "mute_modal.duration": "Varighet",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Dessverre kan ingen resultater vises akkurat nå. Du kan prøve å bruke søk eller bla gjennom utforske-siden for å finne folk å følge, eller prøve igjen senere.",
   "onboarding.follows.lead": "Hjem-skjermen din er den viktigste måten å oppleve Mastodon på. Jo flere du følger, jo mer aktiv og interessant blir det. For å komme i gang, er her noen forslag:",
   "onboarding.follows.title": "Populært på Mastodon",
+  "onboarding.profile.discoverable": "Fremhevede profiler og innlegg i oppdagelsealgoritmer",
+  "onboarding.profile.display_name": "Visningsnavn",
+  "onboarding.profile.display_name_hint": "Ditt fulle navn eller ditt morsomme navn…",
+  "onboarding.profile.indexable": "Inkluder offentlige innlegg i søkeresultatene",
+  "onboarding.profile.lead": "Du kan alltid fullføre dette senere i innstillingene, der enda flere tilpasningsalternativer er tilgjengelige.",
+  "onboarding.profile.note": "Om meg",
+  "onboarding.profile.note_hint": "Du kan @nevne andre eller #emneknagger…",
+  "onboarding.profile.save_and_continue": "Lagre og fortsett",
+  "onboarding.profile.title": "Konfigurering av profil",
+  "onboarding.profile.upload_avatar": "Last opp profilbilde",
+  "onboarding.profile.upload_header": "Last opp profiltoppbilde",
   "onboarding.share.lead": "La folk vite hvordan de kan finne deg på Mastodon!",
   "onboarding.share.message": "Jeg er {username} på #Mastodon! Kom og følg meg på {url}",
   "onboarding.share.next_steps": "Mulige neste trinn:",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 6b8265450..3812057fb 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -340,7 +340,6 @@
   "lists.search": "Cercar demest lo mond que seguètz",
   "lists.subheading": "Vòstras listas",
   "load_pending": "{count, plural, one {# nòu element} other {# nòu elements}}",
-  "loading_indicator.label": "Cargament…",
   "media_gallery.toggle_visible": "Modificar la visibilitat",
   "mute_modal.duration": "Durada",
   "mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?",
diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json
index 371f9622d..7faf27971 100644
--- a/app/javascript/mastodon/locales/pa.json
+++ b/app/javascript/mastodon/locales/pa.json
@@ -122,7 +122,6 @@
   "lightbox.next": "ਅਗਲੀ",
   "lightbox.previous": "ਪਿਛਲੀ",
   "lists.delete": "ਸੂਚੀ ਮਿਟਾਓ",
-  "loading_indicator.label": "ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ...",
   "mute_modal.duration": "ਮਿਆਦ",
   "navigation_bar.about": "ਸਾਡੇ ਬਾਰੇ",
   "navigation_bar.bookmarks": "ਬੁੱਕਮਾਰਕ",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 643f78a88..a1cc0e26e 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Niestety w tej chwili nie można przedstawić żadnych wyników. Możesz spróbować wyszukać lub przeglądać stronę, aby znaleźć osoby do śledzenia, lub spróbować ponownie później.",
   "onboarding.follows.lead": "Zarządasz swoim własnym kanałem. Im więcej ludzi śledzisz, tym bardziej aktywny i ciekawy będzie Twój kanał. Te profile mogą być dobrym punktem wyjścia— możesz przestać je obserwować w dowolnej chwili!",
   "onboarding.follows.title": "Popularne na Mastodonie",
+  "onboarding.profile.discoverable": "Udostępniaj profil i wpisy funkcjom odkrywania",
+  "onboarding.profile.display_name": "Nazwa wyświetlana",
+  "onboarding.profile.display_name_hint": "Twoje imię lub pseudonim…",
+  "onboarding.profile.indexable": "Pokaż publiczne wpisy w wynikach wyszukiwania",
+  "onboarding.profile.lead": "Możesz wypełnić te dane później w menu ustawień, gdzie dostępnych jest jeszcze więcej opcji.",
+  "onboarding.profile.note": "O mnie",
+  "onboarding.profile.note_hint": "Możesz @wspomnieć użytkowników albo #hasztagi…",
+  "onboarding.profile.save_and_continue": "Zapisz i kontynuuj",
+  "onboarding.profile.title": "Ustawienia profilu",
+  "onboarding.profile.upload_avatar": "Dodaj zdjęcie profilowe",
+  "onboarding.profile.upload_header": "Dodaj zdjęcie nagłówkowe",
   "onboarding.share.lead": "Daj znać ludziom, jak mogą cię znaleźć na Mastodonie!",
   "onboarding.share.message": "Jestem {username} na #Mastodon! Śledź mnie tutaj {url}",
   "onboarding.share.next_steps": "Możliwe dalsze kroki:",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 9c09e2d71..7ce63e9b1 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -390,7 +390,6 @@
   "lists.search": "Procurar entre as pessoas que segue",
   "lists.subheading": "Suas listas",
   "load_pending": "{count, plural, one {# novo item} other {# novos items}}",
-  "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "{number, plural, one {Ocultar mídia} other {Ocultar mídias}}",
   "moved_to_account_banner.text": "Sua conta {disabledAccount} está desativada porque você a moveu para {movedToAccount}.",
   "mute_modal.duration": "Duração",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index 988bec9b0..69e804878 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -390,7 +390,7 @@
   "lists.search": "Pesquisa entre as pessoas que segues",
   "lists.subheading": "As tuas listas",
   "load_pending": "{count, plural, one {# novo item} other {# novos itens}}",
-  "loading_indicator.label": "A carregar...",
+  "loading_indicator.label": "A carregar…",
   "media_gallery.toggle_visible": "Alternar visibilidade",
   "moved_to_account_banner.text": "A sua conta {disabledAccount} está, no momento, desativada, porque você migrou para {movedToAccount}.",
   "mute_modal.duration": "Duração",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Infelizmente, não é possível mostrar resultados neste momento. Pode tentar utilizar a pesquisa ou navegar na página \"Explorar\" para encontrar pessoas para seguir ou tentar novamente mais tarde.",
   "onboarding.follows.lead": "Você personaliza a sua própria página inicial. Quanto mais pessoas seguir, mais ativa e interessante ela será. Estes perfis podem ser um bom ponto de partida - pode sempre deixar de os seguir mais tarde!",
   "onboarding.follows.title": "Popular no Mastodon",
+  "onboarding.profile.discoverable": "Destacar perfil e publicações nos algoritmos de descoberta",
+  "onboarding.profile.display_name": "Nome a apresentar",
+  "onboarding.profile.display_name_hint": "O seu nome completo ou o seu nome divertido…",
+  "onboarding.profile.indexable": "Incluir publicações públicas nos resultados de pesquisa",
+  "onboarding.profile.lead": "Pode sempre completar isto mais tarde, nas configurações, onde ainda estão disponíveis mais opções de personalização.",
+  "onboarding.profile.note": "Bio",
+  "onboarding.profile.note_hint": "Pode @mencionar outras pessoas ou #hashtags…",
+  "onboarding.profile.save_and_continue": "Guardar e continuar",
+  "onboarding.profile.title": "Configuração do perfil",
+  "onboarding.profile.upload_avatar": "Carregar foto de perfil",
+  "onboarding.profile.upload_header": "Carregar cabeçalho do perfil",
   "onboarding.share.lead": "Deixe as pessoas saber como o podem encontrar no Mastodon!",
   "onboarding.share.message": "Eu sou {username} no #Mastodon! Venha seguir-me em {url}",
   "onboarding.share.next_steps": "Próximos passos possíveis:",
diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json
index f0e84463b..5355f9935 100644
--- a/app/javascript/mastodon/locales/ro.json
+++ b/app/javascript/mastodon/locales/ro.json
@@ -353,7 +353,6 @@
   "lists.search": "Caută printre persoanele la care ești abonat",
   "lists.subheading": "Listele tale",
   "load_pending": "{count, plural, one {# element nou} other {# elemente noi}}",
-  "loading_indicator.label": "Se încarcă...",
   "media_gallery.toggle_visible": "{number, plural, one {Ascunde imaginea} other {Ascunde imaginile}}",
   "moved_to_account_banner.text": "Contul tău {disabledAccount} este în acest moment dezactivat deoarece te-ai mutat la {movedToAccount}.",
   "mute_modal.duration": "Durata",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 69db89dc8..5c98d906b 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -389,7 +389,6 @@
   "lists.search": "Искать среди подписок",
   "lists.subheading": "Ваши списки",
   "load_pending": "{count, plural, one {# новый элемент} few {# новых элемента} other {# новых элементов}}",
-  "loading_indicator.label": "Загрузка...",
   "media_gallery.toggle_visible": "Показать/скрыть {number, plural, =1 {изображение} other {изображения}}",
   "moved_to_account_banner.text": "Ваша учетная запись {disabledAccount} в настоящее время заморожена, потому что вы переехали на {movedToAccount}.",
   "mute_modal.duration": "Продолжительность",
diff --git a/app/javascript/mastodon/locales/sa.json b/app/javascript/mastodon/locales/sa.json
index cb92914e5..59379343b 100644
--- a/app/javascript/mastodon/locales/sa.json
+++ b/app/javascript/mastodon/locales/sa.json
@@ -345,7 +345,6 @@
   "lists.search": "त्वया अनुसारितजनेषु अन्विष्य",
   "lists.subheading": "तव सूचयः",
   "load_pending": "{count, plural, one {# नूतनवस्तु} other {# नूतनवस्तूनि}}",
-  "loading_indicator.label": "आरोपयति...",
   "media_gallery.toggle_visible": "{number, plural, one {चित्रं प्रच्छादय} other {चित्राणि प्रच्छादय}}",
   "moved_to_account_banner.text": "तव एकौण्ट् {disabledAccount} अधुना निष्कृतो यतोहि {movedToAccount} अस्मिन्त्वमसार्षीः।",
   "mute_modal.duration": "परिमाणम्",
diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json
index 4528e161b..59c834b95 100644
--- a/app/javascript/mastodon/locales/sc.json
+++ b/app/javascript/mastodon/locales/sc.json
@@ -263,7 +263,6 @@
   "lists.search": "Chirca intre sa gente chi ses sighende",
   "lists.subheading": "Is listas tuas",
   "load_pending": "{count, plural, one {# elementu nou} other {# elementos noos}}",
-  "loading_indicator.label": "Carrighende...",
   "media_gallery.toggle_visible": "Cua {number, plural, one {immàgine} other {immàgines}}",
   "mute_modal.duration": "Durada",
   "mute_modal.hide_notifications": "Boles cuare is notìficas de custa persone?",
diff --git a/app/javascript/mastodon/locales/sco.json b/app/javascript/mastodon/locales/sco.json
index 9b8e6a215..28dac9c2a 100644
--- a/app/javascript/mastodon/locales/sco.json
+++ b/app/javascript/mastodon/locales/sco.json
@@ -330,7 +330,6 @@
   "lists.search": "Seirch amang the fowk ye ken",
   "lists.subheading": "Yer lists",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
-  "loading_indicator.label": "Loadin...",
   "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
   "moved_to_account_banner.text": "Yer accoont {disabledAccount} is disabilt the noo acause ye flittit tae {movedToAccount}.",
   "mute_modal.duration": "Lenth",
diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json
index 7b26a9b48..835f699b8 100644
--- a/app/javascript/mastodon/locales/si.json
+++ b/app/javascript/mastodon/locales/si.json
@@ -261,7 +261,6 @@
   "lists.replies_policy.none": "කිසිවෙක් නැත",
   "lists.replies_policy.title": "පිළිතුරු පෙන්වන්න:",
   "lists.subheading": "ඔබගේ ලැයිස්තු",
-  "loading_indicator.label": "පූරණය වෙමින්...",
   "mute_modal.duration": "පරාසය",
   "mute_modal.hide_notifications": "මෙම පුද්ගලයාගේ දැනුම්දීම් සඟවන්නද?",
   "navigation_bar.about": "පිළිබඳව",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index f2616b31c..24186794b 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -389,7 +389,6 @@
   "lists.search": "Vyhľadávaj medzi užívateľmi, ktorých sleduješ",
   "lists.subheading": "Tvoje zoznamy",
   "load_pending": "{count, plural, one {# nová položka} other {# nových položiek}}",
-  "loading_indicator.label": "Načítam...",
   "media_gallery.toggle_visible": "Zapni/Vypni viditeľnosť",
   "moved_to_account_banner.text": "Vaše konto {disabledAccount} je momentálne zablokované, pretože ste sa presunuli na {movedToAccount}.",
   "mute_modal.duration": "Trvanie",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index f16a91d65..d179a8656 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -390,7 +390,6 @@
   "lists.search": "Iščite med ljudmi, katerim sledite",
   "lists.subheading": "Vaši seznami",
   "load_pending": "{count, plural, one {# nov element} two {# nova elementa} few {# novi elementi} other {# novih elementov}}",
-  "loading_indicator.label": "Nalaganje ...",
   "media_gallery.toggle_visible": "{number, plural,one {Skrij sliko} two {Skrij sliki} other {Skrij slike}}",
   "moved_to_account_banner.text": "Vaš račun {disabledAccount} je trenutno onemogočen, ker ste se prestavili na {movedToAccount}.",
   "mute_modal.duration": "Trajanje",
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 8d54ef41b..1417bed5f 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Mjerisht, s’mund të shfaqen përfundime tani. Mund të provoni të përdorni kërkimin, ose të shfletoni faqen e eksplorimit, që të gjeni persona për ndjekje, ose të riprovoni më vonë.",
   "onboarding.follows.lead": "Ju kujdeseni për prurjen tuaj. Sa më tepër persona të tjerë të ndiqni, aq më aktive dhe interesante do të bëhet ajo. Këto profile mund të jenë një pikënisje e mirë—mundeni përherë të ndërpritni ndjekjen e tyre më vonë!",
   "onboarding.follows.title": "Popullore në Mastodon",
+  "onboarding.profile.discoverable": "Profilin dhe postimet bëji objekt të algoritmeve të zbulimit",
+  "onboarding.profile.display_name": "Emër në ekran",
+  "onboarding.profile.display_name_hint": "Emri juaj i plotë, ose ç’të doni…",
+  "onboarding.profile.indexable": "Përfshi postime publike në përfundime kërkimi",
+  "onboarding.profile.lead": "Këtë mund ta plotësoni përherë më vonë, te rregullimet, ku ka edhe më tepër mundësi përshtatjeje.",
+  "onboarding.profile.note": "Jetëshkrim",
+  "onboarding.profile.note_hint": "Mund të @përmendni persona të tjerë, ose #hashtagë…",
+  "onboarding.profile.save_and_continue": "Ruaje dhe vazhdo",
+  "onboarding.profile.title": "Udjisje profili",
+  "onboarding.profile.upload_avatar": "Ngarkoni foto profili",
+  "onboarding.profile.upload_header": "Ngarkoni krye profili",
   "onboarding.share.lead": "Bëjuni të ditur njerëzve se si mund t’ju gjejnë në Mastodon!",
   "onboarding.share.message": "Jam {username} në #Mastodon! Ejani dhe ndiqmëni te {url}",
   "onboarding.share.next_steps": "Hapa pasues të mundshëm:",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index aa948b1f0..ea6e188cb 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -390,7 +390,6 @@
   "lists.search": "Pretraži među ljudima koje pratite",
   "lists.subheading": "Vaše liste",
   "load_pending": "{count, plural, one {# nova stavka} few {# nove stavke} other {# novih stavki}}",
-  "loading_indicator.label": "Učitavanje...",
   "media_gallery.toggle_visible": "{number, plural, one {Sakrij sliku} few {Sakrij slike} other {Sakrij slike}}",
   "moved_to_account_banner.text": "Vaš nalog {disabledAccount} je trenutno onemogućen jer ste prešli na {movedToAccount}.",
   "mute_modal.duration": "Trajanje",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index 9e6716927..b0a76b32a 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -390,7 +390,6 @@
   "lists.search": "Претражи међу људима које пратите",
   "lists.subheading": "Ваше листе",
   "load_pending": "{count, plural, one {# нова ставка} few {# нове ставке} other {# нових ставки}}",
-  "loading_indicator.label": "Учитавање...",
   "media_gallery.toggle_visible": "{number, plural, one {Сакриј слику} few {Сакриј слике} other {Сакриј слике}}",
   "moved_to_account_banner.text": "Ваш налог {disabledAccount} је тренутно онемогућен јер сте прешли на {movedToAccount}.",
   "mute_modal.duration": "Трајање",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index edf981e11..6f9f37ea1 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -390,7 +390,7 @@
   "lists.search": "Sök bland personer du följer",
   "lists.subheading": "Dina listor",
   "load_pending": "{count, plural, one {# nytt objekt} other {# nya objekt}}",
-  "loading_indicator.label": "Laddar...",
+  "loading_indicator.label": "Laddar…",
   "media_gallery.toggle_visible": "Växla synlighet",
   "moved_to_account_banner.text": "Ditt konto {disabledAccount} är för närvarande inaktiverat eftersom du flyttat till {movedToAccount}.",
   "mute_modal.duration": "Varaktighet",
@@ -479,6 +479,14 @@
   "onboarding.follows.empty": "Tyvärr kan inga resultat visas just nu. Du kan prova att använda sökfunktionen eller utforska sidan för att hitta personer att följa, eller försök igen senare.",
   "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
   "onboarding.follows.title": "Popular on Mastodon",
+  "onboarding.profile.display_name": "Visningsnamn",
+  "onboarding.profile.display_name_hint": "Fullständigt namn eller ditt roliga namn…",
+  "onboarding.profile.indexable": "Inkludera offentliga inlägg i sökresultaten",
+  "onboarding.profile.lead": "Du kan alltid slutföra detta senare i inställningarna, där ännu fler anpassningsalternativ finns tillgängliga.",
+  "onboarding.profile.note": "Bio",
+  "onboarding.profile.note_hint": "Du kan @nämna andra personer eller #hashtags…",
+  "onboarding.profile.save_and_continue": "Spara och fortsätt",
+  "onboarding.profile.upload_avatar": "Ladda upp profilbild",
   "onboarding.share.lead": "Låt folk veta hur de kan hitta dig på Mastodon!",
   "onboarding.share.message": "Jag är {username} på #Mastodon! Följ mig på {url}",
   "onboarding.share.next_steps": "Möjliga nästa steg:",
diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json
index f62088f62..ce9042e62 100644
--- a/app/javascript/mastodon/locales/ta.json
+++ b/app/javascript/mastodon/locales/ta.json
@@ -248,7 +248,6 @@
   "lists.search": "நீங்கள் பின்தொடரும் நபர்கள் மத்தியில் தேடுதல்",
   "lists.subheading": "உங்கள் பட்டியல்கள்",
   "load_pending": "{count, plural,one {# புதியது}other {# புதியவை}}",
-  "loading_indicator.label": "ஏற்றுதல்...",
   "media_gallery.toggle_visible": "நிலைமாற்று தெரியும்",
   "mute_modal.hide_notifications": "இந்த பயனரின் அறிவிப்புகளை மறைக்கவா?",
   "navigation_bar.blocks": "தடுக்கப்பட்ட பயனர்கள்",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index 6cf735991..f21c0ef57 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -180,7 +180,6 @@
   "lists.new.title_placeholder": "కొత్త జాబితా శీర్షిక",
   "lists.search": "మీరు అనుసరించే వ్యక్తులలో శోధించండి",
   "lists.subheading": "మీ జాబితాలు",
-  "loading_indicator.label": "లోడ్ అవుతోంది...",
   "media_gallery.toggle_visible": "దృశ్యమానతను టోగుల్ చేయండి",
   "mute_modal.hide_notifications": "ఈ వినియోగదారు నుండి నోటిఫికేషన్లను దాచాలా?",
   "navigation_bar.blocks": "బ్లాక్ చేయబడిన వినియోగదారులు",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 80f862cbe..2166cd2dd 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -390,7 +390,7 @@
   "lists.search": "ค้นหาในหมู่ผู้คนที่คุณติดตาม",
   "lists.subheading": "รายการของคุณ",
   "load_pending": "{count, plural, other {# รายการใหม่}}",
-  "loading_indicator.label": "กำลังโหลด...",
+  "loading_indicator.label": "กำลังโหลด…",
   "media_gallery.toggle_visible": "{number, plural, other {ซ่อนภาพ}}",
   "moved_to_account_banner.text": "มีการปิดใช้งานบัญชีของคุณ {disabledAccount} ในปัจจุบันเนื่องจากคุณได้ย้ายไปยัง {movedToAccount}",
   "mute_modal.duration": "ระยะเวลา",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "น่าเสียดาย ไม่สามารถแสดงผลลัพธ์ได้ในตอนนี้ คุณสามารถลองใช้การค้นหาหรือเรียกดูหน้าสำรวจเพื่อค้นหาผู้คนที่จะติดตาม หรือลองอีกครั้งในภายหลัง",
   "onboarding.follows.lead": "ฟีดหน้าแรกของคุณเป็นวิธีหลักในการสัมผัส Mastodon ยิ่งคุณติดตามผู้คนมากเท่าไร ฟีดหน้าแรกก็จะยิ่งมีการใช้งานและน่าสนใจมากขึ้นเท่านั้น เพื่อช่วยให้คุณเริ่มต้นใช้งาน นี่คือข้อเสนอแนะบางส่วน:",
   "onboarding.follows.title": "ปรับแต่งฟีดหน้าแรกของคุณ",
+  "onboarding.profile.discoverable": "แสดงโปรไฟล์และโพสต์ในอัลกอริทึมการค้นพบ",
+  "onboarding.profile.display_name": "ชื่อที่แสดง",
+  "onboarding.profile.display_name_hint": "ชื่อเต็มหรือชื่อแบบสนุกสนานของคุณ",
+  "onboarding.profile.indexable": "รวมโพสต์สาธารณะในผลลัพธ์การค้นหา",
+  "onboarding.profile.lead": "คุณสามารถกลับมาทำต่อได้เสมอในการตั้งค่า ซึ่งจะมีตัวเลือกในการปรับแต่งมากกว่า",
+  "onboarding.profile.note": "ชีวประวัติ",
+  "onboarding.profile.note_hint": "คุณสามารถ @กล่าวถึง ผู้คนอื่น ๆ หรือ #แฮชแท็ก",
+  "onboarding.profile.save_and_continue": "บันทึกและดำเนินการต่อ",
+  "onboarding.profile.title": "การตั้งค่าโปรไฟล์",
+  "onboarding.profile.upload_avatar": "อัปโหลดรูปโปรไฟล์",
+  "onboarding.profile.upload_header": "อัปโหลดรูปส่วนหัวโปรไฟล์",
   "onboarding.share.lead": "แจ้งให้ผู้คนทราบวิธีที่เขาสามารถค้นหาคุณใน Mastodon!",
   "onboarding.share.message": "ฉันคือ {username} ใน #Mastodon! มาติดตามฉันที่ {url}",
   "onboarding.share.next_steps": "ขั้นตอนถัดไปที่เป็นไปได้:",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 9ad259493..505b16f4b 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -390,7 +390,7 @@
   "lists.search": "Takip ettiğiniz kişiler arasından arayın",
   "lists.subheading": "Listeleriniz",
   "load_pending": "{count, plural, one {# yeni öğe} other {# yeni öğe}}",
-  "loading_indicator.label": "Yükleniyor...",
+  "loading_indicator.label": "Yükleniyor…",
   "media_gallery.toggle_visible": "{number, plural, one {Resmi} other {Resimleri}} gizle",
   "moved_to_account_banner.text": "{disabledAccount} hesabınız, {movedToAccount} hesabına taşıdığınız için şu an devre dışı.",
   "mute_modal.duration": "Süre",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "Maalesef şu an bir sonuç gösterilemiyor. Takip edilecek kişileri bulmak için arama veya keşfet sayfasına gözatmayı kullanabilirsiniz veya daha sonra tekrar deneyin.",
   "onboarding.follows.lead": "Kendi ana akışınızı siz düzenliyorsunuz. Siz daha fazla insanı takip ettikçe, daha etkin ve ilgi çekici olacaktır. Bu profiller iyi bir başlangıç olabilir, isterseniz izlemeyi daha sonra bırakabilirsiniz:",
   "onboarding.follows.title": "Mastodon'da Popüler",
+  "onboarding.profile.discoverable": "Profil ve gönderileri keşif algoritmalarında kullan",
+  "onboarding.profile.display_name": "Görünen isim",
+  "onboarding.profile.display_name_hint": "Tam adınız veya kullanıcı adınız…",
+  "onboarding.profile.indexable": "Herkese açık gönderileri arama sonuçlarına ekle",
+  "onboarding.profile.lead": "Bunu her zaman daha sonra ayarlardan tamamlayabilirsiniz, hatta daha fazla özelleştirme seçeneğine de ulaşabilirsiniz.",
+  "onboarding.profile.note": "Kişisel bilgiler",
+  "onboarding.profile.note_hint": "Diğer insanlara @değinebilir veya #etiketler kullanabilirsiniz…",
+  "onboarding.profile.save_and_continue": "Kaydet ve ilerle",
+  "onboarding.profile.title": "Profilini ayarla",
+  "onboarding.profile.upload_avatar": "Profil resmi yükle",
+  "onboarding.profile.upload_header": "Profil başlığı yükle",
   "onboarding.share.lead": "Kullanıcılara Mastodon'da size nasıl ulaşabileceklerini ifade edin!",
   "onboarding.share.message": "#Mastodon'da kullanıcı adım {username}! Beni takip etmek için {url} bağlantısını kullanın",
   "onboarding.share.next_steps": "Olası sonraki adımlar:",
diff --git a/app/javascript/mastodon/locales/tt.json b/app/javascript/mastodon/locales/tt.json
index e84bc66cc..6727f3e59 100644
--- a/app/javascript/mastodon/locales/tt.json
+++ b/app/javascript/mastodon/locales/tt.json
@@ -314,7 +314,6 @@
   "lists.replies_policy.none": "Һичкем",
   "lists.subheading": "Исемлегегегезләр",
   "load_pending": "{count, plural, one {# яңа элемент} other {# яңа элемент}}",
-  "loading_indicator.label": "Йөкләү...",
   "mute_modal.duration": "Дәвамлык",
   "mute_modal.indefinite": "Билгесез",
   "navigation_bar.about": "Проект турында",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 09fa58fbf..8649d9310 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -390,7 +390,7 @@
   "lists.search": "Шукати серед людей, на яких ви підписані",
   "lists.subheading": "Ваші списки",
   "load_pending": "{count, plural, one {# новий елемент} other {# нових елементів}}",
-  "loading_indicator.label": "Завантаження...",
+  "loading_indicator.label": "Завантаження…",
   "media_gallery.toggle_visible": "{number, plural, one {Приховати зображення} other {Приховати зображення}}",
   "moved_to_account_banner.text": "Ваш обліковий запис {disabledAccount} наразі вимкнений, оскільки вас перенесено до {movedToAccount}.",
   "mute_modal.duration": "Тривалість",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "На жаль, жоден результат не може бути показаний просто зараз. Ви можете спробувати скористатися пошуком або переглядом сторінки огляду, щоб знайти людей для слідкування або повторіть спробу пізніше.",
   "onboarding.follows.lead": "Ваша домашня стрічка - основний спосіб роботи Mastodon. Чим більше людей, які ви підписані, тим активнішою і цікавою. Ось деякі пропозиції на початок:",
   "onboarding.follows.title": "Персоналізуйте домашню стрічку",
+  "onboarding.profile.discoverable": "Враховувати профіль та дописи в алгоритмах пошуку",
+  "onboarding.profile.display_name": "Видиме ім'я",
+  "onboarding.profile.display_name_hint": "Ваше повне ім'я або ваш псевдонім…",
+  "onboarding.profile.indexable": "Включити загальнодоступні дописи в результати пошуку",
+  "onboarding.profile.lead": "Ви завжди можете завершити це пізніше в Налаштуваннях, де доступно ще більше опцій налаштування.",
+  "onboarding.profile.note": "Біографія",
+  "onboarding.profile.note_hint": "Ви можете @згадувати інших людей або #гештеґи…",
+  "onboarding.profile.save_and_continue": "Зберегти і продовжити",
+  "onboarding.profile.title": "Налаштування профілю",
+  "onboarding.profile.upload_avatar": "Завантажити зображення профілю",
+  "onboarding.profile.upload_header": "Завантажити заголовок профілю",
   "onboarding.share.lead": "Розкажіть людям про те, як вони можуть знайти вас на Mastodon!",
   "onboarding.share.message": "Я {username} на #Mastodon! Стежте за мною на {url}",
   "onboarding.share.next_steps": "Можливі такі кроки:",
diff --git a/app/javascript/mastodon/locales/uz.json b/app/javascript/mastodon/locales/uz.json
index afb4c4c4c..026cc115c 100644
--- a/app/javascript/mastodon/locales/uz.json
+++ b/app/javascript/mastodon/locales/uz.json
@@ -325,7 +325,6 @@
   "lists.search": "Siz kuzatadigan odamlar orasidan qidiring",
   "lists.subheading": "Sizning ro'yxatlaringiz",
   "load_pending": "{count, plural, one {# yangi element} other {# yangi elementlar}}",
-  "loading_indicator.label": "Yuklanmoqda...",
   "media_gallery.toggle_visible": "{number, plural, one {Rasmni yashirish} other {Rasmlarni yashirish}}",
   "moved_to_account_banner.text": "{movedToAccount} hisobiga koʻchganingiz uchun {disabledAccount} hisobingiz hozirda oʻchirib qoʻyilgan.",
   "mute_modal.duration": "Davomiyligi",
diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json
index aa229eceb..c98be2b74 100644
--- a/app/javascript/mastodon/locales/vi.json
+++ b/app/javascript/mastodon/locales/vi.json
@@ -390,7 +390,6 @@
   "lists.search": "Tìm kiếm những người mà bạn quan tâm",
   "lists.subheading": "Danh sách của bạn",
   "load_pending": "{count, plural, one {# tút mới} other {# tút mới}}",
-  "loading_indicator.label": "Đang tải...",
   "media_gallery.toggle_visible": "{number, plural, other {Ẩn hình ảnh}}",
   "moved_to_account_banner.text": "Tài khoản {disabledAccount} của bạn hiện không khả dụng vì bạn đã chuyển sang {movedToAccount}.",
   "mute_modal.duration": "Thời hạn",
diff --git a/app/javascript/mastodon/locales/zgh.json b/app/javascript/mastodon/locales/zgh.json
index 7b19e0e6a..5896a25b0 100644
--- a/app/javascript/mastodon/locales/zgh.json
+++ b/app/javascript/mastodon/locales/zgh.json
@@ -127,7 +127,6 @@
   "lists.replies_policy.title": "ⵙⴽⵏ ⵜⵉⵔⴰⵔⵉⵏ ⵉ:",
   "lists.subheading": "ⵜⵉⵍⴳⴰⵎⵉⵏ ⵏⵏⴽ",
   "load_pending": "{count, plural, one {# ⵓⴼⵔⴷⵉⵙ ⴰⵎⴰⵢⵏⵓ} other {# ⵉⴼⵔⴷⴰⵙ ⵉⵎⴰⵢⵏⵓⵜⵏ}}",
-  "loading_indicator.label": "ⴰⵣⴷⴰⵎ...",
   "media_gallery.toggle_visible": "ⴼⴼⵔ {number, plural, one {ⵜⴰⵡⵍⴰⴼⵜ} other {ⵜⵉⵡⵍⴰⴼⵉⵏ}}",
   "navigation_bar.compose": "Compose new toot",
   "navigation_bar.domain_blocks": "Hidden domains",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index f830aa298..cc6d8994d 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -193,7 +193,7 @@
   "conversation.with": "与 {names}",
   "copypaste.copied": "已复制",
   "copypaste.copy_to_clipboard": "复制到剪贴板",
-  "directory.federated": "来自已知联邦宇宙",
+  "directory.federated": "来自已知的联邦宇宙",
   "directory.local": "仅来自 {domain}",
   "directory.new_arrivals": "新来者",
   "directory.recently_active": "最近活跃",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "很抱歉,现在无法显示任何结果。您可以尝试使用搜索或浏览探索页面来查找要关注的人,或稍后再试。",
   "onboarding.follows.lead": "你管理你自己的家庭饲料。你关注的人越多,它将越活跃和有趣。 这些配置文件可能是一个很好的起点——你可以随时取消关注它们!",
   "onboarding.follows.title": "定制您的主页动态",
+  "onboarding.profile.discoverable": "在发现算法中展示您的个人资料和嘟文",
+  "onboarding.profile.display_name": "昵称",
+  "onboarding.profile.display_name_hint": "您的全名或昵称…",
+  "onboarding.profile.indexable": "将您的公开嘟文纳入搜索范围",
+  "onboarding.profile.lead": "您可以稍后在设置中完成此操作,设置中有更多的自定义选项。",
+  "onboarding.profile.note": "简介",
+  "onboarding.profile.note_hint": "您可以提及 @其他人 或 #标签…",
+  "onboarding.profile.save_and_continue": "保存并继续",
+  "onboarding.profile.title": "设置个人资料",
+  "onboarding.profile.upload_avatar": "上传头像",
+  "onboarding.profile.upload_header": "上传资料卡头图",
   "onboarding.share.lead": "让人们知道他们如何在Mastodon找到你!",
   "onboarding.share.message": "我是来自 #Mastodon 的 {username}!请在 {url} 关注我。",
   "onboarding.share.next_steps": "可能的下一步:",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 7121a7d03..ea932bc5a 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -390,7 +390,7 @@
   "lists.search": "從你關注的人搜索",
   "lists.subheading": "列表",
   "load_pending": "{count, plural, other {# 個新項目}}",
-  "loading_indicator.label": "載入中...",
+  "loading_indicator.label": "載入中…",
   "media_gallery.toggle_visible": "隱藏圖片",
   "moved_to_account_banner.text": "您的帳號 {disabledAccount} 目前已停用,因為您已搬家至 {movedToAccount}。",
   "mute_modal.duration": "時間",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "很遺憾,現在無法顯示任何結果。你可以嘗試搜尋或瀏覽探索頁面來找使用者來追蹤,或者稍後再試。",
   "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
   "onboarding.follows.title": "Popular on Mastodon",
+  "onboarding.profile.discoverable": "在探索的演算法中展示個人檔案和帖文",
+  "onboarding.profile.display_name": "顯示名稱",
+  "onboarding.profile.display_name_hint": "你的全名或暱稱…",
+  "onboarding.profile.indexable": "將公開帖文納入搜尋結果中",
+  "onboarding.profile.lead": "你可以隨時在設定中完成此動作,那裏有更多自訂選項。",
+  "onboarding.profile.note": "簡介",
+  "onboarding.profile.note_hint": "你可以 @提及他人 或使用 #標籤…",
+  "onboarding.profile.save_and_continue": "儲存並繼續",
+  "onboarding.profile.title": "個人檔案設定",
+  "onboarding.profile.upload_avatar": "上載個人檔案頭像",
+  "onboarding.profile.upload_header": "上載個人檔案橫幅圖片",
   "onboarding.share.lead": "讓大家知道如何在 Mastodon 上找到你吧!",
   "onboarding.share.message": "我在 #Mastodon 的使用者名稱是 {username}!快來追蹤我吧 {url}",
   "onboarding.share.next_steps": "接下來你可以:",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 974096d2f..4c52693cc 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -390,7 +390,7 @@
   "lists.search": "搜尋您跟隨的使用者",
   "lists.subheading": "您的列表",
   "load_pending": "{count, plural, one {# 個新項目} other {# 個新項目}}",
-  "loading_indicator.label": "讀取中...",
+  "loading_indicator.label": "正在載入...",
   "media_gallery.toggle_visible": "切換可見性",
   "moved_to_account_banner.text": "您的帳號 {disabledAccount} 目前已停用,因為您已搬家至 {movedToAccount}。",
   "mute_modal.duration": "持續時間",
@@ -479,6 +479,17 @@
   "onboarding.follows.empty": "很遺憾,目前未能顯示任何結果。您可以嘗試使用搜尋、瀏覽探索頁面以找尋人們跟隨、或稍候再試。",
   "onboarding.follows.lead": "您的首頁時間軸是 Mastodon 的核心體驗。若您跟隨更多人的話,它將會變得更活躍有趣。這些個人檔案也許是個好起點,您可以隨時取消跟隨他們!",
   "onboarding.follows.title": "客製化您的首頁時間軸",
+  "onboarding.profile.discoverable": "於探索演算法中推薦個人檔案及嘟文",
+  "onboarding.profile.display_name": "顯示名稱",
+  "onboarding.profile.display_name_hint": "完整名稱或暱稱...",
+  "onboarding.profile.indexable": "允許公開嘟文顯示於搜尋結果中",
+  "onboarding.profile.lead": "您隨時可以稍候於設定中完成此操作,將有更多自訂選項可使用。",
+  "onboarding.profile.note": "個人簡介",
+  "onboarding.profile.note_hint": "您可以 @mention 其他人或者使用 #主題標籤...",
+  "onboarding.profile.save_and_continue": "儲存並繼續",
+  "onboarding.profile.title": "個人檔案設定",
+  "onboarding.profile.upload_avatar": "上傳個人檔案大頭貼",
+  "onboarding.profile.upload_header": "上傳個人檔案封面圖片",
   "onboarding.share.lead": "讓其他人知道他們如何於 Mastodon 上面找到您!",
   "onboarding.share.message": "我是 #Mastodon 上的 {username}!歡迎於 {url} 跟隨我",
   "onboarding.share.next_steps": "可能的下一步:",
diff --git a/config/locales/be.yml b/config/locales/be.yml
index 223b4d1df..fa43373f6 100644
--- a/config/locales/be.yml
+++ b/config/locales/be.yml
@@ -635,6 +635,7 @@ be:
       created_at: Створана
       delete_and_resolve: Выдаліць допісы
       forwarded: Пераслана
+      forwarded_replies_explanation: Гэтае паведамленне паступіла ад выдаленага карыстальніка і дакранаецца выдаленага змесціва. Яно было накіраванае вам, бо змесціва паведамлення з'яўляецца адказам аднаму з вашых карыстальнікаў.
       forwarded_to: Пераслана на %{domain}
       mark_as_resolved: Пазначыць як вырашаную
       mark_as_sensitive: Пазначыць як далікатны
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index 9da834301..dfff7058d 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -1368,6 +1368,7 @@ bg:
       '86400': 1 ден
     expires_in_prompt: Никога
     generate: Генериране на линк за покана
+    invalid: Тази покана не е валидна
     invited_by: 'Бяхте поканени от:'
     max_uses:
       one: 1 използване
diff --git a/config/locales/da.yml b/config/locales/da.yml
index 13010e1ad..7344d789f 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -611,6 +611,7 @@ da:
       created_at: Anmeldt
       delete_and_resolve: Slet indlæg
       forwarded: Videresendt
+      forwarded_replies_explanation: Denne anmeldelse er fra en ekstern bruger og om eksternt indhold. Den er videresendt til dig, da det anmeldte indhold er som svar til en af dine brugere.
       forwarded_to: Videresendt til %{domain}
       mark_as_resolved: Markér som løst
       mark_as_sensitive: Markér som sensitiv
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 81fa4b57f..69309737d 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -611,6 +611,7 @@ de:
       created_at: Gemeldet
       delete_and_resolve: Beiträge löschen
       forwarded: Weitergeleitet
+      forwarded_replies_explanation: Diese Meldung stammt von einem externen Profil und betrifft einen externen Inhalt. Der Inhalt wurde an Dich weitergeleitet, weil er eine Antwort auf ein bei Dir registriertes Profil ist.
       forwarded_to: Weitergeleitet an %{domain}
       mark_as_resolved: Als geklärt markieren
       mark_as_sensitive: Mit einer Inhaltswarnung versehen
diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml
index 9175a1fc1..88248d098 100644
--- a/config/locales/es-AR.yml
+++ b/config/locales/es-AR.yml
@@ -611,6 +611,7 @@ es-AR:
       created_at: Denunciado
       delete_and_resolve: Eliminar mensajes
       forwarded: Reenviado
+      forwarded_replies_explanation: Esta denuncia es de un usuario remoto y sobre contenido remoto. Se te reenvió porque el contenido denunciado es en respuesta a uno de tus usuarios.
       forwarded_to: Reenviado a %{domain}
       mark_as_resolved: Marcar como resuelta
       mark_as_sensitive: Marcar como sensible
diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml
index 75d329b0a..4ecb666b0 100644
--- a/config/locales/es-MX.yml
+++ b/config/locales/es-MX.yml
@@ -611,6 +611,7 @@ es-MX:
       created_at: Denunciado
       delete_and_resolve: Eliminar publicaciones
       forwarded: Reenviado
+      forwarded_replies_explanation: Este reporte es de un usuario remoto y sobre contenido remoto. Se le ha enviado porque el contenido reportado es en respuesta a uno de sus usuarios.
       forwarded_to: Reenviado a %{domain}
       mark_as_resolved: Marcar como resuelto
       mark_as_sensitive: Marcar como sensible
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 4fe0179df..73442396f 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -611,6 +611,7 @@ fi:
       created_at: Raportoitu
       delete_and_resolve: Poista julkaisut
       forwarded: Välitetty
+      forwarded_replies_explanation: Tämä raportti on etäkäyttäjältä ja koskee etäsisältöä. Se on välitetty sinulle, koska raportoitu sisältö on vastaus jollekin käyttäjällesi.
       forwarded_to: Välitetty %{domain}
       mark_as_resolved: Merkitse ratkaistuksi
       mark_as_sensitive: Merkitse arkaluonteiseksi
@@ -1368,6 +1369,7 @@ fi:
       '86400': 1 vuorokausi
     expires_in_prompt: Ei koskaan
     generate: Luo
+    invalid: Tämä kutsu ei ole kelvollinen
     invited_by: 'Sinut kutsui:'
     max_uses:
       one: kertakäyttöinen
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 11e5db453..35dc99650 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -635,6 +635,7 @@ he:
       created_at: מדווח
       delete_and_resolve: מחיקת הודעות
       forwarded: קודם
+      forwarded_replies_explanation: דווח זה הגיע מחשבון משתמש חיצוני על תוכן חיצוני. הוא הועבר אליך כיוון שהתוכן שדווח הוא בתשובה למשתמש.ת שלך.
       forwarded_to: קודם ל-%{domain}
       mark_as_resolved: סימון כפתור
       mark_as_sensitive: סימון כרגיש
@@ -786,7 +787,7 @@ he:
         public_timelines: פידים פומביים
         publish_discovered_servers: פרסום שרתים שנתגלו
         publish_statistics: פרסום הסטטיסטיקות בפומבי
-        title: גילוי
+        title: תגליות
         trends: נושאים חמים
       domain_blocks:
         all: לכולם
@@ -1048,7 +1049,7 @@ he:
     advanced_web_interface_hint: 'אם ברצונך לעשות שימוש במלוא רוחב המסך, ממשק הווב המתקדם מאפשר לך להגדיר עמודות רבות ושונות כדי לראות בו זמנית כמה מידע שתרצה/י: פיד הבית, התראות, פרהסיה ומספר כלשהו של רשימות ותגיות.'
     animations_and_accessibility: הנפשות ונגישות
     confirmation_dialogs: חלונות אישור
-    discovery: גילוי
+    discovery: תגליות
     localization:
       body: מסטודון מתורגם על ידי מתנדבים.
       guide_link: https://crowdin.com/project/mastodon
@@ -1336,7 +1337,7 @@ he:
     deselect: בטל בחירה של הכל
     none: כלום
     order_by: מיין לפי
-    save_changes: שמור שינויים
+    save_changes: לשמור שינויים
     select_all_matching_items:
       many: בחר.י %{count} פריטים שתאמו לחיפוש שלך.
       one: בחר.י פריט %{count} שתאם לחיפוש שלך.
@@ -1780,7 +1781,7 @@ he:
     keep_media_hint: לא מוחק את הודעותיך שמצורפים אליהן קבצי מדיה
     keep_pinned: שמור הודעות מוצמדות
     keep_pinned_hint: לא מוחק אף אחד מההודעות המוצמדות שלך
-    keep_polls: שמור סקרים
+    keep_polls: לשמור סקרים
     keep_polls_hint: לא מוחר אף אחד מהסקרים שלך
     keep_self_bookmark: שמור הודעות שסימנת
     keep_self_bookmark_hint: לא מוחק הודעות שסימנת
@@ -1868,7 +1869,7 @@ he:
         disable: אינך יכול/ה יותר להשתמש בחשבונך, אבל הפרופיל ושאר המידע נשארו על עומדם. ניתן לבקש גיבוי של המידע, לשנות את הגדרות החשבון או למחוק אותו.
         mark_statuses_as_sensitive: כמה מהודעותיך סומנו כרגישות על ידי מנחי הקהילה של %{instance}. זה אומר שאנשים יצטרכו להקיש על המדיה בהודעות לפני שתופיע תצוגה מקדימה. ניתן לסמן את המידע כרגיש בעצמך בהודעותיך העתידיות.
         sensitive: מעתה ואילך כל קבצי המדיה שיועלו על ידך יסומנו כרגישים ויוסתרו מאחורי אזהרה.
-        silence: ניתן עדיין להשתמש בחשבונך אבל רק אנשים שכבר עוקבים אחריך יראו את הודעותיך בשרת זה, וייתכן שתוחרג/י מאמצעי גילוי משתמשים. עם זאת, אחרים יוכלו עדיין לעקוב אחריך.
+        silence: ניתן עדיין להשתמש בחשבונך אבל רק אנשים שכבר עוקבים אחריך יראו את הודעותיך בשרת זה, וייתכן שתוחרג/י ממסכי התגליות. עם זאת, אחרים יוכלו עדיין לעקוב אחריך.
         suspend: לא ניתן יותר להשתמש בחשבונך, ופרופילך וכל מידע אחר לא נגישים יותר. ניתן עדיין להתחבר על מנת לבקש גיבוי של המידע שלך עד שיוסר סופית בעוד כ-30 יום, אבל מידע מסויים ישמר על מנת לוודא שלא תחמוק/י מההשעיה.
       reason: 'סיבה:'
       statuses: 'הודעות מצוטטות:'
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 48f9d5b9d..a0ff3061f 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -611,6 +611,7 @@ hu:
       created_at: Jelentve
       delete_and_resolve: Bejegyzések törlése
       forwarded: Továbbítva
+      forwarded_replies_explanation: Ez a jelentés egy távoli felhasználótól származik, és távoli tartalomról szól. Azért lett neked továbbítva, mert a jelentett tartalom az egyik felhasználódnak küldött válasz.
       forwarded_to: 'Továbbítva ide: %{domain}'
       mark_as_resolved: Megjelölés megoldottként
       mark_as_sensitive: Érzékenynek jelölés
diff --git a/config/locales/is.yml b/config/locales/is.yml
index 390ce0ac0..a4706ee51 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -611,6 +611,7 @@ is:
       created_at: Tilkynnt
       delete_and_resolve: Eyða færslum
       forwarded: Áframsent
+      forwarded_replies_explanation: Þessi kæra er frá fjartengdum notanda og er um fjartengt efni. Hún hefur verið framsend til þín þar sem kærða efnið er í svari til eins af notendunum þínum.
       forwarded_to: Áframsent á %{domain}
       mark_as_resolved: Merkja sem leyst
       mark_as_sensitive: Merkja sem viðkvæmt
diff --git a/config/locales/it.yml b/config/locales/it.yml
index f35e9e42b..82bbf7251 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -611,6 +611,7 @@ it:
       created_at: Segnalato
       delete_and_resolve: Cancella post
       forwarded: Inoltrato
+      forwarded_replies_explanation: Questa segnalazione proviene da un utente remoto e tratta di contenuti remoti. È stato inoltrato a voi perché il contenuto riportato è in risposta a uno dei vostri utenti.
       forwarded_to: Inoltrato a %{domain}
       mark_as_resolved: Segna come risolto
       mark_as_sensitive: Segna come sensibile
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index fb193c75f..e11081fcd 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -601,6 +601,7 @@ ko:
       created_at: 신고 시각
       delete_and_resolve: 게시물 삭제
       forwarded: 전달됨
+      forwarded_replies_explanation: 이 신고는 리모트 사용자가 리모트 컨텐츠에 대해 신고한 것입니다. 이것은 신고된 내용이 로컬 사용자에 대한 답글이기 때문에 첨부되었습니다.
       forwarded_to: "%{domain}에게 전달됨"
       mark_as_resolved: 해결로 표시
       mark_as_sensitive: 민감함으로 설정
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 529eb5a44..035b04462 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -214,6 +214,7 @@ lt:
       comment:
         none: Nėra
       created_at: Reportuotas
+      forwarded_replies_explanation: Šis ataskaita yra iš nuotolinio naudotojo ir susijusi su nuotoliniu turiniu. Jis buvo persiųstas tau, nes turinys, apie kurį pranešta, yra atsakymas vienam iš tavo naudotojų.
       mark_as_resolved: Pažymėti kaip išsprestą
       mark_as_unresolved: Pažymėti kaip neišsprestą
       notes:
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 94a1f29f7..4147078d3 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -611,6 +611,7 @@ nl:
       created_at: Gerapporteerd op
       delete_and_resolve: Bericht verwijderen
       forwarded: Doorgestuurd
+      forwarded_replies_explanation: Dit rapport komt van een externe gebruiker en gaat over externe inhoud. Het is naar u doorgestuurd omdat de gerapporteerde inhoud een reactie is op een van uw gebruikers.
       forwarded_to: Doorgestuurd naar %{domain}
       mark_as_resolved: Markeer als opgelost
       mark_as_sensitive: Als gevoelig markeren
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index 4925d4463..09de24a67 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -611,6 +611,7 @@ nn:
       created_at: Rapportert
       delete_and_resolve: Slett innlegg
       forwarded: Videresendt
+      forwarded_replies_explanation: Denne rapporten er fra en ekstern bruker og handler om eksternt innhold. Den er videresendt til deg fordi det rapporterte innholdet svarer til en av brukerne dine.
       forwarded_to: Videresendt til %{domain}
       mark_as_resolved: Merk som løyst
       mark_as_sensitive: Marker som ømtolig
diff --git a/config/locales/no.yml b/config/locales/no.yml
index a1058bf9f..3cf2df3a1 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -611,6 +611,7 @@
       created_at: Rapportert
       delete_and_resolve: Slettede innlegg
       forwarded: Videresendt
+      forwarded_replies_explanation: Denne rapporten er fra en ekstern bruker og handler om eksternt innhold. Den er videresendt til deg fordi det rapporterte innholdet svarer til en av brukerne dine.
       forwarded_to: Videresendt til %{domain}
       mark_as_resolved: Merk som løst
       mark_as_sensitive: Merk som følsomt
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 69b1aa0a9..4ff81e11e 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -635,6 +635,7 @@ pl:
       created_at: Zgłoszono
       delete_and_resolve: Usuń posty
       forwarded: Przekazano
+      forwarded_replies_explanation: Ten raport nadszedł od zdalnego użytkownika i dotyczy zdalnej treści. Został ci przekazany, bo raportowana treść jest odpowiedzią na jednego z twoich użytkowników.
       forwarded_to: Przekazano do %{domain}
       mark_as_resolved: Oznacz jako rozwiązane
       mark_as_sensitive: Oznacz jako wrażliwe
diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml
index ce7479aa8..8e147ce4c 100644
--- a/config/locales/pt-PT.yml
+++ b/config/locales/pt-PT.yml
@@ -611,6 +611,7 @@ pt-PT:
       created_at: Denunciado
       delete_and_resolve: Eliminar publicações
       forwarded: Encaminhado
+      forwarded_replies_explanation: Esta denúncia é de um utilizador remoto e sobre conteúdo remoto. Foi encaminhada para si porque o conteúdo denunciado é em resposta a um dos seus utilizadores.
       forwarded_to: Encaminhado para %{domain}
       mark_as_resolved: Marcar como resolvido
       mark_as_sensitive: Marcar como problemático
diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml
index bada4c3d0..29f2398a9 100644
--- a/config/locales/simple_form.fi.yml
+++ b/config/locales/simple_form.fi.yml
@@ -188,7 +188,7 @@ fi:
         email: Sähköpostiosoite
         expires_in: Vanhenee
         fields: Lisäkentät
-        header: Otsikkokuva
+        header: Otsakekuva
         honeypot: "%{label} (älä täytä)"
         inbox_url: Välittäjän postilaatikon URL-osoite
         irreversible: Pudota piilottamisen sijaan
diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml
index 13ea8a0c4..04b21cd1b 100644
--- a/config/locales/simple_form.he.yml
+++ b/config/locales/simple_form.he.yml
@@ -85,7 +85,7 @@ he:
         mascot: בחירת ציור למנשק הווב המתקדם.
         media_cache_retention_period: קבצי מדיה שהורדו ימחקו אחרי מספר הימים שיצוינו אם נבחר מספר חיובי, או-אז יורדו שוב מחדש בהתאם לצורך.
         peers_api_enabled: רשימת השרתים ששרת זה פגש בפדיוורס. לא כולל מידע לגבי קשר ישיר עם שרת נתון, אלא רק שידוע לשרת זה על קיומו. מידע זה משמש שירותים האוספים סטטיסטיקות כלליות על הפדרציה.
-        profile_directory: מדריך הפרופילים מפרט את כל המשתמשים שביקשו להיות ניתנים לגילוי.
+        profile_directory: ספריית הפרופילים מציגה ברשימה את כל המשתמשים שביקשו להיות ניתנים לגילוי.
         require_invite_text: כאשר הרשמות דורשות אישור ידני, הפיכת טקסט ה"מדוע את/ה רוצה להצטרף" להכרחי במקום אופציונלי
         site_contact_email: מה היא הדרך ליצור איתך קשר לצורך תמיכה או לצורך תאימות עם החוק.
         site_contact_username: כיצד יכולים אחרים ליצור איתך קשר על רשת מסטודון.
@@ -140,7 +140,7 @@ he:
         url: היעד שאליו יישלחו אירועים
     labels:
       account:
-        discoverable: חשיפת פרופיל משתמש והודעות לאלגוריתם של האתר
+        discoverable: הצג משתמש ופוסטים בעמוד התגליות
         fields:
           name: תווית
           value: תוכן
@@ -188,7 +188,7 @@ he:
         email: כתובת דוא"ל
         expires_in: תפוגה לאחר
         fields: מטא-נתונים על הפרופיל
-        header: כותרת
+        header: תמונת נושא
         honeypot: "%{label} (לא למלא)"
         inbox_url: קישורית לתיבת ממסר
         irreversible: הסרה במקום הסתרה
diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml
index 7b2656155..720012a31 100644
--- a/config/locales/simple_form.ko.yml
+++ b/config/locales/simple_form.ko.yml
@@ -4,7 +4,7 @@ ko:
     hints:
       account:
         discoverable: 내 공개 게시물과 프로필이 마스토돈의 다양한 추천 기능에 나타날 수 있고 프로필이 다른 사용자에게 제안될 수 있습니다
-        display_name: 실명 혹은 별명.
+        display_name: 진짜 이름 또는 재미난 이름.
         fields: 홈페이지, 호칭, 나이, 뭐든지 적고 싶은 것들.
         indexable: 내 공개 게시물이 마스토돈의 검색 결과에 나타날 수 있습니다. 내 게시물과 상호작용했던 사람들은 이 설정과 관계 없이 그 게시물을 검색할 수 있습니다.
         note: '남을 @mention 하거나 #hashtag 태그를 달 수 있습니다.'
@@ -195,7 +195,7 @@ ko:
         locale: 인터페이스 언어
         max_uses: 사용 횟수 제한
         new_password: 새로운 암호 입력
-        note: 소개
+        note: 자기소개
         otp_attempt: 이중 인증 코드
         password: 암호
         phrase: 키워드 또는 문장
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index bd01a8089..b6a7736df 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -1,18 +1,18 @@
 ---
 sq:
   about:
-    about_mastodon_html: 'Rrjeti shoqëror i së ardhmes: Pa reklama, pa survejim nga korporata, konceptim etik dhe decentralizim! Jini zot i të dhënave tuaja, me Mastodon-in!'
-    contact_missing: I parregulluar
+    about_mastodon_html: 'Rrjeti social i së ardhmes: Pa reklama, pa sy vëzhguese nga korporata, etik dhe i decentralizuar! Merrni sërisht zotësinë e të dhënave tuaja, me Mastodon!'
+    contact_missing: E pacaktuar
     contact_unavailable: N/A
-    hosted_on: Mastodon i strehuar në %{domain}
+    hosted_on: Server Mastodon i strehuar në %{domain}
     title: Mbi
   accounts:
     follow: Ndiqeni
     followers:
       one: Ndjekës
       other: Ndjekës
-    following: Ndjekje
-    instance_actor_flash: Kjo llogari është një aktor virtual, i përdorur për të përfaqësuar vetë shërbyesin dhe jo ndonjë përdorues individual. Përdoret për qëllime federimi dhe s’duhet pezulluar.
+    following: Po ndjek
+    instance_actor_flash: Kjo llogari është një aktor virtual, i përdorur për të përfaqësuar vetë serverin dhe jo ndonjë përdorues. Përdoret për qëllime federimi dhe s’duhet pezulluar.
     last_active: aktiv së fundi
     link_verified_on: Pronësia e kësaj lidhjeje qe kontrolluar më %{date}
     nothing_here: S’ka gjë këtu!
@@ -610,6 +610,7 @@ sq:
       created_at: Raportuar më
       delete_and_resolve: Fshiji postimet
       forwarded: U përcoll
+      forwarded_replies_explanation: Ky raportim është nga një përdorues i largët dhe rreth lënde të largët. Ju është përcjellë ngaqë lënda e raportuar gjendet në përgjigje ndaj njërit prej përdoruesve tuaj.
       forwarded_to: U përcoll te %{domain}
       mark_as_resolved: Vëri shenjë si i zgjidhur
       mark_as_sensitive: Vëri shenjë si rezervat
@@ -925,6 +926,7 @@ sq:
         peaked_on_and_decaying: Kulmoi më %{date}, tani në rënie
         title: Hashtag-ë në modë
         trendable: Mund të shfaqet nën të modës
+        trending_rank: 'Trending #%{rank}'
         usable: Mund të përdoret
         usage_comparison: Përdorur %{today} herë sot, krahasuar me %{yesterday} dje
         used_by_over_week:
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 661899896..79d668d3e 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -599,6 +599,7 @@ th:
       created_at: รายงานเมื่อ
       delete_and_resolve: ลบโพสต์
       forwarded: ส่งต่อแล้ว
+      forwarded_replies_explanation: รายงานนี้มาจากผู้ใช้ระยะไกล และเป็นรายงานเกี่ยวกับเนื้อหาระยะไกล ซึ่งถูกส่งต่อมาหาคุณเนื่องจากเนื้อหาที่ถูกรายงานอยู่ในการตอบกลับไปยังหนึ่งในผู้ใช้ของคุณ
       forwarded_to: ส่งต่อไปยัง %{domain} แล้ว
       mark_as_resolved: ทำเครื่องหมายว่าแก้ปัญหาแล้ว
       mark_as_sensitive: ทำเครื่องหมายว่าละเอียดอ่อน
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 2261c647b..e9eee14a1 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -635,6 +635,7 @@ uk:
       created_at: Створено
       delete_and_resolve: Видалити дописи
       forwarded: Переслано
+      forwarded_replies_explanation: Цей звіт належить віддаленому користувачеві і про віддалений вміст. Контент був пересланий вам, тому що він містить повідомлення у відповідь одному з ваших користувачів.
       forwarded_to: Переслано до %{domain}
       mark_as_resolved: Позначити вирішеним
       mark_as_sensitive: Позначити делікатним
diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml
index f13cedad6..5dd0d2e61 100644
--- a/config/locales/zh-HK.yml
+++ b/config/locales/zh-HK.yml
@@ -599,6 +599,7 @@ zh-HK:
       created_at: 日期
       delete_and_resolve: 刪除帖文
       forwarded: 已轉寄
+      forwarded_replies_explanation: 這份檢舉來自一位遠端使用者,並涉及遠端內容。之所以轉交給你,是因為被檢舉的內容是回覆你其中一位使用者。
       forwarded_to: 已轉寄到 %{domain}
       mark_as_resolved: 標示為「已處理」
       mark_as_sensitive: 標記為敏感內容
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 7259afdbe..2f65855fc 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -70,7 +70,7 @@ zh-TW:
       enabled_msg: 成功解除 %{username} 帳號的凍結
       followers: 跟隨者
       follows: 正在跟隨
-      header: 開頭
+      header: 封面圖片
       inbox_url: 收件匣 (Inbox) URL
       invite_request_text: 加入原因
       invited_by: 邀請者
@@ -117,7 +117,7 @@ zh-TW:
       remote_suspension_irreversible: 此帳號之資料已被不可逆地刪除。
       remote_suspension_reversible_hint_html: 這個帳號已於此伺服器被停權,所有資料將會於 %{date} 被刪除。於此之前,遠端伺服器可以完全回復此的帳號。如果您想即時刪除這個帳號的資料,您能於下面進行操作。
       remove_avatar: 取消大頭貼
-      remove_header: 移除開頭
+      remove_header: 移除封面圖片
       removed_avatar_msg: 已成功刪除 %{username} 的大頭貼
       removed_header_msg: 已成功刪除 %{username} 的封面圖片
       resend_confirmation:
@@ -599,6 +599,7 @@ zh-TW:
       created_at: 日期
       delete_and_resolve: 刪除嘟文
       forwarded: 已轉寄
+      forwarded_replies_explanation: 此報告來自聯邦宇宙中非本伺服器帳號,關於非本伺服器內容。此報告轉發給您,因為報告之內容是回覆給您的伺服器上某位使用者。
       forwarded_to: 轉寄到 %{domain}
       mark_as_resolved: 標記為「已解決」
       mark_as_sensitive: 標記為敏感內容

From 87696ea26e832fda3cb7a335db6c5899c5ddbf6d Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 16 Nov 2023 11:23:14 +0100
Subject: [PATCH 49/63] Add prominent share/copy button on profiles in web UI
 (#27865)

---
 .../mastodon/components/copy_icon_button.jsx  | 44 +++++++++++++++++++
 .../features/account/components/header.jsx    | 23 ++++++----
 app/javascript/mastodon/locales/en.json       |  2 +
 .../styles/mastodon/components.scss           | 21 +++++++++
 4 files changed, 81 insertions(+), 9 deletions(-)
 create mode 100644 app/javascript/mastodon/components/copy_icon_button.jsx

diff --git a/app/javascript/mastodon/components/copy_icon_button.jsx b/app/javascript/mastodon/components/copy_icon_button.jsx
new file mode 100644
index 000000000..9b1a36d83
--- /dev/null
+++ b/app/javascript/mastodon/components/copy_icon_button.jsx
@@ -0,0 +1,44 @@
+import PropTypes from 'prop-types';
+import { useState, useCallback } from 'react';
+
+import { defineMessages } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { useDispatch } from 'react-redux';
+
+import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/outlined/content_copy.svg';
+
+import { showAlert } from 'mastodon/actions/alerts';
+import { IconButton } from 'mastodon/components/icon_button';
+
+const messages = defineMessages({
+  copied: { id: 'copy_icon_button.copied', defaultMessage: 'Copied to clipboard' },
+});
+
+export const CopyIconButton = ({ title, value, className }) => {
+  const [copied, setCopied] = useState(false);
+  const dispatch = useDispatch();
+
+  const handleClick = useCallback(() => {
+    navigator.clipboard.writeText(value);
+    setCopied(true);
+    dispatch(showAlert({ message: messages.copied }));
+    setTimeout(() => setCopied(false), 700);
+  }, [setCopied, value, dispatch]);
+
+  return (
+    <IconButton
+      className={classNames(className, copied ? 'copied' : 'copyable')}
+      title={title}
+      onClick={handleClick}
+      iconComponent={ContentCopyIcon}
+    />
+  );
+};
+
+CopyIconButton.propTypes = {
+  title: PropTypes.string,
+  value: PropTypes.string,
+  className: PropTypes.string,
+};
diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx
index 7594135a4..29b46cb43 100644
--- a/app/javascript/mastodon/features/account/components/header.jsx
+++ b/app/javascript/mastodon/features/account/components/header.jsx
@@ -14,10 +14,12 @@ import { ReactComponent as LockIcon } from '@material-symbols/svg-600/outlined/l
 import { ReactComponent as MoreHorizIcon } from '@material-symbols/svg-600/outlined/more_horiz.svg';
 import { ReactComponent as NotificationsIcon } from '@material-symbols/svg-600/outlined/notifications.svg';
 import { ReactComponent as NotificationsActiveIcon } from '@material-symbols/svg-600/outlined/notifications_active-fill.svg';
+import { ReactComponent as ShareIcon } from '@material-symbols/svg-600/outlined/share.svg';
 
 import { Avatar } from 'mastodon/components/avatar';
 import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
 import { Button } from 'mastodon/components/button';
+import { CopyIconButton } from 'mastodon/components/copy_icon_button';
 import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
 import { Icon }  from 'mastodon/components/icon';
 import { IconButton } from 'mastodon/components/icon_button';
@@ -46,6 +48,7 @@ const messages = defineMessages({
   mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
   report: { id: 'account.report', defaultMessage: 'Report @{name}' },
   share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
+  copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' },
   media: { id: 'account.media', defaultMessage: 'Media' },
   blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
   unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
@@ -245,11 +248,10 @@ class Header extends ImmutablePureComponent {
     const isRemote     = account.get('acct') !== account.get('username');
     const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null;
 
-    let info        = [];
-    let actionBtn   = '';
-    let bellBtn     = '';
-    let lockedIcon  = '';
-    let menu        = [];
+    let actionBtn, bellBtn, lockedIcon, shareBtn;
+
+    let info = [];
+    let menu = [];
 
     if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
       info.push(<span key='followed_by' className='relationship-tag'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>);
@@ -267,6 +269,12 @@ class Header extends ImmutablePureComponent {
       bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} iconComponent={account.getIn(['relationship', 'notifying']) ? NotificationsActiveIcon : NotificationsIcon} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
     }
 
+    if ('share' in navigator) {
+      shareBtn = <IconButton className='optional' iconComponent={ShareIcon} title={intl.formatMessage(messages.share, { name: account.get('username') })} onClick={this.handleShare} />;
+    } else {
+      shareBtn = <CopyIconButton className='optional' title={intl.formatMessage(messages.copy)} value={account.get('url')} />;
+    }
+
     if (me !== account.get('id')) {
       if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
         actionBtn = '';
@@ -297,10 +305,6 @@ class Header extends ImmutablePureComponent {
 
     if (isRemote) {
       menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
-    }
-
-    if ('share' in navigator && !account.get('suspended')) {
-      menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
       menu.push(null);
     }
 
@@ -414,6 +418,7 @@ class Header extends ImmutablePureComponent {
                 <>
                   {actionBtn}
                   {bellBtn}
+                  {shareBtn}
                 </>
               )}
 
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 041446037..16941e2ca 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -21,6 +21,7 @@
   "account.blocked": "Blocked",
   "account.browse_more_on_origin_server": "Browse more on the original profile",
   "account.cancel_follow_request": "Cancel follow",
+  "account.copy": "Copy link to profile",
   "account.direct": "Privately mention @{name}",
   "account.disable_notifications": "Stop notifying me when @{name} posts",
   "account.domain_blocked": "Domain blocked",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Mark as read",
   "conversation.open": "View conversation",
   "conversation.with": "With {names}",
+  "copy_icon_button.copied": "Copied to clipboard",
   "copypaste.copied": "Copied",
   "copypaste.copy_to_clipboard": "Copy to clipboard",
   "directory.federated": "From known fediverse",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 9f87352f5..cc9b54d9e 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -286,6 +286,17 @@
     font-size: 12px;
     font-weight: 500;
   }
+
+  &.copyable {
+    transition: all 300ms linear;
+  }
+
+  &.copied {
+    border-color: $valid-value-color;
+    color: $valid-value-color;
+    transition: none;
+    background-color: rgba($valid-value-color, 0.15);
+  }
 }
 
 .text-icon-button {
@@ -7373,6 +7384,16 @@ noscript {
           width: 24px;
           height: 24px;
         }
+
+        &.copied {
+          border-color: $valid-value-color;
+        }
+      }
+
+      @media screen and (width <= 427px) {
+        .optional {
+          display: none;
+        }
       }
     }
 

From c94bedf4e6e5987864b42d63aa90920f80d3644e Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 16 Nov 2023 12:59:45 +0100
Subject: [PATCH 50/63] Use container queries to hide profile share button
 (#27889)

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

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index cc9b54d9e..8a79eddf0 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -7289,6 +7289,7 @@ noscript {
 
 .account__header {
   overflow: hidden;
+  container: account-header / inline-size;
 
   &.inactive {
     opacity: 0.5;
@@ -7390,7 +7391,7 @@ noscript {
         }
       }
 
-      @media screen and (width <= 427px) {
+      @container account-header (max-width: 372px) {
         .optional {
           display: none;
         }

From 0a6ec048a8123a26ba9e00896cecbd03c5e3a990 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 16 Nov 2023 14:43:02 +0100
Subject: [PATCH 51/63] Fix upper border radius of onboarding columns (#27890)

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

diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 8a79eddf0..9c3d9dc2c 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2299,8 +2299,7 @@ $ui-header-height: 55px;
 
   > .scrollable {
     background: $ui-base-color;
-    border-bottom-left-radius: 4px;
-    border-bottom-right-radius: 4px;
+    border-radius: 0 0 4px 4px;
   }
 }
 

From 155fb8414150e78b4e61aa33d483cc7713161134 Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 16 Nov 2023 09:36:59 -0500
Subject: [PATCH 52/63] Improve spec coverage for collection of `workers/`
 classes (#27874)

---
 .../account_deletion_request_fabricator.rb    |  5 ++
 spec/fabricators/import_fabricator.rb         |  7 +++
 spec/workers/account_refresh_worker_spec.rb   | 52 ++++++++++++++++
 .../activitypub/post_upgrade_worker_spec.rb   | 18 ++++++
 ...ze_featured_tags_collection_worker_spec.rb | 29 +++++++++
 spec/workers/admin/suspension_worker_spec.rb  | 28 +++++++++
 .../after_account_domain_block_worker_spec.rb | 29 +++++++++
 spec/workers/backup_worker_spec.rb            | 36 +++++++++++
 spec/workers/delete_mute_worker_spec.rb       | 42 +++++++++++++
 spec/workers/feed_insert_worker_spec.rb       | 21 ++++++-
 spec/workers/import_worker_spec.rb            | 23 +++++++
 .../workers/post_process_media_worker_spec.rb | 34 +++++++++--
 ...blish_announcement_reaction_worker_spec.rb | 38 ++++++++++++
 spec/workers/removal_worker_spec.rb           | 28 +++++++++
 .../scheduler/self_destruct_scheduler_spec.rb | 60 +++++++++++++++++++
 spec/workers/webhooks/delivery_worker_spec.rb | 18 +++++-
 16 files changed, 460 insertions(+), 8 deletions(-)
 create mode 100644 spec/fabricators/account_deletion_request_fabricator.rb
 create mode 100644 spec/fabricators/import_fabricator.rb
 create mode 100644 spec/workers/account_refresh_worker_spec.rb
 create mode 100644 spec/workers/activitypub/post_upgrade_worker_spec.rb
 create mode 100644 spec/workers/activitypub/synchronize_featured_tags_collection_worker_spec.rb
 create mode 100644 spec/workers/admin/suspension_worker_spec.rb
 create mode 100644 spec/workers/after_account_domain_block_worker_spec.rb
 create mode 100644 spec/workers/backup_worker_spec.rb
 create mode 100644 spec/workers/delete_mute_worker_spec.rb
 create mode 100644 spec/workers/import_worker_spec.rb
 create mode 100644 spec/workers/publish_announcement_reaction_worker_spec.rb
 create mode 100644 spec/workers/removal_worker_spec.rb
 create mode 100644 spec/workers/scheduler/self_destruct_scheduler_spec.rb

diff --git a/spec/fabricators/account_deletion_request_fabricator.rb b/spec/fabricators/account_deletion_request_fabricator.rb
new file mode 100644
index 000000000..3d3d37398
--- /dev/null
+++ b/spec/fabricators/account_deletion_request_fabricator.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Fabricator(:account_deletion_request) do
+  account
+end
diff --git a/spec/fabricators/import_fabricator.rb b/spec/fabricators/import_fabricator.rb
new file mode 100644
index 000000000..4951bb9a4
--- /dev/null
+++ b/spec/fabricators/import_fabricator.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+Fabricator(:import) do
+  account
+  type :following
+  data { attachment_fixture('imports.txt') }
+end
diff --git a/spec/workers/account_refresh_worker_spec.rb b/spec/workers/account_refresh_worker_spec.rb
new file mode 100644
index 000000000..361d69aa0
--- /dev/null
+++ b/spec/workers/account_refresh_worker_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AccountRefreshWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(ResolveAccountService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(ResolveAccountService).to receive(:new).and_return(service)
+    end
+
+    context 'when account does not exist' do
+      it 'returns immediately without processing' do
+        worker.perform(123_123_123)
+
+        expect(service).to_not have_received(:call)
+      end
+    end
+
+    context 'when account exists' do
+      context 'when account does not need refreshing' do
+        let(:account) { Fabricate(:account, last_webfingered_at: recent_webfinger_at) }
+
+        it 'returns immediately without processing' do
+          worker.perform(account.id)
+
+          expect(service).to_not have_received(:call)
+        end
+      end
+
+      context 'when account needs refreshing' do
+        let(:account) { Fabricate(:account, last_webfingered_at: outdated_webfinger_at) }
+
+        it 'schedules an account update' do
+          worker.perform(account.id)
+
+          expect(service).to have_received(:call)
+        end
+      end
+
+      def recent_webfinger_at
+        (Account::BACKGROUND_REFRESH_INTERVAL - 3.days).ago
+      end
+
+      def outdated_webfinger_at
+        (Account::BACKGROUND_REFRESH_INTERVAL + 3.days).ago
+      end
+    end
+  end
+end
diff --git a/spec/workers/activitypub/post_upgrade_worker_spec.rb b/spec/workers/activitypub/post_upgrade_worker_spec.rb
new file mode 100644
index 000000000..08de150ad
--- /dev/null
+++ b/spec/workers/activitypub/post_upgrade_worker_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ActivityPub::PostUpgradeWorker do
+  let(:worker) { described_class.new }
+
+  describe '#perform' do
+    let(:domain) { 'host.example' }
+
+    it 'updates relevant values' do
+      account = Fabricate(:account, domain: domain, last_webfingered_at: 1.day.ago, protocol: :ostatus)
+      worker.perform(domain)
+
+      expect(account.reload.last_webfingered_at).to be_nil
+    end
+  end
+end
diff --git a/spec/workers/activitypub/synchronize_featured_tags_collection_worker_spec.rb b/spec/workers/activitypub/synchronize_featured_tags_collection_worker_spec.rb
new file mode 100644
index 000000000..8cf13cb90
--- /dev/null
+++ b/spec/workers/activitypub/synchronize_featured_tags_collection_worker_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ActivityPub::SynchronizeFeaturedTagsCollectionWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(ActivityPub::FetchFeaturedTagsCollectionService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(ActivityPub::FetchFeaturedTagsCollectionService).to receive(:new).and_return(service)
+    end
+
+    let(:account) { Fabricate(:account) }
+    let(:url) { 'https://host.example' }
+
+    it 'sends the account and url to the service' do
+      worker.perform(account.id, url)
+
+      expect(service).to have_received(:call).with(account, url)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123, url)
+
+      expect(result).to be(true)
+    end
+  end
+end
diff --git a/spec/workers/admin/suspension_worker_spec.rb b/spec/workers/admin/suspension_worker_spec.rb
new file mode 100644
index 000000000..da12037ed
--- /dev/null
+++ b/spec/workers/admin/suspension_worker_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::SuspensionWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(SuspendAccountService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(SuspendAccountService).to receive(:new).and_return(service)
+    end
+
+    let(:account) { Fabricate(:account) }
+
+    it 'sends the account to the service' do
+      worker.perform(account.id)
+
+      expect(service).to have_received(:call).with(account)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123)
+
+      expect(result).to be(true)
+    end
+  end
+end
diff --git a/spec/workers/after_account_domain_block_worker_spec.rb b/spec/workers/after_account_domain_block_worker_spec.rb
new file mode 100644
index 000000000..54a113a2b
--- /dev/null
+++ b/spec/workers/after_account_domain_block_worker_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AfterAccountDomainBlockWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(AfterBlockDomainFromAccountService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(AfterBlockDomainFromAccountService).to receive(:new).and_return(service)
+    end
+
+    let(:account) { Fabricate(:account) }
+    let(:domain) { 'host.example' }
+
+    it 'sends the account and domain to the service' do
+      worker.perform(account.id, domain)
+
+      expect(service).to have_received(:call).with(account, domain)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123, domain)
+
+      expect(result).to be(true)
+    end
+  end
+end
diff --git a/spec/workers/backup_worker_spec.rb b/spec/workers/backup_worker_spec.rb
new file mode 100644
index 000000000..1a169513e
--- /dev/null
+++ b/spec/workers/backup_worker_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe BackupWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(BackupService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(BackupService).to receive(:new).and_return(service)
+    end
+
+    let(:backup) { Fabricate(:backup) }
+    let!(:other_backup) { Fabricate(:backup, user: backup.user) }
+
+    it 'sends the backup to the service and removes other backups' do
+      expect do
+        worker.perform(backup.id)
+      end.to change(UserMailer.deliveries, :size).by(1)
+
+      expect(service).to have_received(:call).with(backup)
+      expect { other_backup.reload }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+
+    context 'when sidekiq retries are exhausted' do
+      it 'destroys the backup' do
+        described_class.within_sidekiq_retries_exhausted_block({ 'args' => [backup.id] }) do
+          worker.perform(backup.id)
+        end
+
+        expect { backup.reload }.to raise_error(ActiveRecord::RecordNotFound)
+      end
+    end
+  end
+end
diff --git a/spec/workers/delete_mute_worker_spec.rb b/spec/workers/delete_mute_worker_spec.rb
new file mode 100644
index 000000000..1fc84491c
--- /dev/null
+++ b/spec/workers/delete_mute_worker_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe DeleteMuteWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(UnmuteService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(UnmuteService).to receive(:new).and_return(service)
+    end
+
+    context 'with an expired mute' do
+      let(:mute) { Fabricate(:mute, expires_at: 1.day.ago) }
+
+      it 'sends the mute to the service' do
+        worker.perform(mute.id)
+
+        expect(service).to have_received(:call).with(mute.account, mute.target_account)
+      end
+    end
+
+    context 'with an unexpired mute' do
+      let(:mute) { Fabricate(:mute, expires_at: 1.day.from_now) }
+
+      it 'does not send the mute to the service' do
+        worker.perform(mute.id)
+
+        expect(service).to_not have_received(:call)
+      end
+    end
+
+    context 'with a non-existent mute' do
+      it 'does not send the mute to the service' do
+        worker.perform(123_123_123)
+
+        expect(service).to_not have_received(:call)
+      end
+    end
+  end
+end
diff --git a/spec/workers/feed_insert_worker_spec.rb b/spec/workers/feed_insert_worker_spec.rb
index 97c73c599..e9484879f 100644
--- a/spec/workers/feed_insert_worker_spec.rb
+++ b/spec/workers/feed_insert_worker_spec.rb
@@ -8,6 +8,7 @@ describe FeedInsertWorker do
   describe 'perform' do
     let(:follower) { Fabricate(:account) }
     let(:status) { Fabricate(:status) }
+    let(:list) { Fabricate(:list) }
 
     context 'when there are no records' do
       it 'skips push with missing status' do
@@ -42,11 +43,29 @@ describe FeedInsertWorker do
       it 'pushes the status onto the home timeline without filter' do
         instance = instance_double(FeedManager, push_to_home: nil, filter?: false)
         allow(FeedManager).to receive(:instance).and_return(instance)
-        result = subject.perform(status.id, follower.id)
+        result = subject.perform(status.id, follower.id, :home)
 
         expect(result).to be_nil
         expect(instance).to have_received(:push_to_home).with(follower, status, update: nil)
       end
+
+      it 'pushes the status onto the tags timeline without filter' do
+        instance = instance_double(FeedManager, push_to_home: nil, filter?: false)
+        allow(FeedManager).to receive(:instance).and_return(instance)
+        result = subject.perform(status.id, follower.id, :tags)
+
+        expect(result).to be_nil
+        expect(instance).to have_received(:push_to_home).with(follower, status, update: nil)
+      end
+
+      it 'pushes the status onto the list timeline without filter' do
+        instance = instance_double(FeedManager, push_to_list: nil, filter?: false)
+        allow(FeedManager).to receive(:instance).and_return(instance)
+        result = subject.perform(status.id, list.id, :list)
+
+        expect(result).to be_nil
+        expect(instance).to have_received(:push_to_list).with(list, status, update: nil)
+      end
     end
   end
 end
diff --git a/spec/workers/import_worker_spec.rb b/spec/workers/import_worker_spec.rb
new file mode 100644
index 000000000..4095a5d35
--- /dev/null
+++ b/spec/workers/import_worker_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ImportWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(ImportService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(ImportService).to receive(:new).and_return(service)
+    end
+
+    let(:import) { Fabricate(:import) }
+
+    it 'sends the import to the service' do
+      worker.perform(import.id)
+
+      expect(service).to have_received(:call).with(import)
+      expect { import.reload }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+  end
+end
diff --git a/spec/workers/post_process_media_worker_spec.rb b/spec/workers/post_process_media_worker_spec.rb
index 33072704b..828da5244 100644
--- a/spec/workers/post_process_media_worker_spec.rb
+++ b/spec/workers/post_process_media_worker_spec.rb
@@ -2,12 +2,38 @@
 
 require 'rails_helper'
 
-describe PostProcessMediaWorker do
+describe PostProcessMediaWorker, :paperclip_processing do
   let(:worker) { described_class.new }
 
-  describe 'perform' do
-    it 'runs without error for missing record' do
-      expect { worker.perform(nil) }.to_not raise_error
+  describe '#perform' do
+    let(:media_attachment) { Fabricate(:media_attachment) }
+
+    it 'reprocesses and updates the media attachment' do
+      worker.perform(media_attachment.id)
+
+      expect(media_attachment.processing).to eq('complete')
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123)
+
+      expect(result).to be(true)
+    end
+
+    context 'when sidekiq retries are exhausted' do
+      it 'sets state to failed' do
+        described_class.within_sidekiq_retries_exhausted_block({ 'args' => [media_attachment.id] }) do
+          worker.perform(media_attachment.id)
+        end
+
+        expect(media_attachment.reload.processing).to eq('failed')
+      end
+
+      it 'returns true for non-existent record' do
+        described_class.within_sidekiq_retries_exhausted_block({ 'args' => [123_123_123] }) do
+          expect(worker.perform(123_123_123)).to be(true)
+        end
+      end
     end
   end
 end
diff --git a/spec/workers/publish_announcement_reaction_worker_spec.rb b/spec/workers/publish_announcement_reaction_worker_spec.rb
new file mode 100644
index 000000000..91668b5ad
--- /dev/null
+++ b/spec/workers/publish_announcement_reaction_worker_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PublishAnnouncementReactionWorker do
+  let(:worker) { described_class.new }
+
+  describe '#perform' do
+    before { Fabricate(:account, user: Fabricate(:user, current_sign_in_at: 1.hour.ago)) }
+
+    let(:announcement) { Fabricate(:announcement) }
+    let(:name) { 'name value' }
+
+    it 'sends the announcement and name to the service when subscribed' do
+      allow(redis).to receive(:exists?).and_return(true)
+      allow(redis).to receive(:publish)
+
+      worker.perform(announcement.id, name)
+
+      expect(redis).to have_received(:publish)
+    end
+
+    it 'does not send the announcement and name to the service when not subscribed' do
+      allow(redis).to receive(:exists?).and_return(false)
+      allow(redis).to receive(:publish)
+
+      worker.perform(announcement.id, name)
+
+      expect(redis).to_not have_received(:publish)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123, name)
+
+      expect(result).to be(true)
+    end
+  end
+end
diff --git a/spec/workers/removal_worker_spec.rb b/spec/workers/removal_worker_spec.rb
new file mode 100644
index 000000000..5071e882b
--- /dev/null
+++ b/spec/workers/removal_worker_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe RemovalWorker do
+  let(:worker) { described_class.new }
+  let(:service) { instance_double(RemoveStatusService, call: true) }
+
+  describe '#perform' do
+    before do
+      allow(RemoveStatusService).to receive(:new).and_return(service)
+    end
+
+    let(:status) { Fabricate(:status) }
+
+    it 'sends the status to the service' do
+      worker.perform(status.id)
+
+      expect(service).to have_received(:call).with(status)
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123)
+
+      expect(result).to be(true)
+    end
+  end
+end
diff --git a/spec/workers/scheduler/self_destruct_scheduler_spec.rb b/spec/workers/scheduler/self_destruct_scheduler_spec.rb
new file mode 100644
index 000000000..2bf578357
--- /dev/null
+++ b/spec/workers/scheduler/self_destruct_scheduler_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::SelfDestructScheduler do
+  let(:worker) { described_class.new }
+
+  describe '#perform' do
+    let!(:account) { Fabricate(:account, domain: nil, suspended_at: nil) }
+
+    context 'when not in self destruct mode' do
+      before do
+        allow(SelfDestructHelper).to receive(:self_destruct?).and_return(false)
+      end
+
+      it 'returns without processing' do
+        worker.perform
+
+        expect(account.reload.suspended_at).to be_nil
+      end
+    end
+
+    context 'when in self-destruct mode' do
+      before do
+        allow(SelfDestructHelper).to receive(:self_destruct?).and_return(true)
+      end
+
+      context 'when sidekiq is overwhelmed' do
+        before do
+          stats = instance_double(Sidekiq::Stats, enqueued: described_class::MAX_ENQUEUED**2)
+          allow(Sidekiq::Stats).to receive(:new).and_return(stats)
+        end
+
+        it 'returns without processing' do
+          worker.perform
+
+          expect(account.reload.suspended_at).to be_nil
+        end
+      end
+
+      context 'when sidekiq is operational' do
+        it 'suspends local non-suspended accounts' do
+          worker.perform
+
+          expect(account.reload.suspended_at).to_not be_nil
+        end
+
+        it 'suspends local suspended accounts marked for deletion' do
+          account.update(suspended_at: 10.days.ago)
+          deletion_request = Fabricate(:account_deletion_request, account: account)
+
+          worker.perform
+
+          expect(account.reload.suspended_at).to be > 1.day.ago
+          expect { deletion_request.reload }.to raise_error(ActiveRecord::RecordNotFound)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/workers/webhooks/delivery_worker_spec.rb b/spec/workers/webhooks/delivery_worker_spec.rb
index daf8a3e28..6a5483d1d 100644
--- a/spec/workers/webhooks/delivery_worker_spec.rb
+++ b/spec/workers/webhooks/delivery_worker_spec.rb
@@ -5,9 +5,21 @@ require 'rails_helper'
 describe Webhooks::DeliveryWorker do
   let(:worker) { described_class.new }
 
-  describe 'perform' do
-    it 'runs without error' do
-      expect { worker.perform(nil, nil) }.to_not raise_error
+  describe '#perform' do
+    let(:webhook) { Fabricate(:webhook) }
+
+    it 'reprocesses and updates the webhook' do
+      stub_request(:post, webhook.url).to_return(status: 200, body: '')
+
+      worker.perform(webhook.id, 'body')
+
+      expect(a_request(:post, webhook.url)).to have_been_made.at_least_once
+    end
+
+    it 'returns true for non-existent record' do
+      result = worker.perform(123_123_123, '')
+
+      expect(result).to be(true)
     end
   end
 end

From cb1a4a8713499a0ca7d8e53ce5cfd32939247f0c Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Thu, 16 Nov 2023 09:37:52 -0500
Subject: [PATCH 53/63] Reduce expectations for `RSpec/MultipleExpectations`
 cop in `spec/presenters` specs (#27881)

---
 .../account_relationships_presenter_spec.rb   | 14 +++--
 .../familiar_followers_presenter_spec.rb      | 27 ++++++---
 .../status_relationships_presenter_spec.rb    | 60 +++++++++++++------
 3 files changed, 69 insertions(+), 32 deletions(-)

diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb
index 5c2ba54e0..5b05ac800 100644
--- a/spec/presenters/account_relationships_presenter_spec.rb
+++ b/spec/presenters/account_relationships_presenter_spec.rb
@@ -23,12 +23,14 @@ RSpec.describe AccountRelationshipsPresenter do
       let(:options) { {} }
 
       it 'sets default maps' do
-        expect(presenter.following).to       eq default_map
-        expect(presenter.followed_by).to     eq default_map
-        expect(presenter.blocking).to        eq default_map
-        expect(presenter.muting).to          eq default_map
-        expect(presenter.requested).to       eq default_map
-        expect(presenter.domain_blocking).to eq default_map
+        expect(presenter).to have_attributes(
+          following: default_map,
+          followed_by: default_map,
+          blocking: default_map,
+          muting: default_map,
+          requested: default_map,
+          domain_blocking: default_map
+        )
       end
     end
 
diff --git a/spec/presenters/familiar_followers_presenter_spec.rb b/spec/presenters/familiar_followers_presenter_spec.rb
index c21ffd36e..853babb84 100644
--- a/spec/presenters/familiar_followers_presenter_spec.rb
+++ b/spec/presenters/familiar_followers_presenter_spec.rb
@@ -22,9 +22,12 @@ RSpec.describe FamiliarFollowersPresenter do
     it 'returns followers you follow' do
       result = subject.accounts.first
 
-      expect(result).to_not be_nil
-      expect(result.id).to eq requested_accounts.first.id
-      expect(result.accounts).to contain_exactly(familiar_follower)
+      expect(result)
+        .to be_present
+        .and have_attributes(
+          id: requested_accounts.first.id,
+          accounts: contain_exactly(familiar_follower)
+        )
     end
 
     context 'when requested account hides followers' do
@@ -35,9 +38,12 @@ RSpec.describe FamiliarFollowersPresenter do
       it 'does not return followers you follow' do
         result = subject.accounts.first
 
-        expect(result).to_not be_nil
-        expect(result.id).to eq requested_accounts.first.id
-        expect(result.accounts).to be_empty
+        expect(result)
+          .to be_present
+          .and have_attributes(
+            id: requested_accounts.first.id,
+            accounts: be_empty
+          )
       end
     end
 
@@ -49,9 +55,12 @@ RSpec.describe FamiliarFollowersPresenter do
       it 'does not return followers you follow' do
         result = subject.accounts.first
 
-        expect(result).to_not be_nil
-        expect(result.id).to eq requested_accounts.first.id
-        expect(result.accounts).to be_empty
+        expect(result)
+          .to be_present
+          .and have_attributes(
+            id: requested_accounts.first.id,
+            accounts: be_empty
+          )
       end
     end
   end
diff --git a/spec/presenters/status_relationships_presenter_spec.rb b/spec/presenters/status_relationships_presenter_spec.rb
index 7746c8cd7..af6a93b82 100644
--- a/spec/presenters/status_relationships_presenter_spec.rb
+++ b/spec/presenters/status_relationships_presenter_spec.rb
@@ -22,11 +22,13 @@ RSpec.describe StatusRelationshipsPresenter do
       let(:options) { {} }
 
       it 'sets default maps' do
-        expect(presenter.reblogs_map).to    eq default_map
-        expect(presenter.favourites_map).to eq default_map
-        expect(presenter.bookmarks_map).to  eq default_map
-        expect(presenter.mutes_map).to      eq default_map
-        expect(presenter.pins_map).to       eq default_map
+        expect(presenter).to have_attributes(
+          reblogs_map: eq(default_map),
+          favourites_map: eq(default_map),
+          bookmarks_map: eq(default_map),
+          mutes_map: eq(default_map),
+          pins_map: eq(default_map)
+        )
       end
     end
 
@@ -80,18 +82,30 @@ RSpec.describe StatusRelationshipsPresenter do
 
       it 'sets @filters_map to filter top-level status' do
         matched_filters = presenter.filters_map[statuses[0].id]
-        expect(matched_filters.size).to eq 1
 
-        expect(matched_filters[0].filter.title).to eq 'filter1'
-        expect(matched_filters[0].keyword_matches).to eq ['banned']
+        expect(matched_filters)
+          .to be_an(Array)
+          .and have_attributes(size: 1)
+          .and contain_exactly(
+            have_attributes(
+              filter: have_attributes(title: 'filter1'),
+              keyword_matches: contain_exactly('banned')
+            )
+          )
       end
 
       it 'sets @filters_map to filter reblogged status' do
         matched_filters = presenter.filters_map[statuses[1].reblog_of_id]
-        expect(matched_filters.size).to eq 1
 
-        expect(matched_filters[0].filter.title).to eq 'filter1'
-        expect(matched_filters[0].keyword_matches).to eq ['irrelevant']
+        expect(matched_filters)
+          .to be_an(Array)
+          .and have_attributes(size: 1)
+          .and contain_exactly(
+            have_attributes(
+              filter: have_attributes(title: 'filter1'),
+              keyword_matches: contain_exactly('irrelevant')
+            )
+          )
       end
     end
 
@@ -107,18 +121,30 @@ RSpec.describe StatusRelationshipsPresenter do
 
       it 'sets @filters_map to filter top-level status' do
         matched_filters = presenter.filters_map[statuses[0].id]
-        expect(matched_filters.size).to eq 1
 
-        expect(matched_filters[0].filter.title).to eq 'filter1'
-        expect(matched_filters[0].status_matches).to eq [statuses[0].id]
+        expect(matched_filters)
+          .to be_an(Array)
+          .and have_attributes(size: 1)
+          .and contain_exactly(
+            have_attributes(
+              filter: have_attributes(title: 'filter1'),
+              status_matches: contain_exactly(statuses.first.id)
+            )
+          )
       end
 
       it 'sets @filters_map to filter reblogged status' do
         matched_filters = presenter.filters_map[statuses[1].reblog_of_id]
-        expect(matched_filters.size).to eq 1
 
-        expect(matched_filters[0].filter.title).to eq 'filter1'
-        expect(matched_filters[0].status_matches).to eq [statuses[1].reblog_of_id]
+        expect(matched_filters)
+          .to be_an(Array)
+          .and have_attributes(size: 1)
+          .and contain_exactly(
+            have_attributes(
+              filter: have_attributes(title: 'filter1'),
+              status_matches: contain_exactly(statuses.second.reblog_of_id)
+            )
+          )
       end
     end
   end

From 94178e2f68db9d23b1be1f3b39489564fcb9d50e Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 17 Nov 2023 09:56:23 +0100
Subject: [PATCH 54/63] Update dependency react-redux-loading-bar to v5.0.5
 (#27916)

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

diff --git a/yarn.lock b/yarn.lock
index 32c2546e7..b55bbde3e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13777,17 +13777,17 @@ __metadata:
   linkType: hard
 
 "react-redux-loading-bar@npm:^5.0.4":
-  version: 5.0.4
-  resolution: "react-redux-loading-bar@npm:5.0.4"
+  version: 5.0.5
+  resolution: "react-redux-loading-bar@npm:5.0.5"
   dependencies:
     prop-types: "npm:^15.7.2"
     react-lifecycles-compat: "npm:^3.0.4"
   peerDependencies:
     react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
     react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
-    react-redux: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
-    redux: ^3.0.0 || ^4.0.0
-  checksum: 11eea2ef6dfae232e278eceb83d07f9f57a2ece3ef23ce888dccf24964b669c9ee83a6db12b1f3c757b5b3410f7a7ccda96a4b4216c4ad9b42bf831ccea7a4a2
+    react-redux: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
+    redux: ^3.0.0 || ^4.0.0 || ^5.0.0
+  checksum: 0dbac046c5b8b6bd209ccfc25ccc55dc9158cd737b42b68fd1900dfe46a59c9c7e2b0082d8901b749e7cf2d7e23074590aae74f350a814f205105f47895a6214
   languageName: node
   linkType: hard
 

From 6c2e78f1b11874583ae9e6988cf6d2a2eb1e1b71 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 17 Nov 2023 09:56:39 +0100
Subject: [PATCH 55/63] Update dependency @material-symbols/svg-600 to v0.14.1
 (#27907)

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 b55bbde3e..864ebfc87 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2550,9 +2550,9 @@ __metadata:
   linkType: soft
 
 "@material-symbols/svg-600@npm:^0.14.0":
-  version: 0.14.0
-  resolution: "@material-symbols/svg-600@npm:0.14.0"
-  checksum: e6547a9a0b2072f4109f2e4e0863367ea2507efce740c427a8544100db02ffff52f33608aac1a355f4977e2c0b2ce6cdd6bfee9177bb13cee0b28418f948b5a5
+  version: 0.14.1
+  resolution: "@material-symbols/svg-600@npm:0.14.1"
+  checksum: fb5252285bbeccc45a4b131e8b165470b5b57e146bc7ea586eb82e580037d1218f6dad5fee4e6822c357041ff547f34c9c7432cce0a811b14f7e41d8ae23009b
   languageName: node
   linkType: hard
 

From 1526e54ac6f08431c8c833f20b4be8882355967c Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 17 Nov 2023 04:03:46 -0500
Subject: [PATCH 56/63] Add spec coverage for `workers/redownload_*` worker
 classes (#27892)

---
 spec/workers/redownload_avatar_worker_spec.rb | 45 +++++++++++++++++--
 spec/workers/redownload_header_worker_spec.rb | 45 +++++++++++++++++--
 spec/workers/redownload_media_worker_spec.rb  | 37 +++++++++++++++
 3 files changed, 121 insertions(+), 6 deletions(-)
 create mode 100644 spec/workers/redownload_media_worker_spec.rb

diff --git a/spec/workers/redownload_avatar_worker_spec.rb b/spec/workers/redownload_avatar_worker_spec.rb
index b44ae9f03..4ab368e12 100644
--- a/spec/workers/redownload_avatar_worker_spec.rb
+++ b/spec/workers/redownload_avatar_worker_spec.rb
@@ -5,9 +5,48 @@ require 'rails_helper'
 describe RedownloadAvatarWorker do
   let(:worker) { described_class.new }
 
-  describe 'perform' do
-    it 'runs without error for missing record' do
-      expect { worker.perform(nil) }.to_not raise_error
+  describe '#perform' do
+    it 'returns nil for non-existent record' do
+      result = worker.perform(123_123_123)
+
+      expect(result).to be_nil
+    end
+
+    it 'returns nil for suspended account' do
+      account = Fabricate(:account, suspended_at: 10.days.ago)
+
+      expect(worker.perform(account.id)).to be_nil
+    end
+
+    it 'returns nil with a domain block' do
+      account = Fabricate(:account, domain: 'host.example')
+      Fabricate(:domain_block, domain: account.domain, reject_media: true)
+
+      expect(worker.perform(account.id)).to be_nil
+    end
+
+    it 'returns nil without an avatar remote url' do
+      account = Fabricate(:account, avatar_remote_url: '')
+
+      expect(worker.perform(account.id)).to be_nil
+    end
+
+    it 'returns nil when avatar file name is present' do
+      stub_request(:get, 'https://example.host/file').to_return request_fixture('avatar.txt')
+      account = Fabricate(:account, avatar_remote_url: 'https://example.host/file', avatar_file_name: 'test.jpg')
+
+      expect(worker.perform(account.id)).to be_nil
+    end
+
+    it 'reprocesses a remote avatar' do
+      stub_request(:get, 'https://example.host/file').to_return request_fixture('avatar.txt')
+      account = Fabricate(:account, avatar_remote_url: 'https://example.host/file')
+      account.update_column(:avatar_file_name, nil) # rubocop:disable Rails/SkipsModelValidations
+
+      result = worker.perform(account.id)
+
+      expect(result).to be(true)
+      expect(account.reload.avatar_file_name).to_not be_nil
     end
   end
 end
diff --git a/spec/workers/redownload_header_worker_spec.rb b/spec/workers/redownload_header_worker_spec.rb
index 767ae7a5a..3b6f497bb 100644
--- a/spec/workers/redownload_header_worker_spec.rb
+++ b/spec/workers/redownload_header_worker_spec.rb
@@ -5,9 +5,48 @@ require 'rails_helper'
 describe RedownloadHeaderWorker do
   let(:worker) { described_class.new }
 
-  describe 'perform' do
-    it 'runs without error for missing record' do
-      expect { worker.perform(nil) }.to_not raise_error
+  describe '#perform' do
+    it 'returns nil for non-existent record' do
+      result = worker.perform(123_123_123)
+
+      expect(result).to be_nil
+    end
+
+    it 'returns nil for suspended account' do
+      account = Fabricate(:account, suspended_at: 10.days.ago)
+
+      expect(worker.perform(account.id)).to be_nil
+    end
+
+    it 'returns nil with a domain block' do
+      account = Fabricate(:account, domain: 'host.example')
+      Fabricate(:domain_block, domain: account.domain, reject_media: true)
+
+      expect(worker.perform(account.id)).to be_nil
+    end
+
+    it 'returns nil without an header remote url' do
+      account = Fabricate(:account, header_remote_url: '')
+
+      expect(worker.perform(account.id)).to be_nil
+    end
+
+    it 'returns nil when header file name is present' do
+      stub_request(:get, 'https://example.host/file').to_return request_fixture('avatar.txt')
+      account = Fabricate(:account, header_remote_url: 'https://example.host/file', header_file_name: 'test.jpg')
+
+      expect(worker.perform(account.id)).to be_nil
+    end
+
+    it 'reprocesses a remote header' do
+      stub_request(:get, 'https://example.host/file').to_return request_fixture('avatar.txt')
+      account = Fabricate(:account, header_remote_url: 'https://example.host/file')
+      account.update_column(:header_file_name, nil) # rubocop:disable Rails/SkipsModelValidations
+
+      result = worker.perform(account.id)
+
+      expect(result).to be(true)
+      expect(account.reload.header_file_name).to_not be_nil
     end
   end
 end
diff --git a/spec/workers/redownload_media_worker_spec.rb b/spec/workers/redownload_media_worker_spec.rb
new file mode 100644
index 000000000..cd561d148
--- /dev/null
+++ b/spec/workers/redownload_media_worker_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe RedownloadMediaWorker do
+  let(:worker) { described_class.new }
+
+  describe '#perform' do
+    it 'returns nil for non-existent record' do
+      result = worker.perform(123_123_123)
+
+      expect(result).to be_nil
+    end
+
+    it 'returns nil without a remote_url' do
+      media_attachment = Fabricate(:media_attachment, remote_url: '')
+
+      result = worker.perform(media_attachment.id)
+
+      expect(result).to be_nil
+    end
+
+    context 'with a valid remote url' do
+      let(:url) { 'https://example.host/file.txt' }
+
+      before { stub_request(:get, url).to_return(status: 200) }
+
+      it 'processes downloads for valid record' do
+        media_attachment = Fabricate(:media_attachment, remote_url: url)
+
+        worker.perform(media_attachment.id)
+
+        expect(a_request(:get, url)).to have_been_made
+      end
+    end
+  end
+end

From 9c68741f464b7cffa8b70a6be38cd333a795672b Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 17 Nov 2023 10:16:12 +0100
Subject: [PATCH 57/63] New Crowdin Translations (automated) (#27914)

Co-authored-by: GitHub Actions <noreply@github.com>
---
 app/javascript/mastodon/locales/be.json    |  2 ++
 app/javascript/mastodon/locales/cs.json    |  2 ++
 app/javascript/mastodon/locales/cy.json    |  7 ++++++
 app/javascript/mastodon/locales/da.json    |  2 ++
 app/javascript/mastodon/locales/de.json    |  2 ++
 app/javascript/mastodon/locales/es-AR.json |  2 ++
 app/javascript/mastodon/locales/es-MX.json |  2 ++
 app/javascript/mastodon/locales/es.json    | 12 ++++++++++
 app/javascript/mastodon/locales/eu.json    |  2 ++
 app/javascript/mastodon/locales/fi.json    |  2 ++
 app/javascript/mastodon/locales/fr-QC.json |  4 ++++
 app/javascript/mastodon/locales/fr.json    |  6 ++++-
 app/javascript/mastodon/locales/gl.json    | 12 ++++++++++
 app/javascript/mastodon/locales/he.json    |  2 ++
 app/javascript/mastodon/locales/hu.json    | 14 +++++++-----
 app/javascript/mastodon/locales/is.json    |  2 ++
 app/javascript/mastodon/locales/it.json    |  2 ++
 app/javascript/mastodon/locales/ko.json    |  2 ++
 app/javascript/mastodon/locales/lt.json    | 26 +++++++++++++++++++---
 app/javascript/mastodon/locales/nl.json    |  2 ++
 app/javascript/mastodon/locales/nn.json    | 24 +++++++++++---------
 app/javascript/mastodon/locales/no.json    |  2 ++
 app/javascript/mastodon/locales/pl.json    |  2 ++
 app/javascript/mastodon/locales/pt-PT.json |  2 ++
 app/javascript/mastodon/locales/sq.json    |  2 ++
 app/javascript/mastodon/locales/sr.json    |  3 +++
 app/javascript/mastodon/locales/sv.json    |  2 ++
 app/javascript/mastodon/locales/th.json    | 10 +++++----
 app/javascript/mastodon/locales/uk.json    |  2 ++
 app/javascript/mastodon/locales/zh-CN.json |  2 ++
 app/javascript/mastodon/locales/zh-HK.json |  2 ++
 app/javascript/mastodon/locales/zh-TW.json |  2 ++
 config/locales/es.yml                      |  1 +
 config/locales/eu.yml                      |  1 +
 config/locales/gl.yml                      |  3 ++-
 config/locales/hu.yml                      |  2 +-
 config/locales/ko.yml                      |  2 +-
 config/locales/nn.yml                      | 26 +++++++++++-----------
 config/locales/no.yml                      |  2 +-
 config/locales/sr.yml                      |  1 +
 config/locales/th.yml                      |  4 ++--
 config/locales/tr.yml                      |  1 +
 config/locales/zh-CN.yml                   |  1 +
 43 files changed, 162 insertions(+), 44 deletions(-)

diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json
index 4f5b247b6..59b1ca50b 100644
--- a/app/javascript/mastodon/locales/be.json
+++ b/app/javascript/mastodon/locales/be.json
@@ -21,6 +21,7 @@
   "account.blocked": "Заблакіраваны",
   "account.browse_more_on_origin_server": "Глядзіце больш у арыгінальным профілі",
   "account.cancel_follow_request": "Скасаваць запыт на падпіску",
+  "account.copy": "Скапіраваць спасылку на профіль",
   "account.direct": "Згадаць асабіста @{name}",
   "account.disable_notifications": "Не паведамляць мне пра публікацыі @{name}",
   "account.domain_blocked": "Дамен заблакаваны",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Адзначыць прачытаным",
   "conversation.open": "Прагледзець размову",
   "conversation.with": "З {names}",
+  "copy_icon_button.copied": "Скапіявана ў буфер абмену",
   "copypaste.copied": "Скапіравана",
   "copypaste.copy_to_clipboard": "Капіраваць у буфер абмену",
   "directory.federated": "З вядомага федэсвету",
diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json
index 20ef43733..16bf5020c 100644
--- a/app/javascript/mastodon/locales/cs.json
+++ b/app/javascript/mastodon/locales/cs.json
@@ -21,6 +21,7 @@
   "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.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",
   "account.domain_blocked": "Doména blokována",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Označit jako přečtené",
   "conversation.open": "Zobrazit konverzaci",
   "conversation.with": "S {names}",
+  "copy_icon_button.copied": "Zkopírováno do schránky",
   "copypaste.copied": "Zkopírováno",
   "copypaste.copy_to_clipboard": "Zkopírovat do schránky",
   "directory.federated": "Ze známého fedivesmíru",
diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index dcd5406d0..11d4de440 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -191,6 +191,7 @@
   "conversation.mark_as_read": "Nodi fel wedi'i ddarllen",
   "conversation.open": "Gweld sgwrs",
   "conversation.with": "Gyda {names}",
+  "copy_icon_button.copied": "Copïwyd i'r clipfwrdd",
   "copypaste.copied": "Wedi ei gopïo",
   "copypaste.copy_to_clipboard": "Copïo i'r clipfwrdd",
   "directory.federated": "O'r ffedysawd cyfan",
@@ -390,6 +391,7 @@
   "lists.search": "Chwilio ymysg pobl rydych yn eu dilyn",
   "lists.subheading": "Eich rhestrau",
   "load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
+  "loading_indicator.label": "Yn llwytho…",
   "media_gallery.toggle_visible": "{number, plural, one {Cuddio delwedd} other {Cuddio delwedd}}",
   "moved_to_account_banner.text": "Ar hyn y bryd, mae eich cyfrif {disabledAccount} wedi ei analluogi am i chi symud i {movedToAccount}.",
   "mute_modal.duration": "Hyd",
@@ -478,6 +480,11 @@
   "onboarding.follows.empty": "Yn anffodus, nid oes modd dangos unrhyw ganlyniadau ar hyn o bryd. Gallwch geisio defnyddio chwilio neu bori'r dudalen archwilio i ddod o hyd i bobl i'w dilyn, neu ceisio eto yn nes ymlaen.",
   "onboarding.follows.lead": "Rydych chi'n curadu eich ffrwd gartref eich hun. Po fwyaf o bobl y byddwch chi'n eu dilyn, y mwyaf egnïol a diddorol fydd hi. Gall y proffiliau hyn fod yn fan cychwyn da - gallwch chi bob amser eu dad-ddilyn yn nes ymlaen:",
   "onboarding.follows.title": "Yn boblogaidd ar Mastodon",
+  "onboarding.profile.display_name_hint": "Eich enw llawn neu'ch enw hwyl…",
+  "onboarding.profile.note": "Bywgraffiad",
+  "onboarding.profile.note_hint": "Gallwch @grybwyll pobl eraill neu #hashnodau…",
+  "onboarding.profile.save_and_continue": "Cadw a pharhau",
+  "onboarding.profile.title": "Gosodiad proffil",
   "onboarding.share.lead": "Cofiwch ddweud wrth bobl sut y gallan nhw ddod o hyd i chi ar Mastodon!",
   "onboarding.share.message": "Fi yw {username} ar #Mastodon! Dewch i'm dilyn i yn {url}",
   "onboarding.share.next_steps": "Camau nesaf posib:",
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index 33eda6f43..bde12a7ae 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -21,6 +21,7 @@
   "account.blocked": "Blokeret",
   "account.browse_more_on_origin_server": "Se mere på den oprindelige profil",
   "account.cancel_follow_request": "Annullér anmodning om at følge",
+  "account.copy": "Kopiér link til profil",
   "account.direct": "Privat omtale @{name}",
   "account.disable_notifications": "Advisér mig ikke længere, når @{name} poster",
   "account.domain_blocked": "Domæne blokeret",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Markér som læst",
   "conversation.open": "Vis samtale",
   "conversation.with": "Med {names}",
+  "copy_icon_button.copied": "Kopieret til udklipsholderen",
   "copypaste.copied": "Kopieret",
   "copypaste.copy_to_clipboard": "Kopiér til udklipsholder",
   "directory.federated": "Fra kendt fedivers",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 5c1ccf46d..10fd9b4c5 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -21,6 +21,7 @@
   "account.blocked": "Blockiert",
   "account.browse_more_on_origin_server": "Mehr auf dem Originalprofil durchsuchen",
   "account.cancel_follow_request": "Folgeanfrage zurückziehen",
+  "account.copy": "Link zum Profil kopieren",
   "account.direct": "@{name} privat erwähnen",
   "account.disable_notifications": "Höre auf mich zu benachrichtigen wenn @{name} etwas postet",
   "account.domain_blocked": "Domain versteckt",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Als gelesen markieren",
   "conversation.open": "Unterhaltung anzeigen",
   "conversation.with": "Mit {names}",
+  "copy_icon_button.copied": "In die Zwischenablage kopiert",
   "copypaste.copied": "Kopiert",
   "copypaste.copy_to_clipboard": "In die Zwischenablage kopieren",
   "directory.federated": "Aus bekanntem Fediverse",
diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json
index 680336369..3c99334c3 100644
--- a/app/javascript/mastodon/locales/es-AR.json
+++ b/app/javascript/mastodon/locales/es-AR.json
@@ -21,6 +21,7 @@
   "account.blocked": "Bloqueado",
   "account.browse_more_on_origin_server": "Explorar más en el perfil original",
   "account.cancel_follow_request": "Dejar de seguir",
+  "account.copy": "Copiar enlace al perfil",
   "account.direct": "Mención privada a @{name}",
   "account.disable_notifications": "Dejar de notificarme cuando @{name} envíe mensajes",
   "account.domain_blocked": "Dominio bloqueado",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Marcar como leída",
   "conversation.open": "Ver conversación",
   "conversation.with": "Con {names}",
+  "copy_icon_button.copied": "Copiado en el portapapeles",
   "copypaste.copied": "Copiado",
   "copypaste.copy_to_clipboard": "Copiar al portapapeles",
   "directory.federated": "Desde fediverso conocido",
diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json
index aa8a21edb..70c4efcad 100644
--- a/app/javascript/mastodon/locales/es-MX.json
+++ b/app/javascript/mastodon/locales/es-MX.json
@@ -21,6 +21,7 @@
   "account.blocked": "Bloqueado",
   "account.browse_more_on_origin_server": "Ver más en el perfil original",
   "account.cancel_follow_request": "Retirar solicitud de seguimiento",
+  "account.copy": "Copiar enlace al perfil",
   "account.direct": "Mención privada @{name}",
   "account.disable_notifications": "Dejar de notificarme cuando @{name} publique algo",
   "account.domain_blocked": "Dominio oculto",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Marcar como leído",
   "conversation.open": "Ver conversación",
   "conversation.with": "Con {names}",
+  "copy_icon_button.copied": "Copiado al portapapeles",
   "copypaste.copied": "Copiado",
   "copypaste.copy_to_clipboard": "Copiar al portapapeles",
   "directory.federated": "Desde el fediverso conocido",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 5d1aa004b..3c0d8cd57 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -21,6 +21,7 @@
   "account.blocked": "Bloqueado",
   "account.browse_more_on_origin_server": "Ver más en el perfil original",
   "account.cancel_follow_request": "Retirar solicitud de seguimiento",
+  "account.copy": "Copiar enlace al perfil",
   "account.direct": "Mención privada a @{name}",
   "account.disable_notifications": "Dejar de notificarme cuando @{name} publique algo",
   "account.domain_blocked": "Dominio bloqueado",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Marcar como leído",
   "conversation.open": "Ver conversación",
   "conversation.with": "Con {names}",
+  "copy_icon_button.copied": "Copiado al portapapeles",
   "copypaste.copied": "Copiado",
   "copypaste.copy_to_clipboard": "Copiar al portapapeles",
   "directory.federated": "Desde el fediverso conocido",
@@ -479,7 +481,17 @@
   "onboarding.follows.empty": "Desafortunadamente, no se pueden mostrar resultados en este momento. Puedes intentar usar la búsqueda o navegar por la página de exploración para encontrar personas a las que seguir, o inténtalo de nuevo más tarde.",
   "onboarding.follows.lead": "Tu línea de inicio es la forma principal de experimentar Mastodon. Cuanta más personas sigas, más activa e interesante será. Para empezar, aquí hay algunas sugerencias:",
   "onboarding.follows.title": "Personaliza tu línea de inicio",
+  "onboarding.profile.discoverable": "Destacar perfil y publicaciones en algoritmos de descubrimiento",
   "onboarding.profile.display_name": "Nombre para mostrar",
+  "onboarding.profile.display_name_hint": "Tu nombre completo o tu apodo…",
+  "onboarding.profile.indexable": "Incluir publicaciones públicas en los resultados de búsqueda",
+  "onboarding.profile.lead": "Siempre puedes completar esto más tarde en los ajustes, donde hay aún más opciones de personalización disponibles.",
+  "onboarding.profile.note": "Biografía",
+  "onboarding.profile.note_hint": "Puedes @mencionar a otras personas o #etiquetas…",
+  "onboarding.profile.save_and_continue": "Guardar y continuar",
+  "onboarding.profile.title": "Configuración del perfil",
+  "onboarding.profile.upload_avatar": "Subir foto de perfil",
+  "onboarding.profile.upload_header": "Subir encabezado de perfil",
   "onboarding.share.lead": "¡Cuéntale a otras personas cómo te pueden encontrar en Mastodon!",
   "onboarding.share.message": "¡Soy {username} en #Mastodon! Ven a seguirme en {url}",
   "onboarding.share.next_steps": "Posibles siguientes pasos:",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index 88f70cffb..5bca1cfef 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -21,6 +21,7 @@
   "account.blocked": "Blokeatuta",
   "account.browse_more_on_origin_server": "Arakatu gehiago jatorrizko profilean",
   "account.cancel_follow_request": "Baztertu jarraitzeko eskaera",
+  "account.copy": "Kopiatu profilerako esteka",
   "account.direct": "Aipatu pribatuki @{name}",
   "account.disable_notifications": "Utzi jakinarazteari @{name} erabiltzailearen bidalketetan",
   "account.domain_blocked": "Ezkutatutako domeinua",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Markatu irakurrita bezala",
   "conversation.open": "Ikusi elkarrizketa",
   "conversation.with": "Hauekin: {names}",
+  "copy_icon_button.copied": "Arbelera kopiatu da",
   "copypaste.copied": "Kopiatuta",
   "copypaste.copy_to_clipboard": "Kopiatu arbelera",
   "directory.federated": "Fedibertso ezagunekoak",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 849a7f463..344e37c97 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -21,6 +21,7 @@
   "account.blocked": "Estetty",
   "account.browse_more_on_origin_server": "Selaile lisää alkuperäisellä palvelimella",
   "account.cancel_follow_request": "Peruuta seurantapyyntö",
+  "account.copy": "Kopioi profiilin linkki",
   "account.direct": "Mainitse @{name} yksityisesti",
   "account.disable_notifications": "Lopeta ilmoittamasta minulle, kun @{name} julkaisee",
   "account.domain_blocked": "Verkkotunnus estetty",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Merkitse luetuksi",
   "conversation.open": "Näytä keskustelu",
   "conversation.with": "{names} kanssa",
+  "copy_icon_button.copied": "Kopioitu leikepöydälle",
   "copypaste.copied": "Kopioitu",
   "copypaste.copy_to_clipboard": "Kopioi leikepöydälle",
   "directory.federated": "Koko tunnettu fediversumi",
diff --git a/app/javascript/mastodon/locales/fr-QC.json b/app/javascript/mastodon/locales/fr-QC.json
index e858882f8..d85a6d428 100644
--- a/app/javascript/mastodon/locales/fr-QC.json
+++ b/app/javascript/mastodon/locales/fr-QC.json
@@ -390,6 +390,7 @@
   "lists.search": "Rechercher parmi les gens que vous suivez",
   "lists.subheading": "Vos listes",
   "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}",
+  "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "{number, plural, one {Cacher l’image} other {Cacher les images}}",
   "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous avez déménagé sur {movedToAccount}.",
   "mute_modal.duration": "Durée",
@@ -478,6 +479,9 @@
   "onboarding.follows.empty": "Malheureusement, aucun résultat ne peut être affiché pour le moment. Vous pouvez essayer de rechercher ou de parcourir la page \"Explorer\" pour trouver des personnes à suivre, ou réessayer plus tard.",
   "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
   "onboarding.follows.title": "Popular on Mastodon",
+  "onboarding.profile.discoverable": "Autoriser des algorithmes de découverte à mettre en avant votre profil et vos messages",
+  "onboarding.profile.save_and_continue": "Enregistrer et continuer",
+  "onboarding.profile.upload_avatar": "Importer une photo de profil",
   "onboarding.share.lead": "Faites savoir aux gens comment vous trouver sur Mastodon!",
   "onboarding.share.message": "Je suis {username} sur #Mastodon! Suivez-moi sur {url}",
   "onboarding.share.next_steps": "Étapes suivantes possibles:",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index a7bb4a12f..14e0e13ba 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -107,7 +107,7 @@
   "closed_registrations_modal.preamble": "Mastodon est décentralisé : peu importe où vous créez votre compte, vous serez en mesure de suivre et d'interagir avec quiconque sur ce serveur. Vous pouvez même l'héberger !",
   "closed_registrations_modal.title": "Inscription sur Mastodon",
   "column.about": "À propos",
-  "column.blocks": "Comptes bloqués",
+  "column.blocks": "Utilisateurs bloqués",
   "column.bookmarks": "Marque-pages",
   "column.community": "Fil public local",
   "column.direct": "Mentions privées",
@@ -390,6 +390,7 @@
   "lists.search": "Rechercher parmi les gens que vous suivez",
   "lists.subheading": "Vos listes",
   "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}",
+  "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "{number, plural, one {Cacher l’image} other {Cacher les images}}",
   "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous l'avez déplacé à {movedToAccount}.",
   "mute_modal.duration": "Durée",
@@ -478,6 +479,9 @@
   "onboarding.follows.empty": "Malheureusement, aucun résultat ne peut être affiché pour le moment. Vous pouvez essayer d'utiliser la recherche ou parcourir la page de découverte pour trouver des personnes à suivre, ou réessayez plus tard.",
   "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
   "onboarding.follows.title": "Personnaliser votre flux principal",
+  "onboarding.profile.discoverable": "Autoriser des algorithmes de découverte à mettre en avant votre profil et vos messages",
+  "onboarding.profile.save_and_continue": "Enregistrer et continuer",
+  "onboarding.profile.upload_avatar": "Importer une photo de profil",
   "onboarding.share.lead": "Faites savoir aux gens comment ils peuvent vous trouver sur Mastodon!",
   "onboarding.share.message": "Je suis {username} sur #Mastodon ! Suivez-moi sur {url}",
   "onboarding.share.next_steps": "Étapes suivantes possibles :",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 7d2c6dab9..1f199bc45 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -390,6 +390,7 @@
   "lists.search": "Procurar entre as persoas que segues",
   "lists.subheading": "As túas listaxes",
   "load_pending": "{count, plural, one {# novo elemento} other {# novos elementos}}",
+  "loading_indicator.label": "Estase a cargar…",
   "media_gallery.toggle_visible": "Agochar {number, plural, one {imaxe} other {imaxes}}",
   "moved_to_account_banner.text": "A túa conta {disabledAccount} está actualmente desactivada porque movéchela a {movedToAccount}.",
   "mute_modal.duration": "Duración",
@@ -478,6 +479,17 @@
   "onboarding.follows.empty": "Desgraciadamente agora mesmo non hai nada que mostrar. Podes intentalo coa busca ou na páxina descubrir para atopar persoas ás que seguir, ou intentalo máis tarde.",
   "onboarding.follows.lead": "Podes facer que a túa cronoloxía de inicio sexa como ti a queres. Canta máis xente sigas máis interesante será. Estes perfís poderían axudarche a comezar —sempre poderás deixar de seguilos despois!",
   "onboarding.follows.title": "Popular en Mastodon",
+  "onboarding.profile.discoverable": "Perfil destacado e publicacións nos algoritmos de descubrimento",
+  "onboarding.profile.display_name": "Nome público",
+  "onboarding.profile.display_name_hint": "O teu nome completo ou un nome divertido…",
+  "onboarding.profile.indexable": "Incluír publicacións públicas nos resultados das buscas",
+  "onboarding.profile.lead": "Sempre poderás incluír esta información mais tarde nos axustes, onde terás máis opcións dispoñibles.",
+  "onboarding.profile.note": "Acerca de ti",
+  "onboarding.profile.note_hint": "Podes @mencionar a outras persoas ou usar #cancelos…",
+  "onboarding.profile.save_and_continue": "Gardar e continuar",
+  "onboarding.profile.title": "Configuración do perfil",
+  "onboarding.profile.upload_avatar": "Subir imaxe do perfil",
+  "onboarding.profile.upload_header": "Subir cabeceira para o perfil",
   "onboarding.share.lead": "Fai que as persoas saiban como atoparte en Mastodon!",
   "onboarding.share.message": "Son {username} en #Mastodon! Ségueme en {url}",
   "onboarding.share.next_steps": "Seguintes pasos:",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index 2e0009170..b6716f796 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -21,6 +21,7 @@
   "account.blocked": "לחסום",
   "account.browse_more_on_origin_server": "ראה יותר בפרופיל המקורי",
   "account.cancel_follow_request": "משיכת בקשת מעקב",
+  "account.copy": "להעתיק קישור לפרופיל",
   "account.direct": "הודעה פרטית אל @{name}",
   "account.disable_notifications": "הפסק לשלוח לי התראות כש@{name} מפרסמים",
   "account.domain_blocked": "הדומיין חסום",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "סמן כנקרא",
   "conversation.open": "צפו בשיחה",
   "conversation.with": "עם {names}",
+  "copy_icon_button.copied": "הועתק ללוח",
   "copypaste.copied": "הועתק",
   "copypaste.copy_to_clipboard": "העתקה ללוח הגזירים",
   "directory.federated": "מהפדרציה הידועה",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 96fc720a4..83948b178 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -21,6 +21,7 @@
   "account.blocked": "Letiltva",
   "account.browse_more_on_origin_server": "További böngészés az eredeti profilon",
   "account.cancel_follow_request": "Követési kérés visszavonása",
+  "account.copy": "Hivatkozás másolása a profilba",
   "account.direct": "@{name} személyes említése",
   "account.disable_notifications": "Ne figyelmeztessen, ha @{name} bejegyzést tesz közzé",
   "account.domain_blocked": "Letiltott domain",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Megjelölés olvasottként",
   "conversation.open": "Beszélgetés megtekintése",
   "conversation.with": "Velük: {names}",
+  "copy_icon_button.copied": "A szöveg a vágólapra másolva",
   "copypaste.copied": "Másolva",
   "copypaste.copy_to_clipboard": "Másolás vágólapra",
   "directory.federated": "Az ismert fediverzumból",
@@ -481,13 +483,13 @@
   "onboarding.follows.title": "Népszerű a Mastodonon",
   "onboarding.profile.discoverable": "Profil és bejegyzések szerepeltetése a felfedezési algoritmusokban",
   "onboarding.profile.display_name": "Megjelenített név",
-  "onboarding.profile.display_name_hint": "Teljes név vagy becenév…",
-  "onboarding.profile.indexable": "Nyilvános bejegyzések is a keresési eredményekben",
-  "onboarding.profile.lead": "Ezt később bármikor elvégezhető a beállításoknál, ahol még több testreszabási lehetőség áll rendelkezésre.",
-  "onboarding.profile.note": "Biográfia",
-  "onboarding.profile.note_hint": "@említhetünk másokat vagy #hashtag elemeket…",
+  "onboarding.profile.display_name_hint": "Teljes neved vagy vicces neved…",
+  "onboarding.profile.indexable": "Nyilvános bejegyzések szerepeltetése a keresési eredményekben",
+  "onboarding.profile.lead": "Ezt később bármikor befejezheted a beállításokban, ahol még több testreszabási lehetőség áll rendelkezésre.",
+  "onboarding.profile.note": "Bemutatkozás",
+  "onboarding.profile.note_hint": "Megemlíthetsz @másokat vagy #hashtag-eket…",
   "onboarding.profile.save_and_continue": "Mentés és folytatás",
-  "onboarding.profile.title": "Profil beüzemelés",
+  "onboarding.profile.title": "Profilbeállítás",
   "onboarding.profile.upload_avatar": "Profilkép feltöltése",
   "onboarding.profile.upload_header": "Profil fejléc feltöltése",
   "onboarding.share.lead": "Tudassuk az emberekkel, hogyan találhatnak meg a Mastodonon!",
diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json
index 54123ae4c..b3c55d4f6 100644
--- a/app/javascript/mastodon/locales/is.json
+++ b/app/javascript/mastodon/locales/is.json
@@ -21,6 +21,7 @@
   "account.blocked": "Útilokaður",
   "account.browse_more_on_origin_server": "Skoða nánari upplýsingar á notandasniðinu",
   "account.cancel_follow_request": "Taka fylgjendabeiðni til baka",
+  "account.copy": "Afrita tengil í notandasnið",
   "account.direct": "Einkaspjall við @{name}",
   "account.disable_notifications": "Hætta að láta mig vita þegar @{name} sendir inn",
   "account.domain_blocked": "Lén útilokað",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Merkja sem lesið",
   "conversation.open": "Skoða samtal",
   "conversation.with": "Við {names}",
+  "copy_icon_button.copied": "Afritað á klippispjald",
   "copypaste.copied": "Afritað",
   "copypaste.copy_to_clipboard": "Afrita á klippispjald",
   "directory.federated": "Frá samtengdum vefþjónum",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 284d7739c..02e5991ab 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -21,6 +21,7 @@
   "account.blocked": "Bloccato",
   "account.browse_more_on_origin_server": "Sfoglia di più sul profilo originale",
   "account.cancel_follow_request": "Annulla la richiesta di seguire",
+  "account.copy": "Copia link del profilo",
   "account.direct": "Menziona privatamente @{name}",
   "account.disable_notifications": "Smetti di avvisarmi quando @{name} pubblica un post",
   "account.domain_blocked": "Dominio bloccato",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Segna come letto",
   "conversation.open": "Visualizza conversazione",
   "conversation.with": "Con {names}",
+  "copy_icon_button.copied": "Copiato negli appunti",
   "copypaste.copied": "Copiato",
   "copypaste.copy_to_clipboard": "Copia negli Appunti",
   "directory.federated": "Da un fediverse noto",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 1420be8e0..de72e382c 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -21,6 +21,7 @@
   "account.blocked": "차단함",
   "account.browse_more_on_origin_server": "원본 프로필에서 더 탐색하기",
   "account.cancel_follow_request": "팔로우 취소",
+  "account.copy": "프로필 링크 복사",
   "account.direct": "@{name} 님에게 개인적으로 멘션",
   "account.disable_notifications": "@{name} 의 게시물 알림 끄기",
   "account.domain_blocked": "도메인 차단함",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "읽은 상태로 표시",
   "conversation.open": "대화 보기",
   "conversation.with": "{names} 님과",
+  "copy_icon_button.copied": "클립보드에 복사함",
   "copypaste.copied": "복사됨",
   "copypaste.copy_to_clipboard": "클립보드에 복사",
   "directory.federated": "알려진 연합우주로부터",
diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json
index 21aa797b4..af9a3eaa0 100644
--- a/app/javascript/mastodon/locales/lt.json
+++ b/app/javascript/mastodon/locales/lt.json
@@ -21,6 +21,7 @@
   "account.blocked": "Užblokuota",
   "account.browse_more_on_origin_server": "Naršyti daugiau originaliame profilyje",
   "account.cancel_follow_request": "Atšaukti sekimą",
+  "account.copy": "Kopijuoti nuorodą į profilį",
   "account.direct": "Privačiai paminėti @{name}",
   "account.disable_notifications": "Nustoti man pranešti, kai @{name} paskelbia",
   "account.domain_blocked": "Užblokuotas domenas",
@@ -91,11 +92,28 @@
   "bundle_column_error.routing.body": "Prašyto puslapio nepavyko rasti. Ar esi tikras (-a), kad adreso juostoje nurodytas URL adresas yra teisingas?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "Uždaryti",
+  "bundle_modal_error.retry": "Bandyti dar kartą",
+  "closed_registrations.other_server_instructions": "Kadangi Mastodon yra decentralizuotas, gali susikurti paskyrą kitame serveryje ir vis tiek bendrauti su šiuo serveriu.",
+  "closed_registrations_modal.description": "Sukurti paskyrą {domain} šiuo metu neįmanoma, tačiau nepamiršk, kad norint naudotis Mastodon nebūtina turėti paskyrą {domain}.",
   "closed_registrations_modal.find_another_server": "Rasti kitą serverį",
-  "column.domain_blocks": "Hidden domains",
+  "closed_registrations_modal.preamble": "Mastodon yra decentralizuotas, todėl nesvarbu, kur susikursi paskyrą, galėsi sekti ir bendrauti su bet kuriuo šiame serveryje esančiu asmeniu. Jį gali net savarankiškai talpinti!",
+  "closed_registrations_modal.title": "Užsiregistravimas į Mastodon",
+  "column.about": "Apie",
+  "column.blocks": "Užblokuoti naudotojai",
+  "column.bookmarks": "Žymės",
+  "column.community": "Vietinė laiko skalė",
+  "column.direct": "Privatūs paminėjimai",
+  "column.directory": "Naršyti profilius",
+  "column.domain_blocks": "Užblokuoti domenai",
+  "column.favourites": "Mėgstamiausi",
+  "column.firehose": "Tiesioginiai padavimai",
+  "column.follow_requests": "Sekti prašymus",
+  "column.home": "Pradžia",
   "column.lists": "Sąrašai",
-  "column.mutes": "Užtildyti vartotojai",
-  "column.pins": "Pinned toot",
+  "column.mutes": "Užtildyti naudotojai",
+  "column.notifications": "Pranešimai",
+  "column.pins": "Prisegti įrašai",
+  "column.public": "Federacinė laiko skalė",
   "column_back_button.label": "Atgal",
   "column_header.hide_settings": "Slėpti nustatymus",
   "column_header.pin": "Prisegti",
@@ -106,6 +124,7 @@
   "compose.language.change": "Keisti kalbą",
   "compose.language.search": "Ieškoti kalbų...",
   "compose.published.body": "Įrašas paskelbtas.",
+  "compose_form.direct_message_warning_learn_more": "Sužinoti daugiau",
   "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
   "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
   "compose_form.placeholder": "Kas tavo mintyse?",
@@ -137,6 +156,7 @@
   "confirmations.reply.confirm": "Atsakyti",
   "confirmations.reply.message": "Atsakydamas (-a) dabar perrašysi šiuo metu rašomą žinutę. Ar tikrai nori tęsti?",
   "confirmations.unfollow.confirm": "Nebesekti",
+  "copy_icon_button.copied": "Nukopijuota į iškarpinę",
   "dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
   "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
   "embed.instructions": "Embed this status on your website by copying the code below.",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 6f941999f..e0fa73ef6 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -21,6 +21,7 @@
   "account.blocked": "Geblokkeerd",
   "account.browse_more_on_origin_server": "Zie meer op het originele profiel",
   "account.cancel_follow_request": "Ontvolgen",
+  "account.copy": "Link naar profiel kopiëren",
   "account.direct": "@{name} een privébericht sturen",
   "account.disable_notifications": "Geen melding meer geven wanneer @{name} een bericht plaatst",
   "account.domain_blocked": "Domein geblokkeerd",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Als gelezen markeren",
   "conversation.open": "Gesprek tonen",
   "conversation.with": "Met {names}",
+  "copy_icon_button.copied": "Gekopieerd naar klembord",
   "copypaste.copied": "Gekopieerd",
   "copypaste.copy_to_clipboard": "Naar klembord kopiëren",
   "directory.federated": "Fediverse (wat bekend is)",
diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json
index a3402d660..5e4459e43 100644
--- a/app/javascript/mastodon/locales/nn.json
+++ b/app/javascript/mastodon/locales/nn.json
@@ -21,6 +21,7 @@
   "account.blocked": "Blokkert",
   "account.browse_more_on_origin_server": "Sjå gjennom meir på den opphavlege profilen",
   "account.cancel_follow_request": "Trekk attende fylgeførespurnad",
+  "account.copy": "Kopier lenka til profilen",
   "account.direct": "Nevn @{name} privat",
   "account.disable_notifications": "Slutt å varsle meg når @{name} skriv innlegg",
   "account.domain_blocked": "Domenet er sperra",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Marker som lesen",
   "conversation.open": "Sjå samtale",
   "conversation.with": "Med {names}",
+  "copy_icon_button.copied": "Kopiert til utklyppstavla",
   "copypaste.copied": "Kopiert",
   "copypaste.copy_to_clipboard": "Kopier til utklyppstavla",
   "directory.federated": "Frå den kjende allheimen",
@@ -390,7 +392,7 @@
   "lists.search": "Søk blant folk du fylgjer",
   "lists.subheading": "Listene dine",
   "load_pending": "{count, plural, one {# nytt element} other {# nye element}}",
-  "loading_indicator.label": "Laster…",
+  "loading_indicator.label": "Lastar…",
   "media_gallery.toggle_visible": "{number, plural, one {Skjul bilete} other {Skjul bilete}}",
   "moved_to_account_banner.text": "Kontoen din, {disabledAccount} er for tida deaktivert fordi du har flytta til {movedToAccount}.",
   "mute_modal.duration": "Varigheit",
@@ -479,17 +481,17 @@
   "onboarding.follows.empty": "Me kan ikkje visa deg nokon resultat no. Du kan prøva å søkja eller bla gjennom utforsk-sida for å finna folk å fylgja, eller du kan prøva att seinare.",
   "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
   "onboarding.follows.title": "Popular on Mastodon",
-  "onboarding.profile.discoverable": "Fremhevede profiler og innlegg i oppdagelsealgoritmer",
-  "onboarding.profile.display_name": "Visningsnavn",
-  "onboarding.profile.display_name_hint": "Ditt fulle navn eller ditt morsomme navn…",
-  "onboarding.profile.indexable": "Inkluder offentlige innlegg i søkeresultatene",
-  "onboarding.profile.lead": "Du kan alltid fullføre dette senere i innstillingene, der enda flere tilpasningsalternativer er tilgjengelige.",
+  "onboarding.profile.discoverable": "Ta med profilen og innlegga i oppdagingsalgoritmar",
+  "onboarding.profile.display_name": "Synleg namn",
+  "onboarding.profile.display_name_hint": "Det fulle namnet eller kallenamnet ditt…",
+  "onboarding.profile.indexable": "Ta med offentlege innlegg i søkjeresultat",
+  "onboarding.profile.lead": "Du kan alltid fullføra dette seinare i innstillingane, og der er det endå fleire tilpassingsalternativ.",
   "onboarding.profile.note": "Om meg",
-  "onboarding.profile.note_hint": "Du kan @nevne andre eller #emneknagger…",
-  "onboarding.profile.save_and_continue": "Lagre og fortsett",
-  "onboarding.profile.title": "Konfigurering av profil",
-  "onboarding.profile.upload_avatar": "Last opp profilbilde",
-  "onboarding.profile.upload_header": "Last opp profiltoppbilde",
+  "onboarding.profile.note_hint": "Du kan @nemna folk eller #emneknaggar…",
+  "onboarding.profile.save_and_continue": "Lagre og hald fram",
+  "onboarding.profile.title": "Profiloppsett",
+  "onboarding.profile.upload_avatar": "Last opp profilbilete",
+  "onboarding.profile.upload_header": "Last opp profiltoppbilete",
   "onboarding.share.lead": "La folk vita korleis dei kan finna deg på Mastodon!",
   "onboarding.share.message": "Eg er {username} på #Mastodon! Du kan fylgja meg på {url}",
   "onboarding.share.next_steps": "Dette kan du gjera no:",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index fe3979f0f..0dce1e666 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -21,6 +21,7 @@
   "account.blocked": "Blokkert",
   "account.browse_more_on_origin_server": "Bla mer på den opprinnelige profilen",
   "account.cancel_follow_request": "Avbryt følgeforespørselen",
+  "account.copy": "Kopier lenke til profil",
   "account.direct": "Nevn @{name} privat",
   "account.disable_notifications": "Slutt å varsle meg når @{name} legger ut innlegg",
   "account.domain_blocked": "Domene blokkert",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Marker som lest",
   "conversation.open": "Vis samtale",
   "conversation.with": "Med {names}",
+  "copy_icon_button.copied": "Kopiert til utklippstavlen",
   "copypaste.copied": "Kopiert",
   "copypaste.copy_to_clipboard": "Kopier til utklippstavle",
   "directory.federated": "Fra det kjente strømiverset",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index a1cc0e26e..d316143e1 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -21,6 +21,7 @@
   "account.blocked": "Zablokowany(-a)",
   "account.browse_more_on_origin_server": "Zobacz więcej na oryginalnym profilu",
   "account.cancel_follow_request": "Wycofaj żądanie obserwowania",
+  "account.copy": "Skopiuj odnośnik do profilu",
   "account.direct": "Prywatna wzmianka @{name}",
   "account.disable_notifications": "Przestań powiadamiać mnie o wpisach @{name}",
   "account.domain_blocked": "Ukryto domenę",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Oznacz jako przeczytane",
   "conversation.open": "Zobacz konwersację",
   "conversation.with": "Z {names}",
+  "copy_icon_button.copied": "Skopiowano do schowka",
   "copypaste.copied": "Skopiowano",
   "copypaste.copy_to_clipboard": "Skopiuj do schowka",
   "directory.federated": "Ze znanego fediwersum",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index 69e804878..0ad315152 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -21,6 +21,7 @@
   "account.blocked": "Bloqueado(a)",
   "account.browse_more_on_origin_server": "Encontrar mais no perfil original",
   "account.cancel_follow_request": "Retirar pedido para seguir",
+  "account.copy": "Copiar hiperligação para o perfil",
   "account.direct": "Mencionar @{name} em privado",
   "account.disable_notifications": "Parar de me notificar das publicações de @{name}",
   "account.domain_blocked": "Domínio bloqueado",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Marcar como lida",
   "conversation.open": "Ver conversa",
   "conversation.with": "Com {names}",
+  "copy_icon_button.copied": "Copiado para a área de transferência",
   "copypaste.copied": "Copiado",
   "copypaste.copy_to_clipboard": "Copiar para a área de transferência",
   "directory.federated": "Do fediverso conhecido",
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index 1417bed5f..c3110aa56 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -21,6 +21,7 @@
   "account.blocked": "E bllokuar",
   "account.browse_more_on_origin_server": "Shfletoni më tepër rreth profilit origjinal",
   "account.cancel_follow_request": "Tërhiq mbrapsht kërkesë për ndjekje",
+  "account.copy": "Kopjoje lidhjen te profili",
   "account.direct": "Përmendje private për @{name}",
   "account.disable_notifications": "Resht së njoftuari mua, kur poston @{name}",
   "account.domain_blocked": "Përkatësia u bllokua",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Vëri shenjë si të lexuar",
   "conversation.open": "Shfaq bisedën",
   "conversation.with": "Me {names}",
+  "copy_icon_button.copied": "U kopjua në të papastër",
   "copypaste.copied": "U kopjua",
   "copypaste.copy_to_clipboard": "Kopjoje në të papastër",
   "directory.federated": "Nga fedivers i njohur",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index b0a76b32a..c614215aa 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -21,6 +21,7 @@
   "account.blocked": "Блокиран",
   "account.browse_more_on_origin_server": "Прегледајте још на оригиналном профилу",
   "account.cancel_follow_request": "Откажи праћење",
+  "account.copy": "Копирај везу у профил",
   "account.direct": "Приватно помени @{name}",
   "account.disable_notifications": "Заустави обавештавање за објаве корисника @{name}",
   "account.domain_blocked": "Домен је блокиран",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Означи као прочитано",
   "conversation.open": "Прикажи разговор",
   "conversation.with": "Са {names}",
+  "copy_icon_button.copied": "Копирано",
   "copypaste.copied": "Копирано",
   "copypaste.copy_to_clipboard": "Копирај",
   "directory.federated": "Са знаног федиверзума",
@@ -390,6 +392,7 @@
   "lists.search": "Претражи међу људима које пратите",
   "lists.subheading": "Ваше листе",
   "load_pending": "{count, plural, one {# нова ставка} few {# нове ставке} other {# нових ставки}}",
+  "loading_indicator.label": "Учитавање…",
   "media_gallery.toggle_visible": "{number, plural, one {Сакриј слику} few {Сакриј слике} other {Сакриј слике}}",
   "moved_to_account_banner.text": "Ваш налог {disabledAccount} је тренутно онемогућен јер сте прешли на {movedToAccount}.",
   "mute_modal.duration": "Трајање",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 6f9f37ea1..0b1a7f6ad 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -21,6 +21,7 @@
   "account.blocked": "Blockerad",
   "account.browse_more_on_origin_server": "Läs mer på den ursprungliga profilen",
   "account.cancel_follow_request": "Återkalla din begäran om att få följa",
+  "account.copy": "Kopiera länk till profil",
   "account.direct": "Nämn @{name} privat",
   "account.disable_notifications": "Sluta notifiera mig när @{name} gör inlägg",
   "account.domain_blocked": "Domän blockerad",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Markera som läst",
   "conversation.open": "Visa konversation",
   "conversation.with": "Med {names}",
+  "copy_icon_button.copied": "Kopierad till urklipp",
   "copypaste.copied": "Kopierad",
   "copypaste.copy_to_clipboard": "Kopiera till urklipp",
   "directory.federated": "Från känt fediversum",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 2166cd2dd..c86b7c379 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -21,6 +21,7 @@
   "account.blocked": "ปิดกั้นอยู่",
   "account.browse_more_on_origin_server": "เรียกดูเพิ่มเติมในโปรไฟล์ดั้งเดิม",
   "account.cancel_follow_request": "ยกเลิกการติดตาม",
+  "account.copy": "คัดลอกลิงก์ไปยังโปรไฟล์",
   "account.direct": "กล่าวถึง @{name} แบบส่วนตัว",
   "account.disable_notifications": "หยุดแจ้งเตือนฉันเมื่อ @{name} โพสต์",
   "account.domain_blocked": "ปิดกั้นโดเมนอยู่",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "ทำเครื่องหมายว่าอ่านแล้ว",
   "conversation.open": "ดูการสนทนา",
   "conversation.with": "กับ {names}",
+  "copy_icon_button.copied": "คัดลอกไปยังคลิปบอร์ดแล้ว",
   "copypaste.copied": "คัดลอกแล้ว",
   "copypaste.copy_to_clipboard": "คัดลอกไปยังคลิปบอร์ด",
   "directory.federated": "จากจักรวาลสหพันธ์ที่รู้จัก",
@@ -481,15 +483,15 @@
   "onboarding.follows.title": "ปรับแต่งฟีดหน้าแรกของคุณ",
   "onboarding.profile.discoverable": "แสดงโปรไฟล์และโพสต์ในอัลกอริทึมการค้นพบ",
   "onboarding.profile.display_name": "ชื่อที่แสดง",
-  "onboarding.profile.display_name_hint": "ชื่อเต็มหรือชื่อแบบสนุกสนานของคุณ",
+  "onboarding.profile.display_name_hint": "ชื่อเต็มของคุณหรือชื่อแบบสนุกสนานของคุณ…",
   "onboarding.profile.indexable": "รวมโพสต์สาธารณะในผลลัพธ์การค้นหา",
   "onboarding.profile.lead": "คุณสามารถกลับมาทำต่อได้เสมอในการตั้งค่า ซึ่งจะมีตัวเลือกในการปรับแต่งมากกว่า",
   "onboarding.profile.note": "ชีวประวัติ",
-  "onboarding.profile.note_hint": "คุณสามารถ @กล่าวถึง ผู้คนอื่น ๆ หรือ #แฮชแท็ก",
+  "onboarding.profile.note_hint": "คุณสามารถ @กล่าวถึง ผู้คนอื่น ๆ หรือ #แฮชแท็ก…",
   "onboarding.profile.save_and_continue": "บันทึกและดำเนินการต่อ",
   "onboarding.profile.title": "การตั้งค่าโปรไฟล์",
-  "onboarding.profile.upload_avatar": "อัปโหลดรูปโปรไฟล์",
-  "onboarding.profile.upload_header": "อัปโหลดรูปส่วนหัวโปรไฟล์",
+  "onboarding.profile.upload_avatar": "อัปโหลดรูปภาพโปรไฟล์",
+  "onboarding.profile.upload_header": "อัปโหลดส่วนหัวโปรไฟล์",
   "onboarding.share.lead": "แจ้งให้ผู้คนทราบวิธีที่เขาสามารถค้นหาคุณใน Mastodon!",
   "onboarding.share.message": "ฉันคือ {username} ใน #Mastodon! มาติดตามฉันที่ {url}",
   "onboarding.share.next_steps": "ขั้นตอนถัดไปที่เป็นไปได้:",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 8649d9310..1307d7ea5 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -21,6 +21,7 @@
   "account.blocked": "Заблоковані",
   "account.browse_more_on_origin_server": "Переглянути більше в оригінальному профілі",
   "account.cancel_follow_request": "Відкликати запит на стеження",
+  "account.copy": "Копіювати посилання на профіль",
   "account.direct": "Особиста згадка @{name}",
   "account.disable_notifications": "Не повідомляти мене про дописи @{name}",
   "account.domain_blocked": "Домен заблоковано",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "Позначити як прочитане",
   "conversation.open": "Переглянути бесіду",
   "conversation.with": "З {names}",
+  "copy_icon_button.copied": "Скопійовано до буфера обміну",
   "copypaste.copied": "Скопійовано",
   "copypaste.copy_to_clipboard": "Копіювати до буфера обміну",
   "directory.federated": "З відомого федесвіту",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index cc6d8994d..79f463772 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -21,6 +21,7 @@
   "account.blocked": "已屏蔽",
   "account.browse_more_on_origin_server": "在原始个人资料页面上浏览详情",
   "account.cancel_follow_request": "撤回关注请求",
+  "account.copy": "复制资料卡链接",
   "account.direct": "私下提及 @{name}",
   "account.disable_notifications": "当 @{name} 发布嘟文时不要通知我",
   "account.domain_blocked": "域名已屏蔽",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "标记为已读",
   "conversation.open": "查看对话",
   "conversation.with": "与 {names}",
+  "copy_icon_button.copied": "已复制到剪贴板",
   "copypaste.copied": "已复制",
   "copypaste.copy_to_clipboard": "复制到剪贴板",
   "directory.federated": "来自已知的联邦宇宙",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index ea932bc5a..e7af27c5f 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -21,6 +21,7 @@
   "account.blocked": "已封鎖",
   "account.browse_more_on_origin_server": "前往原始的個人檔案頁瀏覽更多",
   "account.cancel_follow_request": "撤回追蹤請求",
+  "account.copy": "複製個人檔案連結",
   "account.direct": "私下提及 @{name}",
   "account.disable_notifications": "當 @{name} 發文時不要再通知我",
   "account.domain_blocked": "網域被封鎖",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "標為已讀",
   "conversation.open": "檢視對話",
   "conversation.with": "與 {names}",
+  "copy_icon_button.copied": "已複製到剪貼簿",
   "copypaste.copied": "已複製",
   "copypaste.copy_to_clipboard": "複製到剪貼簿",
   "directory.federated": "來自已知的聯盟網絡",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 4c52693cc..d4f3010c9 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -21,6 +21,7 @@
   "account.blocked": "已封鎖",
   "account.browse_more_on_origin_server": "在該伺服器上的個人檔案頁面瀏覽更多",
   "account.cancel_follow_request": "收回跟隨請求",
+  "account.copy": "複製個人檔案連結",
   "account.direct": "私訊 @{name}",
   "account.disable_notifications": "取消來自 @{name} 嘟文的通知",
   "account.domain_blocked": "已封鎖網域",
@@ -191,6 +192,7 @@
   "conversation.mark_as_read": "標記為已讀",
   "conversation.open": "檢視對話",
   "conversation.with": "與 {names}",
+  "copy_icon_button.copied": "已複製到剪貼簿",
   "copypaste.copied": "已複製",
   "copypaste.copy_to_clipboard": "複製到剪貼簿",
   "directory.federated": "來自已知聯邦宇宙",
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 34d1c85dc..72a36250f 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -611,6 +611,7 @@ es:
       created_at: Denunciado
       delete_and_resolve: Eliminar publicaciones
       forwarded: Reenviado
+      forwarded_replies_explanation: Este informe es de un usuario remoto y sobre contenido remoto. Se le ha enviado porque el contenido reportado es en respuesta a uno de sus usuarios.
       forwarded_to: Reenviado a %{domain}
       mark_as_resolved: Marcar como resuelto
       mark_as_sensitive: Marcar como sensible
diff --git a/config/locales/eu.yml b/config/locales/eu.yml
index dd7575c57..6aa92c2d1 100644
--- a/config/locales/eu.yml
+++ b/config/locales/eu.yml
@@ -613,6 +613,7 @@ eu:
       created_at: Salatua
       delete_and_resolve: Ezabatu bidalketak
       forwarded: Birbidalia
+      forwarded_replies_explanation: Salaketa hau beste instantzia bateko erabiltzaile baten edukiari buruzkoa da, eta zure instantziako erabiltzaile bati egidako erantzuna delako bidali dizugu mezu hau.
       forwarded_to: 'Hona birbidalia: %{domain}'
       mark_as_resolved: Markatu konpondutako gisa
       mark_as_sensitive: Markatu hunkigarri gisa
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index 0075c6592..955ec50e7 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -384,7 +384,7 @@ gl:
     domain_blocks:
       add_new: Engadir novo bloqueo de dominio
       confirm_suspension:
-        cancel: Cancelar
+        cancel: Desbotar
         confirm: Suspender
         permanent_action: Ao retirar a suspensión non restableces os datos ou a relación.
         preamble_html: Vas suspender a <strong>%{domain}</strong> e os seus subdominios.
@@ -611,6 +611,7 @@ gl:
       created_at: Denunciado
       delete_and_resolve: Eliminar publicacións
       forwarded: Reenviado
+      forwarded_replies_explanation: Esta denuncia procede dunha usuaria remota e acerca de contido remoto. Enviouseche unha copia porque o contido denunciado é unha resposta a unha das túas usuarias.
       forwarded_to: Reenviado a %{domain}
       mark_as_resolved: Marcar como resolto
       mark_as_sensitive: Marcar como sensible
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index a0ff3061f..60016c935 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -611,7 +611,7 @@ hu:
       created_at: Jelentve
       delete_and_resolve: Bejegyzések törlése
       forwarded: Továbbítva
-      forwarded_replies_explanation: Ez a jelentés egy távoli felhasználótól származik, és távoli tartalomról szól. Azért lett neked továbbítva, mert a jelentett tartalom az egyik felhasználódnak küldött válasz.
+      forwarded_replies_explanation: Ez a jelentés egy távoli flehasználótól származik, és távoli tartalomról szól. Azért lett neked továbbítva, mert a jelentett tartalom az egyik felhasználódnak küldött válasz.
       forwarded_to: 'Továbbítva ide: %{domain}'
       mark_as_resolved: Megjelölés megoldottként
       mark_as_sensitive: Érzékenynek jelölés
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index e11081fcd..7dcb5d632 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -763,7 +763,7 @@ ko:
           open: 누구나 가입 할 수 있음
       security:
         authorized_fetch: 연합된 서버들에게서 인증 필수
-        authorized_fetch_hint: 연합된 서버들에게서 인증을 요구하는 것은 사용자 레벨과 서버 레벨의 차단은 좀 더 확실하게 해줍니다. 한편으로는 성능적인 페널티, 답글의 전달 범위 감소, 몇몇 연합된 서비스들과의 호환성 문제가 있을 가능성이 있습니다. 추가적으로 이 기능은 전용 액터가 공개돤 게시물이나 계정을 페치하는 것은 막지 않습니다.
+        authorized_fetch_hint: 연합된 서버들에게서 인증을 요구하는 것은 사용자 레벨과 서버 레벨의 차단을 좀 더 확실하게 해줍니다. 한편으로는 성능적인 페널티, 답글의 전달 범위 감소, 몇몇 연합된 서비스들과의 호환성 문제가 있을 가능성이 있습니다. 추가적으로 이 기능은 전용 액터가 공개돤 게시물이나 계정을 페치하는 것은 막지 않습니다.
         authorized_fetch_overridden_hint: 현재 이 값은 환경변수에 의해 설정되어 있기에 설정을 변경할 수 없습니다.
         federation_authentication: 연합 인증 필수
       title: 서버 설정
diff --git a/config/locales/nn.yml b/config/locales/nn.yml
index 09de24a67..ad2acdda8 100644
--- a/config/locales/nn.yml
+++ b/config/locales/nn.yml
@@ -534,7 +534,7 @@ nn:
       total_reported: Rapportar om dei
       total_storage: Medievedlegg
       totals_time_period_hint_html: Totalsum vist nedanfor gjeld data for alle tidsperiodar.
-      unknown_instance: Dette domenet er ukjent for denne serveren.
+      unknown_instance: Domenet er ukjent for denne tenaren.
     invites:
       deactivate_all: Slå av alle
       filter:
@@ -611,7 +611,7 @@ nn:
       created_at: Rapportert
       delete_and_resolve: Slett innlegg
       forwarded: Videresendt
-      forwarded_replies_explanation: Denne rapporten er fra en ekstern bruker og handler om eksternt innhold. Den er videresendt til deg fordi det rapporterte innholdet svarer til en av brukerne dine.
+      forwarded_replies_explanation: Denne rapporten gjeld innhald på ein annan nettstad. Rapporten er vidaresend til deg fordi det rapporterte innhaldet er eit svar på noko ein av brukarane på nettstaden din har skrive.
       forwarded_to: Videresendt til %{domain}
       mark_as_resolved: Merk som løyst
       mark_as_sensitive: Marker som ømtolig
@@ -1042,14 +1042,14 @@ nn:
       hint_html: Berre ein ting til! Vi må bekrefte at du er et menneske (så vi kan halde spam ute!). Løys CAPTCHA-en nedanfor og klikk "Fortsett".
       title: Sikkerheitssjekk
     confirmations:
-      awaiting_review: Din e-post adresse er bekreftet! %{domain} ansatte gjennomgår nå registreringen din. Du vil motta en e-post hvis de godkjenner din konto!
-      awaiting_review_title: Din registrering blir vurdert
-      clicking_this_link: klikke på denne lenken
-      login_link: logg inn
-      proceed_to_login_html: Du kan nå fortsette til %{login_link}.
-      redirect_to_app_html: Du burde bli omdirigert til <strong>%{app_name}</strong> -appen. Hvis det ikke skjedde, kan du prøve %{clicking_this_link} eller manuelt gå tilbake til appen.
-      registration_complete: Registreringen på %{domain} er nå fullført!
-      welcome_title: Velkommen, %{name}!
+      awaiting_review: Epostadressa di er stadfesta! Styrarane på %{domain} ser gjennom registreringa di. Du får ein epost frå dei om dei godkjenner brukarkontoen din.
+      awaiting_review_title: Me går gjennom registreringa di
+      clicking_this_link: klikka på denne lenka
+      login_link: logga inn
+      proceed_to_login_html: No kan du %{login_link}.
+      redirect_to_app_html: Du skulle vorte vidaresend til <strong>%{app_name}</strong>-appen. Viss det ikkje skjedde, kan du prøva å %{clicking_this_link} eller manuelt gå tilbake til appen.
+      registration_complete: Du har registrert deg som brukar på %{domain}.
+      welcome_title: Velkomen, %{name}!
       wrong_email_hint: Viss epostadressa er feil, kan du endra ho i kontoinnstillingane.
     delete_account: Slett konto
     delete_account_html: Om du vil sletta kontoen din, kan du <a href="%{path}">gå hit</a>. Du vert spurd etter stadfesting.
@@ -1111,7 +1111,7 @@ nn:
       functional: Kontoen din er fullt operativt.
       pending: Søknaden din ventar på gjennomgang frå personalet vårt. Dette kan taka litt tid. Du får ein e-post om søknaden din vert godkjend.
       redirecting_to: Kontoen din er inaktiv fordi den for øyeblikket omdirigerer til %{acct}.
-      self_destruct: Siden %{domain} stenger, vil du kun ha begrenset tilgang til kontoen din.
+      self_destruct: Av di %{domain} er i ferd med å stenga, vil du berre få avgrensa tilgang til brukarkontoen din.
       view_strikes: Vis tidligere advarsler mot kontoen din
     too_fast: Skjemaet ble sendt inn for raskt, prøv på nytt.
     use_security_key: Bruk sikkerhetsnøkkel
@@ -1583,8 +1583,8 @@ nn:
     over_total_limit: Du har overskredet grensen på %{limit} planlagte tuter
     too_soon: Den planlagte datoen må være i fremtiden
   self_destruct:
-    lead_html: Dessverre stenger <strong>%{domain}</strong> for alltid. Hvis du hadde en konto der vil du ikke kunne fortsette å bruke den, men du kan fremdeles be om en sikkerhetskopi av dataene dine.
-    title: Denne serveren stenger
+    lead_html: Diverre stengjer <strong>%{domain}</strong> dørene for godt. Viss du hadde ein brukarkonto der, vil du ikkje kunna halda fram å bruka han, men du kan få ut ein tryggingskopi av dataa dine.
+    title: Denne tenaren stengjer
   sessions:
     activity: Siste aktivitet
     browser: Nettlesar
diff --git a/config/locales/no.yml b/config/locales/no.yml
index 3cf2df3a1..42bd6193c 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -1533,7 +1533,7 @@
     posting_defaults: Innleggsstandarder
     public_timelines: Offentlige tidslinjer
   privacy:
-    hint_html: "<strong>Tilpass hvordan du vil at din profil og dine innlegg skal bli funnet.</strong> En rekke funksjoner i Mastodon kan hjelpe deg med å nå et bredere publikum når de aktiverte. Ta deg et øyeblikk til å vurdere disse innstillingene for å forsikre deg om at de passer deg og ditt bruk."
+    hint_html: "<strong>Tilpass hvordan du vil at din profil og dine innlegg skal bli funnet.</strong> En rekke funksjoner i Mastodon kan hjelpe deg med å nå et bredere publikum når det er aktivert. Ta deg et øyeblikk til å vurdere disse innstillingene for å forsikre deg om at de passer deg og ditt bruk."
     privacy: Personvern
     privacy_hint_html: Kontrollér hvor mye du ønsker å dele til fordel for andre. Folk oppdager interessante profiler og kule app'er ved å bla gjennom andres følgere og ved å se hvilke app'er de bruker, men du kan velge å holde det skjult.
     reach: Rekkevidde
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 407908517..b271b15f1 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -623,6 +623,7 @@ sr:
       created_at: Пријављена
       delete_and_resolve: Обриши објаве
       forwarded: Прослеђено
+      forwarded_replies_explanation: Овај извештај је од удаљеног корисника и о удаљеном садржају. Прослеђен вам је јер је пријављени садржај у одговору једном од ваших корисника.
       forwarded_to: Прослеђено ка %{domain}
       mark_as_resolved: Означи као решену
       mark_as_sensitive: Обележи као осетљиво
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 79d668d3e..0b7498e7d 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -560,7 +560,7 @@ th:
       enabled: เปิดใช้งานอยู่
       inbox_url: URL ของรีเลย์
       pending: กำลังรอการอนุมัติของรีเลย์
-      save_and_enable: บันทึกแล้วเปิดใช้งาน
+      save_and_enable: บันทึกและเปิดใช้งาน
       setup: ตั้งค่าการเชื่อมต่อรีเลย์
       signatures_not_enabled: รีเลย์อาจทำงานไม่ถูกต้องขณะที่มีการเปิดใช้งานโหมดปลอดภัยหรือโหมดการติดต่อกับภายนอกแบบจำกัด
       status: สถานะ
@@ -599,7 +599,7 @@ th:
       created_at: รายงานเมื่อ
       delete_and_resolve: ลบโพสต์
       forwarded: ส่งต่อแล้ว
-      forwarded_replies_explanation: รายงานนี้มาจากผู้ใช้ระยะไกล และเป็นรายงานเกี่ยวกับเนื้อหาระยะไกล ซึ่งถูกส่งต่อมาหาคุณเนื่องจากเนื้อหาที่ถูกรายงานอยู่ในการตอบกลับไปยังหนึ่งในผู้ใช้ของคุณ
+      forwarded_replies_explanation: รายงานนี้มาจากผู้ใช้ระยะไกลและเกี่ยวกับเนื้อหาระยะไกล มีการส่งต่อรายงานไปยังคุณเนื่องจากเนื้อหาที่รายงานอยู่ในการตอบกลับหนึ่งในผู้ใช้ของคุณ
       forwarded_to: ส่งต่อไปยัง %{domain} แล้ว
       mark_as_resolved: ทำเครื่องหมายว่าแก้ปัญหาแล้ว
       mark_as_sensitive: ทำเครื่องหมายว่าละเอียดอ่อน
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index 9d4d95a83..5882eae31 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -611,6 +611,7 @@ tr:
       created_at: Şikayet edildi
       delete_and_resolve: Gönderileri sil
       forwarded: İletildi
+      forwarded_replies_explanation: Bu bildirim başka bir sunucudaki kullanıcı ve içerik ile ilgili. Bildirilen içerik kullanıcılarınızdan birine yanıt şeklinde olduğu için size yönlendirildi.
       forwarded_to: "%{domain}'e iletildi"
       mark_as_resolved: Giderildi olarak işaretle
       mark_as_sensitive: Hassas olarak işaretle
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index b98193065..b788b53e1 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -599,6 +599,7 @@ zh-CN:
       created_at: 举报时间
       delete_and_resolve: 删除嘟文
       forwarded: 已转发
+      forwarded_replies_explanation: 该举报来自外站用户,涉及外站内容。之所以转发给您,是因为被举报的内容是对您站点一位用户的回复。
       forwarded_to: 转发举报至 %{domain}
       mark_as_resolved: 标记为已处理
       mark_as_sensitive: 标记为敏感内容

From 549e8e7baf62eb4ecb4dd039e301e6280889218d Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 17 Nov 2023 04:50:19 -0500
Subject: [PATCH 58/63] Add `email_spec` and speedup/cleanup to `spec/mailers`
 (#27902)

---
 Gemfile                                       |   3 +
 Gemfile.lock                                  |   5 +
 spec/mailers/admin_mailer_spec.rb             |  97 +++++++-------
 spec/mailers/notification_mailer_spec.rb      | 109 ++++++++--------
 spec/mailers/user_mailer_spec.rb              | 119 ++++++++++++------
 spec/rails_helper.rb                          |   1 +
 .../api/v1/admin/account_actions_spec.rb      |  12 +-
 7 files changed, 194 insertions(+), 152 deletions(-)

diff --git a/Gemfile b/Gemfile
index add7b3606..74672ad06 100644
--- a/Gemfile
+++ b/Gemfile
@@ -109,6 +109,9 @@ group :test do
   # RSpec progress bar formatter
   gem 'fuubar', '~> 2.5'
 
+  # RSpec helpers for email specs
+  gem 'email_spec'
+
   # Extra RSpec extenion methods and helpers for sidekiq
   gem 'rspec-sidekiq', '~> 4.0'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 20c958e2e..beec8b39c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -260,6 +260,10 @@ GEM
     elasticsearch-transport (7.13.3)
       faraday (~> 1)
       multi_json
+    email_spec (2.2.2)
+      htmlentities (~> 4.3.3)
+      launchy (~> 2.1)
+      mail (~> 2.7)
     encryptor (3.0.0)
     erubi (1.12.0)
     et-orbi (1.2.7)
@@ -853,6 +857,7 @@ DEPENDENCIES
   doorkeeper (~> 5.6)
   dotenv-rails (~> 2.8)
   ed25519 (~> 1.3)
+  email_spec
   fabrication (~> 2.30)
   faker (~> 3.2)
   fast_blank (~> 1.0)
diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb
index 423dce88a..9f0d89996 100644
--- a/spec/mailers/admin_mailer_spec.rb
+++ b/spec/mailers/admin_mailer_spec.rb
@@ -13,14 +13,13 @@ RSpec.describe AdminMailer do
       recipient.user.update(locale: :en)
     end
 
-    it 'renders the headers' do
-      expect(mail.subject).to eq("New report for cb6e6126.ngrok.io (##{report.id})")
-      expect(mail.to).to eq [recipient.user_email]
-      expect(mail.from).to eq ['notifications@localhost']
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to eq("Mike,\r\n\r\nJohn has reported Mike\r\n\r\nView: https://cb6e6126.ngrok.io/admin/reports/#{report.id}\r\n")
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(deliver_to(recipient.user_email))
+        .and(deliver_from('notifications@localhost'))
+        .and(have_subject("New report for cb6e6126.ngrok.io (##{report.id})"))
+        .and(have_body_text("Mike,\r\n\r\nJohn has reported Mike\r\n\r\nView: https://cb6e6126.ngrok.io/admin/reports/#{report.id}\r\n"))
     end
   end
 
@@ -33,14 +32,13 @@ RSpec.describe AdminMailer do
       recipient.user.update(locale: :en)
     end
 
-    it 'renders the headers' do
-      expect(mail.subject).to eq("#{appeal.account.username} is appealing a moderation decision on cb6e6126.ngrok.io")
-      expect(mail.to).to eq [recipient.user_email]
-      expect(mail.from).to eq ['notifications@localhost']
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to match "#{appeal.account.username} is appealing a moderation decision by #{appeal.strike.account.username}"
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(deliver_to(recipient.user_email))
+        .and(deliver_from('notifications@localhost'))
+        .and(have_subject("#{appeal.account.username} is appealing a moderation decision on cb6e6126.ngrok.io"))
+        .and(have_body_text("#{appeal.account.username} is appealing a moderation decision by #{appeal.strike.account.username}"))
     end
   end
 
@@ -53,14 +51,13 @@ RSpec.describe AdminMailer do
       recipient.user.update(locale: :en)
     end
 
-    it 'renders the headers' do
-      expect(mail.subject).to eq("New account up for review on cb6e6126.ngrok.io (#{user.account.username})")
-      expect(mail.to).to eq [recipient.user_email]
-      expect(mail.from).to eq ['notifications@localhost']
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to match 'The details of the new account are below. You can approve or reject this application.'
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(deliver_to(recipient.user_email))
+        .and(deliver_from('notifications@localhost'))
+        .and(have_subject("New account up for review on cb6e6126.ngrok.io (#{user.account.username})"))
+        .and(have_body_text('The details of the new account are below. You can approve or reject this application.'))
     end
   end
 
@@ -75,14 +72,13 @@ RSpec.describe AdminMailer do
       recipient.user.update(locale: :en)
     end
 
-    it 'renders the headers' do
-      expect(mail.subject).to eq('New trends up for review on cb6e6126.ngrok.io')
-      expect(mail.to).to eq [recipient.user_email]
-      expect(mail.from).to eq ['notifications@localhost']
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to match 'The following items need a review before they can be displayed publicly'
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(deliver_to(recipient.user_email))
+        .and(deliver_from('notifications@localhost'))
+        .and(have_subject('New trends up for review on cb6e6126.ngrok.io'))
+        .and(have_body_text('The following items need a review before they can be displayed publicly'))
     end
   end
 
@@ -94,14 +90,13 @@ RSpec.describe AdminMailer do
       recipient.user.update(locale: :en)
     end
 
-    it 'renders the headers' do
-      expect(mail.subject).to eq('New Mastodon versions are available for cb6e6126.ngrok.io!')
-      expect(mail.to).to eq [recipient.user_email]
-      expect(mail.from).to eq ['notifications@localhost']
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to match 'New Mastodon versions have been released, you may want to update!'
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(deliver_to(recipient.user_email))
+        .and(deliver_from('notifications@localhost'))
+        .and(have_subject('New Mastodon versions are available for cb6e6126.ngrok.io!'))
+        .and(have_body_text('New Mastodon versions have been released, you may want to update!'))
     end
   end
 
@@ -113,18 +108,16 @@ RSpec.describe AdminMailer do
       recipient.user.update(locale: :en)
     end
 
-    it 'renders the headers', :aggregate_failures do
-      expect(mail.subject).to eq('Critical Mastodon updates are available for cb6e6126.ngrok.io!')
-      expect(mail.to).to eq [recipient.user_email]
-      expect(mail.from).to eq ['notifications@localhost']
-
-      expect(mail['Importance'].value).to eq 'high'
-      expect(mail['Priority'].value).to eq 'urgent'
-      expect(mail['X-Priority'].value).to eq '1'
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to match 'New critical versions of Mastodon have been released, you may want to update as soon as possible!'
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(deliver_to(recipient.user_email))
+        .and(deliver_from('notifications@localhost'))
+        .and(have_subject('Critical Mastodon updates are available for cb6e6126.ngrok.io!'))
+        .and(have_body_text('New critical versions of Mastodon have been released, you may want to update as soon as possible!'))
+        .and(have_header('Importance', 'high'))
+        .and(have_header('Priority', 'urgent'))
+        .and(have_header('X-Priority', '1'))
     end
   end
 end
diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb
index 78a497c06..eab196166 100644
--- a/spec/mailers/notification_mailer_spec.rb
+++ b/spec/mailers/notification_mailer_spec.rb
@@ -8,24 +8,27 @@ RSpec.describe NotificationMailer do
   let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') }
   let(:own_status)     { Fabricate(:status, account: receiver.account, text: 'The body of the own status') }
 
-  shared_examples 'headers' do |type, thread|
-    it 'renders the to and from headers' do
-      expect(mail[:to].value).to eq "#{receiver.account.username} <#{receiver.email}>"
-      expect(mail.from).to eq ['notifications@localhost']
+  shared_examples 'standard headers' do |type|
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(have_header('To', "#{receiver.account.username} <#{receiver.email}>"))
+        .and(have_header('List-ID', "<#{type}.alice.cb6e6126.ngrok.io>"))
+        .and(have_header('List-Unsubscribe', %r{<https://cb6e6126.ngrok.io/unsubscribe\?token=.+>}))
+        .and(have_header('List-Unsubscribe', /&type=#{type}/))
+        .and(have_header('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click'))
+        .and(deliver_to("#{receiver.account.username} <#{receiver.email}>"))
+        .and(deliver_from('notifications@localhost'))
     end
+  end
 
-    it 'renders the list headers' do
-      expect(mail['List-ID'].value).to eq "<#{type}.alice.cb6e6126.ngrok.io>"
-      expect(mail['List-Unsubscribe'].value).to match(%r{<https://cb6e6126.ngrok.io/unsubscribe\?token=.+>})
-      expect(mail['List-Unsubscribe'].value).to match("&type=#{type}")
-      expect(mail['List-Unsubscribe-Post'].value).to eq 'List-Unsubscribe=One-Click'
-    end
-
-    if thread
-      it 'renders the thread headers' do
-        expect(mail['In-Reply-To'].value).to match(/<conversation-\d+.\d\d\d\d-\d\d-\d\d@cb6e6126.ngrok.io>/)
-        expect(mail['References'].value).to match(/<conversation-\d+.\d\d\d\d-\d\d-\d\d@cb6e6126.ngrok.io>/)
-      end
+  shared_examples 'thread headers' do
+    it 'renders the email with conversation thread headers' do
+      conversation_header_regex = /<conversation-\d+.\d\d\d\d-\d\d-\d\d@cb6e6126.ngrok.io>/
+      expect(mail)
+        .to be_present
+        .and(have_header('In-Reply-To', conversation_header_regex))
+        .and(have_header('References', conversation_header_regex))
     end
   end
 
@@ -35,15 +38,15 @@ RSpec.describe NotificationMailer do
     let(:mail) { prepared_mailer_for(receiver.account).mention }
 
     include_examples 'localized subject', 'notification_mailer.mention.subject', name: 'bob'
-    include_examples 'headers', 'mention', true
+    include_examples 'standard headers', 'mention'
+    include_examples 'thread headers'
 
-    it 'renders the subject' do
-      expect(mail.subject).to eq('You were mentioned by bob')
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to match('You were mentioned by bob')
-      expect(mail.body.encoded).to include 'The body of the foreign status'
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(have_subject('You were mentioned by bob'))
+        .and(have_body_text('You were mentioned by bob'))
+        .and(have_body_text('The body of the foreign status'))
     end
   end
 
@@ -53,14 +56,13 @@ RSpec.describe NotificationMailer do
     let(:mail) { prepared_mailer_for(receiver.account).follow }
 
     include_examples 'localized subject', 'notification_mailer.follow.subject', name: 'bob'
-    include_examples 'headers', 'follow', false
+    include_examples 'standard headers', 'follow'
 
-    it 'renders the subject' do
-      expect(mail.subject).to eq('bob is now following you')
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to match('bob is now following you')
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(have_subject('bob is now following you'))
+        .and(have_body_text('bob is now following you'))
     end
   end
 
@@ -70,15 +72,15 @@ RSpec.describe NotificationMailer do
     let(:mail) { prepared_mailer_for(own_status.account).favourite }
 
     include_examples 'localized subject', 'notification_mailer.favourite.subject', name: 'bob'
-    include_examples 'headers', 'favourite', true
+    include_examples 'standard headers', 'favourite'
+    include_examples 'thread headers'
 
-    it 'renders the subject' do
-      expect(mail.subject).to eq('bob favorited your post')
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to match('Your post was favorited by bob')
-      expect(mail.body.encoded).to include 'The body of the own status'
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(have_subject('bob favorited your post'))
+        .and(have_body_text('Your post was favorited by bob'))
+        .and(have_body_text('The body of the own status'))
     end
   end
 
@@ -88,15 +90,15 @@ RSpec.describe NotificationMailer do
     let(:mail) { prepared_mailer_for(own_status.account).reblog }
 
     include_examples 'localized subject', 'notification_mailer.reblog.subject', name: 'bob'
-    include_examples 'headers', 'reblog', true
+    include_examples 'standard headers', 'reblog'
+    include_examples 'thread headers'
 
-    it 'renders the subject' do
-      expect(mail.subject).to eq('bob boosted your post')
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to match('Your post was boosted by bob')
-      expect(mail.body.encoded).to include 'The body of the own status'
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(have_subject('bob boosted your post'))
+        .and(have_body_text('Your post was boosted by bob'))
+        .and(have_body_text('The body of the own status'))
     end
   end
 
@@ -106,14 +108,13 @@ RSpec.describe NotificationMailer do
     let(:mail) { prepared_mailer_for(receiver.account).follow_request }
 
     include_examples 'localized subject', 'notification_mailer.follow_request.subject', name: 'bob'
-    include_examples 'headers', 'follow_request', false
+    include_examples 'standard headers', 'follow_request'
 
-    it 'renders the subject' do
-      expect(mail.subject).to eq('Pending follower: bob')
-    end
-
-    it 'renders the body' do
-      expect(mail.body.encoded).to match('bob has requested to follow you')
+    it 'renders the email' do
+      expect(mail)
+        .to be_present
+        .and(have_subject('Pending follower: bob'))
+        .and(have_body_text('bob has requested to follow you'))
     end
   end
 
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index c661f5bbd..4a4392824 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -10,9 +10,12 @@ describe UserMailer do
 
     it 'renders confirmation instructions' do
       receiver.update!(locale: nil)
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.confirmation_instructions.title')
-      expect(mail.body.encoded).to include 'spec'
-      expect(mail.body.encoded).to include Rails.configuration.x.local_domain
+
+      expect(mail)
+        .to be_present
+        .and(have_body_text(I18n.t('devise.mailer.confirmation_instructions.title')))
+        .and(have_body_text('spec'))
+        .and(have_body_text(Rails.configuration.x.local_domain))
     end
 
     include_examples 'localized subject',
@@ -25,13 +28,17 @@ describe UserMailer do
 
     it 'renders reconfirmation instructions' do
       receiver.update!(email: 'new-email@example.com', locale: nil)
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.reconfirmation_instructions.title')
-      expect(mail.body.encoded).to include 'spec'
-      expect(mail.body.encoded).to include Rails.configuration.x.local_domain
-      expect(mail.subject).to eq I18n.t('devise.mailer.reconfirmation_instructions.subject',
-                                        instance: Rails.configuration.x.local_domain,
-                                        locale: I18n.default_locale)
+
+      expect(mail)
+        .to be_present
+        .and(have_body_text(I18n.t('devise.mailer.reconfirmation_instructions.title')))
+        .and(have_body_text('spec'))
+        .and(have_body_text(Rails.configuration.x.local_domain))
     end
+
+    include_examples 'localized subject',
+                     'devise.mailer.confirmation_instructions.subject',
+                     instance: Rails.configuration.x.local_domain
   end
 
   describe '#reset_password_instructions' do
@@ -39,8 +46,11 @@ describe UserMailer do
 
     it 'renders reset password instructions' do
       receiver.update!(locale: nil)
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.reset_password_instructions.title')
-      expect(mail.body.encoded).to include 'spec'
+
+      expect(mail)
+        .to be_present
+        .and(have_body_text(I18n.t('devise.mailer.reset_password_instructions.title')))
+        .and(have_body_text('spec'))
     end
 
     include_examples 'localized subject',
@@ -52,7 +62,10 @@ describe UserMailer do
 
     it 'renders password change notification' do
       receiver.update!(locale: nil)
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.password_change.title')
+
+      expect(mail)
+        .to be_present
+        .and(have_body_text(I18n.t('devise.mailer.password_change.title')))
     end
 
     include_examples 'localized subject',
@@ -64,7 +77,10 @@ describe UserMailer do
 
     it 'renders email change notification' do
       receiver.update!(locale: nil)
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.email_changed.title')
+
+      expect(mail)
+        .to be_present
+        .and(have_body_text(I18n.t('devise.mailer.email_changed.title')))
     end
 
     include_examples 'localized subject',
@@ -77,8 +93,11 @@ describe UserMailer do
 
     it 'renders warning notification' do
       receiver.update!(locale: nil)
-      expect(mail.body.encoded).to include I18n.t('user_mailer.warning.title.suspend', acct: receiver.account.acct)
-      expect(mail.body.encoded).to include strike.text
+
+      expect(mail)
+        .to be_present
+        .and(have_body_text(I18n.t('user_mailer.warning.title.suspend', acct: receiver.account.acct)))
+        .and(have_body_text(strike.text))
     end
   end
 
@@ -88,7 +107,10 @@ describe UserMailer do
 
     it 'renders webauthn credential deleted notification' do
       receiver.update!(locale: nil)
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.webauthn_credential.deleted.title')
+
+      expect(mail)
+        .to be_present
+        .and(have_body_text(I18n.t('devise.mailer.webauthn_credential.deleted.title')))
     end
 
     include_examples 'localized subject',
@@ -103,7 +125,10 @@ describe UserMailer do
 
     it 'renders suspicious sign in notification' do
       receiver.update!(locale: nil)
-      expect(mail.body.encoded).to include I18n.t('user_mailer.suspicious_sign_in.explanation')
+
+      expect(mail)
+        .to be_present
+        .and(have_body_text(I18n.t('user_mailer.suspicious_sign_in.explanation')))
     end
 
     include_examples 'localized subject',
@@ -115,8 +140,10 @@ describe UserMailer do
     let(:mail) { described_class.appeal_approved(receiver, appeal) }
 
     it 'renders appeal_approved notification' do
-      expect(mail.subject).to eq I18n.t('user_mailer.appeal_approved.subject', date: I18n.l(appeal.created_at))
-      expect(mail.body.encoded).to include I18n.t('user_mailer.appeal_approved.title')
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('user_mailer.appeal_approved.subject', date: I18n.l(appeal.created_at))))
+        .and(have_body_text(I18n.t('user_mailer.appeal_approved.title')))
     end
   end
 
@@ -125,8 +152,10 @@ describe UserMailer do
     let(:mail) { described_class.appeal_rejected(receiver, appeal) }
 
     it 'renders appeal_rejected notification' do
-      expect(mail.subject).to eq I18n.t('user_mailer.appeal_rejected.subject', date: I18n.l(appeal.created_at))
-      expect(mail.body.encoded).to include I18n.t('user_mailer.appeal_rejected.title')
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('user_mailer.appeal_rejected.subject', date: I18n.l(appeal.created_at))))
+        .and(have_body_text(I18n.t('user_mailer.appeal_rejected.title')))
     end
   end
 
@@ -134,8 +163,10 @@ describe UserMailer do
     let(:mail) { described_class.two_factor_enabled(receiver) }
 
     it 'renders two_factor_enabled mail' do
-      expect(mail.subject).to eq I18n.t('devise.mailer.two_factor_enabled.subject')
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.two_factor_enabled.explanation')
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('devise.mailer.two_factor_enabled.subject')))
+        .and(have_body_text(I18n.t('devise.mailer.two_factor_enabled.explanation')))
     end
   end
 
@@ -143,8 +174,10 @@ describe UserMailer do
     let(:mail) { described_class.two_factor_disabled(receiver) }
 
     it 'renders two_factor_disabled mail' do
-      expect(mail.subject).to eq I18n.t('devise.mailer.two_factor_disabled.subject')
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.two_factor_disabled.explanation')
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('devise.mailer.two_factor_disabled.subject')))
+        .and(have_body_text(I18n.t('devise.mailer.two_factor_disabled.explanation')))
     end
   end
 
@@ -152,8 +185,10 @@ describe UserMailer do
     let(:mail) { described_class.webauthn_enabled(receiver) }
 
     it 'renders webauthn_enabled mail' do
-      expect(mail.subject).to eq I18n.t('devise.mailer.webauthn_enabled.subject')
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.webauthn_enabled.explanation')
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('devise.mailer.webauthn_enabled.subject')))
+        .and(have_body_text(I18n.t('devise.mailer.webauthn_enabled.explanation')))
     end
   end
 
@@ -161,8 +196,10 @@ describe UserMailer do
     let(:mail) { described_class.webauthn_disabled(receiver) }
 
     it 'renders webauthn_disabled mail' do
-      expect(mail.subject).to eq I18n.t('devise.mailer.webauthn_disabled.subject')
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.webauthn_disabled.explanation')
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('devise.mailer.webauthn_disabled.subject')))
+        .and(have_body_text(I18n.t('devise.mailer.webauthn_disabled.explanation')))
     end
   end
 
@@ -170,8 +207,10 @@ describe UserMailer do
     let(:mail) { described_class.two_factor_recovery_codes_changed(receiver) }
 
     it 'renders two_factor_recovery_codes_changed mail' do
-      expect(mail.subject).to eq I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.two_factor_recovery_codes_changed.explanation')
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')))
+        .and(have_body_text(I18n.t('devise.mailer.two_factor_recovery_codes_changed.explanation')))
     end
   end
 
@@ -180,8 +219,10 @@ describe UserMailer do
     let(:mail) { described_class.webauthn_credential_added(receiver, credential) }
 
     it 'renders webauthn_credential_added mail' do
-      expect(mail.subject).to eq I18n.t('devise.mailer.webauthn_credential.added.subject')
-      expect(mail.body.encoded).to include I18n.t('devise.mailer.webauthn_credential.added.explanation')
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('devise.mailer.webauthn_credential.added.subject')))
+        .and(have_body_text(I18n.t('devise.mailer.webauthn_credential.added.explanation')))
     end
   end
 
@@ -189,8 +230,10 @@ describe UserMailer do
     let(:mail) { described_class.welcome(receiver) }
 
     it 'renders welcome mail' do
-      expect(mail.subject).to eq I18n.t('user_mailer.welcome.subject')
-      expect(mail.body.encoded).to include I18n.t('user_mailer.welcome.explanation')
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('user_mailer.welcome.subject')))
+        .and(have_body_text(I18n.t('user_mailer.welcome.explanation')))
     end
   end
 
@@ -199,8 +242,10 @@ describe UserMailer do
     let(:mail) { described_class.backup_ready(receiver, backup) }
 
     it 'renders backup_ready mail' do
-      expect(mail.subject).to eq I18n.t('user_mailer.backup_ready.subject')
-      expect(mail.body.encoded).to include I18n.t('user_mailer.backup_ready.explanation')
+      expect(mail)
+        .to be_present
+        .and(have_subject(I18n.t('user_mailer.backup_ready.subject')))
+        .and(have_body_text(I18n.t('user_mailer.backup_ready.explanation')))
     end
   end
 end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 79f98f2e2..68023b70d 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -21,6 +21,7 @@ require 'webmock/rspec'
 require 'paperclip/matchers'
 require 'capybara/rspec'
 require 'chewy/rspec'
+require 'email_spec/rspec'
 
 Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
 
diff --git a/spec/requests/api/v1/admin/account_actions_spec.rb b/spec/requests/api/v1/admin/account_actions_spec.rb
index bdf1f08e4..c14e08c21 100644
--- a/spec/requests/api/v1/admin/account_actions_spec.rb
+++ b/spec/requests/api/v1/admin/account_actions_spec.rb
@@ -8,18 +8,12 @@ RSpec.describe 'Account actions' do
   let(:scopes)  { 'admin:write admin:write:accounts' }
   let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
   let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
-  let(:mailer)  { instance_double(ActionMailer::MessageDelivery, deliver_later!: nil) }
-
-  before do
-    allow(UserMailer).to receive(:warning).with(target_account.user, anything).and_return(mailer)
-  end
 
   shared_examples 'a successful notification delivery' do
     it 'notifies the user about the action taken' do
-      subject
-
-      expect(UserMailer).to have_received(:warning).with(target_account.user, anything).once
-      expect(mailer).to have_received(:deliver_later!).once
+      expect { subject }
+        .to have_enqueued_job(ActionMailer::MailDeliveryJob)
+        .with('UserMailer', 'warning', 'deliver_now!', args: [User, AccountWarning])
     end
   end
 

From 0e9801443f8f91ff49b47f82151ee5984c9bd6c1 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 17 Nov 2023 11:37:04 +0100
Subject: [PATCH 59/63] Change to single opt-in during profile setup in
 onboarding in web UI (#27876)

---
 .../mastodon/features/onboarding/profile.jsx  | 34 ++++++-------
 app/javascript/mastodon/locales/en.json       |  5 +-
 app/javascript/styles/mastodon/forms.scss     | 49 +++++++++++++++++--
 3 files changed, 64 insertions(+), 24 deletions(-)

diff --git a/app/javascript/mastodon/features/onboarding/profile.jsx b/app/javascript/mastodon/features/onboarding/profile.jsx
index 19ba0bcb9..09e6b2c6c 100644
--- a/app/javascript/mastodon/features/onboarding/profile.jsx
+++ b/app/javascript/mastodon/features/onboarding/profile.jsx
@@ -5,8 +5,8 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
 import classNames from 'classnames';
 import { useHistory } from 'react-router-dom';
 
-import { useDispatch } from 'react-redux';
 
+import { useDispatch } from 'react-redux';
 
 import { ReactComponent as AddPhotoAlternateIcon } from '@material-symbols/svg-600/outlined/add_photo_alternate.svg';
 import { ReactComponent as EditIcon } from '@material-symbols/svg-600/outlined/edit.svg';
@@ -33,7 +33,6 @@ export const Profile = () => {
   const [avatar, setAvatar] = useState(null);
   const [header, setHeader] = useState(null);
   const [discoverable, setDiscoverable] = useState(account.get('discoverable'));
-  const [indexable, setIndexable] = useState(account.get('indexable'));
   const [isSaving, setIsSaving] = useState(false);
   const [errors, setErrors] = useState();
   const avatarFileRef = createRef();
@@ -54,10 +53,6 @@ export const Profile = () => {
     setDiscoverable(e.target.checked);
   }, [setDiscoverable]);
 
-  const handleIndexableChange = useCallback(e => {
-    setIndexable(e.target.checked);
-  }, [setIndexable]);
-
   const handleAvatarChange = useCallback(e => {
     setAvatar(e.target?.files?.[0]);
   }, [setAvatar]);
@@ -78,12 +73,12 @@ export const Profile = () => {
       avatar,
       header,
       discoverable,
-      indexable,
+      indexable: discoverable,
     })).then(() => history.push('/start/follows')).catch(err => {
       setIsSaving(false);
       setErrors(err.response.data.details);
     });
-  }, [dispatch, displayName, note, avatar, header, discoverable, indexable, history]);
+  }, [dispatch, displayName, note, avatar, header, discoverable, history]);
 
   return (
     <>
@@ -141,18 +136,21 @@ export const Profile = () => {
               <textarea id='note' value={note} onChange={handleNoteChange} maxLength={500} />
             </div>
           </div>
+
+          <label className='app-form__toggle'>
+            <div className='app-form__toggle__label'>
+              <strong><FormattedMessage id='onboarding.profile.discoverable' defaultMessage='Make my profile discoverable' /></strong> <span className='recommended'><FormattedMessage id='recommended' defaultMessage='Recommended' /></span>
+              <span className='hint'><FormattedMessage id='onboarding.profile.discoverable_hint' defaultMessage='When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.' /></span>
+            </div>
+
+            <div className='app-form__toggle__toggle'>
+              <div>
+                <Toggle checked={discoverable} onChange={handleDiscoverableChange} />
+              </div>
+            </div>
+          </label>
         </div>
 
-        <label className='report-dialog-modal__toggle'>
-          <Toggle checked={discoverable} onChange={handleDiscoverableChange} />
-          <FormattedMessage id='onboarding.profile.discoverable' defaultMessage='Feature profile and posts in discovery algorithms' />
-        </label>
-
-        <label className='report-dialog-modal__toggle'>
-          <Toggle checked={indexable} onChange={handleIndexableChange} />
-          <FormattedMessage id='onboarding.profile.indexable' defaultMessage='Include public posts in search results' />
-        </label>
-
         <div className='onboarding__footer'>
           <Button block onClick={handleSubmit} disabled={isSaving}>{isSaving ? <LoadingIndicator /> : <FormattedMessage id='onboarding.profile.save_and_continue' defaultMessage='Save and continue' />}</Button>
         </div>
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 16941e2ca..ed8bfe166 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -481,10 +481,10 @@
   "onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
   "onboarding.follows.lead": "Your home feed is the primary way to experience Mastodon. The more people you follow, the more active and interesting it will be. To get you started, here are some suggestions:",
   "onboarding.follows.title": "Personalize your home feed",
-  "onboarding.profile.discoverable": "Feature profile and posts in discovery algorithms",
+  "onboarding.profile.discoverable": "Make my profile discoverable",
+  "onboarding.profile.discoverable_hint": "When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.",
   "onboarding.profile.display_name": "Display name",
   "onboarding.profile.display_name_hint": "Your full name or your fun name…",
-  "onboarding.profile.indexable": "Include public posts in search results",
   "onboarding.profile.lead": "You can always complete this later in the settings, where even more customization options are available.",
   "onboarding.profile.note": "Bio",
   "onboarding.profile.note_hint": "You can @mention other people or #hashtags…",
@@ -535,6 +535,7 @@
   "privacy.unlisted.short": "Unlisted",
   "privacy_policy.last_updated": "Last updated {date}",
   "privacy_policy.title": "Privacy Policy",
+  "recommended": "Recommended",
   "refresh": "Refresh",
   "regeneration_indicator.label": "Loading…",
   "regeneration_indicator.sublabel": "Your home feed is being prepared!",
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index e72a01936..555d43cc1 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -1222,10 +1222,6 @@ code {
 }
 
 .app-form {
-  & > * {
-    margin-bottom: 16px;
-  }
-
   &__avatar-input,
   &__header-input {
     display: block;
@@ -1290,4 +1286,49 @@ code {
   &__header-input {
     aspect-ratio: 580/193;
   }
+
+  &__toggle {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    color: $darker-text-color;
+    font-size: 14px;
+    line-height: 20px;
+
+    .icon {
+      flex: 0 0 auto;
+    }
+
+    .icon {
+      width: 24px;
+      height: 24px;
+    }
+
+    &__label {
+      flex: 1 1 auto;
+
+      strong {
+        color: $primary-text-color;
+        font-weight: 600;
+      }
+
+      .recommended {
+        position: absolute;
+        margin: 0 4px;
+        margin-top: -2px;
+      }
+    }
+
+    &__toggle {
+      flex: 0 0 auto;
+      display: flex;
+      align-items: center;
+    }
+
+    &__toggle > div {
+      display: flex;
+      border-inline-start: 1px solid lighten($ui-base-color, 8%);
+      padding-inline-start: 16px;
+    }
+  }
 }

From 92577376750f560064593bff10206da0080340b2 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 17 Nov 2023 12:34:49 +0100
Subject: [PATCH 60/63] Rewrite `/api/v1/accounts` tests as request specs
 (#27888)

---
 spec/requests/api/v1/accounts_show_spec.rb    |  53 -----
 .../api/v1/accounts_spec.rb}                  | 189 +++++++++++++-----
 2 files changed, 136 insertions(+), 106 deletions(-)
 delete mode 100644 spec/requests/api/v1/accounts_show_spec.rb
 rename spec/{controllers/api/v1/accounts_controller_spec.rb => requests/api/v1/accounts_spec.rb} (59%)

diff --git a/spec/requests/api/v1/accounts_show_spec.rb b/spec/requests/api/v1/accounts_show_spec.rb
deleted file mode 100644
index ee6e925aa..000000000
--- a/spec/requests/api/v1/accounts_show_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe 'GET /api/v1/accounts/{account_id}' do
-  it 'returns account entity as 200 OK' do
-    account = Fabricate(:account)
-
-    get "/api/v1/accounts/#{account.id}"
-
-    aggregate_failures do
-      expect(response).to have_http_status(200)
-      expect(body_as_json[:id]).to eq(account.id.to_s)
-    end
-  end
-
-  it 'returns 404 if account not found' do
-    get '/api/v1/accounts/1'
-
-    aggregate_failures do
-      expect(response).to have_http_status(404)
-      expect(body_as_json[:error]).to eq('Record not found')
-    end
-  end
-
-  context 'when with token' do
-    it 'returns account entity as 200 OK if token is valid' do
-      account = Fabricate(:account)
-      user = Fabricate(:user, account: account)
-      token = Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts').token
-
-      get "/api/v1/accounts/#{account.id}", headers: { Authorization: "Bearer #{token}" }
-
-      aggregate_failures do
-        expect(response).to have_http_status(200)
-        expect(body_as_json[:id]).to eq(account.id.to_s)
-      end
-    end
-
-    it 'returns 403 if scope of token is invalid' do
-      account = Fabricate(:account)
-      user = Fabricate(:user, account: account)
-      token = Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:statuses').token
-
-      get "/api/v1/accounts/#{account.id}", headers: { Authorization: "Bearer #{token}" }
-
-      aggregate_failures do
-        expect(response).to have_http_status(403)
-        expect(body_as_json[:error]).to eq('This action is outside the authorized scopes')
-      end
-    end
-  end
-end
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/requests/api/v1/accounts_spec.rb
similarity index 59%
rename from spec/controllers/api/v1/accounts_controller_spec.rb
rename to spec/requests/api/v1/accounts_spec.rb
index 9d0bb73c7..e543c4136 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/requests/api/v1/accounts_spec.rb
@@ -2,59 +2,100 @@
 
 require 'rails_helper'
 
-RSpec.describe Api::V1::AccountsController do
-  render_views
+describe '/api/v1/accounts' do
+  let(:user)    { Fabricate(:user) }
+  let(:scopes)  { '' }
+  let(:token)   { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
 
-  let(:user)   { Fabricate(:user) }
-  let(:scopes) { '' }
-  let(:token)  { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+  describe 'GET /api/v1/accounts/:id' do
+    context 'when logged out' do
+      let(:account) { Fabricate(:account) }
 
-  before do
-    allow(controller).to receive(:doorkeeper_token) { token }
+      it 'returns account entity as 200 OK', :aggregate_failures do
+        get "/api/v1/accounts/#{account.id}"
+
+        expect(response).to have_http_status(200)
+        expect(body_as_json[:id]).to eq(account.id.to_s)
+      end
+    end
+
+    context 'when the account does not exist' do
+      it 'returns http not found' do
+        get '/api/v1/accounts/1'
+
+        expect(response).to have_http_status(404)
+        expect(body_as_json[:error]).to eq('Record not found')
+      end
+    end
+
+    context 'when logged in' do
+      subject do
+        get "/api/v1/accounts/#{account.id}", headers: headers
+      end
+
+      let(:account) { Fabricate(:account) }
+      let(:scopes) { 'read:accounts' }
+
+      it 'returns account entity as 200 OK', :aggregate_failures do
+        subject
+
+        expect(response).to have_http_status(200)
+        expect(body_as_json[:id]).to eq(account.id.to_s)
+      end
+
+      it_behaves_like 'forbidden for wrong scope', 'write:statuses'
+    end
   end
 
-  describe 'POST #create' do
-    let(:app) { Fabricate(:application) }
-    let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) }
-    let(:agreement) { nil }
-
-    before do
-      post :create, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement }
+  describe 'POST /api/v1/accounts' do
+    subject do
+      post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement }
     end
 
+    let(:client_app) { Fabricate(:application) }
+    let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: client_app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) }
+    let(:agreement) { nil }
+
     context 'when given truthy agreement' do
       let(:agreement) { 'true' }
 
       it 'creates a user', :aggregate_failures do
+        subject
+
         expect(response).to have_http_status(200)
         expect(body_as_json[:access_token]).to_not be_blank
 
         user = User.find_by(email: 'hello@world.tld')
         expect(user).to_not be_nil
-        expect(user.created_by_application_id).to eq app.id
+        expect(user.created_by_application_id).to eq client_app.id
       end
     end
 
     context 'when given no agreement' do
       it 'returns http unprocessable entity' do
+        subject
+
         expect(response).to have_http_status(422)
       end
     end
   end
 
-  describe 'POST #follow' do
+  describe 'POST /api/v1/accounts/:id/follow' do
     let(:scopes) { 'write:follows' }
     let(:other_account) { Fabricate(:account, username: 'bob', locked: locked) }
 
     context 'when posting to an other account' do
-      before do
-        post :follow, params: { id: other_account.id }
+      subject do
+        post "/api/v1/accounts/#{other_account.id}/follow", headers: headers
       end
 
       context 'with unlocked account' do
         let(:locked) { false }
 
         it 'creates a following relation between user and target user', :aggregate_failures do
+          subject
+
           expect(response).to have_http_status(200)
 
           json = body_as_json
@@ -72,6 +113,8 @@ RSpec.describe Api::V1::AccountsController do
         let(:locked) { true }
 
         it 'creates a follow request relation between user and target user', :aggregate_failures do
+          subject
+
           expect(response).to have_http_status(200)
 
           json = body_as_json
@@ -94,48 +137,53 @@ RSpec.describe Api::V1::AccountsController do
       end
 
       it 'changes reblogs option' do
-        post :follow, params: { id: other_account.id, reblogs: true }
+        post "/api/v1/accounts/#{other_account.id}/follow", headers: headers, params: { reblogs: true }
 
-        json = body_as_json
-
-        expect(json[:following]).to be true
-        expect(json[:showing_reblogs]).to be true
-        expect(json[:notifying]).to be false
+        expect(body_as_json).to include({
+          following: true,
+          showing_reblogs: true,
+          notifying: false,
+        })
       end
 
       it 'changes notify option' do
-        post :follow, params: { id: other_account.id, notify: true }
+        post "/api/v1/accounts/#{other_account.id}/follow", headers: headers, params: { notify: true }
 
-        json = body_as_json
-
-        expect(json[:following]).to be true
-        expect(json[:showing_reblogs]).to be false
-        expect(json[:notifying]).to be true
+        expect(body_as_json).to include({
+          following: true,
+          showing_reblogs: false,
+          notifying: true,
+        })
       end
 
       it 'changes languages option' do
-        post :follow, params: { id: other_account.id, languages: %w(en es) }
+        post "/api/v1/accounts/#{other_account.id}/follow", headers: headers, params: { languages: %w(en es) }
 
-        json = body_as_json
-
-        expect(json[:following]).to be true
-        expect(json[:showing_reblogs]).to be false
-        expect(json[:notifying]).to be false
-        expect(json[:languages]).to match_array %w(en es)
+        expect(body_as_json).to include({
+          following: true,
+          showing_reblogs: false,
+          notifying: false,
+          languages: match_array(%w(en es)),
+        })
       end
     end
   end
 
-  describe 'POST #unfollow' do
+  describe 'POST /api/v1/accounts/:id/unfollow' do
+    subject do
+      post "/api/v1/accounts/#{other_account.id}/unfollow", headers: headers
+    end
+
     let(:scopes) { 'write:follows' }
     let(:other_account) { Fabricate(:account, username: 'bob') }
 
     before do
       user.account.follow!(other_account)
-      post :unfollow, params: { id: other_account.id }
     end
 
     it 'removes the following relation between user and target user', :aggregate_failures do
+      subject
+
       expect(response).to have_http_status(200)
       expect(user.account.following?(other_account)).to be false
     end
@@ -143,16 +191,21 @@ RSpec.describe Api::V1::AccountsController do
     it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
-  describe 'POST #remove_from_followers' do
+  describe 'POST /api/v1/accounts/:id/remove_from_followers' do
+    subject do
+      post "/api/v1/accounts/#{other_account.id}/remove_from_followers", headers: headers
+    end
+
     let(:scopes) { 'write:follows' }
     let(:other_account) { Fabricate(:account, username: 'bob') }
 
     before do
       other_account.follow!(user.account)
-      post :remove_from_followers, params: { id: other_account.id }
     end
 
     it 'removes the followed relation between user and target user', :aggregate_failures do
+      subject
+
       expect(response).to have_http_status(200)
       expect(user.account.followed_by?(other_account)).to be false
     end
@@ -160,16 +213,21 @@ RSpec.describe Api::V1::AccountsController do
     it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
-  describe 'POST #block' do
+  describe 'POST /api/v1/accounts/:id/block' do
+    subject do
+      post "/api/v1/accounts/#{other_account.id}/block", headers: headers
+    end
+
     let(:scopes) { 'write:blocks' }
     let(:other_account) { Fabricate(:account, username: 'bob') }
 
     before do
       user.account.follow!(other_account)
-      post :block, params: { id: other_account.id }
     end
 
     it 'creates a blocking relation', :aggregate_failures do
+      subject
+
       expect(response).to have_http_status(200)
       expect(user.account.following?(other_account)).to be false
       expect(user.account.blocking?(other_account)).to be true
@@ -178,16 +236,21 @@ RSpec.describe Api::V1::AccountsController do
     it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
-  describe 'POST #unblock' do
+  describe 'POST /api/v1/accounts/:id/unblock' do
+    subject do
+      post "/api/v1/accounts/#{other_account.id}/unblock", headers: headers
+    end
+
     let(:scopes) { 'write:blocks' }
     let(:other_account) { Fabricate(:account, username: 'bob') }
 
     before do
       user.account.block!(other_account)
-      post :unblock, params: { id: other_account.id }
     end
 
     it 'removes the blocking relation between user and target user', :aggregate_failures do
+      subject
+
       expect(response).to have_http_status(200)
       expect(user.account.blocking?(other_account)).to be false
     end
@@ -195,16 +258,21 @@ RSpec.describe Api::V1::AccountsController do
     it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
-  describe 'POST #mute' do
+  describe 'POST /api/v1/accounts/:id/mute' do
+    subject do
+      post "/api/v1/accounts/#{other_account.id}/mute", headers: headers
+    end
+
     let(:scopes) { 'write:mutes' }
     let(:other_account) { Fabricate(:account, username: 'bob') }
 
     before do
       user.account.follow!(other_account)
-      post :mute, params: { id: other_account.id }
     end
 
     it 'mutes notifications', :aggregate_failures do
+      subject
+
       expect(response).to have_http_status(200)
       expect(user.account.following?(other_account)).to be true
       expect(user.account.muting?(other_account)).to be true
@@ -214,16 +282,21 @@ RSpec.describe Api::V1::AccountsController do
     it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
-  describe 'POST #mute with notifications set to false' do
+  describe 'POST /api/v1/accounts/:id/mute with notifications set to false' do
+    subject do
+      post "/api/v1/accounts/#{other_account.id}/mute", headers: headers, params: { notifications: false }
+    end
+
     let(:scopes) { 'write:mutes' }
     let(:other_account) { Fabricate(:account, username: 'bob') }
 
     before do
       user.account.follow!(other_account)
-      post :mute, params: { id: other_account.id, notifications: false }
     end
 
     it 'does not mute notifications', :aggregate_failures do
+      subject
+
       expect(response).to have_http_status(200)
       expect(user.account.following?(other_account)).to be true
       expect(user.account.muting?(other_account)).to be true
@@ -233,16 +306,21 @@ RSpec.describe Api::V1::AccountsController do
     it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
-  describe 'POST #mute with nonzero duration set' do
+  describe 'POST /api/v1/accounts/:id/mute with nonzero duration set' do
+    subject do
+      post "/api/v1/accounts/#{other_account.id}/mute", headers: headers, params: { duration: 300 }
+    end
+
     let(:scopes) { 'write:mutes' }
     let(:other_account) { Fabricate(:account, username: 'bob') }
 
     before do
       user.account.follow!(other_account)
-      post :mute, params: { id: other_account.id, duration: 300 }
     end
 
     it 'mutes notifications', :aggregate_failures do
+      subject
+
       expect(response).to have_http_status(200)
       expect(user.account.following?(other_account)).to be true
       expect(user.account.muting?(other_account)).to be true
@@ -252,16 +330,21 @@ RSpec.describe Api::V1::AccountsController do
     it_behaves_like 'forbidden for wrong scope', 'read:accounts'
   end
 
-  describe 'POST #unmute' do
+  describe 'POST /api/v1/accounts/:id/unmute' do
+    subject do
+      post "/api/v1/accounts/#{other_account.id}/unmute", headers: headers
+    end
+
     let(:scopes) { 'write:mutes' }
     let(:other_account) { Fabricate(:account, username: 'bob') }
 
     before do
       user.account.mute!(other_account)
-      post :unmute, params: { id: other_account.id }
     end
 
     it 'removes the muting relation between user and target user', :aggregate_failures do
+      subject
+
       expect(response).to have_http_status(200)
       expect(user.account.muting?(other_account)).to be false
     end

From 297839c10c121505dcce421f19d41146171193c7 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 17 Nov 2023 12:36:04 +0100
Subject: [PATCH 61/63] Rewrite `/api/v1/statuses` tests as request specs
 (#27891)

---
 .../api/v1/statuses_spec.rb}                  | 132 ++++++++++--------
 1 file changed, 73 insertions(+), 59 deletions(-)
 rename spec/{controllers/api/v1/statuses_controller_spec.rb => requests/api/v1/statuses_spec.rb} (69%)

diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/requests/api/v1/statuses_spec.rb
similarity index 69%
rename from spec/controllers/api/v1/statuses_controller_spec.rb
rename to spec/requests/api/v1/statuses_spec.rb
index 30bafe19a..1b2dd2b5d 100644
--- a/spec/controllers/api/v1/statuses_controller_spec.rb
+++ b/spec/requests/api/v1/statuses_spec.rb
@@ -2,24 +2,26 @@
 
 require 'rails_helper'
 
-RSpec.describe Api::V1::StatusesController do
-  render_views
-
-  let(:user)  { Fabricate(:user) }
-  let(:app)   { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
-  let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: scopes) }
-
+describe '/api/v1/statuses' do
   context 'with an oauth token' do
-    before do
-      allow(controller).to receive(:doorkeeper_token) { token }
-    end
+    let(:user)  { Fabricate(:user) }
+    let(:client_app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
+    let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: client_app, scopes: scopes) }
+    let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
+
+    describe 'GET /api/v1/statuses/:id' do
+      subject do
+        get "/api/v1/statuses/#{status.id}", headers: headers
+      end
 
-    describe 'GET #show' do
       let(:scopes) { 'read:statuses' }
       let(:status) { Fabricate(:status, account: user.account) }
 
+      it_behaves_like 'forbidden for wrong scope', 'write write:statuses'
+
       it 'returns http success' do
-        get :show, params: { id: status.id }
+        subject
+
         expect(response).to have_http_status(200)
       end
 
@@ -31,11 +33,10 @@ RSpec.describe Api::V1::StatusesController do
         end
 
         it 'returns filter information', :aggregate_failures do
-          get :show, params: { id: status.id }
-          json = body_as_json
+          subject
 
           expect(response).to have_http_status(200)
-          expect(json[:filtered][0]).to include({
+          expect(body_as_json[:filtered][0]).to include({
             filter: a_hash_including({
               id: user.account.custom_filters.first.id.to_s,
               title: 'filter1',
@@ -55,11 +56,10 @@ RSpec.describe Api::V1::StatusesController do
         end
 
         it 'returns filter information', :aggregate_failures do
-          get :show, params: { id: status.id }
-          json = body_as_json
+          subject
 
           expect(response).to have_http_status(200)
-          expect(json[:filtered][0]).to include({
+          expect(body_as_json[:filtered][0]).to include({
             filter: a_hash_including({
               id: user.account.custom_filters.first.id.to_s,
               title: 'filter1',
@@ -78,11 +78,10 @@ RSpec.describe Api::V1::StatusesController do
         end
 
         it 'returns filter information', :aggregate_failures do
-          get :show, params: { id: status.id }
-          json = body_as_json
+          subject
 
           expect(response).to have_http_status(200)
-          expect(json[:reblog][:filtered][0]).to include({
+          expect(body_as_json[:reblog][:filtered][0]).to include({
             filter: a_hash_including({
               id: user.account.custom_filters.first.id.to_s,
               title: 'filter1',
@@ -94,7 +93,7 @@ RSpec.describe Api::V1::StatusesController do
       end
     end
 
-    describe 'GET #context' do
+    describe 'GET /api/v1/statuses/:id/context' do
       let(:scopes) { 'read:statuses' }
       let(:status) { Fabricate(:status, account: user.account) }
 
@@ -103,20 +102,26 @@ RSpec.describe Api::V1::StatusesController do
       end
 
       it 'returns http success' do
-        get :context, params: { id: status.id }
+        get "/api/v1/statuses/#{status.id}/context", headers: headers
+
         expect(response).to have_http_status(200)
       end
     end
 
-    describe 'POST #create' do
+    describe 'POST /api/v1/statuses' do
+      subject do
+        post '/api/v1/statuses', headers: headers, params: params
+      end
+
       let(:scopes) { 'write:statuses' }
+      let(:params) { { status: 'Hello world' } }
+
+      it_behaves_like 'forbidden for wrong scope', 'read read:statuses'
 
       context 'with a basic status body' do
-        before do
-          post :create, params: { status: 'Hello world' }
-        end
-
         it 'returns rate limit headers', :aggregate_failures do
+          subject
+
           expect(response).to have_http_status(200)
           expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s
           expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s
@@ -127,22 +132,22 @@ RSpec.describe Api::V1::StatusesController do
         let!(:alice) { Fabricate(:account, username: 'alice') }
         let!(:bob)   { Fabricate(:account, username: 'bob') }
 
-        before do
-          post :create, params: { status: '@alice hm, @bob is really annoying lately', allowed_mentions: [alice.id] }
-        end
+        let(:params) { { status: '@alice hm, @bob is really annoying lately', allowed_mentions: [alice.id] } }
 
         it 'returns serialized extra accounts in body', :aggregate_failures do
+          subject
+
           expect(response).to have_http_status(422)
           expect(body_as_json[:unexpected_accounts].map { |a| a.slice(:id, :acct) }).to eq [{ id: bob.id.to_s, acct: bob.acct }]
         end
       end
 
       context 'with missing parameters' do
-        before do
-          post :create, params: {}
-        end
+        let(:params) { {} }
 
         it 'returns rate limit headers', :aggregate_failures do
+          subject
+
           expect(response).to have_http_status(422)
           expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s
         end
@@ -152,10 +157,11 @@ RSpec.describe Api::V1::StatusesController do
         before do
           rate_limiter = RateLimiter.new(user.account, family: :statuses)
           300.times { rate_limiter.record! }
-          post :create, params: { status: 'Hello world' }
         end
 
         it 'returns rate limit headers', :aggregate_failures do
+          subject
+
           expect(response).to have_http_status(429)
           expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s
           expect(response.headers['X-RateLimit-Remaining']).to eq '0'
@@ -163,29 +169,37 @@ RSpec.describe Api::V1::StatusesController do
       end
     end
 
-    describe 'DELETE #destroy' do
+    describe 'DELETE /api/v1/statuses/:id' do
+      subject do
+        delete "/api/v1/statuses/#{status.id}", headers: headers
+      end
+
       let(:scopes) { 'write:statuses' }
       let(:status) { Fabricate(:status, account: user.account) }
 
-      before do
-        post :destroy, params: { id: status.id }
-      end
+      it_behaves_like 'forbidden for wrong scope', 'read read:statuses'
 
       it 'removes the status', :aggregate_failures do
+        subject
+
         expect(response).to have_http_status(200)
         expect(Status.find_by(id: status.id)).to be_nil
       end
     end
 
-    describe 'PUT #update' do
+    describe 'PUT /api/v1/statuses/:id' do
+      subject do
+        put "/api/v1/statuses/#{status.id}", headers: headers, params: { status: 'I am updated' }
+      end
+
       let(:scopes) { 'write:statuses' }
       let(:status) { Fabricate(:status, account: user.account) }
 
-      before do
-        put :update, params: { id: status.id, status: 'I am updated' }
-      end
+      it_behaves_like 'forbidden for wrong scope', 'read read:statuses'
 
       it 'updates the status', :aggregate_failures do
+        subject
+
         expect(response).to have_http_status(200)
         expect(status.reload.text).to eq 'I am updated'
       end
@@ -193,49 +207,49 @@ RSpec.describe Api::V1::StatusesController do
   end
 
   context 'without an oauth token' do
-    before do
-      allow(controller).to receive(:doorkeeper_token).and_return(nil)
-    end
-
     context 'with a private status' do
-      let(:status) { Fabricate(:status, account: user.account, visibility: :private) }
+      let(:status) { Fabricate(:status, visibility: :private) }
 
-      describe 'GET #show' do
+      describe 'GET /api/v1/statuses/:id' do
         it 'returns http unauthorized' do
-          get :show, params: { id: status.id }
+          get "/api/v1/statuses/#{status.id}"
+
           expect(response).to have_http_status(404)
         end
       end
 
-      describe 'GET #context' do
+      describe 'GET /api/v1/statuses/:id/context' do
         before do
-          Fabricate(:status, account: user.account, thread: status)
+          Fabricate(:status, thread: status)
         end
 
         it 'returns http unauthorized' do
-          get :context, params: { id: status.id }
+          get "/api/v1/statuses/#{status.id}/context"
+
           expect(response).to have_http_status(404)
         end
       end
     end
 
     context 'with a public status' do
-      let(:status) { Fabricate(:status, account: user.account, visibility: :public) }
+      let(:status) { Fabricate(:status, visibility: :public) }
 
-      describe 'GET #show' do
+      describe 'GET /api/v1/statuses/:id' do
         it 'returns http success' do
-          get :show, params: { id: status.id }
+          get "/api/v1/statuses/#{status.id}"
+
           expect(response).to have_http_status(200)
         end
       end
 
-      describe 'GET #context' do
+      describe 'GET /api/v1/statuses/:id/context' do
         before do
-          Fabricate(:status, account: user.account, thread: status)
+          Fabricate(:status, thread: status)
         end
 
         it 'returns http success' do
-          get :context, params: { id: status.id }
+          get "/api/v1/statuses/#{status.id}/context"
+
           expect(response).to have_http_status(200)
         end
       end

From e892efbc4081129fbe807d3b6d8dec024e2175ed Mon Sep 17 00:00:00 2001
From: Matt Jankowski <matt@jankowski.online>
Date: Fri, 17 Nov 2023 06:52:20 -0500
Subject: [PATCH 62/63] Configure elastic search integration with rspec tag
 (#27882)

---
 .github/workflows/test-ruby.yml     |  4 ++--
 lib/tasks/spec.rake                 | 10 ----------
 spec/rails_helper.rb                | 13 ++++++++++---
 spec/support/search_data_manager.rb |  4 ++--
 4 files changed, 14 insertions(+), 17 deletions(-)

diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml
index 101de66ac..ae25648a0 100644
--- a/.github/workflows/test-ruby.yml
+++ b/.github/workflows/test-ruby.yml
@@ -227,7 +227,7 @@ jobs:
           path: tmp/screenshots/
 
   test-search:
-    name: Testing search
+    name: Elastic Search integration testing
     runs-on: ubuntu-latest
 
     needs:
@@ -314,7 +314,7 @@ jobs:
       - name: Load database schema
         run: './bin/rails db:create db:schema:load db:seed'
 
-      - run: bundle exec rake spec:search
+      - run: bin/rspec --tag search
 
       - name: Archive logs
         uses: actions/upload-artifact@v3
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index ec4cd39bf..8f2cbeea3 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -9,13 +9,3 @@ if Rake::Task.task_defined?('spec:system')
 
   Rake::Task['spec:system'].enhance ['spec:enable_system_specs']
 end
-
-if Rake::Task.task_defined?('spec:search')
-  namespace :spec do
-    task :enable_search_specs do # rubocop:disable Rails/RakeEnvironment
-      ENV['RUN_SEARCH_SPECS'] = 'true'
-    end
-  end
-
-  Rake::Task['spec:search'].enhance ['spec:enable_search_specs']
-end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 68023b70d..7deab6c7f 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -4,7 +4,6 @@ ENV['RAILS_ENV'] ||= 'test'
 
 # This needs to be defined before Rails is initialized
 RUN_SYSTEM_SPECS = ENV.fetch('RUN_SYSTEM_SPECS', false)
-RUN_SEARCH_SPECS = ENV.fetch('RUN_SEARCH_SPECS', false)
 
 if RUN_SYSTEM_SPECS
   STREAMING_PORT = ENV.fetch('TEST_STREAMING_PORT', '4020')
@@ -55,20 +54,28 @@ RSpec.configure do |config|
     case type
     when :system
       !RUN_SYSTEM_SPECS
-    when :search
-      !RUN_SEARCH_SPECS
     end
   }
+
+  # By default, skip the elastic search integration specs
+  config.filter_run_excluding search: true
+
   config.fixture_path = Rails.root.join('spec', 'fixtures')
   config.use_transactional_fixtures = true
   config.order = 'random'
   config.infer_spec_type_from_file_location!
   config.filter_rails_from_backtrace!
 
+  # Set type to `cli` for all CLI specs
   config.define_derived_metadata(file_path: Regexp.new('spec/lib/mastodon/cli')) do |metadata|
     metadata[:type] = :cli
   end
 
+  # Set `search` metadata true for all specs in spec/search/
+  config.define_derived_metadata(file_path: Regexp.new('spec/search/*')) do |metadata|
+    metadata[:search] = true
+  end
+
   config.include Devise::Test::ControllerHelpers, type: :controller
   config.include Devise::Test::ControllerHelpers, type: :helper
   config.include Devise::Test::ControllerHelpers, type: :view
diff --git a/spec/support/search_data_manager.rb b/spec/support/search_data_manager.rb
index 176a674ad..3c7140b48 100644
--- a/spec/support/search_data_manager.rb
+++ b/spec/support/search_data_manager.rb
@@ -60,7 +60,7 @@ RSpec.configure do |config|
     end
   end
 
-  config.around :each, type: :search do |example|
+  config.around :each, :search do |example|
     search_data_manager.populate_indexes
     example.run
     search_data_manager.remove_indexes
@@ -73,6 +73,6 @@ RSpec.configure do |config|
   end
 
   def search_examples_present?
-    RUN_SEARCH_SPECS
+    RSpec.world.filtered_examples.values.flatten.any? { |example| example.metadata[:search] == true }
   end
 end

From 60f143e41f4b5d8157a54042c29af08a612249a2 Mon Sep 17 00:00:00 2001
From: Tim Campbell <timetinytim@gmail.com>
Date: Sat, 18 Nov 2023 02:14:51 -0800
Subject: [PATCH 63/63] Fixed yarn not installing node packages for streaming
 (#27967)

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

diff --git a/Dockerfile b/Dockerfile
index 2b23ea6e4..7e032073b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -38,6 +38,7 @@ RUN apt-get update && \
     corepack enable
 
 COPY Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/
+COPY streaming/package.json /opt/mastodon/streaming/
 COPY .yarn /opt/mastodon/.yarn
 
 RUN bundle install -j"$(nproc)"