@decentnetwork/beagle 0.1.111 → 0.1.113
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/cli.js +1 -0
- package/dist/desktop/app.js +347 -57
- package/dist/server.d.ts +4 -0
- package/dist/server.js +104 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -172,6 +172,7 @@ async function main() {
|
|
|
172
172
|
doraRosterPath,
|
|
173
173
|
downloadsDir: resolve(args.configDir, "downloads"),
|
|
174
174
|
messagesPath: resolve(args.configDir, "messages.json"),
|
|
175
|
+
servicePeersPath: resolve(args.configDir, "service-peers-local.json"),
|
|
175
176
|
callIce: {
|
|
176
177
|
keyFile: resolve(decentlanCarrierDir(args.configDir), "keypair.json"),
|
|
177
178
|
bootstrapNodes,
|
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.113";
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// packages/beagle-ui/dist/beagle-ui.js
|
|
@@ -68,8 +68,8 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
68
68
|
speaker: '<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="M16 9a3 3 0 0 1 0 6"/><path d="M19 7a6 6 0 0 1 0 10"/>',
|
|
69
69
|
micFill: '<rect x="9" y="2.5" width="6" height="11.5" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0"/><path d="M12 17.5v3.5"/>'
|
|
70
70
|
};
|
|
71
|
-
function Icon({ name, size = 24, stroke = 1.75, color = "currentColor", fill = "none", style }) {
|
|
72
|
-
const d = ICON_PATHS[name];
|
|
71
|
+
function Icon({ name, size = 24, stroke = 1.75, color = "currentColor", fill = "none", style, svgPath }) {
|
|
72
|
+
const d = svgPath || ICON_PATHS[name];
|
|
73
73
|
if (!d)
|
|
74
74
|
return null;
|
|
75
75
|
return /* @__PURE__ */ React.createElement(
|
|
@@ -111,7 +111,10 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
111
111
|
zap: '<path d="M13 2 4 13h7l-1 9 9-11h-7l1-9Z"/>',
|
|
112
112
|
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.5 2"/>',
|
|
113
113
|
bellOff: '<path d="M8.7 3.4A6 6 0 0 1 18 8.5c0 3 .5 5 1.1 6.3M6.1 6.1A6 6 0 0 0 6 8.5c0 6.5-2.5 8-2.5 8H16"/><path d="M13.7 20.5a2 2 0 0 1-3.4 0"/><path d="m3 3 18 18"/>',
|
|
114
|
-
plug: '<path d="M9 2v6M15 2v6"/><path d="M7 8h10v3a5 5 0 0 1-10 0V8Z"/><path d="M12 16v6"/>'
|
|
114
|
+
plug: '<path d="M9 2v6M15 2v6"/><path d="M7 8h10v3a5 5 0 0 1-10 0V8Z"/><path d="M12 16v6"/>',
|
|
115
|
+
// A form with a filled-in field: the glyph for "Fill with Beagle" and any
|
|
116
|
+
// other host app that puts your identity into someone else's page.
|
|
117
|
+
form: '<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h5"/><path d="M8 16h3"/><path d="m14.5 17.5 1.6 1.6 3.4-3.6"/>'
|
|
115
118
|
});
|
|
116
119
|
var H = {};
|
|
117
120
|
var hostNeedsWelcome = false;
|
|
@@ -119,8 +122,16 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
119
122
|
var hostHasKeyBackup = false;
|
|
120
123
|
var hostEnsWrite = true;
|
|
121
124
|
var hostBrandHref = "";
|
|
125
|
+
var hostTabs = null;
|
|
126
|
+
var hostHideApps = null;
|
|
127
|
+
var hostApps = null;
|
|
128
|
+
var hostProfileSections = null;
|
|
122
129
|
function setUiHost(host2) {
|
|
123
130
|
H = host2 || {};
|
|
131
|
+
hostTabs = Array.isArray(H.hostTabs) ? H.hostTabs : null;
|
|
132
|
+
hostHideApps = Array.isArray(H.hideApps) ? H.hideApps : null;
|
|
133
|
+
hostApps = Array.isArray(H.hostApps) ? H.hostApps : null;
|
|
134
|
+
hostProfileSections = Array.isArray(H.hostProfileSections) ? H.hostProfileSections : null;
|
|
124
135
|
hostNeedsWelcome = !!H.hostNeedsWelcome;
|
|
125
136
|
hostPrefersRtcFile = !!H.hostPrefersRtcFile;
|
|
126
137
|
hostHasKeyBackup = !!H.hostHasKeyBackup;
|
|
@@ -644,6 +655,18 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
644
655
|
confirm: "list",
|
|
645
656
|
scope: "assistant"
|
|
646
657
|
},
|
|
658
|
+
{
|
|
659
|
+
name: "nodes-refresh",
|
|
660
|
+
usage: "/nodes refresh",
|
|
661
|
+
summary: "Look for friends that are provably service peers (the shipped exit table, this machine's dora roster) and offer to reclassify them. Nothing changes until you tick and confirm.",
|
|
662
|
+
params: {
|
|
663
|
+
type: "object",
|
|
664
|
+
properties: {},
|
|
665
|
+
required: []
|
|
666
|
+
},
|
|
667
|
+
confirm: "list",
|
|
668
|
+
scope: "assistant"
|
|
669
|
+
},
|
|
647
670
|
{
|
|
648
671
|
name: "find",
|
|
649
672
|
usage: "/find <text>",
|
|
@@ -818,7 +841,8 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
818
841
|
file: "service-peers.json",
|
|
819
842
|
loader: "loadServicePeers()",
|
|
820
843
|
rule: "A peer is a service peer if and only if its userid is in this list. The Assistant never infers it from a name or from having no messages. /friends excludes them entirely; /nodes is only them.",
|
|
821
|
-
later: "A dora / ENS / bridge / directory service returns the same shape; the bundled file is v1."
|
|
844
|
+
later: "A dora / ENS / bridge / directory service returns the same shape; the bundled file is v1.",
|
|
845
|
+
refresh: "/nodes refresh asks GET /api/assistant/service-candidates for evidence-backed candidates and writes the ticked ones to this machine's overlay (POST /api/assistant/service-peers, stored at <configDir>/service-peers-local.json). The shipped list is never edited at runtime; the overlay wins on the same userid."
|
|
822
846
|
}
|
|
823
847
|
};
|
|
824
848
|
var service_peers_default = {
|
|
@@ -973,6 +997,8 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
973
997
|
const sub = (parts[1] || "").toLowerCase();
|
|
974
998
|
if (sub === "list" || sub === "remove")
|
|
975
999
|
return { ok: true, name: `${head}-${sub}`, args: parts.slice(2) };
|
|
1000
|
+
if (head === "nodes" && sub === "refresh")
|
|
1001
|
+
return { ok: true, name: "nodes-refresh", args: parts.slice(2) };
|
|
976
1002
|
return { ok: false, name: head, args: parts.slice(1), error: `${head}-subcommand` };
|
|
977
1003
|
}
|
|
978
1004
|
return { ok: true, name: head, args: parts.slice(1) };
|
|
@@ -1010,6 +1036,19 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
1010
1036
|
}
|
|
1011
1037
|
return map;
|
|
1012
1038
|
}
|
|
1039
|
+
function mergeServicePeers(bundled, local) {
|
|
1040
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1041
|
+
for (const p of bundled && bundled.peers || [])
|
|
1042
|
+
if (p && p.userid)
|
|
1043
|
+
byId.set(p.userid, { ...p, source: "app" });
|
|
1044
|
+
for (const p of local && local.peers || [])
|
|
1045
|
+
if (p && p.userid)
|
|
1046
|
+
byId.set(p.userid, { ...p, source: "local" });
|
|
1047
|
+
return { version: 1, source: "merged", peers: [...byId.values()] };
|
|
1048
|
+
}
|
|
1049
|
+
function newCandidates(candidates, svcMap) {
|
|
1050
|
+
return (candidates != null ? candidates : []).filter((c) => c && c.userid && !(svcMap && svcMap.has(c.userid)));
|
|
1051
|
+
}
|
|
1013
1052
|
function partitionPeers(friends, svcMap) {
|
|
1014
1053
|
const people = [];
|
|
1015
1054
|
const nodes = [];
|
|
@@ -1092,8 +1131,13 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
1092
1131
|
return { match: partial[0], candidates: partial };
|
|
1093
1132
|
return { match: null, candidates: partial };
|
|
1094
1133
|
}
|
|
1095
|
-
function loadServicePeers() {
|
|
1096
|
-
|
|
1134
|
+
async function loadServicePeers() {
|
|
1135
|
+
try {
|
|
1136
|
+
const local = await fetch("/api/assistant/service-peers").then((r) => r.json());
|
|
1137
|
+
return mergeServicePeers(service_peers_default, local);
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
return service_peers_default;
|
|
1140
|
+
}
|
|
1097
1141
|
}
|
|
1098
1142
|
var ASSISTANT_PEER = {
|
|
1099
1143
|
id: ASSISTANT_ID,
|
|
@@ -1149,10 +1193,12 @@ window.__DK_UI_VERSION="0.1.111";
|
|
|
1149
1193
|
const tail = lang === "zh" ? `\u7B5B\u9009\u6761\u4EF6\uFF08\u53EF\u7EC4\u5408\uFF0C\u53D6\u5E76\u96C6\uFF09\uFF1A
|
|
1150
1194
|
${filters}
|
|
1151
1195
|
|
|
1152
|
-
|
|
1196
|
+
\`/rename\`\u3001\`/mute\`\u3001\`/find\` \u5BF9\u597D\u53CB\u548C\u670D\u52A1\u8282\u70B9\u90FD\u9002\u7528\uFF1B\u53EA\u6709\u5217\u8868\u548C\u5220\u9664\u5206\u6210\u4E24\u8FB9\u3002
|
|
1197
|
+
\u6700\u8FD1 ${RECENT_DAYS} \u5929\u5185\u6DFB\u52A0\u7684\u597D\u53CB\u4E0D\u4F1A\u88AB"\u4ECE\u672A/\u6C89\u9ED8"\u7C7B\u7B5B\u9009\u9009\u4E2D\uFF0C\u5E76\u4F1A\u5217\u51FA\u88AB\u8DF3\u8FC7\u7684\u540D\u5B57\u3002\u5220\u9664\u6C38\u8FDC\u9700\u8981\u5148\u786E\u8BA4\uFF0C\u53D6\u6D88\u52FE\u9009\u5373\u53EF\u4FDD\u7559\u3002` : `Filters (combine them; a friend matching any one is listed):
|
|
1153
1198
|
${filters}
|
|
1154
1199
|
|
|
1155
|
-
|
|
1200
|
+
\`/rename\`, \`/mute\` and \`/find\` work on friends and service peers alike \u2014 only listing and removing are split in two.
|
|
1201
|
+
Friends added in the last ${RECENT_DAYS} days are never matched by the "never/silent" filters, and the ones held back are named. Removal always asks first: untick anyone you want to keep.`;
|
|
1156
1202
|
return `${intro}
|
|
1157
1203
|
${cmds}
|
|
1158
1204
|
|
|
@@ -1166,8 +1212,9 @@ ${tail}`;
|
|
|
1166
1212
|
time: (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
|
1167
1213
|
text
|
|
1168
1214
|
}, extra);
|
|
1169
|
-
function AssistantPanel({ m, T, lang, onRemove }) {
|
|
1215
|
+
function AssistantPanel({ m, T, lang, onRemove, onAdopt }) {
|
|
1170
1216
|
const rows = m.assistant.rows || [];
|
|
1217
|
+
const adopt = m.assistant.kind === "adopt";
|
|
1171
1218
|
const [sel, setSel] = React.useState(() => new Set(rows.map((r) => r.userid)));
|
|
1172
1219
|
const [busy, setBusy] = React.useState(false);
|
|
1173
1220
|
const [done, setDone] = React.useState(null);
|
|
@@ -1178,7 +1225,7 @@ ${tail}`;
|
|
|
1178
1225
|
n.has(id) ? n.delete(id) : n.add(id);
|
|
1179
1226
|
return n;
|
|
1180
1227
|
});
|
|
1181
|
-
const label = lang === "zh" ? `\u5220\u9664 ${sel.size} \u4F4D` : `Remove ${sel.size}`;
|
|
1228
|
+
const label = adopt ? lang === "zh" ? `\u52A0\u5165 ${sel.size} \u4E2A` : `Add ${sel.size} as ${sel.size === 1 ? "a node" : "nodes"}` : lang === "zh" ? `\u5220\u9664 ${sel.size} \u4F4D` : `Remove ${sel.size}`;
|
|
1182
1229
|
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: {
|
|
1183
1230
|
display: "flex",
|
|
1184
1231
|
alignItems: "center",
|
|
@@ -1195,12 +1242,14 @@ ${tail}`;
|
|
|
1195
1242
|
disabled: !!done || busy,
|
|
1196
1243
|
onChange: () => toggle(r.userid)
|
|
1197
1244
|
}
|
|
1198
|
-
), /* @__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: () => {
|
|
1245
|
+
), /* @__PURE__ */ React.createElement("span", { style: { flex: 1 } }, nameOf(r), r.why && /* @__PURE__ */ React.createElement("span", { style: { color: "var(--faint)", fontSize: 11.5 } }, ` \u2014 ${r.why}`)), /* @__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: adopt ? "solid" : "danger", disabled: busy || !sel.size, onClick: () => {
|
|
1199
1246
|
setBusy(true);
|
|
1200
|
-
|
|
1201
|
-
|
|
1247
|
+
const picked = rows.filter((r) => sel.has(r.userid));
|
|
1248
|
+
const run = adopt ? onAdopt(picked) : onRemove([...sel]);
|
|
1249
|
+
run.then((r) => {
|
|
1250
|
+
setDone(adopt ? lang === "zh" ? `\u5DF2\u52A0\u5165 ${r.added} \u4E2A\u670D\u52A1\u8282\u70B9` : `Added ${r.added} as service peers` : lang === "zh" ? `\u5DF2\u5220\u9664 ${r.removed} \u4F4D${r.failed ? `\uFF0C${r.failed} \u4F4D\u5931\u8D25` : ""}` : `Removed ${r.removed}${r.failed ? `, ${r.failed} failed` : ""}`);
|
|
1202
1251
|
}).finally(() => setBusy(false));
|
|
1203
|
-
} }, 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"))));
|
|
1252
|
+
} }, busy ? T.working || "Working\u2026" : label), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, color: "var(--faint)" } }, adopt ? lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u5373\u4FDD\u6301\u4E3A\u597D\u53CB" : "Untick any you want to keep as a friend" : lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u53EF\u4FDD\u7559" : "Untick anyone you want to keep"))));
|
|
1204
1253
|
}
|
|
1205
1254
|
function useAssistant({ lang, T, muted, onToggleMute, sound, flash, setAlert, onReloadFriends }) {
|
|
1206
1255
|
const [thread, setThread] = React.useState(() => [{ day: (/* @__PURE__ */ new Date()).toLocaleDateString() }, asMsg("them", helpText(lang))]);
|
|
@@ -1210,11 +1259,13 @@ ${tail}`;
|
|
|
1210
1259
|
}, [lang]);
|
|
1211
1260
|
const fetchFriends = async () => {
|
|
1212
1261
|
const r = await fetch("/api/assistant/friends").then((x) => x.json());
|
|
1213
|
-
const
|
|
1262
|
+
const svcMap = servicePeerMap(await loadServicePeers());
|
|
1263
|
+
const { people, nodes } = partitionPeers(r.friends || [], svcMap);
|
|
1214
1264
|
return {
|
|
1215
1265
|
friends: people,
|
|
1216
1266
|
nodes,
|
|
1217
1267
|
all: r.friends || [],
|
|
1268
|
+
svcMap,
|
|
1218
1269
|
historySince: r.historySince || null,
|
|
1219
1270
|
now: r.now || Date.now(),
|
|
1220
1271
|
ver: r.beagleVer || ""
|
|
@@ -1281,6 +1332,33 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
1281
1332
|
- Waiting to be accepted: ${pending}
|
|
1282
1333
|
- History on this device since: ${fmtDate(d.historySince, lang)}
|
|
1283
1334
|
- Version: ${d.ver}`));
|
|
1335
|
+
}
|
|
1336
|
+
case "nodes-refresh": {
|
|
1337
|
+
const d = await fetchFriends();
|
|
1338
|
+
const r = await fetch("/api/assistant/service-candidates").then((x) => x.json());
|
|
1339
|
+
const fresh = newCandidates(r.candidates || [], d.svcMap);
|
|
1340
|
+
const src = r.sources || {};
|
|
1341
|
+
const where = lang === "zh" ? `\u67E5\u8BE2\u6765\u6E90\uFF1A\u5185\u7F6E\u51FA\u53E3\u8868\uFF08${src.exits || 0} \u6761\uFF09${src.dora == null ? "\uFF0C\u672C\u673A\u4E0D\u662F dora" : `\uFF0C\u672C\u673A dora \u540D\u518C\uFF08${src.dora} \u6761\uFF09`}` : `Looked at: the shipped exit table (${src.exits || 0})${src.dora == null ? ", and this machine is not a dora" : `, and this machine's dora roster (${src.dora})`}`;
|
|
1342
|
+
if (!fresh.length) {
|
|
1343
|
+
return append(asMsg("them", lang === "zh" ? `**\u6CA1\u6709\u65B0\u7684\u670D\u52A1\u8282\u70B9**
|
|
1344
|
+
|
|
1345
|
+
\u73B0\u6709 ${d.nodes.length} \u4E2A\u5DF2\u8BC6\u522B\u3002${where}\u3002
|
|
1346
|
+
|
|
1347
|
+
*\u53EA\u91C7\u7528\u8BC1\u636E\uFF0C\u4E0D\u9760\u540D\u5B57\u731C\u6D4B\u3002*` : `**Nothing new**
|
|
1348
|
+
|
|
1349
|
+
${d.nodes.length} service peers are already classified. ${where}.
|
|
1350
|
+
|
|
1351
|
+
*Evidence only \u2014 names are never guessed from.*`));
|
|
1352
|
+
}
|
|
1353
|
+
const title = lang === "zh" ? `**\u53D1\u73B0 ${fresh.length} \u4E2A\u53EF\u4EE5\u6807\u4E3A\u670D\u52A1\u8282\u70B9\u7684\u597D\u53CB** \u2014 \u52FE\u9009\u540E\u624D\u4F1A\u751F\u6548
|
|
1354
|
+
|
|
1355
|
+
${where}\u3002` : `**${fresh.length === 1 ? "1 friend looks" : `${fresh.length} friends look`} like service peers** \u2014 nothing changes until you tick and confirm
|
|
1356
|
+
|
|
1357
|
+
${where}.`;
|
|
1358
|
+
const lines = fresh.map((c) => `- **${c.name}** \`${c.kind}${c.role ? " \xB7 " + c.role : ""}\` \u2014 ${c.why}`).join("\n");
|
|
1359
|
+
return append(asMsg("them", `${title}
|
|
1360
|
+
|
|
1361
|
+
${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
|
|
1284
1362
|
}
|
|
1285
1363
|
case "nodes-list":
|
|
1286
1364
|
case "friends-list": {
|
|
@@ -1370,6 +1448,24 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
1370
1448
|
return append(asMsg("them", lang === "zh" ? `\u51FA\u9519\u4E86\uFF1A${e && e.message || e}` : `That failed: ${e && e.message || e}`));
|
|
1371
1449
|
}
|
|
1372
1450
|
}, [append, lang, muted, onToggleMute, setAlert, onReloadFriends]);
|
|
1451
|
+
const adoptServicePeers = React.useCallback(async (rows) => {
|
|
1452
|
+
const add = rows.map((r) => ({ userid: r.userid, name: r.name, kind: r.kind, role: r.role }));
|
|
1453
|
+
let added = 0;
|
|
1454
|
+
try {
|
|
1455
|
+
const r = await fetch("/api/assistant/service-peers", {
|
|
1456
|
+
method: "POST",
|
|
1457
|
+
headers: { "content-type": "application/json" },
|
|
1458
|
+
body: JSON.stringify({ add })
|
|
1459
|
+
}).then((x) => x.json());
|
|
1460
|
+
added = r && r.ok ? add.length : 0;
|
|
1461
|
+
} catch (e) {
|
|
1462
|
+
added = 0;
|
|
1463
|
+
}
|
|
1464
|
+
if (onReloadFriends)
|
|
1465
|
+
onReloadFriends();
|
|
1466
|
+
append(asMsg("them", added ? lang === "zh" ? `\u5DF2\u628A ${added} \u4F4D\u6807\u4E3A\u670D\u52A1\u8282\u70B9\uFF0C\u5B83\u4EEC\u4E0D\u518D\u51FA\u73B0\u5728 \`/friends\` \u4E2D\u3002` : `${added} now count as service peers \u2014 they no longer appear in \`/friends\`.` : lang === "zh" ? "\u5199\u5165\u5931\u8D25\uFF0C\u6CA1\u6709\u4EFB\u4F55\u6539\u52A8\u3002" : "That write failed; nothing changed."));
|
|
1467
|
+
return { added };
|
|
1468
|
+
}, [append, lang, onReloadFriends]);
|
|
1373
1469
|
const removeFriends = React.useCallback(async (ids) => {
|
|
1374
1470
|
let removed = 0;
|
|
1375
1471
|
const failed = [];
|
|
@@ -1389,7 +1485,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
1389
1485
|
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"}.`));
|
|
1390
1486
|
return { removed, failed: failed.length };
|
|
1391
1487
|
}, [append, lang, onReloadFriends]);
|
|
1392
|
-
return { thread, handle, removeFriends };
|
|
1488
|
+
return { thread, handle, removeFriends, adoptServicePeers };
|
|
1393
1489
|
}
|
|
1394
1490
|
var DK_CMD_HIST_KEY = "beagle.assistant.history";
|
|
1395
1491
|
var DK_CMD_HIST_MAX = 50;
|
|
@@ -2196,7 +2292,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
2196
2292
|
return null;
|
|
2197
2293
|
}), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
|
|
2198
2294
|
}
|
|
2199
|
-
function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, answered, selMode, selected, onToggleSel, busy, isGroup }) {
|
|
2295
|
+
function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, onAssistantAdopt, answered, selMode, selected, onToggleSel, busy, isGroup }) {
|
|
2200
2296
|
const mine = m.from === "me";
|
|
2201
2297
|
const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
|
|
2202
2298
|
const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
|
|
@@ -2525,7 +2621,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
2525
2621
|
letterSpacing: -0.1,
|
|
2526
2622
|
wordBreak: "break-word"
|
|
2527
2623
|
} }, /* @__PURE__ */ React.createElement(MarkdownText, { text: read.text, onNameCard })));
|
|
2528
|
-
})(), 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), (() => {
|
|
2624
|
+
})(), 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, onAdopt: onAssistantAdopt }), /* @__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), (() => {
|
|
2529
2625
|
const sentDone = mine && m.status && m.status !== "queued" && m.status !== "sending" && m.status !== "failed";
|
|
2530
2626
|
const via = !mine ? m.via : sentDone ? m.via === "offline" ? "offline" : "online" : null;
|
|
2531
2627
|
if (!via)
|
|
@@ -2570,7 +2666,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
2570
2666
|
) : 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)" })))
|
|
2571
2667
|
);
|
|
2572
2668
|
}
|
|
2573
|
-
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, muted, onToggleMute, phone, onBack }) {
|
|
2669
|
+
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted, onToggleMute, phone, onBack }) {
|
|
2574
2670
|
const scrollRef = React.useRef(null);
|
|
2575
2671
|
const prependRef = React.useRef(null);
|
|
2576
2672
|
const [earlierBusy, setEarlierBusy] = React.useState(false);
|
|
@@ -2933,7 +3029,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
|
|
|
2933
3029
|
// The internal id is not an address anyone can use and not a name
|
|
2934
3030
|
// anyone chose; saying what this row IS is more use than showing it.
|
|
2935
3031
|
/* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, T && T.assistantSub || "Local \xB7 APP")
|
|
2936
|
-
) : 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(
|
|
3032
|
+
) : 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)), onOpenNet && /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), onOpenNet && /* @__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(
|
|
2937
3033
|
Mono,
|
|
2938
3034
|
{
|
|
2939
3035
|
size: 11.5,
|
|
@@ -2968,7 +3064,7 @@ ${peer.address}`
|
|
|
2968
3064
|
} }), phone && onSendRtcFile && !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "file", label: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => {
|
|
2969
3065
|
setMenu(false);
|
|
2970
3066
|
rtcFileRef.current && rtcFileRef.current.click();
|
|
2971
|
-
} }), phone && !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "network", label: T && T.openNet || "Network view", onClick: () => {
|
|
3067
|
+
} }), phone && !peer.app && onOpenNet && /* @__PURE__ */ React.createElement(MenuItem, { icon: "network", label: T && T.openNet || "Network view", onClick: () => {
|
|
2972
3068
|
setMenu(false);
|
|
2973
3069
|
onOpenNet(peer);
|
|
2974
3070
|
} }), !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
|
|
@@ -3003,6 +3099,7 @@ ${peer.address}`
|
|
|
3003
3099
|
isGroup,
|
|
3004
3100
|
onTheater: setTheater,
|
|
3005
3101
|
onAssistantRemove,
|
|
3102
|
+
onAssistantAdopt,
|
|
3006
3103
|
onDelete: (id) => doDelete([id]),
|
|
3007
3104
|
onCancel: doCancel,
|
|
3008
3105
|
onRetry: doRetry,
|
|
@@ -3162,12 +3259,12 @@ ${peer.address}`
|
|
|
3162
3259
|
function ChatEmpty({ T }) {
|
|
3163
3260
|
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));
|
|
3164
3261
|
}
|
|
3165
|
-
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 }) {
|
|
3262
|
+
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
|
|
3166
3263
|
const peer = peers.find((p) => p.id === activeId);
|
|
3167
3264
|
if (phone) {
|
|
3168
|
-
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 });
|
|
3265
|
+
return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, 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 });
|
|
3169
3266
|
}
|
|
3170
|
-
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 }));
|
|
3267
|
+
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, onAssistantAdopt, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
3171
3268
|
}
|
|
3172
3269
|
Object.assign(window, { ChatTab });
|
|
3173
3270
|
function CopyBtn({ value, copiedText = "Copied", copyFailedText = "Copy failed", copyTitle = "Copy" }) {
|
|
@@ -3940,7 +4037,7 @@ ${peer.address}`
|
|
|
3940
4037
|
return /* @__PURE__ */ React.createElement(DkPunkPicker, { T, onPick: pickPunk, onClose: () => setPicking(false) });
|
|
3941
4038
|
return /* @__PURE__ */ React.createElement("div", { onClick: onClose, style: { position: "fixed", inset: 0, zIndex: 90, background: "color-mix(in oklab, #000, transparent 38%)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { width: 360, maxWidth: "100%", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: 16, display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 4 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 40, radius: 10, dot: false }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 700, color: "var(--text)" } }, zh ? "\u66F4\u6362\u5934\u50CF" : "Change avatar"), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), item("sparkles", zh ? "\u9009\u4E00\u4E2A CryptoPunk" : "Choose a CryptoPunk", () => setPicking(true)), canUpload && item("image", zh ? "\u4E0A\u4F20\u7167\u7247" : "Upload a photo", () => fileRef.current && fileRef.current.click()), has && item("trash", zh ? "\u79FB\u9664\u5934\u50CF" : "Remove avatar", () => save(canUpload ? { punkId: null, avatarDataUrl: null } : { punkId: null }), true), /* @__PURE__ */ React.createElement("input", { ref: fileRef, type: "file", accept: "image/*", onChange: onFile, style: { display: "none" } }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)", lineHeight: 1.5, marginTop: 2 } }, zh ? "\u4E0D\u9700\u8981\u6CE8\u518C beagles.eth \u540D\u5B57\u3002\u597D\u53CB\u4F1A\u5728\u4E0B\u6B21\u6536\u5230\u4F60\u7684\u8D44\u6599\u65F6\u770B\u5230\u65B0\u5934\u50CF\u3002" : "No beagles.eth name needed. Friends see it the next time your profile reaches them."), err && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--danger)" } }, err)));
|
|
3942
4039
|
}
|
|
3943
|
-
function ProfileTab({ T, me, onEdit, t, setTweak, onSetAvatar }) {
|
|
4040
|
+
function ProfileTab({ T, me, onEdit, t, setTweak, onSetAvatar, sections, sectionProps }) {
|
|
3944
4041
|
var _a;
|
|
3945
4042
|
const [avatarSheet, setAvatarSheet] = React.useState(false);
|
|
3946
4043
|
const [qr, setQr] = React.useState(null);
|
|
@@ -3972,12 +4069,33 @@ ${peer.address}`
|
|
|
3972
4069
|
},
|
|
3973
4070
|
style: { width: 16, height: 16, accentColor: "var(--accent)" }
|
|
3974
4071
|
}
|
|
3975
|
-
), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, T && T.autoAcceptLabel || "Auto-accept friend requests"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", marginTop: 2 } }, T && T.autoAcceptSub || "Off: requests wait in the sidebar for your approval.")))), hostHasKeyBackup && /* @__PURE__ */ React.createElement(DkKeyBackup, { t: t || {} }), t && setTweak && /* @__PURE__ */ React.createElement(DkAlertSettings, { t, setTweak }), t && setTweak && /* @__PURE__ */ React.createElement(DkDisplaySettings, { t, setTweak }), /* @__PURE__ */ React.createElement(Card, { label: T.network }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.virtualIp, value: me.ip, copy: true }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.wireLabel, value: `${me.wire} \xB7 lossless`, mono: false }), /* @__PURE__ */ React.createElement(DkVersionRow, { T, me }), /* @__PURE__ */ React.createElement(DkUpdateCheck, { T })), /* @__PURE__ */ React.createElement(Card, { label: T.dangerZone }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 8, flexShrink: 0, background: "color-mix(in oklab, var(--danger), transparent 86%)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--danger)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 17, stroke: 2 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--danger)" } }, T.deleteNode), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)", marginTop: 1 } }, T.deleteSub)), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", size: "sm" }, T.delete)))), qr && /* @__PURE__ */ React.createElement(DkQrModal, { T, value: qr.value, label: qr.label, onClose: () => setQr(null) }), avatarSheet && /* @__PURE__ */ React.createElement(DkAvatarSheet, { T, me, zh: t && t.lang === "zh", onSet: onSetAvatar, onClose: () => setAvatarSheet(false) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
|
|
4072
|
+
), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, T && T.autoAcceptLabel || "Auto-accept friend requests"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", marginTop: 2 } }, T && T.autoAcceptSub || "Off: requests wait in the sidebar for your approval.")))), hostHasKeyBackup && /* @__PURE__ */ React.createElement(DkKeyBackup, { t: t || {} }), t && setTweak && /* @__PURE__ */ React.createElement(DkAlertSettings, { t, setTweak }), t && setTweak && /* @__PURE__ */ React.createElement(DkDisplaySettings, { t, setTweak }), /* @__PURE__ */ React.createElement(Card, { label: T.network }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.virtualIp, value: me.ip, copy: true }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.wireLabel, value: `${me.wire} \xB7 lossless`, mono: false }), /* @__PURE__ */ React.createElement(DkVersionRow, { T, me }), /* @__PURE__ */ React.createElement(DkUpdateCheck, { T })), /* @__PURE__ */ React.createElement(Card, { label: T.dangerZone }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 8, flexShrink: 0, background: "color-mix(in oklab, var(--danger), transparent 86%)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--danger)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 17, stroke: 2 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--danger)" } }, T.deleteNode), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)", marginTop: 1 } }, T.deleteSub)), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", size: "sm" }, T.delete)))), Array.isArray(sections) && sections.length > 0 && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "0 20px 40px", display: "flex", flexDirection: "column", gap: 16 } }, sections.map((sec) => sec && typeof sec.render === "function" ? /* @__PURE__ */ React.createElement(DkHostSection, { key: sec.id, sec, props: sectionProps, lang: t && t.lang }) : null)), qr && /* @__PURE__ */ React.createElement(DkQrModal, { T, value: qr.value, label: qr.label, onClose: () => setQr(null) }), avatarSheet && /* @__PURE__ */ React.createElement(DkAvatarSheet, { T, me, zh: t && t.lang === "zh", onSet: onSetAvatar, onClose: () => setAvatarSheet(false) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
|
|
3976
4073
|
if (onEdit)
|
|
3977
4074
|
onEdit(name, description);
|
|
3978
4075
|
setEditing(false);
|
|
3979
4076
|
} }));
|
|
3980
4077
|
}
|
|
4078
|
+
var DkHostSection = class extends React.Component {
|
|
4079
|
+
constructor(props) {
|
|
4080
|
+
super(props);
|
|
4081
|
+
this.state = { err: null };
|
|
4082
|
+
}
|
|
4083
|
+
static getDerivedStateFromError(err) {
|
|
4084
|
+
return { err };
|
|
4085
|
+
}
|
|
4086
|
+
render() {
|
|
4087
|
+
var _a;
|
|
4088
|
+
const { sec, props, lang } = this.props;
|
|
4089
|
+
if (this.state.err) {
|
|
4090
|
+
return /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, lang === "zh" ? "\u8FD9\u4E2A\u9644\u52A0\u677F\u5757\u51FA\u9519\u4E86\u3002" : "This host section failed to render.");
|
|
4091
|
+
}
|
|
4092
|
+
try {
|
|
4093
|
+
return (_a = sec.render(props)) != null ? _a : null;
|
|
4094
|
+
} catch (e) {
|
|
4095
|
+
return null;
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
};
|
|
3981
4099
|
Object.assign(window, { DkShareSheet, ProfileTab, DkPunkAvatar, DkEnsAvatar, DkPublicProfile });
|
|
3982
4100
|
var DK_PHONE_MQ = "(max-width: 700px)";
|
|
3983
4101
|
function useIsPhone() {
|
|
@@ -4191,6 +4309,82 @@ ${peer.address}`
|
|
|
4191
4309
|
return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px", ...style } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform: "uppercase", color: "var(--faint)" } }, label), count != null && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "var(--faint)" } }, count), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, height: 1, background: "var(--line)" } }), trailing);
|
|
4192
4310
|
}
|
|
4193
4311
|
Object.assign(window, { useIsPhone, DkPhoneStyles, DkIdenticon, DkBot, DkAvatar, DkImgAvatar, StatusDot, Mono, shortKey, RouteTag, Tag, Unread, Btn, Section });
|
|
4312
|
+
var BUILTIN_TABS = ["chat", "people", "network", "apps", "profile"];
|
|
4313
|
+
var warned = /* @__PURE__ */ new Set();
|
|
4314
|
+
function warnOnce(what) {
|
|
4315
|
+
if (warned.has(what))
|
|
4316
|
+
return;
|
|
4317
|
+
warned.add(what);
|
|
4318
|
+
try {
|
|
4319
|
+
console.warn(`[beagle-ui] ignoring unknown host tab or app: ${what}`);
|
|
4320
|
+
} catch (e) {
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
function pickText(value, lang, fallback = "") {
|
|
4324
|
+
if (typeof value === "string")
|
|
4325
|
+
return value;
|
|
4326
|
+
if (value && typeof value === "object")
|
|
4327
|
+
return value[lang] || value.en || value.zh || fallback;
|
|
4328
|
+
return fallback;
|
|
4329
|
+
}
|
|
4330
|
+
function resolveTabs(hostTabs2) {
|
|
4331
|
+
if (!Array.isArray(hostTabs2) || !hostTabs2.length) {
|
|
4332
|
+
return BUILTIN_TABS.map((id) => ({ id, kind: "builtin" }));
|
|
4333
|
+
}
|
|
4334
|
+
const out = [];
|
|
4335
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4336
|
+
for (const entry of hostTabs2) {
|
|
4337
|
+
if (typeof entry === "string") {
|
|
4338
|
+
if (!BUILTIN_TABS.includes(entry)) {
|
|
4339
|
+
warnOnce(entry);
|
|
4340
|
+
continue;
|
|
4341
|
+
}
|
|
4342
|
+
if (seen.has(entry))
|
|
4343
|
+
continue;
|
|
4344
|
+
seen.add(entry);
|
|
4345
|
+
out.push({ id: entry, kind: "builtin" });
|
|
4346
|
+
continue;
|
|
4347
|
+
}
|
|
4348
|
+
if (entry && typeof entry === "object" && typeof entry.id === "string" && entry.id) {
|
|
4349
|
+
if (typeof entry.render !== "function") {
|
|
4350
|
+
warnOnce(`${entry.id} (no render function)`);
|
|
4351
|
+
continue;
|
|
4352
|
+
}
|
|
4353
|
+
if (BUILTIN_TABS.includes(entry.id)) {
|
|
4354
|
+
warnOnce(`${entry.id} (a built-in id cannot be redefined)`);
|
|
4355
|
+
continue;
|
|
4356
|
+
}
|
|
4357
|
+
if (seen.has(entry.id))
|
|
4358
|
+
continue;
|
|
4359
|
+
seen.add(entry.id);
|
|
4360
|
+
out.push({
|
|
4361
|
+
id: entry.id,
|
|
4362
|
+
kind: "host",
|
|
4363
|
+
label: entry.label,
|
|
4364
|
+
icon: entry.icon,
|
|
4365
|
+
render: entry.render,
|
|
4366
|
+
badge: typeof entry.badge === "function" ? entry.badge : void 0
|
|
4367
|
+
});
|
|
4368
|
+
continue;
|
|
4369
|
+
}
|
|
4370
|
+
warnOnce(String(entry && entry.id ? entry.id : entry));
|
|
4371
|
+
}
|
|
4372
|
+
return out.length ? out : BUILTIN_TABS.map((id) => ({ id, kind: "builtin" }));
|
|
4373
|
+
}
|
|
4374
|
+
function hasTab(tabs, id) {
|
|
4375
|
+
return (tabs != null ? tabs : []).some((t) => t.id === id);
|
|
4376
|
+
}
|
|
4377
|
+
function resolveApps(builtins, hideApps, hostApps2) {
|
|
4378
|
+
const hidden = new Set(Array.isArray(hideApps) ? hideApps : []);
|
|
4379
|
+
const host2 = (Array.isArray(hostApps2) ? hostApps2 : []).filter((a) => {
|
|
4380
|
+
if (a && typeof a.id === "string" && a.id && typeof a.open === "function")
|
|
4381
|
+
return true;
|
|
4382
|
+
warnOnce(`app ${a && a.id ? a.id : String(a)}`);
|
|
4383
|
+
return false;
|
|
4384
|
+
}).map((a) => ({ ...a, host: true }));
|
|
4385
|
+
const kept = (builtins != null ? builtins : []).filter((a) => !hidden.has(a.id));
|
|
4386
|
+
return host2.concat(kept);
|
|
4387
|
+
}
|
|
4194
4388
|
var APPS = [
|
|
4195
4389
|
{
|
|
4196
4390
|
id: "meet",
|
|
@@ -4230,11 +4424,24 @@ ${peer.address}`
|
|
|
4230
4424
|
const b64 = btoa(unescape(encodeURIComponent(json)));
|
|
4231
4425
|
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
4232
4426
|
}
|
|
4233
|
-
function AppsTab({ T, t }) {
|
|
4427
|
+
function AppsTab({ T, t, me, peers, phone, setTab }) {
|
|
4234
4428
|
const zh = t && t.lang === "zh";
|
|
4429
|
+
const apps = React.useMemo(
|
|
4430
|
+
() => resolveApps(APPS, hostHideApps, hostApps),
|
|
4431
|
+
[]
|
|
4432
|
+
);
|
|
4235
4433
|
const [busy, setBusy] = React.useState(null);
|
|
4236
4434
|
const [err, setErr] = React.useState(null);
|
|
4237
4435
|
const open = async (app) => {
|
|
4436
|
+
if (app.host) {
|
|
4437
|
+
setErr(null);
|
|
4438
|
+
try {
|
|
4439
|
+
app.open({ T, t, me, peers, phone, setTab });
|
|
4440
|
+
} catch (e) {
|
|
4441
|
+
setErr(String(e && e.message || e));
|
|
4442
|
+
}
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4238
4445
|
setBusy(app.id);
|
|
4239
4446
|
setErr(null);
|
|
4240
4447
|
const win = window.open("", "_blank");
|
|
@@ -4256,7 +4463,7 @@ ${peer.address}`
|
|
|
4256
4463
|
setBusy(null);
|
|
4257
4464
|
}
|
|
4258
4465
|
};
|
|
4259
|
-
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 16 } }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, letterSpacing: 0.6, textTransform: "uppercase", color: "var(--faint)" } }, zh ? "\u5E94\u7528" : "Apps"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 6, lineHeight: 1.6 } }, zh ? "\u8FD9\u4E9B\u5E94\u7528\u63A5\u53D7 Beagle \u8EAB\u4EFD\u3002\u4ECE\u8FD9\u91CC\u6253\u5F00\u4E0D\u9700\u8981\u518D\u767B\u5F55\u4E00\u6B21 \u2014\u2014 \u4F1A\u5E26\u4E0A\u4E00\u4E2A\u4E24\u5206\u949F\u5185\u6709\u6548\u7684\u7B7E\u540D\u3002" : "These accept a Beagle identity. Opening one from here does not ask you to sign in again \u2014 it carries a signature that is good for two minutes.")), err && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "#f85149" } }, err),
|
|
4466
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 16 } }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, letterSpacing: 0.6, textTransform: "uppercase", color: "var(--faint)" } }, zh ? "\u5E94\u7528" : "Apps"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 6, lineHeight: 1.6 } }, zh ? "\u8FD9\u4E9B\u5E94\u7528\u63A5\u53D7 Beagle \u8EAB\u4EFD\u3002\u4ECE\u8FD9\u91CC\u6253\u5F00\u4E0D\u9700\u8981\u518D\u767B\u5F55\u4E00\u6B21 \u2014\u2014 \u4F1A\u5E26\u4E0A\u4E00\u4E2A\u4E24\u5206\u949F\u5185\u6709\u6548\u7684\u7B7E\u540D\u3002" : "These accept a Beagle identity. Opening one from here does not ask you to sign in again \u2014 it carries a signature that is good for two minutes.")), err && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "#f85149" } }, err), apps.map((app) => /* @__PURE__ */ React.createElement("div", { key: app.id, style: {
|
|
4260
4467
|
display: "flex",
|
|
4261
4468
|
alignItems: "center",
|
|
4262
4469
|
gap: 14,
|
|
@@ -4273,7 +4480,7 @@ ${peer.address}`
|
|
|
4273
4480
|
display: "flex",
|
|
4274
4481
|
alignItems: "center",
|
|
4275
4482
|
justifyContent: "center"
|
|
4276
|
-
} }, /* @__PURE__ */ React.createElement(Icon, { name: app.icon, size: 19, color: "var(--accent)", stroke: 1.8 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 14, fontWeight: 600, color: "var(--text)" } }, app.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 2 } },
|
|
4483
|
+
} }, /* @__PURE__ */ React.createElement(Icon, { name: app.icon, size: 19, color: "var(--accent)", stroke: 1.8 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 14, fontWeight: 600, color: "var(--text)" } }, pickText(app.name, t && t.lang, app.id)), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 2 } }, pickText(app.blurb, t && t.lang, "")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--dim)", marginTop: 3 } }, app.host ? zh ? "\u672C\u673A\u5E94\u7528" : "runs in this app" : String(app.origin || "").replace(/^https?:\/\//, ""))), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", onClick: busy ? void 0 : () => open(app) }, busy === app.id ? zh ? "\u6253\u5F00\u4E2D\u2026" : "opening\u2026" : zh ? "\u6253\u5F00" : "Open")))));
|
|
4277
4484
|
}
|
|
4278
4485
|
var DK_FALLBACK_ICE_SERVERS = [
|
|
4279
4486
|
{ urls: "stun:stun.l.google.com:19302" },
|
|
@@ -6262,7 +6469,7 @@ ${peer.address}`
|
|
|
6262
6469
|
meetingDesc: "\u8DE8\u670D\u52A1\u5668\u521B\u5EFA\u97F3\u89C6\u9891\u623F\u95F4\uFF0C\u901A\u8FC7\u865A\u62DF IP \u9080\u8BF7\u597D\u53CB\u3002\u7FA4\u7EC4\u901A\u8BDD\u3001\u5C4F\u5E55\u5171\u4EAB\u4E0E\u5171\u4EAB\u51FA\u53E3\u8DEF\u7531\u5C06\u5728\u4E0B\u4E2A\u7248\u672C\u4E0A\u7EBF\u3002"
|
|
6263
6470
|
}
|
|
6264
6471
|
};
|
|
6265
|
-
function RailBtn({ icon, label, active, soon, onClick }) {
|
|
6472
|
+
function RailBtn({ icon, label, active, soon, onClick, svgPath }) {
|
|
6266
6473
|
return /* @__PURE__ */ React.createElement("button", { onClick, title: label, style: {
|
|
6267
6474
|
width: 52,
|
|
6268
6475
|
height: 52,
|
|
@@ -6277,9 +6484,9 @@ ${peer.address}`
|
|
|
6277
6484
|
alignItems: "center",
|
|
6278
6485
|
justifyContent: "center",
|
|
6279
6486
|
gap: 3
|
|
6280
|
-
} }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 20, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 9, fontWeight: 600, letterSpacing: 0.2, color: active ? "var(--accent)" : "var(--faint)" } }, label), soon && /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 5, right: 6, width: 6, height: 6, borderRadius: 999, background: "var(--warn)" } }));
|
|
6487
|
+
} }, /* @__PURE__ */ React.createElement(Icon, { name: icon, svgPath, size: 20, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 9, fontWeight: 600, letterSpacing: 0.2, color: active ? "var(--accent)" : "var(--faint)" } }, label), soon && /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 5, right: 6, width: 6, height: 6, borderRadius: 999, background: "var(--warn)" } }));
|
|
6281
6488
|
}
|
|
6282
|
-
function TabBarBtn({ icon, label, active, badge, onClick }) {
|
|
6489
|
+
function TabBarBtn({ icon, label, active, badge, onClick, svgPath }) {
|
|
6283
6490
|
return /* @__PURE__ */ React.createElement("button", { onClick, style: {
|
|
6284
6491
|
flex: 1,
|
|
6285
6492
|
minWidth: 0,
|
|
@@ -6294,9 +6501,36 @@ ${peer.address}`
|
|
|
6294
6501
|
justifyContent: "center",
|
|
6295
6502
|
gap: 3,
|
|
6296
6503
|
fontFamily: "var(--mono)"
|
|
6297
|
-
} }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 22, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 10, fontWeight: 600, color: active ? "var(--accent)" : "var(--faint)", whiteSpace: "nowrap" } }, label), badge ? /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 4, left: "50%", marginLeft: 6 } }, /* @__PURE__ */ React.createElement(Unread, { n: badge })) : null);
|
|
6504
|
+
} }, /* @__PURE__ */ React.createElement(Icon, { name: icon, svgPath, size: 22, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 10, fontWeight: 600, color: active ? "var(--accent)" : "var(--faint)", whiteSpace: "nowrap" } }, label), badge ? /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 4, left: "50%", marginLeft: 6 } }, /* @__PURE__ */ React.createElement(Unread, { n: badge })) : null);
|
|
6298
6505
|
}
|
|
6299
6506
|
var DK_PEOPLE_TABS = ["here", "recommended", "registered"];
|
|
6507
|
+
var HostTabPane = class extends React.Component {
|
|
6508
|
+
constructor(props) {
|
|
6509
|
+
super(props);
|
|
6510
|
+
this.state = { err: null };
|
|
6511
|
+
}
|
|
6512
|
+
static getDerivedStateFromError(err) {
|
|
6513
|
+
return { err };
|
|
6514
|
+
}
|
|
6515
|
+
componentDidCatch(err) {
|
|
6516
|
+
try {
|
|
6517
|
+
console.error("[beagle-ui] host tab failed", err);
|
|
6518
|
+
} catch (e) {
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
render() {
|
|
6522
|
+
var _a;
|
|
6523
|
+
const { tab, props, lang } = this.props;
|
|
6524
|
+
if (this.state.err) {
|
|
6525
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: 24, textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--faint)", maxWidth: 420 } }, lang === "zh" ? `"${pickText(tab.label, lang, tab.id)}" \u8FD9\u4E2A\u6807\u7B7E\u9875\u51FA\u9519\u4E86\u3002\u5176\u5B83\u6807\u7B7E\u9875\u4E0D\u53D7\u5F71\u54CD\u3002` : `The "${pickText(tab.label, lang, tab.id)}" tab failed to render. The rest of the app is unaffected.`));
|
|
6526
|
+
}
|
|
6527
|
+
try {
|
|
6528
|
+
return (_a = tab.render(props)) != null ? _a : null;
|
|
6529
|
+
} catch (e) {
|
|
6530
|
+
return null;
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
};
|
|
6300
6534
|
function Brand({ T, children, style }) {
|
|
6301
6535
|
if (!hostBrandHref)
|
|
6302
6536
|
return /* @__PURE__ */ React.createElement("span", { style }, children);
|
|
@@ -6413,7 +6647,17 @@ ${peer.address}`
|
|
|
6413
6647
|
}
|
|
6414
6648
|
function DkApp() {
|
|
6415
6649
|
const [t, setTweak] = useTweaks({ ...DK_DEFAULTS, lang: dkDetectLang() });
|
|
6416
|
-
const
|
|
6650
|
+
const tabs = React.useMemo(() => resolveTabs(hostTabs), []);
|
|
6651
|
+
const showPeople = hasTab(tabs, "people");
|
|
6652
|
+
const showNetwork = hasTab(tabs, "network");
|
|
6653
|
+
const showProfile = hasTab(tabs, "profile");
|
|
6654
|
+
const firstTab = tabs[0] ? tabs[0].id === "people" ? "here" : tabs[0].id : "chat";
|
|
6655
|
+
const [tab, setTab] = React.useState(() => {
|
|
6656
|
+
const want = t.startTab || "chat";
|
|
6657
|
+
if (want === "chat" && hasTab(tabs, "chat"))
|
|
6658
|
+
return "chat";
|
|
6659
|
+
return hasTab(tabs, want) || DK_PEOPLE_TABS.includes(want) && showPeople ? want : firstTab;
|
|
6660
|
+
});
|
|
6417
6661
|
const [activeId, setActiveId] = React.useState(null);
|
|
6418
6662
|
const data = useDaemonData();
|
|
6419
6663
|
const me = data.me;
|
|
@@ -6448,11 +6692,13 @@ ${peer.address}`
|
|
|
6448
6692
|
});
|
|
6449
6693
|
}, []);
|
|
6450
6694
|
React.useEffect(() => {
|
|
6695
|
+
if (!showNetwork)
|
|
6696
|
+
return void 0;
|
|
6451
6697
|
refreshBackend();
|
|
6452
6698
|
const ms = backend && backend.state === "releasing" ? 1500 : 8e3;
|
|
6453
6699
|
const t2 = setInterval(refreshBackend, ms);
|
|
6454
6700
|
return () => clearInterval(t2);
|
|
6455
|
-
}, [refreshBackend, backend && backend.state]);
|
|
6701
|
+
}, [refreshBackend, backend && backend.state, showNetwork]);
|
|
6456
6702
|
const armLan = React.useCallback(async () => {
|
|
6457
6703
|
await dkPost("/api/lan-arm", {});
|
|
6458
6704
|
refreshBackend();
|
|
@@ -6721,29 +6967,61 @@ ${peer.address}`
|
|
|
6721
6967
|
setTab("chat");
|
|
6722
6968
|
openConversation(id);
|
|
6723
6969
|
};
|
|
6724
|
-
const onOpenNet = () => setTab("network");
|
|
6725
|
-
const
|
|
6726
|
-
{ id: "chat", icon: "message", label: T.chat },
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6970
|
+
const onOpenNet = showNetwork ? () => setTab("network") : null;
|
|
6971
|
+
const RAIL_FOR = {
|
|
6972
|
+
chat: [{ id: "chat", icon: "message", label: T.chat }],
|
|
6973
|
+
people: [
|
|
6974
|
+
{ id: "here", icon: "users", label: T.here },
|
|
6975
|
+
{ id: "recommended", icon: "sparkles", label: T.recommended },
|
|
6976
|
+
{ id: "registered", icon: "at", label: T.registered }
|
|
6977
|
+
],
|
|
6978
|
+
network: [{ id: "network", icon: "network", label: T.network }],
|
|
6979
|
+
apps: [{ id: "apps", icon: "grid", label: T.apps }],
|
|
6980
|
+
profile: []
|
|
6981
|
+
};
|
|
6982
|
+
const nav = tabs.flatMap((x) => x.kind === "builtin" ? RAIL_FOR[x.id] || [] : [{ id: x.id, icon: x.icon, label: pickText(x.label, t.lang, x.id) }]);
|
|
6733
6983
|
const peopleTab = DK_PEOPLE_TABS.includes(tab);
|
|
6734
6984
|
const lastPeople = React.useRef("here");
|
|
6735
6985
|
if (peopleTab)
|
|
6736
6986
|
lastPeople.current = tab;
|
|
6737
6987
|
const unreadTotal = (peers || []).reduce((n, p) => n + (Number(p.unread) || 0), 0) + (requests || []).length;
|
|
6738
|
-
const
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6988
|
+
const NAV_FOR = {
|
|
6989
|
+
chat: { icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
|
|
6990
|
+
people: { icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
|
|
6991
|
+
network: { icon: "network", label: T.network, active: tab === "network", go: () => setTab("network") },
|
|
6992
|
+
apps: { icon: "grid", label: T.apps, active: tab === "apps", go: () => setTab("apps") },
|
|
6993
|
+
profile: { icon: "userRound", label: T.profile, active: tab === "profile", go: () => setTab("profile") }
|
|
6994
|
+
};
|
|
6995
|
+
const phoneNav = tabs.map((x) => x.kind === "builtin" ? { id: x.id, ...NAV_FOR[x.id] } : {
|
|
6996
|
+
id: x.id,
|
|
6997
|
+
icon: typeof x.icon === "string" ? x.icon : void 0,
|
|
6998
|
+
svgPath: x.icon && typeof x.icon === "object" ? x.icon.svgPath : void 0,
|
|
6999
|
+
label: pickText(x.label, t.lang, x.id),
|
|
7000
|
+
active: tab === x.id,
|
|
7001
|
+
// A host badge is called on every render, like the unread count beside
|
|
7002
|
+
// it; a throwing one must not take the tab bar down with it.
|
|
7003
|
+
badge: (() => {
|
|
7004
|
+
try {
|
|
7005
|
+
return x.badge ? x.badge() : 0;
|
|
7006
|
+
} catch (e) {
|
|
7007
|
+
return 0;
|
|
7008
|
+
}
|
|
7009
|
+
})(),
|
|
7010
|
+
go: () => setTab(x.id)
|
|
7011
|
+
});
|
|
6745
7012
|
const inConversation = phone && tab === "chat" && !!activeId && (peers || []).some((p) => p.id === activeId);
|
|
6746
|
-
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", flexDirection: phone ? "column" : "row", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement(DkPhoneStyles, null), !phone && /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement(Brand, { T, style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8, flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(
|
|
7013
|
+
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", flexDirection: phone ? "column" : "row", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement(DkPhoneStyles, null), !phone && /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement(Brand, { T, style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8, flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(
|
|
7014
|
+
RailBtn,
|
|
7015
|
+
{
|
|
7016
|
+
key: n.id,
|
|
7017
|
+
icon: typeof n.icon === "string" ? n.icon : void 0,
|
|
7018
|
+
svgPath: n.icon && typeof n.icon === "object" ? n.icon.svgPath : void 0,
|
|
7019
|
+
label: n.label,
|
|
7020
|
+
active: tab === n.id,
|
|
7021
|
+
soon: n.soon,
|
|
7022
|
+
onClick: () => setTab(n.id)
|
|
7023
|
+
}
|
|
7024
|
+
)), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), showProfile && /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: {
|
|
6747
7025
|
padding: 4,
|
|
6748
7026
|
borderRadius: 12,
|
|
6749
7027
|
cursor: "pointer",
|
|
@@ -6764,10 +7042,22 @@ ${peer.address}`
|
|
|
6764
7042
|
fontWeight: 600,
|
|
6765
7043
|
background: tab === n.id ? "var(--accent)" : "transparent",
|
|
6766
7044
|
color: tab === n.id ? "#fff" : "var(--dim)"
|
|
6767
|
-
} }, /* @__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 }),
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
7045
|
+
} }, /* @__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, onAssistantAdopt: activeId === ASSISTANT_ID ? assistant.adoptServicePeers : 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, me, peers, phone, setTab }), tabs.map((x) => x.kind === "host" && tab === x.id ? /* @__PURE__ */ React.createElement(HostTabPane, { key: x.id, tab: x, props: { T, t, me, peers, phone, setTab }, lang: t.lang }) : null), tab === "profile" && /* @__PURE__ */ React.createElement(
|
|
7046
|
+
ProfileTab,
|
|
7047
|
+
{
|
|
7048
|
+
T,
|
|
7049
|
+
me,
|
|
7050
|
+
onEdit,
|
|
7051
|
+
t,
|
|
7052
|
+
setTweak,
|
|
7053
|
+
onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
|
|
7054
|
+
data.refresh();
|
|
7055
|
+
return r;
|
|
7056
|
+
}),
|
|
7057
|
+
sections: hostProfileSections,
|
|
7058
|
+
sectionProps: { T, t, me, peers, phone, setTab }
|
|
7059
|
+
}
|
|
7060
|
+
)), 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, svgPath: n.svgPath, label: n.label, active: n.active, badge: n.badge, onClick: n.go }))), data.locked && /* @__PURE__ */ React.createElement(
|
|
6771
7061
|
DkLockedOut,
|
|
6772
7062
|
{
|
|
6773
7063
|
compact: true,
|
package/dist/server.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface BeagleServerOptions {
|
|
|
28
28
|
* (never written) by GET /api/assistant/friends to count a friend's
|
|
29
29
|
* history without hauling it over IPC. Undefined = no counts. */
|
|
30
30
|
messagesPath?: string;
|
|
31
|
+
/** <configDir>/service-peers-local.json — peers THIS machine has adopted as
|
|
32
|
+
* service peers on top of the list shipped in the app. Written only by the
|
|
33
|
+
* Assistant's `/nodes refresh`, and only after the user ticks the rows. */
|
|
34
|
+
servicePeersPath?: string;
|
|
31
35
|
/** The swappable backend, so the UI can show which one is live and drive the
|
|
32
36
|
* LAN handoff. Optional: a caller that pins one backend just omits it and
|
|
33
37
|
* the network panel falls back to "no virtual LAN here". */
|
package/dist/server.js
CHANGED
|
@@ -121,6 +121,21 @@ function friendStoreStamps(keyFile) {
|
|
|
121
121
|
return friendStampsCache?.map ?? empty;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
/** The machine-local service-peer overlay. Absent file = nothing adopted yet,
|
|
125
|
+
* which is the normal state, not an error. */
|
|
126
|
+
function readLocalServicePeers(path) {
|
|
127
|
+
const empty = { version: 1, source: "local", peers: [] };
|
|
128
|
+
if (!path || !existsSync(path))
|
|
129
|
+
return empty;
|
|
130
|
+
try {
|
|
131
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
132
|
+
const peers = Array.isArray(raw?.peers) ? raw.peers.filter((p) => p && typeof p.userid === "string") : [];
|
|
133
|
+
return { version: 1, source: "local", updated: raw?.updated, peers };
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return empty;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
124
139
|
/** The record beagle writes for its own name: what the profile page shows
|
|
125
140
|
* (displayName, description) on top of whatever the record already held —
|
|
126
141
|
* wallet binds, socials, avatar, referee — so a profile edit never strips a
|
|
@@ -2238,6 +2253,95 @@ export function startBeagleServer(opts) {
|
|
|
2238
2253
|
sendJson(res, 200, { me, peers, requests, exits, activeExit });
|
|
2239
2254
|
return;
|
|
2240
2255
|
}
|
|
2256
|
+
// The locally adopted service peers: the app ships a list, and this is
|
|
2257
|
+
// what this machine added to it. Kept server-side rather than in a tab's
|
|
2258
|
+
// localStorage so every browser on this machine agrees about what is a
|
|
2259
|
+
// node — the classification decides what `/friends remove` can touch.
|
|
2260
|
+
if (req.method === "GET" && url === "/api/assistant/service-peers") {
|
|
2261
|
+
sendJson(res, 200, readLocalServicePeers(opts.servicePeersPath));
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
if (req.method === "POST" && url === "/api/assistant/service-peers") {
|
|
2265
|
+
const body = (await readBody(req));
|
|
2266
|
+
if (!opts.servicePeersPath)
|
|
2267
|
+
return sendJson(res, 400, { ok: false, error: "no config dir" });
|
|
2268
|
+
const current = readLocalServicePeers(opts.servicePeersPath);
|
|
2269
|
+
const byId = new Map(current.peers.map((p) => [p.userid, p]));
|
|
2270
|
+
for (const raw of Array.isArray(body.add) ? body.add : []) {
|
|
2271
|
+
const p = raw;
|
|
2272
|
+
const userid = typeof p?.userid === "string" ? p.userid : "";
|
|
2273
|
+
if (!userid)
|
|
2274
|
+
continue;
|
|
2275
|
+
byId.set(userid, {
|
|
2276
|
+
userid,
|
|
2277
|
+
name: String(p.name ?? userid).slice(0, 120),
|
|
2278
|
+
kind: p.kind === "agent" ? "agent" : "node",
|
|
2279
|
+
role: String(p.role ?? "").slice(0, 120),
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
for (const raw of Array.isArray(body.remove) ? body.remove : []) {
|
|
2283
|
+
if (typeof raw === "string")
|
|
2284
|
+
byId.delete(raw);
|
|
2285
|
+
}
|
|
2286
|
+
const next = { version: 1, source: "local", updated: new Date().toISOString().slice(0, 10), peers: [...byId.values()] };
|
|
2287
|
+
try {
|
|
2288
|
+
writeFileSync(opts.servicePeersPath, JSON.stringify(next, null, 2) + "\n");
|
|
2289
|
+
}
|
|
2290
|
+
catch (e) {
|
|
2291
|
+
return sendJson(res, 500, { ok: false, error: e.message });
|
|
2292
|
+
}
|
|
2293
|
+
sendJson(res, 200, { ok: true, ...next });
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
// `/nodes refresh`: which friends does this device have EVIDENCE for?
|
|
2297
|
+
// Evidence only — the shipped exit table and, on a dora, its roster.
|
|
2298
|
+
// Nothing here reads a name or guesses from silence, and nothing is
|
|
2299
|
+
// adopted without the user ticking it.
|
|
2300
|
+
if (req.method === "GET" && url === "/api/assistant/service-candidates") {
|
|
2301
|
+
const flist = await opts.call({ op: "friends-list" });
|
|
2302
|
+
const fl = flist.ok ? (flist.data?.friends ?? []) : [];
|
|
2303
|
+
const evidence = new Map();
|
|
2304
|
+
for (const e of DEFAULT_EXITS) {
|
|
2305
|
+
// Role, not region: the exit table's regions carry place names this
|
|
2306
|
+
// app does not put in front of users (see CLAUDE.md), and "which
|
|
2307
|
+
// exit" is not what this answer is for.
|
|
2308
|
+
if (e.userid)
|
|
2309
|
+
evidence.set(e.userid, { kind: "node", role: "exit", why: "in the shipped exit table" });
|
|
2310
|
+
}
|
|
2311
|
+
let doraCount = 0;
|
|
2312
|
+
if (opts.doraRosterPath && existsSync(opts.doraRosterPath)) {
|
|
2313
|
+
try {
|
|
2314
|
+
const r = yaml.load(readFileSync(opts.doraRosterPath, "utf-8"));
|
|
2315
|
+
for (const rec of r?.records ?? []) {
|
|
2316
|
+
const uid = rec.userid || rec.pubkey;
|
|
2317
|
+
if (!uid)
|
|
2318
|
+
continue;
|
|
2319
|
+
doraCount++;
|
|
2320
|
+
evidence.set(uid, { kind: "node", role: rec.ip ? `dora member (${rec.ip})` : "dora member", why: "allocated in this machine's dora roster" });
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
catch { /* a broken roster is not an error here */ }
|
|
2324
|
+
}
|
|
2325
|
+
const candidates = fl
|
|
2326
|
+
.map((f) => ({ f, ev: evidence.get(f.userid ?? "") }))
|
|
2327
|
+
.filter((x) => x.ev)
|
|
2328
|
+
.map(({ f, ev }) => ({
|
|
2329
|
+
userid: f.userid,
|
|
2330
|
+
name: f.alias || (typeof f.name === "string" ? f.name : "") || f.userid,
|
|
2331
|
+
kind: ev.kind,
|
|
2332
|
+
role: ev.role,
|
|
2333
|
+
why: ev.why,
|
|
2334
|
+
}));
|
|
2335
|
+
sendJson(res, 200, {
|
|
2336
|
+
ok: true,
|
|
2337
|
+
candidates,
|
|
2338
|
+
sources: {
|
|
2339
|
+
exits: DEFAULT_EXITS.length,
|
|
2340
|
+
dora: opts.doraRosterPath ? doraCount : null,
|
|
2341
|
+
},
|
|
2342
|
+
});
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2241
2345
|
// Everything the Assistant's friend filters need, in one answer: the
|
|
2242
2346
|
// friend list plus how much was said to each friend and when. The
|
|
2243
2347
|
// counts come from the store file in our own config dir — see
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.113",
|
|
4
4
|
"description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"yargs": "^17.7.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@decentnetwork/beagle-ui": "^0.2.
|
|
38
|
+
"@decentnetwork/beagle-ui": "^0.2.27",
|
|
39
39
|
"@types/js-yaml": "^4.0.9",
|
|
40
40
|
"@types/node": "^20.11.0",
|
|
41
41
|
"@types/yargs": "^17.0.32",
|