def context
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(DEFAULT_STATUSES_LIMIT, current_account)
- descendants_results = @status.descendants(current_account)
+ descendants_results = @status.descendants(DEFAULT_STATUSES_LIMIT, current_account)
loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status)
include Authorization
ANCESTORS_LIMIT = 20
+ DESCENDANTS_LIMIT = 20
+ DESCENDANTS_DEPTH_LIMIT = 4
layout 'public'
def show
respond_to do |format|
format.html do
- @ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
- @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
- @descendants = cache_collection(@status.descendants(current_account), Status)
+ set_ancestors
+ set_descendants
render 'stream_entries/show'
end
private
+ def create_descendant_thread(depth, statuses)
+ if depth < DESCENDANTS_DEPTH_LIMIT
+ { statuses: statuses }
+ else
+ next_status = statuses.pop
+ { statuses: statuses, next_status: next_status }
+ end
+ end
+
def set_account
@account = Account.find_local!(params[:account_username])
end
+ def set_ancestors
+ @ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
+ @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
+ end
+
+ def set_descendants
+ @max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i
+ @since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i
+
+ descendants = cache_collection(
+ @status.descendants(
+ DESCENDANTS_LIMIT,
+ current_account,
+ @max_descendant_thread_id,
+ @since_descendant_thread_id,
+ DESCENDANTS_DEPTH_LIMIT
+ ),
+ Status
+ )
+ @descendant_threads = []
+
+ if descendants.present?
+ statuses = [descendants.first]
+ depth = 1
+
+ descendants.drop(1).each_with_index do |descendant, index|
+ if descendants[index].id == descendant.in_reply_to_id
+ depth += 1
+ statuses << descendant
+ else
+ @descendant_threads << create_descendant_thread(depth, statuses)
+
+ @descendant_threads.reverse_each do |descendant_thread|
+ statuses = descendant_thread[:statuses]
+
+ index = statuses.find_index do |thread_status|
+ thread_status.id == descendant.in_reply_to_id
+ end
+
+ if index.present?
+ depth += index - statuses.size
+ break
+ end
+
+ depth -= statuses.size
+ end
+
+ statuses = [descendant]
+ end
+ end
+
+ @descendant_threads << create_descendant_thread(depth, statuses)
+ end
+
+ @max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT
+ end
+
def set_link_headers
response.headers['Link'] = LinkHeader.new(
[
find_statuses_from_tree_path(ancestor_ids(limit), account)
end
- def descendants(account = nil)
- find_statuses_from_tree_path(descendant_ids, account)
+ 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)
end
private
SQL
end
- def descendant_ids
- descendant_statuses.pluck(:id)
+ 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
- Status.find_by_sql([<<-SQL.squish, id: id])
+ def descendant_statuses(limit, max_child_id, since_child_id, depth)
+ 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 in_reply_to_id = :id
+ WHERE in_reply_to_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 NOT statuses.id = ANY(path)
+ 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
end
--- /dev/null
+= link_to url, class: 'more light' do
+ = t('statuses.show_more')
- if status.reply? && include_threads
- if @next_ancestor
.entry{ class: entry_classes }
- = link_to short_account_status_url(@next_ancestor.account.username, @next_ancestor), class: 'more light' do
- = t('statuses.show_more')
+ = render 'stream_entries/more', url: short_account_status_url(@next_ancestor.account.username, @next_ancestor)
= render partial: 'stream_entries/status', collection: @ancestors, as: :status, locals: { is_predecessor: true, direct_reply_id: status.in_reply_to_id }
.entry{ class: entry_classes }
= render (centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status'), status: status.proper
- if include_threads
- = render partial: 'stream_entries/status', collection: @descendants, as: :status, locals: { is_successor: true, parent_id: status.id }
+ - if @since_descendant_thread_id
+ .entry{ class: entry_classes }
+ = render 'stream_entries/more', url: short_account_status_url(status.account.username, status, max_descendant_thread_id: @since_descendant_thread_id + 1)
+ - @descendant_threads.each do |thread|
+ = render partial: 'stream_entries/status', collection: thread[:statuses], as: :status, locals: { is_successor: true, parent_id: status.id }
+ - if thread[:next_status]
+ .entry{ class: entry_classes }
+ = render 'stream_entries/more', url: short_account_status_url(thread[:next_status].account.username, thread[:next_status])
+ - if @next_descendant_thread
+ .entry{ class: entry_classes }
+ = render 'stream_entries/more', url: short_account_status_url(status.account.username, status, since_descendant_thread_id: @max_descendant_thread_id - 1)
expect(assigns(:ancestors)).to eq []
end
+ it 'assigns @descendant_threads for a thread with several statuses' do
+ status = Fabricate(:status)
+ child = Fabricate(:status, in_reply_to_id: status.id)
+ grandchild = Fabricate(:status, in_reply_to_id: child.id)
+
+ get :show, params: { account_username: status.account.username, id: status.id }
+
+ expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).to eq [child.id, grandchild.id]
+ end
+
+ it 'assigns @descendant_threads for several threads sharing the same descendant' do
+ status = Fabricate(:status)
+ child = Fabricate(:status, in_reply_to_id: status.id)
+ grandchildren = 2.times.map { Fabricate(:status, in_reply_to_id: child.id) }
+
+ get :show, params: { account_username: status.account.username, id: status.id }
+
+ expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).to eq [child.id, grandchildren[0].id]
+ expect(assigns(:descendant_threads)[1][:statuses].pluck(:id)).to eq [grandchildren[1].id]
+ end
+
+ it 'assigns @max_descendant_thread_id for the last thread if it is hitting the status limit' do
+ stub_const 'StatusesController::DESCENDANTS_LIMIT', 1
+ status = Fabricate(:status)
+ child = Fabricate(:status, in_reply_to_id: status.id)
+
+ get :show, params: { account_username: status.account.username, id: status.id }
+
+ expect(assigns(:descendant_threads)).to eq []
+ expect(assigns(:max_descendant_thread_id)).to eq child.id
+ end
+
+ it 'assigns @descendant_threads for threads with :next_status key if they are hitting the depth limit' do
+ stub_const 'StatusesController::DESCENDANTS_DEPTH_LIMIT', 1
+ status = Fabricate(:status)
+ child = Fabricate(:status, in_reply_to_id: status.id)
+
+ get :show, params: { account_username: status.account.username, id: status.id }
+
+ expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).not_to include child.id
+ expect(assigns(:descendant_threads)[0][:next_status].id).to eq child.id
+ end
+
it 'returns a success' do
status = Fabricate(:status)
get :show, params: { account_username: status.account.username, id: status.id }
let!(:viewer) { Fabricate(:account, username: 'viewer') }
it 'returns replies' do
- expect(status.descendants).to include(reply1, reply2, reply3)
+ expect(status.descendants(4)).to include(reply1, reply2, reply3)
end
it 'does not return replies user is not allowed to see' do
reply1.update(visibility: :private)
reply3.update(visibility: :direct)
- expect(status.descendants(viewer)).to_not include(reply1, reply3)
+ expect(status.descendants(4, viewer)).to_not include(reply1, reply3)
end
it 'does not return replies from blocked users' do
viewer.block!(jeff)
- expect(status.descendants(viewer)).to_not include(reply3)
+ expect(status.descendants(4, viewer)).to_not include(reply3)
end
it 'does not return replies from muted users' do
viewer.mute!(jeff)
- expect(status.descendants(viewer)).to_not include(reply3)
+ expect(status.descendants(4, viewer)).to_not include(reply3)
end
it 'does not return replies from silenced and not followed users' do
jeff.update(silenced: true)
- expect(status.descendants(viewer)).to_not include(reply3)
+ expect(status.descendants(4, viewer)).to_not include(reply3)
end
it 'does not return replies from blocked domains' do
viewer.block_domain!('example.com')
- expect(status.descendants(viewer)).to_not include(reply2)
+ expect(status.descendants(4, viewer)).to_not include(reply2)
end
end
end
assign(:stream_entry, status.stream_entry)
assign(:account, alice)
assign(:type, status.stream_entry.activity_type.downcase)
+ assign(:descendant_threads, [])
render
assign(:account, alice)
assign(:type, reply.stream_entry.activity_type.downcase)
assign(:ancestors, reply.stream_entry.activity.ancestors(1, bob) )
- assign(:descendants, reply.stream_entry.activity.descendants(bob))
+ assign(:descendant_threads, [{ statuses: reply.stream_entry.activity.descendants(1)}])
render
assign(:stream_entry, status.stream_entry)
assign(:account, alice)
assign(:type, status.stream_entry.activity_type.downcase)
+ assign(:descendant_threads, [])
render