@decentnetwork/beagle 0.1.104 → 0.1.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assistant-stats.d.ts +42 -0
- package/dist/assistant-stats.js +78 -0
- package/dist/cli.js +1 -0
- package/dist/desktop/app.js +709 -50
- package/dist/message-store.js +6 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +53 -0
- package/package.json +3 -3
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-friend conversation statistics for the Assistant's friend filters.
|
|
3
|
+
*
|
|
4
|
+
* Computed HERE, in the server process, and never in the browser: the answer
|
|
5
|
+
* for a whole friend list is a few hundred bytes, while the history it is
|
|
6
|
+
* derived from is megabytes (one group thread on this Mac is 9 MB). Paging
|
|
7
|
+
* that into a tab — or over IPC — is what stalled the daemon in 0.1.90.
|
|
8
|
+
*
|
|
9
|
+
* The source is <configDir>/messages.json, the file BOTH backends write (the
|
|
10
|
+
* daemon's src/daemon/message-store.ts and our own src/message-store.ts use
|
|
11
|
+
* the same path and the same shape). Reading it directly is the same move
|
|
12
|
+
* friendStoreStamps() makes for `<keyFile>.friends.json`, and for the same
|
|
13
|
+
* reason: it is a file in our own config dir, and the alternative is an op
|
|
14
|
+
* that hauls every message across a socket to count them.
|
|
15
|
+
*
|
|
16
|
+
* Note for the ports (docs/assistant/SPEC.md): there are no call records in
|
|
17
|
+
* the store, so `lastCallTs` is always undefined and the `never-called`
|
|
18
|
+
* filter is not offered.
|
|
19
|
+
*/
|
|
20
|
+
export interface FriendStats {
|
|
21
|
+
/** Earliest message with this friend held on this device. */
|
|
22
|
+
firstMsgTs?: number;
|
|
23
|
+
lastInboundTs?: number;
|
|
24
|
+
lastOutboundTs?: number;
|
|
25
|
+
inboundCount: number;
|
|
26
|
+
outboundCount: number;
|
|
27
|
+
/** Always undefined: the store holds no call records. Kept so the shape
|
|
28
|
+
* matches the spec and a later version can fill it in. */
|
|
29
|
+
lastCallTs?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface HistoryStats {
|
|
32
|
+
byPeer: Map<string, FriendStats>;
|
|
33
|
+
/** Earliest message timestamp held on this device, across every thread.
|
|
34
|
+
* Every Assistant answer is qualified with it: a friend you only talk to
|
|
35
|
+
* on your phone looks silent here, and the user has to see that. */
|
|
36
|
+
historySince?: number;
|
|
37
|
+
}
|
|
38
|
+
/** Read and aggregate <configDir>/messages.json. Cached by mtime+size, so the
|
|
39
|
+
* 9 MB parse happens once per change however many commands are typed. */
|
|
40
|
+
export declare function historyStats(messagesPath: string | undefined): HistoryStats;
|
|
41
|
+
/** Split out from the file read so tests can feed it a plain object. */
|
|
42
|
+
export declare function aggregate(raw: unknown): HistoryStats;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-friend conversation statistics for the Assistant's friend filters.
|
|
3
|
+
*
|
|
4
|
+
* Computed HERE, in the server process, and never in the browser: the answer
|
|
5
|
+
* for a whole friend list is a few hundred bytes, while the history it is
|
|
6
|
+
* derived from is megabytes (one group thread on this Mac is 9 MB). Paging
|
|
7
|
+
* that into a tab — or over IPC — is what stalled the daemon in 0.1.90.
|
|
8
|
+
*
|
|
9
|
+
* The source is <configDir>/messages.json, the file BOTH backends write (the
|
|
10
|
+
* daemon's src/daemon/message-store.ts and our own src/message-store.ts use
|
|
11
|
+
* the same path and the same shape). Reading it directly is the same move
|
|
12
|
+
* friendStoreStamps() makes for `<keyFile>.friends.json`, and for the same
|
|
13
|
+
* reason: it is a file in our own config dir, and the alternative is an op
|
|
14
|
+
* that hauls every message across a socket to count them.
|
|
15
|
+
*
|
|
16
|
+
* Note for the ports (docs/assistant/SPEC.md): there are no call records in
|
|
17
|
+
* the store, so `lastCallTs` is always undefined and the `never-called`
|
|
18
|
+
* filter is not offered.
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync, statSync } from "fs";
|
|
21
|
+
const EMPTY = { byPeer: new Map(), historySince: undefined };
|
|
22
|
+
let cache = null;
|
|
23
|
+
/** Read and aggregate <configDir>/messages.json. Cached by mtime+size, so the
|
|
24
|
+
* 9 MB parse happens once per change however many commands are typed. */
|
|
25
|
+
export function historyStats(messagesPath) {
|
|
26
|
+
if (!messagesPath)
|
|
27
|
+
return EMPTY;
|
|
28
|
+
try {
|
|
29
|
+
const st = statSync(messagesPath);
|
|
30
|
+
if (cache && cache.path === messagesPath && cache.mtimeMs === st.mtimeMs && cache.size === st.size) {
|
|
31
|
+
return cache.stats;
|
|
32
|
+
}
|
|
33
|
+
const raw = JSON.parse(readFileSync(messagesPath, "utf-8"));
|
|
34
|
+
const stats = aggregate(raw);
|
|
35
|
+
cache = { path: messagesPath, mtimeMs: st.mtimeMs, size: st.size, stats };
|
|
36
|
+
return stats;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Absent (nothing said yet), unreadable, or half-written: whatever we last
|
|
40
|
+
// read, else nothing. The friend list still answers, with zero counts —
|
|
41
|
+
// and the "since" line tells the user the history is not known.
|
|
42
|
+
return cache?.stats ?? EMPTY;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Split out from the file read so tests can feed it a plain object. */
|
|
46
|
+
export function aggregate(raw) {
|
|
47
|
+
const byPeer = new Map();
|
|
48
|
+
let historySince;
|
|
49
|
+
const threads = (raw && typeof raw === "object" ? raw : {});
|
|
50
|
+
for (const [peer, value] of Object.entries(threads)) {
|
|
51
|
+
if (!Array.isArray(value))
|
|
52
|
+
continue;
|
|
53
|
+
const s = { inboundCount: 0, outboundCount: 0 };
|
|
54
|
+
for (const m of value) {
|
|
55
|
+
const ts = typeof m?.ts === "number" ? m.ts : undefined;
|
|
56
|
+
if (m?.dir === "in") {
|
|
57
|
+
s.inboundCount++;
|
|
58
|
+
if (ts !== undefined && (s.lastInboundTs === undefined || ts > s.lastInboundTs))
|
|
59
|
+
s.lastInboundTs = ts;
|
|
60
|
+
}
|
|
61
|
+
else if (m?.dir === "out") {
|
|
62
|
+
s.outboundCount++;
|
|
63
|
+
if (ts !== undefined && (s.lastOutboundTs === undefined || ts > s.lastOutboundTs))
|
|
64
|
+
s.lastOutboundTs = ts;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
continue; // not a message record
|
|
68
|
+
}
|
|
69
|
+
if (ts !== undefined && (s.firstMsgTs === undefined || ts < s.firstMsgTs))
|
|
70
|
+
s.firstMsgTs = ts;
|
|
71
|
+
}
|
|
72
|
+
byPeer.set(peer, s);
|
|
73
|
+
if (s.firstMsgTs !== undefined && (historySince === undefined || s.firstMsgTs < historySince)) {
|
|
74
|
+
historySince = s.firstMsgTs;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { byPeer, historySince };
|
|
78
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -171,6 +171,7 @@ async function main() {
|
|
|
171
171
|
routesPath: resolve(args.configDir, "routes.yaml"),
|
|
172
172
|
doraRosterPath,
|
|
173
173
|
downloadsDir: resolve(args.configDir, "downloads"),
|
|
174
|
+
messagesPath: resolve(args.configDir, "messages.json"),
|
|
174
175
|
callIce: {
|
|
175
176
|
keyFile: resolve(decentlanCarrierDir(args.configDir), "keypair.json"),
|
|
176
177
|
bootstrapNodes,
|
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.107";
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// packages/beagle-ui/dist/beagle-ui.js
|
|
@@ -341,6 +341,42 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
341
341
|
channel: "@next",
|
|
342
342
|
wire: "163"
|
|
343
343
|
};
|
|
344
|
+
var DK_PAGE = 30;
|
|
345
|
+
function dkMsgKey(m) {
|
|
346
|
+
return m.id || m.ts + ":" + m.dir;
|
|
347
|
+
}
|
|
348
|
+
function dkThreadMsg(m) {
|
|
349
|
+
return {
|
|
350
|
+
id: m.id,
|
|
351
|
+
from: m.dir === "out" ? "me" : "them",
|
|
352
|
+
time: dkClock(m.ts),
|
|
353
|
+
text: m.text,
|
|
354
|
+
file: m.file ? {
|
|
355
|
+
name: m.file.name,
|
|
356
|
+
size: dkFileSize(m.file.size),
|
|
357
|
+
dir: m.dir,
|
|
358
|
+
media: dkFileMediaKind(m.file.name),
|
|
359
|
+
status: m.file.status,
|
|
360
|
+
pct: m.file.status === "sending" && m.file.size ? Math.min(100, (m.file.sent || 0) / m.file.size * 100) : void 0,
|
|
361
|
+
kbps: m.file.kbps
|
|
362
|
+
} : void 0,
|
|
363
|
+
// Pass the real state through. Collapsing everything that was not
|
|
364
|
+
// 'queued' to 'read' meant a message still in flight — or one that
|
|
365
|
+
// never went out at all — rendered as delivered.
|
|
366
|
+
status: m.dir === "out" ? m.status === "queued" || m.status === "sending" || m.status === "failed" || m.status === "sent" ? m.status : "read" : void 0,
|
|
367
|
+
error: m.error,
|
|
368
|
+
// false = went to a live session but nobody acknowledged it (one tick).
|
|
369
|
+
// Dropped here before, so that state could never render.
|
|
370
|
+
confirmed: m.confirmed,
|
|
371
|
+
// Delivery path: "online" = live session, "offline" = express relay.
|
|
372
|
+
// Both directions carry it now; the bubble labels them the same way.
|
|
373
|
+
via: m.via,
|
|
374
|
+
// Brief 30: a validated component form, already whitelisted host-side.
|
|
375
|
+
// Passed through untouched — the renderer treats every string as text,
|
|
376
|
+
// never markup.
|
|
377
|
+
components: m.components
|
|
378
|
+
};
|
|
379
|
+
}
|
|
344
380
|
function useDaemonData() {
|
|
345
381
|
const [snap, setSnap] = React.useState({
|
|
346
382
|
me: DK_ME_FALLBACK,
|
|
@@ -386,52 +422,74 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
386
422
|
return () => clearInterval(t);
|
|
387
423
|
}, [refresh]);
|
|
388
424
|
const inFlight = React.useRef(/* @__PURE__ */ new Set());
|
|
389
|
-
const
|
|
390
|
-
|
|
425
|
+
const cache = React.useRef({});
|
|
426
|
+
const [earlier, setEarlier] = React.useState({});
|
|
427
|
+
const publish = React.useCallback((peerId) => {
|
|
428
|
+
const c = cache.current[peerId];
|
|
429
|
+
if (!c)
|
|
430
|
+
return;
|
|
431
|
+
const arr = [...c.byId.values()].sort((a, b) => a.ts - b.ts);
|
|
432
|
+
const msgs = arr.map(dkThreadMsg);
|
|
433
|
+
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
434
|
+
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
435
|
+
setEarlier((e) => e[peerId] === c.more ? e : Object.assign({}, e, { [peerId]: c.more }));
|
|
436
|
+
}, []);
|
|
437
|
+
const loadThread = React.useCallback(async (peerId, opts) => {
|
|
438
|
+
if (!peerId)
|
|
439
|
+
return;
|
|
440
|
+
if (opts && opts.reset)
|
|
441
|
+
delete cache.current[peerId];
|
|
442
|
+
if (inFlight.current.has(peerId))
|
|
391
443
|
return;
|
|
392
444
|
inFlight.current.add(peerId);
|
|
393
445
|
try {
|
|
394
|
-
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=
|
|
446
|
+
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=" + DK_PAGE);
|
|
395
447
|
const arr = d.chats && d.chats[peerId] || [];
|
|
396
448
|
await prefetchThreadFiles(arr);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
kbps: m.file.kbps
|
|
410
|
-
} : void 0,
|
|
411
|
-
// Pass the real state through. Collapsing everything that was not
|
|
412
|
-
// 'queued' to 'read' meant a message still in flight — or one that
|
|
413
|
-
// never went out at all — rendered as delivered.
|
|
414
|
-
status: m.dir === "out" ? m.status === "queued" || m.status === "sending" || m.status === "failed" || m.status === "sent" ? m.status : "read" : void 0,
|
|
415
|
-
error: m.error,
|
|
416
|
-
// false = went to a live session but nobody acknowledged it (one tick).
|
|
417
|
-
// Dropped here before, so that state could never render.
|
|
418
|
-
confirmed: m.confirmed,
|
|
419
|
-
// Delivery path: "online" = live session, "offline" = express relay.
|
|
420
|
-
// Both directions carry it now; the bubble labels them the same way.
|
|
421
|
-
via: m.via,
|
|
422
|
-
// Brief 30: a validated component form, already whitelisted host-side.
|
|
423
|
-
// Passed through untouched — the renderer treats every string as text,
|
|
424
|
-
// never markup.
|
|
425
|
-
components: m.components
|
|
426
|
-
}));
|
|
427
|
-
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
428
|
-
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
449
|
+
let c = cache.current[peerId];
|
|
450
|
+
if (!c)
|
|
451
|
+
c = cache.current[peerId] = { byId: /* @__PURE__ */ new Map(), more: arr.length >= DK_PAGE };
|
|
452
|
+
if (arr.length) {
|
|
453
|
+
const from = arr[0].ts;
|
|
454
|
+
for (const [k, m] of c.byId)
|
|
455
|
+
if (m.ts >= from)
|
|
456
|
+
c.byId.delete(k);
|
|
457
|
+
}
|
|
458
|
+
for (const m of arr)
|
|
459
|
+
c.byId.set(dkMsgKey(m), m);
|
|
460
|
+
publish(peerId);
|
|
429
461
|
} catch (e) {
|
|
430
462
|
} finally {
|
|
431
463
|
inFlight.current.delete(peerId);
|
|
432
464
|
}
|
|
433
|
-
}, []);
|
|
434
|
-
|
|
465
|
+
}, [publish]);
|
|
466
|
+
const loadEarlier = React.useCallback(async (peerId) => {
|
|
467
|
+
const c = cache.current[peerId];
|
|
468
|
+
if (!c || !c.more || c.loadingEarlier)
|
|
469
|
+
return;
|
|
470
|
+
let oldest = Infinity;
|
|
471
|
+
for (const m of c.byId.values())
|
|
472
|
+
if (m.ts < oldest)
|
|
473
|
+
oldest = m.ts;
|
|
474
|
+
if (!Number.isFinite(oldest))
|
|
475
|
+
return;
|
|
476
|
+
c.loadingEarlier = true;
|
|
477
|
+
try {
|
|
478
|
+
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=" + DK_PAGE + "&before=" + oldest);
|
|
479
|
+
const arr = d.chats && d.chats[peerId] || [];
|
|
480
|
+
await prefetchThreadFiles(arr);
|
|
481
|
+
if (cache.current[peerId] !== c)
|
|
482
|
+
return;
|
|
483
|
+
for (const m of arr)
|
|
484
|
+
c.byId.set(dkMsgKey(m), m);
|
|
485
|
+
c.more = arr.length >= DK_PAGE;
|
|
486
|
+
publish(peerId);
|
|
487
|
+
} catch (e) {
|
|
488
|
+
} finally {
|
|
489
|
+
c.loadingEarlier = false;
|
|
490
|
+
}
|
|
491
|
+
}, [publish]);
|
|
492
|
+
return Object.assign({}, snap, { threads, earlier, refresh, loadThread, loadEarlier });
|
|
435
493
|
}
|
|
436
494
|
Object.assign(window, {
|
|
437
495
|
dkHash,
|
|
@@ -443,6 +501,558 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
443
501
|
DK_ME_FALLBACK,
|
|
444
502
|
dkCopy
|
|
445
503
|
});
|
|
504
|
+
var actions_default = {
|
|
505
|
+
version: 1,
|
|
506
|
+
scope: "assistant",
|
|
507
|
+
note: "The Assistant's action list. /help is generated from it, the router dispatches from it, and iOS/Android implement it. A later natural-language layer translates a sentence into one of these actions and nothing else. See docs/assistant/SPEC.md.",
|
|
508
|
+
actions: [
|
|
509
|
+
{
|
|
510
|
+
name: "help",
|
|
511
|
+
usage: "/help",
|
|
512
|
+
summary: "List what the Assistant can do.",
|
|
513
|
+
params: { type: "object", properties: {}, required: [] },
|
|
514
|
+
confirm: "none",
|
|
515
|
+
scope: "assistant"
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: "friends-list",
|
|
519
|
+
usage: "/friends list [filter\u2026]",
|
|
520
|
+
summary: "Show friends as a table: name, last heard from, messages in/out. With no filter, everyone.",
|
|
521
|
+
params: {
|
|
522
|
+
type: "object",
|
|
523
|
+
properties: {
|
|
524
|
+
filters: {
|
|
525
|
+
type: "array",
|
|
526
|
+
items: { enum: ["never-messaged", "never-replied", "silent", "pending"] },
|
|
527
|
+
description: "Several filters are OR'd together."
|
|
528
|
+
},
|
|
529
|
+
silentDays: { type: "integer", minimum: 1, default: 90, description: "Argument of the silent filter: nothing from them in this many days." }
|
|
530
|
+
},
|
|
531
|
+
required: []
|
|
532
|
+
},
|
|
533
|
+
confirm: "none",
|
|
534
|
+
scope: "assistant"
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: "friends-remove",
|
|
538
|
+
usage: "/friends remove <filter\u2026>",
|
|
539
|
+
summary: "Same table with a tick per row and a Remove button. Nothing is removed by typing alone.",
|
|
540
|
+
params: {
|
|
541
|
+
type: "object",
|
|
542
|
+
properties: {
|
|
543
|
+
filters: {
|
|
544
|
+
type: "array",
|
|
545
|
+
items: { enum: ["never-messaged", "never-replied", "silent", "pending"] },
|
|
546
|
+
minItems: 1
|
|
547
|
+
},
|
|
548
|
+
silentDays: { type: "integer", minimum: 1, default: 90 }
|
|
549
|
+
},
|
|
550
|
+
required: ["filters"]
|
|
551
|
+
},
|
|
552
|
+
confirm: "list",
|
|
553
|
+
scope: "assistant"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: "find",
|
|
557
|
+
usage: "/find <text>",
|
|
558
|
+
summary: "Friends whose name, alias, description or ens name contains the text.",
|
|
559
|
+
params: {
|
|
560
|
+
type: "object",
|
|
561
|
+
properties: { text: { type: "string", minLength: 1 } },
|
|
562
|
+
required: ["text"]
|
|
563
|
+
},
|
|
564
|
+
confirm: "none",
|
|
565
|
+
scope: "assistant"
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: "rename",
|
|
569
|
+
usage: "/rename <friend> <new name>",
|
|
570
|
+
summary: "Set a friend's alias \u2014 what you see them as, on this device.",
|
|
571
|
+
params: {
|
|
572
|
+
type: "object",
|
|
573
|
+
properties: {
|
|
574
|
+
friend: { type: "string", description: "Matches name, alias or userid prefix. Ambiguous = nothing happens, the candidates are listed." },
|
|
575
|
+
alias: { type: "string", minLength: 1 }
|
|
576
|
+
},
|
|
577
|
+
required: ["friend", "alias"]
|
|
578
|
+
},
|
|
579
|
+
confirm: "none",
|
|
580
|
+
scope: "assistant"
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: "mute",
|
|
584
|
+
usage: "/mute <friend>",
|
|
585
|
+
summary: "Stop alerts from one conversation.",
|
|
586
|
+
params: {
|
|
587
|
+
type: "object",
|
|
588
|
+
properties: { friend: { type: "string" } },
|
|
589
|
+
required: ["friend"]
|
|
590
|
+
},
|
|
591
|
+
confirm: "none",
|
|
592
|
+
scope: "assistant"
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
name: "unmute",
|
|
596
|
+
usage: "/unmute <friend>",
|
|
597
|
+
summary: "Alerts from that conversation again.",
|
|
598
|
+
params: {
|
|
599
|
+
type: "object",
|
|
600
|
+
properties: { friend: { type: "string" } },
|
|
601
|
+
required: ["friend"]
|
|
602
|
+
},
|
|
603
|
+
confirm: "none",
|
|
604
|
+
scope: "assistant"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "sound",
|
|
608
|
+
usage: "/sound on|off",
|
|
609
|
+
summary: "Sound on a new message.",
|
|
610
|
+
params: {
|
|
611
|
+
type: "object",
|
|
612
|
+
properties: { on: { type: "boolean" } },
|
|
613
|
+
required: ["on"]
|
|
614
|
+
},
|
|
615
|
+
confirm: "none",
|
|
616
|
+
scope: "assistant"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "flash",
|
|
620
|
+
usage: "/flash on|off",
|
|
621
|
+
summary: "Flash the window on a new message.",
|
|
622
|
+
params: {
|
|
623
|
+
type: "object",
|
|
624
|
+
properties: { on: { type: "boolean" } },
|
|
625
|
+
required: ["on"]
|
|
626
|
+
},
|
|
627
|
+
confirm: "none",
|
|
628
|
+
scope: "assistant"
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "status",
|
|
632
|
+
usage: "/status",
|
|
633
|
+
summary: "Friends, pending requests, how far back the history on this device goes, app version.",
|
|
634
|
+
params: { type: "object", properties: {}, required: [] },
|
|
635
|
+
confirm: "none",
|
|
636
|
+
scope: "assistant"
|
|
637
|
+
}
|
|
638
|
+
],
|
|
639
|
+
filters: [
|
|
640
|
+
{
|
|
641
|
+
name: "never-messaged",
|
|
642
|
+
usage: "never-messaged",
|
|
643
|
+
summary: "No message either way.",
|
|
644
|
+
guarded: true
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "never-replied",
|
|
648
|
+
usage: "never-replied",
|
|
649
|
+
summary: "You wrote at least once, they never did.",
|
|
650
|
+
guarded: true
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "silent",
|
|
654
|
+
usage: "silent [days]",
|
|
655
|
+
summary: "Nothing from them for this many days (default 90).",
|
|
656
|
+
guarded: true,
|
|
657
|
+
arg: { name: "days", type: "integer", default: 90, minimum: 1 }
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: "pending",
|
|
661
|
+
usage: "pending",
|
|
662
|
+
summary: "A request you sent that was never accepted.",
|
|
663
|
+
guarded: false
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
dropped: [
|
|
667
|
+
{
|
|
668
|
+
name: "never-called",
|
|
669
|
+
reason: "The message store holds no call records on any platform, so the predicate cannot be evaluated. /api/assistant/friends returns lastCallTs: null and the filter is not offered."
|
|
670
|
+
}
|
|
671
|
+
],
|
|
672
|
+
guard: {
|
|
673
|
+
recentDays: 14,
|
|
674
|
+
applies: "Filters marked guarded never match a friend accepted (or, with no acceptance stamp, requested) less than recentDays ago; the result says how many were skipped."
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
var ASSISTANT_ID = "__assistant__";
|
|
678
|
+
var DAY_MS = 864e5;
|
|
679
|
+
var RECENT_DAYS = 14;
|
|
680
|
+
var DEFAULT_SILENT_DAYS = 90;
|
|
681
|
+
var FILTER_NAMES = ["never-messaged", "never-replied", "silent", "pending"];
|
|
682
|
+
var GUARDED = /* @__PURE__ */ new Set(["never-messaged", "never-replied", "silent"]);
|
|
683
|
+
function parseCommand(line) {
|
|
684
|
+
const text = String(line != null ? line : "").trim();
|
|
685
|
+
if (!text.startsWith("/"))
|
|
686
|
+
return null;
|
|
687
|
+
const parts = text.slice(1).split(/\s+/).filter(Boolean);
|
|
688
|
+
if (!parts.length)
|
|
689
|
+
return null;
|
|
690
|
+
const head = parts[0].toLowerCase();
|
|
691
|
+
if (head === "friends") {
|
|
692
|
+
const sub = (parts[1] || "").toLowerCase();
|
|
693
|
+
if (sub === "list" || sub === "remove")
|
|
694
|
+
return { ok: true, name: `friends-${sub}`, args: parts.slice(2) };
|
|
695
|
+
return { ok: false, name: "friends", args: parts.slice(1), error: "friends-subcommand" };
|
|
696
|
+
}
|
|
697
|
+
return { ok: true, name: head, args: parts.slice(1) };
|
|
698
|
+
}
|
|
699
|
+
function parseFilters(args) {
|
|
700
|
+
const filters = [];
|
|
701
|
+
const unknown = [];
|
|
702
|
+
let silentDays = DEFAULT_SILENT_DAYS;
|
|
703
|
+
const words = (args != null ? args : []).map((a) => String(a).toLowerCase());
|
|
704
|
+
for (let i = 0; i < words.length; i++) {
|
|
705
|
+
const w = words[i];
|
|
706
|
+
if (!FILTER_NAMES.includes(w)) {
|
|
707
|
+
unknown.push(words[i]);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (w === "silent") {
|
|
711
|
+
const n = Number(words[i + 1]);
|
|
712
|
+
if (Number.isInteger(n) && n > 0) {
|
|
713
|
+
silentDays = n;
|
|
714
|
+
i++;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (!filters.includes(w))
|
|
718
|
+
filters.push(w);
|
|
719
|
+
}
|
|
720
|
+
return { filters, silentDays, unknown };
|
|
721
|
+
}
|
|
722
|
+
var acceptedOrRequestedAt = (f) => f.acceptedAt || f.requestedAt || 0;
|
|
723
|
+
function isRecent(friend, now, recentDays = RECENT_DAYS) {
|
|
724
|
+
const at = acceptedOrRequestedAt(friend);
|
|
725
|
+
return at > 0 && now - at < recentDays * DAY_MS;
|
|
726
|
+
}
|
|
727
|
+
function matchesFilter(friend, filter, { now, silentDays = DEFAULT_SILENT_DAYS } = {}) {
|
|
728
|
+
const inCount = friend.inboundCount || 0;
|
|
729
|
+
const outCount = friend.outboundCount || 0;
|
|
730
|
+
switch (filter) {
|
|
731
|
+
case "never-messaged":
|
|
732
|
+
return inCount === 0 && outCount === 0;
|
|
733
|
+
case "never-replied":
|
|
734
|
+
return outCount > 0 && inCount === 0;
|
|
735
|
+
case "silent":
|
|
736
|
+
return !friend.lastInboundTs || now - friend.lastInboundTs > silentDays * DAY_MS;
|
|
737
|
+
case "pending":
|
|
738
|
+
return friend.pending === true;
|
|
739
|
+
default:
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function selectFriends(friends, filters, { now = Date.now(), silentDays = DEFAULT_SILENT_DAYS, recentDays = RECENT_DAYS } = {}) {
|
|
744
|
+
const pool = (friends != null ? friends : []).filter((f) => f && f.userid !== ASSISTANT_ID && !f.pinned);
|
|
745
|
+
if (!filters || !filters.length)
|
|
746
|
+
return { matched: pool.slice(), skippedRecent: [] };
|
|
747
|
+
const matched = [];
|
|
748
|
+
const skippedRecent = [];
|
|
749
|
+
for (const f of pool) {
|
|
750
|
+
let hit = false;
|
|
751
|
+
let guardedOnly = false;
|
|
752
|
+
for (const filter of filters) {
|
|
753
|
+
if (!matchesFilter(f, filter, { now, silentDays }))
|
|
754
|
+
continue;
|
|
755
|
+
if (GUARDED.has(filter) && isRecent(f, now, recentDays)) {
|
|
756
|
+
guardedOnly = true;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
hit = true;
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
if (hit)
|
|
763
|
+
matched.push(f);
|
|
764
|
+
else if (guardedOnly)
|
|
765
|
+
skippedRecent.push(f);
|
|
766
|
+
}
|
|
767
|
+
return { matched, skippedRecent };
|
|
768
|
+
}
|
|
769
|
+
function findFriends(friends, text) {
|
|
770
|
+
const q = String(text != null ? text : "").trim().toLowerCase();
|
|
771
|
+
if (!q)
|
|
772
|
+
return [];
|
|
773
|
+
return (friends != null ? friends : []).filter((f) => f && f.userid !== ASSISTANT_ID).filter((f) => [f.name, f.alias, f.description, f.ens].some((v) => typeof v === "string" && v.toLowerCase().includes(q)));
|
|
774
|
+
}
|
|
775
|
+
function resolveFriend(friends, token) {
|
|
776
|
+
const q = String(token != null ? token : "").trim().toLowerCase();
|
|
777
|
+
if (!q)
|
|
778
|
+
return { match: null, candidates: [] };
|
|
779
|
+
const pool = (friends != null ? friends : []).filter((f) => f && f.userid !== ASSISTANT_ID);
|
|
780
|
+
const exact = pool.filter((f) => [f.name, f.alias].some((v) => typeof v === "string" && v.toLowerCase() === q));
|
|
781
|
+
if (exact.length === 1)
|
|
782
|
+
return { match: exact[0], candidates: exact };
|
|
783
|
+
if (exact.length > 1)
|
|
784
|
+
return { match: null, candidates: exact };
|
|
785
|
+
const partial = pool.filter((f) => [f.name, f.alias].some((v) => typeof v === "string" && v.toLowerCase().startsWith(q)) || typeof f.userid === "string" && f.userid.toLowerCase().startsWith(q));
|
|
786
|
+
if (partial.length === 1)
|
|
787
|
+
return { match: partial[0], candidates: partial };
|
|
788
|
+
return { match: null, candidates: partial };
|
|
789
|
+
}
|
|
790
|
+
var ASSISTANT_PEER = {
|
|
791
|
+
id: ASSISTANT_ID,
|
|
792
|
+
userId: ASSISTANT_ID,
|
|
793
|
+
alias: "Beagle Assistant",
|
|
794
|
+
address: "",
|
|
795
|
+
online: true,
|
|
796
|
+
app: true,
|
|
797
|
+
pinned: true,
|
|
798
|
+
agent: false,
|
|
799
|
+
pending: false,
|
|
800
|
+
via: null,
|
|
801
|
+
ping: null,
|
|
802
|
+
ip: "",
|
|
803
|
+
unread: 0,
|
|
804
|
+
lastMsg: "",
|
|
805
|
+
lastTime: "",
|
|
806
|
+
lastTs: 0,
|
|
807
|
+
lastDir: null,
|
|
808
|
+
acceptedAt: 0,
|
|
809
|
+
requestedAt: 0,
|
|
810
|
+
wire: "163",
|
|
811
|
+
avatarUrl: null,
|
|
812
|
+
punkId: null
|
|
813
|
+
};
|
|
814
|
+
var fmtDate = (ts, lang) => !ts ? lang === "zh" ? "\u4ECE\u672A" : "never" : new Date(ts).toLocaleDateString(lang === "zh" ? "zh-CN" : "en-GB", { year: "numeric", month: "short", day: "numeric" });
|
|
815
|
+
var fmtAgo = (ts, now, lang) => {
|
|
816
|
+
if (!ts)
|
|
817
|
+
return lang === "zh" ? "\u4ECE\u672A" : "never";
|
|
818
|
+
const days = Math.floor((now - ts) / 864e5);
|
|
819
|
+
if (days <= 0)
|
|
820
|
+
return lang === "zh" ? "\u4ECA\u5929" : "today";
|
|
821
|
+
if (days === 1)
|
|
822
|
+
return lang === "zh" ? "1 \u5929\u524D" : "1 day ago";
|
|
823
|
+
return lang === "zh" ? `${days} \u5929\u524D` : `${days} days ago`;
|
|
824
|
+
};
|
|
825
|
+
var nameOf = (f) => f.alias || f.name || f.userid;
|
|
826
|
+
function sinceLine(historySince, lang) {
|
|
827
|
+
if (!historySince)
|
|
828
|
+
return lang === "zh" ? "_\u57FA\u4E8E\u672C\u8BBE\u5907\u4E0A\u7684\u6D88\u606F\uFF08\u672C\u673A\u6682\u65E0\u5386\u53F2\u8BB0\u5F55\uFF09_" : "_based on messages on this device (no history here yet)_";
|
|
829
|
+
return lang === "zh" ? `_\u57FA\u4E8E\u672C\u8BBE\u5907\u81EA ${fmtDate(historySince, lang)} \u8D77\u7684\u6D88\u606F_` : `_based on messages on this device since ${fmtDate(historySince, lang)}_`;
|
|
830
|
+
}
|
|
831
|
+
function friendTable(rows, now, lang) {
|
|
832
|
+
return rows.map((f) => lang === "zh" ? `- **${nameOf(f)}** \u2014 \u6700\u540E\u6536\u5230 ${fmtAgo(f.lastInboundTs, now, lang)} \xB7 \u6536 ${f.inboundCount || 0} / \u53D1 ${f.outboundCount || 0}` : `- **${nameOf(f)}** \u2014 last heard ${fmtAgo(f.lastInboundTs, now, lang)} \xB7 in ${f.inboundCount || 0} / out ${f.outboundCount || 0}`).join("\n");
|
|
833
|
+
}
|
|
834
|
+
function helpText(lang) {
|
|
835
|
+
const intro = lang === "zh" ? "**Beagle \u52A9\u624B** \u2014 \u53EA\u5728\u672C\u673A\u8FD0\u884C\uFF0C\u4E0D\u4F1A\u628A\u4EFB\u4F55\u5185\u5BB9\u53D1\u5230\u7F51\u7EDC\u4E0A\u3002\n\n\u53EF\u7528\u547D\u4EE4\uFF1A" : "**Beagle Assistant** \u2014 runs on this machine only; nothing here is sent over the network.\n\nCommands:";
|
|
836
|
+
const cmds = actions_default.actions.map((a) => `- \`${a.usage}\` \u2014 ${a.summary}`).join("\n");
|
|
837
|
+
const filters = actions_default.filters.map((f) => `- \`${f.usage}\` \u2014 ${f.summary}`).join("\n");
|
|
838
|
+
const tail = lang === "zh" ? `\u7B5B\u9009\u6761\u4EF6\uFF08\u53EF\u7EC4\u5408\uFF0C\u53D6\u5E76\u96C6\uFF09\uFF1A
|
|
839
|
+
${filters}
|
|
840
|
+
|
|
841
|
+
\u6700\u8FD1 ${RECENT_DAYS} \u5929\u5185\u6DFB\u52A0\u7684\u597D\u53CB\u4E0D\u4F1A\u88AB"\u4ECE\u672A/\u6C89\u9ED8"\u7C7B\u7B5B\u9009\u9009\u4E2D\u3002\u5220\u9664\u6C38\u8FDC\u9700\u8981\u5148\u786E\u8BA4\u3002` : `Filters (combine them; a friend matching any one is listed):
|
|
842
|
+
${filters}
|
|
843
|
+
|
|
844
|
+
Friends added in the last ${RECENT_DAYS} days are never matched by the "never/silent" filters. Removal always asks first.`;
|
|
845
|
+
return `${intro}
|
|
846
|
+
${cmds}
|
|
847
|
+
|
|
848
|
+
${tail}`;
|
|
849
|
+
}
|
|
850
|
+
var seq = 0;
|
|
851
|
+
var asMsg = (from, text, extra) => Object.assign({
|
|
852
|
+
id: `as-${Date.now()}-${seq++}`,
|
|
853
|
+
from,
|
|
854
|
+
time: (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
|
855
|
+
text
|
|
856
|
+
}, extra);
|
|
857
|
+
function AssistantPanel({ m, T, lang, onRemove }) {
|
|
858
|
+
const rows = m.assistant.rows || [];
|
|
859
|
+
const [sel, setSel] = React.useState(() => new Set(rows.map((r) => r.userid)));
|
|
860
|
+
const [busy, setBusy] = React.useState(false);
|
|
861
|
+
const [done, setDone] = React.useState(null);
|
|
862
|
+
if (!rows.length)
|
|
863
|
+
return null;
|
|
864
|
+
const toggle = (id) => setSel((s) => {
|
|
865
|
+
const n = new Set(s);
|
|
866
|
+
n.has(id) ? n.delete(id) : n.add(id);
|
|
867
|
+
return n;
|
|
868
|
+
});
|
|
869
|
+
const label = lang === "zh" ? `\u5220\u9664 ${sel.size} \u4F4D` : `Remove ${sel.size}`;
|
|
870
|
+
return /* @__PURE__ */ React.createElement("div", { style: { marginTop: 8, border: "1px solid var(--line)", borderRadius: 10, overflow: "hidden" } }, rows.map((r) => /* @__PURE__ */ React.createElement("label", { key: r.userid, style: {
|
|
871
|
+
display: "flex",
|
|
872
|
+
alignItems: "center",
|
|
873
|
+
gap: 8,
|
|
874
|
+
padding: "7px 10px",
|
|
875
|
+
borderBottom: "1px solid var(--line)",
|
|
876
|
+
fontSize: 13,
|
|
877
|
+
cursor: done ? "default" : "pointer"
|
|
878
|
+
} }, /* @__PURE__ */ React.createElement(
|
|
879
|
+
"input",
|
|
880
|
+
{
|
|
881
|
+
type: "checkbox",
|
|
882
|
+
checked: sel.has(r.userid),
|
|
883
|
+
disabled: !!done || busy,
|
|
884
|
+
onChange: () => toggle(r.userid)
|
|
885
|
+
}
|
|
886
|
+
), /* @__PURE__ */ React.createElement("span", { style: { flex: 1 } }, nameOf(r)), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, r.userid.slice(0, 8), "\u2026"))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "8px 10px" } }, done ? /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12.5, color: "var(--faint)" } }, done) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !sel.size, onClick: () => {
|
|
887
|
+
setBusy(true);
|
|
888
|
+
onRemove([...sel]).then((r) => {
|
|
889
|
+
setDone(lang === "zh" ? `\u5DF2\u5220\u9664 ${r.removed} \u4F4D${r.failed ? `\uFF0C${r.failed} \u4F4D\u5931\u8D25` : ""}` : `Removed ${r.removed}${r.failed ? `, ${r.failed} failed` : ""}`);
|
|
890
|
+
}).finally(() => setBusy(false));
|
|
891
|
+
} }, busy ? T.working || "Working\u2026" : label), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, color: "var(--faint)" } }, lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u53EF\u4FDD\u7559" : "Untick anyone you want to keep"))));
|
|
892
|
+
}
|
|
893
|
+
function useAssistant({ lang, T, muted, onToggleMute, sound, flash, setAlert, onReloadFriends }) {
|
|
894
|
+
const [thread, setThread] = React.useState(() => [{ day: (/* @__PURE__ */ new Date()).toLocaleDateString() }, asMsg("them", helpText(lang))]);
|
|
895
|
+
const append = React.useCallback((...ms) => setThread((t) => t.concat(ms)), []);
|
|
896
|
+
React.useEffect(() => {
|
|
897
|
+
setThread((t) => t.length === 2 ? [t[0], asMsg("them", helpText(lang))] : t);
|
|
898
|
+
}, [lang]);
|
|
899
|
+
const fetchFriends = async () => {
|
|
900
|
+
const r = await fetch("/api/assistant/friends").then((x) => x.json());
|
|
901
|
+
return { friends: r.friends || [], historySince: r.historySince || null, now: r.now || Date.now(), ver: r.beagleVer || "" };
|
|
902
|
+
};
|
|
903
|
+
const listReply = (rows, skipped, data, title) => {
|
|
904
|
+
if (!rows.length) {
|
|
905
|
+
return `${title}
|
|
906
|
+
|
|
907
|
+
${lang === "zh" ? "\u6CA1\u6709\u7B26\u5408\u7684\u597D\u53CB\u3002" : "No friends match."}
|
|
908
|
+
|
|
909
|
+
${sinceLine(data.historySince, lang)}`;
|
|
910
|
+
}
|
|
911
|
+
const skip = skipped.length ? lang === "zh" ? `
|
|
912
|
+
|
|
913
|
+
_\u8DF3\u8FC7\u4E86 ${skipped.length} \u4F4D\u6700\u8FD1 ${RECENT_DAYS} \u5929\u5185\u6DFB\u52A0\u7684\u597D\u53CB\u3002_` : `
|
|
914
|
+
|
|
915
|
+
_${skipped.length} friend${skipped.length > 1 ? "s" : ""} added in the last ${RECENT_DAYS} days ${skipped.length > 1 ? "were" : "was"} skipped._` : "";
|
|
916
|
+
return `${title}
|
|
917
|
+
|
|
918
|
+
${friendTable(rows, data.now, lang)}${skip}
|
|
919
|
+
|
|
920
|
+
${sinceLine(data.historySince, lang)}`;
|
|
921
|
+
};
|
|
922
|
+
const ambiguous = (cands) => lang === "zh" ? `\u6709 ${cands.length} \u4F4D\u597D\u53CB\u7B26\u5408\uFF0C\u8BF7\u8BF4\u5F97\u66F4\u5177\u4F53\u4E00\u4E9B\uFF1A
|
|
923
|
+
${cands.map((c) => `- ${nameOf(c)}`).join("\n")}` : `That matches ${cands.length} friends \u2014 be more specific:
|
|
924
|
+
${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
925
|
+
const notFound = (tok) => lang === "zh" ? `\u6CA1\u6709\u627E\u5230\u597D\u53CB "${tok}"\u3002` : `No friend matches "${tok}".`;
|
|
926
|
+
const handle = React.useCallback(async (text) => {
|
|
927
|
+
append(asMsg("me", text, { status: "read" }));
|
|
928
|
+
const cmd = parseCommand(text);
|
|
929
|
+
if (!cmd) {
|
|
930
|
+
return append(asMsg("them", lang === "zh" ? "\u6211\u53EA\u8BA4\u8BC6\u4EE5 `/` \u5F00\u5934\u7684\u547D\u4EE4\u3002\u8F93\u5165 `/help` \u770B\u770B\u80FD\u505A\u4EC0\u4E48\u3002" : "I only understand commands starting with `/`. Type `/help` to see them."));
|
|
931
|
+
}
|
|
932
|
+
if (cmd.ok === false) {
|
|
933
|
+
return append(asMsg("them", lang === "zh" ? "`/friends` \u9700\u8981 `list` \u6216 `remove`\uFF0C\u4F8B\u5982 `/friends list silent 90`\u3002" : "`/friends` needs `list` or `remove`, for example `/friends list silent 90`."));
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
switch (cmd.name) {
|
|
937
|
+
case "help":
|
|
938
|
+
return append(asMsg("them", helpText(lang)));
|
|
939
|
+
case "status": {
|
|
940
|
+
const d = await fetchFriends();
|
|
941
|
+
const pending = d.friends.filter((f) => f.pending).length;
|
|
942
|
+
return append(asMsg("them", lang === "zh" ? `**\u72B6\u6001**
|
|
943
|
+
|
|
944
|
+
- \u597D\u53CB\uFF1A${d.friends.length}
|
|
945
|
+
- \u5F85\u5BF9\u65B9\u63A5\u53D7\uFF1A${pending}
|
|
946
|
+
- \u672C\u673A\u5386\u53F2\u8D77\u81EA\uFF1A${fmtDate(d.historySince, lang)}
|
|
947
|
+
- \u7248\u672C\uFF1A${d.ver}` : `**Status**
|
|
948
|
+
|
|
949
|
+
- Friends: ${d.friends.length}
|
|
950
|
+
- Waiting to be accepted: ${pending}
|
|
951
|
+
- History on this device since: ${fmtDate(d.historySince, lang)}
|
|
952
|
+
- Version: ${d.ver}`));
|
|
953
|
+
}
|
|
954
|
+
case "friends-list": {
|
|
955
|
+
const { filters, silentDays, unknown } = parseFilters(cmd.args);
|
|
956
|
+
if (unknown.length) {
|
|
957
|
+
return append(asMsg("them", lang === "zh" ? `\u4E0D\u8BA4\u8BC6\u7684\u7B5B\u9009\u6761\u4EF6\uFF1A${unknown.join(", ")}\u3002\u53EF\u7528\uFF1A${actions_default.filters.map((f) => f.name).join(", ")}\u3002` : `I don't know the filter${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}. Try: ${actions_default.filters.map((f) => f.name).join(", ")}.`));
|
|
958
|
+
}
|
|
959
|
+
const d = await fetchFriends();
|
|
960
|
+
const { matched, skippedRecent } = selectFriends(d.friends, filters, { now: d.now, silentDays });
|
|
961
|
+
const title = filters.length ? `**${filters.join(" + ")}**${filters.includes("silent") ? ` (${silentDays}d)` : ""} \u2014 ${matched.length}` : lang === "zh" ? `**\u5168\u90E8\u597D\u53CB** \u2014 ${matched.length}` : `**All friends** \u2014 ${matched.length}`;
|
|
962
|
+
return append(asMsg("them", listReply(matched, skippedRecent, d, title)));
|
|
963
|
+
}
|
|
964
|
+
case "friends-remove": {
|
|
965
|
+
const { filters, silentDays, unknown } = parseFilters(cmd.args);
|
|
966
|
+
if (unknown.length || !filters.length) {
|
|
967
|
+
return append(asMsg("them", lang === "zh" ? `\u5220\u9664\u9700\u8981\u7B5B\u9009\u6761\u4EF6\uFF0C\u4F8B\u5982 \`/friends remove never-messaged\`\u3002\u53EF\u7528\uFF1A${actions_default.filters.map((f) => f.name).join(", ")}\u3002` : `Removing needs a filter, for example \`/friends remove never-messaged\`. Available: ${actions_default.filters.map((f) => f.name).join(", ")}.`));
|
|
968
|
+
}
|
|
969
|
+
const d = await fetchFriends();
|
|
970
|
+
const { matched, skippedRecent } = selectFriends(d.friends, filters, { now: d.now, silentDays });
|
|
971
|
+
const title = lang === "zh" ? `**\u5C06\u8981\u5220\u9664 ${matched.length} \u4F4D\u597D\u53CB** \u2014 \u8BF7\u5148\u6838\u5BF9\u8FD9\u4EFD\u540D\u5355` : `**${matched.length} friend${matched.length === 1 ? "" : "s"} to remove** \u2014 check the list first`;
|
|
972
|
+
return append(asMsg(
|
|
973
|
+
"them",
|
|
974
|
+
listReply(matched, skippedRecent, d, title),
|
|
975
|
+
matched.length ? { assistant: { kind: "confirm", rows: matched } } : void 0
|
|
976
|
+
));
|
|
977
|
+
}
|
|
978
|
+
case "find": {
|
|
979
|
+
const q = cmd.args.join(" ");
|
|
980
|
+
if (!q)
|
|
981
|
+
return append(asMsg("them", lang === "zh" ? "\u7528\u6CD5\uFF1A`/find <\u6587\u5B57>`" : "Usage: `/find <text>`"));
|
|
982
|
+
const d = await fetchFriends();
|
|
983
|
+
const rows = findFriends(d.friends, q);
|
|
984
|
+
const title = lang === "zh" ? `**"${q}"** \u2014 ${rows.length} \u4F4D` : `**"${q}"** \u2014 ${rows.length} found`;
|
|
985
|
+
return append(asMsg("them", listReply(rows, [], d, title)));
|
|
986
|
+
}
|
|
987
|
+
case "rename": {
|
|
988
|
+
const [tok, ...rest] = cmd.args;
|
|
989
|
+
const alias = rest.join(" ").trim();
|
|
990
|
+
if (!tok || !alias)
|
|
991
|
+
return append(asMsg("them", lang === "zh" ? "\u7528\u6CD5\uFF1A`/rename <\u597D\u53CB> <\u65B0\u540D\u5B57>`" : "Usage: `/rename <friend> <new name>`"));
|
|
992
|
+
const d = await fetchFriends();
|
|
993
|
+
const { match, candidates } = resolveFriend(d.friends, tok);
|
|
994
|
+
if (!match)
|
|
995
|
+
return append(asMsg("them", candidates.length ? ambiguous(candidates) : notFound(tok)));
|
|
996
|
+
const was = nameOf(match);
|
|
997
|
+
await dkApi.alias(match.userid, alias);
|
|
998
|
+
if (onReloadFriends)
|
|
999
|
+
onReloadFriends();
|
|
1000
|
+
return append(asMsg("them", lang === "zh" ? `\u5DF2\u628A "${was}" \u6539\u540D\u4E3A "${alias}"\u3002` : `Renamed "${was}" to "${alias}".`));
|
|
1001
|
+
}
|
|
1002
|
+
case "mute":
|
|
1003
|
+
case "unmute": {
|
|
1004
|
+
const tok = cmd.args.join(" ").trim();
|
|
1005
|
+
if (!tok)
|
|
1006
|
+
return append(asMsg("them", `${lang === "zh" ? "\u7528\u6CD5" : "Usage"}: \`/${cmd.name} <friend>\``));
|
|
1007
|
+
const d = await fetchFriends();
|
|
1008
|
+
const { match, candidates } = resolveFriend(d.friends, tok);
|
|
1009
|
+
if (!match)
|
|
1010
|
+
return append(asMsg("them", candidates.length ? ambiguous(candidates) : notFound(tok)));
|
|
1011
|
+
const isMuted = (muted || []).includes(match.userid);
|
|
1012
|
+
const want = cmd.name === "mute";
|
|
1013
|
+
if (isMuted !== want)
|
|
1014
|
+
onToggleMute(match.userid);
|
|
1015
|
+
return append(asMsg("them", want ? lang === "zh" ? `\u5DF2\u9759\u97F3 "${nameOf(match)}"\u3002` : `Muted "${nameOf(match)}".` : lang === "zh" ? `\u5DF2\u53D6\u6D88\u9759\u97F3 "${nameOf(match)}"\u3002` : `Unmuted "${nameOf(match)}".`));
|
|
1016
|
+
}
|
|
1017
|
+
case "sound":
|
|
1018
|
+
case "flash": {
|
|
1019
|
+
const word = (cmd.args[0] || "").toLowerCase();
|
|
1020
|
+
if (word !== "on" && word !== "off") {
|
|
1021
|
+
return append(asMsg("them", `${lang === "zh" ? "\u7528\u6CD5" : "Usage"}: \`/${cmd.name} on|off\``));
|
|
1022
|
+
}
|
|
1023
|
+
const on = word === "on";
|
|
1024
|
+
setAlert(cmd.name, on);
|
|
1025
|
+
const what = cmd.name === "sound" ? lang === "zh" ? "\u63D0\u793A\u97F3" : "Sound" : lang === "zh" ? "\u7A97\u53E3\u95EA\u70C1" : "Window flash";
|
|
1026
|
+
return append(asMsg("them", lang === "zh" ? `${what}\u5DF2${on ? "\u5F00\u542F" : "\u5173\u95ED"}\u3002` : `${what} ${on ? "on" : "off"}.`));
|
|
1027
|
+
}
|
|
1028
|
+
default:
|
|
1029
|
+
return append(asMsg("them", lang === "zh" ? `\u4E0D\u8BA4\u8BC6\u7684\u547D\u4EE4 \`/${cmd.name}\`\u3002\u8F93\u5165 \`/help\` \u770B\u770B\u53EF\u7528\u547D\u4EE4\u3002` : `I don't know \`/${cmd.name}\`. Type \`/help\` for the list.`));
|
|
1030
|
+
}
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
return append(asMsg("them", lang === "zh" ? `\u51FA\u9519\u4E86\uFF1A${e && e.message || e}` : `That failed: ${e && e.message || e}`));
|
|
1033
|
+
}
|
|
1034
|
+
}, [append, lang, muted, onToggleMute, setAlert, onReloadFriends]);
|
|
1035
|
+
const removeFriends = React.useCallback(async (ids) => {
|
|
1036
|
+
let removed = 0;
|
|
1037
|
+
const failed = [];
|
|
1038
|
+
for (const id of ids) {
|
|
1039
|
+
try {
|
|
1040
|
+
const r = await dkApi.remove(id);
|
|
1041
|
+
if (r && r.ok === false)
|
|
1042
|
+
failed.push(id);
|
|
1043
|
+
else
|
|
1044
|
+
removed++;
|
|
1045
|
+
} catch (e) {
|
|
1046
|
+
failed.push(id);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (onReloadFriends)
|
|
1050
|
+
onReloadFriends();
|
|
1051
|
+
append(asMsg("them", failed.length ? lang === "zh" ? `\u5DF2\u5220\u9664 ${removed} \u4F4D\uFF0C${failed.length} \u4F4D\u5931\u8D25\u3002` : `Removed ${removed}, ${failed.length} failed.` : lang === "zh" ? `\u5DF2\u5220\u9664 ${removed} \u4F4D\u597D\u53CB\u3002` : `Removed ${removed} friend${removed === 1 ? "" : "s"}.`));
|
|
1052
|
+
return { removed, failed: failed.length };
|
|
1053
|
+
}, [append, lang, onReloadFriends]);
|
|
1054
|
+
return { thread, handle, removeFriends };
|
|
1055
|
+
}
|
|
446
1056
|
function RequestsBlock({ T, requests, onAct }) {
|
|
447
1057
|
const [acted, setActed] = React.useState({});
|
|
448
1058
|
const [failed, setFailed] = React.useState({});
|
|
@@ -523,7 +1133,7 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
523
1133
|
whiteSpace: "nowrap",
|
|
524
1134
|
overflow: "hidden",
|
|
525
1135
|
textOverflow: "ellipsis"
|
|
526
|
-
} }, peer.alias || shortKey(peer.userId, 8, 5)), peer.agent && /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: 13, color: "var(--faint)", stroke: 2 }), muted && /* @__PURE__ */ React.createElement(Icon, { name: "bellOff", size: 12, color: "var(--faint)", stroke: 2, title: T && T.mutedTag || "sound muted" }), peer.wire === "64" && /* @__PURE__ */ React.createElement(Tag, { tone: "off", style: { height: 15, fontSize: 9.5, padding: "0 4px" } }, "w64")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 2 } }, peer.ip ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, peer.ip), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)", fontSize: 11 } }, "\xB7")) : null, /* @__PURE__ */ React.createElement("span", { style: {
|
|
1136
|
+
} }, peer.alias || shortKey(peer.userId, 8, 5)), peer.agent && /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: 13, color: "var(--faint)", stroke: 2 }), peer.app && /* @__PURE__ */ React.createElement(Tag, { tone: "accent", style: { height: 15, fontSize: 9.5, padding: "0 4px" } }, "APP"), muted && /* @__PURE__ */ React.createElement(Icon, { name: "bellOff", size: 12, color: "var(--faint)", stroke: 2, title: T && T.mutedTag || "sound muted" }), peer.wire === "64" && /* @__PURE__ */ React.createElement(Tag, { tone: "off", style: { height: 15, fontSize: 9.5, padding: "0 4px" } }, "w64")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 2 } }, peer.ip ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, peer.ip), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)", fontSize: 11 } }, "\xB7")) : null, /* @__PURE__ */ React.createElement("span", { style: {
|
|
527
1137
|
fontSize: 11,
|
|
528
1138
|
color: "var(--dim)",
|
|
529
1139
|
whiteSpace: "nowrap",
|
|
@@ -579,6 +1189,8 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
579
1189
|
const s = (p.alias || "") + " " + p.userId + " " + p.ip;
|
|
580
1190
|
return s.toLowerCase().includes(q.toLowerCase());
|
|
581
1191
|
}).slice().sort((a, b) => {
|
|
1192
|
+
if (!!a.pinned !== !!b.pinned)
|
|
1193
|
+
return a.pinned ? -1 : 1;
|
|
582
1194
|
const ta = dkActivityTs(a), tb = dkActivityTs(b);
|
|
583
1195
|
if (ta !== tb)
|
|
584
1196
|
return tb - ta;
|
|
@@ -1244,7 +1856,7 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
1244
1856
|
return null;
|
|
1245
1857
|
}), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
|
|
1246
1858
|
}
|
|
1247
|
-
function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, answered, selMode, selected, onToggleSel, busy, isGroup }) {
|
|
1859
|
+
function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, answered, selMode, selected, onToggleSel, busy, isGroup }) {
|
|
1248
1860
|
const mine = m.from === "me";
|
|
1249
1861
|
const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
|
|
1250
1862
|
const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
|
|
@@ -1368,6 +1980,8 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
1368
1980
|
{
|
|
1369
1981
|
src: dkFileUrl(m.file.name),
|
|
1370
1982
|
alt: m.file.name,
|
|
1983
|
+
loading: "lazy",
|
|
1984
|
+
decoding: "async",
|
|
1371
1985
|
onError: (e) => {
|
|
1372
1986
|
const w = e.currentTarget.closest("[data-media]");
|
|
1373
1987
|
if (w)
|
|
@@ -1571,7 +2185,7 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
1571
2185
|
letterSpacing: -0.1,
|
|
1572
2186
|
wordBreak: "break-word"
|
|
1573
2187
|
} }, /* @__PURE__ */ React.createElement(MarkdownText, { text: read.text, onNameCard })));
|
|
1574
|
-
})(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), (() => {
|
|
2188
|
+
})(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), m.assistant && onAssistantRemove && /* @__PURE__ */ React.createElement(AssistantPanel, { m, T, lang, onRemove: onAssistantRemove }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), (() => {
|
|
1575
2189
|
const sentDone = mine && m.status && m.status !== "queued" && m.status !== "sending" && m.status !== "failed";
|
|
1576
2190
|
const via = !mine ? m.via : sentDone ? m.via === "offline" ? "offline" : "online" : null;
|
|
1577
2191
|
if (!via)
|
|
@@ -1616,8 +2230,10 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
1616
2230
|
) : mine && m.via === "offline" && m.status !== "queued" && m.status !== "sending" ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "#f59e0b", title: T.viaOutOffline || "posted to the offline relay \u2014 they get it when they come online" }) : mine && m.status === "sent" && m.confirmed === false ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "var(--faint)", title: T.sentUnconfirmed || "sent \u2014 not confirmed by the peer" }) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
|
|
1617
2231
|
);
|
|
1618
2232
|
}
|
|
1619
|
-
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted, onToggleMute, phone, onBack }) {
|
|
2233
|
+
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted, onToggleMute, phone, onBack }) {
|
|
1620
2234
|
const scrollRef = React.useRef(null);
|
|
2235
|
+
const prependRef = React.useRef(null);
|
|
2236
|
+
const [earlierBusy, setEarlierBusy] = React.useState(false);
|
|
1621
2237
|
const isGroup = React.useMemo(() => dkThreadIsGroup(threadProp), [threadProp]);
|
|
1622
2238
|
const followScrollRef = React.useRef(true);
|
|
1623
2239
|
const fileRef = React.useRef(null);
|
|
@@ -1695,9 +2311,30 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
1695
2311
|
followScrollRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
|
1696
2312
|
};
|
|
1697
2313
|
React.useLayoutEffect(() => {
|
|
2314
|
+
const el = scrollRef.current;
|
|
2315
|
+
const pre = prependRef.current;
|
|
2316
|
+
if (pre && el && el.scrollHeight !== pre.h) {
|
|
2317
|
+
el.scrollTop = pre.top + (el.scrollHeight - pre.h);
|
|
2318
|
+
prependRef.current = null;
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
1698
2321
|
if (followScrollRef.current)
|
|
1699
2322
|
scrollToBottom();
|
|
1700
2323
|
}, [peer.id, thread]);
|
|
2324
|
+
React.useEffect(() => {
|
|
2325
|
+
prependRef.current = null;
|
|
2326
|
+
setEarlierBusy(false);
|
|
2327
|
+
}, [peer.id]);
|
|
2328
|
+
const loadEarlier = () => {
|
|
2329
|
+
const el = scrollRef.current;
|
|
2330
|
+
if (!onLoadEarlier || earlierBusy)
|
|
2331
|
+
return;
|
|
2332
|
+
if (el)
|
|
2333
|
+
prependRef.current = { h: el.scrollHeight, top: el.scrollTop };
|
|
2334
|
+
followScrollRef.current = false;
|
|
2335
|
+
setEarlierBusy(true);
|
|
2336
|
+
Promise.resolve(onLoadEarlier()).finally(() => setEarlierBusy(false));
|
|
2337
|
+
};
|
|
1701
2338
|
const toggleSel = (id) => setSel((s) => {
|
|
1702
2339
|
const n = new Set(s);
|
|
1703
2340
|
n.has(id) ? n.delete(id) : n.add(id);
|
|
@@ -1898,7 +2535,7 @@ window.__DK_UI_VERSION="0.1.104";
|
|
|
1898
2535
|
style: { width: 40, height: 44, flexShrink: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", padding: 0 }
|
|
1899
2536
|
},
|
|
1900
2537
|
/* @__PURE__ */ React.createElement(Icon, { name: "chevronLeft", size: 26, stroke: 2.2, color: "var(--accent)" })
|
|
1901
|
-
), /* @__PURE__ */ React.createElement("div", { onClick: () => setPubProfile(true), title: T && T.pubOpen || "View public profile", style: { cursor: "pointer", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 })), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: phone ? 1 : void 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 } }, /* @__PURE__ */ React.createElement("span", { onClick: () => setPubProfile(true), style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)", cursor: "pointer", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 } }, peer.alias || shortKey(peer.userId, phone ? 6 : 10, phone ? 4 : 6)), peer.agent && !phone && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), isGroup && !phone && /* @__PURE__ */ React.createElement(Tag, null, T && T.groupTag || "group"), !phone && /* @__PURE__ */ React.createElement(RouteTag, { peer })), phone ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, display: "flex" } }, /* @__PURE__ */ React.createElement(RouteTag, { peer })) : /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), title: T && T.openNet || "Network view", style: { background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--accent)", display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.ip, title: peer.ip }, peer.ip), peer.address && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement(
|
|
2538
|
+
), /* @__PURE__ */ React.createElement("div", { onClick: () => setPubProfile(true), title: T && T.pubOpen || "View public profile", style: { cursor: "pointer", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 })), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: phone ? 1 : void 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 } }, /* @__PURE__ */ React.createElement("span", { onClick: () => setPubProfile(true), style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)", cursor: "pointer", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 } }, peer.alias || shortKey(peer.userId, phone ? 6 : 10, phone ? 4 : 6)), peer.app && !phone && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "APP"), peer.agent && !phone && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), isGroup && !phone && /* @__PURE__ */ React.createElement(Tag, null, T && T.groupTag || "group"), !phone && !peer.app && /* @__PURE__ */ React.createElement(RouteTag, { peer })), phone ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, display: "flex" } }, /* @__PURE__ */ React.createElement(RouteTag, { peer })) : /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), title: T && T.openNet || "Network view", style: { background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--accent)", display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.ip, title: peer.ip }, peer.ip), peer.address && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement(
|
|
1902
2539
|
Mono,
|
|
1903
2540
|
{
|
|
1904
2541
|
size: 11.5,
|
|
@@ -1957,15 +2594,17 @@ ${peer.address}`
|
|
|
1957
2594
|
fontSize: 12.5,
|
|
1958
2595
|
color: "var(--text)"
|
|
1959
2596
|
} }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 13, stroke: 2.2, color: "#d29922" }), /* @__PURE__ */ React.createElement("span", null, T.pendingFriend || "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.")),
|
|
1960
|
-
/* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: phone ? "12px 10px" : "18px 22px", WebkitOverflowScrolling: "touch" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, margin: "0 auto" } }, thread.filter((m) => m.day || !hidden.has(m.id)).map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(
|
|
2597
|
+
/* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: phone ? "12px 10px" : "18px 22px", WebkitOverflowScrolling: "touch" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, margin: "0 auto" } }, hasEarlier && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "center", margin: "4px 0 10px" } }, /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: loadEarlier, disabled: earlierBusy }, earlierBusy ? T.loadingEarlier || "Loading\u2026" : T.loadEarlier || "Load earlier messages")), thread.filter((m) => m.day || !hidden.has(m.id)).map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(
|
|
1961
2598
|
Msg,
|
|
1962
2599
|
{
|
|
1963
2600
|
key: m.id || i,
|
|
1964
2601
|
m,
|
|
1965
2602
|
peer,
|
|
1966
2603
|
T,
|
|
2604
|
+
lang,
|
|
1967
2605
|
isGroup,
|
|
1968
2606
|
onTheater: setTheater,
|
|
2607
|
+
onAssistantRemove,
|
|
1969
2608
|
onDelete: (id) => doDelete([id]),
|
|
1970
2609
|
onCancel: doCancel,
|
|
1971
2610
|
onRetry: doRetry,
|
|
@@ -2117,12 +2756,12 @@ ${peer.address}`
|
|
|
2117
2756
|
function ChatEmpty({ T }) {
|
|
2118
2757
|
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, color: "var(--faint)", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "message", size: 40, stroke: 1.4, color: "var(--line)" }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, T.pickPeer));
|
|
2119
2758
|
}
|
|
2120
|
-
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
|
|
2759
|
+
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
|
|
2121
2760
|
const peer = peers.find((p) => p.id === activeId);
|
|
2122
2761
|
if (phone) {
|
|
2123
|
-
return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide: true });
|
|
2762
|
+
return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide: true });
|
|
2124
2763
|
}
|
|
2125
|
-
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
2764
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
2126
2765
|
}
|
|
2127
2766
|
Object.assign(window, { ChatTab });
|
|
2128
2767
|
function CopyBtn({ value, copiedText = "Copied", copyFailedText = "Copy failed", copyTitle = "Copy" }) {
|
|
@@ -4885,6 +5524,8 @@ ${peer.address}`
|
|
|
4885
5524
|
autoAcceptLabel: "Auto-accept friend requests",
|
|
4886
5525
|
autoAcceptSub: "Off: requests wait in the sidebar for your approval.",
|
|
4887
5526
|
groupSenderUnknown: "The group relays only this display name. Register Beagle as a group agent (/agent add <beagle address> in the group) to receive full member identities you can add as friends.",
|
|
5527
|
+
loadEarlier: "Load earlier messages",
|
|
5528
|
+
loadingEarlier: "Loading\u2026",
|
|
4888
5529
|
pendingFriend: "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.",
|
|
4889
5530
|
cancelling: "Cancelling\u2026",
|
|
4890
5531
|
cancelled: "Transfer cancelled",
|
|
@@ -5077,6 +5718,8 @@ ${peer.address}`
|
|
|
5077
5718
|
autoAcceptLabel: "\u81EA\u52A8\u63A5\u53D7\u597D\u53CB\u8BF7\u6C42",
|
|
5078
5719
|
autoAcceptSub: "\u5173\u95ED\u540E\uFF0C\u597D\u53CB\u8BF7\u6C42\u4F1A\u7B49\u5F85\u4F60\u5728\u4FA7\u8FB9\u680F\u624B\u52A8\u786E\u8BA4\u3002",
|
|
5079
5720
|
groupSenderUnknown: "\u7FA4\u670D\u52A1\u53EA\u8F6C\u53D1\u4E86\u663E\u793A\u540D\uFF0C\u62FF\u4E0D\u5230\u8BE5\u6210\u5458\u7684\u5B8C\u6574\u8EAB\u4EFD\u3002\u5728\u7FA4\u91CC\u6267\u884C /agent add <beagle \u5730\u5740> \u628A Beagle \u6CE8\u518C\u4E3A\u7FA4 agent \u540E\uFF0C\u5373\u53EF\u6536\u5230\u5B8C\u6574\u8EAB\u4EFD\u5E76\u4E00\u952E\u52A0\u597D\u53CB\u3002",
|
|
5721
|
+
loadEarlier: "\u52A0\u8F7D\u66F4\u65E9\u7684\u6D88\u606F",
|
|
5722
|
+
loadingEarlier: "\u52A0\u8F7D\u4E2D\u2026",
|
|
5080
5723
|
pendingFriend: "\u7B49\u5F85\u5BF9\u65B9\u63A5\u53D7\u597D\u53CB\u8BF7\u6C42 \u2014 \u6D88\u606F\u4F1A\u5148\u6392\u961F\uFF0C\u5BF9\u65B9\u63A5\u53D7\u540E\u81EA\u52A8\u9001\u8FBE\u3002",
|
|
5081
5724
|
cancelling: "\u6B63\u5728\u53D6\u6D88\u2026",
|
|
5082
5725
|
cancelled: "\u5DF2\u53D6\u6D88\u4F20\u8F93",
|
|
@@ -5421,7 +6064,7 @@ ${peer.address}`
|
|
|
5421
6064
|
sessionStorage.setItem(key, "1");
|
|
5422
6065
|
window.location.reload();
|
|
5423
6066
|
}, [me && me.beagleVer]);
|
|
5424
|
-
const peers = data.peers;
|
|
6067
|
+
const peers = React.useMemo(() => [ASSISTANT_PEER].concat(data.peers || []), [data.peers]);
|
|
5425
6068
|
const [pendingAddr, setPendingAddr] = React.useState(() => dkParseDeepLinkAddress());
|
|
5426
6069
|
React.useEffect(() => {
|
|
5427
6070
|
const onHash = () => {
|
|
@@ -5485,6 +6128,16 @@ ${peer.address}`
|
|
|
5485
6128
|
flash: t.alertFlash !== false,
|
|
5486
6129
|
muted: mutedIds
|
|
5487
6130
|
});
|
|
6131
|
+
const assistant = useAssistant({
|
|
6132
|
+
lang: t.lang,
|
|
6133
|
+
T,
|
|
6134
|
+
muted: mutedIds,
|
|
6135
|
+
onToggleMute,
|
|
6136
|
+
sound: t.alertSound !== false,
|
|
6137
|
+
flash: t.alertFlash !== false,
|
|
6138
|
+
setAlert: (which, on) => setTweak(which === "sound" ? "alertSound" : "alertFlash", on),
|
|
6139
|
+
onReloadFriends: () => data.refresh()
|
|
6140
|
+
});
|
|
5488
6141
|
const callCtl = useCallController(me.userId, (peerId, video, direction) => {
|
|
5489
6142
|
const text = `WebRTC ${video ? "video" : "audio"} call: ${direction}`;
|
|
5490
6143
|
dkApi.logLocal(peerId, direction === "outgoing" ? "out" : "in", text).then(() => {
|
|
@@ -5547,7 +6200,9 @@ ${peer.address}`
|
|
|
5547
6200
|
React.useEffect(() => {
|
|
5548
6201
|
if (!activeId)
|
|
5549
6202
|
return;
|
|
5550
|
-
|
|
6203
|
+
if (activeId === ASSISTANT_ID)
|
|
6204
|
+
return;
|
|
6205
|
+
data.loadThread(activeId, { reset: true });
|
|
5551
6206
|
dkApi.markRead(activeId).then(data.refresh);
|
|
5552
6207
|
const iv = setInterval(() => {
|
|
5553
6208
|
data.loadThread(activeId);
|
|
@@ -5590,6 +6245,10 @@ ${peer.address}`
|
|
|
5590
6245
|
const onSend = (text) => {
|
|
5591
6246
|
if (!activeId || !text || !text.trim())
|
|
5592
6247
|
return;
|
|
6248
|
+
if (activeId === ASSISTANT_ID) {
|
|
6249
|
+
assistant.handle(text.trim());
|
|
6250
|
+
return;
|
|
6251
|
+
}
|
|
5593
6252
|
dkApi.send(activeId, text.trim()).then(() => data.loadThread(activeId)).then(data.refresh);
|
|
5594
6253
|
};
|
|
5595
6254
|
const peerTakesRtcFile = (id) => {
|
|
@@ -5697,7 +6356,7 @@ ${peer.address}`
|
|
|
5697
6356
|
fontWeight: 600,
|
|
5698
6357
|
background: tab === n.id ? "var(--accent)" : "transparent",
|
|
5699
6358
|
color: tab === n.id ? "#fff" : "var(--dim)"
|
|
5700
|
-
} }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, onBack: closeConversation, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak, onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
|
|
6359
|
+
} }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, onBack: closeConversation, peers, requests, activeId, thread: activeId === ASSISTANT_ID ? assistant.thread : data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), hasEarlier: !!(activeId && activeId !== ASSISTANT_ID && data.earlier[activeId]), onAssistantRemove: activeId === ASSISTANT_ID ? assistant.removeFriends : null, onLoadEarlier: () => activeId && data.loadEarlier(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak, onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
|
|
5701
6360
|
data.refresh();
|
|
5702
6361
|
return r;
|
|
5703
6362
|
}) })), phone && !inConversation && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", borderTop: "1px solid var(--line)", background: "var(--rail)", paddingBottom: "env(safe-area-inset-bottom, 0px)" } }, phoneNav.map((n) => /* @__PURE__ */ React.createElement(TabBarBtn, { key: n.id, icon: n.icon, label: n.label, active: n.active, badge: n.badge, onClick: n.go }))), data.locked && /* @__PURE__ */ React.createElement(
|
package/dist/message-store.js
CHANGED
|
@@ -162,6 +162,12 @@ export class MessageStore {
|
|
|
162
162
|
arr = [];
|
|
163
163
|
this.byPeer.set(peer, arr);
|
|
164
164
|
}
|
|
165
|
+
// ts is the paging cursor (`before` is strict), so it must be unique per
|
|
166
|
+
// thread: a burst in one millisecond would otherwise be split by a page
|
|
167
|
+
// boundary and the rest of it skipped. Order is kept; the id is not touched.
|
|
168
|
+
const newest = arr.length ? arr[arr.length - 1].ts : -Infinity;
|
|
169
|
+
if (msg.ts <= newest)
|
|
170
|
+
msg.ts = newest + 1;
|
|
165
171
|
arr.push(msg);
|
|
166
172
|
if (arr.length > MAX_PER_PEER)
|
|
167
173
|
arr.splice(0, arr.length - MAX_PER_PEER);
|
package/dist/server.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export interface BeagleServerOptions {
|
|
|
24
24
|
/** Directory where the daemon saves received files (<configDir>/downloads).
|
|
25
25
|
* Used to serve GET /api/file-download. Undefined disables downloads. */
|
|
26
26
|
downloadsDir?: string;
|
|
27
|
+
/** <configDir>/messages.json — the chat store BOTH backends write. Read
|
|
28
|
+
* (never written) by GET /api/assistant/friends to count a friend's
|
|
29
|
+
* history without hauling it over IPC. Undefined = no counts. */
|
|
30
|
+
messagesPath?: string;
|
|
27
31
|
/** The swappable backend, so the UI can show which one is live and drive the
|
|
28
32
|
* LAN handoff. Optional: a caller that pins one backend just omits it and
|
|
29
33
|
* the network panel falls back to "no virtual LAN here". */
|
package/dist/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { DEFAULT_EXITS } from "./exits.js";
|
|
|
24
24
|
import * as peerAddr from "@decentnetwork/peer";
|
|
25
25
|
import { bridgeRoster } from "./bridge-roster.js";
|
|
26
26
|
import { isSupervised } from "./relaunch.js";
|
|
27
|
+
import { historyStats } from "./assistant-stats.js";
|
|
27
28
|
// Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
|
|
28
29
|
// scripts/build-ui.mjs emits it next to this compiled module at dist/ui/desktop/.
|
|
29
30
|
const DESKTOP_DIR = join(dirname(fileURLToPath(import.meta.url)), "desktop");
|
|
@@ -2237,6 +2238,58 @@ export function startBeagleServer(opts) {
|
|
|
2237
2238
|
sendJson(res, 200, { me, peers, requests, exits, activeExit });
|
|
2238
2239
|
return;
|
|
2239
2240
|
}
|
|
2241
|
+
// Everything the Assistant's friend filters need, in one answer: the
|
|
2242
|
+
// friend list plus how much was said to each friend and when. The
|
|
2243
|
+
// counts come from the store file in our own config dir — see
|
|
2244
|
+
// assistant-stats.ts for why they are not fetched over IPC.
|
|
2245
|
+
if (req.method === "GET" && url === "/api/assistant/friends") {
|
|
2246
|
+
const [flist, ensPub] = await Promise.all([
|
|
2247
|
+
opts.call({ op: "friends-list" }),
|
|
2248
|
+
ensPublicByUserid(300),
|
|
2249
|
+
]);
|
|
2250
|
+
const stamps = friendStoreStamps(opts.callIce?.keyFile);
|
|
2251
|
+
const stats = historyStats(opts.messagesPath);
|
|
2252
|
+
const fl = flist.ok ? (flist.data?.friends ?? []) : [];
|
|
2253
|
+
const friends = fl.map((f) => {
|
|
2254
|
+
const uid = f.userid ?? "";
|
|
2255
|
+
const stamp = stamps.get(uid);
|
|
2256
|
+
const pub = ensPub.get(uid);
|
|
2257
|
+
const status = f.status;
|
|
2258
|
+
const acceptedAt = f.lastSeen ?? stamp?.acceptedAt ?? 0;
|
|
2259
|
+
const realName = typeof f.name === "string" && f.name !== uid ? f.name : undefined;
|
|
2260
|
+
const s = stats.byPeer.get(uid);
|
|
2261
|
+
return {
|
|
2262
|
+
userid: uid,
|
|
2263
|
+
name: f.alias || realName || pub?.displayName || uid,
|
|
2264
|
+
alias: f.alias || null,
|
|
2265
|
+
description: f.descr || pub?.description || null,
|
|
2266
|
+
ens: pub?.ens ?? null,
|
|
2267
|
+
acceptedAt,
|
|
2268
|
+
requestedAt: f.requestedAt ?? stamp?.requestedAt ?? 0,
|
|
2269
|
+
// A request we sent that was never accepted — same rule the
|
|
2270
|
+
// sidebar's waiting banner uses.
|
|
2271
|
+
pending: f.accepted === false || (status === "requested" && !acceptedAt),
|
|
2272
|
+
firstMsgTs: s?.firstMsgTs ?? null,
|
|
2273
|
+
lastInboundTs: s?.lastInboundTs ?? null,
|
|
2274
|
+
lastOutboundTs: s?.lastOutboundTs ?? null,
|
|
2275
|
+
inboundCount: s?.inboundCount ?? 0,
|
|
2276
|
+
outboundCount: s?.outboundCount ?? 0,
|
|
2277
|
+
// No call records exist in the store, so this is always null and
|
|
2278
|
+
// the Assistant does not offer a never-called filter.
|
|
2279
|
+
lastCallTs: null,
|
|
2280
|
+
// Only ever true if a backend starts saying so; nothing does today.
|
|
2281
|
+
agent: f.agent === true,
|
|
2282
|
+
};
|
|
2283
|
+
});
|
|
2284
|
+
sendJson(res, 200, {
|
|
2285
|
+
ok: true,
|
|
2286
|
+
friends,
|
|
2287
|
+
historySince: stats.historySince ?? null,
|
|
2288
|
+
beagleVer: beagleVersion(),
|
|
2289
|
+
now: Date.now(),
|
|
2290
|
+
});
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2240
2293
|
if (req.method === "GET" && url === "/api/chat-history") {
|
|
2241
2294
|
const q = new URL(req.url || "/", "http://x").searchParams;
|
|
2242
2295
|
const peer = q.get("peer") || undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.107",
|
|
4
4
|
"description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,14 +28,14 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@decentnetwork/chat-components": "^0.1.3",
|
|
30
30
|
"@decentnetwork/lan": "^0.1.283",
|
|
31
|
-
"@decentnetwork/peer": "^0.1.
|
|
31
|
+
"@decentnetwork/peer": "^0.1.168",
|
|
32
32
|
"@decentnetwork/peer-webrtc": "^0.2.16",
|
|
33
33
|
"js-yaml": "^4.1.0",
|
|
34
34
|
"ws": "^8.21.1",
|
|
35
35
|
"yargs": "^17.7.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@decentnetwork/beagle-ui": "^0.2.
|
|
38
|
+
"@decentnetwork/beagle-ui": "^0.2.21",
|
|
39
39
|
"@types/js-yaml": "^4.0.9",
|
|
40
40
|
"@types/node": "^20.11.0",
|
|
41
41
|
"@types/yargs": "^17.0.32",
|