From 30c191aaa03beb8bc4de4bf33db671fb98bafc96 Mon Sep 17 00:00:00 2001
From: jsgoldstein <jakegoldstein95@gmail.com>
Date: Thu, 24 Aug 2023 10:40:04 -0400
Subject: [PATCH 01/81] Add new public status index (#26344)

Co-authored-by: Eugen Rochko <eugen@zeonfederated.com>
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 app/chewy/accounts_index.rb                   |  2 +-
 app/chewy/public_statuses_index.rb            | 50 +++++++++++++
 app/chewy/statuses_index.rb                   | 22 +++---
 .../api/v1/accounts/credentials_controller.rb |  1 +
 .../settings/privacy_controller.rb            |  2 +-
 .../public_statuses_index_importer.rb         | 41 ++++++++++
 app/lib/search_query_transformer.rb           | 49 +++++++++---
 app/lib/vacuum/statuses_vacuum.rb             |  9 ++-
 app/models/account.rb                         |  1 +
 .../concerns/account_statuses_search.rb       | 44 +++++++++++
 app/models/concerns/status_search_concern.rb  | 54 +++++++++++++
 app/models/status.rb                          | 37 ++-------
 .../activitypub/actor_serializer.rb           |  8 +-
 app/services/batched_remove_status_service.rb |  5 +-
 app/services/search_service.rb                | 32 +++-----
 app/services/statuses_search_service.rb       | 75 +++++++++++++++++++
 app/views/settings/privacy/show.html.haml     |  3 +
 .../add_to_public_statuses_index_worker.rb    | 15 ++++
 ...emove_from_public_statuses_index_worker.rb | 15 ++++
 app/workers/scheduler/indexing_scheduler.rb   |  2 +-
 config/locales/simple_form.en.yml             |  2 +
 lib/mastodon/cli/search.rb                    |  1 +
 spec/chewy/public_statuses_index_spec.rb      | 31 ++++++++
 .../public_statuses_index_importer_spec.rb    | 16 ++++
 spec/lib/search_query_transformer_spec.rb     |  4 +-
 .../concerns/account_statuses_search_spec.rb  | 66 ++++++++++++++++
 ...dd_to_public_statuses_index_worker_spec.rb | 42 +++++++++++
 ..._from_public_statuses_index_worker_spec.rb | 42 +++++++++++
 28 files changed, 584 insertions(+), 87 deletions(-)
 create mode 100644 app/chewy/public_statuses_index.rb
 create mode 100644 app/lib/importer/public_statuses_index_importer.rb
 create mode 100644 app/models/concerns/account_statuses_search.rb
 create mode 100644 app/models/concerns/status_search_concern.rb
 create mode 100644 app/services/statuses_search_service.rb
 create mode 100644 app/workers/add_to_public_statuses_index_worker.rb
 create mode 100644 app/workers/remove_from_public_statuses_index_worker.rb
 create mode 100644 spec/chewy/public_statuses_index_spec.rb
 create mode 100644 spec/lib/importer/public_statuses_index_importer_spec.rb
 create mode 100644 spec/models/concerns/account_statuses_search_spec.rb
 create mode 100644 spec/workers/add_to_public_statuses_index_worker_spec.rb
 create mode 100644 spec/workers/remove_from_public_statuses_index_worker_spec.rb

diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb
index 1f8571c09d..61e3399aa8 100644
--- a/app/chewy/accounts_index.rb
+++ b/app/chewy/accounts_index.rb
@@ -62,6 +62,6 @@ class AccountsIndex < Chewy::Index
     field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
     field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
     field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
-    field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
+    field(:text, type: 'text', analyzer: 'whitespace', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
   end
 end
diff --git a/app/chewy/public_statuses_index.rb b/app/chewy/public_statuses_index.rb
new file mode 100644
index 0000000000..1fad5de3a1
--- /dev/null
+++ b/app/chewy/public_statuses_index.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+class PublicStatusesIndex < Chewy::Index
+  settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
+    filter: {
+      english_stop: {
+        type: 'stop',
+        stopwords: '_english_',
+      },
+
+      english_stemmer: {
+        type: 'stemmer',
+        language: 'english',
+      },
+
+      english_possessive_stemmer: {
+        type: 'stemmer',
+        language: 'possessive_english',
+      },
+    },
+
+    analyzer: {
+      content: {
+        tokenizer: 'uax_url_email',
+        filter: %w(
+          english_possessive_stemmer
+          lowercase
+          asciifolding
+          cjk_width
+          english_stop
+          english_stemmer
+        ),
+      },
+    },
+  }
+
+  index_scope ::Status.unscoped
+                      .kept
+                      .indexable
+                      .includes(:media_attachments, :preloadable_poll, :preview_cards)
+
+  root date_detection: false do
+    field(:id, type: 'keyword')
+    field(:account_id, type: 'long')
+    field(:text, type: 'text', analyzer: 'whitespace', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
+    field(:language, type: 'keyword')
+    field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
+    field(:created_at, type: 'date')
+  end
+end
diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb
index 9f680efa52..130f8801df 100644
--- a/app/chewy/statuses_index.rb
+++ b/app/chewy/statuses_index.rb
@@ -1,23 +1,24 @@
 # frozen_string_literal: true
 
 class StatusesIndex < Chewy::Index
-  include FormattingHelper
-
   settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
     filter: {
       english_stop: {
         type: 'stop',
         stopwords: '_english_',
       },
+
       english_stemmer: {
         type: 'stemmer',
         language: 'english',
       },
+
       english_possessive_stemmer: {
         type: 'stemmer',
         language: 'possessive_english',
       },
     },
+
     analyzer: {
       content: {
         tokenizer: 'uax_url_email',
@@ -35,7 +36,7 @@ class StatusesIndex < Chewy::Index
 
   # We do not use delete_if option here because it would call a method that we
   # expect to be called with crutches without crutches, causing n+1 queries
-  index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
+  index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll, :preview_cards)
 
   crutch :mentions do |collection|
     data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
@@ -63,13 +64,12 @@ class StatusesIndex < Chewy::Index
   end
 
   root date_detection: false do
-    field :id, type: 'long'
-    field :account_id, type: 'long'
-
-    field :text, type: 'text', value: ->(status) { status.searchable_text } do
-      field :stemmed, type: 'text', analyzer: 'content'
-    end
-
-    field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
+    field(:id, type: 'keyword')
+    field(:account_id, type: 'long')
+    field(:text, type: 'text', analyzer: 'whitespace', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
+    field(:searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) })
+    field(:language, type: 'keyword')
+    field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
+    field(:created_at, type: 'date')
   end
 end
diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb
index 7c7d70fd32..76ba758245 100644
--- a/app/controllers/api/v1/accounts/credentials_controller.rb
+++ b/app/controllers/api/v1/accounts/credentials_controller.rb
@@ -30,6 +30,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
       :bot,
       :discoverable,
       :hide_collections,
+      :indexable,
       fields_attributes: [:name, :value]
     )
   end
diff --git a/app/controllers/settings/privacy_controller.rb b/app/controllers/settings/privacy_controller.rb
index c2648eedd8..1102c89fad 100644
--- a/app/controllers/settings/privacy_controller.rb
+++ b/app/controllers/settings/privacy_controller.rb
@@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController
   private
 
   def account_params
-    params.require(:account).permit(:discoverable, :unlocked, :show_collections, settings: UserSettings.keys)
+    params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys)
   end
 
   def set_account
diff --git a/app/lib/importer/public_statuses_index_importer.rb b/app/lib/importer/public_statuses_index_importer.rb
new file mode 100644
index 0000000000..8e36e36f90
--- /dev/null
+++ b/app/lib/importer/public_statuses_index_importer.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class Importer::PublicStatusesIndexImporter < Importer::BaseImporter
+  def import!
+    indexable_statuses_scope.find_in_batches(batch_size: @batch_size) do |batch|
+      in_work_unit(batch.map(&:status_id)) do |status_ids|
+        bulk = ActiveRecord::Base.connection_pool.with_connection do
+          Chewy::Index::Import::BulkBuilder.new(index, to_index: Status.includes(:media_attachments, :preloadable_poll).where(id: status_ids)).bulk_body
+        end
+
+        indexed = 0
+        deleted = 0
+
+        bulk.map! do |entry|
+          if entry[:index]
+            indexed += 1
+          else
+            deleted += 1
+          end
+          entry
+        end
+
+        Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
+
+        [indexed, deleted]
+      end
+    end
+
+    wait!
+  end
+
+  private
+
+  def index
+    PublicStatusesIndex
+  end
+
+  def indexable_statuses_scope
+    Status.indexable.select('"statuses"."id", COALESCE("statuses"."reblog_of_id", "statuses"."id") AS status_id')
+  end
+end
diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index aef05e9d9d..dad99cbd2d 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -36,7 +36,7 @@ class SearchQueryTransformer < Parslet::Transform
     def clause_to_filter(clause)
       case clause
       when PrefixClause
-        { term: { clause.filter => clause.term } }
+        { clause.type => { clause.filter => clause.term } }
       else
         raise "Unexpected clause type: #{clause}"
       end
@@ -47,12 +47,10 @@ class SearchQueryTransformer < Parslet::Transform
     class << self
       def symbol(str)
         case str
-        when '+'
+        when '+', nil
           :must
         when '-'
           :must_not
-        when nil
-          :should
         else
           raise "Unknown operator: #{str}"
         end
@@ -81,23 +79,52 @@ class SearchQueryTransformer < Parslet::Transform
   end
 
   class PrefixClause
-    attr_reader :filter, :operator, :term
+    attr_reader :type, :filter, :operator, :term
 
     def initialize(prefix, term)
       @operator = :filter
+
       case prefix
+      when 'has', 'is'
+        @filter = :properties
+        @type = :term
+        @term = term
+      when 'language'
+        @filter = :language
+        @type = :term
+        @term = term
       when 'from'
         @filter = :account_id
-
-        username, domain = term.gsub(/\A@/, '').split('@')
-        domain           = nil if TagManager.instance.local_domain?(domain)
-        account          = Account.find_remote!(username, domain)
-
-        @term = account.id
+        @type = :term
+        @term = account_id_from_term(term)
+      when 'before'
+        @filter = :created_at
+        @type = :range
+        @term = { lt: term }
+      when 'after'
+        @filter = :created_at
+        @type = :range
+        @term = { gt: term }
+      when 'during'
+        @filter = :created_at
+        @type = :range
+        @term = { gte: term, lte: term }
       else
         raise Mastodon::SyntaxError
       end
     end
+
+    private
+
+    def account_id_from_term(term)
+      username, domain = term.gsub(/\A@/, '').split('@')
+      domain = nil if TagManager.instance.local_domain?(domain)
+      account = Account.find_remote(username, domain)
+
+      # If the account is not found, we want to return empty results, so return
+      # an ID that does not exist
+      account&.id || -1
+    end
   end
 
   rule(clause: subtree(:clause)) do
diff --git a/app/lib/vacuum/statuses_vacuum.rb b/app/lib/vacuum/statuses_vacuum.rb
index 28c087b1c2..ad1de07380 100644
--- a/app/lib/vacuum/statuses_vacuum.rb
+++ b/app/lib/vacuum/statuses_vacuum.rb
@@ -20,7 +20,10 @@ class Vacuum::StatusesVacuum
       statuses.direct_visibility
               .includes(mentions: :account)
               .find_each(&:unlink_from_conversations!)
-      remove_from_search_index(statuses.ids) if Chewy.enabled?
+      if Chewy.enabled?
+        remove_from_index(statuses.ids, 'chewy:queue:StatusesIndex')
+        remove_from_index(statuses.ids, 'chewy:queue:PublicStatusesIndex')
+      end
 
       # Foreign keys take care of most associated records for us.
       # Media attachments will be orphaned.
@@ -38,7 +41,7 @@ class Vacuum::StatusesVacuum
     Mastodon::Snowflake.id_at(@retention_period.ago, with_random: false)
   end
 
-  def remove_from_search_index(status_ids)
-    with_redis { |redis| redis.sadd('chewy:queue:StatusesIndex', status_ids) }
+  def remove_from_index(status_ids, index)
+    with_redis { |redis| redis.sadd(index, status_ids) }
   end
 end
diff --git a/app/models/account.rb b/app/models/account.rb
index b1cb9eb5db..244f3da83d 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -82,6 +82,7 @@ class Account < ApplicationRecord
   include DomainMaterializable
   include AccountMerging
   include AccountSearch
+  include AccountStatusesSearch
 
   enum protocol: { ostatus: 0, activitypub: 1 }
   enum suspension_origin: { local: 0, remote: 1 }, _prefix: true
diff --git a/app/models/concerns/account_statuses_search.rb b/app/models/concerns/account_statuses_search.rb
new file mode 100644
index 0000000000..563a871051
--- /dev/null
+++ b/app/models/concerns/account_statuses_search.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module AccountStatusesSearch
+  extend ActiveSupport::Concern
+
+  included do
+    after_update_commit :enqueue_update_public_statuses_index, if: :saved_change_to_indexable?
+    after_destroy_commit :enqueue_remove_from_public_statuses_index, if: :indexable?
+  end
+
+  def enqueue_update_public_statuses_index
+    if indexable?
+      enqueue_add_to_public_statuses_index
+    else
+      enqueue_remove_from_public_statuses_index
+    end
+  end
+
+  def enqueue_add_to_public_statuses_index
+    return unless Chewy.enabled?
+
+    AddToPublicStatusesIndexWorker.perform_async(id)
+  end
+
+  def enqueue_remove_from_public_statuses_index
+    return unless Chewy.enabled?
+
+    RemoveFromPublicStatusesIndexWorker.perform_async(id)
+  end
+
+  def add_to_public_statuses_index!
+    return unless Chewy.enabled?
+
+    statuses.indexable.find_in_batches do |batch|
+      PublicStatusesIndex.import(query: batch)
+    end
+  end
+
+  def remove_from_public_statuses_index!
+    return unless Chewy.enabled?
+
+    PublicStatusesIndex.filter(term: { account_id: id }).delete_all
+  end
+end
diff --git a/app/models/concerns/status_search_concern.rb b/app/models/concerns/status_search_concern.rb
new file mode 100644
index 0000000000..21048b5682
--- /dev/null
+++ b/app/models/concerns/status_search_concern.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module StatusSearchConcern
+  extend ActiveSupport::Concern
+
+  included do
+    scope :indexable, -> { without_reblogs.where(visibility: :public).joins(:account).where(account: { indexable: true }) }
+  end
+
+  def searchable_by(preloaded = nil)
+    ids = []
+
+    ids << account_id if local?
+
+    if preloaded.nil?
+      ids += mentions.joins(:account).merge(Account.local).active.pluck(:account_id)
+      ids += favourites.joins(:account).merge(Account.local).pluck(:account_id)
+      ids += reblogs.joins(:account).merge(Account.local).pluck(:account_id)
+      ids += bookmarks.joins(:account).merge(Account.local).pluck(:account_id)
+      ids += poll.votes.joins(:account).merge(Account.local).pluck(:account_id) if poll.present?
+    else
+      ids += preloaded.mentions[id] || []
+      ids += preloaded.favourites[id] || []
+      ids += preloaded.reblogs[id] || []
+      ids += preloaded.bookmarks[id] || []
+      ids += preloaded.votes[id] || []
+    end
+
+    ids.uniq
+  end
+
+  def searchable_text
+    [
+      spoiler_text,
+      FormattingHelper.extract_status_plain_text(self),
+      preloadable_poll&.options&.join("\n\n"),
+      ordered_media_attachments.map(&:description).join("\n\n"),
+    ].compact.join("\n\n")
+  end
+
+  def searchable_properties
+    [].tap do |properties|
+      properties << 'image' if ordered_media_attachments.any?(&:image?)
+      properties << 'video' if ordered_media_attachments.any?(&:video?)
+      properties << 'audio' if ordered_media_attachments.any?(&:audio?)
+      properties << 'media' if with_media?
+      properties << 'poll' if with_poll?
+      properties << 'link' if with_preview_card?
+      properties << 'embed' if preview_cards.any?(&:video?)
+      properties << 'sensitive' if sensitive?
+      properties << 'reply' if reply?
+    end
+  end
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index 86fd8334a2..760b8ec33e 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -37,6 +37,7 @@ class Status < ApplicationRecord
   include StatusSnapshotConcern
   include RateLimitable
   include StatusSafeReblogInsert
+  include StatusSearchConcern
 
   rate_limit by: :account, family: :statuses
 
@@ -47,6 +48,7 @@ class Status < ApplicationRecord
   attr_accessor :override_timestamps
 
   update_index('statuses', :proper)
+  update_index('public_statuses', :proper)
 
   enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4 }, _suffix: :visibility
 
@@ -165,37 +167,6 @@ class Status < ApplicationRecord
     "v3:#{super}"
   end
 
-  def searchable_by(preloaded = nil)
-    ids = []
-
-    ids << account_id if local?
-
-    if preloaded.nil?
-      ids += mentions.joins(:account).merge(Account.local).active.pluck(:account_id)
-      ids += favourites.joins(:account).merge(Account.local).pluck(:account_id)
-      ids += reblogs.joins(:account).merge(Account.local).pluck(:account_id)
-      ids += bookmarks.joins(:account).merge(Account.local).pluck(:account_id)
-      ids += poll.votes.joins(:account).merge(Account.local).pluck(:account_id) if poll.present?
-    else
-      ids += preloaded.mentions[id] || []
-      ids += preloaded.favourites[id] || []
-      ids += preloaded.reblogs[id] || []
-      ids += preloaded.bookmarks[id] || []
-      ids += preloaded.votes[id] || []
-    end
-
-    ids.uniq
-  end
-
-  def searchable_text
-    [
-      spoiler_text,
-      FormattingHelper.extract_status_plain_text(self),
-      preloadable_poll ? preloadable_poll.options.join("\n\n") : nil,
-      ordered_media_attachments.map(&:description).join("\n\n"),
-    ].compact.join("\n\n")
-  end
-
   def to_log_human_identifier
     account.acct
   end
@@ -270,6 +241,10 @@ class Status < ApplicationRecord
     preview_cards.any?
   end
 
+  def with_poll?
+    preloadable_poll.present?
+  end
+
   def non_sensitive_with_media?
     !sensitive? && with_media?
   end
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 4998d00399..31f39954fb 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -8,13 +8,13 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
 
   context_extensions :manually_approves_followers, :featured, :also_known_as,
                      :moved_to, :property_value, :discoverable, :olm, :suspended,
-                     :memorial
+                     :memorial, :indexable
 
   attributes :id, :type, :following, :followers,
              :inbox, :outbox, :featured, :featured_tags,
              :preferred_username, :name, :summary,
              :url, :manually_approves_followers,
-             :discoverable, :published, :memorial
+             :discoverable, :indexable, :published, :memorial
 
   has_one :public_key, serializer: ActivityPub::PublicKeySerializer
 
@@ -99,6 +99,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
     object.suspended? ? false : (object.discoverable || false)
   end
 
+  def indexable
+    object.suspended? ? false : (object.indexable || false)
+  end
+
   def name
     object.suspended? ? object.username : (object.display_name.presence || object.username)
   end
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index f5cb339cdf..c54cc1d350 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -35,7 +35,10 @@ class BatchedRemoveStatusService < BaseService
 
     # Since we skipped all callbacks, we also need to manually
     # deindex the statuses
-    Chewy.strategy.current.update(StatusesIndex, statuses_and_reblogs) if Chewy.enabled?
+    if Chewy.enabled?
+      Chewy.strategy.current.update(StatusesIndex, statuses_and_reblogs)
+      Chewy.strategy.current.update(PublicStatusesIndex, statuses_and_reblogs)
+    end
 
     return if options[:skip_side_effects]
 
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 30937471bd..4e1e7ea26e 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -39,25 +39,15 @@ class SearchService < BaseService
   end
 
   def perform_statuses_search!
-    definition = parsed_query.apply(StatusesIndex.filter(term: { searchable_by: @account.id }))
-
-    definition = definition.filter(term: { account_id: @options[:account_id] }) if @options[:account_id].present?
-
-    if @options[:min_id].present? || @options[:max_id].present?
-      range      = {}
-      range[:gt] = @options[:min_id].to_i if @options[:min_id].present?
-      range[:lt] = @options[:max_id].to_i if @options[:max_id].present?
-      definition = definition.filter(range: { id: range })
-    end
-
-    results             = definition.limit(@limit).offset(@offset).objects.compact
-    account_ids         = results.map(&:account_id)
-    account_domains     = results.map(&:account_domain)
-    preloaded_relations = @account.relations_map(account_ids, account_domains)
-
-    results.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? }
-  rescue Faraday::ConnectionFailed, Parslet::ParseFailed
-    []
+    StatusesSearchService.new.call(
+      @query,
+      @account,
+      limit: @limit,
+      offset: @offset,
+      account_id: @options[:account_id],
+      min_id: @options[:min_id],
+      max_id: @options[:max_id]
+    )
   end
 
   def perform_hashtags_search!
@@ -114,8 +104,4 @@ class SearchService < BaseService
   def statuses_search?
     @options[:type].blank? || @options[:type] == 'statuses'
   end
-
-  def parsed_query
-    SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query))
-  end
 end
diff --git a/app/services/statuses_search_service.rb b/app/services/statuses_search_service.rb
new file mode 100644
index 0000000000..21d6b71b7d
--- /dev/null
+++ b/app/services/statuses_search_service.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+class StatusesSearchService < BaseService
+  def call(query, account = nil, options = {})
+    @query   = query&.strip
+    @account = account
+    @options = options
+    @limit   = options[:limit].to_i
+    @offset  = options[:offset].to_i
+
+    status_search_results
+  end
+
+  private
+
+  def status_search_results
+    definition = parsed_query.apply(
+      StatusesIndex.filter(
+        bool: {
+          should: [
+            publicly_searchable,
+            non_publicly_searchable,
+          ],
+
+          minimum_should_match: 1,
+        }
+      )
+    )
+
+    # This is the best way to submit identical queries to multi-indexes though chewy
+    definition.instance_variable_get(:@parameters)[:indices].value[:indices] << PublicStatusesIndex
+
+    results             = definition.collapse(field: :id).order(_id: { order: :desc }).limit(@limit).offset(@offset).objects.compact
+    account_ids         = results.map(&:account_id)
+    account_domains     = results.map(&:account_domain)
+    preloaded_relations = @account.relations_map(account_ids, account_domains)
+
+    results.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? }
+  rescue Faraday::ConnectionFailed, Parslet::ParseFailed
+    []
+  end
+
+  def publicly_searchable
+    {
+      bool: {
+        must_not: {
+          exists: {
+            field: 'searchable_by',
+          },
+        },
+      },
+    }
+  end
+
+  def non_publicly_searchable
+    {
+      bool: {
+        must: [
+          {
+            exists: {
+              field: 'searchable_by',
+            },
+          },
+          {
+            term: { searchable_by: @account.id },
+          },
+        ],
+      },
+    }
+  end
+
+  def parsed_query
+    SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query))
+  end
+end
diff --git a/app/views/settings/privacy/show.html.haml b/app/views/settings/privacy/show.html.haml
index ce31e60f06..3c14382587 100644
--- a/app/views/settings/privacy/show.html.haml
+++ b/app/views/settings/privacy/show.html.haml
@@ -24,6 +24,9 @@
 
   %p.lead= t('privacy.search_hint_html')
 
+  .fields-group
+    = f.input :indexable, as: :boolean, wrapper: :with_label
+
   = f.simple_fields_for :settings, current_user.settings do |ff|
     .fields-group
       = ff.input :indexable, wrapper: :with_label
diff --git a/app/workers/add_to_public_statuses_index_worker.rb b/app/workers/add_to_public_statuses_index_worker.rb
new file mode 100644
index 0000000000..409e5e7086
--- /dev/null
+++ b/app/workers/add_to_public_statuses_index_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddToPublicStatusesIndexWorker
+  include Sidekiq::Worker
+
+  def perform(account_id)
+    account = Account.find(account_id)
+
+    return unless account.indexable?
+
+    account.add_to_public_statuses_index!
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+end
diff --git a/app/workers/remove_from_public_statuses_index_worker.rb b/app/workers/remove_from_public_statuses_index_worker.rb
new file mode 100644
index 0000000000..e108a5c209
--- /dev/null
+++ b/app/workers/remove_from_public_statuses_index_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RemoveFromPublicStatusesIndexWorker
+  include Sidekiq::Worker
+
+  def perform(account_id)
+    account = Account.find(account_id)
+
+    return if account.indexable?
+
+    account.remove_from_public_statuses_index!
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+end
diff --git a/app/workers/scheduler/indexing_scheduler.rb b/app/workers/scheduler/indexing_scheduler.rb
index 2868a3b715..6c770d5a8f 100644
--- a/app/workers/scheduler/indexing_scheduler.rb
+++ b/app/workers/scheduler/indexing_scheduler.rb
@@ -23,6 +23,6 @@ class Scheduler::IndexingScheduler
   end
 
   def indexes
-    [AccountsIndex, TagsIndex, StatusesIndex]
+    [AccountsIndex, TagsIndex, PublicStatusesIndex, StatusesIndex]
   end
 end
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 443b7617ff..efda7b778b 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -6,6 +6,7 @@ en:
         discoverable: Your public posts and profile may be featured or recommended in various areas of Mastodon and your profile may be suggested to other users.
         display_name: Your full name or your fun name.
         fields: Your homepage, pronouns, age, anything you want.
+        indexable: Your public posts may appear in search results on Mastodon. People who have interacted with your posts may be able to search them regardless.
         note: 'You can @mention other people or #hashtags.'
         show_collections: People will be able to browse through your follows and followers. People that you follow will see that you follow them regardless.
         unlocked: People will be able to follow you without requesting approval. Uncheck if you want to review follow requests and chose whether to accept or reject new followers.
@@ -143,6 +144,7 @@ en:
         fields:
           name: Label
           value: Content
+        indexable: Include public posts in search results
         show_collections: Show follows and followers on profile
         unlocked: Automatically accept new followers
       account_alias:
diff --git a/lib/mastodon/cli/search.rb b/lib/mastodon/cli/search.rb
index 41862b5b6b..481e01d8e7 100644
--- a/lib/mastodon/cli/search.rb
+++ b/lib/mastodon/cli/search.rb
@@ -10,6 +10,7 @@ module Mastodon::CLI
       InstancesIndex,
       AccountsIndex,
       TagsIndex,
+      PublicStatusesIndex,
       StatusesIndex,
     ].freeze
 
diff --git a/spec/chewy/public_statuses_index_spec.rb b/spec/chewy/public_statuses_index_spec.rb
new file mode 100644
index 0000000000..2f93d0ff02
--- /dev/null
+++ b/spec/chewy/public_statuses_index_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe PublicStatusesIndex do
+  describe 'Searching the index' do
+    before do
+      mock_elasticsearch_response(described_class, raw_response)
+    end
+
+    it 'returns results from a query' do
+      results = described_class.query(match: { name: 'status' })
+
+      expect(results).to eq []
+    end
+  end
+
+  def raw_response
+    {
+      took: 3,
+      hits: {
+        hits: [
+          {
+            _id: '0',
+            _score: 1.6375021,
+          },
+        ],
+      },
+    }
+  end
+end
diff --git a/spec/lib/importer/public_statuses_index_importer_spec.rb b/spec/lib/importer/public_statuses_index_importer_spec.rb
new file mode 100644
index 0000000000..bc7c038a97
--- /dev/null
+++ b/spec/lib/importer/public_statuses_index_importer_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Importer::PublicStatusesIndexImporter do
+  describe 'import!' do
+    let(:pool) { Concurrent::FixedThreadPool.new(5) }
+    let(:importer) { described_class.new(batch_size: 123, executor: pool) }
+
+    before { Fabricate(:status, account: Fabricate(:account, indexable: true)) }
+
+    it 'indexes relevant statuses' do
+      expect { importer.import! }.to update_index(PublicStatusesIndex)
+    end
+  end
+end
diff --git a/spec/lib/search_query_transformer_spec.rb b/spec/lib/search_query_transformer_spec.rb
index 1095334695..953f9acb2f 100644
--- a/spec/lib/search_query_transformer_spec.rb
+++ b/spec/lib/search_query_transformer_spec.rb
@@ -9,8 +9,8 @@ describe SearchQueryTransformer do
     it 'sets attributes' do
       transformer = described_class.new.apply(parser)
 
-      expect(transformer.should_clauses.first).to be_a(SearchQueryTransformer::TermClause)
-      expect(transformer.must_clauses.first).to be_nil
+      expect(transformer.should_clauses.first).to be_nil
+      expect(transformer.must_clauses.first).to be_a(SearchQueryTransformer::TermClause)
       expect(transformer.must_not_clauses.first).to be_nil
       expect(transformer.filter_clauses.first).to be_nil
     end
diff --git a/spec/models/concerns/account_statuses_search_spec.rb b/spec/models/concerns/account_statuses_search_spec.rb
new file mode 100644
index 0000000000..46362936f4
--- /dev/null
+++ b/spec/models/concerns/account_statuses_search_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AccountStatusesSearch do
+  let(:account) { Fabricate(:account, indexable: indexable) }
+
+  before do
+    allow(Chewy).to receive(:enabled?).and_return(true)
+  end
+
+  describe '#enqueue_update_public_statuses_index' do
+    before do
+      allow(account).to receive(:enqueue_add_to_public_statuses_index)
+      allow(account).to receive(:enqueue_remove_from_public_statuses_index)
+    end
+
+    context 'when account is indexable' do
+      let(:indexable) { true }
+
+      it 'enqueues add_to_public_statuses_index and not to remove_from_public_statuses_index' do
+        account.enqueue_update_public_statuses_index
+        expect(account).to have_received(:enqueue_add_to_public_statuses_index)
+        expect(account).to_not have_received(:enqueue_remove_from_public_statuses_index)
+      end
+    end
+
+    context 'when account is not indexable' do
+      let(:indexable) { false }
+
+      it 'enqueues remove_from_public_statuses_index and not to add_to_public_statuses_index' do
+        account.enqueue_update_public_statuses_index
+        expect(account).to have_received(:enqueue_remove_from_public_statuses_index)
+        expect(account).to_not have_received(:enqueue_add_to_public_statuses_index)
+      end
+    end
+  end
+
+  describe '#enqueue_add_to_public_statuses_index' do
+    let(:indexable) { true }
+    let(:worker) { AddToPublicStatusesIndexWorker }
+
+    before do
+      allow(worker).to receive(:perform_async)
+    end
+
+    it 'enqueues AddToPublicStatusesIndexWorker' do
+      account.enqueue_add_to_public_statuses_index
+      expect(worker).to have_received(:perform_async).with(account.id)
+    end
+  end
+
+  describe '#enqueue_remove_from_public_statuses_index' do
+    let(:indexable) { false }
+    let(:worker) { RemoveFromPublicStatusesIndexWorker }
+
+    before do
+      allow(worker).to receive(:perform_async)
+    end
+
+    it 'enqueues RemoveFromPublicStatusesIndexWorker' do
+      account.enqueue_remove_from_public_statuses_index
+      expect(worker).to have_received(:perform_async).with(account.id)
+    end
+  end
+end
diff --git a/spec/workers/add_to_public_statuses_index_worker_spec.rb b/spec/workers/add_to_public_statuses_index_worker_spec.rb
new file mode 100644
index 0000000000..fa15072241
--- /dev/null
+++ b/spec/workers/add_to_public_statuses_index_worker_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe AddToPublicStatusesIndexWorker do
+  describe '#perform' do
+    let(:account) { Fabricate(:account, indexable: indexable) }
+    let(:account_id) { account.id }
+
+    before do
+      allow(Account).to receive(:find).with(account_id).and_return(account) unless account.nil?
+      allow(account).to receive(:add_to_public_statuses_index!) unless account.nil?
+    end
+
+    context 'when account is indexable' do
+      let(:indexable) { true }
+
+      it 'adds the account to the public statuses index' do
+        subject.perform(account_id)
+        expect(account).to have_received(:add_to_public_statuses_index!)
+      end
+    end
+
+    context 'when account is not indexable' do
+      let(:indexable) { false }
+
+      it 'does not add the account to public statuses index' do
+        subject.perform(account_id)
+        expect(account).to_not have_received(:add_to_public_statuses_index!)
+      end
+    end
+
+    context 'when account does not exist' do
+      let(:account) { nil }
+      let(:account_id) { 999 }
+
+      it 'does not raise an error' do
+        expect { subject.perform(account_id) }.to_not raise_error
+      end
+    end
+  end
+end
diff --git a/spec/workers/remove_from_public_statuses_index_worker_spec.rb b/spec/workers/remove_from_public_statuses_index_worker_spec.rb
new file mode 100644
index 0000000000..43ff211eaa
--- /dev/null
+++ b/spec/workers/remove_from_public_statuses_index_worker_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe RemoveFromPublicStatusesIndexWorker do
+  describe '#perform' do
+    let(:account) { Fabricate(:account, indexable: indexable) }
+    let(:account_id) { account.id }
+
+    before do
+      allow(Account).to receive(:find).with(account_id).and_return(account) unless account.nil?
+      allow(account).to receive(:remove_from_public_statuses_index!) unless account.nil?
+    end
+
+    context 'when account is not indexable' do
+      let(:indexable) { false }
+
+      it 'removes the account from public statuses index' do
+        subject.perform(account_id)
+        expect(account).to have_received(:remove_from_public_statuses_index!)
+      end
+    end
+
+    context 'when account is indexable' do
+      let(:indexable) { true }
+
+      it 'does not remove the account from public statuses index' do
+        subject.perform(account_id)
+        expect(account).to_not have_received(:remove_from_public_statuses_index!)
+      end
+    end
+
+    context 'when account does not exist' do
+      let(:account) { nil }
+      let(:account_id) { 999 }
+
+      it 'does not raise an error' do
+        expect { subject.perform(account_id) }.to_not raise_error
+      end
+    end
+  end
+end

From 389b7d23dbd32fa0ab563083d76b8aaf0d6137da Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 24 Aug 2023 21:07:39 +0200
Subject: [PATCH 02/81] Fix changelog wording and missing items (#26638)

---
 CHANGELOG.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 107dfaca3f..fe66adc08a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -101,7 +101,7 @@ The following changelog entries focus on changes visible to users, administrator
 - **Change translation feature to cover Content Warnings, poll options and media descriptions** ([c960657](https://github.com/mastodon/mastodon/pull/24175), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25251), [c960657](https://github.com/mastodon/mastodon/pull/26168), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26452))
 - **Change account search to match by text when opted-in** ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25599), [Gargron](https://github.com/mastodon/mastodon/pull/26378))
 - **Change import feature to be clearer, less error-prone and more reliable** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21054), [mgmn](https://github.com/mastodon/mastodon/pull/24874))
-- **Change local and federated timelines to be in a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247))
+- **Change local and federated timelines to be tabs of a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247))
 - **Change user archive export to be faster and more reliable, and export `.zip` archives instead of `.tar.gz` ones** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23360), [TheEssem](https://github.com/mastodon/mastodon/pull/25034))
 - **Change `mastodon-streaming` systemd unit files to be templated** ([e-nomem](https://github.com/mastodon/mastodon/pull/24751))
 - **Change `statsd` integration to disable sidekiq metrics by default** ([mjankowski](https://github.com/mastodon/mastodon/pull/25265), [mjankowski](https://github.com/mastodon/mastodon/pull/25336), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26310))
@@ -189,6 +189,7 @@ The following changelog entries focus on changes visible to users, administrator
 - **Fix log-in flow when involving both OAuth and external authentication** ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24073))
 - **Fix broken links in account gallery** ([c960657](https://github.com/mastodon/mastodon/pull/24218))
 - **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
+- **Fix migration handler not updating lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24808))
 - Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
 - Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
 - Fix adding column with default value taking longer on Postgres >= 11 ([Gargron](https://github.com/mastodon/mastodon/pull/26375))

From c66f7565226e6ad37d23fd4b89f99f0f277d83a4 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 25 Aug 2023 10:08:44 +0200
Subject: [PATCH 03/81] Update babel monorepo to v7.22.11 (#26640)

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

diff --git a/yarn.lock b/yarn.lock
index 216e22f681..6894f4388c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -50,24 +50,24 @@
   integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==
 
 "@babel/core@^7.10.4", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.22.1":
-  version "7.22.10"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.10.tgz#aad442c7bcd1582252cb4576747ace35bc122f35"
-  integrity sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.11.tgz#8033acaa2aa24c3f814edaaa057f3ce0ba559c24"
+  integrity sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==
   dependencies:
     "@ampproject/remapping" "^2.2.0"
     "@babel/code-frame" "^7.22.10"
     "@babel/generator" "^7.22.10"
     "@babel/helper-compilation-targets" "^7.22.10"
     "@babel/helper-module-transforms" "^7.22.9"
-    "@babel/helpers" "^7.22.10"
-    "@babel/parser" "^7.22.10"
+    "@babel/helpers" "^7.22.11"
+    "@babel/parser" "^7.22.11"
     "@babel/template" "^7.22.5"
-    "@babel/traverse" "^7.22.10"
-    "@babel/types" "^7.22.10"
+    "@babel/traverse" "^7.22.11"
+    "@babel/types" "^7.22.11"
     convert-source-map "^1.7.0"
     debug "^4.1.0"
     gensync "^1.0.0-beta.2"
-    json5 "^2.2.2"
+    json5 "^2.2.3"
     semver "^6.3.1"
 
 "@babel/generator@^7.22.10", "@babel/generator@^7.22.5", "@babel/generator@^7.7.2":
@@ -113,6 +113,21 @@
     lru-cache "^5.1.1"
     semver "^6.3.1"
 
+"@babel/helper-create-class-features-plugin@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz#4078686740459eeb4af3494a273ac09148dfb213"
+  integrity sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.22.5"
+    "@babel/helper-environment-visitor" "^7.22.5"
+    "@babel/helper-function-name" "^7.22.5"
+    "@babel/helper-member-expression-to-functions" "^7.22.5"
+    "@babel/helper-optimise-call-expression" "^7.22.5"
+    "@babel/helper-replace-supers" "^7.22.9"
+    "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
+    "@babel/helper-split-export-declaration" "^7.22.6"
+    semver "^6.3.1"
+
 "@babel/helper-create-class-features-plugin@^7.22.5":
   version "7.22.10"
   resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz#dd2612d59eac45588021ac3d6fa976d08f4e95a3"
@@ -268,14 +283,14 @@
     "@babel/template" "^7.22.5"
     "@babel/types" "^7.22.10"
 
-"@babel/helpers@^7.22.10":
-  version "7.22.10"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a"
-  integrity sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==
+"@babel/helpers@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.11.tgz#b02f5d5f2d7abc21ab59eeed80de410ba70b056a"
+  integrity sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==
   dependencies:
     "@babel/template" "^7.22.5"
-    "@babel/traverse" "^7.22.10"
-    "@babel/types" "^7.22.10"
+    "@babel/traverse" "^7.22.11"
+    "@babel/types" "^7.22.11"
 
 "@babel/highlight@^7.22.10", "@babel/highlight@^7.22.5":
   version "7.22.10"
@@ -286,11 +301,16 @@
     chalk "^2.4.2"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.10", "@babel/parser@^7.22.5":
+"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7":
   version "7.22.10"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55"
   integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==
 
+"@babel/parser@^7.22.11", "@babel/parser@^7.22.5":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.11.tgz#becf8ee33aad2a35ed5607f521fe6e72a615f905"
+  integrity sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==
+
 "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5":
   version "7.22.5"
   resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e"
@@ -640,6 +660,15 @@
     "@babel/helper-module-transforms" "^7.22.5"
     "@babel/helper-plugin-utils" "^7.22.5"
 
+"@babel/plugin-transform-modules-commonjs@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.11.tgz#d7991d3abad199c03b68ee66a64f216c47ffdfae"
+  integrity sha512-o2+bg7GDS60cJMgz9jWqRUsWkMzLCxp+jFDeDUT5sjRlAxcJWZ2ylNdI7QQ2+CH5hWu7OnN+Cv3htt7AkSf96g==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.22.9"
+    "@babel/helper-plugin-utils" "^7.22.5"
+    "@babel/helper-simple-access" "^7.22.5"
+
 "@babel/plugin-transform-modules-commonjs@^7.22.5":
   version "7.22.5"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz#7d9875908d19b8c0536085af7b053fd5bd651bfa"
@@ -683,9 +712,9 @@
     "@babel/helper-plugin-utils" "^7.22.5"
 
 "@babel/plugin-transform-nullish-coalescing-operator@^7.22.3", "@babel/plugin-transform-nullish-coalescing-operator@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz#f8872c65776e0b552e0849d7596cddd416c3e381"
-  integrity sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc"
+  integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
@@ -877,13 +906,13 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
 
-"@babel/plugin-transform-typescript@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.5.tgz#5c0f7adfc1b5f38c4dbc8f79b1f0f8074134bd7d"
-  integrity sha512-SMubA9S7Cb5sGSFFUlqxyClTA9zWJ8qGQrppNUm05LtFuN1ELRFNndkix4zUJrC9F+YivWwa1dHMSyo0e0N9dA==
+"@babel/plugin-transform-typescript@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.11.tgz#9f27fb5e51585729374bb767ab6a6d9005a23329"
+  integrity sha512-0E4/L+7gfvHub7wsbTv03oRtD69X31LByy44fGmFzbZScpupFByMcgCJ0VbBTkzyjSJKuRoGN8tcijOWKTmqOA==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.22.5"
-    "@babel/helper-create-class-features-plugin" "^7.22.5"
+    "@babel/helper-create-class-features-plugin" "^7.22.11"
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-typescript" "^7.22.5"
 
@@ -1026,15 +1055,15 @@
     "@babel/plugin-transform-react-pure-annotations" "^7.22.5"
 
 "@babel/preset-typescript@^7.21.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.22.5.tgz#16367d8b01d640e9a507577ed4ee54e0101e51c8"
-  integrity sha512-YbPaal9LxztSGhmndR46FmAbkJ/1fAsw293tSU+I5E5h+cnJ3d4GTwyUgGYmOXJYdGA+uNePle4qbaRzj2NISQ==
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.22.11.tgz#f218cd0345524ac888aa3dc32f029de5b064b575"
+  integrity sha512-tWY5wyCZYBGY7IlalfKI1rLiGlIfnwsRHZqlky0HVv8qviwQ1Uo/05M6+s+TcTCVa6Bmoo2uJW5TMFX6Wa4qVg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/helper-validator-option" "^7.22.5"
     "@babel/plugin-syntax-jsx" "^7.22.5"
-    "@babel/plugin-transform-modules-commonjs" "^7.22.5"
-    "@babel/plugin-transform-typescript" "^7.22.5"
+    "@babel/plugin-transform-modules-commonjs" "^7.22.11"
+    "@babel/plugin-transform-typescript" "^7.22.11"
 
 "@babel/regjsgen@^0.8.0":
   version "0.8.0"
@@ -1049,9 +1078,9 @@
     regenerator-runtime "^0.12.0"
 
 "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
-  version "7.22.10"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682"
-  integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.11.tgz#7a9ba3bbe406ad6f9e8dd4da2ece453eb23a77a4"
+  integrity sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA==
   dependencies:
     regenerator-runtime "^0.14.0"
 
@@ -1080,10 +1109,10 @@
     debug "^4.1.0"
     globals "^11.1.0"
 
-"@babel/traverse@^7.22.10":
-  version "7.22.10"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa"
-  integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==
+"@babel/traverse@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.11.tgz#71ebb3af7a05ff97280b83f05f8865ac94b2027c"
+  integrity sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==
   dependencies:
     "@babel/code-frame" "^7.22.10"
     "@babel/generator" "^7.22.10"
@@ -1091,12 +1120,12 @@
     "@babel/helper-function-name" "^7.22.5"
     "@babel/helper-hoist-variables" "^7.22.5"
     "@babel/helper-split-export-declaration" "^7.22.6"
-    "@babel/parser" "^7.22.10"
-    "@babel/types" "^7.22.10"
+    "@babel/parser" "^7.22.11"
+    "@babel/types" "^7.22.11"
     debug "^4.1.0"
     globals "^11.1.0"
 
-"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.10", "@babel/types@^7.22.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
+"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
   version "7.22.10"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03"
   integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==
@@ -1114,6 +1143,15 @@
     "@babel/helper-validator-identifier" "^7.22.5"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.22.10", "@babel/types@^7.22.11", "@babel/types@^7.22.5":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.11.tgz#0e65a6a1d4d9cbaa892b2213f6159485fe632ea2"
+  integrity sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==
+  dependencies:
+    "@babel/helper-string-parser" "^7.22.5"
+    "@babel/helper-validator-identifier" "^7.22.5"
+    to-fast-properties "^2.0.0"
+
 "@bcoe/v8-coverage@^0.2.3":
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -3901,7 +3939,12 @@ caniuse-lite@^1.0.30001502:
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz#418aefeed9d024cd3129bfae0ccc782d4cb8f12b"
   integrity sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==
 
-caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001520:
+caniuse-lite@^1.0.30001517:
+  version "1.0.30001522"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz#44b87a406c901269adcdb834713e23582dd71856"
+  integrity sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==
+
+caniuse-lite@^1.0.30001520:
   version "1.0.30001520"
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001520.tgz#62e2b7a1c7b35269594cf296a80bdf8cb9565006"
   integrity sha512-tahF5O9EiiTzwTUqAeFjIZbn4Dnqxzz7ktrgGlMYNLH43Ul26IgTMH/zvL3DG0lZxBYnlT04axvInszUsZULdA==
@@ -5050,9 +5093,9 @@ electron-to-chromium@^1.4.428:
   integrity sha512-/g3UyNDmDd6ebeWapmAoiyy+Sy2HyJ+/X8KyvNeHfKRFfHaA2W8oF5fxD5F3tjBDcjpwo0iek6YNgxNXDBoEtA==
 
 electron-to-chromium@^1.4.477:
-  version "1.4.490"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz#d99286f6e915667fa18ea4554def1aa60eb4d5f1"
-  integrity sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A==
+  version "1.4.500"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.500.tgz#7dd05fdfbe02ed34b9f6099cfe01407b473d5af7"
+  integrity sha512-P38NO8eOuWOKY1sQk5yE0crNtrjgjJj6r3NrbIKtG18KzCHmHE2Bt+aQA7/y0w3uYsHWxDa6icOohzjLJ4vJ4A==
 
 elliptic@^6.5.3:
   version "6.5.4"
@@ -7844,7 +7887,7 @@ json5@^1.0.1, json5@^1.0.2:
   dependencies:
     minimist "^1.2.0"
 
-json5@^2.1.2, json5@^2.2.0, json5@^2.2.2:
+json5@^2.1.2, json5@^2.2.0, json5@^2.2.3:
   version "2.2.3"
   resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
   integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==

From c3a42e1280759ea28bee11f241aef892b148bf6a Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 25 Aug 2023 11:31:20 +0200
Subject: [PATCH 04/81] Add `data-nosnippet` attribute to remote posts and
 local posts with `noindex` (#26648)

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

diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index 30692d1cd9..e1728910ee 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -550,7 +550,7 @@ class Status extends ImmutablePureComponent {
 
     return (
       <HotKeys handlers={handlers}>
-        <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
+        <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
           {prepend}
 
           <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>

From e3825a13c124b9d2b2af221ef6bbe8ced3e8c3c3 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 25 Aug 2023 12:01:55 +0200
Subject: [PATCH 05/81] Add PublicStatusesCheck to Elasticsearch index check on
 admin dashboard (#26650)

---
 app/lib/admin/system_check/elasticsearch_check.rb       | 1 +
 spec/lib/admin/system_check/elasticsearch_check_spec.rb | 2 ++
 2 files changed, 3 insertions(+)

diff --git a/app/lib/admin/system_check/elasticsearch_check.rb b/app/lib/admin/system_check/elasticsearch_check.rb
index 91070756cb..2092ef0dd0 100644
--- a/app/lib/admin/system_check/elasticsearch_check.rb
+++ b/app/lib/admin/system_check/elasticsearch_check.rb
@@ -6,6 +6,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
     AccountsIndex,
     TagsIndex,
     StatusesIndex,
+    PublicStatusesIndex,
   ].freeze
 
   def skip?
diff --git a/spec/lib/admin/system_check/elasticsearch_check_spec.rb b/spec/lib/admin/system_check/elasticsearch_check_spec.rb
index f3918d403d..a885640ce0 100644
--- a/spec/lib/admin/system_check/elasticsearch_check_spec.rb
+++ b/spec/lib/admin/system_check/elasticsearch_check_spec.rb
@@ -17,6 +17,7 @@ describe Admin::SystemCheck::ElasticsearchCheck do
         allow(Chewy.client.indices).to receive_messages(get_mapping: {
           AccountsIndex.index_name => AccountsIndex.mappings_hash.deep_stringify_keys,
           StatusesIndex.index_name => StatusesIndex.mappings_hash.deep_stringify_keys,
+          PublicStatusesIndex.index_name => PublicStatusesIndex.mappings_hash.deep_stringify_keys,
           InstancesIndex.index_name => InstancesIndex.mappings_hash.deep_stringify_keys,
           TagsIndex.index_name => TagsIndex.mappings_hash.deep_stringify_keys,
         }, get_settings: {
@@ -90,6 +91,7 @@ describe Admin::SystemCheck::ElasticsearchCheck do
       allow(Chewy.client.indices).to receive(:get_mapping).and_return({
         AccountsIndex.index_name => AccountsIndex.mappings_hash.deep_stringify_keys,
         StatusesIndex.index_name => StatusesIndex.mappings_hash.deep_stringify_keys,
+        PublicStatusesIndex.index_name => PublicStatusesIndex.mappings_hash.deep_stringify_keys,
         InstancesIndex.index_name => InstancesIndex.mappings_hash.deep_stringify_keys,
         TagsIndex.index_name => TagsIndex.mappings_hash.deep_stringify_keys,
       })

From f2ec2876a45133668154a757d53283e02ecb20d5 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 25 Aug 2023 12:02:09 +0200
Subject: [PATCH 06/81] =?UTF-8?q?Add=20Elasticsearch/OpenSearch=20version?=
 =?UTF-8?q?=20to=20=E2=80=9CSoftware=E2=80=9D=20in=20admin=20dashboard=20(?=
 =?UTF-8?q?#26652)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../dimension/software_versions_dimension.rb   | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/app/lib/admin/metrics/dimension/software_versions_dimension.rb b/app/lib/admin/metrics/dimension/software_versions_dimension.rb
index 9ab3776c91..72a98a88ab 100644
--- a/app/lib/admin/metrics/dimension/software_versions_dimension.rb
+++ b/app/lib/admin/metrics/dimension/software_versions_dimension.rb
@@ -10,7 +10,7 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
   protected
 
   def perform_query
-    [mastodon_version, ruby_version, postgresql_version, redis_version]
+    [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version].compact
   end
 
   def mastodon_version
@@ -57,6 +57,22 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
     }
   end
 
+  def elasticsearch_version
+    return unless Chewy.enabled?
+
+    client_info = Chewy.client.info
+    version = client_info.dig('version', 'number')
+
+    {
+      key: 'elasticsearch',
+      human_key: client_info.dig('version', 'distribution') == 'opensearch' ? 'OpenSearch' : 'Elasticsearch',
+      value: version,
+      human_value: version,
+    }
+  rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error
+    nil
+  end
+
   def redis_info
     @redis_info ||= if redis.is_a?(Redis::Namespace)
                       redis.redis.info

From 82ec6f162b617e9c46f23ebea1cf27e1e039121c Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 25 Aug 2023 17:01:56 +0200
Subject: [PATCH 07/81] Fix statuses search Elasticsearch query (#26657)

---
 app/services/statuses_search_service.rb | 17 +++--------------
 1 file changed, 3 insertions(+), 14 deletions(-)

diff --git a/app/services/statuses_search_service.rb b/app/services/statuses_search_service.rb
index 21d6b71b7d..2a4e533cfb 100644
--- a/app/services/statuses_search_service.rb
+++ b/app/services/statuses_search_service.rb
@@ -15,7 +15,7 @@ class StatusesSearchService < BaseService
 
   def status_search_results
     definition = parsed_query.apply(
-      StatusesIndex.filter(
+      Chewy::Search::Request.new(StatusesIndex, PublicStatusesIndex).filter(
         bool: {
           should: [
             publicly_searchable,
@@ -27,9 +27,6 @@ class StatusesSearchService < BaseService
       )
     )
 
-    # This is the best way to submit identical queries to multi-indexes though chewy
-    definition.instance_variable_get(:@parameters)[:indices].value[:indices] << PublicStatusesIndex
-
     results             = definition.collapse(field: :id).order(_id: { order: :desc }).limit(@limit).offset(@offset).objects.compact
     account_ids         = results.map(&:account_id)
     account_domains     = results.map(&:account_domain)
@@ -42,13 +39,7 @@ class StatusesSearchService < BaseService
 
   def publicly_searchable
     {
-      bool: {
-        must_not: {
-          exists: {
-            field: 'searchable_by',
-          },
-        },
-      },
+      term: { _index: PublicStatusesIndex.index_name },
     }
   end
 
@@ -57,9 +48,7 @@ class StatusesSearchService < BaseService
       bool: {
         must: [
           {
-            exists: {
-              field: 'searchable_by',
-            },
+            term: { _index: StatusesIndex.index_name },
           },
           {
             term: { searchable_by: @account.id },

From e4c0ce18a32cfc8a911a4fcd3fd0b310755f609c Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 25 Aug 2023 17:02:33 +0200
Subject: [PATCH 08/81] Update dependency immutable to v4.3.4 (#26655)

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 6894f4388c..a630a752e2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6686,9 +6686,9 @@ immutable@^3.8.2:
   integrity sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==
 
 immutable@^4.0.0, immutable@^4.0.0-rc.1, immutable@^4.3.0:
-  version "4.3.3"
-  resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.3.tgz#8934ff6826d996a7642c8dc4b46e694dd19561e3"
-  integrity sha512-808ZFYMsIRAjLAu5xkKo0TsbY9LBy9H5MazTKIEHerNkg0ymgilGfBPMR/3G7d/ihGmuK2Hw8S1izY2d3kd3wA==
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
+  integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
 
 import-fresh@^3.2.1:
   version "3.3.0"

From 8e8747c564ce3f06a96fc031cd0da9297d631fe0 Mon Sep 17 00:00:00 2001
From: jsgoldstein <jakegoldstein95@gmail.com>
Date: Fri, 25 Aug 2023 12:16:08 -0400
Subject: [PATCH 09/81] Update ordering to use `id` from body of document
 instead of deprecated `_id` (#26659)

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

diff --git a/app/services/statuses_search_service.rb b/app/services/statuses_search_service.rb
index 2a4e533cfb..0d0de2a9d2 100644
--- a/app/services/statuses_search_service.rb
+++ b/app/services/statuses_search_service.rb
@@ -27,7 +27,7 @@ class StatusesSearchService < BaseService
       )
     )
 
-    results             = definition.collapse(field: :id).order(_id: { order: :desc }).limit(@limit).offset(@offset).objects.compact
+    results             = definition.collapse(field: :id).order(id: { order: :desc }).limit(@limit).offset(@offset).objects.compact
     account_ids         = results.map(&:account_id)
     account_domains     = results.map(&:account_domain)
     preloaded_relations = @account.relations_map(account_ids, account_domains)

From 072112867b9a3ec090ad2c92d6363b47b2265d74 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 25 Aug 2023 18:17:22 +0200
Subject: [PATCH 10/81] Fix dashboard check for Elasticsearch suggested command
 including incorrect names (#26658)

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

diff --git a/app/lib/admin/system_check/elasticsearch_check.rb b/app/lib/admin/system_check/elasticsearch_check.rb
index 2092ef0dd0..c0a1a21e86 100644
--- a/app/lib/admin/system_check/elasticsearch_check.rb
+++ b/app/lib/admin/system_check/elasticsearch_check.rb
@@ -86,7 +86,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
 
   def mismatched_indexes
     @mismatched_indexes ||= INDEXES.filter_map do |klass|
-      klass.index_name if Chewy.client.indices.get_mapping[klass.index_name]&.deep_symbolize_keys != klass.mappings_hash
+      klass.base_name if Chewy.client.indices.get_mapping[klass.index_name]&.deep_symbolize_keys != klass.mappings_hash
     end
   end
 

From f39847476cbc24af6daf755dd51be885726cb631 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 25 Aug 2023 18:26:44 +0200
Subject: [PATCH 11/81] Change the pre-release versioning scheme and associated
 environment variables (#26653)

---
 .github/workflows/build-container-image.yml |  6 ++++--
 .github/workflows/build-nightly.yml         |  9 ++++-----
 .github/workflows/build-push-pr.yml         |  6 +++---
 Dockerfile                                  |  8 ++++----
 lib/mastodon/version.rb                     | 17 ++++++++++++-----
 5 files changed, 27 insertions(+), 19 deletions(-)

diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml
index 1b15d19885..8e9c747664 100644
--- a/.github/workflows/build-container-image.yml
+++ b/.github/workflows/build-container-image.yml
@@ -8,7 +8,9 @@ on:
         type: boolean
       push_to_images:
         type: string
-      version_suffix:
+      version_prerelease:
+        type: string
+      version_metadata:
         type: string
       flavor:
         type: string
@@ -83,7 +85,7 @@ jobs:
       - uses: docker/build-push-action@v4
         with:
           context: .
-          build-args: MASTODON_VERSION_SUFFIX=${{ inputs.version_suffix }}
+          build-args: MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }} MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
           platforms: ${{ inputs.platforms }}
           provenance: false
           builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml
index 5b34c1c3c6..5feb2ade33 100644
--- a/.github/workflows/build-nightly.yml
+++ b/.github/workflows/build-nightly.yml
@@ -16,9 +16,9 @@ jobs:
         env:
           TZ: Etc/UTC
         run: |
-          echo mastodon_version_suffix=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
+          echo mastodon_version_prerelease=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
     outputs:
-      suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }}
+      prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }}
 
   build-image:
     needs: compute-suffix
@@ -29,8 +29,7 @@ jobs:
       push_to_images: |
         tootsuite/mastodon
         ghcr.io/mastodon/mastodon
-      # The `-` is important here, result will be v4.1.2-nightly.2022-03-05
-      version_suffix: -${{ needs.compute-suffix.outputs.suffix }}
+      version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
       labels: |
         org.opencontainers.image.description=Nightly build image used for testing purposes
       flavor: |
@@ -38,5 +37,5 @@ jobs:
       tags: |
         type=raw,value=edge
         type=raw,value=nightly
-        type=schedule,pattern=${{ needs.compute-suffix.outputs.suffix }}
+        type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
     secrets: inherit
diff --git a/.github/workflows/build-push-pr.yml b/.github/workflows/build-push-pr.yml
index 2d20261286..b95e3c14e0 100644
--- a/.github/workflows/build-push-pr.yml
+++ b/.github/workflows/build-push-pr.yml
@@ -21,9 +21,9 @@ jobs:
         uses: actions/checkout@v3
       - id: version_vars
         run: |
-          echo mastodon_version_suffix=+pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
+          echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
     outputs:
-      suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }}
+      metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }}
 
   build-image:
     needs: compute-suffix
@@ -33,7 +33,7 @@ jobs:
       use_native_arm64_builder: true
       push_to_images: |
         ghcr.io/mastodon/mastodon
-      version_suffix: ${{ needs.compute-suffix.outputs.suffix }}
+      version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
       flavor: |
         latest=auto
       tags: |
diff --git a/Dockerfile b/Dockerfile
index cb5096581c..cdabc4c7c8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -42,8 +42,8 @@ RUN apt-get update && \
 FROM node:${NODE_VERSION}
 
 # Use those args to specify your own version flags & suffixes
-ARG MASTODON_VERSION_FLAGS=""
-ARG MASTODON_VERSION_SUFFIX=""
+ARG MASTODON_VERSION_PRERELEASE=""
+ARG MASTODON_VERSION_METADATA=""
 
 ARG UID="991"
 ARG GID="991"
@@ -89,8 +89,8 @@ ENV RAILS_ENV="production" \
     NODE_ENV="production" \
     RAILS_SERVE_STATIC_FILES="true" \
     BIND="0.0.0.0" \
-    MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \
-    MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}"
+    MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
+    MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}"
 
 # Set the run user
 USER mastodon
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index e3347ca17c..c542d5d49a 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -16,12 +16,16 @@ module Mastodon
       0
     end
 
-    def flags
-      ENV['MASTODON_VERSION_FLAGS'].presence || '-beta2'
+    def default_prerelease
+      'beta2'
     end
 
-    def suffix
-      ENV.fetch('MASTODON_VERSION_SUFFIX', '')
+    def prerelease
+      ENV['MASTODON_VERSION_PRERELEASE'].presence || default_prerelease
+    end
+
+    def build_metadata
+      ENV.fetch('MASTODON_VERSION_METADATA', nil)
     end
 
     def to_a
@@ -29,7 +33,10 @@ module Mastodon
     end
 
     def to_s
-      [to_a.join('.'), flags, suffix].join
+      components = [to_a.join('.')]
+      components << "-#{prerelease}" if prerelease.present?
+      components << "+#{build_metadata}" if build_metadata.present?
+      components.join
     end
 
     def repository

From 71641766f2ca6555fe19b309e9bd9f2455575bcc Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 25 Aug 2023 22:03:04 +0200
Subject: [PATCH 12/81] Fix toast saying "published" instead of "saved" after
 editing post in web UI (#26664)

---
 app/javascript/mastodon/actions/compose.js | 3 ++-
 app/javascript/mastodon/locales/en.json    | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 260fb43f08..6abfd6157e 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -84,6 +84,7 @@ const messages = defineMessages({
   uploadErrorPoll:  { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
   open: { id: 'compose.published.open', defaultMessage: 'Open' },
   published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
+  saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
 });
 
 export const ensureComposeIsVisible = (getState, routerHistory) => {
@@ -244,7 +245,7 @@ export function submitCompose(routerHistory) {
       }
 
       dispatch(showAlert({
-        message: messages.published,
+        message: statusId === null ? messages.published : messages.saved,
         action: messages.open,
         dismissAfter: 10000,
         onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index d4cd8c7945..5eeaf8044b 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -137,6 +137,7 @@
   "compose.language.search": "Search languages...",
   "compose.published.body": "Post published.",
   "compose.published.open": "Open",
+  "compose.saved.body": "Post saved.",
   "compose_form.direct_message_warning_learn_more": "Learn more",
   "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
   "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.",

From 925c16adea49adefdb202c19e8a7eaf439e1dada Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Sat, 26 Aug 2023 17:34:36 +0200
Subject: [PATCH 13/81] Fix nightly build version (#26676)

---
 .github/workflows/build-container-image.yml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml
index 8e9c747664..897bb9caaa 100644
--- a/.github/workflows/build-container-image.yml
+++ b/.github/workflows/build-container-image.yml
@@ -85,7 +85,9 @@ jobs:
       - uses: docker/build-push-action@v4
         with:
           context: .
-          build-args: MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }} MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
+          build-args: |
+            MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
+            MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
           platforms: ${{ inputs.platforms }}
           provenance: false
           builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}

From f8d2fea2e6f6eb38c62e9a484980fa5696dfa2a4 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Sun, 27 Aug 2023 18:58:20 +0200
Subject: [PATCH 14/81] Change queue of job when opting into search from
 `default` to `pull` (#26688)

---
 app/workers/add_to_public_statuses_index_worker.rb | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/workers/add_to_public_statuses_index_worker.rb b/app/workers/add_to_public_statuses_index_worker.rb
index 409e5e7086..33529a4c1d 100644
--- a/app/workers/add_to_public_statuses_index_worker.rb
+++ b/app/workers/add_to_public_statuses_index_worker.rb
@@ -3,6 +3,8 @@
 class AddToPublicStatusesIndexWorker
   include Sidekiq::Worker
 
+  sidekiq_options queue: 'pull'
+
   def perform(account_id)
     account = Account.find(account_id)
 

From 5694e24bbf9afccbbf1b44d3a405a4e2bc0ff08d Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Sun, 27 Aug 2023 22:37:35 +0200
Subject: [PATCH 15/81] Fix unnecessary condition causing seqscan when indexing
 (#26689)

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

diff --git a/app/models/concerns/account_statuses_search.rb b/app/models/concerns/account_statuses_search.rb
index 563a871051..626bf38900 100644
--- a/app/models/concerns/account_statuses_search.rb
+++ b/app/models/concerns/account_statuses_search.rb
@@ -31,7 +31,7 @@ module AccountStatusesSearch
   def add_to_public_statuses_index!
     return unless Chewy.enabled?
 
-    statuses.indexable.find_in_batches do |batch|
+    statuses.without_reblogs.where(visibility: :public).find_in_batches do |batch|
       PublicStatusesIndex.import(query: batch)
     end
   end

From e263db276fdc05dab743400977606efe98f1e949 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Sun, 27 Aug 2023 22:38:01 +0200
Subject: [PATCH 16/81] Change indexing jobs to use database replica (#26692)

---
 app/workers/add_to_public_statuses_index_worker.rb | 11 ++++++++---
 app/workers/scheduler/indexing_scheduler.rb        |  6 +++++-
 2 files changed, 13 insertions(+), 4 deletions(-)

diff --git a/app/workers/add_to_public_statuses_index_worker.rb b/app/workers/add_to_public_statuses_index_worker.rb
index 33529a4c1d..80d921eab0 100644
--- a/app/workers/add_to_public_statuses_index_worker.rb
+++ b/app/workers/add_to_public_statuses_index_worker.rb
@@ -2,15 +2,20 @@
 
 class AddToPublicStatusesIndexWorker
   include Sidekiq::Worker
+  include DatabaseHelper
 
   sidekiq_options queue: 'pull'
 
   def perform(account_id)
-    account = Account.find(account_id)
+    with_primary do
+      @account = Account.find(account_id)
+    end
 
-    return unless account.indexable?
+    return unless @account.indexable?
 
-    account.add_to_public_statuses_index!
+    with_read_replica do
+      @account.add_to_public_statuses_index!
+    end
   rescue ActiveRecord::RecordNotFound
     true
   end
diff --git a/app/workers/scheduler/indexing_scheduler.rb b/app/workers/scheduler/indexing_scheduler.rb
index 6c770d5a8f..1b09730c7d 100644
--- a/app/workers/scheduler/indexing_scheduler.rb
+++ b/app/workers/scheduler/indexing_scheduler.rb
@@ -3,6 +3,7 @@
 class Scheduler::IndexingScheduler
   include Sidekiq::Worker
   include Redisable
+  include DatabaseHelper
 
   sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.day.to_i
 
@@ -15,7 +16,10 @@ class Scheduler::IndexingScheduler
     indexes.each do |type|
       with_redis do |redis|
         redis.sscan_each("chewy:queue:#{type.name}", count: SCAN_BATCH_SIZE).each_slice(IMPORT_BATCH_SIZE) do |ids|
-          type.import!(ids)
+          with_read_replica do
+            type.import!(ids)
+          end
+
           redis.srem("chewy:queue:#{type.name}", ids)
         end
       end

From fc14d1f3b02411510467928df56318554c62c11d Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Aug 2023 10:20:02 +0200
Subject: [PATCH 17/81] Update eslint (non-major) (#26694)

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

diff --git a/yarn.lock b/yarn.lock
index a630a752e2..8804218290 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1287,9 +1287,9 @@
     eslint-visitor-keys "^3.3.0"
 
 "@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1":
-  version "4.6.2"
-  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
-  integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.0.tgz#11195513186f68d42fbf449f9a7136b2c0c92005"
+  integrity sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==
 
 "@eslint/eslintrc@^2.1.2":
   version "2.1.2"
@@ -1306,10 +1306,10 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
-"@eslint/js@^8.47.0":
-  version "8.47.0"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d"
-  integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==
+"@eslint/js@8.48.0":
+  version "8.48.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.48.0.tgz#642633964e217905436033a2bd08bf322849b7fb"
+  integrity sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==
 
 "@floating-ui/core@^1.3.1":
   version "1.3.1"
@@ -2608,15 +2608,15 @@
     "@types/yargs-parser" "*"
 
 "@typescript-eslint/eslint-plugin@^6.0.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.4.0.tgz#53428b616f7d80fe879f45a08f11cc0f0b62cf13"
-  integrity sha512-62o2Hmc7Gs3p8SLfbXcipjWAa6qk2wZGChXG2JbBtYpwSRmti/9KHLqfbLs9uDigOexG+3PaQ9G2g3201FWLKg==
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.4.1.tgz#bc0c6f000134b53c304ad0bec4ee4753cd3e89d2"
+  integrity sha512-3F5PtBzUW0dYlq77Lcqo13fv+58KDwUib3BddilE8ajPJT+faGgxmI9Sw+I8ZS22BYwoir9ZhNXcLi+S+I2bkw==
   dependencies:
     "@eslint-community/regexpp" "^4.5.1"
-    "@typescript-eslint/scope-manager" "6.4.0"
-    "@typescript-eslint/type-utils" "6.4.0"
-    "@typescript-eslint/utils" "6.4.0"
-    "@typescript-eslint/visitor-keys" "6.4.0"
+    "@typescript-eslint/scope-manager" "6.4.1"
+    "@typescript-eslint/type-utils" "6.4.1"
+    "@typescript-eslint/utils" "6.4.1"
+    "@typescript-eslint/visitor-keys" "6.4.1"
     debug "^4.3.4"
     graphemer "^1.4.0"
     ignore "^5.2.4"
@@ -2625,31 +2625,31 @@
     ts-api-utils "^1.0.1"
 
 "@typescript-eslint/parser@^6.0.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.4.0.tgz#47e7c6e22ff1248e8675d95f488890484de67600"
-  integrity sha512-I1Ah1irl033uxjxO9Xql7+biL3YD7w9IU8zF+xlzD/YxY6a4b7DYA08PXUUCbm2sEljwJF6ERFy2kTGAGcNilg==
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.4.1.tgz#85ad550bf4ac4aa227504f1becb828f8e46c44e3"
+  integrity sha512-610G6KHymg9V7EqOaNBMtD1GgpAmGROsmfHJPXNLCU9bfIuLrkdOygltK784F6Crboyd5tBFayPB7Sf0McrQwg==
   dependencies:
-    "@typescript-eslint/scope-manager" "6.4.0"
-    "@typescript-eslint/types" "6.4.0"
-    "@typescript-eslint/typescript-estree" "6.4.0"
-    "@typescript-eslint/visitor-keys" "6.4.0"
+    "@typescript-eslint/scope-manager" "6.4.1"
+    "@typescript-eslint/types" "6.4.1"
+    "@typescript-eslint/typescript-estree" "6.4.1"
+    "@typescript-eslint/visitor-keys" "6.4.1"
     debug "^4.3.4"
 
-"@typescript-eslint/scope-manager@6.4.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.4.0.tgz#3048e4262ba3eafa4e2e69b08912d9037ec646ae"
-  integrity sha512-TUS7vaKkPWDVvl7GDNHFQMsMruD+zhkd3SdVW0d7b+7Zo+bd/hXJQ8nsiUZMi1jloWo6c9qt3B7Sqo+flC1nig==
+"@typescript-eslint/scope-manager@6.4.1":
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.4.1.tgz#4b073a30be2dbe603e44e9ae0cff7e1d3ed19278"
+  integrity sha512-p/OavqOQfm4/Hdrr7kvacOSFjwQ2rrDVJRPxt/o0TOWdFnjJptnjnZ+sYDR7fi4OimvIuKp+2LCkc+rt9fIW+A==
   dependencies:
-    "@typescript-eslint/types" "6.4.0"
-    "@typescript-eslint/visitor-keys" "6.4.0"
+    "@typescript-eslint/types" "6.4.1"
+    "@typescript-eslint/visitor-keys" "6.4.1"
 
-"@typescript-eslint/type-utils@6.4.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.4.0.tgz#c8ac92716ed6a9d5443aa3e342910355b0796ba0"
-  integrity sha512-TvqrUFFyGY0cX3WgDHcdl2/mMCWCDv/0thTtx/ODMY1QhEiyFtv/OlLaNIiYLwRpAxAtOLOY9SUf1H3Q3dlwAg==
+"@typescript-eslint/type-utils@6.4.1":
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.4.1.tgz#fa21cb13016c8d6f352fe9b2d6c9ab6edc2d1857"
+  integrity sha512-7ON8M8NXh73SGZ5XvIqWHjgX2f+vvaOarNliGhjrJnv1vdjG0LVIz+ToYfPirOoBi56jxAKLfsLm40+RvxVVXA==
   dependencies:
-    "@typescript-eslint/typescript-estree" "6.4.0"
-    "@typescript-eslint/utils" "6.4.0"
+    "@typescript-eslint/typescript-estree" "6.4.1"
+    "@typescript-eslint/utils" "6.4.1"
     debug "^4.3.4"
     ts-api-utils "^1.0.1"
 
@@ -2658,10 +2658,10 @@
   resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32"
   integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==
 
-"@typescript-eslint/types@6.4.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.4.0.tgz#5b109a59a805f0d8d375895e42d9e5f0037f66ee"
-  integrity sha512-+FV9kVFrS7w78YtzkIsNSoYsnOtrYVnKWSTVXoL1761CsCRv5wpDOINgsXpxD67YCLZtVQekDDyaxfjVWUJmmg==
+"@typescript-eslint/types@6.4.1":
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.4.1.tgz#b2c61159f46dda210fed9f117f5d027f65bb5c3b"
+  integrity sha512-zAAopbNuYu++ijY1GV2ylCsQsi3B8QvfPHVqhGdDcbx/NK5lkqMnCGU53amAjccSpk+LfeONxwzUhDzArSfZJg==
 
 "@typescript-eslint/typescript-estree@5.59.0":
   version "5.59.0"
@@ -2676,30 +2676,30 @@
     semver "^7.3.7"
     tsutils "^3.21.0"
 
-"@typescript-eslint/typescript-estree@6.4.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.0.tgz#3c58d20632db93fec3d6ab902acbedf593d37276"
-  integrity sha512-iDPJArf/K2sxvjOR6skeUCNgHR/tCQXBsa+ee1/clRKr3olZjZ/dSkXPZjG6YkPtnW6p5D1egeEPMCW6Gn4yLA==
+"@typescript-eslint/typescript-estree@6.4.1":
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.1.tgz#91ff88101c710adb0f70a317f2f65efa9441da45"
+  integrity sha512-xF6Y7SatVE/OyV93h1xGgfOkHr2iXuo8ip0gbfzaKeGGuKiAnzS+HtVhSPx8Www243bwlW8IF7X0/B62SzFftg==
   dependencies:
-    "@typescript-eslint/types" "6.4.0"
-    "@typescript-eslint/visitor-keys" "6.4.0"
+    "@typescript-eslint/types" "6.4.1"
+    "@typescript-eslint/visitor-keys" "6.4.1"
     debug "^4.3.4"
     globby "^11.1.0"
     is-glob "^4.0.3"
     semver "^7.5.4"
     ts-api-utils "^1.0.1"
 
-"@typescript-eslint/utils@6.4.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.4.0.tgz#23e996b693603c5924b1fbb733cc73196256baa5"
-  integrity sha512-BvvwryBQpECPGo8PwF/y/q+yacg8Hn/2XS+DqL/oRsOPK+RPt29h5Ui5dqOKHDlbXrAeHUTnyG3wZA0KTDxRZw==
+"@typescript-eslint/utils@6.4.1":
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.4.1.tgz#81bf62ff0c3119a26c19fab683582e29450717bc"
+  integrity sha512-F/6r2RieNeorU0zhqZNv89s9bDZSovv3bZQpUNOmmQK1L80/cV4KEu95YUJWi75u5PhboFoKUJBnZ4FQcoqhDw==
   dependencies:
     "@eslint-community/eslint-utils" "^4.4.0"
     "@types/json-schema" "^7.0.12"
     "@types/semver" "^7.5.0"
-    "@typescript-eslint/scope-manager" "6.4.0"
-    "@typescript-eslint/types" "6.4.0"
-    "@typescript-eslint/typescript-estree" "6.4.0"
+    "@typescript-eslint/scope-manager" "6.4.1"
+    "@typescript-eslint/types" "6.4.1"
+    "@typescript-eslint/typescript-estree" "6.4.1"
     semver "^7.5.4"
 
 "@typescript-eslint/visitor-keys@5.59.0":
@@ -2710,12 +2710,12 @@
     "@typescript-eslint/types" "5.59.0"
     eslint-visitor-keys "^3.3.0"
 
-"@typescript-eslint/visitor-keys@6.4.0":
-  version "6.4.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.0.tgz#96a426cdb1add28274abd7a34aefe27f8b7d51ef"
-  integrity sha512-yJSfyT+uJm+JRDWYRYdCm2i+pmvXJSMtPR9Cq5/XQs4QIgNoLcoRtDdzsLbLsFM/c6um6ohQkg/MLxWvoIndJA==
+"@typescript-eslint/visitor-keys@6.4.1":
+  version "6.4.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.1.tgz#e3ccf7b8d42e625946ac5094ed92a405fb4115e0"
+  integrity sha512-y/TyRJsbZPkJIZQXrHfdnxVnxyKegnpEvnRGNam7s3TRR2ykGefEWOhaef00/UUN3IZxizS7BTO3svd3lCOJRQ==
   dependencies:
-    "@typescript-eslint/types" "6.4.0"
+    "@typescript-eslint/types" "6.4.1"
     eslint-visitor-keys "^3.4.1"
 
 "@webassemblyjs/ast@1.9.0":
@@ -5528,14 +5528,14 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
   integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
 
 eslint@^8.41.0:
-  version "8.47.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.47.0.tgz#c95f9b935463fb4fad7005e626c7621052e90806"
-  integrity sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==
+  version "8.48.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.48.0.tgz#bf9998ba520063907ba7bfe4c480dc8be03c2155"
+  integrity sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
     "@eslint-community/regexpp" "^4.6.1"
     "@eslint/eslintrc" "^2.1.2"
-    "@eslint/js" "^8.47.0"
+    "@eslint/js" "8.48.0"
     "@humanwhocodes/config-array" "^0.11.10"
     "@humanwhocodes/module-importer" "^1.0.1"
     "@nodelib/fs.walk" "^1.2.8"
@@ -5990,14 +5990,15 @@ findup-sync@^3.0.0:
     resolve-dir "^1.0.1"
 
 flat-cache@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
-  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.0.tgz#0e54ab4a1a60fe87e2946b6b00657f1c99e1af3f"
+  integrity sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==
   dependencies:
-    flatted "^3.1.0"
+    flatted "^3.2.7"
+    keyv "^4.5.3"
     rimraf "^3.0.2"
 
-flatted@^3.1.0:
+flatted@^3.2.7:
   version "3.2.7"
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
   integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
@@ -7843,6 +7844,11 @@ jsesc@~0.5.0:
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
   integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
 
+json-buffer@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+  integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
 json-parse-better-errors@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
@@ -7934,6 +7940,13 @@ keycode@^2.1.7:
   resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff"
   integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==
 
+keyv@^4.5.3:
+  version "4.5.3"
+  resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25"
+  integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==
+  dependencies:
+    json-buffer "3.0.1"
+
 killable@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
@@ -11329,7 +11342,6 @@ stringz@^2.1.0:
     char-regex "^1.0.2"
 
 "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
-  name strip-ansi-cjs
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==

From 822a35b9d577539a63984ca81d6668ca3b8435fd Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 28 Aug 2023 10:31:51 +0200
Subject: [PATCH 18/81] Fix not being able to negate prefix clauses in search
 (#26672)

---
 app/lib/search_query_parser.rb      |  2 +-
 app/lib/search_query_transformer.rb | 15 ++++++++++++---
 2 files changed, 13 insertions(+), 4 deletions(-)

diff --git a/app/lib/search_query_parser.rb b/app/lib/search_query_parser.rb
index 15956d4cfd..5d6ffbf29d 100644
--- a/app/lib/search_query_parser.rb
+++ b/app/lib/search_query_parser.rb
@@ -9,7 +9,7 @@ class SearchQueryParser < Parslet::Parser
   rule(:prefix)    { (term >> colon).as(:prefix) }
   rule(:shortcode) { (colon >> term >> colon.maybe).as(:shortcode) }
   rule(:phrase)    { (quote >> (term >> space.maybe).repeat >> quote).as(:phrase) }
-  rule(:clause)    { (prefix.maybe >> operator.maybe >> (phrase | term | shortcode)).as(:clause) }
+  rule(:clause)    { (operator.maybe >> prefix.maybe >> (phrase | term | shortcode)).as(:clause) }
   rule(:query)     { (clause >> space.maybe).repeat.as(:query) }
   root(:query)
 end
diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index dad99cbd2d..915f9c3312 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -36,7 +36,11 @@ class SearchQueryTransformer < Parslet::Transform
     def clause_to_filter(clause)
       case clause
       when PrefixClause
-        { clause.type => { clause.filter => clause.term } }
+        if clause.negated?
+          { bool: { must_not: { clause.type => { clause.filter => clause.term } } } }
+        else
+          { clause.type => { clause.filter => clause.term } }
+        end
       else
         raise "Unexpected clause type: #{clause}"
       end
@@ -81,7 +85,8 @@ class SearchQueryTransformer < Parslet::Transform
   class PrefixClause
     attr_reader :type, :filter, :operator, :term
 
-    def initialize(prefix, term)
+    def initialize(prefix, operator, term)
+      @negated  = operator == '-'
       @operator = :filter
 
       case prefix
@@ -114,6 +119,10 @@ class SearchQueryTransformer < Parslet::Transform
       end
     end
 
+    def negated?
+      @negated
+    end
+
     private
 
     def account_id_from_term(term)
@@ -132,7 +141,7 @@ class SearchQueryTransformer < Parslet::Transform
     operator = clause[:operator]&.to_s
 
     if clause[:prefix]
-      PrefixClause.new(prefix, clause[:term].to_s)
+      PrefixClause.new(prefix, operator, clause[:term].to_s)
     elsif clause[:term]
       TermClause.new(prefix, operator, clause[:term].to_s)
     elsif clause[:shortcode]

From b42fe5e3382ef1415748030ed7842095dcefa6a9 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Aug 2023 10:32:13 +0200
Subject: [PATCH 19/81] Update dependency haml_lint to v0.50.0 (#26665)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index f801b1b560..7a68249c28 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -324,7 +324,7 @@ GEM
       ruby-progressbar (~> 1.4)
     globalid (1.1.0)
       activesupport (>= 5.0)
-    haml (6.1.1)
+    haml (6.1.2)
       temple (>= 0.8.2)
       thor
       tilt
@@ -333,7 +333,7 @@ GEM
       activesupport (>= 5.1)
       haml (>= 4.0.6)
       railties (>= 5.1)
-    haml_lint (0.49.3)
+    haml_lint (0.50.0)
       haml (>= 4.0, < 6.2)
       parallel (~> 1.10)
       rainbow

From 4ea5db90da2d0fd0e888ea6c9f777835874c4302 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Aug 2023 11:04:06 +0200
Subject: [PATCH 20/81] Update DefinitelyTyped types (non-major) (#26693)

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

diff --git a/yarn.lock b/yarn.lock
index 8804218290..4a6d642cef 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1538,12 +1538,12 @@
     "@types/node" "*"
     jest-mock "^29.6.2"
 
-"@jest/expect-utils@^29.6.2":
-  version "29.6.2"
-  resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.2.tgz#1b97f290d0185d264dd9fdec7567a14a38a90534"
-  integrity sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg==
+"@jest/expect-utils@^29.6.2", "@jest/expect-utils@^29.6.4":
+  version "29.6.4"
+  resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.4.tgz#17c7dfe6cec106441f218b0aff4b295f98346679"
+  integrity sha512-FEhkJhqtvBwgSpiTrocquJCdXPsyvNKcl/n7A3u7X4pVoF4bswm11c9d4AV+kfq2Gpv/mM8x7E7DsRvH+djkrg==
   dependencies:
-    jest-get-type "^29.4.3"
+    jest-get-type "^29.6.3"
 
 "@jest/expect@^29.6.2":
   version "29.6.2"
@@ -1605,10 +1605,10 @@
     strip-ansi "^6.0.0"
     v8-to-istanbul "^9.0.1"
 
-"@jest/schemas@^29.6.0":
-  version "29.6.0"
-  resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040"
-  integrity sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==
+"@jest/schemas@^29.6.3":
+  version "29.6.3"
+  resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03"
+  integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==
   dependencies:
     "@sinclair/typebox" "^0.27.8"
 
@@ -1662,12 +1662,12 @@
     slash "^3.0.0"
     write-file-atomic "^4.0.2"
 
-"@jest/types@^29.6.1":
-  version "29.6.1"
-  resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.1.tgz#ae79080278acff0a6af5eb49d063385aaa897bf2"
-  integrity sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==
+"@jest/types@^29.6.1", "@jest/types@^29.6.3":
+  version "29.6.3"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59"
+  integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==
   dependencies:
-    "@jest/schemas" "^29.6.0"
+    "@jest/schemas" "^29.6.3"
     "@types/istanbul-lib-coverage" "^2.0.0"
     "@types/istanbul-reports" "^3.0.0"
     "@types/node" "*"
@@ -2226,9 +2226,9 @@
     "@types/istanbul-lib-report" "*"
 
 "@types/jest@*", "@types/jest@^29.5.2":
-  version "29.5.3"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.3.tgz#7a35dc0044ffb8b56325c6802a4781a626b05777"
-  integrity sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==
+  version "29.5.4"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.4.tgz#9d0a16edaa009a71e6a71a999acd582514dab566"
+  integrity sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==
   dependencies:
     expect "^29.0.0"
     pretty-format "^29.0.0"
@@ -2288,9 +2288,9 @@
   integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
 
 "@types/node@*":
-  version "20.4.9"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.9.tgz#c7164e0f8d3f12dfae336af0b1f7fdec8c6b204f"
-  integrity sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==
+  version "20.5.7"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.7.tgz#4b8ecac87fbefbc92f431d09c30e176fc0a7c377"
+  integrity sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==
 
 "@types/node@14 || 16 || 17":
   version "17.0.45"
@@ -2466,9 +2466,9 @@
     "@types/react" "*"
 
 "@types/react@*", "@types/react@16 || 17 || 18", "@types/react@>=16.9.11", "@types/react@^18.0.26", "@types/react@^18.2.7":
-  version "18.2.20"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.20.tgz#1605557a83df5c8a2cc4eeb743b3dfc0eb6aaeb2"
-  integrity sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==
+  version "18.2.21"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9"
+  integrity sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==
   dependencies:
     "@types/prop-types" "*"
     "@types/scheduler" "*"
@@ -4922,10 +4922,10 @@ detect-passive-events@^2.0.3:
   dependencies:
     detect-it "^4.0.1"
 
-diff-sequences@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
-  integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==
+diff-sequences@^29.6.3:
+  version "29.6.3"
+  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"
+  integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==
 
 diffie-hellman@^5.0.0:
   version "5.0.3"
@@ -5734,7 +5734,18 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
   dependencies:
     homedir-polyfill "^1.0.1"
 
-expect@^29.0.0, expect@^29.6.2:
+expect@^29.0.0:
+  version "29.6.4"
+  resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.4.tgz#a6e6f66d4613717859b2fe3da98a739437b6f4b8"
+  integrity sha512-F2W2UyQ8XYyftHT57dtfg8Ue3X5qLgm2sSug0ivvLRH/VKNRL/pDxg/TH7zVzbQB0tu80clNFy6LU7OS/VSEKA==
+  dependencies:
+    "@jest/expect-utils" "^29.6.4"
+    jest-get-type "^29.6.3"
+    jest-matcher-utils "^29.6.4"
+    jest-message-util "^29.6.3"
+    jest-util "^29.6.3"
+
+expect@^29.6.2:
   version "29.6.2"
   resolved "https://registry.yarnpkg.com/expect/-/expect-29.6.2.tgz#7b08e83eba18ddc4a2cf62b5f2d1918f5cd84521"
   integrity sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA==
@@ -7431,15 +7442,15 @@ jest-config@^29.6.2:
     slash "^3.0.0"
     strip-json-comments "^3.1.1"
 
-jest-diff@^29.6.2:
-  version "29.6.2"
-  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.2.tgz#c36001e5543e82a0805051d3ceac32e6825c1c46"
-  integrity sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==
+jest-diff@^29.6.2, jest-diff@^29.6.4:
+  version "29.6.4"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.4.tgz#85aaa6c92a79ae8cd9a54ebae8d5b6d9a513314a"
+  integrity sha512-9F48UxR9e4XOEZvoUXEHSWY4qC4zERJaOfrbBg9JpbJOO43R1vN76REt/aMGZoY6GD5g84nnJiBIVlscegefpw==
   dependencies:
     chalk "^4.0.0"
-    diff-sequences "^29.4.3"
-    jest-get-type "^29.4.3"
-    pretty-format "^29.6.2"
+    diff-sequences "^29.6.3"
+    jest-get-type "^29.6.3"
+    pretty-format "^29.6.3"
 
 jest-docblock@^29.4.3:
   version "29.4.3"
@@ -7485,10 +7496,10 @@ jest-environment-node@^29.6.2:
     jest-mock "^29.6.2"
     jest-util "^29.6.2"
 
-jest-get-type@^29.4.3:
-  version "29.4.3"
-  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5"
-  integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==
+jest-get-type@^29.4.3, jest-get-type@^29.6.3:
+  version "29.6.3"
+  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1"
+  integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==
 
 jest-haste-map@^29.6.2:
   version "29.6.2"
@@ -7517,28 +7528,28 @@ jest-leak-detector@^29.6.2:
     jest-get-type "^29.4.3"
     pretty-format "^29.6.2"
 
-jest-matcher-utils@^29.6.2:
-  version "29.6.2"
-  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz#39de0be2baca7a64eacb27291f0bd834fea3a535"
-  integrity sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ==
+jest-matcher-utils@^29.6.2, jest-matcher-utils@^29.6.4:
+  version "29.6.4"
+  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.4.tgz#327db7ababea49455df3b23e5d6109fe0c709d24"
+  integrity sha512-KSzwyzGvK4HcfnserYqJHYi7sZVqdREJ9DMPAKVbS98JsIAvumihaNUbjrWw0St7p9IY7A9UskCW5MYlGmBQFQ==
   dependencies:
     chalk "^4.0.0"
-    jest-diff "^29.6.2"
-    jest-get-type "^29.4.3"
-    pretty-format "^29.6.2"
+    jest-diff "^29.6.4"
+    jest-get-type "^29.6.3"
+    pretty-format "^29.6.3"
 
-jest-message-util@^29.6.2:
-  version "29.6.2"
-  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.2.tgz#af7adc2209c552f3f5ae31e77cf0a261f23dc2bb"
-  integrity sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==
+jest-message-util@^29.6.2, jest-message-util@^29.6.3:
+  version "29.6.3"
+  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.3.tgz#bce16050d86801b165f20cfde34dc01d3cf85fbf"
+  integrity sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==
   dependencies:
     "@babel/code-frame" "^7.12.13"
-    "@jest/types" "^29.6.1"
+    "@jest/types" "^29.6.3"
     "@types/stack-utils" "^2.0.0"
     chalk "^4.0.0"
     graceful-fs "^4.2.9"
     micromatch "^4.0.4"
-    pretty-format "^29.6.2"
+    pretty-format "^29.6.3"
     slash "^3.0.0"
     stack-utils "^2.0.3"
 
@@ -7665,12 +7676,12 @@ jest-snapshot@^29.6.2:
     pretty-format "^29.6.2"
     semver "^7.5.3"
 
-jest-util@^29.6.2:
-  version "29.6.2"
-  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.2.tgz#8a052df8fff2eebe446769fd88814521a517664d"
-  integrity sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==
+jest-util@^29.6.2, jest-util@^29.6.3:
+  version "29.6.3"
+  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.3.tgz#e15c3eac8716440d1ed076f09bc63ace1aebca63"
+  integrity sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==
   dependencies:
-    "@jest/types" "^29.6.1"
+    "@jest/types" "^29.6.3"
     "@types/node" "*"
     chalk "^4.0.0"
     ci-info "^3.2.0"
@@ -9710,12 +9721,12 @@ pretty-format@^27.0.2:
     ansi-styles "^5.0.0"
     react-is "^17.0.1"
 
-pretty-format@^29.0.0, pretty-format@^29.6.2:
-  version "29.6.2"
-  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.2.tgz#3d5829261a8a4d89d8b9769064b29c50ed486a47"
-  integrity sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==
+pretty-format@^29.0.0, pretty-format@^29.6.2, pretty-format@^29.6.3:
+  version "29.6.3"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.3.tgz#d432bb4f1ca6f9463410c3fb25a0ba88e594ace7"
+  integrity sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==
   dependencies:
-    "@jest/schemas" "^29.6.0"
+    "@jest/schemas" "^29.6.3"
     ansi-styles "^5.0.0"
     react-is "^18.0.0"
 

From 7bd5ebb0c50bb481c85ef81816089a04746f762d Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 28 Aug 2023 11:36:17 +0200
Subject: [PATCH 21/81] Fix multiple issues with status index mappings (#26686)

---
 app/chewy/accounts_index.rb        |  9 +++++----
 app/chewy/public_statuses_index.rb | 14 ++++++++++----
 app/chewy/statuses_index.rb        | 14 ++++++++++----
 3 files changed, 25 insertions(+), 12 deletions(-)

diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb
index 61e3399aa8..8881b08f66 100644
--- a/app/chewy/accounts_index.rb
+++ b/app/chewy/accounts_index.rb
@@ -21,19 +21,20 @@ class AccountsIndex < Chewy::Index
 
     analyzer: {
       natural: {
-        tokenizer: 'uax_url_email',
+        tokenizer: 'standard',
         filter: %w(
-          english_possessive_stemmer
           lowercase
           asciifolding
           cjk_width
+          elision
+          english_possessive_stemmer
           english_stop
           english_stemmer
         ),
       },
 
       verbatim: {
-        tokenizer: 'standard',
+        tokenizer: 'uax_url_email',
         filter: %w(lowercase asciifolding cjk_width),
       },
 
@@ -62,6 +63,6 @@ class AccountsIndex < Chewy::Index
     field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
     field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
     field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
-    field(:text, type: 'text', analyzer: 'whitespace', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
+    field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
   end
 end
diff --git a/app/chewy/public_statuses_index.rb b/app/chewy/public_statuses_index.rb
index 1fad5de3a1..5c68a13658 100644
--- a/app/chewy/public_statuses_index.rb
+++ b/app/chewy/public_statuses_index.rb
@@ -20,13 +20,19 @@ class PublicStatusesIndex < Chewy::Index
     },
 
     analyzer: {
-      content: {
+      verbatim: {
         tokenizer: 'uax_url_email',
+        filter: %w(lowercase),
+      },
+
+      content: {
+        tokenizer: 'standard',
         filter: %w(
-          english_possessive_stemmer
           lowercase
           asciifolding
           cjk_width
+          elision
+          english_possessive_stemmer
           english_stop
           english_stemmer
         ),
@@ -40,9 +46,9 @@ class PublicStatusesIndex < Chewy::Index
                       .includes(:media_attachments, :preloadable_poll, :preview_cards)
 
   root date_detection: false do
-    field(:id, type: 'keyword')
+    field(:id, type: 'long')
     field(:account_id, type: 'long')
-    field(:text, type: 'text', analyzer: 'whitespace', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
+    field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
     field(:language, type: 'keyword')
     field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
     field(:created_at, type: 'date')
diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb
index 130f8801df..6d33521051 100644
--- a/app/chewy/statuses_index.rb
+++ b/app/chewy/statuses_index.rb
@@ -20,13 +20,19 @@ class StatusesIndex < Chewy::Index
     },
 
     analyzer: {
-      content: {
+      verbatim: {
         tokenizer: 'uax_url_email',
+        filter: %w(lowercase),
+      },
+
+      content: {
+        tokenizer: 'standard',
         filter: %w(
-          english_possessive_stemmer
           lowercase
           asciifolding
           cjk_width
+          elision
+          english_possessive_stemmer
           english_stop
           english_stemmer
         ),
@@ -64,9 +70,9 @@ class StatusesIndex < Chewy::Index
   end
 
   root date_detection: false do
-    field(:id, type: 'keyword')
+    field(:id, type: 'long')
     field(:account_id, type: 'long')
-    field(:text, type: 'text', analyzer: 'whitespace', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
+    field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
     field(:searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) })
     field(:language, type: 'keyword')
     field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })

From 9d9de8d219b7545826e9fb70f84aa65a0409d138 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Aug 2023 12:19:42 +0200
Subject: [PATCH 22/81] Update dependency @testing-library/jest-dom to v6
 (#26479)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
---
 app/javascript/mastodon/test_setup.js |  2 +-
 package.json                          |  2 +-
 yarn.lock                             | 25 +++++++++----------------
 3 files changed, 11 insertions(+), 18 deletions(-)

diff --git a/app/javascript/mastodon/test_setup.js b/app/javascript/mastodon/test_setup.js
index 666127af39..7b0828bfa8 100644
--- a/app/javascript/mastodon/test_setup.js
+++ b/app/javascript/mastodon/test_setup.js
@@ -1 +1 @@
-import '@testing-library/jest-dom/extend-expect';
+import '@testing-library/jest-dom';
diff --git a/package.json b/package.json
index 16c5d529a6..4038831050 100644
--- a/package.json
+++ b/package.json
@@ -152,7 +152,7 @@
   },
   "devDependencies": {
     "@formatjs/cli": "^6.1.1",
-    "@testing-library/jest-dom": "^5.16.5",
+    "@testing-library/jest-dom": "^6.0.0",
     "@testing-library/react": "^14.0.0",
     "@types/babel__core": "^7.20.1",
     "@types/emoji-mart": "^3.0.9",
diff --git a/yarn.lock b/yarn.lock
index 4a6d642cef..3e9c9abf77 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2022,14 +2022,13 @@
     lz-string "^1.5.0"
     pretty-format "^27.0.2"
 
-"@testing-library/jest-dom@^5.16.5":
-  version "5.17.0"
-  resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz#5e97c8f9a15ccf4656da00fecab505728de81e0c"
-  integrity sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==
+"@testing-library/jest-dom@^6.0.0":
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.0.0.tgz#d2ba5a3fd13724d5966b3f8cd24d2cedcab4fa76"
+  integrity sha512-Ye2R3+/oM27jir8CzYPmuWdavTaKwNZcu0d22L9pO/vnOYE0wmrtpw79TQJa8H6gV8/i7yd+pLaqeLlA0rTMfg==
   dependencies:
     "@adobe/css-tools" "^4.0.1"
     "@babel/runtime" "^7.9.2"
-    "@types/testing-library__jest-dom" "^5.9.1"
     aria-query "^5.0.0"
     chalk "^3.0.0"
     css.escape "^1.5.1"
@@ -2225,10 +2224,10 @@
   dependencies:
     "@types/istanbul-lib-report" "*"
 
-"@types/jest@*", "@types/jest@^29.5.2":
-  version "29.5.4"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.4.tgz#9d0a16edaa009a71e6a71a999acd582514dab566"
-  integrity sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==
+"@types/jest@^29.5.2":
+  version "29.5.3"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.3.tgz#7a35dc0044ffb8b56325c6802a4781a626b05777"
+  integrity sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==
   dependencies:
     expect "^29.0.0"
     pretty-format "^29.0.0"
@@ -2535,13 +2534,6 @@
   resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310"
   integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==
 
-"@types/testing-library__jest-dom@^5.9.1":
-  version "5.14.9"
-  resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz#0fb1e6a0278d87b6737db55af5967570b67cb466"
-  integrity sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==
-  dependencies:
-    "@types/jest" "*"
-
 "@types/tough-cookie@*":
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
@@ -11353,6 +11345,7 @@ stringz@^2.1.0:
     char-regex "^1.0.2"
 
 "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+  name strip-ansi-cjs
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==

From 67166de865b00a972e7d1c4e507c9f20a454efbd Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 28 Aug 2023 12:43:00 +0200
Subject: [PATCH 23/81] Add `from:me` syntax to search (#26660)

---
 app/lib/search_query_transformer.rb     | 13 ++++++++-----
 app/services/statuses_search_service.rb |  2 +-
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index 915f9c3312..f1fc687e47 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -85,8 +85,9 @@ class SearchQueryTransformer < Parslet::Transform
   class PrefixClause
     attr_reader :type, :filter, :operator, :term
 
-    def initialize(prefix, operator, term)
+    def initialize(prefix, operator, term, options = {})
       @negated  = operator == '-'
+      @options  = options
       @operator = :filter
 
       case prefix
@@ -105,15 +106,15 @@ class SearchQueryTransformer < Parslet::Transform
       when 'before'
         @filter = :created_at
         @type = :range
-        @term = { lt: term }
+        @term = { lt: term, time_zone: @options[:current_account]&.user_time_zone || 'UTC' }
       when 'after'
         @filter = :created_at
         @type = :range
-        @term = { gt: term }
+        @term = { gt: term, time_zone: @options[:current_account]&.user_time_zone || 'UTC' }
       when 'during'
         @filter = :created_at
         @type = :range
-        @term = { gte: term, lte: term }
+        @term = { gte: term, lte: term, time_zone: @options[:current_account]&.user_time_zone || 'UTC' }
       else
         raise Mastodon::SyntaxError
       end
@@ -126,6 +127,8 @@ class SearchQueryTransformer < Parslet::Transform
     private
 
     def account_id_from_term(term)
+      return @options[:current_account]&.id || -1 if term == 'me'
+
       username, domain = term.gsub(/\A@/, '').split('@')
       domain = nil if TagManager.instance.local_domain?(domain)
       account = Account.find_remote(username, domain)
@@ -141,7 +144,7 @@ class SearchQueryTransformer < Parslet::Transform
     operator = clause[:operator]&.to_s
 
     if clause[:prefix]
-      PrefixClause.new(prefix, operator, clause[:term].to_s)
+      PrefixClause.new(prefix, operator, clause[:term].to_s, current_account: current_account)
     elsif clause[:term]
       TermClause.new(prefix, operator, clause[:term].to_s)
     elsif clause[:shortcode]
diff --git a/app/services/statuses_search_service.rb b/app/services/statuses_search_service.rb
index 0d0de2a9d2..2317a2a1ac 100644
--- a/app/services/statuses_search_service.rb
+++ b/app/services/statuses_search_service.rb
@@ -59,6 +59,6 @@ class StatusesSearchService < BaseService
   end
 
   def parsed_query
-    SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query))
+    SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query), current_account: @account)
   end
 end

From bceb8931591b4b9df6e8dcfb47864c230884262c Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 28 Aug 2023 13:18:39 +0200
Subject: [PATCH 24/81] Add search options to search popout in web UI (#26662)

---
 .../features/compose/components/search.jsx    | 47 +++++++++++++++++--
 app/javascript/mastodon/locales/en.json       |  4 ++
 .../styles/mastodon/components.scss           |  6 +++
 3 files changed, 52 insertions(+), 5 deletions(-)

diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx
index 682f8d3c8c..1c629bcbb4 100644
--- a/app/javascript/mastodon/features/compose/components/search.jsx
+++ b/app/javascript/mastodon/features/compose/components/search.jsx
@@ -1,7 +1,7 @@
 import PropTypes from 'prop-types';
 import { PureComponent } from 'react';
 
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
 
 import classNames from 'classnames';
 
@@ -45,6 +45,16 @@ class Search extends PureComponent {
     options: [],
   };
 
+  defaultOptions = [
+    { label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:') } },
+    { label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:') } },
+    { label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:') } },
+    { label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:') } },
+    { label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:') } },
+    { label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:') } },
+    { label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:') } },
+  ];
+
   setRef = c => {
     this.searchForm = c;
   };
@@ -70,7 +80,7 @@ class Search extends PureComponent {
 
   handleKeyDown = (e) => {
     const { selectedOption } = this.state;
-    const options = this._getOptions();
+    const options = this._getOptions().concat(this.defaultOptions);
 
     switch(e.key) {
     case 'Escape':
@@ -100,11 +110,9 @@ class Search extends PureComponent {
       if (selectedOption === -1) {
         this._submit();
       } else if (options.length > 0) {
-        options[selectedOption].action();
+        options[selectedOption].action(e);
       }
 
-      this._unfocus();
-
       break;
     case 'Delete':
       if (selectedOption > -1 && options.length > 0) {
@@ -147,6 +155,7 @@ class Search extends PureComponent {
 
     router.history.push(`/tags/${query}`);
     onClickSearchResult(query, 'hashtag');
+    this._unfocus();
   };
 
   handleAccountClick = () => {
@@ -157,6 +166,7 @@ class Search extends PureComponent {
 
     router.history.push(`/@${query}`);
     onClickSearchResult(query, 'account');
+    this._unfocus();
   };
 
   handleURLClick = () => {
@@ -164,6 +174,7 @@ class Search extends PureComponent {
     const { value, onOpenURL } = this.props;
 
     onOpenURL(value, router.history);
+    this._unfocus();
   };
 
   handleStatusSearch = () => {
@@ -182,6 +193,8 @@ class Search extends PureComponent {
     } else if (search.get('type') === 'hashtag') {
       router.history.push(`/tags/${search.get('q')}`);
     }
+
+    this._unfocus();
   };
 
   handleForgetRecentSearchClick = search => {
@@ -194,6 +207,18 @@ class Search extends PureComponent {
     document.querySelector('.ui').parentElement.focus();
   }
 
+  _insertText (text) {
+    const { value, onChange } = this.props;
+
+    if (value === '') {
+      onChange(text);
+    } else if (value[value.length - 1] === ' ') {
+      onChange(`${value}${text}`);
+    } else {
+      onChange(`${value} ${text}`);
+    }
+  }
+
   _submit (type) {
     const { onSubmit, openInRoute } = this.props;
     const { router } = this.context;
@@ -203,6 +228,8 @@ class Search extends PureComponent {
     if (openInRoute) {
       router.history.push('/search');
     }
+
+    this._unfocus();
   }
 
   _getOptions () {
@@ -325,6 +352,16 @@ class Search extends PureComponent {
               </div>
             </>
           )}
+
+          <h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
+
+          <div className='search__popout__menu'>
+            {this.defaultOptions.map(({ key, label, action }, i) => (
+              <button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === (options.length + i) })}>
+                {label}
+              </button>
+            ))}
+          </div>
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 5eeaf8044b..5871b08def 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -586,8 +586,12 @@
   "search.quick_action.open_url": "Open URL in Mastodon",
   "search.quick_action.status_search": "Posts matching {x}",
   "search.search_or_paste": "Search or paste URL",
+  "search_popout.language_code": "ISO language code",
+  "search_popout.options": "Search options",
   "search_popout.quick_actions": "Quick actions",
   "search_popout.recent": "Recent searches",
+  "search_popout.specific_date": "specific date",
+  "search_popout.user": "user",
   "search_results.accounts": "Profiles",
   "search_results.all": "All",
   "search_results.hashtags": "Hashtags",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 7e7bf4488c..6fa5c545ea 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4991,6 +4991,12 @@ a.status-card {
     }
 
     &__menu {
+      margin-bottom: 20px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
       &__message {
         color: $dark-text-color;
         padding: 0 10px;

From 0cce7fb6172680fb5819c4d61d7adfba4147dfc1 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Mon, 28 Aug 2023 15:04:57 +0200
Subject: [PATCH 25/81] Fix incorrect call to `PublicStatusesIndex.import`
 (#26697)

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

diff --git a/app/models/concerns/account_statuses_search.rb b/app/models/concerns/account_statuses_search.rb
index 626bf38900..fa9238e6ef 100644
--- a/app/models/concerns/account_statuses_search.rb
+++ b/app/models/concerns/account_statuses_search.rb
@@ -32,7 +32,7 @@ module AccountStatusesSearch
     return unless Chewy.enabled?
 
     statuses.without_reblogs.where(visibility: :public).find_in_batches do |batch|
-      PublicStatusesIndex.import(query: batch)
+      PublicStatusesIndex.import(batch)
     end
   end
 

From 2304cc6456a5a03624e94849f95d324b132ee72b Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 28 Aug 2023 17:08:37 +0200
Subject: [PATCH 26/81] Fix bad search type heuristic (#26673)

---
 app/services/search_service.rb       | 14 ++++++--------
 spec/services/search_service_spec.rb |  9 ---------
 2 files changed, 6 insertions(+), 17 deletions(-)

diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 4e1e7ea26e..40d82fc525 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -17,7 +17,7 @@ class SearchService < BaseService
         results.merge!(url_resource_results) unless url_resource.nil? || @offset.positive? || (@options[:type].present? && url_resource_symbol != @options[:type].to_sym)
       elsif @query.present?
         results[:accounts] = perform_accounts_search! if account_searchable?
-        results[:statuses] = perform_statuses_search! if full_text_searchable?
+        results[:statuses] = perform_statuses_search! if status_searchable?
         results[:hashtags] = perform_hashtags_search! if hashtag_searchable?
       end
     end
@@ -79,18 +79,16 @@ class SearchService < BaseService
     url_resource.class.name.downcase.pluralize.to_sym
   end
 
-  def full_text_searchable?
-    return false unless Chewy.enabled?
-
-    statuses_search? && !@account.nil? && !(@query.include?('@') && !@query.include?(' '))
+  def status_searchable?
+    Chewy.enabled? && status_search? && @account.present?
   end
 
   def account_searchable?
-    account_search? && !(@query.include?('@') && @query.include?(' '))
+    account_search?
   end
 
   def hashtag_searchable?
-    hashtag_search? && !@query.include?('@')
+    hashtag_search?
   end
 
   def account_search?
@@ -101,7 +99,7 @@ class SearchService < BaseService
     @options[:type].blank? || @options[:type] == 'hashtags'
   end
 
-  def statuses_search?
+  def status_search?
     @options[:type].blank? || @options[:type] == 'statuses'
   end
 end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 123bd4f560..cb69af5f54 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -83,15 +83,6 @@ describe SearchService, type: :service do
           expect(Tag).to have_received(:search_for).with('tag', 10, 0, exclude_unreviewed: nil)
           expect(results).to eq empty_results.merge(hashtags: [tag])
         end
-
-        it 'does not include tag when starts with @ character' do
-          query = '@username'
-          allow(Tag).to receive(:search_for)
-
-          results = subject.call(query, nil, 10)
-          expect(Tag).to_not have_received(:search_for)
-          expect(results).to eq empty_results
-        end
       end
     end
   end

From dd72a8d28b4479afdc19ac73cac83609d85b5f9e Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 28 Aug 2023 17:10:58 +0200
Subject: [PATCH 27/81] Update dependency stoplight to v3.0.2 (#26698)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index 7a68249c28..49ada55d2a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -731,7 +731,7 @@ GEM
       net-ssh (>= 2.8.0)
     stackprof (0.2.25)
     statsd-ruby (1.5.0)
-    stoplight (3.0.1)
+    stoplight (3.0.2)
       redlock (~> 1.0)
     strong_migrations (0.8.0)
       activerecord (>= 5.2)

From 01b87a1632a9024eb8be807cfc41a592b6a3c8ce Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 28 Aug 2023 19:40:08 +0200
Subject: [PATCH 28/81] Change video compression parameters (#26631)

---
 app/models/media_attachment.rb | 16 ++++++----------
 lib/paperclip/transcoder.rb    | 10 ++++++++--
 2 files changed, 14 insertions(+), 12 deletions(-)

diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 7474b5653f..88abd93390 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -44,6 +44,7 @@ class MediaAttachment < ApplicationRecord
 
   MAX_VIDEO_MATRIX_LIMIT = 8_294_400 # 3840x2160px
   MAX_VIDEO_FRAME_RATE   = 120
+  MAX_VIDEO_FRAMES       = 36_000 # Approx. 5 minutes at 120 fps
 
   IMAGE_FILE_EXTENSIONS = %w(.jpg .jpeg .png .gif .webp .heic .heif .avif).freeze
   VIDEO_FILE_EXTENSIONS = %w(.webm .mp4 .m4v .mov).freeze
@@ -98,17 +99,12 @@ class MediaAttachment < ApplicationRecord
     convert_options: {
       output: {
         'loglevel' => 'fatal',
-        'movflags' => 'faststart',
-        'pix_fmt' => 'yuv420p',
-        'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
-        'vsync' => 'cfr',
+        'preset' => 'veryfast',
         'c:v' => 'h264',
-        'maxrate' => '1300K',
-        'bufsize' => '1300K',
-        'b:v' => '1300K',
-        'frames:v' => 60 * 60 * 3,
-        'crf' => 18,
+        'c:a' => 'aac',
+        'b:a' => '192k',
         'map_metadata' => '-1',
+        'frames:v' => MAX_VIDEO_FRAMES,
       }.freeze,
     }.freeze,
   }.freeze
@@ -135,7 +131,7 @@ class MediaAttachment < ApplicationRecord
       convert_options: {
         output: {
           'loglevel' => 'fatal',
-          :vf => 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease',
+          :vf => 'scale=\'min(640\, iw):min(640\, ih)\':force_original_aspect_ratio=decrease',
         }.freeze,
       }.freeze,
       format: 'png',
diff --git a/lib/paperclip/transcoder.rb b/lib/paperclip/transcoder.rb
index 0f2e30f7d5..b88cf662c2 100644
--- a/lib/paperclip/transcoder.rb
+++ b/lib/paperclip/transcoder.rb
@@ -4,6 +4,9 @@ module Paperclip
   # This transcoder is only to be used for the MediaAttachment model
   # to check when uploaded videos are actually gifv's
   class Transcoder < Paperclip::Processor
+    # This is the H.264 "High" value taken from https://www.dr-lex.be/info-stuff/videocalc.html
+    BITS_PER_PIXEL = 0.11
+
     def initialize(file, options = {}, attachment = nil)
       super
 
@@ -38,8 +41,11 @@ module Paperclip
         @output_options['vframes'] = 1
       when 'mp4'
         unless eligible_to_passthrough?(metadata)
-          @output_options['acodec'] = 'aac'
-          @output_options['strict'] = 'experimental'
+          bitrate = (metadata.width * metadata.height * 30 * BITS_PER_PIXEL) / 1_000
+
+          @output_options['b:v']     = "#{bitrate}k"
+          @output_options['maxrate'] = "#{bitrate + 192}k"
+          @output_options['bufsize'] = "#{bitrate * 5}k"
 
           if high_vfr?(metadata)
             @output_options['vsync'] = 'vfr'

From 10b06436d13702aedaa92a8ece84fbfde2829d57 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 28 Aug 2023 19:49:38 +0200
Subject: [PATCH 29/81] Fix colors and typography on hashtag bar in web UI
 (#26666)

---
 .../mastodon/components/hashtag_bar.tsx       |  6 +++---
 .../styles/mastodon/components.scss           | 19 ++++++++++++-------
 2 files changed, 15 insertions(+), 10 deletions(-)

diff --git a/app/javascript/mastodon/components/hashtag_bar.tsx b/app/javascript/mastodon/components/hashtag_bar.tsx
index 674c481b81..d45a6e20eb 100644
--- a/app/javascript/mastodon/components/hashtag_bar.tsx
+++ b/app/javascript/mastodon/components/hashtag_bar.tsx
@@ -10,8 +10,8 @@ import { groupBy, minBy } from 'lodash';
 
 import { getStatusContent } from './status_content';
 
-// About two lines on desktop
-const VISIBLE_HASHTAGS = 7;
+// Fit on a single line on desktop
+const VISIBLE_HASHTAGS = 3;
 
 // Those types are not correct, they need to be replaced once this part of the state is typed
 export type TagLike = Record<{ name: string }>;
@@ -210,7 +210,7 @@ const HashtagBar: React.FC<{
 
   const revealedHashtags = expanded
     ? hashtags
-    : hashtags.slice(0, VISIBLE_HASHTAGS - 1);
+    : hashtags.slice(0, VISIBLE_HASHTAGS);
 
   return (
     <div className='hashtag-bar'>
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 6fa5c545ea..c07f95f564 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -9308,19 +9308,24 @@ noscript {
   display: flex;
   flex-wrap: wrap;
   font-size: 14px;
+  line-height: 18px;
   gap: 4px;
+  color: $darker-text-color;
 
   a {
     display: inline-flex;
-    color: $dark-text-color;
+    color: inherit;
     text-decoration: none;
 
-    &:hover {
-      text-decoration: none;
-
-      span {
-        text-decoration: underline;
-      }
+    &:hover span {
+      text-decoration: underline;
     }
   }
+
+  .link-button {
+    color: inherit;
+    font-size: inherit;
+    line-height: inherit;
+    padding: 0;
+  }
 }

From a7d96e6affd4a2da65f09f3f4ffa1b9350bb9a42 Mon Sep 17 00:00:00 2001
From: Lukas Martini <lutoma@ohai.su>
Date: Tue, 29 Aug 2023 09:14:44 +0200
Subject: [PATCH 30/81] Improve error messages when DeepL quota is exceeded
 (#26704)

---
 .../api/v1/statuses/translations_controller.rb         | 10 +++++++++-
 config/locales/en.yml                                  |  4 ++++
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb
index 540b17d009..ec5ea5b85b 100644
--- a/app/controllers/api/v1/statuses/translations_controller.rb
+++ b/app/controllers/api/v1/statuses/translations_controller.rb
@@ -8,7 +8,15 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
   before_action :set_translation
 
   rescue_from TranslationService::NotConfiguredError, with: :not_found
-  rescue_from TranslationService::UnexpectedResponseError, TranslationService::QuotaExceededError, TranslationService::TooManyRequestsError, with: :service_unavailable
+  rescue_from TranslationService::UnexpectedResponseError, with: :service_unavailable
+
+  rescue_from TranslationService::QuotaExceededError do
+    render json: { error: I18n.t('translation.errors.quota_exceeded') }, status: 503
+  end
+
+  rescue_from TranslationService::TooManyRequestsError do
+    render json: { error: I18n.t('translation.errors.too_many_requests') }, status: 503
+  end
 
   def create
     render json: @translation, serializer: REST::TranslationSerializer
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 71121bb2e2..8bdfd1ec91 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1709,6 +1709,10 @@ en:
       default: "%b %d, %Y, %H:%M"
       month: "%b %Y"
       time: "%H:%M"
+  translation:
+    errors:
+      quota_exceeded: The server-wide usage quota for the translation service has been exceeded.
+      too_many_requests: There have been too many requests to the translation service recently.
   two_factor_authentication:
     add: Add
     disable: Disable 2FA

From 0719216368bf3a90fdb7ab27201a0607b63ea203 Mon Sep 17 00:00:00 2001
From: Christian Schmidt <github@chsc.dk>
Date: Tue, 29 Aug 2023 10:16:18 +0200
Subject: [PATCH 31/81] Remove dead code from public.jsx (#26547)

---
 app/javascript/packs/public.jsx | 81 ++++-----------------------------
 1 file changed, 9 insertions(+), 72 deletions(-)

diff --git a/app/javascript/packs/public.jsx b/app/javascript/packs/public.jsx
index 1d917d60ee..ae4a7410e1 100644
--- a/app/javascript/packs/public.jsx
+++ b/app/javascript/packs/public.jsx
@@ -7,8 +7,6 @@ import { defineMessages } from 'react-intl';
 
 import { delegate }  from '@rails/ujs';
 import axios from 'axios';
-import escapeTextContentForBrowser from 'escape-html';
-import { createBrowserHistory }  from 'history';
 import { throttle } from 'lodash';
 
 import { start } from '../mastodon/common';
@@ -48,23 +46,6 @@ window.addEventListener('message', e => {
 function loaded() {
   const { messages: localeData } = getLocale();
 
-  const scrollToDetailedStatus = () => {
-    const history = createBrowserHistory();
-    const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status');
-    const location = history.location;
-
-    if (detailedStatuses.length === 1 && (!location.state || !location.state.scrolledToDetailedStatus)) {
-      detailedStatuses[0].scrollIntoView();
-      history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
-    }
-  };
-
-  const getEmojiAnimationHandler = (swapTo) => {
-    return ({ target }) => {
-      target.src = target.getAttribute(swapTo);
-    };
-  };
-
   const locale = document.documentElement.lang;
 
   const dateTimeFormat = new Intl.DateTimeFormat(locale, {
@@ -158,27 +139,21 @@ function loaded() {
         const root = createRoot(content);
         root.render(<MediaContainer locale={locale} components={reactComponents} />);
         document.body.appendChild(content);
-        scrollToDetailedStatus();
       })
       .catch(error => {
         console.error(error);
-        scrollToDetailedStatus();
       });
-  } else {
-    scrollToDetailedStatus();
   }
 
-  delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
-    const username = document.getElementById('user_account_attributes_username');
-
-    if (username.value && username.value.length > 0) {
-      axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
-        username.setCustomValidity(formatMessage(messages.usernameTaken));
+  delegate(document, '#user_account_attributes_username', 'input', throttle(({ target }) => {
+    if (target.value && target.value.length > 0) {
+      axios.get('/api/v1/accounts/lookup', { params: { acct: target.value } }).then(() => {
+        target.setCustomValidity(formatMessage(messages.usernameTaken));
       }).catch(() => {
-        username.setCustomValidity('');
+        target.setCustomValidity('');
       });
     } else {
-      username.setCustomValidity('');
+      target.setCustomValidity('');
     }
   }, 500, { leading: false, trailing: true }));
 
@@ -196,9 +171,6 @@ function loaded() {
     }
   });
 
-  delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
-  delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
-
   delegate(document, '.status__content__spoiler-link', 'click', function() {
     const statusEl = this.parentNode.parentNode;
 
@@ -220,17 +192,6 @@ function loaded() {
   });
 }
 
-delegate(document, '#account_display_name', 'input', ({ target }) => {
-  const name = document.querySelector('.card .display-name strong');
-  if (name) {
-    if (target.value) {
-      name.innerHTML = emojify(escapeTextContentForBrowser(target.value));
-    } else {
-      name.textContent = target.dataset.default;
-    }
-  }
-});
-
 delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
   const avatar = document.getElementById(target.id + '-preview');
   const [file] = target.files || [];
@@ -239,33 +200,6 @@ delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
   avatar.src = url;
 });
 
-const getProfileAvatarAnimationHandler = (swapTo) => {
-  //animate avatar gifs on the profile page when moused over
-  return ({ target }) => {
-    const swapSrc = target.getAttribute(swapTo);
-    //only change the img source if autoplay is off and the image src is actually different
-    if(target.getAttribute('data-autoplay') !== 'true' && target.src !== swapSrc) {
-      target.src = swapSrc;
-    }
-  };
-};
-
-delegate(document, 'img#profile_page_avatar', 'mouseover', getProfileAvatarAnimationHandler('data-original'));
-
-delegate(document, 'img#profile_page_avatar', 'mouseout', getProfileAvatarAnimationHandler('data-static'));
-
-delegate(document, '#account_locked', 'change', ({ target }) => {
-  const lock = document.querySelector('.card .display-name i');
-
-  if (lock) {
-    if (target.checked) {
-      delete lock.dataset.hidden;
-    } else {
-      lock.dataset.hidden = 'true';
-    }
-  }
-});
-
 delegate(document, '.input-copy input', 'click', ({ target }) => {
   target.focus();
   target.select();
@@ -325,6 +259,9 @@ delegate(document, '.sidebar__toggle__icon', 'keydown', e => {
   }
 });
 
+delegate(document, '.custom-emoji', 'mouseover', ({ target }) => target.src = target.getAttribute('data-original'));
+delegate(document, '.custom-emoji', 'mouseout', ({ target }) => target.src = target.getAttribute('data-static'));
+
 // Empty the honeypot fields in JS in case something like an extension
 // automatically filled them.
 delegate(document, '#registration_new_user,#new_user', 'submit', () => {

From 286a21afdc427a24a32d506dcb5355df434e22ce Mon Sep 17 00:00:00 2001
From: Christian Schmidt <github@chsc.dk>
Date: Tue, 29 Aug 2023 10:17:57 +0200
Subject: [PATCH 32/81] Support webpacker live-reloading on Docker (#26419)

---
 .devcontainer/docker-compose.yml               | 1 +
 Procfile.dev                                   | 2 +-
 config/initializers/content_security_policy.rb | 3 ++-
 config/webpacker.yml                           | 2 +-
 4 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index a2658ea8ba..20aecd71d6 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -25,6 +25,7 @@ services:
     command: sleep infinity
     ports:
       - '127.0.0.1:3000:3000'
+      - '127.0.0.1:3035:3035'
       - '127.0.0.1:4000:4000'
     networks:
       - external_network
diff --git a/Procfile.dev b/Procfile.dev
index ba04fb661b..fbb2c2de23 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
-webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0
+webpack: bin/webpack-dev-server
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index 98c4f541f3..59ac3bdea2 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -34,7 +34,8 @@ Rails.application.config.content_security_policy do |p|
   p.worker_src      :self, :blob, assets_host
 
   if Rails.env.development?
-    webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{Webpacker.dev_server.host_with_port}" }
+    webpacker_public_host = ENV.fetch('WEBPACKER_DEV_SERVER_PUBLIC', Webpacker.config.dev_server[:public])
+    webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{webpacker_public_host}" }
 
     p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url, *webpacker_urls
     p.script_src  :self, :unsafe_inline, :unsafe_eval, assets_host
diff --git a/config/webpacker.yml b/config/webpacker.yml
index 6fd0fa1a0c..f8462e53a0 100644
--- a/config/webpacker.yml
+++ b/config/webpacker.yml
@@ -58,7 +58,7 @@ development:
   # Reference: https://webpack.js.org/configuration/dev-server/
   dev_server:
     https: false
-    host: localhost
+    host: 0.0.0.0
     port: 3035
     public: localhost:3035
     hmr: false

From 075cc8e8a64bd43b83865c7beddb877787ed674f Mon Sep 17 00:00:00 2001
From: Christian Schmidt <github@chsc.dk>
Date: Tue, 29 Aug 2023 10:20:36 +0200
Subject: [PATCH 33/81] Improve Codespaces port forwarding (#26400)

---
 .devcontainer/Dockerfile                   |  4 --
 .devcontainer/codespaces/devcontainer.json | 49 ++++++++++++++++++++++
 .devcontainer/devcontainer.json            | 22 +++++++---
 README.md                                  | 34 +++++++++++----
 config/environments/development.rb         |  2 +
 5 files changed, 93 insertions(+), 18 deletions(-)
 create mode 100644 .devcontainer/codespaces/devcontainer.json

diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index f991036add..b3b1d97a24 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -4,10 +4,6 @@ FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
 # Install Rails
 # RUN gem install rails webdrivers
 
-# Default value to allow debug server to serve content over GitHub Codespace's port forwarding service
-# The value is a comma-separated list of allowed domains
-ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev"
-
 ARG NODE_VERSION="16"
 RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
 
diff --git a/.devcontainer/codespaces/devcontainer.json b/.devcontainer/codespaces/devcontainer.json
new file mode 100644
index 0000000000..ca9156fdaa
--- /dev/null
+++ b/.devcontainer/codespaces/devcontainer.json
@@ -0,0 +1,49 @@
+{
+  "name": "Mastodon on GitHub Codespaces",
+  "dockerComposeFile": "../docker-compose.yml",
+  "service": "app",
+  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
+
+  "features": {
+    "ghcr.io/devcontainers/features/sshd:1": {}
+  },
+
+  "runServices": ["app", "db", "redis"],
+
+  "forwardPorts": [3000, 4000],
+
+  "portsAttributes": {
+    "3000": {
+      "label": "web",
+      "onAutoForward": "notify"
+    },
+    "4000": {
+      "label": "stream",
+      "onAutoForward": "silent"
+    }
+  },
+
+  "otherPortsAttributes": {
+    "onAutoForward": "silent"
+  },
+
+  "remoteEnv": {
+    "LOCAL_DOMAIN": "${localEnv:CODESPACE_NAME}-3000.app.github.dev",
+    "LOCAL_HTTPS": "true",
+    "STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev",
+    "DISABLE_FORGERY_REQUEST_PROTECTION": "true",
+    "ES_ENABLED": "",
+    "LIBRE_TRANSLATE_ENDPOINT": ""
+  },
+
+  "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
+  "postCreateCommand": ".devcontainer/post-create.sh",
+  "waitFor": "postCreateCommand",
+
+  "customizations": {
+    "vscode": {
+      "settings": {},
+      "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
+    }
+  }
+}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index ce14169aae..fa8d6542c1 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,5 +1,5 @@
 {
-  "name": "Mastodon",
+  "name": "Mastodon on local machine",
   "dockerComposeFile": "docker-compose.yml",
   "service": "app",
   "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
@@ -8,13 +8,23 @@
     "ghcr.io/devcontainers/features/sshd:1": {}
   },
 
-  "runServices": ["app", "db", "redis"],
-
   "forwardPorts": [3000, 4000],
 
-  "containerEnv": {
-    "ES_ENABLED": "",
-    "LIBRE_TRANSLATE_ENDPOINT": ""
+  "portsAttributes": {
+    "3000": {
+      "label": "web",
+      "onAutoForward": "notify",
+      "requireLocalPort": true
+    },
+    "4000": {
+      "label": "stream",
+      "onAutoForward": "silent",
+      "requireLocalPort": true
+    }
+  },
+
+  "otherPortsAttributes": {
+    "onAutoForward": "silent"
   },
 
   "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
diff --git a/README.md b/README.md
index 37cd3dfb46..e925bec519 100644
--- a/README.md
+++ b/README.md
@@ -59,13 +59,13 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
 
 ## Deployment
 
-### Tech stack:
+### Tech stack
 
 - **Ruby on Rails** powers the REST API and other web pages
 - **React.js** and Redux are used for the dynamic parts of the interface
 - **Node.js** powers the streaming API
 
-### Requirements:
+### Requirements
 
 - **PostgreSQL** 9.5+
 - **Redis** 4+
@@ -74,6 +74,10 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
 
 The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
 
+## Development
+
+### Vagrant
+
 A **Vagrant** configuration is included for development purposes. To use it, complete the following steps:
 
 - Install Vagrant and Virtualbox
@@ -82,9 +86,11 @@ A **Vagrant** configuration is included for development purposes. To use it, com
 - Run `vagrant ssh -c "cd /vagrant && foreman start"`
 - Open `http://mastodon.local` in your browser
 
+### MacOS
+
 To set up **MacOS** for native development, complete the following steps:
 
-- Install the latest stable Ruby version (use a ruby version manager for easy installation and management of ruby versions)
+- Install the latest stable Ruby version (use a Ruby version manager for easy installation and management of Ruby versions)
 - Run `brew install postgresql@14`
 - Run `brew install redis`
 - Run `brew install imagemagick`
@@ -94,15 +100,27 @@ To set up **MacOS** for native development, complete the following steps:
 - Run `bundle exec rails db:setup` (optionally prepend `RAILS_ENV=development` to target the dev environment)
 - Finally, run `overmind start -f Procfile.dev`
 
-### Getting Started with GitHub Codespaces
+### Docker
 
-To get started, create a codespace for this repository by clicking this 👇
+For development with **Docker**, complete the following steps:
 
-[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283)
+- Install Docker Desktop
+- Run `docker compose -f .devcontainer/docker-compose.yml up -d`
+- Run `docker compose -f .devcontainer/docker-compose.yml exec app .devcontainer/post-create.sh`
+- Finally, run `docker compose -f .devcontainer/docker-compose.yml exec app foreman start -f Procfile.dev`
 
-A codespace will open in a web-based version of Visual Studio Code. The [dev container](.devcontainer/devcontainer.json) is fully configured with the software needed for this project.
+If you are using an IDE with [support for the Development Container specification](https://containers.dev/supporting), it will run the above `docker compose` commands automatically. For **Visual Studio Code** this requires the [Dev Container extension](https://containers.dev/supporting#dev-containers).
 
-**Note**: Dev containers are an open spec that is supported by [GitHub Codespaces](https://github.com/codespaces) and [other tools](https://containers.dev/supporting).
+### GitHub Codespaces
+
+To get you coding in just a few minutes, GitHub Codespaces provides a web-based version of Visual Studio Code and a cloud-hosted development environment fully configured with the software needed for this project..
+
+- Click this button to create a new codespace:<br>
+  [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283&devcontainer_path=.devcontainer%2Fcodespaces%2Fdevcontainer.json)
+- Wait for the environment to build. This will take a few minutes.
+- When the editor is ready, run `foreman start -f Procfile.dev` in the terminal.
+- After a few seconds, a popup will appear with a button labeled _Open in Browser_. This will open Mastodon.
+- On the _Ports_ tab, right click on the “stream” row and select _Port visibility_ → _Public_.
 
 ## Contributing
 
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 31a3962458..9a6637bdb9 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -35,6 +35,8 @@ Rails.application.configure do
     config.cache_store = :null_store
   end
 
+  config.action_controller.forgery_protection_origin_check = ENV['DISABLE_FORGERY_REQUEST_PROTECTION'].nil?
+
   ActiveSupport::Logger.new(STDOUT).tap do |logger|
     logger.formatter = config.log_formatter
     config.logger = ActiveSupport::TaggedLogging.new(logger)

From 25bf6406290f49f87c6ed00474702d6729fc98f2 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Tue, 29 Aug 2023 10:29:07 +0200
Subject: [PATCH 34/81] Add debug logging on signature verification failure
 (#26637)

---
 app/controllers/concerns/signature_verification.rb | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb
index 1d27c92c8c..b0c4fff8bc 100644
--- a/app/controllers/concerns/signature_verification.rb
+++ b/app/controllers/concerns/signature_verification.rb
@@ -119,6 +119,8 @@ module SignatureVerification
   private
 
   def fail_with!(message, **options)
+    Rails.logger.warn { "Signature verification failed: #{message}" }
+
     @signature_verification_failure_reason = { error: message }.merge(options)
     @signed_request_actor = nil
   end

From a67cf439eebe0763ae920d242dfe7b8cd0730f23 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 29 Aug 2023 10:50:27 +0200
Subject: [PATCH 35/81] Update dependency axios to v1.5.0 (#26680)

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 3e9c9abf77..0f34ad3551 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3344,9 +3344,9 @@ axe-core@^4.6.2:
   integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==
 
 axios@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
-  integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267"
+  integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==
   dependencies:
     follow-redirects "^1.15.0"
     form-data "^4.0.0"

From 4ad1c5aa7174e5cef4ba9608a44429916a119bee Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 29 Aug 2023 10:53:01 +0200
Subject: [PATCH 36/81] Update dependency aws-sdk-s3 to v1.133.0 (#26616)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index 49ada55d2a..f26856bf94 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -124,8 +124,8 @@ GEM
     attr_required (1.0.1)
     awrence (1.2.1)
     aws-eventstream (1.2.0)
-    aws-partitions (1.793.0)
-    aws-sdk-core (3.180.3)
+    aws-partitions (1.809.0)
+    aws-sdk-core (3.181.0)
       aws-eventstream (~> 1, >= 1.0.2)
       aws-partitions (~> 1, >= 1.651.0)
       aws-sigv4 (~> 1.5)
@@ -133,8 +133,8 @@ GEM
     aws-sdk-kms (1.71.0)
       aws-sdk-core (~> 3, >= 3.177.0)
       aws-sigv4 (~> 1.1)
-    aws-sdk-s3 (1.132.1)
-      aws-sdk-core (~> 3, >= 3.179.0)
+    aws-sdk-s3 (1.133.0)
+      aws-sdk-core (~> 3, >= 3.181.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.6)
     aws-sigv4 (1.6.0)

From ae6cf33321a9f240ef73666a552e552b65390012 Mon Sep 17 00:00:00 2001
From: Tim Rogers <rogers.timothy.john@gmail.com>
Date: Tue, 29 Aug 2023 03:56:19 -0500
Subject: [PATCH 37/81] Fix bug with favourited view on Toots only showing
 latest favouriting accounts (#26577)

---
 .../mastodon/actions/interactions.js          | 58 ++++++++++++++++++-
 .../mastodon/features/favourites/index.jsx    | 25 +++++---
 .../mastodon/reducers/user_lists.js           | 15 ++++-
 3 files changed, 85 insertions(+), 13 deletions(-)

diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index 092a67ea75..1ffd23db53 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -1,5 +1,6 @@
-import api from '../api';
+import api, { getLinks } from '../api';
 
+import { fetchRelationships } from './accounts';
 import { importFetchedAccounts, importFetchedStatus } from './importer';
 
 export const REBLOG_REQUEST = 'REBLOG_REQUEST';
@@ -26,6 +27,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
 export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
 export const FAVOURITES_FETCH_FAIL    = 'FAVOURITES_FETCH_FAIL';
 
+export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
+export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
+export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
+
 export const PIN_REQUEST = 'PIN_REQUEST';
 export const PIN_SUCCESS = 'PIN_SUCCESS';
 export const PIN_FAIL    = 'PIN_FAIL';
@@ -308,8 +313,10 @@ export function fetchFavourites(id) {
     dispatch(fetchFavouritesRequest(id));
 
     api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
       dispatch(importFetchedAccounts(response.data));
-      dispatch(fetchFavouritesSuccess(id, response.data));
+      dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => {
       dispatch(fetchFavouritesFail(id, error));
     });
@@ -323,17 +330,62 @@ export function fetchFavouritesRequest(id) {
   };
 }
 
-export function fetchFavouritesSuccess(id, accounts) {
+export function fetchFavouritesSuccess(id, accounts, next) {
   return {
     type: FAVOURITES_FETCH_SUCCESS,
     id,
     accounts,
+    next,
   };
 }
 
 export function fetchFavouritesFail(id, error) {
   return {
     type: FAVOURITES_FETCH_FAIL,
+    id,
+    error,
+  };
+}
+
+export function expandFavourites(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'favourited_by', id, 'next']);
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandFavouritesRequest(id));
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => dispatch(expandFavouritesFail(id, error)));
+  };
+}
+
+export function expandFavouritesRequest(id) {
+  return {
+    type: FAVOURITES_EXPAND_REQUEST,
+    id,
+  };
+}
+
+export function expandFavouritesSuccess(id, accounts, next) {
+  return {
+    type: FAVOURITES_EXPAND_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function expandFavouritesFail(id, error) {
+  return {
+    type: FAVOURITES_EXPAND_FAIL,
+    id,
     error,
   };
 }
diff --git a/app/javascript/mastodon/features/favourites/index.jsx b/app/javascript/mastodon/features/favourites/index.jsx
index bfde78708e..b8ba948728 100644
--- a/app/javascript/mastodon/features/favourites/index.jsx
+++ b/app/javascript/mastodon/features/favourites/index.jsx
@@ -8,7 +8,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
 
-import { fetchFavourites } from 'mastodon/actions/interactions';
+import { debounce } from 'lodash';
+
+import { fetchFavourites, expandFavourites } from 'mastodon/actions/interactions';
 import ColumnHeader from 'mastodon/components/column_header';
 import { Icon }  from 'mastodon/components/icon';
 import { LoadingIndicator } from 'mastodon/components/loading_indicator';
@@ -21,7 +23,9 @@ const messages = defineMessages({
 });
 
 const mapStateToProps = (state, props) => ({
-  accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
+  accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'items']),
+  hasMore: !!state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'next']),
+  isLoading: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'isLoading'], true),
 });
 
 class Favourites extends ImmutablePureComponent {
@@ -30,6 +34,8 @@ class Favourites extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     accountIds: ImmutablePropTypes.list,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
     multiColumn: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
@@ -40,18 +46,16 @@ class Favourites extends ImmutablePureComponent {
     }
   }
 
-  UNSAFE_componentWillReceiveProps (nextProps) {
-    if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
-      this.props.dispatch(fetchFavourites(nextProps.params.statusId));
-    }
-  }
-
   handleRefresh = () => {
     this.props.dispatch(fetchFavourites(this.props.params.statusId));
   };
 
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandFavourites(this.props.params.statusId));
+  }, 300, { leading: true });
+
   render () {
-    const { intl, accountIds, multiColumn } = this.props;
+    const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -75,6 +79,9 @@ class Favourites extends ImmutablePureComponent {
 
         <ScrollableList
           scrollKey='favourites'
+          onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
+          isLoading={isLoading}
           emptyMessage={emptyMessage}
           bindToDocument={!multiColumn}
         >
diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js
index e33d365c9c..cc9a8b19a5 100644
--- a/app/javascript/mastodon/reducers/user_lists.js
+++ b/app/javascript/mastodon/reducers/user_lists.js
@@ -46,7 +46,12 @@ import {
 } from '../actions/blocks';
 import {
   REBLOGS_FETCH_SUCCESS,
+  FAVOURITES_FETCH_REQUEST,
   FAVOURITES_FETCH_SUCCESS,
+  FAVOURITES_FETCH_FAIL,
+  FAVOURITES_EXPAND_REQUEST,
+  FAVOURITES_EXPAND_SUCCESS,
+  FAVOURITES_EXPAND_FAIL,
 } from '../actions/interactions';
 import {
   MUTES_FETCH_REQUEST,
@@ -136,7 +141,15 @@ export default function userLists(state = initialState, action) {
   case REBLOGS_FETCH_SUCCESS:
     return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
   case FAVOURITES_FETCH_SUCCESS:
-    return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
+    return normalizeList(state, ['favourited_by', action.id], action.accounts, action.next);
+  case FAVOURITES_EXPAND_SUCCESS:
+    return appendToList(state, ['favourited_by', action.id], action.accounts, action.next);
+  case FAVOURITES_FETCH_REQUEST:
+  case FAVOURITES_EXPAND_REQUEST:
+    return state.setIn(['favourited_by', action.id, 'isLoading'], true);
+  case FAVOURITES_FETCH_FAIL:
+  case FAVOURITES_EXPAND_FAIL:
+    return state.setIn(['favourited_by', action.id, 'isLoading'], false);
   case NOTIFICATIONS_UPDATE:
     return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
   case FOLLOW_REQUESTS_FETCH_SUCCESS:

From c0605747adf028c7f5c7cc8aeca01f8285aa6802 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Tue, 29 Aug 2023 14:06:22 +0200
Subject: [PATCH 38/81] Fix N+1 in `tootctl search deploy` (#26710)

---
 .../public_statuses_index_importer.rb         | 23 ++++++-------------
 app/lib/importer/statuses_index_importer.rb   |  2 +-
 2 files changed, 8 insertions(+), 17 deletions(-)

diff --git a/app/lib/importer/public_statuses_index_importer.rb b/app/lib/importer/public_statuses_index_importer.rb
index 8e36e36f90..72d02318b1 100644
--- a/app/lib/importer/public_statuses_index_importer.rb
+++ b/app/lib/importer/public_statuses_index_importer.rb
@@ -2,23 +2,14 @@
 
 class Importer::PublicStatusesIndexImporter < Importer::BaseImporter
   def import!
-    indexable_statuses_scope.find_in_batches(batch_size: @batch_size) do |batch|
-      in_work_unit(batch.map(&:status_id)) do |status_ids|
+    scope.select(:id).find_in_batches(batch_size: @batch_size) do |batch|
+      in_work_unit(batch.pluck(:id)) do |status_ids|
         bulk = ActiveRecord::Base.connection_pool.with_connection do
-          Chewy::Index::Import::BulkBuilder.new(index, to_index: Status.includes(:media_attachments, :preloadable_poll).where(id: status_ids)).bulk_body
+          Chewy::Index::Import::BulkBuilder.new(index, to_index: Status.includes(:media_attachments, :preloadable_poll, :preview_cards).where(id: status_ids)).bulk_body
         end
 
-        indexed = 0
-        deleted = 0
-
-        bulk.map! do |entry|
-          if entry[:index]
-            indexed += 1
-          else
-            deleted += 1
-          end
-          entry
-        end
+        indexed = bulk.count { |entry| entry[:index] }
+        deleted = bulk.count { |entry| entry[:delete] }
 
         Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 
@@ -35,7 +26,7 @@ class Importer::PublicStatusesIndexImporter < Importer::BaseImporter
     PublicStatusesIndex
   end
 
-  def indexable_statuses_scope
-    Status.indexable.select('"statuses"."id", COALESCE("statuses"."reblog_of_id", "statuses"."id") AS status_id')
+  def scope
+    Status.indexable
   end
 end
diff --git a/app/lib/importer/statuses_index_importer.rb b/app/lib/importer/statuses_index_importer.rb
index b0721c2e02..0277cd0ef5 100644
--- a/app/lib/importer/statuses_index_importer.rb
+++ b/app/lib/importer/statuses_index_importer.rb
@@ -14,7 +14,7 @@ class Importer::StatusesIndexImporter < Importer::BaseImporter
       scope.find_in_batches(batch_size: @batch_size) do |tmp|
         in_work_unit(tmp.map(&:status_id)) do |status_ids|
           bulk = ActiveRecord::Base.connection_pool.with_connection do
-            Chewy::Index::Import::BulkBuilder.new(index, to_index: Status.includes(:media_attachments, :preloadable_poll).where(id: status_ids)).bulk_body
+            Chewy::Index::Import::BulkBuilder.new(index, to_index: Status.includes(:media_attachments, :preloadable_poll, :preview_cards).where(id: status_ids)).bulk_body
           end
 
           indexed = 0

From 74eb7dbf2d79b74f7d6f09ca3d39b3ba67f5f7bf Mon Sep 17 00:00:00 2001
From: Tim Rogers <rogers.timothy.john@gmail.com>
Date: Tue, 29 Aug 2023 07:42:20 -0500
Subject: [PATCH 39/81] Fix bug with reblogged view on Toots only showing
 latest reblogging accounts (#26574)

---
 .../mastodon/actions/interactions.js          | 55 ++++++++++++++++++-
 .../mastodon/features/reblogs/index.jsx       | 27 +++++----
 .../mastodon/reducers/user_lists.js           | 15 ++++-
 3 files changed, 84 insertions(+), 13 deletions(-)

diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index 1ffd23db53..7d0144438a 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -7,6 +7,10 @@ export const REBLOG_REQUEST = 'REBLOG_REQUEST';
 export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
 export const REBLOG_FAIL    = 'REBLOG_FAIL';
 
+export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
+export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
+export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
+
 export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
 export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
 export const FAVOURITE_FAIL    = 'FAVOURITE_FAIL';
@@ -278,8 +282,10 @@ export function fetchReblogs(id) {
     dispatch(fetchReblogsRequest(id));
 
     api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
       dispatch(importFetchedAccounts(response.data));
-      dispatch(fetchReblogsSuccess(id, response.data));
+      dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => {
       dispatch(fetchReblogsFail(id, error));
     });
@@ -293,17 +299,62 @@ export function fetchReblogsRequest(id) {
   };
 }
 
-export function fetchReblogsSuccess(id, accounts) {
+export function fetchReblogsSuccess(id, accounts, next) {
   return {
     type: REBLOGS_FETCH_SUCCESS,
     id,
     accounts,
+    next,
   };
 }
 
 export function fetchReblogsFail(id, error) {
   return {
     type: REBLOGS_FETCH_FAIL,
+    id,
+    error,
+  };
+}
+
+export function expandReblogs(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'reblogged_by', id, 'next']);
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandReblogsRequest(id));
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => dispatch(expandReblogsFail(id, error)));
+  };
+}
+
+export function expandReblogsRequest(id) {
+  return {
+    type: REBLOGS_EXPAND_REQUEST,
+    id,
+  };
+}
+
+export function expandReblogsSuccess(id, accounts, next) {
+  return {
+    type: REBLOGS_EXPAND_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function expandReblogsFail(id, error) {
+  return {
+    type: REBLOGS_EXPAND_FAIL,
+    id,
     error,
   };
 }
diff --git a/app/javascript/mastodon/features/reblogs/index.jsx b/app/javascript/mastodon/features/reblogs/index.jsx
index 8bcef863f2..0c4e6dbb93 100644
--- a/app/javascript/mastodon/features/reblogs/index.jsx
+++ b/app/javascript/mastodon/features/reblogs/index.jsx
@@ -8,9 +8,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
 
+import { debounce } from 'lodash';
+
 import { Icon }  from 'mastodon/components/icon';
 
-import { fetchReblogs } from '../../actions/interactions';
+import { fetchReblogs, expandReblogs } from '../../actions/interactions';
 import ColumnHeader from '../../components/column_header';
 import { LoadingIndicator } from '../../components/loading_indicator';
 import ScrollableList from '../../components/scrollable_list';
@@ -22,7 +24,9 @@ const messages = defineMessages({
 });
 
 const mapStateToProps = (state, props) => ({
-  accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
+  accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'items']),
+  hasMore: !!state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'next']),
+  isLoading: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'isLoading'], true),
 });
 
 class Reblogs extends ImmutablePureComponent {
@@ -31,6 +35,8 @@ class Reblogs extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     accountIds: ImmutablePropTypes.list,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
     multiColumn: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
@@ -39,20 +45,18 @@ class Reblogs extends ImmutablePureComponent {
     if (!this.props.accountIds) {
       this.props.dispatch(fetchReblogs(this.props.params.statusId));
     }
-  }
-
-  UNSAFE_componentWillReceiveProps(nextProps) {
-    if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
-      this.props.dispatch(fetchReblogs(nextProps.params.statusId));
-    }
-  }
+  };
 
   handleRefresh = () => {
     this.props.dispatch(fetchReblogs(this.props.params.statusId));
   };
 
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandReblogs(this.props.params.statusId));
+  }, 300, { leading: true });
+
   render () {
-    const { intl, accountIds, multiColumn } = this.props;
+    const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -76,6 +80,9 @@ class Reblogs extends ImmutablePureComponent {
 
         <ScrollableList
           scrollKey='reblogs'
+          onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
+          isLoading={isLoading}
           emptyMessage={emptyMessage}
           bindToDocument={!multiColumn}
         >
diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js
index cc9a8b19a5..089899398e 100644
--- a/app/javascript/mastodon/reducers/user_lists.js
+++ b/app/javascript/mastodon/reducers/user_lists.js
@@ -45,7 +45,12 @@ import {
   BLOCKS_EXPAND_FAIL,
 } from '../actions/blocks';
 import {
+  REBLOGS_FETCH_REQUEST,
   REBLOGS_FETCH_SUCCESS,
+  REBLOGS_FETCH_FAIL,
+  REBLOGS_EXPAND_REQUEST,
+  REBLOGS_EXPAND_SUCCESS,
+  REBLOGS_EXPAND_FAIL,
   FAVOURITES_FETCH_REQUEST,
   FAVOURITES_FETCH_SUCCESS,
   FAVOURITES_FETCH_FAIL,
@@ -139,7 +144,15 @@ export default function userLists(state = initialState, action) {
   case FOLLOWING_EXPAND_FAIL:
     return state.setIn(['following', action.id, 'isLoading'], false);
   case REBLOGS_FETCH_SUCCESS:
-    return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
+    return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next);
+  case REBLOGS_EXPAND_SUCCESS:
+    return appendToList(state, ['reblogged_by', action.id], action.accounts, action.next);
+  case REBLOGS_FETCH_REQUEST:
+  case REBLOGS_EXPAND_REQUEST:
+    return state.setIn(['reblogged_by', action.id, 'isLoading'], true);
+  case REBLOGS_FETCH_FAIL:
+  case REBLOGS_EXPAND_FAIL:
+    return state.setIn(['reblogged_by', action.id, 'isLoading'], false);
   case FAVOURITES_FETCH_SUCCESS:
     return normalizeList(state, ['favourited_by', action.id], action.accounts, action.next);
   case FAVOURITES_EXPAND_SUCCESS:

From 9e77ab7db245a9a4725600cf69a617c0be1f1018 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Tue, 29 Aug 2023 17:51:13 +0200
Subject: [PATCH 40/81] Change private statuses index to index without crutches
 (#26713)

---
 app/chewy/statuses_index.rb                  | 31 ++------------------
 app/lib/importer/statuses_index_importer.rb  |  2 +-
 app/models/concerns/status_search_concern.rb | 28 +++++++-----------
 app/models/poll.rb                           |  1 +
 app/models/status.rb                         |  6 ++++
 5 files changed, 21 insertions(+), 47 deletions(-)

diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb
index 6d33521051..2be7e45250 100644
--- a/app/chewy/statuses_index.rb
+++ b/app/chewy/statuses_index.rb
@@ -40,40 +40,13 @@ class StatusesIndex < Chewy::Index
     },
   }
 
-  # We do not use delete_if option here because it would call a method that we
-  # expect to be called with crutches without crutches, causing n+1 queries
-  index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll, :preview_cards)
-
-  crutch :mentions do |collection|
-    data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
-    data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
-  end
-
-  crutch :favourites do |collection|
-    data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
-    data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
-  end
-
-  crutch :reblogs do |collection|
-    data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
-    data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
-  end
-
-  crutch :bookmarks do |collection|
-    data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
-    data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
-  end
-
-  crutch :votes do |collection|
-    data = ::PollVote.joins(:poll).where(poll: { status_id: collection.map(&:id) }).where(account: Account.local).pluck(:status_id, :account_id)
-    data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
-  end
+  index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preview_cards, :local_mentioned, :local_favorited, :local_reblogged, :local_bookmarked, preloadable_poll: :local_voters), delete_if: ->(status) { status.searchable_by.empty? }
 
   root date_detection: false do
     field(:id, type: 'long')
     field(:account_id, type: 'long')
     field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
-    field(:searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) })
+    field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
     field(:language, type: 'keyword')
     field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
     field(:created_at, type: 'date')
diff --git a/app/lib/importer/statuses_index_importer.rb b/app/lib/importer/statuses_index_importer.rb
index 0277cd0ef5..285ddc871a 100644
--- a/app/lib/importer/statuses_index_importer.rb
+++ b/app/lib/importer/statuses_index_importer.rb
@@ -14,7 +14,7 @@ class Importer::StatusesIndexImporter < Importer::BaseImporter
       scope.find_in_batches(batch_size: @batch_size) do |tmp|
         in_work_unit(tmp.map(&:status_id)) do |status_ids|
           bulk = ActiveRecord::Base.connection_pool.with_connection do
-            Chewy::Index::Import::BulkBuilder.new(index, to_index: Status.includes(:media_attachments, :preloadable_poll, :preview_cards).where(id: status_ids)).bulk_body
+            Chewy::Index::Import::BulkBuilder.new(index, to_index: index.adapter.default_scope.where(id: status_ids)).bulk_body
           end
 
           indexed = 0
diff --git a/app/models/concerns/status_search_concern.rb b/app/models/concerns/status_search_concern.rb
index 21048b5682..3ef45754ab 100644
--- a/app/models/concerns/status_search_concern.rb
+++ b/app/models/concerns/status_search_concern.rb
@@ -7,26 +7,20 @@ module StatusSearchConcern
     scope :indexable, -> { without_reblogs.where(visibility: :public).joins(:account).where(account: { indexable: true }) }
   end
 
-  def searchable_by(preloaded = nil)
-    ids = []
+  def searchable_by
+    @searchable_by ||= begin
+      ids = []
 
-    ids << account_id if local?
+      ids << account_id if local?
 
-    if preloaded.nil?
-      ids += mentions.joins(:account).merge(Account.local).active.pluck(:account_id)
-      ids += favourites.joins(:account).merge(Account.local).pluck(:account_id)
-      ids += reblogs.joins(:account).merge(Account.local).pluck(:account_id)
-      ids += bookmarks.joins(:account).merge(Account.local).pluck(:account_id)
-      ids += poll.votes.joins(:account).merge(Account.local).pluck(:account_id) if poll.present?
-    else
-      ids += preloaded.mentions[id] || []
-      ids += preloaded.favourites[id] || []
-      ids += preloaded.reblogs[id] || []
-      ids += preloaded.bookmarks[id] || []
-      ids += preloaded.votes[id] || []
+      ids += local_mentioned.pluck(:id)
+      ids += local_favorited.pluck(:id)
+      ids += local_reblogged.pluck(:id)
+      ids += local_bookmarked.pluck(:id)
+      ids += preloadable_poll.local_voters.pluck(:id) if preloadable_poll.present?
+
+      ids.uniq
     end
-
-    ids.uniq
   end
 
   def searchable_text
diff --git a/app/models/poll.rb b/app/models/poll.rb
index 74a77978b9..efa625eb5b 100644
--- a/app/models/poll.rb
+++ b/app/models/poll.rb
@@ -28,6 +28,7 @@ class Poll < ApplicationRecord
 
   has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :delete_all
   has_many :voters, -> { group('accounts.id') }, through: :votes, class_name: 'Account', source: :account
+  has_many :local_voters, -> { group('accounts.id').merge(Account.local) }, through: :votes, class_name: 'Account', source: :account
 
   has_many :notifications, as: :activity, dependent: :destroy
 
diff --git a/app/models/status.rb b/app/models/status.rb
index 760b8ec33e..1c41ef1d52 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -72,6 +72,12 @@ class Status < ApplicationRecord
   has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
   has_many :media_attachments, dependent: :nullify
 
+  # Those associations are used for the private search index
+  has_many :local_mentioned, -> { merge(Account.local) }, through: :active_mentions, source: :account
+  has_many :local_favorited, -> { merge(Account.local) }, through: :favourites, source: :account
+  has_many :local_reblogged, -> { merge(Account.local) }, through: :reblogs, source: :account
+  has_many :local_bookmarked, -> { merge(Account.local) }, through: :bookmarks, source: :account
+
   has_and_belongs_to_many :tags
   has_and_belongs_to_many :preview_cards
 

From 168688ef1c538c295fe08516825c845f08e2b7d5 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 30 Aug 2023 09:34:07 +0200
Subject: [PATCH 41/81] Update dependency webmock to v3.19.1 (#26722)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index f26856bf94..7e5dac0318 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -109,7 +109,7 @@ GEM
       i18n (>= 1.6, < 2)
       minitest (>= 5.1)
       tzinfo (~> 2.0)
-    addressable (2.8.4)
+    addressable (2.8.5)
       public_suffix (>= 2.0.2, < 6.0)
     aes_key_wrap (1.1.0)
     airbrussh (1.4.1)
@@ -795,7 +795,7 @@ GEM
     webfinger (1.2.0)
       activesupport
       httpclient (>= 2.4)
-    webmock (3.18.1)
+    webmock (3.19.1)
       addressable (>= 2.8.0)
       crack (>= 0.3.2)
       hashdiff (>= 0.4.0, < 2.0.0)

From 24deaf2e4a6bd7a3d5fb50fa8ed5102286fa8988 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 30 Aug 2023 09:44:14 +0200
Subject: [PATCH 42/81] Update dependency postcss to v8.4.29 (#26720)

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 0f34ad3551..0a7c840dd5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9625,9 +9625,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
   integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
 
 postcss@^8.2.15, postcss@^8.4.24, postcss@^8.4.25:
-  version "8.4.28"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.28.tgz#c6cc681ed00109072816e1557f889ef51cf950a5"
-  integrity sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==
+  version "8.4.29"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.29.tgz#33bc121cf3b3688d4ddef50be869b2a54185a1dd"
+  integrity sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==
   dependencies:
     nanoid "^3.3.6"
     picocolors "^1.0.0"

From 0e1bff178ecaa4434191335d81d2a8809f31e550 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 30 Aug 2023 09:45:13 +0200
Subject: [PATCH 43/81] Update dependency chewy to v7.3.4 (#26717)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index 7e5dac0318..f1a61c5e09 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -203,7 +203,7 @@ GEM
       activesupport
     cbor (0.5.9.6)
     charlock_holmes (0.7.7)
-    chewy (7.3.3)
+    chewy (7.3.4)
       activesupport (>= 5.2)
       elasticsearch (>= 7.12.0, < 7.14.0)
       elasticsearch-dsl

From bba76e726714e49dae8ea6dd4b2439ae1d7699a8 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Wed, 30 Aug 2023 16:07:26 +0200
Subject: [PATCH 44/81] Change `language:` to attempt to match to a known value
 in search (#26663)

---
 app/lib/search_query_transformer.rb | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index f1fc687e47..86e3f50005 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -98,7 +98,7 @@ class SearchQueryTransformer < Parslet::Transform
       when 'language'
         @filter = :language
         @type = :term
-        @term = term
+        @term = language_code_from_term(term)
       when 'from'
         @filter = :account_id
         @type = :term
@@ -137,6 +137,22 @@ class SearchQueryTransformer < Parslet::Transform
       # an ID that does not exist
       account&.id || -1
     end
+
+    def language_code_from_term(term)
+      language_code = term
+
+      return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
+
+      language_code = term.downcase
+
+      return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
+
+      language_code = term.split(/[_-]/).first.downcase
+
+      return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
+
+      term
+    end
   end
 
   rule(clause: subtree(:clause)) do

From 21ec596dabbe28a5cfc5b29362f46f47b48eb140 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 30 Aug 2023 16:29:52 +0200
Subject: [PATCH 45/81] Fix sign up steps progress layout in right-to-left
 locales (#26728)

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

diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index beb45ab6e9..a7079c1457 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -1185,14 +1185,14 @@ code {
   }
 
   li:first-child .label {
-    left: auto;
     inset-inline-start: 0;
+    inset-inline-end: auto;
     text-align: start;
     transform: none;
   }
 
   li:last-child .label {
-    left: auto;
+    inset-inline-start: auto;
     inset-inline-end: 0;
     text-align: end;
     transform: none;

From 5c38c3a9a1e6bb07f33f5fee598b51cb1fd695ba Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 30 Aug 2023 17:36:16 +0200
Subject: [PATCH 46/81] Change text extraction in `PlainTextFormatter` to be
 faster (#26727)

---
 app/lib/plain_text_formatter.rb | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/app/lib/plain_text_formatter.rb b/app/lib/plain_text_formatter.rb
index 8eac730bec..d1ff6808b2 100644
--- a/app/lib/plain_text_formatter.rb
+++ b/app/lib/plain_text_formatter.rb
@@ -1,8 +1,6 @@
 # frozen_string_literal: true
 
 class PlainTextFormatter
-  include ActionView::Helpers::TextHelper
-
   NEWLINE_TAGS_RE = %r{(<br />|<br>|</p>)+}
 
   attr_reader :text, :local
@@ -18,7 +16,10 @@ class PlainTextFormatter
     if local?
       text
     else
-      html_entities.decode(strip_tags(insert_newlines)).chomp
+      node = Nokogiri::HTML.fragment(insert_newlines)
+      # Elements that are entirely removed with our Sanitize config
+      node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove
+      node.text.chomp
     end
   end
 
@@ -27,8 +28,4 @@ class PlainTextFormatter
   def insert_newlines
     text.gsub(NEWLINE_TAGS_RE) { |match| "#{match}\n" }
   end
-
-  def html_entities
-    HTMLEntities.new
-  end
 end

From 15949e42c292344c63f828bed24d6af045a686e6 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 31 Aug 2023 09:49:34 +0200
Subject: [PATCH 47/81] Update dependency glob to v10.3.4 (#26734)

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

diff --git a/yarn.lock b/yarn.lock
index 0a7c840dd5..ec5566be39 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6231,9 +6231,9 @@ glob-parent@^6.0.2:
     is-glob "^4.0.3"
 
 glob@^10.2.5, glob@^10.2.6:
-  version "10.3.3"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.3.tgz#8360a4ffdd6ed90df84aa8d52f21f452e86a123b"
-  integrity sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==
+  version "10.3.4"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.4.tgz#c85c9c7ab98669102b6defda76d35c5b1ef9766f"
+  integrity sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==
   dependencies:
     foreground-child "^3.1.0"
     jackspeak "^2.0.3"
@@ -7336,9 +7336,9 @@ iterator.prototype@^1.1.0:
     reflect.getprototypeof "^1.0.3"
 
 jackspeak@^2.0.3:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.1.tgz#655e8cf025d872c9c03d3eb63e8f0c024fef16a6"
-  integrity sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.1.tgz#ce2effa4c458e053640e61938865a5b5fae98456"
+  integrity sha512-4iSY3Bh1Htv+kLhiiZunUhQ+OYXIn0ze3ulq8JeWrFKmhPAJSySV2+kdtRh2pGcCeF0s6oR8Oc+pYZynJj4t8A==
   dependencies:
     "@isaacs/cliui" "^8.0.2"
   optionalDependencies:
@@ -8192,9 +8192,9 @@ lru-cache@^6.0.0:
     yallist "^4.0.0"
 
 "lru-cache@^9.1.1 || ^10.0.0":
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.0.tgz#b9e2a6a72a129d81ab317202d93c7691df727e61"
-  integrity sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a"
+  integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==
 
 lz-string@^1.5.0:
   version "1.5.0"
@@ -8526,9 +8526,9 @@ minipass@^5.0.0:
   integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
 
 "minipass@^5.0.0 || ^6.0.2 || ^7.0.0":
-  version "7.0.2"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.2.tgz#58a82b7d81c7010da5bd4b2c0c85ac4b4ec5131e"
-  integrity sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.3.tgz#05ea638da44e475037ed94d1c7efcc76a25e1974"
+  integrity sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==
 
 minizlib@^2.1.1:
   version "2.1.2"
@@ -11345,7 +11345,6 @@ stringz@^2.1.0:
     char-regex "^1.0.2"
 
 "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
-  name strip-ansi-cjs
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==

From ef9a85a2d82908478cf7cd7fbc5ea6311200264e Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 31 Aug 2023 09:52:23 +0200
Subject: [PATCH 48/81] Update dependency webpack-bundle-analyzer to v4.9.1
 (#26733)

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

diff --git a/yarn.lock b/yarn.lock
index ec5566be39..89b11e8e2b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2913,16 +2913,16 @@ acorn@^6.4.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
   integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
 
-acorn@^8.0.4, acorn@^8.8.2:
-  version "8.8.2"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
-  integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
-
-acorn@^8.1.0, acorn@^8.8.1, acorn@^8.9.0:
+acorn@^8.0.4, acorn@^8.1.0, acorn@^8.8.1, acorn@^8.9.0:
   version "8.10.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
   integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
 
+acorn@^8.8.2:
+  version "8.8.2"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
+  integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
+
 agent-base@6:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -8100,6 +8100,16 @@ lodash.debounce@^4.0.8:
   resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
   integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
 
+lodash.escape@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+  integrity sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==
+
+lodash.flatten@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+  integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==
+
 lodash.get@^4.0:
   version "4.4.2"
   resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -8110,6 +8120,11 @@ lodash.has@^4.0:
   resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862"
   integrity sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g==
 
+lodash.invokemap@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.invokemap/-/lodash.invokemap-4.6.0.tgz#1748cda5d8b0ef8369c4eb3ec54c21feba1f2d62"
+  integrity sha512-CfkycNtMqgUlfjfdh2BhKO/ZXrP8ePOX5lEU/g0R3ItJcnuxWDwokMGKx1hWcfOikmyOVx6X9IwWnDGlgKl61w==
+
 lodash.isboolean@^3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
@@ -8135,6 +8150,11 @@ lodash.merge@^4.6.2:
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
+lodash.pullall@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.pullall/-/lodash.pullall-4.2.0.tgz#9d98b8518b7c965b0fae4099bd9fb7df8bbf38ba"
+  integrity sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg==
+
 lodash.sortby@^4.7.0:
   version "4.7.0"
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@@ -8150,6 +8170,11 @@ lodash.uniq@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
 
+lodash.uniqby@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302"
+  integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==
+
 lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -10905,14 +10930,14 @@ signal-exit@^4.0.1:
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
   integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
 
-sirv@^1.0.7:
-  version "1.0.19"
-  resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49"
-  integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==
+sirv@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.3.tgz#ca5868b87205a74bef62a469ed0296abceccd446"
+  integrity sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==
   dependencies:
     "@polka/url" "^1.0.0-next.20"
     mrmime "^1.0.0"
-    totalist "^1.0.0"
+    totalist "^3.0.0"
 
 sisteransi@^1.0.5:
   version "1.0.5"
@@ -11831,10 +11856,10 @@ toidentifier@1.0.1:
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 
-totalist@^1.0.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
-  integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
+totalist@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8"
+  integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
 
 tough-cookie@^4.1.2:
   version "4.1.3"
@@ -12387,19 +12412,26 @@ webpack-assets-manifest@^4.0.6:
     webpack-sources "^1.0"
 
 webpack-bundle-analyzer@^4.8.0:
-  version "4.9.0"
-  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.0.tgz#fc093c4ab174fd3dcbd1c30b763f56d10141209d"
-  integrity sha512-+bXGmO1LyiNx0i9enBu3H8mv42sj/BJWhZNFwjz92tVnBa9J3JMGo2an2IXlEleoDOPn/Hofl5hr/xCpObUDtw==
+  version "4.9.1"
+  resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.1.tgz#d00bbf3f17500c10985084f22f1a2bf45cb2f09d"
+  integrity sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w==
   dependencies:
     "@discoveryjs/json-ext" "0.5.7"
     acorn "^8.0.4"
     acorn-walk "^8.0.0"
-    chalk "^4.1.0"
     commander "^7.2.0"
+    escape-string-regexp "^4.0.0"
     gzip-size "^6.0.0"
-    lodash "^4.17.20"
+    is-plain-object "^5.0.0"
+    lodash.debounce "^4.0.8"
+    lodash.escape "^4.0.1"
+    lodash.flatten "^4.4.0"
+    lodash.invokemap "^4.6.0"
+    lodash.pullall "^4.2.0"
+    lodash.uniqby "^4.7.0"
     opener "^1.5.2"
-    sirv "^1.0.7"
+    picocolors "^1.0.0"
+    sirv "^2.0.3"
     ws "^7.3.1"
 
 webpack-cli@^3.3.12:

From 430eac3eb1021d30f1ca73ec21444a3a5cb0bab7 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 31 Aug 2023 10:23:24 +0200
Subject: [PATCH 49/81] Update dependency @babel/preset-env to v7.22.14
 (#26732)

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

diff --git a/yarn.lock b/yarn.lock
index 89b11e8e2b..880ded9773 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -29,7 +29,7 @@
     jsonpointer "^5.0.0"
     leven "^3.1.0"
 
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5":
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.10":
   version "7.22.10"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3"
   integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==
@@ -44,7 +44,15 @@
   dependencies:
     "@babel/highlight" "^7.22.5"
 
-"@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9":
+"@babel/code-frame@^7.22.5":
+  version "7.22.13"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
+  integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
+  dependencies:
+    "@babel/highlight" "^7.22.13"
+    chalk "^2.4.2"
+
+"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9":
   version "7.22.9"
   resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730"
   integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==
@@ -113,7 +121,7 @@
     lru-cache "^5.1.1"
     semver "^6.3.1"
 
-"@babel/helper-create-class-features-plugin@^7.22.11":
+"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5":
   version "7.22.11"
   resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.11.tgz#4078686740459eeb4af3494a273ac09148dfb213"
   integrity sha512-y1grdYL4WzmUDBRGK0pDbIoFd7UZKoDurDzWEoNMYoj1EL+foGRQNyPWDcC+YyegN5y1DUsFFmzjGijB3nSVAQ==
@@ -128,21 +136,6 @@
     "@babel/helper-split-export-declaration" "^7.22.6"
     semver "^6.3.1"
 
-"@babel/helper-create-class-features-plugin@^7.22.5":
-  version "7.22.10"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz#dd2612d59eac45588021ac3d6fa976d08f4e95a3"
-  integrity sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA==
-  dependencies:
-    "@babel/helper-annotate-as-pure" "^7.22.5"
-    "@babel/helper-environment-visitor" "^7.22.5"
-    "@babel/helper-function-name" "^7.22.5"
-    "@babel/helper-member-expression-to-functions" "^7.22.5"
-    "@babel/helper-optimise-call-expression" "^7.22.5"
-    "@babel/helper-replace-supers" "^7.22.9"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
-    "@babel/helper-split-export-declaration" "^7.22.6"
-    semver "^6.3.1"
-
 "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5":
   version "7.22.9"
   resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz#9d8e61a8d9366fe66198f57c40565663de0825f6"
@@ -292,7 +285,16 @@
     "@babel/traverse" "^7.22.11"
     "@babel/types" "^7.22.11"
 
-"@babel/highlight@^7.22.10", "@babel/highlight@^7.22.5":
+"@babel/highlight@^7.22.10", "@babel/highlight@^7.22.13":
+  version "7.22.13"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16"
+  integrity sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.22.5"
+    chalk "^2.4.2"
+    js-tokens "^4.0.0"
+
+"@babel/highlight@^7.22.5":
   version "7.22.10"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7"
   integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==
@@ -306,11 +308,16 @@
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55"
   integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==
 
-"@babel/parser@^7.22.11", "@babel/parser@^7.22.5":
+"@babel/parser@^7.22.11":
   version "7.22.11"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.11.tgz#becf8ee33aad2a35ed5607f521fe6e72a615f905"
   integrity sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==
 
+"@babel/parser@^7.22.5":
+  version "7.22.14"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.14.tgz#c7de58e8de106e88efca42ce17f0033209dfd245"
+  integrity sha512-1KucTHgOvaw/LzCVrEOAyXkr9rQlp0A1HiHRYnSUE9dmb8PvPW7o5sscg+5169r54n3vGlbx6GevTE/Iw/P3AQ==
+
 "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5":
   version "7.22.5"
   resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e"
@@ -487,10 +494,10 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
 
-"@babel/plugin-transform-async-generator-functions@^7.22.10":
-  version "7.22.10"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.10.tgz#45946cd17f915b10e65c29b8ed18a0a50fc648c8"
-  integrity sha512-eueE8lvKVzq5wIObKK/7dvoeKJ+xc6TvRn6aysIjS6pSCeLy7S/eVi7pEQknZqyqvzaNKdDtem8nUNTBgDVR2g==
+"@babel/plugin-transform-async-generator-functions@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.11.tgz#dbe3b1ff5a52e2e5edc4b19a60d325a675ed2649"
+  integrity sha512-0pAlmeRJn6wU84zzZsEOx1JV1Jf8fqO9ok7wofIJwUnplYo247dcd24P+cMJht7ts9xkzdtB0EPHmOb7F+KzXw==
   dependencies:
     "@babel/helper-environment-visitor" "^7.22.5"
     "@babel/helper-plugin-utils" "^7.22.5"
@@ -528,12 +535,12 @@
     "@babel/helper-create-class-features-plugin" "^7.22.5"
     "@babel/helper-plugin-utils" "^7.22.5"
 
-"@babel/plugin-transform-class-static-block@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz#3e40c46f048403472d6f4183116d5e46b1bff5ba"
-  integrity sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==
+"@babel/plugin-transform-class-static-block@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974"
+  integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==
   dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.22.5"
+    "@babel/helper-create-class-features-plugin" "^7.22.11"
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-class-static-block" "^7.14.5"
 
@@ -582,10 +589,10 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
 
-"@babel/plugin-transform-dynamic-import@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz#d6908a8916a810468c4edff73b5b75bda6ad393e"
-  integrity sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==
+"@babel/plugin-transform-dynamic-import@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa"
+  integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-dynamic-import" "^7.8.3"
@@ -598,10 +605,10 @@
     "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5"
     "@babel/helper-plugin-utils" "^7.22.5"
 
-"@babel/plugin-transform-export-namespace-from@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz#57c41cb1d0613d22f548fddd8b288eedb9973a5b"
-  integrity sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==
+"@babel/plugin-transform-export-namespace-from@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c"
+  integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
@@ -622,10 +629,10 @@
     "@babel/helper-function-name" "^7.22.5"
     "@babel/helper-plugin-utils" "^7.22.5"
 
-"@babel/plugin-transform-json-strings@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz#14b64352fdf7e1f737eed68de1a1468bd2a77ec0"
-  integrity sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==
+"@babel/plugin-transform-json-strings@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835"
+  integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-json-strings" "^7.8.3"
@@ -637,10 +644,10 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
 
-"@babel/plugin-transform-logical-assignment-operators@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz#66ae5f068fd5a9a5dc570df16f56c2a8462a9d6c"
-  integrity sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==
+"@babel/plugin-transform-logical-assignment-operators@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c"
+  integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
@@ -669,22 +676,13 @@
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/helper-simple-access" "^7.22.5"
 
-"@babel/plugin-transform-modules-commonjs@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz#7d9875908d19b8c0536085af7b053fd5bd651bfa"
-  integrity sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==
-  dependencies:
-    "@babel/helper-module-transforms" "^7.22.5"
-    "@babel/helper-plugin-utils" "^7.22.5"
-    "@babel/helper-simple-access" "^7.22.5"
-
-"@babel/plugin-transform-modules-systemjs@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz#18c31410b5e579a0092638f95c896c2a98a5d496"
-  integrity sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==
+"@babel/plugin-transform-modules-systemjs@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz#3386be5875d316493b517207e8f1931d93154bb1"
+  integrity sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==
   dependencies:
     "@babel/helper-hoist-variables" "^7.22.5"
-    "@babel/helper-module-transforms" "^7.22.5"
+    "@babel/helper-module-transforms" "^7.22.9"
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/helper-validator-identifier" "^7.22.5"
 
@@ -711,7 +709,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
 
-"@babel/plugin-transform-nullish-coalescing-operator@^7.22.3", "@babel/plugin-transform-nullish-coalescing-operator@^7.22.5":
+"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11", "@babel/plugin-transform-nullish-coalescing-operator@^7.22.3":
   version "7.22.11"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc"
   integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==
@@ -719,21 +717,21 @@
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
 
-"@babel/plugin-transform-numeric-separator@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz#57226a2ed9e512b9b446517ab6fa2d17abb83f58"
-  integrity sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==
+"@babel/plugin-transform-numeric-separator@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd"
+  integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-numeric-separator" "^7.10.4"
 
-"@babel/plugin-transform-object-rest-spread@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz#9686dc3447df4753b0b2a2fae7e8bc33cdc1f2e1"
-  integrity sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==
+"@babel/plugin-transform-object-rest-spread@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.11.tgz#dbbb06ce783cd994a8f430d8cefa553e9b42ca62"
+  integrity sha512-nX8cPFa6+UmbepISvlf5jhQyaC7ASs/7UxHmMkuJ/k5xSHvDPPaibMo+v3TXwU/Pjqhep/nFNpd3zn4YR59pnw==
   dependencies:
-    "@babel/compat-data" "^7.22.5"
-    "@babel/helper-compilation-targets" "^7.22.5"
+    "@babel/compat-data" "^7.22.9"
+    "@babel/helper-compilation-targets" "^7.22.10"
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
     "@babel/plugin-transform-parameters" "^7.22.5"
@@ -746,18 +744,18 @@
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/helper-replace-supers" "^7.22.5"
 
-"@babel/plugin-transform-optional-catch-binding@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz#842080be3076703be0eaf32ead6ac8174edee333"
-  integrity sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==
+"@babel/plugin-transform-optional-catch-binding@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0"
+  integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
 
-"@babel/plugin-transform-optional-chaining@^7.22.10", "@babel/plugin-transform-optional-chaining@^7.22.5":
-  version "7.22.10"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz#076d28a7e074392e840d4ae587d83445bac0372a"
-  integrity sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g==
+"@babel/plugin-transform-optional-chaining@^7.22.12", "@babel/plugin-transform-optional-chaining@^7.22.5":
+  version "7.22.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.12.tgz#d7ebf6a88cd2f4d307b0e000ab630acd8124b333"
+  integrity sha512-7XXCVqZtyFWqjDsYDY4T45w4mlx1rf7aOgkc/Ww76xkgBiOlmjPkx36PBLHa1k1rwWvVgYMPsbuVnIamx2ZQJw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5"
@@ -778,13 +776,13 @@
     "@babel/helper-create-class-features-plugin" "^7.22.5"
     "@babel/helper-plugin-utils" "^7.22.5"
 
-"@babel/plugin-transform-private-property-in-object@^7.22.5":
-  version "7.22.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz#07a77f28cbb251546a43d175a1dda4cf3ef83e32"
-  integrity sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==
+"@babel/plugin-transform-private-property-in-object@^7.22.11":
+  version "7.22.11"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1"
+  integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==
   dependencies:
     "@babel/helper-annotate-as-pure" "^7.22.5"
-    "@babel/helper-create-class-features-plugin" "^7.22.5"
+    "@babel/helper-create-class-features-plugin" "^7.22.11"
     "@babel/helper-plugin-utils" "^7.22.5"
     "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
 
@@ -948,9 +946,9 @@
     "@babel/helper-plugin-utils" "^7.22.5"
 
 "@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.22.4":
-  version "7.22.10"
-  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.10.tgz#3263b9fe2c8823d191d28e61eac60a79f9ce8a0f"
-  integrity sha512-riHpLb1drNkpLlocmSyEg4oYJIQFeXAK/d7rI6mbD0XsvoTOOweXDmQPG/ErxsEhWk3rl3Q/3F6RFQlVFS8m0A==
+  version "7.22.14"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.14.tgz#1cbb468d899f64fa71c53446f13b7ff8c0005cc1"
+  integrity sha512-daodMIoVo+ol/g+//c/AH+szBkFj4STQUikvBijRGL72Ph+w+AMTSh55DUETe8KJlPlDT1k/mp7NBfOuiWmoig==
   dependencies:
     "@babel/compat-data" "^7.22.9"
     "@babel/helper-compilation-targets" "^7.22.10"
@@ -978,41 +976,41 @@
     "@babel/plugin-syntax-top-level-await" "^7.14.5"
     "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6"
     "@babel/plugin-transform-arrow-functions" "^7.22.5"
-    "@babel/plugin-transform-async-generator-functions" "^7.22.10"
+    "@babel/plugin-transform-async-generator-functions" "^7.22.11"
     "@babel/plugin-transform-async-to-generator" "^7.22.5"
     "@babel/plugin-transform-block-scoped-functions" "^7.22.5"
     "@babel/plugin-transform-block-scoping" "^7.22.10"
     "@babel/plugin-transform-class-properties" "^7.22.5"
-    "@babel/plugin-transform-class-static-block" "^7.22.5"
+    "@babel/plugin-transform-class-static-block" "^7.22.11"
     "@babel/plugin-transform-classes" "^7.22.6"
     "@babel/plugin-transform-computed-properties" "^7.22.5"
     "@babel/plugin-transform-destructuring" "^7.22.10"
     "@babel/plugin-transform-dotall-regex" "^7.22.5"
     "@babel/plugin-transform-duplicate-keys" "^7.22.5"
-    "@babel/plugin-transform-dynamic-import" "^7.22.5"
+    "@babel/plugin-transform-dynamic-import" "^7.22.11"
     "@babel/plugin-transform-exponentiation-operator" "^7.22.5"
-    "@babel/plugin-transform-export-namespace-from" "^7.22.5"
+    "@babel/plugin-transform-export-namespace-from" "^7.22.11"
     "@babel/plugin-transform-for-of" "^7.22.5"
     "@babel/plugin-transform-function-name" "^7.22.5"
-    "@babel/plugin-transform-json-strings" "^7.22.5"
+    "@babel/plugin-transform-json-strings" "^7.22.11"
     "@babel/plugin-transform-literals" "^7.22.5"
-    "@babel/plugin-transform-logical-assignment-operators" "^7.22.5"
+    "@babel/plugin-transform-logical-assignment-operators" "^7.22.11"
     "@babel/plugin-transform-member-expression-literals" "^7.22.5"
     "@babel/plugin-transform-modules-amd" "^7.22.5"
-    "@babel/plugin-transform-modules-commonjs" "^7.22.5"
-    "@babel/plugin-transform-modules-systemjs" "^7.22.5"
+    "@babel/plugin-transform-modules-commonjs" "^7.22.11"
+    "@babel/plugin-transform-modules-systemjs" "^7.22.11"
     "@babel/plugin-transform-modules-umd" "^7.22.5"
     "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5"
     "@babel/plugin-transform-new-target" "^7.22.5"
-    "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.5"
-    "@babel/plugin-transform-numeric-separator" "^7.22.5"
-    "@babel/plugin-transform-object-rest-spread" "^7.22.5"
+    "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11"
+    "@babel/plugin-transform-numeric-separator" "^7.22.11"
+    "@babel/plugin-transform-object-rest-spread" "^7.22.11"
     "@babel/plugin-transform-object-super" "^7.22.5"
-    "@babel/plugin-transform-optional-catch-binding" "^7.22.5"
-    "@babel/plugin-transform-optional-chaining" "^7.22.10"
+    "@babel/plugin-transform-optional-catch-binding" "^7.22.11"
+    "@babel/plugin-transform-optional-chaining" "^7.22.12"
     "@babel/plugin-transform-parameters" "^7.22.5"
     "@babel/plugin-transform-private-methods" "^7.22.5"
-    "@babel/plugin-transform-private-property-in-object" "^7.22.5"
+    "@babel/plugin-transform-private-property-in-object" "^7.22.11"
     "@babel/plugin-transform-property-literals" "^7.22.5"
     "@babel/plugin-transform-regenerator" "^7.22.10"
     "@babel/plugin-transform-reserved-words" "^7.22.5"
@@ -1026,7 +1024,7 @@
     "@babel/plugin-transform-unicode-regex" "^7.22.5"
     "@babel/plugin-transform-unicode-sets-regex" "^7.22.5"
     "@babel/preset-modules" "0.1.6-no-external-plugins"
-    "@babel/types" "^7.22.10"
+    "@babel/types" "^7.22.11"
     babel-plugin-polyfill-corejs2 "^0.4.5"
     babel-plugin-polyfill-corejs3 "^0.8.3"
     babel-plugin-polyfill-regenerator "^0.5.2"
@@ -1125,7 +1123,7 @@
     debug "^4.1.0"
     globals "^11.1.0"
 
-"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
+"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.3.3":
   version "7.22.10"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03"
   integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==
@@ -1143,7 +1141,7 @@
     "@babel/helper-validator-identifier" "^7.22.5"
     to-fast-properties "^2.0.0"
 
-"@babel/types@^7.22.10", "@babel/types@^7.22.11", "@babel/types@^7.22.5":
+"@babel/types@^7.22.10", "@babel/types@^7.22.11", "@babel/types@^7.22.5", "@babel/types@^7.4.4":
   version "7.22.11"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.11.tgz#0e65a6a1d4d9cbaa892b2213f6159485fe632ea2"
   integrity sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==
@@ -3932,9 +3930,9 @@ caniuse-lite@^1.0.30001502:
   integrity sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==
 
 caniuse-lite@^1.0.30001517:
-  version "1.0.30001522"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz#44b87a406c901269adcdb834713e23582dd71856"
-  integrity sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==
+  version "1.0.30001524"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz#1e14bce4f43c41a7deaeb5ebfe86664fe8dadb80"
+  integrity sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==
 
 caniuse-lite@^1.0.30001520:
   version "1.0.30001520"
@@ -4335,11 +4333,11 @@ copy-descriptor@^0.1.0:
   integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==
 
 core-js-compat@^3.31.0:
-  version "3.32.0"
-  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.0.tgz#f41574b6893ab15ddb0ac1693681bd56c8550a90"
-  integrity sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==
+  version "3.32.1"
+  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.1.tgz#55f9a7d297c0761a8eb1d31b593e0f5b6ffae964"
+  integrity sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA==
   dependencies:
-    browserslist "^4.21.9"
+    browserslist "^4.21.10"
 
 core-js@^2.5.0:
   version "2.6.12"
@@ -5085,9 +5083,9 @@ electron-to-chromium@^1.4.428:
   integrity sha512-/g3UyNDmDd6ebeWapmAoiyy+Sy2HyJ+/X8KyvNeHfKRFfHaA2W8oF5fxD5F3tjBDcjpwo0iek6YNgxNXDBoEtA==
 
 electron-to-chromium@^1.4.477:
-  version "1.4.500"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.500.tgz#7dd05fdfbe02ed34b9f6099cfe01407b473d5af7"
-  integrity sha512-P38NO8eOuWOKY1sQk5yE0crNtrjgjJj6r3NrbIKtG18KzCHmHE2Bt+aQA7/y0w3uYsHWxDa6icOohzjLJ4vJ4A==
+  version "1.4.505"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.505.tgz#00571ade5975b58413f0f56a665b065bfc29cdfc"
+  integrity sha512-0A50eL5BCCKdxig2SsCXhpuztnB9PfUgRMojj5tMvt8O54lbwz3t6wNgnpiTRosw5QjlJB7ixhVyeg8daLQwSQ==
 
 elliptic@^6.5.3:
   version "6.5.4"

From cb9f96036cf78835e22aafb30b80717b5a8b7394 Mon Sep 17 00:00:00 2001
From: gunchleoc <fios@foramnagaidhlig.net>
Date: Thu, 31 Aug 2023 12:17:10 +0200
Subject: [PATCH 50/81] Add suggestion for secure cyphers to nginx.conf
 (#26349)

---
 dist/nginx.conf | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/dist/nginx.conf b/dist/nginx.conf
index 39fa58e50d..5bb9903864 100644
--- a/dist/nginx.conf
+++ b/dist/nginx.conf
@@ -36,7 +36,11 @@ server {
   server_name example.com;
 
   ssl_protocols TLSv1.2 TLSv1.3;
-  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
+
+  # You can use https://ssl-config.mozilla.org/ to generate your cipher set.
+  # We recommend their "Intermediate" level.
+  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
+
   ssl_prefer_server_ciphers on;
   ssl_session_cache shared:SSL:10m;
   ssl_session_tickets off;

From 40b69cc1cd62fa57047905a6999985ae6c183f6c Mon Sep 17 00:00:00 2001
From: Stanislas Signoud <signez@stanisoft.net>
Date: Thu, 31 Aug 2023 12:18:46 +0200
Subject: [PATCH 51/81] Add an explanation banner on switching to single column
 mode (#26019)

---
 .../features/ui/components/navigation_panel.jsx  | 16 +++++++++++-----
 app/javascript/mastodon/locales/en.json          |  1 +
 app/javascript/mastodon/locales/fr.json          |  1 +
 app/javascript/styles/mastodon/components.scss   | 16 ++++++++++++++++
 4 files changed, 29 insertions(+), 5 deletions(-)

diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index d36abf8f17..8006ca89a2 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -31,6 +31,7 @@ const messages = defineMessages({
   about: { id: 'navigation_bar.about', defaultMessage: 'About' },
   search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
   advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
+  openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
 });
 
 class NavigationPanel extends Component {
@@ -57,12 +58,17 @@ class NavigationPanel extends Component {
         <div className='navigation-panel__logo'>
           <Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
 
-          {transientSingleColumn && (
-            <a href={`/deck${location.pathname}`} className='button button--block'>
-              {intl.formatMessage(messages.advancedInterface)}
-            </a>
+          {transientSingleColumn ? (
+            <div class='switch-to-advanced'>
+              {intl.formatMessage(messages.openedInClassicInterface)}
+              {" "}
+              <a href={`/deck${location.pathname}`} class='switch-to-advanced__toggle'>
+                {intl.formatMessage(messages.advancedInterface)}
+              </a>
+            </div>
+          ) : (
+            <hr />
           )}
-          <hr />
         </div>
 
         {signedIn && (
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 5871b08def..90bb9616f0 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -411,6 +411,7 @@
   "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Muted users",
+  "navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.",
   "navigation_bar.personal": "Personal",
   "navigation_bar.pins": "Pinned posts",
   "navigation_bar.preferences": "Preferences",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 2bef3bb4b3..116ed66d03 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -409,6 +409,7 @@
   "navigation_bar.lists": "Listes",
   "navigation_bar.logout": "Déconnexion",
   "navigation_bar.mutes": "Comptes masqués",
+  "navigation_bar.opened_in_classic_interface": "Les messages, les comptes et les pages spécifiques sont ouvertes dans l’interface classique.",
   "navigation_bar.personal": "Personnel",
   "navigation_bar.pins": "Messages épinglés",
   "navigation_bar.preferences": "Préférences",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index c07f95f564..95e6667b5d 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -3270,6 +3270,22 @@ $ui-header-height: 55px;
   border-color: $ui-highlight-color;
 }
 
+.switch-to-advanced {
+  color: $classic-primary-color;
+  background-color: $classic-base-color;
+  padding: 15px;
+  border-radius: 4px;
+  margin-top: 4px;
+  margin-bottom: 12px;
+  font-size: 13px;
+  line-height: 18px;
+
+  .switch-to-advanced__toggle {
+    color: $ui-button-tertiary-color;
+    font-weight: bold;
+  }
+}
+
 .column-link {
   background: lighten($ui-base-color, 8%);
   color: $primary-text-color;

From 336ec503c28356a969e2987ef426063f0943b4e3 Mon Sep 17 00:00:00 2001
From: Tyler Deitz <tylerdeitz@gmail.com>
Date: Thu, 31 Aug 2023 04:46:27 -0700
Subject: [PATCH 52/81] Add avatar image to webfinger responses (#26558)

---
 app/serializers/webfinger_serializer.rb       | 37 +++++++----
 .../well_known/webfinger_controller_spec.rb   | 64 +++++++++++++++++++
 2 files changed, 89 insertions(+), 12 deletions(-)

diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb
index 3ca3441169..b67cd2771a 100644
--- a/app/serializers/webfinger_serializer.rb
+++ b/app/serializers/webfinger_serializer.rb
@@ -18,18 +18,31 @@ class WebfingerSerializer < ActiveModel::Serializer
   end
 
   def links
-    if object.instance_actor?
-      [
-        { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: about_more_url(instance_actor: true) },
-        { rel: 'self', type: 'application/activity+json', href: instance_actor_url },
-        { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
-      ]
-    else
-      [
-        { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
-        { rel: 'self', type: 'application/activity+json', href: account_url(object) },
-        { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
-      ]
+    [
+      { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: profile_page_href },
+      { rel: 'self', type: 'application/activity+json', href: self_href },
+      { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
+    ].tap do |x|
+      x << { rel: 'http://webfinger.net/rel/avatar', type: object.avatar.content_type, href: full_asset_url(object.avatar_original_url) } if show_avatar?
     end
   end
+
+  private
+
+  def show_avatar?
+    media_present = object.avatar.present? && object.avatar.content_type.present?
+
+    # Show avatar only if an instance shows profiles to logged out users
+    allowed_by_config = ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] != 'true' && !Rails.configuration.x.limited_federation_mode
+
+    media_present && allowed_by_config
+  end
+
+  def profile_page_href
+    object.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(object)
+  end
+
+  def self_href
+    object.instance_actor? ? instance_actor_url : account_url(object)
+  end
 end
diff --git a/spec/controllers/well_known/webfinger_controller_spec.rb b/spec/controllers/well_known/webfinger_controller_spec.rb
index 8dc0f329b6..20770a7211 100644
--- a/spec/controllers/well_known/webfinger_controller_spec.rb
+++ b/spec/controllers/well_known/webfinger_controller_spec.rb
@@ -3,6 +3,8 @@
 require 'rails_helper'
 
 describe WellKnown::WebfingerController do
+  include RoutingHelper
+
   render_views
 
   describe 'GET #show' do
@@ -167,5 +169,67 @@ describe WellKnown::WebfingerController 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

From ccca542db1e73610a86c1d34d8b42f3f4a818af9 Mon Sep 17 00:00:00 2001
From: Daniel M Brasil <danielmbrasil@protonmail.com>
Date: Thu, 31 Aug 2023 08:53:24 -0300
Subject: [PATCH 53/81] Fix `/api/v1/timelines/tag/:hashtag` allowing for
 unauthenticated access when public preview is disabled (#26237)

---
 .../api/v1/timelines/tag_controller.rb        |  5 ++
 .../api/v1/timelines/tag_controller_spec.rb   | 66 ++++++++++++++-----
 2 files changed, 53 insertions(+), 18 deletions(-)

diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index 9cd7b99046..a79d65c124 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 class Api::V1::Timelines::TagController < Api::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? }
 
@@ -12,6 +13,10 @@ class Api::V1::Timelines::TagController < Api::BaseController
 
   private
 
+  def require_auth?
+    !Setting.timeline_preview
+  end
+
   def load_tag
     @tag = Tag.find_normalized(params[:id])
   end
diff --git a/spec/controllers/api/v1/timelines/tag_controller_spec.rb b/spec/controllers/api/v1/timelines/tag_controller_spec.rb
index 7189110833..1c60798fcf 100644
--- a/spec/controllers/api/v1/timelines/tag_controller_spec.rb
+++ b/spec/controllers/api/v1/timelines/tag_controller_spec.rb
@@ -5,36 +5,66 @@ require 'rails_helper'
 describe Api::V1::Timelines::TagController do
   render_views
 
-  let(:user) { Fabricate(:user) }
+  let(:user)   { Fabricate(:user) }
+  let(:token)  { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') }
 
   before do
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
-  context 'with a user context' do
-    let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) }
+  describe 'GET #show' do
+    subject do
+      get :show, params: { id: 'test' }
+    end
 
-    describe 'GET #show' do
-      before do
-        PostStatusService.new.call(user.account, text: 'It is a #test')
+    before do
+      PostStatusService.new.call(user.account, text: 'It is a #test')
+    end
+
+    context 'when the instance allows public preview' do
+      context 'when the user is not authenticated' do
+        let(:token) { nil }
+
+        it 'returns http success', :aggregate_failures do
+          subject
+
+          expect(response).to have_http_status(200)
+          expect(response.headers['Link'].links.size).to eq(2)
+        end
       end
 
-      it 'returns http success' do
-        get :show, params: { id: 'test' }
-        expect(response).to have_http_status(200)
-        expect(response.headers['Link'].links.size).to eq(2)
+      context 'when the user is authenticated' do
+        it 'returns http success', :aggregate_failures do
+          subject
+
+          expect(response).to have_http_status(200)
+          expect(response.headers['Link'].links.size).to eq(2)
+        end
       end
     end
-  end
 
-  context 'without a user context' do
-    let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil) }
+    context 'when the instance does not allow public preview' do
+      before do
+        Form::AdminSettings.new(timeline_preview: false).save
+      end
 
-    describe 'GET #show' do
-      it 'returns http success' do
-        get :show, params: { id: 'test' }
-        expect(response).to have_http_status(200)
-        expect(response.headers['Link']).to be_nil
+      context 'when the user is not authenticated' do
+        let(:token) { nil }
+
+        it 'returns http unauthorized' do
+          subject
+
+          expect(response).to have_http_status(401)
+        end
+      end
+
+      context 'when the user is authenticated' do
+        it 'returns http success', :aggregate_failures do
+          subject
+
+          expect(response).to have_http_status(200)
+          expect(response.headers['Link'].links.size).to eq(2)
+        end
       end
     end
   end

From f1d250135ccf9be5c4d982a2c48417da89d38eb5 Mon Sep 17 00:00:00 2001
From: Santiago Kozak <kozaksantiago@gmail.com>
Date: Thu, 31 Aug 2023 09:15:58 -0300
Subject: [PATCH 54/81] Allow filter form in profiles directory to wrap
 (#26682)

---
 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 95e6667b5d..f61cd059fe 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2381,6 +2381,7 @@ $ui-header-height: 55px;
 
   .filter-form {
     display: flex;
+    flex-wrap: wrap;
   }
 
   .autosuggest-textarea__textarea {

From cffc5d2b0180e2ead23f32391246cdac357bf71b Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Thu, 31 Aug 2023 14:59:50 +0200
Subject: [PATCH 55/81] Fix comment in build image workflow (#26740)

---
 .github/workflows/build-container-image.yml | 2 --
 .github/workflows/build-releases.yml        | 2 ++
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml
index 897bb9caaa..a1aeddf201 100644
--- a/.github/workflows/build-container-image.yml
+++ b/.github/workflows/build-container-image.yml
@@ -76,8 +76,6 @@ jobs:
         if: ${{ inputs.push_to_images != '' }}
         with:
           images: ${{ inputs.push_to_images }}
-          # Only tag with latest when ran against the latest stable branch
-          # This needs to be updated after each minor version release
           flavor: ${{ inputs.flavor }}
           tags: ${{ inputs.tags }}
           labels: ${{ inputs.labels }}
diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml
index b408174688..f739a69d9a 100644
--- a/.github/workflows/build-releases.yml
+++ b/.github/workflows/build-releases.yml
@@ -17,6 +17,8 @@ jobs:
       push_to_images: |
         tootsuite/mastodon
         ghcr.io/mastodon/mastodon
+      # Only tag with latest when ran against the latest stable branch
+      # This needs to be updated after each minor version release
       flavor: |
         latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
       tags: |

From 6b58cfd8dd2f635dbb1b9d10b6b243d20ba23535 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 31 Aug 2023 15:35:58 +0200
Subject: [PATCH 56/81] Fix searching by username by reverting account verbatim
 tokenizer to `standard` (#26739)

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

diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb
index 8881b08f66..00db257ac7 100644
--- a/app/chewy/accounts_index.rb
+++ b/app/chewy/accounts_index.rb
@@ -34,7 +34,7 @@ class AccountsIndex < Chewy::Index
       },
 
       verbatim: {
-        tokenizer: 'uax_url_email',
+        tokenizer: 'standard',
         filter: %w(lowercase asciifolding cjk_width),
       },
 

From 1471be82255d948460b0b653dd48cdfbc67d90cf Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 31 Aug 2023 15:47:30 +0200
Subject: [PATCH 57/81] Fix `AddUniqueIndexOnPreviewCardsStatuses` migration
 requiring PostgreSQL 12+ in some cases (#26737)

---
 ...1_add_unique_index_on_preview_cards_statuses.rb | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb b/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb
index c35ad80028..3e9ab134b7 100644
--- a/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb
+++ b/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb
@@ -15,10 +15,22 @@ class AddUniqueIndexOnPreviewCardsStatuses < ActiveRecord::Migration[6.1]
 
   private
 
+  def supports_concurrent_reindex?
+    @supports_concurrent_reindex ||= begin
+      version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
+      version >= 12_000
+    end
+  end
+
   def deduplicate_and_reindex!
     deduplicate_preview_cards!
 
-    safety_assured { execute 'REINDEX INDEX CONCURRENTLY preview_cards_statuses_pkey' }
+    if supports_concurrent_reindex?
+      safety_assured { execute 'REINDEX INDEX CONCURRENTLY preview_cards_statuses_pkey' }
+    else
+      remove_index :preview_cards_statuses, name: :preview_cards_statuses_pkey
+      add_index :preview_cards_statuses, [:status_id, :preview_card_id], name: :preview_cards_statuses_pkey, algorithm: :concurrently, unique: true
+    end
   rescue ActiveRecord::RecordNotUnique
     retry
   end

From ecd76fa413e31d4eb26e09fa4b65f8b13bbbb0b7 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 31 Aug 2023 17:21:06 +0200
Subject: [PATCH 58/81] Fix videos not playing in some browsers due to
 unsupported color space (#26745)

---
 app/models/media_attachment.rb | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 88abd93390..984f4252a1 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -100,6 +100,8 @@ class MediaAttachment < ApplicationRecord
       output: {
         'loglevel' => 'fatal',
         'preset' => 'veryfast',
+        'movflags' => 'faststart', # Move metadata to start of file so playback can begin before download finishes
+        'pix_fmt' => 'yuv420p', # Ensure color space for cross-browser compatibility
         'c:v' => 'h264',
         'c:a' => 'aac',
         'b:a' => '192k',

From 9bb2fb6b1484c90c5b2c6cc52ce148019e82a3e2 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Thu, 31 Aug 2023 19:04:27 +0200
Subject: [PATCH 59/81] Change importers to avoid a few inefficiencies (#26721)

---
 app/lib/importer/accounts_index_importer.rb   |  6 ++--
 app/lib/importer/base_importer.rb             |  8 +++++
 app/lib/importer/instances_index_importer.rb  |  6 ++--
 .../public_statuses_index_importer.rb         |  6 ++--
 app/lib/importer/statuses_index_importer.rb   | 35 ++++++++-----------
 app/lib/importer/tags_index_importer.rb       |  6 ++--
 6 files changed, 34 insertions(+), 33 deletions(-)

diff --git a/app/lib/importer/accounts_index_importer.rb b/app/lib/importer/accounts_index_importer.rb
index fd869c3960..d8b9190275 100644
--- a/app/lib/importer/accounts_index_importer.rb
+++ b/app/lib/importer/accounts_index_importer.rb
@@ -4,10 +4,10 @@ class Importer::AccountsIndexImporter < Importer::BaseImporter
   def import!
     scope.includes(:account_stat).find_in_batches(batch_size: @batch_size) do |tmp|
       in_work_unit(tmp) do |accounts|
-        bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: accounts).bulk_body
+        bulk = build_bulk_body(accounts)
 
-        indexed = bulk.count { |entry| entry[:index] }
-        deleted = bulk.count { |entry| entry[:delete] }
+        indexed = bulk.size
+        deleted = 0
 
         Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 
diff --git a/app/lib/importer/base_importer.rb b/app/lib/importer/base_importer.rb
index cc1b7b44d7..a21557d303 100644
--- a/app/lib/importer/base_importer.rb
+++ b/app/lib/importer/base_importer.rb
@@ -68,6 +68,14 @@ class Importer::BaseImporter
 
   protected
 
+  def build_bulk_body(to_import)
+    # Specialize `Chewy::Index::Import::BulkBuilder#bulk_body` to avoid a few
+    # inefficiencies, as none of our fields or join fields and we do not need
+    # `BulkBuilder`'s versatility.
+    crutches = Chewy::Index::Crutch::Crutches.new index, to_import
+    to_import.map { |object| { index: { _id: object.id, data: index.compose(object, crutches, fields: []) } } }
+  end
+
   def in_work_unit(...)
     work_unit = Concurrent::Promises.future_on(@executor, ...)
 
diff --git a/app/lib/importer/instances_index_importer.rb b/app/lib/importer/instances_index_importer.rb
index 7318b51b5d..ebdceb72ed 100644
--- a/app/lib/importer/instances_index_importer.rb
+++ b/app/lib/importer/instances_index_importer.rb
@@ -4,10 +4,10 @@ class Importer::InstancesIndexImporter < Importer::BaseImporter
   def import!
     index.adapter.default_scope.find_in_batches(batch_size: @batch_size) do |tmp|
       in_work_unit(tmp) do |instances|
-        bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: instances).bulk_body
+        bulk = build_bulk_body(instances)
 
-        indexed = bulk.count { |entry| entry[:index] }
-        deleted = bulk.count { |entry| entry[:delete] }
+        indexed = bulk.size
+        deleted = 0
 
         Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 
diff --git a/app/lib/importer/public_statuses_index_importer.rb b/app/lib/importer/public_statuses_index_importer.rb
index 72d02318b1..ebaac3794f 100644
--- a/app/lib/importer/public_statuses_index_importer.rb
+++ b/app/lib/importer/public_statuses_index_importer.rb
@@ -5,11 +5,11 @@ class Importer::PublicStatusesIndexImporter < Importer::BaseImporter
     scope.select(:id).find_in_batches(batch_size: @batch_size) do |batch|
       in_work_unit(batch.pluck(:id)) do |status_ids|
         bulk = ActiveRecord::Base.connection_pool.with_connection do
-          Chewy::Index::Import::BulkBuilder.new(index, to_index: Status.includes(:media_attachments, :preloadable_poll, :preview_cards).where(id: status_ids)).bulk_body
+          build_bulk_body(index.adapter.default_scope.where(id: status_ids))
         end
 
-        indexed = bulk.count { |entry| entry[:index] }
-        deleted = bulk.count { |entry| entry[:delete] }
+        indexed = bulk.size
+        deleted = 0
 
         Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 
diff --git a/app/lib/importer/statuses_index_importer.rb b/app/lib/importer/statuses_index_importer.rb
index 285ddc871a..08ad3e3797 100644
--- a/app/lib/importer/statuses_index_importer.rb
+++ b/app/lib/importer/statuses_index_importer.rb
@@ -13,32 +13,25 @@ class Importer::StatusesIndexImporter < Importer::BaseImporter
 
       scope.find_in_batches(batch_size: @batch_size) do |tmp|
         in_work_unit(tmp.map(&:status_id)) do |status_ids|
-          bulk = ActiveRecord::Base.connection_pool.with_connection do
-            Chewy::Index::Import::BulkBuilder.new(index, to_index: index.adapter.default_scope.where(id: status_ids)).bulk_body
-          end
-
-          indexed = 0
           deleted = 0
 
-          # We can't use the delete_if proc to do the filtering because delete_if
-          # is called before rendering the data and we need to filter based
-          # on the results of the filter, so this filtering happens here instead
-          bulk.map! do |entry|
-            new_entry = if entry[:index] && entry.dig(:index, :data, 'searchable_by').blank?
-                          { delete: entry[:index].except(:data) }
-                        else
-                          entry
-                        end
-
-            if new_entry[:index]
-              indexed += 1
-            else
-              deleted += 1
+          bulk = ActiveRecord::Base.connection_pool.with_connection do
+            to_index = index.adapter.default_scope.where(id: status_ids)
+            crutches = Chewy::Index::Crutch::Crutches.new index, to_index
+            to_index.map do |object|
+              # This is unlikely to happen, but the post may have been
+              # un-interacted with since it was queued for indexing
+              if object.searchable_by.empty?
+                deleted += 1
+                { delete: { _id: object.id } }
+              else
+                { index: { _id: object.id, data: index.compose(object, crutches, fields: []) } }
+              end
             end
-
-            new_entry
           end
 
+          indexed = bulk.size - deleted
+
           Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 
           [indexed, deleted]
diff --git a/app/lib/importer/tags_index_importer.rb b/app/lib/importer/tags_index_importer.rb
index 77710ed7de..067fd8cd2d 100644
--- a/app/lib/importer/tags_index_importer.rb
+++ b/app/lib/importer/tags_index_importer.rb
@@ -4,10 +4,10 @@ class Importer::TagsIndexImporter < Importer::BaseImporter
   def import!
     index.adapter.default_scope.find_in_batches(batch_size: @batch_size) do |tmp|
       in_work_unit(tmp) do |tags|
-        bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: tags).bulk_body
+        bulk = build_bulk_body(tags)
 
-        indexed = bulk.count { |entry| entry[:index] }
-        deleted = bulk.count { |entry| entry[:delete] }
+        indexed = bulk.size
+        deleted = 0
 
         Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 

From 00084581289b4b7afd120845363b16247c5fa93b Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Thu, 31 Aug 2023 19:04:44 +0200
Subject: [PATCH 60/81] Fix search queries with slash causing or-condition
 (#26699)

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

diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index 86e3f50005..f10ccfb283 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -25,7 +25,7 @@ class SearchQueryTransformer < Parslet::Transform
     def clause_to_query(clause)
       case clause
       when TermClause
-        { multi_match: { type: 'most_fields', query: clause.term, fields: ['text', 'text.stemmed'] } }
+        { multi_match: { type: 'most_fields', query: clause.term, fields: ['text', 'text.stemmed'], operator: 'and' } }
       when PhraseClause
         { match_phrase: { text: { query: clause.phrase } } }
       else

From 872145d1c2aec709770de10579d2a4f83c601f37 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 1 Sep 2023 09:27:03 +0200
Subject: [PATCH 61/81] Fix not being able to invoke phrase search using
 unicode quotation marks (#26687)

---
 app/services/search_service.rb | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 40d82fc525..9a40d7bdd5 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -1,8 +1,10 @@
 # frozen_string_literal: true
 
 class SearchService < BaseService
+  QUOTE_EQUIVALENT_CHARACTERS = /[“”„«»「」『』《》]/
+
   def call(query, account, limit, options = {})
-    @query     = query&.strip
+    @query     = query&.strip&.gsub(QUOTE_EQUIVALENT_CHARACTERS, '"')
     @account   = account
     @options   = options
     @limit     = limit.to_i

From e754083e8a33778b5dd8d43efc5604ed50efb0e4 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Fri, 1 Sep 2023 09:43:12 +0200
Subject: [PATCH 62/81] Fix unmatched quotes and prefixes causing search to
 fail (#26701)

---
 app/lib/search_query_parser.rb            |   4 +-
 app/lib/search_query_transformer.rb       | 101 +++++++++++-----------
 spec/lib/search_query_parser_spec.rb      |  98 +++++++++++++++++++++
 spec/lib/search_query_transformer_spec.rb |  57 ++++++++++--
 4 files changed, 200 insertions(+), 60 deletions(-)
 create mode 100644 spec/lib/search_query_parser_spec.rb

diff --git a/app/lib/search_query_parser.rb b/app/lib/search_query_parser.rb
index 5d6ffbf29d..1c57b9b024 100644
--- a/app/lib/search_query_parser.rb
+++ b/app/lib/search_query_parser.rb
@@ -6,10 +6,10 @@ class SearchQueryParser < Parslet::Parser
   rule(:colon)     { str(':') }
   rule(:space)     { match('\s').repeat(1) }
   rule(:operator)  { (str('+') | str('-')).as(:operator) }
-  rule(:prefix)    { (term >> colon).as(:prefix) }
+  rule(:prefix)    { term >> colon }
   rule(:shortcode) { (colon >> term >> colon.maybe).as(:shortcode) }
   rule(:phrase)    { (quote >> (term >> space.maybe).repeat >> quote).as(:phrase) }
-  rule(:clause)    { (operator.maybe >> prefix.maybe >> (phrase | term | shortcode)).as(:clause) }
+  rule(:clause)    { (operator.maybe >> prefix.maybe.as(:prefix) >> (phrase | term | shortcode)).as(:clause) | prefix.as(:clause) | quote.as(:junk) }
   rule(:query)     { (clause >> space.maybe).repeat.as(:query) }
   root(:query)
 end
diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index f10ccfb283..e81c0c081e 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -1,50 +1,32 @@
 # frozen_string_literal: true
 
 class SearchQueryTransformer < Parslet::Transform
+  SUPPORTED_PREFIXES = %w(
+    has
+    is
+    language
+    from
+    before
+    after
+    during
+  ).freeze
+
   class Query
-    attr_reader :should_clauses, :must_not_clauses, :must_clauses, :filter_clauses
+    attr_reader :must_not_clauses, :must_clauses, :filter_clauses
 
     def initialize(clauses)
-      grouped = clauses.chunk(&:operator).to_h
-      @should_clauses = grouped.fetch(:should, [])
+      grouped = clauses.compact.chunk(&:operator).to_h
       @must_not_clauses = grouped.fetch(:must_not, [])
       @must_clauses = grouped.fetch(:must, [])
       @filter_clauses = grouped.fetch(:filter, [])
     end
 
     def apply(search)
-      should_clauses.each { |clause| search = search.query.should(clause_to_query(clause)) }
-      must_clauses.each { |clause| search = search.query.must(clause_to_query(clause)) }
-      must_not_clauses.each { |clause| search = search.query.must_not(clause_to_query(clause)) }
-      filter_clauses.each { |clause| search = search.filter(**clause_to_filter(clause)) }
+      must_clauses.each { |clause| search = search.query.must(clause.to_query) }
+      must_not_clauses.each { |clause| search = search.query.must_not(clause.to_query) }
+      filter_clauses.each { |clause| search = search.filter(**clause.to_query) }
       search.query.minimum_should_match(1)
     end
-
-    private
-
-    def clause_to_query(clause)
-      case clause
-      when TermClause
-        { multi_match: { type: 'most_fields', query: clause.term, fields: ['text', 'text.stemmed'], operator: 'and' } }
-      when PhraseClause
-        { match_phrase: { text: { query: clause.phrase } } }
-      else
-        raise "Unexpected clause type: #{clause}"
-      end
-    end
-
-    def clause_to_filter(clause)
-      case clause
-      when PrefixClause
-        if clause.negated?
-          { bool: { must_not: { clause.type => { clause.filter => clause.term } } } }
-        else
-          { clause.type => { clause.filter => clause.term } }
-        end
-      else
-        raise "Unexpected clause type: #{clause}"
-      end
-    end
   end
 
   class Operator
@@ -63,31 +45,38 @@ class SearchQueryTransformer < Parslet::Transform
   end
 
   class TermClause
-    attr_reader :prefix, :operator, :term
+    attr_reader :operator, :term
 
-    def initialize(prefix, operator, term)
-      @prefix = prefix
+    def initialize(operator, term)
       @operator = Operator.symbol(operator)
       @term = term
     end
+
+    def to_query
+      { multi_match: { type: 'most_fields', query: @term, fields: ['text', 'text.stemmed'], operator: 'and' } }
+    end
   end
 
   class PhraseClause
-    attr_reader :prefix, :operator, :phrase
+    attr_reader :operator, :phrase
 
-    def initialize(prefix, operator, phrase)
-      @prefix = prefix
+    def initialize(operator, phrase)
       @operator = Operator.symbol(operator)
       @phrase = phrase
     end
+
+    def to_query
+      { match_phrase: { text: { query: @phrase } } }
+    end
   end
 
   class PrefixClause
-    attr_reader :type, :filter, :operator, :term
+    attr_reader :operator, :prefix, :term
 
     def initialize(prefix, operator, term, options = {})
-      @negated  = operator == '-'
-      @options  = options
+      @prefix = prefix
+      @negated = operator == '-'
+      @options = options
       @operator = :filter
 
       case prefix
@@ -116,12 +105,16 @@ class SearchQueryTransformer < Parslet::Transform
         @type = :range
         @term = { gte: term, lte: term, time_zone: @options[:current_account]&.user_time_zone || 'UTC' }
       else
-        raise Mastodon::SyntaxError
+        raise "Unknown prefix: #{prefix}"
       end
     end
 
-    def negated?
-      @negated
+    def to_query
+      if @negated
+        { bool: { must_not: { @type => { @filter => @term } } } }
+      else
+        { @type => { @filter => @term } }
+      end
     end
 
     private
@@ -159,18 +152,26 @@ class SearchQueryTransformer < Parslet::Transform
     prefix   = clause[:prefix][:term].to_s if clause[:prefix]
     operator = clause[:operator]&.to_s
 
-    if clause[:prefix]
+    if clause[:prefix] && SUPPORTED_PREFIXES.include?(prefix)
       PrefixClause.new(prefix, operator, clause[:term].to_s, current_account: current_account)
+    elsif clause[:prefix]
+      TermClause.new(operator, "#{prefix} #{clause[:term]}")
     elsif clause[:term]
-      TermClause.new(prefix, operator, clause[:term].to_s)
+      TermClause.new(operator, clause[:term].to_s)
     elsif clause[:shortcode]
-      TermClause.new(prefix, operator, ":#{clause[:term]}:")
+      TermClause.new(operator, ":#{clause[:term]}:")
     elsif clause[:phrase]
-      PhraseClause.new(prefix, operator, clause[:phrase].is_a?(Array) ? clause[:phrase].map { |p| p[:term].to_s }.join(' ') : clause[:phrase].to_s)
+      PhraseClause.new(operator, clause[:phrase].is_a?(Array) ? clause[:phrase].map { |p| p[:term].to_s }.join(' ') : clause[:phrase].to_s)
     else
       raise "Unexpected clause type: #{clause}"
     end
   end
 
-  rule(query: sequence(:clauses)) { Query.new(clauses) }
+  rule(junk: subtree(:junk)) do
+    nil
+  end
+
+  rule(query: sequence(:clauses)) do
+    Query.new(clauses)
+  end
 end
diff --git a/spec/lib/search_query_parser_spec.rb b/spec/lib/search_query_parser_spec.rb
new file mode 100644
index 0000000000..66b0e8f9e2
--- /dev/null
+++ b/spec/lib/search_query_parser_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'parslet/rig/rspec'
+
+describe SearchQueryParser do
+  let(:parser) { described_class.new }
+
+  context 'with term' do
+    it 'consumes "hello"' do
+      expect(parser.term).to parse('hello')
+    end
+  end
+
+  context 'with prefix' do
+    it 'consumes "foo:"' do
+      expect(parser.prefix).to parse('foo:')
+    end
+  end
+
+  context 'with operator' do
+    it 'consumes "+"' do
+      expect(parser.operator).to parse('+')
+    end
+
+    it 'consumes "-"' do
+      expect(parser.operator).to parse('-')
+    end
+  end
+
+  context 'with shortcode' do
+    it 'consumes ":foo:"' do
+      expect(parser.shortcode).to parse(':foo:')
+    end
+  end
+
+  context 'with phrase' do
+    it 'consumes "hello world"' do
+      expect(parser.phrase).to parse('"hello world"')
+    end
+  end
+
+  context 'with clause' do
+    it 'consumes "foo"' do
+      expect(parser.clause).to parse('foo')
+    end
+
+    it 'consumes "-foo"' do
+      expect(parser.clause).to parse('-foo')
+    end
+
+    it 'consumes "foo:bar"' do
+      expect(parser.clause).to parse('foo:bar')
+    end
+
+    it 'consumes "-foo:bar"' do
+      expect(parser.clause).to parse('-foo:bar')
+    end
+
+    it 'consumes \'foo:"hello world"\'' do
+      expect(parser.clause).to parse('foo:"hello world"')
+    end
+
+    it 'consumes \'-foo:"hello world"\'' do
+      expect(parser.clause).to parse('-foo:"hello world"')
+    end
+
+    it 'consumes "foo:"' do
+      expect(parser.clause).to parse('foo:')
+    end
+
+    it 'consumes \'"\'' do
+      expect(parser.clause).to parse('"')
+    end
+  end
+
+  context 'with query' do
+    it 'consumes "hello -world"' do
+      expect(parser.query).to parse('hello -world')
+    end
+
+    it 'consumes \'foo "hello world"\'' do
+      expect(parser.query).to parse('foo "hello world"')
+    end
+
+    it 'consumes "foo:bar hello"' do
+      expect(parser.query).to parse('foo:bar hello')
+    end
+
+    it 'consumes \'"hello" world "\'' do
+      expect(parser.query).to parse('"hello" world "')
+    end
+
+    it 'consumes "foo:bar bar: hello"' do
+      expect(parser.query).to parse('foo:bar bar: hello')
+    end
+  end
+end
diff --git a/spec/lib/search_query_transformer_spec.rb b/spec/lib/search_query_transformer_spec.rb
index 953f9acb2f..17f06d2833 100644
--- a/spec/lib/search_query_transformer_spec.rb
+++ b/spec/lib/search_query_transformer_spec.rb
@@ -3,16 +3,57 @@
 require 'rails_helper'
 
 describe SearchQueryTransformer do
-  describe 'initialization' do
-    let(:parser) { SearchQueryParser.new.parse('query') }
+  subject { described_class.new.apply(parser, current_account: nil) }
 
-    it 'sets attributes' do
-      transformer = described_class.new.apply(parser)
+  let(:parser) { SearchQueryParser.new.parse(query) }
 
-      expect(transformer.should_clauses.first).to be_nil
-      expect(transformer.must_clauses.first).to be_a(SearchQueryTransformer::TermClause)
-      expect(transformer.must_not_clauses.first).to be_nil
-      expect(transformer.filter_clauses.first).to be_nil
+  context 'with "hello world"' do
+    let(:query) { 'hello world' }
+
+    it 'transforms clauses' do
+      expect(subject.must_clauses.map(&:term)).to match_array %w(hello world)
+      expect(subject.must_not_clauses).to be_empty
+      expect(subject.filter_clauses).to be_empty
+    end
+  end
+
+  context 'with "hello -world"' do
+    let(:query) { 'hello -world' }
+
+    it 'transforms clauses' do
+      expect(subject.must_clauses.map(&:term)).to match_array %w(hello)
+      expect(subject.must_not_clauses.map(&:term)).to match_array %w(world)
+      expect(subject.filter_clauses).to be_empty
+    end
+  end
+
+  context 'with "hello is:reply"' do
+    let(:query) { 'hello is:reply' }
+
+    it 'transforms clauses' do
+      expect(subject.must_clauses.map(&:term)).to match_array %w(hello)
+      expect(subject.must_not_clauses).to be_empty
+      expect(subject.filter_clauses.map(&:term)).to match_array %w(reply)
+    end
+  end
+
+  context 'with "foo: bar"' do
+    let(:query) { 'foo: bar' }
+
+    it 'transforms clauses' do
+      expect(subject.must_clauses.map(&:term)).to match_array %w(foo bar)
+      expect(subject.must_not_clauses).to be_empty
+      expect(subject.filter_clauses).to be_empty
+    end
+  end
+
+  context 'with "foo:bar"' do
+    let(:query) { 'foo:bar' }
+
+    it 'transforms clauses' do
+      expect(subject.must_clauses.map(&:term)).to contain_exactly('foo bar')
+      expect(subject.must_not_clauses).to be_empty
+      expect(subject.filter_clauses).to be_empty
     end
   end
 end

From 630e558677d961e74ce82ab5e2a62a0760bbe815 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 1 Sep 2023 10:17:08 +0200
Subject: [PATCH 63/81] Update dependency oj to v3.16.1 (#26749)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index f1a61c5e09..6dd8c567f8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -482,7 +482,7 @@ GEM
     nokogiri (1.15.4)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
-    oj (3.16.0)
+    oj (3.16.1)
     omniauth (2.1.1)
       hashie (>= 3.4.6)
       rack (>= 2.2.3)

From bb0edb178f56a488017071e00a994c9033267ef9 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 1 Sep 2023 10:48:49 +0200
Subject: [PATCH 64/81] Update dependency pg to v1.5.4 (#26750)

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

diff --git a/Gemfile.lock b/Gemfile.lock
index 6dd8c567f8..4e30c42222 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -519,7 +519,7 @@ GEM
     parslet (2.0.0)
     pastel (0.8.0)
       tty-color (~> 0.5)
-    pg (1.5.3)
+    pg (1.5.4)
     pghero (3.3.3)
       activerecord (>= 6)
     posix-spawn (0.3.15)

From 5c0a9aac3b2add81ff2a9c7e724dff4eb9c6451b Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 1 Sep 2023 15:09:44 +0200
Subject: [PATCH 65/81] Revert to using primary database in IndexingScheduler
 (#26754)

---
 app/workers/scheduler/indexing_scheduler.rb | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/app/workers/scheduler/indexing_scheduler.rb b/app/workers/scheduler/indexing_scheduler.rb
index 1b09730c7d..ff1b744442 100644
--- a/app/workers/scheduler/indexing_scheduler.rb
+++ b/app/workers/scheduler/indexing_scheduler.rb
@@ -16,9 +16,7 @@ class Scheduler::IndexingScheduler
     indexes.each do |type|
       with_redis do |redis|
         redis.sscan_each("chewy:queue:#{type.name}", count: SCAN_BATCH_SIZE).each_slice(IMPORT_BATCH_SIZE) do |ids|
-          with_read_replica do
-            type.import!(ids)
-          end
+          type.import!(ids)
 
           redis.srem("chewy:queue:#{type.name}", ids)
         end

From 6c4c72497a5722870e4432ef41dd4c9ec36a8928 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 1 Sep 2023 15:13:27 +0200
Subject: [PATCH 66/81] Fix search popout including full-text search
 instructions when full-text search is disabled (#26755)

---
 .../features/compose/components/search.jsx    | 22 +++++++++++--------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx
index 1c629bcbb4..848b812632 100644
--- a/app/javascript/mastodon/features/compose/components/search.jsx
+++ b/app/javascript/mastodon/features/compose/components/search.jsx
@@ -80,7 +80,7 @@ class Search extends PureComponent {
 
   handleKeyDown = (e) => {
     const { selectedOption } = this.state;
-    const options = this._getOptions().concat(this.defaultOptions);
+    const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
 
     switch(e.key) {
     case 'Escape':
@@ -353,15 +353,19 @@ class Search extends PureComponent {
             </>
           )}
 
-          <h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
+          {searchEnabled && (
+            <>
+              <h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
 
-          <div className='search__popout__menu'>
-            {this.defaultOptions.map(({ key, label, action }, i) => (
-              <button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === (options.length + i) })}>
-                {label}
-              </button>
-            ))}
-          </div>
+              <div className='search__popout__menu'>
+                {this.defaultOptions.map(({ key, label, action }, i) => (
+                  <button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === (options.length + i) })}>
+                    {label}
+                  </button>
+                ))}
+              </div>
+            </>
+          )}
         </div>
       </div>
     );

From 9e26cd55038084638fdf71b75f526494777d2849 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 1 Sep 2023 15:41:10 +0200
Subject: [PATCH 67/81] Add `authorized_fetch` server setting in addition to
 env var (#25798)

---
 app/controllers/application_controller.rb         |  5 +----
 app/helpers/authorized_fetch_helper.rb            | 11 +++++++++++
 app/javascript/styles/mastodon/accounts.scss      |  2 ++
 app/javascript/styles/mastodon/forms.scss         |  1 +
 app/models/form/admin_settings.rb                 | 10 ++++++++++
 app/services/concerns/payloadable.rb              |  4 +++-
 app/views/admin/settings/discovery/show.html.haml |  5 +++++
 config/i18n-tasks.yml                             |  2 +-
 config/initializers/simple_form.rb                |  5 +++--
 config/locales/en.yml                             |  5 +++++
 config/locales/simple_form.en.yml                 |  1 +
 11 files changed, 43 insertions(+), 8 deletions(-)
 create mode 100644 app/helpers/authorized_fetch_helper.rb

diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 975315e247..6ec93f824e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
   include CacheConcern
   include DomainControlHelper
   include DatabaseHelper
+  include AuthorizedFetchHelper
 
   helper_method :current_account
   helper_method :current_session
@@ -51,10 +52,6 @@ class ApplicationController < ActionController::Base
 
   private
 
-  def authorized_fetch_mode?
-    ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.limited_federation_mode
-  end
-
   def public_fetch_mode?
     !authorized_fetch_mode?
   end
diff --git a/app/helpers/authorized_fetch_helper.rb b/app/helpers/authorized_fetch_helper.rb
new file mode 100644
index 0000000000..ce87526e6a
--- /dev/null
+++ b/app/helpers/authorized_fetch_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module AuthorizedFetchHelper
+  def authorized_fetch_mode?
+    ENV.fetch('AUTHORIZED_FETCH') { Setting.authorized_fetch } == 'true' || Rails.configuration.x.limited_federation_mode
+  end
+
+  def authorized_fetch_overridden?
+    ENV.key?('AUTHORIZED_FETCH') || Rails.configuration.x.limited_federation_mode
+  end
+end
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index 2a5285ee02..80d6c13cef 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -188,6 +188,7 @@
 }
 
 .information-badge,
+.simple_form .overridden,
 .simple_form .recommended,
 .simple_form .not_recommended {
   display: inline-block;
@@ -204,6 +205,7 @@
 }
 
 .information-badge,
+.simple_form .overridden,
 .simple_form .recommended,
 .simple_form .not_recommended {
   background-color: rgba($ui-secondary-color, 0.1);
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index a7079c1457..0f8eecee01 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -103,6 +103,7 @@ code {
         }
       }
 
+      .overridden,
       .recommended,
       .not_recommended {
         position: absolute;
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index a6be55fd7b..7be026d85f 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -3,6 +3,8 @@
 class Form::AdminSettings
   include ActiveModel::Model
 
+  include AuthorizedFetchHelper
+
   KEYS = %i(
     site_contact_username
     site_contact_email
@@ -34,6 +36,7 @@ class Form::AdminSettings
     backups_retention_period
     status_page_url
     captcha_enabled
+    authorized_fetch
   ).freeze
 
   INTEGER_KEYS = %i(
@@ -54,6 +57,7 @@ class Form::AdminSettings
     noindex
     require_invite_text
     captcha_enabled
+    authorized_fetch
   ).freeze
 
   UPLOAD_KEYS = %i(
@@ -61,6 +65,10 @@ class Form::AdminSettings
     mascot
   ).freeze
 
+  OVERRIDEN_SETTINGS = {
+    authorized_fetch: :authorized_fetch_mode?,
+  }.freeze
+
   attr_accessor(*KEYS)
 
   validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) }
@@ -80,6 +88,8 @@ class Form::AdminSettings
 
       stored_value = if UPLOAD_KEYS.include?(key)
                        SiteUpload.where(var: key).first_or_initialize(var: key)
+                     elsif OVERRIDEN_SETTINGS.include?(key)
+                       public_send(OVERRIDEN_SETTINGS[key])
                      else
                        Setting.public_send(key)
                      end
diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb
index 1389a42ed6..bd9d9d74b5 100644
--- a/app/services/concerns/payloadable.rb
+++ b/app/services/concerns/payloadable.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 module Payloadable
+  include AuthorizedFetchHelper
+
   # @param [ActiveModelSerializers::Model] record
   # @param [ActiveModelSerializers::Serializer] serializer
   # @param [Hash] options
@@ -23,6 +25,6 @@ module Payloadable
   end
 
   def signing_enabled?
-    ENV['AUTHORIZED_FETCH'] != 'true' && !Rails.configuration.x.limited_federation_mode
+    !authorized_fetch_mode?
   end
 end
diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml
index c48e0fdc62..62011d5c56 100644
--- a/app/views/admin/settings/discovery/show.html.haml
+++ b/app/views/admin/settings/discovery/show.html.haml
@@ -39,6 +39,11 @@
   .fields-group
     = f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, recommended: :recommended
 
+  %h4= t('admin.settings.security.federation_authentication')
+
+  .fields-group
+    = f.input :authorized_fetch, as: :boolean, wrapper: :with_label, label: t('admin.settings.security.authorized_fetch'), warning_hint: authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil, hint: t('admin.settings.security.authorized_fetch_hint'), disabled: authorized_fetch_overridden?, recommended: authorized_fetch_overridden? ? :overridden : nil
+
   %h4= t('admin.settings.discovery.follow_recommendations')
 
   .fields-group
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index d0677b80fb..2d4487ce56 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -50,7 +50,7 @@ ignore_unused:
   - 'activerecord.errors.*'
   - '{devise,pagination,doorkeeper}.*'
   - '{date,datetime,time,number}.*'
-  - 'simple_form.{yes,no,recommended,not_recommended}'
+  - 'simple_form.{yes,no,recommended,not_recommended,overridden}'
   - 'simple_form.{placeholders,hints,labels}.*'
   - 'simple_form.{error_notification,required}.:'
   - 'errors.messages.*'
diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb
index 9d90cc6ca8..a7a2d251e5 100644
--- a/config/initializers/simple_form.rb
+++ b/config/initializers/simple_form.rb
@@ -97,7 +97,8 @@ SimpleForm.setup do |config|
       end
     end
 
-    b.use :hint,  wrap_with: { tag: :span, class: :hint }
+    b.use :warning_hint, wrap_with: { tag: :span, class: [:hint, 'warning-hint'] }
+    b.use :hint, wrap_with: { tag: :span, class: :hint }
     b.use :error, wrap_with: { tag: :span, class: :error }
   end
 
@@ -111,8 +112,8 @@ SimpleForm.setup do |config|
   config.wrappers :with_block_label, class: [:input, :with_block_label], hint_class: :field_with_hint, error_class: :field_with_errors do |b|
     b.use :html5
     b.use :label
-    b.use :hint, wrap_with: { tag: :span, class: :hint }
     b.use :warning_hint, wrap_with: { tag: :span, class: [:hint, 'warning-hint'] }
+    b.use :hint, wrap_with: { tag: :span, class: :hint }
     b.use :input, wrap_with: { tag: :div, class: :label_input }
     b.use :error, wrap_with: { tag: :span, class: :error }
   end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 8bdfd1ec91..693155d6ef 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -770,6 +770,11 @@ en:
           approved: Approval required for sign up
           none: Nobody can sign up
           open: Anyone can sign up
+      security:
+        authorized_fetch: Require authentication from federated servers
+        authorized_fetch_hint: Requiring authentication from federated servers enables stricter enforcement of both user-level and server-level blocks. However, this comes at the cost of a performance penalty, reduces the reach of your replies, and may introduce compatibility issues with some federated services. In addition, this will not prevent dedicated actors from fetching your public posts and accounts.
+        authorized_fetch_overridden_hint: You are currently unable to change this setting because it is overridden by an environment variable.
+        federation_authentication: Federation authentication enforcement
       title: Server settings
     site_uploads:
       delete: Delete uploaded file
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index efda7b778b..b1297606bc 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -317,6 +317,7 @@ en:
         url: Endpoint URL
     'no': 'No'
     not_recommended: Not recommended
+    overridden: Overridden
     recommended: Recommended
     required:
       mark: "*"

From be991f1d18006a4820c1e9ca6625bf2bd2bfedac Mon Sep 17 00:00:00 2001
From: Gabriel Simmer <github@gmem.ca>
Date: Fri, 1 Sep 2023 16:44:28 +0100
Subject: [PATCH 68/81] Move to ioredis for streaming (#26581)

Co-authored-by: Emelia Smith <ThisIsMissEm@users.noreply.github.com>
---
 package.json       |   2 +-
 streaming/index.js |  82 +++++++++++++++++-------------------
 yarn.lock          | 101 +++++++++++++++++++++------------------------
 3 files changed, 85 insertions(+), 100 deletions(-)

diff --git a/package.json b/package.json
index 4038831050..e92115610b 100644
--- a/package.json
+++ b/package.json
@@ -82,6 +82,7 @@
     "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",
@@ -118,7 +119,6 @@
     "react-swipeable-views": "^0.14.0",
     "react-textarea-autosize": "^8.4.1",
     "react-toggle": "^4.1.3",
-    "redis": "^4.6.5",
     "redux": "^4.2.1",
     "redux-immutable": "^4.0.0",
     "redux-thunk": "^2.4.2",
diff --git a/streaming/index.js b/streaming/index.js
index a241fa3280..c9fac063df 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -6,12 +6,12 @@ const url = require('url');
 
 const dotenv = require('dotenv');
 const express = require('express');
+const Redis = require('ioredis');
 const { JSDOM } = require('jsdom');
 const log = require('npmlog');
 const pg = require('pg');
 const dbUrlToConfig = require('pg-connection-string').parse;
 const metrics = require('prom-client');
-const redis = require('redis');
 const uuid = require('uuid');
 const WebSocket = require('ws');
 
@@ -24,30 +24,12 @@ dotenv.config({
 log.level = process.env.LOG_LEVEL || 'verbose';
 
 /**
- * @param {Object.<string, any>} defaultConfig
- * @param {string} redisUrl
+ * @param {Object.<string, any>} config
  */
-const redisUrlToClient = async (defaultConfig, redisUrl) => {
-  const config = defaultConfig;
-
-  let client;
-
-  if (!redisUrl) {
-    client = redis.createClient(config);
-  } else if (redisUrl.startsWith('unix://')) {
-    client = redis.createClient(Object.assign(config, {
-      socket: {
-        path: redisUrl.slice(7),
-      },
-    }));
-  } else {
-    client = redis.createClient(Object.assign(config, {
-      url: redisUrl,
-    }));
-  }
-
+const createRedisClient = async (config) => {
+  const { redisParams, redisUrl } = config;
+  const client = new Redis(redisUrl, redisParams);
   client.on('error', (err) => log.error('Redis Client Error!', err));
-  await client.connect();
 
   return client;
 };
@@ -147,23 +129,22 @@ const pgConfigFromEnv = (env) => {
  * @returns {Object.<string, any>} configuration for the Redis connection
  */
 const redisConfigFromEnv = (env) => {
-  const redisNamespace = env.REDIS_NAMESPACE || null;
+  // ioredis *can* transparently add prefixes for us, but it doesn't *in some cases*,
+  // which means we can't use it. But this is something that should be looked into.
+  const redisPrefix = env.REDIS_NAMESPACE ? `${env.REDIS_NAMESPACE}:` : '';
 
   const redisParams = {
-    socket: {
-      host: env.REDIS_HOST || '127.0.0.1',
-      port: env.REDIS_PORT || 6379,
-    },
-    database: env.REDIS_DB || 0,
+    host: env.REDIS_HOST || '127.0.0.1',
+    port: env.REDIS_PORT || 6379,
+    db: env.REDIS_DB || 0,
     password: env.REDIS_PASSWORD || undefined,
   };
 
-  if (redisNamespace) {
-    redisParams.namespace = redisNamespace;
+  // redisParams.path takes precedence over host and port.
+  if (env.REDIS_URL && env.REDIS_URL.startsWith('unix://')) {
+    redisParams.path = env.REDIS_URL.slice(7);
   }
 
-  const redisPrefix = redisNamespace ? `${redisNamespace}:` : '';
-
   return {
     redisParams,
     redisPrefix,
@@ -179,15 +160,15 @@ const startServer = async () => {
   const pgPool = new pg.Pool(pgConfigFromEnv(process.env));
   const server = http.createServer(app);
 
-  const { redisParams, redisUrl, redisPrefix } = redisConfigFromEnv(process.env);
-
   /**
    * @type {Object.<string, Array.<function(Object<string, any>): void>>}
    */
   const subs = {};
 
-  const redisSubscribeClient = await redisUrlToClient(redisParams, redisUrl);
-  const redisClient = await redisUrlToClient(redisParams, redisUrl);
+  const redisConfig = redisConfigFromEnv(process.env);
+  const redisSubscribeClient = await createRedisClient(redisConfig);
+  const redisClient = await createRedisClient(redisConfig);
+  const { redisPrefix } = redisConfig;
 
   // Collect metrics from Node.js
   metrics.collectDefaultMetrics();
@@ -277,13 +258,13 @@ const startServer = async () => {
   };
 
   /**
-   * @param {string} message
    * @param {string} channel
+   * @param {string} message
    */
-  const onRedisMessage = (message, channel) => {
+  const onRedisMessage = (channel, message) => {
     const callbacks = subs[channel];
 
-    log.silly(`New message on channel ${channel}`);
+    log.silly(`New message on channel ${redisPrefix}${channel}`);
 
     if (!callbacks) {
       return;
@@ -294,6 +275,7 @@ const startServer = async () => {
 
     callbacks.forEach(callback => callback(json));
   };
+  redisSubscribeClient.on("message", onRedisMessage);
 
   /**
    * @callback SubscriptionListener
@@ -312,8 +294,14 @@ const startServer = async () => {
 
     if (subs[channel].length === 0) {
       log.verbose(`Subscribe ${channel}`);
-      redisSubscribeClient.subscribe(channel, onRedisMessage);
-      redisSubscriptions.inc();
+      redisSubscribeClient.subscribe(channel, (err, count) => {
+        if (err) {
+          log.error(`Error subscribing to ${channel}`);
+        }
+        else {
+          redisSubscriptions.set(count);
+        }
+      });
     }
 
     subs[channel].push(callback);
@@ -334,8 +322,14 @@ const startServer = async () => {
 
     if (subs[channel].length === 0) {
       log.verbose(`Unsubscribe ${channel}`);
-      redisSubscribeClient.unsubscribe(channel);
-      redisSubscriptions.dec();
+      redisSubscribeClient.unsubscribe(channel, (err, count) => {
+        if (err) {
+          log.error(`Error unsubscribing to ${channel}`);
+        }
+        else {
+          redisSubscriptions.set(count);
+        }
+      });
       delete subs[channel];
     }
   };
diff --git a/yarn.lock b/yarn.lock
index 880ded9773..74ed5214e4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1452,6 +1452,11 @@
   resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
   integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 
+"@ioredis/commands@^1.1.1":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
+  integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
+
 "@isaacs/cliui@^8.0.2":
   version "8.0.2"
   resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -1786,40 +1791,6 @@
   resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.0.7.tgz#54af8d66160a8a7bf7d8f184703d2bf4b3fab914"
   integrity sha512-J2v5Ca7HgejO7diGKiDylaVDQKmbQ5FJih6Oo3hXuBKEuXlcaccJu64lj8MNVLaPVyZx0g4gaOQZQz95QEb/hg==
 
-"@redis/bloom@1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71"
-  integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==
-
-"@redis/client@1.5.9":
-  version "1.5.9"
-  resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.9.tgz#c4ee81bbfedb4f1d9c7c5e9859661b9388fb4021"
-  integrity sha512-SffgN+P1zdWJWSXBvJeynvEnmnZrYmtKSRW00xl8pOPFOMJjxRR9u0frSxJpPR6Y4V+k54blJjGW7FgxbTI7bQ==
-  dependencies:
-    cluster-key-slot "1.1.2"
-    generic-pool "3.9.0"
-    yallist "4.0.0"
-
-"@redis/graph@1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.0.tgz#cc2b82e5141a29ada2cce7d267a6b74baa6dd519"
-  integrity sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==
-
-"@redis/json@1.0.4":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.4.tgz#f372b5f93324e6ffb7f16aadcbcb4e5c3d39bda1"
-  integrity sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==
-
-"@redis/search@1.1.3":
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.3.tgz#b5a6837522ce9028267fe6f50762a8bcfd2e998b"
-  integrity sha512-4Dg1JjvCevdiCBTZqjhKkGoC5/BcB7k9j99kdMnaXFXg8x4eyOIVg9487CMv7/BUVkFLZCaIh8ead9mU15DNng==
-
-"@redis/time-series@1.0.5":
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.5.tgz#a6d70ef7a0e71e083ea09b967df0a0ed742bc6ad"
-  integrity sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==
-
 "@reduxjs/toolkit@^1.9.5":
   version "1.9.5"
   resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4"
@@ -4111,7 +4082,7 @@ clone-deep@^4.0.1:
     kind-of "^6.0.2"
     shallow-clone "^3.0.0"
 
-cluster-key-slot@1.1.2:
+cluster-key-slot@^1.1.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
   integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
@@ -4857,6 +4828,11 @@ delegates@^1.0.0:
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
   integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==
 
+denque@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
+  integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
+
 depd@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
@@ -6139,11 +6115,6 @@ gauge@^5.0.0:
     strip-ansi "^6.0.1"
     wide-align "^1.1.5"
 
-generic-pool@3.9.0:
-  version "3.9.0"
-  resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4"
-  integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==
-
 gensync@^1.0.0-beta.2:
   version "1.0.0-beta.2"
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -6823,6 +6794,21 @@ invariant@^2.2.2, invariant@^2.2.4:
   dependencies:
     loose-envify "^1.0.0"
 
+ioredis@^5.3.2:
+  version "5.3.2"
+  resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7"
+  integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==
+  dependencies:
+    "@ioredis/commands" "^1.1.1"
+    cluster-key-slot "^1.1.0"
+    debug "^4.3.4"
+    denque "^2.1.0"
+    lodash.defaults "^4.2.0"
+    lodash.isarguments "^3.1.0"
+    redis-errors "^1.2.0"
+    redis-parser "^3.0.0"
+    standard-as-callback "^2.1.0"
+
 ip-regex@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -10283,17 +10269,17 @@ redent@^4.0.0:
     indent-string "^5.0.0"
     strip-indent "^4.0.0"
 
-redis@^4.6.5:
-  version "4.6.8"
-  resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.8.tgz#54c5992e8a5ba512506fe9f53142cadc405547e7"
-  integrity sha512-S7qNkPUYrsofQ0ztWlTHSaK0Qqfl1y+WMIxrzeAGNG+9iUZB4HGeBgkHxE6uJJ6iXrkvLd1RVJ2nvu6H1sAzfQ==
+redis-errors@^1.0.0, redis-errors@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
+  integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
+
+redis-parser@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
+  integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
   dependencies:
-    "@redis/bloom" "1.2.0"
-    "@redis/client" "1.5.9"
-    "@redis/graph" "1.1.0"
-    "@redis/json" "1.0.4"
-    "@redis/search" "1.1.3"
-    "@redis/time-series" "1.0.5"
+    redis-errors "^1.0.0"
 
 redux-immutable@^4.0.0:
   version "4.0.0"
@@ -11211,6 +11197,11 @@ stacktrace-js@^2.0.2:
     stack-generator "^2.0.5"
     stacktrace-gps "^3.0.4"
 
+standard-as-callback@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
+  integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
+
 static-extend@^0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -12966,16 +12957,16 @@ y18n@^5.0.5:
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
   integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
 
-yallist@4.0.0, yallist@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
-  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
 yallist@^3.0.2:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
   integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
 
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
 yaml@^1.10.0:
   version "1.10.2"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"

From 16681e0f20e1f8584e11439953c8d59b322571f5 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 1 Sep 2023 17:47:07 +0200
Subject: [PATCH 69/81] Add admin notifications for new Mastodon versions
 (#26582)

---
 .../admin/software_updates_controller.rb      |  18 ++
 .../components/critical_update_banner.tsx     |  26 +++
 .../mastodon/features/home_timeline/index.jsx |  14 +-
 app/javascript/mastodon/initial_state.js      |   2 +
 app/javascript/mastodon/locales/en.json       |   3 +
 app/javascript/styles/mastodon/admin.scss     |   5 +
 .../styles/mastodon/components.scss           |  18 +-
 app/javascript/styles/mastodon/tables.scss    |   5 +
 app/lib/admin/system_check.rb                 |   1 +
 .../system_check/software_version_check.rb    |  27 +++
 app/mailers/admin_mailer.rb                   |  16 ++
 app/models/software_update.rb                 |  40 +++++
 app/models/user_settings.rb                   |   1 +
 app/policies/software_update_policy.rb        |   7 +
 app/presenters/initial_state_presenter.rb     |   6 +-
 app/serializers/initial_state_serializer.rb   |   2 +
 app/services/software_update_check_service.rb |  82 +++++++++
 .../admin/software_updates/index.html.haml    |  29 ++++
 .../new_critical_software_updates.text.erb    |   5 +
 .../new_software_updates.text.erb             |   5 +
 .../preferences/notifications/show.html.haml  |   6 +-
 .../software_update_check_scheduler.rb        |  11 ++
 config/locales/en.yml                         |  25 +++
 config/locales/simple_form.en.yml             |   6 +
 config/navigation.rb                          |   3 +
 config/routes/admin.rb                        |   2 +
 config/sidekiq.yml                            |   4 +
 .../20230822081029_create_software_updates.rb |  16 ++
 db/schema.rb                                  |  12 +-
 lib/mastodon/version.rb                       |   4 +
 lib/tasks/mastodon.rake                       |   4 +
 .../fabricators/software_update_fabricator.rb |   7 +
 spec/features/admin/software_updates_spec.rb  |  23 +++
 .../software_version_check_spec.rb            | 133 +++++++++++++++
 spec/mailers/admin_mailer_spec.rb             |  42 +++++
 spec/models/software_update_spec.rb           |  87 ++++++++++
 spec/policies/software_update_policy_spec.rb  |  25 +++
 .../software_update_check_service_spec.rb     | 158 ++++++++++++++++++
 .../software_update_check_scheduler_spec.rb   |  20 +++
 39 files changed, 892 insertions(+), 8 deletions(-)
 create mode 100644 app/controllers/admin/software_updates_controller.rb
 create mode 100644 app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx
 create mode 100644 app/lib/admin/system_check/software_version_check.rb
 create mode 100644 app/models/software_update.rb
 create mode 100644 app/policies/software_update_policy.rb
 create mode 100644 app/services/software_update_check_service.rb
 create mode 100644 app/views/admin/software_updates/index.html.haml
 create mode 100644 app/views/admin_mailer/new_critical_software_updates.text.erb
 create mode 100644 app/views/admin_mailer/new_software_updates.text.erb
 create mode 100644 app/workers/scheduler/software_update_check_scheduler.rb
 create mode 100644 db/migrate/20230822081029_create_software_updates.rb
 create mode 100644 spec/fabricators/software_update_fabricator.rb
 create mode 100644 spec/features/admin/software_updates_spec.rb
 create mode 100644 spec/lib/admin/system_check/software_version_check_spec.rb
 create mode 100644 spec/models/software_update_spec.rb
 create mode 100644 spec/policies/software_update_policy_spec.rb
 create mode 100644 spec/services/software_update_check_service_spec.rb
 create mode 100644 spec/workers/scheduler/software_update_check_scheduler_spec.rb

diff --git a/app/controllers/admin/software_updates_controller.rb b/app/controllers/admin/software_updates_controller.rb
new file mode 100644
index 0000000000..52d8cb41e6
--- /dev/null
+++ b/app/controllers/admin/software_updates_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Admin
+  class SoftwareUpdatesController < BaseController
+    before_action :check_enabled!
+
+    def index
+      authorize :software_update, :index?
+      @software_updates = SoftwareUpdate.all.sort_by(&:gem_version)
+    end
+
+    private
+
+    def check_enabled!
+      not_found unless SoftwareUpdate.check_enabled?
+    end
+  end
+end
diff --git a/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx b/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx
new file mode 100644
index 0000000000..d0dd2b6acd
--- /dev/null
+++ b/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx
@@ -0,0 +1,26 @@
+import { FormattedMessage } from 'react-intl';
+
+export const CriticalUpdateBanner = () => (
+  <div className='warning-banner'>
+    <div className='warning-banner__message'>
+      <h1>
+        <FormattedMessage
+          id='home.pending_critical_update.title'
+          defaultMessage='Critical security update available!'
+        />
+      </h1>
+      <p>
+        <FormattedMessage
+          id='home.pending_critical_update.body'
+          defaultMessage='Please update your Mastodon server as soon as possible!'
+        />{' '}
+        <a href='/admin/software_updates'>
+          <FormattedMessage
+            id='home.pending_critical_update.link'
+            defaultMessage='See updates'
+          />
+        </a>
+      </p>
+    </div>
+  </div>
+);
diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx
index 1cd6edd7aa..8ff0377946 100644
--- a/app/javascript/mastodon/features/home_timeline/index.jsx
+++ b/app/javascript/mastodon/features/home_timeline/index.jsx
@@ -14,7 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an
 import { IconWithBadge } from 'mastodon/components/icon_with_badge';
 import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
 import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
-import { me } from 'mastodon/initial_state';
+import { me, criticalUpdatesPending } from 'mastodon/initial_state';
 
 import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 import { expandHomeTimeline } from '../../actions/timelines';
@@ -23,6 +23,7 @@ import ColumnHeader from '../../components/column_header';
 import StatusListContainer from '../ui/containers/status_list_container';
 
 import { ColumnSettings } from './components/column_settings';
+import { CriticalUpdateBanner } from './components/critical_update_banner';
 import { ExplorePrompt } from './components/explore_prompt';
 
 const messages = defineMessages({
@@ -156,8 +157,9 @@ class HomeTimeline extends PureComponent {
     const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
     const pinned = !!columnId;
     const { signedIn } = this.context.identity;
+    const banners = [];
 
-    let announcementsButton, banner;
+    let announcementsButton;
 
     if (hasAnnouncements) {
       announcementsButton = (
@@ -173,8 +175,12 @@ class HomeTimeline extends PureComponent {
       );
     }
 
+    if (criticalUpdatesPending) {
+      banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
+    }
+
     if (tooSlow) {
-      banner = <ExplorePrompt />;
+      banners.push(<ExplorePrompt key='explore-prompt' />);
     }
 
     return (
@@ -196,7 +202,7 @@ class HomeTimeline extends PureComponent {
 
         {signedIn ? (
           <StatusListContainer
-            prepend={banner}
+            prepend={banners}
             alwaysPrepend
             trackScroll={!pinned}
             scrollKey={`home_timeline-${columnId}`}
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index 85792a4ea4..11cd2a1673 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -87,6 +87,7 @@
  * @typedef InitialState
  * @property {Record<string, Account>} accounts
  * @property {InitialStateLanguage[]} languages
+ * @property {boolean=} critical_updates_pending
  * @property {InitialStateMeta} meta
  */
 
@@ -140,6 +141,7 @@ export const useBlurhash = getMeta('use_blurhash');
 export const usePendingItems = getMeta('use_pending_items');
 export const version = getMeta('version');
 export const languages = initialState?.languages;
+export const criticalUpdatesPending = initialState?.critical_updates_pending;
 // @ts-expect-error
 export const statusPageUrl = getMeta('status_page_url');
 export const sso_redirect = getMeta('sso_redirect');
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 90bb9616f0..13cddba723 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -310,6 +310,9 @@
   "home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:",
   "home.explore_prompt.title": "This is your home base within Mastodon.",
   "home.hide_announcements": "Hide announcements",
+  "home.pending_critical_update.body": "Please update your Mastodon server as soon as possible!",
+  "home.pending_critical_update.link": "See updates",
+  "home.pending_critical_update.title": "Critical security update available!",
   "home.show_announcements": "Show announcements",
   "interaction_modal.description.favourite": "With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.",
   "interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index bbb6ffdff7..a65f35e7b1 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -143,6 +143,11 @@ $content-width: 840px;
         }
       }
 
+      .warning a {
+        color: $gold-star;
+        font-weight: 700;
+      }
+
       .simple-navigation-active-leaf a {
         color: $primary-text-color;
         background-color: $ui-highlight-color;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index f61cd059fe..10083a2a32 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -8860,7 +8860,8 @@ noscript {
   }
 }
 
-.dismissable-banner {
+.dismissable-banner,
+.warning-banner {
   position: relative;
   margin: 10px;
   margin-bottom: 5px;
@@ -8938,6 +8939,21 @@ noscript {
   }
 }
 
+.warning-banner {
+  border: 1px solid $warning-red;
+  background: rgba($warning-red, 0.15);
+
+  &__message {
+    h1 {
+      color: $warning-red;
+    }
+
+    a {
+      color: $primary-text-color;
+    }
+  }
+}
+
 .image {
   position: relative;
   overflow: hidden;
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index 38cfc87271..dd5b483ec4 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -12,6 +12,11 @@
     border-top: 1px solid $ui-base-color;
     text-align: start;
     background: darken($ui-base-color, 4%);
+
+    &.critical {
+      font-weight: 700;
+      color: $gold-star;
+    }
   }
 
   & > thead > tr > th {
diff --git a/app/lib/admin/system_check.rb b/app/lib/admin/system_check.rb
index 89dfcef9f1..25c88341a4 100644
--- a/app/lib/admin/system_check.rb
+++ b/app/lib/admin/system_check.rb
@@ -2,6 +2,7 @@
 
 class Admin::SystemCheck
   ACTIVE_CHECKS = [
+    Admin::SystemCheck::SoftwareVersionCheck,
     Admin::SystemCheck::MediaPrivacyCheck,
     Admin::SystemCheck::DatabaseSchemaCheck,
     Admin::SystemCheck::SidekiqProcessCheck,
diff --git a/app/lib/admin/system_check/software_version_check.rb b/app/lib/admin/system_check/software_version_check.rb
new file mode 100644
index 0000000000..e142feddf0
--- /dev/null
+++ b/app/lib/admin/system_check/software_version_check.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class Admin::SystemCheck::SoftwareVersionCheck < Admin::SystemCheck::BaseCheck
+  include RoutingHelper
+
+  def skip?
+    !current_user.can?(:view_devops) || !SoftwareUpdate.check_enabled?
+  end
+
+  def pass?
+    software_updates.empty?
+  end
+
+  def message
+    if software_updates.any?(&:urgent?)
+      Admin::SystemCheck::Message.new(:software_version_critical_check, nil, admin_software_updates_path, true)
+    else
+      Admin::SystemCheck::Message.new(:software_version_patch_check, nil, admin_software_updates_path)
+    end
+  end
+
+  private
+
+  def software_updates
+    @software_updates ||= SoftwareUpdate.pending_to_a.filter { |update| update.urgent? || update.patch_type? }
+  end
+end
diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb
index 5baf9b38a5..990b92c337 100644
--- a/app/mailers/admin_mailer.rb
+++ b/app/mailers/admin_mailer.rb
@@ -45,6 +45,22 @@ class AdminMailer < ApplicationMailer
     end
   end
 
+  def new_software_updates
+    locale_for_account(@me) do
+      mail subject: default_i18n_subject(instance: @instance)
+    end
+  end
+
+  def new_critical_software_updates
+    headers['Priority'] = 'urgent'
+    headers['X-Priority'] = '1'
+    headers['Importance'] = 'high'
+
+    locale_for_account(@me) do
+      mail subject: default_i18n_subject(instance: @instance)
+    end
+  end
+
   private
 
   def process_params
diff --git a/app/models/software_update.rb b/app/models/software_update.rb
new file mode 100644
index 0000000000..cb3a6df2ae
--- /dev/null
+++ b/app/models/software_update.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: software_updates
+#
+#  id            :bigint(8)        not null, primary key
+#  version       :string           not null
+#  urgent        :boolean          default(FALSE), not null
+#  type          :integer          default("patch"), not null
+#  release_notes :string           default(""), not null
+#  created_at    :datetime         not null
+#  updated_at    :datetime         not null
+#
+
+class SoftwareUpdate < ApplicationRecord
+  self.inheritance_column = nil
+
+  enum type: { patch: 0, minor: 1, major: 2 }, _suffix: :type
+
+  def gem_version
+    Gem::Version.new(version)
+  end
+
+  class << self
+    def check_enabled?
+      ENV['UPDATE_CHECK_URL'] != ''
+    end
+
+    def pending_to_a
+      return [] unless check_enabled?
+
+      all.to_a.filter { |update| update.gem_version > Mastodon::Version.gem_version }
+    end
+
+    def urgent_pending?
+      pending_to_a.any?(&:urgent?)
+    end
+  end
+end
diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb
index 678467c75d..030cbec4d8 100644
--- a/app/models/user_settings.rb
+++ b/app/models/user_settings.rb
@@ -44,6 +44,7 @@ class UserSettings
     setting :pending_account, default: true
     setting :trends, default: true
     setting :appeal, default: true
+    setting :software_updates, default: 'critical', in: %w(none critical patch all)
   end
 
   namespace :interactions do
diff --git a/app/policies/software_update_policy.rb b/app/policies/software_update_policy.rb
new file mode 100644
index 0000000000..dcb565814f
--- /dev/null
+++ b/app/policies/software_update_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class SoftwareUpdatePolicy < ApplicationPolicy
+  def index?
+    role.can?(:view_devops)
+  end
+end
diff --git a/app/presenters/initial_state_presenter.rb b/app/presenters/initial_state_presenter.rb
index b87cff51e1..222cc8566c 100644
--- a/app/presenters/initial_state_presenter.rb
+++ b/app/presenters/initial_state_presenter.rb
@@ -3,9 +3,13 @@
 class InitialStatePresenter < ActiveModelSerializers::Model
   attributes :settings, :push_subscription, :token,
              :current_account, :admin, :owner, :text, :visibility,
-             :disabled_account, :moved_to_account
+             :disabled_account, :moved_to_account, :critical_updates_pending
 
   def role
     current_account&.user_role
   end
+
+  def critical_updates_pending
+    role&.can?(:view_devops) && SoftwareUpdate.urgent_pending?
+  end
 end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 9660c941d0..56d45c588e 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -7,6 +7,8 @@ class InitialStateSerializer < ActiveModel::Serializer
              :media_attachments, :settings,
              :languages
 
+  attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? }
+
   has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
   has_one :role, serializer: REST::RoleSerializer
 
diff --git a/app/services/software_update_check_service.rb b/app/services/software_update_check_service.rb
new file mode 100644
index 0000000000..49b92f104d
--- /dev/null
+++ b/app/services/software_update_check_service.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+class SoftwareUpdateCheckService < BaseService
+  def call
+    clean_outdated_updates!
+    return unless SoftwareUpdate.check_enabled?
+
+    process_update_notices!(fetch_update_notices)
+  end
+
+  private
+
+  def clean_outdated_updates!
+    SoftwareUpdate.find_each do |software_update|
+      software_update.delete if Mastodon::Version.gem_version >= software_update.gem_version
+    rescue ArgumentError
+      software_update.delete
+    end
+  end
+
+  def fetch_update_notices
+    Request.new(:get, "#{api_url}?version=#{version}").add_headers('Accept' => 'application/json', 'User-Agent' => 'Mastodon update checker').perform do |res|
+      return Oj.load(res.body_with_limit, mode: :strict) if res.code == 200
+    end
+  rescue HTTP::Error, OpenSSL::SSL::SSLError, Oj::ParseError
+    nil
+  end
+
+  def api_url
+    ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check')
+  end
+
+  def version
+    @version ||= Mastodon::Version.to_s.split('+')[0]
+  end
+
+  def process_update_notices!(update_notices)
+    return if update_notices.blank? || update_notices['updatesAvailable'].blank?
+
+    # Clear notices that are not listed by the update server anymore
+    SoftwareUpdate.where.not(version: update_notices['updatesAvailable'].pluck('version')).delete_all
+
+    # Check if any of the notices is new, and issue notifications
+    known_versions = SoftwareUpdate.where(version: update_notices['updatesAvailable'].pluck('version')).pluck(:version)
+    new_update_notices = update_notices['updatesAvailable'].filter { |notice| known_versions.exclude?(notice['version']) }
+    return if new_update_notices.blank?
+
+    new_updates = new_update_notices.map do |notice|
+      SoftwareUpdate.create!(version: notice['version'], urgent: notice['urgent'], type: notice['type'], release_notes: notice['releaseNotes'])
+    end
+
+    notify_devops!(new_updates)
+  end
+
+  def should_notify_user?(user, urgent_version, patch_version)
+    case user.settings['notification_emails.software_updates']
+    when 'none'
+      false
+    when 'critical'
+      urgent_version
+    when 'patch'
+      urgent_version || patch_version
+    when 'all'
+      true
+    end
+  end
+
+  def notify_devops!(new_updates)
+    has_new_urgent_version = new_updates.any?(&:urgent?)
+    has_new_patch_version  = new_updates.any?(&:patch_type?)
+
+    User.those_who_can(:view_devops).includes(:account).find_each do |user|
+      next unless should_notify_user?(user, has_new_urgent_version, has_new_patch_version)
+
+      if has_new_urgent_version
+        AdminMailer.with(recipient: user.account).new_critical_software_updates.deliver_later
+      else
+        AdminMailer.with(recipient: user.account).new_software_updates.deliver_later
+      end
+    end
+  end
+end
diff --git a/app/views/admin/software_updates/index.html.haml b/app/views/admin/software_updates/index.html.haml
new file mode 100644
index 0000000000..7a223ee07b
--- /dev/null
+++ b/app/views/admin/software_updates/index.html.haml
@@ -0,0 +1,29 @@
+- content_for :page_title do
+  = t('admin.software_updates.title')
+
+.simple_form
+  %p.lead
+    = t('admin.software_updates.description')
+    = link_to t('admin.software_updates.documentation_link'), 'https://docs.joinmastodon.org/admin/upgrading/#automated_checks', target: '_new'
+
+%hr.spacer
+
+- unless @software_updates.empty?
+  .table-wrapper
+    %table.table
+      %thead
+        %tr
+          %th= t('admin.software_updates.version')
+          %th= t('admin.software_updates.type')
+          %th
+          %th
+      %tbody
+        - @software_updates.each do |update|
+          %tr
+            %td= update.version
+            %td= t("admin.software_updates.types.#{update.type}")
+            - if update.urgent?
+              %td.critical= t("admin.software_updates.critical_update")
+            - else
+              %td
+            %td= table_link_to 'link', t('admin.software_updates.release_notes'), update.release_notes
diff --git a/app/views/admin_mailer/new_critical_software_updates.text.erb b/app/views/admin_mailer/new_critical_software_updates.text.erb
new file mode 100644
index 0000000000..c901bc50f7
--- /dev/null
+++ b/app/views/admin_mailer/new_critical_software_updates.text.erb
@@ -0,0 +1,5 @@
+<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
+
+<%= raw t('admin_mailer.new_critical_software_updates.body') %>
+
+<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %>
diff --git a/app/views/admin_mailer/new_software_updates.text.erb b/app/views/admin_mailer/new_software_updates.text.erb
new file mode 100644
index 0000000000..2fc4d1a5f2
--- /dev/null
+++ b/app/views/admin_mailer/new_software_updates.text.erb
@@ -0,0 +1,5 @@
+<%= raw t('application_mailer.salutation', name: display_name(@me)) %>
+
+<%= raw t('admin_mailer.new_software_updates.body') %>
+
+<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %>
diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml
index 0913bda9ae..5cc101069c 100644
--- a/app/views/settings/preferences/notifications/show.html.haml
+++ b/app/views/settings/preferences/notifications/show.html.haml
@@ -22,7 +22,7 @@
     .fields-group
       = ff.input :always_send_emails, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_always_send_emails'), hint: I18n.t('simple_form.hints.defaults.setting_always_send_emails')
 
-    - if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies)
+    - if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies) || (SoftwareUpdate.check_enabled? && current_user.can?(:view_devops))
       %h4= t 'notifications.administration_emails'
 
       .fields-group
@@ -31,6 +31,10 @@
         = ff.input :'notification_emails.pending_account', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.pending_account') if current_user.can?(:manage_users)
         = ff.input :'notification_emails.trends', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.trending_tag') if current_user.can?(:manage_taxonomies)
 
+      - if SoftwareUpdate.check_enabled? && current_user.can?(:view_devops)
+        .fields-group
+          = ff.input :'notification_emails.software_updates', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.software_updates.label'), collection: %w(none critical patch all), label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") }, include_blank: false, hint: false
+
   %h4= t 'notifications.other_settings'
 
   .fields-group
diff --git a/app/workers/scheduler/software_update_check_scheduler.rb b/app/workers/scheduler/software_update_check_scheduler.rb
new file mode 100644
index 0000000000..c732bdedc0
--- /dev/null
+++ b/app/workers/scheduler/software_update_check_scheduler.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Scheduler::SoftwareUpdateCheckScheduler
+  include Sidekiq::Worker
+
+  sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.hour.to_i
+
+  def perform
+    SoftwareUpdateCheckService.new.call
+  end
+end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 693155d6ef..71e5fb843e 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -309,6 +309,7 @@ en:
       unpublish: Unpublish
       unpublished_msg: Announcement successfully unpublished!
       updated_msg: Announcement successfully updated!
+    critical_update_pending: Critical update pending
     custom_emojis:
       assign_category: Assign category
       by_domain: Domain
@@ -779,6 +780,18 @@ en:
     site_uploads:
       delete: Delete uploaded file
       destroyed_msg: Site upload successfully deleted!
+    software_updates:
+      critical_update: Critical — please update quickly
+      description: It is recommended to keep your Mastodon installation up to date to benefit from the latest fixes and features. Moreover, it is sometimes critical to update Mastodon in a timely manner to avoid security issues. For these reasons, Mastodon checks for updates every 30 minutes, and will notify you according to your e-mail notification preferences.
+      documentation_link: Learn more
+      release_notes: Release notes
+      title: Available updates
+      type: Type
+      types:
+        major: Major release
+        minor: Minor release
+        patch: Patch release — bugfixes and easy to apply changes
+      version: Version
     statuses:
       account: Author
       application: Application
@@ -843,6 +856,12 @@ en:
         message_html: You haven't defined any server rules.
       sidekiq_process_check:
         message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration
+      software_version_critical_check:
+        action: See available updates
+        message_html: A critical Mastodon update is available, please update as quickly as possible.
+      software_version_patch_check:
+        action: See available updates
+        message_html: A bugfix Mastodon update is available.
       upload_check_privacy_error:
         action: Check here for more information
         message_html: "<strong>Your web server is misconfigured. The privacy of your users is at risk.</strong>"
@@ -956,6 +975,9 @@ en:
       body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:"
       next_steps: You can approve the appeal to undo the moderation decision, or ignore it.
       subject: "%{username} is appealing a moderation decision on %{instance}"
+    new_critical_software_updates:
+      body: New critical versions of Mastodon have been released, you may want to update as soon as possible!
+      subject: Critical Mastodon updates are available for %{instance}!
     new_pending_account:
       body: The details of the new account are below. You can approve or reject this application.
       subject: New account up for review on %{instance} (%{username})
@@ -963,6 +985,9 @@ en:
       body: "%{reporter} has reported %{target}"
       body_remote: Someone from %{domain} has reported %{target}
       subject: New report for %{instance} (#%{id})
+    new_software_updates:
+      body: New Mastodon versions have been released, you may want to update!
+      subject: New Mastodon versions are available for %{instance}!
     new_trends:
       body: 'The following items need a review before they can be displayed publicly:'
       new_trending_links:
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index b1297606bc..0b718c5b65 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -291,6 +291,12 @@ en:
         pending_account: New account needs review
         reblog: Someone boosted your post
         report: New report is submitted
+        software_updates:
+          all: Notify on all updates
+          critical: Notify on critical updates only
+          label: A new Mastodon version is available
+          none: Never notify of updates (not recommended)
+          patch: Notify on bugfix updates
         trending_tag: New trend requires review
       rule:
         text: Rule
diff --git a/config/navigation.rb b/config/navigation.rb
index f608c2eea7..e86c695a98 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -3,6 +3,9 @@
 SimpleNavigation::Configuration.run do |navigation|
   navigation.items do |n|
     n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_path
+
+    n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.critical_update_pending')]), admin_software_updates_path, if: -> { ENV['UPDATE_CHECK_URL'] != '' && current_user.can?(:view_devops) && SoftwareUpdate.urgent_pending? }, html: { class: 'warning' }
+
     n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification|/settings/privacy}
 
     n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? } do |s|
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 4573878ede..207cb0580d 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -201,4 +201,6 @@ namespace :admin do
       end
     end
   end
+
+  resources :software_updates, only: [:index]
 end
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index 12c45c22a1..f1ba5651dd 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -58,3 +58,7 @@
       interval: 1 minute
       class: Scheduler::SuspendedUserCleanupScheduler
       queue: scheduler
+    software_update_check_scheduler:
+      interval: 30 minutes
+      class: Scheduler::SoftwareUpdateCheckScheduler
+      queue: scheduler
diff --git a/db/migrate/20230822081029_create_software_updates.rb b/db/migrate/20230822081029_create_software_updates.rb
new file mode 100644
index 0000000000..146d5d3037
--- /dev/null
+++ b/db/migrate/20230822081029_create_software_updates.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CreateSoftwareUpdates < ActiveRecord::Migration[7.0]
+  def change
+    create_table :software_updates do |t|
+      t.string :version, null: false
+      t.boolean :urgent, default: false, null: false
+      t.integer :type, default: 0, null: false
+      t.string :release_notes, default: '', null: false
+
+      t.timestamps
+    end
+
+    add_index :software_updates, :version, unique: true
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8b758fc7df..c861069420 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_08_18_142253) do
+ActiveRecord::Schema[7.0].define(version: 2023_08_22_081029) do
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
 
@@ -903,6 +903,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_18_142253) do
     t.index ["var"], name: "index_site_uploads_on_var", unique: true
   end
 
+  create_table "software_updates", force: :cascade do |t|
+    t.string "version", null: false
+    t.boolean "urgent", default: false, null: false
+    t.integer "type", default: 0, null: false
+    t.string "release_notes", default: "", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["version"], name: "index_software_updates_on_version", unique: true
+  end
+
   create_table "status_edits", force: :cascade do |t|
     t.bigint "status_id", null: false
     t.bigint "account_id"
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index c542d5d49a..65f90f93fd 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -39,6 +39,10 @@ module Mastodon
       components.join
     end
 
+    def gem_version
+      @gem_version ||= Gem::Version.new(to_s.split('+')[0])
+    end
+
     def repository
       ENV.fetch('GITHUB_REPOSITORY', 'mastodon/mastodon')
     end
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 010caaf8ea..f68d1cf1f8 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -424,6 +424,10 @@ namespace :mastodon do
         end
       end
 
+      prompt.say "\n"
+
+      env['UPDATE_CHECK_URL'] = '' unless prompt.yes?('Do you want Mastodon to periodically check for important updates and notify you? (Recommended)', default: true)
+
       prompt.say "\n"
       prompt.say 'This configuration will be written to .env.production'
 
diff --git a/spec/fabricators/software_update_fabricator.rb b/spec/fabricators/software_update_fabricator.rb
new file mode 100644
index 0000000000..622fff66e8
--- /dev/null
+++ b/spec/fabricators/software_update_fabricator.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+Fabricator(:software_update) do
+  version '99.99.99'
+  urgent false
+  type 'patch'
+end
diff --git a/spec/features/admin/software_updates_spec.rb b/spec/features/admin/software_updates_spec.rb
new file mode 100644
index 0000000000..4a635d1a79
--- /dev/null
+++ b/spec/features/admin/software_updates_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'finding software updates through the admin interface' do
+  before do
+    Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true, release_notes: 'https://github.com/mastodon/mastodon/releases/v99')
+
+    sign_in Fabricate(:user, role: UserRole.find_by(name: 'Owner')), scope: :user
+  end
+
+  it 'shows a link to the software updates page, which links to release notes' do
+    visit settings_profile_path
+    click_on I18n.t('admin.critical_update_pending')
+
+    expect(page).to have_title(I18n.t('admin.software_updates.title'))
+
+    expect(page).to have_content('99.99.99')
+
+    click_on I18n.t('admin.software_updates.release_notes')
+    expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true)
+  end
+end
diff --git a/spec/lib/admin/system_check/software_version_check_spec.rb b/spec/lib/admin/system_check/software_version_check_spec.rb
new file mode 100644
index 0000000000..de4335fc51
--- /dev/null
+++ b/spec/lib/admin/system_check/software_version_check_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Admin::SystemCheck::SoftwareVersionCheck do
+  include RoutingHelper
+
+  subject(:check) { described_class.new(user) }
+
+  let(:user) { Fabricate(:user) }
+
+  describe 'skip?' do
+    context 'when user cannot view devops' do
+      before { allow(user).to receive(:can?).with(:view_devops).and_return(false) }
+
+      it 'returns true' do
+        expect(check.skip?).to be true
+      end
+    end
+
+    context 'when user can view devops' do
+      before { allow(user).to receive(:can?).with(:view_devops).and_return(true) }
+
+      it 'returns false' do
+        expect(check.skip?).to be false
+      end
+
+      context 'when checks are disabled' do
+        around do |example|
+          ClimateControl.modify UPDATE_CHECK_URL: '' do
+            example.run
+          end
+        end
+
+        it 'returns true' do
+          expect(check.skip?).to be true
+        end
+      end
+    end
+  end
+
+  describe 'pass?' do
+    context 'when there is no known update' do
+      it 'returns true' do
+        expect(check.pass?).to be true
+      end
+    end
+
+    context 'when there is a non-urgent major release' do
+      before do
+        Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: false)
+      end
+
+      it 'returns true' do
+        expect(check.pass?).to be true
+      end
+    end
+
+    context 'when there is an urgent major release' do
+      before do
+        Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true)
+      end
+
+      it 'returns false' do
+        expect(check.pass?).to be false
+      end
+    end
+
+    context 'when there is an urgent minor release' do
+      before do
+        Fabricate(:software_update, version: '99.99.99', type: 'minor', urgent: true)
+      end
+
+      it 'returns false' do
+        expect(check.pass?).to be false
+      end
+    end
+
+    context 'when there is an urgent patch release' do
+      before do
+        Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: true)
+      end
+
+      it 'returns false' do
+        expect(check.pass?).to be false
+      end
+    end
+
+    context 'when there is a non-urgent patch release' do
+      before do
+        Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: false)
+      end
+
+      it 'returns false' do
+        expect(check.pass?).to be false
+      end
+    end
+  end
+
+  describe 'message' do
+    context 'when there is a non-urgent patch release pending' do
+      before do
+        Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: false)
+      end
+
+      it 'sends class name symbol to message instance' do
+        allow(Admin::SystemCheck::Message).to receive(:new)
+          .with(:software_version_patch_check, anything, anything)
+
+        check.message
+
+        expect(Admin::SystemCheck::Message).to have_received(:new)
+          .with(:software_version_patch_check, nil, admin_software_updates_path)
+      end
+    end
+
+    context 'when there is an urgent patch release pending' do
+      before do
+        Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: true)
+      end
+
+      it 'sends class name symbol to message instance' do
+        allow(Admin::SystemCheck::Message).to receive(:new)
+          .with(:software_version_critical_check, anything, anything, anything)
+
+        check.message
+
+        expect(Admin::SystemCheck::Message).to have_received(:new)
+          .with(:software_version_critical_check, nil, admin_software_updates_path, true)
+      end
+    end
+  end
+end
diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb
index 9123804a48..423dce88ab 100644
--- a/spec/mailers/admin_mailer_spec.rb
+++ b/spec/mailers/admin_mailer_spec.rb
@@ -85,4 +85,46 @@ RSpec.describe AdminMailer do
       expect(mail.body.encoded).to match 'The following items need a review before they can be displayed publicly'
     end
   end
+
+  describe '.new_software_updates' do
+    let(:recipient) { Fabricate(:account, username: 'Bob') }
+    let(:mail) { described_class.with(recipient: recipient).new_software_updates }
+
+    before 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!'
+    end
+  end
+
+  describe '.new_critical_software_updates' do
+    let(:recipient) { Fabricate(:account, username: 'Bob') }
+    let(:mail) { described_class.with(recipient: recipient).new_critical_software_updates }
+
+    before 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!'
+    end
+  end
 end
diff --git a/spec/models/software_update_spec.rb b/spec/models/software_update_spec.rb
new file mode 100644
index 0000000000..0a494b0c4c
--- /dev/null
+++ b/spec/models/software_update_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SoftwareUpdate do
+  describe '.pending_to_a' do
+    before do
+      allow(Mastodon::Version).to receive(:gem_version).and_return(Gem::Version.new(mastodon_version))
+
+      Fabricate(:software_update, version: '3.4.42', type: 'patch', urgent: true)
+      Fabricate(:software_update, version: '3.5.0', type: 'minor', urgent: false)
+      Fabricate(:software_update, version: '4.2.0', type: 'major', urgent: false)
+    end
+
+    context 'when the Mastodon version is an outdated release' do
+      let(:mastodon_version) { '3.4.0' }
+
+      it 'returns the expected versions' do
+        expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('3.4.42', '3.5.0', '4.2.0')
+      end
+    end
+
+    context 'when the Mastodon version is more recent than anything last returned by the server' do
+      let(:mastodon_version) { '5.0.0' }
+
+      it 'returns the expected versions' do
+        expect(described_class.pending_to_a.pluck(:version)).to eq []
+      end
+    end
+
+    context 'when the Mastodon version is an outdated nightly' do
+      let(:mastodon_version) { '4.3.0-nightly.2023-09-10' }
+
+      before do
+        Fabricate(:software_update, version: '4.3.0-nightly.2023-09-12', type: 'major', urgent: true)
+      end
+
+      it 'returns the expected versions' do
+        expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-nightly.2023-09-12')
+      end
+    end
+
+    context 'when the Mastodon version is a very outdated nightly' do
+      let(:mastodon_version) { '4.2.0-nightly.2023-07-10' }
+
+      it 'returns the expected versions' do
+        expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.2.0')
+      end
+    end
+
+    context 'when the Mastodon version is an outdated dev version' do
+      let(:mastodon_version) { '4.3.0-0.dev.0' }
+
+      before do
+        Fabricate(:software_update, version: '4.3.0-0.dev.2', type: 'major', urgent: true)
+      end
+
+      it 'returns the expected versions' do
+        expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-0.dev.2')
+      end
+    end
+
+    context 'when the Mastodon version is an outdated beta version' do
+      let(:mastodon_version) { '4.3.0-beta1' }
+
+      before do
+        Fabricate(:software_update, version: '4.3.0-beta2', type: 'major', urgent: true)
+      end
+
+      it 'returns the expected versions' do
+        expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-beta2')
+      end
+    end
+
+    context 'when the Mastodon version is an outdated beta version and there is a rc' do
+      let(:mastodon_version) { '4.3.0-beta1' }
+
+      before do
+        Fabricate(:software_update, version: '4.3.0-rc1', type: 'major', urgent: true)
+      end
+
+      it 'returns the expected versions' do
+        expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-rc1')
+      end
+    end
+  end
+end
diff --git a/spec/policies/software_update_policy_spec.rb b/spec/policies/software_update_policy_spec.rb
new file mode 100644
index 0000000000..e19ba61612
--- /dev/null
+++ b/spec/policies/software_update_policy_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+require 'pundit/rspec'
+
+RSpec.describe SoftwareUpdatePolicy do
+  subject { described_class }
+
+  let(:admin)   { Fabricate(:user, role: UserRole.find_by(name: 'Owner')).account }
+  let(:john)    { Fabricate(:account) }
+
+  permissions :index? do
+    context 'when owner' do
+      it 'permits' do
+        expect(subject).to permit(admin, SoftwareUpdate)
+      end
+    end
+
+    context 'when not owner' do
+      it 'denies' do
+        expect(subject).to_not permit(john, SoftwareUpdate)
+      end
+    end
+  end
+end
diff --git a/spec/services/software_update_check_service_spec.rb b/spec/services/software_update_check_service_spec.rb
new file mode 100644
index 0000000000..c8821348ac
--- /dev/null
+++ b/spec/services/software_update_check_service_spec.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe SoftwareUpdateCheckService, type: :service do
+  subject { described_class.new }
+
+  shared_examples 'when the feature is enabled' do
+    let(:full_update_check_url) { "#{update_check_url}?version=#{Mastodon::Version.to_s.split('+')[0]}" }
+
+    let(:devops_role)     { Fabricate(:user_role, name: 'DevOps', permissions: UserRole::FLAGS[:view_devops]) }
+    let(:owner_user)      { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) }
+    let(:old_devops_user) { Fabricate(:user) }
+    let(:none_user)       { Fabricate(:user, role: devops_role) }
+    let(:patch_user)      { Fabricate(:user, role: devops_role) }
+    let(:critical_user)   { Fabricate(:user, role: devops_role) }
+
+    around do |example|
+      queue_adapter = ActiveJob::Base.queue_adapter
+      ActiveJob::Base.queue_adapter = :test
+
+      example.run
+
+      ActiveJob::Base.queue_adapter = queue_adapter
+    end
+
+    before do
+      Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false)
+      Fabricate(:software_update, version: '42.13.12', type: 'major', urgent: false)
+
+      owner_user.settings.update('notification_emails.software_updates': 'all')
+      owner_user.save!
+
+      old_devops_user.settings.update('notification_emails.software_updates': 'all')
+      old_devops_user.save!
+
+      none_user.settings.update('notification_emails.software_updates': 'none')
+      none_user.save!
+
+      patch_user.settings.update('notification_emails.software_updates': 'patch')
+      patch_user.save!
+
+      critical_user.settings.update('notification_emails.software_updates': 'critical')
+      critical_user.save!
+    end
+
+    context 'when the update server errors out' do
+      before do
+        stub_request(:get, full_update_check_url).to_return(status: 404)
+      end
+
+      it 'deletes outdated update records but keeps valid update records' do
+        expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['42.13.12'])
+      end
+    end
+
+    context 'when the server returns new versions' do
+      let(:server_json) do
+        {
+          updatesAvailable: [
+            {
+              version: '4.2.1',
+              urgent: false,
+              type: 'patch',
+              releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.2.1',
+            },
+            {
+              version: '4.3.0',
+              urgent: false,
+              type: 'minor',
+              releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.3.0',
+            },
+            {
+              version: '5.0.0',
+              urgent: false,
+              type: 'minor',
+              releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0',
+            },
+          ],
+        }
+      end
+
+      before do
+        stub_request(:get, full_update_check_url).to_return(body: Oj.dump(server_json))
+      end
+
+      it 'updates the list of known updates' do
+        expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['4.2.1', '4.3.0', '5.0.0'])
+      end
+
+      context 'when no update is urgent' do
+        it 'sends e-mail notifications according to settings', :aggregate_failures do
+          expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_software_updates)
+            .with(hash_including(params: { recipient: owner_user.account })).once
+            .and(have_enqueued_mail(AdminMailer, :new_software_updates).with(hash_including(params: { recipient: patch_user.account })).once)
+            .and(have_enqueued_mail.at_most(2))
+        end
+      end
+
+      context 'when an update is urgent' do
+        let(:server_json) do
+          {
+            updatesAvailable: [
+              {
+                version: '5.0.0',
+                urgent: true,
+                type: 'minor',
+                releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0',
+              },
+            ],
+          }
+        end
+
+        it 'sends e-mail notifications according to settings', :aggregate_failures do
+          expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_critical_software_updates)
+            .with(hash_including(params: { recipient: owner_user.account })).once
+            .and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: patch_user.account })).once)
+            .and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: critical_user.account })).once)
+            .and(have_enqueued_mail.at_most(3))
+        end
+      end
+    end
+  end
+
+  context 'when update checking is disabled' do
+    around do |example|
+      ClimateControl.modify UPDATE_CHECK_URL: '' do
+        example.run
+      end
+    end
+
+    before do
+      Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false)
+    end
+
+    it 'deletes outdated update records' do
+      expect { subject.call }.to change(SoftwareUpdate, :count).from(1).to(0)
+    end
+  end
+
+  context 'when using the default update checking API' do
+    let(:update_check_url) { 'https://api.joinmastodon.org/update-check' }
+
+    it_behaves_like 'when the feature is enabled'
+  end
+
+  context 'when using a custom update check URL' do
+    let(:update_check_url) { 'https://api.example.com/update_check' }
+
+    around do |example|
+      ClimateControl.modify UPDATE_CHECK_URL: 'https://api.example.com/update_check' do
+        example.run
+      end
+    end
+
+    it_behaves_like 'when the feature is enabled'
+  end
+end
diff --git a/spec/workers/scheduler/software_update_check_scheduler_spec.rb b/spec/workers/scheduler/software_update_check_scheduler_spec.rb
new file mode 100644
index 0000000000..f596c0a1ec
--- /dev/null
+++ b/spec/workers/scheduler/software_update_check_scheduler_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Scheduler::SoftwareUpdateCheckScheduler do
+  subject { described_class.new }
+
+  describe 'perform' do
+    let(:service_double) { instance_double(SoftwareUpdateCheckService, call: nil) }
+
+    before do
+      allow(SoftwareUpdateCheckService).to receive(:new).and_return(service_double)
+    end
+
+    it 'calls SoftwareUpdateCheckService' do
+      subject.perform
+      expect(service_double).to have_received(:call)
+    end
+  end
+end

From 05093266e6e3c54f9096da9cdcdafdc83703c578 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Sat, 2 Sep 2023 09:02:44 +0200
Subject: [PATCH 70/81] Fix some video encoding failing due to uneven
 dimensions (#26766)

---
 app/models/media_attachment.rb | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 984f4252a1..f0b072e23f 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -102,6 +102,7 @@ class MediaAttachment < ApplicationRecord
         'preset' => 'veryfast',
         'movflags' => 'faststart', # Move metadata to start of file so playback can begin before download finishes
         'pix_fmt' => 'yuv420p', # Ensure color space for cross-browser compatibility
+        'vf' => 'crop=floor(iw/2)*2:floor(ih/2)*2', # h264 requires width and height to be even. Crop instead of scale to avoid blurring
         'c:v' => 'h264',
         'c:a' => 'aac',
         'b:a' => '192k',

From 023673c0d9956065f29d9c080d5a0c27460a39f6 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 25 Aug 2023 11:31:20 +0200
Subject: [PATCH 71/81] [Glitch] Add `data-nosnippet` attribute to remote posts
 and local posts with `noindex`

Port c3a42e1280759ea28bee11f241aef892b148bf6a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 app/javascript/flavours/glitch/components/status.jsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx
index d3d432ae05..36abc69930 100644
--- a/app/javascript/flavours/glitch/components/status.jsx
+++ b/app/javascript/flavours/glitch/components/status.jsx
@@ -792,6 +792,7 @@ class Status extends ImmutablePureComponent {
           tabIndex={0}
           data-featured={featured ? 'true' : null}
           aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
+          data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}
         >
           {!muted && prepend}
 

From cb50d95c067fc82b788ad6a762ad339de7d1c3fa Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 28 Aug 2023 13:18:39 +0200
Subject: [PATCH 72/81] [Glitch] Add search options to search popout in web UI

Port bceb8931591b4b9df6e8dcfb47864c230884262c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../features/compose/components/search.jsx    | 50 ++++++++++++++++---
 .../glitch/styles/components/search.scss      |  6 +++
 2 files changed, 48 insertions(+), 8 deletions(-)

diff --git a/app/javascript/flavours/glitch/features/compose/components/search.jsx b/app/javascript/flavours/glitch/features/compose/components/search.jsx
index 2f1b46e5d9..218518a7b6 100644
--- a/app/javascript/flavours/glitch/features/compose/components/search.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/search.jsx
@@ -1,11 +1,7 @@
 import PropTypes from 'prop-types';
 import { PureComponent } from 'react';
 
-import {
-  injectIntl,
-  FormattedMessage,
-  defineMessages,
-} from 'react-intl';
+import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
 
 import classNames from 'classnames';
 
@@ -52,6 +48,16 @@ class Search extends PureComponent {
     options: [],
   };
 
+  defaultOptions = [
+    { label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:') } },
+    { label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:') } },
+    { label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:') } },
+    { label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:') } },
+    { label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:') } },
+    { label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:') } },
+    { label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:') } },
+  ];
+
   setRef = c => {
     this.searchForm = c;
   };
@@ -100,7 +106,7 @@ class Search extends PureComponent {
 
   handleKeyDown = (e) => {
     const { selectedOption } = this.state;
-    const options = this._getOptions();
+    const options = this._getOptions().concat(this.defaultOptions);
 
     switch(e.key) {
     case 'Escape':
@@ -131,10 +137,9 @@ class Search extends PureComponent {
       if (selectedOption === -1) {
         this._submit();
       } else if (options.length > 0) {
-        options[selectedOption].action();
+        options[selectedOption].action(e);
       }
 
-      this._unfocus();
       break;
     case 'Delete':
       if (selectedOption > -1 && options.length > 0) {
@@ -161,6 +166,7 @@ class Search extends PureComponent {
 
     router.history.push(`/tags/${query}`);
     onClickSearchResult(query, 'hashtag');
+    this._unfocus();
   };
 
   handleAccountClick = () => {
@@ -171,6 +177,7 @@ class Search extends PureComponent {
 
     router.history.push(`/@${query}`);
     onClickSearchResult(query, 'account');
+    this._unfocus();
   };
 
   handleURLClick = () => {
@@ -178,6 +185,7 @@ class Search extends PureComponent {
     const { onOpenURL } = this.props;
 
     onOpenURL(router.history);
+    this._unfocus();
   };
 
   handleStatusSearch = () => {
@@ -196,6 +204,8 @@ class Search extends PureComponent {
     } else if (search.get('type') === 'hashtag') {
       router.history.push(`/tags/${search.get('q')}`);
     }
+
+    this._unfocus();
   };
 
   handleForgetRecentSearchClick = search => {
@@ -208,6 +218,18 @@ class Search extends PureComponent {
     document.querySelector('.ui').parentElement.focus();
   }
 
+  _insertText (text) {
+    const { value, onChange } = this.props;
+
+    if (value === '') {
+      onChange(text);
+    } else if (value[value.length - 1] === ' ') {
+      onChange(`${value}${text}`);
+    } else {
+      onChange(`${value} ${text}`);
+    }
+  }
+
   _submit (type) {
     const { onSubmit, openInRoute } = this.props;
     const { router } = this.context;
@@ -217,6 +239,8 @@ class Search extends PureComponent {
     if (openInRoute) {
       router.history.push('/search');
     }
+
+    this._unfocus();
   }
 
   _getOptions () {
@@ -337,6 +361,16 @@ class Search extends PureComponent {
               </div>
             </>
           )}
+
+          <h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
+
+          <div className='search__popout__menu'>
+            {this.defaultOptions.map(({ key, label, action }, i) => (
+              <button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === (options.length + i) })}>
+                {label}
+              </button>
+            ))}
+          </div>
         </div>
       </div>
     );
diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss
index 99ed697e37..be35077dcd 100644
--- a/app/javascript/flavours/glitch/styles/components/search.scss
+++ b/app/javascript/flavours/glitch/styles/components/search.scss
@@ -25,6 +25,12 @@
     }
 
     &__menu {
+      margin-bottom: 20px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
       &__message {
         color: $dark-text-color;
         padding: 0 10px;

From 871ab8485434d9c89dddb14a2ee1fda8ad60035e Mon Sep 17 00:00:00 2001
From: Tim Rogers <rogers.timothy.john@gmail.com>
Date: Tue, 29 Aug 2023 03:56:19 -0500
Subject: [PATCH 73/81] [Glitch] Fix bug with favourited view on Toots only
 showing latest favouriting accounts

Port ae6cf33321a9f240ef73666a552e552b65390012 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../flavours/glitch/actions/interactions.js   | 58 ++++++++++++++++++-
 .../glitch/features/favourites/index.jsx      | 25 +++++---
 .../flavours/glitch/reducers/user_lists.js    | 15 ++++-
 3 files changed, 85 insertions(+), 13 deletions(-)

diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js
index 6b8864a039..78f7741ab8 100644
--- a/app/javascript/flavours/glitch/actions/interactions.js
+++ b/app/javascript/flavours/glitch/actions/interactions.js
@@ -1,5 +1,6 @@
-import api from '../api';
+import api, { getLinks } from '../api';
 
+import { fetchRelationships } from './accounts';
 import { importFetchedAccounts, importFetchedStatus } from './importer';
 
 export const REBLOG_REQUEST = 'REBLOG_REQUEST';
@@ -26,6 +27,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
 export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
 export const FAVOURITES_FETCH_FAIL    = 'FAVOURITES_FETCH_FAIL';
 
+export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
+export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
+export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
+
 export const PIN_REQUEST = 'PIN_REQUEST';
 export const PIN_SUCCESS = 'PIN_SUCCESS';
 export const PIN_FAIL    = 'PIN_FAIL';
@@ -294,8 +299,10 @@ export function fetchFavourites(id) {
     dispatch(fetchFavouritesRequest(id));
 
     api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
       dispatch(importFetchedAccounts(response.data));
-      dispatch(fetchFavouritesSuccess(id, response.data));
+      dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => {
       dispatch(fetchFavouritesFail(id, error));
     });
@@ -309,17 +316,62 @@ export function fetchFavouritesRequest(id) {
   };
 }
 
-export function fetchFavouritesSuccess(id, accounts) {
+export function fetchFavouritesSuccess(id, accounts, next) {
   return {
     type: FAVOURITES_FETCH_SUCCESS,
     id,
     accounts,
+    next,
   };
 }
 
 export function fetchFavouritesFail(id, error) {
   return {
     type: FAVOURITES_FETCH_FAIL,
+    id,
+    error,
+  };
+}
+
+export function expandFavourites(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'favourited_by', id, 'next']);
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandFavouritesRequest(id));
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => dispatch(expandFavouritesFail(id, error)));
+  };
+}
+
+export function expandFavouritesRequest(id) {
+  return {
+    type: FAVOURITES_EXPAND_REQUEST,
+    id,
+  };
+}
+
+export function expandFavouritesSuccess(id, accounts, next) {
+  return {
+    type: FAVOURITES_EXPAND_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function expandFavouritesFail(id, error) {
+  return {
+    type: FAVOURITES_EXPAND_FAIL,
+    id,
     error,
   };
 }
diff --git a/app/javascript/flavours/glitch/features/favourites/index.jsx b/app/javascript/flavours/glitch/features/favourites/index.jsx
index 2b36945eee..49fd62b966 100644
--- a/app/javascript/flavours/glitch/features/favourites/index.jsx
+++ b/app/javascript/flavours/glitch/features/favourites/index.jsx
@@ -8,7 +8,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
 
-import { fetchFavourites } from 'flavours/glitch/actions/interactions';
+import { debounce } from 'lodash';
+
+import { fetchFavourites, expandFavourites } from 'flavours/glitch/actions/interactions';
 import ColumnHeader from 'flavours/glitch/components/column_header';
 import { Icon } from 'flavours/glitch/components/icon';
 import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
@@ -23,7 +25,9 @@ const messages = defineMessages({
 });
 
 const mapStateToProps = (state, props) => ({
-  accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
+  accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'items']),
+  hasMore: !!state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'next']),
+  isLoading: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'isLoading'], true),
 });
 
 class Favourites extends ImmutablePureComponent {
@@ -32,6 +36,8 @@ class Favourites extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     accountIds: ImmutablePropTypes.list,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
     multiColumn: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
@@ -42,12 +48,6 @@ class Favourites extends ImmutablePureComponent {
     }
   }
 
-  UNSAFE_componentWillReceiveProps (nextProps) {
-    if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
-      this.props.dispatch(fetchFavourites(nextProps.params.statusId));
-    }
-  }
-
   handleHeaderClick = () => {
     this.column.scrollTop();
   };
@@ -60,8 +60,12 @@ class Favourites extends ImmutablePureComponent {
     this.props.dispatch(fetchFavourites(this.props.params.statusId));
   };
 
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandFavourites(this.props.params.statusId));
+  }, 300, { leading: true });
+
   render () {
-    const { intl, accountIds, multiColumn } = this.props;
+    const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -87,6 +91,9 @@ class Favourites extends ImmutablePureComponent {
         />
         <ScrollableList
           scrollKey='favourites'
+          onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
+          isLoading={isLoading}
           emptyMessage={emptyMessage}
           bindToDocument={!multiColumn}
         >
diff --git a/app/javascript/flavours/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js
index dd240e99d4..a10fed4334 100644
--- a/app/javascript/flavours/glitch/reducers/user_lists.js
+++ b/app/javascript/flavours/glitch/reducers/user_lists.js
@@ -45,7 +45,12 @@ import {
 } from 'flavours/glitch/actions/featured_tags';
 import {
   REBLOGS_FETCH_SUCCESS,
+  FAVOURITES_FETCH_REQUEST,
   FAVOURITES_FETCH_SUCCESS,
+  FAVOURITES_FETCH_FAIL,
+  FAVOURITES_EXPAND_REQUEST,
+  FAVOURITES_EXPAND_SUCCESS,
+  FAVOURITES_EXPAND_FAIL,
 } from 'flavours/glitch/actions/interactions';
 import {
   MUTES_FETCH_REQUEST,
@@ -135,7 +140,15 @@ export default function userLists(state = initialState, action) {
   case REBLOGS_FETCH_SUCCESS:
     return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
   case FAVOURITES_FETCH_SUCCESS:
-    return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
+    return normalizeList(state, ['favourited_by', action.id], action.accounts, action.next);
+  case FAVOURITES_EXPAND_SUCCESS:
+    return appendToList(state, ['favourited_by', action.id], action.accounts, action.next);
+  case FAVOURITES_FETCH_REQUEST:
+  case FAVOURITES_EXPAND_REQUEST:
+    return state.setIn(['favourited_by', action.id, 'isLoading'], true);
+  case FAVOURITES_FETCH_FAIL:
+  case FAVOURITES_EXPAND_FAIL:
+    return state.setIn(['favourited_by', action.id, 'isLoading'], false);
   case NOTIFICATIONS_UPDATE:
     return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
   case FOLLOW_REQUESTS_FETCH_SUCCESS:

From beb5fcd0dc158218fd3c82e7d545a53f7dc904b7 Mon Sep 17 00:00:00 2001
From: Tim Rogers <rogers.timothy.john@gmail.com>
Date: Tue, 29 Aug 2023 07:42:20 -0500
Subject: [PATCH 74/81] [Glitch] Fix bug with reblogged view on Toots only
 showing latest reblogging accounts

Port 74eb7dbf2d79b74f7d6f09ca3d39b3ba67f5f7bf to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../flavours/glitch/actions/interactions.js   | 55 ++++++++++++++++++-
 .../glitch/features/reblogs/index.jsx         | 29 +++++-----
 .../flavours/glitch/reducers/user_lists.js    | 15 ++++-
 3 files changed, 83 insertions(+), 16 deletions(-)

diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js
index 78f7741ab8..095fb3155e 100644
--- a/app/javascript/flavours/glitch/actions/interactions.js
+++ b/app/javascript/flavours/glitch/actions/interactions.js
@@ -7,6 +7,10 @@ export const REBLOG_REQUEST = 'REBLOG_REQUEST';
 export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
 export const REBLOG_FAIL    = 'REBLOG_FAIL';
 
+export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
+export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
+export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
+
 export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
 export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
 export const FAVOURITE_FAIL    = 'FAVOURITE_FAIL';
@@ -264,8 +268,10 @@ export function fetchReblogs(id) {
     dispatch(fetchReblogsRequest(id));
 
     api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
       dispatch(importFetchedAccounts(response.data));
-      dispatch(fetchReblogsSuccess(id, response.data));
+      dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
     }).catch(error => {
       dispatch(fetchReblogsFail(id, error));
     });
@@ -279,17 +285,62 @@ export function fetchReblogsRequest(id) {
   };
 }
 
-export function fetchReblogsSuccess(id, accounts) {
+export function fetchReblogsSuccess(id, accounts, next) {
   return {
     type: REBLOGS_FETCH_SUCCESS,
     id,
     accounts,
+    next,
   };
 }
 
 export function fetchReblogsFail(id, error) {
   return {
     type: REBLOGS_FETCH_FAIL,
+    id,
+    error,
+  };
+}
+
+export function expandReblogs(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'reblogged_by', id, 'next']);
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandReblogsRequest(id));
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
+      dispatch(fetchRelationships(response.data.map(item => item.id)));
+    }).catch(error => dispatch(expandReblogsFail(id, error)));
+  };
+}
+
+export function expandReblogsRequest(id) {
+  return {
+    type: REBLOGS_EXPAND_REQUEST,
+    id,
+  };
+}
+
+export function expandReblogsSuccess(id, accounts, next) {
+  return {
+    type: REBLOGS_EXPAND_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function expandReblogsFail(id, error) {
+  return {
+    type: REBLOGS_EXPAND_FAIL,
+    id,
     error,
   };
 }
diff --git a/app/javascript/flavours/glitch/features/reblogs/index.jsx b/app/javascript/flavours/glitch/features/reblogs/index.jsx
index 90d10db628..8cc4c004f0 100644
--- a/app/javascript/flavours/glitch/features/reblogs/index.jsx
+++ b/app/javascript/flavours/glitch/features/reblogs/index.jsx
@@ -8,7 +8,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
 
-import { fetchReblogs } from 'flavours/glitch/actions/interactions';
+import { debounce } from 'lodash';
+
+import { fetchReblogs, expandReblogs } from 'flavours/glitch/actions/interactions';
 import ColumnHeader from 'flavours/glitch/components/column_header';
 import { Icon } from 'flavours/glitch/components/icon';
 import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
@@ -16,17 +18,15 @@ import ScrollableList from 'flavours/glitch/components/scrollable_list';
 import AccountContainer from 'flavours/glitch/containers/account_container';
 import Column from 'flavours/glitch/features/ui/components/column';
 
-
-
-
-
 const messages = defineMessages({
   heading: { id: 'column.reblogged_by', defaultMessage: 'Boosted by' },
   refresh: { id: 'refresh', defaultMessage: 'Refresh' },
 });
 
 const mapStateToProps = (state, props) => ({
-  accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
+  accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'items']),
+  hasMore: !!state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'next']),
+  isLoading: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'isLoading'], true),
 });
 
 class Reblogs extends ImmutablePureComponent {
@@ -35,6 +35,8 @@ class Reblogs extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     accountIds: ImmutablePropTypes.list,
+    hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
     multiColumn: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
@@ -45,12 +47,6 @@ class Reblogs extends ImmutablePureComponent {
     }
   }
 
-  UNSAFE_componentWillReceiveProps(nextProps) {
-    if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
-      this.props.dispatch(fetchReblogs(nextProps.params.statusId));
-    }
-  }
-
   handleHeaderClick = () => {
     this.column.scrollTop();
   };
@@ -63,8 +59,12 @@ class Reblogs extends ImmutablePureComponent {
     this.props.dispatch(fetchReblogs(this.props.params.statusId));
   };
 
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandReblogs(this.props.params.statusId));
+  }, 300, { leading: true });
+
   render () {
-    const { intl, accountIds, multiColumn } = this.props;
+    const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
 
     if (!accountIds) {
       return (
@@ -91,6 +91,9 @@ class Reblogs extends ImmutablePureComponent {
 
         <ScrollableList
           scrollKey='reblogs'
+          onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
+          isLoading={isLoading}
           emptyMessage={emptyMessage}
           bindToDocument={!multiColumn}
         >
diff --git a/app/javascript/flavours/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js
index a10fed4334..d37451d005 100644
--- a/app/javascript/flavours/glitch/reducers/user_lists.js
+++ b/app/javascript/flavours/glitch/reducers/user_lists.js
@@ -44,7 +44,12 @@ import {
   FEATURED_TAGS_FETCH_FAIL,
 } from 'flavours/glitch/actions/featured_tags';
 import {
+  REBLOGS_FETCH_REQUEST,
   REBLOGS_FETCH_SUCCESS,
+  REBLOGS_FETCH_FAIL,
+  REBLOGS_EXPAND_REQUEST,
+  REBLOGS_EXPAND_SUCCESS,
+  REBLOGS_EXPAND_FAIL,
   FAVOURITES_FETCH_REQUEST,
   FAVOURITES_FETCH_SUCCESS,
   FAVOURITES_FETCH_FAIL,
@@ -138,7 +143,15 @@ export default function userLists(state = initialState, action) {
   case FOLLOWING_EXPAND_FAIL:
     return state.setIn(['following', action.id, 'isLoading'], false);
   case REBLOGS_FETCH_SUCCESS:
-    return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
+    return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next);
+  case REBLOGS_EXPAND_SUCCESS:
+    return appendToList(state, ['reblogged_by', action.id], action.accounts, action.next);
+  case REBLOGS_FETCH_REQUEST:
+  case REBLOGS_EXPAND_REQUEST:
+    return state.setIn(['reblogged_by', action.id, 'isLoading'], true);
+  case REBLOGS_FETCH_FAIL:
+  case REBLOGS_EXPAND_FAIL:
+    return state.setIn(['reblogged_by', action.id, 'isLoading'], false);
   case FAVOURITES_FETCH_SUCCESS:
     return normalizeList(state, ['favourited_by', action.id], action.accounts, action.next);
   case FAVOURITES_EXPAND_SUCCESS:

From f5bd2014e2fc5960bfeb9bda9735ac75f455b87d Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Wed, 30 Aug 2023 16:29:52 +0200
Subject: [PATCH 75/81] [Glitch] Fix sign up steps progress layout in
 right-to-left locales

Port 21ec596dabbe28a5cfc5b29362f46f47b48eb140 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 app/javascript/flavours/glitch/styles/forms.scss | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index 2b7c02f115..7d2f5c1bd2 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -1187,14 +1187,14 @@ code {
   }
 
   li:first-child .label {
-    left: auto;
     inset-inline-start: 0;
+    inset-inline-end: auto;
     text-align: start;
     transform: none;
   }
 
   li:last-child .label {
-    left: auto;
+    inset-inline-start: auto;
     inset-inline-end: 0;
     text-align: end;
     transform: none;

From 6c05ac4068f2c942331458c00bbb8a835f0f56e5 Mon Sep 17 00:00:00 2001
From: Stanislas Signoud <signez@stanisoft.net>
Date: Thu, 31 Aug 2023 12:18:46 +0200
Subject: [PATCH 76/81] [Glitch] Add an explanation banner on switching to
 single column mode

Port 40b69cc1cd62fa57047905a6999985ae6c183f6c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../features/ui/components/navigation_panel.jsx  | 11 ++++++++---
 .../glitch/styles/components/columns.scss        | 16 ++++++++++++++++
 2 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx
index c0726404ee..f6984d5adb 100644
--- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx
@@ -29,6 +29,7 @@ const messages = defineMessages({
   about: { id: 'navigation_bar.about', defaultMessage: 'About' },
   search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
   advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
+  openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
   app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
 });
 
@@ -56,9 +57,13 @@ class NavigationPanel extends Component {
       <div className='navigation-panel'>
         {transientSingleColumn && (
           <div className='navigation-panel__logo'>
-            <a href={`/deck${location.pathname}`} className='button button--block'>
-              {intl.formatMessage(messages.advancedInterface)}
-            </a>
+            <div class='switch-to-advanced'>
+              {intl.formatMessage(messages.openedInClassicInterface)}
+              {" "}
+              <a href={`/deck${location.pathname}`} class='switch-to-advanced__toggle'>
+                {intl.formatMessage(messages.advancedInterface)}
+              </a>
+            </div>
             <hr />
           </div>
         )}
diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index d4860258ed..6c97137964 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -228,6 +228,22 @@ $ui-header-height: 55px;
   top: -48px;
 }
 
+.switch-to-advanced {
+  color: $classic-primary-color;
+  background-color: $classic-base-color;
+  padding: 15px;
+  border-radius: 4px;
+  margin-top: 4px;
+  margin-bottom: 12px;
+  font-size: 13px;
+  line-height: 18px;
+
+  .switch-to-advanced__toggle {
+    color: $ui-button-tertiary-color;
+    font-weight: bold;
+  }
+}
+
 .column-link {
   background: lighten($ui-base-color, 8%);
   color: $primary-text-color;

From 64e661b93058ca55cf1278cd09417d01edfba4b8 Mon Sep 17 00:00:00 2001
From: Santiago Kozak <kozaksantiago@gmail.com>
Date: Thu, 31 Aug 2023 09:15:58 -0300
Subject: [PATCH 77/81] [Glitch] Allow filter form in profiles directory to
 wrap

Port f1d250135ccf9be5c4d982a2c48417da89d38eb5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../flavours/glitch/styles/components/single_column.scss         | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss
index 2f8f7e2dd2..7efcf0c097 100644
--- a/app/javascript/flavours/glitch/styles/components/single_column.scss
+++ b/app/javascript/flavours/glitch/styles/components/single_column.scss
@@ -120,6 +120,7 @@
 
   .filter-form {
     display: flex;
+    flex-wrap: wrap;
   }
 
   .autosuggest-textarea__textarea {

From 807c0591e177ce8ec70298aabd1df3c89c38e19d Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 1 Sep 2023 15:13:27 +0200
Subject: [PATCH 78/81] [Glitch] Fix search popout including full-text search
 instructions when full-text search is disabled

Port 6c4c72497a5722870e4432ef41dd4c9ec36a8928 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../features/compose/components/search.jsx    | 22 +++++++++++--------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/app/javascript/flavours/glitch/features/compose/components/search.jsx b/app/javascript/flavours/glitch/features/compose/components/search.jsx
index 218518a7b6..e83c1761ec 100644
--- a/app/javascript/flavours/glitch/features/compose/components/search.jsx
+++ b/app/javascript/flavours/glitch/features/compose/components/search.jsx
@@ -106,7 +106,7 @@ class Search extends PureComponent {
 
   handleKeyDown = (e) => {
     const { selectedOption } = this.state;
-    const options = this._getOptions().concat(this.defaultOptions);
+    const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
 
     switch(e.key) {
     case 'Escape':
@@ -362,15 +362,19 @@ class Search extends PureComponent {
             </>
           )}
 
-          <h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
+          {searchEnabled && (
+            <>
+              <h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
 
-          <div className='search__popout__menu'>
-            {this.defaultOptions.map(({ key, label, action }, i) => (
-              <button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === (options.length + i) })}>
-                {label}
-              </button>
-            ))}
-          </div>
+              <div className='search__popout__menu'>
+                {this.defaultOptions.map(({ key, label, action }, i) => (
+                  <button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === (options.length + i) })}>
+                    {label}
+                  </button>
+                ))}
+              </div>
+            </>
+          )}
         </div>
       </div>
     );

From 4adb12ca9157ae0efc0b813df3a09ef8f6d579de Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Sat, 2 Sep 2023 14:23:18 +0200
Subject: [PATCH 79/81] Fix test failures due to different default settings in
 glitch-soc

---
 spec/controllers/api/v1/timelines/tag_controller_spec.rb | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/spec/controllers/api/v1/timelines/tag_controller_spec.rb b/spec/controllers/api/v1/timelines/tag_controller_spec.rb
index 1c60798fcf..8896f02a77 100644
--- a/spec/controllers/api/v1/timelines/tag_controller_spec.rb
+++ b/spec/controllers/api/v1/timelines/tag_controller_spec.rb
@@ -22,6 +22,10 @@ describe Api::V1::Timelines::TagController do
     end
 
     context 'when the instance allows public preview' do
+      before do
+        Setting.timeline_preview = true
+      end
+
       context 'when the user is not authenticated' do
         let(:token) { nil }
 

From 4371df7fbe226179cc2520bfce05d18a1cdfbba1 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 1 Sep 2023 15:41:10 +0200
Subject: [PATCH 80/81] [Glitch] Add `authorized_fetch` server setting in
 addition to env var

Port SCSS changes from 9e26cd55038084638fdf71b75f526494777d2849 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 app/javascript/flavours/glitch/styles/accounts.scss | 2 ++
 app/javascript/flavours/glitch/styles/forms.scss    | 1 +
 2 files changed, 3 insertions(+)

diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss
index ad0dfe0177..b0fe21bcf1 100644
--- a/app/javascript/flavours/glitch/styles/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/accounts.scss
@@ -192,6 +192,8 @@
 }
 
 .account-role,
+.information-badge,
+.simple_form .overridden,
 .simple_form .recommended,
 .simple_form .not_recommended,
 .simple_form .glitch_only {
diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index 7d2f5c1bd2..b8fc4a653f 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -103,6 +103,7 @@ code {
         }
       }
 
+      .overridden,
       .recommended,
       .not_recommended,
       .glitch_only {

From 5c7df20c13ebd3aef2fa1580d46058b683d30971 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Fri, 1 Sep 2023 17:47:07 +0200
Subject: [PATCH 81/81] [Glitch] Add admin notifications for new Mastodon
 versions

Port front-end changes from 16681e0f20e1f8584e11439953c8d59b322571f5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
---
 .../components/critical_update_banner.tsx     | 26 +++++++++++++++++++
 .../glitch/features/home_timeline/index.jsx   | 14 +++++++---
 .../flavours/glitch/initial_state.js          |  2 ++
 .../flavours/glitch/styles/admin.scss         |  5 ++++
 .../glitch/styles/components/columns.scss     | 18 ++++++++++++-
 .../flavours/glitch/styles/tables.scss        |  5 ++++
 6 files changed, 65 insertions(+), 5 deletions(-)
 create mode 100644 app/javascript/flavours/glitch/features/home_timeline/components/critical_update_banner.tsx

diff --git a/app/javascript/flavours/glitch/features/home_timeline/components/critical_update_banner.tsx b/app/javascript/flavours/glitch/features/home_timeline/components/critical_update_banner.tsx
new file mode 100644
index 0000000000..d0dd2b6acd
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/home_timeline/components/critical_update_banner.tsx
@@ -0,0 +1,26 @@
+import { FormattedMessage } from 'react-intl';
+
+export const CriticalUpdateBanner = () => (
+  <div className='warning-banner'>
+    <div className='warning-banner__message'>
+      <h1>
+        <FormattedMessage
+          id='home.pending_critical_update.title'
+          defaultMessage='Critical security update available!'
+        />
+      </h1>
+      <p>
+        <FormattedMessage
+          id='home.pending_critical_update.body'
+          defaultMessage='Please update your Mastodon server as soon as possible!'
+        />{' '}
+        <a href='/admin/software_updates'>
+          <FormattedMessage
+            id='home.pending_critical_update.link'
+            defaultMessage='See updates'
+          />
+        </a>
+      </p>
+    </div>
+  </div>
+);
diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.jsx b/app/javascript/flavours/glitch/features/home_timeline/index.jsx
index e17680d8bb..80dae5e4d0 100644
--- a/app/javascript/flavours/glitch/features/home_timeline/index.jsx
+++ b/app/javascript/flavours/glitch/features/home_timeline/index.jsx
@@ -14,7 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/act
 import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
 import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator';
 import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container';
-import { me } from 'flavours/glitch/initial_state';
+import { me, criticalUpdatesPending } from 'flavours/glitch/initial_state';
 
 import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 import { expandHomeTimeline } from '../../actions/timelines';
@@ -23,6 +23,7 @@ import ColumnHeader from '../../components/column_header';
 import StatusListContainer from '../ui/containers/status_list_container';
 
 import { ColumnSettings } from './components/column_settings';
+import { CriticalUpdateBanner } from './components/critical_update_banner';
 import { ExplorePrompt } from './components/explore_prompt';
 
 const messages = defineMessages({
@@ -158,8 +159,9 @@ class HomeTimeline extends PureComponent {
     const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
     const pinned = !!columnId;
     const { signedIn } = this.context.identity;
+    const banners = [];
 
-    let announcementsButton, banner;
+    let announcementsButton;
 
     if (hasAnnouncements) {
       announcementsButton = (
@@ -174,8 +176,12 @@ class HomeTimeline extends PureComponent {
       );
     }
 
+    if (criticalUpdatesPending) {
+      banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
+    }
+
     if (tooSlow) {
-      banner = <ExplorePrompt />;
+      banners.push(<ExplorePrompt key='explore-prompt' />);
     }
 
     return (
@@ -197,7 +203,7 @@ class HomeTimeline extends PureComponent {
 
         {signedIn ? (
           <StatusListContainer
-            prepend={banner}
+            prepend={banners}
             alwaysPrepend
             trackScroll={!pinned}
             scrollKey={`home_timeline-${columnId}`}
diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js
index 46228c3306..8d277f8c18 100644
--- a/app/javascript/flavours/glitch/initial_state.js
+++ b/app/javascript/flavours/glitch/initial_state.js
@@ -100,6 +100,7 @@ export const hasMultiColumnPath = initialPath === '/'
  * @typedef InitialState
  * @property {Record<string, Account>} accounts
  * @property {InitialStateLanguage[]} languages
+ * @property {boolean=} critical_updates_pending
  * @property {InitialStateMeta} meta
  * @property {object} local_settings
  * @property {number} max_toot_chars
@@ -160,6 +161,7 @@ export const useBlurhash = getMeta('use_blurhash');
 export const usePendingItems = getMeta('use_pending_items');
 export const version = getMeta('version');
 export const languages = initialState?.languages;
+export const criticalUpdatesPending = initialState?.critical_updates_pending;
 export const statusPageUrl = getMeta('status_page_url');
 export const sso_redirect = getMeta('sso_redirect');
 
diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 7adeaeee01..2f4027b03f 100644
--- a/app/javascript/flavours/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
@@ -143,6 +143,11 @@ $content-width: 840px;
         }
       }
 
+      .warning a {
+        color: $gold-star;
+        font-weight: 700;
+      }
+
       .simple-navigation-active-leaf a {
         color: $primary-text-color;
         background-color: $ui-highlight-color;
diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index 6c97137964..39b9bd291d 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -977,7 +977,8 @@ $ui-header-height: 55px;
   }
 }
 
-.dismissable-banner {
+.dismissable-banner,
+.warning-banner {
   position: relative;
   margin: 10px;
   margin-bottom: 5px;
@@ -1055,6 +1056,21 @@ $ui-header-height: 55px;
   }
 }
 
+.warning-banner {
+  border: 1px solid $warning-red;
+  background: rgba($warning-red, 0.15);
+
+  &__message {
+    h1 {
+      color: $warning-red;
+    }
+
+    a {
+      color: $primary-text-color;
+    }
+  }
+}
+
 .hashtag-header {
   border-bottom: 1px solid lighten($ui-base-color, 8%);
   padding: 15px;
diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index b583d3d8ea..44ef00ba73 100644
--- a/app/javascript/flavours/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
@@ -12,6 +12,11 @@
     border-top: 1px solid $ui-base-color;
     text-align: start;
     background: darken($ui-base-color, 4%);
+
+    &.critical {
+      font-weight: 700;
+      color: $gold-star;
+    }
   }
 
   & > thead > tr > th {