mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 20:24:05 -06:00
335be272ff
We cap new and unread at 2/5th of SiteSetting.max_tracked_new_unread This dynamic capping is applied under 2 conditions: 1. New capping is applied once every 15 minutes in the periodical job, this effectively ensures that usually even super active sites are capped at 200 new items 2. Unread capping is applied if a user hits max_tracked_new_unread, meaning if new + unread == 500, we defer a job that runs within 15 minutes that will cap user at 200 unread This logic ensures that at worst case a user gets "bad" numbers for 15 minutes and then the system goes ahead and fixes itself up
204 lines
6.3 KiB
Ruby
204 lines
6.3 KiB
Ruby
# this class is used to mirror unread and new status back to end users
|
|
# in JavaScript there is a mirror class that is kept in-sync using the mssage bus
|
|
# the allows end users to always know which topics have unread posts in them
|
|
# and which topics are new
|
|
|
|
class TopicTrackingState
|
|
|
|
include ActiveModel::SerializerSupport
|
|
|
|
CHANNEL = "/user-tracking"
|
|
|
|
attr_accessor :user_id,
|
|
:topic_id,
|
|
:highest_post_number,
|
|
:last_read_post_number,
|
|
:created_at,
|
|
:category_id,
|
|
:notification_level
|
|
|
|
def self.publish_new(topic)
|
|
|
|
message = {
|
|
topic_id: topic.id,
|
|
message_type: "new_topic",
|
|
payload: {
|
|
last_read_post_number: nil,
|
|
highest_post_number: 1,
|
|
created_at: topic.created_at,
|
|
topic_id: topic.id,
|
|
category_id: topic.category_id
|
|
}
|
|
}
|
|
|
|
group_ids = topic.category && topic.category.secure_group_ids
|
|
|
|
MessageBus.publish("/new", message.as_json, group_ids: group_ids)
|
|
publish_read(topic.id, 1, topic.user_id)
|
|
end
|
|
|
|
def self.publish_latest(topic)
|
|
return unless topic.archetype == "regular"
|
|
|
|
message = {
|
|
topic_id: topic.id,
|
|
message_type: "latest",
|
|
payload: {
|
|
bumped_at: topic.bumped_at,
|
|
topic_id: topic.id,
|
|
category_id: topic.category_id
|
|
}
|
|
}
|
|
|
|
group_ids = topic.category && topic.category.secure_group_ids
|
|
MessageBus.publish("/latest", message.as_json, group_ids: group_ids)
|
|
end
|
|
|
|
def self.publish_unread(post)
|
|
# TODO at high scale we are going to have to defer this,
|
|
# perhaps cut down to users that are around in the last 7 days as well
|
|
#
|
|
group_ids = post.topic.category && post.topic.category.secure_group_ids
|
|
|
|
TopicUser
|
|
.tracking(post.topic_id)
|
|
.select([:user_id,:last_read_post_number, :notification_level])
|
|
.each do |tu|
|
|
|
|
message = {
|
|
topic_id: post.topic_id,
|
|
message_type: "unread",
|
|
payload: {
|
|
last_read_post_number: tu.last_read_post_number,
|
|
highest_post_number: post.post_number,
|
|
created_at: post.created_at,
|
|
topic_id: post.topic_id,
|
|
notification_level: tu.notification_level
|
|
}
|
|
}
|
|
|
|
MessageBus.publish("/unread/#{tu.user_id}", message.as_json, group_ids: group_ids)
|
|
end
|
|
|
|
end
|
|
|
|
def self.publish_read(topic_id, last_read_post_number, user_id, notification_level=nil)
|
|
|
|
highest_post_number = Topic.where(id: topic_id).pluck(:highest_post_number).first
|
|
|
|
message = {
|
|
topic_id: topic_id,
|
|
message_type: "read",
|
|
payload: {
|
|
last_read_post_number: last_read_post_number,
|
|
highest_post_number: highest_post_number,
|
|
topic_id: topic_id,
|
|
notification_level: notification_level
|
|
}
|
|
}
|
|
|
|
MessageBus.publish("/unread/#{user_id}", message.as_json, user_ids: [user_id])
|
|
|
|
end
|
|
|
|
def self.treat_as_new_topic_clause
|
|
User.where("GREATEST(CASE
|
|
WHEN COALESCE(u.new_topic_duration_minutes, :default_duration) = :always THEN u.created_at
|
|
WHEN COALESCE(u.new_topic_duration_minutes, :default_duration) = :last_visit THEN COALESCE(u.previous_visit_at,u.created_at)
|
|
ELSE (:now::timestamp - INTERVAL '1 MINUTE' * COALESCE(u.new_topic_duration_minutes, :default_duration))
|
|
END, us.new_since, :min_date)",
|
|
now: DateTime.now,
|
|
last_visit: User::NewTopicDuration::LAST_VISIT,
|
|
always: User::NewTopicDuration::ALWAYS,
|
|
default_duration: SiteSetting.default_other_new_topic_duration_minutes,
|
|
min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime
|
|
).where_values[0]
|
|
end
|
|
|
|
def self.report(user_id, topic_id = nil)
|
|
|
|
# Sam: this is a hairy report, in particular I need custom joins and fancy conditions
|
|
# Dropping to sql_builder so I can make sense of it.
|
|
#
|
|
# Keep in mind, we need to be able to filter on a GROUP of users, and zero in on topic
|
|
# all our existing scope work does not do this
|
|
#
|
|
# This code needs to be VERY efficient as it is triggered via the message bus and may steal
|
|
# cycles from usual requests
|
|
#
|
|
#
|
|
sql = report_raw_sql(topic_id: topic_id)
|
|
|
|
sql = <<SQL
|
|
WITH x AS (
|
|
#{sql}
|
|
) SELECT * FROM x LIMIT #{SiteSetting.max_tracked_new_unread.to_i}
|
|
SQL
|
|
|
|
SqlBuilder.new(sql)
|
|
.map_exec(TopicTrackingState, user_id: user_id, topic_id: topic_id)
|
|
|
|
end
|
|
|
|
|
|
def self.report_raw_sql(opts=nil)
|
|
|
|
unread =
|
|
if opts && opts[:skip_unread]
|
|
"1=0"
|
|
else
|
|
TopicQuery.unread_filter(Topic).where_values.join(" AND ")
|
|
end
|
|
|
|
new =
|
|
if opts && opts[:skip_new]
|
|
"1=0"
|
|
else
|
|
TopicQuery.new_filter(Topic, "xxx").where_values.join(" AND ").gsub!("'xxx'", treat_as_new_topic_clause)
|
|
end
|
|
|
|
select = (opts && opts[:select]) || "
|
|
u.id AS user_id,
|
|
topics.id AS topic_id,
|
|
topics.created_at,
|
|
highest_post_number,
|
|
last_read_post_number,
|
|
c.id AS category_id,
|
|
tu.notification_level"
|
|
|
|
|
|
sql = <<SQL
|
|
SELECT #{select}
|
|
FROM topics
|
|
JOIN users u on u.id = :user_id
|
|
JOIN user_stats AS us ON us.user_id = u.id
|
|
JOIN categories c ON c.id = topics.category_id
|
|
LEFT JOIN topic_users tu ON tu.topic_id = topics.id AND tu.user_id = u.id
|
|
WHERE u.id = :user_id AND
|
|
topics.archetype <> 'private_message' AND
|
|
((#{unread}) OR (#{new})) AND
|
|
(topics.visible OR u.admin OR u.moderator) AND
|
|
topics.deleted_at IS NULL AND
|
|
( NOT c.read_restricted OR u.admin OR category_id IN (
|
|
SELECT c2.id FROM categories c2
|
|
JOIN category_groups cg ON cg.category_id = c2.id
|
|
JOIN group_users gu ON gu.user_id = :user_id AND cg.group_id = gu.group_id
|
|
WHERE c2.read_restricted )
|
|
)
|
|
AND NOT EXISTS( SELECT 1 FROM category_users cu
|
|
WHERE last_read_post_number IS NULL AND
|
|
cu.user_id = :user_id AND
|
|
cu.category_id = topics.category_id AND
|
|
cu.notification_level = #{CategoryUser.notification_levels[:muted]})
|
|
|
|
SQL
|
|
|
|
if opts && opts[:topic_id]
|
|
sql << " AND topics.id = :topic_id"
|
|
end
|
|
|
|
sql << " ORDER BY topics.bumped_at DESC"
|
|
end
|
|
|
|
end
|