Add: Webでの引用表示 (#50)

* Add compacted component

* 引用表示の間にコンテナをはさみ、不要なコードを削除

* 引用APIを作成、ついでにブロック状況を引用APIに反映

* テスト修正など

* 引用をキャッシュに登録

* `quote_id`が`quote_of_id`になったのをSerializerに反映

* Fix test

* 引用をフィルターの対象に含める設定+エラー修正

* ストリーミングの存在しないプロパティ削除によるエラーを修正

* Fix lint

* 他のサーバーから来た引用付き投稿を処理

* Fix test

* フィルター設定時エラーの調整

* 画像つき投稿のスタイルを調整

* 画像つき投稿の最大高さを調整

* 引用禁止・非表示の設定を追加

* ブロック対応

* マイグレーションコード調整

* 引用設定の翻訳を作成

* Lint修正

* 参照1つの場合は引用に変換する設定を削除

* 不要になったテストを削除

* ブロック設定追加、バグ修正

* 他サーバーへ引用送信・受け入れ
This commit is contained in:
KMY(雪あすか) 2023-10-02 11:12:51 +09:00 committed by GitHub
parent 3c649aa74d
commit 44b739a39a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 1362 additions and 120 deletions

View file

@ -330,6 +330,13 @@ class Account < ApplicationRecord
true
end
def allow_quote?
return user.setting_allow_quote if local? && user.present?
return settings['allow_quote'] if settings.present? && settings.key?('allow_quote')
true
end
def public_statuses_count
hide_statuses_count? ? 0 : statuses_count
end
@ -407,6 +414,7 @@ class Account < ApplicationRecord
'hide_followers_count' => hide_followers_count?,
'translatable_private' => translatable_private?,
'link_preview' => link_preview?,
'allow_quote' => allow_quote?,
}
if Setting.enable_emoji_reaction
config = config.merge({

View file

@ -111,6 +111,22 @@ module HasUserSettings
settings['web.use_system_font']
end
def setting_show_quote_in_home
settings['web.show_quote_in_home']
end
def setting_show_quote_in_public
settings['web.show_quote_in_public']
end
def setting_hide_blocking_quote
settings['web.hide_blocking_quote']
end
def setting_allow_quote
settings['allow_quote']
end
def setting_noindex
settings['noindex']
end
@ -127,10 +143,6 @@ module HasUserSettings
settings['link_preview']
end
def setting_single_ref_to_quote
settings['single_ref_to_quote']
end
def setting_dtl_force_with_tag
settings['dtl_force_with_tag']&.to_sym || :none
end

View file

@ -14,6 +14,7 @@
# action :integer default("warn"), not null
# exclude_follows :boolean default(FALSE), not null
# exclude_localusers :boolean default(FALSE), not null
# with_quote :boolean default(TRUE), not null
#
class CustomFilter < ApplicationRecord
@ -33,7 +34,7 @@ class CustomFilter < ApplicationRecord
include Expireable
include Redisable
enum action: { warn: 0, hide: 1 }, _suffix: :action
enum action: { warn: 0, hide: 1, half_warn: 2 }, _suffix: :action
belongs_to :account
has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy
@ -103,11 +104,15 @@ class CustomFilter < ApplicationRecord
if rules[:keywords].present?
match = rules[:keywords].match(status.proper.searchable_text)
match = rules[:keywords].match(status.proper.references.pluck(:text).join("\n\n")) if match.nil? && status.proper.references.exists?
if match.nil? && filter.with_quote && status.proper.references.exists?
match = rules[:keywords].match(status.proper.references.pluck(:text).join("\n\n"))
match = rules[:keywords].match(status.proper.references.pluck(:spoiler_text).join("\n\n")) if match.nil?
end
end
keyword_matches = [match.to_s] unless match.nil?
status_matches = [status.id, status.reblog_of_id].compact & rules[:status_ids] if rules[:status_ids].present?
reference_ids = filter.with_quote ? status.proper.references.pluck(:id) : []
status_matches = ([status.id, status.reblog_of_id] + reference_ids).compact & rules[:status_ids] if rules[:status_ids].present?
next if keyword_matches.blank? && status_matches.blank?

View file

@ -30,6 +30,7 @@
# searchability :integer
# markdown :boolean default(FALSE)
# limited_scope :integer
# quote_of_id :bigint(8)
#
require 'ostruct'
@ -69,12 +70,14 @@ class Status < ApplicationRecord
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
belongs_to :quote, foreign_key: 'quote_of_id', class_name: 'Status', inverse_of: :quotes, optional: true
has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :emoji_reactions, inverse_of: :status, dependent: :destroy
has_many :bookmarks, inverse_of: :status, dependent: :destroy
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
has_many :reblogged_by_accounts, through: :reblogs, class_name: 'Account', source: :account
has_many :quotes, foreign_key: 'quote_of_id', class_name: 'Status', inverse_of: :quote
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
has_many :mentions, dependent: :destroy, inverse_of: :status
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
@ -193,6 +196,19 @@ class Status < ApplicationRecord
account: [:account_stat, user: :role],
active_mentions: { account: :account_stat },
],
quote: [
:application,
:tags,
:preview_cards,
:media_attachments,
:conversation,
:status_stat,
:preloadable_poll,
:reference_objects,
:scheduled_expiration_status,
account: [:account_stat, user: :role],
active_mentions: { account: :account_stat },
],
thread: { account: :account_stat }
delegate :domain, to: :account, prefix: true
@ -227,8 +243,8 @@ class Status < ApplicationRecord
!reblog_of_id.nil?
end
def quote
reference_objects.where(attribute_type: 'QT').first&.target_status
def quote?
!quote_of_id.nil?
end
def within_realtime_window?
@ -480,12 +496,16 @@ class Status < ApplicationRecord
ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).each_with_object({}) { |m, h| h[m.conversation_id] = true }
end
def pins_map(status_ids, account_id)
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true }
def blocks_map(account_ids, account_id)
Block.where(account_id: account_id, target_account_id: account_ids).each_with_object({}) { |b, h| h[b.target_account_id] = true }
end
def emoji_reactions_map(status_ids, account_id)
EmojiReaction.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |e, h| h[e.status_id] = true }
def domain_blocks_map(domains, account_id)
AccountDomainBlock.where(account_id: account_id, domain: domains).each_with_object({}) { |d, h| h[d.domain] = true }
end
def pins_map(status_ids, account_id)
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true }
end
def emoji_reaction_allows_map(status_ids, account_id)

