diff --git a/apps/dispatch-server/index.ts b/apps/dispatch-server/index.ts index 9a05a1d6..6ed92107 100644 --- a/apps/dispatch-server/index.ts +++ b/apps/dispatch-server/index.ts @@ -5,9 +5,10 @@ import { Server } from "socket.io"; import { createAdapter } from "@socket.io/redis-adapter"; import { jwtMiddleware } from "modules/socketJWTmiddleware"; import { pubClient, subClient } from "modules/redis"; -import { handle } from "socket-events/connect-dispatch"; +import { handleConnectDispatch } from "socket-events/connect-dispatch"; import router from "routes/router"; import cors from "cors"; +import { handleSendMessage } from "socket-events/send-message"; const app = express(); const server = createServer(app); @@ -19,7 +20,8 @@ const io = new Server(server, { io.use(jwtMiddleware); io.on("connection", (socket) => { - socket.on("connect-dispatch", handle(socket, io)); + socket.on("connect-dispatch", handleConnectDispatch(socket, io)); + socket.on("send-message", handleSendMessage(socket, io)); }); app.use(cors()); diff --git a/apps/dispatch-server/routes/dispatcher.ts b/apps/dispatch-server/routes/dispatcher.ts new file mode 100644 index 00000000..937d1bc4 --- /dev/null +++ b/apps/dispatch-server/routes/dispatcher.ts @@ -0,0 +1,21 @@ +import { Router } from "express"; +import { pubClient } from "modules/redis"; + +const router = Router(); + +router.get("/", async (req, res) => { + const keys = await pubClient.keys("Dispatcher:*"); + const user = await Promise.all( + keys.map(async (key) => { + const data = await pubClient.json.get(key); + return { + ...(typeof data === "object" && data !== null ? data : {}), + userId: key.split(":")[1], + }; + }), + ); + + res.json(user); +}); + +export default router; diff --git a/apps/dispatch-server/routes/router.ts b/apps/dispatch-server/routes/router.ts index a549da94..61170c33 100644 --- a/apps/dispatch-server/routes/router.ts +++ b/apps/dispatch-server/routes/router.ts @@ -1,8 +1,10 @@ import { Router } from "express"; import livekitRouter from "./livekit"; +import dispatcherRotuer from "./dispatcher"; const router = Router(); router.use("/livekit", livekitRouter); +router.use("/dispatcher", dispatcherRotuer); export default router; diff --git a/apps/dispatch-server/socket-events/connect-dispatch.ts b/apps/dispatch-server/socket-events/connect-dispatch.ts index c7171c3e..ffbda74a 100644 --- a/apps/dispatch-server/socket-events/connect-dispatch.ts +++ b/apps/dispatch-server/socket-events/connect-dispatch.ts @@ -1,7 +1,7 @@ import { pubClient } from "modules/redis"; import { Server, Socket } from "socket.io"; -export const handle = +export const handleConnectDispatch = (socket: Socket, io: Server) => async ({ logoffTime, @@ -11,27 +11,40 @@ export const handle = selectedZone: string; }) => { const userId = socket.data.user.id; // User ID aus dem JWT-Token + const name = `${socket.data.user.firstname} ${socket.data.user.lastname[0]}. - ${socket.data.user.publicId}`; console.log("User connected to dispatch server"); - await pubClient.json.set(`dispatchers:${socket.id}`, "$", { + await pubClient.json.set(`Dispatcher:${userId}`, "$", { logoffTime, + name, loginTime: new Date().toISOString(), lastSeen: new Date().toISOString(), selectedZone, - userId, + socketId: socket.id, }); socket.join("dispatchers"); // Dem Dispatcher-Raum beitreten + socket.join(`user:${userId}`); // Dem User-Raum beitreten - const keys = await pubClient.keys("dispatchers:*"); - const dispatchers = await Promise.all( + /* const keys = await pubClient.keys("Dispatcher:*"); + await Promise.all( keys.map(async (key) => { return await pubClient.json.get(key); }), - ); - console.log(dispatchers); + ); */ socket.on("disconnect", async () => { console.log("Disconnected from dispatch server"); - await pubClient.json.del(`dispatchers:${socket.id}`); + await pubClient.json.del(`Dispatcher:${userId}`); + }); + socket.on("reconnect", async () => { + console.log("Reconnected to dispatch server"); + await pubClient.json.set(`Dispatcher:${userId}`, "$", { + logoffTime, + loginTime: new Date().toISOString(), + lastSeen: new Date().toISOString(), + name, + selectedZone, + socketId: socket.id, + }); }); }; diff --git a/apps/dispatch-server/socket-events/send-message.ts b/apps/dispatch-server/socket-events/send-message.ts new file mode 100644 index 00000000..2dbcb2e1 --- /dev/null +++ b/apps/dispatch-server/socket-events/send-message.ts @@ -0,0 +1,50 @@ +import { prisma } from "@repo/db"; +import { pubClient } from "modules/redis"; +import { Server, Socket } from "socket.io"; + +export const handleSendMessage = + (socket: Socket, io: Server) => + async ( + { userId, message }: { userId: string; message: string }, + cb: (err: { error?: string }) => void, + ) => { + console.log("send-message", userId, message); + const senderId = socket.data.user.id; + + const senderUser = await prisma.user.findUnique({ + where: { id: senderId }, + }); + + const receiverUser = await prisma.user.findUnique({ + where: { id: userId }, + }); + + const dbMessage = await prisma.chatMessage.create({ + data: { + text: message, + receiverId: userId, + senderId, + receiverName: `${receiverUser?.firstname} ${receiverUser?.lastname[0]}. - ${receiverUser?.publicId}`, + senderName: `${senderUser?.firstname} ${senderUser?.lastname[0]}. - ${senderUser?.publicId}`, + }, + }); + + io.to(`user:${dbMessage.receiverId}`).emit("chat-message", { + userId: dbMessage.senderId, + message: dbMessage, + }); + io.to(`user:${dbMessage.senderId}`).emit("chat-message", { + userId: dbMessage.receiverId, + message: dbMessage, + }); + const recvSockets = await io.in(`user:${userId}`).fetchSockets(); + await io.in(`user:${senderId}`).fetchSockets(); + + console.log(`Sockets in room user:${userId}:`); + if (!recvSockets.length) { + cb({ error: "User is not connected" }); + } else { + cb({}); + } + console.log(`${socket.data.user.publicId} sent message to ${userId}`); + }; diff --git a/apps/dispatch/app/(dispatch)/_components/navbar/_components/Chat.tsx b/apps/dispatch/app/(dispatch)/_components/navbar/_components/Chat.tsx index 0367cb5d..316fc6db 100644 --- a/apps/dispatch/app/(dispatch)/_components/navbar/_components/Chat.tsx +++ b/apps/dispatch/app/(dispatch)/_components/navbar/_components/Chat.tsx @@ -1,81 +1,196 @@ +"use client"; import { ChatBubbleIcon, PaperPlaneIcon } from "@radix-ui/react-icons"; +import { useChatStore } from "_store/chatStore"; +import { useSession } from "next-auth/react"; +import { Fragment, useEffect, useRef, useState } from "react"; +import { + Dispatcher, + getDispatcher, +} from "(dispatch)/_components/navbar/_components/action"; +import { cn } from "helpers/cn"; +import { socket } from "(dispatch)/socket"; +import { ChatMessage } from "@repo/db"; export const Chat = () => { + const { sendMessage, addChat, chats, addMessage, setOwnId } = useChatStore(); + const [sending, setSending] = useState(false); + const session = useSession(); + const [dropdownOpen, setDropdownOpen] = useState(false); + const [addTabValue, setAddTabValue] = useState(""); + const [selectedTab, setSelectedTab] = useState(null); + const [message, setMessage] = useState(""); + const [dispatcher, setDispatcher] = useState(null); + const timeout = useRef(null); + + useEffect(() => { + if (!session.data?.user.id) return; + setOwnId(session.data.user.id); + }, [session]); + + useEffect(() => { + const fetchDispatcher = async () => { + const data = await getDispatcher(); + setDispatcher(data); + setAddTabValue(data[0]?.userId || ""); + }; + + timeout.current = setInterval(() => { + fetchDispatcher(); + }, 10000); + fetchDispatcher(); + + return () => { + if (timeout.current) { + clearInterval(timeout.current); + } + }; + }, []); + return ( -
-
+
-
-
-
- - -
-
- -
-
-
- <LST_01> Nicolas Kratsos #VAR0002 -
-
LST v2 ist schon nice
-
-
-
Hast recht.
-
+ + {dropdownOpen && ( +
+
+
+ +
- - -
-
-
- <LST_01> Nicolas Kratsos #VAR0002 -
-
get rekt lmao
-
-
-
no u.
-
+
+ {Object.keys(chats).map((userId) => { + const chat = chats[userId]; + if (!chat) return null; + return ( + + `} + checked={selectedTab === userId} + onChange={(e) => { + if (e.target.checked) { + // Handle tab change + setSelectedTab(userId); + } + }} + /> +
+ {chat.messages.map((chatMessage) => { + const isSender = + chatMessage.senderId === session.data?.user.id; + return ( +
+

+ {new Date( + chatMessage.timestamp, + ).toLocaleTimeString()} +

+
+ {chatMessage.text} +
+
+ ); + })} + {!chat.messages.length && ( +

+ Noch keine Nachrichten +

+ )} +
+
+ ); + })}
-
-
-
- +
+
+ +
+
-
-
+ )}
); }; diff --git a/apps/dispatch/app/(dispatch)/_components/navbar/_components/action.ts b/apps/dispatch/app/(dispatch)/_components/navbar/_components/action.ts new file mode 100644 index 00000000..6daa4544 --- /dev/null +++ b/apps/dispatch/app/(dispatch)/_components/navbar/_components/action.ts @@ -0,0 +1,18 @@ +"use server"; + +export interface Dispatcher { + userId: string; + lastSeen: string; + loginTime: string; + logoffTime: string; + selectedZone: string; + name: string; + socketId: string; +} + +export const getDispatcher = async () => { + const res = await fetch(` + ${process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL}/dispatcher`); + const data = await res.json(); + return data as Dispatcher[]; +}; diff --git a/apps/dispatch/app/_store/chatStore.ts b/apps/dispatch/app/_store/chatStore.ts new file mode 100644 index 00000000..915958b7 --- /dev/null +++ b/apps/dispatch/app/_store/chatStore.ts @@ -0,0 +1,70 @@ +import { create } from "zustand"; +import { ChatMessage } from "@repo/db"; +import { socket } from "(dispatch)/socket"; + +interface ChatStore { + ownId: null | string; + setOwnId: (id: string) => void; + chats: Record; + sendMessage: (userId: string, message: string) => Promise; + addChat: (userId: string, name: string) => void; + addMessage: (userId: string, message: ChatMessage) => void; +} + +export const useChatStore = create((set, get) => ({ + ownId: null, + setOwnId: (id: string) => set({ ownId: id }), + chats: {}, + sendMessage: (userId: string, message: string) => { + return new Promise((resolve, reject) => { + console.log("sendMessage", userId, message); + socket.emit( + "send-message", + { userId, message }, + ({ error }: { error?: string }) => { + if (error) { + reject(error); + } else { + resolve(); + } + }, + ); + }); + }, + addChat: (userId, name) => { + set((state) => ({ + chats: { + ...state.chats, // Bestehende Chats beibehalten + [userId]: { name, messages: [] }, // Neuen Chat hinzufügen + }, + })); + }, + addMessage: (userId: string, message: ChatMessage) => { + console.log("addMessage", userId, message); + set((state) => { + const user = state.chats[userId] || { name: userId, messages: [] }; + const isSender = message.senderId === state.ownId; + + return { + chats: { + ...state.chats, + [userId]: { + ...user, + name: isSender ? message.receiverName : message.senderName, + messages: [...user.messages, message], // Neuen Zustand erzeugen + }, + }, + }; + }); + }, +})); + +socket.on( + "chat-message", + ({ userId, message }: { userId: string; message: ChatMessage }) => { + const store = useChatStore.getState(); + console.log("chat-message", userId, message); + // Update the chat store with the new message + store.addMessage(userId, message); + }, +); diff --git a/apps/dispatch/app/api/dispatcher/route.ts b/apps/dispatch/app/api/dispatcher/route.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/dispatch/app/api/livekit/token/route.ts b/apps/dispatch/app/api/livekit/token/route.ts index 05a6b8fd..65cf7506 100644 --- a/apps/dispatch/app/api/livekit/token/route.ts +++ b/apps/dispatch/app/api/livekit/token/route.ts @@ -10,7 +10,6 @@ if (!process.env.LIVEKIT_API_SECRET) export const GET = async () => { const session = await getServerSession(); - console.log(session?.user.permissions); if (!session) return Response.json({ message: "Unauthorized" }, { status: 401 }); const user = await prisma.user.findUnique({ @@ -18,7 +17,6 @@ export const GET = async () => { id: session.user.id, }, }); - console.log(user); if (!user || !user.permissions.includes("AUDIO")) return Response.json({ message: "Missing permissions" }, { status: 401 }); diff --git a/apps/dispatch/package.json b/apps/dispatch/package.json index a9f2a693..003ba692 100644 --- a/apps/dispatch/package.json +++ b/apps/dispatch/package.json @@ -14,8 +14,10 @@ "@livekit/components-react": "^2.8.1", "@livekit/components-styles": "^1.1.4", "@radix-ui/react-icons": "^1.3.2", + "@repo/db": "*", "@repo/ui": "*", "@tailwindcss/postcss": "^4.0.14", + "@tanstack/react-query": "^5.69.0", "leaflet": "^1.9.4", "livekit-client": "^2.9.7", "livekit-server-sdk": "^2.10.2", diff --git a/apps/hub/app/(app)/admin/message/action.tsx b/apps/hub/app/(app)/admin/message/action.tsx index eff17da8..646973d6 100644 --- a/apps/hub/app/(app)/admin/message/action.tsx +++ b/apps/hub/app/(app)/admin/message/action.tsx @@ -1,15 +1,15 @@ "use server"; import { prisma, Prisma } from "@repo/db"; -export const addMessage = async (message: Prisma.MessageCreateInput) => { +export const addMessage = async (message: Prisma.NotamCreateInput) => { try { // Set all current messages with isMainMsg=true to active=false - await prisma.message.updateMany({ + await prisma.notam.updateMany({ where: { isMainMsg: true }, data: { active: false }, }); - await prisma.message.create({ + await prisma.notam.create({ data: { message: message.message, color: message.color, @@ -26,7 +26,7 @@ export const addMessage = async (message: Prisma.MessageCreateInput) => { export const disableMessage = async () => { try { // Set all current messages with isMainMsg=true to active=false - await prisma.message.updateMany({ + await prisma.notam.updateMany({ where: { isMainMsg: true }, data: { active: false }, }); diff --git a/apps/hub/app/_components/ui/PageAlert.tsx b/apps/hub/app/_components/ui/PageAlert.tsx index 71d00f43..2148320d 100644 --- a/apps/hub/app/_components/ui/PageAlert.tsx +++ b/apps/hub/app/_components/ui/PageAlert.tsx @@ -1,6 +1,6 @@ import { prisma } from "@repo/db"; const fetchMainMessage = async () => { - return await prisma.message.findFirst({ + return await prisma.notam.findFirst({ where: { active: true, isMainMsg: true, diff --git a/package-lock.json b/package-lock.json index 47a7b384..d3feada4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,10 @@ "@livekit/components-react": "^2.8.1", "@livekit/components-styles": "^1.1.4", "@radix-ui/react-icons": "^1.3.2", + "@repo/db": "*", "@repo/ui": "*", "@tailwindcss/postcss": "^4.0.14", + "@tanstack/react-query": "^5.69.0", "leaflet": "^1.9.4", "livekit-client": "^2.9.7", "livekit-server-sdk": "^2.10.2", @@ -2876,9 +2878,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.67.3", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.67.3.tgz", - "integrity": "sha512-pq76ObpjcaspAW4OmCbpXLF6BCZP2Zr/J5ztnyizXhSlNe7fIUp0QKZsd0JMkw9aDa+vxDX/OY7N+hjNY/dCGg==", + "version": "5.69.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.69.0.tgz", + "integrity": "sha512-Kn410jq6vs1P8Nm+ZsRj9H+U3C0kjuEkYLxbiCyn3MDEiYor1j2DGVULqAz62SLZtUZ/e9Xt6xMXiJ3NJ65WyQ==", "license": "MIT", "funding": { "type": "github", @@ -2886,12 +2888,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.67.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.67.3.tgz", - "integrity": "sha512-u/n2HsQeH1vpZIOzB/w2lqKlXUDUKo6BxTdGXSMvNzIq5MHYFckRMVuFABp+QB7RN8LFXWV6X1/oSkuDq+MPIA==", + "version": "5.69.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.69.0.tgz", + "integrity": "sha512-Ift3IUNQqTcaFa1AiIQ7WCb/PPy8aexZdq9pZWLXhfLcLxH0+PZqJ2xFImxCpdDZrFRZhLJrh76geevS5xjRhA==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.67.3" + "@tanstack/query-core": "5.69.0" }, "funding": { "type": "github", diff --git a/packages/database/prisma/schema/chatMessage.prisma b/packages/database/prisma/schema/chatMessage.prisma new file mode 100644 index 00000000..57faf593 --- /dev/null +++ b/packages/database/prisma/schema/chatMessage.prisma @@ -0,0 +1,13 @@ +model ChatMessage { + id Int @id @default(autoincrement()) + text String + senderId String + receiverId String + timestamp DateTime @default(now()) + receiverName String + senderName String + + // relations: + sender User @relation("SentMessages", fields: [senderId], references: [id]) + receiver User @relation("ReceivedMessages", fields: [receiverId], references: [id]) +} diff --git a/packages/database/prisma/schema/message.prisma b/packages/database/prisma/schema/notam.prisma similarity index 95% rename from packages/database/prisma/schema/message.prisma rename to packages/database/prisma/schema/notam.prisma index e04aff1d..7f12c4ce 100644 --- a/packages/database/prisma/schema/message.prisma +++ b/packages/database/prisma/schema/notam.prisma @@ -7,7 +7,7 @@ enum GlobalColor { ERROR } -model Message { +model Notam { id Int @id @default(autoincrement()) color GlobalColor message String diff --git a/packages/database/prisma/schema/user.prisma b/packages/database/prisma/schema/user.prisma index 38fc1fed..93fcf586 100644 --- a/packages/database/prisma/schema/user.prisma +++ b/packages/database/prisma/schema/user.prisma @@ -40,6 +40,8 @@ model User { participants Participant[] EventAppointmentUser EventAppointment[] @relation("EventAppointmentUser") EventAppointment EventAppointment[] + SentMessages ChatMessage[] @relation("SentMessages") + ReceivedMessages ChatMessage[] @relation("ReceivedMessages") @@map(name: "users") }