'use client' import { useEffect, useRef } from 'react' import { BellOffIcon } from '../../icons-v2-generated/interface/bell-off-icon' import { ClockHistoryIcon, ArrowRightUpIcon } from '../../icons-v2-generated' import { useAppLayoutDrawerContainer } from '../../navigation/app-layout-context' import { AppLayoutDrawer, AppLayoutDrawerContent } from '../../navigation/app-layout-drawer' import { SplitButton } from '../../ui/button' import { Drawer, DrawerContent, DrawerTitle } from '../../ui/drawer' import { Switch } from '../../ui/switch' import { cn } from '../../../utils/cn' import { useNotificationPermission } from '../../../hooks/ui' import { useOptionalNotifications } from './notifications-context' import { NotificationTile } from './notification-tile' import type { Notification, RenderNotificationTile } from './types' export interface NotificationDrawerProps { className?: string liveDurationMs?: number loadMoreRootMargin?: string renderTile?: RenderNotificationTile } export function NotificationDrawer({ className, liveDurationMs, loadMoreRootMargin = '200px', renderTile: renderTileProp, }: NotificationDrawerProps) { const ctx = useOptionalNotifications() // Inside AppLayout the drawer renders in-layout (dims and covers only the // main content area — header and sidebar stay interactive). Outside of it // (Storybook, non-AppLayout hosts) it falls back to the viewport Drawer. const layoutContainer = useAppLayoutDrawerContainer() if (!ctx) return null const { notifications, unreadCount, isOpen, showPopups, open, close, markRead, markAllRead, markSettled, setShowPopups, showDesktopPopups, setShowDesktopPopups, desktopPopupsConfigured, onHistoryClick, historyHref, hasMore, isLoadingMore, loadMore, renderTile: ctxRenderTile, } = ctx const renderTile = renderTileProp ?? ctxRenderTile const unreadNotifications = notifications.filter((n) => !n.read) const content = ( <>
Mark All Complete } > New Notifications
{desktopPopupsConfigured && ( )}
{ onHistoryClick(); close() } : undefined} historyHref={historyHref} />
) if (layoutContainer) { return ( (v ? open() : close())}> {content} ) } return ( (v ? open() : close())}> {content} ) } interface DrawerScrollListProps { unreadNotifications: Notification[] liveDurationMs?: number onComplete: (id: string) => void onSettle: (id: string) => void hasMore: boolean isLoadingMore: boolean loadMore?: () => void loadMoreRootMargin: string renderTile?: RenderNotificationTile } function DrawerScrollList({ unreadNotifications, liveDurationMs, onComplete, onSettle, hasMore, isLoadingMore, loadMore, loadMoreRootMargin, renderTile, }: DrawerScrollListProps) { const scrollRef = useRef(null) const sentinelRef = useRef(null) const loadMoreRef = useRef(loadMore) loadMoreRef.current = loadMore useEffect(() => { if (!loadMore || !hasMore || isLoadingMore) return const sentinel = sentinelRef.current const root = scrollRef.current if (!sentinel || !root) return const observer = new IntersectionObserver( entries => { if (entries[0]?.isIntersecting) loadMoreRef.current?.() }, { root, rootMargin: loadMoreRootMargin }, ) observer.observe(sentinel) return () => observer.disconnect() }, [hasMore, isLoadingMore, loadMore, loadMoreRootMargin]) const isEmpty = unreadNotifications.length === 0 return (
{isEmpty && !isLoadingMore ? ( ) : ( <> {unreadNotifications.map((n) => { const custom = renderTile?.(n, { onComplete, onSettle, liveDurationMs }) if (custom) return
{custom}
return ( ) })} {isLoadingMore && } {loadMore && hasMore && ) } function DrawerLoadingTiles() { return ( <>