Applied mailbox constraints
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
Inbox,
|
||||
Send,
|
||||
@@ -17,14 +18,16 @@ import {
|
||||
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 } from '../types';
|
||||
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';
|
||||
|
||||
@@ -33,8 +36,15 @@ export const InboxPage: React.FC = () => {
|
||||
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<User[]>({
|
||||
queryKey: ['users-directory'],
|
||||
queryFn: () => getUsers(1, 200),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const {
|
||||
allMailboxNotifications,
|
||||
inboxNotifications,
|
||||
@@ -77,6 +87,8 @@ export const InboxPage: React.FC = () => {
|
||||
// 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(() => {
|
||||
@@ -162,15 +174,51 @@ export const InboxPage: React.FC = () => {
|
||||
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) => {
|
||||
const rawDest = msg.destination || '';
|
||||
let replyEmail = '';
|
||||
if (rawDest.includes('From:')) {
|
||||
const fromPart = rawDest.split('From:')[1]?.trim() || '';
|
||||
replyEmail = fromPart.replace(/[<>]/g, '').trim();
|
||||
} else if (rawDest.includes('@')) {
|
||||
replyEmail = rawDest.trim();
|
||||
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'}`;
|
||||
@@ -178,9 +226,9 @@ export const InboxPage: React.FC = () => {
|
||||
|
||||
setSelectedDraft(null);
|
||||
setComposeInitialRecipient(
|
||||
replyEmail
|
||||
replyEmail && matchedSender
|
||||
? {
|
||||
id: String(msg.recipient),
|
||||
id: String(matchedSender.id),
|
||||
email: replyEmail,
|
||||
name: replyEmail.split('@')[0],
|
||||
}
|
||||
@@ -214,18 +262,29 @@ export const InboxPage: React.FC = () => {
|
||||
const handleSendInlineReply = async () => {
|
||||
if (!selectedMessage || !inlineReplyText.trim()) return;
|
||||
|
||||
const rawDest = selectedMessage.destination || '';
|
||||
let replyEmail = '';
|
||||
if (rawDest.includes('From:')) {
|
||||
replyEmail = rawDest.split('From:')[1]?.trim().replace(/[<>]/g, '') || '';
|
||||
} else if (rawDest.includes('@')) {
|
||||
replyEmail = rawDest.trim();
|
||||
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: String(selectedMessage.recipient),
|
||||
recipientEmail: replyEmail || currentUserEmail || 'team@dsi.gov.za',
|
||||
recipientId: targetRecipientId,
|
||||
recipientEmail: replyEmail || 'team@dsi.gov.za',
|
||||
subject: selectedMessage.subject?.startsWith('Re:')
|
||||
? selectedMessage.subject
|
||||
: `Re: ${selectedMessage.subject || 'Notification'}`,
|
||||
@@ -874,6 +933,46 @@ export const InboxPage: React.FC = () => {
|
||||
initialPlatform={composeInitialPlatform}
|
||||
draftToEdit={selectedDraft}
|
||||
/>
|
||||
|
||||
{/* Self-Reply Prevention Modal */}
|
||||
{isSelfReplyModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm animate-in fade-in duration-150">
|
||||
<div className="w-full max-w-md bg-[#0d1322] border border-rose-500/30 rounded-2xl shadow-2xl overflow-hidden p-6 space-y-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-rose-500/15 border border-rose-500/30 flex items-center justify-center text-rose-400 shrink-0">
|
||||
<ShieldAlert className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-white">Action Not Allowed</h3>
|
||||
<p className="text-[11px] text-rose-300 font-medium">Self-Reply Restricted</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSelfReplyModalOpen(false)}
|
||||
className="p-1 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-300 leading-relaxed">
|
||||
You cannot reply to a notification that originated from your own account. Please compose a new message and select another recipient from the directory if you wish to follow up with a colleague.
|
||||
</p>
|
||||
|
||||
<div className="pt-2 border-t border-slate-800 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSelfReplyModalOpen(false)}
|
||||
className="px-4 py-2 rounded-xl bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold shadow-md shadow-blue-600/25 transition cursor-pointer"
|
||||
>
|
||||
Understood
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user