FEATURE: Sort thread list by unread threads first (#22272)

* FEATURE: Sort thread list by unread threads first

This commit changes the thread list to show the threads that
have unread messages at the top of the list sorted by the
last reply date + time, then all other threads sorted by
last reply date + time.

This also fixes some issues by removing the last_reply
relationship on the thread, which did not work for complex
querying scenarios because its order would be discarded.

* FIX: Various fixes for thread list loading

* Use the channel.threadsManager and find the channel first rather
  than use activeChannel in the threads manager, otherwise we may
  be looking at differenct channels.
* Look at threadsManager directly instead of storing result for threads
  list otherwise it can get out of sync because of replace: true in
  other places we are loading threads into the store.
* Fix sorting for thread.last_reply, needed a resort.
This commit is contained in:
Martin Brennan
2023-06-28 13:14:01 +10:00
committed by GitHub
parent 78bc42be2e
commit 1526d1f97d
15 changed files with 243 additions and 84 deletions

View File

@ -24,15 +24,25 @@ module Chat
class_name: "Chat::Message"
has_many :user_chat_thread_memberships
# Since the `replies` for the thread can all be deleted, to avoid errors
# in lists and previews of the thread, we can consider the original message
# as the last "reply" in this case, so we don't exclude that here.
has_one :last_reply, -> { order("created_at DESC, id DESC") }, class_name: "Chat::Message"
enum :status, { open: 0, read_only: 1, closed: 2, archived: 3 }, scopes: false
validates :title, length: { maximum: Chat::Thread::MAX_TITLE_LENGTH }
# Since the `replies` for the thread can all be deleted, to avoid errors
# in lists and previews of the thread, we can consider the original message
# as the last "reply" in this case, so we don't exclude that here.
#
# This is a manual getter/setter so we can avoid N1 queries. This used to be
# a has_one relationship on the model, but that has some awkward behaviour
# and still caused N1s, and ordering was not applied in complex AR queries.
def last_reply
@last_reply ||= self.chat_messages.reorder("created_at DESC, id DESC").first
end
def last_reply=(message)
@last_reply = message
end
def add(user)
Chat::UserChatThreadMembership.find_or_create_by!(user: user, thread: self)
end

View File

@ -15,6 +15,8 @@ module Chat
class LookupChannelThreads
include Service::Base
MAX_THREADS = 50
# @!method call(channel_id:, guardian:)
# @param [Integer] channel_id
# @param [Guardian] guardian
@ -54,41 +56,54 @@ module Chat
end
def fetch_threads(guardian:, channel:, **)
Chat::Thread
.strict_loading
.includes(
:channel,
last_reply: %i[user uploads],
original_message_user: :user_status,
original_message: [
:chat_webhook_event,
:chat_mentions,
:chat_channel,
user: :user_status,
],
)
.joins(:chat_messages, :user_chat_thread_memberships)
.joins(
"LEFT JOIN chat_messages original_messages ON chat_threads.original_message_id = original_messages.id",
)
.where(
"chat_threads.channel_id = :channel_id AND chat_messages.chat_channel_id = :channel_id",
channel_id: channel.id,
)
.where("user_chat_thread_memberships.user_id = ?", guardian.user.id)
.where(
"user_chat_thread_memberships.notification_level IN (?)",
[
Chat::UserChatThreadMembership.notification_levels[:normal],
Chat::UserChatThreadMembership.notification_levels[:tracking],
],
)
.where(
"original_messages.deleted_at IS NULL AND chat_messages.deleted_at IS NULL AND original_messages.id IS NOT NULL",
)
.group("chat_threads.id")
.order("MAX(chat_messages.created_at) DESC")
.limit(50)
read_threads = []
unread_threads =
threads_query(guardian, channel)
.where(<<~SQL)
user_chat_thread_memberships_chat_threads.last_read_message_id IS NULL
OR tracked_threads_subquery.latest_message_id > user_chat_thread_memberships_chat_threads.last_read_message_id
SQL
.order("tracked_threads_subquery.latest_message_created_at DESC")
.limit(MAX_THREADS)
.to_a
# We do this to avoid having to query additional threads if the user
# already has a lot of unread threads.
if unread_threads.length < MAX_THREADS
final_limit = MAX_THREADS - unread_threads.length
read_threads =
threads_query(guardian, channel)
.where(<<~SQL)
tracked_threads_subquery.latest_message_id <= user_chat_thread_memberships_chat_threads.last_read_message_id
SQL
.order("tracked_threads_subquery.latest_message_created_at DESC")
.limit(final_limit)
.to_a
end
threads = unread_threads + read_threads
last_replies =
Chat::Message
.strict_loading
.includes(:user, :uploads)
.from(<<~SQL)
(
SELECT thread_id, MAX(created_at) AS latest_created_at, MAX(id) AS latest_message_id
FROM chat_messages
WHERE thread_id IN (#{threads.map(&:id).join(",")})
GROUP BY thread_id
) AS last_replies_subquery
SQL
.joins(
"INNER JOIN chat_messages ON chat_messages.id = last_replies_subquery.latest_message_id",
)
.index_by(&:thread_id)
threads.each { |thread| thread.last_reply = last_replies[thread.id] }
threads
end
def fetch_tracking(guardian:, threads:, **)
@ -107,5 +122,55 @@ module Chat
user_id: guardian.user.id,
)
end
def threads_query(guardian, channel)
Chat::Thread
.strict_loading
.includes(
:channel,
:user_chat_thread_memberships,
original_message_user: :user_status,
original_message: [
:chat_webhook_event,
:chat_mentions,
:chat_channel,
user: :user_status,
],
)
.joins(
"JOIN (#{tracked_threads_subquery(guardian, channel)}) tracked_threads_subquery
ON tracked_threads_subquery.thread_id = chat_threads.id",
)
.joins(:user_chat_thread_memberships)
.where(user_chat_thread_memberships_chat_threads: { user_id: guardian.user.id })
end
def tracked_threads_subquery(guardian, channel)
Chat::Thread
.joins(:chat_messages, :user_chat_thread_memberships)
.joins(
"LEFT JOIN chat_messages original_messages ON chat_threads.original_message_id = original_messages.id",
)
.where(user_chat_thread_memberships: { user_id: guardian.user.id })
.where(
"chat_threads.channel_id = :channel_id AND chat_messages.chat_channel_id = :channel_id",
channel_id: channel.id,
)
.where(
"user_chat_thread_memberships.notification_level IN (?)",
[
Chat::UserChatThreadMembership.notification_levels[:normal],
Chat::UserChatThreadMembership.notification_levels[:tracking],
],
)
.where(
"original_messages.deleted_at IS NULL AND chat_messages.deleted_at IS NULL AND original_messages.id IS NOT NULL",
)
.group("chat_threads.id")
.select(
"chat_threads.id AS thread_id, MAX(chat_messages.created_at) AS latest_message_created_at, MAX(chat_messages.id) AS latest_message_id",
)
.to_sql
end
end
end

View File

@ -44,7 +44,6 @@ module Chat
def fetch_thread(contract:, **)
Chat::Thread.includes(
:channel,
last_reply: :user,
original_message_user: :user_status,
original_message: :chat_webhook_event,
).find_by(id: contract.thread_id, channel_id: contract.channel_id)