mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Fix dialog click closing the context menu
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user