@ttt-productions/notification-core 0.15.8 → 0.15.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export type { NotificationDoc, PendingNotification, NotificationCategoryConfig, NotificationTypeConfig, NotificationSystemConfig, UseActiveNotificationsOptions, UseNotificationHistoryOptions, NotificationHistoryItem, UseUnreadCountOptions, UseArchiveNotificationOptions, UseArchiveAllNotificationsOptions, ArchiveAllJobStatus, EnqueueArchiveAllResult, ArchiveAllJobSnapshot, ArchiveAllPollResult, NotificationRowActions, NotificationListProps, NotificationHistoryListProps, NotificationEmptyStateProps, NotificationUnreadBadgeProps, } from './types.js';
1
+ export type { NotificationDoc, PendingNotification, NotificationCategoryConfig, NotificationTypeConfig, NotificationSystemConfig, UseActiveNotificationsOptions, UseNotificationHistoryOptions, NotificationHistoryItem, UseUnreadCountOptions, UseArchiveNotificationOptions, UseArchiveAllNotificationsOptions, ArchiveAllJobStatus, EnqueueArchiveAllResult, ArchiveAllJobSnapshot, ArchiveAllPollResult, NotificationRowActions, NotificationArchiveResult, NotificationArchiveFn, NotificationListErrorState, NotificationListProps, NotificationHistoryListProps, NotificationEmptyStateProps, NotificationUnreadBadgeProps, } from './types.js';
2
2
  export { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,iCAAiC,EACjC,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,4BAA4B,EAC5B,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,iCAAiC,EACjC,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,0BAA0B,EAC1B,qBAAqB,EACrB,4BAA4B,EAC5B,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAuBA,kFAAkF;AAClF,6EAA6E;AAC7E,yCAAyC;AACzC,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC","sourcesContent":["export type {\n NotificationDoc,\n PendingNotification,\n NotificationCategoryConfig,\n NotificationTypeConfig,\n NotificationSystemConfig,\n UseActiveNotificationsOptions,\n UseNotificationHistoryOptions,\n NotificationHistoryItem,\n UseUnreadCountOptions,\n UseArchiveNotificationOptions,\n UseArchiveAllNotificationsOptions,\n ArchiveAllJobStatus,\n EnqueueArchiveAllResult,\n ArchiveAllJobSnapshot,\n ArchiveAllPollResult,\n NotificationRowActions,\n NotificationListProps,\n NotificationHistoryListProps,\n NotificationEmptyStateProps,\n NotificationUnreadBadgeProps,\n} from './types.js';\n\n// Runtime schemas for the generic notification doc shapes (notification-core owns\n// these; consuming apps compose them instead of hand-mirroring). Server- and\n// UI-safe (zod only, no React/firebase).\nexport { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA0BA,kFAAkF;AAClF,6EAA6E;AAC7E,yCAAyC;AACzC,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC","sourcesContent":["export type {\n NotificationDoc,\n PendingNotification,\n NotificationCategoryConfig,\n NotificationTypeConfig,\n NotificationSystemConfig,\n UseActiveNotificationsOptions,\n UseNotificationHistoryOptions,\n NotificationHistoryItem,\n UseUnreadCountOptions,\n UseArchiveNotificationOptions,\n UseArchiveAllNotificationsOptions,\n ArchiveAllJobStatus,\n EnqueueArchiveAllResult,\n ArchiveAllJobSnapshot,\n ArchiveAllPollResult,\n NotificationRowActions,\n NotificationArchiveResult,\n NotificationArchiveFn,\n NotificationListErrorState,\n NotificationListProps,\n NotificationHistoryListProps,\n NotificationEmptyStateProps,\n NotificationUnreadBadgeProps,\n} from './types.js';\n\n// Runtime schemas for the generic notification doc shapes (notification-core owns\n// these; consuming apps compose them instead of hand-mirroring). Server- and\n// UI-safe (zod only, no React/firebase).\nexport { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';\n"]}
@@ -1,9 +1,9 @@
1
1
  import type { NotificationHistoryListProps } from '../../types.js';
2
2
  /**
3
3
  * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are
4
- * inert (DJ ruling 2026-07-07) and read-only — archive is one-way, there is no
5
- * re-archive, so the row exposes no `archive` action. Any per-row control (e.g. an
4
+ * inert and read-only — archive is one-way, there is no re-archive, so the row
5
+ * exposes no `archive` action. Any per-row control (e.g. an
6
6
  * ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
7
7
  */
8
- export declare function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, title, renderRowAction, }: NotificationHistoryListProps): import("react").JSX.Element;
8
+ export declare function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, renderError, title, renderRowAction, }: NotificationHistoryListProps): import("react").JSX.Element;
9
9
  //# sourceMappingURL=NotificationHistoryList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA2B,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BAqF9B"}
1
+ {"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA2B,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,WAAW,EACX,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BAuG9B"}
@@ -7,22 +7,35 @@ import { NotificationEmptyState } from './NotificationEmptyState.js';
7
7
  import { formatRelativeTime } from './relative-time.js';
8
8
  /**
9
9
  * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are
10
- * inert (DJ ruling 2026-07-07) and read-only — archive is one-way, there is no
11
- * re-archive, so the row exposes no `archive` action. Any per-row control (e.g. an
10
+ * inert and read-only — archive is one-way, there is no re-archive, so the row
11
+ * exposes no `archive` action. Any per-row control (e.g. an
12
12
  * ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
13
13
  */
