From e878ab1d7d7805ca2fe2375fab2c65d81e9e931f Mon Sep 17 00:00:00 2001
From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com>
Date: Thu, 16 Apr 2026 23:41:34 +0530
Subject: [PATCH 1/3] feat: show unread notification count in sidebar
---
.../notification_log/notification_log.py | 5 ++++
.../frappe/ui/notifications/notifications.js | 29 +++++++++++++++++++
frappe/public/js/frappe/ui/sidebar/sidebar.js | 1 +
frappe/public/scss/desk/notification.scss | 11 +++++++
4 files changed, 46 insertions(+)
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;
}
From 5f85777c8e0a9c062470621e7b4e74a5ba21e77d Mon Sep 17 00:00:00 2001
From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com>
Date: Thu, 16 Apr 2026 23:51:58 +0530
Subject: [PATCH 2/3] refactor: source notification unread count from boot
---
frappe/boot.py | 3 +++
.../doctype/notification_log/notification_log.py | 5 -----
.../js/frappe/ui/notifications/notifications.js | 14 ++------------
3 files changed, 5 insertions(+), 17 deletions(-)
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/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 3c0d653ca8..5b4f49526c 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -207,11 +207,6 @@ 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 3d0f77909d..c759c3c01e 100644
--- a/frappe/public/js/frappe/ui/notifications/notifications.js
+++ b/frappe/public/js/frappe/ui/notifications/notifications.js
@@ -234,12 +234,12 @@ class NotificationsView extends BaseNotificationsView {
this.dropdown_items = [];
this.notifications_fetched = false;
- this.unread_count = 0;
+ this.unread_count = frappe.boot.notification_unread_count || 0;
if (this.settings && this.settings.seen == 0) {
this.toggle_notification_icon(false);
}
- this.fetch_unread_count();
+ this.update_count_badge(this.unread_count);
}
update_dropdown() {
@@ -418,16 +418,6 @@ 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;
From 7572e6fe459c9d3735b9b60cffbba58998ffe08a Mon Sep 17 00:00:00 2001
From: Kaushal Shriwas <64089478+kaulith@users.noreply.github.com>
Date: Fri, 17 Apr 2026 00:22:33 +0530
Subject: [PATCH 3/3] feat: add aria-label to sidebar notification count
---
frappe/public/js/frappe/ui/notifications/notifications.js | 7 +++++--
frappe/public/js/frappe/ui/sidebar/sidebar.js | 2 +-
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js
index c759c3c01e..b2179e7e61 100644
--- a/frappe/public/js/frappe/ui/notifications/notifications.js
+++ b/frappe/public/js/frappe/ui/notifications/notifications.js
@@ -402,9 +402,12 @@ class NotificationsView extends BaseNotificationsView {
if (!$suffix?.length) return;
if (count > 0) {
- $suffix.text(count > 99 ? "99+" : count).removeClass("hidden");
+ $suffix
+ .text(count > 99 ? "99+" : count)
+ .attr("aria-label", __("{0} unread notifications", [count]))
+ .removeClass("hidden");
} else {
- $suffix.addClass("hidden");
+ $suffix.removeAttr("aria-label").addClass("hidden");
}
}
diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js
index 7f3a78aab5..b3d6099768 100644
--- a/frappe/public/js/frappe/ui/sidebar/sidebar.js
+++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js
@@ -487,7 +487,7 @@ frappe.ui.Sidebar = class Sidebar {
standard: true,
type: "Button",
class: "sidebar-notification hidden",
- suffix: "",
+ suffix: "",
onClick: () => {
const $dropdown = this.wrapper.find(".dropdown-notifications");
$dropdown.toggleClass("hidden");