feat(communication): add chat reactions emojis and images

This commit is contained in:
2026-10-03 17:56:07 +02:00
parent 59538f46b7
commit 29a9ec2242
25 changed files with 737 additions and 57 deletions

View File

@@ -111,6 +111,9 @@ export async function collaborationRoutes(server: FastifyInstance, options: Coll
if (action === "room") return service.room(ctx.principal, id)
if (action === "messages") return service.messages(ctx.principal, id, input)
if (action === "send") return service.send(ctx.principal, id, input)
if (action === "image") return service.image(ctx.principal, id, input)
if (action === "reactions") return service.reactions(ctx.principal, id, input)
if (action === "react") return service.react(ctx.principal, id, input)
if (action === "read") return service.markRead(ctx.principal, id, input.seq)
return { members: await service.members(ctx.principal, id) }
}
@@ -118,6 +121,9 @@ export async function collaborationRoutes(server: FastifyInstance, options: Coll
server.get("/collaboration/rooms/:id/messages", async (req) => request(req, "messages"))
server.post("/collaboration/rooms/:id/messages", async (req) => request(req, "send"))
server.post("/collaboration/rooms/:id/read", async (req) => request(req, "read"))
server.get("/collaboration/rooms/:id/image", async (req) => request(req, "image"))
server.get("/collaboration/rooms/:id/reactions", async (req) => request(req, "reactions"))
server.put("/collaboration/rooms/:id/reactions", async (req) => request(req, "react"))
server.get("/collaboration/rooms/:id/members", async (req) => request(req, "members"))
}
@@ -137,5 +143,8 @@ export async function collaborationGuestRoutes(server: FastifyInstance, { servic
server.get("/guest/api/rooms/:id/messages", async (req: any) => service.messages(await principal(req), req.params.id, req.query))
server.get("/guest/api/rooms/:id/members", async (req: any) => ({ members: await service.members(await principal(req), req.params.id) }))
server.post("/guest/api/rooms/:id/messages", async (req: any) => service.send(await principal(req), req.params.id, body(req)))
server.get("/guest/api/rooms/:id/image", async (req: any) => service.image(await principal(req), req.params.id, req.query))
server.get("/guest/api/rooms/:id/reactions", async (req: any) => service.reactions(await principal(req), req.params.id, req.query))
server.put("/guest/api/rooms/:id/reactions", async (req: any) => service.react(await principal(req), req.params.id, body(req)))
server.post("/guest/api/rooms/:id/read", async (req: any) => service.markRead(await principal(req), req.params.id, body(req).seq))
}

View File

@@ -15,6 +15,7 @@ import {
} from "../../db/schema"
import { NotificationService, UserDirectory } from "../modules/notification.service"
import { assertCustomRoomKey, chatError, projectChatAccess } from "../modules/communication-access"
import { chatContent, chatImage, chatReactions, imageMetadata, setChatReaction } from "../modules/chat-content"
type ChatUser = {
userId: string
@@ -160,7 +161,10 @@ export default async function communicationRoutes(server: FastifyInstance) {
const messagesForRoom = async (roomId: string, currentUserId: string, tenantId: number, afterId = 0, limit = 50) => {
const conditions = [eq(communicationMessages.roomId, roomId)]
if (afterId > 0) conditions.push(gt(communicationMessages.id, afterId))
const rows = await server.db.select().from(communicationMessages)
const rows = await server.db.select({ id: communicationMessages.id, body: communicationMessages.body,
authorUserId: communicationMessages.authorUserId, createdAt: communicationMessages.createdAt,
image: sql`${communicationMessages.image} - 'dataUrl'`,
}).from(communicationMessages)
.where(and(...conditions))
.orderBy(afterId > 0 ? asc(communicationMessages.id) : desc(communicationMessages.id))
.limit(Math.min(Math.max(limit, 1), 100))
@@ -181,6 +185,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
return orderedRows.map((message) => ({
id: message.id,
body: message.body,
image: imageMetadata(message.image),
sender: message.authorUserId,
senderDisplayName: displayName(authorById.get(message.authorUserId) || {}),
timestamp: message.createdAt.getTime(),
@@ -466,17 +471,33 @@ export default async function communicationRoutes(server: FastifyInstance) {
return { ...roomToApi(room), messages, nextId: messages[messages.length - 1]?.id || afterId }
})
server.post("/communication/chat/rooms/:roomKey/messages", async (req: any, reply) => {
server.post("/communication/chat/rooms/:roomKey/messages", async (req: any) => {
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
const body = String(req.body?.text || "").trim()
if (!body) return reply.code(400).send({ error: "Nachricht darf nicht leer sein" })
if (body.length > 10_000) return reply.code(400).send({ error: "Nachricht ist zu lang" })
const { body, image } = await chatContent(req.body)
const [created] = await server.db.insert(communicationMessages).values({
tenantId, roomId: room.id, authorUserId: req.user.user_id, body,
tenantId, roomId: room.id, authorUserId: req.user.user_id, body, image,
}).returning()
await notifyMessageRecipients(tenantId, room, req.user.user_id, { id: created.id, body })
return { id: created.id, body: created.body, sender: created.authorUserId, senderDisplayName: "Du", timestamp: created.createdAt.getTime(), own: true }
await notifyMessageRecipients(tenantId, room, req.user.user_id, { id: created.id, body: body || "📷 Bild" })
return { id: created.id, body: created.body, image: imageMetadata(created.image), sender: created.authorUserId, senderDisplayName: "Du", timestamp: created.createdAt.getTime(), own: true }
})
server.get("/communication/chat/rooms/:roomKey/image", async (req: any, reply) => {
reply.header("Cache-Control", "no-store")
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
return chatImage(server.db, "communication", room.id, req.query.messageId)
})
server.get("/communication/chat/rooms/:roomKey/reactions", async (req: any, reply) => {
reply.header("Cache-Control", "no-store")
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
return chatReactions(server.db, "communication", room.id, req.user.user_id, req.query.ids)
})
server.put("/communication/chat/rooms/:roomKey/reactions", async (req: any) => {
const tenantId = await requireTenant(req)
const room = await requireRoom(req, tenantId, req.user.user_id, req.params.roomKey)
return setChatReaction(server.db, "communication", room.id, req.user.user_id, req.body)
})
server.post("/communication/chat/rooms/:roomKey/read", async (req: any) => {