mirror of
https://github.com/discourse/discourse.git
synced 2024-12-28 12:43:53 +08:00
f71e9aad60
This change creates a user setting that they can toggle if they don't want to receive unread notifications when someone closes a topic they have read and are watching/tracking it.
248 lines
7.5 KiB
Ruby
248 lines
7.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class PostTiming < ActiveRecord::Base
|
|
belongs_to :topic
|
|
belongs_to :user
|
|
|
|
validates_presence_of :post_number
|
|
validates_presence_of :msecs
|
|
|
|
def self.pretend_read(topic_id, actual_read_post_number, pretend_read_post_number, user_ids = nil)
|
|
# This is done in SQL cause the logic is quite tricky and we want to do this in one db hit
|
|
#
|
|
user_ids_condition = user_ids.present? ? "AND user_id = ANY(ARRAY[:user_ids]::int[])" : ""
|
|
sql_query = <<-SQL
|
|
INSERT INTO post_timings(topic_id, user_id, post_number, msecs)
|
|
SELECT :topic_id, user_id, :pretend_read_post_number, 1
|
|
FROM post_timings pt
|
|
WHERE topic_id = :topic_id AND
|
|
post_number = :actual_read_post_number
|
|
#{user_ids_condition}
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM post_timings pt1
|
|
WHERE pt1.topic_id = pt.topic_id AND
|
|
pt1.post_number = :pretend_read_post_number AND
|
|
pt1.user_id = pt.user_id
|
|
)
|
|
SQL
|
|
|
|
params = {
|
|
pretend_read_post_number: pretend_read_post_number,
|
|
topic_id: topic_id,
|
|
actual_read_post_number: actual_read_post_number,
|
|
}
|
|
params[:user_ids] = user_ids if user_ids.present?
|
|
|
|
DB.exec(sql_query, params)
|
|
|
|
TopicUser.ensure_consistency!(topic_id)
|
|
end
|
|
|
|
def self.record_new_timing(args)
|
|
row_count =
|
|
DB.exec(
|
|
"INSERT INTO post_timings (topic_id, user_id, post_number, msecs)
|
|
SELECT :topic_id, :user_id, :post_number, :msecs
|
|
ON CONFLICT DO NOTHING",
|
|
args,
|
|
)
|
|
|
|
# concurrency is hard, we are not running serialized so this can possibly
|
|
# still happen, if it happens we just don't care, its an invalid record anyway
|
|
return if row_count == 0
|
|
Post.where(
|
|
["topic_id = :topic_id and post_number = :post_number", args],
|
|
).update_all "reads = reads + 1"
|
|
|
|
return if Topic.exists?(id: args[:topic_id], archetype: Archetype.private_message)
|
|
UserStat.where(user_id: args[:user_id]).update_all "posts_read_count = posts_read_count + 1"
|
|
end
|
|
|
|
# Increases a timer if a row exists, otherwise create it
|
|
def self.record_timing(args)
|
|
rows = DB.exec(<<~SQL, args)
|
|
UPDATE post_timings
|
|
SET msecs = msecs + :msecs
|
|
WHERE topic_id = :topic_id
|
|
AND user_id = :user_id
|
|
AND post_number = :post_number
|
|
SQL
|
|
|
|
record_new_timing(args) if rows == 0
|
|
end
|
|
|
|
def self.destroy_last_for(user, topic_id: nil, topic: nil)
|
|
topic ||= Topic.find(topic_id)
|
|
post_number = user.whisperer? ? topic.highest_staff_post_number : topic.highest_post_number
|
|
|
|
last_read = post_number - 1
|
|
|
|
PostTiming.transaction do
|
|
PostTiming.where(
|
|
"topic_id = ? AND user_id = ? AND post_number > ?",
|
|
topic.id,
|
|
user.id,
|
|
last_read,
|
|
).delete_all
|
|
last_read = nil if last_read < 1
|
|
|
|
TopicUser.where(user_id: user.id, topic_id: topic.id).update_all(
|
|
last_read_post_number: last_read,
|
|
)
|
|
|
|
topic.posts.find_by(post_number: post_number).decrement!(:reads)
|
|
|
|
if topic.private_message?
|
|
set_minimum_first_unread_pm!(topic: topic, user_id: user.id, date: topic.updated_at)
|
|
else
|
|
set_minimum_first_unread!(user_id: user.id, date: topic.updated_at)
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.destroy_for(user_id, topic_ids)
|
|
PostTiming.transaction do
|
|
PostTiming.where("user_id = ? and topic_id in (?)", user_id, topic_ids).delete_all
|
|
|
|
TopicUser.where("user_id = ? and topic_id in (?)", user_id, topic_ids).delete_all
|
|
|
|
Post.where(topic_id: topic_ids).update_all("reads = reads - 1")
|
|
|
|
date = Topic.listable_topics.where(id: topic_ids).minimum(:updated_at)
|
|
|
|
set_minimum_first_unread!(user_id: user_id, date: date) if date
|
|
end
|
|
end
|
|
|
|
def self.set_minimum_first_unread_pm!(topic:, user_id:, date:)
|
|
if topic.topic_allowed_users.exists?(user_id: user_id)
|
|
UserStat.where("first_unread_pm_at > ? AND user_id = ?", date, user_id).update_all(
|
|
first_unread_pm_at: date,
|
|
)
|
|
else
|
|
DB.exec(<<~SQL, date: date, user_id: user_id, topic_id: topic.id)
|
|
UPDATE group_users gu
|
|
SET first_unread_pm_at = :date
|
|
FROM (
|
|
SELECT
|
|
gu2.user_id,
|
|
gu2.group_id
|
|
FROM group_users gu2
|
|
INNER JOIN topic_allowed_groups tag ON tag.group_id = gu2.group_id AND tag.topic_id = :topic_id
|
|
WHERE gu2.user_id = :user_id
|
|
) Y
|
|
WHERE gu.user_id = Y.user_id AND gu.group_id = Y.group_id
|
|
SQL
|
|
end
|
|
end
|
|
|
|
def self.set_minimum_first_unread!(user_id:, date:)
|
|
DB.exec(<<~SQL, date: date, user_id: user_id)
|
|
UPDATE user_stats
|
|
SET first_unread_at = :date
|
|
WHERE first_unread_at > :date AND
|
|
user_id = :user_id
|
|
SQL
|
|
end
|
|
|
|
MAX_READ_TIME_PER_BATCH = 60 * 1000.0
|
|
|
|
def self.process_timings(current_user, topic_id, topic_time, timings, opts = {})
|
|
UserStat.update_time_read!(current_user.id)
|
|
|
|
max_time_per_post = ((Time.now - current_user.created_at) * 1000.0)
|
|
max_time_per_post = MAX_READ_TIME_PER_BATCH if max_time_per_post > MAX_READ_TIME_PER_BATCH
|
|
|
|
highest_seen = 1
|
|
new_posts_read = 0
|
|
|
|
join_table = []
|
|
|
|
i = timings.length
|
|
while i > 0
|
|
i -= 1
|
|
timings[i][1] = max_time_per_post if timings[i][1] > max_time_per_post
|
|
timings.delete_at(i) if timings[i][0] < 1
|
|
end
|
|
|
|
timings.each_with_index do |(post_number, time), index|
|
|
join_table << "SELECT #{topic_id.to_i} topic_id, #{post_number.to_i} post_number,
|
|
#{current_user.id.to_i} user_id, #{time.to_i} msecs, #{index} idx"
|
|
|
|
highest_seen = post_number.to_i > highest_seen ? post_number.to_i : highest_seen
|
|
end
|
|
|
|
if join_table.length > 0
|
|
sql = <<~SQL
|
|
UPDATE post_timings t
|
|
SET msecs = LEAST(t.msecs::bigint + x.msecs, 2^31 - 1)
|
|
FROM (#{join_table.join(" UNION ALL ")}) x
|
|
WHERE x.topic_id = t.topic_id AND
|
|
x.post_number = t.post_number AND
|
|
x.user_id = t.user_id
|
|
RETURNING x.idx
|
|
SQL
|
|
|
|
existing = Set.new(DB.query_single(sql))
|
|
|
|
sql = <<~SQL
|
|
SELECT 1 FROM topics
|
|
WHERE deleted_at IS NULL AND
|
|
archetype = 'regular' AND
|
|
id = :topic_id
|
|
SQL
|
|
|
|
is_regular = DB.exec(sql, topic_id: topic_id) == 1
|
|
new_posts_read = timings.size - existing.size if is_regular
|
|
|
|
timings.each_with_index do |(post_number, time), index|
|
|
unless existing.include?(index)
|
|
PostTiming.record_new_timing(
|
|
topic_id: topic_id,
|
|
post_number: post_number,
|
|
user_id: current_user.id,
|
|
msecs: time,
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
total_changed = 0
|
|
if timings.length > 0
|
|
total_changed = Notification.mark_posts_read(current_user, topic_id, timings.map { |t| t[0] })
|
|
end
|
|
|
|
topic_time = max_time_per_post if topic_time > max_time_per_post
|
|
|
|
TopicUser.update_last_read(
|
|
current_user,
|
|
topic_id,
|
|
highest_seen,
|
|
new_posts_read,
|
|
topic_time,
|
|
opts,
|
|
)
|
|
TopicGroup.update_last_read(current_user, topic_id, highest_seen)
|
|
|
|
if total_changed > 0
|
|
current_user.reload
|
|
current_user.publish_notifications_state
|
|
end
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: post_timings
|
|
#
|
|
# topic_id :integer not null
|
|
# post_number :integer not null
|
|
# user_id :integer not null
|
|
# msecs :integer not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_post_timings_on_user_id (user_id)
|
|
# post_timings_unique (topic_id,post_number,user_id) UNIQUE
|
|
#
|