mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
Small action posts (closing, opening, pinning, archiving, category changes, auto-close, etc. — post_type 3) no longer count toward a topic's counters or unread tracking. They still occupy a post_number so the post stream stays gapless, but they contribute to nothing: not highest_post_number, highest_staff_post_number, posts_count, last_posted_at, last_post_user_id, word_count or bumped_at, and they never mark a topic unread or publish unread updates — for regular users and staff alike. Previously this exclusion only applied to private messages. Because small actions advanced highest_post_number, a topic whose only new post was a small action (say an auto-close at the end of a thread) was treated as unread: the nav "Unread (N)" badge counted it while /unread did not list it — the "Unread (N) but /unread is empty" phantom reported in https://meta.discourse.org/t/-/403986. More broadly, routine administrivia such as bulk closes and auto-close timers should not notify watchers or inflate reply counts (https://meta.discourse.org/t/-/404058). How it works: - Topic.next_post_number assigns small actions a post_number but advances no counter (whispers bump only the staff counter; small actions bump neither). - Topic.reset_highest / reset_all_highest! exclude them via two shared SQL fragments — public_post_types_sql (no whispers, no small actions) and staff_post_types_sql (no small actions) — collapsing the old regular-vs-PM branches into one. - PostCreator skips last_posted_at / last_post_user_id / word_count / bumped_at and the user post count for small actions; PostDestroyer's last-post lookup skips them too. - TopicTrackingState / PrivateMessageTopicTrackingState#publish_unread and the PostUpdateTopicTrackingState job return early for small actions. - Category time-based post counts, UserStat#calc_topic_reply_count! and the import.rake topic backfill exclude them as well. - A post-deploy migration recomputes the affected counters for existing topics and clamps any topic_users.last_read_post_number left pointing past the new highest (whisperers to highest_staff_post_number, everyone else to highest_post_number) so nobody is stuck with phantom read state. Keeping the unread count and the /unread list filtering on the same column is a separate, complementary fix that ships on its own. This supersedes the earlier proof-of-concept PRs #40290 and #39093.
229 lines
8 KiB
Ruby
Vendored
229 lines
8 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe PrivateMessageTopicTrackingState do
|
|
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
|
fab!(:user_2, :user)
|
|
|
|
fab!(:group) do
|
|
Fabricate(:group, messageable_level: Group::ALIAS_LEVELS[:everyone]).tap { |g| g.add(user_2) }
|
|
end
|
|
|
|
fab!(:group_message) do
|
|
create_post(
|
|
user: user,
|
|
target_group_names: [group.name],
|
|
archetype: Archetype.private_message,
|
|
).topic
|
|
end
|
|
|
|
fab!(:private_message) do
|
|
create_post(
|
|
user: user,
|
|
target_usernames: [user_2.username],
|
|
archetype: Archetype.private_message,
|
|
).topic
|
|
end
|
|
|
|
fab!(:private_message_2) do
|
|
create_post(
|
|
user: user,
|
|
target_usernames: [Fabricate(:user).username],
|
|
archetype: Archetype.private_message,
|
|
).topic
|
|
end
|
|
|
|
describe ".report" do
|
|
it "returns the right tracking state" do
|
|
TopicUser.find_by(user: user_2, topic: group_message).update!(last_read_post_number: 1)
|
|
|
|
expect(described_class.report(user_2).map(&:topic_id)).to contain_exactly(private_message.id)
|
|
|
|
create_post(user: user, topic: group_message)
|
|
|
|
report = described_class.report(user_2)
|
|
|
|
expect(report.map(&:topic_id)).to contain_exactly(group_message.id, private_message.id)
|
|
|
|
state = report.first
|
|
|
|
expect(state.topic_id).to eq(private_message.id)
|
|
expect(state.user_id).to eq(user_2.id)
|
|
expect(state.last_read_post_number).to eq(nil)
|
|
expect(state.notification_level).to eq(NotificationLevels.all[:watching])
|
|
expect(state.highest_post_number).to eq(1)
|
|
expect(state.group_ids).to eq([])
|
|
|
|
expect(report.last.group_ids).to contain_exactly(group.id)
|
|
end
|
|
|
|
it "returns the right tracking state when topics contain whispers" do
|
|
SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}"
|
|
TopicUser.find_by(user: user_2, topic: private_message).update!(last_read_post_number: 1)
|
|
|
|
create_post(
|
|
raw: "this is a test post",
|
|
topic: private_message,
|
|
post_type: Post.types[:whisper],
|
|
user: Fabricate(:admin),
|
|
)
|
|
|
|
expect(described_class.report(user_2).map(&:topic_id)).to contain_exactly(group_message.id)
|
|
|
|
user_2.grant_admin!
|
|
|
|
tracking_state = described_class.report(user_2)
|
|
|
|
expect(
|
|
tracking_state.map { |topic| [topic.topic_id, topic.highest_post_number] },
|
|
).to contain_exactly([group_message.id, 1], [private_message.id, 2])
|
|
end
|
|
|
|
it "returns the right tracking state when topics have been dismissed" do
|
|
DismissedTopicUser.create!(user_id: user_2.id, topic_id: group_message.id)
|
|
|
|
expect(described_class.report(user_2).map(&:topic_id)).to contain_exactly(private_message.id)
|
|
end
|
|
|
|
it "excludes invisible topics for non-staff users" do
|
|
TopicUser.find_by(user: user_2, topic: private_message).update!(last_read_post_number: 1)
|
|
|
|
private_message.update!(visible: false, highest_post_number: 2)
|
|
|
|
report = described_class.report(user_2)
|
|
expect(report.map(&:topic_id)).to contain_exactly(group_message.id)
|
|
end
|
|
|
|
it "includes invisible topics for staff users" do
|
|
user_2.grant_admin!
|
|
TopicUser.find_by(user: user_2, topic: private_message).update!(last_read_post_number: 1)
|
|
|
|
private_message.update!(visible: false, highest_post_number: 2)
|
|
|
|
report = described_class.report(user_2)
|
|
expect(report.map(&:topic_id)).to contain_exactly(group_message.id, private_message.id)
|
|
end
|
|
end
|
|
|
|
describe ".publish_new" do
|
|
it "should publish the right message_bus message" do
|
|
messages = MessageBus.track_publish { described_class.publish_new(private_message) }
|
|
|
|
expect(messages.map(&:channel)).to contain_exactly(described_class.user_channel(user_2.id))
|
|
|
|
data = messages.first.data
|
|
|
|
expect(data["message_type"]).to eq(described_class::NEW_MESSAGE_TYPE)
|
|
expect(data["topic_id"]).to eq(private_message.id)
|
|
expect(data["payload"]["last_read_post_number"]).to eq(nil)
|
|
expect(data["payload"]["highest_post_number"]).to eq(1)
|
|
expect(data["payload"]["group_ids"]).to eq([])
|
|
expect(data["payload"]["created_by_user_id"]).to eq(private_message.user_id)
|
|
end
|
|
|
|
it "should publish the right message_bus message for a group message" do
|
|
messages = MessageBus.track_publish { described_class.publish_new(group_message) }
|
|
|
|
expect(messages.map(&:channel)).to contain_exactly(described_class.group_channel(group.id))
|
|
|
|
data = messages.first.data
|
|
|
|
expect(data["message_type"]).to eq(described_class::NEW_MESSAGE_TYPE)
|
|
expect(data["topic_id"]).to eq(group_message.id)
|
|
expect(data["payload"]["last_read_post_number"]).to eq(nil)
|
|
expect(data["payload"]["highest_post_number"]).to eq(1)
|
|
expect(data["payload"]["group_ids"]).to eq([group.id])
|
|
expect(data["payload"]["created_by_user_id"]).to eq(group_message.user_id)
|
|
end
|
|
end
|
|
|
|
describe ".publish_unread" do
|
|
it "should publish the right message_bus message" do
|
|
messages =
|
|
MessageBus.track_publish { described_class.publish_unread(private_message.first_post) }
|
|
|
|
expect(messages.map(&:channel)).to contain_exactly(described_class.user_channel(user_2.id))
|
|
|
|
data = messages.first.data
|
|
|
|
expect(data["message_type"]).to eq(described_class::UNREAD_MESSAGE_TYPE)
|
|
expect(data["topic_id"]).to eq(private_message.id)
|
|
expect(data["payload"]["last_read_post_number"]).to eq(nil)
|
|
expect(data["payload"]["highest_post_number"]).to eq(1)
|
|
expect(data["payload"]["created_by_user_id"]).to eq(private_message.first_post.user_id)
|
|
expect(data["payload"]["notification_level"]).to eq(NotificationLevels.all[:watching])
|
|
expect(data["payload"]["group_ids"]).to eq([])
|
|
end
|
|
|
|
it "does not publish message_bus message if post in topic is not new for user" do
|
|
group_message.update!(created_at: 3.days.ago)
|
|
user_2.user_option.update!(new_topic_duration_minutes: 2.days.minutes)
|
|
|
|
messages =
|
|
MessageBus.track_publish { described_class.publish_unread(group_message.first_post) }
|
|
|
|
expect(messages).to eq([])
|
|
end
|
|
|
|
it "does not publish small_action posts" do
|
|
small_action =
|
|
Fabricate(
|
|
:post,
|
|
topic: private_message,
|
|
user: Discourse.system_user,
|
|
post_type: Post.types[:small_action],
|
|
action_code: "visible.disabled",
|
|
)
|
|
|
|
messages = MessageBus.track_publish { described_class.publish_unread(small_action) }
|
|
|
|
expect(messages).to eq([])
|
|
end
|
|
end
|
|
|
|
describe ".publish_group_archived" do
|
|
it "should publish the right message_bus message" do
|
|
user_3 = Fabricate(:user)
|
|
group.add(user_3)
|
|
|
|
messages =
|
|
MessageBus.track_publish do
|
|
described_class.publish_group_archived(
|
|
topic: group_message,
|
|
group_id: group.id,
|
|
acting_user_id: user_3.id,
|
|
)
|
|
end
|
|
|
|
expect(messages.map(&:channel)).to contain_exactly(described_class.group_channel(group.id))
|
|
|
|
data =
|
|
messages.find { |message| message.channel == described_class.group_channel(group.id) }.data
|
|
|
|
expect(data["message_type"]).to eq(described_class::GROUP_ARCHIVE_MESSAGE_TYPE)
|
|
expect(data["topic_id"]).to eq(group_message.id)
|
|
expect(data["payload"]["group_ids"]).to contain_exactly(group.id)
|
|
expect(data["payload"]["acting_user_id"]).to eq(user_3.id)
|
|
end
|
|
end
|
|
|
|
describe ".publish_read" do
|
|
it "should publish the right message_bus message" do
|
|
message =
|
|
MessageBus
|
|
.track_publish(described_class.user_channel(user.id)) do
|
|
PrivateMessageTopicTrackingState.publish_read(private_message.id, 1, user)
|
|
end
|
|
.first
|
|
|
|
data = message.data
|
|
|
|
expect(message.user_ids).to contain_exactly(user.id)
|
|
expect(message.group_ids).to eq(nil)
|
|
expect(data["topic_id"]).to eq(private_message.id)
|
|
expect(data["message_type"]).to eq(described_class::READ_MESSAGE_TYPE)
|
|
expect(data["payload"]["last_read_post_number"]).to eq(1)
|
|
expect(data["payload"]["highest_post_number"]).to eq(1)
|
|
expect(data["payload"]["notification_level"]).to eq(nil)
|
|
end
|
|
end
|
|
end
|