@decentnetwork/lan 0.1.223 → 0.1.225

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.
@@ -41,6 +41,9 @@ export interface IpcHandlers {
41
41
  friendsReject: (userid: string) => Promise<void>;
42
42
  /** Send a chat message (Carrier text, packet 64) to a friend and log it. */
43
43
  chatSend: (userid: string, text: string) => Promise<void>;
44
+ /** Append a local-only chat history entry. Used for browser-local events such
45
+ * as WebRTC file receive; does not transmit anything to the peer. */
46
+ chatLogLocal: (userid: string, dir: "in" | "out", text: string) => Promise<void>;
44
47
  /** Return persisted chat history. With `peer`, returns just that thread and
45
48
  * honours `before`/`limit` pagination; without, every thread (legacy shape). */
46
49
  chatHistory: (peer?: string, before?: number, limit?: number) => Promise<Record<string, unknown>>;
@@ -115,11 +118,12 @@ export interface IpcHandlers {
115
118
  selfRestart: () => Promise<Record<string, unknown>>;
116
119
  }
117
120
  export interface IpcRequest {
118
- op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "file-retry" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "self-restart";
121
+ op: "friend-request" | "ping" | "diag" | "friends-pending" | "friends-accept" | "friends-reject" | "chat-send" | "chat-log-local" | "chat-history" | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept" | "set-profile" | "file-send" | "file-delete" | "file-cancel" | "file-retry" | "chat-mark-read" | "subscribe" | "sign" | "call-signal" | "call-poll" | "proxy-reload" | "proxy-access" | "self-restart";
119
122
  address?: string;
120
123
  hello?: string;
121
124
  userid?: string;
122
125
  text?: string;
126
+ dir?: "in" | "out";
123
127
  alias?: string;
124
128
  name?: string;
125
129
  description?: string;
@@ -196,6 +196,12 @@ export class IpcServer {
196
196
  await this.handlers.chatSend(req.userid, req.text ?? "");
197
197
  return;
198
198
  }
199
+ case "chat-log-local": {
200
+ if (!req.userid)
201
+ throw new Error("userid is required");
202
+ await this.handlers.chatLogLocal(req.userid, req.dir === "out" ? "out" : "in", req.text ?? "");
203
+ return;
204
+ }
199
205
  case "chat-history":
200
206
  return await this.handlers.chatHistory(req.userid, req.before, req.limit);
201
207
  case "friends-list":
@@ -381,6 +381,11 @@ export class DaemonServer {
381
381
  this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
382
382
  this.logger.info(`Queued text for offline ${userid.slice(0, 8)} (delivers on reconnect)`);
383
383
  },
384
+ chatLogLocal: async (userid, dir, text) => {
385
+ if (!text)
386
+ return;
387
+ this.logChat(userid, dir, text);
388
+ },
384
389
  chatHistory: async (peer, before, limit) => {
385
390
  return { chats: this.messageStore?.history(peer, { before, limit }) ?? {} };
386
391
  },
@@ -220,6 +220,7 @@ function dkCopyLegacy(s) {
220
220
  }
221
221
  const dkApi = {
222
222
  send: (userid, text) => dkPost("/api/chat-send", { userid, text }),
223
+ logLocal: (userid, dir, text) => dkPost("/api/chat-log-local", { userid, dir, text }),
223
224
  add: (address) => dkPost("/api/add", { address }),
224
225
  accept: (userid) => dkPost("/api/accept", { userid }),
225
226
  reject: (userid) => dkPost("/api/reject", { userid }),
@@ -1547,9 +1548,11 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1547
1548
  if (!files || !files.length || !sender) return;
1548
1549
  const file = files[0];
1549
1550
  followScrollRef.current = true;
1550
- setSending({ name: file.name, via: mode });
1551
- Promise.resolve(sender(file)).then((r) => {
1551
+ setSending({ name: file.name, via: mode, pct: 0 });
1552
+ const onProgress = (p) => setSending((s) => s && s.name === file.name ? { ...s, pct: p && p.pct } : s);
1553
+ Promise.resolve(sender(file, onProgress)).then((r) => {
1552
1554
  if (r && r.ok === false) window.alert((lang === "zh" ? "\u53D1\u9001\u5931\u8D25: " : "Send failed: ") + (r.error || ""));
1555
+ else showFlash("ok", mode === "webrtc" ? lang === "zh" ? "WebRTC \u6587\u4EF6\u53D1\u9001\u5B8C\u6210" : "WebRTC file sent" : lang === "zh" ? "\u6587\u4EF6\u5DF2\u63D0\u4EA4\u53D1\u9001" : "File send started");
1553
1556
  }).finally(() => {
1554
1557
  setSending(null);
1555
1558
  requestAnimationFrame(scrollToBottom);
@@ -1633,7 +1636,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
1633
1636
  stroke: 2.2,
1634
1637
  color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
1635
1638
  }
1636
- ), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + (sending.via === "webrtc" ? "WebRTC \xB7 " : "") + sending.name), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
1639
+ ), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto 8px", display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "paperclip", size: 13, stroke: 2, color: "var(--accent)" }), (lang === "zh" ? "\u53D1\u9001\u4E2D: " : "Sending: ") + (sending.via === "webrtc" ? "WebRTC \xB7 " : "") + sending.name + (sending.pct != null ? " \xB7 " + Math.min(100, Math.max(0, sending.pct)).toFixed(1) + "%" : "")), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
1637
1640
  "input",
1638
1641
  {
1639
1642
  ref: fileRef,
@@ -1897,15 +1900,43 @@ function dkWaitForOpen(dc, timeoutMs, describeState) {
1897
1900
  }
1898
1901
  function dkWaitBufferedLow(dc, highWater) {
1899
1902
  if (dc.bufferedAmount < highWater) return Promise.resolve();
1900
- return new Promise((resolve) => {
1903
+ return new Promise((resolve, reject) => {
1901
1904
  const prev = dc.bufferedAmountLowThreshold;
1902
1905
  dc.bufferedAmountLowThreshold = Math.floor(highWater / 2);
1906
+ let poll = null;
1907
+ let timer = null;
1903
1908
  const done = () => {
1909
+ cleanup();
1910
+ resolve();
1911
+ };
1912
+ const cleanup = () => {
1913
+ if (poll) clearInterval(poll);
1914
+ if (timer) clearTimeout(timer);
1904
1915
  dc.removeEventListener("bufferedamountlow", done);
1916
+ dc.removeEventListener("close", onClose);
1917
+ dc.removeEventListener("error", onError);
1905
1918
  dc.bufferedAmountLowThreshold = prev;
1906
- resolve();
1919
+ };
1920
+ const check = () => {
1921
+ if (dc.readyState !== "open") return onClose();
1922
+ if (dc.bufferedAmount < highWater) done();
1923
+ };
1924
+ const onClose = () => {
1925
+ cleanup();
1926
+ reject(new Error("WebRTC data channel closed while draining"));
1927
+ };
1928
+ const onError = () => {
1929
+ cleanup();
1930
+ reject(new Error("WebRTC data channel failed while draining"));
1907
1931
  };
1908
1932
  dc.addEventListener("bufferedamountlow", done);
1933
+ dc.addEventListener("close", onClose);
1934
+ dc.addEventListener("error", onError);
1935
+ poll = setInterval(check, 250);
1936
+ timer = setTimeout(() => {
1937
+ cleanup();
1938
+ reject(new Error("WebRTC data channel drain timed out buffered=" + dc.bufferedAmount));
1939
+ }, 3e4);
1909
1940
  });
1910
1941
  }
1911
1942
  function dkFileDownload(blob, name) {
@@ -2113,10 +2144,12 @@ function useRtcFileController(selfId, onReceivedText) {
2113
2144
  function RtcFileInbox({ T, peers, ctl }) {
2114
2145
  const items = ctl && ctl.incoming || [];
2115
2146
  if (!items.length) return null;
2116
- return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", right: 18, bottom: 18, zIndex: 90, width: 360, display: "flex", flexDirection: "column", gap: 10 } }, items.map((it) => {
2147
+ const closeLabel = T && T.receivedClose || "Close";
2148
+ const downloadLabel = T && T.downloadFile || "Download";
2149
+ return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", right: 72, bottom: 18, zIndex: 90, width: 360, maxWidth: "calc(100vw - 96px)", display: "flex", flexDirection: "column", gap: 10 } }, items.map((it) => {
2117
2150
  const peer = (peers || []).find((p) => p.id === it.peerId || p.userId === it.peerId) || {};
2118
2151
  const who = peer.alias || peer.name || shortKey(it.peerId, 8, 5);
2119
- return /* @__PURE__ */ React.createElement("div", { key: it.id, style: { border: "1px solid var(--line)", background: "var(--panel)", borderRadius: 12, padding: 12, boxShadow: "0 14px 40px rgba(0,0,0,0.35)" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", marginBottom: 6 } }, T && T.receivedFile || "Received file", " \xB7 ", who), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 13, fontWeight: 700, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, it.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", marginTop: 3 } }, dkFileSize(it.size)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 10 } }, /* @__PURE__ */ React.createElement("button", { onClick: () => ctl.dismissIncoming(it.id), style: { height: 30, borderRadius: 8, border: "1px solid var(--line)", background: "transparent", color: "var(--dim)", cursor: "pointer" } }, T && T.cancel || "Close"), /* @__PURE__ */ React.createElement("a", { href: it.url, download: it.name, onClick: () => setTimeout(() => ctl.dismissIncoming(it.id), 500), style: { height: 30, borderRadius: 8, padding: "6px 10px", background: "var(--accent)", color: "#fff", textDecoration: "none", fontFamily: "var(--mono)", fontSize: 12 } }, T && T.open || "Download")));
2152
+ return /* @__PURE__ */ React.createElement("div", { key: it.id, style: { border: "1px solid var(--line)", background: "var(--panel)", borderRadius: 12, padding: 12, boxShadow: "0 14px 40px rgba(0,0,0,0.35)" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", marginBottom: 6 } }, T && T.receivedFile || "Received file", " \xB7 ", who), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 13, fontWeight: 700, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, it.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", marginTop: 3 } }, dkFileSize(it.size)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 10, paddingRight: 2 } }, /* @__PURE__ */ React.createElement("button", { onClick: () => ctl.dismissIncoming(it.id), style: { height: 30, minWidth: 58, borderRadius: 8, border: "1px solid var(--line)", background: "transparent", color: "var(--dim)", cursor: "pointer" } }, closeLabel), /* @__PURE__ */ React.createElement("a", { href: it.url, download: it.name, onClick: () => setTimeout(() => ctl.dismissIncoming(it.id), 500), style: { height: 30, minWidth: 76, textAlign: "center", borderRadius: 8, padding: "6px 10px", background: "var(--accent)", color: "#fff", textDecoration: "none", fontFamily: "var(--mono)", fontSize: 12 } }, downloadLabel)));
2120
2153
  }));
