@decentnetwork/beagle-ui 0.2.20 → 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 +589 -11
- 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) ----
|
|
@@ -506,6 +506,564 @@ Object.assign(window, {
|
|
|
506
506
|
dkCopy
|
|
507
507
|
});
|
|
508
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
|
+
|
|
509
1067
|
// src/chat.jsx
|
|
510
1068
|
function RequestsBlock({ T, requests, onAct }) {
|
|
511
1069
|
const [acted, setActed] = React.useState({});
|
|
@@ -587,7 +1145,7 @@ function PeerRow({ peer, T, active, onClick, muted }) {
|
|
|
587
1145
|
whiteSpace: "nowrap",
|
|
588
1146
|
overflow: "hidden",
|
|
589
1147
|
textOverflow: "ellipsis"
|
|
590
|
-
} }, 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: {
|
|
591
1149
|
fontSize: 11,
|
|
592
1150
|
color: "var(--dim)",
|
|
593
1151
|
whiteSpace: "nowrap",
|
|
@@ -643,6 +1201,8 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, pre
|
|
|
643
1201
|
const s = (p.alias || "") + " " + p.userId + " " + p.ip;
|
|
644
1202
|
return s.toLowerCase().includes(q.toLowerCase());
|
|
645
1203
|
}).slice().sort((a, b) => {
|
|
1204
|
+
if (!!a.pinned !== !!b.pinned)
|
|
1205
|
+
return a.pinned ? -1 : 1;
|
|
646
1206
|
const ta = dkActivityTs(a), tb = dkActivityTs(b);
|
|
647
1207
|
if (ta !== tb)
|
|
648
1208
|
return tb - ta;
|
|
@@ -1308,7 +1868,7 @@ function DkChatForm({ form, submitted, peer, onSubmit }) {
|
|
|
1308
1868
|
return null;
|
|
1309
1869
|
}), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
|
|
1310
1870
|
}
|
|
1311
|
-
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 }) {
|
|
1312
1872
|
const mine = m.from === "me";
|
|
1313
1873
|
const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
|
|
1314
1874
|
const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
|
|
@@ -1637,7 +2197,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onRetryMsg, o
|
|
|
1637
2197
|
letterSpacing: -0.1,
|
|
1638
2198
|
wordBreak: "break-word"
|
|
1639
2199
|
} }, /* @__PURE__ */ React.createElement(MarkdownText, { text: read.text, onNameCard })));
|
|
1640
|
-
})(), 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), (() => {
|
|
1641
2201
|
const sentDone = mine && m.status && m.status !== "queued" && m.status !== "sending" && m.status !== "failed";
|
|
1642
2202
|
const via = !mine ? m.via : sentDone ? m.via === "offline" ? "offline" : "online" : null;
|
|
1643
2203
|
if (!via)
|
|
@@ -1682,7 +2242,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onRetryMsg, o
|
|
|
1682
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)" })))
|
|
1683
2243
|
);
|
|
1684
2244
|
}
|
|
1685
|
-
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, 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 }) {
|
|
1686
2246
|
const scrollRef = React.useRef(null);
|
|
1687
2247
|
const prependRef = React.useRef(null);
|
|
1688
2248
|
const [earlierBusy, setEarlierBusy] = React.useState(false);
|
|
@@ -1987,7 +2547,7 @@ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, on
|
|
|
1987
2547
|
style: { width: 40, height: 44, flexShrink: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", padding: 0 }
|
|
1988
2548
|
},
|
|
1989
2549
|
/* @__PURE__ */ React.createElement(Icon, { name: "chevronLeft", size: 26, stroke: 2.2, color: "var(--accent)" })
|
|
1990
|
-
), /* @__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(
|
|
1991
2551
|
Mono,
|
|
1992
2552
|
{
|
|
1993
2553
|
size: 11.5,
|
|
@@ -2053,8 +2613,10 @@ ${peer.address}`
|
|
|
2053
2613
|
m,
|
|
2054
2614
|
peer,
|
|
2055
2615
|
T,
|
|
2616
|
+
lang,
|
|
2056
2617
|
isGroup,
|
|
2057
2618
|
onTheater: setTheater,
|
|
2619
|
+
onAssistantRemove,
|
|
2058
2620
|
onDelete: (id) => doDelete([id]),
|
|
2059
2621
|
onCancel: doCancel,
|
|
2060
2622
|
onRetry: doRetry,
|
|
@@ -2206,12 +2768,12 @@ function MenuItem({ icon, label, onClick, danger }) {
|
|
|
2206
2768
|
function ChatEmpty({ T }) {
|
|
2207
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));
|
|
2208
2770
|
}
|
|
2209
|
-
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, 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 }) {
|
|
2210
2772
|
const peer = peers.find((p) => p.id === activeId);
|
|
2211
2773
|
if (phone) {
|
|
2212
|
-
return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, 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 });
|
|
2213
2775
|
}
|
|
2214
|
-
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, 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 }));
|
|
2215
2777
|
}
|
|
2216
2778
|
Object.assign(window, { ChatTab });
|
|
2217
2779
|
|
|
@@ -5536,7 +6098,7 @@ function DkApp() {
|
|
|
5536
6098
|
sessionStorage.setItem(key, "1");
|
|
5537
6099
|
window.location.reload();
|
|
5538
6100
|
}, [me && me.beagleVer]);
|
|
5539
|
-
const peers = data.peers;
|
|
6101
|
+
const peers = React.useMemo(() => [ASSISTANT_PEER].concat(data.peers || []), [data.peers]);
|
|
5540
6102
|
const [pendingAddr, setPendingAddr] = React.useState(() => dkParseDeepLinkAddress());
|
|
5541
6103
|
React.useEffect(() => {
|
|
5542
6104
|
const onHash = () => {
|
|
@@ -5600,6 +6162,16 @@ function DkApp() {
|
|
|
5600
6162
|
flash: t.alertFlash !== false,
|
|
5601
6163
|
muted: mutedIds
|
|
5602
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
|
+
});
|
|
5603
6175
|
const callCtl = useCallController(me.userId, (peerId, video, direction) => {
|
|
5604
6176
|
const text = `WebRTC ${video ? "video" : "audio"} call: ${direction}`;
|
|
5605
6177
|
dkApi.logLocal(peerId, direction === "outgoing" ? "out" : "in", text).then(() => {
|
|
@@ -5662,6 +6234,8 @@ function DkApp() {
|
|
|
5662
6234
|
React.useEffect(() => {
|
|
5663
6235
|
if (!activeId)
|
|
5664
6236
|
return;
|
|
6237
|
+
if (activeId === ASSISTANT_ID)
|
|
6238
|
+
return;
|
|
5665
6239
|
data.loadThread(activeId, { reset: true });
|
|
5666
6240
|
dkApi.markRead(activeId).then(data.refresh);
|
|
5667
6241
|
const iv = setInterval(() => {
|
|
@@ -5705,6 +6279,10 @@ function DkApp() {
|
|
|
5705
6279
|
const onSend = (text) => {
|
|
5706
6280
|
if (!activeId || !text || !text.trim())
|
|
5707
6281
|
return;
|
|
6282
|
+
if (activeId === ASSISTANT_ID) {
|
|
6283
|
+
assistant.handle(text.trim());
|
|
6284
|
+
return;
|
|
6285
|
+
}
|
|
5708
6286
|
dkApi.send(activeId, text.trim()).then(() => data.loadThread(activeId)).then(data.refresh);
|
|
5709
6287
|
};
|
|
5710
6288
|
const peerTakesRtcFile = (id) => {
|
|
@@ -5812,7 +6390,7 @@ function DkApp() {
|
|
|
5812
6390
|
fontWeight: 600,
|
|
5813
6391
|
background: tab === n.id ? "var(--accent)" : "transparent",
|
|
5814
6392
|
color: tab === n.id ? "#fff" : "var(--dim)"
|
|
5815
|
-
} }, /* @__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), hasEarlier: !!(activeId && data.earlier[activeId]), 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) => {
|
|
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) => {
|
|
5816
6394
|
data.refresh();
|
|
5817
6395
|
return r;
|
|
5818
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