From abcc0b38fab3c3e5987943741588aa64054fdc22 Mon Sep 17 00:00:00 2001
From: Claire <claire.github-309c@sitedethib.com>
Date: Tue, 19 Sep 2023 17:31:58 +0200
Subject: [PATCH 01/15] Fix incorrect PostgreSQL version check (#26979)

---
 ...20230803082451_add_unique_index_on_preview_cards_statuses.rb | 2 +-
 lib/mastodon/migration_helpers.rb                               | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

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 3e9ab134b7..d29d7847c5 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
@@ -18,7 +18,7 @@ class AddUniqueIndexOnPreviewCardsStatuses < ActiveRecord::Migration[6.1]
   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
+      version >= 120_000
     end
   end
 
diff --git a/lib/mastodon/migration_helpers.rb b/lib/mastodon/migration_helpers.rb
index 4a43f67c27..c382b5fbd5 100644
--- a/lib/mastodon/migration_helpers.rb
+++ b/lib/mastodon/migration_helpers.rb
@@ -202,7 +202,7 @@ module Mastodon
     def supports_add_column_with_default?
       version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
 
-      version >= 11_000
+      version >= 110_000
     end
 
     # Adds a foreign key with only minimal locking on the tables involved.

From d257157276d832758d6bbea0c2ace8799fae6586 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Wed, 20 Sep 2023 10:35:24 +0900
Subject: [PATCH 02/15] Hide private post reference notification

---
 app/services/process_references_service.rb    |  6 ++---
 .../process_references_service_spec.rb        | 23 ++++++++++++++++++-
 2 files changed, 25 insertions(+), 4 deletions(-)

diff --git a/app/services/process_references_service.rb b/app/services/process_references_service.rb
index 05aa5ef345..faf5965a7a 100644
--- a/app/services/process_references_service.rb
+++ b/app/services/process_references_service.rb
@@ -58,11 +58,11 @@ class ProcessReferencesService < BaseService
   private
 
   def references
-    @references = @reference_parameters + scan_text!
+    @references ||= @reference_parameters + scan_text!
   end
 
   def old_references
-    @old_references = @status.references.pluck(:id)
+    @old_references ||= @status.references.pluck(:id)
   end
 
   def added_references
@@ -112,7 +112,7 @@ class ProcessReferencesService < BaseService
   def create_notifications!
     return if @added_objects.blank?
 
-    local_reference_objects = @added_objects.filter { |ref| ref.target_status.account.local? }
+    local_reference_objects = @added_objects.filter { |ref| ref.target_status.account.local? && StatusPolicy.new(ref.target_status.account, ref.status).show? }
     return if local_reference_objects.empty?
 
     LocalNotificationWorker.push_bulk(local_reference_objects) do |ref|
diff --git a/spec/services/process_references_service_spec.rb b/spec/services/process_references_service_spec.rb
index e9099fb935..61def42034 100644
--- a/spec/services/process_references_service_spec.rb
+++ b/spec/services/process_references_service_spec.rb
@@ -6,8 +6,9 @@ RSpec.describe ProcessReferencesService, type: :service do
   let(:text) { 'Hello' }
   let(:account) { Fabricate(:user).account }
   let(:visibility) { :public }
+  let(:target_status_visibility) { :public }
   let(:status) { Fabricate(:status, account: account, text: text, visibility: visibility) }
-  let(:target_status) { Fabricate(:status, account: Fabricate(:user).account) }
+  let(:target_status) { Fabricate(:status, account: Fabricate(:user).account, visibility: target_status_visibility) }
   let(:target_status_uri) { ActivityPub::TagManager.instance.uri_for(target_status) }
 
   describe 'posting new status' do
@@ -42,6 +43,26 @@ RSpec.describe ProcessReferencesService, type: :service do
       end
     end
 
+    context 'when private post' do
+      let(:text) { "Hello RT #{target_status_uri}" }
+      let(:visibility) { :private }
+
+      it 'post status' do
+        expect(subject.size).to eq 1
+        expect(subject.pluck(0)).to include target_status.id
+        expect(subject.pluck(1)).to include 'RT'
+      end
+    end
+
+    context 'when cannot show private post' do
+      let(:text) { "Hello RT #{target_status_uri}" }
+      let(:target_status_visibility) { :private }
+
+      it 'post status' do
+        expect(subject.size).to eq 0
+      end
+    end
+
     context 'when url only' do
       let(:text) { "Hello #{target_status_uri}" }
 

From e16a885c81a5c38398992f00873a4212d01c7809 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Wed, 20 Sep 2023 12:34:11 +0900
Subject: [PATCH 03/15] Add QT attribute to send as quote

---
 app/models/status.rb                                |  4 ++++
 app/serializers/activitypub/note_serializer.rb      | 13 ++++++-------
 .../serializers/activitypub/note_serializer_spec.rb |  1 -
 3 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/app/models/status.rb b/app/models/status.rb
index f9587b3c71..9f0299a511 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -226,6 +226,10 @@ class Status < ApplicationRecord
     !reblog_of_id.nil?
   end
 
+  def quote
+    reference_objects.where(attribute_type: 'QT').first&.target_status
+  end
+
   def within_realtime_window?
     created_at >= REAL_TIME_WINDOW.ago
   end
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index 109f8115df..d8f7a328ed 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -17,7 +17,6 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
 
   attribute :quote_uri, if: :quote?
   attribute :misskey_quote, key: :_misskey_quote, if: :quote?
-  attribute :misskey_content, key: :_misskey_content, if: :quote?
 
   has_many :virtual_attachments, key: :attachment
   has_many :virtual_tags, key: :tag
@@ -172,21 +171,21 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
   end
 
   def quote?
-    object.references.count == 1 && object.account.user&.settings&.[]('single_ref_to_quote')
+    @quote ||= (object.reference_objects.count == 1 && object.account.user&.settings&.[]('single_ref_to_quote')) || object.reference_objects.where(attribute_type: 'QT').count == 1
+  end
+
+  def quote_post
+    @quote_post ||= object.quote || object.references.first
   end
 
   def quote_uri
-    ActivityPub::TagManager.instance.uri_for(object.references.first)
+    ActivityPub::TagManager.instance.uri_for(quote_post)
   end
 
   def misskey_quote
     quote_uri
   end
 
-  def misskey_content
-    object.text
-  end
-
   def poll_options
     object.preloadable_poll.loaded_options
   end
diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb
index f4248e548e..0425e2e66b 100644
--- a/spec/serializers/activitypub/note_serializer_spec.rb
+++ b/spec/serializers/activitypub/note_serializer_spec.rb
@@ -73,7 +73,6 @@ describe ActivityPub::NoteSerializer do
       expect(subject['quoteUri']).to_not be_nil
       expect(subject['quoteUri']).to eq referred.uri
       expect(subject['_misskey_quote']).to eq referred.uri
-      expect(subject['_misskey_content']).to eq referred.text
       expect(subject['references']['first']['items']).to include referred.uri
     end
   end

From c2ddcf1a565cb71bb8d5f5dea290bce85e4f6644 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Wed, 20 Sep 2023 12:59:01 +0900
Subject: [PATCH 04/15] Add quote test

---
 .../process_references_service_spec.rb        | 26 +++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/spec/services/process_references_service_spec.rb b/spec/services/process_references_service_spec.rb
index 61def42034..569e8b7cc3 100644
--- a/spec/services/process_references_service_spec.rb
+++ b/spec/services/process_references_service_spec.rb
@@ -63,6 +63,32 @@ RSpec.describe ProcessReferencesService, type: :service do
       end
     end
 
+    context 'with quote' do
+      let(:text) { "Hello QT #{target_status_uri}" }
+
+      it 'post status' do
+        expect(subject.size).to eq 1
+        expect(subject.pluck(0)).to include target_status.id
+        expect(subject.pluck(1)).to include 'QT'
+        expect(status.quote).to_not be_nil
+        expect(status.quote.id).to eq target_status.id
+      end
+    end
+
+    context 'with quote and reference' do
+      let(:target_status2) { Fabricate(:status) }
+      let(:target_status2_uri) { ActivityPub::TagManager.instance.uri_for(target_status2) }
+      let(:text) { "Hello QT #{target_status_uri}\nBT #{target_status2_uri}" }
+
+      it 'post status' do
+        expect(subject.size).to eq 2
+        expect(subject).to include [target_status.id, 'QT']
+        expect(subject).to include [target_status2.id, 'BT']
+        expect(status.quote).to_not be_nil
+        expect(status.quote.id).to eq target_status.id
+      end
+    end
+
     context 'when url only' do
       let(:text) { "Hello #{target_status_uri}" }
 

From 53561a51f67133f892bd87e40794095bbb5efa84 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Wed, 20 Sep 2023 16:43:46 +0900
Subject: [PATCH 05/15] Add banning CCBot

---
 public/robots.txt | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/public/robots.txt b/public/robots.txt
index 6672eeba1f..3bd45adaef 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -3,6 +3,9 @@
 User-agent: GPTBot
 Disallow: /
 
+User-agent: CCBot
+Disallow: /
+
 User-agent: *
 Disallow: /media_proxy/
 Disallow: /interact/

From 353e76c9c9a32db7f1ddf6b663ecbae5dfee1dc6 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Wed, 20 Sep 2023 16:49:16 +0900
Subject: [PATCH 06/15] Add quote menu

---
 app/javascript/mastodon/actions/compose.js            |  3 ++-
 .../mastodon/components/status_action_bar.jsx         |  8 ++++++++
 .../mastodon/containers/status_container.jsx          |  6 +++++-
 .../features/status/components/action_bar.jsx         |  7 +++++++
 app/javascript/mastodon/features/status/index.jsx     |  7 ++++++-
 app/javascript/mastodon/locales/en.json               |  1 +
 app/javascript/mastodon/locales/ja.json               |  1 +
 app/javascript/mastodon/reducers/compose.js           | 11 ++++++-----
 8 files changed, 36 insertions(+), 8 deletions(-)

diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 3ea159f019..1f682d1321 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -785,11 +785,12 @@ export function insertExpirationCompose(position, data) {
   };
 }
 
