diff --git a/backend/package.json b/backend/package.json
index a7e4f7a..5c80ad8 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "rosterchirp-backend",
- "version": "0.12.10",
+ "version": "0.12.11",
"description": "RosterChirp backend server",
"main": "src/index.js",
"scripts": {
diff --git a/build.sh b/build.sh
index 4f58bd5..230aaa9 100644
--- a/build.sh
+++ b/build.sh
@@ -13,7 +13,7 @@
# ─────────────────────────────────────────────────────────────
set -euo pipefail
-VERSION="${1:-0.12.10}"
+VERSION="${1:-0.12.11}"
ACTION="${2:-}"
REGISTRY="${REGISTRY:-}"
IMAGE_NAME="rosterchirp"
diff --git a/frontend/package.json b/frontend/package.json
index 18528db..80e3eac 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "rosterchirp-frontend",
- "version": "0.12.10",
+ "version": "0.12.11",
"private": true,
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/GlobalBar.jsx b/frontend/src/components/GlobalBar.jsx
index 9d9b75d..0d61d08 100644
--- a/frontend/src/components/GlobalBar.jsx
+++ b/frontend/src/components/GlobalBar.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { useSocket } from '../contexts/SocketContext.jsx';
import { api } from '../utils/api.js';
-export default function GlobalBar({ isMobile, showSidebar, onBurger }) {
+export default function GlobalBar({ isMobile, showSidebar, onBurger, hasUnread = false }) {
const { connected } = useSocket();
const [settings, setSettings] = useState({ app_name: 'rosterchirp', logo_url: '' });
const [isDark, setIsDark] = useState(() => document.documentElement.getAttribute('data-theme') === 'dark');
@@ -41,11 +41,22 @@ export default function GlobalBar({ isMobile, showSidebar, onBurger }) {
title="Menu"
aria-label="Open menu"
>
-
-
-
-
-
+
+
+
+
+
+
+ {hasUnread && (
+
+ )}
+
diff --git a/frontend/src/components/NavDrawer.css b/frontend/src/components/NavDrawer.css
index 9434e9b..1efb3bc 100644
--- a/frontend/src/components/NavDrawer.css
+++ b/frontend/src/components/NavDrawer.css
@@ -83,3 +83,12 @@
color: var(--primary);
}
.nav-drawer-item.active:hover { background: var(--primary-light); }
+
+.nav-drawer-unread-dot {
+ margin-left: auto;
+ width: 9px;
+ height: 9px;
+ border-radius: 50%;
+ background: var(--primary);
+ flex-shrink: 0;
+}
diff --git a/frontend/src/components/NavDrawer.jsx b/frontend/src/components/NavDrawer.jsx
index f675b0f..f7e12ad 100644
--- a/frontend/src/components/NavDrawer.jsx
+++ b/frontend/src/components/NavDrawer.jsx
@@ -13,7 +13,7 @@ const NAV_ICON = {
settings:
,
};
-export default function NavDrawer({ open, onClose, onMessages, onGroupMessages, onSchedule, onScheduleManager, onBranding, onSettings, onUsers, onGroupManager, onHostPanel, features = {}, currentPage = 'chat', isMobile = false }) {
+export default function NavDrawer({ open, onClose, onMessages, onGroupMessages, onSchedule, onScheduleManager, onBranding, onSettings, onUsers, onGroupManager, onHostPanel, features = {}, currentPage = 'chat', isMobile = false, unreadMessages = false, unreadGroupMessages = false }) {
const { user } = useAuth();
const drawerRef = useRef(null);
const isAdmin = user?.role === 'admin';
@@ -36,7 +36,7 @@ export default function NavDrawer({ open, onClose, onMessages, onGroupMessages,
}, [open, onClose]);
const item = (icon, label, onClick, opts = {}) => {
- const { active, disabled, badge } = opts;
+ const { active, disabled, badge, dot } = opts;
return (
{label}
{badge && {badge} }
+ {dot && }
);
};
@@ -64,8 +65,8 @@ export default function NavDrawer({ open, onClose, onMessages, onGroupMessages,
{/* User section */}
- {item(NAV_ICON.messages, 'Messages', onMessages, { active: currentPage === 'chat' })}
- {hasUserGroups && item(NAV_ICON.groupmessages, 'Group Messages', onGroupMessages, { active: currentPage === 'groupmessages' })}
+ {item(NAV_ICON.messages, 'Messages', onMessages, { active: currentPage === 'chat', dot: unreadMessages })}
+ {hasUserGroups && item(NAV_ICON.groupmessages, 'Group Messages', onGroupMessages, { active: currentPage === 'groupmessages', dot: unreadGroupMessages })}
{features.scheduleManager && item(NAV_ICON.schedules, 'Schedules', onSchedule, { active: currentPage === 'schedule' })}
{/* Admin section */}
diff --git a/frontend/src/pages/Chat.jsx b/frontend/src/pages/Chat.jsx
index b90fd9d..0772de0 100644
--- a/frontend/src/pages/Chat.jsx
+++ b/frontend/src/pages/Chat.jsx
@@ -395,10 +395,20 @@ export default function Chat() {
const isToolManager = user?.role === 'admin' || (features.teamToolManagers || []).some(gid => (features.userGroupMemberships || []).includes(gid));
+ // Unread indicators for burger icon and nav drawer
+ const allGroupsFlat = [...(groups.publicGroups || []), ...(groups.privateGroups || [])];
+ const hasUnreadChat = allGroupsFlat.some(g =>
+ (g.type === 'public' || !g.is_managed) && (unreadGroups.get(g.id) || 0) > 0
+ );
+ const hasUnreadGroupMessages = (groups.privateGroups || []).some(g =>
+ g.is_managed && (unreadGroups.get(g.id) || 0) > 0
+ );
+ const hasAnyUnread = hasUnreadChat || hasUnreadGroupMessages;
+
if (page === 'users') {
return (
-
setDrawerOpen(true)} />
+ setDrawerOpen(true)} hasUnread={hasAnyUnread} />
setModal('profile')} onHelp={() => setModal('help')} onAbout={() => setModal('about')} />
@@ -412,7 +422,8 @@ export default function Chat() {
onSettings={() => { setDrawerOpen(false); setModal('settings'); }}
onUsers={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('users'); }}
onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }}
- features={features} currentPage={page} isMobile={isMobile} />
+ features={features} currentPage={page} isMobile={isMobile}
+ unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} />
{modal === 'profile' && setModal(null)} />}
{modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && setModal(null)} />}
@@ -425,20 +436,10 @@ export default function Chat() {
if (page === 'groups') {
return (
-
setDrawerOpen(true)} />
+ setDrawerOpen(true)} hasUnread={hasAnyUnread} />
setModal('profile')} onHelp={() => setModal('help')} onAbout={() => setModal('about')} />
- setDrawerOpen(false)}
- onMessages={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('chat'); }}
- onSchedule={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('schedule'); }}
- onGroupManager={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('groups'); }}
- onBranding={() => { setDrawerOpen(false); setModal('branding'); }}
- onSettings={() => { setDrawerOpen(false); setModal('settings'); }}
- onUsers={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('users'); }}
- onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }}
- features={features} currentPage={page} isMobile={isMobile} />
setDrawerOpen(false)}
onMessages={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('chat'); }}
@@ -449,7 +450,8 @@ export default function Chat() {
onSettings={() => { setDrawerOpen(false); setModal('settings'); }}
onUsers={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('users'); }}
onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }}
- features={features} currentPage={page} isMobile={isMobile} />
+ features={features} currentPage={page} isMobile={isMobile}
+ unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} />
{modal === 'profile' && setModal(null)} />}
{modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && setModal(null)} />}
@@ -462,7 +464,7 @@ export default function Chat() {
if (page === 'groupmessages') {
return (
-
setDrawerOpen(true)} />
+ setDrawerOpen(true)} hasUnread={hasAnyUnread} />
{(!isMobile || showSidebar) && (
{ setDrawerOpen(false); setModal('settings'); }}
onUsers={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('users'); }}
onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }}
- features={features} currentPage={page} isMobile={isMobile} />
+ features={features} currentPage={page} isMobile={isMobile}
+ unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} />
{modal === 'profile' && setModal(null)} />}
{modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && setModal(null)} />}
@@ -520,7 +523,7 @@ export default function Chat() {
if (page === 'hostpanel') {
return (
-
setDrawerOpen(true)} />
+ setDrawerOpen(true)} hasUnread={hasAnyUnread} />
setModal('profile')} onHelp={() => setModal('help')} onAbout={() => setModal('about')} />
@@ -538,7 +541,8 @@ export default function Chat() {
onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }}
features={features}
currentPage={page}
- isMobile={isMobile} />
+ isMobile={isMobile}
+ unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} />
{modal === 'profile' && setModal(null)} />}
{modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && setModal(null)} />}
@@ -551,7 +555,7 @@ export default function Chat() {
if (page === 'schedule') {
return (
-
setDrawerOpen(true)} />
+ setDrawerOpen(true)} hasUnread={hasAnyUnread} />
{ setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }}
features={features}
currentPage={page}
- isMobile={isMobile} />
+ isMobile={isMobile}
+ unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} />
{modal === 'profile' && setModal(null)} />}
{modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && setModal(null)} />}
@@ -594,7 +599,7 @@ export default function Chat() {
return (
{/* Global top bar — spans full width on desktop, visible on mobile sidebar view */}
-
setDrawerOpen(true)} />
+ setDrawerOpen(true)} hasUnread={hasAnyUnread} />
{(!isMobile || showSidebar) && (
@@ -645,7 +650,8 @@ export default function Chat() {
onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }}
features={features}
currentPage={page}
- isMobile={isMobile} />
+ isMobile={isMobile}
+ unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} />
{modal === 'profile' &&
setModal(null)} />}
{modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />}
{modal === 'branding' && setModal(null)} />}