diff --git a/client/src/components/ChatInput.tsx b/client/src/components/ChatInput.tsx
index 04263e7..3b600e6 100644
--- a/client/src/components/ChatInput.tsx
+++ b/client/src/components/ChatInput.tsx
@@ -1,17 +1,25 @@
import React, { useState } from "react";
+import { ClientChatMessage, ClientMessage } from "../../../shared";
interface ChatInputProps {
- onSendMessage: (content: string) => void;
+ sendMessage: (message: ClientMessage) => void;
}
-function ChatInput({ onSendMessage }: ChatInputProps) {
+function ChatInput({ sendMessage }: ChatInputProps) {
const [message, setMessage] = useState("");
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!message.trim()) return;
- onSendMessage(message);
+ const chatMessage: ClientChatMessage = {
+ type: "CHAT_MESSAGE",
+ payload: {
+ content: message,
+ },
+ };
+
+ sendMessage(chatMessage);
setMessage("");
}
diff --git a/client/src/components/ChatMessage.tsx b/client/src/components/ChatMessage.tsx
index a20d3c3..752759f 100644
--- a/client/src/components/ChatMessage.tsx
+++ b/client/src/components/ChatMessage.tsx
@@ -4,11 +4,19 @@ import { ChatMessage } from "../../../shared";
function ChatMessageDisplay({
message,
userId,
+ onDelete,
+ onEdit,
}: {
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.
+ userId: string | undefined;
+ onDelete: (messageId: string) => void;
+ onEdit: (messageId: string) => void;
}) {
const isOwn = userId === message.author.userId;
+ const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
return (
@@ -19,21 +27,38 @@ function ChatMessageDisplay({
{!isOwn && (
{message.author.name}
)}
-
- {new Date(message.timestamp).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- })}
-
+ {timestamp}
{isOwn && (
{message.author.name}
)}
-
- {message.content}
+
+
+ {message.content}
+
+ {isOwn && (
+
+
+ •
+
+
+ )}
);
diff --git a/client/src/components/ChatProvider.tsx b/client/src/components/ChatProvider.tsx
index b98ee68..20508a9 100644
--- a/client/src/components/ChatProvider.tsx
+++ b/client/src/components/ChatProvider.tsx
@@ -14,6 +14,8 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
state,
dispatch,
actions: {
+ deleteMessage: (messageId: string) =>
+ dispatch({ type: "DELETE_MESSAGE", payload: messageId }),
addMessage: (message: Message) =>
dispatch({ type: "ADD_MESSAGE", payload: message }),
setUser: (user: User) => dispatch({ type: "SET_USER", payload: user }),
diff --git a/client/src/components/MessageDisplay.tsx b/client/src/components/MessageDisplay.tsx
index 2c1f459..0f7821c 100644
--- a/client/src/components/MessageDisplay.tsx
+++ b/client/src/components/MessageDisplay.tsx
@@ -1,9 +1,17 @@
import React, { useEffect, useRef } from "react";
-import { ServerMessage } from "../../../shared";
+import {
+ ClientDeleteMessage,
+ ClientMessage,
+ ServerMessage,
+} from "../../../shared";
import { useChatState } from "../contexts/ChatContext";
import ChatMessage from "./ChatMessage";
-function ChatMessages() {
+interface MessageDisplayProps {
+ sendMessage: (message: ClientMessage) => void;
+}
+
+function ChatMessages({ sendMessage }: MessageDisplayProps) {
const { state } = useChatState();
const userId = state.currentUser?.userId;
const messagesEndRef = useRef(null); // ref to an empty div at the end to scroll to
@@ -12,6 +20,19 @@ function ChatMessages() {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
+ const onDelete = (messageId: string) => {
+ const deleteMessage: ClientDeleteMessage = {
+ type: "DELETE_MESSAGE",
+ payload: {
+ messageId,
+ },
+ };
+
+ sendMessage(deleteMessage);
+ };
+
+ const onEdit = (messageId: string) => {};
+
// scroll to bottom when messages change
useEffect(() => {
scrollToBottom();
@@ -25,6 +46,8 @@ function ChatMessages() {
message={message.payload}
userId={userId}
key={message.payload.id}
+ onDelete={onDelete}
+ onEdit={onEdit}
/>
);
case "USER_JOINED":
diff --git a/client/src/contexts/ChatContext.tsx b/client/src/contexts/ChatContext.tsx
index 8d2288a..18b43da 100644
--- a/client/src/contexts/ChatContext.tsx
+++ b/client/src/contexts/ChatContext.tsx
@@ -19,12 +19,14 @@ export type ChatState = {
export type ChatAction =
| { type: "ADD_MESSAGE"; payload: Message }
+ | { type: "DELETE_MESSAGE"; payload: string }
| { type: "SET_USER"; payload: User }
| { type: "SET_CONNECTED"; payload: boolean }
| { type: "CLEAR_MESSAGES" };
export type ChatActions = {
addMessage: (message: Message) => void;
+ deleteMessage: (messageId: string) => void;
setUser: (user: User) => void;
setConnected: (isConnected: boolean) => void;
};
diff --git a/client/src/hooks/useWebSocket.ts b/client/src/hooks/useWebSocket.ts
index f9ef96d..5c6dc8d 100644
--- a/client/src/hooks/useWebSocket.ts
+++ b/client/src/hooks/useWebSocket.ts
@@ -28,6 +28,10 @@ export function useWebSocket(roomId: string) {
actions.addMessage(message);
break;
+ case "MESSAGE_DELETED":
+ actions.deleteMessage(message.payload.messageId);
+ break;
+
default:
console.error("Unknown message type", message);
}
@@ -37,12 +41,8 @@ export function useWebSocket(roomId: string) {
// Send a message to the server
const sendMessage = useCallback(
- (content: string) => {
+ (message: ClientMessage) => {
if (ws?.readyState === WebSocket.OPEN) {
- const message: ClientMessage = {
- type: "CHAT_MESSAGE",
- payload: { content },
- };
ws.send(JSON.stringify(message));
} else {
console.error("WebSocket connection not established");
diff --git a/client/src/pages/Room.tsx b/client/src/pages/Room.tsx
index 99dbb07..162ca40 100644
--- a/client/src/pages/Room.tsx
+++ b/client/src/pages/Room.tsx
@@ -33,8 +33,8 @@ function Room() {
{!isConnected && }
{isConnected && (
<>
-
-
+
+
>
)}
diff --git a/client/src/reducers/chatReducers.ts b/client/src/reducers/chatReducers.ts
index cb27885..5eeaf11 100644
--- a/client/src/reducers/chatReducers.ts
+++ b/client/src/reducers/chatReducers.ts
@@ -2,6 +2,17 @@ import { ChatAction, ChatState } from "../contexts/ChatContext";
export function chatReducer(state: ChatState, action: ChatAction): ChatState {
switch (action.type) {
+ case "DELETE_MESSAGE":
+ return {
+ ...state,
+ messages: state.messages.filter((message) => {
+ // only potentially delete chat messages, dont touch system messages
+ if ("type" in message && message.type === "CHAT_MESSAGE") {
+ return message.payload.id !== action.payload;
+ }
+ return true;
+ }),
+ };
case "ADD_MESSAGE":
return {
...state,
diff --git a/server/src/handleClientMessage.ts b/server/src/handleClientMessage.ts
index eff88e8..6834794 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,
+ ServerMessageDeletedMessage,
ServerRegistrationConfirmed,
ServerUserJoinedMessage,
User,
@@ -61,5 +62,20 @@ export default function handleClientMessage(
socket.send(JSON.stringify(chatMessage));
}
break;
+
+ case "DELETE_MESSAGE":
+ // verify that the sender is the author of the message
+ // TODO: implement this, would need to store all messages on the server side
+
+ const deleteMessage: ServerMessageDeletedMessage = {
+ type: "MESSAGE_DELETED",
+ payload: {
+ messageId: data.payload.messageId,
+ },
+ };
+
+ for (const [socket] of room.userConnections) {
+ socket.send(JSON.stringify(deleteMessage));
+ }
}
}