diff --git a/app/assets/javascripts/discourse/app/components/user-menu/bookmark-item.js b/app/assets/javascripts/discourse/app/components/user-menu/bookmark-item.js new file mode 100644 index 00000000000..4c0d2583eb2 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-menu/bookmark-item.js @@ -0,0 +1,36 @@ +import UserMenuItem from "discourse/components/user-menu/menu-item"; +import { NO_REMINDER_ICON } from "discourse/models/bookmark"; + +export default class UserMenuBookmarkItem extends UserMenuItem { + get className() { + return "bookmark"; + } + + get linkHref() { + return this.bookmark.bookmarkable_url; + } + + get linkTitle() { + return this.bookmark.name; + } + + get icon() { + return NO_REMINDER_ICON; + } + + get label() { + return this.bookmark.user?.username; + } + + get description() { + return this.bookmark.title; + } + + get topicId() { + return this.bookmark.topic_id; + } + + get bookmark() { + return this.args.item; + } +} 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 new file mode 100644 index 00000000000..e0829c1c076 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-menu/bookmark-notification-item.hbs @@ -0,0 +1 @@ +{{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 new file mode 100644 index 00000000000..58dc4c66739 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-menu/bookmark-notification-item.js @@ -0,0 +1,12 @@ +import GlimmerComponent from "discourse/components/glimmer"; +import Notification from "discourse/models/notification"; + +export default class UserMenuBookmarkNotificationItem extends GlimmerComponent { + 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.hbs b/app/assets/javascripts/discourse/app/components/user-menu/bookmarks-list-empty-state.hbs new file mode 100644 index 00000000000..6e4528e070f --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-menu/bookmarks-list-empty-state.hbs @@ -0,0 +1,10 @@ +
+ + {{i18n "user.no_bookmarks_title"}} + +
+

+ {{html-safe (i18n "user.no_bookmarks_body" icon=(d-icon "bookmark"))}} +

+
+
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 new file mode 100644 index 00000000000..639b30cc404 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-menu/bookmarks-list.js @@ -0,0 +1,72 @@ +import UserMenuNotificationsList from "discourse/components/user-menu/notifications-list"; +import { ajax } from "discourse/lib/ajax"; +import Notification from "discourse/models/notification"; +import showModal from "discourse/lib/show-modal"; +import I18n from "I18n"; + +export default class UserMenuBookmarksList extends UserMenuNotificationsList { + get dismissTypes() { + return ["bookmark_reminder"]; + } + + get showAllHref() { + return `${this.currentUser.path}/activity/bookmarks`; + } + + get showAllTitle() { + return I18n.t("user_menu.view_all_bookmarks"); + } + + get showDismiss() { + return this.#unreadBookmarkRemindersCount > 0; + } + + get dismissTitle() { + return I18n.t("user.dismiss_bookmarks_tooltip"); + } + + get itemsCacheKey() { + return "user-menu-bookmarks-tab"; + } + + get itemComponent() { + return "user-menu/bookmark-notification-item"; + } + + get emptyStateComponent() { + return "user-menu/bookmarks-list-empty-state"; + } + + get #unreadBookmarkRemindersCount() { + const key = `grouped_unread_high_priority_notifications.${this.site.notification_types.bookmark_reminder}`; + // we're retrieving the value with get() so that Ember tracks the property + // and re-renders the UI when it changes. + // we can stop using `get()` when the User model is refactored into native + // class with @tracked properties. + return this.currentUser.get(key) || 0; + } + + fetchItems() { + return ajax(`/u/${this.currentUser.username}/user-menu-bookmarks`).then( + (data) => { + const content = []; + data.notifications.forEach((notification) => { + content.push(Notification.create(notification)); + }); + content.push(...data.bookmarks); + return content; + } + ); + } + + dismissWarningModal() { + const modalController = showModal("dismiss-notification-confirmation"); + modalController.set( + "confirmationMessage", + I18n.t("notifications.dismiss_confirmation.body.bookmarks", { + count: this.#unreadBookmarkRemindersCount, + }) + ); + return modalController; + } +} diff --git a/app/assets/javascripts/discourse/app/components/user-menu/likes-notifications-list.js b/app/assets/javascripts/discourse/app/components/user-menu/likes-notifications-list.js index bf1b654adc9..3b936477a63 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/likes-notifications-list.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/likes-notifications-list.js @@ -5,6 +5,10 @@ export default class UserMenuLikesNotificationsList extends UserMenuNotification return ["liked", "liked_consolidated"]; } + get dismissTypes() { + return this.filterByTypes; + } + dismissWarningModal() { return null; } diff --git a/app/assets/javascripts/discourse/app/components/user-menu/mentions-notifications-list.js b/app/assets/javascripts/discourse/app/components/user-menu/mentions-notifications-list.js index 8b8a2bad668..2a7a4396e87 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/mentions-notifications-list.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/mentions-notifications-list.js @@ -5,6 +5,10 @@ export default class UserMenuMentionsNotificationsList extends UserMenuNotificat return ["mentioned"]; } + get dismissTypes() { + return this.filterByTypes; + } + dismissWarningModal() { return null; } diff --git a/app/assets/javascripts/discourse/app/components/user-menu/notification-item.hbs b/app/assets/javascripts/discourse/app/components/user-menu/menu-item.hbs similarity index 60% rename from app/assets/javascripts/discourse/app/components/user-menu/notification-item.hbs rename to app/assets/javascripts/discourse/app/components/user-menu/menu-item.hbs index b9e155ad1c6..e5ca33d4ae7 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/notification-item.hbs +++ b/app/assets/javascripts/discourse/app/components/user-menu/menu-item.hbs @@ -7,15 +7,12 @@ {{d-icon this.icon}}
{{#if this.label}} - + {{this.label}} {{/if}} {{#if this.description}} - + {{this.description}} {{/if}} 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 new file mode 100644 index 00000000000..47ef43be914 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/user-menu/menu-item.js @@ -0,0 +1,35 @@ +import GlimmerComponent from "discourse/components/glimmer"; +import { action } from "@ember/object"; + +export default class UserMenuItem extends GlimmerComponent { + 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() {} + + @action + onClick() {} +} diff --git a/app/assets/javascripts/discourse/app/components/user-menu/menu.js b/app/assets/javascripts/discourse/app/components/user-menu/menu.js index 6fec39d2c62..7fd6991cbce 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/menu.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/menu.js @@ -1,6 +1,7 @@ import GlimmerComponent from "discourse/components/glimmer"; import { tracked } from "@glimmer/tracking"; import { action } from "@ember/object"; +import { NO_REMINDER_ICON } from "discourse/models/bookmark"; import UserMenuTab from "discourse/lib/user-menu/tab"; const DEFAULT_TAB_ID = "all-notifications"; @@ -69,6 +70,24 @@ const CORE_TOP_TABS = [ } }, + class extends UserMenuTab { + get id() { + return "bookmarks"; + } + + get icon() { + return NO_REMINDER_ICON; + } + + get panelComponent() { + return "user-menu/bookmarks-list"; + } + + get count() { + return this.getUnreadCountForType("bookmark_reminder"); + } + }, + class extends UserMenuTab { get id() { return REVIEW_QUEUE_TAB_ID; 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 084c84962ce..d0febb11024 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,11 +1,11 @@ -import GlimmerComponent from "discourse/components/glimmer"; +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 cookie from "discourse/lib/cookie"; -export default class UserMenuNotificationItem extends GlimmerComponent { +export default class UserMenuNotificationItem extends UserMenuItem { constructor() { super(...arguments); this.renderDirector = getRenderDirector( @@ -18,9 +18,11 @@ export default class UserMenuNotificationItem extends GlimmerComponent { } get className() { - const classes = []; + const classes = ["notification"]; if (this.notification.read) { classes.push("read"); + } else { + classes.push("unread"); } if (this.#notificationName) { classes.push(this.#notificationName.replace(/_/g, "-")); @@ -51,16 +53,20 @@ export default class UserMenuNotificationItem extends GlimmerComponent { return this.renderDirector.label; } - get labelWrapperClasses() { - return this.renderDirector.labelWrapperClasses?.join(" ") || ""; + get labelClass() { + return this.renderDirector.labelClasses?.join(" ") || ""; } get description() { return this.renderDirector.description; } - get descriptionWrapperClasses() { - return this.renderDirector.descriptionWrapperClasses?.join(" ") || ""; + get descriptionClass() { + return this.renderDirector.descriptionClasses?.join(" ") || ""; + } + + get topicId() { + return this.notification.topic_id; } get notification() { 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 79110e5bcd2..f9200599824 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 @@ -10,6 +10,10 @@ export default class UserMenuNotificationsList extends UserMenuItemsList { return null; } + get dismissTypes() { + return null; + } + get showAllHref() { return `${this.currentUser.path}/notifications`; } @@ -70,8 +74,10 @@ export default class UserMenuNotificationsList extends UserMenuItemsList { if (this.currentUser.unread_high_priority_notifications > 0) { const modalController = showModal("dismiss-notification-confirmation"); modalController.set( - "count", - this.currentUser.unread_high_priority_notifications + "confirmationMessage", + I18n.t("notifications.dismiss_confirmation.body.default", { + count: this.currentUser.unread_high_priority_notifications, + }) ); return modalController; } @@ -80,7 +86,7 @@ export default class UserMenuNotificationsList extends UserMenuItemsList { @action dismissButtonClick() { const opts = { type: "PUT" }; - const dismissTypes = this.filterByTypes; + const dismissTypes = this.dismissTypes; if (dismissTypes?.length > 0) { opts.data = { dismiss_types: dismissTypes.join(",") }; } diff --git a/app/assets/javascripts/discourse/app/components/user-menu/replies-notifications-list.js b/app/assets/javascripts/discourse/app/components/user-menu/replies-notifications-list.js index f747b81e919..b7d878be33f 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/replies-notifications-list.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/replies-notifications-list.js @@ -5,6 +5,10 @@ export default class UserMenuRepliesNotificationsList extends UserMenuNotificati return ["replied"]; } + get dismissTypes() { + return this.filterByTypes; + } + dismissWarningModal() { return null; } diff --git a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.hbs b/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.hbs deleted file mode 100644 index 10db4fc1943..00000000000 --- a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.hbs +++ /dev/null @@ -1,9 +0,0 @@ -
  • - - {{d-icon this.icon}} -
    - {{this.actor}} - {{this.description}} -
    -
    -
  • 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 index b08d973fbd0..eab3e3563a8 100644 --- a/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.js +++ b/app/assets/javascripts/discourse/app/components/user-menu/reviewable-item.js @@ -1,7 +1,8 @@ -import GlimmerComponent from "discourse/components/glimmer"; +import UserMenuItem from "discourse/components/user-menu/menu-item"; +import getURL from "discourse-common/lib/get-url"; import { getRenderDirector } from "discourse/lib/reviewable-item"; -export default class UserMenuReviewableItem extends GlimmerComponent { +export default class UserMenuReviewableItem extends UserMenuItem { constructor() { super(...arguments); this.reviewable = this.args.item; @@ -14,15 +15,34 @@ export default class UserMenuReviewableItem extends GlimmerComponent { ); } - get actor() { + get className() { + const classes = ["reviewable"]; + if (this.reviewable.pending) { + classes.push("pending"); + } else { + classes.push("reviewed"); + } + return classes.join(" "); + } + + get linkHref() { + return getURL(`/review/${this.reviewable.id}`); + } + + get linkTitle() { + // TODO(osama): add title + return ""; + } + + get icon() { + return this.renderDirector.icon; + } + + get label() { return this.renderDirector.actor; } get description() { return this.renderDirector.description; } - - get icon() { - return this.renderDirector.icon; - } } diff --git a/app/assets/javascripts/discourse/app/controllers/user-notifications.js b/app/assets/javascripts/discourse/app/controllers/user-notifications.js index dda292f5889..3890d5a199d 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-notifications.js +++ b/app/assets/javascripts/discourse/app/controllers/user-notifications.js @@ -63,7 +63,12 @@ export default Controller.extend({ if (unreadHighPriorityNotifications > 0) { showModal("dismiss-notification-confirmation").setProperties({ - count: unreadHighPriorityNotifications, + confirmationMessage: I18n.t( + "notifications.dismiss_confirmation.body.default", + { + count: unreadHighPriorityNotifications, + } + ), dismissNotifications: () => this.markRead(), }); } else { diff --git a/app/assets/javascripts/discourse/app/initializers/badging.js b/app/assets/javascripts/discourse/app/initializers/badging.js index 6ac623bdb87..38f6db84ade 100644 --- a/app/assets/javascripts/discourse/app/initializers/badging.js +++ b/app/assets/javascripts/discourse/app/initializers/badging.js @@ -15,8 +15,16 @@ export default { const appEvents = container.lookup("service:app-events"); appEvents.on("notifications:changed", () => { - const notifications = - user.unread_notifications + user.unread_high_priority_notifications; + let notifications; + if (user.redesigned_user_menu_enabled) { + notifications = user.all_unread_notifications_count; + if (user.unseen_reviewable_count) { + notifications += user.unseen_reviewable_count; + } + } else { + notifications = + user.unread_notifications + user.unread_high_priority_notifications; + } navigator.setAppBadge(notifications); }); diff --git a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js index 90ed40130b7..0399ff9c1b1 100644 --- a/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js +++ b/app/assets/javascripts/discourse/app/initializers/subscribe-user-notifications.js @@ -51,6 +51,8 @@ export default { data.unread_high_priority_notifications, read_first_notification: data.read_first_notification, all_unread_notifications_count: data.all_unread_notifications_count, + grouped_unread_high_priority_notifications: + data.grouped_unread_high_priority_notifications, }); if ( diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/base.js b/app/assets/javascripts/discourse/app/lib/notification-items/base.js index 455db134b0b..fb7a6961019 100644 --- a/app/assets/javascripts/discourse/app/lib/notification-items/base.js +++ b/app/assets/javascripts/discourse/app/lib/notification-items/base.js @@ -82,16 +82,16 @@ export default class NotificationItemBase { onClick() {} /** - * @returns {string[]} Include additional classes to the label's wrapper . + * @returns {string[]} Include additional classes to the label. */ - get labelWrapperClasses() { + get labelClasses() { return []; } /** - * @returns {string[]} Include additional classes to the description's wrapper . + * @returns {string[]} Include additional classes to the description. */ - get descriptionWrapperClasses() { + get descriptionClasses() { return []; } 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 index 0333d6e44d8..b9ba3ee4c5b 100644 --- a/app/assets/javascripts/discourse/app/lib/notification-items/group-mentioned.js +++ b/app/assets/javascripts/discourse/app/lib/notification-items/group-mentioned.js @@ -5,7 +5,7 @@ export default class extends NotificationItemBase { return `${this.username} @${this.notification.data.group_name}`; } - get labelWrapperClasses() { + get labelClasses() { return ["mention-group", "notify"]; } } diff --git a/app/assets/javascripts/discourse/app/lib/notification-items/liked.js b/app/assets/javascripts/discourse/app/lib/notification-items/liked.js index 6cae82971b6..e41376b0dae 100644 --- a/app/assets/javascripts/discourse/app/lib/notification-items/liked.js +++ b/app/assets/javascripts/discourse/app/lib/notification-items/liked.js @@ -20,7 +20,7 @@ export default class extends NotificationItemBase { } } - get labelWrapperClasses() { + get labelClasses() { if (this.count === 2) { return ["double-user"]; } else if (this.count > 2) { diff --git a/app/assets/javascripts/discourse/app/lib/user-menu/tab.js b/app/assets/javascripts/discourse/app/lib/user-menu/tab.js index cc6999a7e4a..11d2cacef51 100644 --- a/app/assets/javascripts/discourse/app/lib/user-menu/tab.js +++ b/app/assets/javascripts/discourse/app/lib/user-menu/tab.js @@ -24,4 +24,13 @@ export default class UserMenuTab { get icon() { throw new Error("not implemented"); } + + getUnreadCountForType(type) { + const key = `grouped_unread_high_priority_notifications.${this.site.notification_types[type]}`; + // we're retrieving the value with get() so that Ember tracks the property + // and re-renders the UI when it changes. + // we can stop using `get()` when the User model is refactored into native + // class with @tracked properties. + return this.currentUser.get(key) || 0; + } } diff --git a/app/assets/javascripts/discourse/app/templates/modal/dismiss-notification-confirmation.hbs b/app/assets/javascripts/discourse/app/templates/modal/dismiss-notification-confirmation.hbs index 2e01e98bf42..d16c8b8ae1d 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/dismiss-notification-confirmation.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/dismiss-notification-confirmation.hbs @@ -1,5 +1,5 @@ - {{i18n "notifications.dismiss_confirmation.body" count=this.count}} + {{this.confirmationMessage}}