Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
83 linesCodeBlameRaw
1import { useEffect } from "react";
2import { Outlet, data, type ShouldRevalidateFunctionArgs } from "react-router";
3
4import { type FoliosSidebar as Sidebar, shownUsername } from "@g1t/contracts";
5
6import type { Route } from "./+types/layout";
7import { sidebarChangedSinceLoad, sidebarLoaded } from "../../../components/folios/actions";
8import type { DocMentionable } from "../../../components/folios/doc/editor";
9import { workspacePeople } from "../../../lib/chat.server";
10import { readCookie } from "../../../lib/mission";
11import { folios } from "../../../lib/services.server";
12import { requireUser, roleIn } from "../../../lib/session.server";
13import { knownTimeZone } from "../../../lib/time-zone";
14
15/**
16 * Artifacts mode (code says "folio"): what its
17 * sidebar shows (the shell draws it from this data: favorites, spaces and
18 * their trees, Private, Shared, projects' docs), who can be mentioned or
19 * shared with, and the viewer's time zone for the home list's days. Each
20 * part is empty rather than an error when its service does not answer.
21 */
22export type FoliosLayoutData = {
23 slug: string;
24 role: string;
25 me: { key: string; name: string; display_name: string; avatar: string | null };
26 sidebar: Sidebar | null;
27 mentionables: DocMentionable[];
28 /** The viewer's zone, as their browser last told the site (Home sets it); null until then. */
29 zone: string | null;
30};
31
32export async function loader({ params, context, request }: Route.LoaderArgs): Promise<FoliosLayoutData> {
33 const viewer = requireUser(context, request);
34 const role = roleIn(viewer, params.owner);
35 if (!role) throw data(null, { status: 404 });
36 const slug = params.owner.toLowerCase();
37 const [sidebar, { people, agents }] = await Promise.all([
38 folios
39 .sidebar(slug, viewer)
40 .then((r) => (r.ok ? r.value : null))
41 .catch(() => null),
42 workspacePeople(slug, viewer).catch(() => ({ people: [], agents: [] })),
43 ]);
44 const self = people.find((p) => p.name === viewer.username.toLowerCase());
45 const zone = readCookie(request.headers.get("cookie"), "g1t_tz");
46 return {
47 slug,
48 role,
49 me: {
50 key: `user:${viewer.id}`,
51 name: viewer.username.toLowerCase(),
52 display_name: self?.display_name ?? shownUsername({ username: viewer.username, display_username: viewer.display_username ?? null }),
53 avatar: viewer.avatar ?? null,
54 },
55 sidebar,
56 mentionables: [
57 ...people.map((p): DocMentionable => ({ kind: "user", id: p.name, name: p.name, display_name: p.display_name, avatar: p.avatar })),
58 ...agents.map((a): DocMentionable => ({ kind: "agent", id: a.id, name: a.handle, display_name: a.display_name, avatar: a.avatar, avatar_seed: a.avatar_seed ?? null, look: a.look ?? null })),
59 ],
60 zone: zone && knownTimeZone(zone) ? zone : null,
61 };
62}
63
64/**
65 * The sidebar changes when something is made, renamed, moved, shared,
66 * trashed, restored or deleted: a submission, a page asking again with
67 * `useRevalidator`, or a change followed by a navigation (which
68 * `takeSidebarChange` remembers, since a navigation cancels a reload in
69 * flight, so the sidebar reloads with it); not when moving between
70 * pages.
71 */
72export function shouldRevalidate({ currentParams, nextParams, currentUrl, nextUrl, formMethod, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {
73 if (sidebarChangedSinceLoad()) return true;
74 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
75 if (currentUrl.href === nextUrl.href) return defaultShouldRevalidate;
76 return currentParams.owner !== nextParams.owner;
77}
78
79export default function FoliosLayout({ loaderData }: Route.ComponentProps) {
80 // Fresh sidebar data: every change made before it is shown.
81 useEffect(() => sidebarLoaded(), [loaderData]);
82 return <Outlet />;
83}