import React, { useState, useEffect, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { Inbox, Send, FileText, Star, Trash2, Mail, MailOpen, Search, PenSquare, RefreshCw, Reply, CheckCircle2, Clock, RotateCcw, X, Smartphone, ShieldAlert, } from 'lucide-react'; import { useInAppNotifications, DraftMessage } from '../hooks/useInAppNotifications'; import { ComposeMessageModal } from '../components/notifications/ComposeMessageModal'; import { MarkdownViewer } from '../components/notifications/MarkdownViewer'; import { formatDate } from '../lib/formatters'; import { Notification, NotificationPlatform, NotificationStatus, User } from '../types'; import { useAuth } from '../auth/AuthProvider'; import { stripMarkdown } from '../lib/markdownUtils'; import { getUsers } from '../services/api'; type MailboxFolder = 'inbox' | 'starred' | 'sent' | 'drafts' | 'trash'; export const InboxPage: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); const targetIdParam = searchParams.get('id'); const { user, claims } = useAuth(); const currentUserId = String(user?.id || claims?.sub || '').trim().toLowerCase(); const currentUserEmail = (user?.email || claims?.email || '') as string; const { data: directoryUsers = [] } = useQuery({ queryKey: ['users-directory'], queryFn: () => getUsers(1, 200), }); const { allMailboxNotifications, inboxNotifications, unreadCount, starredNotifications, trashedNotifications, sentNotifications, drafts, starredIds, isLoading, refetch, markAsRead, toggleStar, deleteMessage, restoreMessage, permanentDelete, deleteDraft, sendMessage, isSending, } = useInAppNotifications(); // Active folder const [activeFolder, setActiveFolder] = useState('inbox'); // Search query const [searchTerm, setSearchTerm] = useState(''); // Filter for unread only in Inbox const [showUnreadOnly, setShowUnreadOnly] = useState(false); // Selected message to view in detail const [selectedMessage, setSelectedMessage] = useState(null); // Selected draft to edit/resume const [selectedDraft, setSelectedDraft] = useState(null); // Compose modal state const [isComposeOpen, setIsComposeOpen] = useState(false); const [composeInitialRecipient, setComposeInitialRecipient] = useState(undefined); const [composeInitialSubject, setComposeInitialSubject] = useState(''); const [composeInitialMessage, setComposeInitialMessage] = useState(''); const [composeInitialPlatform, setComposeInitialPlatform] = useState(NotificationPlatform.InApp); // Inline quick reply state const [inlineReplyText, setInlineReplyText] = useState(''); const [inlineReplySuccess, setInlineReplySuccess] = useState(false); // Self-reply prevention modal state const [isSelfReplyModalOpen, setIsSelfReplyModalOpen] = useState(false); // Active folder message items (always In-App notifications) const folderMessages = useMemo(() => { switch (activeFolder) { case 'inbox': return inboxNotifications; case 'starred': return starredNotifications; case 'sent': return sentNotifications; case 'trash': return trashedNotifications; case 'drafts': return []; default: return inboxNotifications; } }, [activeFolder, inboxNotifications, starredNotifications, sentNotifications, trashedNotifications]); // Filtered by search and unread toggle const filteredMessages = useMemo(() => { return folderMessages.filter(item => { // Unread only filter if (activeFolder === 'inbox' && showUnreadOnly) { if (Number(item.status) === NotificationStatus.Read) return false; } // Search term if (!searchTerm.trim()) return true; const term = searchTerm.toLowerCase(); const subject = (item.subject || '').toLowerCase(); const message = (item.message || '').toLowerCase(); const dest = (item.destination || '').toLowerCase(); return subject.includes(term) || message.includes(term) || dest.includes(term); }); }, [folderMessages, activeFolder, showUnreadOnly, searchTerm]); // Filtered drafts for Drafts folder const filteredDrafts = useMemo(() => { if (activeFolder !== 'drafts') return []; if (!searchTerm.trim()) return drafts; const term = searchTerm.toLowerCase(); return drafts.filter( d => d.subject.toLowerCase().includes(term) || d.message.toLowerCase().includes(term) || d.recipientEmail.toLowerCase().includes(term) ); }, [drafts, activeFolder, searchTerm]); // Automatically select message when URL param ?id=... is present useEffect(() => { if (targetIdParam) { const found = folderMessages.find(m => String(m.id) === targetIdParam); if (found) { setSelectedMessage(found); if (Number(found.status) !== NotificationStatus.Read) { markAsRead(found.id); } } } else if (!selectedMessage && folderMessages.length > 0 && activeFolder !== 'drafts') { setSelectedMessage(folderMessages[0]); } }, [targetIdParam, folderMessages, activeFolder]); // Handle message selection const handleSelectMessage = (item: Notification) => { setSelectedMessage(item); setSearchParams({ id: String(item.id) }); if (Number(item.status) !== NotificationStatus.Read) { markAsRead(item.id); } setInlineReplyText(''); setInlineReplySuccess(false); }; // Open Compose Modal for a new message const handleOpenCompose = (defaultPlatform: NotificationPlatform = NotificationPlatform.InApp) => { setSelectedDraft(null); setComposeInitialRecipient(undefined); setComposeInitialSubject(''); setComposeInitialMessage(''); setComposeInitialPlatform(defaultPlatform); setIsComposeOpen(true); }; const extractSenderEmail = (msg: Notification): string => { const rawDest = msg.destination || ''; if (rawDest.includes('From:')) { const fromPart = rawDest.split('From:')[1]?.split('|')[0]?.trim() || ''; return fromPart.replace(/[<>]/g, '').trim(); } if (!rawDest.includes('To:') && rawDest.includes('@')) { return rawDest.trim(); } return ''; }; const isNotificationFromSelf = (msg: Notification): boolean => { if (activeFolder === 'sent' || String(msg.id).startsWith('sent_')) { return true; } const senderEmail = extractSenderEmail(msg).toLowerCase(); const myEmail = currentUserEmail.trim().toLowerCase(); if (myEmail && senderEmail && senderEmail === myEmail) { return true; } if (!senderEmail && String(msg.recipient || '').trim().toLowerCase() === currentUserId) { return true; } return false; }; // Open Compose Modal for a Reply const handleReplyMessage = (msg: Notification) => { if (isNotificationFromSelf(msg)) { setIsSelfReplyModalOpen(true); return; } const replyEmail = extractSenderEmail(msg); const matchedSender = replyEmail ? directoryUsers.find(u => (u.email || '').trim().toLowerCase() === replyEmail.toLowerCase()) : undefined; if ( (matchedSender && String(matchedSender.id).trim().toLowerCase() === currentUserId) || (replyEmail && replyEmail.toLowerCase() === currentUserEmail.trim().toLowerCase()) ) { setIsSelfReplyModalOpen(true); return; } const replySubject = msg.subject?.startsWith('Re:') ? msg.subject : `Re: ${msg.subject || 'Communication'}`; const quotedBody = `\n\n--- Original Message (${formatDate(msg.createdAt)}) ---\n> ${stripMarkdown(msg.message || '')}`; setSelectedDraft(null); setComposeInitialRecipient( replyEmail && matchedSender ? { id: String(matchedSender.id), email: replyEmail, name: replyEmail.split('@')[0], } : undefined ); setComposeInitialSubject(replySubject); setComposeInitialMessage(quotedBody); setComposeInitialPlatform(msg.platform as NotificationPlatform || NotificationPlatform.InApp); setIsComposeOpen(true); }; // Resume editing a saved draft const handleEditDraft = (draft: DraftMessage) => { setSelectedDraft(draft); setComposeInitialRecipient( draft.recipientId ? { id: draft.recipientId, email: draft.recipientEmail, name: draft.recipientName, } : undefined ); setComposeInitialSubject(draft.subject); setComposeInitialMessage(draft.message); setComposeInitialPlatform(draft.platform || NotificationPlatform.InApp); setIsComposeOpen(true); }; // Send inline quick reply const handleSendInlineReply = async () => { if (!selectedMessage || !inlineReplyText.trim()) return; if (isNotificationFromSelf(selectedMessage)) { setIsSelfReplyModalOpen(true); return; } const replyEmail = extractSenderEmail(selectedMessage); const matchedSender = replyEmail ? directoryUsers.find(u => (u.email || '').trim().toLowerCase() === replyEmail.toLowerCase()) : undefined; const targetRecipientId = matchedSender ? String(matchedSender.id) : String(selectedMessage.recipient); if ( targetRecipientId.trim().toLowerCase() === currentUserId || (replyEmail && replyEmail.toLowerCase() === currentUserEmail.trim().toLowerCase()) ) { setIsSelfReplyModalOpen(true); return; } try { await sendMessage({ recipientId: targetRecipientId, recipientEmail: replyEmail || 'team@dsi.gov.za', subject: selectedMessage.subject?.startsWith('Re:') ? selectedMessage.subject : `Re: ${selectedMessage.subject || 'Notification'}`, message: inlineReplyText.trim(), platform: selectedMessage.platform as NotificationPlatform || NotificationPlatform.InApp, }); setInlineReplySuccess(true); setInlineReplyText(''); setTimeout(() => { setInlineReplySuccess(false); }, 3000); } catch (err) { console.error('Failed to send inline reply:', err); } }; return (
{/* Top Mailbox Breadcrumb & Action Banner */}

