162 lines
5.4 KiB
TypeScript
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' });
|
|
}
|
|
}
|
|
);
|
|
}
|