import { createContext, memo, useContext, useMemo, type ComponentPropsWithoutRef } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { Components } from "react-markdown"; import type { PluggableList } from "unified"; import { isMobileViewport } from "../hooks/useViewportMode"; import { linkifyReactChildren } from "../utils/filePathLinkify"; import { sharedRehypePlugins, createMermaidCodeComponent } from "./markdownPipeline"; /* FNXC:Markdown 2026-06-23-03:30: The sanitize schema, rehype plugin chain (rehype-raw -> rehype-sanitize), and the mermaid-aware code component now live in ./markdownPipeline so the task description + summary in TaskDetailModal share the exact same XSS posture. This component consumes those shared exports; see markdownPipeline.tsx for the rationale. */ const mailboxMarkdownComponents: Components = { p: ({ children, ...props }) =>

{linkifyReactChildren(children)}

, li: ({ children, ...props }) =>
  • {linkifyReactChildren(children)}
  • , pre: ({ children, ...props }) => (
          {children}
        
    ), table: ({ children, ...props }) => ( {children}
    ), // Code-block override: fenced ```mermaid renders as a diagram; all other code // keeps default rendering. See createMermaidCodeComponent in markdownPipeline. code: createMermaidCodeComponent("mailbox-mermaid-diagram"), }; const TASK_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]*-\d+$/; const MailboxTaskLinkContext = createContext<((taskId: string) => void) | undefined>(undefined); type MailboxMarkdownAnchorProps = ComponentPropsWithoutRef<"a">; /** * Returns a task id only for same-origin dashboard deep links with a valid task parameter. * * FNXC:MailboxTaskLinks 2026-08-01-07:20: * Mail task links must open in the existing tab; on non-mobile viewports they open the * task detail modal through the shared onOpenTask handler instead of booting another dashboard tab. */ export function parseInAppTaskHref(href: string | undefined): string | null { if (!href || typeof window === "undefined") return null; try { const url = new URL(href, window.location.origin); const taskId = url.searchParams.get("task"); return url.origin === window.location.origin && taskId && TASK_ID_PATTERN.test(taskId) ? taskId : null; } catch { return null; } } function MailboxMarkdownAnchor({ children, ...props }: MailboxMarkdownAnchorProps) { const onOpenTask = useContext(MailboxTaskLinkContext); const taskId = parseInAppTaskHref(props.href); if (!taskId) { /* FNXC:MailboxTaskLinks 2026-08-01-07:34: Only verified same-origin task deep links stay in the dashboard. Markdown sanitization makes all remaining hrefs safe, while external and non-task links retain their established new-tab behavior. */ return {children}; } if (!onOpenTask) return {children}; return ( { if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return; event.preventDefault(); /* FNXC:MailboxTaskLinks 2026-08-01-07:20: Mail task links retain their href for copy and modified-click behavior, but ordinary clicks stay in the existing tab. Both viewport paths call the shared handler: desktop opens the task detail modal and mobile opens its existing full-screen task sheet. */ if (isMobileViewport()) { onOpenTask(taskId); return; } onOpenTask(taskId); }} > {children} ); } const remarkPlugins: PluggableList = [remarkGfm]; interface MailboxMessageContentProps { /** Raw message body. Rendered as GitHub-flavored markdown. */ content: string; /** Optional extra class for the wrapper. */ className?: string; /** Optional data-testid for test selectors. */ testId?: string; /** * FNXC:MailboxTaskLinks 2026-08-01-07:34: * The supplied dashboard handler preserves existing-tab task navigation and selects the appropriate * task-detail surface for the current viewport. */ onOpenTask?: (taskId: string) => void; } /** * Renders a mailbox message body as GitHub-flavored markdown. * * Supports embedded raw HTML (details/summary/kbd/sub/tables) via rehype-raw, with * rehype-sanitize stripping XSS (script/style/iframe/event-handlers/javascript:). * Fenced ```mermaid blocks render as diagrams via the lazy-loaded MermaidDiagram. * HTML comments (``) are dropped and never rendered. * * Memoized because mailbox detail panes can re-render on selection / SSE * updates while the underlying message body is unchanged. */ export const MailboxMessageContent = memo(function MailboxMessageContent({ content, className, testId, onOpenTask, }: MailboxMessageContentProps) { const markdownComponents = useMemo( () => ({ ...mailboxMarkdownComponents, a: MailboxMarkdownAnchor }), [onOpenTask], ); const wrapperClass = className ? `mailbox-markdown ${className}` : "mailbox-markdown"; return (
    {content}
    ); });