import { db } from "@/lib/db"; import { Prisma } from "@prisma/client"; /** * New-comment tracking for the shot-status board. A comment or reply is new to * a user when someone else wrote it on one of the shot's versions (directly or * through a task on the shot) after the user's ShotCommentRead marker. With no * marker the baseline is when the account was created, so a new user does not * inherit the whole back catalogue. * * Comments with no author (the author's account was deleted) count as someone * else's. Pasted WhatsApp notes are authored by whoever pasted them, so they * are new to everyone but that person. */ export type UnreadCommentSummary = { count: number; /** createdAt of the newest unread item — what "mark read" reads up to. */ latestAt: string; }; export async function getUnreadCommentSummaries( userId: string, projectId: string ): Promise> { const rows = await db.$queryRaw<{ shotId: string; count: number; latestAt: Date }[]>(Prisma.sql` WITH me AS ( SELECT "createdAt" FROM "users" WHERE "id" = ${userId} ), items AS ( SELECT COALESCE(v."shotId", t."shotId") AS "shotId", c."createdAt", c."authorId" FROM "comments" c JOIN "versions" v ON v."id" = c."versionId" LEFT JOIN "tasks" t ON t."id" = v."taskId" UNION ALL SELECT COALESCE(v."shotId", t."shotId") AS "shotId", r."createdAt", r."authorId" FROM "comment_replies" r JOIN "comments" c ON c."id" = r."commentId" JOIN "versions" v ON v."id" = c."versionId" LEFT JOIN "tasks" t ON t."id" = v."taskId" ) SELECT i."shotId", COUNT(*)::int AS "count", MAX(i."createdAt") AS "latestAt" FROM items i JOIN "shots" s ON s."id" = i."shotId" LEFT JOIN "shot_comment_reads" scr ON scr."shotId" = s."id" AND scr."userId" = ${userId} WHERE s."projectId" = ${projectId} AND i."authorId" IS DISTINCT FROM ${userId} AND i."createdAt" > COALESCE(scr."readAt", (SELECT "createdAt" FROM me)) GROUP BY i."shotId" `); return Object.fromEntries( rows.map((r) => [r.shotId, { count: r.count, latestAt: r.latestAt.toISOString() }]) ); } export type UnreadCommentItem = { id: string; kind: "comment" | "reply"; text: string; authorName: string; source: string | null; versionId: string; versionNumber: number; frameNumber: number; createdAt: string; }; /** The unread comments and replies on one shot, newest first. */ export async function getUnreadCommentsForShot( userId: string, shotId: string ): Promise { const [user, marker] = await Promise.all([ db.user.findUnique({ where: { id: userId }, select: { createdAt: true } }), db.shotCommentRead.findUnique({ where: { userId_shotId: { userId, shotId } } }), ]); const since = marker?.readAt ?? user?.createdAt ?? new Date(0); const onShot: Prisma.VersionWhereInput = { OR: [{ shotId }, { task: { shotId } }] }; // `not: userId` alone would also drop authorless rows (SQL NULL), which count. const byOthers = { OR: [{ authorId: null }, { authorId: { not: userId } }] }; const author = { select: { name: true, email: true } }; const version = { select: { id: true, versionNumber: true } }; const [comments, replies] = await Promise.all([ db.comment.findMany({ where: { version: onShot, createdAt: { gt: since }, ...byOthers }, select: { id: true, text: true, source: true, frameNumber: true, createdAt: true, author, version }, }), db.commentReply.findMany({ where: { comment: { version: onShot }, createdAt: { gt: since }, ...byOthers }, select: { id: true, text: true, createdAt: true, author, comment: { select: { frameNumber: true, version } }, }, }), ]); const items: UnreadCommentItem[] = [ ...comments.map((c) => ({ id: c.id, kind: "comment" as const, text: c.text, authorName: c.author?.name ?? c.author?.email ?? "Unknown", source: c.source, versionId: c.version.id, versionNumber: c.version.versionNumber, frameNumber: c.frameNumber, createdAt: c.createdAt.toISOString(), })), ...replies.map((r) => ({ id: r.id, kind: "reply" as const, text: r.text, authorName: r.author?.name ?? r.author?.email ?? "Unknown", source: null, versionId: r.comment.version.id, versionNumber: r.comment.version.versionNumber, frameNumber: r.comment.frameNumber, createdAt: r.createdAt.toISOString(), })), ]; return items.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); } /** * Move a user's marker forward to `upTo` — the newest item they were shown, * not "now", so a comment that lands while the list is open stays unread. * Never moves a marker backwards. */ export async function markShotCommentsRead(userId: string, shotId: string, upTo: Date) { const readAt = upTo > new Date() ? new Date() : upTo; const existing = await db.shotCommentRead.findUnique({ where: { userId_shotId: { userId, shotId } }, }); if (existing && existing.readAt >= readAt) return; await db.shotCommentRead.upsert({ where: { userId_shotId: { userId, shotId } }, create: { userId, shotId, readAt }, update: { readAt }, }); }