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