2121
2154
  }
2122
2155
  Object.assign(window, { useRtcFileController, RtcFileInbox });
@@ -2451,6 +2484,8 @@ const STR = {
2451
2484
  block: "Block peer",
2452
2485
  remove: "Remove friend",
2453
2486
  receivedFile: "Received file",
2487
+ receivedClose: "Close",
2488
+ downloadFile: "Download",
2454
2489
  attach: "Attach file",
2455
2490
  message: "Message",
2456
2491
  send: "Send",
@@ -2543,6 +2578,8 @@ const STR = {
2543
2578
  block: "\u62C9\u9ED1",
2544
2579
  remove: "\u5220\u9664\u597D\u53CB",
2545
2580
  receivedFile: "\u6536\u5230\u6587\u4EF6",
2581
+ receivedClose: "\u5173\u95ED",
2582
+ downloadFile: "\u4E0B\u8F7D",
2546
2583
  attach: "\u9644\u4EF6",
2547
2584
  message: "\u6D88\u606F",
2548
2585
  send: "\u53D1\u9001",
@@ -2647,7 +2684,12 @@ function DkApp() {
2647
2684
  const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
2648
2685
  const callCtl = useCallController(me.userId);
2649
2686
  const onCall = (peerId, video) => callCtl.start(peerId, !!video);
2650
- const rtcFileCtl = useRtcFileController(me.userId);
2687
+ const rtcFileCtl = useRtcFileController(me.userId, (peerId, text) => {
2688
+ dkApi.logLocal(peerId, "in", text).then(() => {
2689
+ data.loadThread(peerId);
2690
+ data.refresh();
2691
+ });
2692
+ });
2651
2693
  React.useEffect(() => {
2652
2694
  if (!activeId && peers.length) setActiveId(peers[0].id);
2653
2695
  }, [peers, activeId]);
@@ -2682,9 +2724,9 @@ function DkApp() {
2682
2724
  if (!activeId || !text || !text.trim()) return;
2683
2725
  dkApi.send(activeId, text.trim()).then(() => data.loadThread(activeId)).then(data.refresh);
2684
2726
  };
2685
- const onSendFile = (file) => {
2727
+ const onSendFile = (file, onProgress) => {
2686
2728
  if (!activeId || !file) return Promise.resolve({ ok: false });
2687
- return rtcFileCtl.sendFile(activeId, file).then((r) => {
2729
+ return rtcFileCtl.sendFile(activeId, file, onProgress).then((r) => {
2688
2730
  if (r && r.ok && r.via === "webrtc") {
2689
2731
  return dkApi.send(activeId, (t.lang === "zh" ? "\u5DF2\u901A\u8FC7 WebRTC \u53D1\u9001\u6587\u4EF6: " : "sent file via WebRTC: ") + file.name).then(() => ({ ok: true, via: "webrtc" }));
2690
2732
  }
@@ -2695,9 +2737,9 @@ function DkApp() {
2695
2737
  return r;
2696
2738
  });
2697
2739
  };
2698
- const onSendRtcFile = (file) => {
2740
+ const onSendRtcFile = (file, onProgress) => {
2699
2741
  if (!activeId || !file) return Promise.resolve({ ok: false, error: "no active peer or file" });
2700
- return rtcFileCtl.sendFile(activeId, file).then((r) => {
2742
+ return rtcFileCtl.sendFile(activeId, file, onProgress).then((r) => {
2701
2743
  if (r && r.ok && r.via === "webrtc") {
2702
2744
  return dkApi.send(activeId, (t.lang === "zh" ? "\u5DF2\u901A\u8FC7 WebRTC \u53D1\u9001\u6587\u4EF6: " : "sent file via WebRTC: ") + file.name).then(() => ({ ok: true, via: "webrtc" }));
2703
2745
  }
package/dist/ui/server.js CHANGED
@@ -650,6 +650,12 @@ export function startFriendUi(opts) {
650
650
  sendJson(res, r.ok ? 200 : 400, r);
651
651
  return;
652
652
  }
653
+ if (req.method === "POST" && url === "/api/chat-log-local") {
654
+ const { userid, dir, text } = await readBody(req);
655
+ const r = await opts.call({ op: "chat-log-local", userid, dir: dir === "out" ? "out" : "in", text });
656
+ sendJson(res, r.ok ? 200 : 400, r);
657
+ return;
658
+ }
653
659
  if (req.method === "POST" && url === "/api/chat-mark-read") {
654
660
  const { userid } = await readBody(req);
655
661
  const r = await opts.call({ op: "chat-mark-read", userid });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.223",
3
+ "version": "0.1.225",
4
4
  "description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",