discourse/app/models/private_message_topic_tracking_state.rb
Krzysztof Kotlarek 09932738e5
FEATURE: whispers available for groups (#17170)
Before, whispers were only available for staff members.

Config has been changed to allow to configure privileged groups with access to whispers. Post migration was added to move from the old setting into the new one.

I considered having a boolean column `whisperer` on user model similar to `admin/moderator` for performance reason. Finally, I decided to keep looking for groups as queries are only done for current user and didn't notice any N+1 queries.
2022-06-30 10:18:12 +10:00

218 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: [: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 peformance 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([: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