Fix the context menu

This commit is contained in:
2025-09-04 21:02:27 +03:00
Unverified
parent 182fe88db4
commit e99efabdb7
7 changed files with 405 additions and 37 deletions
@@ -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<UserProfile | null>(null);
const [isLoadingProfile, setIsLoadingProfile] = useState(false);
// Context menu state
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
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 (
<>
<div className="chat-messages" id="chat-messages">
<div className="chat-messages" id="chat-messages" onClick={handleClickOutside}>
{messages.map((message) => (
<Message
key={message.id}
@@ -64,6 +156,19 @@ export function ChatMessages() {
}}
userProfile={selectedUserProfile}
/>
{/* Context Menu */}
{contextMenu.isOpen && contextMenu.message && (
<MessageContextMenu
message={contextMenu.message}
isAuthor={contextMenu.message.username === user.currentUser?.username}
onEdit={handleEdit}
onReply={handleReply}
onDelete={handleDelete}
onClose={handleContextMenuClose}
position={contextMenu.position}
/>
)}
</>
);
}
@@ -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 (
<mdui-dialog id="edit-message-dialog" close-on-overlay-click close-on-esc>
<MaterialDialog open={isOpen} onOpenChange={onOpenChange} close-on-overlay-click close-on-esc>
<div className="dialog-content">
<h3>Edit Message</h3>
<mdui-text-field
id="edit-message-input"
value={editContent}
onInput={(e) => setEditContent((e.target as HTMLInputElement).value)}
label="Edit Message"
variant="outlined"
placeholder="Edit your message..."
maxlength={1000}>
</mdui-text-field>
<div className="dialog-actions">
<mdui-button id="edit-cancel" variant="outlined">Cancel</mdui-button>
<mdui-button id="edit-save">Save</mdui-button>
<mdui-button onClick={handleCancel} variant="outlined">Cancel</mdui-button>
<mdui-button onClick={handleSave}>Save</mdui-button>
</div>
</div>
</mdui-dialog>
</MaterialDialog>
);
}
+8 -1
View File
@@ -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 (
<div
className={`message ${isAuthor ? "sent" : "received"}`}
data-id={message.id}
onContextMenu={(e) => onContextMenu(e, message)}
onContextMenu={handleContextMenu}
>
<div className="message-inner">
{/* Add profile picture for received messages */}
@@ -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 (
<div id="message-context-menu" className="context-menu">
<div className="context-menu-item" data-action="reply">
<span className="material-symbols">reply</span>
Reply
<>
<div
className="context-menu"
style={{
position: "fixed",
display: "block",
top: position.y,
left: position.x,
zIndex: 1000
}}
onClick={(e) => e.stopPropagation()}
>
<div className="context-menu-item" onClick={() => handleAction("reply")}>
<span className="material-symbols">reply</span>
Reply
</div>
{isAuthor && (
<>
<div className="context-menu-item" onClick={() => handleAction("edit")}>
<span className="material-symbols">edit</span>
Edit
</div>
<div className="context-menu-item" onClick={() => handleAction("delete")}>
<span className="material-symbols">delete</span>
Delete
</div>
</>
)}
</div>
<div className="context-menu-item" data-action="edit">
<span className="material-symbols">edit</span>
Edit
</div>
<div className="context-menu-item" data-action="delete">
<span className="material-symbols">delete</span>
Delete
</div>
</div>
{/* Edit Dialog */}
<EditMessageDialog
isOpen={editDialogOpen}
onOpenChange={setEditDialogOpen}
message={message}
onSave={handleEditSave}
/>
{/* Reply Dialog */}
<ReplyMessageDialog
isOpen={replyDialogOpen}
onOpenChange={setReplyDialogOpen}
replyToMessage={message}
onSendReply={handleSendReply}
/>
</>
);
}
@@ -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 (
<mdui-dialog id="reply-message-dialog" close-on-overlay-click close-on-esc>
<MaterialDialog open={isOpen} onOpenChange={onOpenChange} close-on-overlay-click close-on-esc>
<div className="dialog-content">
<h3>Reply to Message</h3>
<div className="reply-preview" id="reply-preview"></div>
<div className="reply-preview">
<div className="reply-content">
<span className="reply-username">{replyToMessage.username}</span>
<span className="reply-text">{replyToMessage.content}</span>
</div>
</div>
<mdui-text-field
id="reply-message-input"
value={replyContent}
onInput={(e) => setReplyContent((e.target as HTMLInputElement).value)}
label="Reply"
variant="outlined"
placeholder="Type your reply..."
maxlength={1000}>
</mdui-text-field>
<div className="dialog-actions">
<mdui-button id="reply-cancel" variant="outlined">Cancel</mdui-button>
<mdui-button id="reply-send">Send Reply</mdui-button>
<mdui-button onClick={handleCancel} variant="outlined">Cancel</mdui-button>
<mdui-button onClick={handleSendReply}>Send Reply</mdui-button>
</div>
</div>
</mdui-dialog>
</MaterialDialog>
);
}