@ttt-productions/notification-core 0.4.1 → 0.4.3
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 +30 -138
- package/dist/react/components/NotificationList.d.ts +1 -1
- package/dist/react/components/NotificationList.d.ts.map +1 -1
- package/dist/react/components/NotificationList.js +7 -20
- package/dist/react/components/NotificationList.js.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.d.ts +10 -12
- package/dist/react/hooks/useArchiveAllNotifications.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveAllNotifications.js +10 -58
- package/dist/react/hooks/useArchiveAllNotifications.js.map +1 -1
- package/dist/react/hooks/useArchiveNotification.d.ts +12 -11
- package/dist/react/hooks/useArchiveNotification.d.ts.map +1 -1
- package/dist/react/hooks/useArchiveNotification.js +12 -39
- package/dist/react/hooks/useArchiveNotification.js.map +1 -1
- package/dist/react/hooks/useUnreadCount.d.ts +30 -21
- package/dist/react/hooks/useUnreadCount.d.ts.map +1 -1
- package/dist/react/hooks/useUnreadCount.js +29 -15
- package/dist/react/hooks/useUnreadCount.js.map +1 -1
- package/dist/server/archiveNotificationHelper.d.ts +14 -3
- package/dist/server/archiveNotificationHelper.d.ts.map +1 -1
- package/dist/server/archiveNotificationHelper.js +35 -7
- package/dist/server/archiveNotificationHelper.js.map +1 -1
- package/dist/server/createNotificationHelper.d.ts +1 -1
- package/dist/server/createNotificationHelper.d.ts.map +1 -1
- package/dist/server/createNotificationHelper.js +35 -13
- package/dist/server/createNotificationHelper.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/markSeenHelper.d.ts +17 -0
- package/dist/server/markSeenHelper.d.ts.map +1 -0
- package/dist/server/markSeenHelper.js +29 -0
- package/dist/server/markSeenHelper.js.map +1 -0
- package/dist/server/processBatchHelper.d.ts.map +1 -1
- package/dist/server/processBatchHelper.js +12 -15
- package/dist/server/processBatchHelper.js.map +1 -1
- package/dist/server/types.d.ts +7 -3
- package/dist/server/types.d.ts.map +1 -1
- package/dist/types.d.ts +45 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +73 -73
package/README.md
CHANGED
|
@@ -1,156 +1,48 @@
|
|
|
1
1
|
# @ttt-productions/notification-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generic notification system for TTT Productions apps — a two-tier
|
|
4
|
+
**active → history** model with dedup, batch processing, and themed React UI.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## Entrypoints
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
## Features
|
|
8
|
+
- `@ttt-productions/notification-core` — root-exported shared types.
|
|
9
|
+
- `@ttt-productions/notification-core/react` — React hooks/components.
|
|
10
|
+
- `@ttt-productions/notification-core/server` — backend helper utilities.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
- 📊 Unread count tracking
|
|
15
|
-
- ✅ Mark as read functionality (single or batch)
|
|
16
|
-
- 🎯 Type-safe notification system with TypeScript
|
|
17
|
-
- 🔥 Built on `@ttt-productions/query-core` for optimal caching
|
|
12
|
+
Backend/Functions code should avoid the `/react` subpath.
|
|
18
13
|
|
|
19
|
-
##
|
|
14
|
+
## Model
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
function NotificationList() {
|
|
27
|
-
const { data: notifications, isLoading } = useNotifications({
|
|
28
|
-
userId: currentUser.uid,
|
|
29
|
-
subscribe: true, // Real-time updates
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
if (isLoading) return <div>Loading...</div>;
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<ul>
|
|
36
|
-
{notifications?.map((notification) => (
|
|
37
|
-
<li key={notification.id}>
|
|
38
|
-
{notification.title} - {notification.message}
|
|
39
|
-
</li>
|
|
40
|
-
))}
|
|
41
|
-
</ul>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
```
|
|
16
|
+
There is **no `isRead` flag** — existence in the active collection means unread.
|
|
17
|
+
Archiving moves a notification from the active collection to history (carrying an
|
|
18
|
+
`ArchivalInfo` audit trail). Duplicate triggers for the same `dedupKey`
|
|
19
|
+
increment a single active doc's `count` and append the actor.
|
|
45
20
|
|
|
46
|
-
###
|
|
47
|
-
|
|
48
|
-
```tsx
|
|
49
|
-
import { useUnreadCount } from '@ttt-productions/notification-core';
|
|
50
|
-
|
|
51
|
-
function NotificationBadge() {
|
|
52
|
-
const { data: unreadCount } = useUnreadCount({
|
|
53
|
-
userId: currentUser.uid,
|
|
54
|
-
subscribe: true,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
return <span className="badge">{unreadCount}</span>;
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Mark as Read
|
|
62
|
-
|
|
63
|
-
```tsx
|
|
64
|
-
import { useMarkAsRead } from '@ttt-productions/notification-core';
|
|
21
|
+
### Identity is id-only
|
|
65
22
|
|
|
66
|
-
|
|
67
|
-
|
|
23
|
+
Shared notification docs persist actor **ids only** — `NotificationDoc`
|
|
24
|
+
exposes `latestActorIds`, and `PendingNotification` exposes `actorId`. There are
|
|
25
|
+
**no persisted display names** (no `latestActorNames` / `actorName`); names are
|
|
26
|
+
resolved at read time by the consuming app (e.g. from `publicUsers`) so they
|
|
27
|
+
never drift in the stored doc.
|
|
68
28
|
|
|
69
|
-
|
|
70
|
-
markAsRead.mutate({ notificationId: notification.id });
|
|
71
|
-
// Navigate to target
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
return <div onClick={handleClick}>{notification.message}</div>;
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### Mark All as Read
|
|
29
|
+
## Usage
|
|
79
30
|
|
|
80
31
|
```tsx
|
|
81
|
-
import {
|
|
82
|
-
|
|
83
|
-
function NotificationPanel() {
|
|
84
|
-
const { data: notifications } = useNotifications({
|
|
85
|
-
userId: currentUser.uid,
|
|
86
|
-
unreadOnly: true,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const markAllAsRead = useMarkAllAsRead({ userId: currentUser.uid });
|
|
90
|
-
|
|
91
|
-
const handleMarkAllRead = () => {
|
|
92
|
-
const unreadIds = notifications
|
|
93
|
-
?.filter((n) => !n.isRead)
|
|
94
|
-
.map((n) => n.id) ?? [];
|
|
95
|
-
|
|
96
|
-
if (unreadIds.length > 0) {
|
|
97
|
-
markAllAsRead.mutate({ notificationIds: unreadIds });
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
return <button onClick={handleMarkAllRead}>Mark All Read</button>;
|
|
102
|
-
}
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Data Structure
|
|
106
|
-
|
|
107
|
-
Notifications are stored in Firestore at:
|
|
108
|
-
```
|
|
109
|
-
userData/{userId}/metadata/notifications/{notificationId}
|
|
110
|
-
```
|
|
32
|
+
import { useActiveNotifications } from '@ttt-productions/notification-core/react';
|
|
111
33
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
title: string;
|
|
118
|
-
message: string;
|
|
119
|
-
targetPath: string; // Navigation target
|
|
120
|
-
targetParams?: Record<string, any>;
|
|
121
|
-
isRead: boolean;
|
|
122
|
-
createdAt: Timestamp;
|
|
123
|
-
metadata?: Record<string, any>; // Type-specific data
|
|
124
|
-
}
|
|
34
|
+
const { data: notifications, isLoading } = useActiveNotifications({
|
|
35
|
+
config: TTT_NOTIFICATION_CONFIG,
|
|
36
|
+
userId: currentUser.uid,
|
|
37
|
+
category: 'user',
|
|
38
|
+
});
|
|
125
39
|
```
|
|
126
40
|
|
|
127
|
-
|
|
41
|
+
Backend create/queue (id-only — pass `actorId`, never a name):
|
|
128
42
|
|
|
129
|
-
|
|
43
|
+
```ts
|
|
44
|
+
import { createNotificationHelper } from '@ttt-productions/notification-core/server';
|
|
130
45
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
import { onCall } from 'firebase-functions/v2/https';
|
|
134
|
-
import { getFirestore, Timestamp } from 'firebase-admin/firestore';
|
|
135
|
-
|
|
136
|
-
export const createNotification = onCall(async (request) => {
|
|
137
|
-
const { userId, type, title, message, targetPath, targetParams, metadata } = request.data;
|
|
138
|
-
|
|
139
|
-
await getFirestore()
|
|
140
|
-
.collection('userData')
|
|
141
|
-
.doc(userId)
|
|
142
|
-
.collection('metadata')
|
|
143
|
-
.doc('notifications')
|
|
144
|
-
.collection('notifications')
|
|
145
|
-
.add({
|
|
146
|
-
type,
|
|
147
|
-
title,
|
|
148
|
-
message,
|
|
149
|
-
targetPath,
|
|
150
|
-
targetParams,
|
|
151
|
-
isRead: false,
|
|
152
|
-
createdAt: Timestamp.now(),
|
|
153
|
-
metadata,
|
|
154
|
-
});
|
|
155
|
-
});
|
|
46
|
+
const notifier = createNotificationHelper(db, TTT_NOTIFICATION_CONFIG);
|
|
47
|
+
await notifier.send({ type: 'content_report', actorId, metadata: { itemId } });
|
|
156
48
|
```
|
|
@@ -2,5 +2,5 @@ import type { NotificationListProps } from '../../types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Scrollable list of active notifications with click-to-archive and clear-all.
|
|
4
4
|
*/
|
|
5
|
-
export declare function NotificationList({ config, userId, category, onNotificationClick, onClearAll, refetchInterval,
|
|
5
|
+
export declare function NotificationList({ config, userId, category, onNotificationClick, archiveFn, archiveAllFn, onClearAll, refetchInterval, emptyText, }: NotificationListProps): import("react/jsx-runtime").JSX.Element;
|
|
6
6
|
//# sourceMappingURL=NotificationList.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAE7E;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,
|
|
1
|
+
{"version":3,"file":"NotificationList.d.ts","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAE7E;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,SAAS,EACT,YAAY,EACZ,UAAU,EACV,eAAe,EACf,SAAS,GACV,EAAE,qBAAqB,2CA8HvB"}
|
|
@@ -11,7 +11,7 @@ import { formatRelativeTime } from './relative-time.js';
|
|
|
11
11
|
/**
|
|
12
12
|
* Scrollable list of active notifications with click-to-archive and clear-all.
|
|
13
13
|
*/
|
|
14
|
-
export function NotificationList({ config, userId, category, onNotificationClick,
|
|
14
|
+
export function NotificationList({ config, userId, category, onNotificationClick, archiveFn, archiveAllFn, onClearAll, refetchInterval, emptyText, }) {
|
|
15
15
|
const { data: notifications, isLoading, hasNextPage, nextPage, } = useActiveNotifications({
|
|
16
16
|
config,
|
|
17
17
|
userId,
|
|
@@ -25,46 +25,33 @@ export function NotificationList({ config, userId, category, onNotificationClick
|
|
|
25
25
|
refetchInterval,
|
|
26
26
|
});
|
|
27
27
|
const archiveMutation = useArchiveNotification({
|
|
28
|
-
config,
|
|
29
28
|
userId,
|
|
30
29
|
category,
|
|
30
|
+
archiveFn,
|
|
31
31
|
});
|
|
32
32
|
const archiveAllMutation = useArchiveAllNotifications({
|
|
33
|
-
config,
|
|
34
33
|
userId,
|
|
35
34
|
category,
|
|
35
|
+
archiveAllFn,
|
|
36
36
|
});
|
|
37
37
|
const handleNotificationClick = useCallback(async (notification) => {
|
|
38
38
|
try {
|
|
39
|
-
await archiveMutation.mutateAsync(
|
|
40
|
-
notificationId: notification.id,
|
|
41
|
-
archivalInfo: {
|
|
42
|
-
archivedBy: userId,
|
|
43
|
-
archivedAt: Date.now(),
|
|
44
|
-
device,
|
|
45
|
-
},
|
|
46
|
-
});
|
|
39
|
+
await archiveMutation.mutateAsync(notification.id);
|
|
47
40
|
}
|
|
48
41
|
catch {
|
|
49
42
|
// Still navigate even if archive fails
|
|
50
43
|
}
|
|
51
44
|
onNotificationClick(notification);
|
|
52
|
-
}, [archiveMutation,
|
|
45
|
+
}, [archiveMutation, onNotificationClick]);
|
|
53
46
|
const handleClearAll = useCallback(async () => {
|
|
54
47
|
try {
|
|
55
|
-
await archiveAllMutation.mutateAsync(
|
|
56
|
-
archivalInfo: {
|
|
57
|
-
archivedBy: userId,
|
|
58
|
-
archivedAt: Date.now(),
|
|
59
|
-
device,
|
|
60
|
-
},
|
|
61
|
-
});
|
|
48
|
+
await archiveAllMutation.mutateAsync();
|
|
62
49
|
}
|
|
63
50
|
catch {
|
|
64
51
|
// Silently fail
|
|
65
52
|
}
|
|
66
53
|
onClearAll?.();
|
|
67
|
-
}, [archiveAllMutation,
|
|
54
|
+
}, [archiveAllMutation, onClearAll]);
|
|
68
55
|
const getTypeIcon = useCallback((type) => {
|
|
69
56
|
const typeConfig = config.types[type];
|
|
70
57
|
return typeConfig?.icon ?? '🔔';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxD;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,mBAAmB,EACnB,UAAU,EACV,eAAe,EACf,
|
|
1
|
+
{"version":3,"file":"NotificationList.js","sourceRoot":"","sources":["../../../src/react/components/NotificationList.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { UseArchiveAllNotificationsOptions } from '../../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Archive
|
|
4
|
-
*
|
|
3
|
+
* Archive the caller's whole category through an app-supplied callable adapter.
|
|
4
|
+
*
|
|
5
|
+
* Like {@link useArchiveNotification}, this performs no client Firestore writes
|
|
6
|
+
* — paging + ownership-checked deletion happen server-side via the callable the
|
|
7
|
+
* app wires into `archiveAllFn`. On success it invalidates the read keys.
|
|
5
8
|
*
|
|
6
9
|
* @example
|
|
7
10
|
* ```tsx
|
|
11
|
+
* const archiveNotification = httpsCallable(functions, 'archiveNotification');
|
|
8
12
|
* const archiveAll = useArchiveAllNotifications({
|
|
9
|
-
* config: TTT_NOTIFICATION_CONFIG,
|
|
10
13
|
* userId: user.uid,
|
|
11
14
|
* category: 'user',
|
|
15
|
+
* archiveAllFn: () => archiveNotification({ category: 'user', scope: { kind: 'all' } }),
|
|
12
16
|
* });
|
|
13
17
|
*
|
|
14
|
-
* archiveAll.mutate(
|
|
15
|
-
* archivalInfo: { archivedBy: user.uid, archivedAt: Date.now(), device: 'web' },
|
|
16
|
-
* });
|
|
18
|
+
* archiveAll.mutate();
|
|
17
19
|
* ```
|
|
18
20
|
*/
|
|
19
|
-
export declare function useArchiveAllNotifications({
|
|
20
|
-
totalArchived: number;
|
|
21
|
-
}, Error, {
|
|
22
|
-
archivalInfo: ArchivalInfo;
|
|
23
|
-
}, unknown>;
|
|
21
|
+
export declare function useArchiveAllNotifications({ userId, category, archiveAllFn, invalidateKeys, }: UseArchiveAllNotificationsOptions): import("@tanstack/react-query").UseMutationResult<unknown, Error, void, unknown>;
|
|
24
22
|
//# sourceMappingURL=useArchiveAllNotifications.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useArchiveAllNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useArchiveAllNotifications.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,gBAAgB,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,cAAc,GACf,EAAE,iCAAiC,oFAkBnC"}
|
|
@@ -1,81 +1,33 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
3
|
-
import { collection, query, where, orderBy, limit, getDocs, doc, writeBatch, } from 'firebase/firestore';
|
|
4
|
-
import { useFirestoreDb } from '@ttt-productions/query-core/react';
|
|
5
|
-
const BATCH_SIZE = 50;
|
|
6
3
|
/**
|
|
7
|
-
* Archive
|
|
8
|
-
*
|
|
4
|
+
* Archive the caller's whole category through an app-supplied callable adapter.
|
|
5
|
+
*
|
|
6
|
+
* Like {@link useArchiveNotification}, this performs no client Firestore writes
|
|
7
|
+
* — paging + ownership-checked deletion happen server-side via the callable the
|
|
8
|
+
* app wires into `archiveAllFn`. On success it invalidates the read keys.
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* ```tsx
|
|
12
|
+
* const archiveNotification = httpsCallable(functions, 'archiveNotification');
|
|
12
13
|
* const archiveAll = useArchiveAllNotifications({
|
|
13
|
-
* config: TTT_NOTIFICATION_CONFIG,
|
|
14
14
|
* userId: user.uid,
|
|
15
15
|
* category: 'user',
|
|
16
|
+
* archiveAllFn: () => archiveNotification({ category: 'user', scope: { kind: 'all' } }),
|
|
16
17
|
* });
|
|
17
18
|
*
|
|
18
|
-
* archiveAll.mutate(
|
|
19
|
-
* archivalInfo: { archivedBy: user.uid, archivedAt: Date.now(), device: 'web' },
|
|
20
|
-
* });
|
|
19
|
+
* archiveAll.mutate();
|
|
21
20
|
* ```
|
|
22
21
|
*/
|
|
23
|
-
export function useArchiveAllNotifications({
|
|
24
|
-
const db = useFirestoreDb();
|
|
22
|
+
export function useArchiveAllNotifications({ userId, category, archiveAllFn, invalidateKeys, }) {
|
|
25
23
|
const queryClient = useQueryClient();
|
|
26
|
-
const categoryConfig = config.categories[category];
|
|
27
24
|
const defaultInvalidateKeys = [
|
|
28
25
|
['notifications', 'active', category, userId],
|
|
29
26
|
['notifications', 'unread-count', category, userId],
|
|
30
27
|
['notifications', 'history', category, userId],
|
|
31
28
|
];
|
|
32
29
|
return useMutation({
|
|
33
|
-
mutationFn: async (
|
|
34
|
-
if (!categoryConfig) {
|
|
35
|
-
throw new Error(`[notification-core] Unknown category: ${category}`);
|
|
36
|
-
}
|
|
37
|
-
const activePath = categoryConfig.activePath;
|
|
38
|
-
const historyPath = categoryConfig.historyPath(userId);
|
|
39
|
-
const isPersonal = categoryConfig.audienceType === 'personal';
|
|
40
|
-
let totalArchived = 0;
|
|
41
|
-
let hasMore = true;
|
|
42
|
-
while (hasMore) {
|
|
43
|
-
const constraints = [
|
|
44
|
-
...(isPersonal ? [where('targetUserId', '==', userId)] : []),
|
|
45
|
-
orderBy('updatedAt', 'desc'),
|
|
46
|
-
limit(BATCH_SIZE),
|
|
47
|
-
];
|
|
48
|
-
const q = query(collection(db, activePath), ...constraints);
|
|
49
|
-
const snapshot = await getDocs(q);
|
|
50
|
-
if (snapshot.empty) {
|
|
51
|
-
hasMore = false;
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
const batch = writeBatch(db);
|
|
55
|
-
snapshot.docs.forEach((docSnap) => {
|
|
56
|
-
const data = docSnap.data();
|
|
57
|
-
// Write to history
|
|
58
|
-
const historyRef = doc(db, historyPath, docSnap.id);
|
|
59
|
-
batch.set(historyRef, {
|
|
60
|
-
...data,
|
|
61
|
-
id: docSnap.id,
|
|
62
|
-
archival: archivalInfo,
|
|
63
|
-
...(categoryConfig.audienceType === 'shared'
|
|
64
|
-
? { handledBy: archivalInfo.archivedBy }
|
|
65
|
-
: {}),
|
|
66
|
-
});
|
|
67
|
-
// Delete from active
|
|
68
|
-
const activeRef = doc(db, activePath, docSnap.id);
|
|
69
|
-
batch.delete(activeRef);
|
|
70
|
-
});
|
|
71
|
-
await batch.commit();
|
|
72
|
-
totalArchived += snapshot.docs.length;
|
|
73
|
-
if (snapshot.docs.length < BATCH_SIZE) {
|
|
74
|
-
hasMore = false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return { totalArchived };
|
|
78
|
-
},
|
|
30
|
+
mutationFn: async () => archiveAllFn(),
|
|
79
31
|
onSuccess: () => {
|
|
80
32
|
const keysToInvalidate = invalidateKeys ?? defaultInvalidateKeys;
|
|
81
33
|
keysToInvalidate.forEach((key) => {
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"useArchiveAllNotifications.js","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveAllNotifications.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGpE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,0BAA0B,CAAC,EACzC,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,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,UAAU,EAAE,KAAK,IAAI,EAAE,CAAC,YAAY,EAAE;QACtC,SAAS,EAAE,GAAG,EAAE;YACd,MAAM,gBAAgB,GAAG,cAAc,IAAI,qBAAqB,CAAC;YACjE,gBAAgB,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;gBAC/B,WAAW,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACtE,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { UseArchiveNotificationOptions } from '../../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Archive a single notification
|
|
3
|
+
* Archive a single notification through an app-supplied callable adapter.
|
|
4
|
+
*
|
|
5
|
+
* The notification system is Cloud-Functions-only: clients never write
|
|
6
|
+
* notification docs. This hook performs no Firestore writes — it delegates to
|
|
7
|
+
* `archiveFn` (wired by the app to `httpsCallable(functions, 'archiveNotification')`)
|
|
8
|
+
* and invalidates the read keys on success.
|
|
4
9
|
*
|
|
5
10
|
* @example
|
|
6
11
|
* ```tsx
|
|
12
|
+
* const archiveNotification = httpsCallable(functions, 'archiveNotification');
|
|
7
13
|
* const archive = useArchiveNotification({
|
|
8
|
-
* config: TTT_NOTIFICATION_CONFIG,
|
|
9
14
|
* userId: user.uid,
|
|
10
15
|
* category: 'user',
|
|
16
|
+
* archiveFn: (notificationId) =>
|
|
17
|
+
* archiveNotification({ category: 'user', scope: { kind: 'single', notificationId } }),
|
|
11
18
|
* });
|
|
12
19
|
*
|
|
13
|
-
* archive.mutate(
|
|
14
|
-
* notificationId: 'abc123',
|
|
15
|
-
* archivalInfo: { archivedBy: user.uid, archivedAt: Date.now(), device: 'web' },
|
|
16
|
-
* });
|
|
20
|
+
* archive.mutate('abc123');
|
|
17
21
|
* ```
|
|
18
22
|
*/
|
|
19
|
-
export declare function useArchiveNotification({
|
|
20
|
-
notificationId: string;
|
|
21
|
-
archivalInfo: ArchivalInfo;
|
|
22
|
-
}, unknown>;
|
|
23
|
+
export declare function useArchiveNotification({ userId, category, archiveFn, invalidateKeys, }: UseArchiveNotificationOptions): import("@tanstack/react-query").UseMutationResult<unknown, Error, string, unknown>;
|
|
23
24
|
//# sourceMappingURL=useArchiveNotification.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useArchiveNotification.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveNotification.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useArchiveNotification.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveNotification.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,QAAQ,EACR,SAAS,EACT,cAAc,GACf,EAAE,6BAA6B,sFAkB/B"}
|
|
@@ -1,62 +1,35 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
3
|
-
import { doc, getDoc, setDoc, deleteDoc } from 'firebase/firestore';
|
|
4
|
-
import { useFirestoreDb } from '@ttt-productions/query-core/react';
|
|
5
3
|
/**
|
|
6
|
-
* Archive a single notification
|
|
4
|
+
* Archive a single notification through an app-supplied callable adapter.
|
|
5
|
+
*
|
|
6
|
+
* The notification system is Cloud-Functions-only: clients never write
|
|
7
|
+
* notification docs. This hook performs no Firestore writes — it delegates to
|
|
8
|
+
* `archiveFn` (wired by the app to `httpsCallable(functions, 'archiveNotification')`)
|
|
9
|
+
* and invalidates the read keys on success.
|
|
7
10
|
*
|
|
8
11
|
* @example
|
|
9
12
|
* ```tsx
|
|
13
|
+
* const archiveNotification = httpsCallable(functions, 'archiveNotification');
|
|
10
14
|
* const archive = useArchiveNotification({
|
|
11
|
-
* config: TTT_NOTIFICATION_CONFIG,
|
|
12
15
|
* userId: user.uid,
|
|
13
16
|
* category: 'user',
|
|
17
|
+
* archiveFn: (notificationId) =>
|
|
18
|
+
* archiveNotification({ category: 'user', scope: { kind: 'single', notificationId } }),
|
|
14
19
|
* });
|
|
15
20
|
*
|
|
16
|
-
* archive.mutate(
|
|
17
|
-
* notificationId: 'abc123',
|
|
18
|
-
* archivalInfo: { archivedBy: user.uid, archivedAt: Date.now(), device: 'web' },
|
|
19
|
-
* });
|
|
21
|
+
* archive.mutate('abc123');
|
|
20
22
|
* ```
|
|
21
23
|
*/
|
|
22
|
-
export function useArchiveNotification({
|
|
23
|
-
const db = useFirestoreDb();
|
|
24
|
+
export function useArchiveNotification({ userId, category, archiveFn, invalidateKeys, }) {
|
|
24
25
|
const queryClient = useQueryClient();
|
|
25
|
-
const categoryConfig = config.categories[category];
|
|
26
26
|
const defaultInvalidateKeys = [
|
|
27
27
|
['notifications', 'active', category, userId],
|
|
28
28
|
['notifications', 'unread-count', category, userId],
|
|
29
29
|
['notifications', 'history', category, userId],
|
|
30
30
|
];
|
|
31
31
|
return useMutation({
|
|
32
|
-
mutationFn: async (
|
|
33
|
-
if (!categoryConfig) {
|
|
34
|
-
throw new Error(`[notification-core] Unknown category: ${category}`);
|
|
35
|
-
}
|
|
36
|
-
const activePath = categoryConfig.activePath;
|
|
37
|
-
const historyPath = categoryConfig.historyPath(userId);
|
|
38
|
-
// Read active doc
|
|
39
|
-
const activeRef = doc(db, activePath, notificationId);
|
|
40
|
-
const activeSnap = await getDoc(activeRef);
|
|
41
|
-
if (!activeSnap.exists()) {
|
|
42
|
-
// Already archived or deleted — no-op
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const activeData = { id: activeSnap.id, ...activeSnap.data() };
|
|
46
|
-
// Build history doc
|
|
47
|
-
const historyDoc = {
|
|
48
|
-
...activeData,
|
|
49
|
-
archival: archivalInfo,
|
|
50
|
-
...(categoryConfig.audienceType === 'shared'
|
|
51
|
-
? { handledBy: archivalInfo.archivedBy }
|
|
52
|
-
: {}),
|
|
53
|
-
};
|
|
54
|
-
// Write to history
|
|
55
|
-
const historyRef = doc(db, historyPath, notificationId);
|
|
56
|
-
await setDoc(historyRef, historyDoc);
|
|
57
|
-
// Delete from active
|
|
58
|
-
await deleteDoc(activeRef);
|
|
59
|
-
},
|
|
32
|
+
mutationFn: async (notificationId) => archiveFn(notificationId),
|
|
60
33
|
onSuccess: () => {
|
|
61
34
|
const keysToInvalidate = invalidateKeys ?? defaultInvalidateKeys;
|
|
62
35
|
keysToInvalidate.forEach((key) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useArchiveNotification.js","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveNotification.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"useArchiveNotification.js","sourceRoot":"","sources":["../../../src/react/hooks/useArchiveNotification.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGpE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,sBAAsB,CAAC,EACrC,MAAM,EACN,QAAQ,EACR,SAAS,EACT,cAAc,GACgB;IAC9B,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,UAAU,EAAE,KAAK,EAAE,cAAsB,EAAE,EAAE,CAAC,SAAS,CAAC,cAAc,CAAC;QACvE,SAAS,EAAE,GAAG,EAAE;YACd,MAAM,gBAAgB,GAAG,cAAc,IAAI,qBAAqB,CAAC;YACjE,gBAAgB,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;gBAC/B,WAAW,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACtE,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|