Applied mailbox constraints
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user