| 1 | import { data } from "react-router"; |
| 2 | |
| 3 | import { MAX_EMOJI_BYTES } from "@g1t/contracts"; |
| 4 | |
| 5 | import type { Route } from "./+types/emoji"; |
| 6 | import { type EmojiActionResult, EmojiSettings } from "../../components/emoji/settings"; |
| 7 | import { ErrorText } from "../../components/ui"; |
| 8 | import { useAddresses } from "../../lib/addresses"; |
| 9 | import { sniffImage, toBase64 } from "../../lib/avatar-upload"; |
| 10 | import { page } from "../../lib/meta"; |
| 11 | import { chat } from "../../lib/services.server"; |
| 12 | import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server"; |
| 13 | |
| 14 | export function meta({ params, ...args }: Route.MetaArgs) { |
| 15 | return page(args, { title: `Emoji · ${params.owner} · g1t` }); |
| 16 | } |
| 17 | |
| 18 | export 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. */ |
| 27 | const TYPES = new Set(["image/png", "image/gif", "image/webp"]); |
| 28 | |
| 29 | export 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 | |
| 66 | export 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 | } |