feat: deletion of messages

This commit is contained in:
Alexander Daichendt 2025-02-12 13:18:15 +01:00
parent 97f0483f9a
commit 4d87f86ff7
9 changed files with 111 additions and 24 deletions

View file

@ -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("");
}

View file

@ -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 (
<div key={message.id} className={`mb-4 ${isOwn ? "ml-auto" : "mr-auto"}`}>
@ -19,21 +27,38 @@ function ChatMessageDisplay({
{!isOwn && (
<span className="font-bold text-gray-700">{message.author.name}</span>
)}
<span className="font-medium opacity-45">
{new Date(message.timestamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</span>
<span className="font-medium opacity-45">{timestamp}</span>
{isOwn && (
<span className="font-bold text-gray-700">{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 className="relative">
<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>
{isOwn && (
<div
className={`flex gap-2 mt-0.5 text-[10px]
${isOwn ? "justify-end" : "justify-start"}`}
>
<button
onClick={() => onEdit?.(message.id)}
className="text-blue-600 hover:text-blue-800 active:text-blue-900"
>
Edit
</button>
<span className="text-gray-400"></span>
<button
onClick={() => onDelete?.(message.id)}
className="text-red-600 hover:text-red-800 active:text-red-900"
>
Delete
</button>
</div>
)}
</div>
</div>
);

View file

@ -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 }),

View file

@ -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<HTMLDivElement>(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":

View file

@ -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;
};

View file

@ -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");

View file

@ -33,8 +33,8 @@ function Room() {
{!isConnected && <Connect onConnect={connect} />}
{isConnected && (
<>
<ChatMessages />
<ChatInput onSendMessage={sendMessage} />
<ChatMessages sendMessage={sendMessage} />
<ChatInput sendMessage={sendMessage} />
</>
)}
</section>

View file

@ -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,