@decentnetwork/lan 0.1.214 → 0.1.217
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/message-store.d.ts +3 -3
- package/dist/daemon/server.d.ts +6 -0
- package/dist/daemon/server.js +33 -1
- package/dist/ui/desktop/app.js +389 -49
- package/package.json +2 -2
|
@@ -33,7 +33,7 @@ export interface ChatMessage {
|
|
|
33
33
|
file?: {
|
|
34
34
|
name: string;
|
|
35
35
|
size: number;
|
|
36
|
-
status?: "queued" | "sending" | "sent" | "failed";
|
|
36
|
+
status?: "queued" | "sending" | "sent" | "failed" | "cancelled";
|
|
37
37
|
sent?: number;
|
|
38
38
|
durationMs?: number;
|
|
39
39
|
avgKbps?: number;
|
|
@@ -65,13 +65,13 @@ export declare class MessageStore {
|
|
|
65
65
|
appendFile(peer: string, dir: "in" | "out", file: {
|
|
66
66
|
name: string;
|
|
67
67
|
size: number;
|
|
68
|
-
status?: "queued" | "sending" | "sent" | "failed";
|
|
68
|
+
status?: "queued" | "sending" | "sent" | "failed" | "cancelled";
|
|
69
69
|
sent?: number;
|
|
70
70
|
}, ts?: number): ChatMessage;
|
|
71
71
|
/** Patch an existing file message's transfer fields (status / sent bytes).
|
|
72
72
|
* No-op if the id isn't found. Returns true if it patched. */
|
|
73
73
|
patchFile(peer: string, id: string, patch: {
|
|
74
|
-
status?: "queued" | "sending" | "sent" | "failed";
|
|
74
|
+
status?: "queued" | "sending" | "sent" | "failed" | "cancelled";
|
|
75
75
|
sent?: number;
|
|
76
76
|
durationMs?: number;
|
|
77
77
|
avgKbps?: number;
|
package/dist/daemon/server.d.ts
CHANGED
|
@@ -98,6 +98,12 @@ export declare class DaemonServer {
|
|
|
98
98
|
* restarts). Also ensures a friend-meta entry exists so the friend shows up
|
|
99
99
|
* in the UI list even before it's been renamed. */
|
|
100
100
|
private logChat;
|
|
101
|
+
/** A live file send cannot make progress once the Carrier session drops.
|
|
102
|
+
* Move any active chips for that peer out of "sending" immediately instead
|
|
103
|
+
* of leaving the UI to show a misleading 1 KB/s forever. If the original
|
|
104
|
+
* bytes are still in the outbox, queue it for the next reconnect; otherwise
|
|
105
|
+
* mark failed and let the user explicitly retry/cancel. */
|
|
106
|
+
private markPeerActiveSendsInterrupted;
|
|
101
107
|
/** On startup, re-queue any offline file transfer that was mid-flight when the
|
|
102
108
|
* daemon last stopped: an out chip stuck at "sending" whose bytes still sit in
|
|
103
109
|
* the outbox. Flips it back to "queued" so the next reconnect redelivers it
|
package/dist/daemon/server.js
CHANGED
|
@@ -585,7 +585,7 @@ export class DaemonServer {
|
|
|
585
585
|
killed = true;
|
|
586
586
|
}
|
|
587
587
|
if (killed || wasActive) {
|
|
588
|
-
this.messageStore?.patchFile(userid, id, { status: "
|
|
588
|
+
this.messageStore?.patchFile(userid, id, { status: "cancelled", sent: 0, kbps: undefined });
|
|
589
589
|
n++;
|
|
590
590
|
}
|
|
591
591
|
this.logger.info(`file-cancel peer=${userid.slice(0, 8)} msg=${id} killed=${killed ? 1 : 0} wasActive=${wasActive ? 1 : 0}`);
|
|
@@ -1007,6 +1007,9 @@ export class DaemonServer {
|
|
|
1007
1007
|
if (evt?.status === "connected" && evt.pubkey) {
|
|
1008
1008
|
void this.flushOutbox(evt.pubkey).catch((e) => this.logger.warn(`Outbox flush for ${evt.pubkey.slice(0, 8)} failed: ${e.message}`));
|
|
1009
1009
|
}
|
|
1010
|
+
else if (evt?.status === "disconnected" && evt.pubkey) {
|
|
1011
|
+
this.markPeerActiveSendsInterrupted(evt.pubkey, "peer-disconnected");
|
|
1012
|
+
}
|
|
1010
1013
|
});
|
|
1011
1014
|
// File transfer (toxcore-standard): auto-accept offers from friends and
|
|
1012
1015
|
// save completed files under <configDir>/downloads/.
|
|
@@ -1395,6 +1398,35 @@ export class DaemonServer {
|
|
|
1395
1398
|
this.friendMeta?.ensure(userid);
|
|
1396
1399
|
this.ipcEvents.emit("event", { type: "chat", userid, dir });
|
|
1397
1400
|
}
|
|
1401
|
+
/** A live file send cannot make progress once the Carrier session drops.
|
|
1402
|
+
* Move any active chips for that peer out of "sending" immediately instead
|
|
1403
|
+
* of leaving the UI to show a misleading 1 KB/s forever. If the original
|
|
1404
|
+
* bytes are still in the outbox, queue it for the next reconnect; otherwise
|
|
1405
|
+
* mark failed and let the user explicitly retry/cancel. */
|
|
1406
|
+
markPeerActiveSendsInterrupted(userid, reason) {
|
|
1407
|
+
const affected = [...this.activeSends].filter(([, t]) => t.peer === userid);
|
|
1408
|
+
if (!affected.length)
|
|
1409
|
+
return;
|
|
1410
|
+
let queued = 0;
|
|
1411
|
+
let failed = 0;
|
|
1412
|
+
for (const [fileId, t] of affected) {
|
|
1413
|
+
this.activeSends.delete(fileId);
|
|
1414
|
+
this.peerManager?.cancelSend(userid, fileId);
|
|
1415
|
+
const canQueue = !!this.outboxDir && existsSync(resolve(this.outboxDir, t.msgId));
|
|
1416
|
+
this.messageStore?.patchFile(userid, t.msgId, {
|
|
1417
|
+
status: canQueue ? "queued" : "failed",
|
|
1418
|
+
sent: 0,
|
|
1419
|
+
kbps: undefined,
|
|
1420
|
+
});
|
|
1421
|
+
if (canQueue)
|
|
1422
|
+
queued++;
|
|
1423
|
+
else
|
|
1424
|
+
failed++;
|
|
1425
|
+
}
|
|
1426
|
+
this.logger.warn(`Interrupted ${affected.length} active file send(s) to ${userid.slice(0, 8)} ` +
|
|
1427
|
+
`(${reason}; queued=${queued}, failed=${failed})`);
|
|
1428
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
1429
|
+
}
|
|
1398
1430
|
/** On startup, re-queue any offline file transfer that was mid-flight when the
|
|
1399
1431
|
* daemon last stopped: an out chip stuck at "sending" whose bytes still sit in
|
|
1400
1432
|
* the outbox. Flips it back to "queued" so the next reconnect redelivers it
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -332,6 +332,113 @@ Object.assign(window, {
|
|
|
332
332
|
DK_ME_FALLBACK,
|
|
333
333
|
dkCopy
|
|
334
334
|
});
|
|
335
|
+
function dkRtcParseEnvelope(data) {
|
|
336
|
+
const text = String(data || "").replace(/\0+$/u, "").trim();
|
|
337
|
+
if (!text) return null;
|
|
338
|
+
try {
|
|
339
|
+
const obj = JSON.parse(text);
|
|
340
|
+
if (obj && obj.dkRtc === 1 && typeof obj.kind === "string") return obj;
|
|
341
|
+
} catch (e) {
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
function dkRtcSignalBus() {
|
|
346
|
+
if (window.__dkRtcSignalBus) return window.__dkRtcSignalBus;
|
|
347
|
+
const PW = window.PeerWebRTC;
|
|
348
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
349
|
+
let started = false;
|
|
350
|
+
let stopped = false;
|
|
351
|
+
function emit(kind, userid, payload) {
|
|
352
|
+
const set = handlers.get(kind);
|
|
353
|
+
if (!set) return;
|
|
354
|
+
for (const h of Array.from(set)) {
|
|
355
|
+
try {
|
|
356
|
+
h(userid, payload);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
console.warn("[rtc] signal handler failed", e);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function pollLoop() {
|
|
363
|
+
while (!stopped) {
|
|
364
|
+
let signals = [];
|
|
365
|
+
try {
|
|
366
|
+
const r = await fetch("/api/call-poll", { headers: { "cache-control": "no-cache" } });
|
|
367
|
+
const d = await r.json();
|
|
368
|
+
signals = d && d.signals || [];
|
|
369
|
+
} catch (e) {
|
|
370
|
+
await new Promise((res) => setTimeout(res, 1e3));
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
for (const s of signals) {
|
|
374
|
+
const env = dkRtcParseEnvelope(s.data);
|
|
375
|
+
if (env) {
|
|
376
|
+
emit(env.kind, s.userid, env);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (PW) {
|
|
380
|
+
try {
|
|
381
|
+
emit("call", s.userid, PW.decodeSignal(s.data));
|
|
382
|
+
} catch (e) {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const bus = {
|
|
389
|
+
start() {
|
|
390
|
+
if (started) return;
|
|
391
|
+
started = true;
|
|
392
|
+
stopped = false;
|
|
393
|
+
pollLoop();
|
|
394
|
+
},
|
|
395
|
+
on(kind, cb) {
|
|
396
|
+
if (!handlers.has(kind)) handlers.set(kind, /* @__PURE__ */ new Set());
|
|
397
|
+
handlers.get(kind).add(cb);
|
|
398
|
+
this.start();
|
|
399
|
+
return () => {
|
|
400
|
+
var _a;
|
|
401
|
+
return (_a = handlers.get(kind)) == null ? void 0 : _a.delete(cb);
|
|
402
|
+
};
|
|
403
|
+
},
|
|
404
|
+
async send(userid, data) {
|
|
405
|
+
const body = JSON.stringify({ userid, data: typeof data === "string" ? data : JSON.stringify(data) });
|
|
406
|
+
const r = await fetch("/api/call-signal", {
|
|
407
|
+
method: "POST",
|
|
408
|
+
headers: { "content-type": "application/json" },
|
|
409
|
+
body
|
|
410
|
+
});
|
|
411
|
+
const d = await r.json().catch(() => ({ ok: r.ok }));
|
|
412
|
+
if (!d.ok) throw new Error(d.error || "call-signal send failed");
|
|
413
|
+
},
|
|
414
|
+
stop() {
|
|
415
|
+
stopped = true;
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
window.__dkRtcSignalBus = bus;
|
|
419
|
+
return bus;
|
|
420
|
+
}
|
|
421
|
+
function makeDaemonSignaling() {
|
|
422
|
+
const PW = window.PeerWebRTC;
|
|
423
|
+
const bus = dkRtcSignalBus();
|
|
424
|
+
let off = null;
|
|
425
|
+
return {
|
|
426
|
+
async send(peerId, signal) {
|
|
427
|
+
await bus.send(peerId, PW.encodeSignal(signal));
|
|
428
|
+
},
|
|
429
|
+
onSignal(cb) {
|
|
430
|
+
if (off) off();
|
|
431
|
+
off = bus.on("call", cb);
|
|
432
|
+
},
|
|
433
|
+
stop() {
|
|
434
|
+
if (off) {
|
|
435
|
+
off();
|
|
436
|
+
off = null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
Object.assign(window, { dkRtcSignalBus, makeDaemonSignaling });
|
|
335
442
|
function DkIdenticon({ seed, size = 30, radius = 7 }) {
|
|
336
443
|
const { cells, hue } = dkIdenticon(seed);
|
|
337
444
|
const cell = size / 5;
|
|
@@ -1108,7 +1215,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
|
|
|
1108
1215
|
// Inline preview/player. Received media: as soon as it's saved.
|
|
1109
1216
|
// Sent media: once it's fully delivered ('sent') — the daemon keeps a
|
|
1110
1217
|
// local copy so it plays on the sender's side too.
|
|
1111
|
-
(m.file.media === "image" || m.file.media === "video" || m.file.media === "audio") && (mine ? m.file.status === "sent" : m.file.status !== "sending" && m.file.status !== "failed" && m.file.status !== "queued") ? (
|
|
1218
|
+
(m.file.media === "image" || m.file.media === "video" || m.file.media === "audio") && (mine ? m.file.status === "sent" : m.file.status !== "sending" && m.file.status !== "failed" && m.file.status !== "queued" && m.file.status !== "cancelled") ? (
|
|
1112
1219
|
// Received media → inline preview / player, with name, size, download.
|
|
1113
1220
|
/* @__PURE__ */ React.createElement("div", { style: {
|
|
1114
1221
|
display: "flex",
|
|
@@ -1170,10 +1277,10 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
|
|
|
1170
1277
|
cursor: mine ? "default" : "pointer"
|
|
1171
1278
|
};
|
|
1172
1279
|
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 }));
|
|
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"}`));
|
|
1280
|
+
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` : m.file.status === "cancelled" ? `${m.file.size} \xB7 ${T.cancelled || "cancelled"}` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
|
|
1174
1281
|
if (mine) {
|
|
1175
1282
|
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(
|
|
1283
|
+
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" || m.file.status === "cancelled" ? /* @__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(
|
|
1177
1284
|
"button",
|
|
1178
1285
|
{
|
|
1179
1286
|
title: T.cancel || "cancel",
|
|
@@ -1260,6 +1367,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
|
|
|
1260
1367
|
}
|
|
1261
1368
|
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
|
|
1262
1369
|
const scrollRef = React.useRef(null);
|
|
1370
|
+
const followScrollRef = React.useRef(true);
|
|
1263
1371
|
const fileRef = React.useRef(null);
|
|
1264
1372
|
const [menu, setMenu] = React.useState(false);
|
|
1265
1373
|
const [selMode, setSelMode] = React.useState(false);
|
|
@@ -1284,6 +1392,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1284
1392
|
setFileBusy({});
|
|
1285
1393
|
setFilePatch({});
|
|
1286
1394
|
setFlash(null);
|
|
1395
|
+
followScrollRef.current = true;
|
|
1287
1396
|
}, [peer.id]);
|
|
1288
1397
|
React.useEffect(() => {
|
|
1289
1398
|
setFilePatch((p) => {
|
|
@@ -1307,9 +1416,18 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1307
1416
|
return { ...m, file: { ...m.file, ...filePatch[m.id] } };
|
|
1308
1417
|
});
|
|
1309
1418
|
}, [baseThread, filePatch]);
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1419
|
+
const scrollToBottom = () => {
|
|
1420
|
+
const el = scrollRef.current;
|
|
1421
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
1422
|
+
};
|
|
1423
|
+
const updateFollowScroll = () => {
|
|
1424
|
+
const el = scrollRef.current;
|
|
1425
|
+
if (!el) return;
|
|
1426
|
+
followScrollRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
|
|
1427
|
+
};
|
|
1428
|
+
React.useLayoutEffect(() => {
|
|
1429
|
+
if (followScrollRef.current) scrollToBottom();
|
|
1430
|
+
}, [peer.id, thread]);
|
|
1313
1431
|
const toggleSel = (id) => setSel((s) => {
|
|
1314
1432
|
const n = new Set(s);
|
|
1315
1433
|
n.has(id) ? n.delete(id) : n.add(id);
|
|
@@ -1334,7 +1452,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1334
1452
|
showFlash("err", msg);
|
|
1335
1453
|
window.alert(msg);
|
|
1336
1454
|
} else {
|
|
1337
|
-
setFilePatch((p) => ({ ...p, [id]: { status: "
|
|
1455
|
+
setFilePatch((p) => ({ ...p, [id]: { status: "cancelled", pct: void 0, kbps: void 0 } }));
|
|
1338
1456
|
showFlash("ok", T.cancelled || "Transfer cancelled");
|
|
1339
1457
|
if (onReloadThread) onReloadThread();
|
|
1340
1458
|
}
|
|
@@ -1417,17 +1535,23 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1417
1535
|
}, [theater]);
|
|
1418
1536
|
const sendDraft = () => {
|
|
1419
1537
|
if (draft.trim()) {
|
|
1538
|
+
followScrollRef.current = true;
|
|
1420
1539
|
onSend(draft);
|
|
1421
1540
|
setDraft("");
|
|
1541
|
+
requestAnimationFrame(scrollToBottom);
|
|
1422
1542
|
}
|
|
1423
1543
|
};
|
|
1424
1544
|
const sendFiles = (files) => {
|
|
1425
1545
|
if (!files || !files.length || !onSendFile) return;
|
|
1426
1546
|
const file = files[0];
|
|
1547
|
+
followScrollRef.current = true;
|
|
1427
1548
|
setSending({ name: file.name });
|
|
1428
1549
|
Promise.resolve(onSendFile(file)).then((r) => {
|
|
1429
1550
|
if (r && r.ok === false) window.alert((lang === "zh" ? "\u53D1\u9001\u5931\u8D25: " : "Send failed: ") + (r.error || ""));
|
|
1430
|
-
}).finally(() =>
|
|
1551
|
+
}).finally(() => {
|
|
1552
|
+
setSending(null);
|
|
1553
|
+
requestAnimationFrame(scrollToBottom);
|
|
1554
|
+
});
|
|
1431
1555
|
};
|
|
1432
1556
|
const onDrop = (e) => {
|
|
1433
1557
|
e.preventDefault();
|
|
@@ -1458,7 +1582,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1458
1582
|
setMenu(false);
|
|
1459
1583
|
onRemove(peer);
|
|
1460
1584
|
} }))))),
|
|
1461
|
-
/* @__PURE__ */ React.createElement("div", { ref: scrollRef, 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(
|
|
1585
|
+
/* @__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(
|
|
1462
1586
|
Msg,
|
|
1463
1587
|
{
|
|
1464
1588
|
key: m.id || i,
|
|
@@ -1710,53 +1834,261 @@ function ProfileTab({ T, me, onEdit }) {
|
|
|
1710
1834
|
} }));
|
|
1711
1835
|
}
|
|
1712
1836
|
Object.assign(window, { ProfileTab });
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1837
|
+
const DK_FILE_RTC_KIND = "file";
|
|
1838
|
+
const DK_FILE_RTC_CHUNK = 64 * 1024;
|
|
1839
|
+
const DK_FILE_RTC_OPEN_TIMEOUT_MS = 8e3;
|
|
1840
|
+
const DK_FILE_RTC_SIGNAL_TTL_MS = 10 * 60 * 1e3;
|
|
1841
|
+
function dkFileRtcId() {
|
|
1842
|
+
if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID();
|
|
1843
|
+
return "frtc-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2);
|
|
1844
|
+
}
|
|
1845
|
+
function dkFileRtcEnvelope(fileId, type, extra) {
|
|
1846
|
+
return Object.assign({ dkRtc: 1, kind: DK_FILE_RTC_KIND, fileId, type, ts: Date.now() }, extra || {});
|
|
1847
|
+
}
|
|
1848
|
+
function dkWaitForOpen(dc, timeoutMs) {
|
|
1849
|
+
if (dc.readyState === "open") return Promise.resolve();
|
|
1850
|
+
return new Promise((resolve, reject) => {
|
|
1851
|
+
const timer = setTimeout(() => {
|
|
1852
|
+
cleanup();
|
|
1853
|
+
reject(new Error("WebRTC data channel open timed out"));
|
|
1854
|
+
}, timeoutMs);
|
|
1855
|
+
const cleanup = () => {
|
|
1856
|
+
clearTimeout(timer);
|
|
1857
|
+
dc.removeEventListener("open", onOpen);
|
|
1858
|
+
dc.removeEventListener("error", onError);
|
|
1859
|
+
dc.removeEventListener("close", onClose);
|
|
1860
|
+
};
|
|
1861
|
+
const onOpen = () => {
|
|
1862
|
+
cleanup();
|
|
1863
|
+
resolve();
|
|
1864
|
+
};
|
|
1865
|
+
const onError = () => {
|
|
1866
|
+
cleanup();
|
|
1867
|
+
reject(new Error("WebRTC data channel failed"));
|
|
1868
|
+
};
|
|
1869
|
+
const onClose = () => {
|
|
1870
|
+
cleanup();
|
|
1871
|
+
reject(new Error("WebRTC data channel closed"));
|
|
1872
|
+
};
|
|
1873
|
+
dc.addEventListener("open", onOpen);
|
|
1874
|
+
dc.addEventListener("error", onError);
|
|
1875
|
+
dc.addEventListener("close", onClose);
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
function dkWaitBufferedLow(dc, highWater) {
|
|
1879
|
+
if (dc.bufferedAmount < highWater) return Promise.resolve();
|
|
1880
|
+
return new Promise((resolve) => {
|
|
1881
|
+
const prev = dc.bufferedAmountLowThreshold;
|
|
1882
|
+
dc.bufferedAmountLowThreshold = Math.floor(highWater / 2);
|
|
1883
|
+
const done = () => {
|
|
1884
|
+
dc.removeEventListener("bufferedamountlow", done);
|
|
1885
|
+
dc.bufferedAmountLowThreshold = prev;
|
|
1886
|
+
resolve();
|
|
1887
|
+
};
|
|
1888
|
+
dc.addEventListener("bufferedamountlow", done);
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
function dkFileDownload(blob, name) {
|
|
1892
|
+
const url = URL.createObjectURL(blob);
|
|
1893
|
+
const a = document.createElement("a");
|
|
1894
|
+
a.href = url;
|
|
1895
|
+
a.download = name || "agentnet-file";
|
|
1896
|
+
a.textContent = name || "agentnet-file";
|
|
1897
|
+
return { url, name: a.download };
|
|
1898
|
+
}
|
|
1899
|
+
function useRtcFileController(selfId, onReceivedText) {
|
|
1900
|
+
const [incoming, setIncoming] = React.useState([]);
|
|
1901
|
+
const peersRef = React.useRef(/* @__PURE__ */ new Map());
|
|
1902
|
+
React.useEffect(() => {
|
|
1903
|
+
if (!selfId || !window.RTCPeerConnection || !window.dkRtcSignalBus) return;
|
|
1904
|
+
const bus = dkRtcSignalBus();
|
|
1905
|
+
const cleanup = (fileId) => {
|
|
1906
|
+
const sess = peersRef.current.get(fileId);
|
|
1907
|
+
if (!sess) return;
|
|
1720
1908
|
try {
|
|
1721
|
-
|
|
1722
|
-
const d = await r.json();
|
|
1723
|
-
signals = d && d.signals || [];
|
|
1909
|
+
sess.dc && sess.dc.close();
|
|
1724
1910
|
} catch (e) {
|
|
1725
|
-
await new Promise((res) => setTimeout(res, 1e3));
|
|
1726
|
-
continue;
|
|
1727
1911
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1912
|
+
try {
|
|
1913
|
+
sess.pc && sess.pc.close();
|
|
1914
|
+
} catch (e) {
|
|
1915
|
+
}
|
|
1916
|
+
peersRef.current.delete(fileId);
|
|
1917
|
+
};
|
|
1918
|
+
const sendSignal = (peerId, fileId, type, extra) => bus.send(peerId, dkFileRtcEnvelope(fileId, type, extra));
|
|
1919
|
+
const handleDataChannel = (peerId, fileId, pc, dc, initialMeta) => {
|
|
1920
|
+
dc.binaryType = "arraybuffer";
|
|
1921
|
+
const prev = peersRef.current.get(fileId);
|
|
1922
|
+
const sess = Object.assign(prev || {}, { pc, dc, peerId, meta: initialMeta || null, chunks: [], received: 0 });
|
|
1923
|
+
peersRef.current.set(fileId, sess);
|
|
1924
|
+
dc.onmessage = (ev) => {
|
|
1925
|
+
if (typeof ev.data === "string") {
|
|
1926
|
+
let msg = null;
|
|
1927
|
+
try {
|
|
1928
|
+
msg = JSON.parse(ev.data);
|
|
1929
|
+
} catch (e) {
|
|
1930
|
+
}
|
|
1931
|
+
if (!msg) return;
|
|
1932
|
+
if (msg.type === "meta") {
|
|
1933
|
+
sess.meta = msg;
|
|
1934
|
+
sess.chunks = [];
|
|
1935
|
+
sess.received = 0;
|
|
1936
|
+
} else if (msg.type === "done") {
|
|
1937
|
+
const meta = sess.meta || {};
|
|
1938
|
+
const blob = new Blob(sess.chunks, { type: meta.mime || "application/octet-stream" });
|
|
1939
|
+
const dl = dkFileDownload(blob, meta.name || "agentnet-file");
|
|
1940
|
+
setIncoming((arr) => [{ id: fileId, peerId, name: dl.name, size: blob.size, url: dl.url }].concat(arr).slice(0, 8));
|
|
1941
|
+
if (onReceivedText) onReceivedText(peerId, "received file via WebRTC: " + dl.name + " (" + dkFileSize(blob.size) + ")");
|
|
1942
|
+
cleanup(fileId);
|
|
1943
|
+
} else if (msg.type === "cancel") {
|
|
1944
|
+
cleanup(fileId);
|
|
1945
|
+
}
|
|
1946
|
+
return;
|
|
1733
1947
|
}
|
|
1948
|
+
sess.chunks.push(ev.data);
|
|
1949
|
+
sess.received += ev.data.byteLength || ev.data.size || 0;
|
|
1950
|
+
};
|
|
1951
|
+
dc.onclose = () => setTimeout(() => cleanup(fileId), 1e3);
|
|
1952
|
+
dc.onerror = () => cleanup(fileId);
|
|
1953
|
+
};
|
|
1954
|
+
const off = bus.on(DK_FILE_RTC_KIND, async (peerId, env) => {
|
|
1955
|
+
if (!env || !env.fileId || !env.type) return;
|
|
1956
|
+
if (Date.now() - (env.ts || Date.now()) > DK_FILE_RTC_SIGNAL_TTL_MS) return;
|
|
1957
|
+
const fileId = env.fileId;
|
|
1958
|
+
let sess = peersRef.current.get(fileId);
|
|
1959
|
+
try {
|
|
1960
|
+
if (env.type === "offer") {
|
|
1961
|
+
if (sess) cleanup(fileId);
|
|
1962
|
+
const pc = new RTCPeerConnection({ iceServers: CALL_ICE_SERVERS });
|
|
1963
|
+
pc.onicecandidate = (ev) => {
|
|
1964
|
+
if (ev.candidate) sendSignal(peerId, fileId, "candidate", { candidate: ev.candidate.toJSON() }).catch(() => {
|
|
1965
|
+
});
|
|
1966
|
+
};
|
|
1967
|
+
pc.ondatachannel = (ev) => handleDataChannel(peerId, fileId, pc, ev.channel, env.meta || null);
|
|
1968
|
+
peersRef.current.set(fileId, { pc, dc: null, peerId, chunks: [], meta: env.meta || null, pendingCandidates: [] });
|
|
1969
|
+
await pc.setRemoteDescription({ type: "offer", sdp: env.sdp });
|
|
1970
|
+
sess = peersRef.current.get(fileId);
|
|
1971
|
+
if (sess && sess.pendingCandidates) {
|
|
1972
|
+
for (const c of sess.pendingCandidates.splice(0)) {
|
|
1973
|
+
try {
|
|
1974
|
+
await pc.addIceCandidate(c);
|
|
1975
|
+
} catch (e) {
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
const answer = await pc.createAnswer();
|
|
1980
|
+
await pc.setLocalDescription(answer);
|
|
1981
|
+
await sendSignal(peerId, fileId, "answer", { sdp: answer.sdp });
|
|
1982
|
+
} else if (env.type === "answer" && sess) {
|
|
1983
|
+
await sess.pc.setRemoteDescription({ type: "answer", sdp: env.sdp });
|
|
1984
|
+
if (sess.pendingCandidates) {
|
|
1985
|
+
for (const c of sess.pendingCandidates.splice(0)) {
|
|
1986
|
+
try {
|
|
1987
|
+
await sess.pc.addIceCandidate(c);
|
|
1988
|
+
} catch (e) {
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
} else if (env.type === "candidate" && sess && env.candidate) {
|
|
1993
|
+
if (sess.pc.remoteDescription) {
|
|
1994
|
+
try {
|
|
1995
|
+
await sess.pc.addIceCandidate(env.candidate);
|
|
1996
|
+
} catch (e) {
|
|
1997
|
+
}
|
|
1998
|
+
} else {
|
|
1999
|
+
if (!sess.pendingCandidates) sess.pendingCandidates = [];
|
|
2000
|
+
sess.pendingCandidates.push(env.candidate);
|
|
2001
|
+
}
|
|
2002
|
+
} else if (env.type === "bye") {
|
|
2003
|
+
cleanup(fileId);
|
|
2004
|
+
}
|
|
2005
|
+
} catch (e) {
|
|
2006
|
+
console.warn("[file-rtc] signal failed", e);
|
|
2007
|
+
cleanup(fileId);
|
|
1734
2008
|
}
|
|
2009
|
+
});
|
|
2010
|
+
return () => {
|
|
2011
|
+
off();
|
|
2012
|
+
for (const id of Array.from(peersRef.current.keys())) cleanup(id);
|
|
2013
|
+
};
|
|
2014
|
+
}, [selfId, onReceivedText]);
|
|
2015
|
+
const sendFile = React.useCallback(async (peerId, file, onProgress) => {
|
|
2016
|
+
if (!peerId || !file || !window.RTCPeerConnection || !window.dkRtcSignalBus) {
|
|
2017
|
+
return { ok: false, fallback: true, error: "WebRTC unavailable" };
|
|
1735
2018
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2019
|
+
const bus = dkRtcSignalBus();
|
|
2020
|
+
const fileId = dkFileRtcId();
|
|
2021
|
+
const pc = new RTCPeerConnection({ iceServers: CALL_ICE_SERVERS });
|
|
2022
|
+
const dc = pc.createDataChannel("agentnet-file", { ordered: true });
|
|
2023
|
+
const sess = { pc, dc, peerId, chunks: [], meta: null, pendingCandidates: [] };
|
|
2024
|
+
peersRef.current.set(fileId, sess);
|
|
2025
|
+
const cleanup = () => {
|
|
2026
|
+
try {
|
|
2027
|
+
dc.close();
|
|
2028
|
+
} catch (e) {
|
|
2029
|
+
}
|
|
2030
|
+
try {
|
|
2031
|
+
pc.close();
|
|
2032
|
+
} catch (e) {
|
|
2033
|
+
}
|
|
2034
|
+
peersRef.current.delete(fileId);
|
|
2035
|
+
};
|
|
2036
|
+
const sendSignal = (type, extra) => bus.send(peerId, dkFileRtcEnvelope(fileId, type, extra));
|
|
2037
|
+
try {
|
|
2038
|
+
pc.onicecandidate = (ev) => {
|
|
2039
|
+
if (ev.candidate) sendSignal("candidate", { candidate: ev.candidate.toJSON() }).catch(() => {
|
|
2040
|
+
});
|
|
2041
|
+
};
|
|
2042
|
+
const offer = await pc.createOffer();
|
|
2043
|
+
await pc.setLocalDescription(offer);
|
|
2044
|
+
await sendSignal("offer", {
|
|
2045
|
+
sdp: offer.sdp,
|
|
2046
|
+
meta: { name: file.name, size: file.size, mime: file.type || "application/octet-stream" }
|
|
1744
2047
|
});
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
2048
|
+
await dkWaitForOpen(dc, DK_FILE_RTC_OPEN_TIMEOUT_MS);
|
|
2049
|
+
dc.send(JSON.stringify({ type: "meta", name: file.name, size: file.size, mime: file.type || "application/octet-stream" }));
|
|
2050
|
+
let sent = 0;
|
|
2051
|
+
const highWater = 4 * 1024 * 1024;
|
|
2052
|
+
while (sent < file.size) {
|
|
2053
|
+
if (dc.readyState !== "open") throw new Error("WebRTC data channel closed");
|
|
2054
|
+
const end = Math.min(file.size, sent + DK_FILE_RTC_CHUNK);
|
|
2055
|
+
const buf = await file.slice(sent, end).arrayBuffer();
|
|
2056
|
+
dc.send(buf);
|
|
2057
|
+
sent = end;
|
|
2058
|
+
if (onProgress) onProgress({ sent, size: file.size, pct: file.size ? sent / file.size * 100 : 100 });
|
|
2059
|
+
await dkWaitBufferedLow(dc, highWater);
|
|
1753
2060
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2061
|
+
dc.send(JSON.stringify({ type: "done" }));
|
|
2062
|
+
await sendSignal("bye", { reason: "done" }).catch(() => {
|
|
2063
|
+
});
|
|
2064
|
+
setTimeout(cleanup, 1500);
|
|
2065
|
+
return { ok: true, via: "webrtc", fileId };
|
|
2066
|
+
} catch (e) {
|
|
2067
|
+
await sendSignal("bye", { reason: "fallback" }).catch(() => {
|
|
2068
|
+
});
|
|
2069
|
+
cleanup();
|
|
2070
|
+
return { ok: false, fallback: true, error: e && e.message || String(e) };
|
|
1757
2071
|
}
|
|
1758
|
-
};
|
|
1759
|
-
|
|
2072
|
+
}, []);
|
|
2073
|
+
const dismissIncoming = React.useCallback((id) => {
|
|
2074
|
+
setIncoming((arr) => {
|
|
2075
|
+
const item = arr.find((x) => x.id === id);
|
|
2076
|
+
if (item && item.url) setTimeout(() => URL.revokeObjectURL(item.url), 1e3);
|
|
2077
|
+
return arr.filter((x) => x.id !== id);
|
|
2078
|
+
});
|
|
2079
|
+
}, []);
|
|
2080
|
+
return { incoming, sendFile, dismissIncoming };
|
|
2081
|
+
}
|
|
2082
|
+
function RtcFileInbox({ T, peers, ctl }) {
|
|
2083
|
+
const items = ctl && ctl.incoming || [];
|
|
2084
|
+
if (!items.length) return null;
|
|
2085
|
+
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) => {
|
|
2086
|
+
const peer = (peers || []).find((p) => p.id === it.peerId || p.userId === it.peerId) || {};
|
|
2087
|
+
const who = peer.alias || peer.name || shortKey(it.peerId, 8, 5);
|
|
2088
|
+
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")));
|
|
2089
|
+
}));
|
|
2090
|
+
}
|
|
2091
|
+
Object.assign(window, { useRtcFileController, RtcFileInbox });
|
|
1760
2092
|
const CALL_ICE_SERVERS = [
|
|
1761
2093
|
{ urls: "stun:stun.l.google.com:19302" },
|
|
1762
2094
|
{ urls: "turn:tokyo.fi.chat:3478", username: "allcom", credential: "allcompass" }
|
|
@@ -2085,6 +2417,7 @@ const STR = {
|
|
|
2085
2417
|
sendFile: "Send file",
|
|
2086
2418
|
block: "Block peer",
|
|
2087
2419
|
remove: "Remove friend",
|
|
2420
|
+
receivedFile: "Received file",
|
|
2088
2421
|
attach: "Attach file",
|
|
2089
2422
|
message: "Message",
|
|
2090
2423
|
send: "Send",
|
|
@@ -2175,6 +2508,7 @@ const STR = {
|
|
|
2175
2508
|
sendFile: "\u53D1\u9001\u6587\u4EF6",
|
|
2176
2509
|
block: "\u62C9\u9ED1",
|
|
2177
2510
|
remove: "\u5220\u9664\u597D\u53CB",
|
|
2511
|
+
receivedFile: "\u6536\u5230\u6587\u4EF6",
|
|
2178
2512
|
attach: "\u9644\u4EF6",
|
|
2179
2513
|
message: "\u6D88\u606F",
|
|
2180
2514
|
send: "\u53D1\u9001",
|
|
@@ -2279,6 +2613,7 @@ function DkApp() {
|
|
|
2279
2613
|
const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
|
|
2280
2614
|
const callCtl = useCallController(me.userId);
|
|
2281
2615
|
const onCall = (peerId, video) => callCtl.start(peerId, !!video);
|
|
2616
|
+
const rtcFileCtl = useRtcFileController(me.userId);
|
|
2282
2617
|
React.useEffect(() => {
|
|
2283
2618
|
if (!activeId && peers.length) setActiveId(peers[0].id);
|
|
2284
2619
|
}, [peers, activeId]);
|
|
@@ -2315,7 +2650,12 @@ function DkApp() {
|
|
|
2315
2650
|
};
|
|
2316
2651
|
const onSendFile = (file) => {
|
|
2317
2652
|
if (!activeId || !file) return Promise.resolve({ ok: false });
|
|
2318
|
-
return
|
|
2653
|
+
return rtcFileCtl.sendFile(activeId, file).then((r) => {
|
|
2654
|
+
if (r && r.ok && r.via === "webrtc") {
|
|
2655
|
+
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" }));
|
|
2656
|
+
}
|
|
2657
|
+
return dkApi.sendFile(activeId, file);
|
|
2658
|
+
}).then((r) => {
|
|
2319
2659
|
data.loadThread(activeId);
|
|
2320
2660
|
data.refresh();
|
|
2321
2661
|
return r;
|
|
@@ -2381,6 +2721,6 @@ function DkApp() {
|
|
|
2381
2721
|
color: "#fff",
|
|
2382
2722
|
fontSize: 12,
|
|
2383
2723
|
fontWeight: 600
|
|
2384
|
-
} }, n.label)))), /* @__PURE__ */ React.createElement(IncomingCallModal, { T, ctl: callCtl, peers }), /* @__PURE__ */ React.createElement(CallOverlay, { T, ctl: callCtl, peers }));
|
|
2724
|
+
} }, n.label)))), /* @__PURE__ */ React.createElement(IncomingCallModal, { T, ctl: callCtl, peers }), /* @__PURE__ */ React.createElement(CallOverlay, { T, ctl: callCtl, peers }), /* @__PURE__ */ React.createElement(RtcFileInbox, { T, peers, ctl: rtcFileCtl }));
|
|
2385
2725
|
}
|
|
2386
2726
|
ReactDOM.createRoot(document.getElementById("root")).render(/* @__PURE__ */ React.createElement(DkApp, null));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.217",
|
|
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.109",
|
|
88
88
|
"@decentnetwork/peer-webrtc": "^0.2.10",
|
|
89
89
|
"ink": "^5.2.1",
|
|
90
90
|
"js-yaml": "^4.1.0",
|