feat(iios): include participant names in listThreads (generic)

GET /v1/threads now returns participants[] (member usernames) so a host app can
render conversation titles / member lists without a second call. Still generic —
it only lists members of threads the caller belongs to.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 15:43:58 +05:30
parent ba745bb71a
commit 31f7682a46
@@ -24,6 +24,7 @@ export interface ThreadSummary {
threadId: string; threadId: string;
subject: string | null; subject: string | null;
membership?: string; membership?: string;
participants: string[];
participantCount: number; participantCount: number;
unread: number; unread: number;
lastMessage?: string; lastMessage?: string;
@@ -135,11 +136,20 @@ export class MessageService {
const threadIds = memberships.map((m) => m.threadId); const threadIds = memberships.map((m) => m.threadId);
if (threadIds.length === 0) return []; if (threadIds.length === 0) return [];
const [threads, unreads] = await Promise.all([ const [threads, unreads, allParts] = await Promise.all([
this.prisma.iiosThread.findMany({ where: { id: { in: threadIds } }, include: { _count: { select: { participants: true } } } }), this.prisma.iiosThread.findMany({ where: { id: { in: threadIds } } }),
this.prisma.iiosUnreadCounter.findMany({ where: { threadId: { in: threadIds }, actorId: actor.id } }), this.prisma.iiosUnreadCounter.findMany({ where: { threadId: { in: threadIds }, actorId: actor.id } }),
this.prisma.iiosThreadParticipant.findMany({
where: { threadId: { in: threadIds } },
include: { actor: { include: { sourceHandle: true } } },
}),
]); ]);
const unreadBy = new Map(unreads.map((u) => [u.threadId, u.unreadCount])); const unreadBy = new Map(unreads.map((u) => [u.threadId, u.unreadCount]));
const membersBy = new Map<string, string[]>();
for (const p of allParts) {
const name = p.actor?.sourceHandle?.externalId ?? p.actor?.displayName ?? p.actorId;
membersBy.set(p.threadId, [...(membersBy.get(p.threadId) ?? []), name]);
}
const summaries = await Promise.all( const summaries = await Promise.all(
threads.map(async (t) => { threads.map(async (t) => {
@@ -148,11 +158,13 @@ export class MessageService {
orderBy: { occurredAt: 'desc' }, orderBy: { occurredAt: 'desc' },
include: { parts: { where: { kind: 'TEXT' }, take: 1 } }, include: { parts: { where: { kind: 'TEXT' }, take: 1 } },
}); });
const members = membersBy.get(t.id) ?? [];
return { return {
threadId: t.id, threadId: t.id,
subject: t.subject, subject: t.subject,
membership: (t.metadata as { membership?: string } | null)?.membership, membership: (t.metadata as { membership?: string } | null)?.membership,
participantCount: t._count.participants, participants: members,
participantCount: members.length,
unread: unreadBy.get(t.id) ?? 0, unread: unreadBy.get(t.id) ?? 0,
lastMessage: last?.parts[0]?.bodyText ?? undefined, lastMessage: last?.parts[0]?.bodyText ?? undefined,
lastAt: last?.occurredAt, lastAt: last?.occurredAt,