From 0df1c4eab2e1a15cd2414e88265fb9be329ac00b Mon Sep 17 00:00:00 2001 From: Osama Sayegh Date: Fri, 5 Aug 2022 07:55:00 +0300 Subject: [PATCH] DEV: Refactor notification/reviewable items rendering in the new user menu (#17792) Prior to this commit, we had a default Glimmer component that was responsible for handling generic rendering of notifications in the user menu, and many notification types had a custom Glimmer component that inherited from the default component to customize how they were rendered. That implementation was less than ideal because it meant plugins would have to create Glimmer components to customize notification types added by them and that would make the surface area of the API too big. This commit changes the implementation so there's only one Glimmer component for rendering notifications, and then notification types that need to be customized can create a regular JavaScript class - `renderDirector` in the code - that provides the Glimmer component with the content it should display. We also introduce an API for plugins to register a renderer for a notification type or override an existing one. Some of the changes are partially extracted from https://github.com/discourse/discourse/pull/17379. --- .../group-mentioned-notification-item.js | 11 - ...group-message-summary-notification-item.js | 23 -- .../app/components/user-menu/items-list.hbs | 2 +- .../app/components/user-menu/items-list.js | 17 +- .../liked-consolidated-notification-item.js | 21 -- .../user-menu/moved-post-notification-item.js | 8 - .../user-menu/notification-item.hbs | 12 +- .../components/user-menu/notification-item.js | 82 +++---- .../user-menu/notifications-list.js | 4 + ...eviewable-item.hbs => reviewable-item.hbs} | 0 .../components/user-menu/reviewable-item.js | 28 +++ .../components/user-menu/reviewables-list.js | 4 + .../watching-first-post-notification-item.js | 8 - .../discourse/app/lib/notification-item.js | 51 +++++ .../app/lib/notification-items/base.js | 109 ++++++++++ .../notification-items/bookmark-reminder.js} | 4 +- .../notification-items/custom.js} | 4 +- .../notification-items/granted-badge.js} | 12 +- .../lib/notification-items/group-mentioned.js | 11 + .../group-message-summary.js | 15 ++ .../notification-items/invitee-accepted.js} | 4 +- .../notification-items/liked-consolidated.js | 18 ++ .../notification-items/liked.js} | 28 +-- .../membership-request-accepted.js} | 12 +- .../membership-request-consolidated.js} | 12 +- .../app/lib/notification-items/moved-post.js | 8 + .../notification-items/watching-first-post.js | 8 + .../discourse/app/lib/plugin-api.js | 31 +++ .../discourse/app/lib/reviewable-item.js | 22 ++ .../reviewable-items/base.js} | 11 +- .../reviewable-items/flagged-post.js} | 6 +- .../reviewable-items/queued-post.js} | 6 +- .../reviewable-items/user.js} | 4 +- .../discourse/app/models/notification.js | 30 --- .../app/models/user-menu-reviewable.js | 12 -- .../helpers/notification-items-helper.js | 19 ++ .../discourse/tests/helpers/qunit-helpers.js | 2 + .../tests/helpers/reviewable-items-helper.js | 15 ++ ...ookmark-reminder-notification-item-test.js | 107 ---------- .../granted-badge-notification-item-test.js | 63 ------ .../group-mentioned-notification-item-test.js | 68 ------ ...-message-summary-notification-item-test.js | 55 ----- ...ked-consolidated-notification-item-test.js | 71 ------- .../user-menu/liked-notification-item-test.js | 114 ---------- .../user-menu/notification-item-test.js | 200 +++++++++++++++++- ...e-item-test.js => reviewable-item-test.js} | 6 +- .../reviewable-queued-post-item-test.js | 77 ------- .../bookmark-reminder-test.js | 85 ++++++++ .../notification-items/granted-badge-test.js | 59 ++++++ .../group-mentioned-test.js | 51 +++++ .../group-message-summary-test.js | 51 +++++ .../liked-consolidated-test.js | 62 ++++++ .../unit/lib/notification-items/liked-test.js | 73 +++++++ .../lib/reviewable-items/queued-post-test.js | 56 +++++ .../stylesheets/common/base/menu-panel.scss | 19 +- 55 files changed, 1085 insertions(+), 806 deletions(-) delete mode 100644 app/assets/javascripts/discourse/app/components/user-menu/group-mentioned-notification-item.js delete mode 100644 app/assets/javascripts/discourse/app/components/user-menu/group-message-summary-notification-item.js delete mode 100644 app/assets/javascripts/discourse/app/components/user-menu/liked-consolidated-notification-item.js delete mode 100644 app/assets/javascripts/discourse/app/components/user-menu/moved-post-notification-item.js rename app/assets/javascripts/discourse/app/components/user-menu/{default-reviewable-item.hbs => reviewable-item.hbs} (100%) create mode 100644 app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.js delete mode 100644 app/assets/javascripts/discourse/app/components/user-menu/watching-first-post-notification-item.js create mode 100644 app/assets/javascripts/discourse/app/lib/notification-item.js create mode 100644 app/assets/javascripts/discourse/app/lib/notification-items/base.js rename app/assets/javascripts/discourse/app/{components/user-menu/bookmark-reminder-notification-item.js => lib/notification-items/bookmark-reminder.js} (65%) rename app/assets/javascripts/discourse/app/{components/user-menu/custom-notification-item.js => lib/notification-items/custom.js} (59%) rename app/assets/javascripts/discourse/app/{components/user-menu/granted-badge-notification-item.js => lib/notification-items/granted-badge.js} (77%) create mode 100644 app/assets/javascripts/discourse/app/lib/notification-items/group-mentioned.js create mode 100644 app/assets/javascripts/discourse/app/lib/notification-items/group-message-summary.js rename app/assets/javascripts/discourse/app/{components/user-menu/invitee-accepted-notification-item.js => lib/notification-items/invitee-accepted.js} (57%) create mode 100644 app/assets/javascripts/discourse/app/lib/notification-items/liked-consolidated.js rename app/assets/javascripts/discourse/app/{components/user-menu/liked-notification-item.js => lib/notification-items/liked.js} (67%) rename app/assets/javascripts/discourse/app/{components/user-menu/membership-request-accepted-notification-item.js => lib/notification-items/membership-request-accepted.js} (59%) rename app/assets/javascripts/discourse/app/{components/user-menu/membership-request-consolidated-notification-item.js => lib/notification-items/membership-request-consolidated.js} (64%) create mode 100644 app/assets/javascripts/discourse/app/lib/notification-items/moved-post.js create mode 100644 app/assets/javascripts/discourse/app/lib/notification-items/watching-first-post.js create mode 100644 app/assets/javascripts/discourse/app/lib/reviewable-item.js rename app/assets/javascripts/discourse/app/{components/user-menu/default-reviewable-item.js => lib/reviewable-items/base.js} (62%) rename app/assets/javascripts/discourse/app/{components/user-menu/reviewable-flagged-post-item.js => lib/reviewable-items/flagged-post.js} (70%) rename app/assets/javascripts/discourse/app/{components/user-menu/reviewable-queued-post-item.js => lib/reviewable-items/queued-post.js} (78%) rename app/assets/javascripts/discourse/app/{components/user-menu/reviewable-user-item.js => lib/reviewable-items/user.js} (51%) create mode 100644 app/assets/javascripts/discourse/tests/helpers/notification-items-helper.js create mode 100644 app/assets/javascripts/discourse/tests/helpers/reviewable-items-helper.js delete mode 100644 app/assets/javascripts/discourse/tests/integration/components/user-menu/bookmark-reminder-notification-item-test.js delete mode 100644 app/assets/javascripts/discourse/tests/integration/components/user-menu/granted-badge-notification-item-test.js delete mode 100644 app/assets/javascripts/discourse/tests/integration/components/user-menu/group-mentioned-notification-item-test.js delete mode 100644 app/assets/javascripts/discourse/tests/integration/components/user-menu/group-message-summary-notification-item-test.js delete mode 100644 app/assets/javascripts/discourse/tests/integration/components/user-menu/liked-consolidated-notification-item-test.js delete mode 100644 app/assets/javascripts/discourse/tests/integration/components/user-menu/liked-notification-item-test.js rename app/assets/javascripts/discourse/tests/integration/components/user-menu/{default-reviewable-item-test.js => reviewable-item-test.js} (92%) delete mode 100644 app/assets/javascripts/discourse/tests/integration/components/user-menu/reviewable-queued-post-item-test.js create mode 100644 app/assets/javascripts/discourse/tests/unit/lib/notification-items/bookmark-reminder-test.js create mode 100644 app/assets/javascripts/discourse/tests/unit/lib/notification-items/granted-badge-test.js create mode 100644 app/assets/javascripts/discourse/tests/unit/lib/notification-items/group-mentioned-test.js create mode 100644 app/assets/javascripts/discourse/tests/unit/lib/notification-items/group-message-summary-test.js create mode 100644 app/assets/javascripts/discourse/tests/unit/lib/notification-items/liked-consolidated-test.js create mode 100644 app/assets/javascripts/discourse/tests/unit/lib/notification-items/liked-test.js create mode 100644 app/assets/javascripts/discourse/tests/unit/lib/reviewable-items/queued-post-test.js diff --git a/app/assets/javascripts/discourse/app/components/user-menu/group-mentioned-notification-item.js b/app/assets/javascripts/discourse/app/components/user-menu/group-mentioned-notification-item.js deleted file mode 100644 index 217709f9d18..00000000000 --- a/app/assets/javascripts/discourse/app/components/user-menu/group-mentioned-notification-item.js +++ /dev/null @@ -1,11 +0,0 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; - -export default class UserMenuGroupMentionedNotificationItem extends UserMenuNotificationItem { - get label() { - return `${this.username} @${this.notification.data.group_name}`; - } - - get labelWrapperClasses() { - return "mention-group notify"; - } -} diff --git a/app/assets/javascripts/discourse/app/components/user-menu/group-message-summary-notification-item.js b/app/assets/javascripts/discourse/app/components/user-menu/group-message-summary-notification-item.js deleted file mode 100644 index 2f4eda9f5f5..00000000000 --- a/app/assets/javascripts/discourse/app/components/user-menu/group-message-summary-notification-item.js +++ /dev/null @@ -1,23 +0,0 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; -import I18n from "I18n"; - -export default class UserMenuGroupMessageSummaryNotificationItem extends UserMenuNotificationItem { - get inboxCount() { - return this.notification.data.inbox_count; - } - - get label() { - return I18n.t("notifications.group_message_summary", { - count: this.inboxCount, - group_name: this.notification.data.group_name, - }); - } - - get wrapLabel() { - return false; - } - - get description() { - return null; - } -} 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 4961f29bf5b..fab0afadc89 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 31585618afb..47a46b7f72f 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 @@ -28,6 +28,12 @@ export default class UserMenuItemsList extends GlimmerComponent { 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}` @@ -51,17 +57,6 @@ export default class UserMenuItemsList extends GlimmerComponent { } this.fetchItems() .then((items) => { - const valid = items.every((item) => { - if (!item.userMenuComponent) { - // eslint-disable-next-line no-console - console.error("userMenuComponent property is blank on", item); - return false; - } - return true; - }); - if (!valid) { - throw new Error("userMenuComponent must be present on all items"); - } this._setCachedItems(items); this.items = items; }) diff --git a/app/assets/javascripts/discourse/app/components/user-menu/liked-consolidated-notification-item.js b/app/assets/javascripts/discourse/app/components/user-menu/liked-consolidated-notification-item.js deleted file mode 100644 index 1591c4d6476..00000000000 --- a/app/assets/javascripts/discourse/app/components/user-menu/liked-consolidated-notification-item.js +++ /dev/null @@ -1,21 +0,0 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; -import { userPath } from "discourse/lib/url"; -import I18n from "I18n"; - -export default class UserMenuLikedConsolidatedNotificationItem extends UserMenuNotificationItem { - get linkHref() { - return userPath( - `${ - this.notification.username || this.currentUser.username - }/notifications/likes-received?acting_username=${ - this.notification.data.username - }` - ); - } - - get description() { - return I18n.t("notifications.liked_consolidated_description", { - count: this.notification.data.count, - }); - } -} diff --git a/app/assets/javascripts/discourse/app/components/user-menu/moved-post-notification-item.js b/app/assets/javascripts/discourse/app/components/user-menu/moved-post-notification-item.js deleted file mode 100644 index e4394918fae..00000000000 --- a/app/assets/javascripts/discourse/app/components/user-menu/moved-post-notification-item.js +++ /dev/null @@ -1,8 +0,0 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; -import I18n from "I18n"; - -export default class UserMenuMovedPostNotificationItem extends UserMenuNotificationItem { - get label() { - return I18n.t("notifications.user_moved_post", { username: this.username }); - } -} diff --git a/app/assets/javascripts/discourse/app/components/user-menu/notification-item.hbs b/app/assets/javascripts/discourse/app/components/user-menu/notification-item.hbs index fcb6c90b1ec..b9e155ad1c6 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/notification-item.hbs +++ b/app/assets/javascripts/discourse/app/components/user-menu/notification-item.hbs @@ -7,17 +7,13 @@ {{d-icon this.icon}}
{{#if this.label}} - {{#if this.wrapLabel}} - - {{this.label}} - - {{else}} - {{this.label}} - {{/if}} + + {{this.label}} + {{/if}} {{#if this.description}} {{this.description}} diff --git a/app/assets/javascripts/discourse/app/components/user-menu/notification-item.js b/app/assets/javascripts/discourse/app/components/user-menu/notification-item.js index 007cc63830a..084c84962ce 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/notification-item.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/notification-item.js @@ -1,98 +1,73 @@ import GlimmerComponent from "discourse/components/glimmer"; -import { formatUsername, postUrl } from "discourse/lib/utilities"; -import { userPath } from "discourse/lib/url"; import { setTransientHeader } from "discourse/lib/ajax"; import { action } from "@ember/object"; -import { emojiUnescape } from "discourse/lib/text"; -import { htmlSafe } from "@ember/template"; +import { getRenderDirector } from "discourse/lib/notification-item"; import getURL from "discourse-common/lib/get-url"; import cookie from "discourse/lib/cookie"; -import I18n from "I18n"; export default class UserMenuNotificationItem extends GlimmerComponent { + constructor() { + super(...arguments); + this.renderDirector = getRenderDirector( + this.#notificationName, + this.notification, + this.currentUser, + this.siteSettings, + this.site + ); + } + get className() { const classes = []; if (this.notification.read) { classes.push("read"); } - if (this.notificationName) { - classes.push(this.notificationName.replace(/_/g, "-")); + 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(" "); } get linkHref() { - if (this.topicId) { - return postUrl( - this.notification.slug, - this.topicId, - this.notification.post_number - ); - } - if (this.notification.data.group_id) { - return userPath( - `${this.notification.data.username}/messages/${this.notification.data.group_name}` - ); - } + return this.renderDirector.linkHref; } get linkTitle() { - if (this.notificationName) { - return I18n.t(`notifications.titles.${this.notificationName}`); - } else { - return ""; - } + return this.renderDirector.linkTitle; } get icon() { - return `notification.${this.notificationName}`; + return this.renderDirector.icon; } get label() { - return this.username; + return this.renderDirector.label; } - get wrapLabel() { - return true; - } - - get labelWrapperClasses() {} - - get username() { - return formatUsername(this.notification.data.display_username); + get labelWrapperClasses() { + return this.renderDirector.labelWrapperClasses?.join(" ") || ""; } get description() { - const description = - emojiUnescape(this.notification.fancy_title) || - this.notification.data.topic_title; - - if (this.descriptionHtmlSafe) { - return htmlSafe(description); - } else { - return description; - } + return this.renderDirector.description; } - get descriptionElementClasses() {} - - get descriptionHtmlSafe() { - return !!this.notification.fancy_title; + get descriptionWrapperClasses() { + return this.renderDirector.descriptionWrapperClasses?.join(" ") || ""; } - // the following props are helper props -- they're never referenced directly in the hbs template get notification() { return this.args.item; } - get topicId() { - return this.notification.topic_id; - } - - get notificationName() { + get #notificationName() { return this.site.notificationLookup[this.notification.notification_type]; } @@ -103,5 +78,6 @@ export default class UserMenuNotificationItem extends GlimmerComponent { 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/notifications-list.js b/app/assets/javascripts/discourse/app/components/user-menu/notifications-list.js index 4e5c59670dd..79110e5bcd2 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 @@ -43,6 +43,10 @@ export default class UserMenuNotificationsList extends UserMenuItemsList { } } + get itemComponent() { + return "user-menu/notification-item"; + } + fetchItems() { const params = { limit: 30, diff --git a/app/assets/javascripts/discourse/app/components/user-menu/default-reviewable-item.hbs b/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.hbs similarity index 100% rename from app/assets/javascripts/discourse/app/components/user-menu/default-reviewable-item.hbs rename to app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.hbs diff --git a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.js b/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.js new file mode 100644 index 00000000000..b08d973fbd0 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.js @@ -0,0 +1,28 @@ +import GlimmerComponent from "discourse/components/glimmer"; +import { getRenderDirector } from "discourse/lib/reviewable-item"; + +export default class UserMenuReviewableItem extends GlimmerComponent { + constructor() { + super(...arguments); + this.reviewable = this.args.item; + this.renderDirector = getRenderDirector( + this.reviewable.type, + this.reviewable, + this.currentUser, + this.siteSettings, + this.site + ); + } + + get actor() { + return this.renderDirector.actor; + } + + get description() { + return this.renderDirector.description; + } + + get icon() { + return this.renderDirector.icon; + } +} 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 61b5c87e7d2..2b1be914328 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 @@ -17,6 +17,10 @@ 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) => { diff --git a/app/assets/javascripts/discourse/app/components/user-menu/watching-first-post-notification-item.js b/app/assets/javascripts/discourse/app/components/user-menu/watching-first-post-notification-item.js deleted file mode 100644 index 7d917d2e74d..00000000000 --- a/app/assets/javascripts/discourse/app/components/user-menu/watching-first-post-notification-item.js +++ /dev/null @@ -1,8 +0,0 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; -import I18n from "I18n"; - -export default class UserMenuWatchingFirstPostNotificationItem extends UserMenuNotificationItem { - 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-item.js new file mode 100644 index 00000000000..5ffeb4af9f2 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/notification-item.js @@ -0,0 +1,51 @@ +import NotificationItemBase from "discourse/lib/notification-items/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"; + +const CLASS_FOR_TYPE = { + bookmark_reminder: BookmarkReminder, + custom: Custom, + granted_badge: GrantedBadge, + group_mentioned: GroupMentioned, + group_message_summary: GroupMessageSummary, + invitee_accepted: InviteeAccepted, + liked: Liked, + liked_consolidated: LikedConsolidated, + membership_request_accepted: MembershipRequestAccepted, + membership_request_consolidated: MembershipRequestConsolidated, + moved_post: MovedPost, + watching_first_post: WatchingFirstPost, +}; + +let _customClassForType = {}; + +export function registerNotificationTypeRenderer(notificationType, func) { + _customClassForType[notificationType] = func(NotificationItemBase); +} + +export function resetRenderDirectorForNotifictaionTypes() { + _customClassForType = {}; +} + +export function getRenderDirector( + type, + notification, + currentUser, + siteSettings, + site +) { + const klass = + _customClassForType[type] || CLASS_FOR_TYPE[type] || NotificationItemBase; + 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-items/base.js new file mode 100644 index 00000000000..455db134b0b --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/notification-items/base.js @@ -0,0 +1,109 @@ +import { formatUsername, postUrl } from "discourse/lib/utilities"; +import { userPath } from "discourse/lib/url"; +import { emojiUnescape } from "discourse/lib/text"; +import { htmlSafe } from "@ember/template"; +import I18n from "I18n"; + +export default class NotificationItemBase { + constructor({ notification, currentUser, siteSettings, site }) { + this.notification = notification; + this.currentUser = currentUser; + this.siteSettings = siteSettings; + this.site = site; + } + + /** + * @returns {string[]} An array of addtional classes that should be added to the
  • element of the notification item. + */ + get classNames() { + return []; + } + + /** + * @returns {string} A href/path that the notification item should link to. + */ + get linkHref() { + if (this.topicId) { + return postUrl( + this.notification.slug, + this.topicId, + this.notification.post_number + ); + } + if (this.notification.data.group_id) { + return userPath( + `${this.notification.data.username}/messages/${this.notification.data.group_name}` + ); + } + } + + /** + * @returns {string} A title for the notification item. It shows up when the user hovers over the notification item. + */ + get linkTitle() { + if (this.notificationName) { + return I18n.t(`notifications.titles.${this.notificationName}`); + } else { + // notifications with unknown types, e.g. notifications that come from a + // plugin that's no longer installed + return ""; + } + } + + /** + * @returns {string} An icon for the notification item. + */ + get icon() { + return `notification.${this.notificationName}`; + } + + /** + * @returns {string} The label is the first part of the text content displayed in the notification. For example, in a like notification, the username of the user who liked the post is the label. If a falsey value is returned, the label is omitted. + */ + get label() { + return this.username; + } + + /** + * @returns {string} The description is the second part of the text content displayed in the notification. For example, in a like notification, the topic title is the description. If a falsey value is returned, the description is omitted. + */ + get description() { + const description = emojiUnescape(this.notification.fancy_title); + if (description) { + return htmlSafe(description); + } else { + return this.notification.data.topic_title; + } + } + + /** + * Function that is called when the notification item is clicked. + */ + onClick() {} + + /** + * @returns {string[]} Include additional classes to the label's wrapper . + */ + get labelWrapperClasses() { + return []; + } + + /** + * @returns {string[]} Include additional classes to the description's wrapper . + */ + get descriptionWrapperClasses() { + return []; + } + + get topicId() { + return this.notification.topic_id; + } + + get username() { + return formatUsername(this.notification.data.display_username); + } + + get notificationName() { + return this.site.notificationLookup[this.notification.notification_type]; + } +} diff --git a/app/assets/javascripts/discourse/app/components/user-menu/bookmark-reminder-notification-item.js b/app/assets/javascripts/discourse/app/lib/notification-items/bookmark-reminder.js similarity index 65% rename from app/assets/javascripts/discourse/app/components/user-menu/bookmark-reminder-notification-item.js rename to app/assets/javascripts/discourse/app/lib/notification-items/bookmark-reminder.js index 519ef8ec0d0..d465f4a71b9 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/bookmark-reminder-notification-item.js +++ b/app/assets/javascripts/discourse/app/lib/notification-items/bookmark-reminder.js @@ -1,7 +1,7 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; +import NotificationItemBase from "discourse/lib/notification-items/base"; import I18n from "I18n"; -export default class UserMenuBookmarkReminderNotificationItem extends UserMenuNotificationItem { +export default class extends NotificationItemBase { 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/components/user-menu/custom-notification-item.js b/app/assets/javascripts/discourse/app/lib/notification-items/custom.js similarity index 59% rename from app/assets/javascripts/discourse/app/components/user-menu/custom-notification-item.js rename to app/assets/javascripts/discourse/app/lib/notification-items/custom.js index 269664f2701..09006e37a87 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/custom-notification-item.js +++ b/app/assets/javascripts/discourse/app/lib/notification-items/custom.js @@ -1,7 +1,7 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; +import NotificationItemBase from "discourse/lib/notification-items/base"; import I18n from "I18n"; -export default class UserMenuCustomNotificationItem extends UserMenuNotificationItem { +export default class extends NotificationItemBase { get linkTitle() { if (this.notification.data.title) { return I18n.t(this.notification.data.title); diff --git a/app/assets/javascripts/discourse/app/components/user-menu/granted-badge-notification-item.js b/app/assets/javascripts/discourse/app/lib/notification-items/granted-badge.js similarity index 77% rename from app/assets/javascripts/discourse/app/components/user-menu/granted-badge-notification-item.js rename to app/assets/javascripts/discourse/app/lib/notification-items/granted-badge.js index 5da5a681340..13e6b2ff31a 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/granted-badge-notification-item.js +++ b/app/assets/javascripts/discourse/app/lib/notification-items/granted-badge.js @@ -1,8 +1,8 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; +import NotificationItemBase from "discourse/lib/notification-items/base"; import getURL from "discourse-common/lib/get-url"; import I18n from "I18n"; -export default class UserMenuGrantedBadgeNotificationItem extends UserMenuNotificationItem { +export default class extends NotificationItemBase { get linkHref() { const badgeId = this.notification.data.badge_id; if (badgeId) { @@ -20,17 +20,13 @@ export default class UserMenuGrantedBadgeNotificationItem extends UserMenuNotifi } } - get label() { + get description() { return I18n.t("notifications.granted_badge", { description: this.notification.data.badge_name, }); } - get wrapLabel() { - return false; - } - - get description() { + get label() { return null; } } diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/group-mentioned.js b/app/assets/javascripts/discourse/app/lib/notification-items/group-mentioned.js new file mode 100644 index 00000000000..0333d6e44d8 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/notification-items/group-mentioned.js @@ -0,0 +1,11 @@ +import NotificationItemBase from "discourse/lib/notification-items/base"; + +export default class extends NotificationItemBase { + get label() { + return `${this.username} @${this.notification.data.group_name}`; + } + + get labelWrapperClasses() { + return ["mention-group", "notify"]; + } +} diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/group-message-summary.js b/app/assets/javascripts/discourse/app/lib/notification-items/group-message-summary.js new file mode 100644 index 00000000000..2c02a414fc7 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/notification-items/group-message-summary.js @@ -0,0 +1,15 @@ +import NotificationItemBase from "discourse/lib/notification-items/base"; +import I18n from "I18n"; + +export default class extends NotificationItemBase { + get description() { + return I18n.t("notifications.group_message_summary", { + count: this.notification.data.inbox_count, + group_name: this.notification.data.group_name, + }); + } + + get label() { + return null; + } +} diff --git a/app/assets/javascripts/discourse/app/components/user-menu/invitee-accepted-notification-item.js b/app/assets/javascripts/discourse/app/lib/notification-items/invitee-accepted.js similarity index 57% rename from app/assets/javascripts/discourse/app/components/user-menu/invitee-accepted-notification-item.js rename to app/assets/javascripts/discourse/app/lib/notification-items/invitee-accepted.js index e4635f2e43e..4761a6f4321 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/invitee-accepted-notification-item.js +++ b/app/assets/javascripts/discourse/app/lib/notification-items/invitee-accepted.js @@ -1,8 +1,8 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; +import NotificationItemBase from "discourse/lib/notification-items/base"; import { userPath } from "discourse/lib/url"; import I18n from "I18n"; -export default class UserMenuInviteeAcceptedNotificationItem extends UserMenuNotificationItem { +export default class extends NotificationItemBase { 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-items/liked-consolidated.js new file mode 100644 index 00000000000..639fc1a1ab2 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/notification-items/liked-consolidated.js @@ -0,0 +1,18 @@ +import NotificationItemBase from "discourse/lib/notification-items/base"; +import { userPath } from "discourse/lib/url"; +import I18n from "I18n"; + +export default class extends NotificationItemBase { + get linkHref() { + // TODO(osama): serialize username with notifications + return userPath( + `${this.currentUser.username}/notifications/likes-received?acting_username=${this.notification.data.username}` + ); + } + + get description() { + return I18n.t("notifications.liked_consolidated_description", { + count: this.notification.data.count, + }); + } +} diff --git a/app/assets/javascripts/discourse/app/components/user-menu/liked-notification-item.js b/app/assets/javascripts/discourse/app/lib/notification-items/liked.js similarity index 67% rename from app/assets/javascripts/discourse/app/components/user-menu/liked-notification-item.js rename to app/assets/javascripts/discourse/app/lib/notification-items/liked.js index e4f8b02ce67..6cae82971b6 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/liked-notification-item.js +++ b/app/assets/javascripts/discourse/app/lib/notification-items/liked.js @@ -1,26 +1,18 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; +import NotificationItemBase from "discourse/lib/notification-items/base"; import { formatUsername } from "discourse/lib/utilities"; import I18n from "I18n"; -export default class UserMenuLikedNotificationItem extends UserMenuNotificationItem { - get count() { - return this.notification.data.count; - } - - get username2() { - return formatUsername(this.notification.data.username2); - } - +export default class extends NotificationItemBase { get label() { if (this.count === 2) { return I18n.t("notifications.liked_by_2_users", { username: this.username, - username2: this.username2, + username2: this.#username2, }); } else if (this.count > 2) { return I18n.t("notifications.liked_by_multiple_users", { username: this.username, - username2: this.username2, + username2: this.#username2, count: this.count - 2, }); } else { @@ -30,9 +22,17 @@ export default class UserMenuLikedNotificationItem extends UserMenuNotificationI get labelWrapperClasses() { if (this.count === 2) { - return "double-user"; + return ["double-user"]; } else if (this.count > 2) { - return "multi-user"; + return ["multi-user"]; } } + + get count() { + return this.notification.data.count; + } + + get #username2() { + return formatUsername(this.notification.data.username2); + } } diff --git a/app/assets/javascripts/discourse/app/components/user-menu/membership-request-accepted-notification-item.js b/app/assets/javascripts/discourse/app/lib/notification-items/membership-request-accepted.js similarity index 59% rename from app/assets/javascripts/discourse/app/components/user-menu/membership-request-accepted-notification-item.js rename to app/assets/javascripts/discourse/app/lib/notification-items/membership-request-accepted.js index 850dc4c084c..d32ec41f271 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/membership-request-accepted-notification-item.js +++ b/app/assets/javascripts/discourse/app/lib/notification-items/membership-request-accepted.js @@ -1,23 +1,19 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; +import NotificationItemBase from "discourse/lib/notification-items/base"; import { groupPath } from "discourse/lib/url"; import I18n from "I18n"; -export default class UserMenuMembershipRequestAcceptedNotificationItem extends UserMenuNotificationItem { +export default class extends NotificationItemBase { get linkHref() { return groupPath(this.notification.data.group_name); } - get label() { + get description() { return I18n.t("notifications.membership_request_accepted", { group_name: this.notification.data.group_name, }); } - get wrapLabel() { - return false; - } - - get description() { + get label() { return null; } } diff --git a/app/assets/javascripts/discourse/app/components/user-menu/membership-request-consolidated-notification-item.js b/app/assets/javascripts/discourse/app/lib/notification-items/membership-request-consolidated.js similarity index 64% rename from app/assets/javascripts/discourse/app/components/user-menu/membership-request-consolidated-notification-item.js rename to app/assets/javascripts/discourse/app/lib/notification-items/membership-request-consolidated.js index 0fe0faa7282..0f52b5ea031 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/membership-request-consolidated-notification-item.js +++ b/app/assets/javascripts/discourse/app/lib/notification-items/membership-request-consolidated.js @@ -1,26 +1,22 @@ -import UserMenuNotificationItem from "discourse/components/user-menu/notification-item"; +import NotificationItemBase from "discourse/lib/notification-items/base"; import { userPath } from "discourse/lib/url"; import I18n from "I18n"; -export default class UserMenuMembershipRequestConsolidatedNotificationItem extends UserMenuNotificationItem { +export default class extends NotificationItemBase { get linkHref() { return userPath( `${this.notification.username || this.currentUser.username}/messages` ); } - get label() { + get description() { return I18n.t("notifications.membership_request_consolidated", { group_name: this.notification.data.group_name, count: this.notification.data.count, }); } - get wrapLabel() { - return false; - } - - get description() { + get label() { return null; } } diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/moved-post.js b/app/assets/javascripts/discourse/app/lib/notification-items/moved-post.js new file mode 100644 index 00000000000..58ed1bc37ef --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/notification-items/moved-post.js @@ -0,0 +1,8 @@ +import NotificationItemBase from "discourse/lib/notification-items/base"; +import I18n from "I18n"; + +export default class extends NotificationItemBase { + get label() { + return I18n.t("notifications.user_moved_post", { username: this.username }); + } +} 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 new file mode 100644 index 00000000000..abcecc3b7ca --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/notification-items/watching-first-post.js @@ -0,0 +1,8 @@ +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/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 84eb5dbe7fd..2f1489866b1 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -98,6 +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"; // If you add any methods to the API ensure you bump up the version number // based on Semantic Versioning 2.0.0. Please update the changelog at @@ -1851,6 +1852,36 @@ class PluginApi { addSidebarSection(func) { addSidebarSection(func); } + + /** + * 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 + * documentation and the default renderer. + * + * ``` + * api.registerNotificationTypeRenderer("your_notification_type", (NotificationItemBase) => { + * return class extends NotificationItemBase { + * get label() { + * return "some label"; + * } + * + * get description() { + * return "fancy description"; + * } + * }; + * }); + * ``` + * @callback renderDirectorRegistererCallback + * @param {NotificationItemBase} The base class from which the returned class should inherit. + * @returns {NotificationItemBase} A class that inherits from NotificationItemBase. + * + * @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. + */ + registerNotificationTypeRenderer(notificationType, func) { + registerNotificationTypeRenderer(notificationType, func); + } } // from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number diff --git a/app/assets/javascripts/discourse/app/lib/reviewable-item.js b/app/assets/javascripts/discourse/app/lib/reviewable-item.js new file mode 100644 index 00000000000..7ef29025043 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/reviewable-item.js @@ -0,0 +1,22 @@ +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/components/user-menu/default-reviewable-item.js b/app/assets/javascripts/discourse/app/lib/reviewable-items/base.js similarity index 62% rename from app/assets/javascripts/discourse/app/components/user-menu/default-reviewable-item.js rename to app/assets/javascripts/discourse/app/lib/reviewable-items/base.js index 7cf8154dc5c..b8a71c36212 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/default-reviewable-item.js +++ b/app/assets/javascripts/discourse/app/lib/reviewable-items/base.js @@ -1,10 +1,11 @@ -import GlimmerComponent from "discourse/components/glimmer"; import I18n from "I18n"; -export default class UserMenuReviewableItem extends GlimmerComponent { - constructor() { - super(...arguments); - this.reviewable = this.args.item; +export default class ReviewableItemBase { + constructor({ reviewable, currentUser, siteSettings, site }) { + this.reviewable = reviewable; + this.currentUser = currentUser; + this.siteSettings = siteSettings; + this.site = site; } get actor() { diff --git a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-flagged-post-item.js b/app/assets/javascripts/discourse/app/lib/reviewable-items/flagged-post.js similarity index 70% rename from app/assets/javascripts/discourse/app/components/user-menu/reviewable-flagged-post-item.js rename to app/assets/javascripts/discourse/app/lib/reviewable-items/flagged-post.js index 215972e6ea7..6046f2bf9b3 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-flagged-post-item.js +++ b/app/assets/javascripts/discourse/app/lib/reviewable-items/flagged-post.js @@ -1,8 +1,8 @@ -import UserMenuDefaultReviewableItem from "discourse/components/user-menu/default-reviewable-item"; -import I18n from "I18n"; +import ReviewableItemBase from "discourse/lib/reviewable-items/base"; import { htmlSafe } from "@ember/template"; +import I18n from "I18n"; -export default class UserMenuReviewableFlaggedPostItem extends UserMenuDefaultReviewableItem { +export default class extends ReviewableItemBase { get description() { const title = this.reviewable.topic_fancy_title; const postNumber = this.reviewable.post_number; diff --git a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-queued-post-item.js b/app/assets/javascripts/discourse/app/lib/reviewable-items/queued-post.js similarity index 78% rename from app/assets/javascripts/discourse/app/components/user-menu/reviewable-queued-post-item.js rename to app/assets/javascripts/discourse/app/lib/reviewable-items/queued-post.js index 814fe109a2a..cfe53941f9b 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-queued-post-item.js +++ b/app/assets/javascripts/discourse/app/lib/reviewable-items/queued-post.js @@ -1,10 +1,10 @@ -import UserMenuDefaultReviewableItem from "discourse/components/user-menu/default-reviewable-item"; -import I18n from "I18n"; +import ReviewableItemBase from "discourse/lib/reviewable-items/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 UserMenuReviewableQueuedPostItem extends UserMenuDefaultReviewableItem { +export default class extends ReviewableItemBase { get actor() { return I18n.t("user_menu.reviewable.queue"); } diff --git a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-user-item.js b/app/assets/javascripts/discourse/app/lib/reviewable-items/user.js similarity index 51% rename from app/assets/javascripts/discourse/app/components/user-menu/reviewable-user-item.js rename to app/assets/javascripts/discourse/app/lib/reviewable-items/user.js index 3eb90dcdcee..fd4cfb12999 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-user-item.js +++ b/app/assets/javascripts/discourse/app/lib/reviewable-items/user.js @@ -1,7 +1,7 @@ -import UserMenuDefaultReviewableItem from "discourse/components/user-menu/default-reviewable-item"; +import ReviewableItemBase from "discourse/lib/reviewable-items/base"; import I18n from "I18n"; -export default class UserMenuReviewableUserItem extends UserMenuDefaultReviewableItem { +export default class extends ReviewableItemBase { get description() { return I18n.t("user_menu.reviewable.suspicious_user", { username: this.reviewable.username, diff --git a/app/assets/javascripts/discourse/app/models/notification.js b/app/assets/javascripts/discourse/app/models/notification.js index 4ef83473c81..6e917b91829 100644 --- a/app/assets/javascripts/discourse/app/models/notification.js +++ b/app/assets/javascripts/discourse/app/models/notification.js @@ -1,36 +1,6 @@ import RestModel from "discourse/models/rest"; import { tracked } from "@glimmer/tracking"; -const DEFAULT_ITEM = "user-menu/notification-item"; - -function defaultComponentForType() { - return { - bookmark_reminder: "user-menu/bookmark-reminder-notification-item", - custom: "user-menu/custom-notification-item", - granted_badge: "user-menu/granted-badge-notification-item", - group_mentioned: "user-menu/group-mentioned-notification-item", - group_message_summary: "user-menu/group-message-summary-notification-item", - invitee_accepted: "user-menu/invitee-accepted-notification-item", - liked: "user-menu/liked-notification-item", - liked_consolidated: "user-menu/liked-consolidated-notification-item", - membership_request_accepted: - "user-menu/membership-request-accepted-notification-item", - membership_request_consolidated: - "user-menu/membership-request-consolidated-notification-item", - moved_post: "user-menu/moved-post-notification-item", - watching_first_post: "user-menu/watching-first-post-notification-item", - }; -} - -let _componentForType = defaultComponentForType(); -// TODO(osama): add plugin API - export default class Notification extends RestModel { @tracked read; - - get userMenuComponent() { - const component = - _componentForType[this.site.notificationLookup[this.notification_type]]; - return component || DEFAULT_ITEM; - } } diff --git a/app/assets/javascripts/discourse/app/models/user-menu-reviewable.js b/app/assets/javascripts/discourse/app/models/user-menu-reviewable.js index 954c98772c6..42fe246ac24 100644 --- a/app/assets/javascripts/discourse/app/models/user-menu-reviewable.js +++ b/app/assets/javascripts/discourse/app/models/user-menu-reviewable.js @@ -1,18 +1,6 @@ import RestModel from "discourse/models/rest"; import { tracked } from "@glimmer/tracking"; -const DEFAULT_COMPONENT = "user-menu/default-reviewable-item"; - -const DEFAULT_ITEM_COMPONENTS = { - ReviewableFlaggedPost: "user-menu/reviewable-flagged-post-item", - ReviewableQueuedPost: "user-menu/reviewable-queued-post-item", - ReviewableUser: "user-menu/reviewable-user-item", -}; - export default class UserMenuReviewable extends RestModel { @tracked pending; - - get userMenuComponent() { - return DEFAULT_ITEM_COMPONENTS[this.type] || DEFAULT_COMPONENT; - } } diff --git a/app/assets/javascripts/discourse/tests/helpers/notification-items-helper.js b/app/assets/javascripts/discourse/tests/helpers/notification-items-helper.js new file mode 100644 index 00000000000..4f05a666cb9 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/helpers/notification-items-helper.js @@ -0,0 +1,19 @@ +import { getRenderDirector } from "discourse/lib/notification-item"; +import sessionFixtures from "discourse/tests/fixtures/session-fixtures"; +import User from "discourse/models/user"; +import Site from "discourse/models/site"; + +export function createRenderDirector( + notification, + notificationType, + siteSettings +) { + const director = getRenderDirector( + notificationType, + notification, + User.create(sessionFixtures["/session/current.json"].current_user), + siteSettings, + Site.current() + ); + return director; +} diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index 66591f376f9..5d16648c68d 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -72,6 +72,7 @@ import { import { clearTagsHtmlCallbacks } from "discourse/lib/render-tags"; import { clearToolbarCallbacks } from "discourse/components/d-editor"; import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections"; +import { resetRenderDirectorForNotifictaionTypes } from "discourse/lib/notification-item"; export function currentUser() { return User.create(sessionFixtures["/session/current.json"].current_user); @@ -200,6 +201,7 @@ export function testCleanup(container, app) { clearTagsHtmlCallbacks(); clearToolbarCallbacks(); resetSidebarSection(); + resetRenderDirectorForNotifictaionTypes(); } export function discourseModule(name, options) { diff --git a/app/assets/javascripts/discourse/tests/helpers/reviewable-items-helper.js b/app/assets/javascripts/discourse/tests/helpers/reviewable-items-helper.js new file mode 100644 index 00000000000..4a0c30f1ced --- /dev/null +++ b/app/assets/javascripts/discourse/tests/helpers/reviewable-items-helper.js @@ -0,0 +1,15 @@ +import { getRenderDirector } from "discourse/lib/reviewable-item"; +import sessionFixtures from "discourse/tests/fixtures/session-fixtures"; +import User from "discourse/models/user"; +import Site from "discourse/models/site"; + +export function createRenderDirector(reviewable, reviewableType, siteSettings) { + const director = getRenderDirector( + reviewableType, + reviewable, + User.create(sessionFixtures["/session/current.json"].current_user), + siteSettings, + Site.current() + ); + return director; +} diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/bookmark-reminder-notification-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/bookmark-reminder-notification-item-test.js deleted file mode 100644 index 531c1b8429d..00000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/bookmark-reminder-notification-item-test.js +++ /dev/null @@ -1,107 +0,0 @@ -import { module, test } from "qunit"; -import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { query } from "discourse/tests/helpers/qunit-helpers"; -import { deepMerge } from "discourse-common/lib/object"; -import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types"; -import { render } from "@ember/test-helpers"; -import Notification from "discourse/models/notification"; -import { hbs } from "ember-cli-htmlbars"; -import I18n from "I18n"; - -function getNotification(overrides = {}) { - return Notification.create( - deepMerge( - { - id: 11, - user_id: 1, - notification_type: NOTIFICATION_TYPES.bookmark_reminder, - read: false, - high_priority: true, - 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: { - title: "this is unsafe bookmark title !", - display_username: "osama", - bookmark_name: null, - bookmarkable_url: "/t/sometopic/3232", - }, - }, - overrides - ) - ); -} - -module( - "Integration | Component | user-menu | bookmark-reminder-notification-item", - function (hooks) { - setupRenderingTest(hooks); - - const template = hbs``; - - test("when the bookmark has a name", async function (assert) { - this.set( - "notification", - getNotification({ data: { bookmark_name: "MY BOOKMARK" } }) - ); - await render(template); - const link = query("li a"); - assert.strictEqual( - link.title, - I18n.t("notifications.titles.bookmark_reminder_with_name", { - name: "MY BOOKMARK", - }), - "the notification has a title that includes the bookmark name" - ); - }); - - test("when the bookmark doesn't have a name", async function (assert) { - this.set( - "notification", - getNotification({ data: { bookmark_name: null } }) - ); - await render(template); - const link = query("li a"); - assert.strictEqual( - link.title, - I18n.t("notifications.titles.bookmark_reminder"), - "the notification has a generic title" - ); - }); - - test("when the bookmark reminder doesn't originate from a topic and has a title", async function (assert) { - this.set( - "notification", - getNotification({ - post_number: null, - topic_id: null, - fancy_title: null, - data: { - title: "this is unsafe bookmark title !", - bookmarkable_url: "/chat/channel/33", - }, - }) - ); - await render(template); - const description = query("li .notification-description"); - assert.strictEqual( - description.textContent.trim(), - "this is unsafe bookmark title !", - "the title is rendered safely as description" - ); - }); - - test("when the bookmark reminder originates from a topic", async function (assert) { - this.set("notification", getNotification()); - await render(template); - const description = query("li .notification-description"); - assert.strictEqual( - description.textContent.trim(), - "This is fancy title !", - "fancy_title is safe and rendered correctly" - ); - }); - } -); diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/granted-badge-notification-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/granted-badge-notification-item-test.js deleted file mode 100644 index abbe3a6fda4..00000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/granted-badge-notification-item-test.js +++ /dev/null @@ -1,63 +0,0 @@ -import { module, test } from "qunit"; -import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { exists, query } from "discourse/tests/helpers/qunit-helpers"; -import { deepMerge } from "discourse-common/lib/object"; -import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types"; -import { render } from "@ember/test-helpers"; -import Notification from "discourse/models/notification"; -import { hbs } from "ember-cli-htmlbars"; -import I18n from "I18n"; - -function getNotification(overrides = {}) { - return Notification.create( - deepMerge( - { - id: 11, - user_id: 1, - notification_type: NOTIFICATION_TYPES.granted_badge, - read: false, - high_priority: false, - created_at: "2022-07-01T06:00:32.173Z", - data: { - badge_id: 12, - badge_name: "Tough Guy ", - badge_slug: "tough-guy", - username: "ossa", - badge_title: false, - }, - }, - overrides - ) - ); -} - -module( - "Integration | Component | user-menu | granted-badge-notification-item", - function (hooks) { - setupRenderingTest(hooks); - - const template = hbs``; - - test("links to the badge page and filters by the username", async function (assert) { - this.set("notification", getNotification()); - await render(template); - const link = query("li a"); - assert.ok(link.href.endsWith("/badges/12/tough-guy?username=ossa")); - }); - - test("displays the right notification content", async function (assert) { - this.set("notification", getNotification()); - await render(template); - const div = query("li div"); - assert.strictEqual( - div.textContent.trim(), - I18n.t("notifications.granted_badge", { - description: "Tough Guy ", - }), - "label is rendered safely" - ); - assert.ok(!exists("li .notification-label")); - assert.ok(!exists("li .notification-description")); - }); - } -); diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/group-mentioned-notification-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/group-mentioned-notification-item-test.js deleted file mode 100644 index 18b8ed3e0ad..00000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/group-mentioned-notification-item-test.js +++ /dev/null @@ -1,68 +0,0 @@ -import { module, test } from "qunit"; -import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { query } from "discourse/tests/helpers/qunit-helpers"; -import { deepMerge } from "discourse-common/lib/object"; -import { render } from "@ember/test-helpers"; -import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types"; -import Notification from "discourse/models/notification"; -import { hbs } from "ember-cli-htmlbars"; - -function getNotification(overrides = {}) { - return Notification.create( - deepMerge( - { - id: 11, - user_id: 1, - notification_type: NOTIFICATION_TYPES.group_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 !", - original_post_id: 112, - original_post_type: 1, - original_username: "kolary", - display_username: "osama", - group_id: 333, - group_name: "hikers", - }, - }, - overrides - ) - ); -} - -module( - "Integration | Component | user-menu | group-mentioned-notification-item", - function (hooks) { - setupRenderingTest(hooks); - - const template = hbs``; - - test("notification label displays the user who mentioned and the mentioned group", async function (assert) { - this.set("notification", getNotification()); - await render(template); - const label = query("li .notification-label"); - assert.strictEqual(label.textContent.trim(), "osama @hikers"); - assert.ok( - label.classList.contains("mention-group"), - "label has mention-group class" - ); - assert.ok(label.classList.contains("notify"), "label has notify class"); - }); - - test("notification description displays the topic title", async function (assert) { - this.set("notification", getNotification()); - await render(template); - const description = query("li .notification-description"); - assert.strictEqual( - description.textContent.trim(), - "This is fancy title !" - ); - }); - } -); diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/group-message-summary-notification-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/group-message-summary-notification-item-test.js deleted file mode 100644 index 9b5a2989fd5..00000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/group-message-summary-notification-item-test.js +++ /dev/null @@ -1,55 +0,0 @@ -import { module, test } from "qunit"; -import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { exists, query } from "discourse/tests/helpers/qunit-helpers"; -import { render } from "@ember/test-helpers"; -import { deepMerge } from "discourse-common/lib/object"; -import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types"; -import Notification from "discourse/models/notification"; -import { hbs } from "ember-cli-htmlbars"; -import I18n from "I18n"; - -function getNotification(overrides = {}) { - return Notification.create( - deepMerge( - { - id: 11, - user_id: 1, - notification_type: NOTIFICATION_TYPES.group_message_summary, - read: false, - high_priority: false, - created_at: "2022-07-01T06:00:32.173Z", - data: { - group_id: 321, - group_name: "drummers", - inbox_count: 13, - username: "drummers.boss", - }, - }, - overrides - ) - ); -} - -module( - "Integration | Component | user-menu | group-message-summary-notification-item", - function (hooks) { - setupRenderingTest(hooks); - - const template = hbs``; - - test("the notification displays the right content", async function (assert) { - this.set("notification", getNotification()); - await render(template); - const notification = query("li"); - assert.strictEqual( - notification.textContent.trim(), - I18n.t("notifications.group_message_summary", { - count: 13, - group_name: "drummers", - }) - ); - assert.ok(!exists("li .notification-label")); - assert.ok(!exists("li .notification-description")); - }); - } -); diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/liked-consolidated-notification-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/liked-consolidated-notification-item-test.js deleted file mode 100644 index 0af3a813a07..00000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/liked-consolidated-notification-item-test.js +++ /dev/null @@ -1,71 +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 { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types"; -import Notification from "discourse/models/notification"; -import { hbs } from "ember-cli-htmlbars"; -import I18n from "I18n"; - -function getNotification(overrides = {}) { - return Notification.create( - deepMerge( - { - id: 11, - user_id: 1, - notification_type: NOTIFICATION_TYPES.liked_consolidated, - read: false, - high_priority: false, - created_at: "2022-07-01T06:00:32.173Z", - data: { - topic_title: "this is some topic and it's irrelevant", - original_post_id: 3294, - original_post_type: 1, - original_username: "liker439", - display_username: "liker439", - username: "liker439", - count: 44, - }, - }, - overrides - ) - ); -} - -module( - "Integration | Component | user-menu | liked-consolidated-notification-item", - function (hooks) { - setupRenderingTest(hooks); - - const template = hbs``; - - test("the notification links to the likes received notifications page of the user", async function (assert) { - this.set("notification", getNotification()); - await render(template); - const link = query("li a"); - assert.ok( - link.href.endsWith( - "/u/eviltrout/notifications/likes-received?acting_username=liker439" - ) - ); - }); - - test("the notification label displays the user who liked", async function (assert) { - this.set("notification", getNotification()); - await render(template); - const label = query("li .notification-label"); - assert.strictEqual(label.textContent.trim(), "liker439"); - }); - - test("the notification description displays the number of likes", async function (assert) { - this.set("notification", getNotification()); - await render(template); - const description = query("li .notification-description"); - assert.strictEqual( - description.textContent.trim(), - I18n.t("notifications.liked_consolidated_description", { count: 44 }) - ); - }); - } -); diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/liked-notification-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/liked-notification-item-test.js deleted file mode 100644 index 679e21b1081..00000000000 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/liked-notification-item-test.js +++ /dev/null @@ -1,114 +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 { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types"; -import Notification from "discourse/models/notification"; -import { hbs } from "ember-cli-htmlbars"; -import I18n from "I18n"; - -function getNotification(overrides = {}) { - return Notification.create( - deepMerge( - { - id: 11, - user_id: 1, - notification_type: NOTIFICATION_TYPES.liked, - 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 !", - username: "osama", - display_username: "osama", - username2: "shrek", - count: 2, - }, - }, - overrides - ) - ); -} - -module( - "Integration | Component | user-menu | liked-notification-item", - function (hooks) { - setupRenderingTest(hooks); - - const template = hbs``; - - test("when the likes count is 2", async function (assert) { - this.set("notification", getNotification({ data: { count: 2 } })); - await render(template); - - const label = query("li .notification-label"); - const description = query("li .notification-description"); - assert.strictEqual( - label.textContent.trim(), - "osama, shrek", - "the label displays both usernames comma-concatenated" - ); - assert.ok( - label.classList.contains("double-user"), - "label has double-user class" - ); - assert.strictEqual( - description.textContent.trim(), - "This is fancy title !", - "the description displays the topic title" - ); - }); - - test("when the likes count is more than 2", async function (assert) { - this.set("notification", getNotification({ data: { count: 3 } })); - await render(template); - - const label = query("li .notification-label"); - const description = query("li .notification-description"); - assert.strictEqual( - label.textContent.trim(), - I18n.t("notifications.liked_by_multiple_users", { - username: "osama", - username2: "shrek", - count: 1, - }), - "the label displays the first 2 usernames comma-concatenated with the count of remaining users" - ); - assert.ok( - label.classList.contains("multi-user"), - "label has multi-user class" - ); - assert.strictEqual( - description.textContent.trim(), - "This is fancy title !", - "the description displays the topic title" - ); - }); - - test("when the likes count is 1", async function (assert) { - this.set( - "notification", - getNotification({ data: { count: 1, username2: null } }) - ); - await render(template); - - const label = query("li .notification-label"); - const description = query("li .notification-description"); - assert.strictEqual( - label.textContent.trim(), - "osama", - "the label displays the username" - ); - assert.strictEqual( - description.textContent.trim(), - "This is fancy title !", - "the description displays the topic title" - ); - }); - } -); diff --git a/app/assets/javascripts/discourse/tests/integration/components/user-menu/notification-item-test.js b/app/assets/javascripts/discourse/tests/integration/components/user-menu/notification-item-test.js index 5c25b81bd8a..3198d73921a 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/user-menu/notification-item-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/user-menu/notification-item-test.js @@ -1,11 +1,12 @@ 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 { click, render, settled } from "@ember/test-helpers"; import { deepMerge } from "discourse-common/lib/object"; import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types"; import Notification from "discourse/models/notification"; import { hbs } from "ember-cli-htmlbars"; +import { withPluginApi } from "discourse/lib/plugin-api"; import I18n from "I18n"; function getNotification(overrides = {}) { @@ -194,5 +195,202 @@ module( ); assert.ok(!query("img"), "no exists"); }); + + test("various aspects can be customized according to the notification's render director", async function (assert) { + withPluginApi("0.1", (api) => { + api.registerNotificationTypeRenderer( + "linked", + (NotificationItemBase) => { + return class extends NotificationItemBase { + 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