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([]);