Implement delete confirmation

This commit is contained in:
2025-09-19 21:18:57 +03:00
Unverified
parent 76cb084675
commit b3aa8cebd5
4 changed files with 79 additions and 64 deletions
@@ -6,9 +6,10 @@ 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, type ReactNode } from "react"; import { useEffect, useState, type ReactNode } from "react";
import { delay } from "../../../utils/utils"; import { delay } from "../../../utils/utils";
import { request } from "../../../core/websocket"; import { request } from "../../../core/websocket";
import { MaterialDialog } from "../core/Dialog";
interface ChatMessagesProps { interface ChatMessagesProps {
messages?: MessageType[]; messages?: MessageType[];
@@ -35,7 +36,17 @@ export function ChatMessages({ messages: propMessages, children, isDm = false, o
position: { x: 0, y: 0 } position: { x: 0, y: 0 }
}); });
const handleProfileClick = async (username: string) => { // Delete dialog
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [toBeDeleted, setToBeDeleted] = useState<number | null>(null);
useEffect(() => {
if (!deleteDialogOpen) {
setToBeDeleted(null);
}
}, [deleteDialogOpen]);
async function handleProfileClick(username: string) {
if (!user.authToken) return; if (!user.authToken) return;
setIsLoadingProfile(true); 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(); e.preventDefault();
setContextMenu({ setContextMenu({
isOpen: true, 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 => ({ setContextMenu(prev => ({
...prev, ...prev,
isOpen isOpen
})); }));
}; };
const handleEdit = (message: MessageType) => { function handleEdit(message: MessageType) {
if (onEditSelect) onEditSelect(message); if (onEditSelect) onEditSelect(message);
}; };
const handleReply = (message: MessageType) => { function handleReply(message: MessageType) {
if (onReplySelect) onReplySelect(message); if (onReplySelect) onReplySelect(message);
}; };
const handleDelete = async (message: MessageType) => { async function confirmDelete() {
if (!user.authToken) return; if (toBeDeleted) {
if (!user.authToken) return;
try { try {
await request({ await request({
type: "deleteMessage", type: "deleteMessage",
data: { message_id: message.id }, data: { message_id: toBeDeleted },
credentials: { credentials: {
scheme: "Bearer", scheme: "Bearer",
credentials: user.authToken credentials: user.authToken
} }
}); });
} catch (error) { } catch (error) {
console.error("Failed to delete message:", error); console.error("Failed to delete message:", error);
}
setDeleteDialogOpen(false);
} }
}; }
async function handleDelete(message: MessageType) {
setToBeDeleted(message.id);
setDeleteDialogOpen(true);
}
return ( return (
<> <>
@@ -121,6 +141,14 @@ export function ChatMessages({ messages: propMessages, children, isDm = false, o
userProfile={selectedUserProfile} userProfile={selectedUserProfile}
/> />
<MaterialDialog
headline="Удалить сообщение?"
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}>
<mdui-button slot="action" variant="tonal" onClick={() => setDeleteDialogOpen(false)}>Отменить</mdui-button>
<mdui-button slot="action" variant="filled" onClick={confirmDelete}>Удалить</mdui-button>
</MaterialDialog>
{/* Context Menu */} {/* Context Menu */}
{contextMenu.message && ( {contextMenu.message && (
<MessageContextMenu <MessageContextMenu
@@ -125,6 +125,7 @@ export function MessageContextMenu({
} }
break; break;
} }
onOpenChange(false);
}; };
const handleClose = () => { const handleClose = () => {
@@ -141,10 +142,7 @@ export function MessageContextMenu({
}, 200); // Match the animation duration from _animations.scss }, 200); // Match the animation duration from _animations.scss
}; };
// Inline edit handled by parent via onEdit return isOpen && (
const content = (
<div <div
className={`context-menu ${animationClass}`} className={`context-menu ${animationClass}`}
style={{ style={{
@@ -154,8 +152,7 @@ export function MessageContextMenu({
left: calculatedPosition.x, left: calculatedPosition.x,
zIndex: 1000 zIndex: 1000
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
>
<div className="context-menu-item" onClick={() => handleAction("reply")}> <div className="context-menu-item" onClick={() => handleAction("reply")}>
<span className="material-symbols">reply</span> <span className="material-symbols">reply</span>
Ответить Ответить
@@ -174,13 +171,4 @@ export function MessageContextMenu({
)} )}
</div> </div>
) )
// Don't render if not open
if (!isOpen) return null;
return (
<>
{isOpen ? content : null}
</>
);
} }
+5 -3
View File
@@ -1,16 +1,18 @@
import type { Dialog as MduiDialog } from "mdui/components/dialog"; 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 { createPortal } from "react-dom";
import { id } from "../../../utils/utils"; import { id } from "../../../utils/utils";
import useCombinedRefs from "../../hooks/useCombinedRefs";
export interface BaseDialogProps { export interface BaseDialogProps {
onOpenChange: (value: boolean) => void; onOpenChange: (value: boolean) => void;
ref?: Ref<MduiDialog & HTMLElement>
} }
export type FullDialogProps = React.ComponentPropsWithoutRef<"mdui-dialog"> & BaseDialogProps; export type FullDialogProps = React.ComponentPropsWithoutRef<"mdui-dialog"> & BaseDialogProps;
export function MaterialDialog(props: FullDialogProps) { export function MaterialDialog(props: FullDialogProps) {
const dialogRef = useRef<MduiDialog>(null); const [setDialogRef, dialogRef] = useCombinedRefs(props.ref);
useEffect(() => { useEffect(() => {
const dialog = dialogRef.current; const dialog = dialogRef.current;
@@ -39,5 +41,5 @@ export function MaterialDialog(props: FullDialogProps) {
}; };
}, [dialogRef.current, props.open, props.onOpenChange]); }, [dialogRef.current, props.open, props.onOpenChange]);
return createPortal(<mdui-dialog {...props} ref={dialogRef} />, id("root")); return createPortal(<mdui-dialog {...props} ref={setDialogRef} />, id("root"));
} }
+23 -26
View File
@@ -4,34 +4,31 @@ import { useRef, useCallback, type RefCallback, type Ref } from 'react';
type PossibleRef<T> = Ref<T> | undefined; type PossibleRef<T> = Ref<T> | undefined;
export default function useCombinedRefs<T>(...refs: PossibleRef<T>[]): [RefCallback<T>, React.RefObject<T | null>] { export default function useCombinedRefs<T>(...refs: PossibleRef<T>[]): [RefCallback<T>, React.RefObject<T | null>] {
const targetRef = useRef<T | null>(null); const targetRef = useRef<T | null>(null);
const setRefs = useCallback( const setRefs = useCallback((node: T | null) => {
(node: T | null) => { // Обновляем внутренний ref
// Обновляем внутренний ref targetRef.current = node;
targetRef.current = node;
// Обновляем все переданные refs // Обновляем все переданные refs
refs.forEach((ref) => { refs.forEach((ref) => {
if (!ref) { if (!ref) return;
return;
}
if (typeof ref === 'function') { if (typeof ref === 'function') {
// Если ref - это функция, вызываем её // Если ref - это функция, вызываем её
ref(node); ref(node);
} else { } else {
// Если ref - это объект, обновляем его свойство .current // Если ref - это объект, обновляем его свойство .current
// Используем проверку, чтобы убедиться, что это действительно MutableRefObject // Используем проверку, чтобы убедиться, что это действительно MutableRefObject
// (хотя в реальном коде это почти всегда так) // (хотя в реальном коде это почти всегда так)
ref.current = node; ref.current = node;
} }
}); });
}, },
// Убедитесь, что массив зависимостей всегда актуален // Убедитесь, что массив зависимостей всегда актуален
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[...refs] [...refs]
); );
return [setRefs, targetRef]; return [setRefs, targetRef];
} }