@ttt-productions/notification-core 0.15.16 → 0.15.18
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/README.md +9 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/react/components/NotificationEmptyState.d.ts +0 -3
- package/dist/react/components/NotificationEmptyState.d.ts.map +1 -1
- package/dist/react/components/NotificationEmptyState.js +0 -3
- package/dist/react/components/NotificationEmptyState.js.map +1 -1
- package/dist/react/components/NotificationHistoryList.d.ts +1 -7
- package/dist/react/components/NotificationHistoryList.d.ts.map +1 -1
- package/dist/react/components/NotificationHistoryList.js +4 -9
- package/dist/react/components/NotificationHistoryList.js.map +1 -1
- package/dist/react/components/NotificationList.d.ts +1 -4
- package/dist/react/components/NotificationList.d.ts.map +1 -1
- package/dist/react/components/NotificationList.js +26 -38
- package/dist/react/components/NotificationList.js.map +1 -1
- package/dist/react/components/NotificationUnreadBadge.d.ts +0 -5
- package/dist/react/components/NotificationUnreadBadge.d.ts.map +1 -1
- package/dist/react/components/NotificationUnreadBadge.js +1 -6
- package/dist/react/components/NotificationUnreadBadge.js.map +1 -1
- package/dist/react/components/relative-time.d.ts +0 -3
- package/dist/react/components/relative-time.d.ts.map +1 -1
- package/dist/react/components/relative-time.js +0 -3
- package/dist/react/components/relative-time.js.map +1 -1
- package/dist/react/hooks/useActiveNotifications.d.ts +3 -7
- package/dist/react/hooks/useActiveNotifications.d.ts.map +1 -1
- package/dist/react/hooks/useActiveNotifications.js +3 -8
- package/dist/react/hooks/useActiveNotifications.js.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.d.ts +0 -30
- package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.js +15 -45
- package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
- package/dist/react/hooks/useArchiveNotification.d.ts +0 -8
- package/dist/react/hooks/useArchiveNotification.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveNotification.js +0 -8
- package/dist/react/hooks/useArchiveNotification.js.map +1 -1
- package/dist/react/hooks/useNotificationHistory.d.ts +3 -17
- package/dist/react/hooks/useNotificationHistory.d.ts.map +1 -1
- package/dist/react/hooks/useNotificationHistory.js +3 -20
- package/dist/react/hooks/useNotificationHistory.js.map +1 -1
- package/dist/react/hooks/useUnreadCount.d.ts +0 -22
- package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
- package/dist/react/hooks/useUnreadCount.js +2 -25
- package/dist/react/hooks/useUnreadCount.js.map +1 -1
- package/dist/react/index.d.ts +10 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +10 -5
- package/dist/react/index.js.map +1 -1
- package/dist/react/types.d.ts +30 -0
- package/dist/react/types.d.ts.map +1 -0
- package/dist/react/types.js +2 -0
- package/dist/react/types.js.map +1 -0
- package/dist/schemas.d.ts +0 -22
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +0 -25
- package/dist/schemas.js.map +1 -1
- package/dist/server/activeNotificationId.d.ts +1 -22
- package/dist/server/activeNotificationId.d.ts.map +1 -1
- package/dist/server/activeNotificationId.js +0 -16
- package/dist/server/activeNotificationId.js.map +1 -1
- package/dist/server/delivery-ledger.d.ts +1 -73
- package/dist/server/delivery-ledger.d.ts.map +1 -1
- package/dist/server/delivery-ledger.js +13 -63
- package/dist/server/delivery-ledger.js.map +1 -1
- package/dist/server/errors.d.ts +0 -9
- package/dist/server/errors.d.ts.map +1 -1
- package/dist/server/errors.js +1 -11
- package/dist/server/errors.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +0 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/observed-generation.d.ts +1 -41
- package/dist/server/observed-generation.d.ts.map +1 -1
- package/dist/server/observed-generation.js +1 -31
- package/dist/server/observed-generation.js.map +1 -1
- package/dist/server/processBatchHelper.d.ts +0 -19
- package/dist/server/processBatchHelper.d.ts.map +1 -1
- package/dist/server/processBatchHelper.js +5 -51
- package/dist/server/processBatchHelper.js.map +1 -1
- package/dist/server/types.d.ts +0 -9
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +0 -4
- package/dist/server/types.js.map +1 -1
- package/dist/types.d.ts +2 -266
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +1 -1
- package/package.json +5 -5
- package/src/styles/notifications.css +0 -14
- package/dist/react/components/index.d.ts +0 -5
- package/dist/react/components/index.d.ts.map +0 -1
- package/dist/react/components/index.js +0 -5
- package/dist/react/components/index.js.map +0 -1
- package/dist/react/hooks/index.d.ts +0 -6
- package/dist/react/hooks/index.d.ts.map +0 -1
- package/dist/react/hooks/index.js +0 -6
- package/dist/react/hooks/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ttt-productions/notification-core
|
|
2
2
|
|
|
3
|
-
Generic notification system for
|
|
3
|
+
Generic notification system for consuming apps — a two-tier
|
|
4
4
|
**active → history** model with dedup, batch processing, and themed React UI.
|
|
5
5
|
|
|
6
6
|
## Entrypoints
|
|
@@ -26,14 +26,13 @@ Duplicate triggers for the same `dedupKey` increment a single active doc's
|
|
|
26
26
|
`count`, append the actor, and reset `seenAt` to `0` so new activity re-lights
|
|
27
27
|
the badge.
|
|
28
28
|
|
|
29
|
-
**
|
|
30
|
-
docs.** The archive React hooks (`useArchiveNotification` /
|
|
29
|
+
**The React hooks write no notification docs.** The archive React hooks (`useArchiveNotification` /
|
|
31
30
|
`useArchiveAllNotifications`) perform **no Firestore writes**; they take an
|
|
32
|
-
app-supplied `archiveFn`
|
|
31
|
+
app-supplied adapter (`archiveFn`, or `enqueueArchiveAllFn` + `getArchiveAllStatusFn`) wired to the app's callables
|
|
33
32
|
(e.g. `httpsCallable(functions, 'archiveNotification')`) and invalidate the
|
|
34
|
-
read keys on success. On the server, the app's callable
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
read keys on success. On the server, the app's callable decides who may archive
|
|
34
|
+
a card and then runs the active → history move through
|
|
35
|
+
`archiveNotificationWithGeneration`,
|
|
37
36
|
which keys the history doc on a retry-stable, app-built deterministic id and
|
|
38
37
|
only archives when the card's observed `activityGeneration` still matches.
|
|
39
38
|
|
|
@@ -42,7 +41,7 @@ only archives when the card's observed `activityGeneration` still matches.
|
|
|
42
41
|
Shared notification docs persist actor **ids only** — `NotificationDoc`
|
|
43
42
|
exposes `latestActorIds`, and `PendingNotification` exposes `actorId`. There are
|
|
44
43
|
**no persisted display names** (no `latestActorNames` / `actorName`); names are
|
|
45
|
-
resolved at read time by the consuming app
|
|
44
|
+
resolved at read time by the consuming app from its own public profile source, so they
|
|
46
45
|
never drift in the stored doc.
|
|
47
46
|
|
|
48
47
|
## Usage
|
|
@@ -51,7 +50,7 @@ never drift in the stored doc.
|
|
|
51
50
|
import { useActiveNotifications } from '@ttt-productions/notification-core/react';
|
|
52
51
|
|
|
53
52
|
const { data: notifications, isLoading } = useActiveNotifications({
|
|
54
|
-
config:
|
|
53
|
+
config: NOTIFICATION_CONFIG,
|
|
55
54
|
userId: currentUser.uid,
|
|
56
55
|
category: 'user',
|
|
57
56
|
});
|
|
@@ -67,7 +66,7 @@ through the observed-generation helpers (`markNotificationSeenWithGeneration` /
|
|
|
67
66
|
```ts
|
|
68
67
|
import { createDeliveryLedger } from '@ttt-productions/notification-core/server';
|
|
69
68
|
|
|
70
|
-
const ledger = createDeliveryLedger(db,
|
|
69
|
+
const ledger = createDeliveryLedger(db, NOTIFICATION_CONFIG, options);
|
|
71
70
|
await ledger.enqueue([
|
|
72
71
|
{ deliveryId, notificationType, eventId, recipientUid, aggregationKey, strategy, payload, payloadVersion, materializationClass },
|
|
73
72
|
]);
|
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,
|
|
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
|
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,yBAAyB,EACzB,qBAAqB,EACrB,0BAA0B,EAC1B,
|
|
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;AAEpB,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
// Runtime schemas for the generic notification doc shapes (notification-core owns
|
|
2
|
-
// these; consuming apps compose them instead of hand-mirroring). Server- and
|
|
3
|
-
// UI-safe (zod only, no React/firebase).
|
|
4
1
|
export { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';
|
|
5
2
|
//# sourceMappingURL=index.js.map
|
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":"AAwBA,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\nexport { NotificationDocSchema, PendingNotificationSchema } from './schemas.js';\n"]}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
1
|
import type { NotificationEmptyStateProps } from '../../types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Empty state shown when there are no active notifications.
|
|
4
|
-
*/
|
|
5
2
|
export declare function NotificationEmptyState({ text, }: NotificationEmptyStateProps): import("react").JSX.Element;
|
|
6
3
|
//# sourceMappingURL=NotificationEmptyState.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationEmptyState.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationEmptyState.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAElE
|
|
1
|
+
{"version":3,"file":"NotificationEmptyState.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationEmptyState.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAElE,wBAAgB,sBAAsB,CAAC,EACrC,IAA8B,GAC/B,EAAE,2BAA2B,+BAqB7B"}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* Empty state shown when there are no active notifications.
|
|
5
|
-
*/
|
|
6
3
|
export function NotificationEmptyState({ text = "You're all caught up!", }) {
|
|
7
4
|
return (_jsxs("div", { className: "ntf-empty", children: [_jsxs("svg", { className: "ntf-empty-icon", xmlns: "http://www.w3.org/2000/svg", width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }), _jsx("polyline", { points: "22 4 12 14.01 9 11.01" })] }), _jsx("p", { className: "ntf-empty-text", children: text })] }));
|
|
8
5
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationEmptyState.js","sourceRoot":"","sources":["../../../src/react/components/NotificationEmptyState.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAIb
|
|
1
|
+
{"version":3,"file":"NotificationEmptyState.js","sourceRoot":"","sources":["../../../src/react/components/NotificationEmptyState.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAIb,MAAM,UAAU,sBAAsB,CAAC,EACrC,IAAI,GAAG,uBAAuB,GACF;IAC5B,OAAO,CACL,eAAK,SAAS,EAAC,WAAW,aACxB,eACE,SAAS,EAAC,gBAAgB,EAC1B,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,oCAAoC,GAAG,EAC/C,mBAAU,MAAM,EAAC,uBAAuB,GAAG,IACvC,EACN,YAAG,SAAS,EAAC,gBAAgB,YAAE,IAAI,GAAK,IACpC,CACP,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport type { NotificationEmptyStateProps } from '../../types.js';\n\nexport function NotificationEmptyState({\n text = \"You're all caught up!\",\n}: NotificationEmptyStateProps) {\n return (\n <div className=\"ntf-empty\">\n <svg\n className=\"ntf-empty-icon\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"32\"\n height=\"32\"\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=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\" />\n <polyline points=\"22 4 12 14.01 9 11.01\" />\n </svg>\n <p className=\"ntf-empty-text\">{text}</p>\n </div>\n );\n}\n"]}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import type { NotificationHistoryListProps } from '
|
|
2
|
-
/**
|
|
3
|
-
* Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are
|
|
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
|
-
* ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
|
|
7
|
-
*/
|
|
1
|
+
import type { NotificationHistoryListProps } from '../types.js';
|
|
8
2
|
export declare function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, renderError, title, renderRowAction, }: NotificationHistoryListProps): import("react").JSX.Element;
|
|
9
3
|
//# sourceMappingURL=NotificationHistoryList.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationHistoryList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationHistoryList.tsx"],"names":[],"mappings":"
|
|
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,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,WAAW,EACX,KAAK,EACL,eAAe,GAChB,EAAE,4BAA4B,+BA8F9B"}
|
|
@@ -5,12 +5,6 @@ import { Badge, ListPagination, Separator, Spinner } from '@ttt-productions/ui-c
|
|
|
5
5
|
import { useNotificationHistory } from '../hooks/useNotificationHistory.js';
|
|
6
6
|
import { NotificationEmptyState } from './NotificationEmptyState.js';
|
|
7
7
|
import { formatRelativeTime } from './relative-time.js';
|
|
8
|
-
/**
|
|
9
|
-
* Read-only, paginated list of ARCHIVED notifications (the history tier). Rows are
|
|
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
|
-
* ArrowRight "go to") is rendered by the consumer via `renderRowAction`.
|
|
13
|
-
*/
|
|
14
8
|
export function NotificationHistoryList({ config, userId, category, pageSize, staleTime, emptyText, renderError, title, renderRowAction, }) {
|
|
15
9
|
const { data: notifications, isFetching, retryState, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useNotificationHistory({
|
|
16
10
|
config,
|
|
@@ -20,9 +14,10 @@ export function NotificationHistoryList({ config, userId, category, pageSize, st
|
|
|
20
14
|
staleTime,
|
|
21
15
|
});
|
|
22
16
|
const hasRows = !!notifications && notifications.length > 0;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
const showEmptyState = retryState.error === null &&
|
|
18
|
+
notifications !== undefined &&
|
|
19
|
+
notifications.length === 0 &&
|
|
20
|
+
page === 1;
|
|
26
21
|
const getTypeIcon = useCallback((type) => config.types[type]?.icon ?? '🔔', [config]);
|
|
27
22
|
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
23
|
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":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAC3F,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;
|
|
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\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 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,6 +1,3 @@
|
|
|
1
|
-
import type { NotificationListProps } from '
|
|
2
|
-
/**
|
|
3
|
-
* Scrollable list of active notifications with click-to-archive and clear-all.
|
|
4
|
-
*/
|
|
1
|
+
import type { NotificationListProps } from '../types.js';
|
|
5
2
|
export declare function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, staleTime, emptyText, renderError, onRenderedRowsChange, renderRowAction, }: NotificationListProps): import("react").JSX.Element;
|
|
6
3
|
//# 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,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAezD,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,+BAkRvB"}
|
|
@@ -8,17 +8,10 @@ import { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.
|
|
|
8
8
|
import { NotificationEmptyState } from './NotificationEmptyState.js';
|
|
9
9
|
import { formatRelativeTime } from './relative-time.js';
|
|
10
10
|
const NO_PENDING = new Map();
|
|
11
|
-
/**
|
|
12
|
-
* An archive belongs to the card generation it was sent for. A card relit on the same id gets a new
|
|
13
|
-
* generation, which the archive never covered, so it is not pending for that card.
|
|
14
|
-
*/
|
|
15
11
|
function isPendingFor(pending, notification) {
|
|
16
|
-
return pending.has(notification.id) && pending.get(notification.id) === notification.activityGeneration;
|
|
12
|
+
return (pending.has(notification.id) && pending.get(notification.id) === notification.activityGeneration);
|
|
17
13
|
}
|
|
18
14
|
const FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
19
|
-
/**
|
|
20
|
-
* Scrollable list of active notifications with click-to-archive and clear-all.
|
|
21
|
-
*/
|
|
22
15
|
export function NotificationList({ config, userId, category, archiveFn, enqueueArchiveAllFn, getArchiveAllStatusFn, title, onClearAll, refetchInterval, staleTime, emptyText, renderError, onRenderedRowsChange, renderRowAction, }) {
|
|
23
16
|
const { data: notifications, isFetching, retryState, refetch, page, hasNextPage, hasPrevPage, nextPage, prevPage, } = useActiveNotifications({
|
|
24
17
|
config,
|
|
@@ -38,11 +31,6 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
38
31
|
enqueueArchiveAllFn,
|
|
39
32
|
getArchiveAllStatusFn,
|
|
40
33
|
});
|
|
41
|
-
// Surface a failed/incomplete clear instead of swallowing it. `onClearAll` (and the tray-closing
|
|
42
|
-
// side effects the app wires to it) fires ONLY when the server job fully drained the category —
|
|
43
|
-
// otherwise the user keeps the "notifications remain — try again" affordance. The poller resolves
|
|
44
|
-
// with an explicit terminal result (`complete` | `incomplete` | `failed`) and only rejects on a
|
|
45
|
-
// hard enqueue/poll throw.
|
|
46
34
|
const [clearIncomplete, setClearIncomplete] = useState(false);
|
|
47
35
|
const [pendingArchives, setPendingArchives] = useState(NO_PENDING);
|
|
48
36
|
const [pendingClearAll, setPendingClearAll] = useState(NO_PENDING);
|
|
@@ -52,10 +40,6 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
52
40
|
if (notifications)
|
|
53
41
|
renderedRowsRef.current?.(notifications);
|
|
54
42
|
}, [notifications]);
|
|
55
|
-
// An archive the server confirmed stays visibly pending until the authoritative
|
|
56
|
-
// active-notification query removes the row: the callable's answer alone does not
|
|
57
|
-
// prove the read has caught up. A row the read lists at a newer generation was relit
|
|
58
|
-
// after the archive, so the archive no longer holds it.
|
|
59
43
|
useEffect(() => {
|
|
60
44
|
if (!notifications)
|
|
61
45
|
return;
|
|
@@ -67,16 +51,16 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
67
51
|
setPendingArchives(keepActive);
|
|
68
52
|
setPendingClearAll(keepActive);
|
|
69
53
|
}, [notifications]);
|
|
70
|
-
// A row
|
|
71
|
-
//
|
|
72
|
-
// removal fires no blur, so the row still recorded as focused when the rows change is the row that
|
|
73
|
-
// held focus as it left.
|
|
54
|
+
// A removed row takes the focused control with it and fires no blur, so focus moves to the next
|
|
55
|
+
// row's control or to the list (RULE-STD-FRONTEND-203).
|
|
74
56
|
const listRef = useRef(null);
|
|
75
57
|
const rowRefs = useRef(new Map());
|
|
76
58
|
const focusedRowId = useRef(null);
|
|
77
59
|
const renderedIds = useRef([]);
|
|
78
60
|
useLayoutEffect(() => {
|
|
79
|
-
const ids = retryState.isLoading
|
|
61
|
+
const ids = retryState.isLoading
|
|
62
|
+
? []
|
|
63
|
+
: (notifications ?? []).map((notification) => notification.id);
|
|
80
64
|
const previousIds = renderedIds.current;
|
|
81
65
|
renderedIds.current = ids;
|
|
82
66
|
const leftId = focusedRowId.current;
|
|
@@ -87,9 +71,14 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
87
71
|
if (active !== null && active !== document.body && active.isConnected)
|
|
88
72
|
return;
|
|
89
73
|
const remaining = new Set(ids);
|
|
90
|
-
const followers = previousIds
|
|
74
|
+
const followers = previousIds
|
|
75
|
+
.slice(previousIds.indexOf(leftId) + 1)
|
|
76
|
+
.filter((id) => remaining.has(id));
|
|
91
77
|
const nextControl = followers
|
|
92
|
-
.map((id) => rowRefs.current
|
|
78
|
+
.map((id) => rowRefs.current
|
|
79
|
+
.get(id)
|
|
80
|
+
?.querySelector('.ntf-item-row-action')
|
|
81
|
+
?.querySelector(FOCUSABLE))
|
|
93
82
|
.find((control) => control != null);
|
|
94
83
|
(nextControl ?? listRef.current)?.focus();
|
|
95
84
|
}, [notifications, retryState.isLoading]);
|
|
@@ -105,8 +94,6 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
105
94
|
setPendingArchives((current) => new Map(current).set(notification.id, generation));
|
|
106
95
|
try {
|
|
107
96
|
const result = await archiveMutation.mutateAsync(notification);
|
|
108
|
-
// Nothing was archived (the card changed after it was rendered): the row stays
|
|
109
|
-
// active, so it is not left looking like it is clearing.
|
|
110
97
|
if (!result.archived)
|
|
111
98
|
clearPending();
|
|
112
99
|
}
|
|
@@ -120,26 +107,24 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
120
107
|
if (!hasNotifications)
|
|
121
108
|
return;
|
|
122
109
|
setClearIncomplete(false);
|
|
123
|
-
setPendingClearAll(new Map((notifications ?? []).map((notification) => [
|
|
110
|
+
setPendingClearAll(new Map((notifications ?? []).map((notification) => [
|
|
111
|
+
notification.id,
|
|
112
|
+
notification.activityGeneration,
|
|
113
|
+
])));
|
|
124
114
|
let result;
|
|
125
115
|
try {
|
|
126
116
|
result = await archiveAllMutation.mutateAsync();
|
|
127
117
|
}
|
|
128
118
|
catch {
|
|
129
|
-
// Hard failure (enqueue/poll threw): do not fire onClearAll; surface the retry affordance.
|
|
130
119
|
setPendingClearAll(NO_PENDING);
|
|
131
120
|
setClearIncomplete(true);
|
|
132
121
|
return;
|
|
133
122
|
}
|
|
134
123
|
if (!result.complete) {
|
|
135
|
-
// Job terminated without fully draining the category (incomplete/failed) — keep the
|
|
136
|
-
// notifications and prompt a retry.
|
|
137
124
|
setPendingClearAll(NO_PENDING);
|
|
138
125
|
setClearIncomplete(true);
|
|
139
126
|
return;
|
|
140
127
|
}
|
|
141
|
-
// The cards a complete drain left behind stay in the list and never leave the read, so their
|
|
142
|
-
// pending is released here rather than waiting on a read that will keep listing them.
|
|
143
128
|
if (result.notArchived > 0)
|
|
144
129
|
setPendingClearAll(NO_PENDING);
|
|
145
130
|
onClearAll?.(result);
|
|
@@ -148,12 +133,14 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
148
133
|
const typeConfig = config.types[type];
|
|
149
134
|
return typeConfig?.icon ?? '🔔';
|
|
150
135
|
}, [config]);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
136
|
+
const showEmptyState = retryState.error === null &&
|
|
137
|
+
notifications !== undefined &&
|
|
138
|
+
notifications.length === 0 &&
|
|
139
|
+
page === 1;
|
|
140
|
+
const isClearAllPending = archiveAllMutation.isPending ||
|
|
141
|
+
(notifications ?? []).some((notification) => isPendingFor(pendingClearAll, notification));
|
|
155
142
|
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) => {
|
|
143
|
+
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
144
|
if (row)
|
|
158
145
|
rowRefs.current.set(notification.id, row);
|
|
159
146
|
else
|
|
@@ -166,7 +153,8 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
166
153
|
}
|
|
167
154
|
}, 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
155
|
archive: () => archiveOne(notification),
|
|
169
|
-
isArchivePending: isPendingFor(pendingClearAll, notification) ||
|
|
156
|
+
isArchivePending: isPendingFor(pendingClearAll, notification) ||
|
|
157
|
+
isPendingFor(pendingArchives, notification),
|
|
170
158
|
}) }))] }, notification.id))), retryState.error !== null
|
|
171
159
|
? renderError({
|
|
172
160
|
error: retryState.error,
|
|
@@ -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, 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":"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,SAAS,YAAY,CAAC,OAAwB,EAAE,YAA6B;IAC3E,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,YAAY,CAAC,kBAAkB,CACjG,CAAC;AACJ,CAAC;AAED,MAAM,SAAS,GACb,2IAA2I,CAAC;AAE9I,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,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,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,CACnB,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,UAAU,CACxE,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,gGAAgG;IAChG,wDAAwD;IACxD,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;YAC9B,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QACjE,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;aAC1B,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;aACtC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QACrC,MAAM,WAAW,GAAG,SAAS;aAC1B,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CACV,OAAO,CAAC,OAAO;aACZ,GAAG,CAAC,EAAE,CAAC;YACR,EAAE,aAAa,CAAC,sBAAsB,CAAC;YACvC,EAAE,aAAa,CAAc,SAAS,CAAC,CAC1C;aACA,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,CAC5B,KAAK,EAAE,YAA6B,EAAE,EAAE;QACtC,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;gBAC9E,OAAO,OAAO,CAAC;YACjB,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,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,EACD,CAAC,eAAe,CAAC,CAClB,CAAC;IAEF,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,CACL,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC;YAC1C,YAAY,CAAC,EAAE;YACf,YAAY,CAAC,kBAAkB;SAChC,CAAC,CACH,CACF,CAAC;QACF,IAAI,MAAM,CAAC;QACX,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,kBAAkB,CAAC,WAAW,EAAE,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,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,kBAAkB,CAAC,UAAU,CAAC,CAAC;YAC/B,kBAAkB,CAAC,IAAI,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QACD,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,MAAM,cAAc,GAClB,UAAU,CAAC,KAAK,KAAK,IAAI;QACzB,aAAa,KAAK,SAAS;QAC3B,aAAa,CAAC,MAAM,KAAK,CAAC;QAC1B,IAAI,KAAK,CAAC,CAAC;IACb,MAAM,iBAAiB,GACrB,kBAAkB,CAAC,SAAS;QAC5B,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC;IAC5F,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,CAChB,cAAK,EAAE,EAAE,OAAO,EAAE,SAAS,EAAC,gBAAgB,YACzC,KAAK,GACF,CACP,EAED,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,YAC3B,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,YAAE,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,GAAO,EACrE,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;4CAC3C,YAAY,CAAC,eAAe,EAAE,YAAY,CAAC;qCAC9C,CAAC,GACE,CACP,KArCI,YAAY,CAAC,EAAE,CAsChB,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,GACG,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 } from '../../types.js';\nimport type { NotificationListProps } from '../types.js';\n\ntype PendingArchives = ReadonlyMap<string, string | undefined>;\n\nconst NO_PENDING: PendingArchives = new Map();\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\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 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 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 removed row takes the focused control with it and fires no blur, so focus moves to the next\n // row's control or to the list (RULE-STD-FRONTEND-203).\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 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 setPendingClearAll(NO_PENDING);\n setClearIncomplete(true);\n return;\n }\n if (!result.complete) {\n setPendingClearAll(NO_PENDING);\n setClearIncomplete(true);\n return;\n }\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 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 {/* Never natively disabled: focus must keep a destination as the list empties (RULE-STD-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,8 +1,3 @@
|
|
|
1
1
|
import type { NotificationUnreadBadgeProps } from '../../types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Unread count badge (red circle with number).
|
|
4
|
-
* Renders nothing when count is 0.
|
|
5
|
-
* Apps wrap this around whatever trigger element they want.
|
|
6
|
-
*/
|
|
7
2
|
export declare function NotificationUnreadBadge({ config, userId, category, refetchInterval, }: NotificationUnreadBadgeProps): import("react").JSX.Element | null;
|
|
8
3
|
//# 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
|
|
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,wBAAgB,uBAAuB,CAAC,EACtC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,eAAe,GAChB,EAAE,4BAA4B,sCAa9B"}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useUnreadCount } from '../hooks/useUnreadCount.js';
|
|
4
|
-
/**
|
|
5
|
-
* Unread count badge (red circle with number).
|
|
6
|
-
* Renders nothing when count is 0.
|
|
7
|
-
* Apps wrap this around whatever trigger element they want.
|
|
8
|
-
*/
|
|
9
4
|
export function NotificationUnreadBadge({ config, userId, category, refetchInterval, }) {
|
|
10
5
|
const { count, hasMore } = useUnreadCount({
|
|
11
6
|
config,
|
|
@@ -16,6 +11,6 @@ export function NotificationUnreadBadge({ config, userId, category, refetchInter
|
|
|
16
11
|
if (count === 0)
|
|
17
12
|
return null;
|
|
18
13
|
const displayCount = hasMore ? '99+' : count > 9 ? '9+' : String(count);
|
|
19
|
-
return
|
|
14
|
+
return _jsx("span", { className: "ntf-badge", children: displayCount });
|
|
20
15
|
}
|
|
21
16
|
//# 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
|
|
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,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\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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relative-time.d.ts","sourceRoot":"","sources":["../../../src/react/components/relative-time.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"relative-time.d.ts","sourceRoot":"","sources":["../../../src/react/components/relative-time.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAc1D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relative-time.js","sourceRoot":"","sources":["../../../src/react/components/relative-time.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"relative-time.js","sourceRoot":"","sources":["../../../src/react/components/relative-time.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;IAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IAExC,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,UAAU,CAAC;IACpC,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,GAAG,OAAO,OAAO,CAAC;IAC3C,IAAI,MAAM,GAAG,EAAE;QAAE,OAAO,GAAG,MAAM,OAAO,CAAC;IACzC,IAAI,OAAO,GAAG,CAAC;QAAE,OAAO,GAAG,OAAO,OAAO,CAAC;IAC1C,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;IAC3D,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC;AAC7C,CAAC","sourcesContent":["export function formatRelativeTime(epochMs: number): string {\n const now = Date.now();\n const diffMs = now - epochMs;\n const diffSec = Math.floor(diffMs / 1000);\n const diffMin = Math.floor(diffSec / 60);\n const diffHr = Math.floor(diffMin / 60);\n const diffDay = Math.floor(diffHr / 24);\n\n if (diffSec < 60) return 'just now';\n if (diffMin < 60) return `${diffMin}m ago`;\n if (diffHr < 24) return `${diffHr}h ago`;\n if (diffDay < 7) return `${diffDay}d ago`;\n if (diffDay < 30) return `${Math.floor(diffDay / 7)}w ago`;\n return `${Math.floor(diffDay / 30)}mo ago`;\n}\n"]}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { NotificationDoc, UseActiveNotificationsOptions } from '../../types.js';
|
|
2
|
-
/**
|
|
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
|
-
*/
|
|
6
2
|
export declare function useActiveNotifications({ config, userId, category, enabled, pageSize, refetchInterval, staleTime, }: UseActiveNotificationsOptions): {
|
|
7
3
|
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>>>;
|
|
4
|
+
promise: Promise<import("@ttt-productions/query-core/react").PaginatedPage<import("@ttt-productions/query-core/types").WithId<NotificationDoc>>>;
|
|
9
5
|
error: Error | null;
|
|
10
6
|
isError: boolean;
|
|
11
7
|
isPending: boolean;
|
|
@@ -28,9 +24,9 @@ export declare function useActiveNotifications({ config, userId, category, enabl
|
|
|
28
24
|
isRefetching: boolean;
|
|
29
25
|
isStale: boolean;
|
|
30
26
|
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>>;
|
|
27
|
+
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
28
|
fetchStatus: import("@tanstack/query-core").FetchStatus;
|
|
33
|
-
data: import("@ttt-productions/query-core").WithId<NotificationDoc>[] | undefined;
|
|
29
|
+
data: import("@ttt-productions/query-core/types").WithId<NotificationDoc>[] | undefined;
|
|
34
30
|
page: number;
|
|
35
31
|
pageSize: number;
|
|
36
32
|
hasNextPage: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useActiveNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useActiveNotifications.ts"],"names":[],"mappings":"
|
|
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,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,EACR,OAAc,EACd,QAA4B,EAC5B,eAA0C,EAC1C,SAA8B,GAC/B,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2B/B"}
|