Mailbox & Communications

In-App Messaging

In-App notifications and internal platform communications

{/* Main Mailbox Three-Pane Layout */}
{/* ======================================================== */} {/* PANE 1: Mailbox Folders Sidebar */} {/* ======================================================== */} {/* ======================================================== */} {/* PANE 2: Message List / Summary Cards */} {/* ======================================================== */}
{/* Search Box Header */}
setSearchTerm(e.target.value)} placeholder="Search messages by subject, sender, or text..." className="w-full pl-8 pr-7 py-1.5 rounded-xl bg-slate-900 border border-slate-700/80 text-xs text-slate-100 placeholder-slate-400 focus:border-blue-500 focus:outline-none transition shadow-inner" /> {searchTerm && ( )}
{activeFolder} ({activeFolder === 'drafts' ? filteredDrafts.length : filteredMessages.length}) {activeFolder === 'inbox' && unreadCount > 0 && ( {unreadCount} unread )}
{/* List of Messages or Drafts */}
{activeFolder === 'drafts' ? ( filteredDrafts.length === 0 ? (

No Drafts Saved

When you save unfinished messages, they will appear here.

) : ( filteredDrafts.map(d => (
handleEditDraft(d)} className="p-3.5 hover:bg-slate-800/60 transition cursor-pointer group flex flex-col gap-1.5 relative" >
Draft {formatDate(d.updatedAt)}

{d.subject || 'Untitled Draft'}

{stripMarkdown(d.message || 'No content written yet.')}

To: {d.recipientEmail || 'No recipient selected'}
)) ) ) : filteredMessages.length === 0 ? (

No Messages Found

{searchTerm ? `No messages matched "${searchTerm}"` : `No items in ${activeFolder}`}

) : ( filteredMessages.map(item => { const isSelected = selectedMessage?.id === item.id; const isUnread = Number(item.status) !== NotificationStatus.Read; const isStarred = starredIds.includes(String(item.id)); return (
handleSelectMessage(item)} className={`p-3.5 transition cursor-pointer flex flex-col gap-1 relative ${ isSelected ? 'bg-blue-600/15 border-l-4 border-l-blue-500 text-white shadow-inner' : 'hover:bg-slate-800/50 border-l-4 border-l-transparent' }`} > {/* Top line: unread dot, platform badge, star, time */}
{isUnread && ( )} {item.destination?.includes('From:') ? item.destination.split('From:')[1]?.trim().replace(/[<>]/g, '') : item.destination || 'PFM System'}
In-App {formatDate(item.createdAt)}
{/* Subject */}

{item.subject || 'Statutory Platform Notice'}

{/* Snippet preview stripped of markdown syntax */}

{stripMarkdown(item.message || 'No additional details provided.')}

); }) )}
{/* ======================================================== */} {/* PANE 3: Full Message Detail & Interactive Actions */} {/* ======================================================== */}
{activeFolder === 'drafts' ? (

Drafts Folder

Click any draft on the left pane to resume composing and dispatch your notification.

) : !selectedMessage ? (

No Message Selected

Select an in-app conversation or email notice from the list to view its contents and attachments.

) : (
{/* Message Header Toolbar */}
{activeFolder === 'trash' ? ( <> ) : ( )}
{/* Message Details & Body */}
{/* Subject Header */}

{selectedMessage.subject || 'In-App Notification'}

Channel: In-App ID: #{selectedMessage.id} Received: {formatDate(selectedMessage.createdAt)}
{/* Sender & Recipient Card */}
{selectedMessage.destination?.includes('@') ? selectedMessage.destination.split('@')[0].slice(0, 2).toUpperCase() : 'PF'}

{selectedMessage.destination?.includes('From:') ? selectedMessage.destination.split('From:')[1]?.trim().replace(/[<>]/g, '') : selectedMessage.destination || 'PFM Automated Dispatcher'}

(In-App Notification)

To:{' '} {selectedMessage.destination?.includes('To:') ? selectedMessage.destination.split('To:')[1]?.split('|')[0]?.trim() : currentUserEmail || 'You'}

{formatDate(selectedMessage.createdAt)}
{/* Fully-Rendered Markdown & HTML Message Body with Branding */} {/* Inline Quick Reply Section */}

Quick Reply

Send a quick response below or open the rich text composer
{inlineReplySuccess && (
Reply dispatched successfully!
)}