@xnetjs/react 0.0.2 → 0.1.0
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 +206 -2
- package/dist/chunk-6VOICQZ3.js +760 -0
- package/dist/chunk-7OQXRHEQ.js +4649 -0
- package/dist/chunk-EJ5RW5GI.js +93 -0
- package/dist/chunk-IHTMVTTE.js +1108 -0
- package/dist/chunk-JCOFKBOB.js +11 -0
- package/dist/chunk-KSHTDZ2V.js +893 -0
- package/dist/chunk-QHNYQVUM.js +989 -0
- package/dist/context-CFu9i136.d.ts +392 -0
- package/dist/core.d.ts +372 -0
- package/dist/core.js +29 -0
- package/dist/database.d.ts +383 -0
- package/dist/database.js +19 -0
- package/dist/experimental-B2FrBnkV.d.ts +1584 -0
- package/dist/experimental.d.ts +15 -0
- package/dist/experimental.js +248 -0
- package/dist/index.d.ts +601 -2700
- package/dist/index.js +4782 -4427
- package/dist/{instrumentation-Cn94kn8-.d.ts → instrumentation-CpIuG2y5.d.ts} +32 -1
- package/dist/internal.d.ts +32 -22
- package/dist/internal.js +16 -4
- package/dist/telemetry-context-B7r6H1KW.d.ts +35 -0
- package/dist/useNodeStore-DTCSBF51.d.ts +28 -0
- package/dist/useQuery-D7ajycrc.d.ts +302 -0
- package/package.json +28 -10
- package/dist/chunk-CWHCGYDW.js +0 -2238
|
@@ -0,0 +1,4649 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMutate,
|
|
3
|
+
useQuery
|
|
4
|
+
} from "./chunk-6VOICQZ3.js";
|
|
5
|
+
import {
|
|
6
|
+
XNetContext,
|
|
7
|
+
downloadBackup,
|
|
8
|
+
uploadBackup,
|
|
9
|
+
useNodeStore,
|
|
10
|
+
useSecurityContext,
|
|
11
|
+
useXNet
|
|
12
|
+
} from "./chunk-IHTMVTTE.js";
|
|
13
|
+
|
|
14
|
+
// src/hooks/useTaskProjectionSync.ts
|
|
15
|
+
import { ExternalReferenceSchema, TaskSchema, isCompletedTaskStatus } from "@xnetjs/data";
|
|
16
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
17
|
+
var DEFAULT_DEBOUNCE_MS = 250;
|
|
18
|
+
function arraysEqual(a, b) {
|
|
19
|
+
if (!Array.isArray(a)) return b.length === 0;
|
|
20
|
+
if (a.length !== b.length) return false;
|
|
21
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
22
|
+
if (a[index] !== b[index]) return false;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
function stableHash(input) {
|
|
27
|
+
let hash = 2166136261;
|
|
28
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
29
|
+
hash ^= input.charCodeAt(index);
|
|
30
|
+
hash = Math.imul(hash, 16777619);
|
|
31
|
+
}
|
|
32
|
+
return (hash >>> 0).toString(36);
|
|
33
|
+
}
|
|
34
|
+
function computeExternalReferenceId(taskId, reference) {
|
|
35
|
+
const identity = [
|
|
36
|
+
taskId,
|
|
37
|
+
reference.provider ?? "generic",
|
|
38
|
+
reference.kind ?? "link",
|
|
39
|
+
reference.refId ?? "",
|
|
40
|
+
reference.url
|
|
41
|
+
].join("|");
|
|
42
|
+
return `external_reference_${stableHash(identity)}`;
|
|
43
|
+
}
|
|
44
|
+
function getNextStatus(currentStatus, completed) {
|
|
45
|
+
if (completed) return "done";
|
|
46
|
+
if (!currentStatus || isCompletedTaskStatus(currentStatus)) return "todo";
|
|
47
|
+
return currentStatus;
|
|
48
|
+
}
|
|
49
|
+
function isDid(value) {
|
|
50
|
+
return /^did:[a-z]+:[a-zA-Z0-9._:-]+$/.test(value);
|
|
51
|
+
}
|
|
52
|
+
function normalizeAssignees(assignees) {
|
|
53
|
+
return Array.from(new Set(assignees)).filter(isDid);
|
|
54
|
+
}
|
|
55
|
+
function toDateTimestamp(date) {
|
|
56
|
+
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) return void 0;
|
|
57
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
58
|
+
const timestamp = Date.UTC(year, month - 1, day);
|
|
59
|
+
if (Number.isNaN(timestamp)) return void 0;
|
|
60
|
+
const normalized = new Date(timestamp);
|
|
61
|
+
if (normalized.getUTCFullYear() !== year || normalized.getUTCMonth() !== month - 1 || normalized.getUTCDate() !== day) {
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
return timestamp;
|
|
65
|
+
}
|
|
66
|
+
function normalizeProvider(provider) {
|
|
67
|
+
switch (provider) {
|
|
68
|
+
case "github":
|
|
69
|
+
case "figma":
|
|
70
|
+
case "youtube":
|
|
71
|
+
case "loom":
|
|
72
|
+
case "vimeo":
|
|
73
|
+
case "codesandbox":
|
|
74
|
+
case "spotify":
|
|
75
|
+
case "twitter":
|
|
76
|
+
case "instagram":
|
|
77
|
+
case "tiktok":
|
|
78
|
+
return provider;
|
|
79
|
+
default:
|
|
80
|
+
return "generic";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function normalizeKind(kind) {
|
|
84
|
+
switch (kind) {
|
|
85
|
+
case "issue":
|
|
86
|
+
case "pull-request":
|
|
87
|
+
case "design":
|
|
88
|
+
case "video":
|
|
89
|
+
case "sandbox":
|
|
90
|
+
case "social":
|
|
91
|
+
case "audio":
|
|
92
|
+
return kind;
|
|
93
|
+
default:
|
|
94
|
+
return "link";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function useTaskProjectionSync({
|
|
98
|
+
host,
|
|
99
|
+
hostId,
|
|
100
|
+
debounceMs = DEFAULT_DEBOUNCE_MS
|
|
101
|
+
}) {
|
|
102
|
+
const { create, update, remove, restore } = useMutate();
|
|
103
|
+
const disabledHostId = `__${host}_task_sync_disabled__`;
|
|
104
|
+
const { data: existingTasks } = useQuery(TaskSchema, {
|
|
105
|
+
where: host === "page" ? { page: hostId ?? disabledHostId } : { canvas: hostId ?? disabledHostId },
|
|
106
|
+
includeDeleted: true
|
|
107
|
+
});
|
|
108
|
+
const taskSnapshotsRef = useRef([]);
|
|
109
|
+
const syncRunIdRef = useRef(0);
|
|
110
|
+
const [revision, setRevision] = useState(0);
|
|
111
|
+
const [syncing, setSyncing] = useState(false);
|
|
112
|
+
const [error, setError] = useState(null);
|
|
113
|
+
const existingTaskMap = useMemo(() => {
|
|
114
|
+
return new Map(existingTasks.map((task) => [task.id, task]));
|
|
115
|
+
}, [existingTasks]);
|
|
116
|
+
const handleTasksChange = useCallback((tasks) => {
|
|
117
|
+
taskSnapshotsRef.current = tasks;
|
|
118
|
+
setRevision((value) => value + 1);
|
|
119
|
+
}, []);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!hostId) return;
|
|
122
|
+
let cancelled = false;
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
const run = async () => {
|
|
125
|
+
const runId = syncRunIdRef.current + 1;
|
|
126
|
+
syncRunIdRef.current = runId;
|
|
127
|
+
const currentTasks = taskSnapshotsRef.current;
|
|
128
|
+
const nextTaskIds = new Set(currentTasks.map((task) => task.taskId));
|
|
129
|
+
const tasksToClaimOrCreate = [];
|
|
130
|
+
const tasksToRestore = [];
|
|
131
|
+
const taskUpdates = [];
|
|
132
|
+
const taskDeletes = [];
|
|
133
|
+
const referenceUpserts = [];
|
|
134
|
+
const hostFields = host === "page" ? { page: hostId, source: "page" } : { canvas: hostId, source: "canvas" };
|
|
135
|
+
for (const task of currentTasks) {
|
|
136
|
+
const existingTask = existingTaskMap.get(task.taskId);
|
|
137
|
+
const assignees = normalizeAssignees(task.assignees);
|
|
138
|
+
const dueDate = toDateTimestamp(task.dueDate);
|
|
139
|
+
const primaryAssignee = assignees[0];
|
|
140
|
+
const nextReferenceUpserts = [];
|
|
141
|
+
const referenceIds = task.references.map((reference) => {
|
|
142
|
+
const id = computeExternalReferenceId(task.taskId, reference);
|
|
143
|
+
nextReferenceUpserts.push({
|
|
144
|
+
id,
|
|
145
|
+
data: {
|
|
146
|
+
url: reference.url,
|
|
147
|
+
provider: normalizeProvider(reference.provider),
|
|
148
|
+
kind: normalizeKind(reference.kind),
|
|
149
|
+
...reference.refId ? { refId: reference.refId } : {},
|
|
150
|
+
title: reference.title ?? reference.refId ?? reference.url,
|
|
151
|
+
...reference.subtitle ? { subtitle: reference.subtitle } : {},
|
|
152
|
+
...reference.icon ? { icon: reference.icon } : {},
|
|
153
|
+
...reference.embedUrl ? { embedUrl: reference.embedUrl } : {},
|
|
154
|
+
metadata: reference.metadata
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return id;
|
|
158
|
+
});
|
|
159
|
+
if (!existingTask) {
|
|
160
|
+
tasksToClaimOrCreate.push({
|
|
161
|
+
id: task.taskId,
|
|
162
|
+
data: {
|
|
163
|
+
title: task.title,
|
|
164
|
+
completed: task.completed,
|
|
165
|
+
status: getNextStatus(void 0, task.completed),
|
|
166
|
+
parent: task.parentTaskId ?? void 0,
|
|
167
|
+
...hostFields,
|
|
168
|
+
anchorBlockId: task.blockId,
|
|
169
|
+
sortKey: task.sortKey,
|
|
170
|
+
assignee: primaryAssignee,
|
|
171
|
+
assignees,
|
|
172
|
+
dueDate,
|
|
173
|
+
references: referenceIds
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
referenceUpserts.push(...nextReferenceUpserts);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (existingTask.deleted) {
|
|
180
|
+
tasksToRestore.push(task.taskId);
|
|
181
|
+
}
|
|
182
|
+
const nextStatus = getNextStatus(
|
|
183
|
+
typeof existingTask.status === "string" ? existingTask.status : void 0,
|
|
184
|
+
task.completed
|
|
185
|
+
);
|
|
186
|
+
const updateData = {};
|
|
187
|
+
const nextDueDate = dueDate;
|
|
188
|
+
const nextPrimaryAssignee = primaryAssignee;
|
|
189
|
+
if (existingTask.title !== task.title) updateData.title = task.title;
|
|
190
|
+
if (existingTask.completed !== task.completed) updateData.completed = task.completed;
|
|
191
|
+
if (existingTask.status !== nextStatus) updateData.status = nextStatus;
|
|
192
|
+
if ((existingTask.parent ?? null) !== task.parentTaskId) {
|
|
193
|
+
updateData.parent = task.parentTaskId ?? void 0;
|
|
194
|
+
}
|
|
195
|
+
if (host === "page" && existingTask.page !== hostId) updateData.page = hostId;
|
|
196
|
+
if (host === "canvas" && existingTask.canvas !== hostId) updateData.canvas = hostId;
|
|
197
|
+
if (existingTask.anchorBlockId !== task.blockId) updateData.anchorBlockId = task.blockId;
|
|
198
|
+
if (existingTask.sortKey !== task.sortKey) updateData.sortKey = task.sortKey;
|
|
199
|
+
if (existingTask.source !== host) updateData.source = host;
|
|
200
|
+
if (!arraysEqual(existingTask.assignees, assignees)) {
|
|
201
|
+
updateData.assignees = assignees;
|
|
202
|
+
}
|
|
203
|
+
if ((existingTask.assignee ?? void 0) !== nextPrimaryAssignee) {
|
|
204
|
+
updateData.assignee = nextPrimaryAssignee;
|
|
205
|
+
}
|
|
206
|
+
if ((existingTask.dueDate ?? void 0) !== nextDueDate) {
|
|
207
|
+
updateData.dueDate = nextDueDate;
|
|
208
|
+
}
|
|
209
|
+
if (!arraysEqual(existingTask.references, referenceIds)) {
|
|
210
|
+
updateData.references = referenceIds;
|
|
211
|
+
referenceUpserts.push(...nextReferenceUpserts);
|
|
212
|
+
}
|
|
213
|
+
if (Object.keys(updateData).length > 0) {
|
|
214
|
+
taskUpdates.push({
|
|
215
|
+
id: task.taskId,
|
|
216
|
+
data: updateData
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const existingTask of existingTasks) {
|
|
221
|
+
if (existingTask.deleted) continue;
|
|
222
|
+
if (nextTaskIds.has(existingTask.id)) continue;
|
|
223
|
+
taskDeletes.push(existingTask.id);
|
|
224
|
+
}
|
|
225
|
+
if (tasksToClaimOrCreate.length === 0 && tasksToRestore.length === 0 && taskUpdates.length === 0 && taskDeletes.length === 0 && referenceUpserts.length === 0) {
|
|
226
|
+
if (!cancelled) {
|
|
227
|
+
setSyncing(false);
|
|
228
|
+
setError(null);
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!cancelled) {
|
|
233
|
+
setSyncing(true);
|
|
234
|
+
setError(null);
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
for (const reference of referenceUpserts) {
|
|
238
|
+
if (cancelled || runId !== syncRunIdRef.current) return;
|
|
239
|
+
try {
|
|
240
|
+
await update(ExternalReferenceSchema, reference.id, reference.data);
|
|
241
|
+
} catch {
|
|
242
|
+
await create(ExternalReferenceSchema, reference.data, reference.id);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (const taskId of tasksToRestore) {
|
|
246
|
+
if (cancelled || runId !== syncRunIdRef.current) return;
|
|
247
|
+
await restore(taskId);
|
|
248
|
+
}
|
|
249
|
+
for (const task of tasksToClaimOrCreate) {
|
|
250
|
+
if (cancelled || runId !== syncRunIdRef.current) return;
|
|
251
|
+
let claimed = false;
|
|
252
|
+
try {
|
|
253
|
+
await restore(task.id);
|
|
254
|
+
claimed = true;
|
|
255
|
+
} catch {
|
|
256
|
+
claimed = false;
|
|
257
|
+
}
|
|
258
|
+
if (cancelled || runId !== syncRunIdRef.current) return;
|
|
259
|
+
if (claimed) {
|
|
260
|
+
await update(TaskSchema, task.id, task.data);
|
|
261
|
+
} else {
|
|
262
|
+
await create(TaskSchema, task.data, task.id);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const task of taskUpdates) {
|
|
266
|
+
if (cancelled || runId !== syncRunIdRef.current) return;
|
|
267
|
+
await update(TaskSchema, task.id, task.data);
|
|
268
|
+
}
|
|
269
|
+
for (const taskId of taskDeletes) {
|
|
270
|
+
if (cancelled || runId !== syncRunIdRef.current) return;
|
|
271
|
+
await remove(taskId);
|
|
272
|
+
}
|
|
273
|
+
if (!cancelled) {
|
|
274
|
+
setSyncing(false);
|
|
275
|
+
}
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (!cancelled) {
|
|
278
|
+
setSyncing(false);
|
|
279
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
void run();
|
|
284
|
+
}, debounceMs);
|
|
285
|
+
return () => {
|
|
286
|
+
cancelled = true;
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
};
|
|
289
|
+
}, [
|
|
290
|
+
create,
|
|
291
|
+
debounceMs,
|
|
292
|
+
existingTaskMap,
|
|
293
|
+
existingTasks,
|
|
294
|
+
host,
|
|
295
|
+
hostId,
|
|
296
|
+
remove,
|
|
297
|
+
restore,
|
|
298
|
+
revision,
|
|
299
|
+
update
|
|
300
|
+
]);
|
|
301
|
+
return {
|
|
302
|
+
handleTasksChange,
|
|
303
|
+
syncing,
|
|
304
|
+
error
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/hooks/usePageTaskSync.ts
|
|
309
|
+
function usePageTaskSync({
|
|
310
|
+
pageId,
|
|
311
|
+
debounceMs
|
|
312
|
+
}) {
|
|
313
|
+
return useTaskProjectionSync({
|
|
314
|
+
host: "page",
|
|
315
|
+
hostId: pageId,
|
|
316
|
+
...debounceMs !== void 0 ? { debounceMs } : {}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/hooks/useFind.ts
|
|
321
|
+
import { evaluateQueryASTPlannerGate, executeQueryASTLoadedAggregates } from "@xnetjs/data";
|
|
322
|
+
import { useMemo as useMemo2 } from "react";
|
|
323
|
+
var EMPTY_FIND_OPTIONS = {};
|
|
324
|
+
function addBlocker(blockers, blocker) {
|
|
325
|
+
if (!blockers.includes(blocker)) {
|
|
326
|
+
blockers.push(blocker);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function schemaIdsFor(schema) {
|
|
330
|
+
return [...new Set([schema._schemaId, schema.schema["@id"]].filter(Boolean))];
|
|
331
|
+
}
|
|
332
|
+
function compilePredicate(predicate) {
|
|
333
|
+
const where = {};
|
|
334
|
+
const blockers = [];
|
|
335
|
+
const visit = (next) => {
|
|
336
|
+
if (next.kind === "and") {
|
|
337
|
+
next.predicates.forEach(visit);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (next.kind !== "comparison" || next.op !== "eq") {
|
|
341
|
+
addBlocker(blockers, "usefind-predicate-not-lowerable");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!Object.prototype.hasOwnProperty.call(next, "value")) {
|
|
345
|
+
addBlocker(blockers, "usefind-eq-value-required");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (Object.prototype.hasOwnProperty.call(where, next.field) && where[next.field] !== next.value) {
|
|
349
|
+
addBlocker(blockers, "usefind-conflicting-field-equality");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
where[next.field] = next.value;
|
|
353
|
+
};
|
|
354
|
+
if (predicate) {
|
|
355
|
+
visit(predicate);
|
|
356
|
+
}
|
|
357
|
+
return { where, blockers };
|
|
358
|
+
}
|
|
359
|
+
function compilePage(page, filter, blockers) {
|
|
360
|
+
if (!page) return;
|
|
361
|
+
if ((page.after || page.count) && page.first === void 0) {
|
|
362
|
+
addBlocker(blockers, "usefind-cursor-page-first-required");
|
|
363
|
+
}
|
|
364
|
+
if (page.after && page.offset !== void 0) {
|
|
365
|
+
addBlocker(blockers, "usefind-cursor-and-offset-pagination-not-supported");
|
|
366
|
+
}
|
|
367
|
+
if (page.first !== void 0) {
|
|
368
|
+
filter.page = {
|
|
369
|
+
first: page.first,
|
|
370
|
+
...page.after ? { after: page.after } : {},
|
|
371
|
+
...page.count ? { count: page.count } : {}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (page.offset !== void 0) {
|
|
375
|
+
filter.offset = page.offset;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function compileNodeQuery(schema, ast, options) {
|
|
379
|
+
const blockers = [];
|
|
380
|
+
if (!schemaIdsFor(schema).includes(ast.schemaId)) {
|
|
381
|
+
addBlocker(blockers, "usefind-schema-mismatch");
|
|
382
|
+
}
|
|
383
|
+
if (ast.include && Object.keys(ast.include).length > 0) {
|
|
384
|
+
addBlocker(blockers, "usefind-relation-includes-not-executable");
|
|
385
|
+
}
|
|
386
|
+
const compiledPredicate = compilePredicate(ast.predicate);
|
|
387
|
+
compiledPredicate.blockers.forEach((blocker) => addBlocker(blockers, blocker));
|
|
388
|
+
const filter = {
|
|
389
|
+
...options,
|
|
390
|
+
...Object.keys(compiledPredicate.where).length > 0 ? { where: compiledPredicate.where } : {},
|
|
391
|
+
...ast.orderBy && ast.orderBy.length > 0 ? {
|
|
392
|
+
orderBy: Object.fromEntries(
|
|
393
|
+
ast.orderBy.map((entry) => [entry.field, entry.direction])
|
|
394
|
+
)
|
|
395
|
+
} : {}
|
|
396
|
+
};
|
|
397
|
+
compilePage(ast.page, filter, blockers);
|
|
398
|
+
return { filter, blockers };
|
|
399
|
+
}
|
|
400
|
+
function compileFindQuery(schema, ast, options) {
|
|
401
|
+
if (ast.kind !== "node") {
|
|
402
|
+
return {
|
|
403
|
+
filter: {},
|
|
404
|
+
blockers: ["usefind-query-sets-not-executable"]
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return compileNodeQuery(schema, ast, options);
|
|
408
|
+
}
|
|
409
|
+
function plannerError(blockers) {
|
|
410
|
+
return new Error(`useFind planner blocked query execution: ${blockers.join(", ")}`);
|
|
411
|
+
}
|
|
412
|
+
function useFind(schema, ast, options) {
|
|
413
|
+
const resolvedOptions = options ?? EMPTY_FIND_OPTIONS;
|
|
414
|
+
const plannerGate = useMemo2(() => evaluateQueryASTPlannerGate(ast), [ast]);
|
|
415
|
+
const compiled = useMemo2(
|
|
416
|
+
() => compileFindQuery(schema, ast, resolvedOptions),
|
|
417
|
+
[schema, ast, resolvedOptions]
|
|
418
|
+
);
|
|
419
|
+
const blockers = useMemo2(
|
|
420
|
+
() => [.../* @__PURE__ */ new Set([...plannerGate.blockers, ...compiled.blockers])],
|
|
421
|
+
[compiled.blockers, plannerGate.blockers]
|
|
422
|
+
);
|
|
423
|
+
const canExecute = plannerGate.validation.valid && blockers.length === 0;
|
|
424
|
+
const result = useQuery(schema, canExecute ? compiled.filter : { enabled: false });
|
|
425
|
+
const error = useMemo2(
|
|
426
|
+
() => canExecute ? result.error : plannerError(blockers),
|
|
427
|
+
[blockers, canExecute, result.error]
|
|
428
|
+
);
|
|
429
|
+
const aggregates = useMemo2(
|
|
430
|
+
() => canExecute && ast.kind === "node" && ast.aggregates && ast.aggregates.length > 0 ? executeQueryASTLoadedAggregates(ast, result.data) : null,
|
|
431
|
+
[ast, canExecute, result.data]
|
|
432
|
+
);
|
|
433
|
+
return {
|
|
434
|
+
...result,
|
|
435
|
+
status: error ? "error" : result.status,
|
|
436
|
+
loading: canExecute ? result.loading : false,
|
|
437
|
+
isLoading: canExecute ? result.isLoading : false,
|
|
438
|
+
isFetching: canExecute ? result.isFetching : false,
|
|
439
|
+
error,
|
|
440
|
+
plannerGate,
|
|
441
|
+
blockers,
|
|
442
|
+
canExecute,
|
|
443
|
+
aggregates
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/hooks/useTasks.ts
|
|
448
|
+
import { TaskSchema as TaskSchema2 } from "@xnetjs/data";
|
|
449
|
+
import { useMemo as useMemo3 } from "react";
|
|
450
|
+
function compareOptionalNumbers(a, b) {
|
|
451
|
+
if (a == null && b == null) return 0;
|
|
452
|
+
if (a == null) return 1;
|
|
453
|
+
if (b == null) return -1;
|
|
454
|
+
return a - b;
|
|
455
|
+
}
|
|
456
|
+
function sortTasks(tasks, pageScoped) {
|
|
457
|
+
return [...tasks].sort((a, b) => {
|
|
458
|
+
if (pageScoped) {
|
|
459
|
+
const aKey2 = typeof a.sortKey === "string" ? a.sortKey : "";
|
|
460
|
+
const bKey2 = typeof b.sortKey === "string" ? b.sortKey : "";
|
|
461
|
+
return aKey2.localeCompare(bKey2) || a.id.localeCompare(b.id);
|
|
462
|
+
}
|
|
463
|
+
if (a.completed !== b.completed) {
|
|
464
|
+
return Number(a.completed) - Number(b.completed);
|
|
465
|
+
}
|
|
466
|
+
const dueDateComparison = compareOptionalNumbers(
|
|
467
|
+
typeof a.dueDate === "number" ? a.dueDate : void 0,
|
|
468
|
+
typeof b.dueDate === "number" ? b.dueDate : void 0
|
|
469
|
+
);
|
|
470
|
+
if (dueDateComparison !== 0) return dueDateComparison;
|
|
471
|
+
const updatedAtComparison = b.updatedAt - a.updatedAt;
|
|
472
|
+
if (updatedAtComparison !== 0) return updatedAtComparison;
|
|
473
|
+
const aKey = typeof a.sortKey === "string" ? a.sortKey : "";
|
|
474
|
+
const bKey = typeof b.sortKey === "string" ? b.sortKey : "";
|
|
475
|
+
const aTitle = a.title ?? "";
|
|
476
|
+
const bTitle = b.title ?? "";
|
|
477
|
+
return aKey.localeCompare(bKey) || aTitle.localeCompare(bTitle) || a.id.localeCompare(b.id);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function matchesAssignee(task, assigneeDid) {
|
|
481
|
+
if (!assigneeDid) return true;
|
|
482
|
+
if (typeof task.assignee === "string" && task.assignee === assigneeDid) return true;
|
|
483
|
+
return Array.isArray(task.assignees) && task.assignees.map(String).includes(assigneeDid);
|
|
484
|
+
}
|
|
485
|
+
function matchesStatus(task, statuses) {
|
|
486
|
+
if (!statuses || statuses.length === 0) return true;
|
|
487
|
+
return typeof task.status === "string" && statuses.includes(task.status);
|
|
488
|
+
}
|
|
489
|
+
function matchesParent(task, parentTaskId) {
|
|
490
|
+
if (parentTaskId === void 0) return true;
|
|
491
|
+
return (task.parent ?? null) === parentTaskId;
|
|
492
|
+
}
|
|
493
|
+
function getStartOfUtcDay(timestamp) {
|
|
494
|
+
const date = new Date(timestamp);
|
|
495
|
+
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
496
|
+
}
|
|
497
|
+
function matchesDueDate(task, dueDateFilter) {
|
|
498
|
+
if (!dueDateFilter || dueDateFilter === "any") return true;
|
|
499
|
+
const dueDate = typeof task.dueDate === "number" ? task.dueDate : void 0;
|
|
500
|
+
if (dueDateFilter === "none") return dueDate == null;
|
|
501
|
+
if (dueDate == null) return false;
|
|
502
|
+
const todayStart = getStartOfUtcDay(Date.now());
|
|
503
|
+
const dueDay = getStartOfUtcDay(dueDate);
|
|
504
|
+
switch (dueDateFilter) {
|
|
505
|
+
case "overdue":
|
|
506
|
+
return dueDay < todayStart;
|
|
507
|
+
case "today":
|
|
508
|
+
return dueDay === todayStart;
|
|
509
|
+
case "next-7-days":
|
|
510
|
+
return dueDay >= todayStart && dueDay <= todayStart + 6 * 24 * 60 * 60 * 1e3;
|
|
511
|
+
default:
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function buildTaskTree(tasks) {
|
|
516
|
+
const treeById = /* @__PURE__ */ new Map();
|
|
517
|
+
const roots = [];
|
|
518
|
+
for (const task of tasks) {
|
|
519
|
+
treeById.set(task.id, {
|
|
520
|
+
task,
|
|
521
|
+
depth: 0,
|
|
522
|
+
children: []
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
for (const task of tasks) {
|
|
526
|
+
const current = treeById.get(task.id);
|
|
527
|
+
if (!current) continue;
|
|
528
|
+
const parentId = task.parent ?? null;
|
|
529
|
+
const parent = parentId ? treeById.get(parentId) : void 0;
|
|
530
|
+
if (!parent) {
|
|
531
|
+
roots.push(current);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
parent.children.push(current);
|
|
535
|
+
}
|
|
536
|
+
const setDepth = (node, depth) => {
|
|
537
|
+
node.depth = depth;
|
|
538
|
+
for (const child of node.children) {
|
|
539
|
+
setDepth(child, depth + 1);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
for (const root of roots) {
|
|
543
|
+
setDepth(root, 0);
|
|
544
|
+
}
|
|
545
|
+
return roots;
|
|
546
|
+
}
|
|
547
|
+
function useTasks({
|
|
548
|
+
pageId,
|
|
549
|
+
assigneeDid,
|
|
550
|
+
includeCompleted = true,
|
|
551
|
+
statuses,
|
|
552
|
+
parentTaskId,
|
|
553
|
+
dueDateFilter = "any"
|
|
554
|
+
} = {}) {
|
|
555
|
+
const query = useQuery(
|
|
556
|
+
TaskSchema2,
|
|
557
|
+
pageId ? {
|
|
558
|
+
where: { page: pageId }
|
|
559
|
+
} : {}
|
|
560
|
+
);
|
|
561
|
+
const tasks = useMemo3(() => {
|
|
562
|
+
const filtered = query.data.filter((task) => {
|
|
563
|
+
if (!includeCompleted && task.completed) return false;
|
|
564
|
+
if (!matchesAssignee(task, assigneeDid)) return false;
|
|
565
|
+
if (!matchesStatus(task, statuses)) return false;
|
|
566
|
+
if (!matchesParent(task, parentTaskId)) return false;
|
|
567
|
+
if (!matchesDueDate(task, dueDateFilter)) return false;
|
|
568
|
+
return true;
|
|
569
|
+
});
|
|
570
|
+
return sortTasks(filtered, Boolean(pageId));
|
|
571
|
+
}, [assigneeDid, dueDateFilter, includeCompleted, pageId, parentTaskId, query.data, statuses]);
|
|
572
|
+
const tree = useMemo3(() => buildTaskTree(tasks), [tasks]);
|
|
573
|
+
return {
|
|
574
|
+
data: tasks,
|
|
575
|
+
tree,
|
|
576
|
+
loading: query.loading,
|
|
577
|
+
error: query.error,
|
|
578
|
+
reload: query.reload
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/hooks/useComments.ts
|
|
583
|
+
import { CommentSchema, getMentionedUsers, normalizeMentions } from "@xnetjs/data";
|
|
584
|
+
import { useState as useState2, useEffect as useEffect2, useMemo as useMemo4, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
585
|
+
function useComments({ nodeId, anchorType }) {
|
|
586
|
+
const { store, isReady } = useNodeStore();
|
|
587
|
+
const [comments, setComments] = useState2([]);
|
|
588
|
+
const [loading, setLoading] = useState2(true);
|
|
589
|
+
const [error, setError] = useState2(null);
|
|
590
|
+
const queryRef = useRef2({ nodeId, anchorType });
|
|
591
|
+
queryRef.current = { nodeId, anchorType };
|
|
592
|
+
const loadComments = useCallback2(async () => {
|
|
593
|
+
if (!store || !isReady) return;
|
|
594
|
+
try {
|
|
595
|
+
setLoading(true);
|
|
596
|
+
setError(null);
|
|
597
|
+
const nodes = await store.list({
|
|
598
|
+
schemaId: CommentSchema._schemaId
|
|
599
|
+
});
|
|
600
|
+
const filtered = nodes.filter((n) => {
|
|
601
|
+
if (n.properties.target !== queryRef.current.nodeId) return false;
|
|
602
|
+
if (queryRef.current.anchorType && n.properties.anchorType !== queryRef.current.anchorType && !n.properties.inReplyTo) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
return true;
|
|
606
|
+
});
|
|
607
|
+
const commentNodes = filtered.map((n) => nodeToComment(n));
|
|
608
|
+
setComments(commentNodes);
|
|
609
|
+
} catch (err) {
|
|
610
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
611
|
+
} finally {
|
|
612
|
+
setLoading(false);
|
|
613
|
+
}
|
|
614
|
+
}, [store, isReady]);
|
|
615
|
+
useEffect2(() => {
|
|
616
|
+
loadComments();
|
|
617
|
+
}, [loadComments]);
|
|
618
|
+
useEffect2(() => {
|
|
619
|
+
if (!store || !isReady) return;
|
|
620
|
+
const handleChange = (event) => {
|
|
621
|
+
if (event.node?.schemaId === CommentSchema._schemaId) {
|
|
622
|
+
if (event.node.properties.target === nodeId) {
|
|
623
|
+
loadComments();
|
|
624
|
+
}
|
|
625
|
+
} else if (event.change?.payload?.schemaId === CommentSchema._schemaId) {
|
|
626
|
+
loadComments();
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
const unsubscribe = store.subscribe(handleChange);
|
|
630
|
+
return () => unsubscribe();
|
|
631
|
+
}, [store, isReady, loadComments, nodeId]);
|
|
632
|
+
const threads = useMemo4(() => {
|
|
633
|
+
const threadMap = /* @__PURE__ */ new Map();
|
|
634
|
+
for (const comment of comments) {
|
|
635
|
+
if (!comment.properties.inReplyTo) {
|
|
636
|
+
threadMap.set(comment.id, { root: comment, replies: [] });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
for (const comment of comments) {
|
|
640
|
+
const rootId = comment.properties.inReplyTo;
|
|
641
|
+
if (rootId) {
|
|
642
|
+
const thread = threadMap.get(rootId);
|
|
643
|
+
if (thread) {
|
|
644
|
+
thread.replies.push(comment);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
for (const thread of threadMap.values()) {
|
|
649
|
+
thread.replies.sort((a, b) => a.lamportTime - b.lamportTime);
|
|
650
|
+
}
|
|
651
|
+
return Array.from(threadMap.values());
|
|
652
|
+
}, [comments]);
|
|
653
|
+
const unresolvedCount = useMemo4(() => {
|
|
654
|
+
return threads.filter((t) => !t.root.properties.resolved).length;
|
|
655
|
+
}, [threads]);
|
|
656
|
+
const addComment = useCallback2(
|
|
657
|
+
async (options) => {
|
|
658
|
+
if (!store || !isReady) return null;
|
|
659
|
+
try {
|
|
660
|
+
const node = await store.create({
|
|
661
|
+
schemaId: CommentSchema._schemaId,
|
|
662
|
+
properties: {
|
|
663
|
+
target: nodeId,
|
|
664
|
+
targetSchema: options.targetSchema,
|
|
665
|
+
anchorType: options.anchorType,
|
|
666
|
+
anchorData: options.anchorData,
|
|
667
|
+
content: options.content,
|
|
668
|
+
// Composer-declared structured mentions (0168): DID-form
|
|
669
|
+
// @mentions in the markdown become a queryable field.
|
|
670
|
+
mentions: normalizeMentions({ dids: getMentionedUsers(options.content) }),
|
|
671
|
+
resolved: false,
|
|
672
|
+
edited: false
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
return node.id;
|
|
676
|
+
} catch (err) {
|
|
677
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
[store, isReady, nodeId]
|
|
682
|
+
);
|
|
683
|
+
const replyTo = useCallback2(
|
|
684
|
+
async (rootCommentId, content, context) => {
|
|
685
|
+
if (!store || !isReady) return null;
|
|
686
|
+
const root = threads.find((t) => t.root.id === rootCommentId)?.root;
|
|
687
|
+
if (!root) {
|
|
688
|
+
setError(new Error("Thread root not found"));
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
const node = await store.create({
|
|
693
|
+
schemaId: CommentSchema._schemaId,
|
|
694
|
+
properties: {
|
|
695
|
+
target: nodeId,
|
|
696
|
+
targetSchema: root.properties.targetSchema,
|
|
697
|
+
inReplyTo: rootCommentId,
|
|
698
|
+
// Always points to root (flat threading)
|
|
699
|
+
anchorType: "node",
|
|
700
|
+
// Replies don't need positional anchors
|
|
701
|
+
anchorData: "{}",
|
|
702
|
+
content,
|
|
703
|
+
mentions: normalizeMentions({ dids: getMentionedUsers(content) }),
|
|
704
|
+
replyToUser: context?.replyToUser,
|
|
705
|
+
replyToCommentId: context?.replyToCommentId,
|
|
706
|
+
resolved: false,
|
|
707
|
+
edited: false
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
return node.id;
|
|
711
|
+
} catch (err) {
|
|
712
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
[store, isReady, nodeId, threads]
|
|
717
|
+
);
|
|
718
|
+
const resolveThread = useCallback2(
|
|
719
|
+
async (rootCommentId) => {
|
|
720
|
+
if (!store || !isReady) return;
|
|
721
|
+
try {
|
|
722
|
+
await store.update(rootCommentId, {
|
|
723
|
+
properties: {
|
|
724
|
+
resolved: true,
|
|
725
|
+
resolvedAt: Date.now()
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
} catch (err) {
|
|
729
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
[store, isReady]
|
|
733
|
+
);
|
|
734
|
+
const reopenThread = useCallback2(
|
|
735
|
+
async (rootCommentId) => {
|
|
736
|
+
if (!store || !isReady) return;
|
|
737
|
+
try {
|
|
738
|
+
await store.update(rootCommentId, {
|
|
739
|
+
properties: {
|
|
740
|
+
resolved: false,
|
|
741
|
+
resolvedBy: null,
|
|
742
|
+
resolvedAt: null
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
} catch (err) {
|
|
746
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
[store, isReady]
|
|
750
|
+
);
|
|
751
|
+
const deleteComment = useCallback2(
|
|
752
|
+
async (commentId) => {
|
|
753
|
+
if (!store || !isReady) return;
|
|
754
|
+
try {
|
|
755
|
+
const thread = threads.find((t) => t.root.id === commentId);
|
|
756
|
+
const isRoot = thread !== void 0;
|
|
757
|
+
if (isRoot) {
|
|
758
|
+
if (thread.replies.length > 0) {
|
|
759
|
+
await store.update(commentId, {
|
|
760
|
+
properties: { content: "[deleted]" }
|
|
761
|
+
});
|
|
762
|
+
} else {
|
|
763
|
+
await store.delete(commentId);
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
await store.delete(commentId);
|
|
767
|
+
}
|
|
768
|
+
} catch (err) {
|
|
769
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
[store, isReady, threads]
|
|
773
|
+
);
|
|
774
|
+
const deleteThread = useCallback2(
|
|
775
|
+
async (rootCommentId) => {
|
|
776
|
+
if (!store || !isReady) return;
|
|
777
|
+
const thread = threads.find((t) => t.root.id === rootCommentId);
|
|
778
|
+
if (!thread) return;
|
|
779
|
+
try {
|
|
780
|
+
for (const reply of thread.replies) {
|
|
781
|
+
await store.delete(reply.id);
|
|
782
|
+
}
|
|
783
|
+
await store.delete(rootCommentId);
|
|
784
|
+
} catch (err) {
|
|
785
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
[store, isReady, threads]
|
|
789
|
+
);
|
|
790
|
+
const editComment = useCallback2(
|
|
791
|
+
async (commentId, content) => {
|
|
792
|
+
if (!store || !isReady) return;
|
|
793
|
+
try {
|
|
794
|
+
await store.update(commentId, {
|
|
795
|
+
properties: {
|
|
796
|
+
content,
|
|
797
|
+
edited: true,
|
|
798
|
+
editedAt: Date.now()
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
} catch (err) {
|
|
802
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
[store, isReady]
|
|
806
|
+
);
|
|
807
|
+
return {
|
|
808
|
+
comments,
|
|
809
|
+
threads,
|
|
810
|
+
count: comments.length,
|
|
811
|
+
unresolvedCount,
|
|
812
|
+
loading,
|
|
813
|
+
error,
|
|
814
|
+
addComment,
|
|
815
|
+
replyTo,
|
|
816
|
+
resolveThread,
|
|
817
|
+
reopenThread,
|
|
818
|
+
deleteComment,
|
|
819
|
+
deleteThread,
|
|
820
|
+
editComment,
|
|
821
|
+
reload: loadComments
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function nodeToComment(node) {
|
|
825
|
+
const firstTimestamp = Object.values(node.timestamps)[0];
|
|
826
|
+
const lamportTime = firstTimestamp?.lamport ?? 0;
|
|
827
|
+
const wallTime = firstTimestamp?.wallTime ?? node.createdAt;
|
|
828
|
+
return {
|
|
829
|
+
id: node.id,
|
|
830
|
+
schemaId: node.schemaId,
|
|
831
|
+
createdAt: node.createdAt,
|
|
832
|
+
lamportTime,
|
|
833
|
+
wallTime,
|
|
834
|
+
properties: {
|
|
835
|
+
target: node.properties.target,
|
|
836
|
+
targetSchema: node.properties.targetSchema,
|
|
837
|
+
inReplyTo: node.properties.inReplyTo,
|
|
838
|
+
anchorType: node.properties.anchorType,
|
|
839
|
+
anchorData: node.properties.anchorData,
|
|
840
|
+
content: node.properties.content,
|
|
841
|
+
attachments: node.properties.attachments,
|
|
842
|
+
replyToUser: node.properties.replyToUser,
|
|
843
|
+
replyToCommentId: node.properties.replyToCommentId,
|
|
844
|
+
resolved: node.properties.resolved ?? false,
|
|
845
|
+
resolvedBy: node.properties.resolvedBy,
|
|
846
|
+
resolvedAt: node.properties.resolvedAt,
|
|
847
|
+
edited: node.properties.edited ?? false,
|
|
848
|
+
editedAt: node.properties.editedAt,
|
|
849
|
+
createdBy: node.createdBy
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/hooks/useModeratedComments.ts
|
|
855
|
+
import { ModerationLabelSchema, PublicInteractionPolicySchema } from "@xnetjs/data";
|
|
856
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo5, useState as useState3 } from "react";
|
|
857
|
+
var DEFAULT_HIDDEN_COMMENT_LABELS = [
|
|
858
|
+
"spam",
|
|
859
|
+
"scam",
|
|
860
|
+
"malware",
|
|
861
|
+
"impersonation",
|
|
862
|
+
"harassment"
|
|
863
|
+
];
|
|
864
|
+
var DEFAULT_COLLAPSED_COMMENT_LABELS = [
|
|
865
|
+
"slop",
|
|
866
|
+
"inaccurate",
|
|
867
|
+
"unsupported",
|
|
868
|
+
"stale",
|
|
869
|
+
"synthetic"
|
|
870
|
+
];
|
|
871
|
+
var DEFAULT_HIDE_CONFIDENCE = 0.85;
|
|
872
|
+
var DEFAULT_COLLAPSE_CONFIDENCE = 0.65;
|
|
873
|
+
var DEFAULT_PUBLIC_INTERACTION_MODES = {
|
|
874
|
+
comment: "authenticated",
|
|
875
|
+
reply: "authenticated",
|
|
876
|
+
reaction: "authenticated",
|
|
877
|
+
quote: "trusted",
|
|
878
|
+
mention: "trusted",
|
|
879
|
+
communityNote: "reviewed",
|
|
880
|
+
message: "authenticated",
|
|
881
|
+
crawl: "closed",
|
|
882
|
+
index: "reviewed"
|
|
883
|
+
};
|
|
884
|
+
function summarizeModerationLabel(node, now = Date.now()) {
|
|
885
|
+
const target = asString(node.properties.target);
|
|
886
|
+
const value = asString(node.properties.value);
|
|
887
|
+
const confidence = asNumber(node.properties.confidence);
|
|
888
|
+
const sourceWeight = asNumber(node.properties.sourceWeight) ?? 1;
|
|
889
|
+
const expiresAt = asNumber(node.properties.expiresAt);
|
|
890
|
+
if (!target || !value || confidence === void 0) return null;
|
|
891
|
+
if (expiresAt !== void 0 && expiresAt <= now) return null;
|
|
892
|
+
return {
|
|
893
|
+
id: node.id,
|
|
894
|
+
target,
|
|
895
|
+
value,
|
|
896
|
+
confidence,
|
|
897
|
+
sourceWeight,
|
|
898
|
+
sourceType: asString(node.properties.sourceType),
|
|
899
|
+
sourceDID: asString(node.properties.sourceDID),
|
|
900
|
+
evidenceRefs: asString(node.properties.evidenceRefs),
|
|
901
|
+
expiresAt,
|
|
902
|
+
negates: asString(node.properties.negates),
|
|
903
|
+
createdAt: node.createdAt
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function summarizePublicInteractionPolicy(node) {
|
|
907
|
+
const target = asString(node.properties.target);
|
|
908
|
+
if (!target) return null;
|
|
909
|
+
const updatedAt = asNumber(node.properties.updatedAt) ?? node.updatedAt ?? node.createdAt;
|
|
910
|
+
return {
|
|
911
|
+
id: node.id,
|
|
912
|
+
target,
|
|
913
|
+
targetSchema: asString(node.properties.targetSchema),
|
|
914
|
+
scope: asString(node.properties.scope),
|
|
915
|
+
commentMode: asInteractionMode(node.properties.commentMode, "authenticated"),
|
|
916
|
+
replyMode: asInteractionMode(node.properties.replyMode, "authenticated"),
|
|
917
|
+
reactionMode: asInteractionMode(node.properties.reactionMode, "authenticated"),
|
|
918
|
+
quoteMode: asInteractionMode(node.properties.quoteMode, "trusted"),
|
|
919
|
+
mentionMode: asInteractionMode(node.properties.mentionMode, "trusted"),
|
|
920
|
+
communityNoteMode: asInteractionMode(node.properties.communityNoteMode, "reviewed"),
|
|
921
|
+
messageMode: asInteractionMode(node.properties.messageMode, "authenticated"),
|
|
922
|
+
crawlMode: asInteractionMode(node.properties.crawlMode, "closed"),
|
|
923
|
+
indexMode: asInteractionMode(node.properties.indexMode, "reviewed"),
|
|
924
|
+
defaultVisibility: asVisibility(node.properties.defaultVisibility, "visible"),
|
|
925
|
+
firstContactMode: asFirstContactMode(node.properties.firstContactMode, "slow-mode"),
|
|
926
|
+
moderationMode: asModerationMode(node.properties.moderationMode, "post-review"),
|
|
927
|
+
slowModeSeconds: asNumber(node.properties.slowModeSeconds),
|
|
928
|
+
maxRootCommentsPerHour: asNumber(node.properties.maxRootCommentsPerHour),
|
|
929
|
+
maxRepliesPerHour: asNumber(node.properties.maxRepliesPerHour),
|
|
930
|
+
maxReactionsPerHour: asNumber(node.properties.maxReactionsPerHour),
|
|
931
|
+
maxMentionsPerComment: asNumber(node.properties.maxMentionsPerComment),
|
|
932
|
+
minimumAccountAgeHours: asNumber(node.properties.minimumAccountAgeHours),
|
|
933
|
+
minimumReputation: asNumber(node.properties.minimumReputation),
|
|
934
|
+
trustThreshold: asNumber(node.properties.trustThreshold),
|
|
935
|
+
quarantineConfidenceThreshold: asNumber(node.properties.quarantineConfidenceThreshold),
|
|
936
|
+
hideConfidenceThreshold: asNumber(node.properties.hideConfidenceThreshold),
|
|
937
|
+
requiresVerifiedIdentity: asBoolean(node.properties.requiresVerifiedIdentity, false),
|
|
938
|
+
acceptsPolicySubscriptions: asBoolean(node.properties.acceptsPolicySubscriptions, true),
|
|
939
|
+
policyLists: asStringArray(node.properties.policyLists),
|
|
940
|
+
activeLabels: asStringArray(node.properties.activeLabels),
|
|
941
|
+
maintainers: asStringArray(node.properties.maintainers),
|
|
942
|
+
moderators: asStringArray(node.properties.moderators),
|
|
943
|
+
policyPublishers: asStringArray(node.properties.policyPublishers),
|
|
944
|
+
trustedDIDs: asStringArray(node.properties.trustedDIDs),
|
|
945
|
+
mutedDIDs: asStringArray(node.properties.mutedDIDs),
|
|
946
|
+
blockedDIDs: asStringArray(node.properties.blockedDIDs),
|
|
947
|
+
updatedAt,
|
|
948
|
+
createdAt: node.createdAt
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function selectActiveInteractionPolicy(policies) {
|
|
952
|
+
return [...policies].sort((left, right) => {
|
|
953
|
+
const updatedDelta = right.updatedAt - left.updatedAt;
|
|
954
|
+
return updatedDelta !== 0 ? updatedDelta : right.createdAt - left.createdAt;
|
|
955
|
+
})[0] ?? null;
|
|
956
|
+
}
|
|
957
|
+
function selectPublicInteractionMode(policy, surface) {
|
|
958
|
+
if (!policy) return DEFAULT_PUBLIC_INTERACTION_MODES[surface];
|
|
959
|
+
if (surface === "comment") return policy.commentMode;
|
|
960
|
+
if (surface === "reply") return policy.replyMode;
|
|
961
|
+
if (surface === "reaction") return policy.reactionMode;
|
|
962
|
+
if (surface === "quote") return policy.quoteMode;
|
|
963
|
+
if (surface === "mention") return policy.mentionMode;
|
|
964
|
+
if (surface === "communityNote") return policy.communityNoteMode;
|
|
965
|
+
if (surface === "message") return policy.messageMode;
|
|
966
|
+
if (surface === "crawl") return policy.crawlMode;
|
|
967
|
+
return policy.indexMode;
|
|
968
|
+
}
|
|
969
|
+
function createModerationLabelIndex(labels) {
|
|
970
|
+
return labels.reduce((index, label) => {
|
|
971
|
+
const existing = index.get(label.target) ?? [];
|
|
972
|
+
index.set(label.target, [...existing, label]);
|
|
973
|
+
return index;
|
|
974
|
+
}, /* @__PURE__ */ new Map());
|
|
975
|
+
}
|
|
976
|
+
function evaluateInteractionPermission(mode, policy, options = {}) {
|
|
977
|
+
const reasons = [];
|
|
978
|
+
const viewerDID = options.viewerDID;
|
|
979
|
+
const authenticated = options.isAuthenticated ?? Boolean(viewerDID);
|
|
980
|
+
const trusted = viewerDID !== void 0 && policy?.trustedDIDs.includes(viewerDID);
|
|
981
|
+
const privileged = viewerDID !== void 0 && (policy?.maintainers.includes(viewerDID) || policy?.moderators.includes(viewerDID) || policy?.policyPublishers.includes(viewerDID));
|
|
982
|
+
if (viewerDID !== void 0 && policy?.blockedDIDs.includes(viewerDID)) {
|
|
983
|
+
return { allowed: false, requiresReview: false, mode, reasons: ["viewer-blocked"] };
|
|
984
|
+
}
|
|
985
|
+
if (viewerDID !== void 0 && policy?.mutedDIDs.includes(viewerDID)) {
|
|
986
|
+
return { allowed: false, requiresReview: false, mode, reasons: ["viewer-muted"] };
|
|
987
|
+
}
|
|
988
|
+
if (policy?.requiresVerifiedIdentity && !options.isVerified && !privileged) {
|
|
989
|
+
reasons.push("verified-identity-required");
|
|
990
|
+
}
|
|
991
|
+
if (mode === "closed") reasons.push("interaction-closed");
|
|
992
|
+
if (mode === "authenticated" && !authenticated) reasons.push("authentication-required");
|
|
993
|
+
if (mode === "trusted" && !trusted && !privileged) reasons.push("trusted-viewer-required");
|
|
994
|
+
if (mode === "reviewed" && !authenticated && !privileged) {
|
|
995
|
+
reasons.push("authentication-required");
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
allowed: reasons.length === 0,
|
|
999
|
+
requiresReview: mode === "reviewed",
|
|
1000
|
+
mode,
|
|
1001
|
+
reasons
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function evaluateCommentModeration(comment, options = {}) {
|
|
1005
|
+
const policy = options.policy ?? null;
|
|
1006
|
+
const labels = filterActiveLabels(options.labels ?? [], policy, options.minimumLabelConfidence);
|
|
1007
|
+
const reasons = [];
|
|
1008
|
+
if (policy?.moderationMode === "off") {
|
|
1009
|
+
return {
|
|
1010
|
+
comment,
|
|
1011
|
+
visibility: "visible",
|
|
1012
|
+
labels,
|
|
1013
|
+
reasons,
|
|
1014
|
+
visible: true
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
const safeConfidence = maxConfidence(labels, "safe");
|
|
1018
|
+
const hiddenLabels = new Set(options.hiddenLabels ?? DEFAULT_HIDDEN_COMMENT_LABELS);
|
|
1019
|
+
const collapsedLabels = new Set(options.collapsedLabels ?? DEFAULT_COLLAPSED_COMMENT_LABELS);
|
|
1020
|
+
const hideThreshold = policy?.hideConfidenceThreshold ?? DEFAULT_HIDE_CONFIDENCE;
|
|
1021
|
+
const collapseThreshold = policy?.quarantineConfidenceThreshold ?? DEFAULT_COLLAPSE_CONFIDENCE;
|
|
1022
|
+
const hideLabel = labels.find(
|
|
1023
|
+
(label) => hiddenLabels.has(label.value) && label.confidence >= hideThreshold && label.confidence > safeConfidence
|
|
1024
|
+
);
|
|
1025
|
+
if (hideLabel) {
|
|
1026
|
+
reasons.push(`label:${hideLabel.value}`);
|
|
1027
|
+
return {
|
|
1028
|
+
comment,
|
|
1029
|
+
visibility: "hidden",
|
|
1030
|
+
labels,
|
|
1031
|
+
reasons,
|
|
1032
|
+
visible: isVisibilityIncluded("hidden", options)
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
const collapseLabel = labels.find(
|
|
1036
|
+
(label) => collapsedLabels.has(label.value) && label.confidence >= collapseThreshold && label.confidence > safeConfidence
|
|
1037
|
+
);
|
|
1038
|
+
if (collapseLabel) {
|
|
1039
|
+
reasons.push(`label:${collapseLabel.value}`);
|
|
1040
|
+
const visibility = policy?.defaultVisibility === "quarantined" ? "quarantined" : "collapsed";
|
|
1041
|
+
return {
|
|
1042
|
+
comment,
|
|
1043
|
+
visibility,
|
|
1044
|
+
labels,
|
|
1045
|
+
reasons,
|
|
1046
|
+
visible: isVisibilityIncluded(visibility, options)
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
const defaultVisibility = policy?.defaultVisibility ?? "visible";
|
|
1050
|
+
return {
|
|
1051
|
+
comment,
|
|
1052
|
+
visibility: defaultVisibility,
|
|
1053
|
+
labels,
|
|
1054
|
+
reasons,
|
|
1055
|
+
visible: isVisibilityIncluded(defaultVisibility, options)
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
function moderateThread(thread, options = {}) {
|
|
1059
|
+
const labelIndex = options.labelIndex ?? /* @__PURE__ */ new Map();
|
|
1060
|
+
const root = evaluateCommentModeration(thread.root, {
|
|
1061
|
+
...options,
|
|
1062
|
+
labels: labelIndex.get(thread.root.id)
|
|
1063
|
+
});
|
|
1064
|
+
const replies = thread.replies.map(
|
|
1065
|
+
(reply) => evaluateCommentModeration(reply, {
|
|
1066
|
+
...options,
|
|
1067
|
+
labels: labelIndex.get(reply.id)
|
|
1068
|
+
})
|
|
1069
|
+
);
|
|
1070
|
+
const visibleReplies = replies.filter((reply) => reply.visible);
|
|
1071
|
+
return {
|
|
1072
|
+
root,
|
|
1073
|
+
replies,
|
|
1074
|
+
visibleReplies,
|
|
1075
|
+
visible: root.visible,
|
|
1076
|
+
hiddenReplyCount: countByVisibility(replies, "hidden"),
|
|
1077
|
+
collapsedReplyCount: countByVisibility(replies, "collapsed"),
|
|
1078
|
+
quarantinedReplyCount: countByVisibility(replies, "quarantined")
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function useModeratedThread({
|
|
1082
|
+
thread,
|
|
1083
|
+
labelIndex,
|
|
1084
|
+
policy,
|
|
1085
|
+
hiddenLabels,
|
|
1086
|
+
collapsedLabels,
|
|
1087
|
+
includeCollapsed,
|
|
1088
|
+
includeQuarantined,
|
|
1089
|
+
includeHidden,
|
|
1090
|
+
minimumLabelConfidence
|
|
1091
|
+
}) {
|
|
1092
|
+
return useMemo5(() => {
|
|
1093
|
+
if (!thread) return null;
|
|
1094
|
+
return moderateThread(thread, {
|
|
1095
|
+
labelIndex,
|
|
1096
|
+
policy,
|
|
1097
|
+
hiddenLabels,
|
|
1098
|
+
collapsedLabels,
|
|
1099
|
+
includeCollapsed,
|
|
1100
|
+
includeQuarantined,
|
|
1101
|
+
includeHidden,
|
|
1102
|
+
minimumLabelConfidence
|
|
1103
|
+
});
|
|
1104
|
+
}, [
|
|
1105
|
+
thread,
|
|
1106
|
+
labelIndex,
|
|
1107
|
+
policy,
|
|
1108
|
+
hiddenLabels,
|
|
1109
|
+
collapsedLabels,
|
|
1110
|
+
includeCollapsed,
|
|
1111
|
+
includeQuarantined,
|
|
1112
|
+
includeHidden,
|
|
1113
|
+
minimumLabelConfidence
|
|
1114
|
+
]);
|
|
1115
|
+
}
|
|
1116
|
+
function useVisibleComments(options) {
|
|
1117
|
+
const {
|
|
1118
|
+
viewerDID,
|
|
1119
|
+
isAuthenticated,
|
|
1120
|
+
isVerified,
|
|
1121
|
+
policy: providedPolicy,
|
|
1122
|
+
hiddenLabels,
|
|
1123
|
+
collapsedLabels,
|
|
1124
|
+
includeCollapsed,
|
|
1125
|
+
includeQuarantined,
|
|
1126
|
+
includeHidden,
|
|
1127
|
+
minimumLabelConfidence,
|
|
1128
|
+
...commentOptions
|
|
1129
|
+
} = options;
|
|
1130
|
+
const base = useComments(commentOptions);
|
|
1131
|
+
const { store, isReady } = useNodeStore();
|
|
1132
|
+
const [moderationLabels, setModerationLabels] = useState3([]);
|
|
1133
|
+
const [loadedPolicy, setLoadedPolicy] = useState3(null);
|
|
1134
|
+
const [policyLoading, setPolicyLoading] = useState3(true);
|
|
1135
|
+
const [policyError, setPolicyError] = useState3(null);
|
|
1136
|
+
const commentIds = useMemo5(
|
|
1137
|
+
() => new Set(base.comments.map((comment) => comment.id)),
|
|
1138
|
+
[base.comments]
|
|
1139
|
+
);
|
|
1140
|
+
const loadModerationState = useCallback3(async () => {
|
|
1141
|
+
if (!store || !isReady) {
|
|
1142
|
+
setPolicyLoading(false);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
try {
|
|
1146
|
+
setPolicyLoading(true);
|
|
1147
|
+
setPolicyError(null);
|
|
1148
|
+
const [labelNodes, policyNodes] = await Promise.all([
|
|
1149
|
+
store.list({ schemaId: ModerationLabelSchema._schemaId }),
|
|
1150
|
+
store.list({ schemaId: PublicInteractionPolicySchema._schemaId })
|
|
1151
|
+
]);
|
|
1152
|
+
const labels = labelNodes.map((node) => summarizeModerationLabel(node)).filter((label) => {
|
|
1153
|
+
return label !== null && commentIds.has(label.target);
|
|
1154
|
+
});
|
|
1155
|
+
const policies = policyNodes.map((node) => summarizePublicInteractionPolicy(node)).filter((policy2) => {
|
|
1156
|
+
return policy2 !== null && policy2.target === commentOptions.nodeId;
|
|
1157
|
+
});
|
|
1158
|
+
setModerationLabels(labels);
|
|
1159
|
+
setLoadedPolicy(selectActiveInteractionPolicy(policies));
|
|
1160
|
+
} catch (err) {
|
|
1161
|
+
setPolicyError(err instanceof Error ? err : new Error(String(err)));
|
|
1162
|
+
} finally {
|
|
1163
|
+
setPolicyLoading(false);
|
|
1164
|
+
}
|
|
1165
|
+
}, [store, isReady, commentIds, commentOptions.nodeId]);
|
|
1166
|
+
useEffect3(() => {
|
|
1167
|
+
loadModerationState();
|
|
1168
|
+
}, [loadModerationState]);
|
|
1169
|
+
useEffect3(() => {
|
|
1170
|
+
if (!store || !isReady) return;
|
|
1171
|
+
const handleChange = (event) => {
|
|
1172
|
+
const schemaId = event.node?.schemaId ?? event.change?.payload?.schemaId;
|
|
1173
|
+
if (schemaId === ModerationLabelSchema._schemaId || schemaId === PublicInteractionPolicySchema._schemaId) {
|
|
1174
|
+
loadModerationState();
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
const unsubscribe = store.subscribe(handleChange);
|
|
1178
|
+
return () => unsubscribe();
|
|
1179
|
+
}, [store, isReady, loadModerationState]);
|
|
1180
|
+
const policy = providedPolicy === void 0 ? loadedPolicy : providedPolicy;
|
|
1181
|
+
const labelIndex = useMemo5(() => createModerationLabelIndex(moderationLabels), [moderationLabels]);
|
|
1182
|
+
const filterOptions = useMemo5(
|
|
1183
|
+
() => ({
|
|
1184
|
+
hiddenLabels,
|
|
1185
|
+
collapsedLabels,
|
|
1186
|
+
includeCollapsed,
|
|
1187
|
+
includeQuarantined,
|
|
1188
|
+
includeHidden,
|
|
1189
|
+
minimumLabelConfidence
|
|
1190
|
+
}),
|
|
1191
|
+
[
|
|
1192
|
+
hiddenLabels,
|
|
1193
|
+
collapsedLabels,
|
|
1194
|
+
includeCollapsed,
|
|
1195
|
+
includeQuarantined,
|
|
1196
|
+
includeHidden,
|
|
1197
|
+
minimumLabelConfidence
|
|
1198
|
+
]
|
|
1199
|
+
);
|
|
1200
|
+
const moderatedThreads = useMemo5(
|
|
1201
|
+
() => base.threads.map(
|
|
1202
|
+
(thread) => moderateThread(thread, {
|
|
1203
|
+
...filterOptions,
|
|
1204
|
+
labelIndex,
|
|
1205
|
+
policy
|
|
1206
|
+
})
|
|
1207
|
+
),
|
|
1208
|
+
[base.threads, filterOptions, labelIndex, policy]
|
|
1209
|
+
);
|
|
1210
|
+
const moderationByCommentId = useMemo5(() => {
|
|
1211
|
+
return moderatedThreads.reduce((index, thread) => {
|
|
1212
|
+
index.set(thread.root.comment.id, thread.root);
|
|
1213
|
+
for (const reply of thread.replies) {
|
|
1214
|
+
index.set(reply.comment.id, reply);
|
|
1215
|
+
}
|
|
1216
|
+
return index;
|
|
1217
|
+
}, /* @__PURE__ */ new Map());
|
|
1218
|
+
}, [moderatedThreads]);
|
|
1219
|
+
const threads = useMemo5(
|
|
1220
|
+
() => moderatedThreads.filter((thread) => thread.visible).map((thread) => ({
|
|
1221
|
+
root: thread.root.comment,
|
|
1222
|
+
replies: thread.visibleReplies.map((reply) => reply.comment)
|
|
1223
|
+
})),
|
|
1224
|
+
[moderatedThreads]
|
|
1225
|
+
);
|
|
1226
|
+
const comments = useMemo5(
|
|
1227
|
+
() => threads.flatMap((thread) => [thread.root, ...thread.replies]),
|
|
1228
|
+
[threads]
|
|
1229
|
+
);
|
|
1230
|
+
const canAddRootComment = useMemo5(
|
|
1231
|
+
() => evaluateInteractionPermission(selectPublicInteractionMode(policy, "comment"), policy, {
|
|
1232
|
+
viewerDID,
|
|
1233
|
+
isAuthenticated,
|
|
1234
|
+
isVerified
|
|
1235
|
+
}),
|
|
1236
|
+
[policy, viewerDID, isAuthenticated, isVerified]
|
|
1237
|
+
);
|
|
1238
|
+
const canReply = useMemo5(
|
|
1239
|
+
() => evaluateInteractionPermission(selectPublicInteractionMode(policy, "reply"), policy, {
|
|
1240
|
+
viewerDID,
|
|
1241
|
+
isAuthenticated,
|
|
1242
|
+
isVerified
|
|
1243
|
+
}),
|
|
1244
|
+
[policy, viewerDID, isAuthenticated, isVerified]
|
|
1245
|
+
);
|
|
1246
|
+
const addComment = useCallback3(
|
|
1247
|
+
async (addOptions) => {
|
|
1248
|
+
if (!canAddRootComment.allowed) {
|
|
1249
|
+
setPolicyError(new Error(canAddRootComment.reasons.join(", ") || "Commenting is closed"));
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
return base.addComment(addOptions);
|
|
1253
|
+
},
|
|
1254
|
+
[base, canAddRootComment]
|
|
1255
|
+
);
|
|
1256
|
+
const replyTo = useCallback3(
|
|
1257
|
+
async (rootCommentId, content, context) => {
|
|
1258
|
+
if (!canReply.allowed) {
|
|
1259
|
+
setPolicyError(new Error(canReply.reasons.join(", ") || "Replies are closed"));
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
return base.replyTo(rootCommentId, content, context);
|
|
1263
|
+
},
|
|
1264
|
+
[base, canReply]
|
|
1265
|
+
);
|
|
1266
|
+
return {
|
|
1267
|
+
...base,
|
|
1268
|
+
comments,
|
|
1269
|
+
threads,
|
|
1270
|
+
allComments: base.comments,
|
|
1271
|
+
allThreads: base.threads,
|
|
1272
|
+
moderatedThreads,
|
|
1273
|
+
moderationByCommentId,
|
|
1274
|
+
moderationLabels,
|
|
1275
|
+
policy,
|
|
1276
|
+
policyLoading,
|
|
1277
|
+
policyError,
|
|
1278
|
+
count: comments.length,
|
|
1279
|
+
unresolvedCount: threads.filter((thread) => !thread.root.properties.resolved).length,
|
|
1280
|
+
hiddenCount: countModeratedComments(moderatedThreads, "hidden"),
|
|
1281
|
+
collapsedCount: countModeratedComments(moderatedThreads, "collapsed"),
|
|
1282
|
+
quarantinedCount: countModeratedComments(moderatedThreads, "quarantined"),
|
|
1283
|
+
loading: base.loading || policyLoading,
|
|
1284
|
+
error: base.error ?? policyError,
|
|
1285
|
+
canAddRootComment,
|
|
1286
|
+
canReply,
|
|
1287
|
+
addComment,
|
|
1288
|
+
replyTo
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function filterActiveLabels(labels, policy, minimumLabelConfidence) {
|
|
1292
|
+
const activeLabels = new Set(policy?.activeLabels ?? []);
|
|
1293
|
+
const minimumConfidence = minimumLabelConfidence ?? 0;
|
|
1294
|
+
const negatedLabelIds = new Set(
|
|
1295
|
+
labels.flatMap((label) => label.negates !== void 0 ? [label.negates] : [])
|
|
1296
|
+
);
|
|
1297
|
+
return labels.filter((label) => {
|
|
1298
|
+
if (negatedLabelIds.has(label.id)) return false;
|
|
1299
|
+
if (label.confidence < minimumConfidence) return false;
|
|
1300
|
+
return activeLabels.size === 0 || activeLabels.has(label.value);
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
function maxConfidence(labels, value) {
|
|
1304
|
+
return labels.filter((label) => label.value === value).reduce((confidence, label) => Math.max(confidence, label.confidence), 0);
|
|
1305
|
+
}
|
|
1306
|
+
function countByVisibility(comments, visibility) {
|
|
1307
|
+
return comments.filter((comment) => comment.visibility === visibility).length;
|
|
1308
|
+
}
|
|
1309
|
+
function countModeratedComments(threads, visibility) {
|
|
1310
|
+
return threads.reduce((count, thread) => {
|
|
1311
|
+
const rootCount = thread.root.visibility === visibility ? 1 : 0;
|
|
1312
|
+
return count + rootCount + countByVisibility(thread.replies, visibility);
|
|
1313
|
+
}, 0);
|
|
1314
|
+
}
|
|
1315
|
+
function isVisibilityIncluded(visibility, options) {
|
|
1316
|
+
if (visibility === "visible") return true;
|
|
1317
|
+
if (visibility === "collapsed") return options.includeCollapsed ?? true;
|
|
1318
|
+
if (visibility === "quarantined") return options.includeQuarantined ?? false;
|
|
1319
|
+
return options.includeHidden ?? false;
|
|
1320
|
+
}
|
|
1321
|
+
function asString(value) {
|
|
1322
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1323
|
+
}
|
|
1324
|
+
function asStringArray(value) {
|
|
1325
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1326
|
+
}
|
|
1327
|
+
function asNumber(value) {
|
|
1328
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1329
|
+
}
|
|
1330
|
+
function asBoolean(value, fallback) {
|
|
1331
|
+
return typeof value === "boolean" ? value : fallback;
|
|
1332
|
+
}
|
|
1333
|
+
function asInteractionMode(value, fallback) {
|
|
1334
|
+
return isOneOf(value, ["open", "authenticated", "trusted", "reviewed", "closed"]) ? value : fallback;
|
|
1335
|
+
}
|
|
1336
|
+
function asFirstContactMode(value, fallback) {
|
|
1337
|
+
return isOneOf(value, ["allow", "slow-mode", "quarantine", "review", "block"]) ? value : fallback;
|
|
1338
|
+
}
|
|
1339
|
+
function asModerationMode(value, fallback) {
|
|
1340
|
+
return isOneOf(value, ["off", "label-only", "post-review", "pre-filter", "pre-review"]) ? value : fallback;
|
|
1341
|
+
}
|
|
1342
|
+
function asVisibility(value, fallback) {
|
|
1343
|
+
return isOneOf(value, ["visible", "collapsed", "quarantined", "hidden"]) ? value : fallback;
|
|
1344
|
+
}
|
|
1345
|
+
function isOneOf(value, options) {
|
|
1346
|
+
return typeof value === "string" && options.includes(value);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/hooks/useReactionCounters.ts
|
|
1350
|
+
import { ModerationLabelSchema as ModerationLabelSchema2, ReactionSchema } from "@xnetjs/data";
|
|
1351
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo6, useState as useState4 } from "react";
|
|
1352
|
+
var DEFAULT_HIDDEN_REACTION_LABELS = [
|
|
1353
|
+
"spam",
|
|
1354
|
+
"scam",
|
|
1355
|
+
"malware",
|
|
1356
|
+
"impersonation",
|
|
1357
|
+
"harassment"
|
|
1358
|
+
];
|
|
1359
|
+
function summarizeReactionNode(node) {
|
|
1360
|
+
const target = asString2(node.properties.target);
|
|
1361
|
+
const reactionType = asReactionType(node.properties.reactionType);
|
|
1362
|
+
const reactor = asString2(node.properties.reactor);
|
|
1363
|
+
if (!target || !reactionType || !reactor) return null;
|
|
1364
|
+
return {
|
|
1365
|
+
id: node.id,
|
|
1366
|
+
target,
|
|
1367
|
+
targetSchema: asString2(node.properties.targetSchema),
|
|
1368
|
+
reactionType,
|
|
1369
|
+
reactor,
|
|
1370
|
+
emoji: asString2(node.properties.emoji),
|
|
1371
|
+
annotation: asString2(node.properties.annotation),
|
|
1372
|
+
createdAt: node.createdAt,
|
|
1373
|
+
createdBy: node.createdBy
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function isReactionVisible(reaction, labels, policy, options = {}) {
|
|
1377
|
+
if (policy?.blockedDIDs.includes(reaction.reactor)) return false;
|
|
1378
|
+
if (policy?.mutedDIDs.includes(reaction.reactor)) return false;
|
|
1379
|
+
if (policy?.moderationMode === "off") return true;
|
|
1380
|
+
if (policy?.defaultVisibility === "hidden" && !(options.includeHidden ?? false)) return false;
|
|
1381
|
+
if (policy?.defaultVisibility === "quarantined" && !(options.includeQuarantined ?? false)) {
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
const activeLabels = new Set(policy?.activeLabels ?? []);
|
|
1385
|
+
const hiddenLabels = new Set(options.hiddenLabels ?? DEFAULT_HIDDEN_REACTION_LABELS);
|
|
1386
|
+
const hideThreshold = policy?.hideConfidenceThreshold ?? 0.85;
|
|
1387
|
+
const safeConfidence = labels.filter((label) => label.value === "safe").reduce((confidence, label) => Math.max(confidence, label.confidence), 0);
|
|
1388
|
+
return !labels.some((label) => {
|
|
1389
|
+
if (activeLabels.size > 0 && !activeLabels.has(label.value)) return false;
|
|
1390
|
+
if (!hiddenLabels.has(label.value)) return false;
|
|
1391
|
+
return label.confidence >= hideThreshold && label.confidence > safeConfidence;
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
function dedupeReactions(reactions) {
|
|
1395
|
+
const byActorAndType = reactions.reduce((index, reaction) => {
|
|
1396
|
+
const key = [reaction.reactionType, reaction.reactor, reaction.emoji ?? ""].join(":");
|
|
1397
|
+
const existing = index.get(key);
|
|
1398
|
+
if (!existing || reaction.createdAt >= existing.createdAt) {
|
|
1399
|
+
index.set(key, reaction);
|
|
1400
|
+
}
|
|
1401
|
+
return index;
|
|
1402
|
+
}, /* @__PURE__ */ new Map());
|
|
1403
|
+
return Array.from(byActorAndType.values());
|
|
1404
|
+
}
|
|
1405
|
+
function createReactionCounterSnapshot(reactions, replies) {
|
|
1406
|
+
const unique = dedupeReactions(reactions);
|
|
1407
|
+
const countType = (reactionType) => {
|
|
1408
|
+
return unique.filter((reaction) => reaction.reactionType === reactionType).length;
|
|
1409
|
+
};
|
|
1410
|
+
const totalReactions = unique.length;
|
|
1411
|
+
return {
|
|
1412
|
+
likes: countType("like"),
|
|
1413
|
+
reposts: countType("repost"),
|
|
1414
|
+
bookmarks: countType("bookmark"),
|
|
1415
|
+
emoji: countType("emoji"),
|
|
1416
|
+
replies,
|
|
1417
|
+
totalReactions,
|
|
1418
|
+
total: totalReactions + replies
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
function usePolicyFilteredReactionCounters({
|
|
1422
|
+
nodeId,
|
|
1423
|
+
targetSchema,
|
|
1424
|
+
viewerDID,
|
|
1425
|
+
isAuthenticated,
|
|
1426
|
+
isVerified,
|
|
1427
|
+
policy: providedPolicy,
|
|
1428
|
+
hiddenLabels,
|
|
1429
|
+
collapsedLabels,
|
|
1430
|
+
includeCollapsed,
|
|
1431
|
+
includeQuarantined,
|
|
1432
|
+
includeHidden,
|
|
1433
|
+
minimumLabelConfidence
|
|
1434
|
+
}) {
|
|
1435
|
+
const { store, isReady } = useNodeStore();
|
|
1436
|
+
const visibleComments = useVisibleComments({
|
|
1437
|
+
nodeId,
|
|
1438
|
+
viewerDID,
|
|
1439
|
+
isAuthenticated,
|
|
1440
|
+
isVerified,
|
|
1441
|
+
policy: providedPolicy,
|
|
1442
|
+
hiddenLabels,
|
|
1443
|
+
collapsedLabels,
|
|
1444
|
+
includeCollapsed,
|
|
1445
|
+
includeQuarantined,
|
|
1446
|
+
includeHidden,
|
|
1447
|
+
minimumLabelConfidence
|
|
1448
|
+
});
|
|
1449
|
+
const [reactions, setReactions] = useState4([]);
|
|
1450
|
+
const [labels, setLabels] = useState4([]);
|
|
1451
|
+
const [loading, setLoading] = useState4(true);
|
|
1452
|
+
const [error, setError] = useState4(null);
|
|
1453
|
+
const policy = providedPolicy === void 0 ? visibleComments.policy : providedPolicy;
|
|
1454
|
+
const loadReactions = useCallback4(async () => {
|
|
1455
|
+
if (!store || !isReady) {
|
|
1456
|
+
setLoading(false);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
try {
|
|
1460
|
+
setLoading(true);
|
|
1461
|
+
setError(null);
|
|
1462
|
+
const [reactionNodes, labelNodes] = await Promise.all([
|
|
1463
|
+
store.list({ schemaId: ReactionSchema._schemaId }),
|
|
1464
|
+
store.list({ schemaId: ModerationLabelSchema2._schemaId })
|
|
1465
|
+
]);
|
|
1466
|
+
const nextReactions = reactionNodes.map((node) => summarizeReactionNode(node)).filter((reaction) => {
|
|
1467
|
+
return reaction !== null && reaction.target === nodeId;
|
|
1468
|
+
});
|
|
1469
|
+
const reactionIds = new Set(nextReactions.map((reaction) => reaction.id));
|
|
1470
|
+
const nextLabels = labelNodes.map((node) => summarizeModerationLabel(node)).filter((label) => {
|
|
1471
|
+
return label !== null && reactionIds.has(label.target);
|
|
1472
|
+
});
|
|
1473
|
+
setReactions(nextReactions);
|
|
1474
|
+
setLabels(nextLabels);
|
|
1475
|
+
} catch (err) {
|
|
1476
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1477
|
+
} finally {
|
|
1478
|
+
setLoading(false);
|
|
1479
|
+
}
|
|
1480
|
+
}, [store, isReady, nodeId]);
|
|
1481
|
+
useEffect4(() => {
|
|
1482
|
+
loadReactions();
|
|
1483
|
+
}, [loadReactions]);
|
|
1484
|
+
useEffect4(() => {
|
|
1485
|
+
if (!store || !isReady) return;
|
|
1486
|
+
const handleChange = (event) => {
|
|
1487
|
+
const schemaId = event.node?.schemaId ?? event.change?.payload?.schemaId;
|
|
1488
|
+
if (schemaId === ReactionSchema._schemaId || schemaId === ModerationLabelSchema2._schemaId) {
|
|
1489
|
+
loadReactions();
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
const unsubscribe = store.subscribe(handleChange);
|
|
1493
|
+
return () => unsubscribe();
|
|
1494
|
+
}, [store, isReady, loadReactions]);
|
|
1495
|
+
const labelIndex = useMemo6(() => createModerationLabelIndex(labels), [labels]);
|
|
1496
|
+
const filterOptions = useMemo6(
|
|
1497
|
+
() => ({
|
|
1498
|
+
hiddenLabels,
|
|
1499
|
+
collapsedLabels,
|
|
1500
|
+
includeCollapsed,
|
|
1501
|
+
includeQuarantined,
|
|
1502
|
+
includeHidden,
|
|
1503
|
+
minimumLabelConfidence
|
|
1504
|
+
}),
|
|
1505
|
+
[
|
|
1506
|
+
hiddenLabels,
|
|
1507
|
+
collapsedLabels,
|
|
1508
|
+
includeCollapsed,
|
|
1509
|
+
includeQuarantined,
|
|
1510
|
+
includeHidden,
|
|
1511
|
+
minimumLabelConfidence
|
|
1512
|
+
]
|
|
1513
|
+
);
|
|
1514
|
+
const visibleReactions = useMemo6(() => {
|
|
1515
|
+
return reactions.filter(
|
|
1516
|
+
(reaction) => isReactionVisible(reaction, labelIndex.get(reaction.id) ?? [], policy, filterOptions)
|
|
1517
|
+
);
|
|
1518
|
+
}, [reactions, labelIndex, policy, filterOptions]);
|
|
1519
|
+
const canReact = useMemo6(
|
|
1520
|
+
() => evaluateInteractionPermission(selectPublicInteractionMode(policy, "reaction"), policy, {
|
|
1521
|
+
viewerDID,
|
|
1522
|
+
isAuthenticated,
|
|
1523
|
+
isVerified
|
|
1524
|
+
}),
|
|
1525
|
+
[policy, viewerDID, isAuthenticated, isVerified]
|
|
1526
|
+
);
|
|
1527
|
+
const canRepost = useMemo6(
|
|
1528
|
+
() => evaluateInteractionPermission(selectPublicInteractionMode(policy, "quote"), policy, {
|
|
1529
|
+
viewerDID,
|
|
1530
|
+
isAuthenticated,
|
|
1531
|
+
isVerified
|
|
1532
|
+
}),
|
|
1533
|
+
[policy, viewerDID, isAuthenticated, isVerified]
|
|
1534
|
+
);
|
|
1535
|
+
const counts = useMemo6(
|
|
1536
|
+
() => createReactionCounterSnapshot(visibleReactions, visibleComments.count),
|
|
1537
|
+
[visibleReactions, visibleComments.count]
|
|
1538
|
+
);
|
|
1539
|
+
const rawCounts = useMemo6(
|
|
1540
|
+
() => createReactionCounterSnapshot(reactions, visibleComments.allComments.length),
|
|
1541
|
+
[reactions, visibleComments.allComments.length]
|
|
1542
|
+
);
|
|
1543
|
+
const addReaction = useCallback4(
|
|
1544
|
+
async (options) => {
|
|
1545
|
+
if (!store || !isReady || !viewerDID) return null;
|
|
1546
|
+
const permission = options.reactionType === "repost" ? canRepost : canReact;
|
|
1547
|
+
if (!permission.allowed) {
|
|
1548
|
+
setError(new Error(permission.reasons.join(", ") || "Reactions are closed"));
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
try {
|
|
1552
|
+
const node = await store.create({
|
|
1553
|
+
schemaId: ReactionSchema._schemaId,
|
|
1554
|
+
properties: {
|
|
1555
|
+
target: nodeId,
|
|
1556
|
+
targetSchema,
|
|
1557
|
+
reactionType: options.reactionType,
|
|
1558
|
+
reactor: viewerDID,
|
|
1559
|
+
emoji: options.emoji,
|
|
1560
|
+
annotation: options.annotation
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
return node.id;
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
},
|
|
1569
|
+
[store, isReady, viewerDID, canRepost, canReact, nodeId, targetSchema]
|
|
1570
|
+
);
|
|
1571
|
+
const removeReaction = useCallback4(
|
|
1572
|
+
async (reactionId) => {
|
|
1573
|
+
if (!store || !isReady) return;
|
|
1574
|
+
try {
|
|
1575
|
+
await store.delete(reactionId);
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1578
|
+
}
|
|
1579
|
+
},
|
|
1580
|
+
[store, isReady]
|
|
1581
|
+
);
|
|
1582
|
+
const toggleReaction = useCallback4(
|
|
1583
|
+
async (options) => {
|
|
1584
|
+
const existing = reactions.find((reaction) => {
|
|
1585
|
+
return reaction.reactor === viewerDID && reaction.reactionType === options.reactionType && (reaction.emoji ?? "") === (options.emoji ?? "");
|
|
1586
|
+
});
|
|
1587
|
+
if (existing) {
|
|
1588
|
+
await removeReaction(existing.id);
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
return addReaction(options);
|
|
1592
|
+
},
|
|
1593
|
+
[reactions, viewerDID, removeReaction, addReaction]
|
|
1594
|
+
);
|
|
1595
|
+
return {
|
|
1596
|
+
reactions,
|
|
1597
|
+
visibleReactions,
|
|
1598
|
+
counts,
|
|
1599
|
+
rawCounts,
|
|
1600
|
+
hiddenReactionCount: reactions.length - visibleReactions.length,
|
|
1601
|
+
loading: loading || visibleComments.loading,
|
|
1602
|
+
error: error ?? visibleComments.error,
|
|
1603
|
+
policy,
|
|
1604
|
+
canReact,
|
|
1605
|
+
canRepost,
|
|
1606
|
+
addReaction,
|
|
1607
|
+
removeReaction,
|
|
1608
|
+
toggleReaction,
|
|
1609
|
+
reload: loadReactions
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
function asString2(value) {
|
|
1613
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1614
|
+
}
|
|
1615
|
+
function asReactionType(value) {
|
|
1616
|
+
return isOneOf2(value, ["like", "repost", "bookmark", "emoji"]) ? value : void 0;
|
|
1617
|
+
}
|
|
1618
|
+
function isOneOf2(value, options) {
|
|
1619
|
+
return typeof value === "string" && options.includes(value);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/hooks/useMessageRequests.ts
|
|
1623
|
+
import { MessageRequestSchema } from "@xnetjs/data";
|
|
1624
|
+
import { useCallback as useCallback5, useEffect as useEffect5, useMemo as useMemo7, useState as useState5 } from "react";
|
|
1625
|
+
var REQUEST_STATUSES = [
|
|
1626
|
+
"pending",
|
|
1627
|
+
"accepted",
|
|
1628
|
+
"declined",
|
|
1629
|
+
"quarantined",
|
|
1630
|
+
"blocked",
|
|
1631
|
+
"expired"
|
|
1632
|
+
];
|
|
1633
|
+
var ADMISSIONS = [
|
|
1634
|
+
"allow",
|
|
1635
|
+
"message-request",
|
|
1636
|
+
"quarantine",
|
|
1637
|
+
"review",
|
|
1638
|
+
"block"
|
|
1639
|
+
];
|
|
1640
|
+
var ACTIVE_REQUEST_STATUSES = ["pending", "quarantined"];
|
|
1641
|
+
function createConversationKey(senderDID, recipientDID) {
|
|
1642
|
+
return [senderDID, recipientDID].sort().join("::");
|
|
1643
|
+
}
|
|
1644
|
+
function summarizeMessageRequest(node, now = Date.now()) {
|
|
1645
|
+
const conversationKey = asString3(node.properties.conversationKey);
|
|
1646
|
+
const sender = asString3(node.properties.sender);
|
|
1647
|
+
const recipient = asString3(node.properties.recipient);
|
|
1648
|
+
if (!conversationKey || !sender || !recipient) return null;
|
|
1649
|
+
const expiresAt = asNumber2(node.properties.expiresAt);
|
|
1650
|
+
const rawStatus = asRequestStatus(node.properties.status) ?? "pending";
|
|
1651
|
+
const status = expiresAt !== void 0 && expiresAt <= now && ACTIVE_REQUEST_STATUSES.includes(rawStatus) ? "expired" : rawStatus;
|
|
1652
|
+
return {
|
|
1653
|
+
id: node.id,
|
|
1654
|
+
conversationKey,
|
|
1655
|
+
sender,
|
|
1656
|
+
recipient,
|
|
1657
|
+
target: asString3(node.properties.target),
|
|
1658
|
+
targetSchema: asString3(node.properties.targetSchema),
|
|
1659
|
+
firstMessageRef: asString3(node.properties.firstMessageRef),
|
|
1660
|
+
firstMessagePreview: asString3(node.properties.firstMessagePreview),
|
|
1661
|
+
status,
|
|
1662
|
+
admission: asAdmission(node.properties.admission) ?? "message-request",
|
|
1663
|
+
reasonCodes: asStringArray2(node.properties.reasonCodes),
|
|
1664
|
+
confidence: asNumber2(node.properties.confidence),
|
|
1665
|
+
policy: asString3(node.properties.policy),
|
|
1666
|
+
policyMode: asFirstContactMode2(node.properties.policyMode),
|
|
1667
|
+
quarantineUntil: asNumber2(node.properties.quarantineUntil),
|
|
1668
|
+
expiresAt,
|
|
1669
|
+
respondedAt: asNumber2(node.properties.respondedAt),
|
|
1670
|
+
respondedBy: asString3(node.properties.respondedBy),
|
|
1671
|
+
reviewers: asStringArray2(node.properties.reviewers),
|
|
1672
|
+
notes: asString3(node.properties.notes),
|
|
1673
|
+
createdAt: node.createdAt,
|
|
1674
|
+
createdBy: node.createdBy
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
function findLatestMessageRequest(requests, senderDID, recipientDID) {
|
|
1678
|
+
const conversationKey = createConversationKey(senderDID, recipientDID);
|
|
1679
|
+
return requests.filter((request) => request.conversationKey === conversationKey).sort((a, b) => b.createdAt - a.createdAt)[0] ?? null;
|
|
1680
|
+
}
|
|
1681
|
+
function hasAcceptedContact(requests, senderDID, recipientDID) {
|
|
1682
|
+
return findLatestMessageRequest(requests, senderDID, recipientDID)?.status === "accepted";
|
|
1683
|
+
}
|
|
1684
|
+
function evaluateFirstContactDecision({
|
|
1685
|
+
senderDID,
|
|
1686
|
+
recipientDID,
|
|
1687
|
+
policy,
|
|
1688
|
+
existingRequests = [],
|
|
1689
|
+
isAuthenticated,
|
|
1690
|
+
isVerified = false,
|
|
1691
|
+
now = Date.now()
|
|
1692
|
+
}) {
|
|
1693
|
+
if (!senderDID || !recipientDID) {
|
|
1694
|
+
return createFirstContactDecision("block", ["missing-participants"], 1);
|
|
1695
|
+
}
|
|
1696
|
+
if (senderDID === recipientDID) {
|
|
1697
|
+
return createFirstContactDecision("allow", ["self-message"], 1);
|
|
1698
|
+
}
|
|
1699
|
+
const latest = findLatestMessageRequest(existingRequests, senderDID, recipientDID);
|
|
1700
|
+
if (latest?.status === "accepted") {
|
|
1701
|
+
return createFirstContactDecision("allow", ["known-contact"], 1);
|
|
1702
|
+
}
|
|
1703
|
+
if (policy?.blockedDIDs.includes(senderDID) || latest?.status === "blocked") {
|
|
1704
|
+
return createFirstContactDecision("block", ["sender-blocked"], 1);
|
|
1705
|
+
}
|
|
1706
|
+
if (policy?.mutedDIDs.includes(senderDID)) {
|
|
1707
|
+
return createFirstContactDecision("quarantine", ["sender-muted"], 0.9);
|
|
1708
|
+
}
|
|
1709
|
+
if ((policy?.requiresVerifiedIdentity ?? false) && (!isAuthenticated || !isVerified)) {
|
|
1710
|
+
return createFirstContactDecision("review", ["verified-identity-required"], 0.8);
|
|
1711
|
+
}
|
|
1712
|
+
const messagePermission = evaluateInteractionPermission(
|
|
1713
|
+
selectPublicInteractionMode(policy ?? null, "message"),
|
|
1714
|
+
policy ?? null,
|
|
1715
|
+
{
|
|
1716
|
+
viewerDID: senderDID,
|
|
1717
|
+
isAuthenticated: isAuthenticated ?? Boolean(senderDID),
|
|
1718
|
+
isVerified
|
|
1719
|
+
}
|
|
1720
|
+
);
|
|
1721
|
+
if (!messagePermission.allowed) {
|
|
1722
|
+
return createFirstContactDecision(
|
|
1723
|
+
"block",
|
|
1724
|
+
["message-policy-denied", ...messagePermission.reasons],
|
|
1725
|
+
0.95
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
if (messagePermission.requiresReview) {
|
|
1729
|
+
return createFirstContactDecision("review", ["message-review-required"], 0.75);
|
|
1730
|
+
}
|
|
1731
|
+
if (policy?.trustedDIDs.includes(senderDID)) {
|
|
1732
|
+
return createFirstContactDecision("allow", ["trusted-sender"], 0.95);
|
|
1733
|
+
}
|
|
1734
|
+
const mode = policy?.firstContactMode ?? "slow-mode";
|
|
1735
|
+
const reasons = ["first-contact", `policy-${mode}`];
|
|
1736
|
+
if (mode === "allow") return createFirstContactDecision("allow", reasons, 0.8);
|
|
1737
|
+
if (mode === "block") return createFirstContactDecision("block", reasons, 0.95);
|
|
1738
|
+
if (mode === "review") return createFirstContactDecision("review", reasons, 0.75);
|
|
1739
|
+
if (mode === "quarantine") return createFirstContactDecision("quarantine", reasons, 0.85);
|
|
1740
|
+
return createFirstContactDecision("message-request", reasons, 0.6, {
|
|
1741
|
+
quarantineUntil: policy?.slowModeSeconds !== void 0 ? now + policy.slowModeSeconds * 1e3 : void 0
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
function createMessageRequestProperties(options, decision, policy) {
|
|
1745
|
+
return {
|
|
1746
|
+
conversationKey: createConversationKey(options.senderDID, options.recipientDID),
|
|
1747
|
+
sender: options.senderDID,
|
|
1748
|
+
recipient: options.recipientDID,
|
|
1749
|
+
target: options.target,
|
|
1750
|
+
targetSchema: options.targetSchema,
|
|
1751
|
+
firstMessageRef: options.firstMessageRef,
|
|
1752
|
+
firstMessagePreview: truncatePreview(options.firstMessagePreview),
|
|
1753
|
+
status: decision.status,
|
|
1754
|
+
admission: decision.admission,
|
|
1755
|
+
reasonCodes: decision.reasonCodes,
|
|
1756
|
+
confidence: decision.confidence,
|
|
1757
|
+
policy: policy?.id,
|
|
1758
|
+
policyMode: policy?.firstContactMode,
|
|
1759
|
+
quarantineUntil: decision.quarantineUntil,
|
|
1760
|
+
expiresAt: options.expiresAt,
|
|
1761
|
+
reviewers: [...options.reviewers ?? []],
|
|
1762
|
+
notes: options.notes
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
function useMessageRequests({
|
|
1766
|
+
recipientDID,
|
|
1767
|
+
senderDID,
|
|
1768
|
+
policy,
|
|
1769
|
+
isAuthenticated,
|
|
1770
|
+
isVerified,
|
|
1771
|
+
includeResolved = false
|
|
1772
|
+
} = {}) {
|
|
1773
|
+
const { store, isReady } = useNodeStore();
|
|
1774
|
+
const [requests, setRequests] = useState5([]);
|
|
1775
|
+
const [loading, setLoading] = useState5(true);
|
|
1776
|
+
const [error, setError] = useState5(null);
|
|
1777
|
+
const loadRequests = useCallback5(async () => {
|
|
1778
|
+
if (!store || !isReady) {
|
|
1779
|
+
setLoading(false);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
try {
|
|
1783
|
+
setLoading(true);
|
|
1784
|
+
setError(null);
|
|
1785
|
+
const nodes = await store.list({ schemaId: MessageRequestSchema._schemaId });
|
|
1786
|
+
const nextRequests = nodes.map((node) => summarizeMessageRequest(node)).filter((request) => {
|
|
1787
|
+
if (request === null) return false;
|
|
1788
|
+
if (recipientDID && request.recipient !== recipientDID) return false;
|
|
1789
|
+
if (senderDID && request.sender !== senderDID) return false;
|
|
1790
|
+
if (!includeResolved && !ACTIVE_REQUEST_STATUSES.includes(request.status)) return false;
|
|
1791
|
+
return true;
|
|
1792
|
+
}).sort((a, b) => b.createdAt - a.createdAt);
|
|
1793
|
+
setRequests(nextRequests);
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1796
|
+
} finally {
|
|
1797
|
+
setLoading(false);
|
|
1798
|
+
}
|
|
1799
|
+
}, [store, isReady, recipientDID, senderDID, includeResolved]);
|
|
1800
|
+
useEffect5(() => {
|
|
1801
|
+
loadRequests();
|
|
1802
|
+
}, [loadRequests]);
|
|
1803
|
+
useEffect5(() => {
|
|
1804
|
+
if (!store || !isReady) return;
|
|
1805
|
+
const handleChange = (event) => {
|
|
1806
|
+
const schemaId = event.node?.schemaId ?? event.change?.payload?.schemaId;
|
|
1807
|
+
if (schemaId === MessageRequestSchema._schemaId) {
|
|
1808
|
+
loadRequests();
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
const unsubscribe = store.subscribe(handleChange);
|
|
1812
|
+
return () => unsubscribe();
|
|
1813
|
+
}, [store, isReady, loadRequests]);
|
|
1814
|
+
const evaluateFirstContact = useCallback5(
|
|
1815
|
+
(input) => evaluateFirstContactDecision({
|
|
1816
|
+
policy,
|
|
1817
|
+
existingRequests: requests,
|
|
1818
|
+
isAuthenticated,
|
|
1819
|
+
isVerified,
|
|
1820
|
+
...input
|
|
1821
|
+
}),
|
|
1822
|
+
[policy, requests, isAuthenticated, isVerified]
|
|
1823
|
+
);
|
|
1824
|
+
const createMessageRequest = useCallback5(
|
|
1825
|
+
async (options) => {
|
|
1826
|
+
if (!store || !isReady) return null;
|
|
1827
|
+
const decision = evaluateFirstContact({
|
|
1828
|
+
senderDID: options.senderDID,
|
|
1829
|
+
recipientDID: options.recipientDID
|
|
1830
|
+
});
|
|
1831
|
+
if (!decision.shouldCreateRequest) return null;
|
|
1832
|
+
try {
|
|
1833
|
+
const node = await store.create({
|
|
1834
|
+
schemaId: MessageRequestSchema._schemaId,
|
|
1835
|
+
properties: createMessageRequestProperties(options, decision, policy)
|
|
1836
|
+
});
|
|
1837
|
+
return node.id;
|
|
1838
|
+
} catch (err) {
|
|
1839
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
},
|
|
1843
|
+
[store, isReady, evaluateFirstContact, policy]
|
|
1844
|
+
);
|
|
1845
|
+
const setRequestStatus = useCallback5(
|
|
1846
|
+
async (requestId, status, responderDID) => {
|
|
1847
|
+
if (!store || !isReady) return;
|
|
1848
|
+
try {
|
|
1849
|
+
await store.update(requestId, {
|
|
1850
|
+
properties: {
|
|
1851
|
+
status,
|
|
1852
|
+
respondedAt: Date.now(),
|
|
1853
|
+
respondedBy: responderDID
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1858
|
+
}
|
|
1859
|
+
},
|
|
1860
|
+
[store, isReady]
|
|
1861
|
+
);
|
|
1862
|
+
const pendingRequests = useMemo7(
|
|
1863
|
+
() => requests.filter((request) => request.status === "pending"),
|
|
1864
|
+
[requests]
|
|
1865
|
+
);
|
|
1866
|
+
const quarantinedRequests = useMemo7(
|
|
1867
|
+
() => requests.filter((request) => request.status === "quarantined"),
|
|
1868
|
+
[requests]
|
|
1869
|
+
);
|
|
1870
|
+
const acceptedRequests = useMemo7(
|
|
1871
|
+
() => requests.filter((request) => request.status === "accepted"),
|
|
1872
|
+
[requests]
|
|
1873
|
+
);
|
|
1874
|
+
const blockedRequests = useMemo7(
|
|
1875
|
+
() => requests.filter((request) => request.status === "blocked"),
|
|
1876
|
+
[requests]
|
|
1877
|
+
);
|
|
1878
|
+
return {
|
|
1879
|
+
requests,
|
|
1880
|
+
pendingRequests,
|
|
1881
|
+
quarantinedRequests,
|
|
1882
|
+
acceptedRequests,
|
|
1883
|
+
blockedRequests,
|
|
1884
|
+
loading,
|
|
1885
|
+
error,
|
|
1886
|
+
evaluateFirstContact,
|
|
1887
|
+
createMessageRequest,
|
|
1888
|
+
acceptRequest: (requestId, responderDID) => setRequestStatus(requestId, "accepted", responderDID),
|
|
1889
|
+
declineRequest: (requestId, responderDID) => setRequestStatus(requestId, "declined", responderDID),
|
|
1890
|
+
quarantineRequest: (requestId, responderDID) => setRequestStatus(requestId, "quarantined", responderDID),
|
|
1891
|
+
blockRequest: (requestId, responderDID) => setRequestStatus(requestId, "blocked", responderDID),
|
|
1892
|
+
reload: loadRequests
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
function createFirstContactDecision(admission, reasons, confidence, options = {}) {
|
|
1896
|
+
return {
|
|
1897
|
+
admission,
|
|
1898
|
+
status: statusForAdmission(admission),
|
|
1899
|
+
visibility: visibilityForAdmission(admission),
|
|
1900
|
+
notifyRecipient: admission === "message-request" || admission === "review",
|
|
1901
|
+
requiresReview: admission === "review" || admission === "quarantine",
|
|
1902
|
+
shouldCreateRequest: admission === "message-request" || admission === "review" || admission === "quarantine",
|
|
1903
|
+
reasons: [...reasons],
|
|
1904
|
+
reasonCodes: normalizeReasonCodes(reasons),
|
|
1905
|
+
confidence,
|
|
1906
|
+
quarantineUntil: options.quarantineUntil
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
function statusForAdmission(admission) {
|
|
1910
|
+
if (admission === "allow") return "accepted";
|
|
1911
|
+
if (admission === "block") return "blocked";
|
|
1912
|
+
if (admission === "quarantine") return "quarantined";
|
|
1913
|
+
return "pending";
|
|
1914
|
+
}
|
|
1915
|
+
function visibilityForAdmission(admission) {
|
|
1916
|
+
if (admission === "allow") return "inbox";
|
|
1917
|
+
if (admission === "message-request" || admission === "review") return "requests";
|
|
1918
|
+
if (admission === "quarantine") return "quarantine";
|
|
1919
|
+
return "hidden";
|
|
1920
|
+
}
|
|
1921
|
+
function normalizeReasonCodes(reasons) {
|
|
1922
|
+
return reasons.map((reason) => {
|
|
1923
|
+
if (reason === "policy-slow-mode") return "policy-slow-mode";
|
|
1924
|
+
if (reason.startsWith("policy-")) return reason;
|
|
1925
|
+
return reason;
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
function truncatePreview(preview) {
|
|
1929
|
+
if (preview === void 0) return void 0;
|
|
1930
|
+
return preview.length > 1e3 ? preview.slice(0, 1e3) : preview;
|
|
1931
|
+
}
|
|
1932
|
+
function asString3(value) {
|
|
1933
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1934
|
+
}
|
|
1935
|
+
function asNumber2(value) {
|
|
1936
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1937
|
+
}
|
|
1938
|
+
function asStringArray2(value) {
|
|
1939
|
+
if (!Array.isArray(value)) return [];
|
|
1940
|
+
return value.filter((item) => typeof item === "string");
|
|
1941
|
+
}
|
|
1942
|
+
function asRequestStatus(value) {
|
|
1943
|
+
return isOneOf3(value, REQUEST_STATUSES) ? value : void 0;
|
|
1944
|
+
}
|
|
1945
|
+
function asAdmission(value) {
|
|
1946
|
+
return isOneOf3(value, ADMISSIONS) ? value : void 0;
|
|
1947
|
+
}
|
|
1948
|
+
function asFirstContactMode2(value) {
|
|
1949
|
+
return isOneOf3(value, ["allow", "slow-mode", "quarantine", "review", "block"]) ? value : void 0;
|
|
1950
|
+
}
|
|
1951
|
+
function isOneOf3(value, options) {
|
|
1952
|
+
return typeof value === "string" && options.includes(value);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// src/hooks/useCommentCount.ts
|
|
1956
|
+
import { useMemo as useMemo8 } from "react";
|
|
1957
|
+
function useCommentCount(nodeId) {
|
|
1958
|
+
const { unresolvedCount } = useComments({ nodeId });
|
|
1959
|
+
return unresolvedCount;
|
|
1960
|
+
}
|
|
1961
|
+
function useCommentCounts(nodeId) {
|
|
1962
|
+
const { threads } = useComments({ nodeId });
|
|
1963
|
+
return useMemo8(() => {
|
|
1964
|
+
const resolved = threads.filter((t) => t.root.properties.resolved).length;
|
|
1965
|
+
const unresolved = threads.length - resolved;
|
|
1966
|
+
return {
|
|
1967
|
+
total: threads.length,
|
|
1968
|
+
unresolved,
|
|
1969
|
+
resolved
|
|
1970
|
+
};
|
|
1971
|
+
}, [threads]);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// src/hooks/useHistory.ts
|
|
1975
|
+
import {
|
|
1976
|
+
HistoryEngine,
|
|
1977
|
+
SnapshotCache,
|
|
1978
|
+
MemorySnapshotStorage
|
|
1979
|
+
} from "@xnetjs/history";
|
|
1980
|
+
import { useState as useState6, useEffect as useEffect6, useCallback as useCallback6, useRef as useRef3 } from "react";
|
|
1981
|
+
function useHistory(nodeId) {
|
|
1982
|
+
const { store, isReady } = useNodeStore();
|
|
1983
|
+
const [timeline, setTimeline] = useState6([]);
|
|
1984
|
+
const [loading, setLoading] = useState6(false);
|
|
1985
|
+
const [error, setError] = useState6(null);
|
|
1986
|
+
const engineRef = useRef3(null);
|
|
1987
|
+
const getEngine = useCallback6(() => {
|
|
1988
|
+
if (!store) return null;
|
|
1989
|
+
const storage = store.getStorageAdapter();
|
|
1990
|
+
if (!storage) return null;
|
|
1991
|
+
if (!engineRef.current || engineRef.current.storage !== storage) {
|
|
1992
|
+
const snapshotStorage = new MemorySnapshotStorage();
|
|
1993
|
+
const snapshots = new SnapshotCache(snapshotStorage, { interval: 50 });
|
|
1994
|
+
engineRef.current = { engine: new HistoryEngine(storage, snapshots), storage };
|
|
1995
|
+
}
|
|
1996
|
+
return engineRef.current.engine;
|
|
1997
|
+
}, [store]);
|
|
1998
|
+
const loadTimeline = useCallback6(async () => {
|
|
1999
|
+
if (!nodeId || !isReady) return;
|
|
2000
|
+
const engine = getEngine();
|
|
2001
|
+
if (!engine) return;
|
|
2002
|
+
setLoading(true);
|
|
2003
|
+
setError(null);
|
|
2004
|
+
try {
|
|
2005
|
+
const entries = await engine.getTimeline(nodeId);
|
|
2006
|
+
setTimeline(entries);
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2009
|
+
} finally {
|
|
2010
|
+
setLoading(false);
|
|
2011
|
+
}
|
|
2012
|
+
}, [nodeId, isReady, getEngine]);
|
|
2013
|
+
useEffect6(() => {
|
|
2014
|
+
loadTimeline();
|
|
2015
|
+
}, [loadTimeline]);
|
|
2016
|
+
useEffect6(() => {
|
|
2017
|
+
if (!store || !nodeId) return;
|
|
2018
|
+
const unsub = store.subscribe((event) => {
|
|
2019
|
+
if (event.change.payload.nodeId === nodeId) {
|
|
2020
|
+
loadTimeline();
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
return unsub;
|
|
2024
|
+
}, [store, nodeId, loadTimeline]);
|
|
2025
|
+
const materializeAt = useCallback6(
|
|
2026
|
+
async (target) => {
|
|
2027
|
+
if (!nodeId) return null;
|
|
2028
|
+
const engine = getEngine();
|
|
2029
|
+
if (!engine) return null;
|
|
2030
|
+
try {
|
|
2031
|
+
return await engine.materializeAt(nodeId, target);
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2034
|
+
return null;
|
|
2035
|
+
}
|
|
2036
|
+
},
|
|
2037
|
+
[nodeId, getEngine]
|
|
2038
|
+
);
|
|
2039
|
+
const diff = useCallback6(
|
|
2040
|
+
async (from, to) => {
|
|
2041
|
+
if (!nodeId) return [];
|
|
2042
|
+
const engine = getEngine();
|
|
2043
|
+
if (!engine) return [];
|
|
2044
|
+
try {
|
|
2045
|
+
return await engine.diff(nodeId, from, to);
|
|
2046
|
+
} catch (err) {
|
|
2047
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2048
|
+
return [];
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
[nodeId, getEngine]
|
|
2052
|
+
);
|
|
2053
|
+
const createRevertPayload = useCallback6(
|
|
2054
|
+
async (target) => {
|
|
2055
|
+
if (!nodeId || !store) return null;
|
|
2056
|
+
const engine = getEngine();
|
|
2057
|
+
if (!engine) return null;
|
|
2058
|
+
try {
|
|
2059
|
+
const current = await store.get(nodeId);
|
|
2060
|
+
if (!current) return null;
|
|
2061
|
+
return await engine.createRevertPayload(nodeId, target, current);
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
},
|
|
2067
|
+
[nodeId, store, getEngine]
|
|
2068
|
+
);
|
|
2069
|
+
return {
|
|
2070
|
+
timeline,
|
|
2071
|
+
changeCount: timeline.length,
|
|
2072
|
+
materializeAt,
|
|
2073
|
+
diff,
|
|
2074
|
+
createRevertPayload,
|
|
2075
|
+
loading,
|
|
2076
|
+
error,
|
|
2077
|
+
reload: loadTimeline
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/hooks/useUndo.ts
|
|
2082
|
+
import { UndoManager } from "@xnetjs/history";
|
|
2083
|
+
import { useState as useState7, useEffect as useEffect7, useCallback as useCallback7, useMemo as useMemo9, useRef as useRef4 } from "react";
|
|
2084
|
+
function useUndo(nodeId, opts) {
|
|
2085
|
+
const { store } = useNodeStore();
|
|
2086
|
+
const [undoCount, setUndoCount] = useState7(0);
|
|
2087
|
+
const [redoCount, setRedoCount] = useState7(0);
|
|
2088
|
+
const hasOptions = opts.options !== void 0;
|
|
2089
|
+
const optionLocalOnly = opts.options?.localOnly;
|
|
2090
|
+
const optionMaxStackSize = opts.options?.maxStackSize;
|
|
2091
|
+
const optionMergeInterval = opts.options?.mergeInterval;
|
|
2092
|
+
const managerRef = useRef4(null);
|
|
2093
|
+
const normalizedOptions = useMemo9(
|
|
2094
|
+
() => hasOptions ? {
|
|
2095
|
+
localOnly: optionLocalOnly,
|
|
2096
|
+
maxStackSize: optionMaxStackSize,
|
|
2097
|
+
mergeInterval: optionMergeInterval
|
|
2098
|
+
} : void 0,
|
|
2099
|
+
[hasOptions, optionLocalOnly, optionMaxStackSize, optionMergeInterval]
|
|
2100
|
+
);
|
|
2101
|
+
useEffect7(() => {
|
|
2102
|
+
if (!store || !opts.localDID) {
|
|
2103
|
+
managerRef.current = null;
|
|
2104
|
+
setUndoCount(0);
|
|
2105
|
+
setRedoCount(0);
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
const manager = new UndoManager(store, opts.localDID, normalizedOptions);
|
|
2109
|
+
manager.start();
|
|
2110
|
+
managerRef.current = manager;
|
|
2111
|
+
return () => {
|
|
2112
|
+
manager.stop();
|
|
2113
|
+
managerRef.current = null;
|
|
2114
|
+
};
|
|
2115
|
+
}, [store, opts.localDID, normalizedOptions]);
|
|
2116
|
+
const syncCounts = useCallback7(() => {
|
|
2117
|
+
if (!managerRef.current || !nodeId) {
|
|
2118
|
+
setUndoCount(0);
|
|
2119
|
+
setRedoCount(0);
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
setUndoCount(managerRef.current.getUndoCount(nodeId));
|
|
2123
|
+
setRedoCount(managerRef.current.getRedoCount(nodeId));
|
|
2124
|
+
}, [nodeId]);
|
|
2125
|
+
useEffect7(() => {
|
|
2126
|
+
syncCounts();
|
|
2127
|
+
}, [syncCounts]);
|
|
2128
|
+
useEffect7(() => {
|
|
2129
|
+
if (!store || !nodeId) return;
|
|
2130
|
+
const unsub = store.subscribe((event) => {
|
|
2131
|
+
if (event.change.payload.nodeId === nodeId) {
|
|
2132
|
+
syncCounts();
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
return unsub;
|
|
2136
|
+
}, [store, nodeId, syncCounts]);
|
|
2137
|
+
const undo = useCallback7(async () => {
|
|
2138
|
+
if (!managerRef.current || !nodeId) return false;
|
|
2139
|
+
const result = await managerRef.current.undo(nodeId);
|
|
2140
|
+
syncCounts();
|
|
2141
|
+
return result;
|
|
2142
|
+
}, [nodeId, syncCounts]);
|
|
2143
|
+
const redo = useCallback7(async () => {
|
|
2144
|
+
if (!managerRef.current || !nodeId) return false;
|
|
2145
|
+
const result = await managerRef.current.redo(nodeId);
|
|
2146
|
+
syncCounts();
|
|
2147
|
+
return result;
|
|
2148
|
+
}, [nodeId, syncCounts]);
|
|
2149
|
+
const undoBatch = useCallback7(
|
|
2150
|
+
async (batchId) => {
|
|
2151
|
+
if (!managerRef.current) return false;
|
|
2152
|
+
const result = await managerRef.current.undoBatch(batchId);
|
|
2153
|
+
syncCounts();
|
|
2154
|
+
return result;
|
|
2155
|
+
},
|
|
2156
|
+
[syncCounts]
|
|
2157
|
+
);
|
|
2158
|
+
const clear = useCallback7(() => {
|
|
2159
|
+
if (!managerRef.current || !nodeId) return;
|
|
2160
|
+
managerRef.current.clear(nodeId);
|
|
2161
|
+
syncCounts();
|
|
2162
|
+
}, [nodeId, syncCounts]);
|
|
2163
|
+
return {
|
|
2164
|
+
undo,
|
|
2165
|
+
redo,
|
|
2166
|
+
undoBatch,
|
|
2167
|
+
canUndo: undoCount > 0,
|
|
2168
|
+
canRedo: redoCount > 0,
|
|
2169
|
+
undoCount,
|
|
2170
|
+
redoCount,
|
|
2171
|
+
clear
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// src/hooks/useAudit.ts
|
|
2176
|
+
import { AuditIndex } from "@xnetjs/history";
|
|
2177
|
+
import { useState as useState8, useEffect as useEffect8, useCallback as useCallback8, useRef as useRef5 } from "react";
|
|
2178
|
+
function useAudit(nodeId, options) {
|
|
2179
|
+
const { store, isReady } = useNodeStore();
|
|
2180
|
+
const [entries, setEntries] = useState8([]);
|
|
2181
|
+
const [activity, setActivity] = useState8(null);
|
|
2182
|
+
const [loading, setLoading] = useState8(false);
|
|
2183
|
+
const [error, setError] = useState8(null);
|
|
2184
|
+
const auditRef = useRef5(null);
|
|
2185
|
+
const getAudit = useCallback8(() => {
|
|
2186
|
+
if (!store) return null;
|
|
2187
|
+
const storage = store.getStorageAdapter();
|
|
2188
|
+
if (!storage) return null;
|
|
2189
|
+
if (!auditRef.current || auditRef.current.storage !== storage) {
|
|
2190
|
+
auditRef.current = { audit: new AuditIndex(storage), storage };
|
|
2191
|
+
}
|
|
2192
|
+
return auditRef.current.audit;
|
|
2193
|
+
}, [store]);
|
|
2194
|
+
const load = useCallback8(async () => {
|
|
2195
|
+
if (!nodeId || !isReady) return;
|
|
2196
|
+
const audit = getAudit();
|
|
2197
|
+
if (!audit) return;
|
|
2198
|
+
setLoading(true);
|
|
2199
|
+
setError(null);
|
|
2200
|
+
try {
|
|
2201
|
+
const query = {
|
|
2202
|
+
nodeId,
|
|
2203
|
+
order: options?.order ?? "desc",
|
|
2204
|
+
limit: options?.limit ?? 200
|
|
2205
|
+
};
|
|
2206
|
+
if (options?.operations) query.operations = options.operations;
|
|
2207
|
+
if (options?.author) query.author = options.author;
|
|
2208
|
+
if (options?.timeRange) {
|
|
2209
|
+
query.fromWallTime = options.timeRange[0];
|
|
2210
|
+
query.toWallTime = options.timeRange[1];
|
|
2211
|
+
}
|
|
2212
|
+
const [auditEntries, activitySummary] = await Promise.all([
|
|
2213
|
+
audit.query(query),
|
|
2214
|
+
audit.getNodeActivity(nodeId)
|
|
2215
|
+
]);
|
|
2216
|
+
setEntries(auditEntries);
|
|
2217
|
+
setActivity(activitySummary);
|
|
2218
|
+
} catch (err) {
|
|
2219
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2220
|
+
} finally {
|
|
2221
|
+
setLoading(false);
|
|
2222
|
+
}
|
|
2223
|
+
}, [
|
|
2224
|
+
nodeId,
|
|
2225
|
+
isReady,
|
|
2226
|
+
getAudit,
|
|
2227
|
+
options?.operations,
|
|
2228
|
+
options?.author,
|
|
2229
|
+
options?.timeRange,
|
|
2230
|
+
options?.limit,
|
|
2231
|
+
options?.order
|
|
2232
|
+
]);
|
|
2233
|
+
useEffect8(() => {
|
|
2234
|
+
load();
|
|
2235
|
+
}, [load]);
|
|
2236
|
+
useEffect8(() => {
|
|
2237
|
+
if (!store || !nodeId) return;
|
|
2238
|
+
const unsub = store.subscribe((event) => {
|
|
2239
|
+
if (event.change.payload.nodeId === nodeId) {
|
|
2240
|
+
load();
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
return unsub;
|
|
2244
|
+
}, [store, nodeId, load]);
|
|
2245
|
+
return { entries, activity, loading, error, reload: load };
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// src/hooks/useDiff.ts
|
|
2249
|
+
import {
|
|
2250
|
+
HistoryEngine as HistoryEngine2,
|
|
2251
|
+
SnapshotCache as SnapshotCache2,
|
|
2252
|
+
MemorySnapshotStorage as MemorySnapshotStorage2,
|
|
2253
|
+
DiffEngine
|
|
2254
|
+
} from "@xnetjs/history";
|
|
2255
|
+
import { useState as useState9, useCallback as useCallback9, useRef as useRef6 } from "react";
|
|
2256
|
+
function useDiff(nodeId) {
|
|
2257
|
+
const { store } = useNodeStore();
|
|
2258
|
+
const [result, setResult] = useState9(null);
|
|
2259
|
+
const [loading, setLoading] = useState9(false);
|
|
2260
|
+
const [error, setError] = useState9(null);
|
|
2261
|
+
const engineRef = useRef6(null);
|
|
2262
|
+
const getEngine = useCallback9(() => {
|
|
2263
|
+
if (!store) return null;
|
|
2264
|
+
const storage = store.getStorageAdapter();
|
|
2265
|
+
if (!storage) return null;
|
|
2266
|
+
if (!engineRef.current || engineRef.current.storage !== storage) {
|
|
2267
|
+
const snapshotStorage = new MemorySnapshotStorage2();
|
|
2268
|
+
const snapshots = new SnapshotCache2(snapshotStorage, { interval: 50 });
|
|
2269
|
+
const history = new HistoryEngine2(storage, snapshots);
|
|
2270
|
+
engineRef.current = { diff: new DiffEngine(history), storage };
|
|
2271
|
+
}
|
|
2272
|
+
return engineRef.current.diff;
|
|
2273
|
+
}, [store]);
|
|
2274
|
+
const diff = useCallback9(
|
|
2275
|
+
async (from, to) => {
|
|
2276
|
+
if (!nodeId) return;
|
|
2277
|
+
const engine = getEngine();
|
|
2278
|
+
if (!engine) return;
|
|
2279
|
+
setLoading(true);
|
|
2280
|
+
setError(null);
|
|
2281
|
+
try {
|
|
2282
|
+
const diffResult = await engine.diffNode(nodeId, from, to);
|
|
2283
|
+
setResult(diffResult);
|
|
2284
|
+
} catch (err) {
|
|
2285
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2286
|
+
} finally {
|
|
2287
|
+
setLoading(false);
|
|
2288
|
+
}
|
|
2289
|
+
},
|
|
2290
|
+
[nodeId, getEngine]
|
|
2291
|
+
);
|
|
2292
|
+
const diffFromCurrent = useCallback9(
|
|
2293
|
+
async (changesAgo) => {
|
|
2294
|
+
if (!nodeId) return;
|
|
2295
|
+
const engine = getEngine();
|
|
2296
|
+
if (!engine) return;
|
|
2297
|
+
setLoading(true);
|
|
2298
|
+
setError(null);
|
|
2299
|
+
try {
|
|
2300
|
+
const diffResult = await engine.diffFromCurrent(nodeId, changesAgo);
|
|
2301
|
+
setResult(diffResult);
|
|
2302
|
+
} catch (err) {
|
|
2303
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2304
|
+
} finally {
|
|
2305
|
+
setLoading(false);
|
|
2306
|
+
}
|
|
2307
|
+
},
|
|
2308
|
+
[nodeId, getEngine]
|
|
2309
|
+
);
|
|
2310
|
+
return { diff, diffFromCurrent, result, loading, error };
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// src/hooks/useBlame.ts
|
|
2314
|
+
import { BlameEngine } from "@xnetjs/history";
|
|
2315
|
+
import { useState as useState10, useEffect as useEffect9, useCallback as useCallback10, useRef as useRef7 } from "react";
|
|
2316
|
+
function useBlame(nodeId) {
|
|
2317
|
+
const { store, isReady } = useNodeStore();
|
|
2318
|
+
const [blame, setBlame] = useState10([]);
|
|
2319
|
+
const [loading, setLoading] = useState10(false);
|
|
2320
|
+
const [error, setError] = useState10(null);
|
|
2321
|
+
const engineRef = useRef7(null);
|
|
2322
|
+
const getEngine = useCallback10(() => {
|
|
2323
|
+
if (!store) return null;
|
|
2324
|
+
const storage = store.getStorageAdapter();
|
|
2325
|
+
if (!storage) return null;
|
|
2326
|
+
if (!engineRef.current || engineRef.current.storage !== storage) {
|
|
2327
|
+
engineRef.current = { engine: new BlameEngine(storage), storage };
|
|
2328
|
+
}
|
|
2329
|
+
return engineRef.current.engine;
|
|
2330
|
+
}, [store]);
|
|
2331
|
+
const load = useCallback10(async () => {
|
|
2332
|
+
if (!nodeId || !isReady) return;
|
|
2333
|
+
const engine = getEngine();
|
|
2334
|
+
if (!engine) return;
|
|
2335
|
+
setLoading(true);
|
|
2336
|
+
setError(null);
|
|
2337
|
+
try {
|
|
2338
|
+
const info = await engine.getBlame(nodeId);
|
|
2339
|
+
setBlame(info);
|
|
2340
|
+
} catch (err) {
|
|
2341
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2342
|
+
} finally {
|
|
2343
|
+
setLoading(false);
|
|
2344
|
+
}
|
|
2345
|
+
}, [nodeId, isReady, getEngine]);
|
|
2346
|
+
useEffect9(() => {
|
|
2347
|
+
load();
|
|
2348
|
+
}, [load]);
|
|
2349
|
+
useEffect9(() => {
|
|
2350
|
+
if (!store || !nodeId) return;
|
|
2351
|
+
const unsub = store.subscribe((event) => {
|
|
2352
|
+
if (event.change.payload.nodeId === nodeId) {
|
|
2353
|
+
load();
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
return unsub;
|
|
2357
|
+
}, [store, nodeId, load]);
|
|
2358
|
+
return { blame, loading, error, reload: load };
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// src/hooks/useVerification.ts
|
|
2362
|
+
import {
|
|
2363
|
+
VerificationEngine
|
|
2364
|
+
} from "@xnetjs/history";
|
|
2365
|
+
import { useState as useState11, useCallback as useCallback11, useRef as useRef8 } from "react";
|
|
2366
|
+
function useVerification(nodeId) {
|
|
2367
|
+
const { store } = useNodeStore();
|
|
2368
|
+
const [result, setResult] = useState11(null);
|
|
2369
|
+
const [loading, setLoading] = useState11(false);
|
|
2370
|
+
const [progress, setProgress] = useState11(0);
|
|
2371
|
+
const [error, setError] = useState11(null);
|
|
2372
|
+
const engineRef = useRef8(null);
|
|
2373
|
+
const getEngine = useCallback11(() => {
|
|
2374
|
+
if (!store) return null;
|
|
2375
|
+
const storage = store.getStorageAdapter();
|
|
2376
|
+
if (!storage) return null;
|
|
2377
|
+
if (!engineRef.current || engineRef.current.storage !== storage) {
|
|
2378
|
+
engineRef.current = { engine: new VerificationEngine(storage), storage };
|
|
2379
|
+
}
|
|
2380
|
+
return engineRef.current.engine;
|
|
2381
|
+
}, [store]);
|
|
2382
|
+
const verify = useCallback11(
|
|
2383
|
+
async (options) => {
|
|
2384
|
+
if (!nodeId) return;
|
|
2385
|
+
const engine = getEngine();
|
|
2386
|
+
if (!engine) return;
|
|
2387
|
+
setLoading(true);
|
|
2388
|
+
setProgress(0);
|
|
2389
|
+
setError(null);
|
|
2390
|
+
try {
|
|
2391
|
+
const verifyResult = await engine.verifyNodeHistory(nodeId, {
|
|
2392
|
+
...options,
|
|
2393
|
+
onProgress: (p) => {
|
|
2394
|
+
setProgress(p);
|
|
2395
|
+
options?.onProgress?.(p);
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2398
|
+
setResult(verifyResult);
|
|
2399
|
+
} catch (err) {
|
|
2400
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2401
|
+
} finally {
|
|
2402
|
+
setLoading(false);
|
|
2403
|
+
setProgress(1);
|
|
2404
|
+
}
|
|
2405
|
+
},
|
|
2406
|
+
[nodeId, getEngine]
|
|
2407
|
+
);
|
|
2408
|
+
const quickCheck = useCallback11(async () => {
|
|
2409
|
+
if (!nodeId) return null;
|
|
2410
|
+
const engine = getEngine();
|
|
2411
|
+
if (!engine) return null;
|
|
2412
|
+
try {
|
|
2413
|
+
return await engine.quickCheck(nodeId);
|
|
2414
|
+
} catch (err) {
|
|
2415
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
2416
|
+
return null;
|
|
2417
|
+
}
|
|
2418
|
+
}, [nodeId, getEngine]);
|
|
2419
|
+
return { verify, quickCheck, result, loading, progress, error };
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// src/hooks/useHubStatus.ts
|
|
2423
|
+
import { useContext } from "react";
|
|
2424
|
+
function useHubStatus() {
|
|
2425
|
+
const context = useContext(XNetContext);
|
|
2426
|
+
return context?.hubStatus ?? "disconnected";
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/hooks/useCan.ts
|
|
2430
|
+
import { useEffect as useEffect10, useRef as useRef9, useState as useState12 } from "react";
|
|
2431
|
+
var GRANT_SCHEMA_ID = "xnet://xnet.fyi/Grant";
|
|
2432
|
+
var INITIAL_STATE = {
|
|
2433
|
+
canRead: false,
|
|
2434
|
+
canWrite: false,
|
|
2435
|
+
canDelete: false,
|
|
2436
|
+
canShare: false,
|
|
2437
|
+
loading: true,
|
|
2438
|
+
error: null,
|
|
2439
|
+
isFresh: false,
|
|
2440
|
+
evaluatedAt: 0
|
|
2441
|
+
};
|
|
2442
|
+
function useCan(nodeId) {
|
|
2443
|
+
const { store, isReady } = useNodeStore();
|
|
2444
|
+
const [state, setState] = useState12(INITIAL_STATE);
|
|
2445
|
+
const requestRef = useRef9(0);
|
|
2446
|
+
useEffect10(() => {
|
|
2447
|
+
if (!store || !isReady) {
|
|
2448
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (!store.auth) {
|
|
2452
|
+
setState({
|
|
2453
|
+
...INITIAL_STATE,
|
|
2454
|
+
loading: false,
|
|
2455
|
+
error: new Error("Authorization API is not configured on this NodeStore")
|
|
2456
|
+
});
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
const run = async () => {
|
|
2460
|
+
const requestId = ++requestRef.current;
|
|
2461
|
+
setState((prev) => ({ ...prev, loading: true, error: null }));
|
|
2462
|
+
try {
|
|
2463
|
+
const [read, write, del, share] = await Promise.all([
|
|
2464
|
+
store.auth.can({ action: "read", nodeId }),
|
|
2465
|
+
store.auth.can({ action: "write", nodeId }),
|
|
2466
|
+
store.auth.can({ action: "delete", nodeId }),
|
|
2467
|
+
store.auth.can({ action: "share", nodeId })
|
|
2468
|
+
]);
|
|
2469
|
+
if (requestRef.current !== requestId) return;
|
|
2470
|
+
setState({
|
|
2471
|
+
canRead: read.allowed,
|
|
2472
|
+
canWrite: write.allowed,
|
|
2473
|
+
canDelete: del.allowed,
|
|
2474
|
+
canShare: share.allowed,
|
|
2475
|
+
loading: false,
|
|
2476
|
+
error: null,
|
|
2477
|
+
isFresh: !read.cached,
|
|
2478
|
+
evaluatedAt: read.evaluatedAt
|
|
2479
|
+
});
|
|
2480
|
+
} catch (error) {
|
|
2481
|
+
if (requestRef.current !== requestId) return;
|
|
2482
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
2483
|
+
setState((prev) => ({ ...prev, loading: false, error: normalized }));
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
void run();
|
|
2487
|
+
const unsubscribe = store.subscribe((event) => {
|
|
2488
|
+
const typedEvent = event;
|
|
2489
|
+
const eventNodeId = typedEvent.change?.payload?.nodeId;
|
|
2490
|
+
if (eventNodeId === nodeId) {
|
|
2491
|
+
void run();
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
const isGrantUpdate = typedEvent.node?.schemaId === GRANT_SCHEMA_ID;
|
|
2495
|
+
const grantResource = typedEvent.node?.properties?.resource;
|
|
2496
|
+
if (isGrantUpdate && grantResource === nodeId) {
|
|
2497
|
+
void run();
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
return () => {
|
|
2501
|
+
unsubscribe();
|
|
2502
|
+
};
|
|
2503
|
+
}, [isReady, nodeId, store]);
|
|
2504
|
+
return state;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// src/hooks/useCanEdit.ts
|
|
2508
|
+
import { useEffect as useEffect11, useRef as useRef10, useState as useState13 } from "react";
|
|
2509
|
+
var GRANT_SCHEMA_ID2 = "xnet://xnet.fyi/Grant";
|
|
2510
|
+
var INITIAL_STATE2 = {
|
|
2511
|
+
canEdit: false,
|
|
2512
|
+
canView: false,
|
|
2513
|
+
loading: true,
|
|
2514
|
+
error: null,
|
|
2515
|
+
roles: []
|
|
2516
|
+
};
|
|
2517
|
+
function useCanEdit(nodeId) {
|
|
2518
|
+
const { store, isReady } = useNodeStore();
|
|
2519
|
+
const [state, setState] = useState13(INITIAL_STATE2);
|
|
2520
|
+
const requestRef = useRef10(0);
|
|
2521
|
+
useEffect11(() => {
|
|
2522
|
+
if (!store || !isReady) {
|
|
2523
|
+
setState((prev) => ({ ...prev, loading: true }));
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (!store.auth) {
|
|
2527
|
+
setState({
|
|
2528
|
+
...INITIAL_STATE2,
|
|
2529
|
+
loading: false,
|
|
2530
|
+
error: new Error("Authorization API is not configured on this NodeStore")
|
|
2531
|
+
});
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
const run = async () => {
|
|
2535
|
+
const requestId = ++requestRef.current;
|
|
2536
|
+
setState((prev) => ({ ...prev, loading: true, error: null }));
|
|
2537
|
+
try {
|
|
2538
|
+
const [read, write] = await Promise.all([
|
|
2539
|
+
store.auth.can({ action: "read", nodeId }),
|
|
2540
|
+
store.auth.can({ action: "write", nodeId })
|
|
2541
|
+
]);
|
|
2542
|
+
if (requestRef.current !== requestId) return;
|
|
2543
|
+
const roleSet = /* @__PURE__ */ new Set([...read.roles, ...write.roles]);
|
|
2544
|
+
setState({
|
|
2545
|
+
canEdit: write.allowed,
|
|
2546
|
+
canView: read.allowed && !write.allowed,
|
|
2547
|
+
loading: false,
|
|
2548
|
+
error: null,
|
|
2549
|
+
roles: Array.from(roleSet)
|
|
2550
|
+
});
|
|
2551
|
+
} catch (error) {
|
|
2552
|
+
if (requestRef.current !== requestId) return;
|
|
2553
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
2554
|
+
setState((prev) => ({ ...prev, loading: false, error: normalized }));
|
|
2555
|
+
}
|
|
2556
|
+
};
|
|
2557
|
+
void run();
|
|
2558
|
+
const unsubscribe = store.subscribe((event) => {
|
|
2559
|
+
const typedEvent = event;
|
|
2560
|
+
const eventNodeId = typedEvent.change?.payload?.nodeId;
|
|
2561
|
+
if (eventNodeId === nodeId) {
|
|
2562
|
+
void run();
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
const isGrantUpdate = typedEvent.node?.schemaId === GRANT_SCHEMA_ID2;
|
|
2566
|
+
const grantResource = typedEvent.node?.properties?.resource;
|
|
2567
|
+
if (isGrantUpdate && grantResource === nodeId) {
|
|
2568
|
+
void run();
|
|
2569
|
+
}
|
|
2570
|
+
});
|
|
2571
|
+
return () => {
|
|
2572
|
+
unsubscribe();
|
|
2573
|
+
};
|
|
2574
|
+
}, [isReady, nodeId, store]);
|
|
2575
|
+
return state;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// src/hooks/useGrants.ts
|
|
2579
|
+
import { useCallback as useCallback12, useEffect as useEffect12, useState as useState14 } from "react";
|
|
2580
|
+
var GRANT_SCHEMA_ID3 = "xnet://xnet.fyi/Grant";
|
|
2581
|
+
var DEFAULT_GRANT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2582
|
+
function describeGrantConsent(input, defaultResource, now = Date.now()) {
|
|
2583
|
+
const resource = input.resource ?? defaultResource;
|
|
2584
|
+
const expiresAt = computeGrantExpiration(input.expiresIn, now);
|
|
2585
|
+
const actions = [...input.actions];
|
|
2586
|
+
return {
|
|
2587
|
+
grantee: input.to,
|
|
2588
|
+
resource,
|
|
2589
|
+
actions,
|
|
2590
|
+
expiresAt,
|
|
2591
|
+
what: actions.join(", "),
|
|
2592
|
+
where: resource,
|
|
2593
|
+
howLong: formatGrantExpiration(expiresAt, now)
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
function useGrants(nodeId) {
|
|
2597
|
+
const { store, isReady } = useNodeStore();
|
|
2598
|
+
const [grants, setGrants] = useState14([]);
|
|
2599
|
+
const [loading, setLoading] = useState14(true);
|
|
2600
|
+
const [error, setError] = useState14(null);
|
|
2601
|
+
const load = useCallback12(async () => {
|
|
2602
|
+
if (!store || !store.auth || !isReady) return;
|
|
2603
|
+
setLoading(true);
|
|
2604
|
+
setError(null);
|
|
2605
|
+
try {
|
|
2606
|
+
const next = await store.auth.listGrants({ nodeId });
|
|
2607
|
+
setGrants(next);
|
|
2608
|
+
setLoading(false);
|
|
2609
|
+
} catch (err) {
|
|
2610
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
2611
|
+
setError(normalized);
|
|
2612
|
+
setLoading(false);
|
|
2613
|
+
}
|
|
2614
|
+
}, [isReady, nodeId, store]);
|
|
2615
|
+
useEffect12(() => {
|
|
2616
|
+
if (!store || !isReady) {
|
|
2617
|
+
setLoading(true);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (!store.auth) {
|
|
2621
|
+
setLoading(false);
|
|
2622
|
+
setError(new Error("Authorization API is not configured on this NodeStore"));
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
void load();
|
|
2626
|
+
const unsubscribe = store.subscribe((event) => {
|
|
2627
|
+
const typedEvent = event;
|
|
2628
|
+
const isGrantUpdate = typedEvent.node?.schemaId === GRANT_SCHEMA_ID3;
|
|
2629
|
+
const resource = typedEvent.node?.properties?.resource;
|
|
2630
|
+
if (isGrantUpdate && resource === nodeId) {
|
|
2631
|
+
void load();
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
return () => {
|
|
2635
|
+
unsubscribe();
|
|
2636
|
+
};
|
|
2637
|
+
}, [isReady, load, nodeId, store]);
|
|
2638
|
+
const grant = useCallback12(
|
|
2639
|
+
async (input) => {
|
|
2640
|
+
if (!store?.auth) {
|
|
2641
|
+
throw new Error("Authorization API is not configured on this NodeStore");
|
|
2642
|
+
}
|
|
2643
|
+
const created = await store.auth.grant({ ...input, resource: input.resource ?? nodeId });
|
|
2644
|
+
await load();
|
|
2645
|
+
return created;
|
|
2646
|
+
},
|
|
2647
|
+
[load, nodeId, store]
|
|
2648
|
+
);
|
|
2649
|
+
const revoke = useCallback12(
|
|
2650
|
+
async (grantId) => {
|
|
2651
|
+
if (!store?.auth) {
|
|
2652
|
+
throw new Error("Authorization API is not configured on this NodeStore");
|
|
2653
|
+
}
|
|
2654
|
+
await store.auth.revoke({ grantId });
|
|
2655
|
+
await load();
|
|
2656
|
+
},
|
|
2657
|
+
[load, store]
|
|
2658
|
+
);
|
|
2659
|
+
return { grants, loading, error, grant, revoke };
|
|
2660
|
+
}
|
|
2661
|
+
function computeGrantExpiration(expiresIn, now) {
|
|
2662
|
+
if (typeof expiresIn === "number") {
|
|
2663
|
+
return expiresIn;
|
|
2664
|
+
}
|
|
2665
|
+
if (typeof expiresIn === "string") {
|
|
2666
|
+
const parsed = parseDuration(expiresIn);
|
|
2667
|
+
if (parsed !== null) {
|
|
2668
|
+
return now + parsed;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
return now + DEFAULT_GRANT_TTL_MS;
|
|
2672
|
+
}
|
|
2673
|
+
function formatGrantExpiration(expiresAt, now) {
|
|
2674
|
+
const durationMs = Math.max(0, expiresAt - now);
|
|
2675
|
+
const roundedDays = Math.round(durationMs / (24 * 60 * 60 * 1e3));
|
|
2676
|
+
const roundedHours = Math.round(durationMs / (60 * 60 * 1e3));
|
|
2677
|
+
const roundedMinutes = Math.round(durationMs / (60 * 1e3));
|
|
2678
|
+
if (roundedDays >= 1) {
|
|
2679
|
+
return `${roundedDays}d`;
|
|
2680
|
+
}
|
|
2681
|
+
if (roundedHours >= 1) {
|
|
2682
|
+
return `${roundedHours}h`;
|
|
2683
|
+
}
|
|
2684
|
+
if (roundedMinutes >= 1) {
|
|
2685
|
+
return `${roundedMinutes}m`;
|
|
2686
|
+
}
|
|
2687
|
+
return `${Math.round(durationMs / 1e3)}s`;
|
|
2688
|
+
}
|
|
2689
|
+
function parseDuration(value) {
|
|
2690
|
+
const match = value.match(/^(\d+)([smhdw])$/);
|
|
2691
|
+
if (!match) {
|
|
2692
|
+
return null;
|
|
2693
|
+
}
|
|
2694
|
+
const amount = Number(match[1]);
|
|
2695
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
2696
|
+
return null;
|
|
2697
|
+
}
|
|
2698
|
+
const unit = match[2];
|
|
2699
|
+
switch (unit) {
|
|
2700
|
+
case "s":
|
|
2701
|
+
return amount * 1e3;
|
|
2702
|
+
case "m":
|
|
2703
|
+
return amount * 60 * 1e3;
|
|
2704
|
+
case "h":
|
|
2705
|
+
return amount * 60 * 60 * 1e3;
|
|
2706
|
+
case "d":
|
|
2707
|
+
return amount * 24 * 60 * 60 * 1e3;
|
|
2708
|
+
case "w":
|
|
2709
|
+
return amount * 7 * 24 * 60 * 60 * 1e3;
|
|
2710
|
+
default:
|
|
2711
|
+
return null;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
// src/hooks/useAuthTrace.ts
|
|
2716
|
+
import { useCallback as useCallback13, useEffect as useEffect13, useRef as useRef11, useState as useState15 } from "react";
|
|
2717
|
+
var GRANT_SCHEMA_ID4 = "xnet://xnet.fyi/Grant";
|
|
2718
|
+
function summarizeAuthTrace(trace) {
|
|
2719
|
+
return {
|
|
2720
|
+
allowed: trace.allowed,
|
|
2721
|
+
action: trace.action,
|
|
2722
|
+
resource: trace.resource,
|
|
2723
|
+
roles: trace.roles,
|
|
2724
|
+
grants: trace.grants,
|
|
2725
|
+
reasons: trace.reasons,
|
|
2726
|
+
evaluatedAt: trace.evaluatedAt,
|
|
2727
|
+
duration: trace.duration,
|
|
2728
|
+
steps: trace.steps
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
function useAuthTrace({
|
|
2732
|
+
nodeId,
|
|
2733
|
+
action,
|
|
2734
|
+
enabled = true
|
|
2735
|
+
}) {
|
|
2736
|
+
const { store, isReady } = useNodeStore();
|
|
2737
|
+
const [trace, setTrace] = useState15(null);
|
|
2738
|
+
const [summary, setSummary] = useState15(null);
|
|
2739
|
+
const [loading, setLoading] = useState15(enabled);
|
|
2740
|
+
const [error, setError] = useState15(null);
|
|
2741
|
+
const requestRef = useRef11(0);
|
|
2742
|
+
const refresh = useCallback13(async () => {
|
|
2743
|
+
if (!enabled) {
|
|
2744
|
+
setLoading(false);
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
if (!store || !isReady) {
|
|
2748
|
+
setLoading(true);
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
if (!store.auth) {
|
|
2752
|
+
setTrace(null);
|
|
2753
|
+
setSummary(null);
|
|
2754
|
+
setLoading(false);
|
|
2755
|
+
setError(new Error("Authorization API is not configured on this NodeStore"));
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const requestId = ++requestRef.current;
|
|
2759
|
+
setLoading(true);
|
|
2760
|
+
setError(null);
|
|
2761
|
+
try {
|
|
2762
|
+
const nextTrace = await store.auth.explain({ action, nodeId });
|
|
2763
|
+
if (requestRef.current !== requestId) return;
|
|
2764
|
+
setTrace(nextTrace);
|
|
2765
|
+
setSummary(summarizeAuthTrace(nextTrace));
|
|
2766
|
+
setLoading(false);
|
|
2767
|
+
} catch (err) {
|
|
2768
|
+
if (requestRef.current !== requestId) return;
|
|
2769
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
2770
|
+
setTrace(null);
|
|
2771
|
+
setSummary(null);
|
|
2772
|
+
setLoading(false);
|
|
2773
|
+
setError(normalized);
|
|
2774
|
+
}
|
|
2775
|
+
}, [action, enabled, isReady, nodeId, store]);
|
|
2776
|
+
useEffect13(() => {
|
|
2777
|
+
void refresh();
|
|
2778
|
+
if (!store || !isReady || !enabled) {
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
const unsubscribe = store.subscribe((event) => {
|
|
2782
|
+
const typedEvent = event;
|
|
2783
|
+
const eventNodeId = typedEvent.change?.payload?.nodeId;
|
|
2784
|
+
if (eventNodeId === nodeId) {
|
|
2785
|
+
void refresh();
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
const isGrantUpdate = typedEvent.node?.schemaId === GRANT_SCHEMA_ID4;
|
|
2789
|
+
const grantResource = typedEvent.node?.properties?.resource;
|
|
2790
|
+
if (isGrantUpdate && grantResource === nodeId) {
|
|
2791
|
+
void refresh();
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
return () => {
|
|
2795
|
+
unsubscribe();
|
|
2796
|
+
};
|
|
2797
|
+
}, [enabled, isReady, nodeId, refresh, store]);
|
|
2798
|
+
return {
|
|
2799
|
+
trace,
|
|
2800
|
+
summary,
|
|
2801
|
+
loading,
|
|
2802
|
+
error,
|
|
2803
|
+
refresh
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// src/hooks/useBackup.ts
|
|
2808
|
+
import { useCallback as useCallback14, useContext as useContext2, useMemo as useMemo10, useState as useState16 } from "react";
|
|
2809
|
+
function useBackup() {
|
|
2810
|
+
const context = useContext2(XNetContext);
|
|
2811
|
+
const [uploading, setUploading] = useState16(false);
|
|
2812
|
+
const backupConfig = useMemo10(() => {
|
|
2813
|
+
if (!context) return null;
|
|
2814
|
+
if (!context.hubUrl || !context.encryptionKey) return null;
|
|
2815
|
+
return {
|
|
2816
|
+
hubUrl: context.hubUrl,
|
|
2817
|
+
encryptionKey: context.encryptionKey,
|
|
2818
|
+
getAuthToken: context.getHubAuthToken
|
|
2819
|
+
};
|
|
2820
|
+
}, [context]);
|
|
2821
|
+
const upload = useCallback14(
|
|
2822
|
+
async (docId, plaintext) => {
|
|
2823
|
+
if (!backupConfig) {
|
|
2824
|
+
throw new Error("Hub backup is not configured");
|
|
2825
|
+
}
|
|
2826
|
+
setUploading(true);
|
|
2827
|
+
try {
|
|
2828
|
+
await uploadBackup(backupConfig, docId, plaintext);
|
|
2829
|
+
} finally {
|
|
2830
|
+
setUploading(false);
|
|
2831
|
+
}
|
|
2832
|
+
},
|
|
2833
|
+
[backupConfig]
|
|
2834
|
+
);
|
|
2835
|
+
const download = useCallback14(
|
|
2836
|
+
async (docId) => {
|
|
2837
|
+
if (!backupConfig) return null;
|
|
2838
|
+
return downloadBackup(backupConfig, docId);
|
|
2839
|
+
},
|
|
2840
|
+
[backupConfig]
|
|
2841
|
+
);
|
|
2842
|
+
return {
|
|
2843
|
+
upload,
|
|
2844
|
+
download,
|
|
2845
|
+
uploading
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
// src/hooks/useFileUpload.ts
|
|
2850
|
+
import { hashHex } from "@xnetjs/crypto";
|
|
2851
|
+
import { useCallback as useCallback15, useContext as useContext3, useState as useState17 } from "react";
|
|
2852
|
+
var toHttpUrl = (hubUrl) => {
|
|
2853
|
+
try {
|
|
2854
|
+
const url = new URL(hubUrl);
|
|
2855
|
+
if (url.protocol === "ws:") url.protocol = "http:";
|
|
2856
|
+
if (url.protocol === "wss:") url.protocol = "https:";
|
|
2857
|
+
return url.toString().replace(/\/$/, "");
|
|
2858
|
+
} catch {
|
|
2859
|
+
return hubUrl;
|
|
2860
|
+
}
|
|
2861
|
+
};
|
|
2862
|
+
function useFileUpload() {
|
|
2863
|
+
const context = useContext3(XNetContext);
|
|
2864
|
+
const [uploading, setUploading] = useState17(false);
|
|
2865
|
+
const [progress, setProgress] = useState17(0);
|
|
2866
|
+
const upload = useCallback15(
|
|
2867
|
+
async (file) => {
|
|
2868
|
+
if (!context?.hubUrl) throw new Error("Hub URL not configured");
|
|
2869
|
+
setUploading(true);
|
|
2870
|
+
setProgress(0);
|
|
2871
|
+
try {
|
|
2872
|
+
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
2873
|
+
setProgress(0.3);
|
|
2874
|
+
const cid = `cid:blake3:${hashHex(buffer)}`;
|
|
2875
|
+
setProgress(0.5);
|
|
2876
|
+
const token = context.getHubAuthToken ? await context.getHubAuthToken() : "";
|
|
2877
|
+
const httpUrl = toHttpUrl(context.hubUrl);
|
|
2878
|
+
const res = await fetch(`${httpUrl}/files/${cid}`, {
|
|
2879
|
+
method: "PUT",
|
|
2880
|
+
headers: {
|
|
2881
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
2882
|
+
"Content-Type": file.type || "application/octet-stream",
|
|
2883
|
+
"X-File-Name": file.name
|
|
2884
|
+
},
|
|
2885
|
+
body: buffer
|
|
2886
|
+
});
|
|
2887
|
+
setProgress(0.9);
|
|
2888
|
+
if (!res.ok) {
|
|
2889
|
+
let message = `Upload failed: ${res.status}`;
|
|
2890
|
+
try {
|
|
2891
|
+
const err = await res.json();
|
|
2892
|
+
if (err?.error) message = err.error;
|
|
2893
|
+
} catch {
|
|
2894
|
+
}
|
|
2895
|
+
throw new Error(message);
|
|
2896
|
+
}
|
|
2897
|
+
setProgress(1);
|
|
2898
|
+
return {
|
|
2899
|
+
cid,
|
|
2900
|
+
name: file.name,
|
|
2901
|
+
mimeType: file.type || "application/octet-stream",
|
|
2902
|
+
size: file.size
|
|
2903
|
+
};
|
|
2904
|
+
} finally {
|
|
2905
|
+
setUploading(false);
|
|
2906
|
+
}
|
|
2907
|
+
},
|
|
2908
|
+
[context]
|
|
2909
|
+
);
|
|
2910
|
+
return { upload, uploading, progress };
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
// src/hooks/useHubSearch.ts
|
|
2914
|
+
import { useCallback as useCallback16, useContext as useContext4, useEffect as useEffect14, useMemo as useMemo11, useRef as useRef12, useState as useState18 } from "react";
|
|
2915
|
+
var createRequestId = () => {
|
|
2916
|
+
const cryptoObj = globalThis.crypto;
|
|
2917
|
+
if (cryptoObj && "randomUUID" in cryptoObj) {
|
|
2918
|
+
return cryptoObj.randomUUID();
|
|
2919
|
+
}
|
|
2920
|
+
return Math.random().toString(36).slice(2);
|
|
2921
|
+
};
|
|
2922
|
+
function useHubSearch() {
|
|
2923
|
+
const context = useContext4(XNetContext);
|
|
2924
|
+
const connection = context?.hubConnection ?? context?.syncManager?.connection ?? null;
|
|
2925
|
+
const [results, setResults] = useState18([]);
|
|
2926
|
+
const [loading, setLoading] = useState18(false);
|
|
2927
|
+
const [error, setError] = useState18(null);
|
|
2928
|
+
const pendingRef = useRef12(/* @__PURE__ */ new Map());
|
|
2929
|
+
useEffect14(() => {
|
|
2930
|
+
if (!connection) return;
|
|
2931
|
+
const unsubscribe = connection.onMessage((message) => {
|
|
2932
|
+
const msg = message;
|
|
2933
|
+
if (!msg.type || !msg.id) return;
|
|
2934
|
+
if (msg.type === "query-response") {
|
|
2935
|
+
const pending = pendingRef.current.get(msg.id);
|
|
2936
|
+
if (pending) {
|
|
2937
|
+
pendingRef.current.delete(msg.id);
|
|
2938
|
+
clearTimeout(pending.timeoutId);
|
|
2939
|
+
pending.resolve(msg.results ?? []);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
if (msg.type === "query-error") {
|
|
2943
|
+
const pending = pendingRef.current.get(msg.id);
|
|
2944
|
+
if (pending) {
|
|
2945
|
+
pendingRef.current.delete(msg.id);
|
|
2946
|
+
clearTimeout(pending.timeoutId);
|
|
2947
|
+
pending.reject(new Error(msg.error ?? "Query failed"));
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2951
|
+
return () => {
|
|
2952
|
+
unsubscribe();
|
|
2953
|
+
for (const pending of pendingRef.current.values()) {
|
|
2954
|
+
clearTimeout(pending.timeoutId);
|
|
2955
|
+
pending.reject(new Error("Query cancelled"));
|
|
2956
|
+
}
|
|
2957
|
+
pendingRef.current.clear();
|
|
2958
|
+
};
|
|
2959
|
+
}, [connection]);
|
|
2960
|
+
const search = useCallback16(
|
|
2961
|
+
async (query, options) => {
|
|
2962
|
+
if (!connection || connection.status !== "connected") {
|
|
2963
|
+
setError(new Error("Hub connection not available"));
|
|
2964
|
+
setResults([]);
|
|
2965
|
+
return [];
|
|
2966
|
+
}
|
|
2967
|
+
setLoading(true);
|
|
2968
|
+
setError(null);
|
|
2969
|
+
const id = createRequestId();
|
|
2970
|
+
const promise = new Promise((resolve, reject) => {
|
|
2971
|
+
const timeoutId = setTimeout(() => {
|
|
2972
|
+
pendingRef.current.delete(id);
|
|
2973
|
+
reject(new Error("Query timeout"));
|
|
2974
|
+
}, 5e3);
|
|
2975
|
+
pendingRef.current.set(id, { resolve, reject, timeoutId });
|
|
2976
|
+
});
|
|
2977
|
+
connection.sendRaw({
|
|
2978
|
+
type: "query-request",
|
|
2979
|
+
id,
|
|
2980
|
+
query,
|
|
2981
|
+
filters: options?.schemaIri ? { schemaIri: options.schemaIri } : void 0,
|
|
2982
|
+
limit: options?.limit,
|
|
2983
|
+
offset: options?.offset
|
|
2984
|
+
});
|
|
2985
|
+
try {
|
|
2986
|
+
const response = await promise;
|
|
2987
|
+
setResults(response);
|
|
2988
|
+
return response;
|
|
2989
|
+
} catch (err) {
|
|
2990
|
+
const errorValue = err instanceof Error ? err : new Error("Search failed");
|
|
2991
|
+
setError(errorValue);
|
|
2992
|
+
setResults([]);
|
|
2993
|
+
return [];
|
|
2994
|
+
} finally {
|
|
2995
|
+
setLoading(false);
|
|
2996
|
+
}
|
|
2997
|
+
},
|
|
2998
|
+
[connection]
|
|
2999
|
+
);
|
|
3000
|
+
return useMemo11(
|
|
3001
|
+
() => ({
|
|
3002
|
+
search,
|
|
3003
|
+
results,
|
|
3004
|
+
loading,
|
|
3005
|
+
error
|
|
3006
|
+
}),
|
|
3007
|
+
[search, results, loading, error]
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
// src/hooks/useRemoteSchema.ts
|
|
3012
|
+
import { useContext as useContext5, useEffect as useEffect15, useMemo as useMemo12, useState as useState19 } from "react";
|
|
3013
|
+
var toHubHttpUrl = (hubUrl) => {
|
|
3014
|
+
if (hubUrl.startsWith("http://") || hubUrl.startsWith("https://")) return hubUrl;
|
|
3015
|
+
return hubUrl.replace("wss://", "https://").replace("ws://", "http://");
|
|
3016
|
+
};
|
|
3017
|
+
var useRemoteSchema = (iri) => {
|
|
3018
|
+
const context = useContext5(XNetContext);
|
|
3019
|
+
const hubUrl = context?.hubUrl ?? null;
|
|
3020
|
+
const getHubAuthToken = context?.getHubAuthToken;
|
|
3021
|
+
const [schema, setSchema] = useState19(null);
|
|
3022
|
+
const [loading, setLoading] = useState19(false);
|
|
3023
|
+
const [error, setError] = useState19(null);
|
|
3024
|
+
useEffect15(() => {
|
|
3025
|
+
if (!iri || !hubUrl) {
|
|
3026
|
+
setSchema(null);
|
|
3027
|
+
setLoading(false);
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
let cancelled = false;
|
|
3031
|
+
const controller = new AbortController();
|
|
3032
|
+
const fetchSchema = async () => {
|
|
3033
|
+
setLoading(true);
|
|
3034
|
+
setError(null);
|
|
3035
|
+
try {
|
|
3036
|
+
const hubHttpUrl = toHubHttpUrl(hubUrl);
|
|
3037
|
+
const encodedIri = encodeURIComponent(iri);
|
|
3038
|
+
const token = getHubAuthToken ? await getHubAuthToken() : "";
|
|
3039
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : void 0;
|
|
3040
|
+
const response = await fetch(`${hubHttpUrl}/schemas/resolve/${encodedIri}`, {
|
|
3041
|
+
headers,
|
|
3042
|
+
signal: controller.signal
|
|
3043
|
+
});
|
|
3044
|
+
if (!response.ok) {
|
|
3045
|
+
if (response.status === 404) {
|
|
3046
|
+
if (!cancelled) setSchema(null);
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
throw new Error(`Failed to resolve schema: ${response.status}`);
|
|
3050
|
+
}
|
|
3051
|
+
const data = await response.json();
|
|
3052
|
+
if (!cancelled) setSchema(data);
|
|
3053
|
+
} catch (err) {
|
|
3054
|
+
if (cancelled) return;
|
|
3055
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
3056
|
+
const errorValue = err instanceof Error ? err : new Error("Schema lookup failed");
|
|
3057
|
+
setError(errorValue);
|
|
3058
|
+
} finally {
|
|
3059
|
+
if (!cancelled) setLoading(false);
|
|
3060
|
+
}
|
|
3061
|
+
};
|
|
3062
|
+
void fetchSchema();
|
|
3063
|
+
return () => {
|
|
3064
|
+
cancelled = true;
|
|
3065
|
+
controller.abort();
|
|
3066
|
+
};
|
|
3067
|
+
}, [getHubAuthToken, hubUrl, iri]);
|
|
3068
|
+
return useMemo12(
|
|
3069
|
+
() => ({
|
|
3070
|
+
schema,
|
|
3071
|
+
loading,
|
|
3072
|
+
error
|
|
3073
|
+
}),
|
|
3074
|
+
[schema, loading, error]
|
|
3075
|
+
);
|
|
3076
|
+
};
|
|
3077
|
+
|
|
3078
|
+
// src/hooks/usePeerDiscovery.ts
|
|
3079
|
+
import { useCallback as useCallback17, useContext as useContext6, useMemo as useMemo13, useState as useState20 } from "react";
|
|
3080
|
+
var toHubHttpUrl2 = (hubUrl) => hubUrl.replace("wss://", "https://").replace("ws://", "http://");
|
|
3081
|
+
var usePeerDiscovery = () => {
|
|
3082
|
+
const context = useContext6(XNetContext);
|
|
3083
|
+
const hubUrl = context?.hubUrl ?? null;
|
|
3084
|
+
const [peers, setPeers] = useState20([]);
|
|
3085
|
+
const [loading, setLoading] = useState20(false);
|
|
3086
|
+
const refresh = useCallback17(async () => {
|
|
3087
|
+
if (!hubUrl) return;
|
|
3088
|
+
setLoading(true);
|
|
3089
|
+
try {
|
|
3090
|
+
const hubHttpUrl = toHubHttpUrl2(hubUrl);
|
|
3091
|
+
const res = await fetch(`${hubHttpUrl}/dids?limit=50`);
|
|
3092
|
+
if (!res.ok) return;
|
|
3093
|
+
const { peers: records } = await res.json();
|
|
3094
|
+
const now = Date.now();
|
|
3095
|
+
setPeers(
|
|
3096
|
+
records.map((record) => ({
|
|
3097
|
+
did: record.did,
|
|
3098
|
+
displayName: record.displayName,
|
|
3099
|
+
endpoints: record.endpoints,
|
|
3100
|
+
lastSeen: record.lastSeen,
|
|
3101
|
+
isOnline: now - record.lastSeen < 5 * 60 * 1e3
|
|
3102
|
+
}))
|
|
3103
|
+
);
|
|
3104
|
+
} finally {
|
|
3105
|
+
setLoading(false);
|
|
3106
|
+
}
|
|
3107
|
+
}, [hubUrl]);
|
|
3108
|
+
const resolve = useCallback17(
|
|
3109
|
+
async (did) => {
|
|
3110
|
+
if (!hubUrl) return null;
|
|
3111
|
+
const hubHttpUrl = toHubHttpUrl2(hubUrl);
|
|
3112
|
+
const res = await fetch(`${hubHttpUrl}/dids/${encodeURIComponent(did)}`);
|
|
3113
|
+
if (!res.ok) return null;
|
|
3114
|
+
const record = await res.json();
|
|
3115
|
+
return {
|
|
3116
|
+
did: record.did,
|
|
3117
|
+
displayName: record.displayName,
|
|
3118
|
+
endpoints: record.endpoints,
|
|
3119
|
+
lastSeen: record.lastSeen,
|
|
3120
|
+
isOnline: Date.now() - record.lastSeen < 5 * 60 * 1e3
|
|
3121
|
+
};
|
|
3122
|
+
},
|
|
3123
|
+
[hubUrl]
|
|
3124
|
+
);
|
|
3125
|
+
return useMemo13(
|
|
3126
|
+
() => ({
|
|
3127
|
+
peers,
|
|
3128
|
+
resolve,
|
|
3129
|
+
refresh,
|
|
3130
|
+
loading
|
|
3131
|
+
}),
|
|
3132
|
+
[peers, resolve, refresh, loading]
|
|
3133
|
+
);
|
|
3134
|
+
};
|
|
3135
|
+
|
|
3136
|
+
// src/components/HubStatusIndicator.tsx
|
|
3137
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3138
|
+
var STATUS_CONFIG = {
|
|
3139
|
+
disconnected: { color: "var(--muted)", label: "Offline" },
|
|
3140
|
+
connecting: { color: "var(--warning)", label: "Connecting..." },
|
|
3141
|
+
connected: { color: "var(--success)", label: "Synced to hub" },
|
|
3142
|
+
error: { color: "var(--destructive)", label: "Connection error" }
|
|
3143
|
+
};
|
|
3144
|
+
function HubStatusIndicator() {
|
|
3145
|
+
const status = useHubStatus();
|
|
3146
|
+
const config = STATUS_CONFIG[status];
|
|
3147
|
+
return /* @__PURE__ */ jsxs(
|
|
3148
|
+
"div",
|
|
3149
|
+
{
|
|
3150
|
+
style: { display: "flex", alignItems: "center", gap: "6px", fontSize: "12px" },
|
|
3151
|
+
title: config.label,
|
|
3152
|
+
children: [
|
|
3153
|
+
/* @__PURE__ */ jsx(
|
|
3154
|
+
"span",
|
|
3155
|
+
{
|
|
3156
|
+
style: {
|
|
3157
|
+
width: "8px",
|
|
3158
|
+
height: "8px",
|
|
3159
|
+
borderRadius: "50%",
|
|
3160
|
+
backgroundColor: config.color,
|
|
3161
|
+
display: "inline-block",
|
|
3162
|
+
animation: status === "connecting" ? "pulse 1.5s infinite" : void 0
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
),
|
|
3166
|
+
/* @__PURE__ */ jsx("span", { style: { color: "var(--muted-foreground)" }, children: config.label })
|
|
3167
|
+
]
|
|
3168
|
+
}
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// src/components/Skeleton.tsx
|
|
3173
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
3174
|
+
var stylesInjected = false;
|
|
3175
|
+
function ensureStyles() {
|
|
3176
|
+
if (stylesInjected || typeof document === "undefined") return;
|
|
3177
|
+
const id = "xnet-skeleton-styles";
|
|
3178
|
+
if (document.getElementById(id)) {
|
|
3179
|
+
stylesInjected = true;
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
const el = document.createElement("style");
|
|
3183
|
+
el.id = id;
|
|
3184
|
+
el.textContent = `
|
|
3185
|
+
@keyframes xnet-skeleton-pulse {
|
|
3186
|
+
0% { background-position: 200% 0; }
|
|
3187
|
+
100% { background-position: -200% 0; }
|
|
3188
|
+
}
|
|
3189
|
+
`;
|
|
3190
|
+
document.head.appendChild(el);
|
|
3191
|
+
stylesInjected = true;
|
|
3192
|
+
}
|
|
3193
|
+
function Skeleton({
|
|
3194
|
+
width = "100%",
|
|
3195
|
+
height = "1em",
|
|
3196
|
+
borderRadius = "4px",
|
|
3197
|
+
style,
|
|
3198
|
+
lines = 1,
|
|
3199
|
+
gap = "0.5rem"
|
|
3200
|
+
}) {
|
|
3201
|
+
ensureStyles();
|
|
3202
|
+
const baseStyle = {
|
|
3203
|
+
display: "block",
|
|
3204
|
+
width,
|
|
3205
|
+
height,
|
|
3206
|
+
borderRadius,
|
|
3207
|
+
background: "linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%)",
|
|
3208
|
+
backgroundSize: "200% 100%",
|
|
3209
|
+
animation: "xnet-skeleton-pulse 1.5s ease-in-out infinite",
|
|
3210
|
+
...style
|
|
3211
|
+
};
|
|
3212
|
+
if (lines <= 1) {
|
|
3213
|
+
return /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", style: baseStyle });
|
|
3214
|
+
}
|
|
3215
|
+
return /* @__PURE__ */ jsx2("div", { "aria-hidden": "true", style: { display: "flex", flexDirection: "column", gap }, children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx2(
|
|
3216
|
+
"span",
|
|
3217
|
+
{
|
|
3218
|
+
style: {
|
|
3219
|
+
...baseStyle,
|
|
3220
|
+
// Last line is shorter for visual realism
|
|
3221
|
+
width: i === lines - 1 ? "60%" : width
|
|
3222
|
+
}
|
|
3223
|
+
},
|
|
3224
|
+
i
|
|
3225
|
+
)) });
|
|
3226
|
+
}
|
|
3227
|
+
function injectSkeletonStyles() {
|
|
3228
|
+
if (typeof document === "undefined") return;
|
|
3229
|
+
const id = "xnet-skeleton-styles";
|
|
3230
|
+
if (document.getElementById(id)) return;
|
|
3231
|
+
const style = document.createElement("style");
|
|
3232
|
+
style.id = id;
|
|
3233
|
+
style.textContent = `
|
|
3234
|
+
@keyframes xnet-skeleton-pulse {
|
|
3235
|
+
0% { background-position: 200% 0; }
|
|
3236
|
+
100% { background-position: -200% 0; }
|
|
3237
|
+
}
|
|
3238
|
+
`;
|
|
3239
|
+
document.head.appendChild(style);
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
// src/components/DemoBanner.tsx
|
|
3243
|
+
import { useState as useState21 } from "react";
|
|
3244
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
3245
|
+
var STORAGE_KEY = "xnet:demo-banner-dismissed";
|
|
3246
|
+
function DemoBanner({ evictionHours, onDismiss }) {
|
|
3247
|
+
const [dismissed, setDismissed] = useState21(() => {
|
|
3248
|
+
if (typeof window === "undefined") return false;
|
|
3249
|
+
return localStorage.getItem(STORAGE_KEY) === "true";
|
|
3250
|
+
});
|
|
3251
|
+
if (dismissed) return null;
|
|
3252
|
+
const handleDismiss = () => {
|
|
3253
|
+
localStorage.setItem(STORAGE_KEY, "true");
|
|
3254
|
+
setDismissed(true);
|
|
3255
|
+
onDismiss?.();
|
|
3256
|
+
};
|
|
3257
|
+
return /* @__PURE__ */ jsxs2("div", { className: "fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-4 py-2 bg-amber-100 dark:bg-amber-900/50 border-b border-amber-300 dark:border-amber-700 text-amber-900 dark:text-amber-100 text-sm", children: [
|
|
3258
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
|
|
3259
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
3260
|
+
"Demo mode \u2014 data is saved locally; encrypted backups expire after ",
|
|
3261
|
+
evictionHours,
|
|
3262
|
+
"h of inactivity."
|
|
3263
|
+
] }),
|
|
3264
|
+
/* @__PURE__ */ jsx3(
|
|
3265
|
+
"a",
|
|
3266
|
+
{
|
|
3267
|
+
href: "https://xnet.fyi/download",
|
|
3268
|
+
className: "ml-2 px-2 py-0.5 bg-amber-500 hover:bg-amber-600 text-white rounded text-xs font-medium transition-colors",
|
|
3269
|
+
children: "Download desktop app"
|
|
3270
|
+
}
|
|
3271
|
+
)
|
|
3272
|
+
] }),
|
|
3273
|
+
/* @__PURE__ */ jsx3(
|
|
3274
|
+
"button",
|
|
3275
|
+
{
|
|
3276
|
+
onClick: handleDismiss,
|
|
3277
|
+
className: "p-1 hover:bg-amber-200 dark:hover:bg-amber-800 rounded transition-colors",
|
|
3278
|
+
"aria-label": "Dismiss banner",
|
|
3279
|
+
children: /* @__PURE__ */ jsx3("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx3(
|
|
3280
|
+
"path",
|
|
3281
|
+
{
|
|
3282
|
+
strokeLinecap: "round",
|
|
3283
|
+
strokeLinejoin: "round",
|
|
3284
|
+
strokeWidth: 2,
|
|
3285
|
+
d: "M6 18L18 6M6 6l12 12"
|
|
3286
|
+
}
|
|
3287
|
+
) })
|
|
3288
|
+
}
|
|
3289
|
+
)
|
|
3290
|
+
] });
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// src/components/DemoQuotaIndicator.tsx
|
|
3294
|
+
import { formatBytes } from "@xnetjs/core";
|
|
3295
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
3296
|
+
function DemoQuotaIndicator({ usedBytes, limitBytes }) {
|
|
3297
|
+
const percentage = Math.round(usedBytes / limitBytes * 100);
|
|
3298
|
+
const isWarning = percentage >= 80;
|
|
3299
|
+
const isCritical = percentage >= 95;
|
|
3300
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
3301
|
+
/* @__PURE__ */ jsx4("div", { className: "w-16 h-1.5 bg-border rounded-full overflow-hidden", children: /* @__PURE__ */ jsx4(
|
|
3302
|
+
"div",
|
|
3303
|
+
{
|
|
3304
|
+
className: `h-full transition-all ${isCritical ? "bg-red-500" : isWarning ? "bg-amber-500" : "bg-primary"}`,
|
|
3305
|
+
style: { width: `${Math.min(percentage, 100)}%` }
|
|
3306
|
+
}
|
|
3307
|
+
) }),
|
|
3308
|
+
/* @__PURE__ */ jsxs3("span", { className: isCritical ? "text-red-500" : isWarning ? "text-amber-500" : "", children: [
|
|
3309
|
+
formatBytes(usedBytes),
|
|
3310
|
+
" / ",
|
|
3311
|
+
formatBytes(limitBytes)
|
|
3312
|
+
] })
|
|
3313
|
+
] });
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
// src/components/DemoDataExpiredScreen.tsx
|
|
3317
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3318
|
+
function DemoDataExpiredScreen() {
|
|
3319
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col items-center justify-center min-h-screen p-6 text-center bg-background", children: [
|
|
3320
|
+
/* @__PURE__ */ jsx5("div", { className: "text-6xl mb-6", children: "\u{1F550}" }),
|
|
3321
|
+
/* @__PURE__ */ jsx5("h1", { className: "text-2xl font-bold mb-3 text-foreground", children: "Your demo data has expired" }),
|
|
3322
|
+
/* @__PURE__ */ jsx5("p", { className: "text-muted-foreground max-w-md mb-6", children: "Your local data is still saved in this browser, but encrypted hub backups are removed after 24 hours of inactivity. Cross-device sync requires an active hub connection." }),
|
|
3323
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex gap-3", children: [
|
|
3324
|
+
/* @__PURE__ */ jsx5(
|
|
3325
|
+
"button",
|
|
3326
|
+
{
|
|
3327
|
+
onClick: () => window.location.reload(),
|
|
3328
|
+
className: "px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors",
|
|
3329
|
+
children: "Start Fresh"
|
|
3330
|
+
}
|
|
3331
|
+
),
|
|
3332
|
+
/* @__PURE__ */ jsx5(
|
|
3333
|
+
"a",
|
|
3334
|
+
{
|
|
3335
|
+
href: "https://xnet.fyi/download",
|
|
3336
|
+
className: "px-4 py-2 border border-border rounded-md hover:bg-accent transition-colors text-foreground no-underline",
|
|
3337
|
+
children: "Download Desktop App"
|
|
3338
|
+
}
|
|
3339
|
+
)
|
|
3340
|
+
] }),
|
|
3341
|
+
/* @__PURE__ */ jsx5("p", { className: "mt-8 text-sm text-muted-foreground", children: "The desktop app stores data permanently on your device." })
|
|
3342
|
+
] });
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
// src/hooks/useDemoMode.ts
|
|
3346
|
+
import { useState as useState22, useEffect as useEffect16 } from "react";
|
|
3347
|
+
function useDemoMode() {
|
|
3348
|
+
const { syncManager } = useXNet();
|
|
3349
|
+
const [state, setState] = useState22({ isDemo: false });
|
|
3350
|
+
useEffect16(() => {
|
|
3351
|
+
if (!syncManager?.connection) return;
|
|
3352
|
+
const connection = syncManager.connection;
|
|
3353
|
+
const unsubscribe = connection.onMessage((message) => {
|
|
3354
|
+
if (message.type !== "handshake") return;
|
|
3355
|
+
const isDemo = message.isDemo === true;
|
|
3356
|
+
if (!isDemo) {
|
|
3357
|
+
setState({ isDemo: false });
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
const demoLimits = message.demoLimits;
|
|
3361
|
+
setState({
|
|
3362
|
+
isDemo: true,
|
|
3363
|
+
limits: demoLimits ? {
|
|
3364
|
+
quotaBytes: demoLimits.quotaBytes,
|
|
3365
|
+
maxDocs: demoLimits.maxDocs,
|
|
3366
|
+
evictionHours: Math.round(demoLimits.evictionTtlMs / 36e5)
|
|
3367
|
+
} : void 0
|
|
3368
|
+
});
|
|
3369
|
+
});
|
|
3370
|
+
return unsubscribe;
|
|
3371
|
+
}, [syncManager]);
|
|
3372
|
+
return state;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
// src/onboarding/machine.ts
|
|
3376
|
+
var TRANSITIONS = {
|
|
3377
|
+
welcome: {
|
|
3378
|
+
AUTHENTICATE: "authenticating",
|
|
3379
|
+
CREATE_NEW: "authenticating",
|
|
3380
|
+
CREATE_RECOVERABLE: "creating-recoverable",
|
|
3381
|
+
IMPORT_EXISTING: "import-identity",
|
|
3382
|
+
BROWSER_UNSUPPORTED: "unsupported-browser"
|
|
3383
|
+
},
|
|
3384
|
+
"creating-recoverable": {
|
|
3385
|
+
RECOVERABLE_CREATED: "show-recovery-phrase",
|
|
3386
|
+
PASSKEY_FAILED: "auth-error",
|
|
3387
|
+
BACK_TO_WELCOME: "welcome"
|
|
3388
|
+
},
|
|
3389
|
+
"show-recovery-phrase": {
|
|
3390
|
+
PHRASE_SAVED: "connecting-hub"
|
|
3391
|
+
},
|
|
3392
|
+
authenticating: {
|
|
3393
|
+
PASSKEY_SUCCESS: "connecting-hub",
|
|
3394
|
+
PASSKEY_FAILED: "auth-error"
|
|
3395
|
+
},
|
|
3396
|
+
"auth-error": {
|
|
3397
|
+
RETRY_AUTH: "authenticating",
|
|
3398
|
+
BACK_TO_WELCOME: "welcome"
|
|
3399
|
+
},
|
|
3400
|
+
"import-identity": {
|
|
3401
|
+
SCAN_QR: "qr-scan",
|
|
3402
|
+
ENTER_PHRASE: "recovery-phrase",
|
|
3403
|
+
ENTER_GUARDIAN_SHARES: "guardian-recovery",
|
|
3404
|
+
USE_SYNCED_PASSKEY: "authenticating",
|
|
3405
|
+
BACK_TO_WELCOME: "welcome"
|
|
3406
|
+
},
|
|
3407
|
+
"guardian-recovery": {
|
|
3408
|
+
IDENTITY_IMPORTED: "connecting-hub",
|
|
3409
|
+
IMPORT_FAILED: "guardian-recovery",
|
|
3410
|
+
BACK_TO_WELCOME: "welcome"
|
|
3411
|
+
},
|
|
3412
|
+
"qr-scan": {
|
|
3413
|
+
IDENTITY_IMPORTED: "connecting-hub",
|
|
3414
|
+
BACK_TO_WELCOME: "welcome"
|
|
3415
|
+
},
|
|
3416
|
+
"recovery-phrase": {
|
|
3417
|
+
IDENTITY_IMPORTED: "connecting-hub",
|
|
3418
|
+
IMPORT_FAILED: "recovery-phrase",
|
|
3419
|
+
BACK_TO_WELCOME: "welcome"
|
|
3420
|
+
},
|
|
3421
|
+
"connecting-hub": {
|
|
3422
|
+
HUB_CONNECTED: "ready",
|
|
3423
|
+
HUB_FAILED: "ready"
|
|
3424
|
+
// Continue anyway — hub is optional
|
|
3425
|
+
},
|
|
3426
|
+
ready: {
|
|
3427
|
+
CREATE_FIRST_PAGE: "complete"
|
|
3428
|
+
}
|
|
3429
|
+
// 'unsupported-browser' and 'complete' have no transitions (terminal)
|
|
3430
|
+
};
|
|
3431
|
+
function onboardingReducer(current, event) {
|
|
3432
|
+
const transitions = TRANSITIONS[current.state];
|
|
3433
|
+
const nextState = transitions?.[event.type];
|
|
3434
|
+
if (!nextState) {
|
|
3435
|
+
return current;
|
|
3436
|
+
}
|
|
3437
|
+
const nextContext = { ...current.context };
|
|
3438
|
+
switch (event.type) {
|
|
3439
|
+
case "PASSKEY_SUCCESS":
|
|
3440
|
+
nextContext.identity = event.identity;
|
|
3441
|
+
nextContext.keyBundle = event.keyBundle;
|
|
3442
|
+
nextContext.error = null;
|
|
3443
|
+
break;
|
|
3444
|
+
case "PASSKEY_FAILED":
|
|
3445
|
+
nextContext.error = event.error;
|
|
3446
|
+
break;
|
|
3447
|
+
case "IDENTITY_IMPORTED":
|
|
3448
|
+
nextContext.identity = event.identity;
|
|
3449
|
+
nextContext.keyBundle = event.keyBundle;
|
|
3450
|
+
nextContext.error = null;
|
|
3451
|
+
break;
|
|
3452
|
+
case "RECOVERABLE_CREATED":
|
|
3453
|
+
nextContext.identity = event.identity;
|
|
3454
|
+
nextContext.keyBundle = event.keyBundle;
|
|
3455
|
+
nextContext.recoveryPhrase = event.phrase;
|
|
3456
|
+
nextContext.error = null;
|
|
3457
|
+
break;
|
|
3458
|
+
case "IMPORT_FAILED":
|
|
3459
|
+
nextContext.error = event.error;
|
|
3460
|
+
break;
|
|
3461
|
+
case "CREATE_RECOVERABLE":
|
|
3462
|
+
nextContext.error = null;
|
|
3463
|
+
break;
|
|
3464
|
+
case "HUB_FAILED":
|
|
3465
|
+
nextContext.error = event.error;
|
|
3466
|
+
break;
|
|
3467
|
+
case "HUB_CONNECTED":
|
|
3468
|
+
nextContext.error = null;
|
|
3469
|
+
break;
|
|
3470
|
+
case "RETRY_AUTH":
|
|
3471
|
+
case "AUTHENTICATE":
|
|
3472
|
+
case "CREATE_NEW":
|
|
3473
|
+
nextContext.error = null;
|
|
3474
|
+
break;
|
|
3475
|
+
}
|
|
3476
|
+
return { state: nextState, context: nextContext };
|
|
3477
|
+
}
|
|
3478
|
+
function createInitialState(hubUrl) {
|
|
3479
|
+
return {
|
|
3480
|
+
state: "welcome",
|
|
3481
|
+
context: {
|
|
3482
|
+
identity: null,
|
|
3483
|
+
keyBundle: null,
|
|
3484
|
+
hubUrl: hubUrl ?? null,
|
|
3485
|
+
error: null,
|
|
3486
|
+
isDemo: false,
|
|
3487
|
+
recoveryPhrase: null
|
|
3488
|
+
}
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
// src/onboarding/OnboardingProvider.tsx
|
|
3493
|
+
import {
|
|
3494
|
+
detectPasskeySupport,
|
|
3495
|
+
createIdentityManager,
|
|
3496
|
+
isTestBypassEnabled,
|
|
3497
|
+
parseShare
|
|
3498
|
+
} from "@xnetjs/identity";
|
|
3499
|
+
import {
|
|
3500
|
+
createContext,
|
|
3501
|
+
useContext as useContext7,
|
|
3502
|
+
useReducer,
|
|
3503
|
+
useCallback as useCallback18,
|
|
3504
|
+
useEffect as useEffect17,
|
|
3505
|
+
useRef as useRef13,
|
|
3506
|
+
useMemo as useMemo14
|
|
3507
|
+
} from "react";
|
|
3508
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
3509
|
+
var OnboardingCtx = createContext(null);
|
|
3510
|
+
function OnboardingProvider({
|
|
3511
|
+
children,
|
|
3512
|
+
defaultHubUrl = "wss://hub.xnet.fyi",
|
|
3513
|
+
onComplete
|
|
3514
|
+
}) {
|
|
3515
|
+
const [{ state, context }, dispatch] = useReducer(
|
|
3516
|
+
onboardingReducer,
|
|
3517
|
+
createInitialState(defaultHubUrl)
|
|
3518
|
+
);
|
|
3519
|
+
const manager = useMemo14(() => createIdentityManager(), []);
|
|
3520
|
+
useEffect17(() => {
|
|
3521
|
+
let cancelled = false;
|
|
3522
|
+
manager.preflight().catch(() => {
|
|
3523
|
+
});
|
|
3524
|
+
detectPasskeySupport().then((support) => {
|
|
3525
|
+
if (isTestBypassEnabled()) {
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
if (!cancelled && (!support.webauthn || !support.platform)) {
|
|
3529
|
+
dispatch({ type: "BROWSER_UNSUPPORTED" });
|
|
3530
|
+
}
|
|
3531
|
+
}).catch(() => {
|
|
3532
|
+
});
|
|
3533
|
+
return () => {
|
|
3534
|
+
cancelled = true;
|
|
3535
|
+
};
|
|
3536
|
+
}, [manager]);
|
|
3537
|
+
useEffect17(() => {
|
|
3538
|
+
if (state === "complete" && context.identity && context.keyBundle && onComplete) {
|
|
3539
|
+
onComplete(context.identity, context.keyBundle);
|
|
3540
|
+
}
|
|
3541
|
+
}, [state, context.identity, context.keyBundle, onComplete]);
|
|
3542
|
+
const authInFlight = useRef13(false);
|
|
3543
|
+
const send = useCallback18(
|
|
3544
|
+
(event) => {
|
|
3545
|
+
if ((event.type === "AUTHENTICATE" || event.type === "CREATE_NEW") && state === "welcome" || event.type === "RETRY_AUTH" && state === "auth-error") {
|
|
3546
|
+
if (authInFlight.current) return;
|
|
3547
|
+
authInFlight.current = true;
|
|
3548
|
+
manager.create().then((keyBundle) => {
|
|
3549
|
+
dispatch({
|
|
3550
|
+
type: "PASSKEY_SUCCESS",
|
|
3551
|
+
identity: keyBundle.identity,
|
|
3552
|
+
keyBundle
|
|
3553
|
+
});
|
|
3554
|
+
}).catch((err) => {
|
|
3555
|
+
dispatch({
|
|
3556
|
+
type: "PASSKEY_FAILED",
|
|
3557
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
3558
|
+
});
|
|
3559
|
+
}).finally(() => {
|
|
3560
|
+
authInFlight.current = false;
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
if (event.type === "CREATE_RECOVERABLE" && state === "welcome") {
|
|
3564
|
+
if (authInFlight.current) return;
|
|
3565
|
+
authInFlight.current = true;
|
|
3566
|
+
manager.createRecoverable().then(({ keyBundle, phrase }) => {
|
|
3567
|
+
dispatch({
|
|
3568
|
+
type: "RECOVERABLE_CREATED",
|
|
3569
|
+
identity: keyBundle.identity,
|
|
3570
|
+
keyBundle,
|
|
3571
|
+
phrase
|
|
3572
|
+
});
|
|
3573
|
+
}).catch((err) => {
|
|
3574
|
+
dispatch({
|
|
3575
|
+
type: "PASSKEY_FAILED",
|
|
3576
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
3577
|
+
});
|
|
3578
|
+
}).finally(() => {
|
|
3579
|
+
authInFlight.current = false;
|
|
3580
|
+
});
|
|
3581
|
+
}
|
|
3582
|
+
if (event.type === "USE_SYNCED_PASSKEY" && state === "import-identity") {
|
|
3583
|
+
if (authInFlight.current) return;
|
|
3584
|
+
authInFlight.current = true;
|
|
3585
|
+
manager.recoverViaSyncedPasskey().then((keyBundle) => {
|
|
3586
|
+
if (keyBundle) {
|
|
3587
|
+
dispatch({ type: "PASSKEY_SUCCESS", identity: keyBundle.identity, keyBundle });
|
|
3588
|
+
} else {
|
|
3589
|
+
dispatch({
|
|
3590
|
+
type: "PASSKEY_FAILED",
|
|
3591
|
+
error: new Error("No synced passkey found on this device")
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
}).catch((err) => {
|
|
3595
|
+
dispatch({
|
|
3596
|
+
type: "PASSKEY_FAILED",
|
|
3597
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
3598
|
+
});
|
|
3599
|
+
}).finally(() => {
|
|
3600
|
+
authInFlight.current = false;
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
if (event.type === "SUBMIT_GUARDIAN_SHARES" && state === "guardian-recovery") {
|
|
3604
|
+
if (authInFlight.current) return;
|
|
3605
|
+
authInFlight.current = true;
|
|
3606
|
+
Promise.resolve().then(() => {
|
|
3607
|
+
const shares = event.codes.map((code) => parseShare(code));
|
|
3608
|
+
return manager.recoverFromGuardianShares(shares);
|
|
3609
|
+
}).then(({ keyBundle }) => {
|
|
3610
|
+
dispatch({ type: "IDENTITY_IMPORTED", identity: keyBundle.identity, keyBundle });
|
|
3611
|
+
}).catch((err) => {
|
|
3612
|
+
dispatch({
|
|
3613
|
+
type: "IMPORT_FAILED",
|
|
3614
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
3615
|
+
});
|
|
3616
|
+
}).finally(() => {
|
|
3617
|
+
authInFlight.current = false;
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
if (event.type === "SUBMIT_PHRASE" && state === "recovery-phrase") {
|
|
3621
|
+
if (authInFlight.current) return;
|
|
3622
|
+
authInFlight.current = true;
|
|
3623
|
+
manager.importRecoveryPhrase(event.phrase).then(({ keyBundle }) => {
|
|
3624
|
+
dispatch({
|
|
3625
|
+
type: "IDENTITY_IMPORTED",
|
|
3626
|
+
identity: keyBundle.identity,
|
|
3627
|
+
keyBundle
|
|
3628
|
+
});
|
|
3629
|
+
}).catch((err) => {
|
|
3630
|
+
dispatch({
|
|
3631
|
+
type: "IMPORT_FAILED",
|
|
3632
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
3633
|
+
});
|
|
3634
|
+
}).finally(() => {
|
|
3635
|
+
authInFlight.current = false;
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
dispatch(event);
|
|
3639
|
+
},
|
|
3640
|
+
[state, manager]
|
|
3641
|
+
);
|
|
3642
|
+
return /* @__PURE__ */ jsx6(OnboardingCtx.Provider, { value: { state, context, send }, children });
|
|
3643
|
+
}
|
|
3644
|
+
function useOnboarding() {
|
|
3645
|
+
const ctx = useContext7(OnboardingCtx);
|
|
3646
|
+
if (!ctx) {
|
|
3647
|
+
throw new Error("useOnboarding must be used within <OnboardingProvider>");
|
|
3648
|
+
}
|
|
3649
|
+
return ctx;
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
// src/onboarding/helpers.ts
|
|
3653
|
+
function getPlatformAuthName() {
|
|
3654
|
+
if (typeof navigator === "undefined") return "Biometric authentication";
|
|
3655
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
3656
|
+
if (ua.includes("mac") || ua.includes("iphone") || ua.includes("ipad")) {
|
|
3657
|
+
return "Touch ID";
|
|
3658
|
+
}
|
|
3659
|
+
if (ua.includes("windows")) {
|
|
3660
|
+
return "Windows Hello";
|
|
3661
|
+
}
|
|
3662
|
+
if (ua.includes("android")) {
|
|
3663
|
+
return "Fingerprint";
|
|
3664
|
+
}
|
|
3665
|
+
return "Biometric authentication";
|
|
3666
|
+
}
|
|
3667
|
+
function truncateDid(did, headLen = 16, tailLen = 4) {
|
|
3668
|
+
if (did.length <= headLen + tailLen + 3) return did;
|
|
3669
|
+
return `${did.slice(0, headLen)}...${did.slice(-tailLen)}`;
|
|
3670
|
+
}
|
|
3671
|
+
async function copyToClipboard(text) {
|
|
3672
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
3673
|
+
return false;
|
|
3674
|
+
}
|
|
3675
|
+
try {
|
|
3676
|
+
await navigator.clipboard.writeText(text);
|
|
3677
|
+
return true;
|
|
3678
|
+
} catch {
|
|
3679
|
+
return false;
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
// src/onboarding/screens/AuthenticatingScreen.tsx
|
|
3684
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3685
|
+
function AuthenticatingScreen() {
|
|
3686
|
+
return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col items-center justify-center min-h-screen bg-background text-foreground p-6", children: [
|
|
3687
|
+
/* @__PURE__ */ jsx7("div", { className: "w-12 h-12 mb-6 border-4 border-primary border-t-transparent rounded-full animate-spin" }),
|
|
3688
|
+
/* @__PURE__ */ jsxs5("h1", { className: "text-2xl font-semibold mb-2", children: [
|
|
3689
|
+
"Waiting for ",
|
|
3690
|
+
getPlatformAuthName()
|
|
3691
|
+
] }),
|
|
3692
|
+
/* @__PURE__ */ jsx7("p", { className: "text-muted-foreground text-center", children: "Complete the biometric prompt to continue..." })
|
|
3693
|
+
] });
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
// src/onboarding/screens/AuthErrorScreen.tsx
|
|
3697
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3698
|
+
function AuthErrorScreen() {
|
|
3699
|
+
const { send, context } = useOnboarding();
|
|
3700
|
+
return /* @__PURE__ */ jsxs6("div", { className: "flex flex-col items-center justify-center min-h-screen bg-background text-foreground p-6", children: [
|
|
3701
|
+
/* @__PURE__ */ jsx8("div", { className: "text-5xl mb-4", children: "!" }),
|
|
3702
|
+
/* @__PURE__ */ jsx8("h1", { className: "text-2xl font-semibold mb-2", children: "Authentication failed" }),
|
|
3703
|
+
/* @__PURE__ */ jsxs6("p", { className: "text-muted-foreground mb-2", children: [
|
|
3704
|
+
"Could not set up ",
|
|
3705
|
+
getPlatformAuthName(),
|
|
3706
|
+
"."
|
|
3707
|
+
] }),
|
|
3708
|
+
context.error && /* @__PURE__ */ jsx8("p", { className: "text-destructive text-sm mb-4 max-w-md text-center", children: context.error.message }),
|
|
3709
|
+
/* @__PURE__ */ jsx8(
|
|
3710
|
+
"button",
|
|
3711
|
+
{
|
|
3712
|
+
className: "px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors mb-3",
|
|
3713
|
+
onClick: () => send({ type: "RETRY_AUTH" }),
|
|
3714
|
+
children: "Try again"
|
|
3715
|
+
}
|
|
3716
|
+
),
|
|
3717
|
+
/* @__PURE__ */ jsx8(
|
|
3718
|
+
"button",
|
|
3719
|
+
{
|
|
3720
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors mb-6",
|
|
3721
|
+
onClick: () => send({ type: "BACK_TO_WELCOME" }),
|
|
3722
|
+
children: "Back to welcome"
|
|
3723
|
+
}
|
|
3724
|
+
),
|
|
3725
|
+
/* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground max-w-sm text-center", children: "Make sure your browser supports passkeys (Chrome 116+, Safari 18+, Edge 116+)." })
|
|
3726
|
+
] });
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
// src/onboarding/screens/HubConnectScreen.tsx
|
|
3730
|
+
import { useEffect as useEffect18 } from "react";
|
|
3731
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3732
|
+
function HubConnectScreen({ connectToHub }) {
|
|
3733
|
+
const { send, context } = useOnboarding();
|
|
3734
|
+
useEffect18(() => {
|
|
3735
|
+
let cancelled = false;
|
|
3736
|
+
if (connectToHub) {
|
|
3737
|
+
connectToHub().then(() => {
|
|
3738
|
+
if (!cancelled) send({ type: "HUB_CONNECTED" });
|
|
3739
|
+
}).catch((err) => {
|
|
3740
|
+
if (!cancelled) {
|
|
3741
|
+
send({
|
|
3742
|
+
type: "HUB_FAILED",
|
|
3743
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
});
|
|
3747
|
+
} else {
|
|
3748
|
+
send({ type: "HUB_CONNECTED" });
|
|
3749
|
+
}
|
|
3750
|
+
return () => {
|
|
3751
|
+
cancelled = true;
|
|
3752
|
+
};
|
|
3753
|
+
}, []);
|
|
3754
|
+
return /* @__PURE__ */ jsxs7("div", { className: "flex flex-col items-center justify-center min-h-screen bg-background text-foreground p-6", children: [
|
|
3755
|
+
/* @__PURE__ */ jsx9("div", { className: "w-12 h-12 mb-6 border-4 border-primary border-t-transparent rounded-full animate-spin" }),
|
|
3756
|
+
/* @__PURE__ */ jsx9("h1", { className: "text-2xl font-semibold mb-2", children: "Connecting to sync server" }),
|
|
3757
|
+
/* @__PURE__ */ jsx9("p", { className: "text-muted-foreground mb-4", children: "Setting up secure connection..." }),
|
|
3758
|
+
context.hubUrl && /* @__PURE__ */ jsx9("p", { className: "text-xs text-muted-foreground font-mono", children: context.hubUrl })
|
|
3759
|
+
] });
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// src/onboarding/screens/ImportIdentityScreen.tsx
|
|
3763
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3764
|
+
function ImportIdentityScreen() {
|
|
3765
|
+
const { send } = useOnboarding();
|
|
3766
|
+
return /* @__PURE__ */ jsxs8("div", { className: "flex flex-col items-center justify-center min-h-screen bg-background text-foreground p-6", children: [
|
|
3767
|
+
/* @__PURE__ */ jsx10("h1", { className: "text-2xl font-semibold mb-2", children: "Import your identity" }),
|
|
3768
|
+
/* @__PURE__ */ jsx10("p", { className: "text-muted-foreground text-center mb-8 max-w-md", children: "Bring your existing xNet identity to this device." }),
|
|
3769
|
+
/* @__PURE__ */ jsx10(
|
|
3770
|
+
"button",
|
|
3771
|
+
{
|
|
3772
|
+
className: "px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors mb-3 w-64",
|
|
3773
|
+
onClick: () => send({ type: "USE_SYNCED_PASSKEY" }),
|
|
3774
|
+
children: "Use a synced passkey"
|
|
3775
|
+
}
|
|
3776
|
+
),
|
|
3777
|
+
/* @__PURE__ */ jsx10(
|
|
3778
|
+
"button",
|
|
3779
|
+
{
|
|
3780
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors mb-2",
|
|
3781
|
+
onClick: () => send({ type: "SCAN_QR" }),
|
|
3782
|
+
children: "Scan from another device"
|
|
3783
|
+
}
|
|
3784
|
+
),
|
|
3785
|
+
/* @__PURE__ */ jsx10(
|
|
3786
|
+
"button",
|
|
3787
|
+
{
|
|
3788
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors mb-2",
|
|
3789
|
+
onClick: () => send({ type: "ENTER_PHRASE" }),
|
|
3790
|
+
children: "Enter recovery phrase"
|
|
3791
|
+
}
|
|
3792
|
+
),
|
|
3793
|
+
/* @__PURE__ */ jsx10(
|
|
3794
|
+
"button",
|
|
3795
|
+
{
|
|
3796
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors mb-6",
|
|
3797
|
+
onClick: () => send({ type: "ENTER_GUARDIAN_SHARES" }),
|
|
3798
|
+
children: "Recover with guardian shares"
|
|
3799
|
+
}
|
|
3800
|
+
),
|
|
3801
|
+
/* @__PURE__ */ jsx10(
|
|
3802
|
+
"button",
|
|
3803
|
+
{
|
|
3804
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors",
|
|
3805
|
+
onClick: () => send({ type: "BACK_TO_WELCOME" }),
|
|
3806
|
+
children: "Back"
|
|
3807
|
+
}
|
|
3808
|
+
)
|
|
3809
|
+
] });
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
// src/onboarding/screens/ReadyScreen.tsx
|
|
3813
|
+
import { useState as useState23, useEffect as useEffect19, useRef as useRef14 } from "react";
|
|
3814
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3815
|
+
function ReadyScreen() {
|
|
3816
|
+
const { send, context } = useOnboarding();
|
|
3817
|
+
const [copied, setCopied] = useState23(false);
|
|
3818
|
+
const timerRef = useRef14(null);
|
|
3819
|
+
useEffect19(() => {
|
|
3820
|
+
return () => {
|
|
3821
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3822
|
+
};
|
|
3823
|
+
}, []);
|
|
3824
|
+
const handleCopy = async () => {
|
|
3825
|
+
if (context.identity?.did) {
|
|
3826
|
+
const ok = await copyToClipboard(context.identity.did);
|
|
3827
|
+
if (ok) {
|
|
3828
|
+
setCopied(true);
|
|
3829
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
3830
|
+
timerRef.current = setTimeout(() => setCopied(false), 2e3);
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
};
|
|
3834
|
+
return /* @__PURE__ */ jsxs9("div", { className: "flex flex-col items-center justify-center min-h-screen bg-background text-foreground p-6", children: [
|
|
3835
|
+
/* @__PURE__ */ jsx11("div", { className: "text-5xl mb-4", children: "\u2713" }),
|
|
3836
|
+
/* @__PURE__ */ jsx11("h1", { className: "text-2xl font-semibold mb-6", children: "You're all set!" }),
|
|
3837
|
+
context.identity && /* @__PURE__ */ jsxs9("div", { className: "flex flex-col items-center mb-4", children: [
|
|
3838
|
+
/* @__PURE__ */ jsx11("label", { className: "text-xs text-muted-foreground mb-1", children: "Your identity" }),
|
|
3839
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
|
|
3840
|
+
/* @__PURE__ */ jsx11("code", { className: "text-sm font-mono bg-muted px-3 py-1 rounded", children: truncateDid(context.identity.did) }),
|
|
3841
|
+
/* @__PURE__ */ jsx11(
|
|
3842
|
+
"button",
|
|
3843
|
+
{
|
|
3844
|
+
className: "text-xs text-primary hover:text-primary/80 transition-colors",
|
|
3845
|
+
onClick: handleCopy,
|
|
3846
|
+
title: "Copy full DID",
|
|
3847
|
+
children: copied ? "Copied!" : "Copy"
|
|
3848
|
+
}
|
|
3849
|
+
)
|
|
3850
|
+
] })
|
|
3851
|
+
] }),
|
|
3852
|
+
context.hubUrl && /* @__PURE__ */ jsxs9("div", { className: "flex flex-col items-center mb-4", children: [
|
|
3853
|
+
/* @__PURE__ */ jsx11("label", { className: "text-xs text-muted-foreground mb-1", children: "Connected to" }),
|
|
3854
|
+
/* @__PURE__ */ jsx11("span", { className: "text-sm font-mono", children: context.hubUrl })
|
|
3855
|
+
] }),
|
|
3856
|
+
context.isDemo && /* @__PURE__ */ jsx11("div", { className: "px-4 py-2 bg-amber-100 dark:bg-amber-900/30 text-amber-900 dark:text-amber-100 rounded-lg text-sm mb-6", children: "Demo mode \u2014 data is saved locally; encrypted backups expire after 24h of inactivity." }),
|
|
3857
|
+
/* @__PURE__ */ jsx11(
|
|
3858
|
+
"button",
|
|
3859
|
+
{
|
|
3860
|
+
className: "px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors",
|
|
3861
|
+
onClick: () => send({ type: "CREATE_FIRST_PAGE" }),
|
|
3862
|
+
children: "Create your first page"
|
|
3863
|
+
}
|
|
3864
|
+
)
|
|
3865
|
+
] });
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
// src/onboarding/screens/UnsupportedBrowserScreen.tsx
|
|
3869
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3870
|
+
function UnsupportedBrowserScreen() {
|
|
3871
|
+
return /* @__PURE__ */ jsxs10("div", { className: "flex flex-col items-center justify-center min-h-screen bg-background text-foreground p-6", children: [
|
|
3872
|
+
/* @__PURE__ */ jsx12("div", { className: "text-5xl mb-4", children: ":(" }),
|
|
3873
|
+
/* @__PURE__ */ jsx12("h1", { className: "text-2xl font-semibold mb-2", children: "Browser not supported" }),
|
|
3874
|
+
/* @__PURE__ */ jsxs10("p", { className: "text-muted-foreground text-center mb-6 max-w-md", children: [
|
|
3875
|
+
"xNet requires ",
|
|
3876
|
+
getPlatformAuthName(),
|
|
3877
|
+
" which isn't available in this browser."
|
|
3878
|
+
] }),
|
|
3879
|
+
/* @__PURE__ */ jsx12(
|
|
3880
|
+
"a",
|
|
3881
|
+
{
|
|
3882
|
+
href: "/download",
|
|
3883
|
+
className: "px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors mb-6 no-underline",
|
|
3884
|
+
children: "Download Desktop App"
|
|
3885
|
+
}
|
|
3886
|
+
),
|
|
3887
|
+
/* @__PURE__ */ jsx12("p", { className: "text-xs text-muted-foreground", children: "Supported browsers: Chrome 116+, Safari 18+, Edge 116+" })
|
|
3888
|
+
] });
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
// src/onboarding/screens/WelcomeScreen.tsx
|
|
3892
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3893
|
+
function WelcomeScreen() {
|
|
3894
|
+
const { send } = useOnboarding();
|
|
3895
|
+
return /* @__PURE__ */ jsxs11("div", { className: "flex flex-col items-center justify-center min-h-screen bg-gradient-to-b from-background to-muted/30 text-foreground p-6", children: [
|
|
3896
|
+
/* @__PURE__ */ jsx13("div", { className: "w-16 h-16 mb-8 rounded-2xl bg-primary/10 flex items-center justify-center", children: /* @__PURE__ */ jsxs11(
|
|
3897
|
+
"svg",
|
|
3898
|
+
{
|
|
3899
|
+
className: "w-8 h-8 text-primary",
|
|
3900
|
+
viewBox: "0 0 24 24",
|
|
3901
|
+
fill: "none",
|
|
3902
|
+
stroke: "currentColor",
|
|
3903
|
+
strokeWidth: "2",
|
|
3904
|
+
strokeLinecap: "round",
|
|
3905
|
+
strokeLinejoin: "round",
|
|
3906
|
+
children: [
|
|
3907
|
+
/* @__PURE__ */ jsx13("path", { d: "M12 2L2 7l10 5 10-5-10-5z" }),
|
|
3908
|
+
/* @__PURE__ */ jsx13("path", { d: "M2 17l10 5 10-5" }),
|
|
3909
|
+
/* @__PURE__ */ jsx13("path", { d: "M2 12l10 5 10-5" })
|
|
3910
|
+
]
|
|
3911
|
+
}
|
|
3912
|
+
) }),
|
|
3913
|
+
/* @__PURE__ */ jsx13("h1", { className: "text-4xl font-bold mb-3 tracking-tight", children: "Welcome to xNet" }),
|
|
3914
|
+
/* @__PURE__ */ jsx13("p", { className: "text-muted-foreground text-center mb-10 max-w-sm text-lg leading-relaxed", children: "Your private workspace that syncs everywhere and belongs to you." }),
|
|
3915
|
+
/* @__PURE__ */ jsx13(
|
|
3916
|
+
"button",
|
|
3917
|
+
{
|
|
3918
|
+
className: "group px-8 py-4 bg-primary text-primary-foreground rounded-xl font-semibold text-lg shadow-lg shadow-primary/25 hover:shadow-xl hover:shadow-primary/30 hover:scale-[1.02] active:scale-[0.98] transition-all duration-200 mb-4",
|
|
3919
|
+
onClick: () => send({ type: "AUTHENTICATE" }),
|
|
3920
|
+
children: /* @__PURE__ */ jsxs11("span", { className: "flex items-center gap-2", children: [
|
|
3921
|
+
getPlatformAuthName() === "Touch ID" && /* @__PURE__ */ jsx13("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx13("path", { d: "M17.81 4.47c-.08 0-.16-.02-.23-.06C15.66 3.42 14 3 12.01 3c-1.98 0-3.86.47-5.57 1.41-.24.13-.54.04-.68-.2-.13-.24-.04-.55.2-.68C7.82 2.52 9.86 2 12.01 2c2.13 0 3.99.47 6.03 1.52.25.13.34.43.21.67-.09.18-.26.28-.44.28zM3.5 9.72c-.1 0-.2-.03-.29-.09-.23-.16-.28-.47-.12-.7.99-1.4 2.25-2.5 3.75-3.27C9.98 4.04 14 4.03 17.15 5.65c1.5.77 2.76 1.86 3.75 3.25.16.22.11.54-.12.7-.23.16-.54.11-.7-.12-.9-1.26-2.04-2.25-3.39-2.94-2.87-1.47-6.54-1.47-9.4.01-1.36.7-2.5 1.7-3.4 2.96-.08.14-.23.21-.39.21zm6.25 12.07c-.13 0-.26-.05-.35-.15-.87-.87-1.34-1.43-2.01-2.64-.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39c0 .28-.22.5-.5.5s-.5-.22-.5-.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44.32 2.77.93 3.85.64 1.15 1.08 1.64 1.85 2.42.19.2.19.51 0 .71-.11.1-.24.15-.37.15zm7.17-1.85c-1.19 0-2.24-.3-3.1-.89-1.49-1.01-2.38-2.65-2.38-4.39 0-.28.22-.5.5-.5s.5.22.5.5c0 1.41.72 2.74 1.94 3.56.71.48 1.54.71 2.54.71.24 0 .64-.03 1.04-.1.27-.05.53.13.58.41.05.27-.13.53-.41.58-.57.11-1.07.12-1.21.12zM14.91 22c-.04 0-.09-.01-.13-.02-1.59-.44-2.63-1.03-3.72-2.1-1.4-1.39-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07.93 1.94 2.08 1.94s2.08-.87 2.08-1.94c0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.44 1.58-6.61 4.03-.39.81-.59 1.76-.59 2.8 0 .78.07 2.01.67 3.61.1.26-.03.55-.29.64-.26.1-.55-.04-.64-.29-.49-1.31-.73-2.61-.73-3.96 0-1.2.23-2.29.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.55 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94s-3.08-1.32-3.08-2.94c0-1.07-.93-1.94-2.08-1.94s-2.08.87-2.08 1.94c0 1.71.66 3.31 1.87 4.51.95.94 1.86 1.46 3.27 1.85.27.07.42.35.35.61-.05.23-.26.38-.48.38z" }) }),
|
|
3922
|
+
getPlatformAuthName() === "Face ID" && /* @__PURE__ */ jsx13("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx13("path", { d: "M9 11.75c-.69 0-1.25.56-1.25 1.25s.56 1.25 1.25 1.25 1.25-.56 1.25-1.25-.56-1.25-1.25-1.25zm6 0c-.69 0-1.25.56-1.25 1.25s.56 1.25 1.25 1.25 1.25-.56 1.25-1.25-.56-1.25-1.25-1.25zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8 0-.29.02-.58.05-.86 2.36-1.05 4.23-2.98 5.21-5.37C11.07 8.33 14.05 10 17.42 10c.78 0 1.53-.09 2.25-.26.21.71.33 1.47.33 2.26 0 4.41-3.59 8-8 8z" }) }),
|
|
3923
|
+
getPlatformAuthName() === "Windows Hello" && /* @__PURE__ */ jsx13("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx13("path", { d: "M3 5v14h18V5H3zm16 12H5V7h14v10z" }) }),
|
|
3924
|
+
!["Touch ID", "Face ID", "Windows Hello"].includes(getPlatformAuthName()) && /* @__PURE__ */ jsxs11(
|
|
3925
|
+
"svg",
|
|
3926
|
+
{
|
|
3927
|
+
className: "w-5 h-5",
|
|
3928
|
+
viewBox: "0 0 24 24",
|
|
3929
|
+
fill: "none",
|
|
3930
|
+
stroke: "currentColor",
|
|
3931
|
+
strokeWidth: "2",
|
|
3932
|
+
children: [
|
|
3933
|
+
/* @__PURE__ */ jsx13("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }),
|
|
3934
|
+
/* @__PURE__ */ jsx13("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })
|
|
3935
|
+
]
|
|
3936
|
+
}
|
|
3937
|
+
),
|
|
3938
|
+
"Get started with ",
|
|
3939
|
+
getPlatformAuthName()
|
|
3940
|
+
] })
|
|
3941
|
+
}
|
|
3942
|
+
),
|
|
3943
|
+
/* @__PURE__ */ jsx13("p", { className: "text-xs text-muted-foreground/70 text-center max-w-xs mt-2 mb-6", children: "Creates a secure passkey on your device. No passwords needed." }),
|
|
3944
|
+
/* @__PURE__ */ jsx13(
|
|
3945
|
+
"button",
|
|
3946
|
+
{
|
|
3947
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline mb-2",
|
|
3948
|
+
onClick: () => send({ type: "CREATE_RECOVERABLE" }),
|
|
3949
|
+
children: "Set up a recovery phrase too"
|
|
3950
|
+
}
|
|
3951
|
+
),
|
|
3952
|
+
/* @__PURE__ */ jsx13(
|
|
3953
|
+
"button",
|
|
3954
|
+
{
|
|
3955
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors underline-offset-4 hover:underline",
|
|
3956
|
+
onClick: () => send({ type: "IMPORT_EXISTING" }),
|
|
3957
|
+
children: "I already have an identity"
|
|
3958
|
+
}
|
|
3959
|
+
)
|
|
3960
|
+
] });
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
// src/onboarding/screens/GuardianRecoveryScreen.tsx
|
|
3964
|
+
import { parseShare as parseShare2 } from "@xnetjs/identity";
|
|
3965
|
+
import { useMemo as useMemo15, useState as useState24 } from "react";
|
|
3966
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3967
|
+
function GuardianRecoveryScreen() {
|
|
3968
|
+
const { send, context } = useOnboarding();
|
|
3969
|
+
const [text, setText] = useState24("");
|
|
3970
|
+
const { validCodes, invalidCount, threshold } = useMemo15(() => {
|
|
3971
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
3972
|
+
const valid = [];
|
|
3973
|
+
let invalid = 0;
|
|
3974
|
+
let need = null;
|
|
3975
|
+
for (const line of lines) {
|
|
3976
|
+
try {
|
|
3977
|
+
const share = parseShare2(line);
|
|
3978
|
+
valid.push(line);
|
|
3979
|
+
need ??= share.threshold;
|
|
3980
|
+
} catch {
|
|
3981
|
+
invalid += 1;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
return { validCodes: valid, invalidCount: invalid, threshold: need };
|
|
3985
|
+
}, [text]);
|
|
3986
|
+
const canRecover = threshold !== null && validCodes.length >= threshold;
|
|
3987
|
+
const hint = (() => {
|
|
3988
|
+
if (context.error) return context.error.message;
|
|
3989
|
+
if (text.trim().length === 0) return "Paste one share code per line";
|
|
3990
|
+
if (invalidCount > 0)
|
|
3991
|
+
return `${invalidCount} code${invalidCount === 1 ? "" : "s"} not recognized`;
|
|
3992
|
+
if (threshold !== null) return `${validCodes.length} of ${threshold} needed`;
|
|
3993
|
+
return `${validCodes.length} share${validCodes.length === 1 ? "" : "s"} pasted`;
|
|
3994
|
+
})();
|
|
3995
|
+
return /* @__PURE__ */ jsxs12("div", { className: "flex min-h-screen flex-col items-center justify-center bg-background p-6 text-foreground", children: [
|
|
3996
|
+
/* @__PURE__ */ jsx14("h1", { className: "mb-2 text-2xl font-semibold", children: "Recover with your guardians" }),
|
|
3997
|
+
/* @__PURE__ */ jsx14("p", { className: "mb-6 max-w-md text-center text-muted-foreground", children: "Paste the share codes your guardians gave you \u2014 one per line. You need enough of them to restore your identity; we\u2019ll tell you when you have enough." }),
|
|
3998
|
+
/* @__PURE__ */ jsx14(
|
|
3999
|
+
"textarea",
|
|
4000
|
+
{
|
|
4001
|
+
autoFocus: true,
|
|
4002
|
+
spellCheck: false,
|
|
4003
|
+
autoCapitalize: "none",
|
|
4004
|
+
autoCorrect: "off",
|
|
4005
|
+
value: text,
|
|
4006
|
+
onChange: (e) => setText(e.target.value),
|
|
4007
|
+
placeholder: "xnet-share:\u2026\nxnet-share:\u2026",
|
|
4008
|
+
rows: 5,
|
|
4009
|
+
className: "mb-2 w-80 max-w-full resize-none rounded-lg border border-border bg-muted/30 p-3 font-mono text-xs outline-none focus:border-primary"
|
|
4010
|
+
}
|
|
4011
|
+
),
|
|
4012
|
+
/* @__PURE__ */ jsx14("div", { className: "mb-4 h-5 text-xs", children: /* @__PURE__ */ jsx14(
|
|
4013
|
+
"span",
|
|
4014
|
+
{
|
|
4015
|
+
className: context.error || invalidCount > 0 ? "text-destructive" : "text-muted-foreground",
|
|
4016
|
+
children: hint
|
|
4017
|
+
}
|
|
4018
|
+
) }),
|
|
4019
|
+
/* @__PURE__ */ jsx14(
|
|
4020
|
+
"button",
|
|
4021
|
+
{
|
|
4022
|
+
disabled: !canRecover,
|
|
4023
|
+
className: "mb-3 w-64 rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50",
|
|
4024
|
+
onClick: () => canRecover && send({ type: "SUBMIT_GUARDIAN_SHARES", codes: validCodes }),
|
|
4025
|
+
children: "Recover my identity"
|
|
4026
|
+
}
|
|
4027
|
+
),
|
|
4028
|
+
/* @__PURE__ */ jsx14(
|
|
4029
|
+
"button",
|
|
4030
|
+
{
|
|
4031
|
+
className: "text-sm text-muted-foreground transition-colors hover:text-foreground",
|
|
4032
|
+
onClick: () => send({ type: "BACK_TO_WELCOME" }),
|
|
4033
|
+
children: "Back"
|
|
4034
|
+
}
|
|
4035
|
+
)
|
|
4036
|
+
] });
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
// src/onboarding/screens/RecoveryPhraseScreen.tsx
|
|
4040
|
+
import { validateRecoveryPhrase } from "@xnetjs/identity";
|
|
4041
|
+
import { useMemo as useMemo16, useState as useState25 } from "react";
|
|
4042
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
4043
|
+
function RecoveryPhraseScreen() {
|
|
4044
|
+
const { send, context } = useOnboarding();
|
|
4045
|
+
const [phrase, setPhrase] = useState25("");
|
|
4046
|
+
const validation = useMemo16(() => validateRecoveryPhrase(phrase), [phrase]);
|
|
4047
|
+
const empty = phrase.trim().length === 0;
|
|
4048
|
+
const hint = (() => {
|
|
4049
|
+
if (empty || validation.ok) return null;
|
|
4050
|
+
if (validation.reason === "too-short") {
|
|
4051
|
+
return `Enter your full phrase (at least 12 words) \u2014 ${validation.wordCount} so far.`;
|
|
4052
|
+
}
|
|
4053
|
+
return `Not in the wordlist: ${validation.unknownWords.join(", ")}`;
|
|
4054
|
+
})();
|
|
4055
|
+
return /* @__PURE__ */ jsxs13("div", { className: "flex min-h-screen flex-col items-center justify-center bg-background p-6 text-foreground", children: [
|
|
4056
|
+
/* @__PURE__ */ jsx15("h1", { className: "mb-2 text-2xl font-semibold", children: "Enter your recovery phrase" }),
|
|
4057
|
+
/* @__PURE__ */ jsx15("p", { className: "mb-6 max-w-md text-center text-muted-foreground", children: "Type the recovery phrase you saved. It restores the same identity and your encrypted data on this device." }),
|
|
4058
|
+
/* @__PURE__ */ jsx15(
|
|
4059
|
+
"textarea",
|
|
4060
|
+
{
|
|
4061
|
+
autoFocus: true,
|
|
4062
|
+
spellCheck: false,
|
|
4063
|
+
autoCapitalize: "none",
|
|
4064
|
+
autoCorrect: "off",
|
|
4065
|
+
value: phrase,
|
|
4066
|
+
onChange: (e) => setPhrase(e.target.value),
|
|
4067
|
+
placeholder: "amber anchor apple \u2026",
|
|
4068
|
+
rows: 3,
|
|
4069
|
+
className: "mb-2 w-80 max-w-full resize-none rounded-lg border border-border bg-muted/30 p-3 font-mono text-sm outline-none focus:border-primary"
|
|
4070
|
+
}
|
|
4071
|
+
),
|
|
4072
|
+
/* @__PURE__ */ jsxs13("div", { className: "mb-4 h-5 text-xs", children: [
|
|
4073
|
+
hint && /* @__PURE__ */ jsx15("span", { className: "text-destructive", children: hint }),
|
|
4074
|
+
context.error && !hint && /* @__PURE__ */ jsx15("span", { className: "text-destructive", children: context.error.message })
|
|
4075
|
+
] }),
|
|
4076
|
+
/* @__PURE__ */ jsx15(
|
|
4077
|
+
"button",
|
|
4078
|
+
{
|
|
4079
|
+
disabled: !validation.ok,
|
|
4080
|
+
className: "mb-3 w-64 rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50",
|
|
4081
|
+
onClick: () => validation.ok && send({ type: "SUBMIT_PHRASE", phrase }),
|
|
4082
|
+
children: "Recover my identity"
|
|
4083
|
+
}
|
|
4084
|
+
),
|
|
4085
|
+
/* @__PURE__ */ jsx15(
|
|
4086
|
+
"button",
|
|
4087
|
+
{
|
|
4088
|
+
className: "text-sm text-muted-foreground transition-colors hover:text-foreground",
|
|
4089
|
+
onClick: () => send({ type: "BACK_TO_WELCOME" }),
|
|
4090
|
+
children: "Back"
|
|
4091
|
+
}
|
|
4092
|
+
)
|
|
4093
|
+
] });
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
// src/onboarding/screens/ShowRecoveryPhraseScreen.tsx
|
|
4097
|
+
import { useState as useState26 } from "react";
|
|
4098
|
+
import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
4099
|
+
function ShowRecoveryPhraseScreen() {
|
|
4100
|
+
const { send, context } = useOnboarding();
|
|
4101
|
+
const [saved, setSaved] = useState26(false);
|
|
4102
|
+
const [copied, setCopied] = useState26(false);
|
|
4103
|
+
const phrase = context.recoveryPhrase ?? "";
|
|
4104
|
+
const words = phrase.split(" ").filter(Boolean);
|
|
4105
|
+
const copy = () => {
|
|
4106
|
+
void navigator.clipboard?.writeText(phrase).then(
|
|
4107
|
+
() => setCopied(true),
|
|
4108
|
+
() => setCopied(false)
|
|
4109
|
+
);
|
|
4110
|
+
};
|
|
4111
|
+
return /* @__PURE__ */ jsxs14("div", { className: "flex min-h-screen flex-col items-center justify-center bg-background p-6 text-foreground", children: [
|
|
4112
|
+
/* @__PURE__ */ jsx16("h1", { className: "mb-2 text-2xl font-semibold", children: "Save your recovery phrase" }),
|
|
4113
|
+
/* @__PURE__ */ jsx16("p", { className: "mb-6 max-w-md text-center text-muted-foreground", children: "Write these words down and keep them somewhere safe. They are the only way to recover your workspace if you lose your passkey \u2014 we can\u2019t recover them for you." }),
|
|
4114
|
+
/* @__PURE__ */ jsx16("ol", { className: "mb-3 grid w-80 max-w-full grid-cols-2 gap-x-6 gap-y-1 rounded-lg border border-border bg-muted/30 p-4 font-mono text-sm", children: words.map((word, i) => /* @__PURE__ */ jsxs14("li", { className: "flex gap-2", children: [
|
|
4115
|
+
/* @__PURE__ */ jsx16("span", { className: "w-5 select-none text-right text-muted-foreground", children: i + 1 }),
|
|
4116
|
+
/* @__PURE__ */ jsx16("span", { children: word })
|
|
4117
|
+
] }, `${i}-${word}`)) }),
|
|
4118
|
+
/* @__PURE__ */ jsx16(
|
|
4119
|
+
"button",
|
|
4120
|
+
{
|
|
4121
|
+
className: "mb-5 text-xs text-muted-foreground transition-colors hover:text-foreground",
|
|
4122
|
+
onClick: copy,
|
|
4123
|
+
children: copied ? "Copied \u2713" : "Copy to clipboard"
|
|
4124
|
+
}
|
|
4125
|
+
),
|
|
4126
|
+
/* @__PURE__ */ jsxs14("label", { className: "mb-4 flex max-w-xs cursor-pointer items-start gap-2 text-sm text-muted-foreground", children: [
|
|
4127
|
+
/* @__PURE__ */ jsx16(
|
|
4128
|
+
"input",
|
|
4129
|
+
{
|
|
4130
|
+
type: "checkbox",
|
|
4131
|
+
checked: saved,
|
|
4132
|
+
onChange: (e) => setSaved(e.target.checked),
|
|
4133
|
+
className: "mt-0.5"
|
|
4134
|
+
}
|
|
4135
|
+
),
|
|
4136
|
+
"I\u2019ve saved my recovery phrase somewhere safe."
|
|
4137
|
+
] }),
|
|
4138
|
+
/* @__PURE__ */ jsx16(
|
|
4139
|
+
"button",
|
|
4140
|
+
{
|
|
4141
|
+
disabled: !saved,
|
|
4142
|
+
className: "w-64 rounded-lg bg-primary px-6 py-3 font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50",
|
|
4143
|
+
onClick: () => send({ type: "PHRASE_SAVED" }),
|
|
4144
|
+
children: "Continue"
|
|
4145
|
+
}
|
|
4146
|
+
)
|
|
4147
|
+
] });
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
// src/onboarding/OnboardingFlow.tsx
|
|
4151
|
+
import { Fragment, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
4152
|
+
function OnboardingFlow({ connectToHub, children }) {
|
|
4153
|
+
const { state } = useOnboarding();
|
|
4154
|
+
switch (state) {
|
|
4155
|
+
case "welcome":
|
|
4156
|
+
return /* @__PURE__ */ jsx17(WelcomeScreen, {});
|
|
4157
|
+
case "authenticating":
|
|
4158
|
+
return /* @__PURE__ */ jsx17(AuthenticatingScreen, {});
|
|
4159
|
+
case "auth-error":
|
|
4160
|
+
return /* @__PURE__ */ jsx17(AuthErrorScreen, {});
|
|
4161
|
+
case "unsupported-browser":
|
|
4162
|
+
return /* @__PURE__ */ jsx17(UnsupportedBrowserScreen, {});
|
|
4163
|
+
case "import-identity":
|
|
4164
|
+
return /* @__PURE__ */ jsx17(ImportIdentityScreen, {});
|
|
4165
|
+
case "qr-scan":
|
|
4166
|
+
return /* @__PURE__ */ jsxs15("div", { className: "flex flex-col items-center justify-center min-h-screen bg-background text-foreground p-6", children: [
|
|
4167
|
+
/* @__PURE__ */ jsx17("h1", { className: "text-2xl font-semibold mb-2", children: "QR Scan" }),
|
|
4168
|
+
/* @__PURE__ */ jsx17("p", { className: "text-muted-foreground", children: "Coming soon \u2014 scan a QR code from another device." })
|
|
4169
|
+
] });
|
|
4170
|
+
case "recovery-phrase":
|
|
4171
|
+
return /* @__PURE__ */ jsx17(RecoveryPhraseScreen, {});
|
|
4172
|
+
case "guardian-recovery":
|
|
4173
|
+
return /* @__PURE__ */ jsx17(GuardianRecoveryScreen, {});
|
|
4174
|
+
case "creating-recoverable":
|
|
4175
|
+
return /* @__PURE__ */ jsx17(AuthenticatingScreen, {});
|
|
4176
|
+
case "show-recovery-phrase":
|
|
4177
|
+
return /* @__PURE__ */ jsx17(ShowRecoveryPhraseScreen, {});
|
|
4178
|
+
case "connecting-hub":
|
|
4179
|
+
return /* @__PURE__ */ jsx17(HubConnectScreen, { connectToHub });
|
|
4180
|
+
case "ready":
|
|
4181
|
+
return /* @__PURE__ */ jsx17(ReadyScreen, {});
|
|
4182
|
+
case "complete":
|
|
4183
|
+
return /* @__PURE__ */ jsx17(Fragment, { children });
|
|
4184
|
+
default:
|
|
4185
|
+
return /* @__PURE__ */ jsx17(WelcomeScreen, {});
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
// src/onboarding/screens/SmartWelcome.tsx
|
|
4190
|
+
import { discoverExistingPasskey } from "@xnetjs/identity";
|
|
4191
|
+
import { useEffect as useEffect20, useState as useState27 } from "react";
|
|
4192
|
+
import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
4193
|
+
function SmartWelcome() {
|
|
4194
|
+
const { send } = useOnboarding();
|
|
4195
|
+
const [checking, setChecking] = useState27(true);
|
|
4196
|
+
const [hasExisting, setHasExisting] = useState27(false);
|
|
4197
|
+
useEffect20(() => {
|
|
4198
|
+
let cancelled = false;
|
|
4199
|
+
discoverExistingPasskey().then((passkey) => {
|
|
4200
|
+
if (!cancelled) {
|
|
4201
|
+
setHasExisting(passkey !== null);
|
|
4202
|
+
setChecking(false);
|
|
4203
|
+
}
|
|
4204
|
+
}).catch(() => {
|
|
4205
|
+
if (!cancelled) {
|
|
4206
|
+
setChecking(false);
|
|
4207
|
+
}
|
|
4208
|
+
});
|
|
4209
|
+
return () => {
|
|
4210
|
+
cancelled = true;
|
|
4211
|
+
};
|
|
4212
|
+
}, []);
|
|
4213
|
+
if (checking) {
|
|
4214
|
+
return /* @__PURE__ */ jsx18("div", { className: "onboarding-screen", children: /* @__PURE__ */ jsx18("p", { className: "spinner-text", children: "Checking for existing identity..." }) });
|
|
4215
|
+
}
|
|
4216
|
+
if (hasExisting) {
|
|
4217
|
+
const authName = getPlatformAuthName();
|
|
4218
|
+
return /* @__PURE__ */ jsxs16("div", { className: "onboarding-screen welcome-back", children: [
|
|
4219
|
+
/* @__PURE__ */ jsx18("h1", { children: "Welcome back!" }),
|
|
4220
|
+
/* @__PURE__ */ jsxs16("p", { className: "subtitle", children: [
|
|
4221
|
+
"We found your xNet identity. Use ",
|
|
4222
|
+
authName,
|
|
4223
|
+
" to sign in."
|
|
4224
|
+
] }),
|
|
4225
|
+
/* @__PURE__ */ jsxs16("button", { className: "primary-button", onClick: () => send({ type: "AUTHENTICATE" }), children: [
|
|
4226
|
+
"Sign in with ",
|
|
4227
|
+
authName
|
|
4228
|
+
] }),
|
|
4229
|
+
/* @__PURE__ */ jsx18("button", { className: "text-button", onClick: () => send({ type: "CREATE_NEW" }), children: "Create a new identity instead" })
|
|
4230
|
+
] });
|
|
4231
|
+
}
|
|
4232
|
+
return /* @__PURE__ */ jsx18(WelcomeScreen, {});
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
// src/onboarding/screens/SyncProgressOverlay.tsx
|
|
4236
|
+
import { formatBytes as formatBytes2 } from "@xnetjs/core";
|
|
4237
|
+
import { useEffect as useEffect21 } from "react";
|
|
4238
|
+
import { Fragment as Fragment2, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
4239
|
+
function SyncProgressOverlay({
|
|
4240
|
+
progress,
|
|
4241
|
+
onComplete
|
|
4242
|
+
}) {
|
|
4243
|
+
useEffect21(() => {
|
|
4244
|
+
if (progress.phase === "complete") {
|
|
4245
|
+
const timer = setTimeout(onComplete, 1500);
|
|
4246
|
+
return () => clearTimeout(timer);
|
|
4247
|
+
}
|
|
4248
|
+
}, [progress.phase, onComplete]);
|
|
4249
|
+
return /* @__PURE__ */ jsx19("div", { className: "sync-progress-overlay", children: /* @__PURE__ */ jsxs17("div", { className: "sync-card", children: [
|
|
4250
|
+
progress.phase === "connecting" && /* @__PURE__ */ jsxs17(Fragment2, { children: [
|
|
4251
|
+
/* @__PURE__ */ jsx19("h2", { children: "Connecting to server..." }),
|
|
4252
|
+
/* @__PURE__ */ jsx19("p", { className: "spinner-text", children: "Please wait" })
|
|
4253
|
+
] }),
|
|
4254
|
+
progress.phase === "syncing" && /* @__PURE__ */ jsxs17(Fragment2, { children: [
|
|
4255
|
+
/* @__PURE__ */ jsx19("h2", { children: "Syncing your data" }),
|
|
4256
|
+
/* @__PURE__ */ jsx19("div", { className: "progress-bar", children: /* @__PURE__ */ jsx19(
|
|
4257
|
+
"div",
|
|
4258
|
+
{
|
|
4259
|
+
className: "progress-fill",
|
|
4260
|
+
style: {
|
|
4261
|
+
width: `${progress.roomsSynced / Math.max(progress.roomsTotal, 1) * 100}%`
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
) }),
|
|
4265
|
+
/* @__PURE__ */ jsxs17("p", { className: "progress-text", children: [
|
|
4266
|
+
progress.roomsSynced,
|
|
4267
|
+
" of ",
|
|
4268
|
+
progress.roomsTotal,
|
|
4269
|
+
" items"
|
|
4270
|
+
] }),
|
|
4271
|
+
/* @__PURE__ */ jsxs17("p", { className: "bytes-text", children: [
|
|
4272
|
+
formatBytes2(progress.bytesReceived),
|
|
4273
|
+
" received"
|
|
4274
|
+
] })
|
|
4275
|
+
] }),
|
|
4276
|
+
progress.phase === "complete" && /* @__PURE__ */ jsxs17(Fragment2, { children: [
|
|
4277
|
+
/* @__PURE__ */ jsx19("h2", { children: "All synced!" }),
|
|
4278
|
+
/* @__PURE__ */ jsxs17("p", { children: [
|
|
4279
|
+
progress.roomsTotal,
|
|
4280
|
+
" items synchronized"
|
|
4281
|
+
] })
|
|
4282
|
+
] }),
|
|
4283
|
+
progress.phase === "error" && /* @__PURE__ */ jsxs17(Fragment2, { children: [
|
|
4284
|
+
/* @__PURE__ */ jsx19("h2", { children: "Sync issue" }),
|
|
4285
|
+
/* @__PURE__ */ jsx19("p", { children: "Some data may not be up to date." }),
|
|
4286
|
+
progress.error && /* @__PURE__ */ jsx19("p", { className: "error-detail", children: progress.error.message }),
|
|
4287
|
+
/* @__PURE__ */ jsx19("button", { className: "primary-button", onClick: onComplete, children: "Continue anyway" })
|
|
4288
|
+
] })
|
|
4289
|
+
] }) });
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
// src/onboarding/templates.ts
|
|
4293
|
+
var QUICK_START_TEMPLATES = [
|
|
4294
|
+
{
|
|
4295
|
+
id: "blank-page",
|
|
4296
|
+
name: "Blank Page",
|
|
4297
|
+
description: "Start from scratch",
|
|
4298
|
+
icon: "file"
|
|
4299
|
+
},
|
|
4300
|
+
{
|
|
4301
|
+
id: "meeting-notes",
|
|
4302
|
+
name: "Meeting Notes",
|
|
4303
|
+
description: "Template for taking meeting notes",
|
|
4304
|
+
icon: "users"
|
|
4305
|
+
},
|
|
4306
|
+
{
|
|
4307
|
+
id: "project-tracker",
|
|
4308
|
+
name: "Project Tracker",
|
|
4309
|
+
description: "Database for tracking tasks",
|
|
4310
|
+
icon: "kanban"
|
|
4311
|
+
},
|
|
4312
|
+
{
|
|
4313
|
+
id: "canvas",
|
|
4314
|
+
name: "Whiteboard",
|
|
4315
|
+
description: "Infinite canvas for visual thinking",
|
|
4316
|
+
icon: "pen-tool"
|
|
4317
|
+
}
|
|
4318
|
+
];
|
|
4319
|
+
|
|
4320
|
+
// src/components/pageTaskRows.ts
|
|
4321
|
+
function flattenTaskTree(items, depth = 0) {
|
|
4322
|
+
return items.flatMap((item) => [
|
|
4323
|
+
{
|
|
4324
|
+
id: item.task.id,
|
|
4325
|
+
title: item.task.title ?? "Untitled task",
|
|
4326
|
+
completed: Boolean(item.task.completed),
|
|
4327
|
+
dueDate: typeof item.task.dueDate === "number" ? item.task.dueDate : void 0,
|
|
4328
|
+
depth,
|
|
4329
|
+
assigneeCount: Array.isArray(item.task.assignees) ? item.task.assignees.length : 0
|
|
4330
|
+
},
|
|
4331
|
+
...flattenTaskTree(item.children, depth + 1)
|
|
4332
|
+
]);
|
|
4333
|
+
}
|
|
4334
|
+
function formatTaskDueDate(timestamp) {
|
|
4335
|
+
if (typeof timestamp !== "number") return null;
|
|
4336
|
+
return new Date(timestamp).toLocaleDateString(void 0, {
|
|
4337
|
+
month: "short",
|
|
4338
|
+
day: "numeric"
|
|
4339
|
+
});
|
|
4340
|
+
}
|
|
4341
|
+
function getStartOfUtcDay2(timestamp) {
|
|
4342
|
+
const date = new Date(timestamp);
|
|
4343
|
+
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
4344
|
+
}
|
|
4345
|
+
function isTaskOverdue(timestamp, completed) {
|
|
4346
|
+
if (typeof timestamp !== "number" || completed) return false;
|
|
4347
|
+
return getStartOfUtcDay2(timestamp) < getStartOfUtcDay2(Date.now());
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
// src/components/PageTasksPanel.tsx
|
|
4351
|
+
import { Calendar, CheckSquare2, ChevronDown, ChevronRight, Square, Users } from "lucide-react";
|
|
4352
|
+
import { useMemo as useMemo17, useState as useState28 } from "react";
|
|
4353
|
+
import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
4354
|
+
function PageTasksPanel({ pageId }) {
|
|
4355
|
+
const [expanded, setExpanded] = useState28(true);
|
|
4356
|
+
const { tree, loading } = useTasks({ pageId });
|
|
4357
|
+
const rows = useMemo17(() => flattenTaskTree(tree), [tree]);
|
|
4358
|
+
return /* @__PURE__ */ jsxs18("div", { className: "mt-8 overflow-hidden rounded-lg border border-border", children: [
|
|
4359
|
+
/* @__PURE__ */ jsx20(
|
|
4360
|
+
"button",
|
|
4361
|
+
{
|
|
4362
|
+
className: "w-full cursor-pointer border-none bg-secondary p-3 px-4 text-left",
|
|
4363
|
+
onClick: () => setExpanded((value) => !value),
|
|
4364
|
+
type: "button",
|
|
4365
|
+
children: /* @__PURE__ */ jsxs18("h3", { className: "m-0 flex items-center justify-between text-sm font-semibold text-foreground", children: [
|
|
4366
|
+
/* @__PURE__ */ jsxs18("span", { children: [
|
|
4367
|
+
"Tasks on this page (",
|
|
4368
|
+
rows.length,
|
|
4369
|
+
")"
|
|
4370
|
+
] }),
|
|
4371
|
+
expanded ? /* @__PURE__ */ jsx20(ChevronDown, { size: 16, className: "text-muted-foreground" }) : /* @__PURE__ */ jsx20(ChevronRight, { size: 16, className: "text-muted-foreground" })
|
|
4372
|
+
] })
|
|
4373
|
+
}
|
|
4374
|
+
),
|
|
4375
|
+
expanded && /* @__PURE__ */ jsx20("div", { className: "p-3", children: loading ? /* @__PURE__ */ jsx20("p", { className: "m-0 text-sm text-muted-foreground", children: "Loading tasks..." }) : rows.length === 0 ? /* @__PURE__ */ jsx20("p", { className: "m-0 text-sm text-muted-foreground", children: "Checklist items on this page will appear here." }) : /* @__PURE__ */ jsx20("ul", { className: "m-0 list-none space-y-1 p-0", children: rows.map((task) => {
|
|
4376
|
+
const dueDateLabel = formatTaskDueDate(task.dueDate);
|
|
4377
|
+
const overdue = isTaskOverdue(task.dueDate, task.completed);
|
|
4378
|
+
return /* @__PURE__ */ jsx20("li", { children: /* @__PURE__ */ jsxs18(
|
|
4379
|
+
"div",
|
|
4380
|
+
{
|
|
4381
|
+
className: "flex items-start gap-2 rounded-md px-2 py-2 transition-colors hover:bg-accent/40",
|
|
4382
|
+
style: { paddingLeft: `${task.depth * 18 + 8}px` },
|
|
4383
|
+
children: [
|
|
4384
|
+
task.completed ? /* @__PURE__ */ jsx20(CheckSquare2, { size: 15, className: "mt-0.5 flex-shrink-0 text-primary" }) : /* @__PURE__ */ jsx20(Square, { size: 15, className: "mt-0.5 flex-shrink-0 text-muted-foreground" }),
|
|
4385
|
+
/* @__PURE__ */ jsxs18("div", { className: "min-w-0 flex-1", children: [
|
|
4386
|
+
/* @__PURE__ */ jsx20(
|
|
4387
|
+
"div",
|
|
4388
|
+
{
|
|
4389
|
+
className: `text-sm ${task.completed ? "line-through text-muted-foreground" : "text-foreground"}`,
|
|
4390
|
+
children: task.title
|
|
4391
|
+
}
|
|
4392
|
+
),
|
|
4393
|
+
/* @__PURE__ */ jsxs18("div", { className: "mt-1 flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground", children: [
|
|
4394
|
+
dueDateLabel ? /* @__PURE__ */ jsxs18(
|
|
4395
|
+
"span",
|
|
4396
|
+
{
|
|
4397
|
+
className: `inline-flex items-center gap-1 ${overdue ? "text-red-500" : "text-muted-foreground"}`,
|
|
4398
|
+
children: [
|
|
4399
|
+
/* @__PURE__ */ jsx20(Calendar, { size: 11 }),
|
|
4400
|
+
dueDateLabel
|
|
4401
|
+
]
|
|
4402
|
+
}
|
|
4403
|
+
) : null,
|
|
4404
|
+
task.assigneeCount > 0 ? /* @__PURE__ */ jsxs18("span", { className: "inline-flex items-center gap-1", children: [
|
|
4405
|
+
/* @__PURE__ */ jsx20(Users, { size: 11 }),
|
|
4406
|
+
task.assigneeCount
|
|
4407
|
+
] }) : null
|
|
4408
|
+
] })
|
|
4409
|
+
] })
|
|
4410
|
+
]
|
|
4411
|
+
}
|
|
4412
|
+
) }, task.id);
|
|
4413
|
+
}) }) })
|
|
4414
|
+
] });
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
// src/components/TaskCollectionEmbed.tsx
|
|
4418
|
+
import { useMemo as useMemo18 } from "react";
|
|
4419
|
+
import { jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
4420
|
+
function TaskCollectionEmbed({
|
|
4421
|
+
currentPageId,
|
|
4422
|
+
currentDid,
|
|
4423
|
+
scope,
|
|
4424
|
+
assignee,
|
|
4425
|
+
dueDate,
|
|
4426
|
+
status,
|
|
4427
|
+
showHierarchy
|
|
4428
|
+
}) {
|
|
4429
|
+
const missingPageContext = scope === "current-page" && !currentPageId;
|
|
4430
|
+
const pageId = scope === "current-page" ? currentPageId ?? "__task_collection_embed_missing_page_context__" : void 0;
|
|
4431
|
+
const assigneeDid = assignee === "me" ? currentDid : void 0;
|
|
4432
|
+
const statuses = status === "done" ? ["done"] : void 0;
|
|
4433
|
+
const includeCompleted = status !== "open";
|
|
4434
|
+
const { data, tree, loading } = useTasks({
|
|
4435
|
+
pageId,
|
|
4436
|
+
assigneeDid,
|
|
4437
|
+
includeCompleted,
|
|
4438
|
+
statuses: statuses ? [...statuses] : void 0,
|
|
4439
|
+
dueDateFilter: dueDate
|
|
4440
|
+
});
|
|
4441
|
+
const rows = useMemo18(() => {
|
|
4442
|
+
if (showHierarchy) return flattenTaskTree(tree);
|
|
4443
|
+
return data.map((task) => ({
|
|
4444
|
+
id: task.id,
|
|
4445
|
+
title: task.title ?? "Untitled task",
|
|
4446
|
+
completed: Boolean(task.completed),
|
|
4447
|
+
dueDate: typeof task.dueDate === "number" ? task.dueDate : void 0,
|
|
4448
|
+
depth: 0
|
|
4449
|
+
}));
|
|
4450
|
+
}, [data, showHierarchy, tree]);
|
|
4451
|
+
if (missingPageContext) {
|
|
4452
|
+
return /* @__PURE__ */ jsx21("div", { className: "p-4 text-sm text-muted-foreground", children: "This view needs a page context." });
|
|
4453
|
+
}
|
|
4454
|
+
if (loading) {
|
|
4455
|
+
return /* @__PURE__ */ jsx21("div", { className: "p-4 text-sm text-muted-foreground", children: "Loading tasks..." });
|
|
4456
|
+
}
|
|
4457
|
+
if (rows.length === 0) {
|
|
4458
|
+
return /* @__PURE__ */ jsx21("div", { className: "p-4 text-sm text-muted-foreground", children: "No tasks match the saved filters for this view." });
|
|
4459
|
+
}
|
|
4460
|
+
return /* @__PURE__ */ jsx21("div", { className: "p-3", children: /* @__PURE__ */ jsx21("ul", { className: "m-0 list-none space-y-1 p-0", children: rows.map((task) => {
|
|
4461
|
+
const dueDateLabel = formatTaskDueDate(task.dueDate);
|
|
4462
|
+
const overdue = isTaskOverdue(task.dueDate, task.completed);
|
|
4463
|
+
return /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsxs19(
|
|
4464
|
+
"div",
|
|
4465
|
+
{
|
|
4466
|
+
className: "flex items-start gap-2 rounded-lg px-2 py-2 hover:bg-accent/40",
|
|
4467
|
+
style: { paddingLeft: `${task.depth * 18 + 8}px` },
|
|
4468
|
+
children: [
|
|
4469
|
+
/* @__PURE__ */ jsx21("span", { className: task.completed ? "text-primary" : "text-muted-foreground", children: task.completed ? "\u2611" : "\u2610" }),
|
|
4470
|
+
/* @__PURE__ */ jsxs19("div", { className: "min-w-0 flex-1", children: [
|
|
4471
|
+
/* @__PURE__ */ jsx21(
|
|
4472
|
+
"div",
|
|
4473
|
+
{
|
|
4474
|
+
className: task.completed ? "truncate text-sm text-muted-foreground line-through" : "truncate text-sm text-foreground",
|
|
4475
|
+
children: task.title
|
|
4476
|
+
}
|
|
4477
|
+
),
|
|
4478
|
+
dueDateLabel ? /* @__PURE__ */ jsx21(
|
|
4479
|
+
"div",
|
|
4480
|
+
{
|
|
4481
|
+
className: overdue ? "mt-1 text-[11px] text-red-500" : "mt-1 text-[11px] text-muted-foreground",
|
|
4482
|
+
children: dueDateLabel
|
|
4483
|
+
}
|
|
4484
|
+
) : null
|
|
4485
|
+
] })
|
|
4486
|
+
]
|
|
4487
|
+
}
|
|
4488
|
+
) }, task.id);
|
|
4489
|
+
}) }) });
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
// src/hooks/useSecurity.ts
|
|
4493
|
+
import {
|
|
4494
|
+
hybridSign,
|
|
4495
|
+
hybridVerify
|
|
4496
|
+
} from "@xnetjs/crypto";
|
|
4497
|
+
import { parseDID } from "@xnetjs/identity";
|
|
4498
|
+
import { useCallback as useCallback19, useMemo as useMemo19 } from "react";
|
|
4499
|
+
function useSecurity(options = {}) {
|
|
4500
|
+
const context = useSecurityContext();
|
|
4501
|
+
const effectiveLevel = options.level ?? context.level;
|
|
4502
|
+
const hasPQKeys = useMemo19(
|
|
4503
|
+
() => context.keyBundle?.pqSigningKey !== void 0,
|
|
4504
|
+
[context.keyBundle]
|
|
4505
|
+
);
|
|
4506
|
+
const hasKeyBundle = useMemo19(() => context.keyBundle !== void 0, [context.keyBundle]);
|
|
4507
|
+
const maxLevel = useMemo19(() => hasPQKeys ? 2 : 0, [hasPQKeys]);
|
|
4508
|
+
const canSignAt = useCallback19(
|
|
4509
|
+
(level) => {
|
|
4510
|
+
if (!context.keyBundle) return false;
|
|
4511
|
+
if (level === 0) return true;
|
|
4512
|
+
return hasPQKeys;
|
|
4513
|
+
},
|
|
4514
|
+
[context.keyBundle, hasPQKeys]
|
|
4515
|
+
);
|
|
4516
|
+
const sign = useCallback19(
|
|
4517
|
+
(data) => {
|
|
4518
|
+
if (!context.keyBundle) {
|
|
4519
|
+
throw new Error(
|
|
4520
|
+
"No key bundle available. Ensure keyBundle is provided to SecurityProvider."
|
|
4521
|
+
);
|
|
4522
|
+
}
|
|
4523
|
+
if (!canSignAt(effectiveLevel)) {
|
|
4524
|
+
throw new Error(
|
|
4525
|
+
`Cannot sign at Level ${effectiveLevel}: PQ keys required. Current key bundle only supports Level 0.`
|
|
4526
|
+
);
|
|
4527
|
+
}
|
|
4528
|
+
return hybridSign(
|
|
4529
|
+
data,
|
|
4530
|
+
{
|
|
4531
|
+
ed25519: context.keyBundle.signingKey,
|
|
4532
|
+
mlDsa: context.keyBundle.pqSigningKey
|
|
4533
|
+
},
|
|
4534
|
+
effectiveLevel
|
|
4535
|
+
);
|
|
4536
|
+
},
|
|
4537
|
+
[context.keyBundle, effectiveLevel, canSignAt]
|
|
4538
|
+
);
|
|
4539
|
+
const verify = useCallback19(
|
|
4540
|
+
async (data, signature, did) => {
|
|
4541
|
+
const ed25519PublicKey = parseDID(did);
|
|
4542
|
+
const pqPublicKey = await context.registry.lookup(did);
|
|
4543
|
+
return hybridVerify(
|
|
4544
|
+
data,
|
|
4545
|
+
signature,
|
|
4546
|
+
{
|
|
4547
|
+
ed25519: ed25519PublicKey,
|
|
4548
|
+
mlDsa: pqPublicKey ?? void 0
|
|
4549
|
+
},
|
|
4550
|
+
{
|
|
4551
|
+
minLevel: context.minVerificationLevel,
|
|
4552
|
+
policy: context.verificationPolicy
|
|
4553
|
+
}
|
|
4554
|
+
);
|
|
4555
|
+
},
|
|
4556
|
+
[context.registry, context.minVerificationLevel, context.verificationPolicy]
|
|
4557
|
+
);
|
|
4558
|
+
return {
|
|
4559
|
+
level: effectiveLevel,
|
|
4560
|
+
hasPQKeys,
|
|
4561
|
+
maxLevel,
|
|
4562
|
+
sign,
|
|
4563
|
+
verify,
|
|
4564
|
+
setLevel: context.setLevel,
|
|
4565
|
+
canSignAt,
|
|
4566
|
+
hasKeyBundle
|
|
4567
|
+
};
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
export {
|
|
4571
|
+
useTaskProjectionSync,
|
|
4572
|
+
usePageTaskSync,
|
|
4573
|
+
useFind,
|
|
4574
|
+
useTasks,
|
|
4575
|
+
useComments,
|
|
4576
|
+
summarizeModerationLabel,
|
|
4577
|
+
summarizePublicInteractionPolicy,
|
|
4578
|
+
selectActiveInteractionPolicy,
|
|
4579
|
+
selectPublicInteractionMode,
|
|
4580
|
+
createModerationLabelIndex,
|
|
4581
|
+
evaluateInteractionPermission,
|
|
4582
|
+
evaluateCommentModeration,
|
|
4583
|
+
moderateThread,
|
|
4584
|
+
useModeratedThread,
|
|
4585
|
+
useVisibleComments,
|
|
4586
|
+
summarizeReactionNode,
|
|
4587
|
+
isReactionVisible,
|
|
4588
|
+
dedupeReactions,
|
|
4589
|
+
createReactionCounterSnapshot,
|
|
4590
|
+
usePolicyFilteredReactionCounters,
|
|
4591
|
+
createConversationKey,
|
|
4592
|
+
summarizeMessageRequest,
|
|
4593
|
+
findLatestMessageRequest,
|
|
4594
|
+
hasAcceptedContact,
|
|
4595
|
+
evaluateFirstContactDecision,
|
|
4596
|
+
createMessageRequestProperties,
|
|
4597
|
+
useMessageRequests,
|
|
4598
|
+
useCommentCount,
|
|
4599
|
+
useCommentCounts,
|
|
4600
|
+
useHistory,
|
|
4601
|
+
useUndo,
|
|
4602
|
+
useAudit,
|
|
4603
|
+
useDiff,
|
|
4604
|
+
useBlame,
|
|
4605
|
+
useVerification,
|
|
4606
|
+
useHubStatus,
|
|
4607
|
+
useCan,
|
|
4608
|
+
useCanEdit,
|
|
4609
|
+
describeGrantConsent,
|
|
4610
|
+
useGrants,
|
|
4611
|
+
summarizeAuthTrace,
|
|
4612
|
+
useAuthTrace,
|
|
4613
|
+
useBackup,
|
|
4614
|
+
useFileUpload,
|
|
4615
|
+
useHubSearch,
|
|
4616
|
+
useRemoteSchema,
|
|
4617
|
+
usePeerDiscovery,
|
|
4618
|
+
HubStatusIndicator,
|
|
4619
|
+
Skeleton,
|
|
4620
|
+
injectSkeletonStyles,
|
|
4621
|
+
DemoBanner,
|
|
4622
|
+
DemoQuotaIndicator,
|
|
4623
|
+
DemoDataExpiredScreen,
|
|
4624
|
+
useDemoMode,
|
|
4625
|
+
onboardingReducer,
|
|
4626
|
+
createInitialState,
|
|
4627
|
+
OnboardingProvider,
|
|
4628
|
+
useOnboarding,
|
|
4629
|
+
getPlatformAuthName,
|
|
4630
|
+
truncateDid,
|
|
4631
|
+
copyToClipboard,
|
|
4632
|
+
AuthenticatingScreen,
|
|
4633
|
+
AuthErrorScreen,
|
|
4634
|
+
HubConnectScreen,
|
|
4635
|
+
ImportIdentityScreen,
|
|
4636
|
+
ReadyScreen,
|
|
4637
|
+
UnsupportedBrowserScreen,
|
|
4638
|
+
WelcomeScreen,
|
|
4639
|
+
OnboardingFlow,
|
|
4640
|
+
SmartWelcome,
|
|
4641
|
+
SyncProgressOverlay,
|
|
4642
|
+
QUICK_START_TEMPLATES,
|
|
4643
|
+
flattenTaskTree,
|
|
4644
|
+
formatTaskDueDate,
|
|
4645
|
+
isTaskOverdue,
|
|
4646
|
+
PageTasksPanel,
|
|
4647
|
+
TaskCollectionEmbed,
|
|
4648
|
+
useSecurity
|
|
4649
|
+
};
|