changemaker.lite/api/src/modules/media/routes/chat-threads.routes.ts

162 lines
5.4 KiB
TypeScript

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' });
}
}
);
}