diff --git a/client/src/App.tsx b/client/src/App.tsx
index 26ca021..ea05220 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,13 +1,21 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Room from "./pages/Room";
+import { ChatProvider } from "./components/ChatProvider";
function App() {
return (
} />
- } />
+
+
+
+ }
+ />
);
diff --git a/client/src/components/ChatInput.tsx b/client/src/components/ChatInput.tsx
new file mode 100644
index 0000000..fbef502
--- /dev/null
+++ b/client/src/components/ChatInput.tsx
@@ -0,0 +1,36 @@
+import React, { useState } from "react";
+
+interface ChatInputProps {
+ onSendMessage: (content: string) => void;
+}
+
+function ChatInput({ onSendMessage }: ChatInputProps) {
+ const [message, setMessage] = useState("");
+
+ function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!message.trim()) return;
+
+ onSendMessage(message);
+ setMessage("");
+ }
+
+ return (
+
+ );
+}
+
+export default React.memo(ChatInput);
diff --git a/client/src/components/ChatMessages.tsx b/client/src/components/ChatMessages.tsx
new file mode 100644
index 0000000..76e4139
--- /dev/null
+++ b/client/src/components/ChatMessages.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import { useChatState } from "../contexts/ChatContext";
+
+function ChatMessages() {
+ const { messages } = useChatState();
+
+ return (
+ <>
+ {messages.map((message) => (
+
+
+ {message.author.name}
+ {new Date(message.timestamp).toLocaleTimeString()}
+
+
{message.content}
+
+ ))}
+ >
+ );
+}
+
+export default React.memo(ChatMessages);
diff --git a/client/src/components/ChatProvider.tsx b/client/src/components/ChatProvider.tsx
new file mode 100644
index 0000000..f2b2349
--- /dev/null
+++ b/client/src/components/ChatProvider.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+import { ChatMessage, User } from "../../../shared";
+import { ChatContext } from "../contexts/ChatContext";
+
+export function ChatProvider({ children }: { children: React.ReactNode }) {
+ const [messages, setMessages] = React.useState([]);
+ const [currentUser, setCurrentUser] = React.useState(null);
+
+ const addMessage = (message: ChatMessage) => {
+ setMessages((prev) => [...prev, message]);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/client/src/components/Connect.tsx b/client/src/components/ConnectWithName.tsx
similarity index 77%
rename from client/src/components/Connect.tsx
rename to client/src/components/ConnectWithName.tsx
index 32341d6..9f40135 100644
--- a/client/src/components/Connect.tsx
+++ b/client/src/components/ConnectWithName.tsx
@@ -1,10 +1,12 @@
import React, { useState } from "react";
+import { useChatState } from "../contexts/ChatContext";
interface ConnectProps {
onConnect: (name: string) => void;
}
function Connect({ onConnect }: ConnectProps) {
+ const { setCurrentUser } = useChatState();
const [name, setName] = useState("");
function handleConnect() {
@@ -12,6 +14,8 @@ function Connect({ onConnect }: ConnectProps) {
return;
}
+ // leave userId empty for now, it is server generated
+ setCurrentUser({ name, userId: "" });
onConnect(name);
}
@@ -31,11 +35,7 @@ function Connect({ onConnect }: ConnectProps) {
onKeyDown={handleKeyPress}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
-
diff --git a/client/src/pages/Room.tsx b/client/src/pages/Room.tsx
index 2663b23..5036218 100644
--- a/client/src/pages/Room.tsx
+++ b/client/src/pages/Room.tsx
@@ -1,17 +1,24 @@
import { useParams } from "react-router-dom";
import Layout from "../components/Layout";
-import Connect from "../components/Connect";
+import Connect from "../components/ConnectWithName";
import { useWebSocket } from "../hooks/useWebSocket";
+import ChatMessages from "../components/ChatMessages";
+import ChatInput from "../components/ChatInput";
+import { useChatState } from "../contexts/ChatContext";
function Room() {
const { roomId } = useParams();
- const { isConnected, connect } = useWebSocket(roomId ?? "");
+ const { isConnected, connect, sendMessage } = useWebSocket(roomId ?? "");
+ const { currentUser } = useChatState();
return (
Room ID: {roomId}
+ {currentUser && (
+ User: {currentUser?.name}
+ )}
{!isConnected && }
+ {isConnected && (
+ <>
+
+
+ >
+ )}
);
diff --git a/server/src/handleClientMessage.ts b/server/src/handleClientMessage.ts
index 1f37f29..b29395d 100644
--- a/server/src/handleClientMessage.ts
+++ b/server/src/handleClientMessage.ts
@@ -1,12 +1,62 @@
-import type { ServerWebSocket } from "bun";
-import type { ClientMessage } from "../../shared";
+import { randomUUIDv7, type ServerWebSocket } from "bun";
+import type {
+ ClientMessage,
+ ServerChatMessage,
+ ServerUserJoinedMessage,
+ User,
+} from "../../shared";
import type { Room, WebSocketData } from "./types";
export default function handleClientMessage(
client: ServerWebSocket,
data: ClientMessage,
room: Room,
+ sender: User | undefined,
) {
// handle client messages
console.log("Received new message ", data);
+
+ switch (data.type) {
+ case "JOIN":
+ const userId = randomUUIDv7();
+ const name = data.payload.username;
+
+ room.userConnections.set(client, {
+ name,
+ userId,
+ });
+
+ const joinMessage: ServerUserJoinedMessage = {
+ type: "USER_JOINED",
+ payload: {
+ user: {
+ name,
+ userId,
+ },
+ },
+ };
+
+ for (const [socket] of room.userConnections) {
+ socket.send(JSON.stringify(joinMessage));
+ }
+
+ break;
+
+ case "CHAT_MESSAGE":
+ const chatMessage: ServerChatMessage = {
+ type: "CHAT_MESSAGE",
+ payload: {
+ id: randomUUIDv7(),
+ author: sender!,
+ content: data.payload.content,
+ timestamp: Date.now(),
+ },
+ };
+
+ // broadcast the message to all clients in the room
+ for (const [socket] of room.userConnections) {
+ socket.send(JSON.stringify(chatMessage));
+ }
+ break;
+ }
}
diff --git a/server/src/index.ts b/server/src/index.ts
index dc43441..18306be 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -78,7 +78,14 @@ const server = Bun.serve({
const data = JSON.parse(message.toString()) as ClientMessage;
- handleClientMessage(ws, data, room);
+ const sender = room.userConnections.get(ws);
+
+ if (!sender && data.type !== "JOIN") {
+ console.log("User not found in room", roomId);
+ return;
+ }
+
+ handleClientMessage(ws, data, room, sender);
},
},
});
diff --git a/server/src/types.ts b/server/src/types.ts
index 7d6d285..ccb85e6 100644
--- a/server/src/types.ts
+++ b/server/src/types.ts
@@ -7,11 +7,5 @@ export interface WebSocketData {
export interface Room {
roomId: string;
- userConnections: Map<
- string, // userId
- {
- user: User;
- socket: ServerWebSocket;
- }
- >;
+ userConnections: Map, User>;
}
diff --git a/shared.ts b/shared.ts
index 237a0b6..d5547af 100644
--- a/shared.ts
+++ b/shared.ts
@@ -3,7 +3,7 @@
export interface User {
name: string;
- uid: string;
+ userId: string;
}
export interface ClientJoinMessage {
@@ -51,40 +51,40 @@ export type ClientMessage =
// Server messages
-interface ChatMessage {
+export interface ChatMessage {
id: string;
content: string;
timestamp: number;
author: User;
}
-interface ServerUserJoinedMessage {
+export interface ServerUserJoinedMessage {
type: "USER_JOINED";
payload: {
user: User;
};
}
-interface ServerUserLeftMessage {
+export interface ServerUserLeftMessage {
type: "USER_LEFT";
payload: {
user: User;
};
}
-interface ServerChatMessage {
+export interface ServerChatMessage {
type: "CHAT_MESSAGE";
payload: ChatMessage;
}
-interface ServerMessageDeletedMessage {
+export interface ServerMessageDeletedMessage {
type: "MESSAGE_DELETED";
payload: {
messageId: string;
};
}
-interface ServerMessageEditedMessage {
+export interface ServerMessageEditedMessage {
type: "MESSAGE_EDITED";
payload: {
messageId: string;
@@ -92,7 +92,7 @@ interface ServerMessageEditedMessage {
};
}
-interface ServerUserListMessage {
+export interface ServerUserListMessage {
type: "USER_LIST";
payload: {
users: User[];