mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 20:54:00 -06:00
c6c633e041
This commit fixes two issues at play. The first was introduced
in f6c852b
(or maybe not introduced
but rather revealed). When a user posted a new message in a topic,
they received the unread topic tracking state MessageBus message,
and the Unread (X) indicator was incremented by one, because with the
aforementioned perf commit we "guess" the correct last read post
for the user, because we no longer calculate individual users' read
status there. This meant that every time a user posted in a topic
they tracked, the unread indicator was incremented. To get around
this, we can just exclude the user who created the post from the
target users of the unread state message.
The second issue was related to the private message topic tracking
state, and was somewhat similar. Whenever a user created a new private
message, the New (X) indicator was incremented, and could not be
cleared until the page was refreshed. To solve this, we just don't
update the topic state for the user when the new_topic tracking state
message comes through if the user who created the topic is the
same as the current user.
cf. https://meta.discourse.org/t/bottom-of-topic-shows-there-is-1-unread-remaining-when-there-are-actually-0-unread-topics-remaining/220817
218 lines
6.8 KiB
Ruby
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(staff: user.staff?)}
|
|
#{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.staff?)},
|
|
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
|