diff --git a/frappe/boot.py b/frappe/boot.py index 5ea9235b18..b187e04047 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -78,6 +78,9 @@ def get_bootinfo(): bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) bootinfo.navbar_settings = get_navbar_settings() bootinfo.notification_settings = get_notification_settings() + bootinfo.notification_unread_count = frappe.db.count( + "Notification Log", {"read": 0, "for_user": frappe.session.user} + ) bootinfo.onboarding_tours = get_onboarding_ui_tours() set_time_zone(bootinfo) diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js index 528bf2912d..b2179e7e61 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 = frappe.boot.notification_unread_count || 0; if (this.settings && this.settings.seen == 0) { this.toggle_notification_icon(false); } + this.update_count_badge(this.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,23 @@ 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) + .attr("aria-label", __("{0} unread notifications", [count])) + .removeClass("hidden"); + } else { + $suffix.removeAttr("aria-label").addClass("hidden"); + } + } + toggle_seen(flag) { frappe.call( "frappe.desk.doctype.notification_settings.notification_settings.set_seen_value", @@ -404,6 +425,7 @@ class NotificationsView extends BaseNotificationsView { 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..b3d6099768 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 b83ef8a030..8615aadf81 100644 --- a/frappe/public/scss/desk/notification.scss +++ b/frappe/public/scss/desk/notification.scss @@ -75,6 +75,17 @@ opacity: 0; } +.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 .standard-sidebar-item .item-anchor { overflow: visible; }