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' }