View file

@ -10,6 +10,7 @@
# created_at :datetime not null
# updated_at :datetime not null
# attribute_type :string
# quote :boolean default(FALSE), not null
#
class StatusReference < ApplicationRecord
@ -19,6 +20,8 @@ class StatusReference < ApplicationRecord
has_one :notification, as: :activity, dependent: :destroy
after_commit :reset_parent_cache
after_create_commit :set_quote
after_destroy_commit :remove_quote
private
@ -26,4 +29,18 @@ class StatusReference < ApplicationRecord
Rails.cache.delete("statuses/#{status_id}")
Rails.cache.delete("statuses/#{target_status_id}")
end
def set_quote
return unless quote
return if status.quote_of_id.present?
status.quote_of_id = target_status_id
end
def remove_quote
return unless quote
return unless status.quote_of_id == target_status_id
status.quote_of_id = nil
end
end

View file

@ -41,7 +41,7 @@ class UserSettings
setting :dtl_force_with_tag, default: :none, in: %w(full searchability none)
setting :dtl_force_subscribable, default: false
setting :lock_follow_from_bot, default: false
setting :single_ref_to_quote, default: false
setting :allow_quote, default: true
setting_inverse_alias :indexable, :noindex
@ -67,6 +67,9 @@ class UserSettings
setting :display_media_expand, default: true
setting :auto_play, default: true
setting :simple_timeline_menu, default: false
setting :show_quote_in_home, default: true
setting :show_quote_in_public, default: false
setting :hide_blocking_quote, default: true
end
namespace :notification_emails do