import { FastifyInstance, FastifyRequest } from 'fastify'; import { prisma } from '../../../config/database'; import { authenticate } from '../middleware/auth'; interface ThreadsQuery { limit?: string; offset?: string; } export async function chatThreadsRoutes(fastify: FastifyInstance) { // All routes require authentication fastify.addHook('preHandler', authenticate); /** * GET /chat/threads * List videos where the authenticated user has commented, * ordered by latest activity, with unread counts. */ fastify.get( '/chat/threads', async ( request: FastifyRequest<{ Querystring: ThreadsQuery }>, reply ) => { try { const userId = request.user!.id; const limit = Math.min(parseInt(request.query.limit || '20', 10), 50); const offset = parseInt(request.query.offset || '0', 10); // Find distinct video IDs where user has commented const userVideoIds = await prisma.comment.findMany({ where: { userId }, select: { mediaId: true }, distinct: ['mediaId'], }); if (userVideoIds.length === 0) { return reply.send({ threads: [], total: 0 }); } const mediaIds = userVideoIds.map((c) => c.mediaId); // Get the user's read statuses const readStatuses = await prisma.chatThreadReadStatus.findMany({ where: { userId, mediaId: { in: mediaIds } }, }); const readStatusMap = new Map(readStatuses.map((r) => [r.mediaId, r.lastSeenAt])); // For each video, get latest comment and unread count const threads = await Promise.all( mediaIds.map(async (mediaId) => { const lastSeenAt = readStatusMap.get(mediaId); const [latestComment, totalComments, unreadCount, video] = await Promise.all([ prisma.comment.findFirst({ where: { mediaId, isHidden: { not: true } }, orderBy: { createdAt: 'desc' }, include: { user: { select: { id: true, name: true, email: true } }, }, }), prisma.comment.count({ where: { mediaId, isHidden: { not: true } }, }), lastSeenAt ? prisma.comment.count({ where: { mediaId, isHidden: { not: true }, createdAt: { gt: lastSeenAt }, }, }) : prisma.comment.count({ where: { mediaId, isHidden: { not: true } }, }), prisma.video.findUnique({ where: { id: mediaId }, select: { id: true, filename: true, thumbnailPath: true }, }), ]); return { mediaId, videoTitle: video?.filename || `Video #${mediaId}`, thumbnailPath: video?.thumbnailPath || null, totalComments, unreadCount, lastActivity: latestComment?.createdAt.toISOString() || null, lastMessage: latestComment ? { content: latestComment.content.length > 100 ? latestComment.content.substring(0, 100) + '...' : latestComment.content, userName: latestComment.user?.name || latestComment.user?.email || 'Anonymous', createdAt: latestComment.createdAt.toISOString(), } : null, }; }) ); // Sort by lastActivity descending threads.sort((a, b) => { if (!a.lastActivity) return 1; if (!b.lastActivity) return -1; return new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime(); }); // Paginate const paginated = threads.slice(offset, offset + limit); return reply.send({ threads: paginated, total: threads.length }); } catch (error) { console.error('Failed to fetch chat threads:', error); return reply.code(500).send({ message: 'Failed to fetch chat threads' }); } } ); /** * POST /chat/threads/:mediaId/read * Upsert ChatThreadReadStatus (lastSeenAt: now()) */ fastify.post( '/chat/threads/:mediaId/read', async ( request: FastifyRequest<{ Params: { mediaId: string } }>, reply ) => { try { const userId = request.user!.id; const mediaId = parseInt(request.params.mediaId, 10); if (isNaN(mediaId)) { return reply.code(400).send({ message: 'Invalid media ID' }); } // Find existing read status by unique index const existing = await prisma.chatThreadReadStatus.findFirst({ where: { userId, mediaId }, }); if (existing) { await prisma.chatThreadReadStatus.update({ where: { id: existing.id }, data: { lastSeenAt: new Date() }, }); } else { await prisma.chatThreadReadStatus.create({ data: { userId, mediaId, lastSeenAt: new Date() }, }); } return reply.send({ message: 'Thread marked as read' }); } catch (error) { console.error('Failed to mark thread as read:', error); return reply.code(500).send({ message: 'Failed to mark thread as read' }); } } ); }