diff --git a/src/App.test.tsx b/src/App.test.tsx index eebcd47..5edf7d8 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -13,6 +13,7 @@ vi.mock("./lib/tauri", async (importOriginal) => { return { ...actual, authGetStatus: vi.fn().mockResolvedValue({ connected: true, username: "test-user", error: null }), + listNotifications: vi.fn().mockResolvedValue([]), }; }); diff --git a/src/components/Sidebar/Sidebar.test.tsx b/src/components/Sidebar/Sidebar.test.tsx index 8553b93..55c419f 100644 --- a/src/components/Sidebar/Sidebar.test.tsx +++ b/src/components/Sidebar/Sidebar.test.tsx @@ -66,6 +66,38 @@ vi.mock("../../lib/tauri", () => ({ lastSyncAt: "2026-03-28T10:00:00Z", }, ]), + listNotifications: vi.fn().mockResolvedValue([ + { + id: "notif-1", + title: "Unread review request", + url: "https://github.com/acme/frontend/pull/1", + unread: true, + updatedAt: "2026-03-28T10:00:00Z", + repo: "acme/frontend", + reason: "review_requested", + notificationType: "pullRequest", + }, + { + id: "notif-2", + title: "Unread issue mention", + url: "https://github.com/acme/frontend/issues/2", + unread: true, + updatedAt: "2026-03-28T11:00:00Z", + repo: "acme/frontend", + reason: "mention", + notificationType: "issue", + }, + { + id: "notif-3", + title: "Read merge comment", + url: "https://github.com/acme/frontend/pull/3", + unread: false, + updatedAt: "2026-03-28T12:00:00Z", + repo: "acme/frontend", + reason: "comment", + notificationType: "pullRequest", + }, + ]), setRepoEnabled: vi.fn().mockResolvedValue({}), authGetStatus: vi.fn().mockResolvedValue({ connected: true, @@ -109,6 +141,11 @@ describe("Sidebar", () => { expect(screen.getByText("3")).toBeInTheDocument(); }); + it("should show unread notification count", async () => { + renderSidebar(); + expect(await screen.findByRole("button", { name: /notifications \(2\)/i })).toBeInTheDocument(); + }); + it("should show workspace dots with state colors", () => { const { container } = renderSidebar(); const dots = container.querySelectorAll("[data-state]"); diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 7fc409f..5054916 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -6,7 +6,7 @@ import type { DashboardView } from "../../stores/dashboard"; import { useWorkspacesStore } from "../../stores/workspaces"; import { useGitHubData } from "../../hooks/useGitHubData"; import { FOCUS_RING } from "../../lib/a11y"; -import { listRepos, setRepoEnabled, authGetStatus } from "../../lib/tauri"; +import { listNotifications, listRepos, setRepoEnabled, authGetStatus } from "../../lib/tauri"; import { NavItem } from "./NavItem"; import { WorkspaceList } from "./WorkspaceList"; import { RepoList } from "./RepoList"; @@ -54,6 +54,15 @@ export function Sidebar(): ReactElement { refetchOnWindowFocus: false, }); + const isGitHubConnected = authQuery.data?.connected === true; + + const notificationsQuery = useQuery({ + queryKey: ["github", "notifications"], + queryFn: listNotifications, + staleTime: 60_000, + enabled: isGitHubConnected, + }); + const toggleRepoMutation = useMutation({ mutationFn: ({ repoId, enabled }: { repoId: string; enabled: boolean }) => setRepoEnabled(repoId, enabled), @@ -87,6 +96,8 @@ export function Sidebar(): ReactElement { const repos = reposQuery.data ?? []; const enabledRepos = repos.filter((r) => r.enabled); const username = authQuery.data?.username ?? null; + const unreadNotificationsCount = + notificationsQuery.data?.filter((item) => item.unread).length ?? 0; useEffect(() => { if (typeof document !== "undefined" && navGroupRef.current?.contains(document.activeElement)) { @@ -173,7 +184,13 @@ export function Sidebar(): ReactElement { }} label={item.label} view={item.view} - count={item.countKey && stats ? stats[item.countKey] : undefined} + count={ + item.view === "notifications" + ? unreadNotificationsCount + : item.countKey && stats + ? stats[item.countKey] + : undefined + } isActive={currentView === item.view} tabIndex={focusedView === item.view ? 0 : -1} onClick={handleNavClick}