From e99efabdb70eef71c8fcdcc5e2801d7011936f21 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Thu, 4 Sep 2025 21:02:27 +0300 Subject: [PATCH] Fix the context menu --- frontend/src/resources/css/_chat.scss | 88 ++++++++++++ .../src/ui/components/chat/ChatMessages.tsx | 111 ++++++++++++++- .../ui/components/chat/EditMessageDialog.tsx | 46 ++++++- frontend/src/ui/components/chat/Message.tsx | 9 +- .../ui/components/chat/MessageContextMenu.tsx | 129 ++++++++++++++++-- .../ui/components/chat/ReplyMessageDialog.tsx | 53 ++++++- frontend/src/ui/screen/ChatScreen.tsx | 6 - 7 files changed, 405 insertions(+), 37 deletions(-) diff --git a/frontend/src/resources/css/_chat.scss b/frontend/src/resources/css/_chat.scss index 42f54e7..8c39343 100644 --- a/frontend/src/resources/css/_chat.scss +++ b/frontend/src/resources/css/_chat.scss @@ -362,3 +362,91 @@ } } +.dialog-content { + padding: 1.5rem; + + h3 { + margin: 0 0 1rem 0; + color: $color-dark-on-surface; + font-size: 1.2rem; + font-weight: 600; + } + + mdui-text-field { + width: 100%; + margin-bottom: 1.5rem; + } + + .reply-preview { + margin-bottom: 1rem; + padding: 0.75rem; + background-color: $color-dark-surface-container; + border-radius: 8px; + border: 1px solid $color-dark-outline; + + .reply-content { + display: flex; + flex-direction: column; + gap: 0.25rem; + + .reply-username { + font-weight: 600; + color: $color-dark-on-surface; + font-size: 0.85rem; + } + + .reply-text { + color: $color-dark-on-surface-variant; + font-size: 0.9rem; + line-height: 1.4; + } + } + } + + .dialog-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; + margin-top: 1rem; + } +} + +.context-menu { + position: fixed; + background: $color-dark-surface; + border: 1px solid $color-dark-outline; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + padding: 0.5rem 0; + min-width: 160px; + z-index: 1000; + + .context-menu-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + cursor: pointer; + color: $color-dark-on-surface; + font-size: 0.9rem; + transition: background-color 0.2s ease; + + &:hover { + background-color: $color-dark-surface-container; + } + + .material-symbols { + font-size: 1.1rem; + color: $color-dark-on-surface-variant; + } + + &:first-child { + border-radius: 8px 8px 0 0; + } + + &:last-child { + border-radius: 0 0 8px 8px; + } + } +} + diff --git a/frontend/src/ui/components/chat/ChatMessages.tsx b/frontend/src/ui/components/chat/ChatMessages.tsx index cee6d0d..4bd1e69 100644 --- a/frontend/src/ui/components/chat/ChatMessages.tsx +++ b/frontend/src/ui/components/chat/ChatMessages.tsx @@ -4,9 +4,13 @@ import { useAppState } from "../../state"; import type { Message as MessageType } from "../../../core/types"; import type { UserProfile } from "../../../core/types"; import { UserProfileDialog } from "./UserProfileDialog"; +import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu"; import { fetchUserProfile } from "../../api/profileApi"; import { useState } from "react"; import { delay } from "../../../utils/utils"; +import { request } from "../../../websocket"; + + export function ChatMessages() { const { messages } = useChat(); @@ -14,6 +18,13 @@ export function ChatMessages() { const [profileDialogOpen, setProfileDialogOpen] = useState(false); const [selectedUserProfile, setSelectedUserProfile] = useState(null); const [isLoadingProfile, setIsLoadingProfile] = useState(false); + + // Context menu state + const [contextMenu, setContextMenu] = useState({ + isOpen: false, + message: null, + position: { x: 0, y: 0 } + }); const handleProfileClick = async (username: string) => { if (!user.authToken) return; @@ -34,13 +45,94 @@ export function ChatMessages() { const handleContextMenu = (e: React.MouseEvent, message: MessageType) => { e.preventDefault(); - // TODO: Show context menu - console.log("Show context menu for message:", message.id); + console.log("Context menu triggered for message:", message.id, "at position:", e.clientX, e.clientY); + setContextMenu({ + isOpen: true, + message, + position: { x: e.clientX, y: e.clientY } + }); + }; + + const handleContextMenuClose = () => { + setContextMenu({ + isOpen: false, + message: null, + position: { x: 0, y: 0 } + }); + }; + + 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 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 handleDelete = async (message: MessageType) => { + if (!user.authToken) return; + + try { + await request({ + type: "deleteMessage", + data: { message_id: message.id }, + credentials: { + scheme: "Bearer", + credentials: user.authToken + } + }); + } catch (error) { + console.error("Failed to delete message:", error); + } + }; + + // Close context menu when clicking outside + const handleClickOutside = (e: React.MouseEvent) => { + if (contextMenu.isOpen) { + // Only close if clicking on the chat messages container, not on the context menu itself + if (e.target === e.currentTarget) { + handleContextMenuClose(); + } + } }; return ( <> -
+
{messages.map((message) => ( + + {/* Context Menu */} + {contextMenu.isOpen && contextMenu.message && ( + + )} ); } diff --git a/frontend/src/ui/components/chat/EditMessageDialog.tsx b/frontend/src/ui/components/chat/EditMessageDialog.tsx index a9119eb..5e2e8b1 100644 --- a/frontend/src/ui/components/chat/EditMessageDialog.tsx +++ b/frontend/src/ui/components/chat/EditMessageDialog.tsx @@ -1,20 +1,54 @@ -export function EditMessageDialog() { +import { useState, useEffect } from "react"; +import type { Message } from "../../../core/types"; +import { MaterialDialog } from "../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 + Cancel + Save
-
+ ); } diff --git a/frontend/src/ui/components/chat/Message.tsx b/frontend/src/ui/components/chat/Message.tsx index 6817f2a..0536724 100644 --- a/frontend/src/ui/components/chat/Message.tsx +++ b/frontend/src/ui/components/chat/Message.tsx @@ -11,11 +11,18 @@ interface MessageProps { } export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLoadingProfile = false }: MessageProps) { + const handleContextMenu = (e: React.MouseEvent) => { + console.log("Message context menu event triggered for message:", message.id); + e.preventDefault(); + e.stopPropagation(); + onContextMenu(e, message); + }; + return (
onContextMenu(e, message)} + onContextMenu={handleContextMenu} >
{/* Add profile picture for received messages */} diff --git a/frontend/src/ui/components/chat/MessageContextMenu.tsx b/frontend/src/ui/components/chat/MessageContextMenu.tsx index 4df2572..7b09729 100644 --- a/frontend/src/ui/components/chat/MessageContextMenu.tsx +++ b/frontend/src/ui/components/chat/MessageContextMenu.tsx @@ -1,18 +1,119 @@ -export function MessageContextMenu() { +import { useState } from "react"; +import type { Message } from "../../../core/types"; +import { EditMessageDialog } from "./EditMessageDialog"; +import { ReplyMessageDialog } from "./ReplyMessageDialog"; + +interface MessageContextMenuProps { + message: Message; + isAuthor: boolean; + onEdit: (message: Message) => void; + onReply: (message: Message) => void; + onDelete: (message: Message) => void; + onClose: () => void; + position: { x: number; y: number }; +} + +export interface ContextMenuState { + isOpen: boolean; + message: Message | null; + position: { x: number; y: number }; +} + +export function MessageContextMenu({ + message, + isAuthor, + onEdit, + onReply, + onDelete, + onClose, + position +}: MessageContextMenuProps) { + console.log("MessageContextMenu rendered with position:", position, "message:", message.id); + + // Internal state for dialogs + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [replyDialogOpen, setReplyDialogOpen] = useState(false); + + const handleAction = (action: string) => { + console.log("Context menu action triggered:", action); + switch (action) { + case "reply": + setReplyDialogOpen(true); + break; + case "edit": + if (isAuthor) { + setEditDialogOpen(true); + } + break; + case "delete": + if (isAuthor) { + onDelete(message); + onClose(); + } + break; + } + }; + + const handleEditSave = (messageId: number, newContent: string) => { + // Create a temporary message object with the updated content + const updatedMessage = { ...message, content: newContent }; + onEdit(updatedMessage); + 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); + }; + return ( -
-
- reply - Reply + <> +
e.stopPropagation()} + > +
handleAction("reply")}> + reply + Reply +
+ {isAuthor && ( + <> +
handleAction("edit")}> + edit + Edit +
+
handleAction("delete")}> + delete + Delete +
+ + )}
-
- edit - Edit -
-
- delete - Delete -
-
+ + {/* Edit Dialog */} + + + {/* Reply Dialog */} + + ); } diff --git a/frontend/src/ui/components/chat/ReplyMessageDialog.tsx b/frontend/src/ui/components/chat/ReplyMessageDialog.tsx index 25140cd..25c99fe 100644 --- a/frontend/src/ui/components/chat/ReplyMessageDialog.tsx +++ b/frontend/src/ui/components/chat/ReplyMessageDialog.tsx @@ -1,21 +1,60 @@ -export function ReplyMessageDialog() { +import { useState, useEffect } from "react"; +import type { Message } from "../../../core/types"; +import { MaterialDialog } from "../Dialog"; + +interface ReplyMessageDialogProps { + isOpen: boolean; + onOpenChange: (value: boolean) => void; + replyToMessage: Message | null; + onSendReply: (content: string, replyToId: number) => void; +} + +export function ReplyMessageDialog({ isOpen, onOpenChange, replyToMessage, onSendReply }: ReplyMessageDialogProps) { + const [replyContent, setReplyContent] = useState(""); + + useEffect(() => { + if (replyToMessage) { + setReplyContent(""); + } + }, [replyToMessage]); + + const handleSendReply = () => { + if (replyToMessage && replyContent.trim()) { + onSendReply(replyContent.trim(), replyToMessage.id); + onOpenChange(false); + } + }; + + const handleCancel = () => { + onOpenChange(false); + setReplyContent(""); + }; + + if (!replyToMessage) return null; + return ( - +

Reply to Message

-
+
+
+ {replyToMessage.username} + {replyToMessage.content} +
+
setReplyContent((e.target as HTMLInputElement).value)} label="Reply" variant="outlined" placeholder="Type your reply..." maxlength={1000}>
- Cancel - Send Reply + Cancel + Send Reply
-
+ ); } diff --git a/frontend/src/ui/screen/ChatScreen.tsx b/frontend/src/ui/screen/ChatScreen.tsx index 2c6406b..2067bee 100644 --- a/frontend/src/ui/screen/ChatScreen.tsx +++ b/frontend/src/ui/screen/ChatScreen.tsx @@ -1,17 +1,11 @@ import { ChatInterface } from "../components/chat/ChatInterface"; import { CropperDialog } from "../components/profile/CropperDialog"; -import { MessageContextMenu } from "../components/chat/MessageContextMenu"; -import { EditMessageDialog } from "../components/chat/EditMessageDialog"; -import { ReplyMessageDialog } from "../components/chat/ReplyMessageDialog"; export default function ChatScreen() { return ( <> - - - ); } \ No newline at end of file