Skip to content
70 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 { slug, me: viewer.id, list: list?.ok ? list.value : null, error: list && !list.ok ? list.error.message : list ? null : "Chat didn't answer. Try again in a moment." };
24}
25
26/** The types the chat service keeps; it reads them from the bytes again. */
27const TYPES = new Set(["image/png", "image/gif", "image/webp"]);
28
29export async function action({ params, context, request }: Route.ActionArgs): Promise<EmojiActionResult> {
30 assertSameOrigin(request);
31 const viewer = requireUser(context, request);
32 const slug = params.owner.toLowerCase();
33 const form = await request.formData();
34 const intent = String(form.get("intent") ?? "");
35 const fail = (error: string): EmojiActionResult => ({ ok: false, intent, error });
36 try {
37 if (intent === "add") {
38 const file = form.get("file");
39 if (!file || typeof file === "string" || file.size === 0) return fail("Choose an image.");
40 if (file.size > MAX_EMOJI_BYTES) return fail(`An emoji image is at most ${MAX_EMOJI_BYTES / 1024} KB.`);
41 const bytes = new Uint8Array(await file.arrayBuffer());
42 if (!TYPES.has(sniffImage(bytes) ?? "")) return fail("Use a PNG, GIF or WebP image.");
43 const added = await chat.addEmoji(slug, viewer, String(form.get("name") ?? ""), { data: toBase64(bytes) });
44 return added.ok ? { ok: true, intent } : fail(added.error.message);
45 }
46 if (intent === "alias") {
47 const made = await chat.aliasEmoji(slug, viewer, String(form.get("name") ?? ""), String(form.get("target") ?? ""));
48 return made.ok ? { ok: true, intent } : fail(made.error.message);
49 }
50 if (intent === "remove") {
51 const removed = await chat.removeEmoji(slug, viewer, String(form.get("name") ?? ""));
52 return removed.ok ? { ok: true, intent } : fail(removed.error.message);
53 }
54 if (intent === "setting") {
55 const value = form.get("value") === "admins" ? "admins" : "members";
56 const set = await chat.setEmojiUpload(slug, viewer, value);
57 return set.ok ? { ok: true, intent } : fail(set.error.message);
58 }
59 return fail("Unknown request.");
60 } catch (error) {
61 console.error("emoji: the chat service did not answer", error);
62 return fail("Chat didn't answer. Try again in a moment.");
63 }
64}
65
66export default function WorkspaceEmoji({ loaderData }: Route.ComponentProps) {
67 const { usercontent } = useAddresses();
68 if (!loaderData.list) return <ErrorText>{loaderData.error}</ErrorText>;
69 return <EmojiSettings slug={loaderData.slug} list={loaderData.list} usercontent={usercontent} meId={loaderData.me} />;
70}