diff --git a/app/assets/javascripts/discourse/app/components/sidebar/topics-section.js b/app/assets/javascripts/discourse/app/components/sidebar/topics-section.js index 31a19ebc1fe..d96b7229763 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/topics-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/topics-section.js @@ -17,15 +17,17 @@ const DEFAULT_SECTION_LINKS = [ ]; export default class SidebarTopicsSection extends GlimmerComponent { - get sectionLinks() { - return [...DEFAULT_SECTION_LINKS, ...customSectionLinks].map( - (sectionLinkClass) => { - return new sectionLinkClass({ - topicTrackingState: this.topicTrackingState, - currentUser: this.currentUser, - }); - } - ); + configuredSectionLinks = [...DEFAULT_SECTION_LINKS, ...customSectionLinks]; + + sectionLinks = this.configuredSectionLinks.map((sectionLinkClass) => { + return new sectionLinkClass({ + topicTrackingState: this.topicTrackingState, + currentUser: this.currentUser, + }); + }); + + willDestroy() { + this.sectionLinks.forEach((sectionLink) => sectionLink.teardown()); } @action diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/base-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/base-section-link.js index 3712d0a5659..d62ab343518 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/base-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/base-section-link.js @@ -7,6 +7,11 @@ export default class BaseSectionLink { this.currentUser = currentUser; } + /** + * Called when topics-section component is torn down. + */ + teardown() {} + /** * @returns {string} The name of the section link. Needs to be dasherized and lowercase. */ diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/everything-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/everything-section-link.js index 261364a76e4..8192a75f688 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/everything-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/everything-section-link.js @@ -1,44 +1,35 @@ import I18n from "I18n"; import { tracked } from "@glimmer/tracking"; - -import discourseDebounce from "discourse-common/lib/debounce"; +import { bind } from "discourse-common/utils/decorators"; import BaseSectionLink from "discourse/lib/sidebar/topics-section/base-section-link"; export default class EverythingSectionLink extends BaseSectionLink { @tracked totalUnread = 0; @tracked totalNew = 0; + callbackId = null; constructor() { super(...arguments); - this._refreshCounts(); - - this.topicTrackingState.onStateChange( - this._topicTrackingStateUpdated.bind(this) + this.callbackId = this.topicTrackingState.onStateChange( + this._refreshCounts ); + + this._refreshCounts(); } - _topicTrackingStateUpdated() { - // refreshing section counts by looping through the states in topicTrackingState is an expensive operation so - // we debounce this. - discourseDebounce(this, this._refreshCounts, 100); + teardown() { + this.topicTrackingState.offStateChange(this.callbackId); } + @bind _refreshCounts() { - let totalUnread = 0; - let totalNew = 0; + this.totalUnread = this.topicTrackingState.countUnread(); - this.topicTrackingState.forEachTracked((topic, isNew, isUnread) => { - if (isNew) { - totalNew += 1; - } else if (isUnread) { - totalUnread += 1; - } - }); - - this.totalUnread = totalUnread; - this.totalNew = totalNew; + if (this.totalUnread === 0) { + this.totalNew = this.topicTrackingState.countNew(); + } } get name() { diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/tracked-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/tracked-section-link.js index 4822162fca0..997903c91a7 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/tracked-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/topics-section/tracked-section-link.js @@ -1,14 +1,43 @@ import I18n from "I18n"; +import { tracked } from "@glimmer/tracking"; +import { bind } from "discourse-common/utils/decorators"; import BaseSectionLink from "discourse/lib/sidebar/topics-section/base-section-link"; +import { isTrackedTopic } from "discourse/lib/topic-list-tracked-filter"; export default class TrackedSectionLink extends BaseSectionLink { - get name() { - return "tracked"; + @tracked totalUnread = 0; + @tracked totalNew = 0; + callbackId = null; + + constructor() { + super(...arguments); + + this.callbackId = this.topicTrackingState.onStateChange( + this._refreshCounts + ); + this._refreshCounts(); } - get route() { - return "discovery.latest"; + teardown() { + this.topicTrackingState.offStateChange(this.callbackId); + } + + @bind + _refreshCounts() { + this.totalUnread = this.topicTrackingState.countUnread({ + customFilterFn: isTrackedTopic, + }); + + if (this.totalUnread === 0) { + this.totalNew = this.topicTrackingState.countNew({ + customFilterFn: isTrackedTopic, + }); + } + } + + get name() { + return "tracked"; } get query() { @@ -22,4 +51,32 @@ export default class TrackedSectionLink extends BaseSectionLink { get text() { return I18n.t("sidebar.sections.topics.links.tracked.content"); } + + get currentWhen() { + return "discovery.latest discovery.new discovery.unread discovery.top"; + } + + get badgeText() { + if (this.totalUnread > 0) { + return I18n.t("sidebar.unread_count", { + count: this.totalUnread, + }); + } else if (this.totalNew > 0) { + return I18n.t("sidebar.new_count", { + count: this.totalNew, + }); + } else { + return; + } + } + + get route() { + if (this.totalUnread > 0) { + return "discovery.unread"; + } else if (this.totalNew > 0) { + return "discovery.new"; + } else { + return "discovery.latest"; + } + } } diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-topics-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-topics-section-test.js index d518a5a5d30..49770538f96 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-topics-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-topics-section-test.js @@ -13,9 +13,16 @@ import { isLegacyEmber } from "discourse-common/config/environment"; import topicFixtures from "discourse/tests/fixtures/discovery-fixtures"; import { cloneJSON } from "discourse-common/lib/object"; import { withPluginApi } from "discourse/lib/plugin-api"; +import Site from "discourse/models/site"; +import { NotificationLevels } from "discourse/lib/notification-levels"; acceptance("Sidebar - Topics Section", function (needs) { - needs.user({ experimental_sidebar_enabled: true }); + needs.user({ + experimental_sidebar_enabled: true, + tracked_tags: ["tag1"], + watched_tags: ["tag2"], + watching_first_post_tags: ["tag3"], + }); needs.pretender((server, helper) => { server.get("/new.json", () => { @@ -322,7 +329,7 @@ acceptance("Sidebar - Topics Section", function (needs) { assert.ok( query(".sidebar-section-link-everything").href.endsWith("/unread"), - "is links to unread filter" + "it links to unread filter" ); // simulate reading topic 2 @@ -380,7 +387,7 @@ acceptance("Sidebar - Topics Section", function (needs) { assert.ok( query(".sidebar-section-link-everything").href.endsWith("/new"), - "is links to new filter" + "it links to new filter" ); publishToMessageBus("/unread", { @@ -404,7 +411,242 @@ acceptance("Sidebar - Topics Section", function (needs) { assert.ok( query(".sidebar-section-link-everything").href.endsWith("/latest"), - "is links to latest filter" + "it links to latest filter" + ); + } + ); + + conditionalTest( + "visiting top route with tracked filter", + !isLegacyEmber(), + async function (assert) { + await visit("/top?f=tracked"); + + assert.strictEqual( + queryAll(".sidebar-section-topics .sidebar-section-link.active").length, + 1, + "only one link is marked as active" + ); + + assert.ok( + exists(".sidebar-section-topics .sidebar-section-link-tracked.active"), + "the tracked link is marked as active" + ); + } + ); + + conditionalTest( + "visiting unread route with tracked filter", + !isLegacyEmber(), + async function (assert) { + await visit("/unread?f=tracked"); + + assert.strictEqual( + queryAll(".sidebar-section-topics .sidebar-section-link.active").length, + 1, + "only one link is marked as active" + ); + + assert.ok( + exists(".sidebar-section-topics .sidebar-section-link-tracked.active"), + "the tracked link is marked as active" + ); + } + ); + + conditionalTest( + "visiting new route with tracked filter", + !isLegacyEmber(), + async function (assert) { + await visit("/new?f=tracked"); + + assert.strictEqual( + queryAll(".sidebar-section-topics .sidebar-section-link.active").length, + 1, + "only one link is marked as active" + ); + + assert.ok( + exists(".sidebar-section-topics .sidebar-section-link-tracked.active"), + "the tracked link is marked as active" + ); + } + ); + + conditionalTest( + "new and unread count for tracked link", + !isLegacyEmber(), + 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( + ".sidebar-section-link-tracked .sidebar-section-link-content-badge" + ).textContent.trim(), + "3 unread", + "it displays the right unread count" + ); + + assert.ok( + query(".sidebar-section-link-tracked").href.endsWith( + "/unread?f=tracked" + ), + "it links to unread url with tracked filter" + ); + + // simulate reading topic id 2 + publishToMessageBus("/unread", { + topic_id: 2, + message_type: "read", + payload: { + last_read_post_number: 12, + highest_post_number: 12, + }, + }); + + await settled(); + + assert.strictEqual( + query( + ".sidebar-section-link-tracked .sidebar-section-link-content-badge" + ).textContent.trim(), + "2 unread", + "it updates the unread count" + ); + + // simulate reading topic id 3 + publishToMessageBus("/unread", { + topic_id: 3, + message_type: "read", + payload: { + last_read_post_number: 17, + highest_post_number: 17, + }, + }); + + // simulate reading topic id 6 + publishToMessageBus("/unread", { + topic_id: 6, + message_type: "read", + payload: { + last_read_post_number: 17, + highest_post_number: 17, + }, + }); + + assert.strictEqual( + query( + ".sidebar-section-link-tracked .sidebar-section-link-content-badge" + ).textContent.trim(), + "1 new", + "it displays the new count once there are no tracked unread topics" + ); + + assert.ok( + query(".sidebar-section-link-tracked").href.endsWith("/new?f=tracked"), + "it links to new url with tracked filter" + ); + + // simulate reading topic id 1 + publishToMessageBus("/unread", { + topic_id: 1, + message_type: "read", + payload: { + last_read_post_number: 1, + highest_post_number: 1, + }, + }); + + await settled(); + + assert.ok( + !exists( + ".sidebar-section-link-tracked .sidebar-section-link-content-badge" + ), + "it removes new count once there are no tracked new topics" + ); + + assert.ok( + query(".sidebar-section-link-tracked").href.endsWith( + "/latest?f=tracked" + ), + "it links to latest url with tracked filter" ); } ); @@ -494,4 +736,28 @@ acceptance("Sidebar - Topics Section", function (needs) { ); } ); + + conditionalTest( + "clean up topic tracking state state changed callbacks when section is destroyed", + !isLegacyEmber(), + async function (assert) { + await visit("/"); + + const topicTrackingState = this.container.lookup( + "topic-tracking-state:main" + ); + + const initialCallbackCount = Object.keys( + topicTrackingState.stateChangeCallbacks + ).length; + + await click(".header-sidebar-toggle .btn"); + await click(".header-sidebar-toggle .btn"); + + assert.strictEqual( + Object.keys(topicTrackingState.stateChangeCallbacks).length, + initialCallbackCount + ); + } + ); });