forked from fedi/mastodon
Paginate descendant statuses in public page (#7148)
This commit is contained in:
parent
06817b3c1f
commit
1258efa882
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
[
|
[
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
2
app/views/stream_entries/_more.html.haml
Normal file
2
app/views/stream_entries/_more.html.haml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
= link_to url, class: 'more light' do
|
||||||
|
= t('statuses.show_more')
|
|
@ -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)
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue