1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
# frozen_string_literal: true
module StatusThreadingConcern
extend ActiveSupport::Concern
def ancestors(limit, account = nil)
find_statuses_from_tree_path(ancestor_ids(limit), account)
end
def descendants(limit, account = nil, max_child_id = nil, since_child_id = nil, depth = nil)
find_statuses_from_tree_path(descendant_ids(limit, max_child_id, since_child_id, depth), account, promote: true)
end
def self_replies(limit)
account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted, :local]).reorder(id: :asc).limit(limit)
end
private
def ancestor_ids(limit)
key = "ancestors:#{id}"
ancestors = Rails.cache.fetch(key)
if ancestors.nil? || ancestors[:limit] < limit
ids = ancestor_statuses(limit).pluck(:id).reverse!
Rails.cache.write key, limit: limit, ids: ids
ids
else
ancestors[:ids].last(limit)
end
end
def ancestor_statuses(limit)
Status.find_by_sql([<<-SQL.squish, id: in_reply_to_id, limit: limit])
WITH RECURSIVE search_tree(id, in_reply_to_id, path)
AS (
SELECT id, in_reply_to_id, ARRAY[id]
FROM statuses
WHERE id = :id
UNION ALL
SELECT statuses.id, statuses.in_reply_to_id, path || statuses.id
FROM search_tree
JOIN statuses ON statuses.id = search_tree.in_reply_to_id
WHERE NOT statuses.id = ANY(path)
)
SELECT id
FROM search_tree
ORDER BY path
LIMIT :limit
SQL
end
def descendant_ids(limit, max_child_id, since_child_id, depth)
descendant_statuses(limit, max_child_id, since_child_id, depth).pluck(:id)
end
def descendant_statuses(limit, max_child_id, since_child_id, depth)
# use limit + 1 and depth + 1 because 'self' is included
depth += 1 if depth.present?
limit += 1 if limit.present?
descendants_with_self = Status.find_by_sql([<<-SQL.squish, id: id, limit: limit, max_child_id: max_child_id, since_child_id: since_child_id, depth: depth])
WITH RECURSIVE search_tree(id, path)
AS (
SELECT id, ARRAY[id]
FROM statuses
WHERE id = :id AND COALESCE(id < :max_child_id, TRUE) AND COALESCE(id > :since_child_id, TRUE)
UNION ALL
SELECT statuses.id, path || statuses.id
FROM search_tree
JOIN statuses ON statuses.in_reply_to_id = search_tree.id
WHERE COALESCE(array_length(path, 1) < :depth, TRUE) AND NOT statuses.id = ANY(path)
)
SELECT id
FROM search_tree
ORDER BY path
LIMIT :limit
SQL
descendants_with_self - [self]
end
def find_statuses_from_tree_path(ids, account, promote: false)
statuses = statuses_with_accounts(ids).to_a
account_ids = statuses.map(&:account_id).uniq
domains = statuses.map(&:account_domain).compact.uniq
relations = relations_map_for_account(account, account_ids, domains)
statuses.reject! { |status| filter_from_context?(status, account, relations) }
# Order ancestors/descendants by tree path
statuses.sort_by! { |status| ids.index(status.id) }
# Bring self-replies to the top
if promote
promote_by!(statuses) { |status| status.in_reply_to_account_id == status.account_id }
else
statuses
end
end
def promote_by!(arr)
insert_at = arr.find_index { |item| !yield(item) }
return arr if insert_at.nil?
arr.each_with_index do |item, index|
next if index <= insert_at || !yield(item)
arr.insert(insert_at, arr.delete_at(index))
insert_at += 1
end
arr
end
def relations_map_for_account(account, account_ids, domains)
return {} if account.nil?
{
blocking: Account.blocking_map(account_ids, account.id),
blocked_by: Account.blocked_by_map(account_ids, account.id),
muting: Account.muting_map(account_ids, account.id),
following: Account.following_map(account_ids, account.id),
domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
}
end
def statuses_with_accounts(ids)
kicked_accounts = ConversationKick.select(:account_id).where(conversation_id: self.conversation_id)
Status.where(id: ids).where.not(account_id: kicked_accounts).includes(:account)
end
def filter_from_context?(status, account, relations)
StatusFilter.new(status, account, relations).filtered?
end
end
|