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.

75 linesCodeBlameRaw
1import { data } from "react-router";
2
3import { MAX_EMOJI_BYTES } from "@g1t/contracts";
4
5import type { Route } from "./+types/emoji";
6import { type EmojiActionResult, EmojiSettings } from "../../components/emoji/settings";
7import { ErrorText } from "../../components/ui";
8import { useAddresses } from "../../lib/addresses";
9import { sniffImage, toBase64 } from "../../lib/avatar-upload";
10import { page } from "../../lib/meta";
11import { chat } from "../../lib/services.server";
12import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
13
14export function meta({ params, ...args }: Route.MetaArgs) {
15 return page(args, { title: `Emoji · ${params.owner} · g1t` });
16}
17
18export async function loader({ params, context, request }: Route.LoaderArgs) {
19 const viewer = requireUser(context, request);
20 if (!roleIn(getViewer(context), params.owner)) throw data(null, { status: 404 });
21 const slug = params.owner.toLowerCase();
22 const list = await chat.listEmoji(slug, viewer).catch(() => null);
23 return {
24 slug,
25 me: viewer.id,
26 list: list?.ok ? list.value : null,
27 error: list && !list.ok ? list.error.message : list ? null : "Chat didn't answer. Try again in a moment.",
28 };
29}
30
31/** The types the chat service keeps; it reads them from the bytes again. */
32const TYPES = new Set(["image/png", "image/gif", "image/webp"]);
33
34export async function action({ params, context, request }: Route.ActionArgs): Promise<EmojiActionResult> {
35 assertSameOrigin(request);
36 const viewer = requireUser(context, request);
37 const slug = params.owner.toLowerCase();
38 const form = await request.formData();
39 const intent = String(form.get("intent") ?? "");
40 const fail = (error: string): EmojiActionResult => ({ ok: false, intent, error });
41 try {
42 if (intent === "add") {
43 const file = form.get("file");
44 if (!file || typeof file === "string" || file.size === 0) return fail("Choose an image.");
45 if (file.size > MAX_EMOJI_BYTES) return fail(`An emoji image is at most ${MAX_EMOJI_BYTES / 1024} KB.`);
46 const bytes = new Uint8Array(await file.arrayBuffer());
47 if (!TYPES.has(sniffImage(bytes) ?? "")) return fail("Use a PNG, GIF or WebP image.");
48 const added = await chat.addEmoji(slug, viewer, String(form.get("name") ?? ""), { data: toBase64(bytes) });
49 return added.ok ? { ok: true, intent } : fail(added.error.message);
50 }
51 if (intent === "alias") {
52 const made = await chat.aliasEmoji(slug, viewer, String(form.get("name") ?? ""), String(form.get("target") ?? ""));
53 return made.ok ? { ok: true, intent } : fail(made.error.message);
54 }
55 if (intent === "remove") {
56 const removed = await chat.removeEmoji(slug, viewer, String(form.get("name") ?? ""));
57 return removed.ok ? { ok: true, intent } : fail(removed.error.message);
58 }
59 if (intent === "setting") {
60 const value = form.get("value") === "admins" ? "admins" : "members";
61 const set = await chat.setEmojiUpload(slug, viewer, value);
62 return set.ok ? { ok: true, intent } : fail(set.error.message);
63 }
64 return fail("Unknown request.");
65 } catch (error) {
66 console.error("emoji: the chat service did not answer", error);
67 return fail("Chat didn't answer. Try again in a moment.");
68 }
69}
70
71export default function WorkspaceEmoji({ loaderData }: Route.ComponentProps) {
72 const { usercontent } = useAddresses();
73 if (!loaderData.list) return <ErrorText>{loaderData.error}</ErrorText>;
74 return <EmojiSettings slug={loaderData.slug} list={loaderData.list} usercontent={usercontent} meId={loaderData.me} />;
75}