feat: editing of messages
This commit is contained in:
parent
4d87f86ff7
commit
f27adc3360
7 changed files with 115 additions and 11 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { ChatMessage } from "../../../shared";
|
import { ChatMessage } from "../../../shared";
|
||||||
|
|
||||||
function ChatMessageDisplay({
|
function ChatMessageDisplay({
|
||||||
|
|
@ -10,14 +10,28 @@ function ChatMessageDisplay({
|
||||||
message: ChatMessage;
|
message: ChatMessage;
|
||||||
userId: string | undefined;
|
userId: string | undefined;
|
||||||
onDelete: (messageId: string) => void;
|
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 isOwn = userId === message.author.userId;
|
||||||
const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
|
const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "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 (
|
return (
|
||||||
<div key={message.id} className={`mb-4 ${isOwn ? "ml-auto" : "mr-auto"}`}>
|
<div key={message.id} className={`mb-4 ${isOwn ? "ml-auto" : "mr-auto"}`}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -33,19 +47,47 @@ function ChatMessageDisplay({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
{isEditing ? (
|
||||||
className={`max-w-[80%] p-3 rounded-2xl shadow-sm bg-gray-200
|
<div className={`max-w-[80%] ${isOwn ? "ml-auto" : "mr-auto"}`}>
|
||||||
${isOwn ? "ml-auto rounded-tr-none" : "mr-auto rounded-tl-none"}`}
|
<textarea
|
||||||
>
|
value={editContent}
|
||||||
{message.content}
|
onChange={(e) => setEditContent(e.target.value)}
|
||||||
</div>
|
className="w-full p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
{isOwn && (
|
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
|
<div
|
||||||
className={`flex gap-2 mt-0.5 text-[10px]
|
className={`flex gap-2 mt-0.5 text-[10px]
|
||||||
${isOwn ? "justify-end" : "justify-start"}`}
|
${isOwn ? "justify-end" : "justify-start"}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => onEdit?.(message.id)}
|
onClick={() => setIsEditing(true)}
|
||||||
className="text-blue-600 hover:text-blue-800 active:text-blue-900"
|
className="text-blue-600 hover:text-blue-800 active:text-blue-900"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
|
||||||
state,
|
state,
|
||||||
dispatch,
|
dispatch,
|
||||||
actions: {
|
actions: {
|
||||||
|
editMessage: (messageId: string, newContent: string) =>
|
||||||
|
dispatch({ type: "EDIT_MESSAGE", payload: { messageId, newContent } }),
|
||||||
deleteMessage: (messageId: string) =>
|
deleteMessage: (messageId: string) =>
|
||||||
dispatch({ type: "DELETE_MESSAGE", payload: messageId }),
|
dispatch({ type: "DELETE_MESSAGE", payload: messageId }),
|
||||||
addMessage: (message: Message) =>
|
addMessage: (message: Message) =>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
ClientDeleteMessage,
|
ClientDeleteMessage,
|
||||||
|
ClientEditMessage,
|
||||||
ClientMessage,
|
ClientMessage,
|
||||||
ServerMessage,
|
ServerMessage,
|
||||||
} from "../../../shared";
|
} from "../../../shared";
|
||||||
|
|
@ -31,7 +32,17 @@ function ChatMessages({ sendMessage }: MessageDisplayProps) {
|
||||||
sendMessage(deleteMessage);
|
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
|
// scroll to bottom when messages change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export type ChatState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChatAction =
|
export type ChatAction =
|
||||||
|
| { type: "EDIT_MESSAGE"; payload: { messageId: string; newContent: string } }
|
||||||
| { type: "ADD_MESSAGE"; payload: Message }
|
| { type: "ADD_MESSAGE"; payload: Message }
|
||||||
| { type: "DELETE_MESSAGE"; payload: string }
|
| { type: "DELETE_MESSAGE"; payload: string }
|
||||||
| { type: "SET_USER"; payload: User }
|
| { type: "SET_USER"; payload: User }
|
||||||
|
|
@ -25,6 +26,7 @@ export type ChatAction =
|
||||||
| { type: "CLEAR_MESSAGES" };
|
| { type: "CLEAR_MESSAGES" };
|
||||||
|
|
||||||
export type ChatActions = {
|
export type ChatActions = {
|
||||||
|
editMessage: (messageId: string, newContent: string) => void;
|
||||||
addMessage: (message: Message) => void;
|
addMessage: (message: Message) => void;
|
||||||
deleteMessage: (messageId: string) => void;
|
deleteMessage: (messageId: string) => void;
|
||||||
setUser: (user: User) => void;
|
setUser: (user: User) => void;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,13 @@ export function useWebSocket(roomId: string) {
|
||||||
actions.deleteMessage(message.payload.messageId);
|
actions.deleteMessage(message.payload.messageId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "MESSAGE_EDITED":
|
||||||
|
actions.editMessage(
|
||||||
|
message.payload.messageId,
|
||||||
|
message.payload.content,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error("Unknown message type", message);
|
console.error("Unknown message type", message);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,25 @@ import { ChatAction, ChatState } from "../contexts/ChatContext";
|
||||||
|
|
||||||
export function chatReducer(state: ChatState, action: ChatAction): ChatState {
|
export function chatReducer(state: ChatState, action: ChatAction): ChatState {
|
||||||
switch (action.type) {
|
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":
|
case "DELETE_MESSAGE":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type {
|
||||||
ClientMessage,
|
ClientMessage,
|
||||||
ServerChatMessage,
|
ServerChatMessage,
|
||||||
ServerMessageDeletedMessage,
|
ServerMessageDeletedMessage,
|
||||||
|
ServerMessageEditedMessage,
|
||||||
ServerRegistrationConfirmed,
|
ServerRegistrationConfirmed,
|
||||||
ServerUserJoinedMessage,
|
ServerUserJoinedMessage,
|
||||||
User,
|
User,
|
||||||
|
|
@ -77,5 +78,25 @@ export default function handleClientMessage(
|
||||||
for (const [socket] of room.userConnections) {
|
for (const [socket] of room.userConnections) {
|
||||||
socket.send(JSON.stringify(deleteMessage));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue