From 4d87f86ff73d517bfe0f4c5378d9472b761a68c1 Mon Sep 17 00:00:00 2001 From: Alexander Daichendt Date: Wed, 12 Feb 2025 13:18:15 +0100 Subject: [PATCH] feat: deletion of messages --- client/src/components/ChatInput.tsx | 14 +++++-- client/src/components/ChatMessage.tsx | 49 ++++++++++++++++++------ client/src/components/ChatProvider.tsx | 2 + client/src/components/MessageDisplay.tsx | 27 ++++++++++++- client/src/contexts/ChatContext.tsx | 2 + client/src/hooks/useWebSocket.ts | 10 ++--- client/src/pages/Room.tsx | 4 +- client/src/reducers/chatReducers.ts | 11 ++++++ server/src/handleClientMessage.ts | 16 ++++++++ 9 files changed, 111 insertions(+), 24 deletions(-) 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)); + } } }