@ttt-productions/notification-core 0.15.15 → 0.15.16
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/react/components/NotificationList.d.ts.map +1 -1
- package/dist/react/components/NotificationList.js +32 -20
- package/dist/react/components/NotificationList.js.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.js +17 -4
- package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
- package/dist/types.d.ts +14 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAkB7E;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,EACL,UAAU,EACV,eAAe,EACf,SAAS,EACT,SAAS,EACT,WAAW,EACX,oBAAoB,EACpB,eAAe,GAChB,EAAE,qBAAqB,+BA6QvB"}
|
|
@@ -7,6 +7,14 @@ import { useArchiveNotification } from '../hooks/useArchiveNotification.js';
|
|
|
7
7
|
import { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';
|
|
8
8
|
import { NotificationEmptyState } from './NotificationEmptyState.js';
|
|
9
9
|
import { formatRelativeTime } from './relative-time.js';
|
|
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
|
+
function isPendingFor(pending, notification) {
|
|
16
|
+
return pending.has(notification.id) && pending.get(notification.id) === notification.activityGeneration;
|
|
17
|
+
}
|
|
10
18
|
const FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
11
19
|
/**
|
|
12
20
|
* Scrollable list of active notifications with click-to-archive and clear-all.
|
|
@@ -36,8 +44,8 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
36
44
|
// with an explicit terminal result (`complete` | `incomplete` | `failed`) and only rejects on a
|
|
37
45
|
// hard enqueue/poll throw.
|
|
38
46
|
const [clearIncomplete, setClearIncomplete] = useState(false);
|
|
39
|
-
const [
|
|
40
|
-
const [
|
|
47
|
+
const [pendingArchives, setPendingArchives] = useState(NO_PENDING);
|
|
48
|
+
const [pendingClearAll, setPendingClearAll] = useState(NO_PENDING);
|
|
41
49
|
const renderedRowsRef = useRef(onRenderedRowsChange);
|
|
42
50
|
renderedRowsRef.current = onRenderedRowsChange;
|
|
43
51
|
useEffect(() => {
|
|
@@ -46,17 +54,18 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
46
54
|
}, [notifications]);
|
|
47
55
|
// An archive the server confirmed stays visibly pending until the authoritative
|
|
48
56
|
// active-notification query removes the row: the callable's answer alone does not
|
|
49
|
-
// prove the read has caught up.
|
|
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.
|
|
50
59
|
useEffect(() => {
|
|
51
60
|
if (!notifications)
|
|
52
61
|
return;
|
|
53
|
-
const
|
|
62
|
+
const activeGenerations = new Map(notifications.map((notification) => [notification.id, notification.activityGeneration]));
|
|
54
63
|
const keepActive = (current) => {
|
|
55
|
-
const next = new
|
|
64
|
+
const next = new Map([...current].filter(([id, generation]) => activeGenerations.has(id) && activeGenerations.get(id) === generation));
|
|
56
65
|
return next.size === current.size ? current : next;
|
|
57
66
|
};
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
setPendingArchives(keepActive);
|
|
68
|
+
setPendingClearAll(keepActive);
|
|
60
69
|
}, [notifications]);
|
|
61
70
|
// A row that leaves the list while focus is inside it takes the focused control with it, so focus
|
|
62
71
|
// moves to the control of the row that followed it, or to the list itself (FRONTEND-203). A row's
|
|
@@ -85,16 +94,15 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
85
94
|
(nextControl ?? listRef.current)?.focus();
|
|
86
95
|
}, [notifications, retryState.isLoading]);
|
|
87
96
|
const archiveOne = useCallback(async (notification) => {
|
|
88
|
-
const
|
|
89
|
-
|
|
97
|
+
const generation = notification.activityGeneration;
|
|
98
|
+
const clearPending = () => setPendingArchives((current) => {
|
|
99
|
+
if (!current.has(notification.id) || current.get(notification.id) !== generation)
|
|
100
|
+
return current;
|
|
101
|
+
const next = new Map(current);
|
|
90
102
|
next.delete(notification.id);
|
|
91
103
|
return next;
|
|
92
104
|
});
|
|
93
|
-
|
|
94
|
-
const next = new Set(current);
|
|
95
|
-
next.add(notification.id);
|
|
96
|
-
return next;
|
|
97
|
-
});
|
|
105
|
+
setPendingArchives((current) => new Map(current).set(notification.id, generation));
|
|
98
106
|
try {
|
|
99
107
|
const result = await archiveMutation.mutateAsync(notification);
|
|
100
108
|
// Nothing was archived (the card changed after it was rendered): the row stays
|
|
@@ -112,25 +120,29 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
112
120
|
if (!hasNotifications)
|
|
113
121
|
return;
|
|
114
122
|
setClearIncomplete(false);
|
|
115
|
-
|
|
123
|
+
setPendingClearAll(new Map((notifications ?? []).map((notification) => [notification.id, notification.activityGeneration])));
|
|
116
124
|
let result;
|
|
117
125
|
try {
|
|
118
126
|
result = await archiveAllMutation.mutateAsync();
|
|
119
127
|
}
|
|
120
128
|
catch {
|
|
121
129
|
// Hard failure (enqueue/poll threw): do not fire onClearAll; surface the retry affordance.
|
|
122
|
-
|
|
130
|
+
setPendingClearAll(NO_PENDING);
|
|
123
131
|
setClearIncomplete(true);
|
|
124
132
|
return;
|
|
125
133
|
}
|
|
126
134
|
if (!result.complete) {
|
|
127
135
|
// Job terminated without fully draining the category (incomplete/failed) — keep the
|
|
128
136
|
// notifications and prompt a retry.
|
|
129
|
-
|
|
137
|
+
setPendingClearAll(NO_PENDING);
|
|
130
138
|
setClearIncomplete(true);
|
|
131
139
|
return;
|
|
132
140
|
}
|
|
133
|
-
|
|
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
|
+
if (result.notArchived > 0)
|
|
144
|
+
setPendingClearAll(NO_PENDING);
|
|
145
|
+
onClearAll?.(result);
|
|
134
146
|
}, [archiveAllMutation, hasNotifications, notifications, onClearAll]);
|
|
135
147
|
const getTypeIcon = useCallback((type) => {
|
|
136
148
|
const typeConfig = config.types[type];
|
|
@@ -139,7 +151,7 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
139
151
|
// A later page that came back empty keeps its pager so the user can step back; the
|
|
140
152
|
// empty state is the answer for page 1 only.
|
|
141
153
|
const showEmptyState = retryState.error === null && notifications !== undefined && notifications.length === 0 && page === 1;
|
|
142
|
-
const isClearAllPending = archiveAllMutation.isPending ||
|
|
154
|
+
const isClearAllPending = archiveAllMutation.isPending || (notifications ?? []).some((notification) => isPendingFor(pendingClearAll, notification));
|
|
143
155
|
const titleId = useId();
|
|
144
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) => {
|
|
145
157
|
if (row)
|
|
@@ -154,7 +166,7 @@ export function NotificationList({ config, userId, category, archiveFn, enqueueA
|
|
|
154
166
|
}
|
|
155
167
|
}, 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, {
|
|
156
168
|
archive: () => archiveOne(notification),
|
|
157
|
-
isArchivePending:
|
|
169
|
+
isArchivePending: isPendingFor(pendingClearAll, notification) || isPendingFor(pendingArchives, notification),
|
|
158
170
|
}) }))] }, notification.id))), retryState.error !== null
|
|
159
171
|
? renderError({
|
|
160
172
|
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\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 [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 = 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 clearPending = () =>\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.delete(notification.id);\n return next;\n });\n setPendingArchiveIds((current) => {\n const next = new Set(current);\n next.add(notification.id);\n return next;\n });\n try {\n const result = await archiveMutation.mutateAsync(notification);\n // Nothing was archived (the card changed after it was rendered): the row stays\n // active, so it is not left looking like it is clearing.\n if (!result.archived) clearPending();\n } catch (archiveError) {\n clearPending();\n throw archiveError;\n }\n }, [archiveMutation]);\n\n const 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 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 = archiveAllMutation.isPending || pendingClearAllIds.size > 0;\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 pendingClearAllIds.has(notification.id) || pendingArchiveIds.has(notification.id),\n })}\n </div>\n )}\n </div>\n ))}\n {retryState.error !== null\n ? renderError({\n error: retryState.error,\n retry: () => {\n void refetch();\n },\n retrying: retryState.isRetrying,\n hasRows: hasNotifications,\n })\n : null}\n {showEmptyState && <NotificationEmptyState text={emptyText} />}\n <ListPagination\n pagination={{\n currentPage: page,\n canPreviousPage: hasPrevPage,\n canNextPage: hasNextPage,\n goToPreviousPage: prevPage,\n goToNextPage: nextPage,\n }}\n busy={isFetching}\n className=\"ntf-list-footer mt-0\"\n />\n </>\n )}\n </div>\n </div>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';\nimport { Badge, Button, ListPagination, Separator, Spinner } from '@ttt-productions/ui-core/react';\nimport { useActiveNotifications } from '../hooks/useActiveNotifications.js';\nimport { useArchiveNotification } from '../hooks/useArchiveNotification.js';\nimport { useArchiveAllNotifications } from '../hooks/useArchiveAllNotifications.js';\nimport { NotificationEmptyState } from './NotificationEmptyState.js';\nimport { formatRelativeTime } from './relative-time.js';\nimport type { NotificationDoc, 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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useArchiveAllNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,oBAAoB,EACpB,iCAAiC,EAClC,MAAM,gBAAgB,CAAC;AASxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAyC,EACzC,QAA4B,EAC5B,cAAc,GACf,EAAE,iCAAiC,
|
|
1
|
+
{"version":3,"file":"useArchiveAllNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,oBAAoB,EACpB,iCAAiC,EAClC,MAAM,gBAAgB,CAAC;AASxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAyC,EACzC,QAA4B,EAC5B,cAAc,GACf,EAAE,iCAAiC,iGAkEnC"}
|
|
@@ -57,20 +57,33 @@ export function useArchiveAllNotifications({ userId, category, enqueueArchiveAll
|
|
|
57
57
|
const snap = await getArchiveAllStatusFn(jobId);
|
|
58
58
|
lastArchived = snap.archived ?? 0;
|
|
59
59
|
if (snap.state === 'complete') {
|
|
60
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
status: 'complete',
|
|
62
|
+
category,
|
|
63
|
+
archived: lastArchived,
|
|
64
|
+
notArchived: snap.notArchived ?? 0,
|
|
65
|
+
complete: true,
|
|
66
|
+
};
|
|
61
67
|
}
|
|
62
68
|
if (snap.state === 'incomplete') {
|
|
63
|
-
return { status: 'incomplete', category, archived: lastArchived, complete: false };
|
|
69
|
+
return { status: 'incomplete', category, archived: lastArchived, notArchived: 0, complete: false };
|
|
64
70
|
}
|
|
65
71
|
if (snap.state === 'failed') {
|
|
66
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
status: 'failed',
|
|
74
|
+
category,
|
|
75
|
+
archived: lastArchived,
|
|
76
|
+
notArchived: 0,
|
|
77
|
+
complete: false,
|
|
78
|
+
error: snap.error,
|
|
79
|
+
};
|
|
67
80
|
}
|
|
68
81
|
// Still in-progress — wait before the next poll unless this was the last allowed poll.
|
|
69
82
|
if (poll < maxPolls - 1)
|
|
70
83
|
await sleep(pollIntervalMs);
|
|
71
84
|
}
|
|
72
85
|
// Guard: the job never reached a terminal state within the poll budget — report incomplete.
|
|
73
|
-
return { status: 'incomplete', category, archived: lastArchived, complete: false };
|
|
86
|
+
return { status: 'incomplete', category, archived: lastArchived, notArchived: 0, complete: false };
|
|
74
87
|
},
|
|
75
88
|
onSuccess: (result) => {
|
|
76
89
|
// Only a completed drain changed the active set; still invalidate on an incomplete drain since
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useArchiveAllNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAMpE,MAAM,wBAAwB,GAAG,IAAI,CAAC;AACtC,MAAM,iBAAiB,GAAG,GAAG,CAAC,CAAC,iCAAiC;AAEhE,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,GAAG,wBAAwB,EACzC,QAAQ,GAAG,iBAAiB,EAC5B,cAAc,GACoB;IAClC,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,MAAM,qBAAqB,GAAG;QAC5B,CAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC;QAC7C,CAAC,eAAe,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC;QACnD,CAAC,eAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC;KAC/C,CAAC;IAEF,OAAO,WAAW,CAAC;QACjB,gGAAgG;QAChG,4FAA4F;QAC5F,gGAAgG;QAChG,oEAAoE;QACpE,UAAU,EAAE,KAAK,IAAmC,EAAE;YACpD,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAC;YAEtD,4FAA4F;YAC5F,6FAA6F;YAC7F,0FAA0F;YAC1F,IAAI,YAAY,GAAG,CAAC,CAAC;YACrB,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,QAAQ,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;gBAC9C,MAAM,IAAI,GAAG,MAAM,qBAAqB,CAAC,KAAK,CAAC,CAAC;gBAChD,YAAY,GAAG,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;gBAElC,IAAI,IAAI,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;oBAC9B,OAAO,
|
|
1
|
+
{"version":3,"file":"useArchiveAllNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\nimport type {\n ArchiveAllPollResult,\n UseArchiveAllNotificationsOptions,\n} from '../../types.js';\n\nconst DEFAULT_POLL_INTERVAL_MS = 1500;\nconst DEFAULT_MAX_POLLS = 120; // ~3 min at the default interval\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Trigger + poll a SERVER-OWNED archive-all job for one notification category.\n *\n * This is a THIN STATUS POLLER, not a client loop. It:\n * 1. enqueues ONE server job via `enqueueArchiveAllFn(category)` (the category — the tab the tray\n * was invoked on — flows into the job, preserving per-tab scoping: the worker archives ONLY\n * that category's active cards, never all tabs);\n * 2. polls `getArchiveAllStatusFn(jobId)` until the job reaches a terminal state\n * (`complete` | `incomplete` | `failed`);\n * 3. resolves an {@link ArchiveAllPollResult} the caller gates side effects on, and invalidates\n * the read keys on a completed drain.\n *\n * All paging + ownership-checked per-card deletion happen server-side. The hook performs no client\n * Firestore writes and never chains archive calls. The generic package hardcodes no callable name —\n * the app injects both adapters.\n *\n * @example\n * ```tsx\n * const enqueue = httpsCallable(functions, 'enqueueArchiveAll');\n * const getStatus = httpsCallable(functions, 'getArchiveAllStatus');\n * const archiveAll = useArchiveAllNotifications({\n * userId: user.uid,\n * category: 'user',\n * enqueueArchiveAllFn: (category) => enqueue({ category }).then((r) => r.data),\n * getArchiveAllStatusFn: (jobId) => getStatus({ jobId }).then((r) => r.data),\n * });\n *\n * archiveAll.mutate();\n * ```\n */\nexport function useArchiveAllNotifications({\n userId,\n category,\n enqueueArchiveAllFn,\n getArchiveAllStatusFn,\n pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,\n maxPolls = DEFAULT_MAX_POLLS,\n invalidateKeys,\n}: UseArchiveAllNotificationsOptions) {\n const queryClient = useQueryClient();\n\n const defaultInvalidateKeys = [\n ['notifications', 'active', category, userId],\n ['notifications', 'unread-count', category, userId],\n ['notifications', 'history', category, userId],\n ];\n\n return useMutation({\n // Server-owned archive-all: enqueue ONE job scoped to `category`, then poll its status until it\n // reaches a terminal state. No browser loop, no chain of archive transactions. The mutation\n // resolves with an explicit terminal result (never throws for an incomplete drain) — `complete`\n // is the load-bearing flag callers gate post-clear side effects on.\n mutationFn: async (): Promise<ArchiveAllPollResult> => {\n const { jobId } = await enqueueArchiveAllFn(category);\n\n // Poll until terminal or until the guard bound is hit. A job that never terminates resolves\n // `incomplete` (mirrors the old loop's honest \"some remain\" result) rather than hanging. The\n // first poll happens immediately so a fast/already-complete job resolves without a delay.\n let lastArchived = 0;\n for (let poll = 0; poll < maxPolls; poll += 1) {\n const snap = await getArchiveAllStatusFn(jobId);\n lastArchived = snap.archived ?? 0;\n\n if (snap.state === 'complete') {\n return {\n status: 'complete',\n category,\n archived: lastArchived,\n notArchived: snap.notArchived ?? 0,\n complete: true,\n };\n }\n if (snap.state === 'incomplete') {\n return { status: 'incomplete', category, archived: lastArchived, notArchived: 0, complete: false };\n }\n if (snap.state === 'failed') {\n return {\n status: 'failed',\n category,\n archived: lastArchived,\n notArchived: 0,\n complete: false,\n error: snap.error,\n };\n }\n\n // Still in-progress — wait before the next poll unless this was the last allowed poll.\n if (poll < maxPolls - 1) await sleep(pollIntervalMs);\n }\n\n // Guard: the job never reached a terminal state within the poll budget — report incomplete.\n return { status: 'incomplete', category, archived: lastArchived, notArchived: 0, complete: false };\n },\n onSuccess: (result) => {\n // Only a completed drain changed the active set; still invalidate on an incomplete drain since\n // SOME cards were archived. A hard enqueue/poll failure rejects the mutation (onError), so this\n // never runs for `failed`.\n const keysToInvalidate = invalidateKeys ?? defaultInvalidateKeys;\n keysToInvalidate.forEach((key) => {\n queryClient.invalidateQueries({ queryKey: [...key], exact: false });\n });\n void result;\n },\n });\n}\n"]}
|
package/dist/types.d.ts
CHANGED
|
@@ -249,6 +249,12 @@ export interface ArchiveAllJobSnapshot {
|
|
|
249
249
|
category: string;
|
|
250
250
|
/** Cards archived so far (monotonic); optional progress for the UI. */
|
|
251
251
|
archived?: number;
|
|
252
|
+
/**
|
|
253
|
+
* On a `complete` job: cards the drain could not archive, which are still in the list. A job
|
|
254
|
+
* completes once it has walked the whole category, so a card that keeps failing on its own does
|
|
255
|
+
* not hold the rest of the category back.
|
|
256
|
+
*/
|
|
257
|
+
notArchived?: number;
|
|
252
258
|
/** Human-readable failure reason when `state === 'failed'` (optional). */
|
|
253
259
|
error?: string;
|
|
254
260
|
}
|
|
@@ -261,6 +267,8 @@ export interface ArchiveAllPollResult {
|
|
|
261
267
|
status: Extract<ArchiveAllJobStatus, 'complete' | 'incomplete' | 'failed'>;
|
|
262
268
|
category: string;
|
|
263
269
|
archived: number;
|
|
270
|
+
/** Cards a complete drain left in the list (each failed on its own); 0 otherwise. */
|
|
271
|
+
notArchived: number;
|
|
264
272
|
complete: boolean;
|
|
265
273
|
error?: string;
|
|
266
274
|
}
|
|
@@ -340,7 +348,12 @@ export interface NotificationListProps {
|
|
|
340
348
|
getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;
|
|
341
349
|
/** Left-aligned title for the active-list header. */
|
|
342
350
|
title?: ReactNode;
|
|
343
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Fires with the terminal result ONLY when the archive-all job completed — the whole category was
|
|
353
|
+
* walked. A complete result can still carry `notArchived > 0` (cards that each failed and stay in
|
|
354
|
+
* the list); the copy for that is the app's.
|
|
355
|
+
*/
|
|
356
|
+
onClearAll?: (result: ArchiveAllPollResult) => void;
|
|
344
357
|
/** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */
|
|
345
358
|
refetchInterval?: number;
|
|
346
359
|
/** How long a read page counts as fresh, in ms (default 30s). */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * @ttt-productions/notification-core — Type definitions\n *\n * Two-tier notification system: active (unread) → history (archived).\n * No `isRead` flag — existence in the active collection means unread.\n */\n\n// Type-only import: erased at compile time, so this does not force React into\n// the server-safe main entry's runtime graph (the `react` peer dep stays optional).\nimport type { ReactNode } from 'react';\n\n// ============================================================================\n// DOCUMENT TYPES\n// ============================================================================\n\n/**\n * Active notification document stored in Firestore.\n * Same shape for both user and admin collections.\n */\nexport interface NotificationDoc {\n /** Firestore doc ID */\n id: string;\n /** Notification type (e.g. 'content_report', 'entity_invite') */\n type: string;\n /** Dedup key (e.g. 'report_entityXYZ') — same key = same notification */\n dedupKey: string;\n /** Category: 'user' | 'admin' (extensible) */\n category: string;\n\n // Targeting\n /** userId for 'user' category, null for 'admin' (shared) */\n targetUserId: string | null;\n\n // Display\n /** Notification title */\n title: string;\n /** Notification body */\n message: string;\n\n // Dedup aggregation\n /** Incremented on duplicates */\n count: number;\n /**\n * Last N actor user IDs (capped). Identity is stored id-only — display names\n * are resolved at read time by the consuming app (e.g. from publicUsers), so\n * they never drift in the persisted doc.\n */\n latestActorIds: string[];\n\n // Navigation\n /**\n * Route path when clicked (e.g. '/admin' or '/entities/abc'). ABSENT when the\n * type declares no `defaultTargetPath` — a linkless, informational-only\n * notification (the consumer renders no \"go to\" affordance for it).\n */\n targetPath?: string;\n /** Type-specific metadata (e.g. { entityId, reason }) */\n metadata: Record<string, unknown>;\n\n // Seen state (personal notifications)\n /**\n * Epoch ms the recipient last marked this notification seen, or `0` when\n * unseen. Generic field — no domain knowledge. Always present (initialized\n * to `0` on creation and reset to `0` on dedup-increment) so the unread\n * `count()` aggregation can match it with a `seenAt == 0` equality predicate.\n * `seenAt` is a personal concept; shared notifications carry it for shape\n * uniformity but their unread indicator is existence-based and ignores it.\n */\n seenAt: number;\n\n /**\n * Opaque per-generation token (uuid), minted FRESH whenever the active doc is\n * created or materially re-lit. The SEEN/ARCHIVE precondition compares against\n * THIS, not a restartable integer: the active doc id is deterministic, so a\n * delete+recreate would restart an integer and let a stale tab's \"version 1\"\n * match a different card it never saw (ABA). An opaque token cannot repeat.\n * Optional on the legacy active doc shape; the ledger materializer always sets it.\n */\n activityGeneration?: string;\n /** The `activityGeneration` observed at the time `seenAt` was last set. */\n seenAtGeneration?: string;\n\n // Timestamps (epoch ms)\n /** First occurrence */\n createdAt: number;\n /** Latest occurrence */\n updatedAt: number;\n}\n\n// Archived history docs are persisted as an untyped wrapper by the server archive\n// helper (server/observed-generation.ts) — `archivedSnapshot` (the full active doc)\n// plus archive metadata + a native-TTL `expireAt` Timestamp. There is no consumer-\n// facing TS type for that shape; the canonical registry schema lives in the\n// consuming app's core package (doc-schemas/notifications.ts → NotificationHistoryDocSchema).\n\n/**\n * Pending notification — queue item for the batch processor.\n */\nexport interface PendingNotification {\n id: string;\n /** Notification type */\n type: string;\n /** Target category */\n category: string;\n /** Target userId (null for admin/shared) */\n targetUserId: string | null;\n /** Who triggered this (id-only; display name resolved at read time) */\n actorId: string;\n /** Type-specific data for building the notification */\n metadata: Record<string, unknown>;\n /** Epoch ms */\n createdAt: number;\n}\n\n// ============================================================================\n// CONFIGURATION TYPES\n// ============================================================================\n\n/**\n * Per-category configuration (user vs admin).\n */\nexport interface NotificationCategoryConfig {\n /** Firestore collection path for active notifications */\n activePath: string;\n /** Function that returns the history collection path */\n historyPath: (userId?: string) => string;\n /** 'personal' requires targetUserId, 'shared' = all members see everything */\n audienceType: 'personal' | 'shared';\n}\n\n/**\n * Per-type configuration for a notification type.\n */\nexport interface NotificationTypeConfig {\n /** Which category this type belongs to */\n category: string;\n /** Delivery mode */\n delivery: 'realtime' | 'queued';\n /** Function to build the dedupKey from metadata */\n dedupKeyPattern: (metadata: Record<string, unknown>) => string;\n /** Function to build the title from metadata */\n titlePattern: (metadata: Record<string, unknown>) => string;\n /** Function to build the message from metadata + count */\n messagePattern: (metadata: Record<string, unknown>, count: number) => string;\n /**\n * Default target path, or function to build from metadata. OMIT to declare a\n * linkless type: the written doc carries no `targetPath` and the consumer\n * renders no \"go to\" affordance (clear-only rows).\n */\n defaultTargetPath?: string | ((metadata: Record<string, unknown>) => string);\n /** Max count value (default 5000) */\n countCap?: number;\n /** Max latestActorIds array length (default 5) */\n actorCap?: number;\n /** Icon/emoji for display (optional) */\n icon?: string;\n}\n\n/**\n * Full notification system configuration — provided by each app.\n */\nexport interface NotificationSystemConfig {\n /** Category definitions */\n categories: Record<string, NotificationCategoryConfig>;\n /** Type definitions */\n types: Record<string, NotificationTypeConfig>;\n /** Batch processor interval in minutes (default 10) */\n batchIntervalMinutes?: number;\n /** Firestore collection for pending notifications (default 'pendingNotifications') */\n pendingCollectionPath?: string;\n /** Firestore collection for the delivery ledger (default 'notificationDeliveries'). */\n deliveriesCollectionPath?: string;\n /**\n * Factory that converts epoch-ms to the app's Firestore `Timestamp` instance.\n * REQUIRED for the delivery ledger: Firestore native TTL acts ONLY on\n * `Timestamp` fields, so `expireAt` must be a real Timestamp (never a number).\n * The generic package never imports firebase-admin, so the app injects this\n * (e.g. `(ms) => admin.firestore.Timestamp.fromMillis(ms)`).\n */\n timestampFromMillis?: (ms: number) => unknown;\n /** Delivery-row TTL after materialization, in ms (default 90 days). */\n deliveryTtlMs?: number;\n /** Max delivery attempts before a row dead-letters (default 8). */\n maxDeliveryAttempts?: number;\n}\n\n// ============================================================================\n// HOOK OPTION TYPES\n// ============================================================================\n\nexport interface UseActiveNotificationsOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n /** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */\n refetchInterval?: number;\n /** How long a read page counts as fresh, in ms (default 30s). */\n staleTime?: number;\n}\n\n/**\n * A flattened archived-history item for the read surface. The history doc is a\n * wrapper (`archivedSnapshot` + archive metadata + TTL); this shape lifts the\n * snapshot's display fields to the top level so the history list renders exactly\n * like the active list, plus `archivedAt` for ordering/display and\n * `archiveOccurrenceId` as the stable React key.\n */\nexport interface NotificationHistoryItem extends NotificationDoc {\n /** The history doc id (stable key — the `archiveOccurrenceId`). */\n archiveOccurrenceId: string;\n /** Epoch ms the notification was archived (history docs order by this). */\n archivedAt: number;\n}\n\nexport interface UseNotificationHistoryOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n /** Read-freshness stale time in ms (archived rows are immutable; default 60s). */\n staleTime?: number;\n}\n\nexport interface UseUnreadCountOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n refetchInterval?: number;\n countLimit?: number;\n}\n\n/**\n * What an archive adapter answers: whether the card was archived. `false` means the\n * server archived nothing (for example the card gained new activity after it was\n * rendered); the row stays active and is not shown as clearing.\n */\nexport interface NotificationArchiveResult {\n archived: boolean;\n}\n\n/**\n * App-supplied adapter that archives one rendered notification (active → history).\n * It receives the row exactly as the list rendered it, so the app can send what it\n * observed (e.g. the row's activity generation). It rejects on failure and resolves\n * whether the card was archived.\n */\nexport type NotificationArchiveFn = (notification: NotificationDoc) => Promise<NotificationArchiveResult>;\n\nexport interface UseArchiveNotificationOptions {\n userId: string;\n category: string;\n /**\n * Performs the archive through the app's callable. The hook performs no client\n * Firestore writes; it only invalidates the read keys on success.\n */\n archiveFn: NotificationArchiveFn;\n invalidateKeys?: readonly unknown[][];\n}\n\n// ----------------------------------------------------------------------------\n// Archive-all — server-owned job + thin client status poller\n// ----------------------------------------------------------------------------\n//\n// Archive-all is NOT a browser loop. The client enqueues ONE server-owned\n// archive-all job (scoped to a single notification category/tab), then polls its\n// status until the job reaches a terminal state. All paging + ownership-checked\n// per-card deletion happen server-side in a bounded scheduled/queued worker; the\n// hook never touches Firestore and never chains archive calls. The generic\n// package hardcodes NO callable name — the app injects both adapters below.\n\n/**\n * Terminal + transient states of a server-owned archive-all job, as surfaced to the UI.\n *\n * - `idle` — nothing enqueued yet (initial / reset state).\n * - `enqueuing` — the enqueue adapter is in flight (job not yet acknowledged).\n * - `in-progress` — the job is enqueued/running; the worker is still draining the category.\n * - `complete` — TERMINAL: the worker fully drained the category's active set.\n * - `incomplete` — TERMINAL: the job stopped without draining everything (e.g. hit its own\n * bound / a non-advanceable card). The tray keeps a \"some remain — try again\"\n * affordance, mirroring the old loop's incomplete result.\n * - `failed` — TERMINAL: the enqueue call threw, or the job dead-lettered / reported failure.\n */\nexport type ArchiveAllJobStatus =\n | 'idle'\n | 'enqueuing'\n | 'in-progress'\n | 'complete'\n | 'incomplete'\n | 'failed';\n\n/**\n * Result of the INJECTED enqueue adapter — triggers exactly one server-owned archive-all job for\n * the given category and returns immediately with the job's id (idempotent per user+category+request\n * on the app side). The app wires this to its `httpsCallable(functions, 'enqueueArchiveAll')`\n * (or equivalent); the generic package never names the callable.\n */\nexport interface EnqueueArchiveAllResult {\n /** Server job id the poller then polls via the status adapter. */\n jobId: string;\n}\n\n/**\n * Snapshot of a server-owned archive-all job, returned by the INJECTED status-query adapter.\n * The poller maps `state` → {@link ArchiveAllJobStatus} and stops polling on any terminal state.\n * `category` echoes the tab/category the job was enqueued for so the UI can assert per-tab scoping.\n */\nexport interface ArchiveAllJobSnapshot {\n /** Job lifecycle state as owned by the server worker. */\n state: 'in-progress' | 'complete' | 'incomplete' | 'failed';\n /** The notification category/tab this job is scoped to (must match the enqueue category). */\n category: string;\n /** Cards archived so far (monotonic); optional progress for the UI. */\n archived?: number;\n /** Human-readable failure reason when `state === 'failed'` (optional). */\n error?: string;\n}\n\n/**\n * Aggregate result the poller resolves with once the job reaches a terminal state.\n * `complete` is the load-bearing completion contract (true ONLY when the whole category was\n * drained), preserved from the old loop so callers gate side effects the same way.\n */\nexport interface ArchiveAllPollResult {\n status: Extract<ArchiveAllJobStatus, 'complete' | 'incomplete' | 'failed'>;\n category: string;\n archived: number;\n complete: boolean;\n error?: string;\n}\n\nexport interface UseArchiveAllNotificationsOptions {\n userId: string;\n category: string;\n /**\n * App-supplied adapter that ENQUEUES one server-owned archive-all job scoped to `category` and\n * returns its `jobId`. The `category` is passed so the app callable can carry the tab/source into\n * the job (per-tab scoping is REQUIRED — the worker archives only that category's active cards).\n * Typically `(category) => enqueueArchiveAll({ category })` over an `httpsCallable`.\n */\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n /**\n * App-supplied adapter that returns the current {@link ArchiveAllJobSnapshot} for a job id — the\n * hook polls it until the job reaches a terminal state. Typically\n * `(jobId) => getArchiveAllStatus({ jobId })` over an `httpsCallable`, or a Firestore read of the\n * job doc. No client Firestore writes.\n */\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n /** Poll interval in ms while the job is in-progress (default 1500). */\n pollIntervalMs?: number;\n /**\n * Max number of status polls before the poller gives up and resolves `incomplete` (guards a job\n * that never reaches a terminal state — default 120, i.e. ~3 min at the default interval).\n */\n maxPolls?: number;\n invalidateKeys?: readonly unknown[][];\n}\n\n// ============================================================================\n// COMPONENT PROP TYPES\n// ============================================================================\n\n/**\n * Per-row actions the list hands to `renderRowAction`. The row itself is inert\n * — every affordance lives in the controls the consumer\n * renders in the slot. `archive` performs the active→history archive for that\n * row; it is present ONLY on the active list (archived history rows cannot be\n * re-archived, so it is absent there).\n */\nexport interface NotificationRowActions {\n /** Archives this row; rejects when the archive fails. */\n archive?: () => Promise<void>;\n /**\n * True while this row is archiving, including a category-wide Clear All. After an\n * archive the server confirmed, it stays true until the row leaves the active list.\n */\n isArchivePending?: boolean;\n}\n\n/**\n * What a list hands its `renderError` slot when its read failed. `hasRows` is true when\n * rows from an earlier read are still on screen above the slot (a failed refresh);\n * `retry` re-reads the displayed page and `retrying` is that read's pending flag.\n */\nexport interface NotificationListErrorState {\n error: Error;\n retry: () => void;\n retrying: boolean;\n hasRows: boolean;\n}\n\nexport interface NotificationListProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n /**\n * Archives one rendered notification. Passed straight through to\n * `useArchiveNotification`; the app wires it to its callable.\n */\n archiveFn: NotificationArchiveFn;\n /**\n * Adapter that ENQUEUES one server-owned archive-all job scoped to `category` and returns its\n * `jobId`. Passed straight through to `useArchiveAllNotifications`; the app wires it to its\n * enqueue callable. Per-tab scoping: the category flows through to the job.\n */\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n /**\n * Adapter that returns the current status snapshot for an archive-all `jobId`. Passed straight\n * through to `useArchiveAllNotifications`, which polls it until the job is terminal; the app wires\n * it to its status callable / job-doc read.\n */\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n /** Left-aligned title for the active-list header. */\n title?: ReactNode;\n onClearAll?: () => void;\n /** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */\n refetchInterval?: number;\n /** How long a read page counts as fresh, in ms (default 30s). */\n staleTime?: number;\n /** Empty-state text, shown only for an answered, empty first page. */\n emptyText?: string;\n /** The failed-read state, shown before (never instead of) the pager. Copy is the app's. */\n renderError: (state: NotificationListErrorState) => ReactNode;\n /**\n * Called with the rows on screen each time the displayed page's answered rows change\n * (not while a page is loading or failed with nothing shown) — the rows to mark seen.\n */\n onRenderedRowsChange?: (rows: readonly NotificationDoc[]) => void;\n /**\n * Per-row control slot. The row itself is inert; the consumer renders the row's\n * controls here — typically an ArrowRight \"go to\" (which may call `actions.archive`\n * then navigate) plus a clear/archive button (calls `actions.archive`). Omit to render\n * a row with no controls.\n */\n renderRowAction?: (notification: NotificationDoc, actions: NotificationRowActions) => ReactNode;\n}\n\nexport interface NotificationHistoryListProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n pageSize?: number;\n staleTime?: number;\n /** Empty-state text, shown only for an answered, empty first page. */\n emptyText?: string;\n /** The failed-read state, shown before (never instead of) the pager. Copy is the app's. */\n renderError: (state: NotificationListErrorState) => ReactNode;\n /** Left-aligned title for the read-only archived-list header. */\n title?: ReactNode;\n /**\n * Per-row control slot for archived rows. The row is inert; archived rows are\n * read-only (no re-archive), so `actions.archive` is absent — the consumer\n * typically renders only an ArrowRight \"go to\". Omit for a row with no controls.\n */\n renderRowAction?: (notification: NotificationHistoryItem, actions: NotificationRowActions) => ReactNode;\n}\n\nexport interface NotificationEmptyStateProps {\n text?: string;\n}\n\nexport interface NotificationUnreadBadgeProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n refetchInterval?: number;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * @ttt-productions/notification-core — Type definitions\n *\n * Two-tier notification system: active (unread) → history (archived).\n * No `isRead` flag — existence in the active collection means unread.\n */\n\n// Type-only import: erased at compile time, so this does not force React into\n// the server-safe main entry's runtime graph (the `react` peer dep stays optional).\nimport type { ReactNode } from 'react';\n\n// ============================================================================\n// DOCUMENT TYPES\n// ============================================================================\n\n/**\n * Active notification document stored in Firestore.\n * Same shape for both user and admin collections.\n */\nexport interface NotificationDoc {\n /** Firestore doc ID */\n id: string;\n /** Notification type (e.g. 'content_report', 'entity_invite') */\n type: string;\n /** Dedup key (e.g. 'report_entityXYZ') — same key = same notification */\n dedupKey: string;\n /** Category: 'user' | 'admin' (extensible) */\n category: string;\n\n // Targeting\n /** userId for 'user' category, null for 'admin' (shared) */\n targetUserId: string | null;\n\n // Display\n /** Notification title */\n title: string;\n /** Notification body */\n message: string;\n\n // Dedup aggregation\n /** Incremented on duplicates */\n count: number;\n /**\n * Last N actor user IDs (capped). Identity is stored id-only — display names\n * are resolved at read time by the consuming app (e.g. from publicUsers), so\n * they never drift in the persisted doc.\n */\n latestActorIds: string[];\n\n // Navigation\n /**\n * Route path when clicked (e.g. '/admin' or '/entities/abc'). ABSENT when the\n * type declares no `defaultTargetPath` — a linkless, informational-only\n * notification (the consumer renders no \"go to\" affordance for it).\n */\n targetPath?: string;\n /** Type-specific metadata (e.g. { entityId, reason }) */\n metadata: Record<string, unknown>;\n\n // Seen state (personal notifications)\n /**\n * Epoch ms the recipient last marked this notification seen, or `0` when\n * unseen. Generic field — no domain knowledge. Always present (initialized\n * to `0` on creation and reset to `0` on dedup-increment) so the unread\n * `count()` aggregation can match it with a `seenAt == 0` equality predicate.\n * `seenAt` is a personal concept; shared notifications carry it for shape\n * uniformity but their unread indicator is existence-based and ignores it.\n */\n seenAt: number;\n\n /**\n * Opaque per-generation token (uuid), minted FRESH whenever the active doc is\n * created or materially re-lit. The SEEN/ARCHIVE precondition compares against\n * THIS, not a restartable integer: the active doc id is deterministic, so a\n * delete+recreate would restart an integer and let a stale tab's \"version 1\"\n * match a different card it never saw (ABA). An opaque token cannot repeat.\n * Optional on the legacy active doc shape; the ledger materializer always sets it.\n */\n activityGeneration?: string;\n /** The `activityGeneration` observed at the time `seenAt` was last set. */\n seenAtGeneration?: string;\n\n // Timestamps (epoch ms)\n /** First occurrence */\n createdAt: number;\n /** Latest occurrence */\n updatedAt: number;\n}\n\n// Archived history docs are persisted as an untyped wrapper by the server archive\n// helper (server/observed-generation.ts) — `archivedSnapshot` (the full active doc)\n// plus archive metadata + a native-TTL `expireAt` Timestamp. There is no consumer-\n// facing TS type for that shape; the canonical registry schema lives in the\n// consuming app's core package (doc-schemas/notifications.ts → NotificationHistoryDocSchema).\n\n/**\n * Pending notification — queue item for the batch processor.\n */\nexport interface PendingNotification {\n id: string;\n /** Notification type */\n type: string;\n /** Target category */\n category: string;\n /** Target userId (null for admin/shared) */\n targetUserId: string | null;\n /** Who triggered this (id-only; display name resolved at read time) */\n actorId: string;\n /** Type-specific data for building the notification */\n metadata: Record<string, unknown>;\n /** Epoch ms */\n createdAt: number;\n}\n\n// ============================================================================\n// CONFIGURATION TYPES\n// ============================================================================\n\n/**\n * Per-category configuration (user vs admin).\n */\nexport interface NotificationCategoryConfig {\n /** Firestore collection path for active notifications */\n activePath: string;\n /** Function that returns the history collection path */\n historyPath: (userId?: string) => string;\n /** 'personal' requires targetUserId, 'shared' = all members see everything */\n audienceType: 'personal' | 'shared';\n}\n\n/**\n * Per-type configuration for a notification type.\n */\nexport interface NotificationTypeConfig {\n /** Which category this type belongs to */\n category: string;\n /** Delivery mode */\n delivery: 'realtime' | 'queued';\n /** Function to build the dedupKey from metadata */\n dedupKeyPattern: (metadata: Record<string, unknown>) => string;\n /** Function to build the title from metadata */\n titlePattern: (metadata: Record<string, unknown>) => string;\n /** Function to build the message from metadata + count */\n messagePattern: (metadata: Record<string, unknown>, count: number) => string;\n /**\n * Default target path, or function to build from metadata. OMIT to declare a\n * linkless type: the written doc carries no `targetPath` and the consumer\n * renders no \"go to\" affordance (clear-only rows).\n */\n defaultTargetPath?: string | ((metadata: Record<string, unknown>) => string);\n /** Max count value (default 5000) */\n countCap?: number;\n /** Max latestActorIds array length (default 5) */\n actorCap?: number;\n /** Icon/emoji for display (optional) */\n icon?: string;\n}\n\n/**\n * Full notification system configuration — provided by each app.\n */\nexport interface NotificationSystemConfig {\n /** Category definitions */\n categories: Record<string, NotificationCategoryConfig>;\n /** Type definitions */\n types: Record<string, NotificationTypeConfig>;\n /** Batch processor interval in minutes (default 10) */\n batchIntervalMinutes?: number;\n /** Firestore collection for pending notifications (default 'pendingNotifications') */\n pendingCollectionPath?: string;\n /** Firestore collection for the delivery ledger (default 'notificationDeliveries'). */\n deliveriesCollectionPath?: string;\n /**\n * Factory that converts epoch-ms to the app's Firestore `Timestamp` instance.\n * REQUIRED for the delivery ledger: Firestore native TTL acts ONLY on\n * `Timestamp` fields, so `expireAt` must be a real Timestamp (never a number).\n * The generic package never imports firebase-admin, so the app injects this\n * (e.g. `(ms) => admin.firestore.Timestamp.fromMillis(ms)`).\n */\n timestampFromMillis?: (ms: number) => unknown;\n /** Delivery-row TTL after materialization, in ms (default 90 days). */\n deliveryTtlMs?: number;\n /** Max delivery attempts before a row dead-letters (default 8). */\n maxDeliveryAttempts?: number;\n}\n\n// ============================================================================\n// HOOK OPTION TYPES\n// ============================================================================\n\nexport interface UseActiveNotificationsOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n /** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */\n refetchInterval?: number;\n /** How long a read page counts as fresh, in ms (default 30s). */\n staleTime?: number;\n}\n\n/**\n * A flattened archived-history item for the read surface. The history doc is a\n * wrapper (`archivedSnapshot` + archive metadata + TTL); this shape lifts the\n * snapshot's display fields to the top level so the history list renders exactly\n * like the active list, plus `archivedAt` for ordering/display and\n * `archiveOccurrenceId` as the stable React key.\n */\nexport interface NotificationHistoryItem extends NotificationDoc {\n /** The history doc id (stable key — the `archiveOccurrenceId`). */\n archiveOccurrenceId: string;\n /** Epoch ms the notification was archived (history docs order by this). */\n archivedAt: number;\n}\n\nexport interface UseNotificationHistoryOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n /** Read-freshness stale time in ms (archived rows are immutable; default 60s). */\n staleTime?: number;\n}\n\nexport interface UseUnreadCountOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n refetchInterval?: number;\n countLimit?: number;\n}\n\n/**\n * What an archive adapter answers: whether the card was archived. `false` means the\n * server archived nothing (for example the card gained new activity after it was\n * rendered); the row stays active and is not shown as clearing.\n */\nexport interface NotificationArchiveResult {\n archived: boolean;\n}\n\n/**\n * App-supplied adapter that archives one rendered notification (active → history).\n * It receives the row exactly as the list rendered it, so the app can send what it\n * observed (e.g. the row's activity generation). It rejects on failure and resolves\n * whether the card was archived.\n */\nexport type NotificationArchiveFn = (notification: NotificationDoc) => Promise<NotificationArchiveResult>;\n\nexport interface UseArchiveNotificationOptions {\n userId: string;\n category: string;\n /**\n * Performs the archive through the app's callable. The hook performs no client\n * Firestore writes; it only invalidates the read keys on success.\n */\n archiveFn: NotificationArchiveFn;\n invalidateKeys?: readonly unknown[][];\n}\n\n// ----------------------------------------------------------------------------\n// Archive-all — server-owned job + thin client status poller\n// ----------------------------------------------------------------------------\n//\n// Archive-all is NOT a browser loop. The client enqueues ONE server-owned\n// archive-all job (scoped to a single notification category/tab), then polls its\n// status until the job reaches a terminal state. All paging + ownership-checked\n// per-card deletion happen server-side in a bounded scheduled/queued worker; the\n// hook never touches Firestore and never chains archive calls. The generic\n// package hardcodes NO callable name — the app injects both adapters below.\n\n/**\n * Terminal + transient states of a server-owned archive-all job, as surfaced to the UI.\n *\n * - `idle` — nothing enqueued yet (initial / reset state).\n * - `enqueuing` — the enqueue adapter is in flight (job not yet acknowledged).\n * - `in-progress` — the job is enqueued/running; the worker is still draining the category.\n * - `complete` — TERMINAL: the worker fully drained the category's active set.\n * - `incomplete` — TERMINAL: the job stopped without draining everything (e.g. hit its own\n * bound / a non-advanceable card). The tray keeps a \"some remain — try again\"\n * affordance, mirroring the old loop's incomplete result.\n * - `failed` — TERMINAL: the enqueue call threw, or the job dead-lettered / reported failure.\n */\nexport type ArchiveAllJobStatus =\n | 'idle'\n | 'enqueuing'\n | 'in-progress'\n | 'complete'\n | 'incomplete'\n | 'failed';\n\n/**\n * Result of the INJECTED enqueue adapter — triggers exactly one server-owned archive-all job for\n * the given category and returns immediately with the job's id (idempotent per user+category+request\n * on the app side). The app wires this to its `httpsCallable(functions, 'enqueueArchiveAll')`\n * (or equivalent); the generic package never names the callable.\n */\nexport interface EnqueueArchiveAllResult {\n /** Server job id the poller then polls via the status adapter. */\n jobId: string;\n}\n\n/**\n * Snapshot of a server-owned archive-all job, returned by the INJECTED status-query adapter.\n * The poller maps `state` → {@link ArchiveAllJobStatus} and stops polling on any terminal state.\n * `category` echoes the tab/category the job was enqueued for so the UI can assert per-tab scoping.\n */\nexport interface ArchiveAllJobSnapshot {\n /** Job lifecycle state as owned by the server worker. */\n state: 'in-progress' | 'complete' | 'incomplete' | 'failed';\n /** The notification category/tab this job is scoped to (must match the enqueue category). */\n category: string;\n /** Cards archived so far (monotonic); optional progress for the UI. */\n archived?: number;\n /**\n * On a `complete` job: cards the drain could not archive, which are still in the list. A job\n * completes once it has walked the whole category, so a card that keeps failing on its own does\n * not hold the rest of the category back.\n */\n notArchived?: number;\n /** Human-readable failure reason when `state === 'failed'` (optional). */\n error?: string;\n}\n\n/**\n * Aggregate result the poller resolves with once the job reaches a terminal state.\n * `complete` is the load-bearing completion contract (true ONLY when the whole category was\n * drained), preserved from the old loop so callers gate side effects the same way.\n */\nexport interface ArchiveAllPollResult {\n status: Extract<ArchiveAllJobStatus, 'complete' | 'incomplete' | 'failed'>;\n category: string;\n archived: number;\n /** Cards a complete drain left in the list (each failed on its own); 0 otherwise. */\n notArchived: number;\n complete: boolean;\n error?: string;\n}\n\nexport interface UseArchiveAllNotificationsOptions {\n userId: string;\n category: string;\n /**\n * App-supplied adapter that ENQUEUES one server-owned archive-all job scoped to `category` and\n * returns its `jobId`. The `category` is passed so the app callable can carry the tab/source into\n * the job (per-tab scoping is REQUIRED — the worker archives only that category's active cards).\n * Typically `(category) => enqueueArchiveAll({ category })` over an `httpsCallable`.\n */\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n /**\n * App-supplied adapter that returns the current {@link ArchiveAllJobSnapshot} for a job id — the\n * hook polls it until the job reaches a terminal state. Typically\n * `(jobId) => getArchiveAllStatus({ jobId })` over an `httpsCallable`, or a Firestore read of the\n * job doc. No client Firestore writes.\n */\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n /** Poll interval in ms while the job is in-progress (default 1500). */\n pollIntervalMs?: number;\n /**\n * Max number of status polls before the poller gives up and resolves `incomplete` (guards a job\n * that never reaches a terminal state — default 120, i.e. ~3 min at the default interval).\n */\n maxPolls?: number;\n invalidateKeys?: readonly unknown[][];\n}\n\n// ============================================================================\n// COMPONENT PROP TYPES\n// ============================================================================\n\n/**\n * Per-row actions the list hands to `renderRowAction`. The row itself is inert\n * — every affordance lives in the controls the consumer\n * renders in the slot. `archive` performs the active→history archive for that\n * row; it is present ONLY on the active list (archived history rows cannot be\n * re-archived, so it is absent there).\n */\nexport interface NotificationRowActions {\n /** Archives this row; rejects when the archive fails. */\n archive?: () => Promise<void>;\n /**\n * True while this row is archiving, including a category-wide Clear All. After an\n * archive the server confirmed, it stays true until the row leaves the active list.\n */\n isArchivePending?: boolean;\n}\n\n/**\n * What a list hands its `renderError` slot when its read failed. `hasRows` is true when\n * rows from an earlier read are still on screen above the slot (a failed refresh);\n * `retry` re-reads the displayed page and `retrying` is that read's pending flag.\n */\nexport interface NotificationListErrorState {\n error: Error;\n retry: () => void;\n retrying: boolean;\n hasRows: boolean;\n}\n\nexport interface NotificationListProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n /**\n * Archives one rendered notification. Passed straight through to\n * `useArchiveNotification`; the app wires it to its callable.\n */\n archiveFn: NotificationArchiveFn;\n /**\n * Adapter that ENQUEUES one server-owned archive-all job scoped to `category` and returns its\n * `jobId`. Passed straight through to `useArchiveAllNotifications`; the app wires it to its\n * enqueue callable. Per-tab scoping: the category flows through to the job.\n */\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n /**\n * Adapter that returns the current status snapshot for an archive-all `jobId`. Passed straight\n * through to `useArchiveAllNotifications`, which polls it until the job is terminal; the app wires\n * it to its status callable / job-doc read.\n */\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n /** Left-aligned title for the active-list header. */\n title?: ReactNode;\n /**\n * Fires with the terminal result ONLY when the archive-all job completed — the whole category was\n * walked. A complete result can still carry `notArchived > 0` (cards that each failed and stay in\n * the list); the copy for that is the app's.\n */\n onClearAll?: (result: ArchiveAllPollResult) => void;\n /** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */\n refetchInterval?: number;\n /** How long a read page counts as fresh, in ms (default 30s). */\n staleTime?: number;\n /** Empty-state text, shown only for an answered, empty first page. */\n emptyText?: string;\n /** The failed-read state, shown before (never instead of) the pager. Copy is the app's. */\n renderError: (state: NotificationListErrorState) => ReactNode;\n /**\n * Called with the rows on screen each time the displayed page's answered rows change\n * (not while a page is loading or failed with nothing shown) — the rows to mark seen.\n */\n onRenderedRowsChange?: (rows: readonly NotificationDoc[]) => void;\n /**\n * Per-row control slot. The row itself is inert; the consumer renders the row's\n * controls here — typically an ArrowRight \"go to\" (which may call `actions.archive`\n * then navigate) plus a clear/archive button (calls `actions.archive`). Omit to render\n * a row with no controls.\n */\n renderRowAction?: (notification: NotificationDoc, actions: NotificationRowActions) => ReactNode;\n}\n\nexport interface NotificationHistoryListProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n pageSize?: number;\n staleTime?: number;\n /** Empty-state text, shown only for an answered, empty first page. */\n emptyText?: string;\n /** The failed-read state, shown before (never instead of) the pager. Copy is the app's. */\n renderError: (state: NotificationListErrorState) => ReactNode;\n /** Left-aligned title for the read-only archived-list header. */\n title?: ReactNode;\n /**\n * Per-row control slot for archived rows. The row is inert; archived rows are\n * read-only (no re-archive), so `actions.archive` is absent — the consumer\n * typically renders only an ArrowRight \"go to\". Omit for a row with no controls.\n */\n renderRowAction?: (notification: NotificationHistoryItem, actions: NotificationRowActions) => ReactNode;\n}\n\nexport interface NotificationEmptyStateProps {\n text?: string;\n}\n\nexport interface NotificationUnreadBadgeProps {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n refetchInterval?: number;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/notification-core",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.16",
|
|
4
4
|
"description": "Shared notification system for TTT Productions apps — active/history two-tier architecture with dedup, batch processing, and themed UI components",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|