@decentnetwork/beagle 0.1.88 → 0.1.90
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/desktop/app.js +241 -12
- package/dist/embedded-host.js +22 -19
- package/dist/server.js +31 -6
- package/package.json +1 -1
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.90";
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// packages/beagle-ui/dist/beagle-ui.js
|
|
@@ -380,11 +380,13 @@ window.__DK_UI_VERSION="0.1.87";
|
|
|
380
380
|
const t = setInterval(refresh, 2500);
|
|
381
381
|
return () => clearInterval(t);
|
|
382
382
|
}, [refresh]);
|
|
383
|
+
const inFlight = React.useRef(/* @__PURE__ */ new Set());
|
|
383
384
|
const loadThread = React.useCallback(async (peerId) => {
|
|
384
|
-
if (!peerId)
|
|
385
|
+
if (!peerId || inFlight.current.has(peerId))
|
|
385
386
|
return;
|
|
387
|
+
inFlight.current.add(peerId);
|
|
386
388
|
try {
|
|
387
|
-
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId));
|
|
389
|
+
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=200");
|
|
388
390
|
const arr = d.chats && d.chats[peerId] || [];
|
|
389
391
|
await prefetchThreadFiles(arr);
|
|
390
392
|
const msgs = arr.map((m) => ({
|
|
@@ -417,6 +419,8 @@ window.__DK_UI_VERSION="0.1.87";
|
|
|
417
419
|
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
418
420
|
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
419
421
|
} catch (e) {
|
|
422
|
+
} finally {
|
|
423
|
+
inFlight.current.delete(peerId);
|
|
420
424
|
}
|
|
421
425
|
}, []);
|
|
422
426
|
return Object.assign({}, snap, { threads, refresh, loadThread });
|
|
@@ -521,6 +525,9 @@ window.__DK_UI_VERSION="0.1.87";
|
|
|
521
525
|
minWidth: 0
|
|
522
526
|
} }, dkContactPreview(peer.lastMsg) || peer.lastMsg))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, peer.lastTime), peer.unread ? /* @__PURE__ */ React.createElement(Unread, { n: peer.unread }) : peer.pending ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--warn, #d29922)", title: "friend request pending" }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
|
|
523
527
|
}
|
|
528
|
+
function dkActivityTs(p) {
|
|
529
|
+
return Math.max(p.lastTs || 0, p.acceptedAt || 0, p.requestedAt || 0);
|
|
530
|
+
}
|
|
524
531
|
function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }) {
|
|
525
532
|
const [q, setQ] = React.useState("");
|
|
526
533
|
const [addr, setAddr] = React.useState("");
|
|
@@ -558,12 +565,9 @@ window.__DK_UI_VERSION="0.1.87";
|
|
|
558
565
|
const s = (p.alias || "") + " " + p.userId + " " + p.ip;
|
|
559
566
|
return s.toLowerCase().includes(q.toLowerCase());
|
|
560
567
|
}).slice().sort((a, b) => {
|
|
561
|
-
const ta = a
|
|
568
|
+
const ta = dkActivityTs(a), tb = dkActivityTs(b);
|
|
562
569
|
if (ta !== tb)
|
|
563
570
|
return tb - ta;
|
|
564
|
-
const aa = a.acceptedAt || 0, ba = b.acceptedAt || 0;
|
|
565
|
-
if (aa !== ba)
|
|
566
|
-
return ba - aa;
|
|
567
571
|
return String(a.alias || a.userId || "").localeCompare(String(b.alias || b.userId || ""));
|
|
568
572
|
});
|
|
569
573
|
const online = peers.filter((p) => p.online).length;
|
|
@@ -2720,6 +2724,35 @@ ${peer.address}`
|
|
|
2720
2724
|
)));
|
|
2721
2725
|
return /* @__PURE__ */ React.createElement(Card, { label: zh ? "\u663E\u793A" : "Display" }, /* @__PURE__ */ React.createElement("div", { style: { ...row, borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("span", { style: name }, zh ? "\u8BED\u8A00" : "Language"), seg("lang", t.lang, (v) => setTweak("lang", v), [["en", "EN"], ["zh", "\u4E2D\u6587"]])), /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: name }, zh ? "\u4E3B\u9898" : "Theme"), seg("theme", t.theme, (v) => setTweak("theme", v), [["dark", zh ? "\u6DF1\u8272" : "Dark"], ["light", zh ? "\u6D45\u8272" : "Light"]])));
|
|
2722
2726
|
}
|
|
2727
|
+
function DkAlertSettings({ t, setTweak }) {
|
|
2728
|
+
const zh = t.lang === "zh";
|
|
2729
|
+
const toggle = (key, label, sub, last) => /* @__PURE__ */ React.createElement("label", { style: {
|
|
2730
|
+
display: "flex",
|
|
2731
|
+
alignItems: "center",
|
|
2732
|
+
gap: 13,
|
|
2733
|
+
padding: "13px 16px",
|
|
2734
|
+
cursor: "pointer",
|
|
2735
|
+
borderBottom: last ? "none" : "1px solid var(--line)"
|
|
2736
|
+
} }, /* @__PURE__ */ React.createElement(
|
|
2737
|
+
"input",
|
|
2738
|
+
{
|
|
2739
|
+
type: "checkbox",
|
|
2740
|
+
checked: t[key] !== false,
|
|
2741
|
+
onChange: (e) => setTweak(key, e.target.checked),
|
|
2742
|
+
style: { width: 16, height: 16, accentColor: "var(--accent)" }
|
|
2743
|
+
}
|
|
2744
|
+
), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, label), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", marginTop: 2 } }, sub)));
|
|
2745
|
+
return /* @__PURE__ */ React.createElement(Card, { label: zh ? "\u63D0\u9192" : "Alerts" }, toggle(
|
|
2746
|
+
"alertSound",
|
|
2747
|
+
zh ? "\u65B0\u6D88\u606F\u63D0\u793A\u97F3" : "Sound for new messages",
|
|
2748
|
+
zh ? "\u6536\u5230\u65B0\u6D88\u606F\u6216\u597D\u53CB\u8BF7\u6C42\u65F6\u64AD\u653E\u77ED\u4FC3\u63D0\u793A\u97F3\u3002\u6B63\u5728\u770B\u7684\u5F53\u524D\u4F1A\u8BDD\u4E0D\u4F1A\u54CD\u3002" : "A short chime when a message or friend request arrives. Silent for the conversation you are reading."
|
|
2749
|
+
), toggle(
|
|
2750
|
+
"alertFlash",
|
|
2751
|
+
zh ? "\u6807\u7B7E\u9875\u95EA\u70C1" : "Flash the tab",
|
|
2752
|
+
zh ? "\u7A97\u53E3\u4E0D\u5728\u524D\u53F0\u65F6\uFF0C\u6807\u7B7E\u6807\u9898\u548C\u56FE\u6807\u95EA\u70C1\u63D0\u793A\uFF0C\u56DE\u5230\u7A97\u53E3\u5373\u505C\u6B62\u3002" : "While this window is in the background, the tab title and icon blink until you come back.",
|
|
2753
|
+
true
|
|
2754
|
+
));
|
|
2755
|
+
}
|
|
2723
2756
|
function ProfileTab({ T, me, onEdit, t, setTweak }) {
|
|
2724
2757
|
var _a;
|
|
2725
2758
|
const [qr, setQr] = React.useState(null);
|
|
@@ -2741,7 +2774,7 @@ ${peer.address}`
|
|
|
2741
2774
|
},
|
|
2742
2775
|
style: { width: 16, height: 16, accentColor: "var(--accent)" }
|
|
2743
2776
|
}
|
|
2744
|
-
), /* @__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(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) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
|
|
2777
|
+
), /* @__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) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
|
|
2745
2778
|
if (onEdit)
|
|
2746
2779
|
onEdit(name, description);
|
|
2747
2780
|
setEditing(false);
|
|
@@ -4290,6 +4323,181 @@ ${peer.address}`
|
|
|
4290
4323
|
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1040, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 26 } }, /* @__PURE__ */ React.createElement(MyNode, { T, me, activeExit, peers, reqCount }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.peerRouting, count: peers.length }), /* @__PURE__ */ React.createElement(PeerTable, { T, peers, onOpenChat })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.exitNodes, trailing: /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { icon: "plus", size: "sm" }, T.addExit)) }), activeExit && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderRadius: 10, background: "color-mix(in oklab, var(--warn), transparent 90%)", border: "1px solid color-mix(in oklab, var(--warn), transparent 70%)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "route", size: 17, color: "var(--warn)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, T.egressVia), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: activeExit }, activeExit), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "unlink", size: "sm", onClick: () => onSetExit(null) }, T.stopRouting)), /* @__PURE__ */ React.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } }, exits.map((r) => /* @__PURE__ */ React.createElement(ExitCard, { key: r.region, T, region: r, activeExit, onSetExit }))))));
|
|
4291
4324
|
}
|
|
4292
4325
|
Object.assign(window, { NetworkTab });
|
|
4326
|
+
var dkAudioCtx = null;
|
|
4327
|
+
function dkAudio() {
|
|
4328
|
+
if (dkAudioCtx && dkAudioCtx.state !== "closed")
|
|
4329
|
+
return dkAudioCtx;
|
|
4330
|
+
try {
|
|
4331
|
+
dkAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
4332
|
+
} catch (e) {
|
|
4333
|
+
dkAudioCtx = null;
|
|
4334
|
+
}
|
|
4335
|
+
return dkAudioCtx;
|
|
4336
|
+
}
|
|
4337
|
+
function dkUnlockAudioOnGesture() {
|
|
4338
|
+
const unlock = () => {
|
|
4339
|
+
const c = dkAudio();
|
|
4340
|
+
if (c && c.state === "suspended")
|
|
4341
|
+
c.resume().catch(() => {
|
|
4342
|
+
});
|
|
4343
|
+
};
|
|
4344
|
+
window.addEventListener("pointerdown", unlock, { passive: true });
|
|
4345
|
+
window.addEventListener("keydown", unlock, { passive: true });
|
|
4346
|
+
return () => {
|
|
4347
|
+
window.removeEventListener("pointerdown", unlock);
|
|
4348
|
+
window.removeEventListener("keydown", unlock);
|
|
4349
|
+
};
|
|
4350
|
+
}
|
|
4351
|
+
function dkChime(kind) {
|
|
4352
|
+
const c = dkAudio();
|
|
4353
|
+
if (!c)
|
|
4354
|
+
return;
|
|
4355
|
+
if (c.state === "suspended")
|
|
4356
|
+
c.resume().catch(() => {
|
|
4357
|
+
});
|
|
4358
|
+
const notes = kind === "request" ? [[660, 0], [880, 0.14]] : [[880, 0], [1175, 0.11]];
|
|
4359
|
+
const t0 = c.currentTime;
|
|
4360
|
+
for (const [freq, at] of notes) {
|
|
4361
|
+
const o = c.createOscillator(), g = c.createGain();
|
|
4362
|
+
o.type = "sine";
|
|
4363
|
+
o.frequency.value = freq;
|
|
4364
|
+
o.connect(g);
|
|
4365
|
+
g.connect(c.destination);
|
|
4366
|
+
const t = t0 + at;
|
|
4367
|
+
g.gain.setValueAtTime(1e-4, t);
|
|
4368
|
+
g.gain.exponentialRampToValueAtTime(0.18, t + 0.015);
|
|
4369
|
+
g.gain.exponentialRampToValueAtTime(1e-4, t + 0.17);
|
|
4370
|
+
o.start(t);
|
|
4371
|
+
o.stop(t + 0.2);
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
var dkFlash = { timer: null, title: "", link: null, href: null, badged: null, on: false, n: 0, label: "" };
|
|
4375
|
+
function dkBadgedIcon(href) {
|
|
4376
|
+
return new Promise((res, rej) => {
|
|
4377
|
+
const img = new Image();
|
|
4378
|
+
img.onload = () => {
|
|
4379
|
+
try {
|
|
4380
|
+
if (!img.naturalWidth)
|
|
4381
|
+
return rej(new Error("icon has no intrinsic size"));
|
|
4382
|
+
const s = 64, cv = document.createElement("canvas");
|
|
4383
|
+
cv.width = s;
|
|
4384
|
+
cv.height = s;
|
|
4385
|
+
const cx = cv.getContext("2d");
|
|
4386
|
+
cx.drawImage(img, 0, 0, s, s);
|
|
4387
|
+
cx.beginPath();
|
|
4388
|
+
cx.arc(s - 14, 14, 12, 0, Math.PI * 2);
|
|
4389
|
+
cx.fillStyle = "#e5484d";
|
|
4390
|
+
cx.fill();
|
|
4391
|
+
cx.lineWidth = 3;
|
|
4392
|
+
cx.strokeStyle = "#fff";
|
|
4393
|
+
cx.stroke();
|
|
4394
|
+
res(cv.toDataURL("image/png"));
|
|
4395
|
+
} catch (e) {
|
|
4396
|
+
rej(e);
|
|
4397
|
+
}
|
|
4398
|
+
};
|
|
4399
|
+
img.onerror = () => rej(new Error("icon failed to load"));
|
|
4400
|
+
img.src = href;
|
|
4401
|
+
});
|
|
4402
|
+
}
|
|
4403
|
+
function dkFlashTick() {
|
|
4404
|
+
dkFlash.on = !dkFlash.on;
|
|
4405
|
+
document.title = `(${dkFlash.n}) ${dkFlash.on ? dkFlash.label : dkFlash.title}`;
|
|
4406
|
+
}
|
|
4407
|
+
function dkFlashStart(label, count) {
|
|
4408
|
+
dkFlash.n += count || 1;
|
|
4409
|
+
dkFlash.label = label;
|
|
4410
|
+
if (dkFlash.timer)
|
|
4411
|
+
return;
|
|
4412
|
+
dkFlash.title = document.title;
|
|
4413
|
+
dkFlash.link = document.querySelector('link[rel~="icon"]');
|
|
4414
|
+
dkFlash.href = dkFlash.link ? dkFlash.link.href : null;
|
|
4415
|
+
dkFlash.badged = null;
|
|
4416
|
+
dkFlash.on = false;
|
|
4417
|
+
if (dkFlash.href) {
|
|
4418
|
+
dkBadgedIcon(dkFlash.href).then((b) => {
|
|
4419
|
+
dkFlash.badged = b;
|
|
4420
|
+
if (dkFlash.timer && dkFlash.link)
|
|
4421
|
+
dkFlash.link.href = b;
|
|
4422
|
+
}).catch(() => {
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
dkFlashTick();
|
|
4426
|
+
dkFlash.timer = setInterval(dkFlashTick, 1e3);
|
|
4427
|
+
}
|
|
4428
|
+
function dkFlashStop() {
|
|
4429
|
+
if (!dkFlash.timer)
|
|
4430
|
+
return;
|
|
4431
|
+
clearInterval(dkFlash.timer);
|
|
4432
|
+
dkFlash.timer = null;
|
|
4433
|
+
dkFlash.n = 0;
|
|
4434
|
+
document.title = dkFlash.title;
|
|
4435
|
+
if (dkFlash.link && dkFlash.href)
|
|
4436
|
+
dkFlash.link.href = dkFlash.href;
|
|
4437
|
+
}
|
|
4438
|
+
function dkLooking() {
|
|
4439
|
+
return document.visibilityState === "visible" && document.hasFocus();
|
|
4440
|
+
}
|
|
4441
|
+
function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true }) {
|
|
4442
|
+
const prev = React.useRef(null);
|
|
4443
|
+
React.useEffect(() => dkUnlockAudioOnGesture(), []);
|
|
4444
|
+
React.useEffect(() => {
|
|
4445
|
+
if (!flash)
|
|
4446
|
+
dkFlashStop();
|
|
4447
|
+
}, [flash]);
|
|
4448
|
+
React.useEffect(() => {
|
|
4449
|
+
const seen = () => {
|
|
4450
|
+
if (dkLooking())
|
|
4451
|
+
dkFlashStop();
|
|
4452
|
+
};
|
|
4453
|
+
window.addEventListener("focus", seen);
|
|
4454
|
+
window.addEventListener("pointerdown", seen, { passive: true });
|
|
4455
|
+
document.addEventListener("visibilitychange", seen);
|
|
4456
|
+
return () => {
|
|
4457
|
+
window.removeEventListener("focus", seen);
|
|
4458
|
+
window.removeEventListener("pointerdown", seen);
|
|
4459
|
+
document.removeEventListener("visibilitychange", seen);
|
|
4460
|
+
dkFlashStop();
|
|
4461
|
+
};
|
|
4462
|
+
}, []);
|
|
4463
|
+
React.useEffect(() => {
|
|
4464
|
+
if (!loaded)
|
|
4465
|
+
return;
|
|
4466
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4467
|
+
for (const p of peers || []) {
|
|
4468
|
+
const dir = p.lastDir || (String(p.lastMsg || "").startsWith("you: ") ? "out" : "in");
|
|
4469
|
+
byId.set(p.id, { ts: p.lastTs || 0, dir });
|
|
4470
|
+
}
|
|
4471
|
+
const reqIds = new Set((requests || []).map((r) => r.id));
|
|
4472
|
+
const was = prev.current;
|
|
4473
|
+
prev.current = { byId, reqIds };
|
|
4474
|
+
if (!was)
|
|
4475
|
+
return;
|
|
4476
|
+
let msgs = 0, elsewhere = 0, reqs = 0;
|
|
4477
|
+
for (const [id, cur] of byId) {
|
|
4478
|
+
if (cur.dir !== "in" || !cur.ts)
|
|
4479
|
+
continue;
|
|
4480
|
+
const old = was.byId.get(id);
|
|
4481
|
+
if (old ? cur.ts <= old.ts : Date.now() - cur.ts > 3e4)
|
|
4482
|
+
continue;
|
|
4483
|
+
msgs += 1;
|
|
4484
|
+
if (id !== activeId)
|
|
4485
|
+
elsewhere += 1;
|
|
4486
|
+
}
|
|
4487
|
+
for (const id of reqIds)
|
|
4488
|
+
if (!was.reqIds.has(id))
|
|
4489
|
+
reqs += 1;
|
|
4490
|
+
if (!msgs && !reqs)
|
|
4491
|
+
return;
|
|
4492
|
+
const looking = dkLooking();
|
|
4493
|
+
if (sound && !(looking && !reqs && !elsewhere))
|
|
4494
|
+
dkChime(reqs && !msgs ? "request" : "message");
|
|
4495
|
+
if (flash && !looking) {
|
|
4496
|
+
const label = msgs ? T && T.alertMsg || "New message" : T && T.alertReq || "New friend request";
|
|
4497
|
+
dkFlashStart(label, msgs + reqs);
|
|
4498
|
+
}
|
|
4499
|
+
}, [peers, requests, loaded, activeId, T, sound, flash]);
|
|
4500
|
+
}
|
|
4293
4501
|
var TWEAKS_STORAGE_KEY = "decentlan.tweaks";
|
|
4294
4502
|
function useTweaks(defaults) {
|
|
4295
4503
|
const [values, setValues] = React.useState(() => {
|
|
@@ -4347,7 +4555,9 @@ ${peer.address}`
|
|
|
4347
4555
|
"accent": "#7B6CF6",
|
|
4348
4556
|
"density": "compact",
|
|
4349
4557
|
"lang": "en",
|
|
4350
|
-
"startTab": "chat"
|
|
4558
|
+
"startTab": "chat",
|
|
4559
|
+
"alertSound": true,
|
|
4560
|
+
"alertFlash": true
|
|
4351
4561
|
}
|
|
4352
4562
|
);
|
|
4353
4563
|
function dkDetectLang() {
|
|
@@ -4416,6 +4626,8 @@ ${peer.address}`
|
|
|
4416
4626
|
network: "Network",
|
|
4417
4627
|
apps: "Apps",
|
|
4418
4628
|
profile: "Profile",
|
|
4629
|
+
alertMsg: "New message",
|
|
4630
|
+
alertReq: "New friend request",
|
|
4419
4631
|
recommended: "Discover",
|
|
4420
4632
|
registered: "Names",
|
|
4421
4633
|
here: "Here",
|
|
@@ -4596,6 +4808,8 @@ ${peer.address}`
|
|
|
4596
4808
|
network: "\u7F51\u7EDC",
|
|
4597
4809
|
apps: "\u5E94\u7528",
|
|
4598
4810
|
profile: "\u6211\u7684",
|
|
4811
|
+
alertMsg: "\u65B0\u6D88\u606F",
|
|
4812
|
+
alertReq: "\u65B0\u7684\u597D\u53CB\u8BF7\u6C42",
|
|
4599
4813
|
recommended: "\u63A8\u8350",
|
|
4600
4814
|
registered: "\u540D\u5F55",
|
|
4601
4815
|
here: "\u5728\u573A",
|
|
@@ -4988,6 +5202,15 @@ ${peer.address}`
|
|
|
4988
5202
|
const T = STR[t.lang] || STR.en;
|
|
4989
5203
|
const vars = dkTheme(t.theme, t.accent);
|
|
4990
5204
|
const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
|
|
5205
|
+
useIncomingAlerts({
|
|
5206
|
+
T,
|
|
5207
|
+
peers,
|
|
5208
|
+
requests,
|
|
5209
|
+
loaded: data.loaded,
|
|
5210
|
+
activeId,
|
|
5211
|
+
sound: t.alertSound !== false,
|
|
5212
|
+
flash: t.alertFlash !== false
|
|
5213
|
+
});
|
|
4991
5214
|
const callCtl = useCallController(me.userId, (peerId, video, direction) => {
|
|
4992
5215
|
const text = `WebRTC ${video ? "video" : "audio"} call: ${direction}`;
|
|
4993
5216
|
dkApi.logLocal(peerId, direction === "outgoing" ? "out" : "in", text).then(() => {
|
|
@@ -5129,10 +5352,16 @@ ${peer.address}`
|
|
|
5129
5352
|
{ id: "recommended", icon: "sparkles", label: T.recommended },
|
|
5130
5353
|
{ id: "registered", icon: "at", label: T.registered },
|
|
5131
5354
|
{ id: "network", icon: "network", label: T.network },
|
|
5132
|
-
{ id: "apps", icon: "grid", label: T.apps }
|
|
5133
|
-
{ id: "profile", icon: "userRound", label: T.profile }
|
|
5355
|
+
{ id: "apps", icon: "grid", label: T.apps }
|
|
5134
5356
|
];
|
|
5135
|
-
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__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("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("
|
|
5357
|
+
return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__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("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: {
|
|
5358
|
+
padding: 4,
|
|
5359
|
+
borderRadius: 12,
|
|
5360
|
+
cursor: "pointer",
|
|
5361
|
+
display: "flex",
|
|
5362
|
+
border: "1px solid " + (tab === "profile" ? "var(--line)" : "transparent"),
|
|
5363
|
+
background: tab === "profile" ? "var(--panel-2)" : "transparent"
|
|
5364
|
+
} }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr("") }), 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 })), data.locked && /* @__PURE__ */ React.createElement(
|
|
5136
5365
|
DkLockedOut,
|
|
5137
5366
|
{
|
|
5138
5367
|
compact: true,
|
package/dist/embedded-host.js
CHANGED
|
@@ -90,31 +90,30 @@ export class EmbeddedHost {
|
|
|
90
90
|
this.#messages = new MessageStore(resolve(opts.configDir, "messages.json"));
|
|
91
91
|
this.#meta = new FriendMetaStore(resolve(opts.configDir, "friends-meta.json"));
|
|
92
92
|
}
|
|
93
|
-
/** Friend-list order: pinned first, then most recent
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
93
|
+
/** Friend-list order: pinned first, then most recent ACTIVITY, then by
|
|
94
|
+
* name. Activity is the newest of the last message, the friendship being
|
|
95
|
+
* accepted, and our request going out — so a friend you added or accepted
|
|
96
|
+
* a moment ago ranks exactly as if they had just written to you. The
|
|
97
|
+
* earlier rule sorted by last message alone and ranked new friends only
|
|
98
|
+
* among the never-messaged group at the bottom, which on a long list still
|
|
99
|
+
* put them below the fold and made the accept look like a no-op. Returns
|
|
100
|
+
* a new array — `friends()` hands back the SDK's own list and sorting it
|
|
101
|
+
* in place would reorder the friend store as a side effect of merely
|
|
102
|
+
* rendering the sidebar. */
|
|
97
103
|
#sortForList(friends, last) {
|
|
98
104
|
const rank = (f) => [
|
|
99
105
|
this.#meta.get(f.carrierId)?.pinned ? 0 : 1,
|
|
100
|
-
// Negated so a LARGER timestamp sorts first.
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
last.has(f.carrierId) ? -last.get(f.carrierId).ts : Number.MAX_SAFE_INTEGER,
|
|
106
|
-
// Among friends you have never messaged, NEWEST FRIENDSHIP first.
|
|
107
|
-
// Alphabetical put someone you accepted ten seconds ago wherever their
|
|
108
|
-
// name happened to land — usually far down a 50-name list, so accepting
|
|
109
|
-
// a request looked like nothing had happened. The one friend you are
|
|
110
|
-
// most likely to open is the one you just added.
|
|
111
|
-
-(f.acceptedAt ?? 0),
|
|
106
|
+
// Negated so a LARGER timestamp sorts first. Nothing at all → 0, which
|
|
107
|
+
// parks the entry after every real one; a plain number, so two such
|
|
108
|
+
// entries compare 0 - 0 = 0 and fall through to the name (Infinity
|
|
109
|
+
// would give NaN there).
|
|
110
|
+
-Math.max(last.get(f.carrierId)?.ts ?? 0, f.acceptedAt ?? 0, f.requestedAt ?? 0),
|
|
112
111
|
(f.name || f.carrierId).toLowerCase(),
|
|
113
112
|
];
|
|
114
113
|
return [...friends].sort((a, b) => {
|
|
115
|
-
const [ap, at,
|
|
116
|
-
const [bp, bt,
|
|
117
|
-
return ap - bp || at - bt ||
|
|
114
|
+
const [ap, at, an] = rank(a);
|
|
115
|
+
const [bp, bt, bn] = rank(b);
|
|
116
|
+
return ap - bp || at - bt || an.localeCompare(bn);
|
|
118
117
|
});
|
|
119
118
|
}
|
|
120
119
|
get downloadsDir() {
|
|
@@ -459,6 +458,10 @@ export class EmbeddedHost {
|
|
|
459
458
|
f.status === "online" ||
|
|
460
459
|
this.#messages.hasInbound(f.carrierId),
|
|
461
460
|
lastSeen: f.acceptedAt,
|
|
461
|
+
// When WE sent the request. Counts as activity for the sidebar
|
|
462
|
+
// order: someone you just added belongs at the top while you
|
|
463
|
+
// wait for them, not filed by name at the bottom.
|
|
464
|
+
requestedAt: f.requestedAt,
|
|
462
465
|
// The avatar they advertised over Carrier (userinfo `gender`).
|
|
463
466
|
// Peer-to-peer, so it works for a friend who never registered a
|
|
464
467
|
// beagles.eth name — until now that meant an identicon.
|
package/dist/server.js
CHANGED
|
@@ -1975,11 +1975,19 @@ export function startBeagleServer(opts) {
|
|
|
1975
1975
|
// be ordered, which is why the sidebar never reordered on new
|
|
1976
1976
|
// activity.
|
|
1977
1977
|
lastTs: lm?.ts ?? 0,
|
|
1978
|
-
//
|
|
1979
|
-
//
|
|
1980
|
-
//
|
|
1981
|
-
|
|
1978
|
+
// Which way the last message went. The UI's new-message alert
|
|
1979
|
+
// needs to know an inbound stamp moved, and inferring that from
|
|
1980
|
+
// the "you: " prefix on the preview is a convention, not a field.
|
|
1981
|
+
lastDir: lm?.dir ?? null,
|
|
1982
|
+
// When the friendship was established, and when we asked for it.
|
|
1983
|
+
// The sidebar ranks a friend by the newest of these and the last
|
|
1984
|
+
// message, so someone you just accepted — or just added — lands
|
|
1985
|
+
// at the top like a fresh message would, not in a no-history
|
|
1986
|
+
// group at the bottom. A daemon backend reports only lastSeen
|
|
1987
|
+
// (its acceptedAt); requestedAt is embedded-only and reads 0
|
|
1988
|
+
// elsewhere.
|
|
1982
1989
|
acceptedAt: f.lastSeen ?? 0,
|
|
1990
|
+
requestedAt: f.requestedAt ?? 0,
|
|
1983
1991
|
wire: "163",
|
|
1984
1992
|
avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
|
|
1985
1993
|
// What they advertised over Carrier wins over the directory: it is
|
|
@@ -2098,8 +2106,25 @@ export function startBeagleServer(opts) {
|
|
|
2098
2106
|
return;
|
|
2099
2107
|
}
|
|
2100
2108
|
if (req.method === "GET" && url === "/api/chat-history") {
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2109
|
+
const q = new URL(req.url || "/", "http://x").searchParams;
|
|
2110
|
+
const peer = q.get("peer") || undefined;
|
|
2111
|
+
// A PAGE, never the whole thread. The UI re-polls the open
|
|
2112
|
+
// conversation every 2.5s, and this used to ask for everything each
|
|
2113
|
+
// time: a group with 2000 six-kilobyte bot reports is 12 MB of JSON
|
|
2114
|
+
// per poll per tab, which the daemon could not push over IPC inside
|
|
2115
|
+
// the 30s call timeout. While it tried, its socket stopped answering
|
|
2116
|
+
// the 400ms liveness probe, Beagle declared it gone and dropped the
|
|
2117
|
+
// backend, and the peer list came back empty — a blank chat panel
|
|
2118
|
+
// caused by one busy group. Default 200 is a full screen and then
|
|
2119
|
+
// some; `before` pages further back; the cap keeps a hand-written
|
|
2120
|
+
// URL from recreating the problem. Both `userid` and `peer` are sent:
|
|
2121
|
+
// the daemon reads the first, the embedded host the second, and with
|
|
2122
|
+
// only `userid` the embedded host returned every thread in the store.
|
|
2123
|
+
const lim = Number(q.get("limit") ?? 200);
|
|
2124
|
+
const limit = Number.isFinite(lim) && lim > 0 ? Math.min(lim, 1000) : 200;
|
|
2125
|
+
const bef = Number(q.get("before"));
|
|
2126
|
+
const before = Number.isFinite(bef) && bef > 0 ? bef : undefined;
|
|
2127
|
+
const r = await opts.call({ op: "chat-history", userid: peer, peer, before, limit });
|
|
2103
2128
|
sendJson(res, 200, r.ok ? (r.data ?? { chats: {} }) : { chats: {} });
|
|
2104
2129
|
return;
|
|
2105
2130
|
}
|
package/package.json
CHANGED