143 lines
4.2 KiB
TypeScript
143 lines
4.2 KiB
TypeScript
|
|
'use client'
|
||
|
|
|
||
|
|
import Link from 'next/link'
|
||
|
|
import { Star, Paperclip } from 'lucide-react'
|
||
|
|
import { cn } from '@/lib/utils'
|
||
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
||
|
|
|
||
|
|
export interface MessageListItem {
|
||
|
|
uid: number
|
||
|
|
from: { name: string; address: string } | null
|
||
|
|
subject: string
|
||
|
|
date: string
|
||
|
|
flags: string[]
|
||
|
|
preview: string
|
||
|
|
}
|
||
|
|
|
||
|
|
interface MessageListProps {
|
||
|
|
messages: MessageListItem[]
|
||
|
|
folder: string
|
||
|
|
loading?: boolean
|
||
|
|
selectedUid?: number
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatDate(dateStr: string): string {
|
||
|
|
const date = new Date(dateStr)
|
||
|
|
const now = new Date()
|
||
|
|
const diffMs = now.getTime() - date.getTime()
|
||
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
||
|
|
|
||
|
|
if (diffDays === 0) {
|
||
|
|
return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
||
|
|
}
|
||
|
|
if (diffDays === 1) return 'Yesterday'
|
||
|
|
if (diffDays < 7) {
|
||
|
|
return date.toLocaleDateString(undefined, { weekday: 'short' })
|
||
|
|
}
|
||
|
|
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
||
|
|
}
|
||
|
|
|
||
|
|
function senderDisplay(from: { name: string; address: string } | null): string {
|
||
|
|
if (!from) return 'Unknown'
|
||
|
|
return from.name || from.address
|
||
|
|
}
|
||
|
|
|
||
|
|
function senderInitial(from: { name: string; address: string } | null): string {
|
||
|
|
const display = senderDisplay(from)
|
||
|
|
return display.charAt(0).toUpperCase()
|
||
|
|
}
|
||
|
|
|
||
|
|
export function MessageList({ messages, folder, loading, selectedUid }: MessageListProps) {
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div className="divide-y">
|
||
|
|
{Array.from({ length: 8 }).map((_, i) => (
|
||
|
|
<div key={i} className="flex items-start gap-3 p-4">
|
||
|
|
<Skeleton className="h-9 w-9 rounded-full shrink-0" />
|
||
|
|
<div className="flex-1 space-y-2">
|
||
|
|
<Skeleton className="h-4 w-32" />
|
||
|
|
<Skeleton className="h-4 w-48" />
|
||
|
|
<Skeleton className="h-3 w-64" />
|
||
|
|
</div>
|
||
|
|
<Skeleton className="h-3 w-12" />
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (messages.length === 0) {
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
|
||
|
|
<p className="text-sm">No messages</p>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="divide-y">
|
||
|
|
{messages.map((msg) => {
|
||
|
|
const isRead = msg.flags.includes('\\Seen')
|
||
|
|
const isStarred = msg.flags.includes('\\Flagged')
|
||
|
|
const encodedFolder = encodeURIComponent(folder)
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Link
|
||
|
|
key={msg.uid}
|
||
|
|
href={`/email/${encodedFolder}/${msg.uid}`}
|
||
|
|
className={cn(
|
||
|
|
'flex items-start gap-3 p-4 transition-colors hover:bg-accent/50',
|
||
|
|
!isRead && 'bg-accent/20',
|
||
|
|
selectedUid === msg.uid && 'bg-accent'
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
'flex h-9 w-9 items-center justify-center rounded-full shrink-0 text-sm font-medium',
|
||
|
|
isRead
|
||
|
|
? 'bg-muted text-muted-foreground'
|
||
|
|
: 'bg-primary text-primary-foreground'
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{senderInitial(msg.from)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<span
|
||
|
|
className={cn(
|
||
|
|
'text-sm truncate',
|
||
|
|
!isRead && 'font-semibold'
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{senderDisplay(msg.from)}
|
||
|
|
</span>
|
||
|
|
{isStarred && (
|
||
|
|
<Star className="h-3.5 w-3.5 fill-yellow-400 text-yellow-400 shrink-0" />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<p
|
||
|
|
className={cn(
|
||
|
|
'text-sm truncate',
|
||
|
|
!isRead ? 'font-medium text-foreground' : 'text-muted-foreground'
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{msg.subject}
|
||
|
|
</p>
|
||
|
|
{msg.preview && (
|
||
|
|
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
||
|
|
{msg.preview}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<span className="text-xs text-muted-foreground whitespace-nowrap shrink-0">
|
||
|
|
{formatDate(msg.date)}
|
||
|
|
</span>
|
||
|
|
</Link>
|
||
|
|
)
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|