FEATURE: thread pagination (#22624)

Prior to this commit we were loading a large number of thread messages without any pagination. This commit attempts to fix this and also improves the following points:

- code sharing between channels and threads:
Attempts to reuse/share the code use in channels for threads. To make it possible part of this code has been extracted in dedicated helpers or has been improved to reduce the duplication needed.

Examples of extracted helpers:
- `stackingContextFix`: the ios hack for rendering bug when momentum scrolling is interrupted
- `scrollListToMessage`, `scrollListToTop`, `scrollListToBottom`:  a series of helper to correctly scroll to a specific position in the list of messages

- better general performance of listing messages:
One of the main changes which has been made is to remove the computation of visible message during scroll, it will only happen when needed (update last read for example). This constant recomputation of `message.visible` on intersection observer event while scrolling was consuming a lot of CPU time.
This commit is contained in:
Joffrey JAFFEUX
2023-07-27 09:57:03 +02:00
committed by GitHub
parent 7fb4bd3f43
commit 2d567cee26
105 changed files with 2533 additions and 2576 deletions

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe Chat::Api::ChannelMessagesController do
fab!(:current_user) { Fabricate(:user) }
fab!(:channel) { Fabricate(:chat_channel) }
before do
SiteSetting.chat_enabled = true
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
channel.add(current_user)
sign_in(current_user)
end
describe "index" do
describe "success" do
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel) }
fab!(:message_2) { Fabricate(:chat_message) }
it "works" do
get "/chat/api/channels/#{channel.id}/messages"
expect(response.status).to eq(200)
expect(response.parsed_body["messages"].map { |m| m["id"] }).to contain_exactly(
message_1.id,
)
end
end
context "when channnel doesn’t exist" do
it "returns a 404" do
get "/chat/api/channels/-999/messages"
expect(response.status).to eq(404)
end
end
context "when target message doesn’t exist" do
it "returns a 404" do
get "/chat/api/channels/#{channel.id}/messages?target_message_id=-999"
expect(response.status).to eq(404)
end
end
context "when user can’t see channel" do
fab!(:channel) { Fabricate(:private_category_channel) }
it "returns a 403" do
get "/chat/api/channels/#{channel.id}/messages"
expect(response.status).to eq(403)
end
end
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe Chat::Api::ChannelThreadMessagesController do
fab!(:current_user) { Fabricate(:user) }
fab!(:thread) do
Fabricate(:chat_thread, channel: Fabricate(:chat_channel, threading_enabled: true))
end
before do
SiteSetting.chat_enabled = true
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
thread.channel.add(current_user)
sign_in(current_user)
end
describe "index" do
describe "success" do
fab!(:message_1) { Fabricate(:chat_message, thread: thread) }
fab!(:message_2) { Fabricate(:chat_message) }
it "works" do
get "/chat/api/channels/#{thread.channel.id}/threads/#{thread.id}/messages"
expect(response.status).to eq(200)
expect(response.parsed_body["messages"].map { |m| m["id"] }).to contain_exactly(
thread.original_message.id,
message_1.id,
)
end
end
context "when thread doesn’t exist" do
it "returns a 404" do
get "/chat/api/channels/#{thread.channel.id}/threads/-999/messages"
expect(response.status).to eq(404)
end
end
context "when target message doesn’t exist" do
it "returns a 404" do
get "/chat/api/channels/#{thread.channel.id}/threads/#{thread.id}/messages?target_message_id=-999"
expect(response.status).to eq(404)
end
end
context "when user can’t see channel" do
fab!(:thread) do
Fabricate(
:chat_thread,
channel: Fabricate(:private_category_channel, threading_enabled: true),
)
end
it "returns a 403" do
get "/chat/api/channels/#{thread.channel.id}/threads/#{thread.id}/messages"
expect(response.status).to eq(403)
end
end
context "when channel disabled threading" do
fab!(:thread) do
Fabricate(:chat_thread, channel: Fabricate(:chat_channel, threading_enabled: false))
end
it "returns a 404" do
get "/chat/api/channels/#{thread.channel.id}/threads/#{thread.id}/messages"
expect(response.status).to eq(404)
end
end
end
end

View File

