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 { 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<number | null>(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}
/>
<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 */}
{contextMenu.message && (
@@ -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 && (
<div
className={`context-menu ${animationClass}`}
style={{
@@ -154,8 +152,7 @@ export function MessageContextMenu({
left: calculatedPosition.x,
zIndex: 1000
}}
onClick={(e) => e.stopPropagation()}
>
onClick={(e) => e.stopPropagation()}>
<div className="context-menu-item" onClick={() => handleAction("reply")}>
<span className="material-symbols">reply</span>
Ответить
@@ -174,13 +171,4 @@ export function MessageContextMenu({
)}
</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 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<MduiDialog & HTMLElement>
}
export type FullDialogProps = React.ComponentPropsWithoutRef<"mdui-dialog"> & BaseDialogProps;
export function MaterialDialog(props: FullDialogProps) {
const dialogRef = useRef<MduiDialog>(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(<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;
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(
(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];
}