diff --git a/app/mobile/tickets/[id]/page.tsx b/app/mobile/tickets/[id]/page.tsx index 0911096..2b1c1c8 100644 --- a/app/mobile/tickets/[id]/page.tsx +++ b/app/mobile/tickets/[id]/page.tsx @@ -53,8 +53,42 @@ function fmtHours(h: string | number) { if (n < 1) return `${Math.round(n * 60)}m`; return `${n.toFixed(1)}h`; } -function stripHtml(s: string) { - return s?.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim() ?? ''; +function renderContent(s: string): React.ReactNode[] { + if (!s) return []; + + // Step 1: convert ... → placeholder §URL§ + // Collect links in order + const links: string[] = []; + const withPlaceholders = s.replace(/]*\bhref=["']([^"']+)["'][^>]*>.*?<\/a>/gi, (_, href) => { + links.push(href); + return `\x00LINK${links.length - 1}\x00`; + }); + + // Step 2: strip remaining HTML tags + const plain = withPlaceholders.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); + + // Step 3: split on LINK placeholders and bare URLs + const URL_RE = /\x00LINK(\d+)\x00|(https?:\/\/[^\s<>"']+)/g; + const nodes: React.ReactNode[] = []; + let last = 0; + let match: RegExpExecArray | null; + let key = 0; + + while ((match = URL_RE.exec(plain)) !== null) { + if (match.index > last) { + nodes.push(plain.slice(last, match.index)); + } + const href = match[1] !== undefined ? links[parseInt(match[1])] : match[2]; + nodes.push( + e.stopPropagation()}> + [link] + + ); + last = match.index + match[0].length; + } + if (last < plain.length) nodes.push(plain.slice(last)); + return nodes; } function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defaultOpen?: boolean }) { @@ -118,7 +152,7 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau
{stripHtml(te.notes)}
} + {te.notes &&{renderContent(te.notes)}
} )} @@ -130,7 +164,8 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau const note = item.data; const isInternal = note.publish === 1; const isSystem = item.is_system; - const body = stripHtml(note.description ?? ''); + const bodyNodes = renderContent(note.description ?? ''); + const bodyPlain = (note.description ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); const iconColor = isSystem ? 'text-muted-foreground' : isInternal ? 'text-slate-400' : 'text-amber-500'; const iconBg = isSystem ? 'bg-muted' : isInternal ? 'bg-muted' : 'bg-amber-500/10'; return ( @@ -149,7 +184,7 @@ function TimelineCard({ item, defaultOpen = false }: { item: TimelineItem; defau{note.title || (isInternal ? 'Internal Note' : 'Customer Note')}
- {!open && body &&{body}
} + {!open && bodyPlain &&{bodyPlain}
} {open ?
{body}
+{bodyNodes}
)}