FIX: update PM tracking state when marking messages as unread (#36258)

When marking a private message as unread via "destroy_timings", two
  issues prevented old PMs from appearing in the Unread list:

1. PostTiming.destroy_for used Topic.listable_topics which excludes PMs,
so first_unread_pm_at was never updated. Now it explicitly handles PMs
by calling set_minimum_first_unread_pm!

2. The client-side PM tracking state wasn't notified of the change.
Unlike regular topics which sync state on navigation, PM tracking relies
on MessageBus for updates. Added publish_read call to notify the client.

Internal ref - t/155983
This commit is contained in:
Régis Hanol
2025-11-26 18:29:35 +01:00
committed by GitHub
parent c062c0634c
commit 9eb2a66879
4 changed files with 75 additions and 2 deletions
+13
View File
@@ -341,6 +341,19 @@ class TopicsController < ApplicationController
last_notification.update!(read: false) if last_notification
topic = Topic.find_by(id: topic_id)
if topic&.private_message?
topic_user = TopicUser.find_by(user: current_user, topic:)
PrivateMessageTopicTrackingState.publish_read(
topic_id,
topic_user&.last_read_post_number,
current_user,
topic_user&.notification_level,
)
end
render body: nil
end
+8 -2
View File
@@ -108,9 +108,15 @@ class PostTiming < ActiveRecord::Base
Post.where(topic_id: topic_ids).update_all("reads = reads - 1")
date = Topic.listable_topics.where(id: topic_ids).minimum(:updated_at)
topics = Topic.where(id: topic_ids)
set_minimum_first_unread!(user_id: user_id, date: date) if date
if (date = topics.listable_topics.minimum(:updated_at))
set_minimum_first_unread!(user_id:, date:)
end
topics.private_messages.find_each do |topic|
set_minimum_first_unread_pm!(topic:, user_id:, date: topic.updated_at)
end
end
end
+26
View File
@@ -226,4 +226,30 @@ RSpec.describe PostTiming do
)
end
end
describe ".destroy_for" do
it "updates first unread for a user correctly when topic is a pm" do
post = Fabricate(:private_message_post)
post.topic.update!(updated_at: 10.minutes.ago)
PostTiming.process_timings(post.user, post.topic_id, 1, [[post.post_number, 100]])
PostTiming.destroy_for(post.user.id, [post.topic_id])
expect(post.user.user_stat.reload.first_unread_pm_at).to eq_time(post.topic.updated_at)
end
it "updates first unread for a user correctly when topic is a group pm" do
topic = Fabricate(:private_message_topic, updated_at: 10.minutes.ago)
post = Fabricate(:post, topic:)
user = Fabricate(:user)
group = Fabricate(:group)
group.add(user)
topic.allowed_groups << group
PostTiming.process_timings(user, topic.id, 1, [[post.post_number, 100]])
PostTiming.destroy_for(user.id, [topic.id])
expect(GroupUser.find_by(user:, group:).first_unread_pm_at).to eq_time(topic.updated_at)
end
end
end
+28
View File
@@ -1489,6 +1489,34 @@ RSpec.describe TopicsController do
}.from([1, 1]).to([0, 0])
end
end
context "for private messages" do
fab!(:pm_post, :private_message_post)
fab!(:pm_topic) { pm_post.topic }
fab!(:pm_user) { pm_topic.user }
before do
sign_in(pm_user)
TopicUser.create!(
topic: pm_topic,
user: pm_user,
last_read_post_number: 1,
notification_level: TopicUser.notification_levels[:watching],
)
PostTiming.create!(topic: pm_topic, user: pm_user, post_number: 1, msecs: 1000)
end
it "publishes a message to update the client-side tracking state" do
messages =
MessageBus.track_publish(PrivateMessageTopicTrackingState.user_channel(pm_user.id)) do
delete "/t/#{pm_topic.id}/timings.json"
end
expect(messages.size).to eq(1)
expect(messages.first.data["message_type"]).to eq("read")
expect(messages.first.data["topic_id"]).to eq(pm_topic.id)
end
end
end
describe "#mute/unmute" do