diff --git a/app/controllers/api/v1/antennas_controller.rb b/app/controllers/api/v1/antennas_controller.rb
index c4b00c7784856d..37bfb7f55242f5 100644
--- a/app/controllers/api/v1/antennas_controller.rb
+++ b/app/controllers/api/v1/antennas_controller.rb
@@ -42,6 +42,6 @@ def set_antenna
end
def antenna_params
- params.permit(:title, :list_id, :insert_feeds, :stl, :with_media_only, :ignore_reblog)
+ params.permit(:title, :list_id, :insert_feeds, :stl, :ltl, :with_media_only, :ignore_reblog)
end
end
diff --git a/app/javascript/mastodon/actions/antennas.js b/app/javascript/mastodon/actions/antennas.js
index 51002b6c5891cb..4716897586fe5e 100644
--- a/app/javascript/mastodon/actions/antennas.js
+++ b/app/javascript/mastodon/actions/antennas.js
@@ -236,10 +236,10 @@ export const createAntennaFail = error => ({
error,
});
-export const updateAntenna = (id, title, shouldReset, list_id, stl, with_media_only, ignore_reblog, insert_feeds) => (dispatch, getState) => {
+export const updateAntenna = (id, title, shouldReset, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds) => (dispatch, getState) => {
dispatch(updateAntennaRequest(id));
- api(getState).put(`/api/v1/antennas/${id}`, { title, list_id, stl, with_media_only, ignore_reblog, insert_feeds }).then(({ data }) => {
+ api(getState).put(`/api/v1/antennas/${id}`, { title, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds }).then(({ data }) => {
dispatch(updateAntennaSuccess(data));
if (shouldReset) {
diff --git a/app/javascript/mastodon/features/antenna_setting/index.jsx b/app/javascript/mastodon/features/antenna_setting/index.jsx
index 83f21fd066ec3b..086c1de6b63f84 100644
--- a/app/javascript/mastodon/features/antenna_setting/index.jsx
+++ b/app/javascript/mastodon/features/antenna_setting/index.jsx
@@ -198,31 +198,37 @@ class AntennaSetting extends PureComponent {
onStlToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
- dispatch(updateAntenna(id, undefined, false, undefined, target.checked, undefined, undefined, undefined));
+ dispatch(updateAntenna(id, undefined, false, undefined, target.checked, undefined, undefined, undefined, undefined));
+ };
+
+ onLtlToggle = ({ target }) => {
+ const { dispatch } = this.props;
+ const { id } = this.props.params;
+ dispatch(updateAntenna(id, undefined, false, undefined, undefined, target.checked, undefined, undefined, undefined));
};
onMediaOnlyToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
- dispatch(updateAntenna(id, undefined, false, undefined, undefined, target.checked, undefined, undefined));
+ dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, target.checked, undefined, undefined));
};
onIgnoreReblogToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
- dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, target.checked, undefined));
+ dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, undefined, target.checked, undefined));
};
onNoInsertFeedsToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
- dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, undefined, target.checked));
+ dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, undefined, undefined, target.checked));
};
onSelect = value => {
const { dispatch } = this.props;
const { id } = this.props.params;
- dispatch(updateAntenna(id, undefined, false, value.value, undefined, undefined, undefined, undefined));
+ dispatch(updateAntenna(id, undefined, false, value.value, undefined, undefined, undefined, undefined, undefined));
};
onHomeSelect = () => this.onSelect({ value: '0' });
@@ -293,6 +299,7 @@ class AntennaSetting extends PureComponent {
const pinned = !!columnId;
const title = antenna ? antenna.get('title') : id;
const isStl = antenna ? antenna.get('stl') : undefined;
+ const isLtl = antenna ? antenna.get('ltl') : undefined;
const isMediaOnly = antenna ? antenna.get('with_media_only') : undefined;
const isIgnoreReblog = antenna ? antenna.get('ignore_reblog') : undefined;
const isInsertFeeds = antenna ? antenna.get('insert_feeds') : undefined;
@@ -312,7 +319,7 @@ class AntennaSetting extends PureComponent {
}
let columnSettings;
- if (!isStl) {
+ if (!isStl && !isLtl) {
columnSettings = (
<>
@@ -339,6 +346,12 @@ class AntennaSetting extends PureComponent {
);
+ } else if (isLtl) {
+ stlAlert = (
+
+ );
}
const rangeRadioValues = ImmutableList([
@@ -384,12 +397,23 @@ class AntennaSetting extends PureComponent {
-
-
-
-
+ {!isLtl && (
+
+
+
+
+ )}
+
+ {!isStl && (
+
+
+
+
+ )}
@@ -429,7 +453,7 @@ class AntennaSetting extends PureComponent {
>
)}
- {!isStl && (
+ {!isStl && !isLtl && (
<>
diff --git a/app/models/antenna.rb b/app/models/antenna.rb
index 29872be182594a..c14892e138bac4 100644
--- a/app/models/antenna.rb
+++ b/app/models/antenna.rb
@@ -25,6 +25,7 @@
# stl :boolean default(FALSE), not null
# ignore_reblog :boolean default(FALSE), not null
# insert_feeds :boolean default(FALSE), not null
+# ltl :boolean default(FALSE), not null
#
class Antenna < ApplicationRecord
include Expireable
@@ -45,16 +46,19 @@ class Antenna < ApplicationRecord
belongs_to :list, optional: true
scope :stls, -> { where(stl: true) }
+ scope :ltls, -> { where(ltl: true) }
scope :all_keywords, -> { where(any_keywords: true) }
scope :all_domains, -> { where(any_domains: true) }
scope :all_accounts, -> { where(any_accounts: true) }
scope :all_tags, -> { where(any_tags: true) }
scope :availables, -> { where(available: true).where(Arel.sql('any_keywords = FALSE OR any_domains = FALSE OR any_accounts = FALSE OR any_tags = FALSE')) }
scope :available_stls, -> { where(available: true, stl: true) }
+ scope :available_ltls, -> { where(available: true, stl: false, ltl: true) }
validate :list_owner
validate :validate_limit
validate :validate_stl_limit
+ validate :validate_ltl_limit
def list_owner
raise Mastodon::ValidationError, I18n.t('antennas.errors.invalid_list_owner') if !list_id.zero? && list.present? && list.account != account
@@ -235,6 +239,22 @@ def validate_stl_limit
stls = account.antennas.where(stl: true).where.not(id: id)
- errors.add(:base, I18n.t('antennas.errors.over_stl_limit', limit: 1)) if list_id.zero? ? stls.any? { |tl| tl.list_id.zero? } : stls.any? { |tl| tl.list_id != 0 }
+ errors.add(:base, I18n.t('antennas.errors.over_stl_limit', limit: 1)) if if insert_feeds
+ list_id.zero? ? stls.any? { |tl| tl.list_id.zero? } : stls.any? { |tl| tl.list_id != 0 }
+ else
+ stls.any? { |tl| !tl.insert_feeds }
+ end
+ end
+
+ def validate_ltl_limit
+ return unless ltl
+
+ ltls = account.antennas.where(ltl: true).where.not(id: id)
+
+ errors.add(:base, I18n.t('antennas.errors.over_ltl_limit', limit: 1)) if if insert_feeds
+ list_id.zero? ? ltls.any? { |tl| tl.list_id.zero? } : ltls.any? { |tl| tl.list_id != 0 }
+ else
+ ltls.any? { |tl| !tl.insert_feeds }
+ end
end
end
diff --git a/app/serializers/rest/antenna_serializer.rb b/app/serializers/rest/antenna_serializer.rb
index 0d449a208931da..c0d03c7282f5c0 100644
--- a/app/serializers/rest/antenna_serializer.rb
+++ b/app/serializers/rest/antenna_serializer.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class REST::AntennaSerializer < ActiveModel::Serializer
- attributes :id, :title, :stl, :insert_feeds, :with_media_only, :ignore_reblog, :accounts_count, :domains_count, :tags_count, :keywords_count
+ attributes :id, :title, :stl, :ltl, :insert_feeds, :with_media_only, :ignore_reblog, :accounts_count, :domains_count, :tags_count, :keywords_count
class ListSerializer < ActiveModel::Serializer
attributes :id, :title
diff --git a/app/services/delivery_antenna_service.rb b/app/services/delivery_antenna_service.rb
index 9c4914515d7ab1..7600dd47858c4e 100644
--- a/app/services/delivery_antenna_service.rb
+++ b/app/services/delivery_antenna_service.rb
@@ -3,15 +3,19 @@
class DeliveryAntennaService
include FormattingHelper
- def call(status, update, stl_home)
+ def call(status, update, **options)
@status = status
@account = @status.account
@update = update
- if stl_home
- delivery_stl!
- else
+ mode = options[:mode] || :home
+ case mode
+ when :home
delivery!
+ when :stl
+ delivery_stl!
+ when :ltl
+ delivery_ltl!
end
end
@@ -44,7 +48,7 @@ def delivery!
antennas = antennas.where(account: @status.mentioned_accounts) if @status.visibility.to_sym == :limited
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)
+ antennas = antennas.where(stl: false, ltl: false)
collection = AntennaCollection.new(@status, @update, false)
content = extract_status_plain_text_with_spoiler_text(@status)
@@ -72,7 +76,7 @@ def delivery_stl!
antennas = antennas.where(account_id: Account.without_suspended.joins(:user).select('accounts.id').where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago))
home_post = !@account.domain.nil? || @status.reblog? || [:public, :public_unlisted, :login].exclude?(@status.visibility.to_sym)
- antennas = antennas.where(account: @account.followers).or(antennas.where(account: @account)).where.not(list_id: 0) if home_post
+ antennas = antennas.where(account: @account.followers).or(antennas.where(account: @account)).where('insert_feeds IS FALSE OR list_id > 0') if home_post
collection = AntennaCollection.new(@status, @update, home_post)
@@ -87,6 +91,26 @@ def delivery_stl!
collection.deliver!
end
+ def delivery_ltl!
+ return if %i(public public_unlisted login).exclude?(@status.visibility.to_sym)
+ return unless @account.local?
+
+ antennas = Antenna.available_ltls
+ antennas = antennas.where(account_id: Account.without_suspended.joins(:user).select('accounts.id').where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago))
+
+ collection = AntennaCollection.new(@status, @update, false)
+
+ antennas.in_batches do |ans|
+ ans.each do |antenna|
+ next if antenna.expired?
+
+ collection.push(antenna)
+ end
+ end
+
+ collection.deliver!
+ end
+
class AntennaCollection
def initialize(status, update, stl_home = false) # rubocop:disable Style/OptionalBooleanParameter
@status = status
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 156a811f51727f..8fe660e346159d 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -53,6 +53,7 @@ def fan_out_to_local_recipients!
deliver_to_lists!
deliver_to_antennas! if !@account.dissubscribable || (@status.dtl? && @account.user&.setting_dtl_force_subscribable && @status.tags.exists?(name: 'kmyblue'))
deliver_to_stl_antennas!
+ deliver_to_ltl_antennas!
when :limited
deliver_to_lists_mentioned_accounts_only!
deliver_to_antennas! unless @account.dissubscribable
@@ -135,11 +136,15 @@ def deliver_to_lists_mentioned_accounts_only!
end
def deliver_to_stl_antennas!
- DeliveryAntennaService.new.call(@status, @options[:update], true)
+ DeliveryAntennaService.new.call(@status, @options[:update], mode: :stl)
+ end
+
+ def deliver_to_ltl_antennas!
+ DeliveryAntennaService.new.call(@status, @options[:update], mode: :ltl)
end
def deliver_to_antennas!
- DeliveryAntennaService.new.call(@status, @options[:update], false)
+ DeliveryAntennaService.new.call(@status, @options[:update], mode: :home)
end
def deliver_to_mentioned_followers!
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 3a4a53d1d6c622..80d20c806198db 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1113,6 +1113,7 @@ en:
invalid_context: None or invalid context supplied
invalid_list_owner: This list is not yours
over_limit: You have exceeded the limit of %{limit} antennas
+ over_ltl_limit: You have exceeded the limit of %{limit} ltl antennas
over_stl_limit: You have exceeded the limit of %{limit} stl antennas
index:
contexts: Antennas in %{contexts}
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 4176170227c596..1719f17acd538c 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -1032,6 +1032,7 @@ ja:
keywords: 登録できるキーワード数の上限に達しています
tags: 登録できるタグ数の上限に達しています
over_limit: 所持できるアンテナ数 %{limit}を超えています
+ over_ltl_limit: 所持できるLTLモード付きアンテナ数 (ホーム/リストそれぞれにつき%{limit}) を超えています
over_stl_limit: 所持できるSTLモード付きアンテナ数 (ホーム/リストそれぞれにつき%{limit}) を超えています
too_short_keyword: キーワードが短すぎます
edit:
diff --git a/db/migrate/20230911022527_add_ltl_to_antennas.rb b/db/migrate/20230911022527_add_ltl_to_antennas.rb
new file mode 100644
index 00000000000000..18eeb19a3ccde1
--- /dev/null
+++ b/db/migrate/20230911022527_add_ltl_to_antennas.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require Rails.root.join('lib', 'mastodon', 'migration_helpers')
+
+class AddLtlToAntennas < ActiveRecord::Migration[7.0]
+ include Mastodon::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def change
+ safety_assured do
+ add_column_with_default :antennas, :ltl, :boolean, default: false, allow_null: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1ade0757828a37..f322d5e715336d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
+ActiveRecord::Schema[7.0].define(version: 2023_09_11_022527) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -50,6 +50,15 @@
t.index ["account_id", "domain"], name: "index_account_domain_blocks_on_account_id_and_domain", unique: true
end
+ create_table "account_groups", force: :cascade do |t|
+ t.bigint "account_id", null: false
+ t.bigint "group_account_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id"], name: "index_account_groups_on_account_id"
+ t.index ["group_account_id"], name: "index_account_groups_on_group_account_id"
+ end
+
create_table "account_migrations", force: :cascade do |t|
t.bigint "account_id"
t.string "acct", default: "", null: false
@@ -313,6 +322,7 @@
t.boolean "stl", default: false, null: false
t.boolean "ignore_reblog", default: false, null: false
t.boolean "insert_feeds", default: false, null: false
+ t.boolean "ltl", default: false, null: false
t.index ["account_id"], name: "index_antennas_on_account_id"
t.index ["any_accounts"], name: "index_antennas_on_any_accounts"
t.index ["any_domains"], name: "index_antennas_on_any_domains"
@@ -1357,6 +1367,7 @@
add_foreign_key "account_conversations", "conversations", on_delete: :cascade
add_foreign_key "account_deletion_requests", "accounts", on_delete: :cascade
add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
+ add_foreign_key "account_groups", "accounts", on_delete: :cascade
add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify
add_foreign_key "account_migrations", "accounts", on_delete: :cascade
add_foreign_key "account_moderation_notes", "accounts"
diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb
index e3d8c952dc2840..142794f4db7ea0 100644
--- a/spec/services/fan_out_on_write_service_spec.rb
+++ b/spec/services/fan_out_on_write_service_spec.rb
@@ -56,6 +56,10 @@ def antenna_with_account(owner, target_account)
antenna
end
+ def antenna_with_options(owner, **options)
+ Fabricate(:antenna, account: owner, **options)
+ end
+
context 'when status is public' do
let(:visibility) { 'public' }
@@ -97,6 +101,26 @@ def antenna_with_account(owner, target_account)
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
+
+ context 'with STL antenna' do
+ let!(:antenna) { antenna_with_options(bob, stl: true) }
+ let!(:empty_antenna) { antenna_with_options(tom) }
+
+ it 'is added to the antenna feed of antenna follower' do
+ expect(antenna_feed_of(antenna)).to include status.id
+ expect(antenna_feed_of(empty_antenna)).to_not include status.id
+ end
+ end
+
+ context 'with LTL antenna' do
+ let!(:antenna) { antenna_with_options(bob, ltl: true) }
+ let!(:empty_antenna) { antenna_with_options(tom) }
+
+ it 'is added to the antenna feed of antenna follower' do
+ expect(antenna_feed_of(antenna)).to include status.id
+ expect(antenna_feed_of(empty_antenna)).to_not include status.id
+ end
+ end
end
context 'when status is limited' do
@@ -176,6 +200,24 @@ def antenna_with_account(owner, target_account)
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
+
+ context 'with STL antenna' do
+ let!(:antenna) { antenna_with_options(bob, stl: true) }
+ let!(:empty_antenna) { antenna_with_options(ohagi, stl: true) }
+
+ it 'is added to the antenna feed of antenna follower' do
+ expect(antenna_feed_of(antenna)).to include status.id
+ expect(antenna_feed_of(empty_antenna)).to_not include status.id
+ end
+ end
+
+ context 'with LTL antenna' do
+ let!(:empty_antenna) { antenna_with_options(bob, ltl: true) }
+
+ it 'is added to the antenna feed of antenna follower' do
+ expect(antenna_feed_of(empty_antenna)).to_not include status.id
+ end
+ end
end
context 'when status is unlisted' do
@@ -215,6 +257,24 @@ def antenna_with_account(owner, target_account)
end
end
+ context 'with STL antenna' do
+ let!(:antenna) { antenna_with_options(bob, stl: true) }
+ let!(:empty_antenna) { antenna_with_options(ohagi, stl: true) }
+
+ it 'is added to the antenna feed of antenna follower' do
+ expect(antenna_feed_of(antenna)).to include status.id
+ expect(antenna_feed_of(empty_antenna)).to_not include status.id
+ end
+ end
+
+ context 'with LTL antenna' do
+ let!(:empty_antenna) { antenna_with_options(bob, ltl: true) }
+
+ it 'is added to the antenna feed of antenna follower' do
+ expect(antenna_feed_of(empty_antenna)).to_not include status.id
+ end
+ end
+
context 'with non-public searchability' do
let(:searchability) { 'direct' }