diff --git a/client/src/components/ChatMessage.tsx b/client/src/components/ChatMessage.tsx new file mode 100644 index 0000000..684308e --- /dev/null +++ b/client/src/components/ChatMessage.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { ChatMessage } from "../../../shared"; + +function ChatMessageDisplay({ + message, + userId, +}: { + message: ChatMessage; + userId: string | undefined; // pass userId as string to avoid rerendering. if we useChatState here, it would rerender every single message every time a new chat message is added. +}) { + const isOwn = userId === message.author.userId; + + return ( +
+
+ {!isOwn && {message.author.name}} + {new Date(message.timestamp).toLocaleTimeString()} + {isOwn && {message.author.name}} +
+
+ {message.content} +
+
+ ); +} + +export default React.memo(ChatMessageDisplay); diff --git a/client/src/components/ChatMessages.tsx b/client/src/components/MessageDisplay.tsx similarity index 53% rename from client/src/components/ChatMessages.tsx rename to client/src/components/MessageDisplay.tsx index e10ff24..177f0ed 100644 --- a/client/src/components/ChatMessages.tsx +++ b/client/src/components/MessageDisplay.tsx @@ -1,19 +1,15 @@ import React from "react"; import { useChatState } from "../contexts/ChatContext"; +import ChatMessage from "./ChatMessage"; function ChatMessages() { const { state } = useChatState(); + const userId = state.currentUser?.userId; return ( <> {state.messages.map((message) => ( -
-
- {message.author.name} - {new Date(message.timestamp).toLocaleTimeString()} -
-
{message.content}
-
+ ))} ); diff --git a/client/src/contexts/ChatContext.tsx b/client/src/contexts/ChatContext.tsx index 6049b29..771f464 100644 --- a/client/src/contexts/ChatContext.tsx +++ b/client/src/contexts/ChatContext.tsx @@ -34,3 +34,8 @@ export const useChatState = () => { } return context; }; + +export const useCurrentUser = () => { + const { state } = useChatState(); + return state.currentUser; +}; diff --git a/client/src/hooks/useWebSocket.ts b/client/src/hooks/useWebSocket.ts index 3a45d2a..c805b3b 100644 --- a/client/src/hooks/useWebSocket.ts +++ b/client/src/hooks/useWebSocket.ts @@ -9,7 +9,7 @@ import { useChatState } from "../contexts/ChatContext"; export function useWebSocket(roomId: string) { const [ws, setWs] = useState(null); const [isConnected, setIsConnected] = useState(false); - const { state, actions } = useChatState(); + const { actions } = useChatState(); // Handle incoming messages const handleMessage = useCallback( @@ -17,20 +17,22 @@ export function useWebSocket(roomId: string) { const message = JSON.parse(event.data) as ServerMessage; switch (message.type) { - case "USER_JOINED": - console.log("User joined", message.payload); - if (message.payload.user.name === state.currentUser?.name) { - actions.setUser(message.payload.user); - } + case "REGISTRATION_CONFIRMED": + actions.setUser(message.payload.user); break; + + case "USER_JOINED": + break; + case "CHAT_MESSAGE": actions.addMessage(message.payload); break; + default: console.error("Unknown message type", message); } }, - [state.currentUser, actions], + [actions], ); // Send a message to the server diff --git a/client/src/pages/Room.tsx b/client/src/pages/Room.tsx index 03b2f49..99dbb07 100644 --- a/client/src/pages/Room.tsx +++ b/client/src/pages/Room.tsx @@ -2,7 +2,7 @@ import { useParams } from "react-router-dom"; import Layout from "../components/Layout"; import Connect from "../components/ConnectWithName"; import { useWebSocket } from "../hooks/useWebSocket"; -import ChatMessages from "../components/ChatMessages"; +import ChatMessages from "../components/MessageDisplay"; import ChatInput from "../components/ChatInput"; import { useChatState } from "../contexts/ChatContext"; diff --git a/server/src/handleClientMessage.ts b/server/src/handleClientMessage.ts index b29395d..eff88e8 100644 --- a/server/src/handleClientMessage.ts +++ b/server/src/handleClientMessage.ts @@ -2,6 +2,7 @@ import { randomUUIDv7, type ServerWebSocket } from "bun"; import type { ClientMessage, ServerChatMessage, + ServerRegistrationConfirmed, ServerUserJoinedMessage, User, } from "../../shared"; @@ -18,21 +19,23 @@ export default function handleClientMessage( switch (data.type) { case "JOIN": - const userId = randomUUIDv7(); - const name = data.payload.username; + const user = { + name: data.payload.username, + userId: randomUUIDv7(), + }; + room.userConnections.set(client, user); - room.userConnections.set(client, { - name, - userId, - }); + const registrationConfirm: ServerRegistrationConfirmed = { + type: "REGISTRATION_CONFIRMED", + payload: { user }, + }; + client.send(JSON.stringify(registrationConfirm)); + // notify everyone else that a new user has joined const joinMessage: ServerUserJoinedMessage = { type: "USER_JOINED", payload: { - user: { - name, - userId, - }, + user, }, }; diff --git a/shared.ts b/shared.ts index d5547af..9792666 100644 --- a/shared.ts +++ b/shared.ts @@ -58,6 +58,15 @@ export interface ChatMessage { author: User; } +// sent to the newly registered client to confirm that the username was not taken +export interface ServerRegistrationConfirmed { + type: "REGISTRATION_CONFIRMED"; + payload: { + user: User; + }; +} + +// sent to all clients when a new client has joined the chat export interface ServerUserJoinedMessage { type: "USER_JOINED"; payload: { @@ -100,6 +109,7 @@ export interface ServerUserListMessage { } export type ServerMessage = + | ServerRegistrationConfirmed | ServerUserJoinedMessage | ServerUserLeftMessage | ServerChatMessage