DEV: Convert topic-notifications-button to gjs (#29237)

This commit is contained in:
Jarek Radosz 2024-10-21 17:34:56 +02:00 committed by GitHub
parent 97be676b99
commit 481d0645a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 426 additions and 443 deletions

View File

@ -101,10 +101,7 @@
/>
{{/if}}
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
<TopicNotificationsButton @topic={{this.topic}} />
{{/if}}
<PluginOutlet

View File

@ -175,12 +175,11 @@
{{#if this.currentUser}}
<TopicNotificationsButton
@notificationLevel={{@model.details.notification_level}}
@topic={{@model}}
@showFullTitle={{false}}
@appendReason={{false}}
@placement="bottom-end"
@showCaret={{false}}
@placement="bottom-end"
/>
{{#if @mobileView}}
<TopicAdminMenu

View File

@ -19,9 +19,9 @@ acceptance("Topic Notifications button", function (needs) {
await visit("/t/internationalization-localization/280");
assert.ok(
assert.true(
notificationOptions.exists(),
"it should display the notification options button in the topic's footer"
"displays the notification options button in the topic's footer"
);
await notificationOptions.expand();
@ -30,7 +30,7 @@ acceptance("Topic Notifications button", function (needs) {
assert.strictEqual(
notificationOptions.header().label(),
"Watching",
"it should display the right notification level"
"displays the right notification level"
);
const timelineNotificationOptions = selectKit(
@ -40,7 +40,7 @@ acceptance("Topic Notifications button", function (needs) {
assert.strictEqual(
timelineNotificationOptions.header().value(),
"3",
"it should display the right notification level"
"displays the right notification level"
);
await timelineNotificationOptions.expand();
@ -49,13 +49,13 @@ acceptance("Topic Notifications button", function (needs) {
assert.strictEqual(
timelineNotificationOptions.header().value(),
"0",
"it should display the right notification level"
"displays the right notification level"
);
assert.strictEqual(
notificationOptions.header().label(),
"Muted",
"it should display the right notification level"
"displays the right notification level"
);
});
});

View File

@ -0,0 +1,242 @@
import { tracked } from "@glimmer/tracking";
import { getOwner } from "@ember/owner";
import { render, settled } from "@ember/test-helpers";
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 TopicNotificationsButton from "select-kit/components/topic-notifications-button";
class TestClass {
@tracked topic;
}
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 correct labels", async function (assert) {
const state = new TestClass();
state.topic = buildTopic.call(this, { level: 1 });
await render(<template>
<TopicNotificationsButton @topic={{state.topic}} />
</template>);
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(<template>
<TopicNotificationsButton @topic={{topic}} />
</template>);
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(<template>
<TopicNotificationsButton @topic={{topic}} />
</template>);
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(<template>
<TopicNotificationsButton @topic={{state.topic}} />
</template>);
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(<template>
<TopicNotificationsButton @topic={{topic}} />
</template>);
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(<template>
<TopicNotificationsButton @topic={{topic}} />
</template>);
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(<template>
<TopicNotificationsButton @topic={{topic}} />
</template>);
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(<template>
<TopicNotificationsButton @topic={{topic}} />
</template>);
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(<template>
<TopicNotificationsButton @topic={{topic}} />
</template>);
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(<template>
<TopicNotificationsButton @topic={{topic}} />
</template>);
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"
);
});
}
);

View File

@ -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`
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
`);
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`
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
`);
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`
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
`);
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`
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
`);
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`
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
`);
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`
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
`);
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`
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
`);
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`
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
`);
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`
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
`);
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`
<TopicNotificationsButton
@notificationLevel={{this.topic.details.notification_level}}
@topic={{this.topic}}
/>
`);
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"
);
});
}
);

View File

@ -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", {
const topic = store.createRecord("topic", {
id: 4563,
title: "Qunit Test Topic",
archetype: "regular",
details: {
notification_level: 1,
},
})
);
});
await render(hbs`
await render(<template>
<TopicNotificationsOptions
@value={{this.topic.details.notification_level}}
@topic={{this.topic}}
@value={{topic.details.notification_level}}
@topic={{topic}}
/>
`);
</template>);
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", {
const topic = store.createRecord("topic", {
id: 4563,
title: "Qunit Test Topic",
archetype: "private_message",
details: {
notification_level: 1,
},
})
);
});
await render(hbs`
await render(<template>
<TopicNotificationsOptions
@value={{this.topic.details.notification_level}}
@topic={{this.topic}}
@value={{topic.details.notification_level}}
@topic={{topic}}
/>
`);
</template>);
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"
);
});
});

View File

@ -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;
}
}
<template>
<div class="topic-notifications-button">
{{#if this.appendReason}}
<p class="reason">
<TopicNotificationsOptions
@value={{this.notificationLevel}}
@topic={{@topic}}
@onChange={{this.changeTopicNotificationLevel}}
@options={{hash
icon=(if this.isLoading "spinner")
showFullTitle=this.showFullTitle
showCaret=this.showCaret
headerAriaLabel=(i18n "topic.notifications.title")
}}
/>
<span class="text">{{htmlSafe this.reasonText}}</span>
</p>
{{else}}
<TopicNotificationsOptions
@value={{this.notificationLevel}}
@topic={{@topic}}
@onChange={{this.changeTopicNotificationLevel}}
@options={{hash
icon=(if this.isLoading "spinner")
showFullTitle=this.showFullTitle
showCaret=this.showCaret
headerAriaLabel=(i18n "topic.notifications.title")
}}
/>
{{/if}}
</div>
</template>
}

View File

@ -1,28 +0,0 @@
{{#if this.appendReason}}
<p class="reason">
<TopicNotificationsOptions
@value={{this.notificationLevel}}
@topic={{this.topic}}
@onChange={{action "changeTopicNotificationLevel"}}
@options={{hash
icon=this.icon
showFullTitle=this.showFullTitle
showCaret=this.showCaret
headerAriaLabel=(i18n "topic.notifications.title")
}}
/>
<span class="text">{{html-safe this.notificationReasonText}}</span>
</p>
{{else}}
<TopicNotificationsOptions
@value={{this.notificationLevel}}
@topic={{this.topic}}
@onChange={{action "changeTopicNotificationLevel"}}
@options={{hash
icon=this.icon
showFullTitle=this.showFullTitle
showCaret=this.showCaret
headerAriaLabel=(i18n "topic.notifications.title")
}}
/>
{{/if}}

View File

@ -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;
}
}