Applied mailbox constraints #159
@@ -124,7 +124,7 @@ const navItems: NavItem[] = [
|
||||
claim: 'tenant_management',
|
||||
},
|
||||
{
|
||||
name: 'Mailbox & Inbox',
|
||||
name: 'Inbox',
|
||||
path: '/inbox',
|
||||
icon: Inbox,
|
||||
help: 'In-app notifications, communications, deliverable messages, drafts, and replies across active tenant users.',
|
||||
|
||||
@@ -55,6 +55,8 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
|
||||
draftToEdit,
|
||||
}) => {
|
||||
const { user, claims } = useAuth();
|
||||
const currentUserId = String(user?.id || claims?.sub || '').trim().toLowerCase();
|
||||
const currentUserEmail = String(user?.email || claims?.email || '').trim().toLowerCase();
|
||||
const tenantName =
|
||||
(claims?.tenant as string) ||
|
||||
user?.tenant ||
|
||||
@@ -71,6 +73,15 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
|
||||
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isCurrentUser = (candidate?: { id?: string; email?: string }) => {
|
||||
if (!candidate) return false;
|
||||
const cid = String(candidate.id || '').trim().toLowerCase();
|
||||
const cemail = String(candidate.email || '').trim().toLowerCase();
|
||||
if (currentUserId && cid === currentUserId) return true;
|
||||
if (currentUserEmail && cemail === currentUserEmail) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const toInitialRichHtml = (raw: string): string => {
|
||||
if (!raw || !raw.trim()) return '';
|
||||
if (isHtmlContent(raw)) return raw;
|
||||
@@ -83,23 +94,22 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
|
||||
|
||||
let initialHtml = '';
|
||||
if (draftToEdit) {
|
||||
setSelectedUsers(
|
||||
draftToEdit.recipientId
|
||||
? [
|
||||
{
|
||||
id: draftToEdit.recipientId,
|
||||
email: draftToEdit.recipientEmail,
|
||||
name: draftToEdit.recipientName,
|
||||
},
|
||||
]
|
||||
: []
|
||||
);
|
||||
const candidate = draftToEdit.recipientId
|
||||
? {
|
||||
id: draftToEdit.recipientId,
|
||||
email: draftToEdit.recipientEmail,
|
||||
name: draftToEdit.recipientName,
|
||||
}
|
||||
: undefined;
|
||||
setSelectedUsers(candidate && !isCurrentUser(candidate) ? [candidate] : []);
|
||||
setSubject(draftToEdit.subject || '');
|
||||
initialHtml = toInitialRichHtml(draftToEdit.message || '');
|
||||
setMessage(initialHtml);
|
||||
setPlatform(draftToEdit.platform || NotificationPlatform.InApp);
|
||||
} else {
|
||||
setSelectedUsers(initialRecipient ? [initialRecipient] : []);
|
||||
setSelectedUsers(
|
||||
initialRecipient && !isCurrentUser(initialRecipient) ? [initialRecipient] : []
|
||||
);
|
||||
setSubject(initialSubject || '');
|
||||
initialHtml = toInitialRichHtml(initialMessage || '');
|
||||
setMessage(initialHtml);
|
||||
@@ -128,6 +138,10 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSelectUser = (selected: SelectedUser) => {
|
||||
if (isCurrentUser(selected)) {
|
||||
setError('You cannot send a message to yourself.');
|
||||
return;
|
||||
}
|
||||
setSelectedUsers([selected]);
|
||||
setError(null);
|
||||
};
|
||||
@@ -220,6 +234,16 @@ export const ComposeMessageModal: React.FC<ComposeMessageModalProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedUsers.length > 1) {
|
||||
setError('Only one recipient is allowed per message');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCurrentUser(selectedUsers[0])) {
|
||||
setError('You cannot send a message to yourself');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!subject.trim()) {
|
||||
setError('Please enter a subject');
|
||||
return;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { getUsers } from '../../services/api';
|
||||
import { User, ActivityStatus } from '../../types';
|
||||
import { useAuth } from '../../auth/AuthProvider';
|
||||
|
||||
export interface SelectedUser {
|
||||
id: string;
|
||||
@@ -43,9 +44,12 @@ export const UserSearchPicker: React.FC<UserSearchPickerProps> = ({
|
||||
selectedUsers,
|
||||
onSelectUser,
|
||||
onRemoveUser,
|
||||
multiple = false,
|
||||
placeholder = 'Search by name, email, or role...',
|
||||
}) => {
|
||||
const { user, claims } = useAuth();
|
||||
const currentUserId = String(user?.id || claims?.sub || '').trim().toLowerCase();
|
||||
const currentUserEmail = String(user?.email || claims?.email || '').trim().toLowerCase();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
|
||||
@@ -57,10 +61,17 @@ export const UserSearchPicker: React.FC<UserSearchPickerProps> = ({
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Filter active users
|
||||
// Filter active users and exclude the current logged-in user so they can never message themselves
|
||||
const activeUsers = useMemo(() => {
|
||||
return users.filter(u => u.status === ActivityStatus.Active && !!u.email);
|
||||
}, [users]);
|
||||
return users.filter(u => {
|
||||
if (u.status !== ActivityStatus.Active || !u.email) return false;
|
||||
const uid = String(u.id || '').trim().toLowerCase();
|
||||
const uemail = u.email.trim().toLowerCase();
|
||||
if (currentUserId && uid === currentUserId) return false;
|
||||
if (currentUserEmail && uemail === currentUserEmail) return false;
|
||||
return true;
|
||||
});
|
||||
}, [users, currentUserId, currentUserEmail]);
|
||||
|
||||
// Organize users into intuitive functional groups
|
||||
const groupedUsers = useMemo(() => {
|
||||
@@ -172,87 +183,83 @@ export const UserSearchPicker: React.FC<UserSearchPickerProps> = ({
|
||||
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ');
|
||||
|
||||
if (!multiple) {
|
||||
// Single selection replaces
|
||||
onSelectUser({
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
name: formattedName,
|
||||
role: String(u.role ?? 'User'),
|
||||
});
|
||||
setIsDropdownOpen(false);
|
||||
} else {
|
||||
onSelectUser({
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
name: formattedName,
|
||||
role: String(u.role ?? 'User'),
|
||||
});
|
||||
}
|
||||
// Strictly single-recipient selection
|
||||
onSelectUser({
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
name: formattedName,
|
||||
role: String(u.role ?? 'User'),
|
||||
});
|
||||
setSearchTerm('');
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const singleSelectedUser = selectedUsers[0];
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
{/* Selected Users Chips */}
|
||||
{selectedUsers.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||
{selectedUsers.map(su => (
|
||||
<span
|
||||
key={su.id}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-blue-500/15 border border-blue-500/30 text-blue-200 text-xs font-medium shadow-xs"
|
||||
>
|
||||
<div className="w-4 h-4 rounded-full bg-blue-600/40 text-[9px] font-bold flex items-center justify-center text-blue-300">
|
||||
{su.name ? su.name.slice(0, 2).toUpperCase() : su.email.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<span className="truncate max-w-[180px]">{su.name || su.email}</span>
|
||||
{su.role && (
|
||||
<span className="text-[10px] text-blue-400 bg-blue-900/40 px-1 py-0.2 rounded border border-blue-800/40">
|
||||
{su.role}
|
||||
{/* Single Selected Recipient or Search Input Box */}
|
||||
{singleSelectedUser ? (
|
||||
<div className="flex items-center justify-between px-3 py-2 rounded-xl bg-blue-500/10 border border-blue-500/30 text-blue-100 text-xs shadow-xs">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="w-6 h-6 rounded-full bg-blue-600/40 text-[10px] font-bold flex items-center justify-center text-blue-200 shrink-0 border border-blue-400/30">
|
||||
{(singleSelectedUser.name || singleSelectedUser.email).slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="font-semibold text-white truncate">
|
||||
{singleSelectedUser.name || singleSelectedUser.email}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-400 font-mono truncate">
|
||||
<{singleSelectedUser.email}>
|
||||
</span>
|
||||
{singleSelectedUser.role && (
|
||||
<span className="text-[10px] text-blue-300 bg-blue-900/40 px-1.5 py-0.5 rounded border border-blue-800/40 shrink-0">
|
||||
{singleSelectedUser.role}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemoveUser(su.id)}
|
||||
className="text-blue-400 hover:text-white hover:bg-blue-600/30 p-0.5 rounded transition cursor-pointer"
|
||||
title="Remove recipient"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemoveUser(singleSelectedUser.id)}
|
||||
className="flex items-center gap-1 text-[11px] text-blue-300 hover:text-white hover:bg-blue-600/30 px-2 py-1 rounded-lg transition cursor-pointer shrink-0"
|
||||
title="Remove recipient to select another user"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
<span>Change</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400">
|
||||
<Search className="w-4 h-4" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={e => {
|
||||
setSearchTerm(e.target.value);
|
||||
if (!isDropdownOpen) setIsDropdownOpen(true);
|
||||
}}
|
||||
onFocus={() => setIsDropdownOpen(true)}
|
||||
placeholder={placeholder}
|
||||
className="w-full pl-9 pr-8 py-2 rounded-xl bg-slate-900/90 border border-slate-700/80 hover:border-slate-600 focus:border-blue-500 text-slate-100 placeholder-slate-400 text-xs focus:outline-none transition shadow-inner"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-white"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Input Box */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400">
|
||||
<Search className="w-4 h-4" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={e => {
|
||||
setSearchTerm(e.target.value);
|
||||
if (!isDropdownOpen) setIsDropdownOpen(true);
|
||||
}}
|
||||
onFocus={() => setIsDropdownOpen(true)}
|
||||
placeholder={placeholder}
|
||||
className="w-full pl-9 pr-8 py-2 rounded-xl bg-slate-900/90 border border-slate-700/80 hover:border-slate-600 focus:border-blue-500 text-slate-100 placeholder-slate-400 text-xs focus:outline-none transition shadow-inner"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-white"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Organized User Directory Dropdown */}
|
||||
{isDropdownOpen && (
|
||||
{/* Organized User Directory Dropdown (only when no recipient is locked in) */}
|
||||
{isDropdownOpen && !singleSelectedUser && (
|
||||
<div className="absolute left-0 right-0 top-full mt-1.5 max-h-72 overflow-y-auto bg-[#0d1322] border border-slate-700/80 rounded-2xl shadow-2xl z-50 p-2 space-y-3 backdrop-blur-md animate-in fade-in slide-in-from-top-1">
|
||||
{isLoading ? (
|
||||
<div className="py-6 text-center text-xs text-slate-400 flex items-center justify-center gap-2">
|
||||
|
||||
@@ -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