@ttt-productions/notification-core 0.15.16 → 0.15.17

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.
Files changed (58) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/react/components/NotificationHistoryList.d.ts +1 -1
  5. package/dist/react/components/NotificationHistoryList.d.ts.map +1 -1
  6. package/dist/react/components/NotificationHistoryList.js +4 -1
  7. package/dist/react/components/NotificationHistoryList.js.map +1 -1
  8. package/dist/react/components/NotificationList.d.ts +1 -1
  9. package/dist/react/components/NotificationList.d.ts.map +1 -1
  10. package/dist/react/components/NotificationList.js +24 -9
  11. package/dist/react/components/NotificationList.js.map +1 -1
  12. package/dist/react/components/NotificationUnreadBadge.d.ts.map +1 -1
  13. package/dist/react/components/NotificationUnreadBadge.js +1 -1
  14. package/dist/react/components/NotificationUnreadBadge.js.map +1 -1
  15. package/dist/react/hooks/useActiveNotifications.d.ts +3 -3
  16. package/dist/react/hooks/useActiveNotifications.d.ts.map +1 -1
  17. package/dist/react/hooks/useActiveNotifications.js +1 -2
  18. package/dist/react/hooks/useActiveNotifications.js.map +1 -1
  19. package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
  20. package/dist/react/hooks/useArchiveAllNotifications.js +14 -2
  21. package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
  22. package/dist/react/hooks/useNotificationHistory.d.ts +3 -3
  23. package/dist/react/hooks/useNotificationHistory.d.ts.map +1 -1
  24. package/dist/react/hooks/useNotificationHistory.js +1 -2
  25. package/dist/react/hooks/useNotificationHistory.js.map +1 -1
  26. package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
  27. package/dist/react/hooks/useUnreadCount.js +1 -3
  28. package/dist/react/hooks/useUnreadCount.js.map +1 -1
  29. package/dist/react/index.d.ts +10 -2
  30. package/dist/react/index.d.ts.map +1 -1
  31. package/dist/react/index.js +10 -3
  32. package/dist/react/index.js.map +1 -1
  33. package/dist/react/types.d.ts +72 -0
  34. package/dist/react/types.d.ts.map +1 -0
  35. package/dist/react/types.js +2 -0
  36. package/dist/react/types.js.map +1 -0
  37. package/dist/server/delivery-ledger.d.ts.map +1 -1
  38. package/dist/server/delivery-ledger.js +6 -4
  39. package/dist/server/delivery-ledger.js.map +1 -1
  40. package/dist/server/observed-generation.d.ts.map +1 -1
  41. package/dist/server/observed-generation.js.map +1 -1
  42. package/dist/server/processBatchHelper.d.ts.map +1 -1
  43. package/dist/server/processBatchHelper.js +3 -5
  44. package/dist/server/processBatchHelper.js.map +1 -1
  45. package/dist/server/types.d.ts.map +1 -1
  46. package/dist/server/types.js.map +1 -1
  47. package/dist/types.d.ts +0 -70
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/types.js.map +1 -1
  50. package/package.json +5 -5
  51. package/dist/react/components/index.d.ts +0 -5
  52. package/dist/react/components/index.d.ts.map +0 -1
  53. package/dist/react/components/index.js +0 -5
  54. package/dist/react/components/index.js.map +0 -1
  55. package/dist/react/hooks/index.d.ts +0 -6
  56. package/dist/react/hooks/index.d.ts.map +0 -1
  57. package/dist/react/hooks/index.js +0 -6
  58. package/dist/react/hooks/index.js.map +0 -1
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, NotificationArchiveResult, NotificationArchiveFn, NotificationListErrorState, 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, 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,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"}
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,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":"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
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAwBA,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 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,4 +1,4 @@
1
- import type { NotificationHistoryListProps } from '../../types.js';
1
+ import type { NotificationHistoryListProps } from '../types.js';
2
2
  /**
3
3
  * Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are
4
4
  * inert and read-only — archive is one-way, there is no re-archive, so the row
@@ -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,WAAW,EACX,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BAqG9B"}
1
+ {"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAEhE;;;;;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,+BAgG9B"}
@@ -22,7 +22,10 @@ export function NotificationHistoryList({ config, userId, category, pageSize, st
22
22
  const hasRows = !!notifications && notifications.length > 0;
23
23
  // A later page that came back empty keeps its pager so the user can step back; the
24
24
  // empty state is the answer for page 1 only.
25
- const showEmptyState = retryState.error === null && notifications !== undefined && notifications.length === 0 && page === 1;
25
+ const showEmptyState = retryState.error === null &&
26
+ notifications !== undefined &&
27
+ notifications.length === 0 &&
28
+ page === 1;
26
29
  const getTypeIcon = useCallback((type) => config.types[type]?.icon ?? '🔔', [config]);
27
30
  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: retryState.isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => {
28
31
  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));
@@ -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 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 isFetching,\n retryState,\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 retryState.error === null && 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 {retryState.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 {retryState.error !== null\n ? renderError({\n error: retryState.error,\n retry: () => {\n void refetch();\n },\n retrying: retryState.isRetrying,\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"]}
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 } from '../../types.js';\nimport type { 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 isFetching,\n retryState,\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 retryState.error === null &&\n notifications !== undefined &&\n notifications.length === 0 &&\n page === 1;\n\n const getTypeIcon = useCallback((type: string) => config.types[type]?.icon ?? '🔔', [config]);\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 {retryState.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 key={notification.archiveOccurrenceId} className=\"ntf-item ntf-item-archived\">\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\">{renderRowAction(notification, {})}</div>\n )}\n </div>\n );\n })}\n {retryState.error !== null\n ? renderError({\n error: retryState.error,\n retry: () => {\n void refetch();\n },\n retrying: retryState.isRetrying,\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"]}
@@ -1,4 +1,4 @@
1
- import type { NotificationListProps } from '../../types.js';
1
+ import type { NotificationListProps } from '../types.js';
2
2
  /**
3
3
  * Scrollable list of active notifications with click-to-archive and clear-all.
4
4
  */
@@ -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;AAkB7E;;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,+BA6QvB"}
1
+ {"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAoBzD;;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,+BAuSvB"}
@@ -13,7 +13,7 @@ const NO_PENDING = new Map();
13
13
  * generation, which the archive never covered, so it is not pending for that card.
14
14
  */
15
15
  function isPendingFor(pending, notification) {
16
- return pending.has(notification.id) && pending.get(notification.id) === notification.activityGeneration;
16
+ return (pending.has(notification.id) && pending.get(notification.id) === notification.activityGeneration);
17
17
  }
18
18
  const FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
