"use client"; import { useAtom } from "jotai"; import { Bell } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useMemo, useState } from "react"; import { setTargetCommentIdAtom } from "@/atoms/chat/current-thread.atom"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { isCommentReplyMetadata, isInsufficientCreditsMetadata, isNewMentionMetadata, } from "@/contracts/types/inbox.types"; import type { InboxItem } from "@/hooks/use-inbox"; import { cn } from "@/lib/utils"; export interface NotificationsDataSource { items: InboxItem[]; unreadCount: number; loading: boolean; loadingMore?: boolean; hasMore?: boolean; loadMore?: () => void; markAsRead: (id: number) => Promise; markAllAsRead: () => Promise; } export interface NotificationsDropdownData { totalUnreadCount: number; comments: NotificationsDataSource; status: NotificationsDataSource; } interface NotificationsDropdownProps { notifications: NotificationsDropdownData; onCloseMobileSidebar?: () => void; } type NotificationFilter = "mentions" | "status" | null; function formatNotificationCount(count: number): string { if (count <= 999) { return count.toString(); } const thousands = Math.floor(count / 1000); return `${thousands}k+`; } function formatTime(dateString: string): 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 ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return `${Math.floor(diffDays / 7)}w ago`; } catch { return "now"; } } function isCommentNotification(item: InboxItem): boolean { return item.type === "new_mention" || item.type === "comment_reply"; } export function NotificationsDropdown({ notifications, onCloseMobileSidebar, }: NotificationsDropdownProps) { const router = useRouter(); const [, setTargetCommentId] = useAtom(setTargetCommentIdAtom); const [open, setOpen] = useState(false); const [activeFilter, setActiveFilter] = useState(null); const [markingAsReadId, setMarkingAsReadId] = useState(null); const [markingAllAsRead, setMarkingAllAsRead] = useState(false); const unreadLabel = formatNotificationCount(notifications.totalUnreadCount); const visibleUnreadCount = activeFilter === "mentions" ? notifications.comments.unreadCount : activeFilter === "status" ? notifications.status.unreadCount : notifications.totalUnreadCount; const visibleUnreadLabel = formatNotificationCount(visibleUnreadCount); const isLoading = activeFilter === "mentions" ? notifications.comments.loading : activeFilter === "status" ? notifications.status.loading : notifications.comments.loading || notifications.status.loading; const items = useMemo(() => { const sourceItems = activeFilter === "mentions" ? notifications.comments.items : activeFilter === "status" ? notifications.status.items : [...notifications.comments.items, ...notifications.status.items]; return sourceItems .toSorted((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) .slice(0, 8); }, [activeFilter, notifications.comments.items, notifications.status.items]); const markItemAsRead = useCallback( async (item: InboxItem) => { if (item.read) return; setMarkingAsReadId(item.id); try { await (isCommentNotification(item) ? notifications.comments.markAsRead(item.id) : notifications.status.markAsRead(item.id)); } finally { setMarkingAsReadId(null); } }, [notifications.comments, notifications.status] ); const handleItemClick = useCallback( async (item: InboxItem) => { await markItemAsRead(item); if (item.type === "new_mention" && isNewMentionMetadata(item.metadata)) { const threadId = item.metadata.thread_id; const commentId = item.metadata.comment_id; if (item.workspace_id && threadId) { if (commentId) setTargetCommentId(commentId); setOpen(false); onCloseMobileSidebar?.(); router.push( commentId ? `/dashboard/${item.workspace_id}/new-chat/${threadId}?commentId=${commentId}` : `/dashboard/${item.workspace_id}/new-chat/${threadId}` ); } return; } if (item.type === "comment_reply" && isCommentReplyMetadata(item.metadata)) { const threadId = item.metadata.thread_id; const replyId = item.metadata.reply_id; if (item.workspace_id && threadId) { if (replyId) setTargetCommentId(replyId); setOpen(false); onCloseMobileSidebar?.(); router.push( replyId ? `/dashboard/${item.workspace_id}/new-chat/${threadId}?commentId=${replyId}` : `/dashboard/${item.workspace_id}/new-chat/${threadId}` ); } return; } if (item.type === "insufficient_credits" && isInsufficientCreditsMetadata(item.metadata)) { if (item.metadata.action_url) { setOpen(false); onCloseMobileSidebar?.(); router.push(item.metadata.action_url); } } }, [markItemAsRead, onCloseMobileSidebar, router, setTargetCommentId] ); const handleMarkAllAsRead = useCallback(async () => { if (visibleUnreadCount === 0 || markingAllAsRead) return; setMarkingAllAsRead(true); try { if (activeFilter === "mentions") { await notifications.comments.markAllAsRead(); } else if (activeFilter === "status") { await notifications.status.markAllAsRead(); } else { await Promise.all([ notifications.comments.markAllAsRead(), notifications.status.markAllAsRead(), ]); } } finally { setMarkingAllAsRead(false); } }, [activeFilter, markingAllAsRead, notifications, visibleUnreadCount]); const handleFilterClick = useCallback((filter: Exclude) => { setActiveFilter((current) => (current === filter ? null : filter)); }, []); const emptyStateCopy = activeFilter === "mentions" ? { title: "No mentions", description: "Mentions and replies will appear here.", } : activeFilter === "status" ? { title: "No status updates", description: "Connector and document updates will appear here.", } : { title: "No notifications", description: "Mentions, replies, and status updates will appear here.", }; return ( Notifications

Notifications

{visibleUnreadCount > 0 ? `${visibleUnreadLabel} unread` : "You're all caught up"}

{isLoading ? (
{[82, 64, 74].map((width) => (
))}
) : items.length > 0 ? (
{items.map((item) => { const isMarkingAsRead = markingAsReadId === item.id; return ( ); })}
) : (

{emptyStateCopy.title}

{emptyStateCopy.description}

)}
); }