@decentnetwork/beagle 0.1.103 → 0.1.105
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 +145 -51
- package/dist/embedded-host.js +3 -0
- package/dist/lan-handoff.d.ts +5 -0
- package/dist/lan-handoff.js +43 -0
- package/dist/message-store.d.ts +3 -0
- package/dist/message-store.js +10 -0
- package/package.json +2 -2
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.105";
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// packages/beagle-ui/dist/beagle-ui.js
|
|
@@ -341,6 +341,42 @@ window.__DK_UI_VERSION="0.1.103";
|
|
|
341
341
|
channel: "@next",
|
|
342
342
|
wire: "163"
|
|
343
343
|
};
|
|
344
|
+
var DK_PAGE = 30;
|
|
345
|
+
function dkMsgKey(m) {
|
|
346
|
+
return m.id || m.ts + ":" + m.dir;
|
|
347
|
+
}
|
|
348
|
+
function dkThreadMsg(m) {
|
|
349
|
+
return {
|
|
350
|
+
id: m.id,
|
|
351
|
+
from: m.dir === "out" ? "me" : "them",
|
|
352
|
+
time: dkClock(m.ts),
|
|
353
|
+
text: m.text,
|
|
354
|
+
file: m.file ? {
|
|
355
|
+
name: m.file.name,
|
|
356
|
+
size: dkFileSize(m.file.size),
|
|
357
|
+
dir: m.dir,
|
|
358
|
+
media: dkFileMediaKind(m.file.name),
|
|
359
|
+
status: m.file.status,
|
|
360
|
+
pct: m.file.status === "sending" && m.file.size ? Math.min(100, (m.file.sent || 0) / m.file.size * 100) : void 0,
|
|
361
|
+
kbps: m.file.kbps
|
|
362
|
+
} : void 0,
|
|
363
|
+
// Pass the real state through. Collapsing everything that was not
|
|
364
|
+
// 'queued' to 'read' meant a message still in flight — or one that
|
|
365
|
+
// never went out at all — rendered as delivered.
|
|
366
|
+
status: m.dir === "out" ? m.status === "queued" || m.status === "sending" || m.status === "failed" || m.status === "sent" ? m.status : "read" : void 0,
|
|
367
|
+
error: m.error,
|
|
368
|
+
// false = went to a live session but nobody acknowledged it (one tick).
|
|
369
|
+
// Dropped here before, so that state could never render.
|
|
370
|
+
confirmed: m.confirmed,
|
|
371
|
+
// Delivery path: "online" = live session, "offline" = express relay.
|
|
372
|
+
// Both directions carry it now; the bubble labels them the same way.
|
|
373
|
+
via: m.via,
|
|
374
|
+
// Brief 30: a validated component form, already whitelisted host-side.
|
|
375
|
+
// Passed through untouched — the renderer treats every string as text,
|
|
376
|
+
// never markup.
|
|
377
|
+
components: m.components
|
|
378
|
+
};
|
|
379
|
+
}
|
|
344
380
|
function useDaemonData() {
|
|
345
381
|
const [snap, setSnap] = React.useState({
|
|
346
382
|
me: DK_ME_FALLBACK,
|
|
@@ -386,49 +422,74 @@ window.__DK_UI_VERSION="0.1.103";
|
|
|
386
422
|
return () => clearInterval(t);
|
|
387
423
|
}, [refresh]);
|
|
388
424
|
const inFlight = React.useRef(/* @__PURE__ */ new Set());
|
|
389
|
-
const
|
|
390
|
-
|
|
425
|
+
const cache = React.useRef({});
|
|
426
|
+
const [earlier, setEarlier] = React.useState({});
|
|
427
|
+
const publish = React.useCallback((peerId) => {
|
|
428
|
+
const c = cache.current[peerId];
|
|
429
|
+
if (!c)
|
|
430
|
+
return;
|
|
431
|
+
const arr = [...c.byId.values()].sort((a, b) => a.ts - b.ts);
|
|
432
|
+
const msgs = arr.map(dkThreadMsg);
|
|
433
|
+
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
434
|
+
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
435
|
+
setEarlier((e) => e[peerId] === c.more ? e : Object.assign({}, e, { [peerId]: c.more }));
|
|
436
|
+
}, []);
|
|
437
|
+
const loadThread = React.useCallback(async (peerId, opts) => {
|
|
438
|
+
if (!peerId)
|
|
439
|
+
return;
|
|
440
|
+
if (opts && opts.reset)
|
|
441
|
+
delete cache.current[peerId];
|
|
442
|
+
if (inFlight.current.has(peerId))
|
|
391
443
|
return;
|
|
392
444
|
inFlight.current.add(peerId);
|
|
393
445
|
try {
|
|
394
|
-
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=
|
|
446
|
+
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=" + DK_PAGE);
|
|
395
447
|
const arr = d.chats && d.chats[peerId] || [];
|
|
396
448
|
await prefetchThreadFiles(arr);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
kbps: m.file.kbps
|
|
410
|
-
} : void 0,
|
|
411
|
-
// Pass the real state through. Collapsing everything that was not
|
|
412
|
-
// 'queued' to 'read' meant a message still in flight — or one that
|
|
413
|
-
// never went out at all — rendered as delivered.
|
|
414
|
-
status: m.dir === "out" ? m.status === "queued" || m.status === "sending" || m.status === "failed" ? m.status : "read" : void 0,
|
|
415
|
-
error: m.error,
|
|
416
|
-
// Delivery path: "online" = live session, "offline" = express relay.
|
|
417
|
-
// Incoming messages carry this; the bubble colors them differently.
|
|
418
|
-
via: m.via,
|
|
419
|
-
// Brief 30: a validated component form, already whitelisted host-side.
|
|
420
|
-
// Passed through untouched — the renderer treats every string as text,
|
|
421
|
-
// never markup.
|
|
422
|
-
components: m.components
|
|
423
|
-
}));
|
|
424
|
-
const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
|
|
425
|
-
setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
|
|
449
|
+
let c = cache.current[peerId];
|
|
450
|
+
if (!c)
|
|
451
|
+
c = cache.current[peerId] = { byId: /* @__PURE__ */ new Map(), more: arr.length >= DK_PAGE };
|
|
452
|
+
if (arr.length) {
|
|
453
|
+
const from = arr[0].ts;
|
|
454
|
+
for (const [k, m] of c.byId)
|
|
455
|
+
if (m.ts >= from)
|
|
456
|
+
c.byId.delete(k);
|
|
457
|
+
}
|
|
458
|
+
for (const m of arr)
|
|
459
|
+
c.byId.set(dkMsgKey(m), m);
|
|
460
|
+
publish(peerId);
|
|
426
461
|
} catch (e) {
|
|
427
462
|
} finally {
|
|
428
463
|
inFlight.current.delete(peerId);
|
|
429
464
|
}
|
|
430
|
-
}, []);
|
|
431
|
-
|
|
465
|
+
}, [publish]);
|
|
466
|
+
const loadEarlier = React.useCallback(async (peerId) => {
|
|
467
|
+
const c = cache.current[peerId];
|
|
468
|
+
if (!c || !c.more || c.loadingEarlier)
|
|
469
|
+
return;
|
|
470
|
+
let oldest = Infinity;
|
|
471
|
+
for (const m of c.byId.values())
|
|
472
|
+
if (m.ts < oldest)
|
|
473
|
+
oldest = m.ts;
|
|
474
|
+
if (!Number.isFinite(oldest))
|
|
475
|
+
return;
|
|
476
|
+
c.loadingEarlier = true;
|
|
477
|
+
try {
|
|
478
|
+
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId) + "&limit=" + DK_PAGE + "&before=" + oldest);
|
|
479
|
+
const arr = d.chats && d.chats[peerId] || [];
|
|
480
|
+
await prefetchThreadFiles(arr);
|
|
481
|
+
if (cache.current[peerId] !== c)
|
|
482
|
+
return;
|
|
483
|
+
for (const m of arr)
|
|
484
|
+
c.byId.set(dkMsgKey(m), m);
|
|
485
|
+
c.more = arr.length >= DK_PAGE;
|
|
486
|
+
publish(peerId);
|
|
487
|
+
} catch (e) {
|
|
488
|
+
} finally {
|
|
489
|
+
c.loadingEarlier = false;
|
|
490
|
+
}
|
|
491
|
+
}, [publish]);
|
|
492
|
+
return Object.assign({}, snap, { threads, earlier, refresh, loadThread, loadEarlier });
|
|
432
493
|
}
|
|
433
494
|
Object.assign(window, {
|
|
434
495
|
dkHash,
|
|
@@ -1365,6 +1426,8 @@ window.__DK_UI_VERSION="0.1.103";
|
|
|
1365
1426
|
{
|
|
1366
1427
|
src: dkFileUrl(m.file.name),
|
|
1367
1428
|
alt: m.file.name,
|
|
1429
|
+
loading: "lazy",
|
|
1430
|
+
decoding: "async",
|
|
1368
1431
|
onError: (e) => {
|
|
1369
1432
|
const w = e.currentTarget.closest("[data-media]");
|
|
1370
1433
|
if (w)
|
|
@@ -1568,14 +1631,14 @@ window.__DK_UI_VERSION="0.1.103";
|
|
|
1568
1631
|
letterSpacing: -0.1,
|
|
1569
1632
|
wordBreak: "break-word"
|
|
1570
1633
|
} }, /* @__PURE__ */ React.createElement(MarkdownText, { text: read.text, onNameCard })));
|
|
1571
|
-
})(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time),
|
|
1572
|
-
"
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
), !selMode && m.id && /* @__PURE__ */ React.createElement(
|
|
1634
|
+
})(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), (() => {
|
|
1635
|
+
const sentDone = mine && m.status && m.status !== "queued" && m.status !== "sending" && m.status !== "failed";
|
|
1636
|
+
const via = !mine ? m.via : sentDone ? m.via === "offline" ? "offline" : "online" : null;
|
|
1637
|
+
if (!via)
|
|
1638
|
+
return null;
|
|
1639
|
+
const title = mine ? via === "offline" ? T.viaOutOffline || "posted to the offline relay \u2014 they get it when they come online" : T.viaOutOnline || "sent over a live session (online)" : via === "offline" ? "delivered via express relay (offline)" : "delivered over a live session (online)";
|
|
1640
|
+
return /* @__PURE__ */ React.createElement("span", { title, style: { fontFamily: "var(--mono)", fontSize: 9, color: via === "offline" ? "#f59e0b" : "var(--faint)" } }, via === "offline" ? "\u79BB\u7EBF" : "\u5728\u7EBF");
|
|
1641
|
+
})(), !selMode && m.id && /* @__PURE__ */ React.createElement(
|
|
1579
1642
|
"button",
|
|
1580
1643
|
{
|
|
1581
1644
|
title: T.delete || "delete",
|
|
@@ -1610,11 +1673,13 @@ window.__DK_UI_VERSION="0.1.103";
|
|
|
1610
1673
|
},
|
|
1611
1674
|
/* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }),
|
|
1612
1675
|
/* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.status === "sending" ? T.sendingMsg || "sending" : T.queued || "queued")
|
|
1613
|
-
) : 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)" })))
|
|
1676
|
+
) : 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)" })))
|
|
1614
1677
|
);
|
|
1615
1678
|
}
|
|
1616
|
-
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted, onToggleMute, phone, onBack }) {
|
|
1679
|
+
function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, muted, onToggleMute, phone, onBack }) {
|
|
1617
1680
|
const scrollRef = React.useRef(null);
|
|
1681
|
+
const prependRef = React.useRef(null);
|
|
1682
|
+
const [earlierBusy, setEarlierBusy] = React.useState(false);
|
|
1618
1683
|
const isGroup = React.useMemo(() => dkThreadIsGroup(threadProp), [threadProp]);
|
|
1619
1684
|
const followScrollRef = React.useRef(true);
|
|
1620
1685
|
const fileRef = React.useRef(null);
|
|
@@ -1692,9 +1757,30 @@ window.__DK_UI_VERSION="0.1.103";
|
|
|
1692
1757
|
followScrollRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
|
1693
1758
|
};
|
|
1694
1759
|
React.useLayoutEffect(() => {
|
|
1760
|
+
const el = scrollRef.current;
|
|
1761
|
+
const pre = prependRef.current;
|
|
1762
|
+
if (pre && el && el.scrollHeight !== pre.h) {
|
|
1763
|
+
el.scrollTop = pre.top + (el.scrollHeight - pre.h);
|
|
1764
|
+
prependRef.current = null;
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1695
1767
|
if (followScrollRef.current)
|
|
1696
1768
|
scrollToBottom();
|
|
1697
1769
|
}, [peer.id, thread]);
|
|
1770
|
+
React.useEffect(() => {
|
|
1771
|
+
prependRef.current = null;
|
|
1772
|
+
setEarlierBusy(false);
|
|
1773
|
+
}, [peer.id]);
|
|
1774
|
+
const loadEarlier = () => {
|
|
1775
|
+
const el = scrollRef.current;
|
|
1776
|
+
if (!onLoadEarlier || earlierBusy)
|
|
1777
|
+
return;
|
|
1778
|
+
if (el)
|
|
1779
|
+
prependRef.current = { h: el.scrollHeight, top: el.scrollTop };
|
|
1780
|
+
followScrollRef.current = false;
|
|
1781
|
+
setEarlierBusy(true);
|
|
1782
|
+
Promise.resolve(onLoadEarlier()).finally(() => setEarlierBusy(false));
|
|
1783
|
+
};
|
|
1698
1784
|
const toggleSel = (id) => setSel((s) => {
|
|
1699
1785
|
const n = new Set(s);
|
|
1700
1786
|
n.has(id) ? n.delete(id) : n.add(id);
|
|
@@ -1954,7 +2040,7 @@ ${peer.address}`
|
|
|
1954
2040
|
fontSize: 12.5,
|
|
1955
2041
|
color: "var(--text)"
|
|
1956
2042
|
} }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 13, stroke: 2.2, color: "#d29922" }), /* @__PURE__ */ React.createElement("span", null, T.pendingFriend || "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.")),
|
|
1957
|
-
/* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: phone ? "12px 10px" : "18px 22px", WebkitOverflowScrolling: "touch" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, margin: "0 auto" } }, thread.filter((m) => m.day || !hidden.has(m.id)).map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(
|
|
2043
|
+
/* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: phone ? "12px 10px" : "18px 22px", WebkitOverflowScrolling: "touch" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, margin: "0 auto" } }, hasEarlier && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "center", margin: "4px 0 10px" } }, /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: loadEarlier, disabled: earlierBusy }, earlierBusy ? T.loadingEarlier || "Loading\u2026" : T.loadEarlier || "Load earlier messages")), thread.filter((m) => m.day || !hidden.has(m.id)).map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(
|
|
1958
2044
|
Msg,
|
|
1959
2045
|
{
|
|
1960
2046
|
key: m.id || i,
|
|
@@ -2114,12 +2200,12 @@ ${peer.address}`
|
|
|
2114
2200
|
function ChatEmpty({ T }) {
|
|
2115
2201
|
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));
|
|
2116
2202
|
}
|
|
2117
|
-
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
|
|
2203
|
+
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
|
|
2118
2204
|
const peer = peers.find((p) => p.id === activeId);
|
|
2119
2205
|
if (phone) {
|
|
2120
|
-
return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide: true });
|
|
2206
|
+
return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide: true });
|
|
2121
2207
|
}
|
|
2122
|
-
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
2208
|
+
return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
2123
2209
|
}
|
|
2124
2210
|
Object.assign(window, { ChatTab });
|
|
2125
2211
|
function CopyBtn({ value, copiedText = "Copied", copyFailedText = "Copy failed", copyTitle = "Copy" }) {
|
|
@@ -4882,6 +4968,8 @@ ${peer.address}`
|
|
|
4882
4968
|
autoAcceptLabel: "Auto-accept friend requests",
|
|
4883
4969
|
autoAcceptSub: "Off: requests wait in the sidebar for your approval.",
|
|
4884
4970
|
groupSenderUnknown: "The group relays only this display name. Register Beagle as a group agent (/agent add <beagle address> in the group) to receive full member identities you can add as friends.",
|
|
4971
|
+
loadEarlier: "Load earlier messages",
|
|
4972
|
+
loadingEarlier: "Loading\u2026",
|
|
4885
4973
|
pendingFriend: "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.",
|
|
4886
4974
|
cancelling: "Cancelling\u2026",
|
|
4887
4975
|
cancelled: "Transfer cancelled",
|
|
@@ -4960,6 +5048,8 @@ ${peer.address}`
|
|
|
4960
5048
|
updTitle: "Update available",
|
|
4961
5049
|
updLater: "Later",
|
|
4962
5050
|
brandAbout: "What is Beagle?",
|
|
5051
|
+
viaOutOffline: "posted to the offline relay \u2014 they get it when they come online",
|
|
5052
|
+
viaOutOnline: "sent over a live session (online)",
|
|
4963
5053
|
updNow: "Update now",
|
|
4964
5054
|
updRestart: "Restart",
|
|
4965
5055
|
updBusy: "Updating\u2026 (about a minute)",
|
|
@@ -5072,6 +5162,8 @@ ${peer.address}`
|
|
|
5072
5162
|
autoAcceptLabel: "\u81EA\u52A8\u63A5\u53D7\u597D\u53CB\u8BF7\u6C42",
|
|
5073
5163
|
autoAcceptSub: "\u5173\u95ED\u540E\uFF0C\u597D\u53CB\u8BF7\u6C42\u4F1A\u7B49\u5F85\u4F60\u5728\u4FA7\u8FB9\u680F\u624B\u52A8\u786E\u8BA4\u3002",
|
|
5074
5164
|
groupSenderUnknown: "\u7FA4\u670D\u52A1\u53EA\u8F6C\u53D1\u4E86\u663E\u793A\u540D\uFF0C\u62FF\u4E0D\u5230\u8BE5\u6210\u5458\u7684\u5B8C\u6574\u8EAB\u4EFD\u3002\u5728\u7FA4\u91CC\u6267\u884C /agent add <beagle \u5730\u5740> \u628A Beagle \u6CE8\u518C\u4E3A\u7FA4 agent \u540E\uFF0C\u5373\u53EF\u6536\u5230\u5B8C\u6574\u8EAB\u4EFD\u5E76\u4E00\u952E\u52A0\u597D\u53CB\u3002",
|
|
5165
|
+
loadEarlier: "\u52A0\u8F7D\u66F4\u65E9\u7684\u6D88\u606F",
|
|
5166
|
+
loadingEarlier: "\u52A0\u8F7D\u4E2D\u2026",
|
|
5075
5167
|
pendingFriend: "\u7B49\u5F85\u5BF9\u65B9\u63A5\u53D7\u597D\u53CB\u8BF7\u6C42 \u2014 \u6D88\u606F\u4F1A\u5148\u6392\u961F\uFF0C\u5BF9\u65B9\u63A5\u53D7\u540E\u81EA\u52A8\u9001\u8FBE\u3002",
|
|
5076
5168
|
cancelling: "\u6B63\u5728\u53D6\u6D88\u2026",
|
|
5077
5169
|
cancelled: "\u5DF2\u53D6\u6D88\u4F20\u8F93",
|
|
@@ -5150,6 +5242,8 @@ ${peer.address}`
|
|
|
5150
5242
|
updTitle: "\u53D1\u73B0\u65B0\u7248\u672C",
|
|
5151
5243
|
updLater: "\u7A0D\u540E",
|
|
5152
5244
|
brandAbout: "Beagle \u662F\u4EC0\u4E48?",
|
|
5245
|
+
viaOutOffline: "\u5DF2\u5B58\u5230\u79BB\u7EBF\u4E2D\u8F6C\u2014\u2014\u5BF9\u65B9\u4E0A\u7EBF\u540E\u6536\u5230",
|
|
5246
|
+
viaOutOnline: "\u5728\u7EBF\u76F4\u63A5\u9001\u8FBE",
|
|
5153
5247
|
updNow: "\u7ACB\u5373\u66F4\u65B0",
|
|
5154
5248
|
updRestart: "\u91CD\u542F",
|
|
5155
5249
|
updBusy: "\u66F4\u65B0\u4E2D\u2026(\u7EA6\u4E00\u5206\u949F)",
|
|
@@ -5540,7 +5634,7 @@ ${peer.address}`
|
|
|
5540
5634
|
React.useEffect(() => {
|
|
5541
5635
|
if (!activeId)
|
|
5542
5636
|
return;
|
|
5543
|
-
data.loadThread(activeId);
|
|
5637
|
+
data.loadThread(activeId, { reset: true });
|
|
5544
5638
|
dkApi.markRead(activeId).then(data.refresh);
|
|
5545
5639
|
const iv = setInterval(() => {
|
|
5546
5640
|
data.loadThread(activeId);
|
|
@@ -5690,7 +5784,7 @@ ${peer.address}`
|
|
|
5690
5784
|
fontWeight: 600,
|
|
5691
5785
|
background: tab === n.id ? "var(--accent)" : "transparent",
|
|
5692
5786
|
color: tab === n.id ? "#fff" : "var(--dim)"
|
|
5693
|
-
} }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, onBack: closeConversation, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak, onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
|
|
5787
|
+
} }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, onBack: closeConversation, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), hasEarlier: !!(activeId && data.earlier[activeId]), onLoadEarlier: () => activeId && data.loadEarlier(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak, onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
|
|
5694
5788
|
data.refresh();
|
|
5695
5789
|
return r;
|
|
5696
5790
|
}) })), phone && !inConversation && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", borderTop: "1px solid var(--line)", background: "var(--rail)", paddingBottom: "env(safe-area-inset-bottom, 0px)" } }, phoneNav.map((n) => /* @__PURE__ */ React.createElement(TabBarBtn, { key: n.id, icon: n.icon, label: n.label, active: n.active, badge: n.badge, onClick: n.go }))), data.locked && /* @__PURE__ */ React.createElement(
|
package/dist/embedded-host.js
CHANGED
|
@@ -597,6 +597,9 @@ export class EmbeddedHost {
|
|
|
597
597
|
// unconfirmed message is how a phone ends up with two copies.
|
|
598
598
|
const r = await this.#node.sendText(uid, text);
|
|
599
599
|
this.#messages.setStatus(uid, msg.id, "sent", !r || r.delivery === "acked");
|
|
600
|
+
// Same field incoming messages carry, so the bubble can say which
|
|
601
|
+
// road it took: a relay post is waiting for them, not with them.
|
|
602
|
+
this.#messages.setVia(uid, msg.id, r && r.delivery === "offline" ? "offline" : "online");
|
|
600
603
|
}
|
|
601
604
|
catch (e) {
|
|
602
605
|
// Offline peer or failed delivery: same recovery either way —
|
package/dist/lan-handoff.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export declare const DAEMON_GRACE_MS: number;
|
|
|
13
13
|
* Only a streak of misses that also spans real time is believed. */
|
|
14
14
|
export declare const DAEMON_MISS_STREAK = 3;
|
|
15
15
|
export declare const DAEMON_MISS_WINDOW_MS = 5000;
|
|
16
|
+
/** Is the process named in <dataDir>/daemon.pid still there? A root daemon
|
|
17
|
+
* answers signal 0 with EPERM, which still proves it exists. */
|
|
18
|
+
export declare function daemonPidAlive(dataDir: string): boolean;
|
|
16
19
|
export interface SwitchableHostOptions {
|
|
17
20
|
dataDir: string;
|
|
18
21
|
/** Build a fresh embedded host. Called on first start and on every fallback,
|
|
@@ -30,6 +33,8 @@ export interface SwitchableHostOptions {
|
|
|
30
33
|
graceMs?: number;
|
|
31
34
|
/** Liveness probe, injectable for tests. Defaults to a real connect. */
|
|
32
35
|
probe?: (dataDir: string) => Promise<boolean>;
|
|
36
|
+
/** Test seam: does the daemon's process exist? Default reads <dataDir>/daemon.pid. */
|
|
37
|
+
pidAlive?: (dataDir: string) => boolean;
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
35
40
|
* A PeerHost whose backend can change under it.
|
package/dist/lan-handoff.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// `releasing` is the honest name for the middle state: our peer is down and the
|
|
14
14
|
// daemon is not up yet, so messaging is offline and the UI must say so. Hiding
|
|
15
15
|
// that gap would make dropped messages look like a network fault.
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
16
18
|
import { probeDaemon } from "./ipc.js";
|
|
17
19
|
import { Logger } from "./logger.js";
|
|
18
20
|
/** How often we look for the daemon socket appearing or vanishing. */
|
|
@@ -30,6 +32,26 @@ export const DAEMON_GRACE_MS = 60 * 1000;
|
|
|
30
32
|
* Only a streak of misses that also spans real time is believed. */
|
|
31
33
|
export const DAEMON_MISS_STREAK = 3;
|
|
32
34
|
export const DAEMON_MISS_WINDOW_MS = 5000;
|
|
35
|
+
/** Is the process named in <dataDir>/daemon.pid still there? A root daemon
|
|
36
|
+
* answers signal 0 with EPERM, which still proves it exists. */
|
|
37
|
+
export function daemonPidAlive(dataDir) {
|
|
38
|
+
let pid = 0;
|
|
39
|
+
try {
|
|
40
|
+
pid = Number(readFileSync(join(dataDir, "daemon.pid"), "utf-8").trim());
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
46
|
+
return false;
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return err.code === "EPERM";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
33
55
|
/**
|
|
34
56
|
* A PeerHost whose backend can change under it.
|
|
35
57
|
*
|
|
@@ -49,12 +71,14 @@ export class SwitchablePeerHost {
|
|
|
49
71
|
* grace period; reset the moment it answers again. */
|
|
50
72
|
#daemonMissingSince = 0;
|
|
51
73
|
#probe;
|
|
74
|
+
#pidAlive;
|
|
52
75
|
constructor(initial, opts) {
|
|
53
76
|
this.#inner = initial;
|
|
54
77
|
this.#state = initial.kind === "daemon" ? "daemon" : "embedded";
|
|
55
78
|
this.#opts = opts;
|
|
56
79
|
this.#log = opts.log ?? new Logger({ prefix: "LAN" });
|
|
57
80
|
this.#probe = opts.probe ?? ((dir) => probeDaemon(dir));
|
|
81
|
+
this.#pidAlive = opts.pidAlive ?? daemonPidAlive;
|
|
58
82
|
this.#watch();
|
|
59
83
|
}
|
|
60
84
|
get kind() {
|
|
@@ -144,6 +168,8 @@ export class SwitchablePeerHost {
|
|
|
144
168
|
}
|
|
145
169
|
/** Consecutive missed probes; 0 the moment the daemon answers. */
|
|
146
170
|
#missStreak = 0;
|
|
171
|
+
/** One "not answering but alive" line per stall, not one per tick. */
|
|
172
|
+
#stallLogged = false;
|
|
147
173
|
/** A probe can take its full 2 s timeout against a 1.5 s watch interval.
|
|
148
174
|
* Overlapping ticks would each act on the other's half-made decision. */
|
|
149
175
|
#ticking = false;
|
|
@@ -161,6 +187,10 @@ export class SwitchablePeerHost {
|
|
|
161
187
|
async #step() {
|
|
162
188
|
const daemonUp = await this.#probe(this.#opts.dataDir);
|
|
163
189
|
if (daemonUp) {
|
|
190
|
+
if (this.#stallLogged && this.#daemonMissingSince) {
|
|
191
|
+
this.#log.info(`the daemon answers again after ${((Date.now() - this.#daemonMissingSince) / 1000).toFixed(1)} s`);
|
|
192
|
+
}
|
|
193
|
+
this.#stallLogged = false;
|
|
164
194
|
this.#daemonMissingSince = 0;
|
|
165
195
|
this.#missStreak = 0;
|
|
166
196
|
}
|
|
@@ -189,6 +219,19 @@ export class SwitchablePeerHost {
|
|
|
189
219
|
const missedMs = Date.now() - this.#daemonMissingSince;
|
|
190
220
|
if (this.#missStreak < DAEMON_MISS_STREAK || missedMs < DAEMON_MISS_WINDOW_MS)
|
|
191
221
|
return;
|
|
222
|
+
// Not answering is not gone. On 2026-09-18 mac-dev's daemon (pid
|
|
223
|
+
// unchanged for 19 h) refused every connect for ~20 s at a time while
|
|
224
|
+
// it was busy, and each stretch emptied the friend list. While its
|
|
225
|
+
// process exists it is stalled, and it will answer again; only a
|
|
226
|
+
// dead pid is an absence. Say so once per stall, then keep serving.
|
|
227
|
+
if (this.#pidAlive(this.#opts.dataDir)) {
|
|
228
|
+
if (!this.#stallLogged) {
|
|
229
|
+
this.#stallLogged = true;
|
|
230
|
+
this.#log.info(`the daemon is not answering (${this.#missStreak} probes over ${(missedMs / 1000).toFixed(1)} s) ` +
|
|
231
|
+
`but its process is alive — treating it as busy, not gone`);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
192
235
|
this.#log.info(`the daemon went away — waiting for it to come back (forced daemon backend; ` +
|
|
193
236
|
`${this.#missStreak} probes missed over ${(missedMs / 1000).toFixed(1)} s)`);
|
|
194
237
|
const old = this.#inner;
|
package/dist/message-store.d.ts
CHANGED
|
@@ -70,6 +70,9 @@ export declare class MessageStore {
|
|
|
70
70
|
* resent; the flag is what the UI reads to stop over-claiming.
|
|
71
71
|
*/
|
|
72
72
|
setStatus(peer: string, id: string, status?: "sending" | "queued" | "sent" | "failed", confirmed?: boolean): boolean;
|
|
73
|
+
/** Record how an outgoing text travelled: "online" over a live session,
|
|
74
|
+
* "offline" posted to the express relay for the peer to collect. */
|
|
75
|
+
setVia(peer: string, id: string, via: "online" | "offline"): boolean;
|
|
73
76
|
/** Has this peer ever sent us anything? Used as proof-of-friendship for
|
|
74
77
|
* records that predate the SDK's acceptedAt bookkeeping — a message can
|
|
75
78
|
* only arrive over an established session. */
|
package/dist/message-store.js
CHANGED
|
@@ -86,6 +86,16 @@ export class MessageStore {
|
|
|
86
86
|
this.scheduleSave();
|
|
87
87
|
return true;
|
|
88
88
|
}
|
|
89
|
+
/** Record how an outgoing text travelled: "online" over a live session,
|
|
90
|
+
* "offline" posted to the express relay for the peer to collect. */
|
|
91
|
+
setVia(peer, id, via) {
|
|
92
|
+
const msg = this.byPeer.get(peer)?.find((m) => m.id === id);
|
|
93
|
+
if (!msg || msg.file)
|
|
94
|
+
return false;
|
|
95
|
+
msg.via = via;
|
|
96
|
+
this.scheduleSave();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
89
99
|
/** Has this peer ever sent us anything? Used as proof-of-friendship for
|
|
90
100
|
* records that predate the SDK's acceptedAt bookkeeping — a message can
|
|
91
101
|
* only arrive over an established session. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/beagle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.105",
|
|
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.20",
|
|
39
39
|
"@types/js-yaml": "^4.0.9",
|
|
40
40
|
"@types/node": "^20.11.0",
|
|
41
41
|
"@types/yargs": "^17.0.32",
|