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
+88
View File
@@ -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;
}
}
}
@@ -4,9 +4,13 @@ import { useAppState } from "../../state";
import type { Message as MessageType } from "../../../core/types"; import type { Message as MessageType } from "../../../core/types";
import type { UserProfile } from "../../../core/types"; import type { UserProfile } from "../../../core/types";
import { UserProfileDialog } from "./UserProfileDialog"; import { UserProfileDialog } from "./UserProfileDialog";
import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu";
import { fetchUserProfile } from "../../api/profileApi"; import { fetchUserProfile } from "../../api/profileApi";
import { useState } from "react"; import { useState } from "react";
import { delay } from "../../../utils/utils"; import { delay } from "../../../utils/utils";
import { request } from "../../../websocket";
export function ChatMessages() { export function ChatMessages() {
const { messages } = useChat(); const { messages } = useChat();
@@ -14,6 +18,13 @@ export function ChatMessages() {
const [profileDialogOpen, setProfileDialogOpen] = useState(false); const [profileDialogOpen, setProfileDialogOpen] = useState(false);
const [selectedUserProfile, setSelectedUserProfile] = useState<UserProfile | null>(null); const [selectedUserProfile, setSelectedUserProfile] = useState<UserProfile | null>(null);
const [isLoadingProfile, setIsLoadingProfile] = useState(false); 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) => { const handleProfileClick = async (username: string) => {
if (!user.authToken) return; if (!user.authToken) return;
@@ -34,13 +45,94 @@ export function ChatMessages() {
const handleContextMenu = (e: React.MouseEvent, message: MessageType) => { const handleContextMenu = (e: React.MouseEvent, message: MessageType) => {
e.preventDefault(); e.preventDefault();
// TODO: Show context menu console.log("Context menu triggered for message:", message.id, "at position:", e.clientX, e.clientY);
console.log("Show context menu for message:", message.id); 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 ( return (
<> <>
<div className="chat-messages" id="chat-messages"> <div className="chat-messages" id="chat-messages" onClick={handleClickOutside}>
{messages.map((message) => ( {messages.map((message) => (
<Message <Message
key={message.id} key={message.id}
@@ -64,6 +156,19 @@ export function ChatMessages() {
}} }}
userProfile={selectedUserProfile} 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 ( 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"> <div className="dialog-content">
<h3>Edit Message</h3> <h3>Edit Message</h3>
<mdui-text-field <mdui-text-field
id="edit-message-input" value={editContent}
onInput={(e) => setEditContent((e.target as HTMLInputElement).value)}
label="Edit Message" label="Edit Message"
variant="outlined" variant="outlined"
placeholder="Edit your message..." placeholder="Edit your message..."
maxlength={1000}> maxlength={1000}>
</mdui-text-field> </mdui-text-field>
<div className="dialog-actions"> <div className="dialog-actions">
<mdui-button id="edit-cancel" variant="outlined">Cancel</mdui-button> <mdui-button onClick={handleCancel} variant="outlined">Cancel</mdui-button>
<mdui-button id="edit-save">Save</mdui-button> <mdui-button onClick={handleSave}>Save</mdui-button>
</div> </div>
</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) { 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 ( return (
<div <div
className={`message ${isAuthor ? "sent" : "received"}`} className={`message ${isAuthor ? "sent" : "received"}`}
data-id={message.id} data-id={message.id}
onContextMenu={(e) => onContextMenu(e, message)} onContextMenu={handleContextMenu}
> >
<div className="message-inner"> <div className="message-inner">
{/* Add profile picture for received messages */} {/* 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 ( return (
<div id="message-context-menu" className="context-menu"> <>
<div className="context-menu-item" data-action="reply"> <div
<span className="material-symbols">reply</span> className="context-menu"
Reply 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>
<div className="context-menu-item" data-action="edit">
<span className="material-symbols">edit</span> {/* Edit Dialog */}
Edit <EditMessageDialog
</div> isOpen={editDialogOpen}
<div className="context-menu-item" data-action="delete"> onOpenChange={setEditDialogOpen}
<span className="material-symbols">delete</span> message={message}
Delete onSave={handleEditSave}
</div> />
</div>
{/* 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 ( 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"> <div className="dialog-content">
<h3>Reply to Message</h3> <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 <mdui-text-field
id="reply-message-input" value={replyContent}
onInput={(e) => setReplyContent((e.target as HTMLInputElement).value)}
label="Reply" label="Reply"
variant="outlined" variant="outlined"
placeholder="Type your reply..." placeholder="Type your reply..."
maxlength={1000}> maxlength={1000}>
</mdui-text-field> </mdui-text-field>
<div className="dialog-actions"> <div className="dialog-actions">
<mdui-button id="reply-cancel" variant="outlined">Cancel</mdui-button> <mdui-button onClick={handleCancel} variant="outlined">Cancel</mdui-button>
<mdui-button id="reply-send">Send Reply</mdui-button> <mdui-button onClick={handleSendReply}>Send Reply</mdui-button>
</div> </div>
</div> </div>
</mdui-dialog> </MaterialDialog>
); );
} }
-6
View File
@@ -1,17 +1,11 @@
import { ChatInterface } from "../components/chat/ChatInterface"; import { ChatInterface } from "../components/chat/ChatInterface";
import { CropperDialog } from "../components/profile/CropperDialog"; 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() { export default function ChatScreen() {
return ( return (
<> <>
<ChatInterface /> <ChatInterface />
<CropperDialog /> <CropperDialog />
<MessageContextMenu />
<EditMessageDialog />
<ReplyMessageDialog />
</> </>
); );
} }