'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 (
<>
>
)
}
function EmptyState() {
return (
)
}
interface ToggleRowProps {
checked: boolean
onChange: (value: boolean) => void
}
function ShowNotificationsToggleRow({ checked, onChange }: ToggleRowProps) {
return (
Show Notifications
Show pop-up messages for new alerts
)
}
/** Switching on prompts for browser permission and commits only when granted; denied renders disabled (can't re-prompt programmatically); unsupported (SSR, iOS Safari) hides the row. */
function DesktopNotificationsToggleRow({ checked, onChange }: ToggleRowProps) {
const { supported, permission, request } = useNotificationPermission()
if (!supported) return null
const blocked = permission === 'denied'
const handleChange = async (value: boolean) => {
if (!value || permission === 'granted') {
onChange(value)
return
}
if ((await request()) === 'granted') onChange(true)
}
return (
Desktop Notifications
{blocked
? 'Enable notifications in your browser settings'
: 'Notify when this tab is in the background'}
)
}
interface HistoryButtonProps {
onClick?: () => void
historyHref?: string
}
function NotificationsHistoryButton({ onClick, historyHref }: HistoryButtonProps) {
return (
}
groupAriaLabel="Notifications history"
iconAction={{
icon: ,
'aria-label': 'Open notifications history in a new tab',
href: historyHref,
openInNewTab: true,
disabled: !historyHref,
}}
>
Notifications History
)
}