feat: editing of messages

This commit is contained in:
Alexander Daichendt 2025-02-12 13:28:28 +01:00
parent 4d87f86ff7
commit f27adc3360
7 changed files with 115 additions and 11 deletions

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import { ChatMessage } from "../../../shared";
function ChatMessageDisplay({
@ -10,14 +10,28 @@ function ChatMessageDisplay({
message: ChatMessage;
userId: string | undefined;
onDelete: (messageId: string) => void;
onEdit: (messageId: string) => void;
onEdit: (messageId: string, newContent: string) => void;
}) {
const [isEditing, setIsEditing] = useState(false);
const [editContent, setEditContent] = useState(message.content);
const isOwn = userId === message.author.userId;
const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
const handleSave = () => {
if (editContent.trim() !== message.content) {
onEdit(message.id, editContent);
}
setIsEditing(false);
};
const handleCancel = () => {
setEditContent(message.content);
setIsEditing(false);
};
return (
<div key={message.id} className={`mb-4 ${isOwn ? "ml-auto" : "mr-auto"}`}>
<div
@ -33,19 +47,47 @@ function ChatMessageDisplay({
)}
</div>
<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 && (
{isEditing ? (
<div className={`max-w-[80%] ${isOwn ? "ml-auto" : "mr-auto"}`}>
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
className="w-full p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={2}
autoFocus
/>
<div
className={`flex gap-2 mt-2 ${isOwn ? "justify-end" : "justify-start"}`}
>
<button
onClick={handleSave}
className="px-3 py-1 text-sm text-white bg-blue-600 rounded hover:bg-blue-700"
>
Save
</button>
<button
onClick={handleCancel}
className="px-3 py-1 text-sm text-gray-600 bg-gray-200 rounded hover:bg-gray-300"
>
Cancel
</button>
</div>
</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>
)}
{isOwn && !isEditing && (
<div
className={`flex gap-2 mt-0.5 text-[10px]
${isOwn ? "justify-end" : "justify-start"}`}
>
<button
onClick={() => onEdit?.(message.id)}
onClick={() => setIsEditing(true)}
className="text-blue-600 hover:text-blue-800 active:text-blue-900"
>
Edit

View file

@ -14,6 +14,8 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
state,
dispatch,
actions: {
editMessage: (messageId: string, newContent: string) =>
dispatch({ type: "EDIT_MESSAGE", payload: { messageId, newContent } }),
deleteMessage: (messageId: string) =>
dispatch({ type: "DELETE_MESSAGE", payload: messageId }),
addMessage: (message: Message) =>

View file

@ -1,6 +1,7 @@
import React, { useEffect, useRef } from "react";
import {
ClientDeleteMessage,
ClientEditMessage,
ClientMessage,
ServerMessage,
} from "../../../shared";
@ -31,7 +32,17 @@ function ChatMessages({ sendMessage }: MessageDisplayProps) {
sendMessage(deleteMessage);
};
const onEdit = (messageId: string) => {};
const onEdit = (messageId: string, newContent: string) => {
const editMessage: ClientEditMessage = {
type: "EDIT_MESSAGE",
payload: {
messageId,
content: newContent,
},
};
sendMessage(editMessage);
};
// scroll to bottom when messages change
useEffect(() => {

View file

@ -18,6 +18,7 @@ export type ChatState = {
};
export type ChatAction =
| { type: "EDIT_MESSAGE"; payload: { messageId: string; newContent: string } }
| { type: "ADD_MESSAGE"; payload: Message }
| { type: "DELETE_MESSAGE"; payload: string }
| { type: "SET_USER"; payload: User }
@ -25,6 +26,7 @@ export type ChatAction =
| { type: "CLEAR_MESSAGES" };
export type ChatActions = {
editMessage: (messageId: string, newContent: string) => void;
addMessage: (message: Message) => void;
deleteMessage: (messageId: string) => void;
setUser: (user: User) => void;

View file

@ -32,6 +32,13 @@ export function useWebSocket(roomId: string) {
actions.deleteMessage(message.payload.messageId);
break;
case "MESSAGE_EDITED":
actions.editMessage(
message.payload.messageId,
message.payload.content,
);
break;
default:
console.error("Unknown message type", message);
}

View file

@ -2,6 +2,25 @@ import { ChatAction, ChatState } from "../contexts/ChatContext";
export function chatReducer(state: ChatState, action: ChatAction): ChatState {
switch (action.type) {
case "EDIT_MESSAGE":
return {
...state,
messages: state.messages.map((message) => {
if ("type" in message && message.type === "CHAT_MESSAGE") {
if (message.payload.id === action.payload.messageId) {
return {
...message,
payload: {
...message.payload,
content: action.payload.newContent,
},
};
}
}
return message;
}),
};
case "DELETE_MESSAGE":
return {
...state,

View file

@ -3,6 +3,7 @@ import type {
ClientMessage,
ServerChatMessage,
ServerMessageDeletedMessage,
ServerMessageEditedMessage,
ServerRegistrationConfirmed,
ServerUserJoinedMessage,
User,
@ -77,5 +78,25 @@ export default function handleClientMessage(
for (const [socket] of room.userConnections) {
socket.send(JSON.stringify(deleteMessage));
}
break;
case "EDIT_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 editMessage: ServerMessageEditedMessage = {
type: "MESSAGE_EDITED",
payload: {
messageId: data.payload.messageId,
content: data.payload.content,
},
};
for (const [socket] of room.userConnections) {
socket.send(JSON.stringify(editMessage));
}
break;
default:
console.error("Unknown message type", data);
}
}