Skip to content
60 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1import { useEffect } from "react";
2import { useLocation, useNavigate } from "react-router";
3
4import { applyAttention, refreshPush, reportInbox, setNavigator, setViewing, startFeed, useLiveBadges } from "../../lib/notify-client";
5import { Toaster } from "./toaster";
6
7/**
8 * Live notifications for the signed-in person, on every page: the feed
9 * socket (lib/notify-client.ts), the toasts, the count in the tab's title
10 * and on the app icon. Mounted once, in root.tsx.
11 *
12 * `workspace` is the one the page is in, whose counts the feed reads on
13 * connect; `inbox` the inbox count the page's data last read, handed to
14 * the feed so every tab shows it.
15 */
16export function LiveNotifications({ workspace, inbox }: { workspace: string | null; inbox: number | null }) {
17 const navigate = useNavigate();
18 const { pathname, search } = useLocation();
19 const badges = useLiveBadges(workspace);
20
21 useEffect(() => {
22 setNavigator((href) => navigate(href));
23 return () => setNavigator(null);
24 }, [navigate]);
25
26 useEffect(() => startFeed(workspace), [workspace]);
27
28 useEffect(() => setViewing({ path: pathname + search }), [pathname, search]);
29
30 // The inbox as the page just read it (after marking items, say).
31 useEffect(() => {
32 if (inbox != null) reportInbox(inbox);
33 }, [inbox]);
34
35 // "(3) Chat · g1t": kept on whatever title each page sets.
36 useEffect(() => {
37 applyAttention(badges);
38 const head = document.querySelector("head");
39 if (!head) return;
40 const observer = new MutationObserver(() => applyAttention(badges));
41 observer.observe(head, { subtree: true, childList: true, characterData: true });
42 return () => observer.disconnect();
43 }, [badges?.chat, badges?.mentions, badges?.inbox]); // eslint-disable-line react-hooks/exhaustive-deps
44
45 // Pushes keep arriving for someone who turned them on; a click on one
46 // with g1t open moves the tab there.
47 useEffect(() => {
48 const idle = window.setTimeout(() => void refreshPush().catch(() => {}), 3_000);
49 const onMessage = (event: MessageEvent) => {
50 if (event.data?.type === "g1t:navigate" && typeof event.data.href === "string" && event.data.href.startsWith("/")) navigate(event.data.href);
51 };
52 navigator.serviceWorker?.addEventListener("message", onMessage);
53 return () => {
54 window.clearTimeout(idle);
55 navigator.serviceWorker?.removeEventListener("message", onMessage);
56 };
57 }, [navigate]);
58
59 return <Toaster />;
60}

This file's history is long; its oldest lines are credited to the oldest commit read.