Files
pfm/PostFundManagement.PartnerUI/src/components/notifications/MessageViewer.tsx
T

117 lines
4.9 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { Copy, Check } from 'lucide-react';
import { NotificationPlatform } from '../../types';
import { parseMarkdownToHtml, isHtmlContent, stripMarkdown } from '../../lib/markdownUtils';
export interface MessageViewerProps {
content?: string;
message?: string;
platform?: NotificationPlatform | number | string;
subject?: string;
className?: string;
showFormatHeader?: boolean;
maxHeightClass?: string;
variant?: string;
createdDate?: string;
}
export const MessageViewer: React.FC<MessageViewerProps> = ({
content,
message,
platform,
subject,
className = '',
maxHeightClass = 'max-h-[65vh]',
}) => {
const rawContent = (content !== undefined && content !== null ? content : message) ?? '';
const [copied, setCopied] = useState(false);
// Determine whether this message represents an Email (HTML) or In-App (Markdown)
const isEmail = useMemo(() => {
if (
platform === NotificationPlatform.Email ||
platform === 1 ||
platform === '1' ||
String(platform).toLowerCase() === 'email'
) {
return true;
}
if (
platform !== NotificationPlatform.InApp &&
platform !== 5 &&
platform !== '5' &&
String(platform).toLowerCase() !== 'inapp' &&
isHtmlContent(rawContent)
) {
return true;
}
return false;
}, [platform, rawContent]);
// Requirement #1: In-App message contents encoded as markdown, rendered for user; Email in HTML
// Requirement #8: User is none the wiser of raw form or format in any Messages UI
const renderedHtml = useMemo(() => {
if (!rawContent || !rawContent.trim()) {
return '<p class="text-slate-400 italic">No message content</p>';
}
if (isEmail) {
if (isHtmlContent(rawContent)) {
return rawContent.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
}
return parseMarkdownToHtml(rawContent);
}
// In-App: parsed from Markdown to styled HTML
return parseMarkdownToHtml(rawContent);
}, [rawContent, isEmail]);
const handleCopy = () => {
const textToCopy = stripMarkdown(rawContent) || rawContent;
navigator.clipboard.writeText(textToCopy);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className={`flex flex-col rounded-2xl bg-slate-900/60 border border-white/10 overflow-hidden w-full max-w-full ${className}`}>
{/* Top action toolbar - clean without technical format metadata */}
<div className="px-5 py-3 border-b border-white/[0.08] bg-[#0b101d]/90 flex items-center justify-between text-xs text-slate-400 gap-3 shrink-0">
<span className="text-xs font-semibold text-slate-200 truncate">
{subject || 'Message Content'}
</span>
{/* Copy Clean Text */}
<button
type="button"
onClick={handleCopy}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-800/90 hover:bg-slate-700 text-slate-300 hover:text-white text-xs font-medium transition cursor-pointer border border-white/10"
title="Copy message content"
>
{copied ? (
<>
<Check className="w-3.5 h-3.5 text-emerald-400" />
<span className="text-emerald-300">Copied</span>
</>
) : (
<>
<Copy className="w-3.5 h-3.5 text-slate-400" />
<span>Copy</span>
</>
)}
</button>
</div>
{/* Rendered Body - Prose & Markdown styling */}
<div
className={`w-full max-w-full p-5 sm:p-7 overflow-x-auto overflow-y-auto ${maxHeightClass} text-sm leading-relaxed text-slate-200 [scrollbar-width:thin] [scrollbar-color:#334155_transparent]`}
>
<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>
</div>
);
};