Paginate descendant statuses in public page (#7148)

This commit is contained in:
Akihiko Odaki 2018-04-24 02:27:35 +09:00 committed by Eugen Rochko
parent 06817b3c1f
commit 1258efa882
8 changed files with 146 additions and 22 deletions

View file

@ -18,7 +18,7 @@ class Api::V1::StatusesController < Api::BaseController
def context def context
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(DEFAULT_STATUSES_LIMIT, current_account) 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_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status) loaded_descendants = cache_collection(descendants_results, Status)

View file

@ -5,6 +5,8 @@ class StatusesController < ApplicationController
include Authorization include Authorization
ANCESTORS_LIMIT = 20 ANCESTORS_LIMIT = 20
DESCENDANTS_LIMIT = 20
DESCENDANTS_DEPTH_LIMIT = 4
layout 'public' layout 'public'
@ -19,9 +21,8 @@ class StatusesController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
@ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : [] set_ancestors
@next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift set_descendants
@descendants = cache_collection(@status.descendants(current_account), Status)
render 'stream_entries/show' render 'stream_entries/show'
end end
@ -51,10 +52,76 @@ class StatusesController < ApplicationController
private 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 def set_account
@account = Account.find_local!(params[:account_username]) @account = Account.find_local!(params[:account_username])
end 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 def set_link_headers
response.headers['Link'] = LinkHeader.new( response.headers['Link'] = LinkHeader.new(
[ [

View file

@ -7,8 +7,8 @@ module StatusThreadingConcern
find_statuses_from_tree_path(ancestor_ids(limit), account) find_statuses_from_tree_path(ancestor_ids(limit), account)
end end
def descendants(account = nil) def descendants(limit, account = nil, max_child_id = nil, since_child_id = nil, depth = nil)
find_statuses_from_tree_path(descendant_ids, account) find_statuses_from_tree_path(descendant_ids(limit, max_child_id, since_child_id, depth), account)
end end
private private
@ -46,26 +46,27 @@ module StatusThreadingConcern
SQL SQL
end end
def descendant_ids def descendant_ids(limit, max_child_id, since_child_id, depth)
descendant_statuses.pluck(:id) descendant_statuses(limit, max_child_id, since_child_id, depth).pluck(:id)
end end
def descendant_statuses def descendant_statuses(limit, max_child_id, since_child_id, depth)
Status.find_by_sql([<<-SQL.squish, id: id]) 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) WITH RECURSIVE search_tree(id, path)
AS ( AS (
SELECT id, ARRAY[id] SELECT id, ARRAY[id]
FROM statuses 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 UNION ALL
SELECT statuses.id, path || statuses.id SELECT statuses.id, path || statuses.id
FROM search_tree FROM search_tree
JOIN statuses ON statuses.in_reply_to_id = search_tree.id 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 SELECT id
FROM search_tree FROM search_tree
ORDER BY path ORDER BY path
LIMIT :limit
SQL SQL
end end

View file

@ -0,0 +1,2 @@
= link_to url, class: 'more light' do
= t('statuses.show_more')

View file

@ -16,8 +16,7 @@
- if status.reply? && include_threads - if status.reply? && include_threads
- if @next_ancestor - if @next_ancestor
.entry{ class: entry_classes } .entry{ class: entry_classes }
= link_to short_account_status_url(@next_ancestor.account.username, @next_ancestor), class: 'more light' do = render 'stream_entries/more', url: short_account_status_url(@next_ancestor.account.username, @next_ancestor)
= t('statuses.show_more')
= render partial: 'stream_entries/status', collection: @ancestors, as: :status, locals: { is_predecessor: true, direct_reply_id: status.in_reply_to_id } = 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 } .entry{ class: entry_classes }
@ -40,4 +39,14 @@
= render (centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status'), status: status.proper = render (centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status'), status: status.proper
- if include_threads - 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)

View file

@ -82,6 +82,49 @@ describe StatusesController do
expect(assigns(:ancestors)).to eq [] expect(assigns(:ancestors)).to eq []
end 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 it 'returns a success' do
status = Fabricate(:status) status = Fabricate(:status)
get :show, params: { account_username: status.account.username, id: status.id } get :show, params: { account_username: status.account.username, id: status.id }

View file

@ -89,34 +89,34 @@ describe StatusThreadingConcern do
let!(:viewer) { Fabricate(:account, username: 'viewer') } let!(:viewer) { Fabricate(:account, username: 'viewer') }
it 'returns replies' do it 'returns replies' do
expect(status.descendants).to include(reply1, reply2, reply3) expect(status.descendants(4)).to include(reply1, reply2, reply3)
end end
it 'does not return replies user is not allowed to see' do it 'does not return replies user is not allowed to see' do
reply1.update(visibility: :private) reply1.update(visibility: :private)
reply3.update(visibility: :direct) reply3.update(visibility: :direct)
expect(status.descendants(viewer)).to_not include(reply1, reply3) expect(status.descendants(4, viewer)).to_not include(reply1, reply3)
end end
it 'does not return replies from blocked users' do it 'does not return replies from blocked users' do
viewer.block!(jeff) viewer.block!(jeff)
expect(status.descendants(viewer)).to_not include(reply3) expect(status.descendants(4, viewer)).to_not include(reply3)
end end
it 'does not return replies from muted users' do it 'does not return replies from muted users' do
viewer.mute!(jeff) viewer.mute!(jeff)
expect(status.descendants(viewer)).to_not include(reply3) expect(status.descendants(4, viewer)).to_not include(reply3)
end end
it 'does not return replies from silenced and not followed users' do it 'does not return replies from silenced and not followed users' do
jeff.update(silenced: true) jeff.update(silenced: true)
expect(status.descendants(viewer)).to_not include(reply3) expect(status.descendants(4, viewer)).to_not include(reply3)
end end
it 'does not return replies from blocked domains' do it 'does not return replies from blocked domains' do
viewer.block_domain!('example.com') 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 end
end end

View file

@ -24,6 +24,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
assign(:stream_entry, status.stream_entry) assign(:stream_entry, status.stream_entry)
assign(:account, alice) assign(:account, alice)
assign(:type, status.stream_entry.activity_type.downcase) assign(:type, status.stream_entry.activity_type.downcase)
assign(:descendant_threads, [])
render render
@ -49,7 +50,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
assign(:account, alice) assign(:account, alice)
assign(:type, reply.stream_entry.activity_type.downcase) assign(:type, reply.stream_entry.activity_type.downcase)
assign(:ancestors, reply.stream_entry.activity.ancestors(1, bob) ) 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 render
@ -75,6 +76,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d
assign(:stream_entry, status.stream_entry) assign(:stream_entry, status.stream_entry)
assign(:account, alice) assign(:account, alice)
assign(:type, status.stream_entry.activity_type.downcase) assign(:type, status.stream_entry.activity_type.downcase)
assign(:descendant_threads, [])
render render