@ttt-productions/notification-core 0.15.8 → 0.15.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/components/NotificationHistoryList.d.ts +3 -3
- package/dist/react/components/NotificationHistoryList.d.ts.map +1 -1
- package/dist/react/components/NotificationHistoryList.js +23 -10
- package/dist/react/components/NotificationHistoryList.js.map +1 -1
- package/dist/react/components/NotificationList.d.ts +1 -1
- package/dist/react/components/NotificationList.d.ts.map +1 -1
- package/dist/react/components/NotificationList.js +105 -26
- package/dist/react/components/NotificationList.js.map +1 -1
- package/dist/react/components/NotificationUnreadBadge.d.ts +1 -1
- package/dist/react/components/NotificationUnreadBadge.d.ts.map +1 -1
- package/dist/react/components/NotificationUnreadBadge.js +2 -1
- package/dist/react/components/NotificationUnreadBadge.js.map +1 -1
- package/dist/react/components/notification-type-icon.d.ts +11 -0
- package/dist/react/components/notification-type-icon.d.ts.map +1 -0
- package/dist/react/components/notification-type-icon.js +12 -0
- package/dist/react/components/notification-type-icon.js.map +1 -0
- package/dist/react/hooks/useActiveNotifications.d.ts +3 -3
- package/dist/react/hooks/useActiveNotifications.d.ts.map +1 -1
- package/dist/react/hooks/useActiveNotifications.js +8 -5
- package/dist/react/hooks/useActiveNotifications.js.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.d.ts +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.js +3 -7
- package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
- package/dist/react/hooks/useArchiveNotification.d.ts +7 -20
- package/dist/react/hooks/useArchiveNotification.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveNotification.js +9 -26
- package/dist/react/hooks/useArchiveNotification.js.map +1 -1
- package/dist/react/hooks/useNotificationHistory.d.ts +1 -1
- package/dist/react/hooks/useNotificationHistory.d.ts.map +1 -1
- package/dist/react/hooks/useNotificationHistory.js +3 -2
- package/dist/react/hooks/useNotificationHistory.js.map +1 -1
- package/dist/react/hooks/useUnreadCount.d.ts +10 -2
- package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
- package/dist/react/hooks/useUnreadCount.js +16 -6
- package/dist/react/hooks/useUnreadCount.js.map +1 -1
- package/dist/react/query-keys.d.ts +5 -0
- package/dist/react/query-keys.d.ts.map +1 -0
- package/dist/react/query-keys.js +15 -0
- package/dist/react/query-keys.js.map +1 -0
- package/dist/server/delivery-ledger.d.ts +34 -5
- package/dist/server/delivery-ledger.d.ts.map +1 -1
- package/dist/server/delivery-ledger.js +24 -5
- package/dist/server/delivery-ledger.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +123 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -5
- package/src/styles/notifications.css +10 -33
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export type { NotificationDoc, PendingNotification, NotificationCategoryConfig, NotificationTypeConfig, NotificationSystemConfig, UseActiveNotificationsOptions, UseNotificationHistoryOptions, NotificationHistoryItem, UseUnreadCountOptions, UseArchiveNotificationOptions, UseArchiveAllNotificationsOptions, ArchiveAllJobStatus, EnqueueArchiveAllResult, ArchiveAllJobSnapshot, ArchiveAllPollResult, NotificationRowActions, NotificationListProps, NotificationHistoryListProps, NotificationEmptyStateProps, NotificationUnreadBadgeProps, } from './types.js';
|
|
1
|
+
export type { NotificationDoc, PendingNotification, NotificationCategoryConfig, NotificationTypeConfig, NotificationSystemConfig, UseActiveNotificationsOptions, UseNotificationHistoryOptions, NotificationHistoryItem, UseUnreadCountOptions, UseArchiveNotificationOptions, UseArchiveAllNotificationsOptions, ArchiveAllJobStatus, EnqueueArchiveAllResult, ArchiveAllJobSnapshot, ArchiveAllPollResult, NotificationRowActions, NotificationArchiveResult, NotificationArchiveFn, NotificationQueryKeys, NotificationListErrorState, NotificationListLabels, NotificationListProps, NotificationHistoryListProps, NotificationEmptyStateProps, NotificationUnreadBadgeProps, } from './types.js';
|
|
2
2
|
export { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,iCAAiC,EACjC,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,qBAAqB,EACrB,4BAA4B,EAC5B,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,uBAAuB,EACvB,qBAAqB,EACrB,6BAA6B,EAC7B,iCAAiC,EACjC,mBAAmB,EACnB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,qBAAqB,EACrB,0BAA0B,EAC1B,sBAAsB,EACtB,qBAAqB,EACrB,4BAA4B,EAC5B,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4BA,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 NotificationQueryKeys,\n NotificationListErrorState,\n NotificationListLabels,\n NotificationListProps,\n NotificationHistoryListProps,\n NotificationEmptyStateProps,\n NotificationUnreadBadgeProps,\n} from './types.js';\n\n// Runtime schemas for the generic notification doc shapes (notification-core owns\n// these; consuming apps compose them instead of hand-mirroring). Server- and\n// UI-safe (zod only, no React/firebase).\nexport { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';\n"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { NotificationHistoryListProps } from '../../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are
|
|
4
|
-
* inert
|
|
5
|
-
*
|
|
4
|
+
* inert and read-only — archive is one-way, there is no re-archive, so the row
|
|
5
|
+
* exposes no `archive` action. Any per-row control (e.g. an
|
|
6
6
|
* ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
|
|
7
7
|
*/
|
|
8
|
-
export declare function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, title, renderRowAction, }: NotificationHistoryListProps): import("react").JSX.Element;
|
|
8
|
+
export declare function NotificationHistoryList({ config, userId, category, queryKeys, pageSize, staleTime, emptyText, labels, renderError, title, renderRowAction, }: NotificationHistoryListProps): import("react").JSX.Element;
|
|
9
9
|
//# sourceMappingURL=NotificationHistoryList.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA2B,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+
|
|
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,SAAS,EACT,QAAQ,EACR,SAAS,EACT,SAAS,EACT,MAAM,EACN,WAAW,EACX,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BAmG9B"}
|
|
@@ -1,33 +1,46 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback } from 'react';
|
|
4
3
|
import { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';
|
|
5
4
|
import { useNotificationHistory } from '../hooks/useNotificationHistory.js';
|
|
6
5
|
import { NotificationEmptyState } from './NotificationEmptyState.js';
|
|
6
|
+
import { NotificationTypeIcon } from './notification-type-icon.js';
|
|
7
7
|
import { formatRelativeTime } from './relative-time.js';
|
|
8
8
|
/**
|
|
9
9
|
* Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are
|
|
10
|
-
* inert
|
|
11
|
-
*
|
|
10
|
+
* inert and read-only — archive is one-way, there is no re-archive, so the row
|
|
11
|
+
* exposes no `archive` action. Any per-row control (e.g. an
|
|
12
12
|
* ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
|
|
13
13
|
*/
|
|
14
|
-
export function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, title, renderRowAction, }) {
|
|
15
|
-
const { data: notifications, isLoading, isFetching, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useNotificationHistory({
|
|
14
|
+
export function NotificationHistoryList({ config, userId, category, queryKeys, pageSize, staleTime, emptyText, labels, renderError, title, renderRowAction, }) {
|
|
15
|
+
const { data: notifications, isLoading, isFetching, isError, error, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useNotificationHistory({
|
|
16
16
|
config,
|
|
17
17
|
userId,
|
|
18
18
|
category,
|
|
19
19
|
pageSize,
|
|
20
20
|
staleTime,
|
|
21
|
+
queryKeys,
|
|
21
22
|
});
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const hasRows = !!notifications && notifications.length > 0;
|
|
24
|
+
// A later page that came back empty keeps its pager so the user can step back; the
|
|
25
|
+
// empty state is the answer for page 1 only.
|
|
26
|
+
const showEmptyState = !isError && notifications !== undefined && notifications.length === 0 && page === 1;
|
|
27
|
+
return (_jsxs("div", { className: "ntf-list ntf-list-history", children: [title != null && (_jsxs(_Fragment, { children: [_jsx("div", { className: "ntf-list-header ntf-list-header-read-only", children: _jsx("div", { className: "ntf-list-title", children: title }) }), _jsx(Separator, {})] })), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: labels?.loading ?? 'Loading notifications' }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => {
|
|
28
|
+
return (_jsxs("div", { className: "ntf-item ntf-item-archived", children: [_jsx(NotificationTypeIcon, { config: config, type: 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));
|
|
29
|
+
}), isError && error
|
|
30
|
+
? renderError({
|
|
31
|
+
error,
|
|
32
|
+
retry: () => {
|
|
33
|
+
void refetch();
|
|
34
|
+
},
|
|
35
|
+
retrying: isFetching,
|
|
36
|
+
hasRows,
|
|
37
|
+
})
|
|
38
|
+
: null, showEmptyState && _jsx(NotificationEmptyState, { text: emptyText }), _jsx(ListPagination, { pagination: {
|
|
26
39
|
currentPage: page,
|
|
27
40
|
canPreviousPage: hasPrevPage,
|
|
28
41
|
canNextPage: hasNextPage,
|
|
29
42
|
goToPreviousPage: prevPage,
|
|
30
43
|
goToNextPage: nextPage,
|
|
31
|
-
}, busy: isFetching, className: "ntf-list-footer
|
|
44
|
+
}, busy: isFetching, className: "ntf-list-footer" })] })) })] }));
|
|
32
45
|
}
|
|
33
46
|
//# sourceMappingURL=NotificationHistoryList.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationHistoryList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"NotificationHistoryList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useNotificationHistory } from '../hooks/useNotificationHistory.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { NotificationTypeIcon } from './notification-type-icon.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 queryKeys,\n pageSize,\n staleTime,\n emptyText,\n labels,\n renderError,\n title,\n renderRowAction,\n}: NotificationHistoryListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n isError,\n error,\n refetch,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useNotificationHistory({\n config,\n userId,\n category,\n pageSize,\n staleTime,\n queryKeys,\n });\n\n const hasRows = !!notifications && notifications.length > 0;\n // A later page that came back empty keeps its pager so the user can step back; the\n // empty state is the answer for page 1 only.\n const showEmptyState =\n !isError && notifications !== undefined && notifications.length === 0 && page === 1;\n\n return (\n <div className=\"ntf-list ntf-list-history\">\n {title != null && (\n <>\n <div className=\"ntf-list-header ntf-list-header-read-only\">\n <div className=\"ntf-list-title\">{title}</div>\n </div>\n <Separator />\n </>\n )}\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label={labels?.loading ?? '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 <NotificationTypeIcon config={config} type={notification.type} />\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.archivedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {})}\n </div>\n )}\n </div>\n );\n })}\n {isError && error\n ? renderError({\n error,\n retry: () => {\n void refetch();\n },\n retrying: isFetching,\n hasRows,\n })\n : null}\n {showEmptyState && <NotificationEmptyState text={emptyText} />}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
|
|
@@ -2,5 +2,5 @@ import type { NotificationListProps } from '../../types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Scrollable list of active notifications with click-to-archive and clear-all.
|
|
4
4
|
*/
|
|
5
|
-
export declare function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, emptyText, renderRowAction, }: NotificationListProps): import("react").JSX.Element;
|
|
5
|
+
export declare function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, queryKeys, title, onClearAll, refetchInterval, staleTime, emptyText, labels, renderError, onRenderedRowsChange, renderRowAction, }: NotificationListProps): import("react").JSX.Element;
|
|
6
6
|
//# sourceMappingURL=NotificationList.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAIV,qBAAqB,EACtB,MAAM,gBAAgB,CAAC;AAYxB;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,SAAS,EACT,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,SAAS,EACT,MAAM,EACN,WAAW,EACX,oBAAoB,EACpB,eAAe,GAChB,EAAE,qBAAqB,+BAyQvB"}
|
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';
|
|
5
5
|
import { useActiveNotifications } from '../hooks/useActiveNotifications.js';
|
|
6
6
|
import { useArchiveNotification } from '../hooks/useArchiveNotification.js';
|
|
7
7
|
import { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';
|
|
8
8
|
import { NotificationEmptyState } from './NotificationEmptyState.js';
|
|
9
|
+
import { NotificationTypeIcon } from './notification-type-icon.js';
|
|
9
10
|
import { formatRelativeTime } from './relative-time.js';
|
|
11
|
+
const DEFAULT_LABELS = {
|
|
12
|
+
clearAll: 'Clear All',
|
|
13
|
+
clearing: 'Clearing...',
|
|
14
|
+
clearIncomplete: 'Some notifications remain — try again.',
|
|
15
|
+
loading: 'Loading notifications',
|
|
16
|
+
};
|
|
17
|
+
const FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
10
18
|
/**
|
|
11
19
|
* Scrollable list of active notifications with click-to-archive and clear-all.
|
|
12
20
|
*/
|
|
13
|
-
export function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, emptyText, renderRowAction, }) {
|
|
14
|
-
const { data: notifications, isLoading, isFetching, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useActiveNotifications({
|
|
21
|
+
export function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, queryKeys, title, onClearAll, refetchInterval, staleTime, emptyText, labels, renderError, onRenderedRowsChange, renderRowAction, }) {
|
|
22
|
+
const { data: notifications, isLoading, isFetching, isError, error, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useActiveNotifications({
|
|
15
23
|
config,
|
|
16
24
|
userId,
|
|
17
25
|
category,
|
|
18
26
|
refetchInterval,
|
|
27
|
+
staleTime,
|
|
28
|
+
queryKeys,
|
|
19
29
|
});
|
|
20
30
|
const archiveMutation = useArchiveNotification({
|
|
21
31
|
userId,
|
|
22
32
|
category,
|
|
23
33
|
archiveFn,
|
|
34
|
+
queryKeys,
|
|
24
35
|
});
|
|
25
36
|
const archiveAllMutation = useArchiveAllNotifications({
|
|
26
37
|
userId,
|
|
27
38
|
category,
|
|
28
39
|
enqueueArchiveAllFn,
|
|
29
40
|
getArchiveAllStatusFn,
|
|
41
|
+
queryKeys,
|
|
30
42
|
});
|
|
31
43
|
// Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing
|
|
32
44
|
// side effects the app wires to it) fires ONLY when the server job fully drained the category —
|
|
@@ -36,9 +48,15 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
36
48
|
const [clearIncomplete, setClearIncomplete] = useState(false);
|
|
37
49
|
const [pendingArchiveIds, setPendingArchiveIds] = useState(() => new Set());
|
|
38
50
|
const [pendingClearAllIds, setPendingClearAllIds] = useState(() => new Set());
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
const renderedRowsRef = useRef(onRenderedRowsChange);
|
|
52
|
+
renderedRowsRef.current = onRenderedRowsChange;
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (notifications)
|
|
55
|
+
renderedRowsRef.current?.(notifications);
|
|
56
|
+
}, [notifications]);
|
|
57
|
+
// An archive the server confirmed stays visibly pending until the authoritative
|
|
58
|
+
// active-notification query removes the row: the callable's answer alone does not
|
|
59
|
+
// prove the read has caught up.
|
|
42
60
|
useEffect(() => {
|
|
43
61
|
if (!notifications)
|
|
44
62
|
return;
|
|
@@ -50,25 +68,60 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
50
68
|
setPendingArchiveIds(keepActive);
|
|
51
69
|
setPendingClearAllIds(keepActive);
|
|
52
70
|
}, [notifications]);
|
|
53
|
-
|
|
71
|
+
// A row that leaves the list while focus is inside it takes the focused control with it, so focus
|
|
72
|
+
// moves to the control of the row that followed it, or to the list itself (FRONTEND-203). A row's
|
|
73
|
+
// removal fires no blur, so the row still recorded as focused when the rows change is the row that
|
|
74
|
+
// held focus as it left.
|
|
75
|
+
const listRef = useRef(null);
|
|
76
|
+
const rowRefs = useRef(new Map());
|
|
77
|
+
const focusedRowId = useRef(null);
|
|
78
|
+
const renderedIds = useRef([]);
|
|
79
|
+
useLayoutEffect(() => {
|
|
80
|
+
const ids = isLoading ? [] : (notifications ?? []).map((notification) => notification.id);
|
|
81
|
+
const previousIds = renderedIds.current;
|
|
82
|
+
renderedIds.current = ids;
|
|
83
|
+
const leftId = focusedRowId.current;
|
|
84
|
+
if (leftId === null || ids.includes(leftId))
|
|
85
|
+
return;
|
|
86
|
+
focusedRowId.current = null;
|
|
87
|
+
const active = document.activeElement;
|
|
88
|
+
if (active !== null && active !== document.body && active.isConnected)
|
|
89
|
+
return;
|
|
90
|
+
const remaining = new Set(ids);
|
|
91
|
+
const followers = previousIds.slice(previousIds.indexOf(leftId) + 1).filter((id) => remaining.has(id));
|
|
92
|
+
const nextControl = followers
|
|
93
|
+
.map((id) => rowRefs.current.get(id)?.querySelector('.ntf-item-row-action')?.querySelector(FOCUSABLE))
|
|
94
|
+
.find((control) => control != null);
|
|
95
|
+
(nextControl ?? listRef.current)?.focus();
|
|
96
|
+
}, [notifications, isLoading]);
|
|
97
|
+
const archiveOne = useCallback(async (notification) => {
|
|
98
|
+
const clearPending = () => setPendingArchiveIds((current) => {
|
|
99
|
+
const next = new Set(current);
|
|
100
|
+
next.delete(notification.id);
|
|
101
|
+
return next;
|
|
102
|
+
});
|
|
54
103
|
setPendingArchiveIds((current) => {
|
|
55
104
|
const next = new Set(current);
|
|
56
|
-
next.add(
|
|
105
|
+
next.add(notification.id);
|
|
57
106
|
return next;
|
|
58
107
|
});
|
|
59
108
|
try {
|
|
60
|
-
await archiveMutation.mutateAsync(
|
|
109
|
+
const result = await archiveMutation.mutateAsync(notification);
|
|
110
|
+
// Nothing was archived (the card changed after it was rendered): the row stays
|
|
111
|
+
// active, so it is not left looking like it is clearing.
|
|
112
|
+
if (!result.archived)
|
|
113
|
+
clearPending();
|
|
114
|
+
return result;
|
|
61
115
|
}
|
|
62
|
-
catch (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
next.delete(notificationId);
|
|
66
|
-
return next;
|
|
67
|
-
});
|
|
68
|
-
throw error;
|
|
116
|
+
catch (archiveError) {
|
|
117
|
+
clearPending();
|
|
118
|
+
throw archiveError;
|
|
69
119
|
}
|
|
70
120
|
}, [archiveMutation]);
|
|
121
|
+
const hasNotifications = !!notifications && notifications.length > 0;
|
|
71
122
|
const handleClearAll = useCallback(async () => {
|
|
123
|
+
if (!hasNotifications)
|
|
124
|
+
return;
|
|
72
125
|
setClearIncomplete(false);
|
|
73
126
|
setPendingClearAllIds(new Set((notifications ?? []).map((notification) => notification.id)));
|
|
74
127
|
let result;
|
|
@@ -89,22 +142,48 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
89
142
|
return;
|
|
90
143
|
}
|
|
91
144
|
onClearAll?.();
|
|
92
|
-
}, [archiveAllMutation, notifications, onClearAll]);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}, [config]);
|
|
97
|
-
const hasNotifications = !!notifications && notifications.length > 0;
|
|
145
|
+
}, [archiveAllMutation, hasNotifications, notifications, onClearAll]);
|
|
146
|
+
// A later page that came back empty keeps its pager so the user can step back; the
|
|
147
|
+
// empty state is the answer for page 1 only.
|
|
148
|
+
const showEmptyState = !isError && notifications !== undefined && notifications.length === 0 && page === 1;
|
|
98
149
|
const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;
|
|
99
|
-
|
|
100
|
-
|
|
150
|
+
const titleId = useId();
|
|
151
|
+
// Per key, so a label passed as `undefined` keeps its default rather than erasing it.
|
|
152
|
+
const words = {
|
|
153
|
+
clearAll: labels?.clearAll ?? DEFAULT_LABELS.clearAll,
|
|
154
|
+
clearing: labels?.clearing ?? DEFAULT_LABELS.clearing,
|
|
155
|
+
clearIncomplete: labels?.clearIncomplete ?? DEFAULT_LABELS.clearIncomplete,
|
|
156
|
+
loading: labels?.loading ?? DEFAULT_LABELS.loading,
|
|
157
|
+
};
|
|
158
|
+
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 ? words.clearing : words.clearAll }), clearIncomplete && !isClearAllPending && (_jsx("div", { className: "ntf-list-clear-error", role: "status", children: words.clearIncomplete }))] }), _jsx(Separator, {}), _jsx("div", { className: "ntf-list-body", children: isLoading ? (_jsx("div", { className: "ntf-loading", children: _jsx(Spinner, { size: "md", label: words.loading }) })) : (_jsxs(_Fragment, { children: [(notifications ?? []).map((notification) => (_jsxs("div", { ref: (row) => {
|
|
159
|
+
if (row)
|
|
160
|
+
rowRefs.current.set(notification.id, row);
|
|
161
|
+
else
|
|
162
|
+
rowRefs.current.delete(notification.id);
|
|
163
|
+
}, className: "ntf-item", onFocus: () => {
|
|
164
|
+
focusedRowId.current = notification.id;
|
|
165
|
+
}, onBlur: (event) => {
|
|
166
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
167
|
+
focusedRowId.current = null;
|
|
168
|
+
}
|
|
169
|
+
}, children: [_jsx(NotificationTypeIcon, { config: config, type: 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, {
|
|
170
|
+
archive: () => archiveOne(notification),
|
|
101
171
|
isArchivePending: pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),
|
|
102
|
-
}) }))] }, notification.id))),
|
|
172
|
+
}) }))] }, notification.id))), isError && error
|
|
173
|
+
? renderError({
|
|
174
|
+
error,
|
|
175
|
+
retry: () => {
|
|
176
|
+
void refetch();
|
|
177
|
+
},
|
|
178
|
+
retrying: isFetching,
|
|
179
|
+
hasRows: hasNotifications,
|
|
180
|
+
})
|
|
181
|
+
: null, showEmptyState && _jsx(NotificationEmptyState, { text: emptyText }), _jsx(ListPagination, { pagination: {
|
|
103
182
|
currentPage: page,
|
|
104
183
|
canPreviousPage: hasPrevPage,
|
|
105
184
|
canNextPage: hasNextPage,
|
|
106
185
|
goToPreviousPage: prevPage,
|
|
107
186
|
goToNextPage: nextPage,
|
|
108
|
-
}, busy: isFetching, className: "ntf-list-footer
|
|
187
|
+
}, busy: isFetching, className: "ntf-list-footer" })] })) })] }));
|
|
109
188
|
}
|
|
110
189
|
//# sourceMappingURL=NotificationList.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useState } from 'react';\nimport { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useActiveNotifications } from '../hooks/useActiveNotifications.js';\nimport { useArchiveNotification } from '../hooks/useArchiveNotification.js';\nimport { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationDoc, NotificationListProps } from '../../types.js';\n\n/**\n * Scrollable list of active notifications with click-to-archive and clear-all.\n */\nexport function NotificationList({\n config,\n userId,\n category,\n archiveFn,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n title,\n onClearAll,\n refetchInterval,\n emptyText,\n renderRowAction,\n}: NotificationListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useActiveNotifications({\n config,\n userId,\n category,\n refetchInterval,\n });\n\n const archiveMutation = useArchiveNotification({\n userId,\n category,\n archiveFn,\n });\n\n const archiveAllMutation = useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n });\n\n // Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing\n // side effects the app wires to it) fires ONLY when the server job fully drained the category —\n // otherwise the user keeps the \"notifications remain — try again\" affordance. The poller resolves\n // with an explicit terminal result (`complete` | `incomplete` | `failed`) and only rejects on a\n // hard enqueue/poll throw.\n const [clearIncomplete, setClearIncomplete] = useState(false);\n const [pendingArchiveIds, setPendingArchiveIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const [pendingClearAllIds, setPendingClearAllIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n\n // Successful archive requests stay visibly pending until the authoritative\n // active-notification query removes each row. A callable response alone does\n // not prove the listener/cache has reconciled yet.\n useEffect(() => {\n if (!notifications) return;\n const activeIds = new Set(notifications.map((notification) => notification.id));\n const keepActive = (current: ReadonlySet<string>) => {\n const next = new Set([...current].filter((id) => activeIds.has(id)));\n return next.size === current.size ? current : next;\n };\n setPendingArchiveIds(keepActive);\n setPendingClearAllIds(keepActive);\n }, [notifications]);\n\n const archiveOne = useCallback(async (notificationId: string) => {\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.add(notificationId);\n return next;\n });\n try {\n await archiveMutation.mutateAsync(notificationId);\n } catch (error) {\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.delete(notificationId);\n return next;\n });\n throw error;\n }\n }, [archiveMutation]);\n\n const handleClearAll = useCallback(async () => {\n setClearIncomplete(false);\n setPendingClearAllIds(new Set((notifications ?? []).map((notification) => notification.id)));\n let result;\n try {\n result = await archiveAllMutation.mutateAsync();\n } catch {\n // Hard failure (enqueue/poll threw): do not fire onClearAll; surface the retry affordance.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n if (!result.complete) {\n // Job terminated without fully draining the category (incomplete/failed) — keep the\n // notifications and prompt a retry.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n onClearAll?.();\n }, [archiveAllMutation, notifications, onClearAll]);\n\n const getTypeIcon = useCallback(\n (type: string) => {\n const typeConfig = config.types[type];\n return typeConfig?.icon ?? '🔔';\n },\n [config],\n );\n\n const hasNotifications = !!notifications && notifications.length > 0;\n const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\n\n return (\n <div className=\"ntf-list\">\n <div className=\"ntf-list-header\">\n {title != null && <div className=\"ntf-list-title\">{title}</div>}\n <Button\n className=\"ntf-list-clear-all\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClearAll}\n disabled={!hasNotifications}\n pending={isClearAllPending}\n >\n {isClearAllPending ? 'Clearing...' : 'Clear All'}\n </Button>\n {clearIncomplete && !isClearAllPending && (\n <div className=\"ntf-list-clear-error\" role=\"status\">\n Some notifications remain — try again.\n </div>\n )}\n </div>\n <Separator />\n\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label=\"Loading notifications\" />\n </div>\n ) : !notifications || notifications.length === 0 ? (\n <NotificationEmptyState text={emptyText} />\n ) : (\n <>\n {notifications.map((notification: NotificationDoc) => (\n <div key={notification.id} className=\"ntf-item\">\n <div className=\"ntf-item-icon\">\n {getTypeIcon(notification.type)}\n </div>\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.updatedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {\n archive: () => archiveOne(notification.id),\n isArchivePending:\n pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),\n })}\n </div>\n )}\n </div>\n ))}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, 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 { NotificationTypeIcon } from './notification-type-icon.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type {\n NotificationArchiveResult,\n NotificationDoc,\n NotificationListLabels,\n NotificationListProps,\n} from '../../types.js';\n\nconst DEFAULT_LABELS: NotificationListLabels = {\n clearAll: 'Clear All',\n clearing: 'Clearing...',\n clearIncomplete: 'Some notifications remain — try again.',\n loading: 'Loading notifications',\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 queryKeys,\n title,\n onClearAll,\n refetchInterval,\n staleTime,\n emptyText,\n labels,\n renderError,\n onRenderedRowsChange,\n renderRowAction,\n}: NotificationListProps) {\n const {\n data: notifications,\n isLoading,\n isFetching,\n isError,\n error,\n refetch,\n page,\n hasNextPage,\n hasPrevPage,\n nextPage,\n prevPage,\n } = useActiveNotifications({\n config,\n userId,\n category,\n refetchInterval,\n staleTime,\n queryKeys,\n });\n\n const archiveMutation = useArchiveNotification({\n userId,\n category,\n archiveFn,\n queryKeys,\n });\n\n const archiveAllMutation = useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n queryKeys,\n });\n\n // Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing\n // side effects the app wires to it) fires ONLY when the server job fully drained the category —\n // otherwise the user keeps the \"notifications remain — try again\" affordance. The poller resolves\n // with an explicit terminal result (`complete` | `incomplete` | `failed`) and only rejects on a\n // hard enqueue/poll throw.\n const [clearIncomplete, setClearIncomplete] = useState(false);\n const [pendingArchiveIds, setPendingArchiveIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const [pendingClearAllIds, setPendingClearAllIds] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n\n const renderedRowsRef = useRef(onRenderedRowsChange);\n renderedRowsRef.current = onRenderedRowsChange;\n useEffect(() => {\n if (notifications) renderedRowsRef.current?.(notifications);\n }, [notifications]);\n\n // An archive the server confirmed stays visibly pending until the authoritative\n // active-notification query removes the row: the callable's answer alone does not\n // prove the read has caught up.\n useEffect(() => {\n if (!notifications) return;\n const activeIds = new Set(notifications.map((notification) => notification.id));\n const keepActive = (current: ReadonlySet<string>) => {\n const next = new Set([...current].filter((id) => activeIds.has(id)));\n return next.size === current.size ? current : next;\n };\n setPendingArchiveIds(keepActive);\n setPendingClearAllIds(keepActive);\n }, [notifications]);\n\n // 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 = 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, isLoading]);\n\n const archiveOne = useCallback(async (notification: NotificationDoc): Promise<NotificationArchiveResult> => {\n const clearPending = () =>\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.delete(notification.id);\n return next;\n });\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.add(notification.id);\n return next;\n });\n try {\n const result = await archiveMutation.mutateAsync(notification);\n // Nothing was archived (the card changed after it was rendered): the row stays\n // active, so it is not left looking like it is clearing.\n if (!result.archived) clearPending();\n return result;\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 setPendingClearAllIds(new Set((notifications ?? []).map((notification) => notification.id)));\n let result;\n try {\n result = await archiveAllMutation.mutateAsync();\n } catch {\n // Hard failure (enqueue/poll threw): do not fire onClearAll; surface the retry affordance.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n if (!result.complete) {\n // Job terminated without fully draining the category (incomplete/failed) — keep the\n // notifications and prompt a retry.\n setPendingClearAllIds(new Set());\n setClearIncomplete(true);\n return;\n }\n onClearAll?.();\n }, [archiveAllMutation, hasNotifications, notifications, onClearAll]);\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 !isError && notifications !== undefined && notifications.length === 0 && page === 1;\n const isClearAllPending = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\n const titleId = useId();\n // Per key, so a label passed as `undefined` keeps its default rather than erasing it.\n const words: NotificationListLabels = {\n clearAll: labels?.clearAll ?? DEFAULT_LABELS.clearAll,\n clearing: labels?.clearing ?? DEFAULT_LABELS.clearing,\n clearIncomplete: labels?.clearIncomplete ?? DEFAULT_LABELS.clearIncomplete,\n loading: labels?.loading ?? DEFAULT_LABELS.loading,\n };\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 ? words.clearing : words.clearAll}\n </Button>\n {clearIncomplete && !isClearAllPending && (\n <div className=\"ntf-list-clear-error\" role=\"status\">\n {words.clearIncomplete}\n </div>\n )}\n </div>\n <Separator />\n\n <div className=\"ntf-list-body\">\n {isLoading ? (\n <div className=\"ntf-loading\">\n <Spinner size=\"md\" label={words.loading} />\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 <NotificationTypeIcon config={config} type={notification.type} />\n <div className=\"ntf-item-content\">\n <div className=\"ntf-item-title\">{notification.title}</div>\n <div className=\"ntf-item-message\">{notification.message}</div>\n <div className=\"ntf-item-timestamp\">\n {formatRelativeTime(notification.updatedAt)}\n </div>\n </div>\n {notification.count > 1 && (\n <div className=\"ntf-item-count\">\n <Badge variant=\"secondary\">×{notification.count}</Badge>\n </div>\n )}\n {renderRowAction && (\n <div className=\"ntf-item-row-action\">\n {renderRowAction(notification, {\n archive: () => archiveOne(notification),\n isArchivePending:\n pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),\n })}\n </div>\n )}\n </div>\n ))}\n {isError && error\n ? renderError({\n error,\n retry: () => {\n void refetch();\n },\n retrying: isFetching,\n hasRows: hasNotifications,\n })\n : null}\n {showEmptyState && <NotificationEmptyState text={emptyText} />}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
|
|
@@ -4,5 +4,5 @@ import type { NotificationUnreadBadgeProps } from '../../types.js';
|
|
|
4
4
|
* Renders nothing when count is 0.
|
|
5
5
|
* Apps wrap this around whatever trigger element they want.
|
|
6
6
|
*/
|
|
7
|
-
export declare function NotificationUnreadBadge({ config, userId, category, refetchInterval, }: NotificationUnreadBadgeProps): import("react").JSX.Element | null;
|
|
7
|
+
export declare function NotificationUnreadBadge({ config, userId, category, queryKeys, refetchInterval, }: NotificationUnreadBadgeProps): import("react").JSX.Element | null;
|
|
8
8
|
//# sourceMappingURL=NotificationUnreadBadge.d.ts.map
|
|
@@ -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,
|
|
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,SAAS,EACT,eAAe,GAChB,EAAE,4BAA4B,sCAgB9B"}
|
|
@@ -6,12 +6,13 @@ import { useUnreadCount } from '../hooks/useUnreadCount.js';
|
|
|
6
6
|
* Renders nothing when count is 0.
|
|
7
7
|
* Apps wrap this around whatever trigger element they want.
|
|
8
8
|
*/
|
|
9
|
-
export function NotificationUnreadBadge({ config, userId, category, refetchInterval, }) {
|
|
9
|
+
export function NotificationUnreadBadge({ config, userId, category, queryKeys, refetchInterval, }) {
|
|
10
10
|
const { count, hasMore } = useUnreadCount({
|
|
11
11
|
config,
|
|
12
12
|
userId,
|
|
13
13
|
category,
|
|
14
14
|
refetchInterval,
|
|
15
|
+
queryKeys,
|
|
15
16
|
});
|
|
16
17
|
if (count === 0)
|
|
17
18
|
return null;
|
|
@@ -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;
|
|
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,SAAS,EACT,eAAe,GACc;IAC7B,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC;QACxC,MAAM;QACN,MAAM;QACN,QAAQ;QACR,eAAe;QACf,SAAS;KACV,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 queryKeys,\n refetchInterval,\n}: NotificationUnreadBadgeProps) {\n const { count, hasMore } = useUnreadCount({\n config,\n userId,\n category,\n refetchInterval,\n queryKeys,\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"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NotificationSystemConfig } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A row's type icon: the type's own `icon` text, or a bell drawn in `currentColor`, so the
|
|
4
|
+
* row's icon class colors and sizes it like any other glyph. Decorative — the row's title says
|
|
5
|
+
* what it is.
|
|
6
|
+
*/
|
|
7
|
+
export declare function NotificationTypeIcon({ config, type }: {
|
|
8
|
+
config: NotificationSystemConfig;
|
|
9
|
+
type: string;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=notification-type-icon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-type-icon.d.ts","sourceRoot":"","sources":["../../../src/react/components/notification-type-icon.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAE/D;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;IAAE,MAAM,EAAE,wBAAwB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,+BAuBxG"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* A row's type icon: the type's own `icon` text, or a bell drawn in `currentColor`, so the
|
|
5
|
+
* row's icon class colors and sizes it like any other glyph. Decorative — the row's title says
|
|
6
|
+
* what it is.
|
|
7
|
+
*/
|
|
8
|
+
export function NotificationTypeIcon({ config, type }) {
|
|
9
|
+
const icon = config.types[type]?.icon;
|
|
10
|
+
return (_jsx("div", { className: "ntf-item-icon", "aria-hidden": "true", children: icon ?? (_jsxs("svg", { className: "ntf-item-icon-glyph", xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }), _jsx("path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" })] })) }));
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=notification-type-icon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-type-icon.js","sourceRoot":"","sources":["../../../src/react/components/notification-type-icon.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAIb;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,MAAM,EAAE,IAAI,EAAsD;IACvG,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACtC,OAAO,CACL,cAAK,SAAS,EAAC,eAAe,iBAAa,MAAM,YAC9C,IAAI,IAAI,CACP,eACE,SAAS,EAAC,qBAAqB,EAC/B,KAAK,EAAC,4BAA4B,EAClC,KAAK,EAAC,IAAI,EACV,MAAM,EAAC,IAAI,EACX,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,aAEtB,eAAM,CAAC,EAAC,2CAA2C,GAAG,EACtD,eAAM,CAAC,EAAC,gCAAgC,GAAG,IACvC,CACP,GACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport type { NotificationSystemConfig } from '../../types.js';\n\n/**\n * A row's type icon: the type's own `icon` text, or a bell drawn in `currentColor`, so the\n * row's icon class colors and sizes it like any other glyph. Decorative — the row's title says\n * what it is.\n */\nexport function NotificationTypeIcon({ config, type }: { config: NotificationSystemConfig; type: string }) {\n const icon = config.types[type]?.icon;\n return (\n <div className=\"ntf-item-icon\" aria-hidden=\"true\">\n {icon ?? (\n <svg\n className=\"ntf-item-icon-glyph\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\" />\n <path d=\"M10.3 21a1.94 1.94 0 0 0 3.4 0\" />\n </svg>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NotificationDoc, UseActiveNotificationsOptions } from '../../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Paginated
|
|
4
|
-
*
|
|
3
|
+
* Paginated read of active notifications. The displayed page is re-read every
|
|
4
|
+
* `refetchInterval` ms while mounted — polling, not a realtime listener, for cost.
|
|
5
5
|
*/
|
|
6
|
-
export declare function useActiveNotifications({ config, userId, category, enabled, pageSize, refetchInterval, }: UseActiveNotificationsOptions): import("@ttt-productions/query-core/react").UseFirestorePaginatedResult<NotificationDoc>;
|
|
6
|
+
export declare function useActiveNotifications({ config, userId, category, enabled, pageSize, refetchInterval, staleTime, queryKeys, }: UseActiveNotificationsOptions): import("@ttt-productions/query-core/react").UseFirestorePaginatedResult<NotificationDoc>;
|
|
7
7
|
//# sourceMappingURL=useActiveNotifications.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAOrF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,eAA0C,EAC1C,SAA8B,EAC9B,SAAS,GACV,EAAE,6BAA6B,4FAuB/B"}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useFirestorePaginated } from '@ttt-productions/query-core/react';
|
|
3
3
|
import { orderBy, where } from 'firebase/firestore';
|
|
4
|
+
import { resolveNotificationQueryKeys } from '../query-keys.js';
|
|
4
5
|
const DEFAULT_PAGE_SIZE = 20;
|
|
5
6
|
const DEFAULT_REFETCH_INTERVAL = 30_000;
|
|
7
|
+
const DEFAULT_STALE_TIME = 30_000;
|
|
6
8
|
/**
|
|
7
|
-
* Paginated
|
|
8
|
-
*
|
|
9
|
+
* Paginated read of active notifications. The displayed page is re-read every
|
|
10
|
+
* `refetchInterval` ms while mounted — polling, not a realtime listener, for cost.
|
|
9
11
|
*/
|
|
10
|
-
export function useActiveNotifications({ config, userId, category, enabled = true, pageSize = DEFAULT_PAGE_SIZE, refetchInterval = DEFAULT_REFETCH_INTERVAL, }) {
|
|
12
|
+
export function useActiveNotifications({ config, userId, category, enabled = true, pageSize = DEFAULT_PAGE_SIZE, refetchInterval = DEFAULT_REFETCH_INTERVAL, staleTime = DEFAULT_STALE_TIME, queryKeys, }) {
|
|
11
13
|
const categoryConfig = config.categories[category];
|
|
12
14
|
if (!categoryConfig) {
|
|
13
15
|
throw new Error(`[notification-core] Unknown category: ${category}`);
|
|
@@ -20,11 +22,12 @@ export function useActiveNotifications({ config, userId, category, enabled = tru
|
|
|
20
22
|
];
|
|
21
23
|
return useFirestorePaginated({
|
|
22
24
|
collectionPath,
|
|
23
|
-
queryKey: [
|
|
25
|
+
queryKey: [...resolveNotificationQueryKeys(queryKeys).active(category, userId), { pageSize }],
|
|
24
26
|
constraints,
|
|
25
27
|
pageSize,
|
|
26
28
|
enabled: enabled && !!userId,
|
|
27
|
-
|
|
29
|
+
refetchInterval,
|
|
30
|
+
staleTime,
|
|
28
31
|
});
|
|
29
32
|
}
|
|
30
33
|
//# sourceMappingURL=useActiveNotifications.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useActiveNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAwB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"useActiveNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAwB,MAAM,oBAAoB,CAAC;AAE1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAEhE,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,EAC9B,SAAS,GACqB;IAC9B,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACnD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,yCAAyC,QAAQ,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,cAAc,GAAG,cAAc,CAAC,UAAU,CAAC;IACjD,MAAM,UAAU,GAAG,cAAc,CAAC,YAAY,KAAK,UAAU,CAAC;IAE9D,MAAM,WAAW,GAAsB;QACrC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC;KAC7B,CAAC;IAEF,OAAO,qBAAqB,CAAkB;QAC5C,cAAc;QACd,QAAQ,EAAE,CAAC,GAAG,4BAA4B,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC;QAC7F,WAAW;QACX,QAAQ;QACR,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,MAAM;QAC5B,eAAe;QACf,SAAS;KACV,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useFirestorePaginated } from '@ttt-productions/query-core/react';\nimport { orderBy, where, type QueryConstraint } from 'firebase/firestore';\nimport type { NotificationDoc, UseActiveNotificationsOptions } from '../../types.js';\nimport { resolveNotificationQueryKeys } from '../query-keys.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 queryKeys,\n}: UseActiveNotificationsOptions) {\n const categoryConfig = config.categories[category];\n if (!categoryConfig) {\n throw new Error(`[notification-core] Unknown category: ${category}`);\n }\n\n const collectionPath = categoryConfig.activePath;\n const isPersonal = categoryConfig.audienceType === 'personal';\n\n const constraints: QueryConstraint[] = [\n ...(isPersonal ? [where('targetUserId', '==', userId)] : []),\n orderBy('updatedAt', 'desc'),\n ];\n\n return useFirestorePaginated<NotificationDoc>({\n collectionPath,\n queryKey: [...resolveNotificationQueryKeys(queryKeys).active(category, userId), { pageSize }],\n constraints,\n pageSize,\n enabled: enabled && !!userId,\n refetchInterval,\n staleTime,\n });\n}\n"]}
|