Files
discourse/plugins/chat/app/models/chat/thread.rb
Martin Brennan d6374fdc53 FEATURE: Allow users to manually track threads without replying (#22100)
This commit adds a tracking dropdown to each individual thread, similar to topics,
that allows the user to change the notification level for a thread manually. Previously
the user had to reply to a thread to track it and see unread indicators.

Since the user can now manually track threads, the thread index has also been changed
to only show threads that the user is a member of, rather than threads that they had sent
messages in.

Unread indicators also respect the notification level -- Normal level thread tracking
will not show unread indicators in the UI when new messages are sent in the thread.
2023-06-16 12:08:26 +10:00

151 lines
5.4 KiB
Ruby

# frozen_string_literal: true
module Chat
class Thread < ActiveRecord::Base
EXCERPT_LENGTH = 150
MAX_TITLE_LENGTH = 100
include Chat::ThreadCache
self.table_name = "chat_threads"
belongs_to :channel, foreign_key: "channel_id", class_name: "Chat::Channel"
belongs_to :original_message_user, foreign_key: "original_message_user_id", class_name: "User"
belongs_to :original_message, foreign_key: "original_message_id", class_name: "Chat::Message"
has_many :chat_messages,
-> {
where("deleted_at IS NULL").order(
"chat_messages.created_at ASC, chat_messages.id ASC",
)
},
foreign_key: :thread_id,
primary_key: :id,
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 }
def add(user)
Chat::UserChatThreadMembership.find_or_create_by!(user: user, thread: self)
end
def remove(user)
Chat::UserChatThreadMembership.find_by(user: user, thread: self)&.destroy
end
def membership_for(user)
user_chat_thread_memberships.find_by(user: user)
end
def replies
self.chat_messages.where.not(id: self.original_message_id).order("created_at ASC, id ASC")
end
def url
"#{channel.url}/t/#{self.id}"
end
def relative_url
"#{channel.relative_url}/t/#{self.id}"
end
def excerpt
original_message.rich_excerpt(max_length: EXCERPT_LENGTH)
end
def latest_not_deleted_message_id(anchor_message_id: nil)
DB.query_single(
<<~SQL,
SELECT id FROM chat_messages
WHERE chat_channel_id = :channel_id
AND thread_id = :thread_id
AND deleted_at IS NULL
#{anchor_message_id ? "AND id < :anchor_message_id" : ""}
ORDER BY created_at DESC, id DESC
LIMIT 1
SQL
channel_id: self.channel_id,
thread_id: self.id,
anchor_message_id: anchor_message_id,
).first
end
def self.grouped_messages(thread_ids: nil, message_ids: nil, include_original_message: true)
DB.query(<<~SQL, message_ids: message_ids, thread_ids: thread_ids)
SELECT thread_id,
array_agg(chat_messages.id ORDER BY chat_messages.created_at, chat_messages.id) AS thread_message_ids,
chat_threads.original_message_id
FROM chat_messages
INNER JOIN chat_threads ON chat_threads.id = chat_messages.thread_id
WHERE thread_id IS NOT NULL
#{thread_ids ? "AND thread_id IN (:thread_ids)" : ""}
#{message_ids ? "AND chat_messages.id IN (:message_ids)" : ""}
#{include_original_message ? "" : "AND chat_messages.id != chat_threads.original_message_id"}
GROUP BY thread_id, chat_threads.original_message_id;
SQL
end
def self.ensure_consistency!
return if !SiteSetting.enable_experimental_chat_threaded_discussions
update_counts
end
def self.update_counts
# NOTE: Chat::Thread#replies_count is not updated every time
# a message is created or deleted in a channel, the UI will lag
# behind unless it is kept in sync with MessageBus. The count
# has 1 subtracted from it to account for the original message.
#
# It is updated eventually via Jobs::Chat::PeriodicalUpdates. In
# future we may want to update this more frequently.
updated_thread_ids = DB.query_single <<~SQL
UPDATE chat_threads threads
SET replies_count = subquery.replies_count
FROM (
SELECT COUNT(*) - 1 AS replies_count, thread_id
FROM chat_messages
WHERE chat_messages.deleted_at IS NULL AND thread_id IS NOT NULL
GROUP BY thread_id
) subquery
WHERE threads.id = subquery.thread_id
AND subquery.replies_count != threads.replies_count
RETURNING threads.id AS thread_id;
SQL
return if updated_thread_ids.empty?
self.clear_caches!(updated_thread_ids)
end
end
end
# == Schema Information
#
# Table name: chat_threads
#
# id :bigint not null, primary key
# channel_id :integer not null
# original_message_id :integer not null
# original_message_user_id :integer not null
# status :integer default("open"), not null
# title :string
# created_at :datetime not null
# updated_at :datetime not null
# replies_count :integer default(0), not null
#
# Indexes
#
# index_chat_threads_on_channel_id (channel_id)
# index_chat_threads_on_channel_id_and_status (channel_id,status)
# index_chat_threads_on_original_message_id (original_message_id)
# index_chat_threads_on_original_message_user_id (original_message_user_id)
# index_chat_threads_on_replies_count (replies_count)
# index_chat_threads_on_status (status)
#