mirror of
https://github.com/rustmailer/rustmailer.git
synced 2026-08-18 16:01:07 +00:00
refactor: compact MailList UI for higher density
This commit is contained in:
@@ -1,9 +1,3 @@
|
||||
/*
|
||||
* Copyright © 2025 rustmailer.com
|
||||
* Licensed under RustMailer License Agreement v1.0
|
||||
* Unauthorized use or distribution is prohibited.
|
||||
*/
|
||||
|
||||
import { cn, formatFileSize } from "@/lib/utils"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { formatDistanceToNow } from "date-fns"
|
||||
@@ -14,14 +8,14 @@ import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { MailboxDialogType } from "../context"
|
||||
|
||||
interface MailListProps {
|
||||
items: EmailEnvelope[],
|
||||
items: EmailEnvelope[]
|
||||
isLoading: boolean
|
||||
currentEnvelope: EmailEnvelope | undefined
|
||||
onEnvelopeChanged: (envelope: EmailEnvelope) => void
|
||||
setOpen: (str: MailboxDialogType | null) => void,
|
||||
setSelectedIds: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setDeleteIds: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
selectedIds: string[];
|
||||
setOpen: (str: MailboxDialogType | null) => void
|
||||
setSelectedIds: React.Dispatch<React.SetStateAction<string[]>>
|
||||
setDeleteIds: React.Dispatch<React.SetStateAction<string[]>>
|
||||
selectedIds: string[]
|
||||
}
|
||||
|
||||
export function MailList({
|
||||
@@ -35,31 +29,26 @@ export function MailList({
|
||||
selectedIds
|
||||
}: MailListProps) {
|
||||
const handleDelete = (envelope: EmailEnvelope) => {
|
||||
setDeleteIds([envelope.id]);
|
||||
setOpen("move-to-trash");
|
||||
setDeleteIds([envelope.id])
|
||||
setOpen("move-to-trash")
|
||||
}
|
||||
|
||||
const handleCheckboxChange = (value: boolean | 'indeterminate', id: string) => {
|
||||
if (value === true) {
|
||||
setSelectedIds((prev) => [...prev, id]);
|
||||
setSelectedIds(prev => [...prev, id])
|
||||
} else if (value === false) {
|
||||
setSelectedIds((prev) => prev.filter((x) => x !== id));
|
||||
setSelectedIds(prev => prev.filter(x => x !== id))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<div key={index} className="flex flex-col gap-2 p-2 rounded-lg border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-4 rounded" />
|
||||
<Skeleton className="h-3 w-24 rounded" />
|
||||
<Skeleton className="h-2 w-2 rounded-full ml-auto" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 pl-6">
|
||||
<Skeleton className="h-3 w-3/4 rounded" />
|
||||
</div>
|
||||
<div className="space-y-1 p-1">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-2 p-1.5 rounded border">
|
||||
<Skeleton className="h-3 w-3 rounded" />
|
||||
<Skeleton className="h-3 flex-1" />
|
||||
<Skeleton className="h-2 w-2 rounded-full ml-auto" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -67,135 +56,105 @@ export function MailList({
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<div className="grid grid-cols-1 gap-1.5 p-1 sm:p-2">
|
||||
<div className="divide-y divide-border">
|
||||
{items.map((item) => {
|
||||
const isUnread = !item.is_read;
|
||||
const isUnread = !item.is_read
|
||||
const hasAttachments = item.attachments && item.attachments.length > 0;
|
||||
const attachmentCount = item.attachments?.length || 0;
|
||||
const isSelected = selectedIds.includes(item.id)
|
||||
const isActive = currentEnvelope?.id === item.id
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"flex flex-col gap-1.5 p-2 rounded-lg border transition-all cursor-pointer",
|
||||
"hover:bg-accent/50",
|
||||
currentEnvelope?.id === item.id && "bg-accent",
|
||||
selectedIds.includes(item.id) && "bg-primary/5"
|
||||
"group flex items-center gap-2 p-1.5 pr-2 rounded-md border transition-all cursor-pointer text-xs",
|
||||
"hover:bg-accent/70",
|
||||
isActive && "bg-accent",
|
||||
isSelected && "bg-primary/10 ring-1 ring-primary/20"
|
||||
)}
|
||||
onClick={() => onEnvelopeChanged(item)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Checkbox
|
||||
checked={
|
||||
selectedIds.includes(item.id)
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
handleCheckboxChange(checked, item.id)
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="h-4 w-3 shrink-0"
|
||||
/>
|
||||
{/* Checkbox */}
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) => handleCheckboxChange(checked, item.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="h-3.5 w-3.5 shrink-0"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-1 min-w-0 flex-1">
|
||||
{isUnread ? (
|
||||
<MailIcon className="h-3.5 w-3.5 text-blue-600 shrink-0" />
|
||||
) : (
|
||||
<MailOpen className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
{/* <span className="text-xs text-muted-foreground">
|
||||
{isGmailApi ? `mid: ${item.id}` : `uid: ${item.id}`}
|
||||
</span> */}
|
||||
{/* Unread Icon */}
|
||||
{isUnread ? (
|
||||
<MailIcon className="h-3 w-3 text-blue-600 shrink-0" />
|
||||
) : (
|
||||
<MailOpen className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
|
||||
<p className={cn(
|
||||
"text-xs font-medium truncate ml-1",
|
||||
isUnread && "font-semibold"
|
||||
)}>
|
||||
{item.from ? `${item.from.name || ""} <${item.from.address}>` : "Unknown"}
|
||||
</p>
|
||||
{/* Sender */}
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium truncate max-w-[140px]",
|
||||
isUnread && "font-semibold"
|
||||
)}
|
||||
title={item.from?.address}
|
||||
>
|
||||
{item.from?.name || item.from?.address || "Unknown"}
|
||||
</span>
|
||||
|
||||
{isUnread && (
|
||||
<span className="flex h-1.5 w-1.5 rounded-full bg-blue-600 shrink-0 ml-1" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{hasAttachments && (
|
||||
<div className="flex items-center gap-0.5 mr-1">
|
||||
<Paperclip className={cn(
|
||||
"h-3 w-3",
|
||||
isUnread ? "text-blue-600" : "text-muted-foreground"
|
||||
)} />
|
||||
<span className={cn(
|
||||
"text-xs",
|
||||
isUnread ? "text-blue-600 font-medium" : "text-muted-foreground"
|
||||
)}>
|
||||
{attachmentCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatFileSize(item.size)}
|
||||
</span>
|
||||
|
||||
<span className={cn(
|
||||
"text-xs",
|
||||
currentEnvelope?.id === item.id
|
||||
? "text-foreground font-medium"
|
||||
: "text-muted-foreground"
|
||||
)}>
|
||||
{item.date && formatDistanceToNow(new Date(item.date), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center pl-7">
|
||||
<h3 className={cn(
|
||||
"text-xs line-clamp-1",
|
||||
isUnread ? "font-medium" : "text-muted-foreground"
|
||||
)}>
|
||||
{/* Subject + Attachments */}
|
||||
<div className="flex-1 flex items-center gap-1 min-w-0">
|
||||
{hasAttachments && (
|
||||
<Paperclip className={cn("h-2.5 w-2.5 shrink-0", isUnread ? "text-blue-600" : "text-muted-foreground")} />
|
||||
)}
|
||||
<span className={cn("truncate", isUnread ? "font-medium" : "text-muted-foreground")}>
|
||||
{item.subject || "(No Subject)"}
|
||||
</h3>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pl-7">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{item.flags?.map((flag) => (
|
||||
<Badge
|
||||
key={flag.flag + (flag.custom || "")}
|
||||
variant={getBadgeVariantFromFlag(flag.flag)}
|
||||
className="h-4 px-1 text-[11px] leading-none"
|
||||
>
|
||||
{isCustomFlag(flag.flag) ? flag.custom : flag.flag}
|
||||
</Badge>
|
||||
))}
|
||||
{item.labels?.length > 0 &&
|
||||
item.labels.map((label) => (
|
||||
<Badge
|
||||
key={label}
|
||||
variant="outline"
|
||||
className="h-4 px-1 text-[11px] leading-none"
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="text-muted-foreground hover:text-destructive transition-colors p-0.5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(item);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{/* Flags / Labels */}
|
||||
<div className="flex items-center gap-0.5 shrink-0">
|
||||
{item.flags?.slice(0, 2).map((flag) => (
|
||||
<Badge
|
||||
key={flag.flag + (flag.custom || "")}
|
||||
variant={getBadgeVariantFromFlag(flag.flag)}
|
||||
className="h-3.5 px-1 text-xs leading-none"
|
||||
>
|
||||
{isCustomFlag(flag.flag) ? flag.custom : flag.flag}
|
||||
</Badge>
|
||||
))}
|
||||
{item.labels?.[0] && (
|
||||
<Badge variant="outline" className="h-3.5 px-1 text-xs leading-none">
|
||||
{item.labels[0]}
|
||||
</Badge>
|
||||
)}
|
||||
{(item.flags?.length ?? 0) > 2 || item.labels?.length > 1 ? (
|
||||
<span className="text-xs text-muted-foreground">+{((item.flags?.length ?? 0) - 2) + (item.labels?.length - 1)}</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Size & Time */}
|
||||
<span className="text-xs text-muted-foreground shrink-0 ml-1">
|
||||
{formatFileSize(item.size)}
|
||||
</span>
|
||||
<span className={cn(
|
||||
"text-xs shrink-0",
|
||||
isActive ? "text-foreground font-medium" : "text-muted-foreground"
|
||||
)}>
|
||||
{item.date && formatDistanceToNow(new Date(item.date), { addSuffix: true })}
|
||||
</span>
|
||||
|
||||
{/* Delete Button */}
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 text-muted-foreground hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDelete(item)
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -177,7 +177,7 @@ export function Mail({
|
||||
const pageTokenMapRef = React.useRef<Record<number, string | undefined>>({});
|
||||
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageSize, setPageSize] = React.useState(30);
|
||||
const [selectedIds, setSelectedIds] = React.useState<string[]>([]);
|
||||
const [deleteIds, setDeleteIds] = React.useState<string[]>([]);
|
||||
const [currentFilter, setCurrentFilter] = React.useState<FilterForm | undefined>(undefined);
|
||||
|
||||
Reference in New Issue
Block a user