{"version":3,"file":"use-notification-inbox.cjs","names":[],"sources":["../../../src/components/NotificationCenter/use-notification-inbox.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from \"react\";\n\n/** One entry in the inbox. */\nexport interface NotificationItem {\n    /** Stable id. Re-adding an existing id updates that entry instead of duplicating it. */\n    id: string;\n    title: string;\n    /** Body text. Optional — a title-only notification is a legitimate shape. */\n    body?: string;\n    /** Epoch milliseconds. Used for ordering and for the relative timestamp. */\n    receivedAt: number;\n    read?: boolean;\n    /** Where activating the notification should take the user. */\n    url?: string;\n    /** Free-form payload the app passes through, e.g. the entity it refers to. */\n    data?: Record<string, unknown>;\n}\n\nexport interface UseNotificationInboxOptions {\n    /** Entries the inbox starts with — typically what the API returned. */\n    initialItems?: readonly NotificationItem[];\n    /**\n     * Listen for `message` events from the service worker and add whatever\n     * arrives. Default `true`.\n     *\n     * This is the missing half of web push: a service worker runs outside the\n     * page and cannot touch React state, so a push that arrives while the app is\n     * open shows an OS notification and then vanishes as far as the UI is\n     * concerned. Have the worker `postMessage` the payload and it lands here.\n     */\n    listenToServiceWorker?: boolean;\n    /**\n     * Message `type` to accept from the worker. Default `\"tempest:notification\"`.\n     *\n     * Filtering by type matters because the SW message channel is shared — a\n     * sync-progress ping or a cache-updated notice would otherwise show up in the\n     * user's inbox.\n     */\n    messageType?: string;\n    /**\n     * Cap on stored entries; the oldest are dropped past it. Default `100`.\n     *\n     * An inbox fed by push grows without bound otherwise, and it lives in memory.\n     */\n    limit?: number;\n    /** Called whenever the list changes — the hook for persisting it. */\n    onChange?: (items: readonly NotificationItem[]) => void;\n}\n\nexport interface UseNotificationInboxResult {\n    items: NotificationItem[];\n    unreadCount: number;\n    /** Add an entry, or update the existing one with the same id. */\n    add: (item: NotificationItem) => void;\n    markRead: (id: string) => void;\n    markUnread: (id: string) => void;\n    markAllRead: () => void;\n    remove: (id: string) => void;\n    clear: () => void;\n}\n\n/** Coerce an unknown service-worker payload into an inbox entry. */\nfunction toItem(payload: Record<string, unknown>, index: number): NotificationItem | null {\n    const title = typeof payload.title === \"string\" ? payload.title : null;\n    if (!title) return null;\n    return {\n        id: typeof payload.id === \"string\" ? payload.id : `sw-${payload.tag ?? index}-${title}`,\n        title,\n        body: typeof payload.body === \"string\" ? payload.body : undefined,\n        receivedAt:\n            typeof payload.receivedAt === \"number\" ? payload.receivedAt : new Date().getTime(),\n        url: typeof payload.url === \"string\" ? payload.url : undefined,\n        data: (payload.data as Record<string, unknown> | undefined) ?? undefined,\n        read: false,\n    };\n}\n\n/**\n * Client-side inbox state for received notifications.\n *\n * Newest first, deduplicated by `id`, capped at `limit`. Pairs with\n * `NotificationCenter` for the UI and with the `push` module for the source:\n * point a service worker's `postMessage` at it and a push that arrives while the\n * app is open shows up in the inbox instead of only as an OS notification.\n *\n * Persistence is deliberately left out — where an inbox belongs (server, Dexie,\n * `localStorage`) is an app decision. Use `onChange` to write it wherever it goes,\n * and `initialItems` to read it back.\n *\n * @example\n * const inbox = useNotificationInbox({\n *     initialItems: await api.notifications.list(),\n *     onChange: (items) => storage.set(\"inbox\", items),\n * });\n *\n * <NotificationCenter\n *     items={inbox.items}\n *     onMarkRead={inbox.markRead}\n *     onMarkAllRead={inbox.markAllRead}\n *     onDismiss={inbox.remove}\n * />\n */\nexport function useNotificationInbox(\n    options: UseNotificationInboxOptions = {},\n): UseNotificationInboxResult {\n    const {\n        initialItems = [],\n        listenToServiceWorker = true,\n        messageType = \"tempest:notification\",\n        limit = 100,\n        onChange,\n    } = options;\n\n    const [items, setItems] = useState<NotificationItem[]>(() =>\n        [...initialItems].sort((a, b) => b.receivedAt - a.receivedAt).slice(0, limit),\n    );\n\n    const update = useCallback(\n        (next: (current: NotificationItem[]) => NotificationItem[]) => {\n            setItems((current) => {\n                const result = next(current);\n                onChange?.(result);\n                return result;\n            });\n        },\n        [onChange],\n    );\n\n    const add = useCallback(\n        (item: NotificationItem) => {\n            update((current) => {\n                const without = current.filter((entry) => entry.id !== item.id);\n                return [item, ...without]\n                    .sort((a, b) => b.receivedAt - a.receivedAt)\n                    .slice(0, limit);\n            });\n        },\n        [update, limit],\n    );\n\n    const setRead = useCallback(\n        (id: string, read: boolean) => {\n            update((current) =>\n                current.map((entry) => (entry.id === id ? { ...entry, read } : entry)),\n            );\n        },\n        [update],\n    );\n\n    const markRead = useCallback((id: string) => setRead(id, true), [setRead]);\n    const markUnread = useCallback((id: string) => setRead(id, false), [setRead]);\n\n    const markAllRead = useCallback(() => {\n        update((current) => current.map((entry) => ({ ...entry, read: true })));\n    }, [update]);\n\n    const remove = useCallback(\n        (id: string) => {\n            update((current) => current.filter((entry) => entry.id !== id));\n        },\n        [update],\n    );\n\n    const clear = useCallback(() => update(() => []), [update]);\n\n    useEffect(() => {\n        if (!listenToServiceWorker) return;\n        if (typeof navigator === \"undefined\" || !navigator.serviceWorker) return;\n\n        /**\n         * Captured, not re-read in the cleanup.\n         *\n         * Looking `navigator.serviceWorker` up again at teardown throws if it is\n         * gone by then, and the listener would leak either way — the container has\n         * to be the same object the listener was added to.\n         */\n        const container = navigator.serviceWorker;\n\n        const handler = (event: MessageEvent) => {\n            const payload = event.data as Record<string, unknown> | null;\n            if (!payload || payload.type !== messageType) return;\n            const raw = (payload.notification ?? payload.payload ?? payload) as Record<\n                string,\n                unknown\n            >;\n            const item = toItem(raw, 0);\n            if (item) add(item);\n        };\n\n        container.addEventListener(\"message\", handler);\n        return () => container.removeEventListener(\"message\", handler);\n    }, [listenToServiceWorker, messageType, add]);\n\n    const unreadCount = useMemo(() => items.filter((item) => !item.read).length, [items]);\n\n    return { items, unreadCount, add, markRead, markUnread, markAllRead, remove, clear };\n}\n"],"mappings":"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"}