@ -159,396 +159,6 @@ RSpec.describe Chat::Api::ChannelsController do
end
end
end
context "when include_messages is true" do
fab!(:current_user) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:category_channel) }
fab!(:other_user) { Fabricate(:user) }
describe "target message lookup" do
let!(:message) { Fabricate(:chat_message, chat_channel: channel_1) }
let(:chatable) { channel_1.chatable }
before { sign_in(current_user) }
context "when the message doesn’t belong to the channel" do
let!(:message) { Fabricate(:chat_message) }
it "returns a 404" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
target_message_id: message.id,
include_messages: true,
}
expect(response.status).to eq(404)
end
end
context "when the chat channel is for a category" do
it "ensures the user can access that category" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
target_message_id: message.id,
include_messages: true,
}
expect(response.status).to eq(200)
expect(response.parsed_body["chat_messages"][0]["id"]).to eq(message.id)
group = Fabricate(:group)
chatable.update!(read_restricted: true)
Fabricate(:category_group, group: group, category: chatable)
get "/chat/api/channels/#{channel_1.id}.json",
params: {
target_message_id: message.id,
include_messages: true,
}
expect(response.status).to eq(403)
GroupUser.create!(user: current_user, group: group)
get "/chat/api/channels/#{channel_1.id}.json",
params: {
target_message_id: message.id,
include_messages: true,
}
expect(response.status).to eq(200)
expect(response.parsed_body["chat_messages"][0]["id"]).to eq(message.id)
end
end
context "when the chat channel is for a direct message channel" do
let(:channel_1) { Fabricate(:direct_message_channel) }
it "ensures the user can access that direct message channel" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
target_message_id: message.id,
include_messages: true,
}
expect(response.status).to eq(403)
Chat::DirectMessageUser.create!(user: current_user, direct_message: chatable)
get "/chat/api/channels/#{channel_1.id}.json",
params: {
target_message_id: message.id,
include_messages: true,
}
expect(response.status).to eq(200)
expect(response.parsed_body["chat_messages"][0]["id"]).to eq(message.id)
end
end
end
describe "messages pagination and direction" do
let(:page_size) { 30 }
message_count = 70
message_count.times do |n|
fab!("message_#{n}") do
Fabricate(
:chat_message,
chat_channel: channel_1,
user: other_user,
message: "message #{n}",
)
end
end
before do
sign_in(current_user)
Group.refresh_automatic_groups!
end
it "errors for user when they are not allowed to chat" do
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff]
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.status).to eq(403)
end
it "errors when page size is over the maximum" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: Chat::MessagesQuery::MAX_PAGE_SIZE + 1,
}
expect(response.status).to eq(400)
expect(response.parsed_body["errors"]).to include(
"Page size must be less than or equal to #{Chat::MessagesQuery::MAX_PAGE_SIZE}",
)
end
it "errors when page size is nil" do
get "/chat/api/channels/#{channel_1.id}.json", params: { include_messages: true }
expect(response.status).to eq(400)
expect(response.parsed_body["errors"]).to include("Page size can't be blank")
end
it "returns the latest messages in created_at, id order" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
messages = response.parsed_body["chat_messages"]
expect(messages.count).to eq(page_size)
expect(messages.first["id"]).to eq(message_40.id)
expect(messages.last["id"]).to eq(message_69.id)
end
it "returns `can_flag=true` for public channels" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.parsed_body["meta"]["can_flag"]).to be true
end
it "returns `can_flag=true` for DM channels" do
dm_chat_channel = Fabricate(:direct_message_channel, users: [current_user, other_user])
get "/chat/api/channels/#{dm_chat_channel.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.parsed_body["meta"]["can_flag"]).to be true
end
it "returns `can_moderate=true` based on whether the user can moderate the chatable" do
1.upto(4) do |n|
current_user.update!(trust_level: n)
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.parsed_body["meta"]["can_moderate"]).to be false
end
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.parsed_body["meta"]["can_moderate"]).to be false
current_user.update!(admin: true)
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.parsed_body["meta"]["can_moderate"]).to be true
current_user.update!(admin: false)
SiteSetting.enable_category_group_moderation = true
group = Fabricate(:group)
group.add(current_user)
channel_1.category.update!(reviewable_by_group: group)
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.parsed_body["meta"]["can_moderate"]).to be true
end
it "serializes `user_flag_status` for user who has a pending flag" do
chat_message = channel_1.chat_messages.last
reviewable = flag_message(chat_message, current_user)
score = reviewable.reviewable_scores.last
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.parsed_body["chat_messages"].last["user_flag_status"]).to eq(
score.status_for_database,
)
end
it "doesn't serialize `reviewable_ids` for non-staff" do
reviewable = flag_message(channel_1.chat_messages.last, Fabricate(:admin))
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.parsed_body["chat_messages"].last["reviewable_id"]).to be_nil
end
it "serializes `reviewable_ids` correctly for staff" do
admin = Fabricate(:admin)
sign_in(admin)
reviewable = flag_message(channel_1.chat_messages.last, admin)
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.parsed_body["chat_messages"].last["reviewable_id"]).to eq(reviewable.id)
end
it "correctly marks reactions as 'reacted' for the current_user" do
heart_emoji = ":heart:"
smile_emoji = ":smile"
last_message = channel_1.chat_messages.last
last_message.reactions.create(user: current_user, emoji: heart_emoji)
last_message.reactions.create(user: Fabricate(:admin), emoji: smile_emoji)
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
reactions = response.parsed_body["chat_messages"].last["reactions"]
heart_reaction = reactions.find { |r| r["emoji"] == heart_emoji }
expect(heart_reaction["reacted"]).to be true
smile_reaction = reactions.find { |r| r["emoji"] == smile_emoji }
expect(smile_reaction["reacted"]).to be false
end
it "sends the last message bus id for the channel" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
page_size: page_size,
}
expect(response.parsed_body["meta"]["channel_message_bus_last_id"]).not_to eq(nil)
end
describe "scrolling to the past" do
it "returns the correct messages in created_at, id order" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
target_message_id: message_40.id,
page_size: page_size,
direction: Chat::MessagesQuery::PAST,
}
messages = response.parsed_body["chat_messages"]
expect(messages.count).to eq(page_size)
expect(messages.first["id"]).to eq(message_10.id)
expect(messages.last["id"]).to eq(message_39.id)
end
it "returns 'can_load...' properly when there are more past messages" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
target_message_id: message_40.id,
page_size: page_size,
direction: Chat::MessagesQuery::PAST,
}
expect(response.parsed_body["meta"]["can_load_more_past"]).to be true
expect(response.parsed_body["meta"]["can_load_more_future"]).to be_nil
end
it "returns 'can_load...' properly when there are no past messages" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
target_message_id: message_3.id,
page_size: page_size,
direction: Chat::MessagesQuery::PAST,
}
expect(response.parsed_body["meta"]["can_load_more_past"]).to be false
expect(response.parsed_body["meta"]["can_load_more_future"]).to be_nil
end
end
describe "scrolling to the future" do
it "returns the correct messages in created_at, id order when there are many after" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
target_message_id: message_10.id,
page_size: page_size,
direction: Chat::MessagesQuery::FUTURE,
}
messages = response.parsed_body["chat_messages"]
expect(messages.count).to eq(page_size)
expect(messages.first["id"]).to eq(message_11.id)
expect(messages.last["id"]).to eq(message_40.id)
end
it "return 'can_load..' properly when there are future messages" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
target_message_id: message_10.id,
page_size: page_size,
direction: Chat::MessagesQuery::FUTURE,
}
expect(response.parsed_body["meta"]["can_load_more_past"]).to be_nil
expect(response.parsed_body["meta"]["can_load_more_future"]).to be true
end
it "returns 'can_load..' properly when there are no future messages" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
include_messages: true,
target_message_id: message_60.id,
page_size: page_size,
direction: Chat::MessagesQuery::FUTURE,
}
expect(response.parsed_body["meta"]["can_load_more_past"]).to be_nil
expect(response.parsed_body["meta"]["can_load_more_future"]).to be false
end
end
describe "without direction (latest messages)" do
it "signals there are no future messages" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
page_size: page_size,
include_messages: true,
}
expect(response.parsed_body["meta"]["can_load_more_future"]).to eq(false)
end
it "signals there are more messages in the past" do
get "/chat/api/channels/#{channel_1.id}.json",
params: {
page_size: page_size,
include_messages: true,
}
expect(response.parsed_body["meta"]["can_load_more_past"]).to eq(true)
end
it "signals there are no more messages" do
new_channel = Fabricate(:category_channel)
Fabricate(
:chat_message,
chat_channel: new_channel,
user: other_user,
message: "message",
)
chat_messages_qty = 1
get "/chat/api/channels/#{new_channel.id}.json",
params: {
page_size: chat_messages_qty + 1,
include_messages: true,
}
expect(response.parsed_body["meta"]["can_load_more_past"]).to eq(false)
end
end
end
end
end
describe "#destroy" do

View File

@ -15,13 +15,7 @@ describe ListController do
Fabricate(:direct_message_channel, users: [current_user, user_1])
public_channel_1 = Fabricate(:chat_channel)
public_channel_2 = Fabricate(:chat_channel)
Fabricate(
:user_chat_channel_membership,
user: current_user,
chat_channel: public_channel_1,
following: true,
)
public_channel_1.add(current_user)
# warm up
get "/latest.html"
@ -41,12 +35,7 @@ describe ListController do
end
end.count
Fabricate(
:user_chat_channel_membership,
user: current_user,
chat_channel: public_channel_2,
following: true,
)
public_channel_2.add(current_user)
user_2 = Fabricate(:user)
Fabricate(:direct_message_channel, users: [current_user, user_2])