diff --git a/app/assets/javascripts/discourse/app/components/topic-footer-buttons.hbs b/app/assets/javascripts/discourse/app/components/topic-footer-buttons.hbs index e7498a441ec..c35b3ce8e7c 100644 --- a/app/assets/javascripts/discourse/app/components/topic-footer-buttons.hbs +++ b/app/assets/javascripts/discourse/app/components/topic-footer-buttons.hbs @@ -101,10 +101,7 @@ /> {{/if}} - + {{/if}} {{#if @mobileView}} + + ); + + assert.strictEqual( + selectKit().header().label(), + "Normal", + "has the correct label" + ); + + state.topic = buildTopic.call(this, { level: 2 }); + await settled(); + + assert.strictEqual( + selectKit().header().label(), + "Tracking", + "correctly changes the label" + ); + }); + + test("the header has a localized title", async function (assert) { + I18n.translations.en.js.topic.notifications.tracking_pm.title = `${originalTranslation} PM`; + const topic = buildTopic.call(this, { + level: 2, + archetype: "private_message", + }); + + await render(); + + assert.strictEqual( + selectKit().header().label(), + `${originalTranslation} PM`, + "has the correct label for PMs" + ); + }); + + test("notification reason text - user mailing list mode", async function (assert) { + this.currentUser.set("user_option.mailing_list_mode", true); + const topic = buildTopic.call(this, { level: 2 }); + + await render(); + + assert + .dom(".topic-notifications-button .text") + .hasText( + I18n.t("topic.notifications.reasons.mailing_list_mode"), + "mailing_list_mode enabled for the user shows unique text" + ); + }); + + test("notification reason text - bad notification reason", async function (assert) { + const state = new TestClass(); + state.topic = buildTopic.call(this, { level: 2 }); + + await render(); + + state.topic = buildTopic.call(this, { level: 3, reason: 999 }); + await settled(); + + assert + .dom(".topic-notifications-button .text") + .hasText( + I18n.t("topic.notifications.reasons.3"), + "fallback to regular level translation if reason does not exist" + ); + }); + + test("notification reason text - user tracking category", async function (assert) { + this.currentUser.set("tracked_category_ids", [88]); + const topic = buildTopic.call(this, { + level: 2, + reason: 8, + category_id: 88, + }); + + await render(); + + assert + .dom(".topic-notifications-button .text") + .hasText( + I18n.t("topic.notifications.reasons.2_8"), + "use 2_8 notification if user is still tracking category" + ); + }); + + test("notification reason text - user no longer tracking category", async function (assert) { + this.currentUser.set("tracked_category_ids", []); + const topic = buildTopic.call(this, { + level: 2, + reason: 8, + category_id: 88, + }); + + await render(); + + assert + .dom(".topic-notifications-button .text") + .hasText( + I18n.t("topic.notifications.reasons.2_8_stale"), + "use _stale notification if user is no longer tracking category" + ); + }); + + test("notification reason text - user watching category", async function (assert) { + this.currentUser.set("watched_category_ids", [88]); + const topic = buildTopic.call(this, { + level: 3, + reason: 6, + category_id: 88, + }); + + await render(); + + assert + .dom(".topic-notifications-button .text") + .hasText( + I18n.t("topic.notifications.reasons.3_6"), + "use 3_6 notification if user is still watching category" + ); + }); + + test("notification reason text - user no longer watching category", async function (assert) { + this.currentUser.set("watched_category_ids", []); + const topic = buildTopic.call(this, { + level: 3, + reason: 6, + category_id: 88, + }); + + await render(); + + assert + .dom(".topic-notifications-button .text") + .hasText( + I18n.t("topic.notifications.reasons.3_6_stale"), + "use _stale notification if user is no longer watching category" + ); + }); + + test("notification reason text - user watching tag", async function (assert) { + this.currentUser.set("watched_tags", ["test"]); + const topic = buildTopic.call(this, { + level: 3, + reason: 10, + tags: ["test"], + }); + + await render(); + + assert + .dom(".topic-notifications-button .text") + .hasText( + I18n.t("topic.notifications.reasons.3_10"), + "use 3_10 notification if user is still watching tag" + ); + }); + + test("notification reason text - user no longer watching tag", async function (assert) { + this.currentUser.set("watched_tags", []); + const topic = buildTopic.call(this, { + level: 3, + reason: 10, + tags: ["test"], + }); + + await render(); + + assert + .dom(".topic-notifications-button .text") + .hasText( + I18n.t("topic.notifications.reasons.3_10_stale"), + "use _stale notification if user is no longer watching tag" + ); + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-button-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-button-test.js deleted file mode 100644 index b52739bcb2b..00000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-button-test.js +++ /dev/null @@ -1,250 +0,0 @@ -import { getOwner } from "@ember/owner"; -import { render } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; -import { module, test } from "qunit"; -import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { query } from "discourse/tests/helpers/qunit-helpers"; -import selectKit from "discourse/tests/helpers/select-kit-helper"; -import I18n from "discourse-i18n"; - -function buildTopic(opts) { - return this.store.createRecord("topic", { - id: 4563, - title: "Qunit Test Topic", - details: { - notification_level: opts.level, - notifications_reason_id: opts.reason || null, - }, - archetype: opts.archetype || "regular", - category_id: opts.category_id || null, - tags: opts.tags || [], - }); -} - -const originalTranslation = - I18n.translations.en.js.topic.notifications.tracking_pm.title; - -module( - "Integration | Component | select-kit/topic-notifications-button", - function (hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function () { - this.store = getOwner(this).lookup("service:store"); - }); - - hooks.afterEach(function () { - I18n.translations.en.js.topic.notifications.tracking_pm.title = - originalTranslation; - }); - - test("the header has a localized title", async function (assert) { - this.set("topic", buildTopic.call(this, { level: 1 })); - - await render(hbs` - - `); - - assert.strictEqual( - selectKit().header().label(), - "Normal", - "it has the correct label" - ); - - this.set("topic", buildTopic.call(this, { level: 2 })); - - assert.strictEqual( - selectKit().header().label(), - "Tracking", - "it correctly changes the label" - ); - }); - - test("the header has a localized title", async function (assert) { - I18n.translations.en.js.topic.notifications.tracking_pm.title = `${originalTranslation} PM`; - this.set( - "topic", - buildTopic.call(this, { level: 2, archetype: "private_message" }) - ); - - await render(hbs` - - `); - - assert.strictEqual( - selectKit().header().label(), - `${originalTranslation} PM`, - "it has the correct label for PMs" - ); - }); - - test("notification reason text - user mailing list mode", async function (assert) { - this.currentUser.set("user_option.mailing_list_mode", true); - this.set("topic", buildTopic.call(this, { level: 2 })); - - await render(hbs` - - `); - - assert.strictEqual( - query(".topic-notifications-button .text").innerText, - I18n.t("topic.notifications.reasons.mailing_list_mode"), - "mailing_list_mode enabled for the user shows unique text" - ); - }); - - test("notification reason text - bad notification reason", async function (assert) { - this.set("topic", buildTopic.call(this, { level: 2 })); - - await render(hbs` - - `); - - this.set("topic", buildTopic.call(this, { level: 3, reason: 999 })); - - assert.strictEqual( - query(".topic-notifications-button .text").innerText, - I18n.t("topic.notifications.reasons.3"), - "fallback to regular level translation if reason does not exist" - ); - }); - - test("notification reason text - user tracking category", async function (assert) { - this.currentUser.set("tracked_category_ids", [88]); - this.set( - "topic", - buildTopic.call(this, { level: 2, reason: 8, category_id: 88 }) - ); - - await render(hbs` - - `); - - assert.strictEqual( - query(".topic-notifications-button .text").innerText, - I18n.t("topic.notifications.reasons.2_8"), - "use 2_8 notification if user is still tracking category" - ); - }); - - test("notification reason text - user no longer tracking category", async function (assert) { - this.currentUser.set("tracked_category_ids", []); - this.set( - "topic", - buildTopic.call(this, { level: 2, reason: 8, category_id: 88 }) - ); - - await render(hbs` - - `); - - assert.strictEqual( - query(".topic-notifications-button .text").innerText, - I18n.t("topic.notifications.reasons.2_8_stale"), - "use _stale notification if user is no longer tracking category" - ); - }); - - test("notification reason text - user watching category", async function (assert) { - this.currentUser.set("watched_category_ids", [88]); - this.set( - "topic", - buildTopic.call(this, { level: 3, reason: 6, category_id: 88 }) - ); - - await render(hbs` - - `); - - assert.strictEqual( - query(".topic-notifications-button .text").innerText, - I18n.t("topic.notifications.reasons.3_6"), - "use 3_6 notification if user is still watching category" - ); - }); - - test("notification reason text - user no longer watching category", async function (assert) { - this.currentUser.set("watched_category_ids", []); - this.set( - "topic", - buildTopic.call(this, { level: 3, reason: 6, category_id: 88 }) - ); - - await render(hbs` - - `); - - assert.strictEqual( - query(".topic-notifications-button .text").innerText, - I18n.t("topic.notifications.reasons.3_6_stale"), - "use _stale notification if user is no longer watching category" - ); - }); - - test("notification reason text - user watching tag", async function (assert) { - this.currentUser.set("watched_tags", ["test"]); - this.set( - "topic", - buildTopic.call(this, { level: 3, reason: 10, tags: ["test"] }) - ); - - await render(hbs` - - `); - - assert.strictEqual( - query(".topic-notifications-button .text").innerText, - I18n.t("topic.notifications.reasons.3_10"), - "use 3_10 notification if user is still watching tag" - ); - }); - - test("notification reason text - user no longer watching tag", async function (assert) { - this.currentUser.set("watched_tags", []); - this.set( - "topic", - buildTopic.call(this, { level: 3, reason: 10, tags: ["test"] }) - ); - - await render(hbs` - - `); - - assert.strictEqual( - query(".topic-notifications-button .text").innerText, - I18n.t("topic.notifications.reasons.3_10_stale"), - "use _stale notification if user is no longer watching tag" - ); - }); - } -); diff --git a/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-options-test.js b/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-options-test.gjs similarity index 67% rename from app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-options-test.js rename to app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-options-test.gjs index dd24f162b8a..80edada0611 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-options-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/select-kit/topic-notifications-options-test.gjs @@ -1,10 +1,10 @@ import { getOwner } from "@ember/owner"; import { render } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; import { module, test } from "qunit"; import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import I18n from "discourse-i18n"; +import TopicNotificationsOptions from "select-kit/components/topic-notifications-options"; function extractDescriptions(rows) { return [...rows].map((el) => el.querySelector(".desc").textContent.trim()); @@ -23,24 +23,21 @@ module( test("regular topic notification level descriptions", async function (assert) { const store = getOwner(this).lookup("service:store"); - this.set( - "topic", - store.createRecord("topic", { - id: 4563, - title: "Qunit Test Topic", - archetype: "regular", - details: { - notification_level: 1, - }, - }) - ); + const topic = store.createRecord("topic", { + id: 4563, + title: "Qunit Test Topic", + archetype: "regular", + details: { + notification_level: 1, + }, + }); - await render(hbs` + await render(); await selectKit().expand(); @@ -50,38 +47,35 @@ module( assert.strictEqual( uiTexts.length, descriptions.length, - "it has the correct copy" + "has the correct copy" ); uiTexts.forEach((text, index) => { assert.strictEqual( text.trim(), descriptions[index].trim(), - "it has the correct copy" + "has the correct copy" ); }); }); test("PM topic notification level descriptions", async function (assert) { const store = getOwner(this).lookup("service:store"); - this.set( - "topic", - store.createRecord("topic", { - id: 4563, - title: "Qunit Test Topic", - archetype: "private_message", - details: { - notification_level: 1, - }, - }) - ); + const topic = store.createRecord("topic", { + id: 4563, + title: "Qunit Test Topic", + archetype: "private_message", + details: { + notification_level: 1, + }, + }); - await render(hbs` + await render(); await selectKit().expand(); @@ -91,14 +85,14 @@ module( assert.strictEqual( uiTexts.length, descriptions.length, - "it has the correct copy" + "has the correct copy" ); uiTexts.forEach((text, index) => { assert.strictEqual( text.trim(), descriptions[index].trim(), - "it has the correct copy" + "has the correct copy" ); }); }); diff --git a/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.gjs b/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.gjs new file mode 100644 index 00000000000..80d018ae8c1 --- /dev/null +++ b/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.gjs @@ -0,0 +1,147 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { hash } from "@ember/helper"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import { htmlSafe } from "@ember/template"; +import { isEmpty } from "@ember/utils"; +import { NotificationLevels } from "discourse/lib/notification-levels"; +import i18n from "discourse-common/helpers/i18n"; +import getURL from "discourse-common/lib/get-url"; +import I18n from "discourse-i18n"; +import TopicNotificationsOptions from "select-kit/components/topic-notifications-options"; + +export default class TopicNotificationsButton extends Component { + @service currentUser; + + @tracked isLoading = false; + + get notificationLevel() { + return this.args.topic.get("details.notification_level"); + } + + get appendReason() { + return this.args.appendReason ?? true; + } + + get showFullTitle() { + return this.args.showFullTitle ?? true; + } + + get showCaret() { + return this.args.showCaret ?? true; + } + + get reasonText() { + const topic = this.args.topic; + const level = topic.get("details.notification_level") ?? 1; + const reason = topic.get("details.notifications_reason_id"); + let localeString = `topic.notifications.reasons.${level}`; + + if (typeof reason === "number") { + let localeStringWithReason = `${localeString}_${reason}`; + + if (this._reasonStale(level, reason)) { + localeStringWithReason += "_stale"; + } + + // some sane protection for missing translations of edge cases + if (I18n.lookup(localeStringWithReason, { locale: "en" })) { + localeString = localeStringWithReason; + } + } + + if ( + this.currentUser?.user_option.mailing_list_mode && + level > NotificationLevels.MUTED + ) { + return I18n.t("topic.notifications.reasons.mailing_list_mode"); + } else { + return I18n.t(localeString, { + username: this.currentUser?.username_lower, + basePath: getURL(""), + }); + } + } + + // The user may have changed their category or tag tracking settings + // since this topic was tracked/watched based on those settings in the + // past. In that case we need to alter the reason message we show them + // otherwise it is very confusing for the end user to be told they are + // tracking a topic because of a category, when they are no longer tracking + // that category. + _reasonStale(level, reason) { + if (!this.currentUser) { + return; + } + + const watchedCategoryIds = this.currentUser.watched_category_ids || []; + const trackedCategoryIds = this.currentUser.tracked_category_ids || []; + const watchedTags = this.currentUser.watched_tags || []; + + if (this.args.topic.category_id) { + if (level === 2 && reason === 8) { + // 2_8 tracking category + return !trackedCategoryIds.includes(this.args.topic.category_id); + } else if (level === 3 && reason === 6) { + // 3_6 watching category + return !watchedCategoryIds.includes(this.args.topic.category_id); + } + } else if (!isEmpty(this.args.topic.tags)) { + if (level === 3 && reason === 10) { + // 3_10 watching tag + return !this.args.topic.tags.some((tag) => watchedTags.includes(tag)); + } + } + + return false; + } + + @action + async changeTopicNotificationLevel(levelId) { + if (levelId === this.notificationLevel) { + return; + } + + this.isLoading = true; + + try { + await this.args.topic.details.updateNotifications(levelId); + } finally { + this.isLoading = false; + } + } + + +} diff --git a/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.hbs b/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.hbs deleted file mode 100644 index eb980433ca3..00000000000 --- a/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.hbs +++ /dev/null @@ -1,28 +0,0 @@ -{{#if this.appendReason}} -

- - {{html-safe this.notificationReasonText}} -

-{{else}} - -{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.js b/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.js deleted file mode 100644 index 8fc2121ae77..00000000000 --- a/app/assets/javascripts/select-kit/addon/components/topic-notifications-button.js +++ /dev/null @@ -1,118 +0,0 @@ -import Component from "@ember/component"; -import { action, computed } from "@ember/object"; -import { isEmpty } from "@ember/utils"; -import { classNameBindings, classNames } from "@ember-decorators/component"; -import { NotificationLevels } from "discourse/lib/notification-levels"; -import getURL from "discourse-common/lib/get-url"; -import discourseComputed from "discourse-common/utils/decorators"; -import I18n from "discourse-i18n"; - -@classNames("topic-notifications-button") -@classNameBindings("isLoading") -export default class TopicNotificationsButton extends Component { - appendReason = true; - showFullTitle = true; - notificationLevel = null; - topic = null; - showCaret = true; - isLoading = false; - - @computed("isLoading") - get icon() { - return this.isLoading ? "spinner" : null; - } - - @action - changeTopicNotificationLevel(levelId) { - if (levelId !== this.notificationLevel) { - this.set("isLoading", true); - this.topic.details - .updateNotifications(levelId) - .finally(() => this.set("isLoading", false)); - } - } - - @discourseComputed( - "topic", - "topic.details.{notification_level,notifications_reason_id}" - ) - notificationReasonText(topic, topicDetails) { - let level = topicDetails.notification_level; - let reason = topicDetails.notifications_reason_id; - - if (typeof level !== "number") { - level = 1; - } - - let localeString = `topic.notifications.reasons.${level}`; - if (typeof reason === "number") { - let localeStringWithReason = localeString + "_" + reason; - - if ( - this._notificationReasonStale(level, reason, topic, this.currentUser) - ) { - localeStringWithReason += "_stale"; - } - - // some sane protection for missing translations of edge cases - if (I18n.lookup(localeStringWithReason, { locale: "en" })) { - localeString = localeStringWithReason; - } - } - - if ( - this.currentUser && - this.currentUser.user_option.mailing_list_mode && - level > NotificationLevels.MUTED - ) { - return I18n.t("topic.notifications.reasons.mailing_list_mode"); - } else { - return I18n.t(localeString, { - username: this.currentUser && this.currentUser.username_lower, - basePath: getURL(""), - }); - } - } - - // The user may have changed their category or tag tracking settings - // since this topic was tracked/watched based on those settings in the - // past. In that case we need to alter the reason message we show them - // otherwise it is very confusing for the end user to be told they are - // tracking a topic because of a category, when they are no longer tracking - // that category. - _notificationReasonStale(level, reason, topic, currentUser) { - if (!currentUser) { - return; - } - - let categoryId = topic.category_id; - let tags = topic.tags; - let watchedCategoryIds = currentUser.watched_category_ids || []; - let trackedCategoryIds = currentUser.tracked_category_ids || []; - let watchedTags = currentUser.watched_tags || []; - - // 2_8 tracking category - if (categoryId) { - if (level === 2 && reason === 8) { - if (!trackedCategoryIds.includes(categoryId)) { - return true; - } - - // 3_6 watching category - } else if (level === 3 && reason === 6) { - if (!watchedCategoryIds.includes(categoryId)) { - return true; - } - } - } else if (!isEmpty(tags)) { - // 3_10 watching tag - if (level === 3 && reason === 10) { - if (!tags.some((tag) => watchedTags.includes(tag))) { - return true; - } - } - } - - return false; - } -}