feat: proper chat message styling without rerendering

This commit is contained in:
Alexander Daichendt 2025-02-12 12:09:48 +01:00
parent 7150ca87c3
commit 748e154ec4
7 changed files with 74 additions and 25 deletions

View file

@ -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 (
<div key={message.id} className={`mb-4 ${isOwn ? "ml-auto" : "mr-auto"}`}>
<div
className={`flex items-center gap-2 text-sm text-gray-600 mb-1
${isOwn ? "justify-end" : "justify-start"}`}
>
{!isOwn && <span className="font-medium">{message.author.name}</span>}
<span>{new Date(message.timestamp).toLocaleTimeString()}</span>
{isOwn && <span className="font-medium">{message.author.name}</span>}
</div>
<div
className={`max-w-[80%] p-3 rounded-2xl shadow-sm bg-gray-200
${isOwn ? "ml-auto rounded-tr-none" : "mr-auto rounded-tl-none"}`}
>
{message.content}
</div>
</div>
);
}
export default React.memo(ChatMessageDisplay);

View file

@ -1,19 +1,15 @@
import React from "react"; import React from "react";
import { useChatState } from "../contexts/ChatContext"; import { useChatState } from "../contexts/ChatContext";
import ChatMessage from "./ChatMessage";
function ChatMessages() { function ChatMessages() {
const { state } = useChatState(); const { state } = useChatState();
const userId = state.currentUser?.userId;
return ( return (
<> <>
{state.messages.map((message) => ( {state.messages.map((message) => (
<div key={message.id}> <ChatMessage message={message} userId={userId} />
<div>
<span>{message.author.name}</span>
<span>{new Date(message.timestamp).toLocaleTimeString()}</span>
</div>
<div>{message.content}</div>
</div>
))} ))}
</> </>
); );

View file

@ -34,3 +34,8 @@ export const useChatState = () => {
} }
return context; return context;
}; };
export const useCurrentUser = () => {
const { state } = useChatState();
return state.currentUser;
};

View file

@ -9,7 +9,7 @@ import { useChatState } from "../contexts/ChatContext";
export function useWebSocket(roomId: string) { export function useWebSocket(roomId: string) {
const [ws, setWs] = useState<WebSocket | null>(null); const [ws, setWs] = useState<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const { state, actions } = useChatState(); const { actions } = useChatState();
// Handle incoming messages // Handle incoming messages
const handleMessage = useCallback( const handleMessage = useCallback(
@ -17,20 +17,22 @@ export function useWebSocket(roomId: string) {
const message = JSON.parse(event.data) as ServerMessage; const message = JSON.parse(event.data) as ServerMessage;
switch (message.type) { switch (message.type) {
case "USER_JOINED": case "REGISTRATION_CONFIRMED":
console.log("User joined", message.payload); actions.setUser(message.payload.user);
if (message.payload.user.name === state.currentUser?.name) {
actions.setUser(message.payload.user);
}
break; break;
case "USER_JOINED":
break;
case "CHAT_MESSAGE": case "CHAT_MESSAGE":
actions.addMessage(message.payload); actions.addMessage(message.payload);
break; break;
default: default:
console.error("Unknown message type", message); console.error("Unknown message type", message);
} }
}, },
[state.currentUser, actions], [actions],
); );
// Send a message to the server // Send a message to the server

View file

@ -2,7 +2,7 @@ import { useParams } from "react-router-dom";
import Layout from "../components/Layout"; import Layout from "../components/Layout";
import Connect from "../components/ConnectWithName"; import Connect from "../components/ConnectWithName";
import { useWebSocket } from "../hooks/useWebSocket"; import { useWebSocket } from "../hooks/useWebSocket";
import ChatMessages from "../components/ChatMessages"; import ChatMessages from "../components/MessageDisplay";
import ChatInput from "../components/ChatInput"; import ChatInput from "../components/ChatInput";
import { useChatState } from "../contexts/ChatContext"; import { useChatState } from "../contexts/ChatContext";

View file

@ -2,6 +2,7 @@ import { randomUUIDv7, type ServerWebSocket } from "bun";
import type { import type {
ClientMessage, ClientMessage,
ServerChatMessage, ServerChatMessage,
ServerRegistrationConfirmed,
ServerUserJoinedMessage, ServerUserJoinedMessage,
User, User,
} from "../../shared"; } from "../../shared";
@ -18,21 +19,23 @@ export default function handleClientMessage(
switch (data.type) { switch (data.type) {
case "JOIN": case "JOIN":
const userId = randomUUIDv7(); const user = {
const name = data.payload.username; name: data.payload.username,
userId: randomUUIDv7(),
};
room.userConnections.set(client, user);
room.userConnections.set(client, { const registrationConfirm: ServerRegistrationConfirmed = {
name, type: "REGISTRATION_CONFIRMED",
userId, payload: { user },
}); };
client.send(JSON.stringify(registrationConfirm));
// notify everyone else that a new user has joined
const joinMessage: ServerUserJoinedMessage = { const joinMessage: ServerUserJoinedMessage = {
type: "USER_JOINED", type: "USER_JOINED",
payload: { payload: {
user: { user,
name,
userId,
},
}, },
}; };

View file

@ -58,6 +58,15 @@ export interface ChatMessage {
author: User; 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 { export interface ServerUserJoinedMessage {
type: "USER_JOINED"; type: "USER_JOINED";
payload: { payload: {
@ -100,6 +109,7 @@ export interface ServerUserListMessage {
} }
export type ServerMessage = export type ServerMessage =
| ServerRegistrationConfirmed
| ServerUserJoinedMessage | ServerUserJoinedMessage
| ServerUserLeftMessage | ServerUserLeftMessage
| ServerChatMessage | ServerChatMessage