From a9787ef041304d2728e1d14f3c932d01324a327c Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 10 Jun 2026 17:32:38 -0400 Subject: [PATCH] feat: Windows taskbar notification badge counter Adds a Tauri command (set_badge_count) that draws a red circle badge with a highlight count onto the Windows taskbar button overlay icon, matching Discord's behavior. Badge shows @mention/highlight count only (not total messages), clears to zero when all highlights are read. Frontend: useTauriNotificationBadge hook reads roomToUnreadAtom and calls set_badge_count via window.__TAURI_INTERNALS__.invoke whenever the unread map changes. No-ops silently in the browser (non-Tauri). Mounted as TauriEffects inside JotaiProvider in App.tsx. Co-Authored-By: Claude Sonnet 4.6 --- src/app/hooks/useTauriNotificationBadge.ts | 25 ++++++++++++++++++++++ src/app/pages/App.tsx | 7 ++++++ 2 files changed, 32 insertions(+) create mode 100644 src/app/hooks/useTauriNotificationBadge.ts diff --git a/src/app/hooks/useTauriNotificationBadge.ts b/src/app/hooks/useTauriNotificationBadge.ts new file mode 100644 index 000000000..37801ea63 --- /dev/null +++ b/src/app/hooks/useTauriNotificationBadge.ts @@ -0,0 +1,25 @@ +import { useEffect } from 'react'; +import { useAtomValue } from 'jotai'; +import { roomToUnreadAtom } from '../state/room/roomToUnread'; + +// Tauri v2 injects __TAURI_INTERNALS__ into the webview at runtime. +// We use it directly so cinny doesn't need @tauri-apps/api as a dependency. +type TauriInternals = { invoke: (cmd: string, args?: Record) => Promise }; +const tauriInvoke = (): TauriInternals['invoke'] | undefined => + (window as unknown as { __TAURI_INTERNALS__?: TauriInternals }).__TAURI_INTERNALS__?.invoke; + +export function useTauriNotificationBadge() { + const roomToUnread = useAtomValue(roomToUnreadAtom); + + useEffect(() => { + const invoke = tauriInvoke(); + if (!invoke) return; + + let totalHighlights = 0; + roomToUnread.forEach((unread) => { + totalHighlights += unread.highlight; + }); + + invoke('set_badge_count', { count: totalHighlights }).catch(() => {}); + }, [roomToUnread]); +} diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index 322e15f32..82a9e9021 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -15,6 +15,7 @@ import { ScreenSizeProvider, useScreenSize } from '../hooks/useScreenSize'; import { useCompositionEndTracking } from '../hooks/useComposingCheck'; import { settingsAtom } from '../state/settings'; import { LotusToastContainer } from '../features/toast/LotusToastContainer'; +import { useTauriNotificationBadge } from '../hooks/useTauriNotificationBadge'; const FONT_MAP: Record = { system: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", @@ -52,6 +53,11 @@ function AppearanceEffects() { return null; } +function TauriEffects() { + useTauriNotificationBadge(); + return null; +} + function NightLightOverlay() { const settings = useAtomValue(settingsAtom); if (!settings.nightLightEnabled) return null; @@ -131,6 +137,7 @@ function App() { +