From 1e9f132b1588cc9ee415113012dbcc867fb9efaf Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Wed, 1 Jun 2022 14:54:42 +0800 Subject: [PATCH] FIX: Topic list nav items count not respecting tracked filter. (#16935) This commit seeks to only handle the `f=tracked` and `filter=tracked` query params for a topic list. There are other "hidden" filters for a topic list which can be activated by passing the right query param to the request. However, they are hidden because there is no way to activate those filters via the UI. We are handling the `f=tracked` filter because we will soon be adding a link that allows a user to quickly view their tracked topics. --- .../app/components/navigation-item.js | 1 + .../app/lib/topic-list-tracked-filter.js | 58 ++++ .../discourse/app/models/category.js | 4 +- .../discourse/app/models/nav-item.js | 18 +- .../javascripts/discourse/app/models/site.js | 14 + .../app/models/topic-tracking-state.js | 109 +++++-- .../javascripts/discourse/app/models/user.js | 9 + .../discourse/app/widgets/hamburger-menu.js | 2 +- .../topic-discovery-tracked-test.js | 307 ++++++++++++++++++ .../discourse/tests/fixtures/site-fixtures.js | 52 +++ .../select-kit/category-chooser-test.js | 2 +- .../unit/models/topic-tracking-state-test.js | 54 ++- lib/topic_query.rb | 3 + 13 files changed, 582 insertions(+), 51 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/lib/topic-list-tracked-filter.js create mode 100644 app/assets/javascripts/discourse/tests/acceptance/topic-discovery-tracked-test.js diff --git a/app/assets/javascripts/discourse/app/components/navigation-item.js b/app/assets/javascripts/discourse/app/components/navigation-item.js index 29b95a405c7..539494b225a 100644 --- a/app/assets/javascripts/discourse/app/components/navigation-item.js +++ b/app/assets/javascripts/discourse/app/components/navigation-item.js @@ -9,6 +9,7 @@ export default Component.extend(FilterModeMixin, { "content.hasIcon:has-icon", "content.classNames", "isHidden:hidden", + "content.name", ], attributeBindings: ["content.title:title"], hidden: false, diff --git a/app/assets/javascripts/discourse/app/lib/topic-list-tracked-filter.js b/app/assets/javascripts/discourse/app/lib/topic-list-tracked-filter.js new file mode 100644 index 00000000000..446fbabf0ed --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/topic-list-tracked-filter.js @@ -0,0 +1,58 @@ +import Site from "discourse/models/site"; +import User from "discourse/models/user"; + +export function hasTrackedFilter(queryParams) { + if (!queryParams) { + return false; + } + + return queryParams.f === "tracked" || queryParams.filter === "tracked"; +} + +/** + * Logic here needs to be in sync with `TopicQuery#tracked_filter` on the server side. See `TopicQuery#tracked_filter` + * for the rational behind this decision. + */ +export function isTrackedTopic(topic) { + if (topic.category_id) { + const categories = Site.current().trackedCategoriesList; + + for (const category of categories) { + if (category.id === topic.category_id) { + return true; + } + + if ( + category.subcategories && + category.subcategories.some((subCategory) => { + if (subCategory.id === topic.category_id) { + return true; + } + + if ( + subCategory.subcategories && + subCategory.subcategories.some((c) => { + return c.id === topic.category_id; + }) + ) { + return true; + } + }) + ) { + return true; + } + } + } + + if (topic.tags) { + const tags = User.current().trackedTags; + + for (const tag of tags) { + if (topic.tags.indexOf(tag) > -1) { + return true; + } + } + } + + return false; +} diff --git a/app/assets/javascripts/discourse/app/models/category.js b/app/assets/javascripts/discourse/app/models/category.js index e4435bd8b1a..6e6f644314b 100644 --- a/app/assets/javascripts/discourse/app/models/category.js +++ b/app/assets/javascripts/discourse/app/models/category.js @@ -298,12 +298,12 @@ const Category = RestModel.extend({ @discourseComputed("id", "topicTrackingState.messageCount") unreadTopics(id) { - return this.topicTrackingState.countUnread(id); + return this.topicTrackingState.countUnread({ categoryId: id }); }, @discourseComputed("id", "topicTrackingState.messageCount") newTopics(id) { - return this.topicTrackingState.countNew(id); + return this.topicTrackingState.countNew({ categoryId: id }); }, setNotification(notification_level) { diff --git a/app/assets/javascripts/discourse/app/models/nav-item.js b/app/assets/javascripts/discourse/app/models/nav-item.js index f1f1d574b81..c61cd7be07b 100644 --- a/app/assets/javascripts/discourse/app/models/nav-item.js +++ b/app/assets/javascripts/discourse/app/models/nav-item.js @@ -8,6 +8,10 @@ import deprecated from "discourse-common/lib/deprecated"; import discourseComputed from "discourse-common/utils/decorators"; import { emojiUnescape } from "discourse/lib/text"; import { getOwner } from "discourse-common/lib/get-owner"; +import { + hasTrackedFilter, + isTrackedTopic, +} from "discourse/lib/topic-list-tracked-filter"; import getURL from "discourse-common/lib/get-url"; import { reads } from "@ember/object/computed"; @@ -76,12 +80,22 @@ const NavItem = EmberObject.extend({ "category", "tagId", "noSubcategories", + "currentRouteQueryParams", "topicTrackingState.messageCount" ) - count(name, category, tagId, noSubcategories) { + count(name, category, tagId, noSubcategories, currentRouteQueryParams) { const state = this.topicTrackingState; + if (state) { - return state.lookupCount(name, category, tagId, noSubcategories); + return state.lookupCount({ + type: name, + category, + tagId, + noSubcategories, + customFilterFn: hasTrackedFilter(currentRouteQueryParams) + ? isTrackedTopic + : undefined, + }); } }, }); diff --git a/app/assets/javascripts/discourse/app/models/site.js b/app/assets/javascripts/discourse/app/models/site.js index 72666a3623b..343ed0913bd 100644 --- a/app/assets/javascripts/discourse/app/models/site.js +++ b/app/assets/javascripts/discourse/app/models/site.js @@ -11,6 +11,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import { getOwner } from "discourse-common/lib/get-owner"; import { isEmpty } from "@ember/utils"; import { htmlSafe } from "@ember/template"; +import { NotificationLevels } from "discourse/lib/notification-levels"; const Site = RestModel.extend({ isReadOnly: alias("is_readonly"), @@ -83,6 +84,19 @@ const Site = RestModel.extend({ : this.sortedCategories; }, + @discourseComputed("categories.[]") + trackedCategoriesList(categories) { + const trackedCategories = []; + + for (const category of categories) { + if (category.notification_level >= NotificationLevels.TRACKING) { + trackedCategories.push(category); + } + } + + return trackedCategories; + }, + postActionTypeById(id) { return this.get("postActionByIdLookup.action" + id); }, diff --git a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js index 15a5a94241c..109bd65707e 100644 --- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js @@ -476,7 +476,13 @@ const TopicTrackingState = EmberObject.extend({ return new Set(result); }, - countCategoryByState(type, categoryId, tagId, noSubcategories) { + countCategoryByState({ + type, + categoryId, + tagId, + noSubcategories, + customFilterFn, + }) { const subcategoryIds = noSubcategories ? new Set([categoryId]) : this.getSubCategoryIds(categoryId); @@ -486,28 +492,53 @@ const TopicTrackingState = EmberObject.extend({ ); let filterFn = type === "new" ? isNew : isUnread; - return Array.from(this.states.values()).filter( - (topic) => - filterFn(topic) && - (!categoryId || subcategoryIds.has(topic.category_id)) && - (!tagId || (topic.tags && topic.tags.indexOf(tagId) > -1)) && - (type !== "new" || - !mutedCategoryIds || - mutedCategoryIds.indexOf(topic.category_id) === -1) - ).length; + return Array.from(this.states.values()).filter((topic) => { + if (!filterFn(topic)) { + return false; + } + + if (categoryId && !subcategoryIds.has(topic.category_id)) { + return false; + } + + if (tagId && !(topic.tags && topic.tags.indexOf(tagId) > -1)) { + return false; + } + + if ( + type === "new" && + mutedCategoryIds && + mutedCategoryIds.indexOf(topic.category_id) !== -1 + ) { + return false; + } + + if (customFilterFn && !customFilterFn.call(this, topic)) { + return false; + } + + return true; + }).length; }, - countNew(categoryId, tagId, noSubcategories) { - return this.countCategoryByState("new", categoryId, tagId, noSubcategories); - }, - - countUnread(categoryId, tagId, noSubcategories) { - return this.countCategoryByState( - "unread", + countNew({ categoryId, tagId, noSubcategories, customFilterFn } = {}) { + return this.countCategoryByState({ + type: "new", categoryId, tagId, - noSubcategories - ); + noSubcategories, + customFilterFn, + }); + }, + + countUnread({ categoryId, tagId, noSubcategories, customFilterFn } = {}) { + return this.countCategoryByState({ + type: "unread", + categoryId, + tagId, + noSubcategories, + customFilterFn, + }); }, /** @@ -597,22 +628,44 @@ const TopicTrackingState = EmberObject.extend({ return sum; }, - lookupCount(name, category, tagId, noSubcategories) { - if (name === "latest") { + lookupCount({ type, category, tagId, noSubcategories, customFilterFn } = {}) { + if (type === "latest") { return ( - this.lookupCount("new", category, tagId, noSubcategories) + - this.lookupCount("unread", category, tagId, noSubcategories) + this.lookupCount({ + type: "new", + category, + tagId, + noSubcategories, + customFilterFn, + }) + + this.lookupCount({ + type: "unread", + category, + tagId, + noSubcategories, + customFilterFn, + }) ); } let categoryId = category ? get(category, "id") : null; - if (name === "new") { - return this.countNew(categoryId, tagId, noSubcategories); - } else if (name === "unread") { - return this.countUnread(categoryId, tagId, noSubcategories); + if (type === "new") { + return this.countNew({ + categoryId, + tagId, + noSubcategories, + customFilterFn, + }); + } else if (type === "unread") { + return this.countUnread({ + categoryId, + tagId, + noSubcategories, + customFilterFn, + }); } else { - const categoryName = name.split("/")[1]; + const categoryName = type.split("/")[1]; if (categoryName) { return this.countCategory(categoryId, tagId); } diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js index 646ac080051..6cc4116b2ea 100644 --- a/app/assets/javascripts/discourse/app/models/user.js +++ b/app/assets/javascripts/discourse/app/models/user.js @@ -1008,6 +1008,15 @@ const User = RestModel.extend({ new Date(this.do_not_disturb_until) >= new Date() ); }, + + @discourseComputed( + "tracked_tags.[]", + "watched_tags.[]", + "watching_first_post_tags.[]" + ) + trackedTags(trackedTags, watchedTags, watchingFirstPostTags) { + return [...trackedTags, ...watchedTags, ...watchingFirstPostTags]; + }, }); User.reopenClass(Singleton, { diff --git a/app/assets/javascripts/discourse/app/widgets/hamburger-menu.js b/app/assets/javascripts/discourse/app/widgets/hamburger-menu.js index 1e65119e400..f97ea37fd57 100644 --- a/app/assets/javascripts/discourse/app/widgets/hamburger-menu.js +++ b/app/assets/javascripts/discourse/app/widgets/hamburger-menu.js @@ -96,7 +96,7 @@ export default createWidget("hamburger-menu", { lookupCount(type) { const tts = this.register.lookup("topic-tracking-state:main"); - return tts ? tts.lookupCount(type) : 0; + return tts ? tts.lookupCount({ type }) : 0; }, generalLinks() { diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-tracked-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-tracked-test.js new file mode 100644 index 00000000000..8f91d708f7b --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-discovery-tracked-test.js @@ -0,0 +1,307 @@ +import { settled, visit } from "@ember/test-helpers"; +import I18n from "I18n"; +import { test } from "qunit"; + +import { + acceptance, + publishToMessageBus, + query, +} from "discourse/tests/helpers/qunit-helpers"; +import Site from "discourse/models/site"; +import { NotificationLevels } from "discourse/lib/notification-levels"; +import topicFixtures from "discourse/tests/fixtures/discovery-fixtures"; +import { cloneJSON } from "discourse-common/lib/object"; + +acceptance("Topic Discovery Tracked", function (needs) { + needs.user({ + tracked_tags: ["tag1"], + watched_tags: ["tag2"], + watching_first_post_tags: ["tag3"], + }); + + needs.pretender((server, helper) => { + server.get("/c/:category-slug/:category-id/l/latest.json", () => { + return helper.response(cloneJSON(topicFixtures["/latest.json"])); + }); + + server.get("/tag/:tag_name/notifications", () => { + return helper.response({ + tag_notification: { + id: "test", + notification_level: NotificationLevels.TRACKING, + }, + }); + }); + + server.get("/tag/:tag_name/l/latest.json", () => { + return helper.response(cloneJSON(topicFixtures["/latest.json"])); + }); + }); + + test("visit discovery pages with tracked filter", async function (assert) { + const categories = Site.current().categories; + + // Category id 1001 has two subcategories + const category = categories.find((c) => c.id === 1001); + category.set("notification_level", NotificationLevels.TRACKING); + + this.container.lookup("topic-tracking-state:main").loadStates([ + { + topic_id: 1, + highest_post_number: 1, + last_read_post_number: null, + created_at: "2022-05-11T03:09:31.959Z", + category_id: category.id, + notification_level: NotificationLevels.TRACKING, + created_in_new_period: true, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + }, + { + topic_id: 2, + highest_post_number: 12, + last_read_post_number: 11, + created_at: "2020-02-09T09:40:02.672Z", + category_id: category.subcategories[0].id, + notification_level: NotificationLevels.TRACKING, + created_in_new_period: false, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + }, + { + topic_id: 3, + highest_post_number: 12, + last_read_post_number: 11, + created_at: "2020-02-09T09:40:02.672Z", + category_id: category.subcategories[0].subcategories[0].id, + notification_level: NotificationLevels.TRACKING, + created_in_new_period: false, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + }, + { + topic_id: 4, + highest_post_number: 15, + last_read_post_number: 14, + created_at: "2021-06-14T12:41:02.477Z", + category_id: 3, + notification_level: NotificationLevels.TRACKING, + created_in_new_period: false, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + }, + { + topic_id: 5, + highest_post_number: 1, + last_read_post_number: null, + created_at: "2021-06-14T12:41:02.477Z", + category_id: 3, + notification_level: null, + created_in_new_period: true, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + }, + { + topic_id: 6, + highest_post_number: 17, + last_read_post_number: 16, + created_at: "2020-10-31T03:41:42.257Z", + category_id: 1234, + notification_level: NotificationLevels.TRACKING, + created_in_new_period: false, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + tags: ["tag3"], + }, + ]); + + await visit("/"); + + assert.strictEqual( + query("#navigation-bar li.unread").textContent.trim(), + I18n.t("filters.unread.title_with_count", { count: 4 }), + "displays the right content on unread link" + ); + + assert.strictEqual( + query("#navigation-bar li.new").textContent.trim(), + I18n.t("filters.new.title_with_count", { count: 2 }), + "displays the right content on new link" + ); + + await visit("/?f=tracked"); + + assert.strictEqual( + query("#navigation-bar li.unread").textContent.trim(), + I18n.t("filters.unread.title_with_count", { count: 3 }), + "displays the right content on unread link" + ); + + assert.strictEqual( + query("#navigation-bar li.new").textContent.trim(), + I18n.t("filters.new.title_with_count", { count: 1 }), + "displays the right content on new link" + ); + + // simulate reading topic id 1 + publishToMessageBus("/unread", { + topic_id: 1, + message_type: "read", + payload: { + last_read_post_number: 1, + highest_post_number: 1, + }, + }); + + // simulate reading topic id 3 + publishToMessageBus("/unread", { + topic_id: 3, + message_type: "read", + payload: { + last_read_post_number: 12, + highest_post_number: 12, + }, + }); + + await settled(); + + assert.strictEqual( + query("#navigation-bar li.unread").textContent.trim(), + I18n.t("filters.unread.title_with_count", { count: 2 }), + "displays the right content on unread link" + ); + + assert.strictEqual( + query("#navigation-bar li.new").textContent.trim(), + I18n.t("filters.new.title"), + "displays the right content on new link" + ); + }); + + test("visit discovery page filtered by tag with tracked filter", async function (assert) { + this.container.lookup("topic-tracking-state:main").loadStates([ + { + topic_id: 1, + highest_post_number: 1, + last_read_post_number: null, + created_at: "2022-05-11T03:09:31.959Z", + category_id: 1, + notification_level: null, + created_in_new_period: true, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + tags: ["someothertag"], + }, + ]); + + await visit("/tag/someothertag"); + + assert.strictEqual( + query("#navigation-bar li.unread").textContent.trim(), + I18n.t("filters.unread.title"), + "displays the right content on unread link" + ); + + assert.strictEqual( + query("#navigation-bar li.new").textContent.trim(), + I18n.t("filters.new.title_with_count", { count: 1 }), + "displays the right content on new link" + ); + + await visit("/tag/someothertag?f=tracked"); + + assert.strictEqual( + query("#navigation-bar li.unread").textContent.trim(), + I18n.t("filters.unread.title"), + "displays the right content on unread link" + ); + + assert.strictEqual( + query("#navigation-bar li.new").textContent.trim(), + I18n.t("filters.new.title"), + "displays the right content on new link" + ); + }); + + test("visit discovery page filtered by category with tracked filter", async function (assert) { + const categories = Site.current().categories; + const category = categories.at(-1); + category.set("notification_level", NotificationLevels.TRACKING); + + this.container.lookup("topic-tracking-state:main").loadStates([ + { + topic_id: 1, + highest_post_number: 1, + last_read_post_number: null, + created_at: "2022-05-11T03:09:31.959Z", + category_id: category.id, + notification_level: null, + created_in_new_period: true, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + }, + { + topic_id: 2, + highest_post_number: 12, + last_read_post_number: 11, + created_at: "2020-02-09T09:40:02.672Z", + category_id: category.id, + notification_level: NotificationLevels.TRACKING, + created_in_new_period: false, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + }, + { + topic_id: 3, + highest_post_number: 15, + last_read_post_number: 14, + created_at: "2021-06-14T12:41:02.477Z", + category_id: 3, + notification_level: NotificationLevels.TRACKING, + created_in_new_period: false, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + }, + { + topic_id: 4, + highest_post_number: 17, + last_read_post_number: 16, + created_at: "2020-10-31T03:41:42.257Z", + category_id: 1234, + notification_level: NotificationLevels.TRACKING, + created_in_new_period: false, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + tags: ["tag3"], + }, + ]); + + await visit(`/c/3`); + + assert.strictEqual( + query("#navigation-bar li.unread").textContent.trim(), + I18n.t("filters.unread.title_with_count", { count: 1 }), + "displays the right content on unread link" + ); + + assert.strictEqual( + query("#navigation-bar li.new").textContent.trim(), + I18n.t("filters.new.title"), + "displays the right content on new link" + ); + + await visit(`/c/3?f=tracked`); + + assert.strictEqual( + query("#navigation-bar li.unread").textContent.trim(), + I18n.t("filters.unread.title"), + "displays the right content on unread link" + ); + + assert.strictEqual( + query("#navigation-bar li.new").textContent.trim(), + I18n.t("filters.new.title"), + "displays the right content on new link" + ); + }); +}); diff --git a/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js b/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js index cfc12850afe..fba47e9f929 100644 --- a/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js +++ b/app/assets/javascripts/discourse/tests/fixtures/site-fixtures.js @@ -500,6 +500,58 @@ export default { default_view: "latest", subcategory_list_style: "boxes", }, + { + id: 1001, + name: "Parent Category", + color: "27AA5B", + text_color: "FFFFFF", + slug: "parent-category", + topic_count: 95, + post_count: 827, + description: "some description", + description_text: "some description", + topic_url: "/t/some-url", + read_restricted: false, + permission: 1, + parent_category_id: null, + notification_level: null, + background_url: null, + }, + { + id: 1002, + name: "Sub Category", + color: "27AA5B", + text_color: "FFFFFF", + slug: "sub-category", + topic_count: 95, + post_count: 827, + description: "some description", + description_text: "some description", + topic_url: "/t/some-url", + read_restricted: false, + permission: 1, + parent_category_id: 1001, + notification_level: null, + background_url: null, + }, + { + id: 1003, + name: "Sub Sub Category", + color: "27AA5B", + text_color: "FFFFFF", + slug: "sub-sub-category", + topic_count: 95, + post_count: 827, + description: "some description", + description_text: "some description", + topic_url: "/t/some-url", + read_restricted: false, + permission: 1, + parent_category_id: 1002, + notification_level: null, + background_url: null, + }, + ], post_action_types: [ { diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js index 8ae75bbd32f..aff1ac24273 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/category-chooser-test.js @@ -107,7 +107,7 @@ discourseModule( assert.strictEqual( this.subject.rows().length, - 22, + 25, "all categories are visible" ); diff --git a/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js b/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js index bea13230ff4..5978f449e36 100644 --- a/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js +++ b/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js @@ -1025,9 +1025,9 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) { const trackingState = TopicTrackingState.create({ currentUser }); - assert.strictEqual(trackingState.countNew(1), 0); - assert.strictEqual(trackingState.countNew(2), 0); - assert.strictEqual(trackingState.countNew(3), 0); + assert.strictEqual(trackingState.countNew({ categoryId: 1 }), 0); + assert.strictEqual(trackingState.countNew({ categoryId: 2 }), 0); + assert.strictEqual(trackingState.countNew({ categoryId: 3 }), 0); trackingState.states.set("t112", { last_read_post_number: null, @@ -1037,11 +1037,18 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) { created_in_new_period: true, }); - assert.strictEqual(trackingState.countNew(1), 1); - assert.strictEqual(trackingState.countNew(1, undefined, true), 0); - assert.strictEqual(trackingState.countNew(1, "missing-tag"), 0); - assert.strictEqual(trackingState.countNew(2), 1); - assert.strictEqual(trackingState.countNew(3), 0); + assert.strictEqual(trackingState.countNew({ categoryId: 1 }), 1); + + assert.strictEqual( + trackingState.countNew({ categoryId: 1, noSubcategories: true }), + 0 + ); + assert.strictEqual( + trackingState.countNew({ categoryId: 1, tagId: "missing-tag" }), + 0 + ); + assert.strictEqual(trackingState.countNew({ categoryId: 2 }), 1); + assert.strictEqual(trackingState.countNew({ categoryId: 3 }), 0); trackingState.states.set("t113", { last_read_post_number: null, @@ -1052,11 +1059,23 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) { created_in_new_period: true, }); - assert.strictEqual(trackingState.countNew(1), 2); - assert.strictEqual(trackingState.countNew(2), 2); - assert.strictEqual(trackingState.countNew(3), 1); - assert.strictEqual(trackingState.countNew(3, "amazing"), 1); - assert.strictEqual(trackingState.countNew(3, "missing"), 0); + assert.strictEqual(trackingState.countNew({ categoryId: 1 }), 2); + assert.strictEqual(trackingState.countNew({ categoryId: 2 }), 2); + assert.strictEqual(trackingState.countNew({ categoryId: 3 }), 1); + assert.strictEqual( + trackingState.countNew({ + categoryId: 3, + tagId: "amazing", + }), + 1 + ); + assert.strictEqual( + trackingState.countNew({ + categoryId: 3, + tagId: "missing", + }), + 0 + ); trackingState.states.set("t111", { last_read_post_number: null, @@ -1066,16 +1085,17 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) { created_in_new_period: true, }); - assert.strictEqual(trackingState.countNew(1), 3); - assert.strictEqual(trackingState.countNew(2), 2); - assert.strictEqual(trackingState.countNew(3), 1); + assert.strictEqual(trackingState.countNew({ categoryId: 1 }), 3); + assert.strictEqual(trackingState.countNew({ categoryId: 2 }), 2); + assert.strictEqual(trackingState.countNew({ categoryId: 3 }), 1); trackingState.states.set("t115", { last_read_post_number: null, id: 115, category_id: 4, }); - assert.strictEqual(trackingState.countNew(4), 0); + + assert.strictEqual(trackingState.countNew({ categoryId: 4 }), 0); }); test("mute and unmute topic", function (assert) { diff --git a/lib/topic_query.rb b/lib/topic_query.rb index f52c6977469..134e50fb607 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -345,6 +345,9 @@ class TopicQuery regular: TopicUser.notification_levels[:regular], tracking: TopicUser.notification_levels[:tracking]) end + # Any changes here will need to be reflected in `lib/topic-list-tracked-filter.js` for the `isTrackedTopic` function on + # the client side. The `f=tracked` query param is not heavily used so we do not want to be querying for a topic's + # tracked status by default. Instead, the client will handle the filtering when the `f=tracked` query params is present. def self.tracked_filter(list, user_id) tracked_category_ids_sql = <<~SQL SELECT cd.category_id FROM category_users cd