"use client"; import { AlertCircle, Archive, AtSign, BellDot, Check, CheckCheck, CheckCircle2, History, Inbox, ListFilter, Loader2, MoreHorizontal, RotateCcw, Search, X, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { convertRenderedToDisplay } from "@/components/chat-comments/comment-item/comment-item"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import type { InboxItem } from "@/hooks/use-inbox"; import { cn } from "@/lib/utils"; /** * Get initials from name or email for avatar fallback */ function getInitials(name: string | null | undefined, email: string | null | undefined): string { if (name) { return name .split(" ") .map((n) => n[0]) .join("") .toUpperCase() .slice(0, 2); } if (email) { const localPart = email.split("@")[0]; return localPart.slice(0, 2).toUpperCase(); } return "U"; } type InboxTab = "mentions" | "status"; type InboxFilter = "all" | "unread" | "archived"; interface InboxSidebarProps { open: boolean; onOpenChange: (open: boolean) => void; inboxItems: InboxItem[]; unreadCount: number; loading: boolean; markAsRead: (id: number) => Promise; markAllAsRead: () => Promise; archiveItem: (id: number, archived: boolean) => Promise; onCloseMobileSidebar?: () => void; } export function InboxSidebar({ open, onOpenChange, inboxItems, unreadCount, loading, markAsRead, markAllAsRead, archiveItem, onCloseMobileSidebar, }: InboxSidebarProps) { const t = useTranslations("sidebar"); const router = useRouter(); const [searchQuery, setSearchQuery] = useState(""); const [activeTab, setActiveTab] = useState("mentions"); const [activeFilter, setActiveFilter] = useState("all"); const [mounted, setMounted] = useState(false); // Unified dropdown state: "filter" | "options" | number (item id) | null const [openDropdown, setOpenDropdown] = useState<"filter" | "options" | number | null>(null); const [markingAsReadId, setMarkingAsReadId] = useState(null); const [archivingItemId, setArchivingItemId] = useState(null); useEffect(() => { setMounted(true); }, []); useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && open) { onOpenChange(false); } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); useEffect(() => { if (open) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [open]); // Split items by type const mentionItems = useMemo( () => inboxItems.filter((item) => item.type === "new_mention"), [inboxItems] ); const statusItems = useMemo( () => inboxItems.filter( (item) => item.type === "connector_indexing" || item.type === "document_processing" ), [inboxItems] ); // Get items for current tab const currentTabItems = activeTab === "mentions" ? mentionItems : statusItems; // Filter items based on filter type and search query const filteredItems = useMemo(() => { let items = currentTabItems; // Apply filter // Note: Use `item.archived === true` to handle undefined/null as false if (activeFilter === "all") { // "Unread & read" shows all non-archived items items = items.filter((item) => item.archived !== true); } else if (activeFilter === "unread") { // "Unread" shows only unread non-archived items items = items.filter((item) => !item.read && item.archived !== true); } else if (activeFilter === "archived") { // "Archived" shows only archived items (must be explicitly true) items = items.filter((item) => item.archived === true); } // Apply search query if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); items = items.filter( (item) => item.title.toLowerCase().includes(query) || item.message.toLowerCase().includes(query) ); } return items; }, [currentTabItems, activeFilter, searchQuery]); // Count unread items per tab const unreadMentionsCount = useMemo( () => mentionItems.filter((item) => !item.read).length, [mentionItems] ); const unreadStatusCount = useMemo( () => statusItems.filter((item) => !item.read).length, [statusItems] ); const handleItemClick = useCallback( async (item: InboxItem) => { if (!item.read) { setMarkingAsReadId(item.id); await markAsRead(item.id); setMarkingAsReadId(null); } if (item.type === "new_mention") { const metadata = item.metadata as { thread_id?: number; comment_id?: number; }; const searchSpaceId = item.search_space_id; const threadId = metadata?.thread_id; const commentId = metadata?.comment_id; if (searchSpaceId && threadId) { const url = commentId ? `/dashboard/${searchSpaceId}/new-chat/${threadId}?commentId=${commentId}` : `/dashboard/${searchSpaceId}/new-chat/${threadId}`; onOpenChange(false); onCloseMobileSidebar?.(); router.push(url); } } }, [markAsRead, router, onOpenChange, onCloseMobileSidebar] ); const handleMarkAsRead = useCallback( async (itemId: number) => { setMarkingAsReadId(itemId); await markAsRead(itemId); setMarkingAsReadId(null); }, [markAsRead] ); const handleMarkAllAsRead = useCallback(async () => { await markAllAsRead(); }, [markAllAsRead]); const handleToggleArchive = useCallback( async (itemId: number, currentlyArchived: boolean) => { setArchivingItemId(itemId); await archiveItem(itemId, !currentlyArchived); setArchivingItemId(null); }, [archiveItem] ); const handleClearSearch = useCallback(() => { setSearchQuery(""); }, []); const formatTime = (dateString: string) => { try { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffMins < 1) return "now"; if (diffMins < 60) return `${diffMins}m`; if (diffHours < 24) return `${diffHours}h`; if (diffDays < 7) return `${diffDays}d`; return `${Math.floor(diffDays / 7)}w`; } catch { return "now"; } }; const getStatusIcon = (item: InboxItem) => { // For mentions, show the author's avatar with initials fallback if (item.type === "new_mention") { const metadata = item.metadata as { author_name?: string; author_avatar_url?: string | null; author_email?: string; }; const authorName = metadata?.author_name; const avatarUrl = metadata?.author_avatar_url; const authorEmail = metadata?.author_email; return ( {avatarUrl && } {getInitials(authorName, authorEmail)} ); } // For status items (connector/document), show status icons const status = item.metadata?.status as string | undefined; switch (status) { case "in_progress": return (
); case "completed": return (
); case "failed": return (
); default: return (
); } }; const getEmptyStateMessage = () => { if (activeTab === "mentions") { return { title: t("no_mentions") || "No mentions", hint: t("no_mentions_hint") || "You'll see mentions from others here", }; } return { title: t("no_status_updates") || "No status updates", hint: t("no_status_updates_hint") || "Document and connector updates will appear here", }; }; if (!mounted) return null; return createPortal( {open && ( <> onOpenChange(false)} aria-hidden="true" />

{t("inbox") || "Inbox"}

setOpenDropdown(isOpen ? "filter" : null)} > {t("filter") || "Filter"} setActiveFilter("all")} className="flex items-center justify-between" > {t("unread_and_read") || "Unread & read"} {activeFilter === "all" && } setActiveFilter("unread")} className="flex items-center justify-between" > {t("unread") || "Unread"} {activeFilter === "unread" && } setActiveFilter("archived")} className="flex items-center justify-between" > {t("archived") || "Archived"} {activeFilter === "archived" && } setOpenDropdown(isOpen ? "options" : null)} > {t("mark_all_read") || "Mark all as read"}
setSearchQuery(e.target.value)} className="pl-9 pr-8 h-9" /> {searchQuery && ( )}
setActiveTab(value as InboxTab)} className="shrink-0 mx-4" > {t("mentions") || "Mentions"} {unreadMentionsCount > 0 && ( {unreadMentionsCount} )} {t("status") || "Status"} {unreadStatusCount > 0 && ( {unreadStatusCount} )}
{loading ? (
) : filteredItems.length > 0 ? (
{filteredItems.map((item) => { const isMarkingAsRead = markingAsReadId === item.id; const isArchiving = archivingItemId === item.id; const isBusy = isMarkingAsRead || isArchiving; const isArchived = item.archived === true; return (
)}

{item.title}

{convertRenderedToDisplay(item.message)}

{/* Time/dot and 3-dot button container - swap on hover (desktop only) */}
{/* Time and unread dot - visible by default, hidden on hover or when dropdown is open */}
{formatTime(item.created_at)} {!item.read && ( )}
{/* 3-dot menu - hidden by default, visible on hover or when dropdown is open */} setOpenDropdown(isOpen ? item.id : null) } > {!item.read && ( <> handleMarkAsRead(item.id)} disabled={isBusy} > {t("mark_as_read") || "Mark as read"} )} handleToggleArchive(item.id, isArchived)} disabled={isArchiving} > {isArchived ? ( <> {t("unarchive") || "Restore"} ) : ( <> {t("archive") || "Archive"} )}
{/* Mobile time and unread dot - always visible on mobile */}
{formatTime(item.created_at)} {!item.read && ( )}
); })} ) : searchQuery ? (

{t("no_results_found") || "No results found"}

{t("try_different_search") || "Try a different search term"}

) : (
{activeTab === "mentions" ? ( ) : ( )}

{getEmptyStateMessage().title}

{getEmptyStateMessage().hint}

)}
)}
, document.body ); }