FEATURE: add Unseen view (#13977)

FEATURE: add Unseen view (#13977)

This view is the same as Latest except it hides the topics you have fully read. Based on this plugin of @davidtaylorhq Simple Unread List Plugin (discourse-simple-unread) - plugin - Discourse Meta.

diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index a421f32..84863d0 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 7ef0cb3..50be628 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 c9a7ece..dd599d1 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 250c435..b2e2b0c 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 744ccd6..fd879b5 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

[... diff too long, it was truncated ...]

GitHub sha: 622859dbe6876ca621d5d14919185c199b8edda5

This commit appears in #13977 which was approved by eviltrout. It was merged by AndrewPrigorshnev.