209 lines
7.7 KiB
TypeScript
209 lines
7.7 KiB
TypeScript
"use client"
|
|
|
|
import { format, isToday, isYesterday, isThisWeek, isThisYear } from "date-fns"
|
|
import {
|
|
Search,
|
|
Pin,
|
|
VolumeX,
|
|
MoreVertical,
|
|
Users,
|
|
Hash,
|
|
Settings,
|
|
UserPlus,
|
|
Filter
|
|
} from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { useChat, type Conversation } from "@/app/chat/use-chat"
|
|
|
|
interface ConversationListProps {
|
|
conversations: Conversation[]
|
|
selectedConversation: string | null
|
|
onSelectConversation: (conversationId: string) => void
|
|
}
|
|
|
|
// Enhanced time formatting function
|
|
function formatMessageTime(timestamp: string): string {
|
|
const date = new Date(timestamp)
|
|
|
|
if (isToday(date)) {
|
|
return format(date, 'h:mm a') // 3:30 PM
|
|
} else if (isYesterday(date)) {
|
|
return 'Yesterday'
|
|
} else if (isThisWeek(date)) {
|
|
return format(date, 'EEEE') // Day name
|
|
} else if (isThisYear(date)) {
|
|
return format(date, 'MMM d') // Jan 15
|
|
} else {
|
|
return format(date, 'dd/MM/yy') // 15/01/24
|
|
}
|
|
}
|
|
|
|
export function ConversationList({
|
|
conversations,
|
|
selectedConversation,
|
|
onSelectConversation
|
|
}: ConversationListProps) {
|
|
const { searchQuery, setSearchQuery } = useChat()
|
|
|
|
const filteredConversations = conversations.filter((conversation) =>
|
|
conversation.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
)
|
|
|
|
const sortedConversations = filteredConversations.sort((a, b) => {
|
|
// Pinned conversations first
|
|
if (a.isPinned && !b.isPinned) return -1
|
|
if (!a.isPinned && b.isPinned) return 1
|
|
|
|
// Then by last message timestamp
|
|
return new Date(b.lastMessage.timestamp).getTime() - new Date(a.lastMessage.timestamp).getTime()
|
|
})
|
|
|
|
const getOnlineStatus = (conversation: Conversation) => {
|
|
if (conversation.type === "direct" && conversation.participants.length === 1) {
|
|
// In a real app, you'd check user online status
|
|
return Math.random() > 0.5 // Mock online status
|
|
}
|
|
return false
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full overflow-hidden">
|
|
{/* Header - Hidden on mobile (handled by parent) */}
|
|
<div className="hidden lg:flex items-center justify-between h-16 px-4 border-b flex-shrink-0">
|
|
<h2 className="text-lg font-semibold">Messages</h2>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 w-8 p-0 cursor-pointer"
|
|
>
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem className="cursor-pointer">
|
|
<UserPlus className="h-4 w-4 mr-2" />
|
|
New Chat
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="cursor-pointer">
|
|
<Filter className="h-4 w-4 mr-2" />
|
|
Filter Messages
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem className="cursor-pointer">
|
|
<Settings className="h-4 w-4 mr-2" />
|
|
Chat Settings
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
{/* Search */}
|
|
<div className="px-4 py-3 border-b flex-shrink-0">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type="text"
|
|
placeholder="Search conversations..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-9 cursor-text"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Conversations */}
|
|
<ScrollArea className="flex-1">
|
|
<div className="p-2">
|
|
{sortedConversations.map((conversation) => (
|
|
<div
|
|
key={conversation.id}
|
|
className={cn(
|
|
"flex items-center gap-3 p-3 rounded-lg cursor-pointer relative overflow-hidden hover:bg-accent/50 transition-colors",
|
|
selectedConversation === conversation.id
|
|
? "bg-accent text-accent-foreground"
|
|
: ""
|
|
)}
|
|
onClick={() => onSelectConversation(conversation.id)}
|
|
>
|
|
{/* Avatar with online indicator */}
|
|
<div className="relative flex-shrink-0">
|
|
<Avatar className={cn(
|
|
"h-12 w-12",
|
|
selectedConversation === conversation.id && "ring-2 ring-background"
|
|
)}>
|
|
<AvatarImage src={conversation.avatar} alt={conversation.name} />
|
|
<AvatarFallback className="text-sm">
|
|
{conversation.type === "group" ? (
|
|
<Users className="h-5 w-5" />
|
|
) : (
|
|
conversation.name.split(' ').map(n => n[0]).join('').slice(0, 2)
|
|
)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
|
|
{/* Online indicator for direct messages */}
|
|
{conversation.type === "direct" && getOnlineStatus(conversation) && (
|
|
<div className="absolute -bottom-1 -right-1 h-4 w-4 bg-green-500 border-2 border-background rounded-full" />
|
|
)}
|
|
|
|
{/* Group indicator */}
|
|
{conversation.type === "group" && (
|
|
<div className="absolute -bottom-1 -right-1 h-4 w-4 bg-blue-500 border-2 border-background rounded-full flex items-center justify-center">
|
|
<Hash className="h-2 w-2 text-white" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 min-w-0 overflow-hidden">
|
|
<div className="flex items-center justify-between mb-1 min-w-0">
|
|
<div className="flex items-center gap-1 min-w-0 flex-1 overflow-hidden pr-2">
|
|
<h3 className="font-medium truncate min-w-0 max-w-[160px] lg:max-w-[180px]">{conversation.name}</h3>
|
|
{conversation.isPinned && (
|
|
<Pin className="h-3 w-3 text-muted-foreground flex-shrink-0" />
|
|
)}
|
|
{conversation.isMuted && (
|
|
<VolumeX className="h-3 w-3 text-muted-foreground flex-shrink-0" />
|
|
)}
|
|
</div>
|
|
<span className="text-xs text-muted-foreground flex-shrink-0 whitespace-nowrap">
|
|
{formatMessageTime(conversation.lastMessage.timestamp)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-2 min-w-0">
|
|
<p className="text-sm text-muted-foreground truncate flex-1 min-w-0 max-w-[180px] lg:max-w-[200px] pr-2">
|
|
{conversation.lastMessage.content}
|
|
</p>
|
|
|
|
{/* Unread count */}
|
|
{conversation.unreadCount > 0 && (
|
|
<Badge variant="default" className="min-w-[20px] h-5 text-xs cursor-pointer flex-shrink-0">
|
|
{conversation.unreadCount > 99 ? "99+" : conversation.unreadCount}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
)
|
|
}
|