@decentnetwork/beagle-ui 0.2.19 → 0.2.21
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/beagle-ui.js +715 -50
- package/package.json +1 -1
package/dist/beagle-ui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const BEAGLE_UI_VERSION = "0.2.
|
|
1
|
+
export const BEAGLE_UI_VERSION = "0.2.21";
|
|
2
2
|
// src/icons.jsx
|
|
3
3
|
var ICON_PATHS = {
|
|
4
4
|
// ---- tab bar (the four must feel like one set) ----
|
|
@@ -345,6 +345,42 @@ var DK_ME_FALLBACK = {
|
|
|
345
345
|
channel: "@next",
|
|
346
346
|
wire: "163"
|
|
347
347
|
};
|
|
348
|
+
var DK_PAGE = 30;
|
|
349
|
+
function dkMsgKey(m) {
|
|
350
|
+
return m.id || m.ts + ":" + m.dir;
|
|
351
|
+
}
|
|
352
|
+
function dkThreadMsg(m) {
|
|
353
|
+
return {
|
|
354
|
+
id: m.id,
|
|
355
|
+
from: m.dir === "out" ? "me" : "them",
|
|
356
|
+
time: dkClock(m.ts),
|
|
357
|
+
text: m.text,
|
|
358
|
+
file: m.file ? {
|
|
359
|
+
name: m.file.name,
|
|
360
|
+
size: dkFileSize(m.file.size),
|
|
361
|
+
dir: m.dir,
|
|
362
|
+
media: dkFileMediaKind(m.file.name),
|
|
363
|
+
status: m.file.status,
|
|
364
|
+
pct: m.file.status === "sending" && m.file.size ? Math.min(100, (m.file.sent || 0) / m.file.size * 100) : void 0,
|
|
365
|
+
kbps: m.file.kbps
|
|
366
|
+
} : void 0,
|
|
367
|
+
// Pass the real state through. Collapsing everything that was not
|
|
368
|
+
// 'queued' to 'read' meant a message still in flight — or one that
|
|
369
|
+
// never went out at all — rendered as delivered.
|
|
370
|
+
status: m.dir === "out" ? m.status === "queued" || m.status === "sending" || m.status === "failed" || m.status === "sent" ? m.status : "read" : void 0,
|
|
371
|
+
error: m.error,
|
|
372
|
+
// false = went to a live session but nobody acknowledged it (one tick).
|
|
373
|
+
// Dropped here before, so that state could never render.
|
|
374
|
+
confirmed: m.confirmed,
|
|
375
|
+
// Delivery path: "online" = live session, "offline" = express relay.
|
|
376
|
+
// Both directions carry it now; the bubble labels them the same way.
|
|
377
|
+
via: m.via,
|
|
378
|
+
// Brief 30: a validated component form, already whitelisted host-side.
|
|
379
|
+
// Passed through untouched — the renderer treats every string as text,
|
|
380
|
+
// never markup.
|
|
381
|
+
components: m.components
|
|
382
|
+
};
|
|
383
|
+
}
|
|
348
384
|
function useDaemonData() {
|
|
349
385
|
const [snap, setSnap] = React.useState({
|
|
350
386
|
me: DK_ME_FALLBACK,
|
|
@@ -390,52 +426,74 @@ function useDaemonData() {
|
|
|
390
426
|
return () => clearInterval(t);
|
|
391
427
|
}, [refresh]);
|
|
392
428
|
const inFlight = React.useRef(/* @__PURE__ */ new Set());
|
|
393
|
-
const
|
|
394
|
-
|
|
429
|
+
const cache = React.useRef({});
|
|
430
|
+
const [earlier, setEarlier] = React.useState({});
|
|
431
|
+
const publish = React.useCallback((peerId) => {
|
|
432
|
+
const c = cache.current[peerId];
|
|
433
|
+
if (!c)
|
|
434
|
+
return;
|
|
435
|
+
const arr = [...c.byId.values()].sort((a, b) => a.ts - b.ts);
|
|
436
|
+
const msgs = arr.map(dkThreadMsg);
|
|
437
|
+
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
438
|
+
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
439
|
+
setEarlier((e) => e[peerId] === c.more ? e : Object.assign({}, e, { [peerId]: c.more }));
|
|
440
|
+
}, []);
|
|
441
|
+
const loadThread = React.useCallback(async (peerId, opts) => {
|
|
442
|
+
if (!peerId)
|
|
443
|
+
return;
|
|
444
|
+
if (opts && opts.reset)
|
|
445
|
+
delete cache.current[peerId];
|
|
446
|
+
if (inFlight.current.has(peerId))
|
|
395
447
|
return;
|
|
396
448
|
inFlight.current.add(peerId);
|
|
397
449
|
try {
|
|
398
|
-
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=
|
|
450
|
+
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=" + DK_PAGE);
|
|
399
451
|
const arr = d.chats && d.chats[peerId] || [];
|
|
400
452
|
await prefetchThreadFiles(arr);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
kbps: m.file.kbps
|
|
414
|
-
} : void 0,
|
|
415
|
-
// Pass the real state through. Collapsing everything that was not
|
|
416
|
-
// 'queued' to 'read' meant a message still in flight — or one that
|
|
417
|
-
// never went out at all — rendered as delivered.
|
|
418
|
-
status: m.dir === "out" ? m.status === "queued" || m.status === "sending" || m.status === "failed" || m.status === "sent" ? m.status : "read" : void 0,
|
|
419
|
-
error: m.error,
|
|
420
|
-
// false = went to a live session but nobody acknowledged it (one tick).
|
|
421
|
-
// Dropped here before, so that state could never render.
|
|
422
|
-
confirmed: m.confirmed,
|
|
423
|
-
// Delivery path: "online" = live session, "offline" = express relay.
|
|
424
|
-
// Both directions carry it now; the bubble labels them the same way.
|
|
425
|
-
via: m.via,
|
|
426
|
-
// Brief 30: a validated component form, already whitelisted host-side.
|
|
427
|
-
// Passed through untouched — the renderer treats every string as text,
|
|
428
|
-
// never markup.
|
|
429
|
-
components: m.components
|
|
430
|
-
}));
|
|
431
|
-
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
432
|
-
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
453
|
+
let c = cache.current[peerId];
|
|
454
|
+
if (!c)
|
|
455
|
+
c = cache.current[peerId] = { byId: /* @__PURE__ */ new Map(), more: arr.length >= DK_PAGE };
|
|
456
|
+
if (arr.length) {
|
|
457
|
+
const from = arr[0].ts;
|
|
458
|
+
for (const [k, m] of c.byId)
|
|
459
|
+
if (m.ts >= from)
|
|
460
|
+
c.byId.delete(k);
|
|
461
|
+
}
|
|
462
|
+
for (const m of arr)
|
|
463
|
+
c.byId.set(dkMsgKey(m), m);
|
|
464
|
+
publish(peerId);
|
|
433
465
|
} catch (e) {
|
|
434
466
|
} finally {
|
|
435
467
|
inFlight.current.delete(peerId);
|
|
436
468
|
}
|
|
437
|
-
}, []);
|
|
438
|
-
|
|
469
|
+
}, [publish]);
|
|
470
|
+
const loadEarlier = React.useCallback(async (peerId) => {
|
|
471
|
+
const c = cache.current[peerId];
|
|
472
|
+
if (!c || !c.more || c.loadingEarlier)
|
|
473
|
+
return;
|
|
474
|
+
let oldest = Infinity;
|
|
475
|
+
for (const m of c.byId.values())
|
|
476
|
+
if (m.ts < oldest)
|
|
477
|
+
oldest = m.ts;
|
|
478
|
+
if (!Number.isFinite(oldest))
|
|
479
|
+
return;
|
|
480
|
+
c.loadingEarlier = true;
|
|
481
|
+
try {
|
|
482
|
+
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=" + DK_PAGE + "&before=" + oldest);
|
|
483
|
+
const arr = d.chats && d.chats[peerId] || [];
|
|
484
|
+
await prefetchThreadFiles(arr);
|
|
485
|
+
if (cache.current[peerId] !== c)
|
|
486
|
+
return;
|
|
487
|
+
for (const m of arr)
|
|
488
|
+
c.byId.set(dkMsgKey(m), m);
|
|
489
|
+
c.more = arr.length >= DK_PAGE;
|
|
490
|
+
publish(peerId);
|
|
491
|
+
} catch (e) {
|
|
492
|
+
} finally {
|
|
493
|
+
c.loadingEarlier = false;
|
|
494
|
+
}
|
|
495
|
+
}, [publish]);
|
|
496
|
+
return Object.assign({}, snap, { threads, earlier, refresh, loadThread, loadEarlier });
|
|
439
497
|
}
|
|
440
498
|
Object.assign(window, {
|
|
441
499
|
dkHash,
|
|
@@ -448,6 +506,564 @@ Object.assign(window, {
|
|
|
448
506
|
dkCopy
|
|
449
507
|
});
|
|
450
508
|
|
|
509
|
+
// src/assistant/actions.json
|
|
510
|
+
var actions_default = {
|
|
511
|
+
version: 1,
|
|
512
|
+
scope: "assistant",
|
|
513
|
+
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.",
|
|
514
|
+
actions: [
|
|
515
|
+
{
|
|
516
|
+
name: "help",
|
|
517
|
+
usage: "/help",
|
|
518
|
+
summary: "List what the Assistant can do.",
|
|
519
|
+
params: { type: "object", properties: {}, required: [] },
|
|
520
|
+
confirm: "none",
|
|
521
|
+
scope: "assistant"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
name: "friends-list",
|
|
525
|
+
usage: "/friends list [filter\u2026]",
|
|
526
|
+
summary: "Show friends as a table: name, last heard from, messages in/out. With no filter, everyone.",
|
|
527
|
+
params: {
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: {
|
|
530
|
+
filters: {
|
|
531
|
+
type: "array",
|
|
532
|
+
items: { enum: ["never-messaged", "never-replied", "silent", "pending"] },
|
|
533
|
+
description: "Several filters are OR'd together."
|
|
534
|
+
},
|
|
535
|
+
silentDays: { type: "integer", minimum: 1, default: 90, description: "Argument of the silent filter: nothing from them in this many days." }
|
|
536
|
+
},
|
|
537
|
+
required: []
|
|
538
|
+
},
|
|
539
|
+
confirm: "none",
|
|
540
|
+
scope: "assistant"
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: "friends-remove",
|
|
544
|
+
usage: "/friends remove <filter\u2026>",
|
|
545
|
+
summary: "Same table with a tick per row and a Remove button. Nothing is removed by typing alone.",
|
|
546
|
+
params: {
|
|
547
|
+
type: "object",
|
|
548
|
+
properties: {
|
|
549
|
+
filters: {
|
|
550
|
+
type: "array",
|
|
551
|
+
items: { enum: ["never-messaged", "never-replied", "silent", "pending"] },
|
|
552
|
+
minItems: 1
|
|
553
|
+
},
|
|
554
|
+
silentDays: { type: "integer", minimum: 1, default: 90 }
|
|
555
|
+
},
|
|
556
|
+
required: ["filters"]
|
|
557
|
+
},
|
|
558
|
+
confirm: "list",
|
|
559
|
+
scope: "assistant"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: "find",
|
|
563
|
+
usage: "/find <text>",
|
|
564
|
+
summary: "Friends whose name, alias, description or ens name contains the text.",
|
|
565
|
+
params: {
|
|
566
|
+
type: "object",
|
|
567
|
+
properties: { text: { type: "string", minLength: 1 } },
|
|
568
|
+
required: ["text"]
|
|
569
|
+
},
|
|
570
|
+
confirm: "none",
|
|
571
|
+
scope: "assistant"
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: "rename",
|
|
575
|
+
usage: "/rename <friend> <new name>",
|
|
576
|
+
summary: "Set a friend's alias \u2014 what you see them as, on this device.",
|
|
577
|
+
params: {
|
|
578
|
+
type: "object",
|
|
579
|
+
properties: {
|
|
580
|
+
friend: { type: "string", description: "Matches name, alias or userid prefix. Ambiguous = nothing happens, the candidates are listed." },
|
|
581
|
+
alias: { type: "string", minLength: 1 }
|
|
582
|
+
},
|
|
583
|
+
required: ["friend", "alias"]
|
|
584
|
+
},
|
|
585
|
+
confirm: "none",
|
|
586
|
+
scope: "assistant"
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
name: "mute",
|
|
590
|
+
usage: "/mute <friend>",
|
|
591
|
+
summary: "Stop alerts from one conversation.",
|
|
592
|
+
params: {
|
|
593
|
+
type: "object",
|
|
594
|
+
properties: { friend: { type: "string" } },
|
|
595
|
+
required: ["friend"]
|
|
596
|
+
},
|
|
597
|
+
confirm: "none",
|
|
598
|
+
scope: "assistant"
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: "unmute",
|
|
602
|
+
usage: "/unmute <friend>",
|
|
603
|
+
summary: "Alerts from that conversation again.",
|
|
604
|
+
params: {
|
|
605
|
+
type: "object",
|
|
606
|
+
properties: { friend: { type: "string" } },
|
|
607
|
+
required: ["friend"]
|
|
608
|
+
},
|
|
609
|
+
confirm: "none",
|
|
610
|
+
scope: "assistant"
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: "sound",
|
|
614
|
+
usage: "/sound on|off",
|
|
615
|
+
summary: "Sound on a new message.",
|
|
616
|
+
params: {
|
|
617
|
+
type: "object",
|
|
618
|
+
properties: { on: { type: "boolean" } },
|
|
619
|
+
required: ["on"]
|
|
620
|
+
},
|
|
621
|
+
confirm: "none",
|
|
622
|
+
scope: "assistant"
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: "flash",
|
|
626
|
+
usage: "/flash on|off",
|
|
627
|
+
summary: "Flash the window on a new message.",
|
|
628
|
+
params: {
|
|
629
|
+
type: "object",
|
|
630
|
+
properties: { on: { type: "boolean" } },
|
|
631
|
+
required: ["on"]
|
|
632
|
+
},
|
|
633
|
+
confirm: "none",
|
|
634
|
+
scope: "assistant"
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: "status",
|
|
638
|
+
usage: "/status",
|
|
639
|
+
summary: "Friends, pending requests, how far back the history on this device goes, app version.",
|
|
640
|
+
params: { type: "object", properties: {}, required: [] },
|
|
641
|
+
confirm: "none",
|
|
642
|
+
scope: "assistant"
|
|
643
|
+
}
|
|
644
|
+
],
|
|
645
|
+
filters: [
|
|
646
|
+
{
|
|
647
|
+
name: "never-messaged",
|
|
648
|
+
usage: "never-messaged",
|
|
649
|
+
summary: "No message either way.",
|
|
650
|
+
guarded: true
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "never-replied",
|
|
654
|
+
usage: "never-replied",
|
|
655
|
+
summary: "You wrote at least once, they never did.",
|
|
656
|
+
guarded: true
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: "silent",
|
|
660
|
+
usage: "silent [days]",
|
|
661
|
+
summary: "Nothing from them for this many days (default 90).",
|
|
662
|
+
guarded: true,
|
|
663
|
+
arg: { name: "days", type: "integer", default: 90, minimum: 1 }
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: "pending",
|
|
667
|
+
usage: "pending",
|
|
668
|
+
summary: "A request you sent that was never accepted.",
|
|
669
|
+
guarded: false
|
|
670
|
+
}
|
|
671
|
+
],
|
|
672
|
+
dropped: [
|
|
673
|
+
{
|
|
674
|
+
name: "never-called",
|
|
675
|
+
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."
|
|
676
|
+
}
|
|
677
|
+
],
|
|
678
|
+
guard: {
|
|
679
|
+
recentDays: 14,
|
|
680
|
+
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."
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// src/assistant/filters.js
|
|
685
|
+
var ASSISTANT_ID = "__assistant__";
|
|
686
|
+
var DAY_MS = 864e5;
|
|
687
|
+
var RECENT_DAYS = 14;
|
|
688
|
+
var DEFAULT_SILENT_DAYS = 90;
|
|
689
|
+
var FILTER_NAMES = ["never-messaged", "never-replied", "silent", "pending"];
|
|
690
|
+
var GUARDED = /* @__PURE__ */ new Set(["never-messaged", "never-replied", "silent"]);
|
|
691
|
+
function parseCommand(line) {
|
|
692
|
+
const text = String(line != null ? line : "").trim();
|
|
693
|
+
if (!text.startsWith("/"))
|
|
694
|
+
return null;
|
|
695
|
+
const parts = text.slice(1).split(/\s+/).filter(Boolean);
|
|
696
|
+
if (!parts.length)
|
|
697
|
+
return null;
|
|
698
|
+
const head = parts[0].toLowerCase();
|
|
699
|
+
if (head === "friends") {
|
|
700
|
+
const sub = (parts[1] || "").toLowerCase();
|
|
701
|
+
if (sub === "list" || sub === "remove")
|
|
702
|
+
return { ok: true, name: `friends-${sub}`, args: parts.slice(2) };
|
|
703
|
+
return { ok: false, name: "friends", args: parts.slice(1), error: "friends-subcommand" };
|
|
704
|
+
}
|
|
705
|
+
return { ok: true, name: head, args: parts.slice(1) };
|
|
706
|
+
}
|
|
707
|
+
function parseFilters(args) {
|
|
708
|
+
const filters = [];
|
|
709
|
+
const unknown = [];
|
|
710
|
+
let silentDays = DEFAULT_SILENT_DAYS;
|
|
711
|
+
const words = (args != null ? args : []).map((a) => String(a).toLowerCase());
|
|
712
|
+
for (let i = 0; i < words.length; i++) {
|
|
713
|
+
const w = words[i];
|
|
714
|
+
if (!FILTER_NAMES.includes(w)) {
|
|
715
|
+
unknown.push(words[i]);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (w === "silent") {
|
|
719
|
+
const n = Number(words[i + 1]);
|
|
720
|
+
if (Number.isInteger(n) && n > 0) {
|
|
721
|
+
silentDays = n;
|
|
722
|
+
i++;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (!filters.includes(w))
|
|
726
|
+
filters.push(w);
|
|
727
|
+
}
|
|
728
|
+
return { filters, silentDays, unknown };
|
|
729
|
+
}
|
|
730
|
+
var acceptedOrRequestedAt = (f) => f.acceptedAt || f.requestedAt || 0;
|
|
731
|
+
function isRecent(friend, now, recentDays = RECENT_DAYS) {
|
|
732
|
+
const at = acceptedOrRequestedAt(friend);
|
|
733
|
+
return at > 0 && now - at < recentDays * DAY_MS;
|
|
734
|
+
}
|
|
735
|
+
function matchesFilter(friend, filter, { now, silentDays = DEFAULT_SILENT_DAYS } = {}) {
|
|
736
|
+
const inCount = friend.inboundCount || 0;
|
|
737
|
+
const outCount = friend.outboundCount || 0;
|
|
738
|
+
switch (filter) {
|
|
739
|
+
case "never-messaged":
|
|
740
|
+
return inCount === 0 && outCount === 0;
|
|
741
|
+
case "never-replied":
|
|
742
|
+
return outCount > 0 && inCount === 0;
|
|
743
|
+
case "silent":
|
|
744
|
+
return !friend.lastInboundTs || now - friend.lastInboundTs > silentDays * DAY_MS;
|
|
745
|
+
case "pending":
|
|
746
|
+
return friend.pending === true;
|
|
747
|
+
default:
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function selectFriends(friends, filters, { now = Date.now(), silentDays = DEFAULT_SILENT_DAYS, recentDays = RECENT_DAYS } = {}) {
|
|
752
|
+
const pool = (friends != null ? friends : []).filter((f) => f && f.userid !== ASSISTANT_ID && !f.pinned);
|
|
753
|
+
if (!filters || !filters.length)
|
|
754
|
+
return { matched: pool.slice(), skippedRecent: [] };
|
|
755
|
+
const matched = [];
|
|
756
|
+
const skippedRecent = [];
|
|
757
|
+
for (const f of pool) {
|
|
758
|
+
let hit = false;
|
|
759
|
+
let guardedOnly = false;
|
|
760
|
+
for (const filter of filters) {
|
|
761
|
+
if (!matchesFilter(f, filter, { now, silentDays }))
|
|
762
|
+
continue;
|
|
763
|
+
if (GUARDED.has(filter) && isRecent(f, now, recentDays)) {
|
|
764
|
+
guardedOnly = true;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
hit = true;
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
if (hit)
|
|
771
|
+
matched.push(f);
|
|
772
|
+
else if (guardedOnly)
|
|
773
|
+
skippedRecent.push(f);
|
|
774
|
+
}
|
|
775
|
+
return { matched, skippedRecent };
|
|
776
|
+
}
|
|
777
|
+
function findFriends(friends, text) {
|
|
778
|
+
const q = String(text != null ? text : "").trim().toLowerCase();
|
|
779
|
+
if (!q)
|
|
780
|
+
return [];
|
|
781
|
+
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)));
|
|
782
|
+
}
|
|
783
|
+
function resolveFriend(friends, token) {
|
|
784
|
+
const q = String(token != null ? token : "").trim().toLowerCase();
|
|
785
|
+
if (!q)
|
|
786
|
+
return { match: null, candidates: [] };
|
|
787
|
+
const pool = (friends != null ? friends : []).filter((f) => f && f.userid !== ASSISTANT_ID);
|
|
788
|
+
const exact = pool.filter((f) => [f.name, f.alias].some((v) => typeof v === "string" && v.toLowerCase() === q));
|
|
789
|
+
if (exact.length === 1)
|
|
790
|
+
return { match: exact[0], candidates: exact };
|
|
791
|
+
if (exact.length > 1)
|
|
792
|
+
return { match: null, candidates: exact };
|
|
793
|
+
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));
|
|
794
|
+
if (partial.length === 1)
|
|
795
|
+
return { match: partial[0], candidates: partial };
|
|
796
|
+
return { match: null, candidates: partial };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/assistant.jsx
|
|
800
|
+
var ASSISTANT_PEER = {
|
|
801
|
+
id: ASSISTANT_ID,
|
|
802
|
+
userId: ASSISTANT_ID,
|
|
803
|
+
alias: "Beagle Assistant",
|
|
804
|
+
address: "",
|
|
805
|
+
online: true,
|
|
806
|
+
app: true,
|
|
807
|
+
pinned: true,
|
|
808
|
+
agent: false,
|
|
809
|
+
pending: false,
|
|
810
|
+
via: null,
|
|
811
|
+
ping: null,
|
|
812
|
+
ip: "",
|
|
813
|
+
unread: 0,
|
|
814
|
+
lastMsg: "",
|
|
815
|
+
lastTime: "",
|
|
816
|
+
lastTs: 0,
|
|
817
|
+
lastDir: null,
|
|
818
|
+
acceptedAt: 0,
|
|
819
|
+
requestedAt: 0,
|
|
820
|
+
wire: "163",
|
|
821
|
+
avatarUrl: null,
|
|
822
|
+
punkId: null
|
|
823
|
+
};
|
|
824
|
+
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" });
|
|
825
|
+
var fmtAgo = (ts, now, lang) => {
|
|
826
|
+
if (!ts)
|
|
827
|
+
return lang === "zh" ? "\u4ECE\u672A" : "never";
|
|
828
|
+
const days = Math.floor((now - ts) / 864e5);
|
|
829
|
+
if (days <= 0)
|
|
830
|
+
return lang === "zh" ? "\u4ECA\u5929" : "today";
|
|
831
|
+
if (days === 1)
|
|
832
|
+
return lang === "zh" ? "1 \u5929\u524D" : "1 day ago";
|
|
833
|
+
return lang === "zh" ? `${days} \u5929\u524D` : `${days} days ago`;
|
|
834
|
+
};
|
|
835
|
+
var nameOf = (f) => f.alias || f.name || f.userid;
|
|
836
|
+
function sinceLine(historySince, lang) {
|
|
837
|
+
if (!historySince)
|
|
838
|
+
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)_";
|
|
839
|
+
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)}_`;
|
|
840
|
+
}
|
|
841
|
+
function friendTable(rows, now, lang) {
|
|
842
|
+
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");
|
|
843
|
+
}
|
|
844
|
+
function helpText(lang) {
|
|
845
|
+
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:";
|
|
846
|
+
const cmds = actions_default.actions.map((a) => `- \`${a.usage}\` \u2014 ${a.summary}`).join("\n");
|
|
847
|
+
const filters = actions_default.filters.map((f) => `- \`${f.usage}\` \u2014 ${f.summary}`).join("\n");
|
|
848
|
+
const tail = lang === "zh" ? `\u7B5B\u9009\u6761\u4EF6\uFF08\u53EF\u7EC4\u5408\uFF0C\u53D6\u5E76\u96C6\uFF09\uFF1A
|
|
849
|
+
${filters}
|
|
850
|
+
|
|
851
|
+
\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):
|
|
852
|
+
${filters}
|
|
853
|
+
|
|
854
|
+
Friends added in the last ${RECENT_DAYS} days are never matched by the "never/silent" filters. Removal always asks first.`;
|
|
855
|
+
return `${intro}
|
|
856
|
+
${cmds}
|
|
857
|
+
|
|
858
|
+
${tail}`;
|
|
859
|
+
}
|
|
860
|
+
var seq = 0;
|
|
861
|
+
var asMsg = (from, text, extra) => Object.assign({
|
|
862
|
+
id: `as-${Date.now()}-${seq++}`,
|
|
863
|
+
from,
|
|
864
|
+
time: (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
|
865
|
+
text
|
|
866
|
+
}, extra);
|
|
867
|
+
function AssistantPanel({ m, T, lang, onRemove }) {
|
|
868
|
+
const rows = m.assistant.rows || [];
|
|
869
|
+
const [sel, setSel] = React.useState(() => new Set(rows.map((r) => r.userid)));
|
|
870
|
+
const [busy, setBusy] = React.useState(false);
|
|
871
|
+
const [done, setDone] = React.useState(null);
|
|
872
|
+
if (!rows.length)
|
|
873
|
+
return null;
|
|
874
|
+
const toggle = (id) => setSel((s) => {
|
|
875
|
+
const n = new Set(s);
|
|
876
|
+
n.has(id) ? n.delete(id) : n.add(id);
|
|
877
|
+
return n;
|
|
878
|
+
});
|
|
879
|
+
const label = lang === "zh" ? `\u5220\u9664 ${sel.size} \u4F4D` : `Remove ${sel.size}`;
|
|
880
|
+
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: {
|
|
881
|
+
display: "flex",
|
|
882
|
+
alignItems: "center",
|
|
883
|
+
gap: 8,
|
|
884
|
+
padding: "7px 10px",
|
|
885
|
+
borderBottom: "1px solid var(--line)",
|
|
886
|
+
fontSize: 13,
|
|
887
|
+
cursor: done ? "default" : "pointer"
|
|
888
|
+
} }, /* @__PURE__ */ React.createElement(
|
|
889
|
+
"input",
|
|
890
|
+
{
|
|
891
|
+
type: "checkbox",
|
|
892
|
+
checked: sel.has(r.userid),
|
|
893
|
+
disabled: !!done || busy,
|
|
894
|
+
onChange: () => toggle(r.userid)
|
|
895
|
+
}
|
|
896
|
+
), /* @__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: () => {
|
|
897
|
+
setBusy(true);
|
|
898
|
+
onRemove([...sel]).then((r) => {
|
|
899
|
+
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` : ""}`);
|
|
900
|
+
}).finally(() => setBusy(false));
|
|
901
|
+
} }, 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"))));
|
|
902
|
+
}
|
|
903
|
+
function useAssistant({ lang, T, muted, onToggleMute, sound, flash, setAlert, onReloadFriends }) {
|
|
904
|
+
const [thread, setThread] = React.useState(() => [{ day: (/* @__PURE__ */ new Date()).toLocaleDateString() }, asMsg("them", helpText(lang))]);
|
|
905
|
+
const append = React.useCallback((...ms) => setThread((t) => t.concat(ms)), []);
|
|
906
|
+
React.useEffect(() => {
|
|
907
|
+
setThread((t) => t.length === 2 ? [t[0], asMsg("them", helpText(lang))] : t);
|
|
908
|
+
}, [lang]);
|
|
909
|
+
const fetchFriends = async () => {
|
|
910
|
+
const r = await fetch("/api/assistant/friends").then((x) => x.json());
|
|
911
|
+
return { friends: r.friends || [], historySince: r.historySince || null, now: r.now || Date.now(), ver: r.beagleVer || "" };
|
|
912
|
+
};
|
|
913
|
+
const listReply = (rows, skipped, data, title) => {
|
|
914
|
+
if (!rows.length) {
|
|
915
|
+
return `${title}
|
|
916
|
+
|
|
917
|
+
${lang === "zh" ? "\u6CA1\u6709\u7B26\u5408\u7684\u597D\u53CB\u3002" : "No friends match."}
|
|
918
|
+
|
|
919
|
+
${sinceLine(data.historySince, lang)}`;
|
|
920
|
+
}
|
|
921
|
+
const skip = skipped.length ? lang === "zh" ? `
|
|
922
|
+
|
|
923
|
+
_\u8DF3\u8FC7\u4E86 ${skipped.length} \u4F4D\u6700\u8FD1 ${RECENT_DAYS} \u5929\u5185\u6DFB\u52A0\u7684\u597D\u53CB\u3002_` : `
|
|
924
|
+
|
|
925
|
+
_${skipped.length} friend${skipped.length > 1 ? "s" : ""} added in the last ${RECENT_DAYS} days ${skipped.length > 1 ? "were" : "was"} skipped._` : "";
|
|
926
|
+
return `${title}
|
|
927
|
+
|
|
928
|
+
${friendTable(rows, data.now, lang)}${skip}
|
|
929
|
+
|
|
930
|
+
${sinceLine(data.historySince, lang)}`;
|
|
931
|
+
};
|
|
932
|
+
const ambiguous = (cands) => lang === "zh" ? `\u6709 ${cands.length} \u4F4D\u597D\u53CB\u7B26\u5408\uFF0C\u8BF7\u8BF4\u5F97\u66F4\u5177\u4F53\u4E00\u4E9B\uFF1A
|
|
933
|
+
${cands.map((c) => `- ${nameOf(c)}`).join("\n")}` : `That matches ${cands.length} friends \u2014 be more specific:
|
|
934
|
+
${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
935
|
+
const notFound = (tok) => lang === "zh" ? `\u6CA1\u6709\u627E\u5230\u597D\u53CB "${tok}"\u3002` : `No friend matches "${tok}".`;
|
|
936
|
+
const handle = React.useCallback(async (text) => {
|
|
937
|
+
append(asMsg("me", text, { status: "read" }));
|
|
938
|
+
const cmd = parseCommand(text);
|
|
939
|
+
if (!cmd) {
|
|
940
|
+
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."));
|
|
941
|
+
}
|
|
942
|
+
if (cmd.ok === false) {
|
|
943
|
+
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`."));
|
|
944
|
+
}
|
|
945
|
+
try {
|
|
946
|
+
switch (cmd.name) {
|
|
947
|
+
case "help":
|
|
948
|
+
return append(asMsg("them", helpText(lang)));
|
|
949
|
+
case "status": {
|
|
950
|
+
const d = await fetchFriends();
|
|
951
|
+
const pending = d.friends.filter((f) => f.pending).length;
|
|
952
|
+
return append(asMsg("them", lang === "zh" ? `**\u72B6\u6001**
|
|
953
|
+
|
|
954
|
+
- \u597D\u53CB\uFF1A${d.friends.length}
|
|
955
|
+
- \u5F85\u5BF9\u65B9\u63A5\u53D7\uFF1A${pending}
|
|
956
|
+
- \u672C\u673A\u5386\u53F2\u8D77\u81EA\uFF1A${fmtDate(d.historySince, lang)}
|
|
957
|
+
- \u7248\u672C\uFF1A${d.ver}` : `**Status**
|
|
958
|
+
|
|
959
|
+
- Friends: ${d.friends.length}
|
|
960
|
+
- Waiting to be accepted: ${pending}
|
|
961
|
+
- History on this device since: ${fmtDate(d.historySince, lang)}
|
|
962
|
+
- Version: ${d.ver}`));
|
|
963
|
+
}
|
|
964
|
+
case "friends-list": {
|
|
965
|
+
const { filters, silentDays, unknown } = parseFilters(cmd.args);
|
|
966
|
+
if (unknown.length) {
|
|
967
|
+
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(", ")}.`));
|
|
968
|
+
}
|
|
969
|
+
const d = await fetchFriends();
|
|
970
|
+
const { matched, skippedRecent } = selectFriends(d.friends, filters, { now: d.now, silentDays });
|
|
971
|
+
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}`;
|
|
972
|
+
return append(asMsg("them", listReply(matched, skippedRecent, d, title)));
|
|
973
|
+
}
|
|
974
|
+
case "friends-remove": {
|
|
975
|
+
const { filters, silentDays, unknown } = parseFilters(cmd.args);
|
|
976
|
+
if (unknown.length || !filters.length) {
|
|
977
|
+
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(", ")}.`));
|
|
978
|
+
}
|
|
979
|
+
const d = await fetchFriends();
|
|
980
|
+
const { matched, skippedRecent } = selectFriends(d.friends, filters, { now: d.now, silentDays });
|
|
981
|
+
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`;
|
|
982
|
+
return append(asMsg(
|
|
983
|
+
"them",
|
|
984
|
+
listReply(matched, skippedRecent, d, title),
|
|
985
|
+
matched.length ? { assistant: { kind: "confirm", rows: matched } } : void 0
|
|
986
|
+
));
|
|
987
|
+
}
|
|
988
|
+
case "find": {
|
|
989
|
+
const q = cmd.args.join(" ");
|
|
990
|
+
if (!q)
|
|
991
|
+
return append(asMsg("them", lang === "zh" ? "\u7528\u6CD5\uFF1A`/find <\u6587\u5B57>`" : "Usage: `/find <text>`"));
|
|
992
|
+
const d = await fetchFriends();
|
|
993
|
+
const rows = findFriends(d.friends, q);
|
|
994
|
+
const title = lang === "zh" ? `**"${q}"** \u2014 ${rows.length} \u4F4D` : `**"${q}"** \u2014 ${rows.length} found`;
|
|
995
|
+
return append(asMsg("them", listReply(rows, [], d, title)));
|
|
996
|
+
}
|
|
997
|
+
case "rename": {
|
|
998
|
+
const [tok, ...rest] = cmd.args;
|
|
999
|
+
const alias = rest.join(" ").trim();
|
|
1000
|
+
if (!tok || !alias)
|
|
1001
|
+
return append(asMsg("them", lang === "zh" ? "\u7528\u6CD5\uFF1A`/rename <\u597D\u53CB> <\u65B0\u540D\u5B57>`" : "Usage: `/rename <friend> <new name>`"));
|
|
1002
|
+
const d = await fetchFriends();
|
|
1003
|
+
const { match, candidates } = resolveFriend(d.friends, tok);
|
|
1004
|
+
if (!match)
|
|
1005
|
+
return append(asMsg("them", candidates.length ? ambiguous(candidates) : notFound(tok)));
|
|
1006
|
+
const was = nameOf(match);
|
|
1007
|
+
await dkApi.alias(match.userid, alias);
|
|
1008
|
+
if (onReloadFriends)
|
|
1009
|
+
onReloadFriends();
|
|
1010
|
+
return append(asMsg("them", lang === "zh" ? `\u5DF2\u628A "${was}" \u6539\u540D\u4E3A "${alias}"\u3002` : `Renamed "${was}" to "${alias}".`));
|
|
1011
|
+
}
|
|
1012
|
+
case "mute":
|
|
1013
|
+
case "unmute": {
|
|
1014
|
+
const tok = cmd.args.join(" ").trim();
|
|
1015
|
+
if (!tok)
|
|
1016
|
+
return append(asMsg("them", `${lang === "zh" ? "\u7528\u6CD5" : "Usage"}: \`/${cmd.name} <friend>\``));
|
|
1017
|
+
const d = await fetchFriends();
|
|
1018
|
+
const { match, candidates } = resolveFriend(d.friends, tok);
|
|
1019
|
+
if (!match)
|
|
1020
|
+
return append(asMsg("them", candidates.length ? ambiguous(candidates) : notFound(tok)));
|
|
1021
|
+
const isMuted = (muted || []).includes(match.userid);
|
|
1022
|
+
const want = cmd.name === "mute";
|
|
1023
|
+
if (isMuted !== want)
|
|
1024
|
+
onToggleMute(match.userid);
|
|
1025
|
+
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)}".`));
|
|
1026
|
+
}
|
|
1027
|
+
case "sound":
|
|
1028
|
+
case "flash": {
|
|
1029
|
+
const word = (cmd.args[0] || "").toLowerCase();
|
|
1030
|
+
if (word !== "on" && word !== "off") {
|
|
1031
|
+
return append(asMsg("them", `${lang === "zh" ? "\u7528\u6CD5" : "Usage"}: \`/${cmd.name} on|off\``));
|
|
1032
|
+
}
|
|
1033
|
+
const on = word === "on";
|
|
1034
|
+
setAlert(cmd.name, on);
|
|
1035
|
+
const what = cmd.name === "sound" ? lang === "zh" ? "\u63D0\u793A\u97F3" : "Sound" : lang === "zh" ? "\u7A97\u53E3\u95EA\u70C1" : "Window flash";
|
|
1036
|
+
return append(asMsg("them", lang === "zh" ? `${what}\u5DF2${on ? "\u5F00\u542F" : "\u5173\u95ED"}\u3002` : `${what} ${on ? "on" : "off"}.`));
|
|
1037
|
+
}
|
|
1038
|
+
default:
|
|
1039
|
+
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.`));
|
|
1040
|
+
}
|
|
1041
|
+
} catch (e) {
|
|
1042
|
+
return append(asMsg("them", lang === "zh" ? `\u51FA\u9519\u4E86\uFF1A${e && e.message || e}` : `That failed: ${e && e.message || e}`));
|
|
1043
|
+
}
|
|
1044
|
+
}, [append, lang, muted, onToggleMute, setAlert, onReloadFriends]);
|
|
1045
|
+
const removeFriends = React.useCallback(async (ids) => {
|
|
1046
|
+
let removed = 0;
|
|
1047
|
+
const failed = [];
|
|
1048
|
+
for (const id of ids) {
|
|
1049
|
+
try {
|
|
1050
|
+
const r = await dkApi.remove(id);
|
|
1051
|
+
if (r && r.ok === false)
|
|
1052
|
+
failed.push(id);
|
|
1053
|
+
else
|
|
1054
|
+
removed++;
|
|
1055
|
+
} catch (e) {
|
|
1056
|
+
failed.push(id);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (onReloadFriends)
|
|
1060
|
+
onReloadFriends();
|
|
1061
|
+
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"}.`));
|
|
1062
|
+
return { removed, failed: failed.length };
|
|
1063
|
+
}, [append, lang, onReloadFriends]);
|
|
1064
|
+
return { thread, handle, removeFriends };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
451
1067
|
// src/chat.jsx
|
|
452
1068
|
function RequestsBlock({ T, requests, onAct }) {
|
|
453
1069
|
const [acted, setActed] = React.useState({});
|
|
@@ -529,7 +1145,7 @@ function PeerRow({ peer, T, active, onClick, muted }) {
|
|
|
529
1145
|
whiteSpace: "nowrap",
|
|
530
1146
|
overflow: "hidden",
|
|
531
1147
|
textOverflow: "ellipsis"
|
|
532
|
-
} }, 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: {
|
|
1148
|
+
} }, 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: {
|
|
533
1149
|
fontSize: 11,
|
|
534
1150
|
color: "var(--dim)",
|
|
535
1151
|
whiteSpace: "nowrap",
|
|
@@ -585,6 +1201,8 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, pre
|
|
|
585
1201
|
const s = (p.alias || "") + " " + p.userId + " " + p.ip;
|
|
586
1202
|
return s.toLowerCase().includes(q.toLowerCase());
|
|
587
1203
|
}).slice().sort((a, b) => {
|
|
1204
|
+
if (!!a.pinned !== !!b.pinned)
|
|
1205
|
+
return a.pinned ? -1 : 1;
|
|
588
1206
|
const ta = dkActivityTs(a), tb = dkActivityTs(b);
|
|
589
1207
|
if (ta !== tb)
|
|
590
1208
|
return tb - ta;
|
|
@@ -1250,7 +1868,7 @@ function DkChatForm({ form, submitted, peer, onSubmit }) {
|
|
|
1250
1868
|
return null;
|
|
1251
1869
|
}), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
|
|
1252
1870
|
}
|
|
1253
|
-
function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, answered, selMode, selected, onToggleSel, busy, isGroup }) {
|
|
1871
|
+
function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, answered, selMode, selected, onToggleSel, busy, isGroup }) {
|
|
1254
1872
|
const mine = m.from === "me";
|
|
1255
1873
|
const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
|
|
1256
1874
|
const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
|
|
@@ -1374,6 +1992,8 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onRetryMsg, o
|
|
|
1374
1992
|
{
|
|
1375
1993
|
src: dkFileUrl(m.file.name),
|
|
1376
1994
|
alt: m.file.name,
|
|
1995
|
+
loading: "lazy",
|
|
1996
|
+
decoding: "async",
|
|
1377
1997
|
onError: (e) => {
|
|
1378
1998
|
const w = e.currentTarget.closest("[data-media]");
|
|
1379
1999
|
if (w)
|
|
@@ -1577,7 +2197,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onRetryMsg, o
|
|
|
1577
2197
|
letterSpacing: -0.1,
|
|
1578
2198
|
wordBreak: "break-word"
|
|
1579
2199
|
} }, /* @__PURE__ */ React.createElement(MarkdownText, { text: read.text, onNameCard })));
|
|
1580
|
-
})(), 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), (() => {
|
|
2200
|
+
})(), 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), (() => {
|
|
1581
2201
|
const sentDone = mine && m.status && m.status !== "queued" && m.status !== "sending" && m.status !== "failed";
|
|
1582
2202
|
const via = !mine ? m.via : sentDone ? m.via === "offline" ? "offline" : "online" : null;
|
|
1583
2203
|
if (!via)
|
|
@@ -1622,8 +2242,10 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onRetryMsg, o
|
|
|
1622
2242
|
) : 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)" })))
|
|
1623
2243
|
);
|
|
1624
2244
|
}
|
|
1625
|
-
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted, onToggleMute, phone, onBack }) {
|
|
2245
|
+
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted, onToggleMute, phone, onBack }) {
|
|
1626
2246
|
const scrollRef = React.useRef(null);
|
|
2247
|
+
const prependRef = React.useRef(null);
|
|
2248
|
+
const [earlierBusy, setEarlierBusy] = React.useState(false);
|
|
1627
2249
|
const isGroup = React.useMemo(() => dkThreadIsGroup(threadProp), [threadProp]);
|
|
1628
2250
|
const followScrollRef = React.useRef(true);
|
|
1629
2251
|
const fileRef = React.useRef(null);
|
|
@@ -1701,9 +2323,30 @@ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, on
|
|
|
1701
2323
|
followScrollRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
|
1702
2324
|
};
|
|
1703
2325
|
React.useLayoutEffect(() => {
|
|
2326
|
+
const el = scrollRef.current;
|
|
2327
|
+
const pre = prependRef.current;
|
|
2328
|
+
if (pre && el && el.scrollHeight !== pre.h) {
|
|
2329
|
+
el.scrollTop = pre.top + (el.scrollHeight - pre.h);
|
|
2330
|
+
prependRef.current = null;
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
1704
2333
|
if (followScrollRef.current)
|
|
1705
2334
|
scrollToBottom();
|
|
1706
2335
|
}, [peer.id, thread]);
|
|
2336
|
+
React.useEffect(() => {
|
|
2337
|
+
prependRef.current = null;
|
|
2338
|
+
setEarlierBusy(false);
|
|
2339
|
+
}, [peer.id]);
|
|
2340
|
+
const loadEarlier = () => {
|
|
2341
|
+
const el = scrollRef.current;
|
|
2342
|
+
if (!onLoadEarlier || earlierBusy)
|
|
2343
|
+
return;
|
|
2344
|
+
if (el)
|
|
2345
|
+
prependRef.current = { h: el.scrollHeight, top: el.scrollTop };
|
|
2346
|
+
followScrollRef.current = false;
|
|
2347
|
+
setEarlierBusy(true);
|
|
2348
|
+
Promise.resolve(onLoadEarlier()).finally(() => setEarlierBusy(false));
|
|
2349
|
+
};
|
|
1707
2350
|
const toggleSel = (id) => setSel((s) => {
|
|
1708
2351
|
const n = new Set(s);
|
|
1709
2352
|
n.has(id) ? n.delete(id) : n.add(id);
|
|
@@ -1904,7 +2547,7 @@ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, on
|
|
|
1904
2547
|
style: { width: 40, height: 44, flexShrink: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", padding: 0 }
|
|
1905
2548
|
},
|
|
1906
2549
|
/* @__PURE__ */ React.createElement(Icon, { name: "chevronLeft", size: 26, stroke: 2.2, color: "var(--accent)" })
|
|
1907
|
-
), /* @__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(
|
|
2550
|
+
), /* @__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(
|
|
1908
2551
|
Mono,
|
|
1909
2552
|
{
|
|
1910
2553
|
size: 11.5,
|
|
@@ -1963,15 +2606,17 @@ ${peer.address}`
|
|
|
1963
2606
|
fontSize: 12.5,
|
|
1964
2607
|
color: "var(--text)"
|
|
1965
2608
|
} }, /* @__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.")),
|
|
1966
|
-
/* @__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(
|
|
2609
|
+
/* @__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(
|
|
1967
2610
|
Msg,
|
|
1968
2611
|
{
|
|
1969
2612
|
key: m.id || i,
|
|
1970
2613
|
m,
|
|
1971
2614
|
peer,
|
|
1972
2615
|
T,
|
|
2616
|
+
lang,
|
|
1973
2617
|
isGroup,
|
|
1974
2618
|
onTheater: setTheater,
|
|
2619
|
+
onAssistantRemove,
|
|
1975
2620
|
onDelete: (id) => doDelete([id]),
|
|
1976
2621
|
onCancel: doCancel,
|
|
1977
2622
|
onRetry: doRetry,
|
|
@@ -2123,12 +2768,12 @@ function MenuItem({ icon, label, onClick, danger }) {
|
|
|
2123
2768
|
function ChatEmpty({ T }) {
|
|
2124
2769
|
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));
|
|
2125
2770
|
}
|
|
2126
|
-
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 }) {
|
|
2771
|
+
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 }) {
|
|
2127
2772
|
const peer = peers.find((p) => p.id === activeId);
|
|
2128
2773
|
if (phone) {
|
|
2129
|
-
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 });
|
|
2774
|
+
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 });
|
|
2130
2775
|
}
|
|
2131
|
-
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 }));
|
|
2776
|
+
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 }));
|
|
2132
2777
|
}
|
|
2133
2778
|
Object.assign(window, { ChatTab });
|
|
2134
2779
|
|
|
@@ -4913,6 +5558,8 @@ var STR = {
|
|
|
4913
5558
|
autoAcceptLabel: "Auto-accept friend requests",
|
|
4914
5559
|
autoAcceptSub: "Off: requests wait in the sidebar for your approval.",
|
|
4915
5560
|
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.",
|
|
5561
|
+
loadEarlier: "Load earlier messages",
|
|
5562
|
+
loadingEarlier: "Loading\u2026",
|
|
4916
5563
|
pendingFriend: "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.",
|
|
4917
5564
|
cancelling: "Cancelling\u2026",
|
|
4918
5565
|
cancelled: "Transfer cancelled",
|
|
@@ -5105,6 +5752,8 @@ var STR = {
|
|
|
5105
5752
|
autoAcceptLabel: "\u81EA\u52A8\u63A5\u53D7\u597D\u53CB\u8BF7\u6C42",
|
|
5106
5753
|
autoAcceptSub: "\u5173\u95ED\u540E\uFF0C\u597D\u53CB\u8BF7\u6C42\u4F1A\u7B49\u5F85\u4F60\u5728\u4FA7\u8FB9\u680F\u624B\u52A8\u786E\u8BA4\u3002",
|
|
5107
5754
|
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",
|
|
5755
|
+
loadEarlier: "\u52A0\u8F7D\u66F4\u65E9\u7684\u6D88\u606F",
|
|
5756
|
+
loadingEarlier: "\u52A0\u8F7D\u4E2D\u2026",
|
|
5108
5757
|
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",
|
|
5109
5758
|
cancelling: "\u6B63\u5728\u53D6\u6D88\u2026",
|
|
5110
5759
|
cancelled: "\u5DF2\u53D6\u6D88\u4F20\u8F93",
|
|
@@ -5449,7 +6098,7 @@ function DkApp() {
|
|
|
5449
6098
|
sessionStorage.setItem(key, "1");
|
|
5450
6099
|
window.location.reload();
|
|
5451
6100
|
}, [me && me.beagleVer]);
|
|
5452
|
-
const peers = data.peers;
|
|
6101
|
+
const peers = React.useMemo(() => [ASSISTANT_PEER].concat(data.peers || []), [data.peers]);
|
|
5453
6102
|
const [pendingAddr, setPendingAddr] = React.useState(() => dkParseDeepLinkAddress());
|
|
5454
6103
|
React.useEffect(() => {
|
|
5455
6104
|
const onHash = () => {
|
|
@@ -5513,6 +6162,16 @@ function DkApp() {
|
|
|
5513
6162
|
flash: t.alertFlash !== false,
|
|
5514
6163
|
muted: mutedIds
|
|
5515
6164
|
});
|
|
6165
|
+
const assistant = useAssistant({
|
|
6166
|
+
lang: t.lang,
|
|
6167
|
+
T,
|
|
6168
|
+
muted: mutedIds,
|
|
6169
|
+
onToggleMute,
|
|
6170
|
+
sound: t.alertSound !== false,
|
|
6171
|
+
flash: t.alertFlash !== false,
|
|
6172
|
+
setAlert: (which, on) => setTweak(which === "sound" ? "alertSound" : "alertFlash", on),
|
|
6173
|
+
onReloadFriends: () => data.refresh()
|
|
6174
|
+
});
|
|
5516
6175
|
const callCtl = useCallController(me.userId, (peerId, video, direction) => {
|
|
5517
6176
|
const text = `WebRTC ${video ? "video" : "audio"} call: ${direction}`;
|
|
5518
6177
|
dkApi.logLocal(peerId, direction === "outgoing" ? "out" : "in", text).then(() => {
|
|
@@ -5575,7 +6234,9 @@ function DkApp() {
|
|
|
5575
6234
|
React.useEffect(() => {
|
|
5576
6235
|
if (!activeId)
|
|
5577
6236
|
return;
|
|
5578
|
-
|
|
6237
|
+
if (activeId === ASSISTANT_ID)
|
|
6238
|
+
return;
|
|
6239
|
+
data.loadThread(activeId, { reset: true });
|
|
5579
6240
|
dkApi.markRead(activeId).then(data.refresh);
|
|
5580
6241
|
const iv = setInterval(() => {
|
|
5581
6242
|
data.loadThread(activeId);
|
|
@@ -5618,6 +6279,10 @@ function DkApp() {
|
|
|
5618
6279
|
const onSend = (text) => {
|
|
5619
6280
|
if (!activeId || !text || !text.trim())
|
|
5620
6281
|
return;
|
|
6282
|
+
if (activeId === ASSISTANT_ID) {
|
|
6283
|
+
assistant.handle(text.trim());
|
|
6284
|
+
return;
|
|
6285
|
+
}
|
|
5621
6286
|
dkApi.send(activeId, text.trim()).then(() => data.loadThread(activeId)).then(data.refresh);
|
|
5622
6287
|
};
|
|
5623
6288
|
const peerTakesRtcFile = (id) => {
|
|
@@ -5725,7 +6390,7 @@ function DkApp() {
|
|
|
5725
6390
|
fontWeight: 600,
|
|
5726
6391
|
background: tab === n.id ? "var(--accent)" : "transparent",
|
|
5727
6392
|
color: tab === n.id ? "#fff" : "var(--dim)"
|
|
5728
|
-
} }, /* @__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) => {
|
|
6393
|
+
} }, /* @__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) => {
|
|
5729
6394
|
data.refresh();
|
|
5730
6395
|
return r;
|
|
5731
6396
|
}) })), 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/package.json
CHANGED