-
Notifications
You must be signed in to change notification settings - Fork 8.6k
DEV: Refactor Chat::LookupChannelThreads to follow best practices #32771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# frozen_string_literal: true | ||
|
||
module Chat | ||
module Action | ||
class FetchThreads < Service::ActionBase | ||
option :user_id | ||
option :channel_id | ||
option :limit | ||
option :offset | ||
|
||
def call | ||
::Chat::Thread | ||
.includes( | ||
:channel, | ||
:user_chat_thread_memberships, | ||
original_message_user: :user_status, | ||
last_message: [ | ||
:uploads, | ||
:chat_webhook_event, | ||
:chat_channel, | ||
user_mentions: { | ||
user: :user_status, | ||
}, | ||
user: :user_status, | ||
], | ||
original_message: [ | ||
:uploads, | ||
:chat_webhook_event, | ||
:chat_channel, | ||
user_mentions: { | ||
user: :user_status, | ||
}, | ||
user: :user_status, | ||
], | ||
) | ||
.joins( | ||
"LEFT JOIN user_chat_thread_memberships ON chat_threads.id = user_chat_thread_memberships.thread_id AND user_chat_thread_memberships.user_id = #{user_id} AND user_chat_thread_memberships.notification_level NOT IN (#{::Chat::UserChatThreadMembership.notification_levels[:muted]})", | ||
) | ||
.joins( | ||
"LEFT JOIN chat_messages AS last_message ON chat_threads.last_message_id = last_message.id", | ||
) | ||
.joins( | ||
"INNER JOIN chat_messages AS original_message ON chat_threads.original_message_id = original_message.id", | ||
) | ||
.where(channel_id:) | ||
.where("original_message.chat_channel_id = chat_threads.channel_id") | ||
.where("original_message.deleted_at IS NULL") | ||
.where("last_message.chat_channel_id = chat_threads.channel_id") | ||
.where("last_message.deleted_at IS NULL") | ||
.where("chat_threads.replies_count > 0") | ||
.order( | ||
"CASE WHEN user_chat_thread_memberships.last_read_message_id IS NULL OR user_chat_thread_memberships.last_read_message_id < chat_threads.last_message_id THEN true ELSE false END DESC, last_message.created_at DESC", | ||
) | ||
.limit(limit) | ||
.offset(offset) | ||
end | ||
end | ||
end | ||
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
plugins/chat/spec/services/actions/fetch_threads_spec.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe Chat::Action::FetchThreads do | ||
subject(:threads) do | ||
described_class.call(user_id: current_user.id, channel_id: channel.id, limit:, offset:) | ||
end | ||
|
||
fab!(:current_user) { Fabricate(:user) } | ||
fab!(:channel) { Fabricate(:chat_channel, threading_enabled: true) } | ||
fab!(:thread_1) { Fabricate(:chat_thread, channel:) } | ||
fab!(:thread_2) { Fabricate(:chat_thread, channel:) } | ||
fab!(:thread_3) { Fabricate(:chat_thread, channel:) } | ||
|
||
let(:limit) { 10 } | ||
let(:offset) { 0 } | ||
|
||
before do | ||
channel.add(current_user) | ||
[thread_1, thread_2, thread_3].each.with_index do |t, index| | ||
t.original_message.update!(created_at: (index + 1).weeks.ago) | ||
t.update!(replies_count: 2) | ||
t.add(current_user) | ||
end | ||
end | ||
|
||
context "when there are new messages" do | ||
before do | ||
[ | ||
[thread_1, 10.minutes.ago], | ||
[thread_2, 1.day.ago], | ||
[thread_3, 2.seconds.ago], | ||
].each do |thread, created_at| | ||
message = | ||
Fabricate(:chat_message, user: current_user, chat_channel: channel, thread:, created_at:) | ||
thread.update!(last_message: message) | ||
end | ||
end | ||
|
||
it "orders threads by the last reply created_at timestamp" do | ||
expect(threads.map(&:id)).to eq([thread_3.id, thread_1.id, thread_2.id]) | ||
end | ||
end | ||
|
||
context "when there are unread messages" do | ||
let(:unread_message) { Fabricate(:chat_message, chat_channel: channel, thread: thread_2) } | ||
|
||
before do | ||
unread_message.update!(created_at: 2.days.ago) | ||
thread_2.update!(last_message: unread_message) | ||
end | ||
|
||
it "sorts by unread over recency" do | ||
expect(threads.map(&:id)).to eq([thread_2.id, thread_1.id, thread_3.id]) | ||
end | ||
end | ||
|
||
context "when there are more threads than the limit" do | ||
let(:limit) { 5 } | ||
let(:thread_4) { Fabricate(:chat_thread, channel:) } | ||
let(:thread_5) { Fabricate(:chat_thread, channel:) } | ||
let(:thread_6) { Fabricate(:chat_thread, channel:) } | ||
let(:thread_7) { Fabricate(:chat_thread, channel:) } | ||
|
||
before do | ||
[thread_4, thread_5, thread_6, thread_7].each do |t| | ||
t.update!(replies_count: 2) | ||
t.add(current_user) | ||
t.membership_for(current_user).mark_read! | ||
end | ||
[thread_1, thread_2, thread_3].each { |t| t.membership_for(current_user).mark_read! } | ||
# The old unread messages. | ||
Fabricate(:chat_message, chat_channel: channel, thread: thread_7).update!( | ||
created_at: 2.months.ago, | ||
) | ||
Fabricate(:chat_message, chat_channel: channel, thread: thread_6).update!( | ||
created_at: 3.months.ago, | ||
) | ||
end | ||
|
||
it "sorts very old unreads to top over recency, and sorts both unreads and other threads by recency" do | ||
expect(threads.map(&:id)).to eq( | ||
[thread_7.id, thread_6.id, thread_5.id, thread_4.id, thread_1.id], | ||
) | ||
end | ||
end | ||
|
||
context "when the original message is trashed" do | ||
before { thread_1.original_message.trash! } | ||
|
||
it "does not return its associated thread" do | ||
expect(threads.map(&:id)).to eq([thread_2.id, thread_3.id]) | ||
end | ||
end | ||
|
||
context "when the original message is deleted" do | ||
before { thread_1.original_message.destroy } | ||
|
||
it "does not return the associated thread" do | ||
expect(threads.map(&:id)).to eq([thread_2.id, thread_3.id]) | ||
end | ||
end | ||
|
||
context "when there are threads in other channels" do | ||
let(:thread_4) { Fabricate(:chat_thread) } | ||
let!(:message) do | ||
Fabricate( | ||
:chat_message, | ||
user: current_user, | ||
thread: thread_4, | ||
chat_channel: thread_4.channel, | ||
created_at: 2.seconds.ago, | ||
) | ||
end | ||
|
||
it "does not return those threads" do | ||
expect(threads.map(&:id)).to eq([thread_1.id, thread_2.id, thread_3.id]) | ||
end | ||
end | ||
|
||
context "when there is a thread with no membership" do | ||
let(:thread_4) { Fabricate(:chat_thread, channel:) } | ||
|
||
before { thread_4.update!(replies_count: 2) } | ||
|
||
it "returns every threads of the channel, no matter the tracking notification level or membership" do | ||
expect(threads.map(&:id)).to match_array([thread_1.id, thread_2.id, thread_3.id, thread_4.id]) | ||
end | ||
end | ||
|
||
context "when there are muted threads" do | ||
let(:thread) { Fabricate(:chat_thread, channel:) } | ||
|
||
before do | ||
thread.add(current_user) | ||
thread.membership_for(current_user).update!( | ||
notification_level: ::Chat::UserChatThreadMembership.notification_levels[:muted], | ||
) | ||
end | ||
|
||
it "does not return them" do | ||
expect(threads.map(&:id)).not_to include(thread.id) | ||
end | ||
end | ||
|
||
context "when there are deleted messages" do | ||
let!(:original_last_message_id) { thread_3.reload.last_message_id } | ||
let(:unread_message) { Fabricate(:chat_message, chat_channel: channel, thread: thread_3) } | ||
|
||
before do | ||
unread_message.update!(created_at: 2.days.ago) | ||
unread_message.trash! | ||
thread_3.reload.update!(last_message_id: original_last_message_id) | ||
end | ||
|
||
it "does not count deleted messages for sort order" do | ||
expect(threads.map(&:id)).to eq([thread_1.id, thread_2.id, thread_3.id]) | ||
end | ||
end | ||
|
||
context "when offset param is set" do | ||
let(:offset) { 1 } | ||
|
||
it "returns results from the offset the number of threads returned" do | ||
expect(threads).to eq([thread_2, thread_3]) | ||
end | ||
end | ||
end |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extracted all the logic/SQL to its own class, as it improves global readability of the service and makes it easier to spec all the edge-cases.