-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathuseClientNotifications.ts
More file actions
211 lines (181 loc) · 7.25 KB
/
Copy pathuseClientNotifications.ts
File metadata and controls
211 lines (181 loc) · 7.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @object-ui/react - useClientNotifications
*
* Bridge hook between the @objectstack/client notifications API
* and the local NotificationContext state. Fetches server-side
* notifications and surfaces them through the existing provider.
*
* ADR-0030 (Notification Convergence): the `client.notifications.*` helpers
* are the stable transport contract — the server routes them to the L5
* `sys_inbox_message` materialization and the `sys_notification_receipt`
* read-state spine (the re-modeled `sys_notification` L2 event carries no
* recipient/read columns). This hook only needs to call the SDK with its
* current signatures: `list(options)`, `markRead(ids[])`, `markAllRead()`.
*
* The former registerDevice/getPreferences/updatePreferences delegates were
* removed (objectstack#3612): the SDK deleted those methods because the
* /notifications/devices and /notifications/preferences server routes were
* never built — every call was a guaranteed error. They return when the
* server side exists.
*/
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { SchemaRendererContext } from '../context/SchemaRendererContext';
import { useNotifications } from '../context/NotificationContext';
import type { NotificationSeverityLevel } from '../context/NotificationContext';
/* ------------------------------------------------------------------ */
/* Public types */
/* ------------------------------------------------------------------ */
export interface UseClientNotificationsOptions {
/** ObjectStack client instance (optional, from context if available) */
client?: any;
/** Whether to auto-fetch notifications on mount */
autoFetch?: boolean;
/** Polling interval in ms (0 to disable) */
pollInterval?: number;
}
export interface UseClientNotificationsResult {
/** Fetch notifications from server */
fetchNotifications: () => Promise<void>;
/** Mark a notification as read on the server */
markAsRead: (id: string) => Promise<void>;
/** Loading state */
loading: boolean;
/** Error state */
error: Error | null;
}
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
/** Normalise severity coming from the server into a valid local level. */
function toSeverity(value: unknown): NotificationSeverityLevel {
const valid: NotificationSeverityLevel[] = ['info', 'success', 'warning', 'error'];
if (typeof value === 'string' && valid.includes(value as NotificationSeverityLevel)) {
return value as NotificationSeverityLevel;
}
return 'info';
}
/**
* Resolve the ObjectStack client from explicit option or SchemaRendererContext.
*
* The dataSource stored in context is typically an ObjectStackAdapter which
* exposes `getClient()`. When the caller passes a client directly we use
* that instead.
*/
function useResolvedClient(explicit?: any): any {
const rendererCtx = useContext(SchemaRendererContext);
if (explicit) return explicit;
const dataSource = rendererCtx?.dataSource;
if (dataSource && typeof dataSource.getClient === 'function') {
return dataSource.getClient();
}
return null;
}
/* ------------------------------------------------------------------ */
/* Hook */
/* ------------------------------------------------------------------ */
/**
* Bridge between the `@objectstack/client` notifications API and the
* local {@link NotificationContext}.
*
* The hook resolves the ObjectStack client either from the explicit
* `client` option or from the `SchemaRendererContext` dataSource, then
* delegates to `client.notifications.*` methods. Fetched notifications
* are fed into the local provider via `useNotifications().notify()`.
*
* @example
* ```tsx
* function NotificationBell() {
* const { fetchNotifications, loading } = useClientNotifications({
* autoFetch: true,
* pollInterval: 30_000,
* });
*
* return <Button onClick={fetchNotifications} disabled={loading}>🔔</Button>;
* }
* ```
*/
export function useClientNotifications(
options: UseClientNotificationsOptions = {},
): UseClientNotificationsResult {
const { client: explicitClient, autoFetch = false, pollInterval = 0 } = options;
const client = useResolvedClient(explicitClient);
const { notify, markAsRead: localMarkAsRead } = useNotifications();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Track IDs that have already been pushed into the local context so we
// don't add duplicates on subsequent fetches / polls.
const seenIdsRef = useRef<Set<string>>(new Set());
/* ---- fetchNotifications ---- */
const fetchNotifications = useCallback(async () => {
if (!client?.notifications) return;
setLoading(true);
setError(null);
try {
const result = await client.notifications.list({});
const items: any[] = Array.isArray(result) ? result : result?.data ?? result?.items ?? [];
for (const item of items) {
const id = String(item.id ?? '');
if (!id || seenIdsRef.current.has(id)) continue;
seenIdsRef.current.add(id);
notify({
title: item.title ?? 'Notification',
message: item.message ?? item.body,
severity: toSeverity(item.severity ?? item.level),
duration: 0, // server-driven notifications are persistent
});
}
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to fetch notifications'));
} finally {
setLoading(false);
}
}, [client, notify]);
/* ---- markAsRead (server + local) ---- */
const markAsRead = useCallback(
async (id: string) => {
// Optimistically mark locally first
localMarkAsRead(id);
if (!client?.notifications) return;
setError(null);
try {
// ADR-0030 / @objectstack/client v7+: the SDK exposes `markRead(ids[])`
// (batch) — there is no single-id `markAsRead`. Server-side this writes
// the `read` state to the `sys_notification_receipt` spine. We keep the
// friendly single-id hook API and adapt to the batch call.
await client.notifications.markRead([id]);
} catch (err) {
const wrapped = err instanceof Error ? err : new Error('Failed to mark as read');
setError(wrapped);
throw wrapped;
}
},
[client, localMarkAsRead],
);
/* ---- auto-fetch on mount ---- */
useEffect(() => {
if (autoFetch && client?.notifications) {
fetchNotifications();
}
}, [autoFetch, client, fetchNotifications]);
/* ---- polling ---- */
useEffect(() => {
if (!pollInterval || pollInterval <= 0 || !client?.notifications) return;
const id = setInterval(() => {
fetchNotifications();
}, pollInterval);
return () => clearInterval(id);
}, [pollInterval, client, fetchNotifications]);
return {
fetchNotifications,
markAsRead,
loading,
error,
};
}