diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index 5b4f49526c..3c0d653ca8 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -207,6 +207,11 @@ def trigger_indicator_hide(): frappe.publish_realtime("indicator_hide", user=frappe.session.user) +@frappe.whitelist() +def get_unread_count(): + return frappe.db.count("Notification Log", {"read": 0, "for_user": frappe.session.user}) + + def set_notifications_as_unseen(user): try: frappe.db.set_value("Notification Settings", user, "seen", 0, update_modified=False) diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js index 528bf2912d..3d0f77909d 100644 --- a/frappe/public/js/frappe/ui/notifications/notifications.js +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -128,6 +128,7 @@ frappe.ui.Notifications = class Notifications { e.stopImmediatePropagation(); this.dropdown_list.find(".unread").removeClass("unread"); frappe.call("frappe.desk.doctype.notification_log.notification_log.mark_all_as_read"); + this.tabs.notifications?.update_count_badge(0); } setup_dropdown_events() { @@ -233,10 +234,12 @@ class NotificationsView extends BaseNotificationsView { this.dropdown_items = []; this.notifications_fetched = false; + this.unread_count = 0; if (this.settings && this.settings.seen == 0) { this.toggle_notification_icon(false); } + this.fetch_unread_count(); } update_dropdown() { @@ -272,6 +275,7 @@ class NotificationsView extends BaseNotificationsView { }) .then(() => { $el.removeClass("unread"); + this.update_count_badge(Math.max(this.unread_count - 1, 0)); }); } @@ -390,6 +394,20 @@ class NotificationsView extends BaseNotificationsView { this.bell_indicator?.toggleClass("indicator blue", !seen); } + update_count_badge(count) { + this.unread_count = count; + const $suffix = this.parent + .closest(".body-sidebar") + ?.find(".sidebar-notification .sidebar-notification-count"); + if (!$suffix?.length) return; + + if (count > 0) { + $suffix.text(count > 99 ? "99+" : count).removeClass("hidden"); + } else { + $suffix.addClass("hidden"); + } + } + toggle_seen(flag) { frappe.call( "frappe.desk.doctype.notification_settings.notification_settings.set_seen_value", @@ -400,10 +418,21 @@ class NotificationsView extends BaseNotificationsView { ); } + fetch_unread_count() { + frappe + .call("frappe.desk.doctype.notification_log.notification_log.get_unread_count") + .then((r) => { + const count = r.message || 0; + this.unread_count = count; + this.update_count_badge(count); + }); + } + setup_notification_listeners() { frappe.realtime.on("notification", () => { this.settings.seen = 0; this.toggle_notification_icon(false); + this.update_count_badge(this.unread_count + 1); this.update_dropdown(); }); diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js index ff8d35653e..7f3a78aab5 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -487,6 +487,7 @@ frappe.ui.Sidebar = class Sidebar { standard: true, type: "Button", class: "sidebar-notification hidden", + suffix: "", onClick: () => { const $dropdown = this.wrapper.find(".dropdown-notifications"); $dropdown.toggleClass("hidden"); diff --git a/frappe/public/scss/desk/notification.scss b/frappe/public/scss/desk/notification.scss index cc2bb95409..5d86ad5fed 100644 --- a/frappe/public/scss/desk/notification.scss +++ b/frappe/public/scss/desk/notification.scss @@ -60,6 +60,17 @@ } } +.sidebar-notification-count { + min-width: 20px; + padding: 1px 7px; + background-color: var(--bg-gray-100); + color: var(--text-muted); + font-size: var(--text-sm); + line-height: 1.2; + text-align: center; + border-radius: 10px; +} + .sidebar-notification .item-anchor { overflow: visible; }