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.

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