diff --git a/app/assets/javascripts/discourse/app/components/sidebar/tags-section.js b/app/assets/javascripts/discourse/app/components/sidebar/tags-section.js index c763d22bfe7..d5f9ba748fb 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/tags-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/tags-section.js @@ -4,10 +4,27 @@ import GlimmerComponent from "discourse/components/glimmer"; import TagSectionLink from "discourse/lib/sidebar/tags-section/tag-section-link"; export default class SidebarTagsSection extends GlimmerComponent { + constructor() { + super(...arguments); + + this.callbackId = this.topicTrackingState.onStateChange(() => { + this.sectionLinks.forEach((sectionLink) => { + sectionLink.refreshCounts(); + }); + }); + } + + willDestroy() { + this.topicTrackingState.offStateChange(this.callbackId); + } + @cached get sectionLinks() { return this.currentUser.trackedTags.map((trackedTag) => { - return new TagSectionLink({ tag: trackedTag }); + return new TagSectionLink({ + tag: trackedTag, + topicTrackingState: this.topicTrackingState, + }); }); } } diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/tags-section/tag-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/tags-section/tag-section-link.js index ff78f2db4e0..cf161a1aff7 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/tags-section/tag-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/tags-section/tag-section-link.js @@ -1,6 +1,30 @@ +import I18n from "I18n"; + +import { tracked } from "@glimmer/tracking"; + +import { bind } from "discourse-common/utils/decorators"; + export default class TagSectionLink { - constructor({ tag }) { + @tracked totalUnread = 0; + @tracked totalNew = 0; + + constructor({ tag, topicTrackingState }) { this.tag = tag; + this.topicTrackingState = topicTrackingState; + this.refreshCounts(); + } + + @bind + refreshCounts() { + this.totalUnread = this.topicTrackingState.countUnread({ + tagId: this.tag, + }); + + if (this.totalUnread === 0) { + this.totalNew = this.topicTrackingState.countNew({ + tagId: this.tag, + }); + } } get name() { @@ -22,4 +46,26 @@ export default class TagSectionLink { get text() { return this.tag; } + + 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, + }); + } + } + + get route() { + if (this.totalUnread > 0) { + return "tag.showUnread"; + } else if (this.totalNew > 0) { + return "tag.showNew"; + } else { + return "tag.show"; + } + } } diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs index 60c9b0fbbb3..78194b4de41 100644 --- a/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs @@ -12,6 +12,7 @@ @title={{sectionLink.title}} @content={{sectionLink.text}} @currentWhen={{sectionLink.currentWhen}} + @badgeText={{sectionLink.badgeText}} @model={{sectionLink.model}}> </Sidebar::SectionLink> {{/each}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-tags-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-tags-section-test.js index bed88ce3d04..03c26f09922 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-tags-section-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-tags-section-test.js @@ -1,11 +1,12 @@ import I18n from "I18n"; -import { click, currentURL, visit } from "@ember/test-helpers"; +import { click, currentURL, settled, visit } from "@ember/test-helpers"; import { acceptance, conditionalTest, exists, + publishToMessageBus, query, queryAll, updateCurrentUser, @@ -226,4 +227,147 @@ acceptance("Sidebar - Tags section", function (needs) { ); } ); + + conditionalTest( + "new and unread count for tag section links", + !isLegacyEmber(), + 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: ["tag1"], + }, + { + topic_id: 2, + highest_post_number: 12, + last_read_post_number: 11, + created_at: "2020-02-09T09:40:02.672Z", + category_id: 2, + notification_level: 2, + created_in_new_period: false, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + tags: ["tag1"], + }, + { + 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: 2, + created_in_new_period: false, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + tags: ["tag2"], + }, + { + topic_id: 4, + highest_post_number: 17, + last_read_post_number: 16, + created_at: "2020-10-31T03:41:42.257Z", + category_id: 4, + notification_level: 2, + created_in_new_period: false, + unread_not_too_old: true, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + tags: ["tag4"], + }, + ]); + + await visit("/"); + + assert.strictEqual( + query( + `.sidebar-section-link-tag1 .sidebar-section-link-content-badge` + ).textContent.trim(), + I18n.t("sidebar.unread_count", { count: 1 }), + `displays 1 unread count for tag1 section link` + ); + + assert.strictEqual( + query( + `.sidebar-section-link-tag2 .sidebar-section-link-content-badge` + ).textContent.trim(), + I18n.t("sidebar.unread_count", { count: 1 }), + `displays 1 unread count for tag2 section link` + ); + + assert.ok( + !exists( + `.sidebar-section-link-tag3 .sidebar-section-link-content-badge` + ), + "does not display any badge for tag3 section link" + ); + + 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-tag1 .sidebar-section-link-content-badge` + ).textContent.trim(), + I18n.t("sidebar.new_count", { count: 1 }), + `displays 1 new count for tag1 section link` + ); + + 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-tag1 .sidebar-section-link-content-badge` + ), + `does not display any badge tag1 section link` + ); + } + ); + + conditionalTest( + "cleans 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 + ); + } + ); }); diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index 1b143b5c2b4..eeec84c2e87 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -278,7 +278,7 @@ class TopicTrackingState end def self.include_tags_in_report? - SiteSetting.tagging_enabled && @include_tags_in_report + SiteSetting.tagging_enabled && (@include_tags_in_report || SiteSetting.enable_experimental_sidebar) end def self.include_tags_in_report=(v) diff --git a/app/serializers/topic_tracking_state_serializer.rb b/app/serializers/topic_tracking_state_serializer.rb index b26bd2995c2..473997204f2 100644 --- a/app/serializers/topic_tracking_state_serializer.rb +++ b/app/serializers/topic_tracking_state_serializer.rb @@ -9,7 +9,8 @@ class TopicTrackingStateSerializer < ApplicationSerializer :notification_level, :created_in_new_period, :unread_not_too_old, - :treat_as_new_topic_start_date + :treat_as_new_topic_start_date, + :tags def created_in_new_period return true if !scope @@ -20,4 +21,8 @@ class TopicTrackingStateSerializer < ApplicationSerializer return true if object.first_unread_at.blank? object.updated_at >= object.first_unread_at end + + def include_tags? + object.respond_to?(:tags) + end end diff --git a/spec/models/topic_tracking_state_spec.rb b/spec/models/topic_tracking_state_spec.rb index e66a8d5f56e..17f13a836a6 100644 --- a/spec/models/topic_tracking_state_spec.rb +++ b/spec/models/topic_tracking_state_spec.rb @@ -539,14 +539,7 @@ describe TopicTrackingState do end context "tag support" do - after do - # this is a bit of an odd hook, but this is a global change - # used by plugins that leverage tagging heavily and need - # tag information in topic tracking state - TopicTrackingState.include_tags_in_report = false - end - - it "correctly handles tags" do + before do SiteSetting.tagging_enabled = true post.topic.notifier.watch_topic!(post.topic.user_id) @@ -556,6 +549,27 @@ describe TopicTrackingState do Guardian.new(Discourse.system_user), ['bananas', 'apples'] ) + end + + it "includes tags when SiteSetting.enable_experimental_sidebar is true" do + report = TopicTrackingState.report(user) + expect(report.length).to eq(1) + row = report[0] + expect(row.respond_to?(:tags)).to eq(false) + + SiteSetting.enable_experimental_sidebar = true + + report = TopicTrackingState.report(user) + expect(report.length).to eq(1) + row = report[0] + expect(row.tags).to contain_exactly("apples", "bananas") + end + + it "includes tags when TopicTrackingState.include_tags_in_report option is enabled" do + report = TopicTrackingState.report(user) + expect(report.length).to eq(1) + row = report[0] + expect(row.respond_to? :tags).to eq(false) TopicTrackingState.include_tags_in_report = true @@ -563,13 +577,8 @@ describe TopicTrackingState do expect(report.length).to eq(1) row = report[0] expect(row.tags).to contain_exactly("apples", "bananas") - + ensure TopicTrackingState.include_tags_in_report = false - - report = TopicTrackingState.report(user) - expect(report.length).to eq(1) - row = report[0] - expect(row.respond_to? :tags).to eq(false) end end diff --git a/spec/serializers/topic_tracking_state_serializer_spec.rb b/spec/serializers/topic_tracking_state_serializer_spec.rb new file mode 100644 index 00000000000..c826daad109 --- /dev/null +++ b/spec/serializers/topic_tracking_state_serializer_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +describe TopicTrackingStateSerializer do + fab!(:user) { Fabricate(:user) } + fab!(:post) { create_post } + + it 'serializes topic tracking state reports' do + report = TopicTrackingState.report(user) + serialized = described_class.new(report[0], scope: Guardian.new(user), root: false).as_json + + expect(serialized[:topic_id]).to eq(post.topic_id) + expect(serialized[:highest_post_number]).to eq(post.topic.highest_post_number) + expect(serialized[:last_read_post_number]).to eq(nil) + expect(serialized[:created_at]).to be_present + expect(serialized[:notification_level]).to eq(nil) + expect(serialized[:created_in_new_period]).to eq(true) + expect(serialized[:unread_not_too_old]).to eq(true) + expect(serialized[:treat_as_new_topic_start_date]).to be_present + expect(serialized.has_key?(:tags)).to eq(false) + end + + it "includes tags attribute when tags are present" do + TopicTrackingState.include_tags_in_report = true + + post.topic.notifier.watch_topic!(post.topic.user_id) + + DiscourseTagging.tag_topic_by_names( + post.topic, + Guardian.new(Discourse.system_user), + ['bananas', 'apples'] + ) + + report = TopicTrackingState.report(user) + serialized = described_class.new(report[0], scope: Guardian.new(user), root: false).as_json + + expect(serialized[:tags]).to contain_exactly("bananas", "apples") + ensure + TopicTrackingState.include_tags_in_report = false + end +end