185 lines
6.2 KiB
TypeScript
185 lines
6.2 KiB
TypeScript
import { z } from 'zod'
|
|
import { router, memberProcedure } from '../trpc'
|
|
import { TRPCError } from '@trpc/server'
|
|
|
|
export const messagesRouter = router({
|
|
// List all conversations for the current member
|
|
getConversations: memberProcedure.query(async ({ ctx }) => {
|
|
const member = await ctx.prisma.member.findFirst({
|
|
where: { userId: ctx.session.user.id, orgId: ctx.orgId },
|
|
})
|
|
if (!member) throw new TRPCError({ code: 'NOT_FOUND' })
|
|
|
|
const convMembers = await ctx.prisma.conversationMember.findMany({
|
|
where: { memberId: member.id },
|
|
include: {
|
|
conversation: {
|
|
include: {
|
|
members: {
|
|
include: { member: { select: { id: true, name: true, betrieb: true, avatarUrl: true } } },
|
|
},
|
|
messages: {
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 1,
|
|
select: { body: true, createdAt: true, senderId: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
orderBy: { conversation: { updatedAt: 'desc' } },
|
|
})
|
|
|
|
return convMembers.map((cm) => {
|
|
const other = cm.conversation.members.find((m) => m.memberId !== member.id)?.member
|
|
const lastMsg = cm.conversation.messages[0] ?? null
|
|
const unread =
|
|
lastMsg &&
|
|
(!cm.lastReadAt || lastMsg.createdAt > cm.lastReadAt) &&
|
|
lastMsg.senderId !== member.id
|
|
return {
|
|
conversationId: cm.conversationId,
|
|
other,
|
|
lastMessage: lastMsg,
|
|
hasUnread: !!unread,
|
|
updatedAt: cm.conversation.updatedAt,
|
|
}
|
|
})
|
|
}),
|
|
|
|
// Get or create a 1-on-1 conversation between current member and another
|
|
getOrCreate: memberProcedure
|
|
.input(z.object({ otherMemberId: z.string() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const member = await ctx.prisma.member.findFirst({
|
|
where: { userId: ctx.session.user.id, orgId: ctx.orgId },
|
|
})
|
|
if (!member) throw new TRPCError({ code: 'NOT_FOUND' })
|
|
|
|
const other = await ctx.prisma.member.findFirst({
|
|
where: { id: input.otherMemberId, orgId: ctx.orgId },
|
|
})
|
|
if (!other) throw new TRPCError({ code: 'NOT_FOUND', message: 'Mitglied nicht gefunden' })
|
|
|
|
// Find existing conversation between the two members in this org
|
|
const existing = await ctx.prisma.conversation.findFirst({
|
|
where: {
|
|
orgId: ctx.orgId,
|
|
members: { every: { memberId: { in: [member.id, other.id] } } },
|
|
AND: [
|
|
{ members: { some: { memberId: member.id } } },
|
|
{ members: { some: { memberId: other.id } } },
|
|
],
|
|
},
|
|
})
|
|
|
|
if (existing) return { conversationId: existing.id }
|
|
|
|
const conv = await ctx.prisma.conversation.create({
|
|
data: {
|
|
orgId: ctx.orgId,
|
|
members: {
|
|
create: [{ memberId: member.id }, { memberId: other.id }],
|
|
},
|
|
},
|
|
})
|
|
return { conversationId: conv.id }
|
|
}),
|
|
|
|
// Get messages for a conversation
|
|
getMessages: memberProcedure
|
|
.input(z.object({ conversationId: z.string(), cursor: z.string().optional() }))
|
|
.query(async ({ ctx, input }) => {
|
|
const member = await ctx.prisma.member.findFirst({
|
|
where: { userId: ctx.session.user.id, orgId: ctx.orgId },
|
|
})
|
|
if (!member) throw new TRPCError({ code: 'NOT_FOUND' })
|
|
|
|
// Verify membership in conversation
|
|
const cm = await ctx.prisma.conversationMember.findUnique({
|
|
where: { conversationId_memberId: { conversationId: input.conversationId, memberId: member.id } },
|
|
})
|
|
if (!cm) throw new TRPCError({ code: 'FORBIDDEN' })
|
|
|
|
const messages = await ctx.prisma.message.findMany({
|
|
where: { conversationId: input.conversationId },
|
|
include: { sender: { select: { id: true, name: true, avatarUrl: true } } },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 40,
|
|
...(input.cursor ? { skip: 1, cursor: { id: input.cursor } } : {}),
|
|
})
|
|
|
|
// Mark as read
|
|
await ctx.prisma.conversationMember.update({
|
|
where: { conversationId_memberId: { conversationId: input.conversationId, memberId: member.id } },
|
|
data: { lastReadAt: new Date() },
|
|
})
|
|
|
|
return {
|
|
messages: messages.reverse(),
|
|
nextCursor: messages.length === 40 ? messages[0]?.id : undefined,
|
|
}
|
|
}),
|
|
|
|
// Send a message
|
|
sendMessage: memberProcedure
|
|
.input(z.object({ conversationId: z.string(), body: z.string().min(1).max(2000) }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const member = await ctx.prisma.member.findFirst({
|
|
where: { userId: ctx.session.user.id, orgId: ctx.orgId },
|
|
})
|
|
if (!member) throw new TRPCError({ code: 'NOT_FOUND' })
|
|
|
|
const cm = await ctx.prisma.conversationMember.findUnique({
|
|
where: { conversationId_memberId: { conversationId: input.conversationId, memberId: member.id } },
|
|
})
|
|
if (!cm) throw new TRPCError({ code: 'FORBIDDEN' })
|
|
|
|
const message = await ctx.prisma.message.create({
|
|
data: {
|
|
conversationId: input.conversationId,
|
|
senderId: member.id,
|
|
body: input.body.trim(),
|
|
},
|
|
include: { sender: { select: { id: true, name: true, avatarUrl: true } } },
|
|
})
|
|
|
|
// Update conversation updatedAt so it sorts to top
|
|
await ctx.prisma.conversation.update({
|
|
where: { id: input.conversationId },
|
|
data: { updatedAt: new Date() },
|
|
})
|
|
|
|
return message
|
|
}),
|
|
|
|
// Count total unread conversations
|
|
unreadCount: memberProcedure.query(async ({ ctx }) => {
|
|
const member = await ctx.prisma.member.findFirst({
|
|
where: { userId: ctx.session.user.id, orgId: ctx.orgId },
|
|
})
|
|
if (!member) return { count: 0 }
|
|
|
|
const convMembers = await ctx.prisma.conversationMember.findMany({
|
|
where: { memberId: member.id },
|
|
include: {
|
|
conversation: {
|
|
include: {
|
|
messages: {
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 1,
|
|
select: { createdAt: true, senderId: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
const count = convMembers.filter((cm) => {
|
|
const last = cm.conversation.messages[0]
|
|
return last && last.senderId !== member.id && (!cm.lastReadAt || last.createdAt > cm.lastReadAt)
|
|
}).length
|
|
|
|
return { count }
|
|
}),
|
|
})
|