Fix dialog click closing the context menu

This commit is contained in:
2025-09-04 21:55:28 +03:00
Unverified
parent 3194379eb1
commit 46dd8887cc
7 changed files with 142 additions and 162 deletions
-68
View File
@@ -321,25 +321,6 @@
} }
} }
// Reply preview styles
.reply-preview {
background-color: $color-dark-surface;
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 1rem;
border-left: 3px solid $color-dark-primary;
.reply-preview-content {
color: $color-dark-on-surface-variant;
font-size: 0.9rem;
line-height: 1.4;
strong {
color: $color-dark-primary;
}
}
}
.message-profile-pic { .message-profile-pic {
img { img {
width: 40px; width: 40px;
@@ -362,55 +343,6 @@
} }
} }
.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 { .context-menu {
position: fixed; position: fixed;
background: $color-dark-surface; background: $color-dark-surface;
@@ -101,12 +101,17 @@ button, input {
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
color: $color-dark-on-surface; color: $color-dark-on-surface;
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 600;
}
mdui-text-field {
width: 100%;
} }
.dialog-actions { .dialog-actions {
display: flex; display: flex;
gap: 0.75rem; gap: 0.75rem;
justify-content: flex-end; justify-content: flex-end;
margin-top: 1.5rem; margin-top: 1rem;
} }
} }
@@ -0,0 +1,31 @@
@use "../common/material" as *;
.reply-dialog .dialog-content {
width: 300px;
overflow-x:hidden;
.reply-preview-dialog {
margin-bottom: 1rem;
padding: 16px;
background-color: $color-dark-surface-container;
border-radius: 16px;
.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;
}
}
}
}
+1
View File
@@ -8,6 +8,7 @@
@use "common/colors" as *; @use "common/colors" as *;
@use "common/material" as *; @use "common/material" as *;
@use "electron"; @use "electron";
@use "dialogs/reply";
@use "lib/fonts/montserrat"; @use "lib/fonts/montserrat";
@use "lib/fonts/material-symbols"; @use "lib/fonts/material-symbols";
@@ -6,7 +6,7 @@ import type { UserProfile } from "../../../core/types";
import { UserProfileDialog } from "./UserProfileDialog"; import { UserProfileDialog } from "./UserProfileDialog";
import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu"; import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu";
import { fetchUserProfile } from "../../api/profileApi"; import { fetchUserProfile } from "../../api/profileApi";
import { useState, useEffect } from "react"; import { useState } from "react";
import { delay } from "../../../utils/utils"; import { delay } from "../../../utils/utils";
import { request } from "../../../websocket"; import { request } from "../../../websocket";
@@ -23,45 +23,6 @@ export function ChatMessages() {
message: null, message: null,
position: { x: 0, y: 0 } position: { x: 0, y: 0 }
}); });
const [isContextMenuClosing, setIsContextMenuClosing] = useState(false);
// Effect to handle clicks outside the context menu
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (contextMenu.isOpen && !isContextMenuClosing) {
// Check if the click is on a context menu element
const target = event.target as Element;
if (!target.closest('.context-menu')) {
handleContextMenuClose();
}
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && contextMenu.isOpen && !isContextMenuClosing) {
handleContextMenuClose();
}
};
const handleWindowBlur = () => {
// Close context menu when browser window loses focus
if (contextMenu.isOpen && !isContextMenuClosing) {
handleContextMenuClose();
}
};
// Add event listeners
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
window.addEventListener('blur', handleWindowBlur);
// Cleanup
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('blur', handleWindowBlur);
};
}, [contextMenu.isOpen, isContextMenuClosing]);
const handleProfileClick = async (username: string) => { const handleProfileClick = async (username: string) => {
if (!user.authToken) return; if (!user.authToken) return;
@@ -83,7 +44,6 @@ export function ChatMessages() {
const handleContextMenu = (e: React.MouseEvent, message: MessageType) => { const handleContextMenu = (e: React.MouseEvent, message: MessageType) => {
e.preventDefault(); e.preventDefault();
console.log("Context menu triggered for message:", message.id, "at position:", e.clientX, e.clientY); console.log("Context menu triggered for message:", message.id, "at position:", e.clientX, e.clientY);
setIsContextMenuClosing(false);
setContextMenu({ setContextMenu({
isOpen: true, isOpen: true,
message, message,
@@ -91,17 +51,11 @@ export function ChatMessages() {
}); });
}; };
const handleContextMenuClose = () => { const handleContextMenuOpenChange = (isOpen: boolean) => {
setIsContextMenuClosing(true); setContextMenu(prev => ({
// Wait for animation to complete before removing from DOM ...prev,
setTimeout(() => { isOpen
setContextMenu({ }));
isOpen: false,
message: null,
position: { x: 0, y: 0 }
});
setIsContextMenuClosing(false);
}, 200); // Match the animation duration from _animations.scss
}; };
const handleEdit = async (message: MessageType) => { const handleEdit = async (message: MessageType) => {
@@ -191,16 +145,16 @@ export function ChatMessages() {
/> />
{/* Context Menu */} {/* Context Menu */}
{contextMenu.isOpen && contextMenu.message && ( {contextMenu.message && (
<MessageContextMenu <MessageContextMenu
message={contextMenu.message} message={contextMenu.message}
isAuthor={contextMenu.message.username === user.currentUser?.username} isAuthor={contextMenu.message.username === user.currentUser?.username}
onEdit={handleEdit} onEdit={handleEdit}
onReply={handleReply} onReply={handleReply}
onDelete={handleDelete} onDelete={handleDelete}
onClose={handleContextMenuClose}
position={contextMenu.position} position={contextMenu.position}
isClosing={isContextMenuClosing} isOpen={contextMenu.isOpen}
onOpenChange={handleContextMenuOpenChange}
/> />
)} )}
</> </>
@@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import type { Message } from "../../../core/types"; import type { Message } from "../../../core/types";
import { EditMessageDialog } from "./EditMessageDialog"; import { EditMessageDialog } from "./EditMessageDialog";
import { ReplyMessageDialog } from "./ReplyMessageDialog"; import { ReplyMessageDialog } from "./ReplyMessageDialog";
@@ -9,9 +9,9 @@ interface MessageContextMenuProps {
onEdit: (message: Message) => void; onEdit: (message: Message) => void;
onReply: (message: Message) => void; onReply: (message: Message) => void;
onDelete: (message: Message) => void; onDelete: (message: Message) => void;
onClose: () => void;
position: { x: number; y: number }; position: { x: number; y: number };
isClosing: boolean; isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
} }
export interface ContextMenuState { export interface ContextMenuState {
@@ -26,15 +26,54 @@ export function MessageContextMenu({
onEdit, onEdit,
onReply, onReply,
onDelete, onDelete,
onClose,
position, position,
isClosing isOpen,
onOpenChange
}: MessageContextMenuProps) { }: MessageContextMenuProps) {
console.log("MessageContextMenu rendered with position:", position, "message:", message.id); console.log("MessageContextMenu rendered with position:", position, "message:", message.id);
// Internal state for dialogs // Internal state for dialogs and closing animation
const [editDialogOpen, setEditDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false);
const [replyDialogOpen, setReplyDialogOpen] = useState(false); const [replyDialogOpen, setReplyDialogOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false);
// Effect to handle clicks outside the context menu
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (isOpen && !isClosing && !editDialogOpen && !replyDialogOpen) {
// Check if the click is on a context menu element
const target = event.target as Element;
if (!target.closest('.context-menu')) {
handleClose();
}
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen && !isClosing && !editDialogOpen && !replyDialogOpen) {
handleClose();
}
};
const handleWindowBlur = () => {
// Close context menu when browser window loses focus
if (isOpen && !isClosing && !editDialogOpen && !replyDialogOpen) {
handleClose();
}
};
// Add event listeners
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
window.addEventListener('blur', handleWindowBlur);
// Cleanup
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('blur', handleWindowBlur);
};
}, [isOpen, isClosing, editDialogOpen, replyDialogOpen]);
const handleAction = (action: string) => { const handleAction = (action: string) => {
console.log("Context menu action triggered:", action); console.log("Context menu action triggered:", action);
@@ -50,12 +89,21 @@ export function MessageContextMenu({
case "delete": case "delete":
if (isAuthor) { if (isAuthor) {
onDelete(message); onDelete(message);
onClose(); handleClose();
} }
break; break;
} }
}; };
const handleClose = () => {
setIsClosing(true);
// Wait for animation to complete before calling onOpenChange
setTimeout(() => {
onOpenChange(false);
setIsClosing(false);
}, 200); // Match the animation duration from _animations.scss
};
const handleEditSave = (messageId: number, newContent: string) => { const handleEditSave = (messageId: number, newContent: string) => {
// Create a temporary message object with the updated content // Create a temporary message object with the updated content
const updatedMessage = { ...message, content: newContent }; const updatedMessage = { ...message, content: newContent };
@@ -70,36 +118,45 @@ export function MessageContextMenu({
setReplyDialogOpen(false); setReplyDialogOpen(false);
}; };
const content = (
<div
className={`context-menu ${isClosing ? 'closing' : 'entering'}`}
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>
)
// Don't render if not open
if (!isOpen && !editDialogOpen && !replyDialogOpen) return null;
return ( return (
<> <>
<div {isOpen ? content : null}
className={`context-menu ${isClosing ? 'closing' : 'entering'}`}
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>
{/* Edit Dialog */} {/* Edit Dialog */}
<EditMessageDialog <EditMessageDialog
@@ -33,10 +33,10 @@ export function ReplyMessageDialog({ isOpen, onOpenChange, replyToMessage, onSen
if (!replyToMessage) return null; if (!replyToMessage) return null;
return ( return (
<MaterialDialog open={isOpen} onOpenChange={onOpenChange} close-on-overlay-click close-on-esc> <MaterialDialog open={isOpen} onOpenChange={onOpenChange} close-on-overlay-click close-on-esc className="reply-dialog">
<div className="dialog-content"> <div className="dialog-content">
<h3>Reply to Message</h3> <h3>Reply to Message</h3>
<div className="reply-preview"> <div className="reply-preview-dialog">
<div className="reply-content"> <div className="reply-content">
<span className="reply-username">{replyToMessage.username}</span> <span className="reply-username">{replyToMessage.username}</span>
<span className="reply-text">{replyToMessage.content}</span> <span className="reply-text">{replyToMessage.content}</span>