@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/dist/types.d.ts
CHANGED
|
@@ -1,151 +1,56 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @ttt-productions/notification-core — Type definitions
|
|
3
|
-
*
|
|
4
|
-
* Two-tier notification system: active (unread) → history (archived).
|
|
5
|
-
* No `isRead` flag — existence in the active collection means unread.
|
|
6
|
-
*/
|
|
7
|
-
import type { ReactNode } from 'react';
|
|
8
|
-
/**
|
|
9
|
-
* Active notification document stored in Firestore.
|
|
10
|
-
* Same shape for both user and admin collections.
|
|
11
|
-
*/
|
|
12
1
|
export interface NotificationDoc {
|
|
13
|
-
/** Firestore doc ID */
|
|
14
2
|
id: string;
|
|
15
|
-
/** Notification type (e.g. 'content_report', 'entity_invite') */
|
|
16
3
|
type: string;
|
|
17
|
-
/** Dedup key (e.g. 'report_entityXYZ') — same key = same notification */
|
|
18
4
|
dedupKey: string;
|
|
19
|
-
/** Category: 'user' | 'admin' (extensible) */
|
|
20
5
|
category: string;
|
|
21
|
-
/** userId for 'user' category, null for 'admin' (shared) */
|
|
22
6
|
targetUserId: string | null;
|
|
23
|
-
/** Notification title */
|
|
24
7
|
title: string;
|
|
25
|
-
/** Notification body */
|
|
26
8
|
message: string;
|
|
27
|
-
/** Incremented on duplicates */
|
|
28
9
|
count: number;
|
|
29
|
-
/**
|
|
30
|
-
* Last N actor user IDs (capped). Identity is stored id-only — display names
|
|
31
|
-
* are resolved at read time by the consuming app (e.g. from publicUsers), so
|
|
32
|
-
* they never drift in the persisted doc.
|
|
33
|
-
*/
|
|
34
10
|
latestActorIds: string[];
|
|
35
|
-
/**
|
|
36
|
-
* Route path when clicked (e.g. '/admin' or '/entities/abc'). ABSENT when the
|
|
37
|
-
* type declares no `defaultTargetPath` — a linkless, informational-only
|
|
38
|
-
* notification (the consumer renders no "go to" affordance for it).
|
|
39
|
-
*/
|
|
40
11
|
targetPath?: string;
|
|
41
|
-
/** Type-specific metadata (e.g. { entityId, reason }) */
|
|
42
12
|
metadata: Record<string, unknown>;
|
|
43
|
-
/**
|
|
44
|
-
* Epoch ms the recipient last marked this notification seen, or `0` when
|
|
45
|
-
* unseen. Generic field — no domain knowledge. Always present (initialized
|
|
46
|
-
* to `0` on creation and reset to `0` on dedup-increment) so the unread
|
|
47
|
-
* `count()` aggregation can match it with a `seenAt == 0` equality predicate.
|
|
48
|
-
* `seenAt` is a personal concept; shared notifications carry it for shape
|
|
49
|
-
* uniformity but their unread indicator is existence-based and ignores it.
|
|
50
|
-
*/
|
|
13
|
+
/** `0` when unseen and never absent: a `count()` equality predicate cannot match a missing field. */
|
|
51
14
|
seenAt: number;
|
|
52
|
-
/**
|
|
53
|
-
* Opaque per-generation token (uuid), minted FRESH whenever the active doc is
|
|
54
|
-
* created or materially re-lit. The SEEN/ARCHIVE precondition compares against
|
|
55
|
-
* THIS, not a restartable integer: the active doc id is deterministic, so a
|
|
56
|
-
* delete+recreate would restart an integer and let a stale tab's "version 1"
|
|
57
|
-
* match a different card it never saw (ABA). An opaque token cannot repeat.
|
|
58
|
-
* Optional on the legacy active doc shape; the ledger materializer always sets it.
|
|
59
|
-
*/
|
|
60
15
|
activityGeneration?: string;
|
|
61
|
-
/** The `activityGeneration` observed at the time `seenAt` was last set. */
|
|
62
16
|
seenAtGeneration?: string;
|
|
63
|
-
/** First occurrence */
|
|
64
17
|
createdAt: number;
|
|
65
|
-
/** Latest occurrence */
|
|
66
18
|
updatedAt: number;
|
|
67
19
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Pending notification — queue item for the batch processor.
|
|
70
|
-
*/
|
|
71
20
|
export interface PendingNotification {
|
|
72
21
|
id: string;
|
|
73
|
-
/** Notification type */
|
|
74
22
|
type: string;
|
|
75
|
-
/** Target category */
|
|
76
23
|
category: string;
|
|
77
|
-
/** Target userId (null for admin/shared) */
|
|
78
24
|
targetUserId: string | null;
|
|
79
|
-
/** Who triggered this (id-only; display name resolved at read time) */
|
|
80
25
|
actorId: string;
|
|
81
|
-
/** Type-specific data for building the notification */
|
|
82
26
|
metadata: Record<string, unknown>;
|
|
83
|
-
/** Epoch ms */
|
|
84
27
|
createdAt: number;
|
|
85
28
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Per-category configuration (user vs admin).
|
|
88
|
-
*/
|
|
89
29
|
export interface NotificationCategoryConfig {
|
|
90
|
-
/** Firestore collection path for active notifications */
|
|
91
30
|
activePath: string;
|
|
92
|
-
/** Function that returns the history collection path */
|
|
93
31
|
historyPath: (userId?: string) => string;
|
|
94
|
-
/** 'personal' requires targetUserId, 'shared' = all members see everything */
|
|
95
32
|
audienceType: 'personal' | 'shared';
|
|
96
33
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Per-type configuration for a notification type.
|
|
99
|
-
*/
|
|
100
34
|
export interface NotificationTypeConfig {
|
|
101
|
-
/** Which category this type belongs to */
|
|
102
35
|
category: string;
|
|
103
|
-
/** Delivery mode */
|
|
104
36
|
delivery: 'realtime' | 'queued';
|
|
105
|
-
/** Function to build the dedupKey from metadata */
|
|
106
37
|
dedupKeyPattern: (metadata: Record<string, unknown>) => string;
|
|
107
|
-
/** Function to build the title from metadata */
|
|
108
38
|
titlePattern: (metadata: Record<string, unknown>) => string;
|
|
109
|
-
/** Function to build the message from metadata + count */
|
|
110
39
|
messagePattern: (metadata: Record<string, unknown>, count: number) => string;
|
|
111
|
-
/**
|
|
112
|
-
* Default target path, or function to build from metadata. OMIT to declare a
|
|
113
|
-
* linkless type: the written doc carries no `targetPath` and the consumer
|
|
114
|
-
* renders no "go to" affordance (clear-only rows).
|
|
115
|
-
*/
|
|
116
40
|
defaultTargetPath?: string | ((metadata: Record<string, unknown>) => string);
|
|
117
|
-
/** Max count value (default 5000) */
|
|
118
41
|
countCap?: number;
|
|
119
|
-
/** Max latestActorIds array length (default 5) */
|
|
120
42
|
actorCap?: number;
|
|
121
|
-
/** Icon/emoji for display (optional) */
|
|
122
43
|
icon?: string;
|
|
123
44
|
}
|
|
124
|
-
/**
|
|
125
|
-
* Full notification system configuration — provided by each app.
|
|
126
|
-
*/
|
|
127
45
|
export interface NotificationSystemConfig {
|
|
128
|
-
/** Category definitions */
|
|
129
46
|
categories: Record<string, NotificationCategoryConfig>;
|
|
130
|
-
/** Type definitions */
|
|
131
47
|
types: Record<string, NotificationTypeConfig>;
|
|
132
|
-
/** Batch processor interval in minutes (default 10) */
|
|
133
48
|
batchIntervalMinutes?: number;
|
|
134
|
-
/** Firestore collection for pending notifications (default 'pendingNotifications') */
|
|
135
49
|
pendingCollectionPath?: string;
|
|
136
|
-
/** Firestore collection for the delivery ledger (default 'notificationDeliveries'). */
|
|
137
50
|
deliveriesCollectionPath?: string;
|
|
138
|
-
/**
|
|
139
|
-
* Factory that converts epoch-ms to the app's Firestore `Timestamp` instance.
|
|
140
|
-
* REQUIRED for the delivery ledger: Firestore native TTL acts ONLY on
|
|
141
|
-
* `Timestamp` fields, so `expireAt` must be a real Timestamp (never a number).
|
|
142
|
-
* The generic package never imports firebase-admin, so the app injects this
|
|
143
|
-
* (e.g. `(ms) => admin.firestore.Timestamp.fromMillis(ms)`).
|
|
144
|
-
*/
|
|
51
|
+
/** Required by the ledger: Firestore TTL acts only on `Timestamp` fields, never a number. */
|
|
145
52
|
timestampFromMillis?: (ms: number) => unknown;
|
|
146
|
-
/** Delivery-row TTL after materialization, in ms (default 90 days). */
|
|
147
53
|
deliveryTtlMs?: number;
|
|
148
|
-
/** Max delivery attempts before a row dead-letters (default 8). */
|
|
149
54
|
maxDeliveryAttempts?: number;
|
|
150
55
|
}
|
|
151
56
|
export interface UseActiveNotificationsOptions {
|
|
@@ -154,22 +59,11 @@ export interface UseActiveNotificationsOptions {
|
|
|
154
59
|
category: string;
|
|
155
60
|
enabled?: boolean;
|
|
156
61
|
pageSize?: number;
|
|
157
|
-
/** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */
|
|
158
62
|
refetchInterval?: number;
|
|
159
|
-
/** How long a read page counts as fresh, in ms (default 30s). */
|
|
160
63
|
staleTime?: number;
|
|
161
64
|
}
|
|
162
|
-
/**
|
|
163
|
-
* A flattened archived-history item for the read surface. The history doc is a
|
|
164
|
-
* wrapper (`archivedSnapshot` + archive metadata + TTL); this shape lifts the
|
|
165
|
-
* snapshot's display fields to the top level so the history list renders exactly
|
|
166
|
-
* like the active list, plus `archivedAt` for ordering/display and
|
|
167
|
-
* `archiveOccurrenceId` as the stable React key.
|
|
168
|
-
*/
|
|
169
65
|
export interface NotificationHistoryItem extends NotificationDoc {
|
|
170
|
-
/** The history doc id (stable key — the `archiveOccurrenceId`). */
|
|
171
66
|
archiveOccurrenceId: string;
|
|
172
|
-
/** Epoch ms the notification was archived (history docs order by this). */
|
|
173
67
|
archivedAt: number;
|
|
174
68
|
}
|
|
175
69
|
export interface UseNotificationHistoryOptions {
|
|
@@ -178,7 +72,6 @@ export interface UseNotificationHistoryOptions {
|
|
|
178
72
|
category: string;
|
|
179
73
|
enabled?: boolean;
|
|
180
74
|
pageSize?: number;
|
|
181
|
-
/** Read-freshness stale time in ms (archived rows are immutable; default 60s). */
|
|
182
75
|
staleTime?: number;
|
|
183
76
|
}
|
|
184
77
|
export interface UseUnreadCountOptions {
|
|
@@ -189,85 +82,31 @@ export interface UseUnreadCountOptions {
|
|
|
189
82
|
refetchInterval?: number;
|
|
190
83
|
countLimit?: number;
|
|
191
84
|
}
|
|
192
|
-
/**
|
|
193
|
-
* What an archive adapter answers: whether the card was archived. `false` means the
|
|
194
|
-
* server archived nothing (for example the card gained new activity after it was
|
|
195
|
-
* rendered); the row stays active and is not shown as clearing.
|
|
196
|
-
*/
|
|
197
85
|
export interface NotificationArchiveResult {
|
|
198
86
|
archived: boolean;
|
|
199
87
|
}
|
|
200
|
-
/**
|
|
201
|
-
* App-supplied adapter that archives one rendered notification (active → history).
|
|
202
|
-
* It receives the row exactly as the list rendered it, so the app can send what it
|
|
203
|
-
* observed (e.g. the row's activity generation). It rejects on failure and resolves
|
|
204
|
-
* whether the card was archived.
|
|
205
|
-
*/
|
|
206
88
|
export type NotificationArchiveFn = (notification: NotificationDoc) => Promise<NotificationArchiveResult>;
|
|
207
89
|
export interface UseArchiveNotificationOptions {
|
|
208
90
|
userId: string;
|
|
209
91
|
category: string;
|
|
210
|
-
/**
|
|
211
|
-
* Performs the archive through the app's callable. The hook performs no client
|
|
212
|
-
* Firestore writes; it only invalidates the read keys on success.
|
|
213
|
-
*/
|
|
214
92
|
archiveFn: NotificationArchiveFn;
|
|
215
93
|
invalidateKeys?: readonly unknown[][];
|
|
216
94
|
}
|
|
217
|
-
/**
|
|
218
|
-
* Terminal + transient states of a server-owned archive-all job, as surfaced to the UI.
|
|
219
|
-
*
|
|
220
|
-
* - `idle` — nothing enqueued yet (initial / reset state).
|
|
221
|
-
* - `enqueuing` — the enqueue adapter is in flight (job not yet acknowledged).
|
|
222
|
-
* - `in-progress` — the job is enqueued/running; the worker is still draining the category.
|
|
223
|
-
* - `complete` — TERMINAL: the worker fully drained the category's active set.
|
|
224
|
-
* - `incomplete` — TERMINAL: the job stopped without draining everything (e.g. hit its own
|
|
225
|
-
* bound / a non-advanceable card). The tray keeps a "some remain — try again"
|
|
226
|
-
* affordance, mirroring the old loop's incomplete result.
|
|
227
|
-
* - `failed` — TERMINAL: the enqueue call threw, or the job dead-lettered / reported failure.
|
|
228
|
-
*/
|
|
229
95
|
export type ArchiveAllJobStatus = 'idle' | 'enqueuing' | 'in-progress' | 'complete' | 'incomplete' | 'failed';
|
|
230
|
-
/**
|
|
231
|
-
* Result of the INJECTED enqueue adapter — triggers exactly one server-owned archive-all job for
|
|
232
|
-
* the given category and returns immediately with the job's id (idempotent per user+category+request
|
|
233
|
-
* on the app side). The app wires this to its `httpsCallable(functions, 'enqueueArchiveAll')`
|
|
234
|
-
* (or equivalent); the generic package never names the callable.
|
|
235
|
-
*/
|
|
236
96
|
export interface EnqueueArchiveAllResult {
|
|
237
|
-
/** Server job id the poller then polls via the status adapter. */
|
|
238
97
|
jobId: string;
|
|
239
98
|
}
|
|
240
|
-
/**
|
|
241
|
-
* Snapshot of a server-owned archive-all job, returned by the INJECTED status-query adapter.
|
|
242
|
-
* The poller maps `state` → {@link ArchiveAllJobStatus} and stops polling on any terminal state.
|
|
243
|
-
* `category` echoes the tab/category the job was enqueued for so the UI can assert per-tab scoping.
|
|
244
|
-
*/
|
|
245
99
|
export interface ArchiveAllJobSnapshot {
|
|
246
|
-
/** Job lifecycle state as owned by the server worker. */
|
|
247
100
|
state: 'in-progress' | 'complete' | 'incomplete' | 'failed';
|
|
248
|
-
/** The notification category/tab this job is scoped to (must match the enqueue category). */
|
|
249
101
|
category: string;
|
|
250
|
-
/** Cards archived so far (monotonic); optional progress for the UI. */
|
|
251
102
|
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
103
|
notArchived?: number;
|
|
258
|
-
/** Human-readable failure reason when `state === 'failed'` (optional). */
|
|
259
104
|
error?: string;
|
|
260
105
|
}
|
|
261
|
-
/**
|
|
262
|
-
* Aggregate result the poller resolves with once the job reaches a terminal state.
|
|
263
|
-
* `complete` is the load-bearing completion contract (true ONLY when the whole category was
|
|
264
|
-
* drained), preserved from the old loop so callers gate side effects the same way.
|
|
265
|
-
*/
|
|
266
106
|
export interface ArchiveAllPollResult {
|
|
267
107
|
status: Extract<ArchiveAllJobStatus, 'complete' | 'incomplete' | 'failed'>;
|
|
268
108
|
category: string;
|
|
269
109
|
archived: number;
|
|
270
|
-
/** Cards a complete drain left in the list (each failed on its own); 0 otherwise. */
|
|
271
110
|
notArchived: number;
|
|
272
111
|
complete: boolean;
|
|
273
112
|
error?: string;
|
|
@@ -275,125 +114,22 @@ export interface ArchiveAllPollResult {
|
|
|
275
114
|
export interface UseArchiveAllNotificationsOptions {
|
|
276
115
|
userId: string;
|
|
277
116
|
category: string;
|
|
278
|
-
/**
|
|
279
|
-
* App-supplied adapter that ENQUEUES one server-owned archive-all job scoped to `category` and
|
|
280
|
-
* returns its `jobId`. The `category` is passed so the app callable can carry the tab/source into
|
|
281
|
-
* the job (per-tab scoping is REQUIRED — the worker archives only that category's active cards).
|
|
282
|
-
* Typically `(category) => enqueueArchiveAll({ category })` over an `httpsCallable`.
|
|
283
|
-
*/
|
|
284
117
|
enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;
|
|
285
|
-
/**
|
|
286
|
-
* App-supplied adapter that returns the current {@link ArchiveAllJobSnapshot} for a job id — the
|
|
287
|
-
* hook polls it until the job reaches a terminal state. Typically
|
|
288
|
-
* `(jobId) => getArchiveAllStatus({ jobId })` over an `httpsCallable`, or a Firestore read of the
|
|
289
|
-
* job doc. No client Firestore writes.
|
|
290
|
-
*/
|
|
291
118
|
getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;
|
|
292
|
-
/** Poll interval in ms while the job is in-progress (default 1500). */
|
|
293
119
|
pollIntervalMs?: number;
|
|
294
|
-
/**
|
|
295
|
-
* Max number of status polls before the poller gives up and resolves `incomplete` (guards a job
|
|
296
|
-
* that never reaches a terminal state — default 120, i.e. ~3 min at the default interval).
|
|
297
|
-
*/
|
|
298
120
|
maxPolls?: number;
|
|
299
121
|
invalidateKeys?: readonly unknown[][];
|
|
300
122
|
}
|
|
301
|
-
/**
|
|
302
|
-
* Per-row actions the list hands to `renderRowAction`. The row itself is inert
|
|
303
|
-
* — every affordance lives in the controls the consumer
|
|
304
|
-
* renders in the slot. `archive` performs the active→history archive for that
|
|
305
|
-
* row; it is present ONLY on the active list (archived history rows cannot be
|
|
306
|
-
* re-archived, so it is absent there).
|
|
307
|
-
*/
|
|
308
123
|
export interface NotificationRowActions {
|
|
309
|
-
/** Archives this row; rejects when the archive fails. */
|
|
310
124
|
archive?: () => Promise<void>;
|
|
311
|
-
/**
|
|
312
|
-
* True while this row is archiving, including a category-wide Clear All. After an
|
|
313
|
-
* archive the server confirmed, it stays true until the row leaves the active list.
|
|
314
|
-
*/
|
|
315
125
|
isArchivePending?: boolean;
|
|
316
126
|
}
|
|
317
|
-
/**
|
|
318
|
-
* What a list hands its `renderError` slot when its read failed. `hasRows` is true when
|
|
319
|
-
* rows from an earlier read are still on screen above the slot (a failed refresh);
|
|
320
|
-
* `retry` re-reads the displayed page and `retrying` is that read's pending flag.
|
|
321
|
-
*/
|
|
322
127
|
export interface NotificationListErrorState {
|
|
323
128
|
error: Error;
|
|
324
129
|
retry: () => void;
|
|
325
130
|
retrying: boolean;
|
|
326
131
|
hasRows: boolean;
|
|
327
132
|
}
|
|
328
|
-
export interface NotificationListProps {
|
|
329
|
-
config: NotificationSystemConfig;
|
|
330
|
-
userId: string;
|
|
331
|
-
category: string;
|
|
332
|
-
/**
|
|
333
|
-
* Archives one rendered notification. Passed straight through to
|
|
334
|
-
* `useArchiveNotification`; the app wires it to its callable.
|
|
335
|
-
*/
|
|
336
|
-
archiveFn: NotificationArchiveFn;
|
|
337
|
-
/**
|
|
338
|
-
* Adapter that ENQUEUES one server-owned archive-all job scoped to `category` and returns its
|
|
339
|
-
* `jobId`. Passed straight through to `useArchiveAllNotifications`; the app wires it to its
|
|
340
|
-
* enqueue callable. Per-tab scoping: the category flows through to the job.
|
|
341
|
-
*/
|
|
342
|
-
enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;
|
|
343
|
-
/**
|
|
344
|
-
* Adapter that returns the current status snapshot for an archive-all `jobId`. Passed straight
|
|
345
|
-
* through to `useArchiveAllNotifications`, which polls it until the job is terminal; the app wires
|
|
346
|
-
* it to its status callable / job-doc read.
|
|
347
|
-
*/
|
|
348
|
-
getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;
|
|
349
|
-
/** Left-aligned title for the active-list header. */
|
|
350
|
-
title?: ReactNode;
|
|
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;
|
|
357
|
-
/** Re-read the displayed page every `refetchInterval` ms while mounted (default 30s). */
|
|
358
|
-
refetchInterval?: number;
|
|
359
|
-
/** How long a read page counts as fresh, in ms (default 30s). */
|
|
360
|
-
staleTime?: number;
|
|
361
|
-
/** Empty-state text, shown only for an answered, empty first page. */
|
|
362
|
-
emptyText?: string;
|
|
363
|
-
/** The failed-read state, shown before (never instead of) the pager. Copy is the app's. */
|
|
364
|
-
renderError: (state: NotificationListErrorState) => ReactNode;
|
|
365
|
-
/**
|
|
366
|
-
* Called with the rows on screen each time the displayed page's answered rows change
|
|
367
|
-
* (not while a page is loading or failed with nothing shown) — the rows to mark seen.
|
|
368
|
-
*/
|
|
369
|
-
onRenderedRowsChange?: (rows: readonly NotificationDoc[]) => void;
|
|
370
|
-
/**
|
|
371
|
-
* Per-row control slot. The row itself is inert; the consumer renders the row's
|
|
372
|
-
* controls here — typically an ArrowRight "go to" (which may call `actions.archive`
|
|
373
|
-
* then navigate) plus a clear/archive button (calls `actions.archive`). Omit to render
|
|
374
|
-
* a row with no controls.
|
|
375
|
-
*/
|
|
376
|
-
renderRowAction?: (notification: NotificationDoc, actions: NotificationRowActions) => ReactNode;
|
|
377
|
-
}
|
|
378
|
-
export interface NotificationHistoryListProps {
|
|
379
|
-
config: NotificationSystemConfig;
|
|
380
|
-
userId: string;
|
|
381
|
-
category: string;
|
|
382
|
-
pageSize?: number;
|
|
383
|
-
staleTime?: number;
|
|
384
|
-
/** Empty-state text, shown only for an answered, empty first page. */
|
|
385
|
-
emptyText?: string;
|
|
386
|
-
/** The failed-read state, shown before (never instead of) the pager. Copy is the app's. */
|
|
387
|
-
renderError: (state: NotificationListErrorState) => ReactNode;
|
|
388
|
-
/** Left-aligned title for the read-only archived-list header. */
|
|
389
|
-
title?: ReactNode;
|
|
390
|
-
/**
|
|
391
|
-
* Per-row control slot for archived rows. The row is inert; archived rows are
|
|
392
|
-
* read-only (no re-archive), so `actions.archive` is absent — the consumer
|
|
393
|
-
* typically renders only an ArrowRight "go to". Omit for a row with no controls.
|
|
394
|
-
*/
|
|
395
|
-
renderRowAction?: (notification: NotificationHistoryItem, actions: NotificationRowActions) => ReactNode;
|
|
396
|
-
}
|
|
397
133
|
export interface NotificationEmptyStateProps {
|
|
398
134
|
text?: string;
|
|
399
135
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
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 /**\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"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface NotificationDoc {\n id: string;\n type: string;\n dedupKey: string;\n category: string;\n\n targetUserId: string | null;\n\n title: string;\n message: string;\n\n count: number;\n latestActorIds: string[];\n\n targetPath?: string;\n metadata: Record<string, unknown>;\n\n /** `0` when unseen and never absent: a `count()` equality predicate cannot match a missing field. */\n seenAt: number;\n\n activityGeneration?: string;\n seenAtGeneration?: string;\n\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface PendingNotification {\n id: string;\n type: string;\n category: string;\n targetUserId: string | null;\n actorId: string;\n metadata: Record<string, unknown>;\n createdAt: number;\n}\n\nexport interface NotificationCategoryConfig {\n activePath: string;\n historyPath: (userId?: string) => string;\n audienceType: 'personal' | 'shared';\n}\n\nexport interface NotificationTypeConfig {\n category: string;\n delivery: 'realtime' | 'queued';\n dedupKeyPattern: (metadata: Record<string, unknown>) => string;\n titlePattern: (metadata: Record<string, unknown>) => string;\n messagePattern: (metadata: Record<string, unknown>, count: number) => string;\n defaultTargetPath?: string | ((metadata: Record<string, unknown>) => string);\n countCap?: number;\n actorCap?: number;\n icon?: string;\n}\n\nexport interface NotificationSystemConfig {\n categories: Record<string, NotificationCategoryConfig>;\n types: Record<string, NotificationTypeConfig>;\n batchIntervalMinutes?: number;\n pendingCollectionPath?: string;\n deliveriesCollectionPath?: string;\n /** Required by the ledger: Firestore TTL acts only on `Timestamp` fields, never a number. */\n timestampFromMillis?: (ms: number) => unknown;\n deliveryTtlMs?: number;\n maxDeliveryAttempts?: number;\n}\n\nexport interface UseActiveNotificationsOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n refetchInterval?: number;\n staleTime?: number;\n}\n\nexport interface NotificationHistoryItem extends NotificationDoc {\n archiveOccurrenceId: string;\n archivedAt: number;\n}\n\nexport interface UseNotificationHistoryOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n pageSize?: number;\n staleTime?: number;\n}\n\nexport interface UseUnreadCountOptions {\n config: NotificationSystemConfig;\n userId: string;\n category: string;\n enabled?: boolean;\n refetchInterval?: number;\n countLimit?: number;\n}\n\nexport interface NotificationArchiveResult {\n archived: boolean;\n}\n\nexport type NotificationArchiveFn = (\n notification: NotificationDoc,\n) => Promise<NotificationArchiveResult>;\n\nexport interface UseArchiveNotificationOptions {\n userId: string;\n category: string;\n archiveFn: NotificationArchiveFn;\n invalidateKeys?: readonly unknown[][];\n}\n\nexport type ArchiveAllJobStatus =\n 'idle' | 'enqueuing' | 'in-progress' | 'complete' | 'incomplete' | 'failed';\n\nexport interface EnqueueArchiveAllResult {\n jobId: string;\n}\n\nexport interface ArchiveAllJobSnapshot {\n state: 'in-progress' | 'complete' | 'incomplete' | 'failed';\n category: string;\n archived?: number;\n notArchived?: number;\n error?: string;\n}\n\nexport interface ArchiveAllPollResult {\n status: Extract<ArchiveAllJobStatus, 'complete' | 'incomplete' | 'failed'>;\n category: string;\n archived: number;\n notArchived: number;\n complete: boolean;\n error?: string;\n}\n\nexport interface UseArchiveAllNotificationsOptions {\n userId: string;\n category: string;\n enqueueArchiveAllFn: (category: string) => Promise<EnqueueArchiveAllResult>;\n getArchiveAllStatusFn: (jobId: string) => Promise<ArchiveAllJobSnapshot>;\n pollIntervalMs?: number;\n maxPolls?: number;\n invalidateKeys?: readonly unknown[][];\n}\n\nexport interface NotificationRowActions {\n archive?: () => Promise<void>;\n isArchivePending?: boolean;\n}\n\nexport interface NotificationListErrorState {\n error: Error;\n retry: () => void;\n retrying: boolean;\n hasRows: boolean;\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.18",
|
|
4
4
|
"description": "Shared notification system for TTT Productions apps — active/history two-tier architecture with dedup, batch processing, and themed UI components",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@tanstack/react-query": ">=5.0.0",
|
|
45
|
-
"@ttt-productions/query-core": "^0.14.
|
|
46
|
-
"@ttt-productions/ui-core": "^0.11.
|
|
45
|
+
"@ttt-productions/query-core": "^0.14.23",
|
|
46
|
+
"@ttt-productions/ui-core": "^0.11.35",
|
|
47
47
|
"firebase": ">=12.0.0",
|
|
48
48
|
"react": ">=19.0.0",
|
|
49
49
|
"react-dom": ">=19.0.0"
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@tanstack/react-query": "^5.0.0",
|
|
73
|
-
"@ttt-productions/query-core": "^0.14.
|
|
74
|
-
"@ttt-productions/ui-core": "^0.11.
|
|
73
|
+
"@ttt-productions/query-core": "^0.14.23",
|
|
74
|
+
"@ttt-productions/ui-core": "^0.11.35",
|
|
75
75
|
"@types/react": "^19.0.0",
|
|
76
76
|
"@types/react-dom": "^19.0.0",
|
|
77
77
|
"firebase": "^12.14.0",
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/* ============================================================================
|
|
2
|
-
* @ttt-productions/notification-core — Notification Styles
|
|
3
|
-
*
|
|
4
|
-
* Uses theme-core CSS variables for automatic light/dark/high-contrast support.
|
|
5
|
-
* All classes use the `ntf-` prefix to avoid collisions.
|
|
6
|
-
* Apps can override any class for custom branding.
|
|
7
|
-
* ============================================================================ */
|
|
8
1
|
|
|
9
2
|
/* === NOTIFICATION LIST === */
|
|
10
3
|
.ntf-list {
|
|
@@ -13,7 +6,6 @@
|
|
|
13
6
|
max-height: 24rem;
|
|
14
7
|
}
|
|
15
8
|
|
|
16
|
-
/* Header stays pinned; only the body scrolls. */
|
|
17
9
|
.ntf-list-body {
|
|
18
10
|
flex: 1;
|
|
19
11
|
min-height: 0;
|
|
@@ -62,9 +54,6 @@
|
|
|
62
54
|
}
|
|
63
55
|
|
|
64
56
|
/* === NOTIFICATION ITEM === */
|
|
65
|
-
/* The row itself is inert — every affordance is a control in its action slot — so it wears no
|
|
66
|
-
* pointer cursor. Its hover tint helps the eye along a long list; it moves on theme-core's motion
|
|
67
|
-
* tokens, so an app's reduced-motion kill switch covers it. */
|
|
68
57
|
.ntf-item {
|
|
69
58
|
display: flex;
|
|
70
59
|
align-items: flex-start;
|
|
@@ -127,9 +116,6 @@
|
|
|
127
116
|
align-self: center;
|
|
128
117
|
}
|
|
129
118
|
|
|
130
|
-
/* Sibling action slot next to the archive control — consumer-supplied via
|
|
131
|
-
* `renderRowAction`. Purely a layout/isolation wrapper; the consumer's node
|
|
132
|
-
* supplies its own visual treatment. */
|
|
133
119
|
.ntf-item-row-action {
|
|
134
120
|
flex-shrink: 0;
|
|
135
121
|
align-self: center;
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { NotificationList } from './NotificationList.js';
|
|
2
|
-
export { NotificationHistoryList } from './NotificationHistoryList.js';
|
|
3
|
-
export { NotificationEmptyState } from './NotificationEmptyState.js';
|
|
4
|
-
export { NotificationUnreadBadge } from './NotificationUnreadBadge.js';
|
|
5
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { NotificationList } from './NotificationList.js';
|
|
2
|
-
export { NotificationHistoryList } from './NotificationHistoryList.js';
|
|
3
|
-
export { NotificationEmptyState } from './NotificationEmptyState.js';
|
|
4
|
-
export { NotificationUnreadBadge } from './NotificationUnreadBadge.js';
|
|
5
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC","sourcesContent":["export { NotificationList } from './NotificationList.js';\nexport { NotificationHistoryList } from './NotificationHistoryList.js';\nexport { NotificationEmptyState } from './NotificationEmptyState.js';\nexport { NotificationUnreadBadge } from './NotificationUnreadBadge.js';\n"]}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { useActiveNotifications } from './useActiveNotifications.js';
|
|
2
|
-
export { useNotificationHistory } from './useNotificationHistory.js';
|
|
3
|
-
export { useUnreadCount } from './useUnreadCount.js';
|
|
4
|
-
export { useArchiveNotification } from './useArchiveNotification.js';
|
|
5
|
-
export { useArchiveAllNotifications } from './useArchiveAllNotifications.js';
|
|
6
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC"}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { useActiveNotifications } from './useActiveNotifications.js';
|
|
2
|
-
export { useNotificationHistory } from './useNotificationHistory.js';
|
|
3
|
-
export { useUnreadCount } from './useUnreadCount.js';
|
|
4
|
-
export { useArchiveNotification } from './useArchiveNotification.js';
|
|
5
|
-
export { useArchiveAllNotifications } from './useArchiveAllNotifications.js';
|
|
6
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC","sourcesContent":["export { useActiveNotifications } from './useActiveNotifications.js';\nexport { useNotificationHistory } from './useNotificationHistory.js';\nexport { useUnreadCount } from './useUnreadCount.js';\nexport { useArchiveNotification } from './useArchiveNotification.js';\nexport { useArchiveAllNotifications } from './useArchiveAllNotifications.js';\n"]}
|