189 lines
7.8 KiB
TypeScript
189 lines
7.8 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import {
|
|
Copy,
|
|
Check,
|
|
Mail,
|
|
MessageSquare,
|
|
Code2,
|
|
Eye,
|
|
ShieldCheck,
|
|
} from 'lucide-react';
|
|
import { NotificationPlatform } from '../../types';
|
|
import { parseMarkdownToHtml, isHtmlContent, stripMarkdown } from '../../lib/markdownUtils';
|
|
|
|
export interface MessageViewerProps {
|
|
content: string;
|
|
platform?: NotificationPlatform | number | string;
|
|
subject?: string;
|
|
className?: string;
|
|
showFormatHeader?: boolean;
|
|
maxHeightClass?: string;
|
|
}
|
|
|
|
export const MessageViewer: React.FC<MessageViewerProps> = ({
|
|
content,
|
|
platform,
|
|
subject,
|
|
className = '',
|
|
showFormatHeader = true,
|
|
maxHeightClass = 'max-h-[60vh]',
|
|
}) => {
|
|
const [copied, setCopied] = useState(false);
|
|
const [showRaw, setShowRaw] = useState(false);
|
|
|
|
// Determine whether this message represents an Email (HTML) or In-App (Markup)
|
|
const isEmail = useMemo(() => {
|
|
if (
|
|
platform === NotificationPlatform.Email ||
|
|
platform === 1 ||
|
|
platform === '1' ||
|
|
String(platform).toLowerCase() === 'email'
|
|
) {
|
|
return true;
|
|
}
|
|
// If platform is not explicitly InApp, detect if content is structured HTML
|
|
if (
|
|
platform !== NotificationPlatform.InApp &&
|
|
platform !== 5 &&
|
|
platform !== '5' &&
|
|
String(platform).toLowerCase() !== 'inapp' &&
|
|
isHtmlContent(content)
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}, [platform, content]);
|
|
|
|
const isInApp = useMemo(() => {
|
|
if (
|
|
platform === NotificationPlatform.InApp ||
|
|
platform === 5 ||
|
|
platform === '5' ||
|
|
String(platform).toLowerCase() === 'inapp'
|
|
) {
|
|
return true;
|
|
}
|
|
return !isEmail;
|
|
}, [platform, isEmail]);
|
|
|
|
// Convert the message to its respective format:
|
|
// - Email: HTML rendered
|
|
// - In-App: Markup (Markdown) parsed to HTML
|
|
const renderedHtml = useMemo(() => {
|
|
if (!content) return '<p class="text-slate-400 italic">No message content</p>';
|
|
|
|
// Format 1: Email => HTML
|
|
if (isEmail) {
|
|
if (isHtmlContent(content)) {
|
|
return content.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
|
}
|
|
return parseMarkdownToHtml(content);
|
|
}
|
|
|
|
// Format 2: In-App => Markup (Markdown)
|
|
return parseMarkdownToHtml(content);
|
|
}, [content, isEmail]);
|
|
|
|
const handleCopy = () => {
|
|
const textToCopy = showRaw ? content : stripMarkdown(content) || content;
|
|
navigator.clipboard.writeText(textToCopy);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<div className={`flex flex-col rounded-2xl bg-slate-900/50 border border-white/10 overflow-hidden w-full max-w-full ${className}`}>
|
|
{/* Format Header & Controls Bar */}
|
|
{showFormatHeader && (
|
|
<div className="px-4 py-2.5 border-b border-white/[0.08] bg-[#0b101d]/90 flex items-center justify-between text-xs text-slate-400 gap-3 shrink-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
{isEmail ? (
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-cyan-500/10 border border-cyan-500/20 text-cyan-300 font-semibold text-[11px]">
|
|
<Mail className="w-3.5 h-3.5 text-cyan-400" />
|
|
<span>Email (HTML)</span>
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-emerald-500/10 border border-emerald-500/20 text-emerald-300 font-semibold text-[11px]">
|
|
<MessageSquare className="w-3.5 h-3.5 text-emerald-400" />
|
|
<span>In-App (Markup)</span>
|
|
</span>
|
|
)}
|
|
|
|
<span className="text-[11px] text-slate-400 hidden sm:inline">
|
|
{isEmail ? 'Rendered as HTML Email' : 'Converted from Markup to Formatted View'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 shrink-0">
|
|
{/* View Mode Toggle: Formatted vs Raw */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowRaw(!showRaw)}
|
|
className="flex items-center gap-1 px-2 py-1 rounded-lg bg-slate-800/80 hover:bg-slate-700 text-slate-300 hover:text-white text-[11px] font-medium transition cursor-pointer border border-white/10"
|
|
title={showRaw ? 'Switch to Formatted view' : 'View Raw message content'}
|
|
>
|
|
{showRaw ? (
|
|
<>
|
|
<Eye className="w-3 h-3 text-emerald-400" />
|
|
<span>Formatted</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Code2 className="w-3 h-3" />
|
|
<span>Raw</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{/* Copy Button */}
|
|
<button
|
|
type="button"
|
|
onClick={handleCopy}
|
|
className="flex items-center gap-1 px-2 py-1 rounded-lg bg-slate-800/80 hover:bg-slate-700 text-slate-300 hover:text-white text-[11px] font-medium transition cursor-pointer border border-white/10"
|
|
title="Copy message content"
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<Check className="w-3 h-3 text-emerald-400" />
|
|
<span className="text-emerald-300">Copied</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="w-3 h-3" />
|
|
<span>Copy</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Message Body Content - with explicit max height, max width, word wrapping, and automatic scrollbars */}
|
|
<div className={`w-full max-w-full p-4 sm:p-6 overflow-x-auto overflow-y-auto ${maxHeightClass} text-sm leading-relaxed text-slate-200 [scrollbar-width:thin] [scrollbar-color:#334155_transparent]`}>
|
|
{showRaw ? (
|
|
<pre className="font-mono text-xs text-slate-300 whitespace-pre-wrap break-words [word-break:break-word] bg-[#080d18] p-4 rounded-xl border border-white/5 max-w-full overflow-x-auto [scrollbar-width:thin] [scrollbar-color:#334155_transparent]">
|
|
{content}
|
|
</pre>
|
|
) : (
|
|
<div
|
|
className="prose prose-invert max-w-full w-full break-words [word-break:break-word] text-xs sm:text-sm leading-relaxed text-slate-200 [&_h1]:text-white [&_h1]:text-xl [&_h1]:font-bold [&_h1]:mb-3 [&_h2]:text-white [&_h2]:text-lg [&_h2]:font-bold [&_h2]:mb-2 [&_h3]:text-white [&_h3]:text-base [&_h3]:font-bold [&_h3]:mb-2 [&_h4]:text-white [&_h4]:font-semibold [&_a]:text-emerald-400 [&_a:hover]:underline [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:my-2 [&_li]:text-slate-300 [&_table]:w-full [&_table]:max-w-full [&_table]:border-collapse [&_table]:my-3 [&_th]:border [&_th]:border-slate-700 [&_th]:bg-slate-800/80 [&_th]:p-2.5 [&_th]:break-words [&_td]:border [&_td]:border-slate-800 [&_td]:p-2.5 [&_td]:break-words [&_blockquote]:border-l-4 [&_blockquote]:border-emerald-500 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:my-2 [&_code]:bg-slate-800 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-emerald-300 [&_code]:break-all [&_pre]:max-w-full [&_pre]:overflow-x-auto [&_img]:max-w-full [&_img]:h-auto [&_iframe]:max-w-full"
|
|
dangerouslySetInnerHTML={{ __html: renderedHtml }}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer Info Stamp */}
|
|
<div className="px-5 py-2.5 border-t border-white/[0.08] bg-[#090e1a] flex items-center justify-between text-[11px] text-slate-400 shrink-0">
|
|
<div className="flex items-center gap-1.5">
|
|
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
|
|
<span>PFM Communications • {isEmail ? 'Email HTML Format' : 'In-App Markup Format'}</span>
|
|
</div>
|
|
<span className="font-mono text-[10px] text-slate-400">
|
|
{stripMarkdown(content).length} characters
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|