diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a421f328e3c..84863d018a2 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -991,6 +991,7 @@ en: all: "All" read: "Read" unread: "Unread" + unseen: "Unseen" ignore_duration_title: "Ignore User" ignore_duration_username: "Username" ignore_duration_when: "Duration:" @@ -2403,6 +2404,7 @@ en: none: unread: "You have no unread topics." + unseen: "You have no unseen topics." new: "You have no new topics." read: "You haven't read any topics yet." posted: "You haven't posted in any topics yet." @@ -2419,6 +2421,7 @@ en: read: "There are no more read topics." new: "There are no more new topics." unread: "There are no more unread topics." + unseen: "There are no more unseen topics." category: "There are no more %{category} topics." tag: "There are no more %{tag} topics." top: "There are no more top topics." @@ -3433,6 +3436,9 @@ en: lower_title_with_count: one: "%{count} unread" other: "%{count} unread" + unseen: + title: "Unseen" + lower_title: "unseen" new: lower_title_with_count: one: "%{count} new" @@ -3728,6 +3734,7 @@ en: topics: none: unread: "You have no unread topics." + unseen: "You have no unseen topics." new: "You have no new topics." read: "You haven't read any topics yet." posted: "You haven't posted in any topics yet." diff --git a/config/site_settings.yml b/config/site_settings.yml index 7ef0cb3df52..50be628769a 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -179,6 +179,7 @@ basic: - latest - new - unread + - unseen - top - categories - read diff --git a/lib/discourse.rb b/lib/discourse.rb index c9a7ece67a8..dd599d1730f 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -245,7 +245,7 @@ module Discourse class ScssError < StandardError; end def self.filters - @filters ||= [:latest, :unread, :new, :top, :read, :posted, :bookmarks] + @filters ||= [:latest, :unread, :new, :unseen, :top, :read, :posted, :bookmarks] end def self.anonymous_filters diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 250c435291b..b2e2b0c9b9d 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -259,6 +259,10 @@ class TopicQuery create_list(:unread, { unordered: true }, unread_results) end + def list_unseen + create_list(:unseen, { unordered: true }, unseen_results) + end + def list_posted create_list(:posted) { |l| l.where('tu.posted') } end @@ -445,9 +449,21 @@ class TopicQuery def latest_results(options = {}) result = default_results(options) - result = remove_muted_topics(result, @user) unless options && options[:state] == "muted" - result = remove_muted_categories(result, @user, exclude: options[:category]) - result = remove_muted_tags(result, @user, options) + result = remove_muted(result, @user, options) + result = apply_shared_drafts(result, get_category_id(options[:category]), options) + + # plugins can remove topics here: + self.class.results_filter_callbacks.each do |filter_callback| + result = filter_callback.call(:latest, result, @user, options) + end + + result + end + + def unseen_results(options = {}) + result = default_results(options) + result = unseen_filter(result, @user.first_seen_at, @user.staff?) if @user + result = remove_muted(result, @user, options) result = apply_shared_drafts(result, get_category_id(options[:category]), options) # plugins can remove topics here: @@ -495,9 +511,7 @@ class TopicQuery default_results(options.reverse_merge(unordered: true)), treat_as_new_topic_start_date: @user.user_option.treat_as_new_topic_start_date ) - result = remove_muted_topics(result, @user) - result = remove_muted_categories(result, @user, exclude: options[:category]) - result = remove_muted_tags(result, @user, options) + result = remove_muted(result, @user, options) result = remove_dismissed(result, @user) self.class.results_filter_callbacks.each do |filter_callback| @@ -791,6 +805,12 @@ class TopicQuery result end + def remove_muted(list, user, options) + list = remove_muted_topics(list, user) unless options && options[:state] == "muted" + list = remove_muted_categories(list, user, exclude: options[:category]) + remove_muted_tags(list, user, options) + end + def remove_muted_topics(list, user) if user list = list.where('COALESCE(tu.notification_level,1) > :muted', muted: TopicUser.notification_levels[:muted]) @@ -1033,4 +1053,13 @@ class TopicQuery result.order('topics.bumped_at DESC') end + + private + + def unseen_filter(list, user_first_seen_at, staff) + list = list.where("topics.bumped_at >= ?", user_first_seen_at) + + col_name = staff ? "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 end diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index 744ccd62a7d..fd879b5f203 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -62,7 +62,7 @@ describe TopicQuery do end end - context "list_topics_by" do + context "#list_topics_by" do it "allows users to view their own invisible topics" do _topic = Fabricate(:topic, user: user) @@ -74,7 +74,7 @@ describe TopicQuery do end - context "prioritize_pinned_topics" do + context "#prioritize_pinned_topics" do it "does the pagination correctly" do num_topics = 15 per_page = 3 @@ -730,7 +730,7 @@ describe TopicQuery do end - context 'list_new' do + context '#list_new' do context 'without a new topic' do it "has no new topics" do @@ -807,7 +807,7 @@ describe TopicQuery do end - context 'list_posted' do + context '#list_posted' do let(:topics) { topic_query.list_posted.topics } it "returns blank when there are no posted topics" do @@ -861,7 +861,58 @@ describe TopicQuery do end end - context 'list_related_for do' do + context '#list_unseen' do + it "returns an empty list when there aren't topics" do + expect(topic_query.list_unseen.topics).to be_blank + end + + it "doesn't return topics that were bumped last time before user joined the forum" do + user.first_seen_at = 10.minutes.ago + create_topic_with_three_posts(bumped_at: 15.minutes.ago) + + expect(topic_query.list_unseen.topics).to be_blank + end + + it "returns only topics that contain unseen posts" do + user.first_seen_at = 10.minutes.ago + topic_with_unseen_posts = create_topic_with_three_posts(bumped_at: 5.minutes.ago) + read_to_post(topic_with_unseen_posts, user, 1) + + fully_read_topic = create_topic_with_three_posts(bumped_at: 5.minutes.ago) + read_to_the_end(fully_read_topic, user) + + expect(topic_query.list_unseen.topics).to eq([topic_with_unseen_posts]) + end + + it "ignores staff posts if user is not staff" do + user.first_seen_at = 10.minutes.ago + topic = create_topic_with_three_posts(bumped_at: 5.minutes.ago) + read_to_the_end(topic, user) + create_post(topic: topic, post_type: Post.types[:whisper]) + + expect(topic_query.list_unseen.topics).to be_blank + end + + def create_topic_with_three_posts(bumped_at:) + topic = Fabricate(:topic, bumped_at: bumped_at) + Fabricate(:post, topic: topic) + Fabricate(:post, topic: topic) + Fabricate(:post, topic: topic) + topic.highest_staff_post_number = 3 + topic.highest_post_number = 3 + topic + end + + def read_to_post(topic, user, post_number) + TopicUser.update_last_read(user, topic.id, post_number, 0, 0) + end + + def read_to_the_end(topic, user) + read_to_post topic, user, topic.highest_post_number + end + end + + context '#list_related_for' do let(:user) do Fabricate(:admin)