From 6fe5af8d8d11a2dab1b4b1e6716fc73e8a72f992 Mon Sep 17 00:00:00 2001 From: talksik Date: Thu, 9 Apr 2026 12:31:14 -0700 Subject: [PATCH] fix: dock app badge shows incorrect number Closes #122 We now only show number based on open streams --- js/src/features/layout.tsx | 4 +++ js/src/features/network-root.tsx | 4 --- js/src/hooks/use-dock-badge.ts | 44 ++++++++++++++++++++++++++------ js/src/hooks/use-particle.ts | 8 +++++- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/js/src/features/layout.tsx b/js/src/features/layout.tsx index ad9996d..942d1e8 100644 --- a/js/src/features/layout.tsx +++ b/js/src/features/layout.tsx @@ -18,6 +18,7 @@ import { particlePath } from "@/lib/particle-path"; import { useParticle } from "@/hooks/use-particle"; import type { Particle } from "@/api/types"; import { PropsWithChildren, useCallback } from "react"; +import { useDockBadge } from "@/hooks/use-dock-badge"; import { toast } from "sonner"; function getParticleDisplayName(particle: Particle): string { @@ -170,6 +171,9 @@ function TopBar() { } export default function Layout({ children }: PropsWithChildren) { + const { networkId } = useParams(); + useDockBadge(networkId); + return (
diff --git a/js/src/features/network-root.tsx b/js/src/features/network-root.tsx index 6f51593..794a258 100644 --- a/js/src/features/network-root.tsx +++ b/js/src/features/network-root.tsx @@ -8,8 +8,6 @@ import { ComposeOverlay } from "./compose/compose-overlay"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useStreamParticles } from "@/hooks/use-stream-particles"; import { useStreamKeyboardNav } from "@/hooks/use-stream-keyboard-nav"; -import { useDockBadge } from "@/hooks/use-dock-badge"; -import { useAuthStore } from "@/stores/auth-store"; /** * Route-level component for /:networkId (index). @@ -21,8 +19,6 @@ export default function NetworkRoot() { const path = particlePath(networkId!, []); const { streams, isLoading } = useStreamParticles(path); - const userId = useAuthStore((s) => s.user?.id); - useDockBadge(streams, userId); const [composeActive, setComposeActive] = useState(false); const [statusTab, setStatusTab] = useState<"open" | "closed">("open"); diff --git a/js/src/hooks/use-dock-badge.ts b/js/src/hooks/use-dock-badge.ts index 1e03dd3..f09de64 100644 --- a/js/src/hooks/use-dock-badge.ts +++ b/js/src/hooks/use-dock-badge.ts @@ -1,24 +1,52 @@ import { useEffect, useMemo } from "react"; +import { where } from "firebase/firestore"; +import { useLiveParticleChildren } from "@/hooks/use-particle"; +import { useAuthStore } from "@/stores/auth-store"; +import { particlePath } from "@/lib/particle-path"; import type { Particle, StreamProperties } from "@/api/types"; type StreamParticle = Particle & { type: "stream"; properties: StreamProperties }; +const openStatusFilter = where("status", "==", "open"); + /** - * Syncs the macOS dock badge with the count of unseen streams. - * Both last_child_created_at and playback_markers store the child particle's - * created_at, so they're directly comparable. + * Self-contained hook that syncs the macOS dock badge with the count of + * unseen open streams the current user is involved in. + * + * Sets up its own Firestore listener so it works independently of + * whatever stream list is rendered on screen. */ -export function useDockBadge(streams: StreamParticle[], userId: string | undefined) { +export function useDockBadge(networkId: string | undefined) { + const user = useAuthStore((s) => s.user); + const userId = user?.id; + + const visibilityScopes = useMemo(() => { + const scopes: string[] = []; + if (userId) scopes.push(`human:${userId}`); + if (networkId) scopes.push(`network:${networkId}`); + return scopes; + }, [userId, networkId]); + + const path = networkId ? particlePath(networkId, []) : undefined; + + const { children } = useLiveParticleChildren(path, { + orderByField: "last_child_created_at", + orderDirection: "desc", + visibilityScopes, + whereFilter: openStatusFilter, + }); + const unseenCount = useMemo(() => { if (!userId) return 0; - return streams.filter((s) => { - const lastActivity = s.last_child_created_at?.getTime(); + return children.filter((c): c is StreamParticle => { + if (c.type !== "stream") return false; + const lastActivity = c.last_child_created_at?.getTime(); if (!lastActivity) return false; - const marker = s.playback_markers?.[userId]?.getTime(); + const marker = c.playback_markers?.[userId]?.getTime(); if (marker === undefined) return false; return lastActivity > marker; }).length; - }, [streams, userId]); + }, [children, userId]); useEffect(() => { window.electronApp.setDockBadge(unseenCount); diff --git a/js/src/hooks/use-particle.ts b/js/src/hooks/use-particle.ts index e989a96..6055399 100644 --- a/js/src/hooks/use-particle.ts +++ b/js/src/hooks/use-particle.ts @@ -66,7 +66,7 @@ interface UseLiveParticleChildrenParams { } export function useLiveParticleChildren( - path: ParticlePath, + path: ParticlePath | undefined, { orderByField = "created_at", orderDirection = "desc", @@ -81,6 +81,12 @@ export function useLiveParticleChildren( const [error, setError] = useState(null); useEffect(() => { + if (!path) { + setChildren([]); + setIsLoading(false); + return; + } + setIsLoading(true); setError(null); setChildren([]);