feat: proper chat message styling without rerendering
This commit is contained in:
parent
7150ca87c3
commit
748e154ec4
7 changed files with 74 additions and 25 deletions
33
client/src/components/ChatMessage.tsx
Normal file
33
client/src/components/ChatMessage.tsx
Normal 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);
|
||||||
|
|
@ -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>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -34,3 +34,8 @@ export const useChatState = () => {
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCurrentUser = () => {
|
||||||
|
const { state } = useChatState();
|
||||||
|
return state.currentUser;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
10
shared.ts
10
shared.ts
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue