From 8f2e5b2c7e875b575ce12d878528e68dde2334f2 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Fri, 19 Sep 2025 17:06:37 +0300 Subject: [PATCH] Implement inline edit --- .../ui/components/chat/ChatInputWrapper.tsx | 42 +++++++++++-- .../src/ui/components/chat/ChatMessages.tsx | 24 ++----- .../ui/components/chat/EditMessageDialog.tsx | 54 ---------------- .../ui/components/chat/MessageContextMenu.tsx | 33 +++------- .../components/chat/MessagePanelRenderer.tsx | 62 ++++++++++++++++++- frontend/src/ui/state.ts | 19 +++++- 6 files changed, 125 insertions(+), 109 deletions(-) delete mode 100644 frontend/src/ui/components/chat/EditMessageDialog.tsx diff --git a/frontend/src/ui/components/chat/ChatInputWrapper.tsx b/frontend/src/ui/components/chat/ChatInputWrapper.tsx index 0f5ad47..8833746 100644 --- a/frontend/src/ui/components/chat/ChatInputWrapper.tsx +++ b/frontend/src/ui/components/chat/ChatInputWrapper.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { RichTextArea } from "../core/RichTextArea"; import type { Message } from "../../../core/types"; import Quote from "../core/Quote"; @@ -6,27 +6,57 @@ import AnimatedHeight from "../core/animations/AnimatedHeight"; interface ChatInputWrapperProps { onSendMessage: (message: string) => void; + onSaveEdit?: (content: string) => void; replyTo?: Message | null; replyToVisible: boolean; onClearReply?: () => void; onCloseReply?: () => void; + editingMessage?: Message | null; + editVisible?: boolean; + onClearEdit?: () => void; + onCloseEdit?: () => void; } -export function ChatInputWrapper({ onSendMessage, replyTo, replyToVisible, onClearReply, onCloseReply }: ChatInputWrapperProps) { +export function ChatInputWrapper({ onSendMessage, onSaveEdit, replyTo, replyToVisible, onClearReply, onCloseReply, editingMessage, editVisible = false, onClearEdit, onCloseEdit }: ChatInputWrapperProps) { const [message, setMessage] = useState(""); + // When entering edit mode, preload the message content + useEffect(() => { + if (editingMessage) { + setMessage(editingMessage.content || ""); + } + }, [editingMessage]); + const handleSubmit = async (e: React.FormEvent | Event) => { e.preventDefault(); if (message.trim()) { - onSendMessage(message); - setMessage(""); - if (onClearReply) onClearReply(); + if (editingMessage && onSaveEdit) { + onSaveEdit(message); + setMessage(""); + if (onClearEdit) onClearEdit(); + } else { + onSendMessage(message); + setMessage(""); + if (onClearReply) onClearReply(); + } } }; return (
+ + {editingMessage && ( +
+ + + {editingMessage!.username} + {editingMessage!.content} + + +
+ )} +
{replyTo && (
@@ -50,7 +80,7 @@ export function ChatInputWrapper({ onSendMessage, replyTo, replyToVisible, onCle onTextChange={(value) => setMessage(value)} onEnter={handleSubmit} />
diff --git a/frontend/src/ui/components/chat/ChatMessages.tsx b/frontend/src/ui/components/chat/ChatMessages.tsx index 80f824c..048caf8 100644 --- a/frontend/src/ui/components/chat/ChatMessages.tsx +++ b/frontend/src/ui/components/chat/ChatMessages.tsx @@ -15,9 +15,10 @@ interface ChatMessagesProps { isDm?: boolean; children?: ReactNode; onReplySelect?: (message: MessageType) => void; + onEditSelect?: (message: MessageType) => void; } -export function ChatMessages({ messages: propMessages, children, isDm = false, onReplySelect }: ChatMessagesProps) { +export function ChatMessages({ messages: propMessages, children, isDm = false, onReplySelect, onEditSelect }: ChatMessagesProps) { const { messages: hookMessages } = useChat(); const { user } = useAppState(); @@ -67,25 +68,8 @@ export function ChatMessages({ messages: propMessages, children, isDm = false, o })); }; - const handleEdit = async (message: MessageType) => { - // This will be called when the edit dialog is saved - if (!user.authToken) return; - - try { - await request({ - type: "editMessage", - data: { - message_id: message.id, - content: message.content // This should be updated content from the dialog - }, - credentials: { - scheme: "Bearer", - credentials: user.authToken - } - }); - } catch (error) { - console.error("Failed to edit message:", error); - } + const handleEdit = (message: MessageType) => { + if (onEditSelect) onEditSelect(message); }; const handleReply = (message: MessageType) => { diff --git a/frontend/src/ui/components/chat/EditMessageDialog.tsx b/frontend/src/ui/components/chat/EditMessageDialog.tsx deleted file mode 100644 index 9ff7b20..0000000 --- a/frontend/src/ui/components/chat/EditMessageDialog.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useState, useEffect } from "react"; -import type { Message } from "../../../core/types"; -import { MaterialDialog } from "../core/Dialog"; - -interface EditMessageDialogProps { - isOpen: boolean; - onOpenChange: (value: boolean) => void; - message: Message | null; - onSave: (messageId: number, newContent: string) => void; -} - -export function EditMessageDialog({ isOpen, onOpenChange, message, onSave }: EditMessageDialogProps) { - const [editContent, setEditContent] = useState(""); - - useEffect(() => { - if (message) { - setEditContent(message.content); - } - }, [message]); - - const handleSave = () => { - if (message && editContent.trim()) { - onSave(message.id, editContent.trim()); - onOpenChange(false); - } - }; - - const handleCancel = () => { - onOpenChange(false); - setEditContent(""); - }; - - if (!message) return null; - - return ( - -
-

Edit Message

- setEditContent((e.target as HTMLInputElement).value)} - label="Edit Message" - variant="outlined" - placeholder="Edit your message..." - maxlength={1000}> - -
- Cancel - Save -
-
-
- ); -} diff --git a/frontend/src/ui/components/chat/MessageContextMenu.tsx b/frontend/src/ui/components/chat/MessageContextMenu.tsx index 5374239..8249ab7 100644 --- a/frontend/src/ui/components/chat/MessageContextMenu.tsx +++ b/frontend/src/ui/components/chat/MessageContextMenu.tsx @@ -1,6 +1,5 @@ import { useState, useEffect } from "react"; import type { Message, Size2D } from "../../../core/types"; -import { EditMessageDialog } from "./EditMessageDialog"; interface MessageContextMenuProps { message: Message; @@ -29,8 +28,7 @@ export function MessageContextMenu({ isOpen, onOpenChange }: MessageContextMenuProps) { - // Internal state for dialogs and closing animation - const [editDialogOpen, setEditDialogOpen] = useState(false); + // Internal state for closing animation const [isClosing, setIsClosing] = useState(false); const [calculatedPosition, setCalculatedPosition] = useState(position); const [animationClass, setAnimationClass] = useState('entering'); @@ -76,7 +74,7 @@ export function MessageContextMenu({ // Effect to handle clicks outside the context menu useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (isOpen && !isClosing && !editDialogOpen) { + if (isOpen && !isClosing) { // Check if the click is on a context menu element const target = event.target as Element; if (!target.closest('.context-menu')) { @@ -86,14 +84,14 @@ export function MessageContextMenu({ }; const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape' && isOpen && !isClosing && !editDialogOpen) { + if (event.key === 'Escape' && isOpen && !isClosing) { handleClose(); } }; const handleWindowBlur = () => { // Close context menu when browser window loses focus - if (isOpen && !isClosing && !editDialogOpen) { + if (isOpen && !isClosing) { handleClose(); } }; @@ -109,7 +107,7 @@ export function MessageContextMenu({ document.removeEventListener('keydown', handleKeyDown); window.removeEventListener('blur', handleWindowBlur); }; - }, [isOpen, isClosing, editDialogOpen]); + }, [isOpen, isClosing]); const handleAction = (action: string) => { switch (action) { @@ -118,9 +116,7 @@ export function MessageContextMenu({ handleClose(); break; case "edit": - if (isAuthor) { - setEditDialogOpen(true); - } + if (isAuthor) onEdit(message); break; case "delete": if (isAuthor) { @@ -145,12 +141,7 @@ export function MessageContextMenu({ }, 200); // Match the animation duration from _animations.scss }; - const handleEditSave = (_messageId: number, newContent: string) => { - // Create a temporary message object with the updated content - const updatedMessage = { ...message, content: newContent }; - onEdit(updatedMessage); - setEditDialogOpen(false); - }; + // Inline edit handled by parent via onEdit const content = ( @@ -185,19 +176,11 @@ export function MessageContextMenu({ ) // Don't render if not open - if (!isOpen && !editDialogOpen) return null; + if (!isOpen) return null; return ( <> {isOpen ? content : null} - - {/* Edit Dialog */} - ); } diff --git a/frontend/src/ui/components/chat/MessagePanelRenderer.tsx b/frontend/src/ui/components/chat/MessagePanelRenderer.tsx index b442436..062511c 100644 --- a/frontend/src/ui/components/chat/MessagePanelRenderer.tsx +++ b/frontend/src/ui/components/chat/MessagePanelRenderer.tsx @@ -18,6 +18,9 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen const messagesEndRef = useRef(null); const [replyTo, setReplyTo] = useState(null); const [replyToVisible, setReplyToVisible] = useState(Boolean(replyTo)); + const [editMessage, setEditMessage] = useState(null); + const [editVisible, setEditVisible] = useState(Boolean(editMessage)); + const [pendingAction, setPendingAction] = useState(null); useEffect(() => { if (replyTo) { @@ -25,6 +28,12 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen } }, [replyTo]); + useEffect(() => { + if (editMessage) { + setEditVisible(true); + } + }, [editMessage]); + // Handle panel state changes useEffect(() => { if (panel) { @@ -141,7 +150,26 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
): ( - setReplyTo(m)}> + { + if (editMessage || editVisible) { + setPendingAction({ type: "reply", message: m }); + setEditVisible(false); // onCloseEdit will apply pending + } else { + setReplyTo(m); + } + }} + onEditSelect={(m) => { + if (replyTo || replyToVisible) { + setPendingAction({ type: "edit", message: m }); + setReplyToVisible(false); // onCloseReply will apply pending + } else { + setEditMessage(m); + } + }} + >
)} @@ -151,10 +179,38 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen panel.handleSendMessage(text, replyTo?.id); setReplyTo(null); }} + onSaveEdit={(content) => { + if (editMessage) { + panel.handleEditMessage(editMessage.id, content); + setEditMessage(null); + } + }} replyTo={replyTo} replyToVisible={replyToVisible} - onClearReply={() => setReplyToVisible(false)} - onCloseReply={() => setReplyTo(null)} + onClearReply={() => { + setPendingAction(null); + setReplyToVisible(false); + }} + onCloseReply={() => { + setReplyTo(null); + if (pendingAction && pendingAction.type === "edit") { + setEditMessage(pendingAction.message); + setPendingAction(null); + } + }} + editingMessage={editMessage} + editVisible={editVisible} + onClearEdit={() => { + setPendingAction(null); + setEditVisible(false); + }} + onCloseEdit={() => { + setEditMessage(null); + if (pendingAction && pendingAction.type === "reply") { + setReplyTo(pendingAction.message); + setPendingAction(null); + } + }} />
diff --git a/frontend/src/ui/state.ts b/frontend/src/ui/state.ts index 335dcf3..cbf1879 100644 --- a/frontend/src/ui/state.ts +++ b/frontend/src/ui/state.ts @@ -248,7 +248,24 @@ export const useAppState = create((set, get) => ({ if (!publicChatPanel) { const callbacks = { onSendMessage: (_content: string) => {}, - onEditMessage: (_messageId: number, _content: string) => {}, + onEditMessage: async (messageId: number, content: string) => { + if (!user.authToken) return; + try { + await request({ + type: "editMessage", + data: { + message_id: messageId, + content: content + }, + credentials: { + scheme: "Bearer", + credentials: user.authToken + } + }); + } catch (error) { + console.error("Failed to edit message:", error); + } + }, onDeleteMessage: (_messageId: number) => {}, onReplyToMessage: (_messageId: number, _content: string) => {}, onProfileClick: () => {}