214 lines
6.8 KiB
Ruby
214 lines
6.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# This class is used to mirror unread and new status for private messages between
|
|
# server and client.
|
|
#
|
|
# On the server side, this class has two main responsibilities. The first is to
|
|
# query the database for the initial state of a user's unread and new private
|
|
# messages. The second is to publish message_bus messages to notify the client
|
|
# of various topic events.
|
|
#
|
|
# On the client side, we have a `PrivateMessageTopicTrackingState` class as well
|
|
# which will load the initial state into memory and subscribes to the relevant
|
|
# message_bus messages. When a message is received, it modifies the in-memory
|
|
# state based on the message type. The filtering for new and unread topics is
|
|
# done on the client side based on the in-memory state in order to derive the
|
|
# count of new and unread topics efficiently.
|
|
class PrivateMessageTopicTrackingState
|
|
include TopicTrackingStatePublishable
|
|
|
|
CHANNEL_PREFIX = "/private-message-topic-tracking-state"
|
|
NEW_MESSAGE_TYPE = "new_topic"
|
|
UNREAD_MESSAGE_TYPE = "unread"
|
|
READ_MESSAGE_TYPE = "read"
|
|
GROUP_ARCHIVE_MESSAGE_TYPE = "group_archive"
|
|
|
|
def self.report(user)
|
|
sql = new_and_unread_sql(user)
|
|
|
|
DB.query(
|
|
sql + "\n\n LIMIT :max_topics",
|
|
{
|
|
max_topics: TopicTrackingState::MAX_TOPICS,
|
|
min_new_topic_date: Time.at(SiteSetting.min_new_topics_time).to_datetime,
|
|
},
|
|
)
|
|
end
|
|
|
|
def self.new_and_unread_sql(user)
|
|
sql = report_raw_sql(user, skip_unread: true)
|
|
sql << "\nUNION ALL\n\n"
|
|
sql << report_raw_sql(user, skip_new: true)
|
|
end
|
|
|
|
def self.report_raw_sql(user, skip_unread: false, skip_new: false)
|
|
unread =
|
|
if skip_unread
|
|
"1=0"
|
|
else
|
|
first_unread_pm_at = DB.query_single(<<~SQL, user_id: user.id).first
|
|
SELECT
|
|
LEAST(
|
|
MIN(user_stats.first_unread_pm_at),
|
|
MIN(group_users.first_unread_pm_at)
|
|
)
|
|
FROM group_users
|
|
JOIN groups ON groups.id = group_users.group_id
|
|
JOIN user_stats ON user_stats.user_id = :user_id
|
|
WHERE group_users.user_id = :user_id;
|
|
SQL
|
|
|
|
<<~SQL
|
|
#{TopicTrackingState.unread_filter_sql(whisperer: user.whisperer?)}
|
|
#{first_unread_pm_at ? "AND topics.updated_at > '#{first_unread_pm_at}'" : ""}
|
|
SQL
|
|
end
|
|
|
|
new =
|
|
if skip_new
|
|
"1=0"
|
|
else
|
|
TopicTrackingState.new_filter_sql
|
|
end
|
|
|
|
sql = +<<~SQL
|
|
SELECT
|
|
DISTINCT topics.id AS topic_id,
|
|
u.id AS user_id,
|
|
last_read_post_number,
|
|
tu.notification_level,
|
|
#{TopicTrackingState.highest_post_number_column_select(user.whisperer?)},
|
|
ARRAY(SELECT group_id FROM topic_allowed_groups WHERE topic_allowed_groups.topic_id = topics.id) AS group_ids
|
|
FROM topics
|
|
JOIN users u on u.id = #{user.id.to_i}
|
|
JOIN user_stats AS us ON us.user_id = u.id
|
|
JOIN user_options AS uo ON uo.user_id = u.id
|
|
LEFT JOIN group_users gu ON gu.user_id = u.id
|
|
LEFT JOIN topic_allowed_groups tag ON tag.topic_id = topics.id AND tag.group_id = gu.group_id
|
|
LEFT JOIN topic_users tu ON tu.topic_id = topics.id AND tu.user_id = u.id
|
|
LEFT JOIN topic_allowed_users tau ON tau.topic_id = topics.id AND tau.user_id = u.id
|
|
#{skip_new ? "" : "LEFT JOIN dismissed_topic_users ON dismissed_topic_users.topic_id = topics.id AND dismissed_topic_users.user_id = #{user.id.to_i}"}
|
|
WHERE (tau.topic_id IS NOT NULL OR tag.topic_id IS NOT NULL) AND
|
|
topics.archetype = 'private_message' AND
|
|
((#{unread}) OR (#{new})) AND
|
|
topics.deleted_at IS NULL
|
|
SQL
|
|
end
|
|
|
|
def self.publish_unread(post)
|
|
topic = post.topic
|
|
return unless topic.private_message?
|
|
|
|
scope = TopicUser.tracking(post.topic_id).includes(user: %i[user_stat user_option])
|
|
|
|
allowed_group_ids = topic.allowed_groups.pluck(:id)
|
|
|
|
group_ids =
|
|
if post.post_type == Post.types[:whisper]
|
|
[Group::AUTO_GROUPS[:staff]]
|
|
else
|
|
allowed_group_ids
|
|
end
|
|
|
|
if group_ids.present?
|
|
scope =
|
|
scope.joins("INNER JOIN group_users gu ON gu.user_id = topic_users.user_id").where(
|
|
"gu.group_id IN (?)",
|
|
group_ids,
|
|
)
|
|
end
|
|
|
|
# Note: At some point we may want to make the same performance optimisation
|
|
# here as we did with the other topic tracking state, where we only send
|
|
# one 'unread' update to all users, not a more accurate unread update to
|
|
# each individual user with their own read state.
|
|
#
|
|
# cf. f6c852bf8e7f4dea519425ba87a114f22f52a8f4
|
|
scope
|
|
.select(%i[user_id last_read_post_number notification_level])
|
|
.each do |tu|
|
|
if tu.last_read_post_number.nil? &&
|
|
topic.created_at < tu.user.user_option.treat_as_new_topic_start_date
|
|
next
|
|
end
|
|
|
|
message = {
|
|
topic_id: post.topic_id,
|
|
message_type: UNREAD_MESSAGE_TYPE,
|
|
payload: {
|
|
last_read_post_number: tu.last_read_post_number,
|
|
highest_post_number: post.post_number,
|
|
notification_level: tu.notification_level,
|
|
group_ids: allowed_group_ids,
|
|
created_by_user_id: post.user_id,
|
|
},
|
|
}
|
|
|
|
MessageBus.publish(self.user_channel(tu.user_id), message.as_json, user_ids: [tu.user_id])
|
|
end
|
|
end
|
|
|
|
def self.publish_new(topic)
|
|
return unless topic.private_message?
|
|
|
|
message = {
|
|
message_type: NEW_MESSAGE_TYPE,
|
|
topic_id: topic.id,
|
|
payload: {
|
|
last_read_post_number: nil,
|
|
highest_post_number: 1,
|
|
group_ids: topic.allowed_groups.pluck(:id),
|
|
created_by_user_id: topic.user_id,
|
|
},
|
|
}.as_json
|
|
|
|
topic
|
|
.allowed_users
|
|
.pluck(:id)
|
|
.each do |user_id|
|
|
MessageBus.publish(self.user_channel(user_id), message, user_ids: [user_id])
|
|
end
|
|
|
|
topic
|
|
.allowed_groups
|
|
.pluck(:id)
|
|
.each do |group_id|
|
|
MessageBus.publish(self.group_channel(group_id), message, group_ids: [group_id])
|
|
end
|
|
end
|
|
|
|
def self.publish_group_archived(topic:, group_id:, acting_user_id: nil)
|
|
return unless topic.private_message?
|
|
|
|
message = {
|
|
message_type: GROUP_ARCHIVE_MESSAGE_TYPE,
|
|
topic_id: topic.id,
|
|
payload: {
|
|
group_ids: [group_id],
|
|
acting_user_id: acting_user_id,
|
|
},
|
|
}.as_json
|
|
|
|
MessageBus.publish(self.group_channel(group_id), message, group_ids: [group_id])
|
|
end
|
|
|
|
def self.publish_read(topic_id, last_read_post_number, user, notification_level = nil)
|
|
self.publish_read_message(
|
|
message_type: READ_MESSAGE_TYPE,
|
|
channel_name: self.user_channel(user.id),
|
|
topic_id: topic_id,
|
|
user: user,
|
|
last_read_post_number: last_read_post_number,
|
|
notification_level: notification_level,
|
|
)
|
|
end
|
|
|
|
def self.user_channel(user_id)
|
|
"#{CHANNEL_PREFIX}/user/#{user_id}"
|
|
end
|
|
|
|
def self.group_channel(group_id)
|
|
"#{CHANNEL_PREFIX}/group/#{group_id}"
|
|
end
|
|
end
|