-export function insertReferenceCompose(position, url) {
+export function insertReferenceCompose(position, url, attributeType) {
   return {
     type: COMPOSE_REFERENCE_INSERT,
     position,
     url,
+    attributeType,
   };
 }
 
diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx
index d689b1ed03..02a42a92dd 100644
--- a/app/javascript/mastodon/components/status_action_bar.jsx
+++ b/app/javascript/mastodon/components/status_action_bar.jsx
@@ -52,6 +52,7 @@ const messages = defineMessages({
   admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
   copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
   reference: { id: 'status.reference', defaultMessage: 'Add reference' },
+  quote: { id: 'status.quote', defaultMessage: 'Add ref (quote in other servers)' },
   hide: { id: 'status.hide', defaultMessage: 'Hide post' },
   blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
   unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
@@ -97,6 +98,8 @@ class StatusActionBar extends ImmutablePureComponent {
     onBookmarkCategoryAdder: PropTypes.func,
     onFilter: PropTypes.func,
     onAddFilter: PropTypes.func,
+    onReference: PropTypes.func,
+    onQuote: PropTypes.func,
     onInteractionModal: PropTypes.func,
     withDismiss: PropTypes.bool,
     withCounters: PropTypes.bool,
@@ -271,6 +274,10 @@ class StatusActionBar extends ImmutablePureComponent {
     this.props.onReference(this.props.status);
   };
 
+  handleQuote = () => {
+    this.props.onQuote(this.props.status);
+  };
+
   handleHideClick = () => {
     this.props.onFilter();
   };
@@ -316,6 +323,7 @@ class StatusActionBar extends ImmutablePureComponent {
 
       if (publicStatus) {
         menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
+        menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote });
       }
 
       menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClickOriginal });
diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx
index 5023b7ef03..da3058334b 100644
--- a/app/javascript/mastodon/containers/status_container.jsx
+++ b/app/javascript/mastodon/containers/status_container.jsx
@@ -203,7 +203,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
   },
 
   onReference (status) {
-    dispatch(insertReferenceCompose(0, status.get('url')));
+    dispatch(insertReferenceCompose(0, status.get('url'), 'BT'));
+  },
+
+  onQuote (status) {
+    dispatch(insertReferenceCompose(0, status.get('url'), 'QT'));
   },
 
   onTranslate (status) {
diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx
index e80a83c907..56086075fc 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.jsx
+++ b/app/javascript/mastodon/features/status/components/action_bar.jsx
@@ -46,6 +46,7 @@ const messages = defineMessages({
   admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
   copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
   reference: { id: 'status.reference', defaultMessage: 'Add reference' },
+  quote: { id: 'status.quote', defaultMessage: 'Add ref (quote in other servers)' },
   blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
   unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
   unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
@@ -74,6 +75,7 @@ class ActionBar extends PureComponent {
     onFavourite: PropTypes.func.isRequired,
     onEmojiReact: PropTypes.func.isRequired,
     onReference: PropTypes.func.isRequired,
+    onQuote: PropTypes.func.isRequired,
     onBookmark: PropTypes.func.isRequired,
     onBookmarkCategoryAdder: PropTypes.func.isRequired,
     onDelete: PropTypes.func.isRequired,
@@ -208,6 +210,10 @@ class ActionBar extends PureComponent {
     this.props.onReference(this.props.status);
   };
 
+  handleQuote = () => {
+    this.props.onQuote(this.props.status);
+  };
+
   handleEmojiPick = (data) => {
     this.props.onEmojiReact(this.props.status, data);
   };
@@ -248,6 +254,7 @@ class ActionBar extends PureComponent {
 
       if (publicStatus) {
         menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
+        menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote });
       }
       menu.push({ text: intl.formatMessage(messages.bookmark_category), action: this.handleBookmarkCategoryAdderClick });
 
diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx
index cf4c2bd6ce..03a721d9c8 100644
--- a/app/javascript/mastodon/features/status/index.jsx
+++ b/app/javascript/mastodon/features/status/index.jsx
@@ -363,7 +363,11 @@ class Status extends ImmutablePureComponent {
   };
 
   handleReference = (status) => {
-    this.props.dispatch(insertReferenceCompose(0, status.get('url')));
+    this.props.dispatch(insertReferenceCompose(0, status.get('url'), 'BT'));
+  };
+
+  handleQuote = (status) => {
+    this.props.dispatch(insertReferenceCompose(0, status.get('url'), 'QT'));
   };
 
   handleBookmarkClick = (status) => {
@@ -750,6 +754,7 @@ class Status extends ImmutablePureComponent {
                   onReblog={this.handleReblogClick}
                   onReblogForceModal={this.handleReblogForceModalClick}
                   onReference={this.handleReference}
+                  onQuote={this.handleQuote}
                   onBookmark={this.handleBookmarkClick}
                   onBookmarkCategoryAdder={this.handleBookmarkCategoryAdderClick}
                   onDelete={this.handleDeleteClick}
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 9de9b72162..b986b45b30 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -683,6 +683,7 @@
   "status.open": "Expand this post",
   "status.pin": "Pin on profile",
   "status.pinned": "Pinned post",
+  "status.quote": "Ref (quote in other servers)",
   "status.read_more": "Read more",
   "status.reblog": "Boost",
   "status.reblog_private": "Boost with original visibility",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index d294d85e0b..e598512178 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -769,6 +769,7 @@
   "status.open": "詳細を表示",
   "status.pin": "プロフィールに固定表示",
   "status.pinned": "固定された投稿",
+  "status.quote": "参照 (他サーバーで引用扱い)",
   "status.read_more": "もっと見る",
   "status.reblog": "ブースト",
   "status.reblog_private": "ブースト",
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 59be1ae6da..612ee01d9b 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -252,10 +252,11 @@ const insertExpiration = (state, position, data) => {
   });
 };
 
-const insertReference = (state, url) => {
+const insertReference = (state, url, attributeType) => {
   const oldText = state.get('text');
+  const attribute = attributeType || 'BT';
 
-  if (oldText.indexOf(`BT ${url}`) >= 0) {
+  if (oldText.indexOf(`${attribute} ${url}`) >= 0) {
     return state;
   }
 
@@ -271,12 +272,12 @@ const insertReference = (state, url) => {
 
   if (oldText.length > 0) {
     const lastLine = oldText.slice(oldText.lastIndexOf('\n') + 1, oldText.length - 1);
-    if (lastLine.startsWith('BT ')) {
+    if (lastLine.startsWith(`${attribute} `)) {
       newLine = '\n';
     }
   }
 
-  const referenceText = `${newLine}BT ${url}`;
+  const referenceText = `${newLine}${attribute} ${url}`;
   const text = `${oldText}${referenceText}`;
 
   return state.merge({
@@ -526,7 +527,7 @@ export default function compose(state = initialState, action) {
   case COMPOSE_EXPIRATION_INSERT:
     return insertExpiration(state, action.position, action.data);
   case COMPOSE_REFERENCE_INSERT:
-    return insertReference(state, action.url);
+    return insertReference(state, action.url, action.attributeType);
   case COMPOSE_UPLOAD_CHANGE_SUCCESS:
     return state
       .set('is_changing_upload', false)

From ee6186a197a42742b9c7e98cdca73abe79dbc696 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Wed, 20 Sep 2023 17:01:10 +0900
Subject: [PATCH 07/15] Add notification check to status reference test

---
 .../process_references_service_spec.rb         | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/spec/services/process_references_service_spec.rb b/spec/services/process_references_service_spec.rb
index 569e8b7cc3..c41144a2aa 100644
--- a/spec/services/process_references_service_spec.rb
+++ b/spec/services/process_references_service_spec.rb
@@ -11,6 +11,11 @@ RSpec.describe ProcessReferencesService, type: :service do
   let(:target_status) { Fabricate(:status, account: Fabricate(:user).account, visibility: target_status_visibility) }
   let(:target_status_uri) { ActivityPub::TagManager.instance.uri_for(target_status) }
 
+  def notify?(target_status_id = nil)
+    target_status_id ||= target_status.id
+    StatusReference.exists?(id: Notification.where(type: 'status_reference').select(:activity_id), target_status_id: target_status_id)
+  end
+
   describe 'posting new status' do
     subject do
       described_class.new.call(status, reference_parameters, urls: urls, fetch_remote: fetch_remote)
@@ -28,6 +33,7 @@ RSpec.describe ProcessReferencesService, type: :service do
         expect(subject.size).to eq 1
         expect(subject.pluck(0)).to include target_status.id
         expect(subject.pluck(1)).to include 'RT'
+        expect(notify?).to be true
       end
     end
 
@@ -40,6 +46,8 @@ RSpec.describe ProcessReferencesService, type: :service do
         expect(subject.size).to eq 2
         expect(subject).to include [target_status.id, 'RT']
         expect(subject).to include [target_status2.id, 'BT']
+        expect(notify?).to be true
+        expect(notify?(target_status2.id)).to be true
       end
     end
 
@@ -51,6 +59,7 @@ RSpec.describe ProcessReferencesService, type: :service do
         expect(subject.size).to eq 1
         expect(subject.pluck(0)).to include target_status.id
         expect(subject.pluck(1)).to include 'RT'
+        expect(notify?).to be false
       end
     end
 
@@ -60,6 +69,7 @@ RSpec.describe ProcessReferencesService, type: :service do
 
       it 'post status' do
         expect(subject.size).to eq 0
+        expect(notify?).to be false
       end
     end
 
@@ -72,6 +82,7 @@ RSpec.describe ProcessReferencesService, type: :service do
         expect(subject.pluck(1)).to include 'QT'
         expect(status.quote).to_not be_nil
         expect(status.quote.id).to eq target_status.id
+        expect(notify?).to be true
       end
     end
 
@@ -86,6 +97,8 @@ RSpec.describe ProcessReferencesService, type: :service do
         expect(subject).to include [target_status2.id, 'BT']
         expect(status.quote).to_not be_nil
         expect(status.quote.id).to eq target_status.id
+        expect(notify?).to be true
+        expect(notify?(target_status2.id)).to be true
       end
     end
 
@@ -94,6 +107,7 @@ RSpec.describe ProcessReferencesService, type: :service do
 
       it 'post status' do
         expect(subject.size).to eq 0
+        expect(notify?).to be false
       end
     end
 
@@ -190,6 +204,7 @@ RSpec.describe ProcessReferencesService, type: :service do
       it 'post status' do
         expect(subject.size).to eq 1
         expect(subject).to include target_status.id
+        expect(notify?).to be true
       end
     end
 
@@ -201,6 +216,7 @@ RSpec.describe ProcessReferencesService, type: :service do
         expect(subject.size).to eq 2
         expect(subject).to include target_status.id
         expect(subject).to include target_status2.id
+        expect(notify?(target_status2.id)).to be true
       end
     end
 
@@ -220,6 +236,7 @@ RSpec.describe ProcessReferencesService, type: :service do
 
       it 'post status' do
         expect(subject.size).to eq 0
+        expect(notify?).to be false
       end
     end
 
@@ -230,6 +247,7 @@ RSpec.describe ProcessReferencesService, type: :service do
       it 'post status' do
         expect(subject.size).to eq 1
         expect(subject).to include target_status2.id
+        expect(notify?(target_status2.id)).to be true
       end
     end
   end

From 7696216cbd35b1ce09bf6f0da4ab4840fd83a148 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Wed, 20 Sep 2023 18:42:08 +0900
Subject: [PATCH 08/15] Change update check source to kmyblue origin

---
 app/models/form/admin_settings.rb                   |  2 ++
 app/services/software_update_check_service.rb       | 11 +++++++++--
 app/views/admin/settings/discovery/show.html.haml   |  3 +++
 config/locales/simple_form.en.yml                   |  1 +
 config/locales/simple_form.ja.yml                   |  1 +
 config/settings.yml                                 |  1 +
 spec/services/software_update_check_service_spec.rb |  4 ++--
 7 files changed, 19 insertions(+), 4 deletions(-)

diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index 57cd9ca415..08b546561d 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -46,6 +46,7 @@ class Form::AdminSettings
     receive_other_servers_emoji_reaction
     streaming_other_servers_emoji_reaction
     enable_emoji_reaction
+    check_lts_version_only
   ).freeze
 
   INTEGER_KEYS = %i(
@@ -72,6 +73,7 @@ class Form::AdminSettings
     receive_other_servers_emoji_reaction
     streaming_other_servers_emoji_reaction
     enable_emoji_reaction
+    check_lts_version_only
   ).freeze
 
   UPLOAD_KEYS = %i(
diff --git a/app/services/software_update_check_service.rb b/app/services/software_update_check_service.rb
index 49b92f104d..0af943a333 100644
--- a/app/services/software_update_check_service.rb
+++ b/app/services/software_update_check_service.rb
@@ -27,11 +27,18 @@ class SoftwareUpdateCheckService < BaseService
   end
 
   def api_url
-    ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check')
+    ENV.fetch('UPDATE_CHECK_URL', 'https://kmy.blue/update-check')
   end
 
   def version
-    @version ||= Mastodon::Version.to_s.split('+')[0]
+    return @version if @version.present?
+
+    if ENV.fetch('UPDATE_CHECK_SOURCE', 'kmyblue') == 'kmyblue'
+      @version = "#{Mastodon::Version.kmyblue_major}.#{Mastodon::Version.kmyblue_minor}"
+      @version += '-lts' if Setting.check_lts_version_only
+    else
+      @version = Mastodon::Version.to_s.split('+')[0]
+    end
   end
 
   def process_update_notices!(update_notices)
diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml
index 60443e6877..6ea9e4fb4b 100644
--- a/app/views/admin/settings/discovery/show.html.haml
+++ b/app/views/admin/settings/discovery/show.html.haml
@@ -55,6 +55,9 @@
   .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
 
+  .fields-group
+    = f.input :check_lts_version_only, as: :boolean, wrapper: :with_label, kmyblue: true, hint: false
+
   %h4= t('admin.settings.discovery.follow_recommendations')
 
   .fields-group
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 738e5f39c2..56cce1c24d 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -315,6 +315,7 @@ en:
         activity_api_enabled: Publish aggregate statistics about user activity in the API
         backups_retention_period: User archive retention period
         bootstrap_timeline_accounts: Always recommend these accounts to new users
+        check_lts_version_only: Check kmyblue LTS version only when update check
         closed_registrations_message: Custom message when sign-ups are not available
         content_cache_retention_period: Content cache retention period
         custom_css: Custom CSS
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index 5c72720a33..b5898f5185 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -330,6 +330,7 @@ ja:
         activity_api_enabled: APIでユーザーアクティビティに関する集計統計を公開する
         backups_retention_period: ユーザーアーカイブの保持期間
         bootstrap_timeline_accounts: おすすめユーザーに常に表示するアカウント
+        check_lts_version_only: 更新チェックの時、LTSバージョンのみ確認する
         closed_registrations_message: アカウント作成を停止している時のカスタムメッセージ
         content_cache_retention_period: コンテンツキャッシュの保持期間
         custom_css: カスタムCSS
diff --git a/config/settings.yml b/config/settings.yml
index 0042b5822e..69b3ed1ee3 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -41,6 +41,7 @@ defaults: &defaults
   receive_other_servers_emoji_reaction: false
   streaming_other_servers_emoji_reaction: false
   enable_emoji_reaction: true
+  check_lts_version_only: true
 
 development:
   <<: *defaults
diff --git a/spec/services/software_update_check_service_spec.rb b/spec/services/software_update_check_service_spec.rb
index c8821348ac..7cc99bb09c 100644
--- a/spec/services/software_update_check_service_spec.rb
+++ b/spec/services/software_update_check_service_spec.rb
@@ -6,7 +6,7 @@ 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(:full_update_check_url) { "#{update_check_url}?version=#{Mastodon::Version.kmyblue_major}.#{Mastodon::Version.kmyblue_minor}-lts" }
 
     let(:devops_role)     { Fabricate(:user_role, name: 'DevOps', permissions: UserRole::FLAGS[:view_devops]) }
     let(:owner_user)      { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) }
@@ -139,7 +139,7 @@ RSpec.describe SoftwareUpdateCheckService, type: :service do
   end
 
   context 'when using the default update checking API' do
-    let(:update_check_url) { 'https://api.joinmastodon.org/update-check' }
+    let(:update_check_url) { 'https://kmy.blue/update-check' }
 
     it_behaves_like 'when the feature is enabled'
   end

From 3ad86a9020fad3671fd71ae4aec5f3d27fe2a250 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Thu, 21 Sep 2023 08:38:10 +0900
Subject: [PATCH 09/15] Add kmyblue gem_version support

---
 lib/mastodon/version.rb | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 0086555a4e..e5d7b0ae8f 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -62,7 +62,11 @@ module Mastodon
     end
 
     def gem_version
-      @gem_version ||= Gem::Version.new(to_s.split('+')[0])
+      @gem_version ||= if ENV.fetch('UPDATE_CHECK_SOURCE', 'kmyblue') == 'kmyblue'
+                         Gem::Version.new("#{kmyblue_major}.#{kmyblue_minor}")
+                       else
+                         Gem::Version.new(to_s.split('+')[0])
+                       end
     end
 
     def repository

From 80a55ed7c29433d92f3941d9d59d09c90cdd7e33 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Thu, 21 Sep 2023 10:00:59 +0900
Subject: [PATCH 10/15] Add antenna support for firefish

---
 app/services/delivery_antenna_service.rb      | 24 ++++++++++-
 .../services/delivery_antenna_service_spec.rb | 41 +++++++++++++++++++
 2 files changed, 63 insertions(+), 2 deletions(-)

diff --git a/app/services/delivery_antenna_service.rb b/app/services/delivery_antenna_service.rb
index e494321917..53bcc226e7 100644
--- a/app/services/delivery_antenna_service.rb
+++ b/app/services/delivery_antenna_service.rb
@@ -47,8 +47,8 @@ class DeliveryAntennaService
     end
 
     antennas = antennas.where(account_id: Account.without_suspended.joins(:user).select('accounts.id').where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago))
-    antennas = antennas.where(account: @status.account.followers) if [:public, :public_unlisted, :login, :limited].exclude?(@status.visibility.to_sym) && !@status.public_searchability?
-    antennas = antennas.where(account: @status.mentioned_accounts) if @status.visibility.to_sym == :limited
+    antennas = antennas.where(account: @status.account.followers) if followers_only?
+    antennas = antennas.where(account: @status.mentioned_accounts) if mentioned_users_only?
     antennas = antennas.where(with_media_only: false) unless @status.with_media?
     antennas = antennas.where(ignore_reblog: false) if @status.reblog?
     antennas = antennas.where(stl: false, ltl: false)
@@ -116,6 +116,26 @@ class DeliveryAntennaService
     collection.deliver!
   end
 
+  def followers_only?
+    case @status.visibility.to_sym
+    when :public, :public_unlisted, :login, :limited
+      false
+    when :unlisted
+      if @status.local?
+        !@status.public_searchability?
+      else
+        info = InstanceInfo.find_by(domain: @status.account.domain)
+        info&.software == 'firefish' || !@status.public_searchability?
+      end
+    else
+      true
+    end
+  end
+
+  def mentioned_users_only?
+    @status.visibility.to_sym == :limited
+  end
+
   class AntennaCollection
     def initialize(status, update, stl_home = false) # rubocop:disable Style/OptionalBooleanParameter
       @status = status
diff --git a/spec/services/delivery_antenna_service_spec.rb b/spec/services/delivery_antenna_service_spec.rb
index 26c23eb930..e9620dc083 100644
--- a/spec/services/delivery_antenna_service_spec.rb
+++ b/spec/services/delivery_antenna_service_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe DeliveryAntennaService, type: :service do
   let(:domain) { nil }
   let(:spoiler_text) { '' }
   let(:tags) { Tag.find_or_create_by_names(['hoge']) }
+  let(:software) { nil }
   let(:status) do
     url = domain.present? ? 'https://example.com/status' : nil
     status = Fabricate(:status, account: alice, spoiler_text: spoiler_text, visibility: visibility, searchability: searchability, text: 'Hello my body #hoge', url: url)
@@ -30,6 +31,8 @@ RSpec.describe DeliveryAntennaService, type: :service do
   let(:mode) { :home }
 
   before do
+    Fabricate(:instance_info, domain: domain, software: software) if domain.present? && software.present?
+
     bob.follow!(alice)
     alice.block!(ohagi)
 
@@ -359,4 +362,42 @@ RSpec.describe DeliveryAntennaService, type: :service do
       expect(antenna_feed_of(antenna)).to include status.id
     end
   end
+
+  context 'with federated unlisted post' do
+    let(:visibility)     { :unlisted }
+    let(:searchability)  { :public }
+    let(:domain)         { 'fast.example.com' }
+    let!(:antenna)       { antenna_with_keyword(bob, 'body') }
+    let!(:empty_antenna) { antenna_with_keyword(tom, 'body') }
+
+    context 'when unknown domain' do
+      let(:software) { nil }
+
+      it 'detecting antenna' do
+        expect(antenna_feed_of(antenna)).to include status.id
+        expect(antenna_feed_of(empty_antenna)).to include status.id
+      end
+    end
+
+    context 'when misskey domain' do
+      let(:software) { 'misskey' }
+
+      it 'detecting antenna' do
+        expect(antenna_feed_of(antenna)).to include status.id
+        expect(antenna_feed_of(empty_antenna)).to include status.id
+      end
+    end
+
+    context 'when firefish domain' do
+      let(:software)       { 'firefish' }
+
+      it 'detecting antenna' do
+        expect(antenna_feed_of(antenna)).to include status.id
+      end
+
+      it 'not detecting antenna' do
+        expect(antenna_feed_of(empty_antenna)).to_not include status.id
+      end
+    end
+  end
 end

From c9ea86861346a40267ad22a825548d7d43128dc1 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Thu, 21 Sep 2023 10:16:45 +0900
Subject: [PATCH 11/15] Change install link

---
 INSTALL.md | 56 ++++--------------------------------------------------
 1 file changed, 4 insertions(+), 52 deletions(-)

diff --git a/INSTALL.md b/INSTALL.md
index 91729e336c..0311120436 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -33,58 +33,10 @@ Sudachiインストール終了後、追加で`/etc/elasticsearch/sudachi/config
 }
 ```
 
-## 新規インストールの場合
+## インストール手順
 
-1. 本家Mastodonとセットアップ手順はほとんど一緒です。kmyblueが独自に必須ソフトウェアを追加したわけではありません。ただしkmyblueはMastodonの開発中コードを取り込んでいるので、Rubyなどのバージョンアップ作業が必要になる場合があります。Mastodon公式のセットアップ手順を盲信せず、画面の指示に従ってインストールを進めてください。CloudFlareを組み合わせてセットアップしたとき、サーバーに接続すると400が出るなどのトラブルが出ることがありますが、大抵はMastodon本家由来のトラブルだと思われるので基本サポートはしません
-2. ただひとつ差異があります。Gitリポジトリはこのkmyblueに向けてください。`kb_development`ブランチの最新コミットではなく、`kb`で始まる最新のタグを取り込むことを強くおすすめします
+[Wiki](https://github.com/kmycode/mastodon/wiki/Installation)を参照してください
 
-## 本家Mastodonからのマイグレーションの場合
+## アップデート手順
 
-kmyblueから本家Mastodonに戻りたい場合もあると思いますので、**必ずデータベースのバックアップをとってください**。
-
-1. kmyblueのリリースノートに、kmyblueバージョンに対応した本家Mastodonのバージョンが記載されています。それを参照して、まず本家Mastodonをそのバージョンまでバージョンアップしてください
-2. Gitのリモートにkmyblueを追加して、そのままチェックアウトしてください
-3. データベースのマイグレーションなどを行ってください
-
-```
-sudo systemctl stop mastodon-*
-
-bundle install
-yarn install
-RAILS_ENV=production bin/rails db:migrate
-RAILS_ENV=production bin/rails assets:clobber
-RAILS_ENV=production bin/rails assets:precompile
-
-# ElasticSearchを使用する場合
-RAILS_ENV=production bin/tootctl search deploy
-
-RAILS_ENV=production bin/tootctl cache clear
-sudo systemctl start mastodon-web mastodon-streaming mastodon-sidekiq
-```
-
-## kmyblueのバージョンをアップデートする
-
-リリースノートを参照して、自分に必要な作業を特定してください。面倒な場合は毎回全部実行してしまっても問題ありません。(プリコンパイルが失敗する可能性があるのでご注意ください)
-
-```
-# Rubyパッケージアップデート
-bundle intall
-
-# JSパッケージアップデート
-yarn install
-
-# DBマイグレーション
-RAILS_ENV=production bin/rails db:migrate
-
-# プリコンパイル
-# うまくいかない場合(エラーは出ないのにWeb表示が崩れる)はclobberしてからprecompile
-# それでもうまくいかない場合はsudo systemctl stop mastodon-webしてから試す
-# それでもうまくいかない場合はサーバーOSを再起動してから試す
-RAILS_ENV=production bin/rails assets:clobber # プリコンパイルがうまくいかない場合
-RAILS_ENV=production bin/rails assets:precompile
-
-# サーバー再起動
-sudo systemctl restart mastodon-web
-sudo systemctl restart mastodon-streaming
-sudo systemctl restart mastodon-sidekiq
-```
+[Wiki](https://github.com/kmycode/mastodon/wiki/Updation)を参照してください

From fa511c3eabd49de07f19548c16a531de0bec1ce5 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Thu, 21 Sep 2023 13:21:08 +0900
Subject: [PATCH 12/15] Fix software update check not working

---
 app/services/software_update_check_service.rb | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/services/software_update_check_service.rb b/app/services/software_update_check_service.rb
index 0af943a333..b20fb8611b 100644
--- a/app/services/software_update_check_service.rb
+++ b/app/services/software_update_check_service.rb
@@ -31,14 +31,14 @@ class SoftwareUpdateCheckService < BaseService
   end
 
   def version
-    return @version if @version.present?
-
     if ENV.fetch('UPDATE_CHECK_SOURCE', 'kmyblue') == 'kmyblue'
       @version = "#{Mastodon::Version.kmyblue_major}.#{Mastodon::Version.kmyblue_minor}"
       @version += '-lts' if Setting.check_lts_version_only
     else
       @version = Mastodon::Version.to_s.split('+')[0]
     end
+
+    @version
   end
 
   def process_update_notices!(update_notices)

From e11be4810037ec5411ebf1843aed06cd8e7680a2 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Thu, 21 Sep 2023 13:22:10 +0900
Subject: [PATCH 13/15] Show major or patch version update on menu

---
 app/models/software_update.rb                          | 8 ++++++++
 app/views/settings/preferences/reaching/show.html.haml | 2 +-
 config/locales/en.yml                                  | 3 +++
 config/locales/ja.yml                                  | 3 +++
 config/navigation.rb                                   | 6 +++++-
 5 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/app/models/software_update.rb b/app/models/software_update.rb
index cb3a6df2ae..4aeb7e665b 100644
--- a/app/models/software_update.rb
+++ b/app/models/software_update.rb
@@ -36,5 +36,13 @@ class SoftwareUpdate < ApplicationRecord
     def urgent_pending?
       pending_to_a.any?(&:urgent?)
     end
+
+    def major_pending?
+      pending_to_a.any?(&:major_type?)
+    end
+
+    def patch_pending?
+      pending_to_a.any?(&:patch_type?)
+    end
   end
 end
diff --git a/app/views/settings/preferences/reaching/show.html.haml b/app/views/settings/preferences/reaching/show.html.haml
index b3d22e8052..cfeaeff68e 100644
--- a/app/views/settings/preferences/reaching/show.html.haml
+++ b/app/views/settings/preferences/reaching/show.html.haml
@@ -31,7 +31,7 @@
 
     .fields-row
       .fields-group.fields-row__column.fields-row__column-12
-        = ff.input :default_searchability, collection: Status.selectable_searchabilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_searchability')
+        = ff.input :default_searchability, collection: Status.selectable_searchabilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_searchability'), hint: I18n.t('simple_form.hints.defaults.setting_default_searchability')
 
     .fields-group
       = ff.input :disallow_unlisted_public_searchability, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_disallow_unlisted_public_searchability'), hint: I18n.t('simple_form.hints.defaults.setting_disallow_unlisted_public_searchability')
diff --git a/config/locales/en.yml b/config/locales/en.yml
index d9d0bb084a..4e477ba146 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -997,6 +997,9 @@ en:
           other: Used by %{count} people over the last week
       title: Trends
       trending: Trending
+    update-pendings:
+      major: Major update pending
+      patch: Patch update pending
     warning_presets:
       add_new: Add new
       delete: Delete
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 286e19d42f..4d6d4e4c6a 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -991,6 +991,9 @@ ja:
           other: 週間%{count}人に使用されました
       title: トレンド
       trending: トレンド
+    update_pendings:
+      major: メジャーアップデートあり
+      patch: パッチアップデートあり
     warning_presets:
       add_new: 追加
       delete: 削除
diff --git a/config/navigation.rb b/config/navigation.rb
index ce8c44d542..bdb86f06a7 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -4,7 +4,11 @@ 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' }
+    if ENV['UPDATE_CHECK_URL'] != '' && current_user.can?(:view_devops)
+      n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.critical_update_pending')]), admin_software_updates_path, if: -> { SoftwareUpdate.urgent_pending? }, html: { class: 'warning' }
+      n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.update_pendings.major')]), admin_software_updates_path, if: -> { !SoftwareUpdate.urgent_pending? && SoftwareUpdate.major_pending? }, html: { class: 'warning' }
+      n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.update_pendings.patch')]), admin_software_updates_path, if: -> { !SoftwareUpdate.urgent_pending? && SoftwareUpdate.patch_pending? }, html: { class: 'warning' }
+    end
 
     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}
 

From 5fd2a575029de0edf6730bcdcef95d3473b46485 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Thu, 21 Sep 2023 13:22:34 +0900
Subject: [PATCH 14/15] Add defalt_searchability hint

---
 config/locales/simple_form.en.yml | 1 +
 config/locales/simple_form.ja.yml | 1 +
 2 files changed, 2 insertions(+)

diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 56cce1c24d..aa17a42816 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -62,6 +62,7 @@ en:
         setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts)
         setting_always_send_emails: Normally e-mail notifications won't be sent when you are actively using Mastodon
         setting_bookmark_category_needed: When removing from all category, unbookmarked automatically
+        setting_default_searchability: kmyblue・Fedibirdでは検索許可設定に基づき検索されます。Misskeyでは当設定に関係なく、全ての公開・ローカル公開・未収載投稿が検索されます。Mastodon・Firefishでは検索許可の代わりにプロフィール設定の「公開投稿を他のサーバーで自由に検索できるようにする」設定が適用されます
         setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click
         setting_display_media_default: Hide media marked as sensitive
         setting_display_media_hide_all: Always hide media
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index b5898f5185..e255c43cd4 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -65,6 +65,7 @@ ja:
         setting_always_send_emails: 通常、Mastodon からメール通知は行われません。
         setting_bookmark_category_needed: すべてのカテゴリから削除したとき、ブックマークが自動で外れるようになります
         setting_boost_modal: ブーストの公開範囲が指定できるようになります
+        setting_default_searchability: kmyblue・Fedibirdでは検索許可設定に基づき検索されます。Misskeyでは当設定に関係なく、全ての公開・ローカル公開・未収載投稿が検索されます。Mastodon・Firefishでは検索許可の代わりにプロフィール設定の「公開投稿を他のサーバーで自由に検索できるようにする」設定が適用されます
         setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります
         setting_disallow_unlisted_public_searchability: この設定を有効にすると、未収載投稿と検索範囲「誰でも」は両立できず不特定多数からの検索が不可になります。Fedibirdと同じ挙動になります
         setting_display_media_default: 閲覧注意としてマークされたメディアは隠す

From 2667e478d3187c6419b62928ebab4a1c8f5f6de7 Mon Sep 17 00:00:00 2001
From: KMY <tt@kmycode.net>
Date: Thu, 21 Sep 2023 14:22:52 +0900
Subject: [PATCH 15/15] Add emoji reaction api pagination support

---
 ...emoji_reactioned_by_accounts_controller.rb |  2 +-
 .../mastodon/actions/interactions.js          | 52 ++++++++++++++++++-
 .../features/emoji_reactions/index.jsx        | 29 +++++++----
 .../mastodon/reducers/user_lists.js           | 36 ++++++++++++-
 4 files changed, 104 insertions(+), 15 deletions(-)

diff --git a/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb
index 92cd5228cc..9c2fb3d4a5 100644
--- a/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb
@@ -33,7 +33,7 @@ class Api::V1::Statuses::EmojiReactionedByAccountsController < Api::BaseControll
 
   def paginated_emoji_reactions
     EmojiReaction.paginate_by_max_id(
-      limit_param(1000), # limit_param(DEFAULT_ACCOUNTS_LIMIT),
+      limit_param(DEFAULT_ACCOUNTS_LIMIT),
       params[:max_id],
       params[:since_id]
     )
diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index a5f6729f41..b361809309 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -51,6 +51,10 @@ export const EMOJI_REACTIONS_FETCH_REQUEST = 'EMOJI_REACTIONS_FETCH_REQUEST';
 export const EMOJI_REACTIONS_FETCH_SUCCESS = 'EMOJI_REACTIONS_FETCH_SUCCESS';
 export const EMOJI_REACTIONS_FETCH_FAIL    = 'EMOJI_REACTIONS_FETCH_FAIL';
 
+export const EMOJI_REACTIONS_EXPAND_REQUEST = 'EMOJI_REACTIONS_EXPAND_REQUEST';
+export const EMOJI_REACTIONS_EXPAND_SUCCESS = 'EMOJI_REACTIONS_EXPAND_SUCCESS';
+export const EMOJI_REACTIONS_EXPAND_FAIL    = 'EMOJI_REACTIONS_EXPAND_FAIL';
+
 export const PIN_REQUEST = 'PIN_REQUEST';
 export const PIN_SUCCESS = 'PIN_SUCCESS';
 export const PIN_FAIL    = 'PIN_FAIL';
@@ -547,8 +551,9 @@ export function fetchEmojiReactions(id) {
     dispatch(fetchEmojiReactionsRequest(id));
 
     api(getState).get(`/api/v1/statuses/${id}/emoji_reactioned_by`).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
       dispatch(importFetchedAccounts(response.data.map((er) => er.account)));
-      dispatch(fetchEmojiReactionsSuccess(id, response.data));
+      dispatch(fetchEmojiReactionsSuccess(id, response.data, next ? next.uri : null));
     }).catch(error => {
       dispatch(fetchEmojiReactionsFail(id, error));
     });
@@ -562,11 +567,12 @@ export function fetchEmojiReactionsRequest(id) {
   };
 }
 
-export function fetchEmojiReactionsSuccess(id, accounts) {
+export function fetchEmojiReactionsSuccess(id, accounts, next) {
   return {
     type: EMOJI_REACTIONS_FETCH_SUCCESS,
     id,
     accounts,
+    next,
   };
 }
 
@@ -577,6 +583,48 @@ export function fetchEmojiReactionsFail(id, error) {
   };
 }
 
+export function expandEmojiReactions(id) {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'emoji_reactioned_by', id, 'next']);
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandEmojiReactionsRequest(id));
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+
+      dispatch(importFetchedAccounts(response.data.map((er) => er.account)));
+      dispatch(expandEmojiReactionsSuccess(id, response.data, next ? next.uri : null));
+    }).catch(error => dispatch(expandEmojiReactionsFail(id, error)));
+  };
+}
+
+export function expandEmojiReactionsRequest(id) {
+  return {
+    type: EMOJI_REACTIONS_EXPAND_REQUEST,
+    id,
+  };
+}
+
+export function expandEmojiReactionsSuccess(id, accounts, next) {
+  return {
+    type: EMOJI_REACTIONS_EXPAND_SUCCESS,
+    id,
+    accounts,
+    next,
+  };
+}
+
+export function expandEmojiReactionsFail(id, error) {
+  return {
+    type: EMOJI_REACTIONS_EXPAND_FAIL,
+    id,
+    error,
+  };
+}
+
 export function fetchStatusReferences(id) {
   return (dispatch, getState) => {
     dispatch(fetchStatusReferencesRequest(id));
diff --git a/app/javascript/mastodon/features/emoji_reactions/index.jsx b/app/javascript/mastodon/features/emoji_reactions/index.jsx
index 78d088e947..5ab2a34958 100644
--- a/app/javascript/mastodon/features/emoji_reactions/index.jsx
+++ b/app/javascript/mastodon/features/emoji_reactions/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 { fetchEmojiReactions } from 'mastodon/actions/interactions';
+import { debounce } from 'lodash';
+
+import { fetchEmojiReactions, expandEmojiReactions } from 'mastodon/actions/interactions';
 import ColumnHeader from 'mastodon/components/column_header';
 import { Icon } from 'mastodon/components/icon';
 import ScrollableList from 'mastodon/components/scrollable_list';
@@ -25,7 +27,9 @@ const messages = defineMessages({
 
 const mapStateToProps = (state, props) => {
   return {
-    accountIds: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId]),
+    accountIds: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'items']),
+    hasMore: !!state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'next']),
+    isLoading: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'isLoading'], true),
   };
 };
 
