From b08f40feb701c972d95e707ffd6655af6622a82e Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sun, 15 Mar 2026 02:48:07 +0100 Subject: [PATCH] feat: inline delete confirmation on message hover bar Click trash icon to arm (morphs to green checkmark), click again to confirm. Auto-cancels after 3s or 2s after mouse leaves. Uses CSS scale+opacity transitions for a smooth icon swap animation. --- packages/web/src/components/chat/Message.tsx | 78 ++++++++++++++------ 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/packages/web/src/components/chat/Message.tsx b/packages/web/src/components/chat/Message.tsx index 76e30502..0d369611 100644 --- a/packages/web/src/components/chat/Message.tsx +++ b/packages/web/src/components/chat/Message.tsx @@ -51,7 +51,9 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) { const [isEditing, setIsEditing] = useState(false); const [editContent, setEditContent] = useState(message.content ?? ''); const [isHovered, setIsHovered] = useState(false); + const [confirmingDelete, setConfirmingDelete] = useState(false); const [showReactionPicker, setShowReactionPicker] = useState(false); + const confirmDeleteTimeout = useRef>(); const reactionPickerBtnRef = useRef(null); const reactionPickerRef = useRef(null); const currentUser = useAuthStore((s) => s.user); @@ -96,9 +98,23 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) { return acc; }, {} as Record); + // Auto-cancel delete confirmation after timeout + const startDeleteConfirm = useCallback(() => { + setConfirmingDelete(true); + clearTimeout(confirmDeleteTimeout.current); + confirmDeleteTimeout.current = setTimeout(() => setConfirmingDelete(false), 3000); + }, []); + + const cancelDeleteConfirm = useCallback(() => { + setConfirmingDelete(false); + clearTimeout(confirmDeleteTimeout.current); + }, []); + + useEffect(() => { + return () => clearTimeout(confirmDeleteTimeout.current); + }, []); + const isGifOnly = isGifOnlyMessage(message.content); - const isSticker = !!(message.stickerId || (message as any).sticker); - const stickerData = (message as any).sticker ?? null; const urlRegex = /(https?:\/\/[^\s]+)/g; const firstUrl = isGifOnly ? null : message.content?.match(urlRegex)?.[0]; @@ -206,7 +222,13 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) { : 'hover:bg-[rgba(255,255,255,0.025)]' }`} onMouseEnter={() => setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} + onMouseLeave={() => { + setIsHovered(false); + if (confirmingDelete) { + clearTimeout(confirmDeleteTimeout.current); + confirmDeleteTimeout.current = setTimeout(() => setConfirmingDelete(false), 2000); + } + }} > {/* Reply Line */} {message.replyTo && ( @@ -289,19 +311,7 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) { ) : (
- {/* Sticker rendering */} - {isSticker && stickerData ? ( -
- {stickerData.name} -
- ) : isSticker ? ( -
Sticker unavailable
- ) : isGifOnly ? ( + {isGifOnly ? (
{canAddReactions && (
@@ -469,13 +479,39 @@ export function Message({ message, isCompact, isFirstInGroup }: MessageProps) { )} {canDelete && ( )}