DEV: Add backend support for unread and new topics list (#20293)

This commit adds backend support for a new topics list that combines both the current unread and new topics lists. We're going to experiment with this new list (name TBD) internally and decide if this feature is something that we want to fully build.

Internal topic: t/77234.
This commit is contained in:
Osama Sayegh 2023-02-16 16:02:09 +03:00 committed by GitHub
parent c541bd05a8
commit 5423e7c5b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 184 additions and 15 deletions

View File

@ -507,21 +507,7 @@ class TopicQuery
whisperer: @user&.whisperer?,
).order("CASE WHEN topics.user_id = tu.user_id THEN 1 ELSE 2 END")
if @user
# micro optimisation so we don't load up all of user stats which we do not need
unread_at =
DB.query_single("select first_unread_at from user_stats where user_id = ?", @user.id).first
if max_age = options[:max_age]
max_age_date = max_age.days.ago
unread_at ||= max_age_date
unread_at = unread_at > max_age_date ? unread_at : max_age_date
end
# perf note, in the past we tried doing this in a subquery but performance was
# terrible, also tried with a join and it was bad
result = result.where("topics.updated_at >= ?", unread_at)
end
result = apply_max_age_limit(result, options)
self.class.results_filter_callbacks.each do |filter_callback|
result = filter_callback.call(:unread, result, @user, options)
@ -548,6 +534,28 @@ class TopicQuery
suggested_ordering(result, options)
end
def new_and_unread_results(options = {})
base = default_results(options.reverse_merge(unordered: true))
new_results =
TopicQuery.new_filter(
base,
treat_as_new_topic_start_date: @user.user_option.treat_as_new_topic_start_date,
)
new_results = remove_muted(new_results, @user, options)
new_results = remove_dismissed(new_results, @user)
unread_results =
apply_max_age_limit(TopicQuery.unread_filter(base, whisperer: @user&.whisperer?), options)
base.joins_values.concat(new_results.joins_values, unread_results.joins_values)
base.joins_values.uniq!
results = base.merge(new_results.or(unread_results))
results = results.order("CASE WHEN topics.user_id = tu.user_id THEN 1 ELSE 2 END")
suggested_ordering(results, options)
end
protected
def per_page_setting
@ -1167,4 +1175,23 @@ class TopicQuery
col_name = whisperer ? "highest_staff_post_number" : "highest_post_number"
list.where("tu.last_read_post_number IS NULL OR tu.last_read_post_number < topics.#{col_name}")
end
def apply_max_age_limit(results, options)
if @user
# micro optimisation so we don't load up all of user stats which we do not need
unread_at =
DB.query_single("select first_unread_at from user_stats where user_id = ?", @user.id).first
if max_age = options[:max_age]
max_age_date = max_age.days.ago
unread_at ||= max_age_date
unread_at = unread_at > max_age_date ? unread_at : max_age_date
end
# perf note, in the past we tried doing this in a subquery but performance was
# terrible, also tried with a join and it was bad
results = results.where("topics.updated_at >= ?", unread_at)
end
results
end
end

View File

@ -1731,4 +1731,146 @@ RSpec.describe TopicQuery do
end
end
end
describe "#new_and_unread_results" do
fab!(:unread_topic) { Fabricate(:post).topic }
fab!(:new_topic) { Fabricate(:post).topic }
fab!(:read_topic) { Fabricate(:post).topic }
before do
unread_post = Fabricate(:post, topic: unread_topic)
read_post = Fabricate(:post, topic: read_topic)
TopicUser.change(
user.id,
unread_topic.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.change(
user.id,
read_topic.id,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.update_last_read(user, unread_topic.id, unread_post.post_number - 1, 1, 1)
TopicUser.update_last_read(user, read_topic.id, read_post.post_number, 1, 1)
end
it "includes unread and new topics for the user" do
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
new_topic.id,
)
end
it "doesn't include deleted topics" do
unread_topic.trash!
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
new_topic.id,
)
end
it "doesn't include muted topics with unread posts" do
TopicUser.change(
user.id,
unread_topic.id,
notification_level: TopicUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
new_topic.id,
)
end
it "doesn't include muted new topics" do
TopicUser.change(
user.id,
new_topic.id,
notification_level: TopicUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
)
end
it "doesn't include new topics in muted category" do
CategoryUser.create!(
user_id: user.id,
category_id: new_topic.category.id,
notification_level: CategoryUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
)
end
it "includes unread and trakced topics even if they're in a muted category" do
new_topic.update!(category: Fabricate(:category))
CategoryUser.create!(
user_id: user.id,
category_id: unread_topic.category.id,
notification_level: CategoryUser.notification_levels[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
new_topic.id,
)
end
it "doesn't include new topics that have a muted tag(s)" do
SiteSetting.tagging_enabled = true
tag = Fabricate(:tag)
new_topic.tags << tag
new_topic.save!
TagUser.create!(
tag_id: tag.id,
user_id: user.id,
notification_level: NotificationLevels.all[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
)
end
it "includes unread and tracked topics even if they have a muted tag(s)" do
SiteSetting.tagging_enabled = true
tag = Fabricate(:tag)
unread_topic.tags << tag
unread_topic.save!
TagUser.create!(
tag_id: tag.id,
user_id: user.id,
notification_level: NotificationLevels.all[:muted],
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
new_topic.id,
)
end
it "doesn't include topics in restricted categories that user cannot access" do
category = Fabricate(:category_with_definition)
group = Fabricate(:group)
category.set_permissions(group => :full)
category.save!
unread_topic.update!(category: category)
new_topic.update!(category: category)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to be_blank
end
it "doesn't include dismissed topics" do
DismissedTopicUser.create!(
user_id: user.id,
topic_id: new_topic.id,
created_at: Time.zone.now,
)
expect(TopicQuery.new(user).new_and_unread_results.pluck(:id)).to contain_exactly(
unread_topic.id,
)
end
end
end