@decentnetwork/lan 0.1.212 → 0.1.215
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.
|
@@ -92,6 +92,11 @@ export declare class PeerManager extends EventEmitter {
|
|
|
92
92
|
acceptFile(userid: string, fileNumber: number): void;
|
|
93
93
|
/** Cancel an in-flight send by content fileId. Returns true if it was found. */
|
|
94
94
|
cancelSend(userid: string, fileId: string): boolean;
|
|
95
|
+
/** Cancel in-flight sends matching size/name when activeSends lost the fileId. */
|
|
96
|
+
cancelSendsMatching(userid: string, match: {
|
|
97
|
+
name?: string;
|
|
98
|
+
size: number;
|
|
99
|
+
}): string[];
|
|
95
100
|
/**
|
|
96
101
|
* Send an outbound friend request to a Carrier address (NOT a bare
|
|
97
102
|
* userid — sendFriendRequest needs the address form because it
|
|
@@ -209,6 +209,10 @@ export class PeerManager extends EventEmitter {
|
|
|
209
209
|
cancelSend(userid, fileId) {
|
|
210
210
|
return this.peer?.cancelFileById(userid, fileId, true) ?? false;
|
|
211
211
|
}
|
|
212
|
+
/** Cancel in-flight sends matching size/name when activeSends lost the fileId. */
|
|
213
|
+
cancelSendsMatching(userid, match) {
|
|
214
|
+
return this.peer?.cancelSendsMatching(userid, match) ?? [];
|
|
215
|
+
}
|
|
212
216
|
/**
|
|
213
217
|
* Send an outbound friend request to a Carrier address (NOT a bare
|
|
214
218
|
* userid — sendFriendRequest needs the address form because it
|
package/dist/daemon/server.js
CHANGED
|
@@ -543,16 +543,52 @@ export class DaemonServer {
|
|
|
543
543
|
return { deleted: removed.length, files };
|
|
544
544
|
},
|
|
545
545
|
fileCancel: async (userid, ids) => {
|
|
546
|
+
// Cancel must ACTUALLY stop the peer pump. An earlier path set
|
|
547
|
+
// stopped=true after only deleting activeSends even when cancelSend
|
|
548
|
+
// returned false — UI said cancelled, file-cc kept running, and the
|
|
549
|
+
// next send looked dead. Try the active registry, content hash, then
|
|
550
|
+
// an exact name+size peer fallback before reporting success/failure.
|
|
551
|
+
const fs = await import("fs/promises");
|
|
552
|
+
const { createHash } = await import("crypto");
|
|
546
553
|
let n = 0;
|
|
547
554
|
for (const id of ids) {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
555
|
+
const msg = this.messageStore?.get(userid, id);
|
|
556
|
+
const wasActive = msg?.file?.status === "sending" || msg?.file?.status === "queued";
|
|
557
|
+
let killed = false;
|
|
558
|
+
const tryKill = (fid) => {
|
|
559
|
+
if (!fid)
|
|
560
|
+
return;
|
|
551
561
|
if (this.peerManager?.cancelSend(userid, fid))
|
|
552
|
-
|
|
562
|
+
killed = true;
|
|
553
563
|
this.activeSends.delete(fid);
|
|
564
|
+
};
|
|
565
|
+
for (const [fid, v] of [...this.activeSends]) {
|
|
566
|
+
if (v.peer !== userid || v.msgId !== id)
|
|
567
|
+
continue;
|
|
568
|
+
tryKill(fid);
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const data = await fs.readFile(resolve(this.outboxDir, id));
|
|
572
|
+
tryKill(createHash("sha256").update(data).digest("hex"));
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// outbox entry may already be gone
|
|
576
|
+
}
|
|
577
|
+
if (msg?.file) {
|
|
578
|
+
const matched = this.peerManager?.cancelSendsMatching(userid, {
|
|
579
|
+
name: msg.file.name,
|
|
580
|
+
size: msg.file.size,
|
|
581
|
+
}) ?? [];
|
|
582
|
+
for (const fid of matched)
|
|
583
|
+
this.activeSends.delete(fid);
|
|
584
|
+
if (matched.length)
|
|
585
|
+
killed = true;
|
|
586
|
+
}
|
|
587
|
+
if (killed || wasActive) {
|
|
554
588
|
this.messageStore?.patchFile(userid, id, { status: "failed", sent: 0, kbps: undefined });
|
|
589
|
+
n++;
|
|
555
590
|
}
|
|
591
|
+
this.logger.info(`file-cancel peer=${userid.slice(0, 8)} msg=${id} killed=${killed ? 1 : 0} wasActive=${wasActive ? 1 : 0}`);
|
|
556
592
|
}
|
|
557
593
|
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
558
594
|
this.logger.info(`Cancelled ${n} send(s) for ${userid.slice(0, 8)}`);
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -16,6 +16,7 @@ const ICON_PATHS = {
|
|
|
16
16
|
checkCheck: '<path d="M18 6 7 17l-5-5"/><path d="m22 10-7.6 7.6L13 16"/>',
|
|
17
17
|
copy: '<rect x="9" y="9" width="12" height="12" rx="2.4"/><path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"/>',
|
|
18
18
|
refresh: '<path d="M21 12a9 9 0 1 1-2.6-6.3"/><path d="M21 4v4.5h-4.5"/>',
|
|
19
|
+
clock: '<circle cx="12" cy="12" r="9.5"/><path d="M12 7.5V12l3 2"/>',
|
|
19
20
|
edit: '<path d="M11 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4Z"/>',
|
|
20
21
|
qr: '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><path d="M14 14h3v3M21 14v.01M14 21h.01M17 21h.01M21 17v4"/>',
|
|
21
22
|
// ---- app tiles ----
|
|
@@ -1072,7 +1073,7 @@ const mediaBtnStyle = {
|
|
|
1072
1073
|
justifyContent: "center",
|
|
1073
1074
|
lineHeight: 0
|
|
1074
1075
|
};
|
|
1075
|
-
function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, selected, onToggleSel }) {
|
|
1076
|
+
function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, selected, onToggleSel, busy }) {
|
|
1076
1077
|
const mine = m.from === "me";
|
|
1077
1078
|
return /* @__PURE__ */ React.createElement(
|
|
1078
1079
|
"div",
|
|
@@ -1169,28 +1170,31 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
|
|
|
1169
1170
|
cursor: mine ? "default" : "pointer"
|
|
1170
1171
|
};
|
|
1171
1172
|
const icon = /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 7, flexShrink: 0, background: mine ? "rgba(255,255,255,0.16)" : "var(--chip)", display: "flex", alignItems: "center", justifyContent: "center", color: mine ? "#fff" : "var(--accent)" } }, /* @__PURE__ */ React.createElement(Icon, { name: m.file.media === "image" ? "image" : m.file.media === "video" ? "video" : m.file.media === "audio" ? "play" : "file", size: 18, stroke: 1.9 }));
|
|
1172
|
-
const meta = /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, m.file.status === "queued" ? `${m.file.size} \xB7 ${T.queued || "queued"}` : m.file.status === "sending" ? `${m.file.size} \xB7 ${dkPct(m.file.pct)}${m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : ""}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
|
|
1173
|
+
const meta = /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: mine ? "#fff" : "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--faint)", marginTop: 1 } }, busy === "cancel" ? `${m.file.size} \xB7 ${T.cancelling || "Cancelling\u2026"}` : busy === "retry" ? `${m.file.size} \xB7 ${T.retrying || "Retrying\u2026"}` : m.file.status === "queued" ? `${m.file.size} \xB7 ${T.queued || "queued"}` : m.file.status === "sending" ? `${m.file.size} \xB7 ${dkPct(m.file.pct)}${m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : ""}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
|
|
1173
1174
|
if (mine) {
|
|
1174
|
-
|
|
1175
|
+
const btnBusy = !!busy;
|
|
1176
|
+
return /* @__PURE__ */ React.createElement("div", { style: { ...cardStyle, opacity: btnBusy ? 0.85 : 1 } }, icon, meta, busy === "cancel" || busy === "retry" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "queued" ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }) : m.file.status === "sending" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "rgba(255,255,255,0.9)", whiteSpace: "nowrap" } }, dkPct(m.file.pct), m.file.kbps ? " \xB7 " + dkSpeed(m.file.kbps) : "") : m.file.status === "failed" ? /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 16, stroke: 2.4, color: "rgba(255,200,190,0.95)" }) : /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 16, stroke: 2, color: "rgba(255,255,255,0.85)" }), (m.file.status === "sending" || m.file.status === "queued" || busy === "cancel") && m.id && /* @__PURE__ */ React.createElement(
|
|
1175
1177
|
"button",
|
|
1176
1178
|
{
|
|
1177
1179
|
title: T.cancel || "cancel",
|
|
1180
|
+
disabled: btnBusy,
|
|
1178
1181
|
onClick: (e) => {
|
|
1179
1182
|
e.stopPropagation();
|
|
1180
|
-
onCancel(m.id);
|
|
1183
|
+
if (!btnBusy) onCancel(m.id);
|
|
1181
1184
|
},
|
|
1182
|
-
style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0 }
|
|
1185
|
+
style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: btnBusy ? "wait" : "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0, opacity: btnBusy ? 0.5 : 1 }
|
|
1183
1186
|
},
|
|
1184
1187
|
/* @__PURE__ */ React.createElement(Icon, { name: "x", size: 13, stroke: 2.4, color: "#fff" })
|
|
1185
1188
|
), m.file.status !== "sent" && m.id && /* @__PURE__ */ React.createElement(
|
|
1186
1189
|
"button",
|
|
1187
1190
|
{
|
|
1188
1191
|
title: T.retry || "retry",
|
|
1192
|
+
disabled: btnBusy,
|
|
1189
1193
|
onClick: (e) => {
|
|
1190
1194
|
e.stopPropagation();
|
|
1191
|
-
onRetry(m.id);
|
|
1195
|
+
if (!btnBusy) onRetry(m.id);
|
|
1192
1196
|
},
|
|
1193
|
-
style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0 }
|
|
1197
|
+
style: { background: "rgba(255,255,255,0.16)", border: "none", borderRadius: 6, cursor: btnBusy ? "wait" : "pointer", padding: 3, marginLeft: 2, display: "inline-flex", flexShrink: 0, opacity: btnBusy ? 0.5 : 1 }
|
|
1194
1198
|
},
|
|
1195
1199
|
/* @__PURE__ */ React.createElement(Icon, { name: "refresh", size: 13, stroke: 2.2, color: "#fff" })
|
|
1196
1200
|
));
|
|
@@ -1254,36 +1258,130 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
|
|
|
1254
1258
|
), mine && m.status === "queued" ? /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 2 }, title: "waiting for peer \u2014 will send when they're online" }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, T.queued || "queued")) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
|
|
1255
1259
|
);
|
|
1256
1260
|
}
|
|
1257
|
-
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }) {
|
|
1261
|
+
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
|
|
1258
1262
|
const scrollRef = React.useRef(null);
|
|
1259
1263
|
const fileRef = React.useRef(null);
|
|
1260
|
-
const thread = threadProp && threadProp.length ? threadProp : [{ day: "Today" }, { from: "them", time: "\u2014", text: lang === "zh" ? "\u6682\u65E0\u6D88\u606F\u8BB0\u5F55\uFF0C\u53D1\u4E2A\u6D88\u606F\u6253\u4E2A\u62DB\u547C\u5427\u3002" : "No messages yet. Say hi." }];
|
|
1261
|
-
React.useEffect(() => {
|
|
1262
|
-
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
1263
|
-
}, [peer.id, threadProp]);
|
|
1264
1264
|
const [menu, setMenu] = React.useState(false);
|
|
1265
1265
|
const [selMode, setSelMode] = React.useState(false);
|
|
1266
1266
|
const [sel, setSel] = React.useState(() => /* @__PURE__ */ new Set());
|
|
1267
1267
|
const [hidden, setHidden] = React.useState(() => /* @__PURE__ */ new Set());
|
|
1268
|
+
const [fileBusy, setFileBusy] = React.useState(() => ({}));
|
|
1269
|
+
const [filePatch, setFilePatch] = React.useState(() => ({}));
|
|
1270
|
+
const [flash, setFlash] = React.useState(null);
|
|
1271
|
+
const flashTimer = React.useRef(null);
|
|
1272
|
+
const showFlash = (kind, msg) => {
|
|
1273
|
+
if (flashTimer.current) clearTimeout(flashTimer.current);
|
|
1274
|
+
setFlash({ kind, msg });
|
|
1275
|
+
flashTimer.current = setTimeout(() => setFlash(null), 2500);
|
|
1276
|
+
};
|
|
1277
|
+
React.useEffect(() => () => {
|
|
1278
|
+
if (flashTimer.current) clearTimeout(flashTimer.current);
|
|
1279
|
+
}, []);
|
|
1268
1280
|
React.useEffect(() => {
|
|
1269
1281
|
setSelMode(false);
|
|
1270
1282
|
setSel(/* @__PURE__ */ new Set());
|
|
1271
1283
|
setHidden(/* @__PURE__ */ new Set());
|
|
1284
|
+
setFileBusy({});
|
|
1285
|
+
setFilePatch({});
|
|
1286
|
+
setFlash(null);
|
|
1272
1287
|
}, [peer.id]);
|
|
1288
|
+
React.useEffect(() => {
|
|
1289
|
+
setFilePatch((p) => {
|
|
1290
|
+
let changed = false;
|
|
1291
|
+
const next = { ...p };
|
|
1292
|
+
for (const m of threadProp || []) {
|
|
1293
|
+
if (!m || !m.id || !next[m.id] || !m.file) continue;
|
|
1294
|
+
if (m.file.status === next[m.id].status) {
|
|
1295
|
+
delete next[m.id];
|
|
1296
|
+
changed = true;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return changed ? next : p;
|
|
1300
|
+
});
|
|
1301
|
+
}, [threadProp]);
|
|
1302
|
+
const baseThread = threadProp && threadProp.length ? threadProp : [{ day: "Today" }, { from: "them", time: "\u2014", text: lang === "zh" ? "\u6682\u65E0\u6D88\u606F\u8BB0\u5F55\uFF0C\u53D1\u4E2A\u6D88\u606F\u6253\u4E2A\u62DB\u547C\u5427\u3002" : "No messages yet. Say hi." }];
|
|
1303
|
+
const thread = React.useMemo(() => {
|
|
1304
|
+
if (!Object.keys(filePatch).length) return baseThread;
|
|
1305
|
+
return baseThread.map((m) => {
|
|
1306
|
+
if (!m || !m.id || !m.file || !filePatch[m.id]) return m;
|
|
1307
|
+
return { ...m, file: { ...m.file, ...filePatch[m.id] } };
|
|
1308
|
+
});
|
|
1309
|
+
}, [baseThread, filePatch]);
|
|
1310
|
+
React.useEffect(() => {
|
|
1311
|
+
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
1312
|
+
}, [peer.id, threadProp]);
|
|
1273
1313
|
const toggleSel = (id) => setSel((s) => {
|
|
1274
1314
|
const n = new Set(s);
|
|
1275
1315
|
n.has(id) ? n.delete(id) : n.add(id);
|
|
1276
1316
|
return n;
|
|
1277
1317
|
});
|
|
1318
|
+
const clearBusy = (id) => setFileBusy((b) => {
|
|
1319
|
+
const n = { ...b };
|
|
1320
|
+
delete n[id];
|
|
1321
|
+
return n;
|
|
1322
|
+
});
|
|
1278
1323
|
const doCancel = (id) => {
|
|
1279
|
-
if (id
|
|
1280
|
-
});
|
|
1324
|
+
if (!id || fileBusy[id]) return;
|
|
1325
|
+
setFileBusy((b) => ({ ...b, [id]: "cancel" }));
|
|
1326
|
+
showFlash("ok", T.cancelling || "Cancelling\u2026");
|
|
1327
|
+
dkApi.cancelSend(peer.userId, [id]).then((r) => {
|
|
1328
|
+
if (!r || r.ok === false) {
|
|
1329
|
+
const msg = (T.cancelFailed || "Cancel failed") + ": " + (r && r.error || "daemon unavailable");
|
|
1330
|
+
showFlash("err", msg);
|
|
1331
|
+
window.alert(msg);
|
|
1332
|
+
} else if (!r.cancelled) {
|
|
1333
|
+
const msg = T.cancelMissing || "No active transfer was found. Refresh and try again.";
|
|
1334
|
+
showFlash("err", msg);
|
|
1335
|
+
window.alert(msg);
|
|
1336
|
+
} else {
|
|
1337
|
+
setFilePatch((p) => ({ ...p, [id]: { status: "failed", pct: void 0, kbps: void 0 } }));
|
|
1338
|
+
showFlash("ok", T.cancelled || "Transfer cancelled");
|
|
1339
|
+
if (onReloadThread) onReloadThread();
|
|
1340
|
+
}
|
|
1341
|
+
}).catch((e) => {
|
|
1342
|
+
const msg = (T.cancelFailed || "Cancel failed") + ": " + String(e);
|
|
1343
|
+
showFlash("err", msg);
|
|
1344
|
+
window.alert(msg);
|
|
1345
|
+
}).finally(() => clearBusy(id));
|
|
1281
1346
|
};
|
|
1282
1347
|
const doRetry = (id) => {
|
|
1283
|
-
if (!id) return;
|
|
1348
|
+
if (!id || fileBusy[id]) return;
|
|
1349
|
+
setFileBusy((b) => ({ ...b, [id]: "retry" }));
|
|
1350
|
+
setFilePatch((p) => ({ ...p, [id]: { status: "sending", pct: 0, kbps: void 0 } }));
|
|
1351
|
+
showFlash("ok", T.retrying || "Retrying\u2026");
|
|
1284
1352
|
dkApi.retrySend(peer.userId, [id]).then((r) => {
|
|
1285
|
-
if (r && r.ok === false)
|
|
1286
|
-
|
|
1353
|
+
if (r && r.ok === false) {
|
|
1354
|
+
setFilePatch((p) => {
|
|
1355
|
+
const n = { ...p };
|
|
1356
|
+
delete n[id];
|
|
1357
|
+
return n;
|
|
1358
|
+
});
|
|
1359
|
+
const msg = (T.retryFailed || "Retry failed") + ": " + (r.error || "");
|
|
1360
|
+
showFlash("err", msg);
|
|
1361
|
+
window.alert(msg);
|
|
1362
|
+
} else if (!r || !r.retried) {
|
|
1363
|
+
setFilePatch((p) => {
|
|
1364
|
+
const n = { ...p };
|
|
1365
|
+
delete n[id];
|
|
1366
|
+
return n;
|
|
1367
|
+
});
|
|
1368
|
+
const msg = T.retryMissing || "No retryable transfer or original file is unavailable.";
|
|
1369
|
+
showFlash("err", msg);
|
|
1370
|
+
window.alert(msg);
|
|
1371
|
+
} else {
|
|
1372
|
+
showFlash("ok", T.retried || "Transfer restarted");
|
|
1373
|
+
if (onReloadThread) onReloadThread();
|
|
1374
|
+
}
|
|
1375
|
+
}).catch((e) => {
|
|
1376
|
+
setFilePatch((p) => {
|
|
1377
|
+
const n = { ...p };
|
|
1378
|
+
delete n[id];
|
|
1379
|
+
return n;
|
|
1380
|
+
});
|
|
1381
|
+
const msg = (T.retryFailed || "Retry failed") + ": " + String(e);
|
|
1382
|
+
showFlash("err", msg);
|
|
1383
|
+
window.alert(msg);
|
|
1384
|
+
}).finally(() => clearBusy(id));
|
|
1287
1385
|
};
|
|
1288
1386
|
const doDelete = (ids) => {
|
|
1289
1387
|
ids = ids.filter(Boolean);
|
|
@@ -1371,6 +1469,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1371
1469
|
onDelete: (id) => doDelete([id]),
|
|
1372
1470
|
onCancel: doCancel,
|
|
1373
1471
|
onRetry: doRetry,
|
|
1472
|
+
busy: m.id ? fileBusy[m.id] : void 0,
|
|
1374
1473
|
selMode,
|
|
1375
1474
|
selected: sel.has(m.id),
|
|
1376
1475
|
onToggleSel: toggleSel
|
|
@@ -1380,7 +1479,24 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1380
1479
|
setSelMode(false);
|
|
1381
1480
|
setSel(/* @__PURE__ */ new Set());
|
|
1382
1481
|
} }, T.cancel || "Cancel"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", icon: "trash", onClick: () => doDelete([...sel]) }, T.delete || "Delete")),
|
|
1383
|
-
/* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } },
|
|
1482
|
+
/* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } }, flash && /* @__PURE__ */ React.createElement("div", { style: {
|
|
1483
|
+
maxWidth: 760,
|
|
1484
|
+
margin: "0 auto 8px",
|
|
1485
|
+
display: "flex",
|
|
1486
|
+
alignItems: "center",
|
|
1487
|
+
gap: 8,
|
|
1488
|
+
fontFamily: "var(--mono)",
|
|
1489
|
+
fontSize: 11.5,
|
|
1490
|
+
color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
|
|
1491
|
+
} }, /* @__PURE__ */ React.createElement(
|
|
1492
|
+
Icon,
|
|
1493
|
+
{
|
|
1494
|
+
name: flash.kind === "err" ? "x" : "check",
|
|
1495
|
+
size: 13,
|
|
1496
|
+
stroke: 2.2,
|
|
1497
|
+
color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
|
|
1498
|
+
}
|
|
1499
|
+
), 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.name), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
|
|
1384
1500
|
"input",
|
|
1385
1501
|
{
|
|
1386
1502
|
ref: fileRef,
|
|
@@ -1475,9 +1591,9 @@ function MenuItem({ icon, label, onClick, danger }) {
|
|
|
1475
1591
|
function ChatEmpty({ T }) {
|
|
1476
1592
|
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));
|
|
1477
1593
|
}
|
|
1478
|
-
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }) {
|
|
1594
|
+
function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
|
|
1479
1595
|
const peer = peers.find((p) => p.id === activeId);
|
|
1480
|
-
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 }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1596
|
+
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 }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
|
|
1481
1597
|
}
|
|
1482
1598
|
Object.assign(window, { ChatTab });
|
|
1483
1599
|
function StatTile({ label, value, sub, tone }) {
|
|
@@ -1977,6 +2093,14 @@ const STR = {
|
|
|
1977
2093
|
open: "open",
|
|
1978
2094
|
retry: "Retry",
|
|
1979
2095
|
cancel: "Cancel",
|
|
2096
|
+
cancelling: "Cancelling\u2026",
|
|
2097
|
+
cancelled: "Transfer cancelled",
|
|
2098
|
+
retrying: "Retrying\u2026",
|
|
2099
|
+
retried: "Transfer restarted",
|
|
2100
|
+
cancelFailed: "Cancel failed",
|
|
2101
|
+
retryFailed: "Retry failed",
|
|
2102
|
+
cancelMissing: "No active transfer was found. Refresh and try again.",
|
|
2103
|
+
retryMissing: "No retryable transfer or original file is unavailable.",
|
|
1980
2104
|
myIp: "my ip",
|
|
1981
2105
|
copyAddr: "copy address",
|
|
1982
2106
|
peersOnline: "peers online",
|
|
@@ -2059,6 +2183,14 @@ const STR = {
|
|
|
2059
2183
|
open: "\u6253\u5F00",
|
|
2060
2184
|
retry: "\u91CD\u8BD5",
|
|
2061
2185
|
cancel: "\u53D6\u6D88",
|
|
2186
|
+
cancelling: "\u6B63\u5728\u53D6\u6D88\u2026",
|
|
2187
|
+
cancelled: "\u5DF2\u53D6\u6D88\u4F20\u8F93",
|
|
2188
|
+
retrying: "\u6B63\u5728\u91CD\u8BD5\u2026",
|
|
2189
|
+
retried: "\u5DF2\u91CD\u65B0\u5F00\u59CB\u4F20\u8F93",
|
|
2190
|
+
cancelFailed: "\u53D6\u6D88\u5931\u8D25",
|
|
2191
|
+
retryFailed: "\u91CD\u8BD5\u5931\u8D25",
|
|
2192
|
+
cancelMissing: "\u6CA1\u6709\u627E\u5230\u6B63\u5728\u8FDB\u884C\u7684\u4F20\u8F93\uFF1B\u8BF7\u5237\u65B0\u9875\u9762\u540E\u91CD\u8BD5\u3002",
|
|
2193
|
+
retryMissing: "\u6CA1\u6709\u53EF\u91CD\u8BD5\u7684\u4F20\u8F93\uFF0C\u6216\u539F\u6587\u4EF6\u5DF2\u4E0D\u53EF\u7528\u3002",
|
|
2062
2194
|
myIp: "\u6211\u7684 IP",
|
|
2063
2195
|
copyAddr: "\u590D\u5236\u5730\u5740",
|
|
2064
2196
|
peersOnline: "\u5728\u7EBF\u597D\u53CB",
|
|
@@ -2206,7 +2338,7 @@ function DkApp() {
|
|
|
2206
2338
|
{ id: "network", icon: "network", label: T.network },
|
|
2207
2339
|
{ id: "profile", icon: "userRound", label: T.profile }
|
|
2208
2340
|
];
|
|
2209
|
-
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("div", { style: { position: "relative" } }, /* @__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("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("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.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, onAlias, onRemove, onOpenNet, onCall }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
2341
|
+
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("div", { style: { position: "relative" } }, /* @__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("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("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.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, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId) }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
|
|
2210
2342
|
TweakRadio,
|
|
2211
2343
|
{
|
|
2212
2344
|
label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.215",
|
|
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",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@decentnetwork/dora": "^0.1.14",
|
|
87
|
-
"@decentnetwork/peer": "^0.1.
|
|
87
|
+
"@decentnetwork/peer": "^0.1.108",
|
|
88
88
|
"@decentnetwork/peer-webrtc": "^0.2.10",
|
|
89
89
|
"ink": "^5.2.1",
|
|
90
90
|
"js-yaml": "^4.1.0",
|