diff --git a/app/assets/javascripts/discourse/app/components/user-menu/bookmark-notification-item.hbs b/app/assets/javascripts/discourse/app/components/user-menu/bookmark-notification-item.hbs
deleted file mode 100644
index e0829c1c076..00000000000
--- a/app/assets/javascripts/discourse/app/components/user-menu/bookmark-notification-item.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{component this.component item=@item}}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/bookmark-notification-item.js b/app/assets/javascripts/discourse/app/components/user-menu/bookmark-notification-item.js
deleted file mode 100644
index cc5679bfb31..00000000000
--- a/app/assets/javascripts/discourse/app/components/user-menu/bookmark-notification-item.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import Component from "@glimmer/component";
-import Notification from "discourse/models/notification";
-
-export default class UserMenuBookmarkNotificationItem extends Component {
- get component() {
- if (this.args.item.constructor === Notification) {
- return "user-menu/notification-item";
- } else {
- return "user-menu/bookmark-item";
- }
- }
-}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/bookmarks-list-empty-state.js b/app/assets/javascripts/discourse/app/components/user-menu/bookmarks-list-empty-state.js
new file mode 100644
index 00000000000..742bb3c18ef
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/user-menu/bookmarks-list-empty-state.js
@@ -0,0 +1,3 @@
+import templateOnly from "@ember/component/template-only";
+
+export default templateOnly();
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/bookmarks-list.js b/app/assets/javascripts/discourse/app/components/user-menu/bookmarks-list.js
index 639b30cc404..add65403229 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/bookmarks-list.js
+++ b/app/assets/javascripts/discourse/app/components/user-menu/bookmarks-list.js
@@ -3,6 +3,8 @@ import { ajax } from "discourse/lib/ajax";
import Notification from "discourse/models/notification";
import showModal from "discourse/lib/show-modal";
import I18n from "I18n";
+import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
+import UserMenuBookmarkItem from "discourse/lib/user-menu/bookmark-item";
export default class UserMenuBookmarksList extends UserMenuNotificationsList {
get dismissTypes() {
@@ -29,10 +31,6 @@ export default class UserMenuBookmarksList extends UserMenuNotificationsList {
return "user-menu-bookmarks-tab";
}
- get itemComponent() {
- return "user-menu/bookmark-notification-item";
- }
-
get emptyStateComponent() {
return "user-menu/bookmarks-list-empty-state";
}
@@ -50,10 +48,22 @@ export default class UserMenuBookmarksList extends UserMenuNotificationsList {
return ajax(`/u/${this.currentUser.username}/user-menu-bookmarks`).then(
(data) => {
const content = [];
- data.notifications.forEach((notification) => {
- content.push(Notification.create(notification));
+ data.notifications.forEach((rawNotification) => {
+ const notification = Notification.create(rawNotification);
+ content.push(
+ new UserMenuNotificationItem({
+ notification,
+ currentUser: this.currentUser,
+ siteSettings: this.siteSettings,
+ site: this.site,
+ })
+ );
});
- content.push(...data.bookmarks);
+ content.push(
+ ...data.bookmarks.map((bookmark) => {
+ return new UserMenuBookmarkItem({ bookmark });
+ })
+ );
return content;
}
);
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/items-list-empty-state.js b/app/assets/javascripts/discourse/app/components/user-menu/items-list-empty-state.js
new file mode 100644
index 00000000000..742bb3c18ef
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/user-menu/items-list-empty-state.js
@@ -0,0 +1,3 @@
+import templateOnly from "@ember/component/template-only";
+
+export default templateOnly();
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/items-list.hbs b/app/assets/javascripts/discourse/app/components/user-menu/items-list.hbs
index fab0afadc89..436bee4b915 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/items-list.hbs
+++ b/app/assets/javascripts/discourse/app/components/user-menu/items-list.hbs
@@ -5,7 +5,7 @@
{{else if this.items.length}}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/items-list.js b/app/assets/javascripts/discourse/app/components/user-menu/items-list.js
index b0fc9039860..7ab92d8fabe 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/items-list.js
+++ b/app/assets/javascripts/discourse/app/components/user-menu/items-list.js
@@ -9,7 +9,7 @@ export default class UserMenuItemsList extends Component {
constructor() {
super(...arguments);
- this._load();
+ this.#load();
}
get itemsCacheKey() {}
@@ -28,12 +28,6 @@ export default class UserMenuItemsList extends Component {
return "user-menu/items-list-empty-state";
}
- get itemComponent() {
- throw new Error(
- `the itemComponent property must be implemented in ${this.constructor.name}`
- );
- }
-
fetchItems() {
throw new Error(
`the fetchItems method must be implemented in ${this.constructor.name}`
@@ -41,15 +35,15 @@ export default class UserMenuItemsList extends Component {
}
refreshList() {
- this._load();
+ this.#load();
}
dismissWarningModal() {
return null;
}
- _load() {
- const cached = this._getCachedItems();
+ #load() {
+ const cached = this.#getCachedItems();
if (cached?.length) {
this.items = cached;
} else {
@@ -57,20 +51,20 @@ export default class UserMenuItemsList extends Component {
}
this.fetchItems()
.then((items) => {
- this._setCachedItems(items);
+ this.#setCachedItems(items);
this.items = items;
})
.finally(() => (this.loading = false));
}
- _getCachedItems() {
+ #getCachedItems() {
const key = this.itemsCacheKey;
if (key) {
return Session.currentProp(`user-menu-items:${key}`);
}
}
- _setCachedItems(newItems) {
+ #setCachedItems(newItems) {
const key = this.itemsCacheKey;
if (key) {
Session.currentProp(`user-menu-items:${key}`, newItems);
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/menu-item.js b/app/assets/javascripts/discourse/app/components/user-menu/menu-item.js
index c16a35f8a77..62dca80f57f 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/menu-item.js
+++ b/app/assets/javascripts/discourse/app/components/user-menu/menu-item.js
@@ -1,35 +1,60 @@
import Component from "@glimmer/component";
+import { emojiUnescape } from "discourse/lib/text";
+import { escapeExpression } from "discourse/lib/utilities";
+import { htmlSafe } from "@ember/template";
import { action } from "@ember/object";
export default class UserMenuItem extends Component {
- get className() {}
+ get className() {
+ return this.#item.className;
+ }
get linkHref() {
- throw new Error("not implemented");
+ return this.#item.linkHref;
}
get linkTitle() {
- throw new Error("not implemented");
+ return this.#item.linkTitle;
}
get icon() {
- throw new Error("not implemented");
+ return this.#item.icon;
}
get label() {
- throw new Error("not implemented");
+ return this.#item.label;
}
- get labelClass() {}
+ get labelClass() {
+ return this.#item.labelClass;
+ }
get description() {
- throw new Error("not implemented");
+ const description = this.#item.description;
+ if (description) {
+ if (typeof description === "string") {
+ // do emoji unescape on all items
+ return htmlSafe(emojiUnescape(escapeExpression(description)));
+ }
+ // it's probably an htmlSafe object, don't try to unescape emojis
+ return description;
+ }
}
- get descriptionClass() {}
+ get descriptionClass() {
+ return this.#item.descriptionClass;
+ }
- get topicId() {}
+ get topicId() {
+ return this.#item.topicId;
+ }
+
+ get #item() {
+ return this.args.item;
+ }
@action
- onClick() {}
+ onClick() {
+ return this.#item.onClick();
+ }
}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/message-notification-item.hbs b/app/assets/javascripts/discourse/app/components/user-menu/message-notification-item.hbs
deleted file mode 100644
index e0829c1c076..00000000000
--- a/app/assets/javascripts/discourse/app/components/user-menu/message-notification-item.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{component this.component item=@item}}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/message-notification-item.js b/app/assets/javascripts/discourse/app/components/user-menu/message-notification-item.js
deleted file mode 100644
index 1726851d27b..00000000000
--- a/app/assets/javascripts/discourse/app/components/user-menu/message-notification-item.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import Component from "@glimmer/component";
-import Notification from "discourse/models/notification";
-
-export default class UserMenuMessageNotificationItem extends Component {
- get component() {
- if (this.args.item.constructor === Notification) {
- return "user-menu/notification-item";
- } else {
- return "user-menu/message-item";
- }
- }
-}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/messages-list-empty-state.js b/app/assets/javascripts/discourse/app/components/user-menu/messages-list-empty-state.js
new file mode 100644
index 00000000000..742bb3c18ef
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/user-menu/messages-list-empty-state.js
@@ -0,0 +1,3 @@
+import templateOnly from "@ember/component/template-only";
+
+export default templateOnly();
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js b/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js
index cfc7a83e986..92364a36693 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js
+++ b/app/assets/javascripts/discourse/app/components/user-menu/messages-list.js
@@ -3,6 +3,8 @@ import { ajax } from "discourse/lib/ajax";
import Notification from "discourse/models/notification";
import showModal from "discourse/lib/show-modal";
import I18n from "I18n";
+import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
+import UserMenuMessageItem from "discourse/lib/user-menu/message-item";
export default class UserMenuMessagesList extends UserMenuNotificationsList {
get dismissTypes() {
@@ -29,10 +31,6 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
return "user-menu-messages-tab";
}
- get itemComponent() {
- return "user-menu/message-notification-item";
- }
-
get emptyStateComponent() {
return "user-menu/messages-list-empty-state";
}
@@ -51,10 +49,22 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
`/u/${this.currentUser.username}/user-menu-private-messages`
).then((data) => {
const content = [];
- data.notifications.forEach((notification) => {
- content.push(Notification.create(notification));
+ data.notifications.forEach((rawNotification) => {
+ const notification = Notification.create(rawNotification);
+ content.push(
+ new UserMenuNotificationItem({
+ notification,
+ currentUser: this.currentUser,
+ siteSettings: this.siteSettings,
+ site: this.site,
+ })
+ );
});
- content.push(...data.topics);
+ content.push(
+ ...data.topics.map((topic) => {
+ return new UserMenuMessageItem({ message: topic });
+ })
+ );
return content;
});
}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/notifications-list.js b/app/assets/javascripts/discourse/app/components/user-menu/notifications-list.js
index 7604fda2ff9..78fe44213a0 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/notifications-list.js
+++ b/app/assets/javascripts/discourse/app/components/user-menu/notifications-list.js
@@ -5,9 +5,11 @@ import { ajax } from "discourse/lib/ajax";
import { postRNWebviewMessage } from "discourse/lib/utilities";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
+import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
export default class UserMenuNotificationsList extends UserMenuItemsList {
@service currentUser;
+ @service siteSettings;
@service site;
@service store;
@@ -28,7 +30,7 @@ export default class UserMenuNotificationsList extends UserMenuItemsList {
}
get showDismiss() {
- return this.items.some((item) => !item.read);
+ return this.items.some((item) => !item.notification.read);
}
get dismissTitle() {
@@ -52,10 +54,6 @@ export default class UserMenuNotificationsList extends UserMenuItemsList {
}
}
- get itemComponent() {
- return "user-menu/notification-item";
- }
-
fetchItems() {
const params = {
limit: 30,
@@ -72,7 +70,16 @@ export default class UserMenuNotificationsList extends UserMenuItemsList {
return this.store
.findStale("notification", params)
.refresh()
- .then((c) => c.content);
+ .then((c) => {
+ return c.content.map((notification) => {
+ return new UserMenuNotificationItem({
+ notification,
+ currentUser: this.currentUser,
+ siteSettings: this.siteSettings,
+ site: this.site,
+ });
+ });
+ });
}
dismissWarningModal() {
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/reviewables-list.js b/app/assets/javascripts/discourse/app/components/user-menu/reviewables-list.js
index 2b1be914328..d8155ef6c88 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/reviewables-list.js
+++ b/app/assets/javascripts/discourse/app/components/user-menu/reviewables-list.js
@@ -3,8 +3,14 @@ import { ajax } from "discourse/lib/ajax";
import UserMenuReviewable from "discourse/models/user-menu-reviewable";
import I18n from "I18n";
import getUrl from "discourse-common/lib/get-url";
+import UserMenuReviewableItem from "discourse/lib/user-menu/reviewable-item";
+import { inject as service } from "@ember/service";
export default class UserMenuReviewablesList extends UserMenuItemsList {
+ @service currentUser;
+ @service siteSettings;
+ @service site;
+
get showAllHref() {
return getUrl("/review");
}
@@ -17,14 +23,15 @@ export default class UserMenuReviewablesList extends UserMenuItemsList {
return "pending-reviewables";
}
- get itemComponent() {
- return "user-menu/reviewable-item";
- }
-
fetchItems() {
return ajax("/review/user-menu-list").then((data) => {
return data.reviewables.map((item) => {
- return UserMenuReviewable.create(item);
+ return new UserMenuReviewableItem({
+ reviewable: UserMenuReviewable.create(item),
+ currentUser: this.currentUser,
+ siteSettings: this.siteSettings,
+ site: this.site,
+ });
});
});
}
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/watching-first-post.js b/app/assets/javascripts/discourse/app/lib/notification-items/watching-first-post.js
deleted file mode 100644
index abcecc3b7ca..00000000000
--- a/app/assets/javascripts/discourse/app/lib/notification-items/watching-first-post.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
-import I18n from "I18n";
-
-export default class extends NotificationItemBase {
- get label() {
- return I18n.t("notifications.watching_first_post_label");
- }
-}
diff --git a/app/assets/javascripts/discourse/app/lib/notification-item.js b/app/assets/javascripts/discourse/app/lib/notification-types-manager.js
similarity index 57%
rename from app/assets/javascripts/discourse/app/lib/notification-item.js
rename to app/assets/javascripts/discourse/app/lib/notification-types-manager.js
index f36399bc6d5..c7a58bee040 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-item.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types-manager.js
@@ -1,17 +1,17 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
-import BookmarkReminder from "discourse/lib/notification-items/bookmark-reminder";
-import Custom from "discourse/lib/notification-items/custom";
-import GrantedBadge from "discourse/lib/notification-items/granted-badge";
-import GroupMentioned from "discourse/lib/notification-items/group-mentioned";
-import GroupMessageSummary from "discourse/lib/notification-items/group-message-summary";
-import InviteeAccepted from "discourse/lib/notification-items/invitee-accepted";
-import LikedConsolidated from "discourse/lib/notification-items/liked-consolidated";
-import Liked from "discourse/lib/notification-items/liked";
-import MembershipRequestAccepted from "discourse/lib/notification-items/membership-request-accepted";
-import MembershipRequestConsolidated from "discourse/lib/notification-items/membership-request-consolidated";
-import MovedPost from "discourse/lib/notification-items/moved-post";
-import WatchingFirstPost from "discourse/lib/notification-items/watching-first-post";
+import BookmarkReminder from "discourse/lib/notification-types/bookmark-reminder";
+import Custom from "discourse/lib/notification-types/custom";
+import GrantedBadge from "discourse/lib/notification-types/granted-badge";
+import GroupMentioned from "discourse/lib/notification-types/group-mentioned";
+import GroupMessageSummary from "discourse/lib/notification-types/group-message-summary";
+import InviteeAccepted from "discourse/lib/notification-types/invitee-accepted";
+import LikedConsolidated from "discourse/lib/notification-types/liked-consolidated";
+import Liked from "discourse/lib/notification-types/liked";
+import MembershipRequestAccepted from "discourse/lib/notification-types/membership-request-accepted";
+import MembershipRequestConsolidated from "discourse/lib/notification-types/membership-request-consolidated";
+import MovedPost from "discourse/lib/notification-types/moved-post";
+import WatchingFirstPost from "discourse/lib/notification-types/watching-first-post";
const CLASS_FOR_TYPE = {
bookmark_reminder: BookmarkReminder,
@@ -31,7 +31,7 @@ const CLASS_FOR_TYPE = {
let _customClassForType = {};
export function registerNotificationTypeRenderer(notificationType, func) {
- _customClassForType[notificationType] = func(NotificationItemBase);
+ _customClassForType[notificationType] = func(NotificationTypeBase);
}
export function resetNotificationTypeRenderers() {
@@ -46,6 +46,6 @@ export function getRenderDirector(
site
) {
const klass =
- _customClassForType[type] || CLASS_FOR_TYPE[type] || NotificationItemBase;
+ _customClassForType[type] || CLASS_FOR_TYPE[type] || NotificationTypeBase;
return new klass({ notification, currentUser, siteSettings, site });
}
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/base.js b/app/assets/javascripts/discourse/app/lib/notification-types/base.js
similarity index 88%
rename from app/assets/javascripts/discourse/app/lib/notification-items/base.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/base.js
index fb7a6961019..579445405c2 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/base.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/base.js
@@ -4,7 +4,7 @@ import { emojiUnescape } from "discourse/lib/text";
import { htmlSafe } from "@ember/template";
import I18n from "I18n";
-export default class NotificationItemBase {
+export default class NotificationTypeBase {
constructor({ notification, currentUser, siteSettings, site }) {
this.notification = notification;
this.currentUser = currentUser;
@@ -16,7 +16,19 @@ export default class NotificationItemBase {
* @returns {string[]} An array of addtional classes that should be added to the
element of the notification item.
*/
get classNames() {
- return [];
+ const classes = ["notification"];
+ if (this.notification.read) {
+ classes.push("read");
+ } else {
+ classes.push("unread");
+ }
+ if (this.notificationName) {
+ classes.push(this.notificationName.replace(/_/g, "-"));
+ }
+ if (this.notification.is_warning) {
+ classes.push("is-warning");
+ }
+ return classes;
}
/**
@@ -76,11 +88,6 @@ export default class NotificationItemBase {
}
}
- /**
- * Function that is called when the notification item is clicked.
- */
- onClick() {}
-
/**
* @returns {string[]} Include additional classes to the label.
*/
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/bookmark-reminder.js b/app/assets/javascripts/discourse/app/lib/notification-types/bookmark-reminder.js
similarity index 83%
rename from app/assets/javascripts/discourse/app/lib/notification-items/bookmark-reminder.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/bookmark-reminder.js
index 60318963539..b3074b148f3 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/bookmark-reminder.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/bookmark-reminder.js
@@ -1,8 +1,8 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
import I18n from "I18n";
import getUrl from "discourse-common/lib/get-url";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get linkTitle() {
if (this.notification.data.bookmark_name) {
return I18n.t("notifications.titles.bookmark_reminder_with_name", {
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/custom.js b/app/assets/javascripts/discourse/app/lib/notification-types/custom.js
similarity index 67%
rename from app/assets/javascripts/discourse/app/lib/notification-items/custom.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/custom.js
index 09006e37a87..7096f79d6f7 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/custom.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/custom.js
@@ -1,7 +1,7 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
import I18n from "I18n";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get linkTitle() {
if (this.notification.data.title) {
return I18n.t(this.notification.data.title);
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/granted-badge.js b/app/assets/javascripts/discourse/app/lib/notification-types/granted-badge.js
similarity index 86%
rename from app/assets/javascripts/discourse/app/lib/notification-items/granted-badge.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/granted-badge.js
index 13e6b2ff31a..2fdad51637f 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/granted-badge.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/granted-badge.js
@@ -1,8 +1,8 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
import getURL from "discourse-common/lib/get-url";
import I18n from "I18n";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get linkHref() {
const badgeId = this.notification.data.badge_id;
if (badgeId) {
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/group-mentioned.js b/app/assets/javascripts/discourse/app/lib/notification-types/group-mentioned.js
similarity index 55%
rename from app/assets/javascripts/discourse/app/lib/notification-items/group-mentioned.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/group-mentioned.js
index b9ba3ee4c5b..6256eda6869 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/group-mentioned.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/group-mentioned.js
@@ -1,6 +1,6 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get label() {
return `${this.username} @${this.notification.data.group_name}`;
}
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/group-message-summary.js b/app/assets/javascripts/discourse/app/lib/notification-types/group-message-summary.js
similarity index 78%
rename from app/assets/javascripts/discourse/app/lib/notification-items/group-message-summary.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/group-message-summary.js
index 01c91e0897a..1d97bf50aec 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/group-message-summary.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/group-message-summary.js
@@ -1,8 +1,8 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get description() {
return I18n.t("notifications.group_message_summary", {
count: this.notification.data.inbox_count,
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/invitee-accepted.js b/app/assets/javascripts/discourse/app/lib/notification-types/invitee-accepted.js
similarity index 66%
rename from app/assets/javascripts/discourse/app/lib/notification-items/invitee-accepted.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/invitee-accepted.js
index 4761a6f4321..4f1e413ff38 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/invitee-accepted.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/invitee-accepted.js
@@ -1,8 +1,8 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get linkHref() {
return userPath(this.notification.data.display_username);
}
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/liked-consolidated.js b/app/assets/javascripts/discourse/app/lib/notification-types/liked-consolidated.js
similarity index 77%
rename from app/assets/javascripts/discourse/app/lib/notification-items/liked-consolidated.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/liked-consolidated.js
index 639fc1a1ab2..8f2494927cc 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/liked-consolidated.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/liked-consolidated.js
@@ -1,8 +1,8 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get linkHref() {
// TODO(osama): serialize username with notifications
return userPath(
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/liked.js b/app/assets/javascripts/discourse/app/lib/notification-types/liked.js
similarity index 86%
rename from app/assets/javascripts/discourse/app/lib/notification-items/liked.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/liked.js
index e41376b0dae..0129ed5130a 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/liked.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/liked.js
@@ -1,8 +1,8 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
import { formatUsername } from "discourse/lib/utilities";
import I18n from "I18n";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get label() {
if (this.count === 2) {
return I18n.t("notifications.liked_by_2_users", {
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/membership-request-accepted.js b/app/assets/javascripts/discourse/app/lib/notification-types/membership-request-accepted.js
similarity index 73%
rename from app/assets/javascripts/discourse/app/lib/notification-items/membership-request-accepted.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/membership-request-accepted.js
index d32ec41f271..71ef2af8f0a 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/membership-request-accepted.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/membership-request-accepted.js
@@ -1,8 +1,8 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
import { groupPath } from "discourse/lib/url";
import I18n from "I18n";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get linkHref() {
return groupPath(this.notification.data.group_name);
}
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/membership-request-consolidated.js b/app/assets/javascripts/discourse/app/lib/notification-types/membership-request-consolidated.js
similarity index 77%
rename from app/assets/javascripts/discourse/app/lib/notification-items/membership-request-consolidated.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/membership-request-consolidated.js
index 0f52b5ea031..4b71deed693 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/membership-request-consolidated.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/membership-request-consolidated.js
@@ -1,8 +1,8 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get linkHref() {
return userPath(
`${this.notification.username || this.currentUser.username}/messages`
diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/moved-post.js b/app/assets/javascripts/discourse/app/lib/notification-types/moved-post.js
similarity index 50%
rename from app/assets/javascripts/discourse/app/lib/notification-items/moved-post.js
rename to app/assets/javascripts/discourse/app/lib/notification-types/moved-post.js
index 58ed1bc37ef..b865f4c934d 100644
--- a/app/assets/javascripts/discourse/app/lib/notification-items/moved-post.js
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/moved-post.js
@@ -1,7 +1,7 @@
-import NotificationItemBase from "discourse/lib/notification-items/base";
+import NotificationTypeBase from "discourse/lib/notification-types/base";
import I18n from "I18n";
-export default class extends NotificationItemBase {
+export default class extends NotificationTypeBase {
get label() {
return I18n.t("notifications.user_moved_post", { username: this.username });
}
diff --git a/app/assets/javascripts/discourse/app/lib/notification-types/watching-first-post.js b/app/assets/javascripts/discourse/app/lib/notification-types/watching-first-post.js
new file mode 100644
index 00000000000..c2df9b89478
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/notification-types/watching-first-post.js
@@ -0,0 +1,8 @@
+import NotificationTypeBase from "discourse/lib/notification-types/base";
+import I18n from "I18n";
+
+export default class extends NotificationTypeBase {
+ get label() {
+ return I18n.t("notifications.watching_first_post_label");
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js
index 437f8bb4f30..aed69342ce8 100644
--- a/app/assets/javascripts/discourse/app/lib/plugin-api.js
+++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js
@@ -98,7 +98,7 @@ import { consolePrefix } from "discourse/lib/source-identifier";
import { addSectionLink as addCustomCommunitySectionLink } from "discourse/lib/sidebar/custom-community-section-links";
import { addSidebarSection } from "discourse/lib/sidebar/custom-sections";
import DiscourseURL from "discourse/lib/url";
-import { registerNotificationTypeRenderer } from "discourse/lib/notification-item";
+import { registerNotificationTypeRenderer } from "discourse/lib/notification-types-manager";
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
// If you add any methods to the API ensure you bump up the version number
@@ -1857,12 +1857,12 @@ class PluginApi {
/**
* EXPERIMENTAL. Do not use.
* Register a custom renderer for a notification type or override the
- * renderer of an existing type. See lib/notification-items/base.js for
+ * renderer of an existing type. See lib/notification-types/base.js for
* documentation and the default renderer.
*
* ```
- * api.registerNotificationTypeRenderer("your_notification_type", (NotificationItemBase) => {
- * return class extends NotificationItemBase {
+ * api.registerNotificationTypeRenderer("your_notification_type", (NotificationTypeBase) => {
+ * return class extends NotificationTypeBase {
* get label() {
* return "some label";
* }
@@ -1874,8 +1874,8 @@ class PluginApi {
* });
* ```
* @callback renderDirectorRegistererCallback
- * @param {NotificationItemBase} The base class from which the returned class should inherit.
- * @returns {NotificationItemBase} A class that inherits from NotificationItemBase.
+ * @param {NotificationTypeBase} The base class from which the returned class should inherit.
+ * @returns {NotificationTypeBase} A class that inherits from NotificationTypeBase.
*
* @param {string} notificationType - ID of the notification type (i.e. the key value of your notification type in the `Notification.types` enum on the server side).
* @param {renderDirectorRegistererCallback} func - Callback function that returns a subclass from the class it receives as its argument.
diff --git a/app/assets/javascripts/discourse/app/lib/reviewable-item.js b/app/assets/javascripts/discourse/app/lib/reviewable-item.js
deleted file mode 100644
index 7ef29025043..00000000000
--- a/app/assets/javascripts/discourse/app/lib/reviewable-item.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import ReviewableItemBase from "discourse/lib/reviewable-items/base";
-
-import FlaggedPost from "discourse/lib/reviewable-items/flagged-post";
-import QueuedPost from "discourse/lib/reviewable-items/queued-post";
-import ReviewableUser from "discourse/lib/reviewable-items/user";
-
-const CLASS_FOR_TYPE = {
- ReviewableFlaggedPost: FlaggedPost,
- ReviewableQueuedPost: QueuedPost,
- ReviewableUser,
-};
-
-export function getRenderDirector(
- type,
- reviewable,
- currentUser,
- siteSettings,
- site
-) {
- const klass = CLASS_FOR_TYPE[type] || ReviewableItemBase;
- return new klass({ reviewable, currentUser, siteSettings, site });
-}
diff --git a/app/assets/javascripts/discourse/app/lib/reviewable-types-manager.js b/app/assets/javascripts/discourse/app/lib/reviewable-types-manager.js
new file mode 100644
index 00000000000..65988330ec9
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/reviewable-types-manager.js
@@ -0,0 +1,22 @@
+import ReviewableTypeBase from "discourse/lib/reviewable-types/base";
+
+import FlaggedPost from "discourse/lib/reviewable-types/flagged-post";
+import QueuedPost from "discourse/lib/reviewable-types/queued-post";
+import ReviewableUser from "discourse/lib/reviewable-types/user";
+
+const CLASS_FOR_TYPE = {
+ ReviewableFlaggedPost: FlaggedPost,
+ ReviewableQueuedPost: QueuedPost,
+ ReviewableUser,
+};
+
+export function getRenderDirector(
+ type,
+ reviewable,
+ currentUser,
+ siteSettings,
+ site
+) {
+ const klass = CLASS_FOR_TYPE[type] || ReviewableTypeBase;
+ return new klass({ reviewable, currentUser, siteSettings, site });
+}
diff --git a/app/assets/javascripts/discourse/app/lib/reviewable-items/base.js b/app/assets/javascripts/discourse/app/lib/reviewable-types/base.js
similarity index 93%
rename from app/assets/javascripts/discourse/app/lib/reviewable-items/base.js
rename to app/assets/javascripts/discourse/app/lib/reviewable-types/base.js
index b8a71c36212..58cfe9f360b 100644
--- a/app/assets/javascripts/discourse/app/lib/reviewable-items/base.js
+++ b/app/assets/javascripts/discourse/app/lib/reviewable-types/base.js
@@ -1,6 +1,6 @@
import I18n from "I18n";
-export default class ReviewableItemBase {
+export default class ReviewableTypeBase {
constructor({ reviewable, currentUser, siteSettings, site }) {
this.reviewable = reviewable;
this.currentUser = currentUser;
diff --git a/app/assets/javascripts/discourse/app/lib/reviewable-items/flagged-post.js b/app/assets/javascripts/discourse/app/lib/reviewable-types/flagged-post.js
similarity index 80%
rename from app/assets/javascripts/discourse/app/lib/reviewable-items/flagged-post.js
rename to app/assets/javascripts/discourse/app/lib/reviewable-types/flagged-post.js
index 6046f2bf9b3..d0a49dc323d 100644
--- a/app/assets/javascripts/discourse/app/lib/reviewable-items/flagged-post.js
+++ b/app/assets/javascripts/discourse/app/lib/reviewable-types/flagged-post.js
@@ -1,8 +1,8 @@
-import ReviewableItemBase from "discourse/lib/reviewable-items/base";
+import ReviewableTypeBase from "discourse/lib/reviewable-types/base";
import { htmlSafe } from "@ember/template";
import I18n from "I18n";
-export default class extends ReviewableItemBase {
+export default class extends ReviewableTypeBase {
get description() {
const title = this.reviewable.topic_fancy_title;
const postNumber = this.reviewable.post_number;
diff --git a/app/assets/javascripts/discourse/app/lib/reviewable-items/queued-post.js b/app/assets/javascripts/discourse/app/lib/reviewable-types/queued-post.js
similarity index 86%
rename from app/assets/javascripts/discourse/app/lib/reviewable-items/queued-post.js
rename to app/assets/javascripts/discourse/app/lib/reviewable-types/queued-post.js
index cfe53941f9b..8ef928b54e1 100644
--- a/app/assets/javascripts/discourse/app/lib/reviewable-items/queued-post.js
+++ b/app/assets/javascripts/discourse/app/lib/reviewable-types/queued-post.js
@@ -1,10 +1,10 @@
-import ReviewableItemBase from "discourse/lib/reviewable-items/base";
+import ReviewableTypeBase from "discourse/lib/reviewable-types/base";
import { htmlSafe } from "@ember/template";
import { escapeExpression } from "discourse/lib/utilities";
import { emojiUnescape } from "discourse/lib/text";
import I18n from "I18n";
-export default class extends ReviewableItemBase {
+export default class extends ReviewableTypeBase {
get actor() {
return I18n.t("user_menu.reviewable.queue");
}
diff --git a/app/assets/javascripts/discourse/app/lib/reviewable-items/user.js b/app/assets/javascripts/discourse/app/lib/reviewable-types/user.js
similarity index 64%
rename from app/assets/javascripts/discourse/app/lib/reviewable-items/user.js
rename to app/assets/javascripts/discourse/app/lib/reviewable-types/user.js
index fd4cfb12999..b3d7f1040ef 100644
--- a/app/assets/javascripts/discourse/app/lib/reviewable-items/user.js
+++ b/app/assets/javascripts/discourse/app/lib/reviewable-types/user.js
@@ -1,7 +1,7 @@
-import ReviewableItemBase from "discourse/lib/reviewable-items/base";
+import ReviewableTypeBase from "discourse/lib/reviewable-types/base";
import I18n from "I18n";
-export default class extends ReviewableItemBase {
+export default class extends ReviewableTypeBase {
get description() {
return I18n.t("user_menu.reviewable.suspicious_user", {
username: this.reviewable.username,
diff --git a/app/assets/javascripts/discourse/app/lib/user-menu/base-item.js b/app/assets/javascripts/discourse/app/lib/user-menu/base-item.js
new file mode 100644
index 00000000000..39baad43981
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/user-menu/base-item.js
@@ -0,0 +1,29 @@
+export default class UserMenuBaseItem {
+ get className() {}
+
+ get linkHref() {
+ throw new Error("not implemented");
+ }
+
+ get linkTitle() {
+ throw new Error("not implemented");
+ }
+
+ get icon() {
+ throw new Error("not implemented");
+ }
+
+ get label() {
+ throw new Error("not implemented");
+ }
+
+ get labelClass() {}
+
+ get description() {
+ throw new Error("not implemented");
+ }
+
+ get descriptionClass() {}
+
+ get topicId() {}
+}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/bookmark-item.js b/app/assets/javascripts/discourse/app/lib/user-menu/bookmark-item.js
similarity index 68%
rename from app/assets/javascripts/discourse/app/components/user-menu/bookmark-item.js
rename to app/assets/javascripts/discourse/app/lib/user-menu/bookmark-item.js
index 4c0d2583eb2..9f69b04f10a 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/bookmark-item.js
+++ b/app/assets/javascripts/discourse/app/lib/user-menu/bookmark-item.js
@@ -1,7 +1,12 @@
-import UserMenuItem from "discourse/components/user-menu/menu-item";
+import UserMenuBaseItem from "discourse/lib/user-menu/base-item";
import { NO_REMINDER_ICON } from "discourse/models/bookmark";
-export default class UserMenuBookmarkItem extends UserMenuItem {
+export default class UserMenuBookmarkItem extends UserMenuBaseItem {
+ constructor({ bookmark }) {
+ super(...arguments);
+ this.bookmark = bookmark;
+ }
+
get className() {
return "bookmark";
}
@@ -29,8 +34,4 @@ export default class UserMenuBookmarkItem extends UserMenuItem {
get topicId() {
return this.bookmark.topic_id;
}
-
- get bookmark() {
- return this.args.item;
- }
}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/message-item.js b/app/assets/javascripts/discourse/app/lib/user-menu/message-item.js
similarity index 67%
rename from app/assets/javascripts/discourse/app/components/user-menu/message-item.js
rename to app/assets/javascripts/discourse/app/lib/user-menu/message-item.js
index 81cc8151c07..d42909d14da 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/message-item.js
+++ b/app/assets/javascripts/discourse/app/lib/user-menu/message-item.js
@@ -1,9 +1,15 @@
-import UserMenuItem from "discourse/components/user-menu/menu-item";
+import UserMenuBaseItem from "discourse/lib/user-menu/base-item";
import { postUrl } from "discourse/lib/utilities";
import { htmlSafe } from "@ember/template";
+import { emojiUnescape } from "discourse/lib/text";
import I18n from "I18n";
-export default class UserMenuMessageItem extends UserMenuItem {
+export default class UserMenuMessageItem extends UserMenuBaseItem {
+ constructor({ message }) {
+ super(...arguments);
+ this.message = message;
+ }
+
get className() {
return "message";
}
@@ -29,14 +35,10 @@ export default class UserMenuMessageItem extends UserMenuItem {
}
get description() {
- return htmlSafe(this.message.fancy_title);
+ return htmlSafe(emojiUnescape(this.message.fancy_title));
}
get topicId() {
return this.message.id;
}
-
- get message() {
- return this.args.item;
- }
}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/notification-item.js b/app/assets/javascripts/discourse/app/lib/user-menu/notification-item.js
similarity index 53%
rename from app/assets/javascripts/discourse/app/components/user-menu/notification-item.js
rename to app/assets/javascripts/discourse/app/lib/user-menu/notification-item.js
index ffb41c123d8..1ca293407e1 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/notification-item.js
+++ b/app/assets/javascripts/discourse/app/lib/user-menu/notification-item.js
@@ -1,45 +1,28 @@
-import UserMenuItem from "discourse/components/user-menu/menu-item";
-import { setTransientHeader } from "discourse/lib/ajax";
-import { action } from "@ember/object";
-import { getRenderDirector } from "discourse/lib/notification-item";
-import getURL from "discourse-common/lib/get-url";
+import UserMenuBaseItem from "discourse/lib/user-menu/base-item";
import cookie from "discourse/lib/cookie";
-import { inject as service } from "@ember/service";
+import getURL from "discourse-common/lib/get-url";
+import { setTransientHeader } from "discourse/lib/ajax";
+import { getRenderDirector } from "discourse/lib/notification-types-manager";
-export default class UserMenuNotificationItem extends UserMenuItem {
- @service currentUser;
- @service siteSettings;
- @service site;
-
- constructor() {
+export default class UserMenuNotificationItem extends UserMenuBaseItem {
+ constructor({ notification, currentUser, siteSettings, site }) {
super(...arguments);
+ this.notification = notification;
+ this.currentUser = currentUser;
+ this.siteSettings = siteSettings;
+ this.site = site;
+
this.renderDirector = getRenderDirector(
this.#notificationName,
- this.notification,
- this.currentUser,
- this.siteSettings,
- this.site
+ notification,
+ currentUser,
+ siteSettings,
+ site
);
}
get className() {
- const classes = ["notification"];
- if (this.notification.read) {
- classes.push("read");
- } else {
- classes.push("unread");
- }
- if (this.#notificationName) {
- classes.push(this.#notificationName.replace(/_/g, "-"));
- }
- if (this.notification.is_warning) {
- classes.push("is-warning");
- }
- const extras = this.renderDirector.classNames;
- if (extras?.length) {
- classes.push(...extras);
- }
- return classes.join(" ");
+ return this.renderDirector.classNames?.join(" ") || "";
}
get linkHref() {
@@ -74,21 +57,15 @@ export default class UserMenuNotificationItem extends UserMenuItem {
return this.notification.topic_id;
}
- get notification() {
- return this.args.item;
- }
-
get #notificationName() {
return this.site.notificationLookup[this.notification.notification_type];
}
- @action
onClick() {
if (!this.notification.read) {
this.notification.set("read", true);
setTransientHeader("Discourse-Clear-Notifications", this.notification.id);
cookie("cn", this.notification.id, { path: getURL("/") });
}
- this.renderDirector.onClick();
}
}
diff --git a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.js b/app/assets/javascripts/discourse/app/lib/user-menu/reviewable-item.js
similarity index 66%
rename from app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.js
rename to app/assets/javascripts/discourse/app/lib/user-menu/reviewable-item.js
index e10d64a35cd..abdd2e2e637 100644
--- a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.js
+++ b/app/assets/javascripts/discourse/app/lib/user-menu/reviewable-item.js
@@ -1,16 +1,15 @@
-import UserMenuItem from "discourse/components/user-menu/menu-item";
+import UserMenuBaseItem from "discourse/lib/user-menu/base-item";
import getURL from "discourse-common/lib/get-url";
-import { getRenderDirector } from "discourse/lib/reviewable-item";
-import { inject as service } from "@ember/service";
+import { getRenderDirector } from "discourse/lib/reviewable-types-manager";
-export default class UserMenuReviewableItem extends UserMenuItem {
- @service currentUser;
- @service siteSettings;
- @service site;
-
- constructor() {
+export default class UserMenuReviewableItem extends UserMenuBaseItem {
+ constructor({ reviewable, currentUser, siteSettings, site }) {
super(...arguments);
- this.reviewable = this.args.item;
+ this.reviewable = reviewable;
+ this.currentUser = currentUser;
+ this.siteSettings = siteSettings;
+ this.site = site;
+
this.renderDirector = getRenderDirector(
this.reviewable.type,
this.reviewable,
diff --git a/app/assets/javascripts/discourse/tests/helpers/notification-items-helper.js b/app/assets/javascripts/discourse/tests/helpers/notification-types-helper.js
similarity index 85%
rename from app/assets/javascripts/discourse/tests/helpers/notification-items-helper.js
rename to app/assets/javascripts/discourse/tests/helpers/notification-types-helper.js
index 4f05a666cb9..5c9c10481e9 100644
--- a/app/assets/javascripts/discourse/tests/helpers/notification-items-helper.js
+++ b/app/assets/javascripts/discourse/tests/helpers/notification-types-helper.js
@@ -1,4 +1,4 @@
-import { getRenderDirector } from "discourse/lib/notification-item";
+import { getRenderDirector } from "discourse/lib/notification-types-manager";
import sessionFixtures from "discourse/tests/fixtures/session-fixtures";
import User from "discourse/models/user";
import Site from "discourse/models/site";
diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
index c23824b4557..332ca5748d2 100644
--- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
+++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js
@@ -73,7 +73,7 @@ import { clearTagsHtmlCallbacks } from "discourse/lib/render-tags";
import { clearToolbarCallbacks } from "discourse/components/d-editor";
import { clearExtraHeaderIcons } from "discourse/widgets/header";
import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections";
-import { resetNotificationTypeRenderers } from "discourse/lib/notification-item";
+import { resetNotificationTypeRenderers } from "discourse/lib/notification-types-manager";
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
export function currentUser() {
diff --git a/app/assets/javascripts/discourse/tests/helpers/reviewable-items-helper.js b/app/assets/javascripts/discourse/tests/helpers/reviewable-types-helper.js
similarity index 85%
rename from app/assets/javascripts/discourse/tests/helpers/reviewable-items-helper.js
rename to app/assets/javascripts/discourse/tests/helpers/reviewable-types-helper.js
index 4a0c30f1ced..5a0b2fc528c 100644
--- a/app/assets/javascripts/discourse/tests/helpers/reviewable-items-helper.js
+++ b/app/assets/javascripts/discourse/tests/helpers/reviewable-types-helper.js
@@ -1,4 +1,4 @@
-import { getRenderDirector } from "discourse/lib/reviewable-item";
+import { getRenderDirector } from "discourse/lib/reviewable-types-manager";
import sessionFixtures from "discourse/tests/fixtures/session-fixtures";
import User from "discourse/models/user";
import Site from "discourse/models/site";
diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/bookmark-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/bookmark-item-test.js
deleted file mode 100644
index 8a6d98c2dfb..00000000000
--- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/bookmark-item-test.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import { module, test } from "qunit";
-import { setupRenderingTest } from "discourse/tests/helpers/component-test";
-import { query } from "discourse/tests/helpers/qunit-helpers";
-import { render } from "@ember/test-helpers";
-import { deepMerge } from "discourse-common/lib/object";
-import { hbs } from "ember-cli-htmlbars";
-
-function getBookmark(overrides = {}) {
- return deepMerge(
- {
- id: 6,
- created_at: "2022-08-05T06:09:39.559Z",
- updated_at: "2022-08-05T06:11:27.246Z",
- name: "",
- reminder_at: "2022-08-05T06:10:42.223Z",
- reminder_at_ics_start: "20220805T061042Z",
- reminder_at_ics_end: "20220805T071042Z",
- pinned: false,
- title: "Test poll topic hello world",
- fancy_title: "Test poll topic hello world",
- excerpt: "poll",
- bookmarkable_id: 1009,
- bookmarkable_type: "Post",
- bookmarkable_url: "http://localhost:4200/t/this-bookmarkable-url/227/1",
- tags: [],
- tags_descriptions: {},
- truncated: true,
- topic_id: 227,
- linked_post_number: 1,
- deleted: false,
- hidden: false,
- category_id: 1,
- closed: false,
- archived: false,
- archetype: "regular",
- highest_post_number: 45,
- last_read_post_number: 31,
- bumped_at: "2022-04-21T15:14:37.359Z",
- slug: "test-poll-topic-hello-world",
- user: {
- id: 1,
- username: "somebody",
- name: "Mr. Somebody",
- avatar_template: "/letter_avatar_proxy/v4/letter/o/f05b48/{size}.png",
- },
- },
- overrides
- );
-}
-
-module("Integration | Component | user-menu | bookmark-item", function (hooks) {
- setupRenderingTest(hooks);
-
- const template = hbs``;
-
- test("uses bookmarkable_url for the href", async function (assert) {
- this.set("bookmark", getBookmark());
- await render(template);
- assert.ok(
- query("li.bookmark a").href.endsWith("/t/this-bookmarkable-url/227/1")
- );
- });
-
- test("item label is the bookmarked post author", async function (assert) {
- this.set(
- "bookmark",
- getBookmark({ user: { username: "bookmarkPostAuthor" } })
- );
- await render(template);
- assert.strictEqual(
- query("li.bookmark .item-label").textContent.trim(),
- "bookmarkPostAuthor"
- );
- });
-
- test("item description is the bookmark title", async function (assert) {
- this.set("bookmark", getBookmark({ title: "Custom bookmark title" }));
- await render(template);
- assert.strictEqual(
- query("li.bookmark .item-description").textContent.trim(),
- "Custom bookmark title"
- );
- });
-});
diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-item-test.js
new file mode 100644
index 00000000000..e1272892ff5
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/integration/components/user-menu/menu-item-test.js
@@ -0,0 +1,616 @@
+import { module, test } from "qunit";
+import { setupRenderingTest } from "discourse/tests/helpers/component-test";
+import { exists, query } from "discourse/tests/helpers/qunit-helpers";
+import { render, settled } from "@ember/test-helpers";
+import { cloneJSON, deepMerge } from "discourse-common/lib/object";
+import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
+import Notification from "discourse/models/notification";
+import UserMenuReviewable from "discourse/models/user-menu-reviewable";
+import { hbs } from "ember-cli-htmlbars";
+import { withPluginApi } from "discourse/lib/plugin-api";
+import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
+import UserMenuMessageItem from "discourse/lib/user-menu/message-item";
+import UserMenuBookmarkItem from "discourse/lib/user-menu/bookmark-item";
+import UserMenuReviewableItem from "discourse/lib/user-menu/reviewable-item";
+import PrivateMessagesFixture from "discourse/tests/fixtures/private-messages-fixtures";
+import I18n from "I18n";
+
+function getNotification(currentUser, siteSettings, site, overrides = {}) {
+ const notification = Notification.create(
+ deepMerge(
+ {
+ id: 11,
+ user_id: 1,
+ notification_type: NOTIFICATION_TYPES.mentioned,
+ read: false,
+ high_priority: false,
+ created_at: "2022-07-01T06:00:32.173Z",
+ post_number: 113,
+ topic_id: 449,
+ fancy_title: "This is fancy title <a>!",
+ slug: "this-is-fancy-title",
+ data: {
+ topic_title: "this is title before it becomes fancy !",
+ display_username: "osama",
+ original_post_id: 1,
+ original_post_type: 1,
+ original_username: "velesin",
+ },
+ },
+ overrides
+ )
+ );
+ return new UserMenuNotificationItem({
+ notification,
+ currentUser,
+ siteSettings,
+ site,
+ });
+}
+
+module(
+ "Integration | Component | user-menu | menu-item | with notification items",
+ function (hooks) {
+ setupRenderingTest(hooks);
+
+ const template = hbs``;
+
+ test("pushes `read` to the classList if the notification is read and `unread` if it isn't", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site)
+ );
+ this.item.notification.read = false;
+ await render(template);
+ assert.notOk(exists("li.read"));
+ assert.ok(exists("li.unread"));
+
+ this.item.notification.read = true;
+ // await pauseTest();
+ await settled();
+
+ assert.ok(
+ exists("li.read"),
+ "the item re-renders when the read property is updated"
+ );
+ assert.notOk(
+ exists("li.unread"),
+ "the item re-renders when the read property is updated"
+ );
+ });
+
+ test("pushes the notification type name to the classList", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site)
+ );
+ await render(template);
+ let item = query("li");
+ assert.ok(item.classList.contains("mentioned"));
+
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site, {
+ notification_type: NOTIFICATION_TYPES.private_message,
+ })
+ );
+ await settled();
+
+ assert.ok(
+ exists("li.private-message"),
+ "replaces underscores in type name with dashes"
+ );
+ });
+
+ test("pushes is-warning to the classList if the notification originates from a warning PM", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site, {
+ is_warning: true,
+ })
+ );
+ await render(template);
+ assert.ok(exists("li.is-warning"));
+ });
+
+ test("doesn't push is-warning to the classList if the notification doesn't originate from a warning PM", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site)
+ );
+ await render(template);
+ assert.ok(!exists("li.is-warning"));
+ assert.ok(exists("li"));
+ });
+
+ test("the item's href links to the topic that the notification originates from", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site)
+ );
+ await render(template);
+ const link = query("li a");
+ assert.ok(link.href.endsWith("/t/this-is-fancy-title/449/113"));
+ });
+
+ test("the item's href links to the group messages if the notification is for a group messages", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site, {
+ topic_id: null,
+ post_number: null,
+ slug: null,
+ data: {
+ group_id: 33,
+ group_name: "grouperss",
+ username: "ossaama",
+ },
+ })
+ );
+ await render(template);
+ const link = query("li a");
+ assert.ok(link.href.endsWith("/u/ossaama/messages/grouperss"));
+ });
+
+ test("the item's link has a title for accessibility", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site)
+ );
+ await render(template);
+ const link = query("li a");
+ assert.strictEqual(link.title, I18n.t("notifications.titles.mentioned"));
+ });
+
+ test("has elements for label and description", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site)
+ );
+ await render(template);
+ const label = query("li a .item-label");
+ const description = query("li a .item-description");
+
+ assert.strictEqual(
+ label.textContent.trim(),
+ "osama",
+ "the label's content is the username by default"
+ );
+
+ assert.strictEqual(
+ description.textContent.trim(),
+ "This is fancy title !",
+ "the description defaults to the fancy_title"
+ );
+ });
+
+ test("the description falls back to topic_title from data if fancy_title is absent", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site, {
+ fancy_title: null,
+ })
+ );
+ await render(template);
+ const description = query("li a .item-description");
+
+ assert.strictEqual(
+ description.textContent.trim(),
+ "this is title before it becomes fancy !",
+ "topic_title from data is rendered safely"
+ );
+ });
+
+ test("fancy_title is emoji-unescaped", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site, {
+ fancy_title: "title with emoji :phone:",
+ })
+ );
+ await render(template);
+ assert.ok(
+ exists("li a .item-description img.emoji"),
+ "emojis are unescaped when fancy_title is used for description"
+ );
+ });
+
+ test("topic_title from data is emoji-unescaped safely", async function (assert) {
+ this.set(
+ "item",
+ getNotification(this.currentUser, this.siteSettings, this.site, {
+ fancy_title: null,
+ data: {
+ topic_title: "unsafe title with unescaped emoji :phone:",
+ },
+ })
+ );
+ await render(template);
+ const description = query("li a .item-description");
+
+ assert.strictEqual(
+ description.textContent.trim(),
+ "unsafe title with unescaped emoji",
+ "topic_title is rendered safely"
+ );
+ assert.ok(
+ exists(".item-description img.emoji"),
+ "emoji is rendered correctly"
+ );
+ });
+
+ test("various aspects can be customized according to the notification's render director", async function (assert) {
+ withPluginApi("0.1", (api) => {
+ api.registerNotificationTypeRenderer(
+ "linked",
+ (NotificationTypeBase) => {
+ return class extends NotificationTypeBase {
+ get classNames() {
+ return ["additional", "classes"];
+ }
+
+ get linkHref() {
+ return "/somewhere/awesome";
+ }
+
+ get linkTitle() {
+ return "hello world this is unsafe '\"";
+ }
+
+ get icon() {
+ return "wrench";
+ }
+
+ get label() {
+ return "notification label 666 ";
+ }
+
+ get description() {
+ return "notification description 123