import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { accountsById } from "~/server/people"; import { repoDb } from "~/server/scoped-db"; import { limitBrowse, limitWrite } from "~/server/limits"; import { requireRepoAccess } from "~/server/repos"; import { subjectRepoId } from "~/server/reactions"; import { forbidden, notFoundError } from "~/server/errors"; import { REACTION_CONTENTS, REACTION_SUBJECTS, REACTORS_SHOWN } from "~/lib/reactions"; import { repoRef } from "~/lib/validation"; export const toggleReaction = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ subjectType: z.enum(REACTION_SUBJECTS), subjectId: z.string().min(1).max(64), content: z.enum(REACTION_CONTENTS), }), ) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "read"); const owning = await subjectRepoId(data.subjectType, data.subjectId); if (owning !== repo.id) throw notFoundError("Nothing to react to"); if (repo.is_archived === 1) throw forbidden("This repository is archived"); const db = repoDb(repo.id); const existing = await db.first( "SELECT 1 FROM reactions WHERE subject_type = ? AND subject_id = ? AND user_id = ? AND content = ?", data.subjectType, data.subjectId, viewer.id, data.content, ); if (existing) { await db.run( "DELETE FROM reactions WHERE subject_type = ? AND subject_id = ? AND user_id = ? AND content = ?", data.subjectType, data.subjectId, viewer.id, data.content, ); } else { await db.run( "INSERT OR IGNORE INTO reactions (subject_type, subject_id, user_id, content, created_at) VALUES (?, ?, ?, ?, ?)", data.subjectType, data.subjectId, viewer.id, data.content, Date.now(), ); } return { reacted: !existing }; }); /** The first few people who reacted with one emoji, for the hover tooltip. */ export const getReactors = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ subjectType: z.enum(REACTION_SUBJECTS), subjectId: z.string().min(1).max(64), content: z.enum(REACTION_CONTENTS), }), ) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); const owning = await subjectRepoId(data.subjectType, data.subjectId); if (owning !== repo.id) throw notFoundError("Nothing to react to"); const rows = await repoDb(repo.id).all<{ user_id: string }>( `SELECT user_id FROM reactions WHERE subject_type = ? AND subject_id = ? AND content = ? ORDER BY created_at LIMIT ?`, data.subjectType, data.subjectId, data.content, REACTORS_SHOWN, ); const people = await accountsById(rows.map((row) => row.user_id)); return rows.flatMap((row) => { const person = people.get(row.user_id); return person ? [person.name] : []; }); });