19
19
  /**
@@ -76,7 +76,9 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
76
76
  const focusedRowId = useRef(null);
77
77
  const renderedIds = useRef([]);
78
78
  useLayoutEffect(() => {
79
- const ids = retryState.isLoading ? [] : (notifications ?? []).map((notification) => notification.id);
79
+ const ids = retryState.isLoading
80
+ ? []
81
+ : (notifications ?? []).map((notification) => notification.id);
80
82
  const previousIds = renderedIds.current;
81
83
  renderedIds.current = ids;
82
84
  const leftId = focusedRowId.current;
@@ -87,9 +89,14 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
87
89
  if (active !== null && active !== document.body && active.isConnected)
88
90
  return;
89
91
  const remaining = new Set(ids);
90
- const followers = previousIds.slice(previousIds.indexOf(leftId) + 1).filter((id) => remaining.has(id));
92
+ const followers = previousIds
93
+ .slice(previousIds.indexOf(leftId) + 1)
94
+ .filter((id) => remaining.has(id));
91
95
  const nextControl = followers
92
- .map((id) => rowRefs.current.get(id)?.querySelector('.ntf-item-row-action')?.querySelector(FOCUSABLE))
96
+ .map((id) => rowRefs.current
97
+ .get(id)
98
+ ?.querySelector('.ntf-item-row-action')
99
+ ?.querySelector(FOCUSABLE))
93
100
  .find((control) => control != null);
94
101
  (nextControl ?? listRef.current)?.focus();
95
102
  }, [notifications, retryState.isLoading]);
@@ -120,7 +127,10 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
120
127
  if (!hasNotifications)
121
128
  return;
122
129
  setClearIncomplete(false);
123
- setPendingClearAll(new Map((notifications ?? []).map((notification) => [notification.id, notification.activityGeneration])));
130
+ setPendingClearAll(new Map((notifications ?? []).map((notification) => [
131
+ notification.id,
132
+ notification.activityGeneration,
133
+ ])));
124
134
  let result;
125
135
  try {
126
136
  result = await archiveAllMutation.mutateAsync();
@@ -150,10 +160,14 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
150
160
  }, [config]);
151
161
  // A later page that came back empty keeps its pager so the user can step back; the
152
162
  // empty state is the answer for page 1 only.
153
- const showEmptyState = retryState.error === null && notifications !== undefined && notifications.length === 0 && page === 1;
154
- const isClearAllPending = archiveAllMutation.isPending || (notifications ?? []).some((notification) => isPendingFor(pendingClearAll, notification));
163
+ const showEmptyState = retryState.error === null &&
164
+ notifications !== undefined &&
165
+ notifications.length === 0 &&
166
+ page === 1;
167
+ const isClearAllPending = archiveAllMutation.isPending ||
168
+ (notifications ?? []).some((notification) => isPendingFor(pendingClearAll, notification));
155
169
  const titleId = useId();
156
- return (_jsxs("div", { ref: listRef, className: "ntf-list", role: "group", "aria-labelledby": title != null ? titleId : undefined, tabIndex: -1, children: [_jsxs("div", { className: "ntf-list-header", children: [title != null && _jsx("div", { id: titleId, className: "ntf-list-title", children: title }), _jsx(Button, { type: "button", className: "ntf-list-clear-all", variant: "ghost", size: "sm", onClick: handleClearAll, "aria-disabled": !hasNotifications || undefined, 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: retryState.isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => (_jsxs("div", { ref: (row) => {
170
+ return (_jsxs("div", { ref: listRef, className: "ntf-list", role: "group", "aria-labelledby": title != null ? titleId : undefined, tabIndex: -1, children: [_jsxs("div", { className: "ntf-list-header", children: [title != null && (_jsx("div", { id: titleId, className: "ntf-list-title", children: title })), _jsx(Button, { type: "button", className: "ntf-list-clear-all", variant: "ghost", size: "sm", onClick: handleClearAll, "aria-disabled": !hasNotifications || undefined, 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: retryState.isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: "Loading notifications" }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => (_jsxs("div", { ref: (row) => {
157
171
  if (row)
158
172
  rowRefs.current.set(notification.id, row);
159
173
  else
@@ -166,7 +180,8 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
166
180
  }
167
181
  }, 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, {
168
182
  archive: () => archiveOne(notification),
169
- isArchivePending: isPendingFor(pendingClearAll, notification) || isPendingFor(pendingArchives, notification),
183
+ isArchivePending: isPendingFor(pendingClearAll, notification) ||
184
+ isPendingFor(pendingArchives, notification),
170
185
  }) }))] }, notification.id))), retryState.error !== null
171
186
  ? renderError({
172
187
  error: retryState.error,
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACzF,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AACnG,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAC;AACpF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAMxD,MAAM,UAAU,GAAoB,IAAI,GAAG,EAAE,CAAC;AAE9C;;;GAGG;AACH,SAAS,YAAY,CAAC,OAAwB,EAAE,YAA6B;IAC3E,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,YAAY,CAAC,kBAAkB,CAAC;AAC1G,CAAC;AAED,MAAM,SAAS,GACb,2IAA2I,CAAC;AAE9I;;GAEG;AACH,MAAM,UAAU,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,GACO;IACtB,MAAM,EACJ,IAAI,EAAE,aAAa,EACnB,UAAU,EACV,UAAU,EACV,OAAO,EACP,IAAI,EACJ,WAAW,EACX,WAAW,EACX,QAAQ,EACR,QAAQ,GACT,GAAG,sBAAsB,CAAC;QACzB,MAAM;QACN,MAAM;QACN,QAAQ;QACR,eAAe;QACf,SAAS;KACV,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG,sBAAsB,CAAC;QAC7C,MAAM;QACN,QAAQ;QACR,SAAS;KACV,CAAC,CAAC;IAEH,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;QACpD,MAAM;QACN,QAAQ;QACR,mBAAmB;QACnB,qBAAqB;KACtB,CAAC,CAAC;IAEH,iGAAiG;IACjG,gGAAgG;IAChG,kGAAkG;IAClG,gGAAgG;IAChG,2BAA2B;IAC3B,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9D,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAkB,UAAU,CAAC,CAAC;IACpF,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAkB,UAAU,CAAC,CAAC;IAEpF,MAAM,eAAe,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;IACrD,eAAe,CAAC,OAAO,GAAG,oBAAoB,CAAC;IAC/C,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,aAAa;YAAE,eAAe,CAAC,OAAO,EAAE,CAAC,aAAa,CAAC,CAAC;IAC9D,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,gFAAgF;IAChF,kFAAkF;IAClF,qFAAqF;IACrF,wDAAwD;IACxD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,aAAa;YAAE,OAAO;QAC3B,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAC/B,aAAa,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,EAAE,YAAY,CAAC,kBAAkB,CAAC,CAAC,CACxF,CAAC;QACF,MAAM,UAAU,GAAG,CAAC,OAAwB,EAAmB,EAAE;YAC/D,MAAM,IAAI,GAAG,IAAI,GAAG,CAClB,CAAC,GAAG,OAAO,CAAC,CAAC,MAAM,CACjB,CAAC,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,UAAU,CAC5F,CACF,CAAC;YACF,OAAO,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QACrD,CAAC,CAAC;QACF,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC/B,kBAAkB,CAAC,UAAU,CAAC,CAAC;IACjC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAEpB,kGAAkG;IAClG,kGAAkG;IAClG,mGAAmG;IACnG,yBAAyB;IACzB,MAAM,OAAO,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,GAAG,EAA0B,CAAC,CAAC;IAC1D,MAAM,YAAY,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IACjD,MAAM,WAAW,GAAG,MAAM,CAAoB,EAAE,CAAC,CAAC;IAClD,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,GAAG,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QACrG,MAAM,WAAW,GAAG,WAAW,CAAC,OAAO,CAAC;QACxC,WAAW,CAAC,OAAO,GAAG,GAAG,CAAC;QAC1B,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC;QACpC,IAAI,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO;QACpD,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;QAC5B,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC;QACtC,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,QAAQ,CAAC,IAAI,IAAI,MAAM,CAAC,WAAW;YAAE,OAAO;QAE9E,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACvG,MAAM,WAAW,GAAG,SAAS;aAC1B,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAAE,aAAa,CAAc,SAAS,CAAC,CAAC;aAClH,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC;QACtC,CAAC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC;IAC5C,CAAC,EAAE,CAAC,aAAa,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAE1C,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,EAAE,YAA6B,EAAE,EAAE;QACrE,MAAM,UAAU,GAAG,YAAY,CAAC,kBAAkB,CAAC;QACnD,MAAM,YAAY,GAAG,GAAG,EAAE,CACxB,kBAAkB,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,UAAU;gBAAE,OAAO,OAAO,CAAC;YACjG,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;YAC9B,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QACL,kBAAkB,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC;QACnF,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;YAC/D,+EAA+E;YAC/E,yDAAyD;YACzD,IAAI,CAAC,MAAM,CAAC,QAAQ;gBAAE,YAAY,EAAE,CAAC;QACvC,CAAC;QAAC,OAAO,YAAY,EAAE,CAAC;YACtB,YAAY,EAAE,CAAC;YACf,MAAM,YAAY,CAAC;QACrB,CAAC;IACH,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAEtB,MAAM,gBAAgB,GAAG,CAAC,CAAC,aAAa,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC;IAErE,MAAM,cAAc,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAC5C,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAC9B,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAC1B,kBAAkB,CAChB,IAAI,GAAG,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,EAAE,YAAY,CAAC,kBAAkB,CAAC,CAAC,CAAC,CACzG,CAAC;QACF,IAAI,MAAM,CAAC;QACX,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,kBAAkB,CAAC,WAAW,EAAE,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,2FAA2F;YAC3F,kBAAkB,CAAC,UAAU,CAAC,CAAC;YAC/B,kBAAkB,CAAC,IAAI,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACrB,oFAAoF;YACpF,oCAAoC;YACpC,kBAAkB,CAAC,UAAU,CAAC,CAAC;YAC/B,kBAAkB,CAAC,IAAI,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QACD,6FAA6F;QAC7F,sFAAsF;QACtF,IAAI,MAAM,CAAC,WAAW,GAAG,CAAC;YAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC3D,UAAU,EAAE,CAAC,MAAM,CAAC,CAAC;IACvB,CAAC,EAAE,CAAC,kBAAkB,EAAE,gBAAgB,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC;IAEtE,MAAM,WAAW,GAAG,WAAW,CAC7B,CAAC,IAAY,EAAE,EAAE;QACf,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,OAAO,UAAU,EAAE,IAAI,IAAI,IAAI,CAAC;IAClC,CAAC,EACD,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,mFAAmF;IACnF,6CAA6C;IAC7C,MAAM,cAAc,GAClB,UAAU,CAAC,KAAK,KAAK,IAAI,IAAI,aAAa,KAAK,SAAS,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,CAAC;IACvG,MAAM,iBAAiB,GACrB,kBAAkB,CAAC,SAAS,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC;IAC5H,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IAExB,OAAO,CACL,eACE,GAAG,EAAE,OAAO,EACZ,SAAS,EAAC,UAAU,EACpB,IAAI,EAAC,OAAO,qBACK,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EACpD,QAAQ,EAAE,CAAC,CAAC,aAEZ,eAAK,SAAS,EAAC,iBAAiB,aAC7B,KAAK,IAAI,IAAI,IAAI,cAAK,EAAE,EAAE,OAAO,EAAE,SAAS,EAAC,gBAAgB,YAAE,KAAK,GAAO,EAG5E,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,oBAAoB,EAC9B,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,cAAc,mBACR,CAAC,gBAAgB,IAAI,SAAS,EAC7C,OAAO,EAAE,iBAAiB,YAEzB,iBAAiB,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,GACzC,EACR,eAAe,IAAI,CAAC,iBAAiB,IAAI,CACxC,cAAK,SAAS,EAAC,sBAAsB,EAAC,IAAI,EAAC,QAAQ,4DAE7C,CACP,IACG,EACN,KAAC,SAAS,KAAG,EAEb,cAAK,SAAS,EAAC,eAAe,YAC7B,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CACtB,cAAK,SAAS,EAAC,aAAa,YAC1B,KAAC,OAAO,IAAC,IAAI,EAAC,IAAI,EAAC,KAAK,EAAC,uBAAuB,GAAG,GAC/C,CACP,CAAC,CAAC,CAAC,CACF,8BACG,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAA6B,EAAE,EAAE,CAAC,CAC5D,eAEE,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE;gCACX,IAAI,GAAG;oCAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;;oCAC9C,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;4BAC/C,CAAC,EACD,SAAS,EAAC,UAAU,EACpB,OAAO,EAAE,GAAG,EAAE;gCACZ,YAAY,CAAC,OAAO,GAAG,YAAY,CAAC,EAAE,CAAC;4BACzC,CAAC,EACD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;gCAChB,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,aAA4B,CAAC,EAAE,CAAC;oCACtE,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;gCAC9B,CAAC;4BACH,CAAC,aAED,cAAK,SAAS,EAAC,eAAe,YAC3B,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,GAC3B,EACN,eAAK,SAAS,EAAC,kBAAkB,aAC/B,cAAK,SAAS,EAAC,gBAAgB,YAAE,YAAY,CAAC,KAAK,GAAO,EAC1D,cAAK,SAAS,EAAC,kBAAkB,YAAE,YAAY,CAAC,OAAO,GAAO,EAC9D,cAAK,SAAS,EAAC,oBAAoB,YAChC,kBAAkB,CAAC,YAAY,CAAC,SAAS,CAAC,GACvC,IACF,EACL,YAAY,CAAC,KAAK,GAAG,CAAC,IAAI,CACzB,cAAK,SAAS,EAAC,gBAAgB,YAC7B,MAAC,KAAK,IAAC,OAAO,EAAC,WAAW,uBAAG,YAAY,CAAC,KAAK,IAAS,GACpD,CACP,EACA,eAAe,IAAI,CAClB,cAAK,SAAS,EAAC,qBAAqB,YACjC,eAAe,CAAC,YAAY,EAAE;wCAC7B,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC;wCACvC,gBAAgB,EACd,YAAY,CAAC,eAAe,EAAE,YAAY,CAAC,IAAI,YAAY,CAAC,eAAe,EAAE,YAAY,CAAC;qCAC7F,CAAC,GACE,CACP,KAtCI,YAAY,CAAC,EAAE,CAuChB,CACP,CAAC,EACD,UAAU,CAAC,KAAK,KAAK,IAAI;4BACxB,CAAC,CAAC,WAAW,CAAC;gCACV,KAAK,EAAE,UAAU,CAAC,KAAK;gCACvB,KAAK,EAAE,GAAG,EAAE;oCACV,KAAK,OAAO,EAAE,CAAC;gCACjB,CAAC;gCACD,QAAQ,EAAE,UAAU,CAAC,UAAU;gCAC/B,OAAO,EAAE,gBAAgB;6BAC1B,CAAC;4BACJ,CAAC,CAAC,IAAI,EACP,cAAc,IAAI,KAAC,sBAAsB,IAAC,IAAI,EAAE,SAAS,GAAI,EAC9D,KAAC,cAAc,IACb,UAAU,EAAE;gCACV,WAAW,EAAE,IAAI;gCACjB,eAAe,EAAE,WAAW;gCAC5B,WAAW,EAAE,WAAW;gCACxB,gBAAgB,EAAE,QAAQ;gCAC1B,YAAY,EAAE,QAAQ;6BACvB,EACD,IAAI,EAAE,UAAU,EAChB,SAAS,EAAC,sBAAsB,GAChC,IACD,CACJ,GACK,IACF,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useId, useLayoutEffect, 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/** Pending archives keyed by card id, holding the activity generation the archive was sent for. */\ntype PendingArchives = ReadonlyMap<string, string | undefined>;\n\nconst NO_PENDING: PendingArchives = new Map();\n\n/**\n * An archive belongs to the card generation it was sent for. A card relit on the same id gets a new\n * generation, which the archive never covered, so it is not pending for that card.\n */\nfunction isPendingFor(pending: PendingArchives, notification: NotificationDoc): boolean {\n return pending.has(notification.id) && pending.get(notification.id) === notification.activityGeneration;\n}\n\nconst FOCUSABLE =\n 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\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 isFetching,\n retryState,\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 [pendingArchives, setPendingArchives] = useState<PendingArchives>(NO_PENDING);\n const [pendingClearAll, setPendingClearAll] = useState<PendingArchives>(NO_PENDING);\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. A row the read lists at a newer generation was relit\n // after the archive, so the archive no longer holds it.\n useEffect(() => {\n if (!notifications) return;\n const activeGenerations = new Map(\n notifications.map((notification) => [notification.id, notification.activityGeneration]),\n );\n const keepActive = (current: PendingArchives): PendingArchives => {\n const next = new Map(\n [...current].filter(\n ([id, generation]) => activeGenerations.has(id) && activeGenerations.get(id) === generation,\n ),\n );\n return next.size === current.size ? current : next;\n };\n setPendingArchives(keepActive);\n setPendingClearAll(keepActive);\n }, [notifications]);\n\n // A row that leaves the list while focus is inside it takes the focused control with it, so focus\n // moves to the control of the row that followed it, or to the list itself (FRONTEND-203). A row's\n // removal fires no blur, so the row still recorded as focused when the rows change is the row that\n // held focus as it left.\n const listRef = useRef<HTMLDivElement>(null);\n const rowRefs = useRef(new Map<string, HTMLDivElement>());\n const focusedRowId = useRef<string | null>(null);\n const renderedIds = useRef<readonly string[]>([]);\n useLayoutEffect(() => {\n const ids = retryState.isLoading ? [] : (notifications ?? []).map((notification) => notification.id);\n const previousIds = renderedIds.current;\n renderedIds.current = ids;\n const leftId = focusedRowId.current;\n if (leftId === null || ids.includes(leftId)) return;\n focusedRowId.current = null;\n const active = document.activeElement;\n if (active !== null && active !== document.body && active.isConnected) return;\n\n const remaining = new Set(ids);\n const followers = previousIds.slice(previousIds.indexOf(leftId) + 1).filter((id) => remaining.has(id));\n const nextControl = followers\n .map((id) => rowRefs.current.get(id)?.querySelector('.ntf-item-row-action')?.querySelector<HTMLElement>(FOCUSABLE))\n .find((control) => control != null);\n (nextControl ?? listRef.current)?.focus();\n }, [notifications, retryState.isLoading]);\n\n const archiveOne = useCallback(async (notification: NotificationDoc) => {\n const generation = notification.activityGeneration;\n const clearPending = () =>\n setPendingArchives((current) => {\n if (!current.has(notification.id) || current.get(notification.id) !== generation) return current;\n const next = new Map(current);\n next.delete(notification.id);\n return next;\n });\n setPendingArchives((current) => new Map(current).set(notification.id, generation));\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 hasNotifications = !!notifications && notifications.length > 0;\n\n const handleClearAll = useCallback(async () => {\n if (!hasNotifications) return;\n setClearIncomplete(false);\n setPendingClearAll(\n new Map((notifications ?? []).map((notification) => [notification.id, notification.activityGeneration])),\n );\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 setPendingClearAll(NO_PENDING);\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 setPendingClearAll(NO_PENDING);\n setClearIncomplete(true);\n return;\n }\n // The cards a complete drain left behind stay in the list and never leave the read, so their\n // pending is released here rather than waiting on a read that will keep listing them.\n if (result.notArchived > 0) setPendingClearAll(NO_PENDING);\n onClearAll?.(result);\n }, [archiveAllMutation, hasNotifications, 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 // 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 retryState.error === null && notifications !== undefined && notifications.length === 0 && page === 1;\n const isClearAllPending =\n archiveAllMutation.isPending || (notifications ?? []).some((notification) => isPendingFor(pendingClearAll, notification));\n const titleId = useId();\n\n return (\n <div\n ref={listRef}\n className=\"ntf-list\"\n role=\"group\"\n aria-labelledby={title != null ? titleId : undefined}\n tabIndex={-1}\n >\n <div className=\"ntf-list-header\">\n {title != null && <div id={titleId} className=\"ntf-list-title\">{title}</div>}\n {/* Unavailable with nothing to clear, but never natively disabled: the list empties under\n a Clear All that still holds focus, and focus must keep a destination (FRONTEND-203). */}\n <Button\n type=\"button\"\n className=\"ntf-list-clear-all\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClearAll}\n aria-disabled={!hasNotifications || undefined}\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 {retryState.isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : (\n <>\n {(notifications ?? []).map((notification: NotificationDoc) => (\n <div\n key={notification.id}\n ref={(row) => {\n if (row) rowRefs.current.set(notification.id, row);\n else rowRefs.current.delete(notification.id);\n }}\n className=\"ntf-item\"\n onFocus={() => {\n focusedRowId.current = notification.id;\n }}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n focusedRowId.current = null;\n }\n }}\n >\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 isPendingFor(pendingClearAll, notification) || isPendingFor(pendingArchives, notification),\n })}\n </div>\n )}\n </div>\n ))}\n {retryState.error !== null\n ? renderError({\n error: retryState.error,\n retry: () => {\n void refetch();\n },\n retrying: retryState.isRetrying,\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
+ {"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, useId, useLayoutEffect, 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 } from '../../types.js';\nimport type { NotificationListProps } from '../types.js';\n\n/** Pending archives keyed by card id, holding the activity generation the archive was sent for. */\ntype PendingArchives = ReadonlyMap<string, string | undefined>;\n\nconst NO_PENDING: PendingArchives = new Map();\n\n/**\n * An archive belongs to the card generation it was sent for. A card relit on the same id gets a new\n * generation, which the archive never covered, so it is not pending for that card.\n */\nfunction isPendingFor(pending: PendingArchives, notification: NotificationDoc): boolean {\n return (\n pending.has(notification.id) && pending.get(notification.id) === notification.activityGeneration\n );\n}\n\nconst FOCUSABLE =\n 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\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 isFetching,\n retryState,\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 [pendingArchives, setPendingArchives] = useState<PendingArchives>(NO_PENDING);\n const [pendingClearAll, setPendingClearAll] = useState<PendingArchives>(NO_PENDING);\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. A row the read lists at a newer generation was relit\n // after the archive, so the archive no longer holds it.\n useEffect(() => {\n if (!notifications) return;\n const activeGenerations = new Map(\n notifications.map((notification) => [notification.id, notification.activityGeneration]),\n );\n const keepActive = (current: PendingArchives): PendingArchives => {\n const next = new Map(\n [...current].filter(\n ([id, generation]) =>\n activeGenerations.has(id) && activeGenerations.get(id) === generation,\n ),\n );\n return next.size === current.size ? current : next;\n };\n setPendingArchives(keepActive);\n setPendingClearAll(keepActive);\n }, [notifications]);\n\n // A row that leaves the list while focus is inside it takes the focused control with it, so focus\n // moves to the control of the row that followed it, or to the list itself (FRONTEND-203). A row's\n // removal fires no blur, so the row still recorded as focused when the rows change is the row that\n // held focus as it left.\n const listRef = useRef<HTMLDivElement>(null);\n const rowRefs = useRef(new Map<string, HTMLDivElement>());\n const focusedRowId = useRef<string | null>(null);\n const renderedIds = useRef<readonly string[]>([]);\n useLayoutEffect(() => {\n const ids = retryState.isLoading\n ? []\n : (notifications ?? []).map((notification) => notification.id);\n const previousIds = renderedIds.current;\n renderedIds.current = ids;\n const leftId = focusedRowId.current;\n if (leftId === null || ids.includes(leftId)) return;\n focusedRowId.current = null;\n const active = document.activeElement;\n if (active !== null && active !== document.body && active.isConnected) return;\n\n const remaining = new Set(ids);\n const followers = previousIds\n .slice(previousIds.indexOf(leftId) + 1)\n .filter((id) => remaining.has(id));\n const nextControl = followers\n .map((id) =>\n rowRefs.current\n .get(id)\n ?.querySelector('.ntf-item-row-action')\n ?.querySelector<HTMLElement>(FOCUSABLE),\n )\n .find((control) => control != null);\n (nextControl ?? listRef.current)?.focus();\n }, [notifications, retryState.isLoading]);\n\n const archiveOne = useCallback(\n async (notification: NotificationDoc) => {\n const generation = notification.activityGeneration;\n const clearPending = () =>\n setPendingArchives((current) => {\n if (!current.has(notification.id) || current.get(notification.id) !== generation)\n return current;\n const next = new Map(current);\n next.delete(notification.id);\n return next;\n });\n setPendingArchives((current) => new Map(current).set(notification.id, generation));\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 },\n [archiveMutation],\n );\n\n const hasNotifications = !!notifications && notifications.length > 0;\n\n const handleClearAll = useCallback(async () => {\n if (!hasNotifications) return;\n setClearIncomplete(false);\n setPendingClearAll(\n new Map(\n (notifications ?? []).map((notification) => [\n notification.id,\n notification.activityGeneration,\n ]),\n ),\n );\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 setPendingClearAll(NO_PENDING);\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 setPendingClearAll(NO_PENDING);\n setClearIncomplete(true);\n return;\n }\n // The cards a complete drain left behind stay in the list and never leave the read, so their\n // pending is released here rather than waiting on a read that will keep listing them.\n if (result.notArchived > 0) setPendingClearAll(NO_PENDING);\n onClearAll?.(result);\n }, [archiveAllMutation, hasNotifications, 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 // 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 retryState.error === null &&\n notifications !== undefined &&\n notifications.length === 0 &&\n page === 1;\n const isClearAllPending =\n archiveAllMutation.isPending ||\n (notifications ?? []).some((notification) => isPendingFor(pendingClearAll, notification));\n const titleId = useId();\n\n return (\n <div\n ref={listRef}\n className=\"ntf-list\"\n role=\"group\"\n aria-labelledby={title != null ? titleId : undefined}\n tabIndex={-1}\n >\n <div className=\"ntf-list-header\">\n {title != null && (\n <div id={titleId} className=\"ntf-list-title\">\n {title}\n </div>\n )}\n {/* Unavailable with nothing to clear, but never natively disabled: the list empties under\n a Clear All that still holds focus, and focus must keep a destination (FRONTEND-203). */}\n <Button\n type=\"button\"\n className=\"ntf-list-clear-all\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClearAll}\n aria-disabled={!hasNotifications || undefined}\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 {retryState.isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : (\n <>\n {(notifications ?? []).map((notification: NotificationDoc) => (\n <div\n key={notification.id}\n ref={(row) => {\n if (row) rowRefs.current.set(notification.id, row);\n else rowRefs.current.delete(notification.id);\n }}\n className=\"ntf-item\"\n onFocus={() => {\n focusedRowId.current = notification.id;\n }}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {\n focusedRowId.current = null;\n }\n }}\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.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 isPendingFor(pendingClearAll, notification) ||\n isPendingFor(pendingArchives, notification),\n })}\n </div>\n )}\n </div>\n ))}\n {retryState.error !== null\n ? renderError({\n error: retryState.error,\n retry: () => {\n void refetch();\n },\n retrying: retryState.isRetrying,\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 +1 @@
1
- {"version":3,"file":"NotificationUnreadBadge.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationUnreadBadge.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAEnE;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,eAAe,GAChB,EAAE,4BAA4B,sCAe9B"}
1
+ {"version":3,"file":"NotificationUnreadBadge.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationUnreadBadge.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAEnE;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,eAAe,GAChB,EAAE,4BAA4B,sCAa9B"}
@@ -16,6 +16,6 @@ export function NotificationUnreadBadge({ config, userId, category, refetchInter
16
16
  if (count === 0)
17
17
  return null;
18
18
  const displayCount = hasMore ? '99+' : count > 9 ? '9+' : String(count);
19
- return (_jsx("span", { className: "ntf-badge", children: displayCount }));
19
+ return _jsx("span", { className: "ntf-badge", children: displayCount });
20
20
  }
21
21
  //# sourceMappingURL=NotificationUnreadBadge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationUnreadBadge.js","sourceRoot":"","sources":["../../../src/react/components/NotificationUnreadBadge.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG5D;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,eAAe,GACc;IAC7B,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC;QACxC,MAAM;QACN,MAAM;QACN,QAAQ;QACR,eAAe;KAChB,CAAC,CAAC;IAEH,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE7B,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAExE,OAAO,CACL,eAAM,SAAS,EAAC,WAAW,YAAE,YAAY,GAAQ,CAClD,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { useUnreadCount } from '../hooks/useUnreadCount.js';\nimport type { NotificationUnreadBadgeProps } from '../../types.js';\n\n/**\n * Unread count badge (red circle with number).\n * Renders nothing when count is 0.\n * Apps wrap this around whatever trigger element they want.\n */\nexport function NotificationUnreadBadge({\n config,\n userId,\n category,\n refetchInterval,\n}: NotificationUnreadBadgeProps) {\n const { count, hasMore } = useUnreadCount({\n config,\n userId,\n category,\n refetchInterval,\n });\n\n if (count === 0) return null;\n\n const displayCount = hasMore ? '99+' : count > 9 ? '9+' : String(count);\n\n return (\n <span className=\"ntf-badge\">{displayCount}</span>\n );\n}\n"]}
1
+ {"version":3,"file":"NotificationUnreadBadge.js","sourceRoot":"","sources":["../../../src/react/components/NotificationUnreadBadge.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG5D;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,eAAe,GACc;IAC7B,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC;QACxC,MAAM;QACN,MAAM;QACN,QAAQ;QACR,eAAe;KAChB,CAAC,CAAC;IAEH,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE7B,MAAM,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAExE,OAAO,eAAM,SAAS,EAAC,WAAW,YAAE,YAAY,GAAQ,CAAC;AAC3D,CAAC","sourcesContent":["'use client';\n\nimport { useUnreadCount } from '../hooks/useUnreadCount.js';\nimport type { NotificationUnreadBadgeProps } from '../../types.js';\n\n/**\n * Unread count badge (red circle with number).\n * Renders nothing when count is 0.\n * Apps wrap this around whatever trigger element they want.\n */\nexport function NotificationUnreadBadge({\n config,\n userId,\n category,\n refetchInterval,\n}: NotificationUnreadBadgeProps) {\n const { count, hasMore } = useUnreadCount({\n config,\n userId,\n category,\n refetchInterval,\n });\n\n if (count === 0) return null;\n\n const displayCount = hasMore ? '99+' : count > 9 ? '9+' : String(count);\n\n return <span className=\"ntf-badge\">{displayCount}</span>;\n}\n"]}
@@ -5,7 +5,7 @@ import type { NotificationDoc, UseActiveNotificationsOptions } from '../../types
5
5
  */
6
6
  export declare function useActiveNotifications({ config, userId, category, enabled, pageSize, refetchInterval, staleTime, }: UseActiveNotificationsOptions): {
7
7
  retryState: import("@ttt-productions/query-core/react").ReadRetryState<Error>;
8
- promise: Promise<import("@ttt-productions/query-core").PaginatedPage<import("@ttt-productions/query-core").WithId<NotificationDoc>>>;
8
+ promise: Promise<import("@ttt-productions/query-core/react").PaginatedPage<import("@ttt-productions/query-core/types").WithId<NotificationDoc>>>;
9
9
  error: Error | null;
10
10
  isError: boolean;
11
11
  isPending: boolean;
@@ -28,9 +28,9 @@ export declare function useActiveNotifications({ config, userId, category, enabl
28
28
  isRefetching: boolean;
29
29
  isStale: boolean;
30
30
  isEnabled: boolean;
31
- refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<import("@ttt-productions/query-core").PaginatedPage<import("@ttt-productions/query-core").WithId<NotificationDoc>>, Error>>;
31
+ refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<import("@ttt-productions/query-core/react").PaginatedPage<import("@ttt-productions/query-core/types").WithId<NotificationDoc>>, Error>>;
32
32
  fetchStatus: import("@tanstack/query-core").FetchStatus;
33
- data: import("@ttt-productions/query-core").WithId<NotificationDoc>[] | undefined;
33
+ data: import("@ttt-productions/query-core/types").WithId<NotificationDoc>[] | undefined;
34
34
  page: number;
35
35
  pageSize: number;
36
36
  hasNextPage: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAKA,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2B/B"}
1
+ {"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAQA,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2B/B"}
@@ -1,6 +1,5 @@
1
1
  'use client';
2
- import { paginatedPageKey } from '@ttt-productions/query-core';
3
- import { useFirestorePaginated, useReadRetryState } from '@ttt-productions/query-core/react';
2
+ import { paginatedPageKey, useFirestorePaginated, useReadRetryState, } from '@ttt-productions/query-core/react';
4
3
  import { orderBy, where } from 'firebase/firestore';
5
4
  const DEFAULT_PAGE_SIZE = 20;
6
5
  const DEFAULT_REFETCH_INTERVAL = 30_000;
@@ -1 +1 @@
1
- {"version":3,"file":"useActiveNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC7F,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,MAAM,QAAQ,GAAG,CAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,qBAAqB,CAAkB;QAClD,cAAc;QACd,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,eAAe;QACf,SAAS;KACV,CAAC,CAAC;IACH,kGAAkG;IAClG,gGAAgG;IAChG,OAAO,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AACjG,CAAC","sourcesContent":["'use client';\n\nimport { paginatedPageKey } from '@ttt-productions/query-core';\nimport { useFirestorePaginated, useReadRetryState } 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 const queryKey = ['notifications', 'active', category, userId, { pageSize }];\n const read = useFirestorePaginated<NotificationDoc>({\n collectionPath,\n queryKey,\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n refetchInterval,\n staleTime,\n });\n // The displayed page's failure held through its retry: a refetch of a never-succeeded page clears\n // `error` while it runs, so the list would drop its failure under the retry the person pressed.\n return { ...read, retryState: useReadRetryState(read, paginatedPageKey(queryKey, read.page)) };\n}\n"]}
1
+ {"version":3,"file":"useActiveNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,mCAAmC,CAAC;AAC3C,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,MAAM,QAAQ,GAAG,CAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,qBAAqB,CAAkB;QAClD,cAAc;QACd,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,eAAe;QACf,SAAS;KACV,CAAC,CAAC;IACH,kGAAkG;IAClG,gGAAgG;IAChG,OAAO,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AACjG,CAAC","sourcesContent":["'use client';\n\nimport {\n paginatedPageKey,\n useFirestorePaginated,\n useReadRetryState,\n} 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 const queryKey = ['notifications', 'active', category, userId, { pageSize }];\n const read = useFirestorePaginated<NotificationDoc>({\n collectionPath,\n queryKey,\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n refetchInterval,\n staleTime,\n });\n // The displayed page's failure held through its retry: a refetch of a never-succeeded page clears\n // `error` while it runs, so the list would drop its failure under the retry the person pressed.\n return { ...read, retryState: useReadRetryState(read, paginatedPageKey(queryKey, read.page)) };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"useArchiveAllNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,oBAAoB,EACpB,iCAAiC,EAClC,MAAM,gBAAgB,CAAC;AASxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAyC,EACzC,QAA4B,EAC5B,cAAc,GACf,EAAE,iCAAiC,iGAkEnC"}
1
+ {"version":3,"file":"useArchiveAllNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,iCAAiC,EAAE,MAAM,gBAAgB,CAAC;AAS9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAyC,EACzC,QAA4B,EAC5B,cAAc,GACf,EAAE,iCAAiC,iGA8EnC"}
@@ -66,7 +66,13 @@ export function useArchiveAllNotifications({ userId, category, enqueueArchiveAll
66
66
  };
67
67
  }
68
68
  if (snap.state === 'incomplete') {
69
- return { status: 'incomplete', category, archived: lastArchived, notArchived: 0, complete: false };
69
+ return {
70
+ status: 'incomplete',
71
+ category,
72
+ archived: lastArchived,
73
+ notArchived: 0,
74
+ complete: false,
75
+ };
70
76
  }
71
77
  if (snap.state === 'failed') {
72
78
  return {
@@ -83,7 +89,13 @@ export function useArchiveAllNotifications({ userId, category, enqueueArchiveAll
83
89
  await sleep(pollIntervalMs);
84
90
  }
85
91
  // Guard: the job never reached a terminal state within the poll budget — report incomplete.
86
- return { status: 'incomplete', category, archived: lastArchived, notArchived: 0, complete: false };
92
+ return {
93
+ status: 'incomplete',
94
+ category,
95
+ archived: lastArchived,
96
+ notArchived: 0,
97
+ complete: false,
98
+ };
87
99
  },
88
100
  onSuccess: (result) => {
89
101
  // Only a completed drain changed the active set; still invalidate on an incomplete drain since
@@ -1 +1 @@
1
- {"version":3,"file":"useArchiveAllNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport type {\n ArchiveAllPollResult,\n UseArchiveAllNotificationsOptions,\n} from '../../types.js';\n\nconst DEFAULT_POLL_INTERVAL_MS = 1500;\nconst DEFAULT_MAX_POLLS = 120; // ~3 min at the default interval\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Trigger + poll a SERVER-OWNED archive-all job for one notification category.\n *\n * This is a THIN STATUS POLLER, not a client loop. It:\n * 1. enqueues ONE server job via `enqueueArchiveAllFn(category)` (the category — the tab the tray\n * was invoked on — flows into the job, preserving per-tab scoping: the worker archives ONLY\n * that category's active cards, never all tabs);\n * 2. polls `getArchiveAllStatusFn(jobId)` until the job reaches a terminal state\n * (`complete` | `incomplete` | `failed`);\n * 3. resolves an {@link ArchiveAllPollResult} the caller gates side effects on, and invalidates\n * the read keys on a completed drain.\n *\n * All paging + ownership-checked per-card deletion happen server-side. The hook performs no client\n * Firestore writes and never chains archive calls. The generic package hardcodes no callable name —\n * the app injects both adapters.\n *\n * @example\n * ```tsx\n * const enqueue = httpsCallable(functions, 'enqueueArchiveAll');\n * const getStatus = httpsCallable(functions, 'getArchiveAllStatus');\n * const archiveAll = useArchiveAllNotifications({\n * userId: user.uid,\n * category: 'user',\n * enqueueArchiveAllFn: (category) => enqueue({ category }).then((r) => r.data),\n * getArchiveAllStatusFn: (jobId) => getStatus({ jobId }).then((r) => r.data),\n * });\n *\n * archiveAll.mutate();\n * ```\n */\nexport function useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,\n maxPolls = DEFAULT_MAX_POLLS,\n invalidateKeys,\n}: UseArchiveAllNotificationsOptions) {\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 // Server-owned archive-all: enqueue ONE job scoped to `category`, then poll its status until it\n // reaches a terminal state. No browser loop, no chain of archive transactions. The mutation\n // resolves with an explicit terminal result (never throws for an incomplete drain) — `complete`\n // is the load-bearing flag callers gate post-clear side effects on.\n mutationFn: async (): Promise<ArchiveAllPollResult> => {\n const { jobId } = await enqueueArchiveAllFn(category);\n\n // Poll until terminal or until the guard bound is hit. A job that never terminates resolves\n // `incomplete` (mirrors the old loop's honest \"some remain\" result) rather than hanging. The\n // first poll happens immediately so a fast/already-complete job resolves without a delay.\n let lastArchived = 0;\n for (let poll = 0; poll < maxPolls; poll += 1) {\n const snap = await getArchiveAllStatusFn(jobId);\n lastArchived = snap.archived ?? 0;\n\n if (snap.state === 'complete') {\n return {\n status: 'complete',\n category,\n archived: lastArchived,\n notArchived: snap.notArchived ?? 0,\n complete: true,\n };\n }\n if (snap.state === 'incomplete') {\n return { status: 'incomplete', category, archived: lastArchived, notArchived: 0, complete: false };\n }\n if (snap.state === 'failed') {\n return {\n status: 'failed',\n category,\n archived: lastArchived,\n notArchived: 0,\n complete: false,\n error: snap.error,\n };\n }\n\n // Still in-progress — wait before the next poll unless this was the last allowed poll.\n if (poll < maxPolls - 1) await sleep(pollIntervalMs);\n }\n\n // Guard: the job never reached a terminal state within the poll budget — report incomplete.\n return { status: 'incomplete', category, archived: lastArchived, notArchived: 0, complete: false };\n },\n onSuccess: (result) => {\n // Only a completed drain changed the active set; still invalidate on an incomplete drain since\n // SOME cards were archived. A hard enqueue/poll failure rejects the mutation (onError), so this\n // never runs for `failed`.\n const keysToInvalidate = invalidateKeys ?? defaultInvalidateKeys;\n keysToInvalidate.forEach((key) => {\n queryClient.invalidateQueries({ queryKey: [...key], exact: false });\n });\n void result;\n },\n });\n}\n"]}
1
+ {"version":3,"file":"useArchiveAllNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGpE,MAAM,wBAAwB,GAAG,IAAI,CAAC;AACtC,MAAM,iBAAiB,GAAG,GAAG,CAAC,CAAC,iCAAiC;AAEhE,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,GAAG,wBAAwB,EACzC,QAAQ,GAAG,iBAAiB,EAC5B,cAAc,GACoB;IAClC,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,gGAAgG;QAChG,4FAA4F;QAC5F,gGAAgG;QAChG,oEAAoE;QACpE,UAAU,EAAE,KAAK,IAAmC,EAAE;YACpD,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAC;YAEtD,4FAA4F;YAC5F,6FAA6F;YAC7F,0FAA0F;YAC1F,IAAI,YAAY,GAAG,CAAC,CAAC;YACrB,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,QAAQ,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;gBAC9C,MAAM,IAAI,GAAG,MAAM,qBAAqB,CAAC,KAAK,CAAC,CAAC;gBAChD,YAAY,GAAG,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;gBAElC,IAAI,IAAI,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;oBAC9B,OAAO;wBACL,MAAM,EAAE,UAAU;wBAClB,QAAQ;wBACR,QAAQ,EAAE,YAAY;wBACtB,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,CAAC;wBAClC,QAAQ,EAAE,IAAI;qBACf,CAAC;gBACJ,CAAC;gBACD,IAAI,IAAI,CAAC,KAAK,KAAK,YAAY,EAAE,CAAC;oBAChC,OAAO;wBACL,MAAM,EAAE,YAAY;wBACpB,QAAQ;wBACR,QAAQ,EAAE,YAAY;wBACtB,WAAW,EAAE,CAAC;wBACd,QAAQ,EAAE,KAAK;qBAChB,CAAC;gBACJ,CAAC;gBACD,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;oBAC5B,OAAO;wBACL,MAAM,EAAE,QAAQ;wBAChB,QAAQ;wBACR,QAAQ,EAAE,YAAY;wBACtB,WAAW,EAAE,CAAC;wBACd,QAAQ,EAAE,KAAK;wBACf,KAAK,EAAE,IAAI,CAAC,KAAK;qBAClB,CAAC;gBACJ,CAAC;gBAED,uFAAuF;gBACvF,IAAI,IAAI,GAAG,QAAQ,GAAG,CAAC;oBAAE,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC;YACvD,CAAC;YAED,4FAA4F;YAC5F,OAAO;gBACL,MAAM,EAAE,YAAY;gBACpB,QAAQ;gBACR,QAAQ,EAAE,YAAY;gBACtB,WAAW,EAAE,CAAC;gBACd,QAAQ,EAAE,KAAK;aAChB,CAAC;QACJ,CAAC;QACD,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE;YACpB,+FAA+F;YAC/F,gGAAgG;YAChG,2BAA2B;YAC3B,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;YACH,KAAK,MAAM,CAAC;QACd,CAAC;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport type { ArchiveAllPollResult, UseArchiveAllNotificationsOptions } from '../../types.js';\n\nconst DEFAULT_POLL_INTERVAL_MS = 1500;\nconst DEFAULT_MAX_POLLS = 120; // ~3 min at the default interval\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Trigger + poll a SERVER-OWNED archive-all job for one notification category.\n *\n * This is a THIN STATUS POLLER, not a client loop. It:\n * 1. enqueues ONE server job via `enqueueArchiveAllFn(category)` (the category — the tab the tray\n * was invoked on — flows into the job, preserving per-tab scoping: the worker archives ONLY\n * that category's active cards, never all tabs);\n * 2. polls `getArchiveAllStatusFn(jobId)` until the job reaches a terminal state\n * (`complete` | `incomplete` | `failed`);\n * 3. resolves an {@link ArchiveAllPollResult} the caller gates side effects on, and invalidates\n * the read keys on a completed drain.\n *\n * All paging + ownership-checked per-card deletion happen server-side. The hook performs no client\n * Firestore writes and never chains archive calls. The generic package hardcodes no callable name —\n * the app injects both adapters.\n *\n * @example\n * ```tsx\n * const enqueue = httpsCallable(functions, 'enqueueArchiveAll');\n * const getStatus = httpsCallable(functions, 'getArchiveAllStatus');\n * const archiveAll = useArchiveAllNotifications({\n * userId: user.uid,\n * category: 'user',\n * enqueueArchiveAllFn: (category) => enqueue({ category }).then((r) => r.data),\n * getArchiveAllStatusFn: (jobId) => getStatus({ jobId }).then((r) => r.data),\n * });\n *\n * archiveAll.mutate();\n * ```\n */\nexport function useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,\n maxPolls = DEFAULT_MAX_POLLS,\n invalidateKeys,\n}: UseArchiveAllNotificationsOptions) {\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 // Server-owned archive-all: enqueue ONE job scoped to `category`, then poll its status until it\n // reaches a terminal state. No browser loop, no chain of archive transactions. The mutation\n // resolves with an explicit terminal result (never throws for an incomplete drain) — `complete`\n // is the load-bearing flag callers gate post-clear side effects on.\n mutationFn: async (): Promise<ArchiveAllPollResult> => {\n const { jobId } = await enqueueArchiveAllFn(category);\n\n // Poll until terminal or until the guard bound is hit. A job that never terminates resolves\n // `incomplete` (mirrors the old loop's honest \"some remain\" result) rather than hanging. The\n // first poll happens immediately so a fast/already-complete job resolves without a delay.\n let lastArchived = 0;\n for (let poll = 0; poll < maxPolls; poll += 1) {\n const snap = await getArchiveAllStatusFn(jobId);\n lastArchived = snap.archived ?? 0;\n\n if (snap.state === 'complete') {\n return {\n status: 'complete',\n category,\n archived: lastArchived,\n notArchived: snap.notArchived ?? 0,\n complete: true,\n };\n }\n if (snap.state === 'incomplete') {\n return {\n status: 'incomplete',\n category,\n archived: lastArchived,\n notArchived: 0,\n complete: false,\n };\n }\n if (snap.state === 'failed') {\n return {\n status: 'failed',\n category,\n archived: lastArchived,\n notArchived: 0,\n complete: false,\n error: snap.error,\n };\n }\n\n // Still in-progress — wait before the next poll unless this was the last allowed poll.\n if (poll < maxPolls - 1) await sleep(pollIntervalMs);\n }\n\n // Guard: the job never reached a terminal state within the poll budget — report incomplete.\n return {\n status: 'incomplete',\n category,\n archived: lastArchived,\n notArchived: 0,\n complete: false,\n };\n },\n onSuccess: (result) => {\n // Only a completed drain changed the active set; still invalidate on an incomplete drain since\n // SOME cards were archived. A hard enqueue/poll failure rejects the mutation (onError), so this\n // never runs for `failed`.\n const keysToInvalidate = invalidateKeys ?? defaultInvalidateKeys;\n keysToInvalidate.forEach((key) => {\n queryClient.invalidateQueries({ queryKey: [...key], exact: false });\n });\n void result;\n },\n });\n}\n"]}
@@ -15,7 +15,7 @@ import type { NotificationHistoryItem, UseNotificationHistoryOptions } from '../
15
15
  */
16
16
  export declare function useNotificationHistory({ config, userId, category, enabled, pageSize, staleTime, }: UseNotificationHistoryOptions): {
17
17
  retryState: import("@ttt-productions/query-core/react").ReadRetryState<Error>;
18
- promise: Promise<import("@ttt-productions/query-core").PaginatedPage<import("@ttt-productions/query-core").WithId<NotificationHistoryItem>>>;
18
+ promise: Promise<import("@ttt-productions/query-core/react").PaginatedPage<import("@ttt-productions/query-core/types").WithId<NotificationHistoryItem>>>;
19
19
  error: Error | null;
20
20
  isError: boolean;
21
21
  isPending: boolean;
@@ -38,9 +38,9 @@ export declare function useNotificationHistory({ config, userId, category, enabl
38
38
  isRefetching: boolean;
39
39
  isStale: boolean;
40
40
  isEnabled: boolean;
41
- refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<import("@ttt-productions/query-core").PaginatedPage<import("@ttt-productions/query-core").WithId<NotificationHistoryItem>>, Error>>;
41
+ refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<import("@ttt-productions/query-core/react").PaginatedPage<import("@ttt-productions/query-core/types").WithId<NotificationHistoryItem>>, Error>>;
42
42
  fetchStatus: import("@tanstack/query-core").FetchStatus;
43
- data: import("@ttt-productions/query-core").WithId<NotificationHistoryItem>[] | undefined;
43
+ data: import("@ttt-productions/query-core/types").WithId<NotificationHistoryItem>[] | undefined;
44
44
  page: number;
45
45
  pageSize: number;
46
46
  hasNextPage: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"useNotificationHistory.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useNotificationHistory.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,uBAAuB,EACvB,6BAA6B,EAC9B,MAAM,gBAAgB,CAAC;AAKxB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,SAA8B,GAC/B,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgC/B"}
1
+ {"version":3,"file":"useNotificationHistory.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useNotificationHistory.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAEV,uBAAuB,EACvB,6BAA6B,EAC9B,MAAM,gBAAgB,CAAC;AAKxB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,SAA8B,GAC/B,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgC/B"}
@@ -1,6 +1,5 @@
1
1
  'use client';
2
- import { paginatedPageKey } from '@ttt-productions/query-core';
3
- import { useFirestorePaginated, useReadRetryState } from '@ttt-productions/query-core/react';
2
+ import { paginatedPageKey, useFirestorePaginated, useReadRetryState, } from '@ttt-productions/query-core/react';
4
3
  import { orderBy } from 'firebase/firestore';
5
4
  const DEFAULT_PAGE_SIZE = 20;
6
5
  const DEFAULT_STALE_TIME = 60_000;
@@ -1 +1 @@
1
- {"version":3,"file":"useNotificationHistory.js","sourceRoot":"","sources":["../../../src/react/hooks/useNotificationHistory.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC7F,OAAO,EAAE,OAAO,EAAwB,MAAM,oBAAoB,CAAC;AAOnE,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,QAAQ,GAAG,iBAAiB,EAC5B,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,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1D,MAAM,WAAW,GAAsB,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IAEvE,MAAM,QAAQ,GAAG,CAAC,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC9E,MAAM,IAAI,GAAG,qBAAqB,CAA0B;QAC1D,cAAc;QACd,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,SAAS;QACT,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACf,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAoB,CAAC;YAClE,OAAO;gBACL,GAAG,QAAQ;gBACX,kEAAkE;gBAClE,mCAAmC;gBACnC,mBAAmB,EAAE,IAAI,CAAC,EAAE;gBAC5B,UAAU,EAAG,IAAI,CAAC,UAAiC,IAAI,QAAQ,CAAC,SAAS,IAAI,CAAC;aAC/E,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;IACH,kGAAkG;IAClG,gGAAgG;IAChG,OAAO,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AACjG,CAAC","sourcesContent":["'use client';\n\nimport { paginatedPageKey } from '@ttt-productions/query-core';\nimport { useFirestorePaginated, useReadRetryState } from '@ttt-productions/query-core/react';\nimport { orderBy, type QueryConstraint } from 'firebase/firestore';\nimport type {\n NotificationDoc,\n NotificationHistoryItem,\n UseNotificationHistoryOptions,\n} from '../../types.js';\n\nconst DEFAULT_PAGE_SIZE = 20;\nconst DEFAULT_STALE_TIME = 60_000;\n\n/**\n * Paginated read of ARCHIVED notification history (the two-tier system's history\n * tier). Personal history lives under a per-user path\n * (`userProfiles/{uid}/notificationHistory`) and shared/admin history under a\n * top-level collection (`adminNotificationHistory`) — both resolved from the\n * category's `historyPath`, so no `where` clause is needed either way. Rows are\n * immutable and ordered newest-first by `archivedAt`.\n *\n * Each history doc is a wrapper (`archivedSnapshot` + archive metadata + a\n * native-TTL `expireAt`); the `select` mapper flattens it into a\n * {@link NotificationHistoryItem} so the read surface renders like the active\n * list. Owner-only (user) / admin-only (admin) reads are enforced by Firestore\n * rules — this hook performs no writes.\n */\nexport function useNotificationHistory({\n config,\n userId,\n category,\n enabled = true,\n pageSize = DEFAULT_PAGE_SIZE,\n staleTime = DEFAULT_STALE_TIME,\n}: UseNotificationHistoryOptions) {\n const categoryConfig = config.categories[category];\n if (!categoryConfig) {\n throw new Error(`[notification-core] Unknown category: ${category}`);\n }\n\n const collectionPath = categoryConfig.historyPath(userId);\n\n const constraints: QueryConstraint[] = [orderBy('archivedAt', 'desc')];\n\n const queryKey = ['notifications', 'history', category, userId, { pageSize }];\n const read = useFirestorePaginated<NotificationHistoryItem>({\n collectionPath,\n queryKey,\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n staleTime,\n select: (data) => {\n const snapshot = (data.archivedSnapshot ?? {}) as NotificationDoc;\n return {\n ...snapshot,\n // The archive occurrence id is the stable history-doc key; expose\n // archivedAt for ordering/display.\n archiveOccurrenceId: data.id,\n archivedAt: (data.archivedAt as number | undefined) ?? snapshot.updatedAt ?? 0,\n };\n },\n });\n // The displayed page's failure held through its retry: a refetch of a never-succeeded page clears\n // `error` while it runs, so the list would drop its failure under the retry the person pressed.\n return { ...read, retryState: useReadRetryState(read, paginatedPageKey(queryKey, read.page)) };\n}\n"]}
1
+ {"version":3,"file":"useNotificationHistory.js","sourceRoot":"","sources":["../../../src/react/hooks/useNotificationHistory.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAwB,MAAM,oBAAoB,CAAC;AAOnE,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,QAAQ,GAAG,iBAAiB,EAC5B,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,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1D,MAAM,WAAW,GAAsB,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IAEvE,MAAM,QAAQ,GAAG,CAAC,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC9E,MAAM,IAAI,GAAG,qBAAqB,CAA0B;QAC1D,cAAc;QACd,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,SAAS;QACT,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACf,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,gBAAgB,IAAI,EAAE,CAAoB,CAAC;YAClE,OAAO;gBACL,GAAG,QAAQ;gBACX,kEAAkE;gBAClE,mCAAmC;gBACnC,mBAAmB,EAAE,IAAI,CAAC,EAAE;gBAC5B,UAAU,EAAG,IAAI,CAAC,UAAiC,IAAI,QAAQ,CAAC,SAAS,IAAI,CAAC;aAC/E,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;IACH,kGAAkG;IAClG,gGAAgG;IAChG,OAAO,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AACjG,CAAC","sourcesContent":["'use client';\n\nimport {\n paginatedPageKey,\n useFirestorePaginated,\n useReadRetryState,\n} from '@ttt-productions/query-core/react';\nimport { orderBy, type QueryConstraint } from 'firebase/firestore';\nimport type {\n NotificationDoc,\n NotificationHistoryItem,\n UseNotificationHistoryOptions,\n} from '../../types.js';\n\nconst DEFAULT_PAGE_SIZE = 20;\nconst DEFAULT_STALE_TIME = 60_000;\n\n/**\n * Paginated read of ARCHIVED notification history (the two-tier system's history\n * tier). Personal history lives under a per-user path\n * (`userProfiles/{uid}/notificationHistory`) and shared/admin history under a\n * top-level collection (`adminNotificationHistory`) — both resolved from the\n * category's `historyPath`, so no `where` clause is needed either way. Rows are\n * immutable and ordered newest-first by `archivedAt`.\n *\n * Each history doc is a wrapper (`archivedSnapshot` + archive metadata + a\n * native-TTL `expireAt`); the `select` mapper flattens it into a\n * {@link NotificationHistoryItem} so the read surface renders like the active\n * list. Owner-only (user) / admin-only (admin) reads are enforced by Firestore\n * rules — this hook performs no writes.\n */\nexport function useNotificationHistory({\n config,\n userId,\n category,\n enabled = true,\n pageSize = DEFAULT_PAGE_SIZE,\n staleTime = DEFAULT_STALE_TIME,\n}: UseNotificationHistoryOptions) {\n const categoryConfig = config.categories[category];\n if (!categoryConfig) {\n throw new Error(`[notification-core] Unknown category: ${category}`);\n }\n\n const collectionPath = categoryConfig.historyPath(userId);\n\n const constraints: QueryConstraint[] = [orderBy('archivedAt', 'desc')];\n\n const queryKey = ['notifications', 'history', category, userId, { pageSize }];\n const read = useFirestorePaginated<NotificationHistoryItem>({\n collectionPath,\n queryKey,\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n staleTime,\n select: (data) => {\n const snapshot = (data.archivedSnapshot ?? {}) as NotificationDoc;\n return {\n ...snapshot,\n // The archive occurrence id is the stable history-doc key; expose\n // archivedAt for ordering/display.\n archiveOccurrenceId: data.id,\n archivedAt: (data.archivedAt as number | undefined) ?? snapshot.updatedAt ?? 0,\n };\n },\n });\n // The displayed page's failure held through its retry: a refetch of a never-succeeded page clears\n // `error` while it runs, so the list would drop its failure under the retry the person pressed.\n return { ...read, retryState: useReadRetryState(read, paginatedPageKey(queryKey, read.page)) };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"useUnreadCount.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useUnreadCount.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAK5D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,eAA0C,EAC1C,UAAgC,GACjC,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8BvB"}
1
+ {"version":3,"file":"useUnreadCount.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useUnreadCount.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAK5D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,eAA0C,EAC1C,UAAgC,GACjC,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4BvB"}
@@ -32,9 +32,7 @@ export function useUnreadCount({ config, userId, category, enabled = true, refet
32
32
  }
33
33
  const collectionPath = categoryConfig.activePath;
34
34
  const isPersonal = categoryConfig.audienceType === 'personal';
35
- const constraints = isPersonal && userId
36
- ? [where('targetUserId', '==', userId), where('seenAt', '==', 0)]
37
- : [];
35
+ const constraints = isPersonal && userId ? [where('targetUserId', '==', userId), where('seenAt', '==', 0)] : [];
38
36
  const { data, ...rest } = useFirestoreCount({
39
37
  collectionPath,
40
38
  queryKey: ['notifications', 'unread-count', category, userId],
@@ -1 +1 @@
1
- {"version":3,"file":"useUnreadCount.js","sourceRoot":"","sources":["../../../src/react/hooks/useUnreadCount.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,KAAK,EAAwB,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAGtE,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,cAAc,CAAC,EAC7B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,eAAe,GAAG,wBAAwB,EAC1C,UAAU,GAAG,mBAAmB,GACV;IACtB,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,GACf,UAAU,IAAI,MAAM;QAClB,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACjE,CAAC,CAAC,EAAE,CAAC;IAET,MAAM,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,GAAG,iBAAiB,CAAC;QAC1C,cAAc;QACd,QAAQ,EAAE,CAAC,eAAe,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC;QAC7D,WAAW;QACX,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,SAAS,EAAE,eAAe;QAC1B,eAAe;KAChB,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC;IAExB,OAAO;QACL,KAAK;QACL,OAAO,EAAE,KAAK,GAAG,UAAU;QAC3B,GAAG,IAAI;KACR,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { where, type QueryConstraint } from 'firebase/firestore';\nimport { useFirestoreCount } from '@ttt-productions/query-core/react';\nimport type { UseUnreadCountOptions } from '../../types.js';\n\nconst DEFAULT_REFETCH_INTERVAL = 30_000;\nconst DEFAULT_COUNT_LIMIT = 99;\n\n/**\n * Unread-count badge backed by a Firestore `count()` aggregation (a server-side\n * count, not a doc fetch), via query-core's `useFirestoreCount`. Polls for cost\n * control; displays capped (`hasMore` → `99+`).\n *\n * - **personal** categories count **unseen** active items (`seenAt == 0`),\n * scoped to the caller (`targetUserId == uid`);\n * - **shared** categories have no per-admin seen state, so the indicator is\n * existence-based — a count of all active items, with no `seenAt` predicate.\n *\n * The personal `seenAt == 0` predicate needs a composite index, captured as an\n * app-side index step.\n *\n * @example\n * ```tsx\n * const { count, isLoading } = useUnreadCount({\n * config: TTT_NOTIFICATION_CONFIG,\n * userId: user.uid,\n * category: 'user',\n * });\n * ```\n */\nexport function useUnreadCount({\n config,\n userId,\n category,\n enabled = true,\n refetchInterval = DEFAULT_REFETCH_INTERVAL,\n countLimit = DEFAULT_COUNT_LIMIT,\n}: UseUnreadCountOptions) {\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 && userId\n ? [where('targetUserId', '==', userId), where('seenAt', '==', 0)]\n : [];\n\n const { data, ...rest } = useFirestoreCount({\n collectionPath,\n queryKey: ['notifications', 'unread-count', category, userId],\n constraints,\n enabled: enabled && !!userId,\n staleTime: refetchInterval,\n refetchInterval,\n });\n\n const count = data ?? 0;\n\n return {\n count,\n hasMore: count > countLimit,\n ...rest,\n };\n}\n"]}
1
+ {"version":3,"file":"useUnreadCount.js","sourceRoot":"","sources":["../../../src/react/hooks/useUnreadCount.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,KAAK,EAAwB,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAGtE,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,cAAc,CAAC,EAC7B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAO,GAAG,IAAI,EACd,eAAe,GAAG,wBAAwB,EAC1C,UAAU,GAAG,mBAAmB,GACV;IACtB,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,GACf,UAAU,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE9F,MAAM,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,GAAG,iBAAiB,CAAC;QAC1C,cAAc;QACd,QAAQ,EAAE,CAAC,eAAe,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC;QAC7D,WAAW;QACX,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,SAAS,EAAE,eAAe;QAC1B,eAAe;KAChB,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC;IAExB,OAAO;QACL,KAAK;QACL,OAAO,EAAE,KAAK,GAAG,UAAU;QAC3B,GAAG,IAAI;KACR,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { where, type QueryConstraint } from 'firebase/firestore';\nimport { useFirestoreCount } from '@ttt-productions/query-core/react';\nimport type { UseUnreadCountOptions } from '../../types.js';\n\nconst DEFAULT_REFETCH_INTERVAL = 30_000;\nconst DEFAULT_COUNT_LIMIT = 99;\n\n/**\n * Unread-count badge backed by a Firestore `count()` aggregation (a server-side\n * count, not a doc fetch), via query-core's `useFirestoreCount`. Polls for cost\n * control; displays capped (`hasMore` → `99+`).\n *\n * - **personal** categories count **unseen** active items (`seenAt == 0`),\n * scoped to the caller (`targetUserId == uid`);\n * - **shared** categories have no per-admin seen state, so the indicator is\n * existence-based — a count of all active items, with no `seenAt` predicate.\n *\n * The personal `seenAt == 0` predicate needs a composite index, captured as an\n * app-side index step.\n *\n * @example\n * ```tsx\n * const { count, isLoading } = useUnreadCount({\n * config: TTT_NOTIFICATION_CONFIG,\n * userId: user.uid,\n * category: 'user',\n * });\n * ```\n */\nexport function useUnreadCount({\n config,\n userId,\n category,\n enabled = true,\n refetchInterval = DEFAULT_REFETCH_INTERVAL,\n countLimit = DEFAULT_COUNT_LIMIT,\n}: UseUnreadCountOptions) {\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 && userId ? [where('targetUserId', '==', userId), where('seenAt', '==', 0)] : [];\n\n const { data, ...rest } = useFirestoreCount({\n collectionPath,\n queryKey: ['notifications', 'unread-count', category, userId],\n constraints,\n enabled: enabled && !!userId,\n staleTime: refetchInterval,\n refetchInterval,\n });\n\n const count = data ?? 0;\n\n return {\n count,\n hasMore: count > countLimit,\n ...rest,\n };\n}\n"]}
@@ -1,3 +1,11 @@
1
- export { useActiveNotifications, useNotificationHistory, useUnreadCount, useArchiveNotification, useArchiveAllNotifications, } from './hooks/index.js';
2
- export { NotificationList, NotificationHistoryList, NotificationEmptyState, NotificationUnreadBadge, } from './components/index.js';
1
+ export { useActiveNotifications } from './hooks/useActiveNotifications.js';
2
+ export { useNotificationHistory } from './hooks/useNotificationHistory.js';
3
+ export { useUnreadCount } from './hooks/useUnreadCount.js';
4
+ export { useArchiveNotification } from './hooks/useArchiveNotification.js';
5
+ export { useArchiveAllNotifications } from './hooks/useArchiveAllNotifications.js';
6
+ export { NotificationList } from './components/NotificationList.js';
7
+ export { NotificationHistoryList } from './components/NotificationHistoryList.js';
8
+ export { NotificationEmptyState } from './components/NotificationEmptyState.js';
9
+ export { NotificationUnreadBadge } from './components/NotificationUnreadBadge.js';
10
+ export type { NotificationListProps, NotificationHistoryListProps } from './types.js';
3
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EACtB,cAAc,EACd,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAGnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAElF,YAAY,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC"}