From 88835e65aea9e2d787598a99da5d404f8de4ae06 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Thu, 18 Sep 2025 18:23:24 +0300 Subject: [PATCH] Start inline reply --- .../ui/components/chat/ChatInputWrapper.tsx | 19 ++++++++++-- .../src/ui/components/chat/ChatMessages.tsx | 24 +++----------- .../ui/components/chat/MessageContextMenu.tsx | 31 +++++-------------- .../components/chat/MessagePanelRenderer.tsx | 13 ++++++-- frontend/src/ui/panels/DMPanel.ts | 2 +- frontend/src/ui/panels/MessagePanel.ts | 6 ++-- frontend/src/ui/panels/PublicChatPanel.ts | 4 +-- 7 files changed, 45 insertions(+), 54 deletions(-) diff --git a/frontend/src/ui/components/chat/ChatInputWrapper.tsx b/frontend/src/ui/components/chat/ChatInputWrapper.tsx index 19f4dd3..3912ece 100644 --- a/frontend/src/ui/components/chat/ChatInputWrapper.tsx +++ b/frontend/src/ui/components/chat/ChatInputWrapper.tsx @@ -1,11 +1,14 @@ import { useState } from "react"; import { RichTextArea } from "../core/RichTextArea"; +import type { Message } from "../../../core/types"; interface ChatInputWrapperProps { - onSendMessage: (message: string) => void; + onSendMessage: (message: string) => void | ((message: string) => void); + replyTo?: Message | null; + onClearReply?: () => void; } -export function ChatInputWrapper({ onSendMessage }: ChatInputWrapperProps) { +export function ChatInputWrapper({ onSendMessage, replyTo, onClearReply }: ChatInputWrapperProps) { const [message, setMessage] = useState(""); const handleSubmit = async (e: React.FormEvent | Event) => { @@ -13,6 +16,7 @@ export function ChatInputWrapper({ onSendMessage }: ChatInputWrapperProps) { if (message.trim()) { onSendMessage(message); setMessage(""); + if (onClearReply) onClearReply(); } }; @@ -20,6 +24,17 @@ export function ChatInputWrapper({ onSendMessage }: ChatInputWrapperProps) {
+ {replyTo && ( +
+
+ {replyTo.username} + {replyTo.content} +
+ +
+ )} void; } -export function ChatMessages({ messages: propMessages, children, isDm = false }: ChatMessagesProps) { +export function ChatMessages({ messages: propMessages, children, isDm = false, onReplySelect }: ChatMessagesProps) { const { messages: hookMessages } = useChat(); const { user } = useAppState(); @@ -87,25 +88,8 @@ export function ChatMessages({ messages: propMessages, children, isDm = false }: } }; - const handleReply = async (message: MessageType) => { - // This will be called when the reply dialog is sent - if (!user.authToken) return; - - try { - await request({ - type: "replyMessage", - data: { - content: message.content, // This should be the reply content from the dialog - reply_to_id: message.id - }, - credentials: { - scheme: "Bearer", - credentials: user.authToken - } - }); - } catch (error) { - console.error("Failed to send reply:", error); - } + const handleReply = (message: MessageType) => { + if (onReplySelect) onReplySelect(message); }; const handleDelete = async (message: MessageType) => { diff --git a/frontend/src/ui/components/chat/MessageContextMenu.tsx b/frontend/src/ui/components/chat/MessageContextMenu.tsx index 493bcf2..5374239 100644 --- a/frontend/src/ui/components/chat/MessageContextMenu.tsx +++ b/frontend/src/ui/components/chat/MessageContextMenu.tsx @@ -1,7 +1,6 @@ import { useState, useEffect } from "react"; import type { Message, Size2D } from "../../../core/types"; import { EditMessageDialog } from "./EditMessageDialog"; -import { ReplyMessageDialog } from "./ReplyMessageDialog"; interface MessageContextMenuProps { message: Message; @@ -32,7 +31,6 @@ export function MessageContextMenu({ }: MessageContextMenuProps) { // Internal state for dialogs and closing animation const [editDialogOpen, setEditDialogOpen] = useState(false); - const [replyDialogOpen, setReplyDialogOpen] = useState(false); const [isClosing, setIsClosing] = useState(false); const [calculatedPosition, setCalculatedPosition] = useState(position); const [animationClass, setAnimationClass] = useState('entering'); @@ -78,7 +76,7 @@ export function MessageContextMenu({ // Effect to handle clicks outside the context menu useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (isOpen && !isClosing && !editDialogOpen && !replyDialogOpen) { + if (isOpen && !isClosing && !editDialogOpen) { // Check if the click is on a context menu element const target = event.target as Element; if (!target.closest('.context-menu')) { @@ -88,14 +86,14 @@ export function MessageContextMenu({ }; const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape' && isOpen && !isClosing && !editDialogOpen && !replyDialogOpen) { + if (event.key === 'Escape' && isOpen && !isClosing && !editDialogOpen) { handleClose(); } }; const handleWindowBlur = () => { // Close context menu when browser window loses focus - if (isOpen && !isClosing && !editDialogOpen && !replyDialogOpen) { + if (isOpen && !isClosing && !editDialogOpen) { handleClose(); } }; @@ -111,12 +109,13 @@ export function MessageContextMenu({ document.removeEventListener('keydown', handleKeyDown); window.removeEventListener('blur', handleWindowBlur); }; - }, [isOpen, isClosing, editDialogOpen, replyDialogOpen]); + }, [isOpen, isClosing, editDialogOpen]); const handleAction = (action: string) => { switch (action) { case "reply": - setReplyDialogOpen(true); + onReply(message); + handleClose(); break; case "edit": if (isAuthor) { @@ -153,14 +152,6 @@ export function MessageContextMenu({ setEditDialogOpen(false); }; - const handleSendReply = (content: string, replyToId: number) => { - // Create a temporary message object with the reply content - const replyMessage = { ...message, content, id: replyToId }; - onReply(replyMessage); - setReplyDialogOpen(false); - }; - - const content = (
@@ -207,14 +198,6 @@ export function MessageContextMenu({ message={message} onSave={handleEditSave} /> - - {/* Reply Dialog */} - ); } diff --git a/frontend/src/ui/components/chat/MessagePanelRenderer.tsx b/frontend/src/ui/components/chat/MessagePanelRenderer.tsx index 313d307..a6ad7f7 100644 --- a/frontend/src/ui/components/chat/MessagePanelRenderer.tsx +++ b/frontend/src/ui/components/chat/MessagePanelRenderer.tsx @@ -3,6 +3,7 @@ import { MessagePanel, type MessagePanelState } from "../../panels/MessagePanel" import { ChatMessages } from "./ChatMessages"; import { ChatInputWrapper } from "./ChatInputWrapper"; import { setGlobalMessageHandler } from "../../../core/websocket"; +import type { Message } from "../../../core/types"; import defaultAvatar from "../../../resources/images/default-avatar.png"; interface MessagePanelRendererProps { @@ -15,6 +16,7 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen const [switchIn, setSwitchIn] = useState(false); const [switchOut, setSwitchOut] = useState(false); const messagesEndRef = useRef(null); + const [replyTo, setReplyTo] = useState(null); // Handle panel state changes useEffect(() => { @@ -132,12 +134,19 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
): ( - + setReplyTo(m)}>
)} - + { + panel.handleSendMessage(text, replyTo?.id); + setReplyTo(null); + }} + replyTo={replyTo} + onClearReply={() => setReplyTo(null)} + />
); diff --git a/frontend/src/ui/panels/DMPanel.ts b/frontend/src/ui/panels/DMPanel.ts index 5b3cd90..d97d968 100644 --- a/frontend/src/ui/panels/DMPanel.ts +++ b/frontend/src/ui/panels/DMPanel.ts @@ -88,7 +88,7 @@ export class DMPanel extends MessagePanel { } } - async sendMessage(content: string): Promise { + async sendMessage(content: string, _replyToId?: number): Promise { if (!this.currentUser.authToken || !this.dmData || !content.trim()) return; try { diff --git a/frontend/src/ui/panels/MessagePanel.ts b/frontend/src/ui/panels/MessagePanel.ts index 146e23c..da704c3 100644 --- a/frontend/src/ui/panels/MessagePanel.ts +++ b/frontend/src/ui/panels/MessagePanel.ts @@ -48,7 +48,7 @@ export abstract class MessagePanel { abstract activate(): Promise; abstract deactivate(): void; abstract loadMessages(): Promise; - abstract sendMessage(content: string): Promise; + abstract sendMessage(content: string, replyToId?: number): Promise; abstract isDm(): boolean; // Optional WebSocket message handler (can be overridden by subclasses) @@ -115,8 +115,8 @@ export abstract class MessagePanel { } // Event handlers - handleSendMessage = (content: string): void => { - this.sendMessage(content); + handleSendMessage = (content: string, replyToId?: number): void => { + this.sendMessage(content, replyToId); }; handleEditMessage = (messageId: number, content: string): void => { diff --git a/frontend/src/ui/panels/PublicChatPanel.ts b/frontend/src/ui/panels/PublicChatPanel.ts index 457e762..d32cf79 100644 --- a/frontend/src/ui/panels/PublicChatPanel.ts +++ b/frontend/src/ui/panels/PublicChatPanel.ts @@ -61,12 +61,12 @@ export class PublicChatPanel extends MessagePanel { } } - async sendMessage(content: string): Promise { + async sendMessage(content: string, replyToId?: number): Promise { if (!this.currentUser.authToken || !content.trim()) return; try { const response = await request({ - data: { content: content.trim() }, + data: { content: content.trim(), ...(replyToId ? { reply_to_id: replyToId } : {}) }, credentials: { scheme: "Bearer", credentials: this.currentUser.authToken