From 3d0ece76117b8bfa6cd89916fa98360d0e84490c Mon Sep 17 00:00:00 2001 From: Zane <75694352+zanesq@users.noreply.github.com> Date: Thu, 19 Feb 2026 07:06:18 -0800 Subject: [PATCH] Improve link confirmation modal (#7333) --- ui/desktop/src/components/MarkdownContent.tsx | 166 ++++++++++-------- .../src/components/ui/ConfirmationModal.tsx | 25 ++- 2 files changed, 116 insertions(+), 75 deletions(-) diff --git a/ui/desktop/src/components/MarkdownContent.tsx b/ui/desktop/src/components/MarkdownContent.tsx index eea331ab78..fd4e68284f 100644 --- a/ui/desktop/src/components/MarkdownContent.tsx +++ b/ui/desktop/src/components/MarkdownContent.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, memo, useMemo } from 'react'; +import React, { useState, useEffect, useRef, memo, useMemo, useCallback } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import remarkBreaks from 'remark-breaks'; @@ -29,6 +29,7 @@ const customOneDarkTheme = { import { Check, Copy } from './icons'; import { wrapHTMLInCodeBlock } from '../utils/htmlSecurity'; import { isProtocolSafe, getProtocol, BLOCKED_PROTOCOLS } from '../utils/urlSecurity'; +import { ConfirmationModal } from './ui/ConfirmationModal'; interface CodeProps extends React.ClassAttributes, React.HTMLAttributes { inline?: boolean; @@ -164,6 +165,7 @@ const MarkdownContent = memo(function MarkdownContent({ className = '', }: MarkdownContentProps) { const [processedContent, setProcessedContent] = useState(content); + const [pendingLink, setPendingLink] = useState<{ protocol: string; href: string } | null>(null); useEffect(() => { try { @@ -175,78 +177,100 @@ const MarkdownContent = memo(function MarkdownContent({ } }, [content]); + const handleConfirmOpen = useCallback(async () => { + if (pendingLink) { + try { + await window.electron.openExternal(pendingLink.href); + } catch { + await window.electron.showMessageBox({ + type: 'error', + buttons: ['OK'], + title: 'Failed to Open Link', + message: `No application found to open this link.`, + detail: pendingLink.href, + }); + } + } + setPendingLink(null); + }, [pendingLink]); + + const handleCancelOpen = useCallback(() => { + setPendingLink(null); + }, []); + return ( -
- { - return ( - { - e.preventDefault(); - e.stopPropagation(); - if (!props.href) return; - - if (isProtocolSafe(props.href)) { - window.electron.openExternal(props.href); - } else { - const protocol = getProtocol(props.href); - if (!protocol) return; - - const result = await window.electron.showMessageBox({ - type: 'question', - buttons: ['Cancel', 'Open'], - defaultId: 0, - title: 'Open External Link', - message: `Open ${protocol} link?`, - detail: `This will open: ${props.href}`, - }); - if (result.response === 1) { - window.electron.openExternal(props.href); - } - } - }} - /> - ); - }, - code: MarkdownCode, - }} + <> +
- {processedContent} - -
+ { + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + if (!props.href) return; + + if (isProtocolSafe(props.href)) { + window.electron.openExternal(props.href); + } else { + const protocol = getProtocol(props.href); + if (!protocol) return; + setPendingLink({ protocol, href: props.href }); + } + }} + /> + ); + }, + code: MarkdownCode, + }} + > + {processedContent} +
+
+ + ); }); diff --git a/ui/desktop/src/components/ui/ConfirmationModal.tsx b/ui/desktop/src/components/ui/ConfirmationModal.tsx index e3add65621..dd1fa7f67f 100644 --- a/ui/desktop/src/components/ui/ConfirmationModal.tsx +++ b/ui/desktop/src/components/ui/ConfirmationModal.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Dialog, DialogContent, @@ -12,6 +13,7 @@ export function ConfirmationModal({ isOpen, title, message, + detail, onConfirm, onCancel, confirmLabel = 'Yes', @@ -22,6 +24,7 @@ export function ConfirmationModal({ isOpen: boolean; title: string; message: string; + detail?: React.ReactNode; onConfirm: () => void; onCancel: () => void; confirmLabel?: string; @@ -31,17 +34,31 @@ export function ConfirmationModal({ }) { return ( !open && onCancel()}> - + {title} {message} - - -