FIX: mark topics in sub categories as unread when dismissing parent

FIX: mark topics in sub categories as unread when dismissing parent

Previously we would only dismiss the parent category and leave the child categories unread

diff --git a/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6 b/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6
index 7dd6331..411dfb6 100644
--- a/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6
+++ b/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6
@@ -18,7 +18,7 @@ export default Ember.Mixin.create({
       this.selected.clear();
     },
 
-    dismissRead(operationType) {
+    dismissRead(operationType, categoryOptions) {
       let operation;
       if (operationType === "posts") {
         operation = { type: "dismiss_posts" };
@@ -36,7 +36,8 @@ export default Ember.Mixin.create({
         promise = Discourse.Topic.bulkOperationByFilter(
           "unread",
           operation,
-          this.get("category.id")
+          this.get("category.id"),
+          categoryOptions
         );
       }
 
diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6
index a86a333..bc53717 100644
--- a/app/assets/javascripts/discourse/models/topic.js.es6
+++ b/app/assets/javascripts/discourse/models/topic.js.es6
@@ -733,8 +733,13 @@ Topic.reopenClass({
     });
   },
 
-  bulkOperationByFilter(filter, operation, categoryId) {
-    const data = { filter, operation };
+  bulkOperationByFilter(filter, operation, categoryId, options) {
+    let data = { filter, operation };
+
+    if (options && options.includeSubcategories) {
+      data.include_subcategories = true;
+    }
+
     if (categoryId) data.category_id = categoryId;
     return ajax("/topics/bulk", {
       type: "PUT",
diff --git a/app/assets/javascripts/discourse/routes/discovery.js.es6 b/app/assets/javascripts/discourse/routes/discovery.js.es6
index 3292656..234ef15 100644
--- a/app/assets/javascripts/discourse/routes/discovery.js.es6
+++ b/app/assets/javascripts/discourse/routes/discovery.js.es6
@@ -60,12 +60,15 @@ export default Discourse.Route.extend(OpenComposer, {
     },
 
     dismissReadTopics(dismissTopics) {
-      var operationType = dismissTopics ? "topics" : "posts";
-      this.controllerFor("discovery/topics").send("dismissRead", operationType);
+      const operationType = dismissTopics ? "topics" : "posts";
+      this.send("dismissRead", operationType);
     },
 
     dismissRead(operationType) {
-      this.controllerFor("discovery/topics").send("dismissRead", operationType);
+      const controller = this.controllerFor("discovery/topics");
+      controller.send("dismissRead", operationType, {
+        includeSubcategories: !controller.noSubcategories
+      });
     }
   }
 });
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index f578291..1535359 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -779,7 +779,17 @@ class TopicsController < ApplicationController
     elsif params[:filter] == 'unread'
       tq = TopicQuery.new(current_user)
       topics = TopicQuery.unread_filter(tq.joined_topic_user, current_user.id, staff: guardian.is_staff?).listable_topics
-      topics = topics.where('category_id = ?', params[:category_id]) if params[:category_id]
+
+      if params[:category_id]
+        if params[:include_subcategories]
+          topics = topics.where(<<~SQL, category_id: params[:category_id])
+            category_id in (select id FROM categories WHERE parent_category_id = :category_id) OR
+            category_id = :category_id
+          SQL
+        else
+          topics = topics.where('category_id = ?', params[:category_id])
+        end
+      end
       topic_ids = topics.pluck(:id)
     else
       raise ActionController::ParameterMissing.new(:topic_ids)
diff --git a/lib/topics_bulk_action.rb b/lib/topics_bulk_action.rb
index 207f2f0..a26dc0e 100644
--- a/lib/topics_bulk_action.rb
+++ b/lib/topics_bulk_action.rb
@@ -68,12 +68,12 @@ class TopicsBulkAction
   end
 
   def dismiss_posts
-    sql = "
-    UPDATE topic_users tu
-    SET highest_seen_post_number = t.highest_post_number , last_read_post_number = highest_post_number
-    FROM topics t
-    WHERE t.id = tu.topic_id AND tu.user_id = :user_id AND t.id IN (:topic_ids)
-    "
+    sql = <<~SQL
+      UPDATE topic_users tu
+      SET highest_seen_post_number = t.highest_post_number , last_read_post_number = highest_post_number
+      FROM topics t
+      WHERE t.id = tu.topic_id AND tu.user_id = :user_id AND t.id IN (:topic_ids)
+    SQL
 
     DB.exec(sql, user_id: @user.id, topic_ids: @topic_ids)
     @changed_ids.concat @topic_ids
diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb
index 6aac3a9..758bda7 100644
--- a/spec/requests/topics_controller_spec.rb
+++ b/spec/requests/topics_controller_spec.rb
@@ -2039,6 +2039,27 @@ RSpec.describe TopicsController do
         expect(response.status).to eq(400)
       end
 
+      it "can mark sub-categories unread" do
+        # TODO do we want to skip category definition by default in fabricator
+        category = Fabricate(:category, skip_category_definition: true)
+        sub = Fabricate(:category, parent_category_id: category.id, skip_category_definition: true)
+
+        topic.update!(category_id: sub.id)
+
+        post1 = create_post(user: user, topic_id: topic.id)
+        create_post(topic_id: topic.id)
+
+        put "/topics/bulk.json", params: {
+          category_id: category.id,
+          include_subcategories: true,
+          filter: 'unread',
+          operation: { type: 'dismiss_posts' }
+        }
+
+        expect(response.status).to eq(200)
+        expect(TopicUser.get(post1.topic, post1.user).last_read_post_number).to eq(2)
+      end
+
       it "can find unread" do
         # mark all unread muted
         put "/topics/bulk.json", params: {

GitHub sha: 5bc5c02a

1 Like