From b3aa8cebd5f78564ceab517c4b9a7d427cb0f6cf Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Fri, 19 Sep 2025 21:18:57 +0300 Subject: [PATCH] Implement delete confirmation --- .../src/ui/components/chat/ChatMessages.tsx | 68 +++++++++++++------ .../ui/components/chat/MessageContextMenu.tsx | 18 +---- frontend/src/ui/components/core/Dialog.tsx | 8 ++- frontend/src/ui/hooks/useCombinedRefs.ts | 49 +++++++------ 4 files changed, 79 insertions(+), 64 deletions(-) diff --git a/frontend/src/ui/components/chat/ChatMessages.tsx b/frontend/src/ui/components/chat/ChatMessages.tsx index 048caf8..c835004 100644 --- a/frontend/src/ui/components/chat/ChatMessages.tsx +++ b/frontend/src/ui/components/chat/ChatMessages.tsx @@ -6,9 +6,10 @@ import type { UserProfile } from "../../../core/types"; import { UserProfileDialog } from "./UserProfileDialog"; import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu"; import { fetchUserProfile } from "../../../api/profileApi"; -import { useState, type ReactNode } from "react"; +import { useEffect, useState, type ReactNode } from "react"; import { delay } from "../../../utils/utils"; import { request } from "../../../core/websocket"; +import { MaterialDialog } from "../core/Dialog"; interface ChatMessagesProps { messages?: MessageType[]; @@ -35,7 +36,17 @@ export function ChatMessages({ messages: propMessages, children, isDm = false, o position: { x: 0, y: 0 } }); - const handleProfileClick = async (username: string) => { + // Delete dialog + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [toBeDeleted, setToBeDeleted] = useState(null); + + useEffect(() => { + if (!deleteDialogOpen) { + setToBeDeleted(null); + } + }, [deleteDialogOpen]); + + async function handleProfileClick(username: string) { if (!user.authToken) return; setIsLoadingProfile(true); @@ -52,7 +63,7 @@ export function ChatMessages({ messages: propMessages, children, isDm = false, o } }; - const handleContextMenu = (e: React.MouseEvent, message: MessageType) => { + function handleContextMenu(e: React.MouseEvent, message: MessageType) { e.preventDefault(); setContextMenu({ isOpen: true, @@ -61,37 +72,46 @@ export function ChatMessages({ messages: propMessages, children, isDm = false, o }); }; - const handleContextMenuOpenChange = (isOpen: boolean) => { + function handleContextMenuOpenChange(isOpen: boolean) { setContextMenu(prev => ({ ...prev, isOpen })); }; - const handleEdit = (message: MessageType) => { + function handleEdit(message: MessageType) { if (onEditSelect) onEditSelect(message); }; - const handleReply = (message: MessageType) => { + function handleReply(message: MessageType) { if (onReplySelect) onReplySelect(message); }; - const handleDelete = async (message: MessageType) => { - if (!user.authToken) return; + async function confirmDelete() { + if (toBeDeleted) { + 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); + try { + await request({ + type: "deleteMessage", + data: { message_id: toBeDeleted }, + credentials: { + scheme: "Bearer", + credentials: user.authToken + } + }); + } catch (error) { + console.error("Failed to delete message:", error); + } + + setDeleteDialogOpen(false); } - }; + } + + async function handleDelete(message: MessageType) { + setToBeDeleted(message.id); + setDeleteDialogOpen(true); + } return ( <> @@ -120,6 +140,14 @@ export function ChatMessages({ messages: propMessages, children, isDm = false, o }} userProfile={selectedUserProfile} /> + + + setDeleteDialogOpen(false)}>Отменить + Удалить + {/* Context Menu */} {contextMenu.message && ( diff --git a/frontend/src/ui/components/chat/MessageContextMenu.tsx b/frontend/src/ui/components/chat/MessageContextMenu.tsx index 8249ab7..696116e 100644 --- a/frontend/src/ui/components/chat/MessageContextMenu.tsx +++ b/frontend/src/ui/components/chat/MessageContextMenu.tsx @@ -125,6 +125,7 @@ export function MessageContextMenu({ } break; } + onOpenChange(false); }; const handleClose = () => { @@ -141,10 +142,7 @@ export function MessageContextMenu({ }, 200); // Match the animation duration from _animations.scss }; - // Inline edit handled by parent via onEdit - - - const content = ( + return isOpen && (
e.stopPropagation()} - > + onClick={(e) => e.stopPropagation()}>
handleAction("reply")}> reply Ответить @@ -174,13 +171,4 @@ export function MessageContextMenu({ )}
) - - // Don't render if not open - if (!isOpen) return null; - - return ( - <> - {isOpen ? content : null} - - ); } diff --git a/frontend/src/ui/components/core/Dialog.tsx b/frontend/src/ui/components/core/Dialog.tsx index 70f8016..d339d7f 100644 --- a/frontend/src/ui/components/core/Dialog.tsx +++ b/frontend/src/ui/components/core/Dialog.tsx @@ -1,16 +1,18 @@ import type { Dialog as MduiDialog } from "mdui/components/dialog"; -import React, { useEffect, useRef } from "react" +import { useEffect, type Ref } from "react" import { createPortal } from "react-dom"; import { id } from "../../../utils/utils"; +import useCombinedRefs from "../../hooks/useCombinedRefs"; export interface BaseDialogProps { onOpenChange: (value: boolean) => void; + ref?: Ref } export type FullDialogProps = React.ComponentPropsWithoutRef<"mdui-dialog"> & BaseDialogProps; export function MaterialDialog(props: FullDialogProps) { - const dialogRef = useRef(null); + const [setDialogRef, dialogRef] = useCombinedRefs(props.ref); useEffect(() => { const dialog = dialogRef.current; @@ -39,5 +41,5 @@ export function MaterialDialog(props: FullDialogProps) { }; }, [dialogRef.current, props.open, props.onOpenChange]); - return createPortal(, id("root")); + return createPortal(, id("root")); } \ No newline at end of file diff --git a/frontend/src/ui/hooks/useCombinedRefs.ts b/frontend/src/ui/hooks/useCombinedRefs.ts index 8694050..f1fa496 100644 --- a/frontend/src/ui/hooks/useCombinedRefs.ts +++ b/frontend/src/ui/hooks/useCombinedRefs.ts @@ -4,34 +4,31 @@ import { useRef, useCallback, type RefCallback, type Ref } from 'react'; type PossibleRef = Ref | undefined; export default function useCombinedRefs(...refs: PossibleRef[]): [RefCallback, React.RefObject] { - const targetRef = useRef(null); + const targetRef = useRef(null); - const setRefs = useCallback( - (node: T | null) => { - // Обновляем внутренний ref - targetRef.current = node; + const setRefs = useCallback((node: T | null) => { + // Обновляем внутренний ref + targetRef.current = node; - // Обновляем все переданные refs - refs.forEach((ref) => { - if (!ref) { - return; - } + // Обновляем все переданные refs + refs.forEach((ref) => { + if (!ref) return; - if (typeof ref === 'function') { - // Если ref - это функция, вызываем её - ref(node); - } else { - // Если ref - это объект, обновляем его свойство .current - // Используем проверку, чтобы убедиться, что это действительно MutableRefObject - // (хотя в реальном коде это почти всегда так) - ref.current = node; - } - }); - }, - // Убедитесь, что массив зависимостей всегда актуален - // eslint-disable-next-line react-hooks/exhaustive-deps - [...refs] - ); + if (typeof ref === 'function') { + // Если ref - это функция, вызываем её + ref(node); + } else { + // Если ref - это объект, обновляем его свойство .current + // Используем проверку, чтобы убедиться, что это действительно MutableRefObject + // (хотя в реальном коде это почти всегда так) + ref.current = node; + } + }); + }, + // Убедитесь, что массив зависимостей всегда актуален + // eslint-disable-next-line react-hooks/exhaustive-deps + [...refs] + ); - return [setRefs, targetRef]; + return [setRefs, targetRef]; } \ No newline at end of file