@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.
- package/dist/daemon/ipc.d.ts +5 -1
- package/dist/daemon/ipc.js +6 -0
- package/dist/daemon/server.js +5 -0
- package/dist/ui/desktop/app.js +54 -12
- package/dist/ui/server.js +6 -0
- package/package.json +1 -1
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -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;
|
package/dist/daemon/ipc.js
CHANGED
|
@@ -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":
|
package/dist/daemon/server.js
CHANGED
|
@@ -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
|
},
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" } },
|
|
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.
|
|
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",
|