14
- export function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, title, renderRowAction, }) {
15
- const { data: notifications, isLoading, isFetching, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useNotificationHistory({
14
+ export function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, renderError, title, renderRowAction, }) {
15
+ const { data: notifications, isLoading, isFetching, isError, error, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useNotificationHistory({
16
16
  config,
17
17
  userId,
18
18
  category,
19
19
  pageSize,
20
20
  staleTime,
21
21
  });
22
+ const hasRows = !!notifications && notifications.length > 0;
23
+ // A later page that came back empty keeps its pager so the user can step back; the
24
+ // empty state is the answer for page 1 only.
25
+ const showEmptyState = !isError && notifications !== undefined && notifications.length === 0 && page === 1;
22
26
  const getTypeIcon = useCallback((type) => config.types[type]?.icon ?? '🔔', [config]);
23
- return (_jsxs("div", { className: "ntf-list ntf-list-history", children: [title != null && (_jsxs(_Fragment, { children: [_jsx("div", { className: "ntf-list-header ntf-list-header-read-only", children: _jsx("div", { className: "ntf-list-title", children: title }) }), _jsx(Separator, {})] })), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : !notifications || notifications.length === 0 ? (_jsx(NotificationEmptyState, { text: emptyText })) : (_jsxs(_Fragment, { children: [notifications.map((notification) => {
27
+ return (_jsxs("div", { className: "ntf-list ntf-list-history", children: [title != null && (_jsxs(_Fragment, { children: [_jsx("div", { className: "ntf-list-header ntf-list-header-read-only", children: _jsx("div", { className: "ntf-list-title", children: title }) }), _jsx(Separator, {})] })), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => {
24
28
  return (_jsxs("div", { className: "ntf-item ntf-item-archived", children: [_jsx("div", { className: "ntf-item-icon", children: getTypeIcon(notification.type) }), _jsxs("div", { className: "ntf-item-content", children: [_jsx("div", { className: "ntf-item-title", children: notification.title }), _jsx("div", { className: "ntf-item-message", children: notification.message }), _jsx("div", { className: "ntf-item-timestamp", children: formatRelativeTime(notification.archivedAt) })] }), notification.count > 1 && (_jsx("div", { className: "ntf-item-count", children: _jsxs(Badge, { variant: "secondary", children: ["\u00D7", notification.count] }) })), renderRowAction && (_jsx("div", { className: "ntf-item-row-action", children: renderRowAction(notification, {}) }))] }, notification.archiveOccurrenceId));
25
- }), _jsx(ListPagination, { pagination: {
29
+ }), isError && error
30
+ ? renderError({
31
+ error,
32
+ retry: () => {
33
+ void refetch();
34
+ },
35
+ retrying: isFetching,
36
+ hasRows,
37
+ })
38
+ : null, showEmptyState && _jsx(NotificationEmptyState, { text: emptyText }), _jsx(ListPagination, { pagination: {
26
39
  currentPage: page,
27
40
  canPreviousPage: hasPrevPage,
28
41
  canNextPage: hasNextPage,
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationHistoryList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useNotificationHistory } from '../hooks/useNotificationHistory.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationHistoryItem, NotificationHistoryListProps } from '../../types.js';\n\n/**\n * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are\n * inert (DJ ruling 2026-07-07) and read-only — archive is one-way, there is no\n * re-archive, so the row exposes no `archive` action. Any per-row control (e.g. an\n * ArrowRight \"go to\") is rendered by the consumer via `renderRowAction`.\n */\nexport function NotificationHistoryList({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n emptyText,\n title,\n renderRowAction,\n}: NotificationHistoryListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useNotificationHistory({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n });\n\n const getTypeIcon = useCallback(\n (type: string) => config.types[type]?.icon ?? '🔔',\n [config],\n );\n\n return (\n <div className=\"ntf-list ntf-list-history\">\n {title != null && (\n <>\n <div className=\"ntf-list-header ntf-list-header-read-only\">\n <div className=\"ntf-list-title\">{title}</div>\n </div>\n <Separator />\n </>\n )}\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : !notifications || notifications.length === 0 ? (\n <NotificationEmptyState text={emptyText} />\n ) : (\n <>\n {notifications.map((notification: NotificationHistoryItem) => {\n return (\n <div\n key={notification.archiveOccurrenceId}\n className=\"ntf-item ntf-item-archived\"\n >\n <div className=\"ntf-item-icon\">{getTypeIcon(notification.type)}</div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.archivedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {})}\n </div>\n )}\n </div>\n );\n })}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"NotificationHistoryList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useNotificationHistory } from '../hooks/useNotificationHistory.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationHistoryItem, NotificationHistoryListProps } from '../../types.js';\n\n/**\n * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are\n * inert and read-only — archive is one-way, there is no re-archive, so the row\n * exposes no `archive` action. Any per-row control (e.g. an\n * ArrowRight \"go to\") is rendered by the consumer via `renderRowAction`.\n */\nexport function NotificationHistoryList({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n emptyText,\n renderError,\n title,\n renderRowAction,\n}: NotificationHistoryListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n isError,\n error,\n refetch,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useNotificationHistory({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n });\n\n const hasRows = !!notifications && notifications.length > 0;\n // A later page that came back empty keeps its pager so the user can step back; the\n // empty state is the answer for page 1 only.\n const showEmptyState =\n !isError && notifications !== undefined && notifications.length === 0 && page === 1;\n\n const getTypeIcon = useCallback(\n (type: string) => config.types[type]?.icon ?? '🔔',\n [config],\n );\n\n return (\n <div className=\"ntf-list ntf-list-history\">\n {title != null && (\n <>\n <div className=\"ntf-list-header ntf-list-header-read-only\">\n <div className=\"ntf-list-title\">{title}</div>\n </div>\n <Separator />\n </>\n )}\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : (\n <>\n {(notifications ?? []).map((notification: NotificationHistoryItem) => {\n return (\n <div\n key={notification.archiveOccurrenceId}\n className=\"ntf-item ntf-item-archived\"\n >\n <div className=\"ntf-item-icon\">{getTypeIcon(notification.type)}</div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.archivedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {})}\n </div>\n )}\n </div>\n );\n })}\n {isError && error\n ? renderError({\n error,\n retry: () => {\n void refetch();\n },\n retrying: isFetching,\n hasRows,\n })\n : null}\n {showEmptyState && <NotificationEmptyState text={emptyText} />}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
@@ -2,5 +2,5 @@ import type { NotificationListProps } from '../../types.js';
2
2
  /**
3
3
  * Scrollable list of active notifications with click-to-archive and clear-all.
4
4
  */
5
- export declare function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, emptyText, renderRowAction, }: NotificationListProps): import("react").JSX.Element;
5
+ export declare function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, staleTime, emptyText, renderError, onRenderedRowsChange, renderRowAction, }: NotificationListProps): import("react").JSX.Element;
6
6
  //# sourceMappingURL=NotificationList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAE7E;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,eAAe,GAChB,EAAE,qBAAqB,+BAuLvB"}
1
+ {"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAE7E;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,SAAS,EACT,WAAW,EACX,oBAAoB,EACpB,eAAe,GAChB,EAAE,qBAAqB,+BAmNvB"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useState } from 'react';
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
4
  import { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';
5
5
  import { useActiveNotifications } from '../hooks/useActiveNotifications.js';
6
6
  import { useArchiveNotification } from '../hooks/useArchiveNotification.js';
@@ -10,12 +10,13 @@ import { formatRelativeTime } from './relative-time.js';
10
10
  /**
11
11
  * Scrollable list of active notifications with click-to-archive and clear-all.
12
12
  */
13
- export function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, emptyText, renderRowAction, }) {
14
- const { data: notifications, isLoading, isFetching, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useActiveNotifications({
13
+ export function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, staleTime, emptyText, renderError, onRenderedRowsChange, renderRowAction, }) {
14
+ const { data: notifications, isLoading, isFetching, isError, error, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useActiveNotifications({
15
15
  config,
16
16
  userId,
17
17
  category,
18
18
  refetchInterval,
19
+ staleTime,
19
20
  });
20
21
  const archiveMutation = useArchiveNotification({
21
22
  userId,
@@ -36,9 +37,15 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
36
37
  const [clearIncomplete, setClearIncomplete] = useState(false);
37
38
  const [pendingArchiveIds, setPendingArchiveIds] = useState(() => new Set());
38
39
  const [pendingClearAllIds, setPendingClearAllIds] = useState(() => new Set());
39
- // Successful archive requests stay visibly pending until the authoritative
40
- // active-notification query removes each row. A callable response alone does
41
- // not prove the listener/cache has reconciled yet.
40
+ const renderedRowsRef = useRef(onRenderedRowsChange);
41
+ renderedRowsRef.current = onRenderedRowsChange;
42
+ useEffect(() => {
43
+ if (notifications)
44
+ renderedRowsRef.current?.(notifications);
45
+ }, [notifications]);
46
+ // An archive the server confirmed stays visibly pending until the authoritative
47
+ // active-notification query removes the row: the callable's answer alone does not
48
+ // prove the read has caught up.
42
49
  useEffect(() => {
43
50
  if (!notifications)
44
51
  return;
@@ -50,22 +57,27 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
50
57
  setPendingArchiveIds(keepActive);
51
58
  setPendingClearAllIds(keepActive);
52
59
  }, [notifications]);
53
- const archiveOne = useCallback(async (notificationId) => {
60
+ const archiveOne = useCallback(async (notification) => {
61
+ const clearPending = () => setPendingArchiveIds((current) => {
62
+ const next = new Set(current);
63
+ next.delete(notification.id);
64
+ return next;
65
+ });
54
66
  setPendingArchiveIds((current) => {
55
67
  const next = new Set(current);
56
- next.add(notificationId);
68
+ next.add(notification.id);
57
69
  return next;
58
70
  });
59
71
  try {
60
- await archiveMutation.mutateAsync(notificationId);
72
+ const result = await archiveMutation.mutateAsync(notification);
73
+ // Nothing was archived (the card changed after it was rendered): the row stays
74
+ // active, so it is not left looking like it is clearing.
75
+ if (!result.archived)
76
+ clearPending();
61
77
  }
62
- catch (error) {
63
- setPendingArchiveIds((current) => {
64
- const next = new Set(current);
65
- next.delete(notificationId);
66
- return next;
67
- });
68
- throw error;
78
+ catch (archiveError) {
79
+ clearPending();
80
+ throw archiveError;
69
81
  }
70
82
  }, [archiveMutation]);
71
83
  const handleClearAll = useCallback(async () => {
@@ -95,11 +107,23 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
95
107
  return typeConfig?.icon ?? '🔔';
96
108
  }, [config]);
97
109
  const hasNotifications = !!notifications && notifications.length > 0;
110
+ // A later page that came back empty keeps its pager so the user can step back; the
111
+ // empty state is the answer for page 1 only.
112
+ const showEmptyState = !isError && notifications !== undefined && notifications.length === 0 && page === 1;
98
113
  const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;
99
- return (_jsxs("div", { className: "ntf-list", children: [_jsxs("div", { className: "ntf-list-header", children: [title != null && _jsx("div", { className: "ntf-list-title", children: title }), _jsx(Button, { className: "ntf-list-clear-all", variant: "ghost", size: "sm", onClick: handleClearAll, disabled: !hasNotifications, pending: isClearAllPending, children: isClearAllPending ? 'Clearing...' : 'Clear All' }), clearIncomplete && !isClearAllPending && (_jsx("div", { className: "ntf-list-clear-error", role: "status", children: "Some notifications remain \u2014 try again." }))] }), _jsx(Separator, {}), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : !notifications || notifications.length === 0 ? (_jsx(NotificationEmptyState, { text: emptyText })) : (_jsxs(_Fragment, { children: [notifications.map((notification) => (_jsxs("div", { className: "ntf-item", children: [_jsx("div", { className: "ntf-item-icon", children: getTypeIcon(notification.type) }), _jsxs("div", { className: "ntf-item-content", children: [_jsx("div", { className: "ntf-item-title", children: notification.title }), _jsx("div", { className: "ntf-item-message", children: notification.message }), _jsx("div", { className: "ntf-item-timestamp", children: formatRelativeTime(notification.updatedAt) })] }), notification.count > 1 && (_jsx("div", { className: "ntf-item-count", children: _jsxs(Badge, { variant: "secondary", children: ["\u00D7", notification.count] }) })), renderRowAction && (_jsx("div", { className: "ntf-item-row-action", children: renderRowAction(notification, {
100
- archive: () => archiveOne(notification.id),
114
+ return (_jsxs("div", { className: "ntf-list", children: [_jsxs("div", { className: "ntf-list-header", children: [title != null && _jsx("div", { className: "ntf-list-title", children: title }), _jsx(Button, { className: "ntf-list-clear-all", variant: "ghost", size: "sm", onClick: handleClearAll, disabled: !hasNotifications, pending: isClearAllPending, children: isClearAllPending ? 'Clearing...' : 'Clear All' }), clearIncomplete && !isClearAllPending && (_jsx("div", { className: "ntf-list-clear-error", role: "status", children: "Some notifications remain \u2014 try again." }))] }), _jsx(Separator, {}), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => (_jsxs("div", { className: "ntf-item", children: [_jsx("div", { className: "ntf-item-icon", children: getTypeIcon(notification.type) }), _jsxs("div", { className: "ntf-item-content", children: [_jsx("div", { className: "ntf-item-title", children: notification.title }), _jsx("div", { className: "ntf-item-message", children: notification.message }), _jsx("div", { className: "ntf-item-timestamp", children: formatRelativeTime(notification.updatedAt) })] }), notification.count > 1 && (_jsx("div", { className: "ntf-item-count", children: _jsxs(Badge, { variant: "secondary", children: ["\u00D7", notification.count] }) })), renderRowAction && (_jsx("div", { className: "ntf-item-row-action", children: renderRowAction(notification, {
115
+ archive: () => archiveOne(notification),
101
116
  isArchivePending: pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),
102
- }) }))] }, notification.id))), _jsx(ListPagination, { pagination: {
117
+ }) }))] }, notification.id))), isError && error
118
+ ? renderError({
119
+ error,
120
+ retry: () => {
121
+ void refetch();
122
+ },
123
+ retrying: isFetching,
124
+ hasRows: hasNotifications,
125
+ })
126
+ : null, showEmptyState && _jsx(NotificationEmptyState, { text: emptyText }), _jsx(ListPagination, { pagination: {
103
127
  currentPage: page,
104
128
  canPreviousPage: hasPrevPage,
105
129
  canNextPage: hasNextPage,
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useState } from 'react';\nimport { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useActiveNotifications } from '../hooks/useActiveNotifications.js';\nimport { useArchiveNotification } from '../hooks/useArchiveNotification.js';\nimport { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationDoc, NotificationListProps } from '../../types.js';\n\n/**\n * Scrollable list of active notifications with click-to-archive and clear-all.\n */\nexport function NotificationList({\n config,\n userId,\n category,\n archiveFn,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n title,\n onClearAll,\n refetchInterval,\n emptyText,\n renderRowAction,\n}: NotificationListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useActiveNotifications({\n config,\n userId,\n category,\n refetchInterval,\n });\n\n const archiveMutation = useArchiveNotification({\n userId,\n category,\n archiveFn,\n });\n\n const archiveAllMutation = useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n });\n\n // Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing\n // side effects the app wires to it) fires ONLY when the server job fully drained the category —\n // otherwise the user keeps the \"notifications remain — try again\" affordance. The poller resolves\n // with an explicit terminal result (`complete` | `incomplete` | `failed`) and only rejects on a\n // hard enqueue/poll throw.\n const [clearIncomplete, setClearIncomplete] = useState(false);\n const [pendingArchiveIds, setPendingArchiveIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const [pendingClearAllIds, setPendingClearAllIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n\n // Successful archive requests stay visibly pending until the authoritative\n // active-notification query removes each row. A callable response alone does\n // not prove the listener/cache has reconciled yet.\n useEffect(() => {\n if (!notifications) return;\n const activeIds = new Set(notifications.map((notification) => notification.id));\n const keepActive = (current: ReadonlySet<string>) => {\n const next = new Set([...current].filter((id) => activeIds.has(id)));\n return next.size === current.size ? current : next;\n };\n setPendingArchiveIds(keepActive);\n setPendingClearAllIds(keepActive);\n }, [notifications]);\n\n const archiveOne = useCallback(async (notificationId: string) => {\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.add(notificationId);\n return next;\n });\n try {\n await archiveMutation.mutateAsync(notificationId);\n } catch (error) {\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.delete(notificationId);\n return next;\n });\n throw error;\n }\n }, [archiveMutation]);\n\n const handleClearAll = useCallback(async () => {\n setClearIncomplete(false);\n setPendingClearAllIds(new Set((notifications ?? []).map((notification) => notification.id)));\n let result;\n try {\n result = await archiveAllMutation.mutateAsync();\n } catch {\n // Hard failure (enqueue/poll threw): do not fire onClearAll; surface the retry affordance.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n if (!result.complete) {\n // Job terminated without fully draining the category (incomplete/failed) — keep the\n // notifications and prompt a retry.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n onClearAll?.();\n }, [archiveAllMutation, notifications, onClearAll]);\n\n const getTypeIcon = useCallback(\n (type: string) => {\n const typeConfig = config.types[type];\n return typeConfig?.icon ?? '🔔';\n },\n [config],\n );\n\n const hasNotifications = !!notifications && notifications.length > 0;\n const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\n\n return (\n <div className=\"ntf-list\">\n <div className=\"ntf-list-header\">\n {title != null && <div className=\"ntf-list-title\">{title}</div>}\n <Button\n className=\"ntf-list-clear-all\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClearAll}\n disabled={!hasNotifications}\n pending={isClearAllPending}\n >\n {isClearAllPending ? 'Clearing...' : 'Clear All'}\n </Button>\n {clearIncomplete && !isClearAllPending && (\n <div className=\"ntf-list-clear-error\" role=\"status\">\n Some notifications remain — try again.\n </div>\n )}\n </div>\n <Separator />\n\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : !notifications || notifications.length === 0 ? (\n <NotificationEmptyState text={emptyText} />\n ) : (\n <>\n {notifications.map((notification: NotificationDoc) => (\n <div key={notification.id} className=\"ntf-item\">\n <div className=\"ntf-item-icon\">\n {getTypeIcon(notification.type)}\n </div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.updatedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {\n archive: () => archiveOne(notification.id),\n isArchivePending:\n pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),\n })}\n </div>\n )}\n </div>\n ))}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useActiveNotifications } from '../hooks/useActiveNotifications.js';\nimport { useArchiveNotification } from '../hooks/useArchiveNotification.js';\nimport { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationDoc, NotificationListProps } from '../../types.js';\n\n/**\n * Scrollable list of active notifications with click-to-archive and clear-all.\n */\nexport function NotificationList({\n config,\n userId,\n category,\n archiveFn,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n title,\n onClearAll,\n refetchInterval,\n staleTime,\n emptyText,\n renderError,\n onRenderedRowsChange,\n renderRowAction,\n}: NotificationListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n isError,\n error,\n refetch,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useActiveNotifications({\n config,\n userId,\n category,\n refetchInterval,\n staleTime,\n });\n\n const archiveMutation = useArchiveNotification({\n userId,\n category,\n archiveFn,\n });\n\n const archiveAllMutation = useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n });\n\n // Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing\n // side effects the app wires to it) fires ONLY when the server job fully drained the category —\n // otherwise the user keeps the \"notifications remain — try again\" affordance. The poller resolves\n // with an explicit terminal result (`complete` | `incomplete` | `failed`) and only rejects on a\n // hard enqueue/poll throw.\n const [clearIncomplete, setClearIncomplete] = useState(false);\n const [pendingArchiveIds, setPendingArchiveIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const [pendingClearAllIds, setPendingClearAllIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n\n const renderedRowsRef = useRef(onRenderedRowsChange);\n renderedRowsRef.current = onRenderedRowsChange;\n useEffect(() => {\n if (notifications) renderedRowsRef.current?.(notifications);\n }, [notifications]);\n\n // An archive the server confirmed stays visibly pending until the authoritative\n // active-notification query removes the row: the callable's answer alone does not\n // prove the read has caught up.\n useEffect(() => {\n if (!notifications) return;\n const activeIds = new Set(notifications.map((notification) => notification.id));\n const keepActive = (current: ReadonlySet<string>) => {\n const next = new Set([...current].filter((id) => activeIds.has(id)));\n return next.size === current.size ? current : next;\n };\n setPendingArchiveIds(keepActive);\n setPendingClearAllIds(keepActive);\n }, [notifications]);\n\n const archiveOne = useCallback(async (notification: NotificationDoc) => {\n const clearPending = () =>\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.delete(notification.id);\n return next;\n });\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.add(notification.id);\n return next;\n });\n try {\n const result = await archiveMutation.mutateAsync(notification);\n // Nothing was archived (the card changed after it was rendered): the row stays\n // active, so it is not left looking like it is clearing.\n if (!result.archived) clearPending();\n } catch (archiveError) {\n clearPending();\n throw archiveError;\n }\n }, [archiveMutation]);\n\n const handleClearAll = useCallback(async () => {\n setClearIncomplete(false);\n setPendingClearAllIds(new Set((notifications ?? []).map((notification) => notification.id)));\n let result;\n try {\n result = await archiveAllMutation.mutateAsync();\n } catch {\n // Hard failure (enqueue/poll threw): do not fire onClearAll; surface the retry affordance.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n if (!result.complete) {\n // Job terminated without fully draining the category (incomplete/failed) — keep the\n // notifications and prompt a retry.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n onClearAll?.();\n }, [archiveAllMutation, notifications, onClearAll]);\n\n const getTypeIcon = useCallback(\n (type: string) => {\n const typeConfig = config.types[type];\n return typeConfig?.icon ?? '🔔';\n },\n [config],\n );\n\n const hasNotifications = !!notifications && notifications.length > 0;\n // A later page that came back empty keeps its pager so the user can step back; the\n // empty state is the answer for page 1 only.\n const showEmptyState =\n !isError && notifications !== undefined && notifications.length === 0 && page === 1;\n const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\n\n return (\n <div className=\"ntf-list\">\n <div className=\"ntf-list-header\">\n {title != null && <div className=\"ntf-list-title\">{title}</div>}\n <Button\n className=\"ntf-list-clear-all\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClearAll}\n disabled={!hasNotifications}\n pending={isClearAllPending}\n >\n {isClearAllPending ? 'Clearing...' : 'Clear All'}\n </Button>\n {clearIncomplete && !isClearAllPending && (\n <div className=\"ntf-list-clear-error\" role=\"status\">\n Some notifications remain — try again.\n </div>\n )}\n </div>\n <Separator />\n\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : (\n <>\n {(notifications ?? []).map((notification: NotificationDoc) => (\n <div key={notification.id} className=\"ntf-item\">\n <div className=\"ntf-item-icon\">\n {getTypeIcon(notification.type)}\n </div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.updatedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {\n archive: () => archiveOne(notification),\n isArchivePending:\n pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),\n })}\n </div>\n )}\n </div>\n ))}\n {isError && error\n ? renderError({\n error,\n retry: () => {\n void refetch();\n },\n retrying: isFetching,\n hasRows: hasNotifications,\n })\n : null}\n {showEmptyState && <NotificationEmptyState text={emptyText} />}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import type { NotificationDoc, UseActiveNotificationsOptions } from '../../types.js';
2
2
  /**
3
- * Paginated fetch of active (unread) notifications.
4
- * Uses polling (refetchInterval) instead of real-time subscriptions for cost control.
3
+ * Paginated read of active notifications. The displayed page is re-read every
4
+ * `refetchInterval` ms while mounted — polling, not a realtime listener, for cost.
5
5
  */
6
- export declare function useActiveNotifications({ config, userId, category, enabled, pageSize, refetchInterval, }: UseActiveNotificationsOptions): import("@ttt-productions/query-core/react").UseFirestorePaginatedResult<NotificationDoc>;
6
+ export declare function useActiveNotifications({ config, userId, category, enabled, pageSize, refetchInterval, staleTime, }: UseActiveNotificationsOptions): import("@ttt-productions/query-core/react").UseFirestorePaginatedResult<NotificationDoc>;
7
7
  //# sourceMappingURL=useActiveNotifications.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAKrF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,eAA0C,GAC3C,EAAE,6BAA6B,4FAsB/B"}
1
+ {"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAMrF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,eAA0C,EAC1C,SAA8B,GAC/B,EAAE,6BAA6B,4FAuB/B"}
@@ -3,11 +3,12 @@ import { useFirestorePaginated } from '@ttt-productions/query-core/react';
3
3
  import { orderBy, where } from 'firebase/firestore';
4
4
  const DEFAULT_PAGE_SIZE = 20;
5
5
  const DEFAULT_REFETCH_INTERVAL = 30_000;
6
+ const DEFAULT_STALE_TIME = 30_000;
6
7
  /**
7
- * Paginated fetch of active (unread) notifications.
8
- * Uses polling (refetchInterval) instead of real-time subscriptions for cost control.
8
+ * Paginated read of active notifications. The displayed page is re-read every
9
+ * `refetchInterval` ms while mounted — polling, not a realtime listener, for cost.
9
10
  */
10
- export function useActiveNotifications({ config, userId, category, enabled = true, pageSize = DEFAULT_PAGE_SIZE, refetchInterval = DEFAULT_REFETCH_INTERVAL, }) {
11
+ export function useActiveNotifications({ config, userId, category, enabled = true, pageSize = DEFAULT_PAGE_SIZE, refetchInterval = DEFAULT_REFETCH_INTERVAL, staleTime = DEFAULT_STALE_TIME, }) {
11
12
  const categoryConfig = config.categories[category];
12
13
  if (!categoryConfig) {
13
14
  throw new Error(`[notification-core] Unknown category: ${category}`);
@@ -24,7 +25,8 @@ export function useActiveNotifications({ config, userId, category, enabled = tru
24
25
  constraints,
25
26
  pageSize,
26
27
  enabled: enabled && !!userId,
27
- staleTime: refetchInterval,
28
+ refetchInterval,
29
+ staleTime,
28
30
  });
29
31
  }
30
32
  //# sourceMappingURL=useActiveNotifications.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useActiveNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAwB,MAAM,oBAAoB,CAAC;AAG1E,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAExC;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,QAAQ,GAAG,iBAAiB,EAC5B,eAAe,GAAG,wBAAwB,GACZ;IAC9B,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACnD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,yCAAyC,QAAQ,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,cAAc,GAAG,cAAc,CAAC,UAAU,CAAC;IACjD,MAAM,UAAU,GAAG,cAAc,CAAC,YAAY,KAAK,UAAU,CAAC;IAE9D,MAAM,WAAW,GAAsB;QACrC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC;KAC7B,CAAC;IAEF,OAAO,qBAAqB,CAAkB;QAC5C,cAAc;QACd,QAAQ,EAAE,CAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC;QACrE,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,SAAS,EAAE,eAAe;KAC3B,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useFirestorePaginated } from '@ttt-productions/query-core/react';\nimport { orderBy, where, type QueryConstraint } from 'firebase/firestore';\nimport type { NotificationDoc, UseActiveNotificationsOptions } from '../../types.js';\n\nconst DEFAULT_PAGE_SIZE = 20;\nconst DEFAULT_REFETCH_INTERVAL = 30_000;\n\n/**\n * Paginated fetch of active (unread) notifications.\n * Uses polling (refetchInterval) instead of real-time subscriptions for cost control.\n */\nexport function useActiveNotifications({\n config,\n userId,\n category,\n enabled = true,\n pageSize = DEFAULT_PAGE_SIZE,\n refetchInterval = DEFAULT_REFETCH_INTERVAL,\n}: UseActiveNotificationsOptions) {\n const categoryConfig = config.categories[category];\n if (!categoryConfig) {\n throw new Error(`[notification-core] Unknown category: ${category}`);\n }\n\n const collectionPath = categoryConfig.activePath;\n const isPersonal = categoryConfig.audienceType === 'personal';\n\n const constraints: QueryConstraint[] = [\n ...(isPersonal ? [where('targetUserId', '==', userId)] : []),\n orderBy('updatedAt', 'desc'),\n ];\n\n return useFirestorePaginated<NotificationDoc>({\n collectionPath,\n queryKey: ['notifications', 'active', category, userId, { pageSize }],\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n staleTime: refetchInterval,\n });\n}\n"]}
1
+ {"version":3,"file":"useActiveNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAwB,MAAM,oBAAoB,CAAC;AAG1E,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,QAAQ,GAAG,iBAAiB,EAC5B,eAAe,GAAG,wBAAwB,EAC1C,SAAS,GAAG,kBAAkB,GACA;IAC9B,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACnD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,yCAAyC,QAAQ,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,cAAc,GAAG,cAAc,CAAC,UAAU,CAAC;IACjD,MAAM,UAAU,GAAG,cAAc,CAAC,YAAY,KAAK,UAAU,CAAC;IAE9D,MAAM,WAAW,GAAsB;QACrC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC;KAC7B,CAAC;IAEF,OAAO,qBAAqB,CAAkB;QAC5C,cAAc;QACd,QAAQ,EAAE,CAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC;QACrE,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,eAAe;QACf,SAAS;KACV,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useFirestorePaginated } from '@ttt-productions/query-core/react';\nimport { orderBy, where, type QueryConstraint } from 'firebase/firestore';\nimport type { NotificationDoc, UseActiveNotificationsOptions } from '../../types.js';\n\nconst DEFAULT_PAGE_SIZE = 20;\nconst DEFAULT_REFETCH_INTERVAL = 30_000;\nconst DEFAULT_STALE_TIME = 30_000;\n\n/**\n * Paginated read of active notifications. The displayed page is re-read every\n * `refetchInterval` ms while mounted — polling, not a realtime listener, for cost.\n */\nexport function useActiveNotifications({\n config,\n userId,\n category,\n enabled = true,\n pageSize = DEFAULT_PAGE_SIZE,\n refetchInterval = DEFAULT_REFETCH_INTERVAL,\n staleTime = DEFAULT_STALE_TIME,\n}: UseActiveNotificationsOptions) {\n const categoryConfig = config.categories[category];\n if (!categoryConfig) {\n throw new Error(`[notification-core] Unknown category: ${category}`);\n }\n\n const collectionPath = categoryConfig.activePath;\n const isPersonal = categoryConfig.audienceType === 'personal';\n\n const constraints: QueryConstraint[] = [\n ...(isPersonal ? [where('targetUserId', '==', userId)] : []),\n orderBy('updatedAt', 'desc'),\n ];\n\n return useFirestorePaginated<NotificationDoc>({\n collectionPath,\n queryKey: ['notifications', 'active', category, userId, { pageSize }],\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n refetchInterval,\n staleTime,\n });\n}\n"]}
@@ -1,24 +1,11 @@
1
- import type { UseArchiveNotificationOptions } from '../../types.js';
1
+ import type { NotificationArchiveResult, NotificationDoc, UseArchiveNotificationOptions } from '../../types.js';
2
2
  /**
3
- * Archive a single notification through an app-supplied callable adapter.
3
+ * Archive a single rendered notification through an app-supplied callable adapter.
4
4
  *
5
- * The notification system is Cloud-Functions-only: clients never write
6
- * notification docs. This hook performs no Firestore writes — it delegates to
7
- * `archiveFn` (wired by the app to `httpsCallable(functions, 'archiveNotification')`)
8
- * and invalidates the read keys on success.
9
- *
10
- * @example
11
- * ```tsx
12
- * const archiveNotification = httpsCallable(functions, 'archiveNotification');
13
- * const archive = useArchiveNotification({
14
- * userId: user.uid,
15
- * category: 'user',
16
- * archiveFn: (notificationId) =>
17
- * archiveNotification({ category: 'user', scope: { kind: 'single', notificationId } }),
18
- * });
19
- *
20
- * archive.mutate('abc123');
21
- * ```
5
+ * The notification system is Cloud-Functions-only: clients never write notification
6
+ * docs. This hook performs no Firestore writes — it delegates to `archiveFn` with the
7
+ * row as rendered, resolves the adapter's `{ archived }` answer, and invalidates the
8
+ * read keys on success.
22
9
  */
23
- export declare function useArchiveNotification({ userId, category, archiveFn, invalidateKeys, }: UseArchiveNotificationOptions): import("@tanstack/react-query").UseMutationResult<unknown, Error, string, unknown>;
10
+ export declare function useArchiveNotification({ userId, category, archiveFn, invalidateKeys, }: UseArchiveNotificationOptions): import("@tanstack/react-query").UseMutationResult<NotificationArchiveResult, Error, NotificationDoc, unknown>;
24
11
  //# sourceMappingURL=useArchiveNotification.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useArchiveNotification.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveNotification.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,QAAQ,EACR,SAAS,EACT,cAAc,GACf,EAAE,6BAA6B,sFAkB/B"}
1
+ {"version":3,"file":"useArchiveNotification.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveNotification.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,yBAAyB,EACzB,eAAe,EACf,6BAA6B,EAC9B,MAAM,gBAAgB,CAAC;AAExB;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,QAAQ,EACR,SAAS,EACT,cAAc,GACf,EAAE,6BAA6B,iHAkB/B"}
@@ -1,25 +1,12 @@
1
1
  'use client';
2
2
  import { useMutation, useQueryClient } from '@tanstack/react-query';
3
3
  /**
4
- * Archive a single notification through an app-supplied callable adapter.
4
+ * Archive a single rendered notification through an app-supplied callable adapter.
5
5
  *
6
- * The notification system is Cloud-Functions-only: clients never write
7
- * notification docs. This hook performs no Firestore writes — it delegates to
8
- * `archiveFn` (wired by the app to `httpsCallable(functions, 'archiveNotification')`)
9
- * and invalidates the read keys on success.
10
- *
11
- * @example
12
- * ```tsx
13
- * const archiveNotification = httpsCallable(functions, 'archiveNotification');
14
- * const archive = useArchiveNotification({
15
- * userId: user.uid,
16
- * category: 'user',
17
- * archiveFn: (notificationId) =>
18
- * archiveNotification({ category: 'user', scope: { kind: 'single', notificationId } }),
19
- * });
20
- *
21
- * archive.mutate('abc123');
22
- * ```
6
+ * The notification system is Cloud-Functions-only: clients never write notification
7
+ * docs. This hook performs no Firestore writes — it delegates to `archiveFn` with the
8
+ * row as rendered, resolves the adapter's `{ archived }` answer, and invalidates the
9
+ * read keys on success.
23
10
  */
24
11
  export function useArchiveNotification({ userId, category, archiveFn, invalidateKeys, }) {
25
12
  const queryClient = useQueryClient();
@@ -29,7 +16,7 @@ export function useArchiveNotification({ userId, category, archiveFn, invalidate
29
16
  ['notifications', 'history', category, userId],
30
17
  ];
31
18
  return useMutation({
32
- mutationFn: async (notificationId) => archiveFn(notificationId),
19
+ mutationFn: (notification) => archiveFn(notification),
33
20
  onSuccess: () => {
34
21
  const keysToInvalidate = invalidateKeys ?? defaultInvalidateKeys;
35
22
  keysToInvalidate.forEach((key) => {
@@ -1 +1 @@
1
- {"version":3,"file":"useArchiveNotification.js","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveNotification.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGpE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,QAAQ,EACR,SAAS,EACT,cAAc,GACgB;IAC9B,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,MAAM,qBAAqB,GAAG;QAC5B,CAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC;QAC7C,CAAC,eAAe,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC;QACnD,CAAC,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC;KAC/C,CAAC;IAEF,OAAO,WAAW,CAAC;QACjB,UAAU,EAAE,KAAK,EAAE,cAAsB,EAAE,EAAE,CAAC,SAAS,CAAC,cAAc,CAAC;QACvE,SAAS,EAAE,GAAG,EAAE;YACd,MAAM,gBAAgB,GAAG,cAAc,IAAI,qBAAqB,CAAC;YACjE,gBAAgB,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;gBAC/B,WAAW,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACtE,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport type { UseArchiveNotificationOptions } from '../../types.js';\n\n/**\n * Archive a single notification through an app-supplied callable adapter.\n *\n * The notification system is Cloud-Functions-only: clients never write\n * notification docs. This hook performs no Firestore writes — it delegates to\n * `archiveFn` (wired by the app to `httpsCallable(functions, 'archiveNotification')`)\n * and invalidates the read keys on success.\n *\n * @example\n * ```tsx\n * const archiveNotification = httpsCallable(functions, 'archiveNotification');\n * const archive = useArchiveNotification({\n * userId: user.uid,\n * category: 'user',\n * archiveFn: (notificationId) =>\n * archiveNotification({ category: 'user', scope: { kind: 'single', notificationId } }),\n * });\n *\n * archive.mutate('abc123');\n * ```\n */\nexport function useArchiveNotification({\n userId,\n category,\n archiveFn,\n invalidateKeys,\n}: UseArchiveNotificationOptions) {\n const queryClient = useQueryClient();\n\n const defaultInvalidateKeys = [\n ['notifications', 'active', category, userId],\n ['notifications', 'unread-count', category, userId],\n ['notifications', 'history', category, userId],\n ];\n\n return useMutation({\n mutationFn: async (notificationId: string) => archiveFn(notificationId),\n onSuccess: () => {\n const keysToInvalidate = invalidateKeys ?? defaultInvalidateKeys;\n keysToInvalidate.forEach((key) => {\n queryClient.invalidateQueries({ queryKey: [...key], exact: false });\n });\n },\n });\n}\n"]}
1
+ {"version":3,"file":"useArchiveNotification.js","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveNotification.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAOpE;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,QAAQ,EACR,SAAS,EACT,cAAc,GACgB;IAC9B,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,MAAM,qBAAqB,GAAG;QAC5B,CAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC;QAC7C,CAAC,eAAe,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC;QACnD,CAAC,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC;KAC/C,CAAC;IAEF,OAAO,WAAW,CAAoD;QACpE,UAAU,EAAE,CAAC,YAA6B,EAAE,EAAE,CAAC,SAAS,CAAC,YAAY,CAAC;QACtE,SAAS,EAAE,GAAG,EAAE;YACd,MAAM,gBAAgB,GAAG,cAAc,IAAI,qBAAqB,CAAC;YACjE,gBAAgB,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;gBAC/B,WAAW,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACtE,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport type {\n NotificationArchiveResult,\n NotificationDoc,\n UseArchiveNotificationOptions,\n} from '../../types.js';\n\n/**\n * Archive a single rendered notification through an app-supplied callable adapter.\n *\n * The notification system is Cloud-Functions-only: clients never write notification\n * docs. This hook performs no Firestore writes — it delegates to `archiveFn` with the\n * row as rendered, resolves the adapter's `{ archived }` answer, and invalidates the\n * read keys on success.\n */\nexport function useArchiveNotification({\n userId,\n category,\n archiveFn,\n invalidateKeys,\n}: UseArchiveNotificationOptions) {\n const queryClient = useQueryClient();\n\n const defaultInvalidateKeys = [\n ['notifications', 'active', category, userId],\n ['notifications', 'unread-count', category, userId],\n ['notifications', 'history', category, userId],\n ];\n\n return useMutation<NotificationArchiveResult, Error, NotificationDoc>({\n mutationFn: (notification: NotificationDoc) => archiveFn(notification),\n onSuccess: () => {\n const keysToInvalidate = invalidateKeys ?? defaultInvalidateKeys;\n keysToInvalidate.forEach((key) => {\n queryClient.invalidateQueries({ queryKey: [...key], exact: false });\n });\n },\n });\n}\n"]}
package/dist/types.d.ts CHANGED
@@ -154,7 +154,10 @@ export interface UseActiveNotificationsOptions {
154
154
  category: string;
155
155
  enabled?: boolean;
156
156
  pageSize?: number;
157
+ /** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */
157
158
  refetchInterval?: number;
159
+ /** How long a read page counts as fresh, in ms (default 30s). */
160
+ staleTime?: number;
158
161
  }
159
162
  /**
160
163
  * A flattened archived-history item for the read surface. The history doc is a
@@ -186,16 +189,29 @@ export interface UseUnreadCountOptions {
186
189
  refetchInterval?: number;
187
190
  countLimit?: number;
188
191
  }
192
+ /**
193
+ * What an archive adapter answers: whether the card was archived. `false` means the
194
+ * server archived nothing (for example the card gained new activity after it was
195
+ * rendered); the row stays active and is not shown as clearing.
196
+ */
197
+ export interface NotificationArchiveResult {
198
+ archived: boolean;
199
+ }
200
+ /**
201
+ * App-supplied adapter that archives one rendered notification (active → history).
202
+ * It receives the row exactly as the list rendered it, so the app can send what it
203
+ * observed (e.g. the row's activity generation). It rejects on failure and resolves
204
+ * whether the card was archived.
205
+ */
206
+ export type NotificationArchiveFn = (notification: NotificationDoc) => Promise<NotificationArchiveResult>;
189
207
  export interface UseArchiveNotificationOptions {
190
208
  userId: string;
191
209
  category: string;
192
210
  /**
193
- * App-supplied adapter that performs the archive (active → history) for one
194
- * notification — typically `httpsCallable(functions, 'archiveNotification')`.
195
- * The hook performs no client Firestore writes; it only invalidates the read
196
- * keys on success.
211
+ * Performs the archive through the app's callable. The hook performs no client
212
+ * Firestore writes; it only invalidates the read keys on success.
197
213
  */
198
- archiveFn: (notificationId: string) => Promise<unknown>;
214
+ archiveFn: NotificationArchiveFn;
199
215
  invalidateKeys?: readonly unknown[][];
200
216
  }
201
217
  /**
@@ -276,25 +292,40 @@ export interface UseArchiveAllNotificationsOptions {
276
292
  }
277
293
  /**
278
294
  * Per-row actions the list hands to `renderRowAction`. The row itself is inert
279
- * (DJ ruling 2026-07-07) — every affordance lives in the controls the consumer
295
+ * — every affordance lives in the controls the consumer
280
296
  * renders in the slot. `archive` performs the active→history archive for that
281
297
  * row; it is present ONLY on the active list (archived history rows cannot be
282
298
  * re-archived, so it is absent there).
283
299
  */
284
300
  export interface NotificationRowActions {
301
+ /** Archives this row; rejects when the archive fails. */
285
302
  archive?: () => Promise<void>;
286
- /** True while this row is archiving, including a category-wide Clear All. */
303
+ /**
304
+ * True while this row is archiving, including a category-wide Clear All. After an
305
+ * archive the server confirmed, it stays true until the row leaves the active list.
306
+ */
287
307
  isArchivePending?: boolean;
288
308
  }
309
+ /**
310
+ * What a list hands its `renderError` slot when its read failed. `hasRows` is true when
311
+ * rows from an earlier read are still on screen above the slot (a failed refresh);
312
+ * `retry` re-reads the displayed page and `retrying` is that read's pending flag.
313
+ */
314
+ export interface NotificationListErrorState {
315
+ error: Error;
316
+ retry: () => void;
317
+ retrying: boolean;
318
+ hasRows: boolean;
319
+ }
289
320
  export interface NotificationListProps {
290
321
  config: NotificationSystemConfig;
291
322
  userId: string;
292
323
  category: string;
293
324
  /**
294
- * Adapter that archives one notification (active → history). Passed straight
295
- * through to `useArchiveNotification`; the app wires it to its callable.
325
+ * Archives one rendered notification. Passed straight through to
326
+ * `useArchiveNotification`; the app wires it to its callable.
296
327
  */
297
- archiveFn: (notificationId: string) => Promise<unknown>;
328
+ archiveFn: NotificationArchiveFn;
298
329
  /**
299
330
  * Adapter that ENQUEUES one server-owned archive-all job scoped to `category` and returns its
300
331
  * `jobId`. Passed straight through to `useArchiveAllNotifications`; the app wires it to its
@@ -310,13 +341,24 @@ export interface NotificationListProps {
310
341
  /** Left-aligned title for the active-list header. */
311
342
  title?: ReactNode;
312
343
  onClearAll?: () => void;
344
+ /** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */
313
345
  refetchInterval?: number;
346
+ /** How long a read page counts as fresh, in ms (default 30s). */
347
+ staleTime?: number;
348
+ /** Empty-state text, shown only for an answered, empty first page. */
314
349
  emptyText?: string;
350
+ /** The failed-read state, shown before (never instead of) the pager. Copy is the app's. */
351
+ renderError: (state: NotificationListErrorState) => ReactNode;
352
+ /**
353
+ * Called with the rows on screen each time the displayed page's answered rows change
354
+ * (not while a page is loading or failed with nothing shown) — the rows to mark seen.
355
+ */
356
+ onRenderedRowsChange?: (rows: readonly NotificationDoc[]) => void;
315
357
  /**
316
- * Per-row control slot. The row itself is inert (DJ ruling 2026-07-07); the
317
- * consumer renders the row's controls here — typically an ArrowRight "go to"
318
- * (which may call `actions.archive` then navigate) plus a clear/archive button
319
- * (calls `actions.archive`). Omit to render a row with no controls.
358
+ * Per-row control slot. The row itself is inert; the consumer renders the row's
359
+ * controls here — typically an ArrowRight "go to" (which may call `actions.archive`
360
+ * then navigate) plus a clear/archive button (calls `actions.archive`). Omit to render
361
+ * a row with no controls.
320
362
  */
321
363
  renderRowAction?: (notification: NotificationDoc, actions: NotificationRowActions) => ReactNode;
322
364
  }
@@ -326,7 +368,10 @@ export interface NotificationHistoryListProps {
326
368
  category: string;
327
369
  pageSize?: number;
328
370
  staleTime?: number;
371
+ /** Empty-state text, shown only for an answered, empty first page. */
329
372
  emptyText?: string;
373
+ /** The failed-read state, shown before (never instead of) the pager. Copy is the app's. */
374
+ renderError: (state: NotificationListErrorState) => ReactNode;
330
375
  /** Left-aligned title for the read-only archived-list header. */
331
376
  title?: ReactNode;
332
377
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * @ttt-productions/notification-core — Type definitions\n *\n * Two-tier notification system: active (unread) → history (archived).\n * No `isRead` flag — existence in the active collection means unread.\n */\n\n// Type-only import: erased at compile time, so this does not force React into\n// the server-safe main entry's runtime graph (the `react` peer dep stays optional).\nimport type { ReactNode } from 'react';\n\n// ============================================================================\n// DOCUMENT TYPES\n// ============================================================================\n\n/**\n * Active notification document stored in Firestore.\n * Same shape for both user and admin collections.\n */\nexport interface NotificationDoc {\n /** Firestore doc ID */\n id: string;\n /** Notification type (e.g. 'content_report', 'entity_invite') */\n type: string;\n /** Dedup key (e.g. 'report_entityXYZ') — same key = same notification */\n dedupKey: string;\n /** Category: 'user' | 'admin' (extensible) */\n category: string;\n\n // Targeting\n /** userId for 'user' category, null for 'admin' (shared) */\n targetUserId: string | null;\n\n // Display\n /** Notification title */\n title: string;\n /** Notification body */\n message: string;\n\n // Dedup aggregation\n /** Incremented on duplicates */\n count: number;\n /**\n * Last N actor user IDs (capped). Identity is stored id-only — display names\n * are resolved at read time by the consuming app (e.g. from publicUsers), so\n * they never drift in the persisted doc.\n */\n latestActorIds: string[];\n\n // Navigation\n /**\n * Route path when clicked (e.g. '/admin' or '/entities/abc'). ABSENT when the\n * type declares no `defaultTargetPath` — a linkless, informational-only\n * notification (the consumer renders no \"go to\" affordance for it).\n */\n targetPath?: string;\n /** Type-specific metadata (e.g. { entityId, reason }) */\n metadata: Record<string, unknown>;\n\n // Seen state (personal notifications)\n /**\n * Epoch ms the recipient last marked this notification seen, or `0` when\n * unseen. Generic field — no domain knowledge. Always present (initialized\n * to `0` on creation and reset to `0` on dedup-increment) so the unread\n * `count()` aggregation can match it with a `seenAt == 0` equality predicate.\n * `seenAt` is a personal concept; shared notifications carry it for shape\n * uniformity but their unread indicator is existence-based and ignores it.\n */\n seenAt: number;\n\n /**\n * Opaque per-generation token (uuid), minted FRESH whenever the active doc is\n * created or materially re-lit. The SEEN/ARCHIVE precondition compares against\n * THIS, not a restartable integer: the active doc id is deterministic, so a\n * delete+recreate would restart an integer and let a stale tab's \"version 1\"\n * match a different card it never saw (ABA). An opaque token cannot repeat.\n * Optional on the legacy active doc shape; the ledger materializer always sets it.\n */\n activityGeneration?: string;\n /** The `activityGeneration` observed at the time `seenAt` was last set. */\n seenAtGeneration?: string;\n\n // Timestamps (epoch ms)\n /** First occurrence */\n createdAt: number;\n /** Latest occurrence */\n updatedAt: number;\n}\n\n// Archived history docs are persisted as an untyped wrapper by the server archive\n// helper (server/observed-generation.ts) — `archivedSnapshot` (the full active doc)\n// plus archive metadata + a native-TTL `expireAt` Timestamp. There is no consumer-\n// facing TS type for that shape; the canonical registry schema lives in the\n// consuming app's core package (doc-schemas/notifications.ts → NotificationHistoryDocSchema).\n\n/**\n * Pending notification — queue item for the batch processor.\n */\nexport interface PendingNotification {\n id: string;\n /** Notification type */\n type: string;\n /** Target category */\n category: string;\n /** Target userId (null for admin/shared) */\n targetUserId: string | null;\n /** Who triggered this (id-only; display name resolved at read time) */\n actorId: string;\n /** Type-specific data for building the notification */\n metadata: Record<string, unknown>;\n /** Epoch ms */\n createdAt: number;\n}\n\n// ============================================================================\n// CONFIGURATION TYPES\n// ============================================================================\n\n/**\n * Per-category configuration (user vs admin).\n */\nexport interface NotificationCategoryConfig {\n /** Firestore collection path for active notifications */\n activePath: string;\n /** Function that returns the history collection path */\n historyPath: (userId?: string) => string;\n /** 'personal' requires targetUserId, 'shared' = all members see everything */\n audienceType: 'personal' | 'shared';\n}\n\n/**\n * Per-type configuration for a notification type.\n */\nexport interface NotificationTypeConfig {\n /** Which category this type belongs to */\n category: string;\n /** Delivery mode */\n delivery: 'realtime' | 'queued';\n /** Function to build the dedupKey from metadata */\n dedupKeyPattern: (metadata: Record<string, unknown>) => string;\n /** Function to build the title from metadata */\n titlePattern: (metadata: Record<string, unknown>) => string;\n /** Function to build the message from metadata + count */\n messagePattern: (metadata: Record<string, unknown>, count: number) => string;\n /**\n * Default target path, or function to build from metadata. OMIT to declare a\n * linkless type: the written doc carries no `targetPath` and the consumer\n * renders no \"go to\" affordance (clear-only rows).\n */\n defaultTargetPath?: string | ((metadata: Record<string, unknown>) => string);\n /** Max count value (default 5000) */\n countCap?: number;\n /** Max latestActorIds array length (default 5) */\n actorCap?: number;\n /** Icon/emoji for display (optional) */\n icon?: string;\n}\n\n/**\n * Full notification system configuration — provided by each app.\n */\nexport interface NotificationSystemConfig {\n /** Category definitions */\n categories: Record<string, NotificationCategoryConfig>;\n /** Type definitions */\n types: Record<string, NotificationTypeConfig>;\n /** Batch processor interval in minutes (default 10) */\n batchIntervalMinutes?: number;\n /** Firestore collection for pending notifications (default 'pendingNotifications') */\n pendingCollectionPath?: string;\n /** Firestore collection for the delivery ledger (default 'notificationDeliveries'). */\n deliveriesCollectionPath?: string;\n /**\n * Factory that converts epoch-ms to the app's Firestore `Timestamp` instance.\n * REQUIRED for the delivery ledger: Firestore native TTL acts ONLY on\n * `Timestamp` fields, so `expireAt` must be a real Timestamp (never a number).\n * The generic package never imports firebase-admin, so the app injects this\n * (e.g. `(ms) => admin.firestore.Timestamp.fromMillis(ms)`).\n */\n timestampFromMillis?: (ms: number) => unknown;\n /** Delivery-row TTL after materialization, in ms (default 90 days). */\n deliveryTtlMs?: number;\n /** Max delivery attempts before a row dead-letters (default 8). */\n maxDeliveryAttempts?: number;\n}\n\n// ============================================================================\n// HOOK OPTION TYPES\n// ============================================================================\n\nexport interface UseActiveNotificationsOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n refetchInterval?: number;\n}\n\n/**\n * A flattened archived-history item for the read surface. The history doc is a\n * wrapper (`archivedSnapshot` + archive metadata + TTL); this shape lifts the\n * snapshot's display fields to the top level so the history list renders exactly\n * like the active list, plus `archivedAt` for ordering/display and\n * `archiveOccurrenceId` as the stable React key.\n */\nexport interface NotificationHistoryItem extends NotificationDoc {\n /** The history doc id (stable key — the `archiveOccurrenceId`). */\n archiveOccurrenceId: string;\n /** Epoch ms the notification was archived (history docs order by this). */\n archivedAt: number;\n}\n\nexport interface UseNotificationHistoryOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n /** Read-freshness stale time in ms (archived rows are immutable; default 60s). */\n staleTime?: number;\n}\n\nexport interface UseUnreadCountOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n refetchInterval?: number;\n countLimit?: number;\n}\n\nexport interface UseArchiveNotificationOptions {\n userId: string;\n category: string;\n /**\n * App-supplied adapter that performs the archive (active → history) for one\n * notification — typically `httpsCallable(functions, 'archiveNotification')`.\n * The hook performs no client Firestore writes; it only invalidates the read\n * keys on success.\n */\n archiveFn: (notificationId: string) => Promise<unknown>;\n invalidateKeys?: readonly unknown[][];\n}\n\n// ----------------------------------------------------------------------------\n// Archive-all — server-owned job + thin client status poller\n// ----------------------------------------------------------------------------\n//\n// Archive-all is NOT a browser loop. The client enqueues ONE server-owned\n// archive-all job (scoped to a single notification category/tab), then polls its\n// status until the job reaches a terminal state. All paging + ownership-checked\n// per-card deletion happen server-side in a bounded scheduled/queued worker; the\n// hook never touches Firestore and never chains archive calls. The generic\n// package hardcodes NO callable name — the app injects both adapters below.\n\n/**\n * Terminal + transient states of a server-owned archive-all job, as surfaced to the UI.\n *\n * - `idle` — nothing enqueued yet (initial / reset state).\n * - `enqueuing` — the enqueue adapter is in flight (job not yet acknowledged).\n * - `in-progress` — the job is enqueued/running; the worker is still draining the category.\n * - `complete` — TERMINAL: the worker fully drained the category's active set.\n * - `incomplete` — TERMINAL: the job stopped without draining everything (e.g. hit its own\n * bound / a non-advanceable card). The tray keeps a \"some remain — try again\"\n * affordance, mirroring the old loop's incomplete result.\n * - `failed` — TERMINAL: the enqueue call threw, or the job dead-lettered / reported failure.\n */\nexport type ArchiveAllJobStatus =\n | 'idle'\n | 'enqueuing'\n | 'in-progress'\n | 'complete'\n | 'incomplete'\n | 'failed';\n\n/**\n * Result of the INJECTED enqueue adapter — triggers exactly one server-owned archive-all job for\n * the given category and returns immediately with the job's id (idempotent per user+category+request\n * on the app side). The app wires this to its `httpsCallable(functions, 'enqueueArchiveAll')`\n * (or equivalent); the generic package never names the callable.\n */\nexport interface EnqueueArchiveAllResult {\n /** Server job id the poller then polls via the status adapter. */\n jobId: string;\n}\n\n/**\n * Snapshot of a server-owned archive-all job, returned by the INJECTED status-query adapter.\n * The poller maps `state` → {@link ArchiveAllJobStatus} and stops polling on any terminal state.\n * `category` echoes the tab/category the job was enqueued for so the UI can assert per-tab scoping.\n */\nexport interface ArchiveAllJobSnapshot {\n /** Job lifecycle state as owned by the server worker. */\n state: 'in-progress' | 'complete' | 'incomplete' | 'failed';\n /** The notification category/tab this job is scoped to (must match the enqueue category). */\n category: string;\n /** Cards archived so far (monotonic); optional progress for the UI. */\n archived?: number;\n /** Human-readable failure reason when `state === 'failed'` (optional). */\n error?: string;\n}\n\n/**\n * Aggregate result the poller resolves with once the job reaches a terminal state.\n * `complete` is the load-bearing completion contract (true ONLY when the whole category was\n * drained), preserved from the old loop so callers gate side effects the same way.\n */\nexport interface ArchiveAllPollResult {\n status: Extract<ArchiveAllJobStatus, 'complete' | 'incomplete' | 'failed'>;\n category: string;\n archived: number;\n complete: boolean;\n error?: string;\n}\n\nexport interface UseArchiveAllNotificationsOptions {\n userId: string;\n category: string;\n /**\n * App-supplied adapter that ENQUEUES one server-owned archive-all job scoped to `category` and\n * returns its `jobId`. The `category` is passed so the app callable can carry the tab/source into\n * the job (per-tab scoping is REQUIRED — the worker archives only that category's active cards).\n * Typically `(category) => enqueueArchiveAll({ category })` over an `httpsCallable`.\n */\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n /**\n * App-supplied adapter that returns the current {@link ArchiveAllJobSnapshot} for a job id — the\n * hook polls it until the job reaches a terminal state. Typically\n * `(jobId) => getArchiveAllStatus({ jobId })` over an `httpsCallable`, or a Firestore read of the\n * job doc. No client Firestore writes.\n */\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n /** Poll interval in ms while the job is in-progress (default 1500). */\n pollIntervalMs?: number;\n /**\n * Max number of status polls before the poller gives up and resolves `incomplete` (guards a job\n * that never reaches a terminal state — default 120, i.e. ~3 min at the default interval).\n */\n maxPolls?: number;\n invalidateKeys?: readonly unknown[][];\n}\n\n// ============================================================================\n// COMPONENT PROP TYPES\n// ============================================================================\n\n/**\n * Per-row actions the list hands to `renderRowAction`. The row itself is inert\n * (DJ ruling 2026-07-07) — every affordance lives in the controls the consumer\n * renders in the slot. `archive` performs the active→history archive for that\n * row; it is present ONLY on the active list (archived history rows cannot be\n * re-archived, so it is absent there).\n */\nexport interface NotificationRowActions {\n archive?: () => Promise<void>;\n /** True while this row is archiving, including a category-wide Clear All. */\n isArchivePending?: boolean;\n}\n\nexport interface NotificationListProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n /**\n * Adapter that archives one notification (active → history). Passed straight\n * through to `useArchiveNotification`; the app wires it to its callable.\n */\n archiveFn: (notificationId: string) => Promise<unknown>;\n /**\n * Adapter that ENQUEUES one server-owned archive-all job scoped to `category` and returns its\n * `jobId`. Passed straight through to `useArchiveAllNotifications`; the app wires it to its\n * enqueue callable. Per-tab scoping: the category flows through to the job.\n */\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n /**\n * Adapter that returns the current status snapshot for an archive-all `jobId`. Passed straight\n * through to `useArchiveAllNotifications`, which polls it until the job is terminal; the app wires\n * it to its status callable / job-doc read.\n */\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n /** Left-aligned title for the active-list header. */\n title?: ReactNode;\n onClearAll?: () => void;\n refetchInterval?: number;\n emptyText?: string;\n /**\n * Per-row control slot. The row itself is inert (DJ ruling 2026-07-07); the\n * consumer renders the row's controls here — typically an ArrowRight \"go to\"\n * (which may call `actions.archive` then navigate) plus a clear/archive button\n * (calls `actions.archive`). Omit to render a row with no controls.\n */\n renderRowAction?: (notification: NotificationDoc, actions: NotificationRowActions) => ReactNode;\n}\n\nexport interface NotificationHistoryListProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n pageSize?: number;\n staleTime?: number;\n emptyText?: string;\n /** Left-aligned title for the read-only archived-list header. */\n title?: ReactNode;\n /**\n * Per-row control slot for archived rows. The row is inert; archived rows are\n * read-only (no re-archive), so `actions.archive` is absent — the consumer\n * typically renders only an ArrowRight \"go to\". Omit for a row with no controls.\n */\n renderRowAction?: (notification: NotificationHistoryItem, actions: NotificationRowActions) => ReactNode;\n}\n\nexport interface NotificationEmptyStateProps {\n text?: string;\n}\n\nexport interface NotificationUnreadBadgeProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n refetchInterval?: number;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * @ttt-productions/notification-core — Type definitions\n *\n * Two-tier notification system: active (unread) → history (archived).\n * No `isRead` flag — existence in the active collection means unread.\n */\n\n// Type-only import: erased at compile time, so this does not force React into\n// the server-safe main entry's runtime graph (the `react` peer dep stays optional).\nimport type { ReactNode } from 'react';\n\n// ============================================================================\n// DOCUMENT TYPES\n// ============================================================================\n\n/**\n * Active notification document stored in Firestore.\n * Same shape for both user and admin collections.\n */\nexport interface NotificationDoc {\n /** Firestore doc ID */\n id: string;\n /** Notification type (e.g. 'content_report', 'entity_invite') */\n type: string;\n /** Dedup key (e.g. 'report_entityXYZ') — same key = same notification */\n dedupKey: string;\n /** Category: 'user' | 'admin' (extensible) */\n category: string;\n\n // Targeting\n /** userId for 'user' category, null for 'admin' (shared) */\n targetUserId: string | null;\n\n // Display\n /** Notification title */\n title: string;\n /** Notification body */\n message: string;\n\n // Dedup aggregation\n /** Incremented on duplicates */\n count: number;\n /**\n * Last N actor user IDs (capped). Identity is stored id-only — display names\n * are resolved at read time by the consuming app (e.g. from publicUsers), so\n * they never drift in the persisted doc.\n */\n latestActorIds: string[];\n\n // Navigation\n /**\n * Route path when clicked (e.g. '/admin' or '/entities/abc'). ABSENT when the\n * type declares no `defaultTargetPath` — a linkless, informational-only\n * notification (the consumer renders no \"go to\" affordance for it).\n */\n targetPath?: string;\n /** Type-specific metadata (e.g. { entityId, reason }) */\n metadata: Record<string, unknown>;\n\n // Seen state (personal notifications)\n /**\n * Epoch ms the recipient last marked this notification seen, or `0` when\n * unseen. Generic field — no domain knowledge. Always present (initialized\n * to `0` on creation and reset to `0` on dedup-increment) so the unread\n * `count()` aggregation can match it with a `seenAt == 0` equality predicate.\n * `seenAt` is a personal concept; shared notifications carry it for shape\n * uniformity but their unread indicator is existence-based and ignores it.\n */\n seenAt: number;\n\n /**\n * Opaque per-generation token (uuid), minted FRESH whenever the active doc is\n * created or materially re-lit. The SEEN/ARCHIVE precondition compares against\n * THIS, not a restartable integer: the active doc id is deterministic, so a\n * delete+recreate would restart an integer and let a stale tab's \"version 1\"\n * match a different card it never saw (ABA). An opaque token cannot repeat.\n * Optional on the legacy active doc shape; the ledger materializer always sets it.\n */\n activityGeneration?: string;\n /** The `activityGeneration` observed at the time `seenAt` was last set. */\n seenAtGeneration?: string;\n\n // Timestamps (epoch ms)\n /** First occurrence */\n createdAt: number;\n /** Latest occurrence */\n updatedAt: number;\n}\n\n// Archived history docs are persisted as an untyped wrapper by the server archive\n// helper (server/observed-generation.ts) — `archivedSnapshot` (the full active doc)\n// plus archive metadata + a native-TTL `expireAt` Timestamp. There is no consumer-\n// facing TS type for that shape; the canonical registry schema lives in the\n// consuming app's core package (doc-schemas/notifications.ts → NotificationHistoryDocSchema).\n\n/**\n * Pending notification — queue item for the batch processor.\n */\nexport interface PendingNotification {\n id: string;\n /** Notification type */\n type: string;\n /** Target category */\n category: string;\n /** Target userId (null for admin/shared) */\n targetUserId: string | null;\n /** Who triggered this (id-only; display name resolved at read time) */\n actorId: string;\n /** Type-specific data for building the notification */\n metadata: Record<string, unknown>;\n /** Epoch ms */\n createdAt: number;\n}\n\n// ============================================================================\n// CONFIGURATION TYPES\n// ============================================================================\n\n/**\n * Per-category configuration (user vs admin).\n */\nexport interface NotificationCategoryConfig {\n /** Firestore collection path for active notifications */\n activePath: string;\n /** Function that returns the history collection path */\n historyPath: (userId?: string) => string;\n /** 'personal' requires targetUserId, 'shared' = all members see everything */\n audienceType: 'personal' | 'shared';\n}\n\n/**\n * Per-type configuration for a notification type.\n */\nexport interface NotificationTypeConfig {\n /** Which category this type belongs to */\n category: string;\n /** Delivery mode */\n delivery: 'realtime' | 'queued';\n /** Function to build the dedupKey from metadata */\n dedupKeyPattern: (metadata: Record<string, unknown>) => string;\n /** Function to build the title from metadata */\n titlePattern: (metadata: Record<string, unknown>) => string;\n /** Function to build the message from metadata + count */\n messagePattern: (metadata: Record<string, unknown>, count: number) => string;\n /**\n * Default target path, or function to build from metadata. OMIT to declare a\n * linkless type: the written doc carries no `targetPath` and the consumer\n * renders no \"go to\" affordance (clear-only rows).\n */\n defaultTargetPath?: string | ((metadata: Record<string, unknown>) => string);\n /** Max count value (default 5000) */\n countCap?: number;\n /** Max latestActorIds array length (default 5) */\n actorCap?: number;\n /** Icon/emoji for display (optional) */\n icon?: string;\n}\n\n/**\n * Full notification system configuration — provided by each app.\n */\nexport interface NotificationSystemConfig {\n /** Category definitions */\n categories: Record<string, NotificationCategoryConfig>;\n /** Type definitions */\n types: Record<string, NotificationTypeConfig>;\n /** Batch processor interval in minutes (default 10) */\n batchIntervalMinutes?: number;\n /** Firestore collection for pending notifications (default 'pendingNotifications') */\n pendingCollectionPath?: string;\n /** Firestore collection for the delivery ledger (default 'notificationDeliveries'). */\n deliveriesCollectionPath?: string;\n /**\n * Factory that converts epoch-ms to the app's Firestore `Timestamp` instance.\n * REQUIRED for the delivery ledger: Firestore native TTL acts ONLY on\n * `Timestamp` fields, so `expireAt` must be a real Timestamp (never a number).\n * The generic package never imports firebase-admin, so the app injects this\n * (e.g. `(ms) => admin.firestore.Timestamp.fromMillis(ms)`).\n */\n timestampFromMillis?: (ms: number) => unknown;\n /** Delivery-row TTL after materialization, in ms (default 90 days). */\n deliveryTtlMs?: number;\n /** Max delivery attempts before a row dead-letters (default 8). */\n maxDeliveryAttempts?: number;\n}\n\n// ============================================================================\n// HOOK OPTION TYPES\n// ============================================================================\n\nexport interface UseActiveNotificationsOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n /** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */\n refetchInterval?: number;\n /** How long a read page counts as fresh, in ms (default 30s). */\n staleTime?: number;\n}\n\n/**\n * A flattened archived-history item for the read surface. The history doc is a\n * wrapper (`archivedSnapshot` + archive metadata + TTL); this shape lifts the\n * snapshot's display fields to the top level so the history list renders exactly\n * like the active list, plus `archivedAt` for ordering/display and\n * `archiveOccurrenceId` as the stable React key.\n */\nexport interface NotificationHistoryItem extends NotificationDoc {\n /** The history doc id (stable key — the `archiveOccurrenceId`). */\n archiveOccurrenceId: string;\n /** Epoch ms the notification was archived (history docs order by this). */\n archivedAt: number;\n}\n\nexport interface UseNotificationHistoryOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n /** Read-freshness stale time in ms (archived rows are immutable; default 60s). */\n staleTime?: number;\n}\n\nexport interface UseUnreadCountOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n refetchInterval?: number;\n countLimit?: number;\n}\n\n/**\n * What an archive adapter answers: whether the card was archived. `false` means the\n * server archived nothing (for example the card gained new activity after it was\n * rendered); the row stays active and is not shown as clearing.\n */\nexport interface NotificationArchiveResult {\n archived: boolean;\n}\n\n/**\n * App-supplied adapter that archives one rendered notification (active → history).\n * It receives the row exactly as the list rendered it, so the app can send what it\n * observed (e.g. the row's activity generation). It rejects on failure and resolves\n * whether the card was archived.\n */\nexport type NotificationArchiveFn = (notification: NotificationDoc) => Promise<NotificationArchiveResult>;\n\nexport interface UseArchiveNotificationOptions {\n userId: string;\n category: string;\n /**\n * Performs the archive through the app's callable. The hook performs no client\n * Firestore writes; it only invalidates the read keys on success.\n */\n archiveFn: NotificationArchiveFn;\n invalidateKeys?: readonly unknown[][];\n}\n\n// ----------------------------------------------------------------------------\n// Archive-all — server-owned job + thin client status poller\n// ----------------------------------------------------------------------------\n//\n// Archive-all is NOT a browser loop. The client enqueues ONE server-owned\n// archive-all job (scoped to a single notification category/tab), then polls its\n// status until the job reaches a terminal state. All paging + ownership-checked\n// per-card deletion happen server-side in a bounded scheduled/queued worker; the\n// hook never touches Firestore and never chains archive calls. The generic\n// package hardcodes NO callable name — the app injects both adapters below.\n\n/**\n * Terminal + transient states of a server-owned archive-all job, as surfaced to the UI.\n *\n * - `idle` — nothing enqueued yet (initial / reset state).\n * - `enqueuing` — the enqueue adapter is in flight (job not yet acknowledged).\n * - `in-progress` — the job is enqueued/running; the worker is still draining the category.\n * - `complete` — TERMINAL: the worker fully drained the category's active set.\n * - `incomplete` — TERMINAL: the job stopped without draining everything (e.g. hit its own\n * bound / a non-advanceable card). The tray keeps a \"some remain — try again\"\n * affordance, mirroring the old loop's incomplete result.\n * - `failed` — TERMINAL: the enqueue call threw, or the job dead-lettered / reported failure.\n */\nexport type ArchiveAllJobStatus =\n | 'idle'\n | 'enqueuing'\n | 'in-progress'\n | 'complete'\n | 'incomplete'\n | 'failed';\n\n/**\n * Result of the INJECTED enqueue adapter — triggers exactly one server-owned archive-all job for\n * the given category and returns immediately with the job's id (idempotent per user+category+request\n * on the app side). The app wires this to its `httpsCallable(functions, 'enqueueArchiveAll')`\n * (or equivalent); the generic package never names the callable.\n */\nexport interface EnqueueArchiveAllResult {\n /** Server job id the poller then polls via the status adapter. */\n jobId: string;\n}\n\n/**\n * Snapshot of a server-owned archive-all job, returned by the INJECTED status-query adapter.\n * The poller maps `state` → {@link ArchiveAllJobStatus} and stops polling on any terminal state.\n * `category` echoes the tab/category the job was enqueued for so the UI can assert per-tab scoping.\n */\nexport interface ArchiveAllJobSnapshot {\n /** Job lifecycle state as owned by the server worker. */\n state: 'in-progress' | 'complete' | 'incomplete' | 'failed';\n /** The notification category/tab this job is scoped to (must match the enqueue category). */\n category: string;\n /** Cards archived so far (monotonic); optional progress for the UI. */\n archived?: number;\n /** Human-readable failure reason when `state === 'failed'` (optional). */\n error?: string;\n}\n\n/**\n * Aggregate result the poller resolves with once the job reaches a terminal state.\n * `complete` is the load-bearing completion contract (true ONLY when the whole category was\n * drained), preserved from the old loop so callers gate side effects the same way.\n */\nexport interface ArchiveAllPollResult {\n status: Extract<ArchiveAllJobStatus, 'complete' | 'incomplete' | 'failed'>;\n category: string;\n archived: number;\n complete: boolean;\n error?: string;\n}\n\nexport interface UseArchiveAllNotificationsOptions {\n userId: string;\n category: string;\n /**\n * App-supplied adapter that ENQUEUES one server-owned archive-all job scoped to `category` and\n * returns its `jobId`. The `category` is passed so the app callable can carry the tab/source into\n * the job (per-tab scoping is REQUIRED — the worker archives only that category's active cards).\n * Typically `(category) => enqueueArchiveAll({ category })` over an `httpsCallable`.\n */\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n /**\n * App-supplied adapter that returns the current {@link ArchiveAllJobSnapshot} for a job id — the\n * hook polls it until the job reaches a terminal state. Typically\n * `(jobId) => getArchiveAllStatus({ jobId })` over an `httpsCallable`, or a Firestore read of the\n * job doc. No client Firestore writes.\n */\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n /** Poll interval in ms while the job is in-progress (default 1500). */\n pollIntervalMs?: number;\n /**\n * Max number of status polls before the poller gives up and resolves `incomplete` (guards a job\n * that never reaches a terminal state — default 120, i.e. ~3 min at the default interval).\n */\n maxPolls?: number;\n invalidateKeys?: readonly unknown[][];\n}\n\n// ============================================================================\n// COMPONENT PROP TYPES\n// ============================================================================\n\n/**\n * Per-row actions the list hands to `renderRowAction`. The row itself is inert\n * — every affordance lives in the controls the consumer\n * renders in the slot. `archive` performs the active→history archive for that\n * row; it is present ONLY on the active list (archived history rows cannot be\n * re-archived, so it is absent there).\n */\nexport interface NotificationRowActions {\n /** Archives this row; rejects when the archive fails. */\n archive?: () => Promise<void>;\n /**\n * True while this row is archiving, including a category-wide Clear All. After an\n * archive the server confirmed, it stays true until the row leaves the active list.\n */\n isArchivePending?: boolean;\n}\n\n/**\n * What a list hands its `renderError` slot when its read failed. `hasRows` is true when\n * rows from an earlier read are still on screen above the slot (a failed refresh);\n * `retry` re-reads the displayed page and `retrying` is that read's pending flag.\n */\nexport interface NotificationListErrorState {\n error: Error;\n retry: () => void;\n retrying: boolean;\n hasRows: boolean;\n}\n\nexport interface NotificationListProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n /**\n * Archives one rendered notification. Passed straight through to\n * `useArchiveNotification`; the app wires it to its callable.\n */\n archiveFn: NotificationArchiveFn;\n /**\n * Adapter that ENQUEUES one server-owned archive-all job scoped to `category` and returns its\n * `jobId`. Passed straight through to `useArchiveAllNotifications`; the app wires it to its\n * enqueue callable. Per-tab scoping: the category flows through to the job.\n */\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n /**\n * Adapter that returns the current status snapshot for an archive-all `jobId`. Passed straight\n * through to `useArchiveAllNotifications`, which polls it until the job is terminal; the app wires\n * it to its status callable / job-doc read.\n */\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n /** Left-aligned title for the active-list header. */\n title?: ReactNode;\n onClearAll?: () => void;\n /** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */\n refetchInterval?: number;\n /** How long a read page counts as fresh, in ms (default 30s). */\n staleTime?: number;\n /** Empty-state text, shown only for an answered, empty first page. */\n emptyText?: string;\n /** The failed-read state, shown before (never instead of) the pager. Copy is the app's. */\n renderError: (state: NotificationListErrorState) => ReactNode;\n /**\n * Called with the rows on screen each time the displayed page's answered rows change\n * (not while a page is loading or failed with nothing shown) — the rows to mark seen.\n */\n onRenderedRowsChange?: (rows: readonly NotificationDoc[]) => void;\n /**\n * Per-row control slot. The row itself is inert; the consumer renders the row's\n * controls here — typically an ArrowRight \"go to\" (which may call `actions.archive`\n * then navigate) plus a clear/archive button (calls `actions.archive`). Omit to render\n * a row with no controls.\n */\n renderRowAction?: (notification: NotificationDoc, actions: NotificationRowActions) => ReactNode;\n}\n\nexport interface NotificationHistoryListProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n pageSize?: number;\n staleTime?: number;\n /** Empty-state text, shown only for an answered, empty first page. */\n emptyText?: string;\n /** The failed-read state, shown before (never instead of) the pager. Copy is the app's. */\n renderError: (state: NotificationListErrorState) => ReactNode;\n /** Left-aligned title for the read-only archived-list header. */\n title?: ReactNode;\n /**\n * Per-row control slot for archived rows. The row is inert; archived rows are\n * read-only (no re-archive), so `actions.archive` is absent — the consumer\n * typically renders only an ArrowRight \"go to\". Omit for a row with no controls.\n */\n renderRowAction?: (notification: NotificationHistoryItem, actions: NotificationRowActions) => ReactNode;\n}\n\nexport interface NotificationEmptyStateProps {\n text?: string;\n}\n\nexport interface NotificationUnreadBadgeProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n refetchInterval?: number;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/notification-core",
3
- "version": "0.15.8",
3
+ "version": "0.15.9",
4
4
  "description": "Shared notification system for TTT Productions apps — active/history two-tier architecture with dedup, batch processing, and themed UI components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,8 +42,8 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@tanstack/react-query": ">=5.0.0",
45
- "@ttt-productions/query-core": "^0.14.4",
46
- "@ttt-productions/ui-core": "^0.11.19",
45
+ "@ttt-productions/query-core": "^0.14.6",
46
+ "@ttt-productions/ui-core": "^0.11.20",
47
47
  "firebase": ">=12.0.0",
48
48
  "react": ">=19.0.0",
49
49
  "react-dom": ">=19.0.0"
@@ -70,8 +70,8 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "@tanstack/react-query": "^5.0.0",
73
- "@ttt-productions/query-core": "^0.14.4",
74
- "@ttt-productions/ui-core": "^0.11.19",
73
+ "@ttt-productions/query-core": "^0.14.6",
74
+ "@ttt-productions/ui-core": "^0.11.20",
75
75
  "@types/react": "^19.0.0",
76
76
  "@types/react-dom": "^19.0.0",
77
77
  "firebase": "^12.14.0",