diff --git a/apps/frontend/src/renderer/components/AppUpdateNotification.tsx b/apps/frontend/src/renderer/components/AppUpdateNotification.tsx index 7bd00f30..535f919e 100644 --- a/apps/frontend/src/renderer/components/AppUpdateNotification.tsx +++ b/apps/frontend/src/renderer/components/AppUpdateNotification.tsx @@ -1,6 +1,8 @@ import { useState, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Download, RefreshCw, CheckCircle2, AlertCircle, ExternalLink } from "lucide-react"; +import ReactMarkdown, { type Components } from "react-markdown"; +import remarkGfm from "remark-gfm"; import { Button } from "./ui/button"; import { Progress } from "./ui/progress"; import { @@ -16,58 +18,52 @@ import type { AppUpdateAvailableEvent, AppUpdateProgress } from "../../shared/ty const CLAUDE_CODE_CHANGELOG_URL = "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md"; -/** - * Simple markdown renderer for release notes - * Handles: headers, bold, lists, line breaks - */ -function ReleaseNotesRenderer({ markdown }: { markdown: string }) { - const html = useMemo(() => { - const result = markdown - // Escape HTML - .replace(/&/g, "&") - .replace(//g, ">") - // Headers (### Header ->
)
- .replace(/`([^`]+)`/g, '$1')
- // List items (- item -> )
- .replace(
- /^- (.+)$/gm,
- " $1 "
- )
- // Wrap consecutive list items
- .replace(/(]*>.*?<\/li>\n?)+/g, '$&
')
- // Line breaks for remaining lines
- .replace(/\n\n/g, '')
- .replace(/\n/g, "
");
+// createSafeLink - factory function that creates a SafeLink component with i18n support
+const createSafeLink = (opensInNewWindowText: string) => {
+ return function SafeLink({
+ href,
+ children,
+ ...props
+ }: React.AnchorHTMLAttributes) {
+ // Validate URL - only allow http, https, and relative links
+ const isValidUrl =
+ href &&
+ (href.startsWith("http://") ||
+ href.startsWith("https://") ||
+ href.startsWith("/") ||
+ href.startsWith("#"));
- return result;
- }, [markdown]);
+ if (!isValidUrl) {
+ // For invalid or potentially malicious URLs, render as plain text
+ return {children};
+ }
- return (
-
- );
-}
+ // External links get security attributes and accessibility indicator
+ const isExternal = href?.startsWith("http://") || href?.startsWith("https://");
+
+ return (
+
+ {children}
+ {isExternal && {opensInNewWindowText}}
+
+ );
+ };
+};
/**
* App Update Notification Dialog
* Shows when a new app version is available and handles download/install workflow
*/
export function AppUpdateNotification() {
- const { t } = useTranslation(["dialogs"]);
+ const { t } = useTranslation(["dialogs", "common"]);
const [isOpen, setIsOpen] = useState(false);
const [updateInfo, setUpdateInfo] = useState(null);
const [downloadProgress, setDownloadProgress] = useState(null);
@@ -75,6 +71,14 @@ export function AppUpdateNotification() {
const [isDownloaded, setIsDownloaded] = useState(false);
const [downloadError, setDownloadError] = useState(null);
+ // Create markdown components with translated accessibility text
+ const markdownComponents: Components = useMemo(
+ () => ({
+ a: createSafeLink(t("common:accessibility.opensInNewWindow")),
+ }),
+ [t]
+ );
+
// Listen for update available event
useEffect(() => {
const cleanup = window.electronAPI.onAppUpdateAvailable((info) => {
@@ -184,7 +188,11 @@ export function AppUpdateNotification() {
{/* Release Notes */}
{updateInfo.releaseNotes && (
-
+
+
+ {updateInfo.releaseNotes}
+
+
)}