@@ -35,6 +39,8 @@ class EmojiReactions 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,18 +51,16 @@ class EmojiReactions extends ImmutablePureComponent {
     }
   }
 
-  componentWillReceiveProps (nextProps) {
-    if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
-      this.props.dispatch(fetchEmojiReactions(nextProps.params.statusId));
-    }
-  }
-
   handleRefresh = () => {
     this.props.dispatch(fetchEmojiReactions(this.props.params.statusId));
   };
 
+  handleLoadMore = debounce(() => {
+    this.props.dispatch(expandEmojiReactions(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 (
@@ -68,7 +72,7 @@ class EmojiReactions extends ImmutablePureComponent {
 
     let groups = {};
     for (const emoji_reaction of accountIds) {
-      const key = emoji_reaction.account.id;
+      const key = emoji_reaction.account_id;
       const value = emoji_reaction;
       if (!groups[key]) groups[key] = [value];
       else groups[key].push(value);
@@ -82,12 +86,15 @@ class EmojiReactions extends ImmutablePureComponent {
           showBackButton
           multiColumn={multiColumn}
           extraButton={(
-            <button type='button' className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
+            <button type='button' className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleLoadMore}><Icon id='refresh' /></button>
           )}
         />
 
         <ScrollableList
           scrollKey='emoji_reactions'
+          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 edc83bd3d4..2cb41cd03d 100644
--- a/app/javascript/mastodon/reducers/user_lists.js
+++ b/app/javascript/mastodon/reducers/user_lists.js
@@ -57,7 +57,12 @@ import {
   FAVOURITES_EXPAND_REQUEST,
   FAVOURITES_EXPAND_SUCCESS,
   FAVOURITES_EXPAND_FAIL,
+  EMOJI_REACTIONS_FETCH_REQUEST,
   EMOJI_REACTIONS_FETCH_SUCCESS,
+  EMOJI_REACTIONS_FETCH_FAIL,
+  EMOJI_REACTIONS_EXPAND_REQUEST,
+  EMOJI_REACTIONS_EXPAND_SUCCESS,
+  EMOJI_REACTIONS_EXPAND_FAIL,
   STATUS_REFERENCES_FETCH_SUCCESS,
 } from '../actions/interactions';
 import {
@@ -101,12 +106,33 @@ const normalizeList = (state, path, accounts, next) => {
   }));
 };
 
+const normalizeEmojiReactionList = (state, path, rows, next) => {
+  return state.setIn(path, ImmutableMap({
+    next,
+    items: ImmutableList(rows.map(normalizeEmojiReactionRow)),
+    isLoading: false,
+  }));
+};
+
 const appendToList = (state, path, accounts, next) => {
   return state.updateIn(path, map => {
     return map.set('next', next).set('isLoading', false).update('items', list => list.concat(accounts.map(item => item.id)));
   });
 };
 
+const appendToEmojiReactionList = (state, path, rows, next) => {
+  return state.updateIn(path, map => {
+    return map.set('next', next).set('isLoading', false).update('items', list => list.concat(rows.map(normalizeEmojiReactionRow)));
+  });
+};
+
+const normalizeEmojiReactionRow = (row) => {
+  const accountId = row.account ? row.account.id : 0;
+  delete row.account;
+  row.account_id = accountId;
+  return row;
+};
+
 const normalizeFollowRequest = (state, notification) => {
   return state.updateIn(['follow_requests', 'items'], list => {
     return list.filterNot(item => item === notification.account.id).unshift(notification.account.id);
@@ -167,8 +193,16 @@ export default function userLists(state = initialState, action) {
   case FAVOURITES_FETCH_FAIL:
   case FAVOURITES_EXPAND_FAIL:
     return state.setIn(['favourited_by', action.id, 'isLoading'], false);
+  case EMOJI_REACTIONS_FETCH_REQUEST:
+  case EMOJI_REACTIONS_EXPAND_REQUEST:
+    return state.setIn(['emoji_reactioned_by', action.id, 'isLoading'], true);
+  case EMOJI_REACTIONS_FETCH_FAIL:
+  case EMOJI_REACTIONS_EXPAND_FAIL:
+    return state.setIn(['emoji_reactioned_by', action.id, 'isLoading'], false);
   case EMOJI_REACTIONS_FETCH_SUCCESS:
-    return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts));
+    return normalizeEmojiReactionList(state, ['emoji_reactioned_by', action.id], action.accounts, action.next);
+  case EMOJI_REACTIONS_EXPAND_SUCCESS:
+    return appendToEmojiReactionList(state, ['emoji_reactioned_by', action.id], action.accounts, action.next);
   case STATUS_REFERENCES_FETCH_SUCCESS:
     return state.setIn(['referred_by', action.id], ImmutableList(action.statuses.map(item => item.id)));
   case NOTIFICATIONS_UPDATE: