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
62 linesCodeBlameRaw
1import { useEffect } from "react";
2import { redirect, useNavigate } from "react-router";
3
4import type { Route } from "./+types/index";
5import { ChatUnavailable, NoChannels } from "../../../components/chat/empty";
6import { ChatSidebar } from "../../../components/chat/sidebar";
7import { NewMessageButton } from "../../../components/chat/actions";
8import { isPhone } from "../../../lib/phone";
9import { channelPath } from "../../../lib/chat";
10import { LAST_CHAT_COOKIE, sidebarOrNull } from "../../../lib/chat.server";
11import { readCookie } from "../../../lib/mission";
12import { page } from "../../../lib/meta";
13import { requireUser } from "../../../lib/session.server";
14
15export function meta({ params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `Chat · ${params.owner} · g1t` });
17}
18
19/**
20 * Chat opens where it was left: the last conversation open in this
21 * workspace, else #general, else the first there is.
22 */
23export async function loader({ params, context, request }: Route.LoaderArgs) {
24 const viewer = requireUser(context, request);
25 const slug = params.owner.toLowerCase();
26 // A phone opens on the list of conversations; a wider screen on one.
27 const phone = isPhone(request.headers);
28 const last = readCookie(request.headers.get("cookie"), LAST_CHAT_COOKIE);
29 const remembered = last && last.startsWith(`/${slug}/-/chat/`) && !last.includes("..") ? last : null;
30 if (remembered && !phone) throw redirect(remembered);
31 const sidebar = await sidebarOrNull(slug, viewer);
32 if (!sidebar) return { unavailable: true, phone, to: null };
33 const entries = sidebar.entries;
34 const first =
35 entries.find((entry) => entry.channel.kind === "channel" && entry.channel.name === "general") ??
36 entries.find((entry) => entry.channel.kind === "channel") ??
37 entries[0];
38 const to = remembered ?? (first ? channelPath(slug, first.channel) : null);
39 if (to && !phone) throw redirect(to);
40 return { unavailable: false, phone, to };
41}
42
43export default function ChatIndex({ loaderData, params }: Route.ComponentProps) {
44 const navigate = useNavigate();
45 const { to } = loaderData;
46 // Told it was a phone, but wider: on to the conversation after all.
47 useEffect(() => {
48 if (to && window.matchMedia("(min-width: 768px)").matches) navigate(to, { replace: true });
49 }, [to, navigate]);
50 return (
51 <>
52 {/* A phone: the list of conversations, full screen. */}
53 <div className="h-[calc(100dvh-var(--topbar-h)-var(--tabbar-h))] md:hidden">
54 <ChatSidebar slug={params.owner.toLowerCase()} heading={false} />
55 <div className="fixed right-4 bottom-[calc(var(--tabbar-h)+1rem)] z-30">
56 <NewMessageButton slug={params.owner.toLowerCase()} variant="fab" />
57 </div>
58 </div>
59 <div className="max-md:hidden">{loaderData.unavailable ? <ChatUnavailable /> : to ? null : <NoChannels />}</div>
60 </>
61 );
62}