@decentnetwork/beagle 0.1.7 → 0.1.8

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.
@@ -1297,7 +1297,7 @@ function PeerRow({ peer, T, active, onClick }) {
1297
1297
  textOverflow: "ellipsis",
1298
1298
  flex: 1,
1299
1299
  minWidth: 0
1300
- } }, 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 }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
1300
+ } }, 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 })));
1301
1301
  }
1302
1302
  function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }) {
1303
1303
  const [q, setQ] = React.useState("");
@@ -2261,6 +2261,17 @@ ${peer.address}`
2261
2261
  setMenu(false);
2262
2262
  onRemove(peer);
2263
2263
  } }))))),
2264
+ peer.pending && /* @__PURE__ */ React.createElement("div", { style: {
2265
+ flexShrink: 0,
2266
+ padding: "7px 16px",
2267
+ display: "flex",
2268
+ alignItems: "center",
2269
+ gap: 8,
2270
+ background: "color-mix(in srgb, #d29922 12%, var(--panel))",
2271
+ borderBottom: "1px solid var(--line)",
2272
+ fontSize: 12.5,
2273
+ color: "var(--text)"
2274
+ } }, /* @__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.")),
2264
2275
  /* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: "18px 22px" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, 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(
2265
2276
  Msg,
2266
2277
  {
@@ -3478,6 +3489,7 @@ const STR = {
3478
3489
  open: "open",
3479
3490
  retry: "Retry",
3480
3491
  cancel: "Cancel",
3492
+ pendingFriend: "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.",
3481
3493
  cancelling: "Cancelling\u2026",
3482
3494
  cancelled: "Transfer cancelled",
3483
3495
  retrying: "Retrying\u2026",
@@ -3573,6 +3585,7 @@ const STR = {
3573
3585
  open: "\u6253\u5F00",
3574
3586
  retry: "\u91CD\u8BD5",
3575
3587
  cancel: "\u53D6\u6D88",
3588
+ 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",
3576
3589
  cancelling: "\u6B63\u5728\u53D6\u6D88\u2026",
3577
3590
  cancelled: "\u5DF2\u53D6\u6D88\u4F20\u8F93",
3578
3591
  retrying: "\u6B63\u5728\u91CD\u8BD5\u2026",
@@ -130,8 +130,21 @@ export class EmbeddedHost {
130
130
  });
131
131
  this.#node.on("friend-connection", (e) => {
132
132
  this.#events.emit("event", { type: "presence", userid: e.pubkey, status: e.status });
133
- if (e.status === "connected")
134
- void this.#flushOutbox(e.pubkey);
133
+ // The connected event can arrive a beat before the session accepts
134
+ // traffic — observed live: flush at connect said "friend is offline",
135
+ // and nothing retried until the user's next send, so a queued message
136
+ // sat visibly stuck despite the peer being online. One delayed retry
137
+ // covers that gap; the per-send chain and later reconnects cover the
138
+ // rest.
139
+ if (e.status === "connected") {
140
+ void this.#flushOutbox(e.pubkey)
141
+ .catch(() => { })
142
+ .then(() => {
143
+ if (this.#messages.queuedOutgoing(e.pubkey).length === 0)
144
+ return;
145
+ return new Promise((r) => setTimeout(r, 4000)).then(() => this.#flushOutbox(e.pubkey).catch(() => { }));
146
+ });
147
+ }
135
148
  });
136
149
  this.#node.on("call-signal", (evt) => {
137
150
  this.#callQueue.push({ userid: evt.pubkey, data: Buffer.from(evt.data).toString("utf-8") });
@@ -312,6 +325,18 @@ export class EmbeddedHost {
312
325
  alias: meta?.alias,
313
326
  name: meta?.alias || f.name || f.carrierId,
314
327
  status: f.status,
328
+ // Shareable Carrier address — the SDK derives it when the
329
+ // record lacks one, so this is always populated (peer ≥0.1.138).
330
+ address: f.address,
331
+ // Friendship established vs merely requested. acceptedAt is the
332
+ // SDK's cryptographic proof (session established, or their
333
+ // DHT-PK announce); an inbound message is equally conclusive
334
+ // and covers records that predate acceptedAt. Without this the
335
+ // UI shows a live-looking chat while every send quietly queues
336
+ // (INBOX 2026-08-05: 13 messages, zero delivered, no clue why).
337
+ accepted: !!f.acceptedAt ||
338
+ f.status === "online" ||
339
+ this.#messages.hasInbound(f.carrierId),
315
340
  lastSeen: f.acceptedAt,
316
341
  pinned: meta?.pinned ?? false,
317
342
  lastMessage: lastMsg ? { dir: lastMsg.dir, text: lastMsg.text, ts: lastMsg.ts } : undefined,
@@ -59,6 +59,10 @@ export declare class MessageStore {
59
59
  * Used to flip a "queued" message to delivered once it's actually sent.
60
60
  * No-op if the id isn't found or is a file entry. Returns true if patched. */
61
61
  setStatus(peer: string, id: string, status?: "sending" | "queued" | "sent" | "failed"): boolean;
62
+ /** Has this peer ever sent us anything? Used as proof-of-friendship for
63
+ * records that predate the SDK's acceptedAt bookkeeping — a message can
64
+ * only arrive over an established session. */
65
+ hasInbound(peer: string): boolean;
62
66
  /** Outgoing messages still awaiting delivery (peer was offline), oldest
63
67
  * first — both queued text and queued file chips. The daemon drains this
64
68
  * on a friend's reconnect. */
@@ -74,6 +74,12 @@ export class MessageStore {
74
74
  this.scheduleSave();
75
75
  return true;
76
76
  }
77
+ /** Has this peer ever sent us anything? Used as proof-of-friendship for
78
+ * records that predate the SDK's acceptedAt bookkeeping — a message can
79
+ * only arrive over an established session. */
80
+ hasInbound(peer) {
81
+ return (this.byPeer.get(peer) ?? []).some((m) => m.dir === "in");
82
+ }
77
83
  /** Outgoing messages still awaiting delivery (peer was offline), oldest
78
84
  * first — both queued text and queued file chips. The daemon drains this
79
85
  * on a friend's reconnect. */
package/dist/server.js CHANGED
@@ -20,6 +20,7 @@ import { dirname, join } from "node:path";
20
20
  import { INSTALL_COMMAND as LAN_INSTALL_COMMAND } from "./lan-handoff.js";
21
21
  import yaml from "js-yaml";
22
22
  import { DEFAULT_EXITS } from "./exits.js";
23
+ import * as peerAddr from "@decentnetwork/peer";
23
24
  // Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
24
25
  // scripts/build-ui.mjs emits it next to this compiled module at dist/ui/desktop/.
25
26
  const DESKTOP_DIR = join(dirname(fileURLToPath(import.meta.url)), "desktop");
@@ -690,6 +691,11 @@ export function startBeagleServer(opts) {
690
691
  // another friend (userid only works once already friends).
691
692
  address: f.address || "",
692
693
  online,
694
+ // Friendship not yet accepted by the other side. Only trusted when
695
+ // the backend states it explicitly (embedded does); a daemon
696
+ // backend that doesn't report it defaults to established, so the
697
+ // banner can never wrongly appear on an old backend.
698
+ pending: f.accepted === false,
693
699
  via: online ? viaFromTransport(sess?.transport) : null,
694
700
  ping: null,
695
701
  ip: ipByUserid.get(uid) ?? "",
@@ -965,7 +971,18 @@ export function startBeagleServer(opts) {
965
971
  return;
966
972
  }
967
973
  if (req.method === "POST" && url === "/api/add") {
968
- const { address } = await readBody(req);
974
+ let { address } = await readBody(req);
975
+ // Accept a 44-char userid too: the address is userid's pubkey plus a
976
+ // derivable suffix (nospam=0 network-wide + checksum), and users
977
+ // frequently have only the userid — it's what every other surface
978
+ // shows. Deriving here beats rejecting with "not an address".
979
+ try {
980
+ const raw = String(address ?? "").trim();
981
+ const bytes = peerAddr.base58ToBytes(raw);
982
+ if (bytes.length === 32)
983
+ address = peerAddr.carrierAddressFromPublicKey(bytes, 0);
984
+ }
985
+ catch { /* not base58 — let the backend judge it as-is */ }
969
986
  // A friend-request is fire-and-forget: it's delivered when the peer is
970
987
  // online and only becomes a friend once they accept. Don't let the UI
971
988
  // hang on the network round-trip (DHT lookup / unreachable peer) — kick
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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",
@@ -27,7 +27,7 @@
27
27
  "dependencies": {
28
28
  "@decentnetwork/chat-components": "^0.1.3",
29
29
  "@decentnetwork/lan": "^0.1.256",
30
- "@decentnetwork/peer": "^0.1.136",
30
+ "@decentnetwork/peer": "^0.1.138",
31
31
  "@decentnetwork/peer-webrtc": "^0.2.10",
32
32
  "js-yaml": "^4.1.0",
33
33
  "yargs": "^17.7.2"