Merge remote-tracking branch 'parent/main' into upstream-20240906
This commit is contained in:
commit
f18eabfe75
689 changed files with 4369 additions and 2434 deletions
|
@ -95,6 +95,7 @@ class Account < ApplicationRecord
|
|||
include DomainMaterializable
|
||||
include DomainNormalizable
|
||||
include Paginable
|
||||
include Reviewable
|
||||
|
||||
enum :protocol, { ostatus: 0, activitypub: 1 }
|
||||
enum :suspension_origin, { local: 0, remote: 1 }, prefix: true
|
||||
|
@ -153,6 +154,7 @@ class Account < ApplicationRecord
|
|||
scope :with_username, ->(value) { where arel_table[:username].lower.eq(value.to_s.downcase) }
|
||||
scope :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) }
|
||||
scope :without_memorial, -> { where(memorial: false) }
|
||||
scope :duplicate_uris, -> { select(:uri, Arel.star.count).group(:uri).having(Arel.star.count.gt(1)) }
|
||||
|
||||
after_update_commit :trigger_update_webhooks
|
||||
|
||||
|
@ -482,22 +484,6 @@ class Account < ApplicationRecord
|
|||
@synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
|
||||
end
|
||||
|
||||
def requires_review?
|
||||
reviewed_at.nil?
|
||||
end
|
||||
|
||||
def reviewed?
|
||||
reviewed_at.present?
|
||||
end
|
||||
|
||||
def requested_review?
|
||||
requested_review_at.present?
|
||||
end
|
||||
|
||||
def requires_review_notification?
|
||||
requires_review? && !requested_review?
|
||||
end
|
||||
|
||||
class << self
|
||||
def readonly_attributes
|
||||
super - %w(statuses_count following_count followers_count)
|
||||
|
|
|
@ -151,7 +151,7 @@ class AccountStatusesCleanupPolicy < ApplicationRecord
|
|||
end
|
||||
|
||||
def without_self_fav_scope
|
||||
Status.where('NOT EXISTS (SELECT 1 FROM favourites fav WHERE fav.account_id = statuses.account_id AND fav.status_id = statuses.id)')
|
||||
Status.where.not(self_status_reference_exists(Favourite))
|
||||
end
|
||||
|
||||
def without_self_emoji_scope
|
||||
|
@ -159,11 +159,11 @@ class AccountStatusesCleanupPolicy < ApplicationRecord
|
|||
end
|
||||
|
||||
def without_self_bookmark_scope
|
||||
Status.where('NOT EXISTS (SELECT 1 FROM bookmarks bookmark WHERE bookmark.account_id = statuses.account_id AND bookmark.status_id = statuses.id)')
|
||||
Status.where.not(self_status_reference_exists(Bookmark))
|
||||
end
|
||||
|
||||
def without_pinned_scope
|
||||
Status.where('NOT EXISTS (SELECT 1 FROM status_pins pin WHERE pin.account_id = statuses.account_id AND pin.status_id = statuses.id)')
|
||||
Status.where.not(self_status_reference_exists(StatusPin))
|
||||
end
|
||||
|
||||
def without_media_scope
|
||||
|
@ -185,4 +185,13 @@ class AccountStatusesCleanupPolicy < ApplicationRecord
|
|||
def account_statuses
|
||||
Status.where(account_id: account_id)
|
||||
end
|
||||
|
||||
def self_status_reference_exists(model)
|
||||
model
|
||||
.where(model.arel_table[:account_id].eq Status.arel_table[:account_id])
|
||||
.where(model.arel_table[:status_id].eq Status.arel_table[:id])
|
||||
.select(1)
|
||||
.arel
|
||||
.exists
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
module Redisable
|
||||
def redis
|
||||
Thread.current[:redis] ||= RedisConfiguration.pool.checkout
|
||||
Thread.current[:redis] ||= RedisConnection.pool.checkout
|
||||
end
|
||||
|
||||
def with_redis(&block)
|
||||
RedisConfiguration.with(&block)
|
||||
RedisConnection.with(&block)
|
||||
end
|
||||
end
|
||||
|
|
21
app/models/concerns/reviewable.rb
Normal file
21
app/models/concerns/reviewable.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Reviewable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def requires_review?
|
||||
reviewed_at.nil?
|
||||
end
|
||||
|
||||
def reviewed?
|
||||
reviewed_at.present?
|
||||
end
|
||||
|
||||
def requested_review?
|
||||
requested_review_at.present?
|
||||
end
|
||||
|
||||
def requires_review_notification?
|
||||
requires_review? && !requested_review?
|
||||
end
|
||||
end
|
|
@ -31,8 +31,4 @@ class Mention < ApplicationRecord
|
|||
to: :account,
|
||||
prefix: true
|
||||
)
|
||||
|
||||
def active?
|
||||
!silent?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NotificationGroup < ActiveModelSerializers::Model
|
||||
attributes :group_key, :sample_accounts, :notifications_count, :notification, :most_recent_notification_id, :emoji_reaction_groups, :list
|
||||
attributes :group_key, :sample_accounts, :notifications_count, :notification, :most_recent_notification_id, :pagination_data, :emoji_reaction_groups, :list
|
||||
|
||||
# Try to keep this consistent with `app/javascript/mastodon/models/notification_group.ts`
|
||||
SAMPLE_ACCOUNTS_SIZE = 8
|
||||
|
@ -11,41 +11,55 @@ class NotificationGroup < ActiveModelSerializers::Model
|
|||
attributes :emoji_reaction, :sample_accounts
|
||||
end
|
||||
|
||||
def self.from_notification(notification, max_id: nil, grouped_types: nil)
|
||||
def self.from_notifications(notifications, pagination_range: nil, grouped_types: nil)
|
||||
return [] if notifications.empty?
|
||||
|
||||
grouped_types = grouped_types.presence&.map(&:to_sym) || Notification::GROUPABLE_NOTIFICATION_TYPES
|
||||
groupable = notification.group_key.present? && grouped_types.include?(notification.type)
|
||||
|
||||
if groupable
|
||||
# TODO: caching, and, if caching, preloading
|
||||
scope = notification.account.notifications.where(group_key: notification.group_key)
|
||||
scope = scope.where(id: ..max_id) if max_id.present?
|
||||
grouped_notifications = notifications.filter { |notification| notification.group_key.present? && grouped_types.include?(notification.type) }
|
||||
group_keys = grouped_notifications.pluck(:group_key)
|
||||
|
||||
# Ideally, we would not load accounts for each notification group
|
||||
most_recent_notifications = scope.order(id: :desc).includes(:from_account, :list_status).take(SAMPLE_ACCOUNTS_SIZE)
|
||||
most_recent_id = most_recent_notifications.first.id
|
||||
sample_accounts = most_recent_notifications.map(&:from_account)
|
||||
emoji_reaction_groups = extract_emoji_reaction_pair(
|
||||
scope.order(id: :desc).includes(emoji_reaction: :account).take(SAMPLE_ACCOUNTS_SIZE_FOR_EMOJI_REACTION)
|
||||
)
|
||||
list = pick_list(most_recent_notifications)
|
||||
notifications_count = scope.count
|
||||
else
|
||||
most_recent_id = notification.id
|
||||
sample_accounts = [notification.from_account]
|
||||
emoji_reaction_groups = extract_emoji_reaction_pair([notification])
|
||||
list = pick_list([notification])
|
||||
notifications_count = 1
|
||||
with_emoji_reaction = grouped_notifications.any? { |notification| notification.type == :emoji_reaction }
|
||||
notifications.any? { |notification| notification.type == :list_status }
|
||||
|
||||
groups_data = load_groups_data(notifications.first.account_id, group_keys, pagination_range: pagination_range)
|
||||
accounts_map = Account.where(id: groups_data.values.pluck(1).flatten).index_by(&:id)
|
||||
|
||||
notifications.map do |notification|
|
||||
if notification.group_key.present? && grouped_types.include?(notification.type)
|
||||
most_recent_notification_id, sample_account_ids, count, activity_ids, *raw_pagination_data = groups_data[notification.group_key]
|
||||
|
||||
pagination_data = raw_pagination_data.empty? ? nil : { min_id: raw_pagination_data[0], latest_notification_at: raw_pagination_data[1] }
|
||||
|
||||
emoji_reaction_groups = with_emoji_reaction ? convert_emoji_reaction_pair(activity_ids) : []
|
||||
|
||||
NotificationGroup.new(
|
||||
notification: notification,
|
||||
group_key: notification.group_key,
|
||||
sample_accounts: sample_account_ids.map { |id| accounts_map[id] },
|
||||
notifications_count: count,
|
||||
most_recent_notification_id: most_recent_notification_id,
|
||||
pagination_data: pagination_data,
|
||||
emoji_reaction_groups: emoji_reaction_groups
|
||||
)
|
||||
else
|
||||
pagination_data = pagination_range.blank? ? nil : { min_id: notification.id, latest_notification_at: notification.created_at }
|
||||
|
||||
emoji_reaction_groups = convert_emoji_reaction_pair([notification.activity_id])
|
||||
list = notification.type == :list_status ? notification.list_status&.list : nil
|
||||
|
||||
NotificationGroup.new(
|
||||
notification: notification,
|
||||
group_key: "ungrouped-#{notification.id}",
|
||||
sample_accounts: [notification.from_account],
|
||||
notifications_count: 1,
|
||||
most_recent_notification_id: notification.id,
|
||||
pagination_data: pagination_data,
|
||||
emoji_reaction_groups: emoji_reaction_groups,
|
||||
list: list
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
NotificationGroup.new(
|
||||
notification: notification,
|
||||
group_key: groupable ? notification.group_key : "ungrouped-#{notification.id}",
|
||||
sample_accounts: sample_accounts,
|
||||
emoji_reaction_groups: emoji_reaction_groups,
|
||||
list: list,
|
||||
notifications_count: notifications_count,
|
||||
most_recent_notification_id: most_recent_id
|
||||
)
|
||||
end
|
||||
|
||||
delegate :type,
|
||||
|
@ -55,24 +69,60 @@ class NotificationGroup < ActiveModelSerializers::Model
|
|||
:account_warning,
|
||||
to: :notification, prefix: false
|
||||
|
||||
def self.extract_emoji_reaction_pair(scope)
|
||||
return [] unless scope.first.type == :emoji_reaction
|
||||
def self.convert_emoji_reaction_pair(activity_ids)
|
||||
return [] if activity_ids.empty?
|
||||
|
||||
scope = scope.filter { |g| g.emoji_reaction.present? }
|
||||
return [] if scope.empty?
|
||||
|
||||
scope
|
||||
.each_with_object({}) { |e, h| h[e.emoji_reaction.name] = (h[e.emoji_reaction.name] || []).push(e.emoji_reaction) }
|
||||
.to_a
|
||||
.map { |pair| NotificationEmojiReactionGroup.new(emoji_reaction: pair[1].first, sample_accounts: pair[1].take(SAMPLE_ACCOUNTS_SIZE).map(&:account)) }
|
||||
EmojiReaction.where(id: activity_ids)
|
||||
.each_with_object({}) { |e, h| h[e.name] = (h[e.name] || []).push(e) }
|
||||
.to_a
|
||||
.map { |pair| NotificationEmojiReactionGroup.new(emoji_reaction: pair[1].first, sample_accounts: pair[1].take(SAMPLE_ACCOUNTS_SIZE).map(&:account)) }
|
||||
end
|
||||
|
||||
def self.pick_list(scope)
|
||||
return [] unless scope.first.type == :list_status
|
||||
class << self
|
||||
private
|
||||
|
||||
scope = scope.filter { |g| g.list_status.present? }
|
||||
return [] if scope.empty?
|
||||
def load_groups_data(account_id, group_keys, pagination_range: nil)
|
||||
return {} if group_keys.empty?
|
||||
|
||||
scope.first.list_status.list
|
||||
if pagination_range.present?
|
||||
binds = [
|
||||
account_id,
|
||||
SAMPLE_ACCOUNTS_SIZE,
|
||||
pagination_range.begin,
|
||||
pagination_range.end,
|
||||
ActiveRecord::Relation::QueryAttribute.new('group_keys', group_keys, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::String.new)),
|
||||
]
|
||||
|
||||
ActiveRecord::Base.connection.select_all(<<~SQL.squish, 'grouped_notifications', binds).cast_values.to_h { |k, *values| [k, values] }
|
||||
SELECT
|
||||
groups.group_key,
|
||||
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT 1),
|
||||
array(SELECT from_account_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT $2),
|
||||
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4) AS notifications_count,
|
||||
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 AND activity_type = 'EmojiReaction'),
|
||||
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id >= $3 ORDER BY id ASC LIMIT 1) AS min_id,
|
||||
(SELECT created_at FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT 1)
|
||||
FROM
|
||||
unnest($5::text[]) AS groups(group_key);
|
||||
SQL
|
||||
else
|
||||
binds = [
|
||||
account_id,
|
||||
SAMPLE_ACCOUNTS_SIZE,
|
||||
ActiveRecord::Relation::QueryAttribute.new('group_keys', group_keys, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::String.new)),
|
||||
]
|
||||
|
||||
ActiveRecord::Base.connection.select_all(<<~SQL.squish, 'grouped_notifications', binds).cast_values.to_h { |k, *values| [k, values] }
|
||||
SELECT
|
||||
groups.group_key,
|
||||
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key ORDER BY id DESC LIMIT 1),
|
||||
array(SELECT from_account_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key ORDER BY id DESC LIMIT $2),
|
||||
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key) AS notifications_count,
|
||||
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND activity_type = 'EmojiReaction')
|
||||
FROM
|
||||
unnest($3::text[]) AS groups(group_key);
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,6 +21,7 @@ class PreviewCardProvider < ApplicationRecord
|
|||
include Paginable
|
||||
include DomainNormalizable
|
||||
include Attachmentable
|
||||
include Reviewable
|
||||
|
||||
ICON_MIME_TYPES = %w(image/x-icon image/vnd.microsoft.icon image/png).freeze
|
||||
LIMIT = 1.megabyte
|
||||
|
@ -36,22 +37,6 @@ class PreviewCardProvider < ApplicationRecord
|
|||
scope :reviewed, -> { where.not(reviewed_at: nil) }
|
||||
scope :pending_review, -> { where(reviewed_at: nil) }
|
||||
|
||||
def requires_review?
|
||||
reviewed_at.nil?
|
||||
end
|
||||
|
||||
def reviewed?
|
||||
reviewed_at.present?
|
||||
end
|
||||
|
||||
def requested_review?
|
||||
requested_review_at.present?
|
||||
end
|
||||
|
||||
def requires_review_notification?
|
||||
requires_review? && !requested_review?
|
||||
end
|
||||
|
||||
def self.matching_domain(domain)
|
||||
segments = domain.split('.')
|
||||
where(domain: segments.map.with_index { |_, i| segments[i..].join('.') }).by_domain_length.first
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
class Tag < ApplicationRecord
|
||||
include Paginable
|
||||
include Reviewable
|
||||
|
||||
# rubocop:disable Rails/HasAndBelongsToMany
|
||||
has_and_belongs_to_many :statuses
|
||||
has_and_belongs_to_many :accounts
|
||||
|
@ -99,22 +101,6 @@ class Tag < ApplicationRecord
|
|||
|
||||
alias trendable? trendable
|
||||
|
||||
def requires_review?
|
||||
reviewed_at.nil?
|
||||
end
|
||||
|
||||
def reviewed?
|
||||
reviewed_at.present?
|
||||
end
|
||||
|
||||
def requested_review?
|
||||
requested_review_at.present?
|
||||
end
|
||||
|
||||
def requires_review_notification?
|
||||
requires_review? && !requested_review?
|
||||
end
|
||||
|
||||
def decaying?
|
||||
max_score_at && max_score_at >= Trends.tags.options[:max_score_cooldown].ago && max_score_at < 1.day.ago
|
||||
end
|
||||
|
|
|
@ -269,10 +269,6 @@ class User < ApplicationRecord
|
|||
unconfirmed? || pending?
|
||||
end
|
||||
|
||||
def inactive_message
|
||||
approved? ? super : :pending
|
||||
end
|
||||
|
||||
def approve!
|
||||
return if approved?
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue