@decentnetwork/lan 0.1.144 → 0.1.146
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/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/daemon/ipc.d.ts +4 -1
- package/dist/daemon/ipc.js +5 -0
- package/dist/daemon/message-store.d.ts +5 -0
- package/dist/daemon/message-store.js +18 -0
- package/dist/daemon/server.js +33 -1
- package/dist/proxy/multi-exit-router.js +11 -5
- package/dist/ui/desktop/app.js +203 -105
- package/dist/ui/server.js +13 -0
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/daemon/ipc.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ export interface IpcHandlers {
|
|
|
76
76
|
subscribe?: (emit: (event: Record<string, unknown>) => void) => () => void;
|
|
77
77
|
/** Offer a local file (by path) to a friend over toxcore file transfer. */
|
|
78
78
|
fileSend: (userid: string, path: string) => Promise<Record<string, unknown>>;
|
|
79
|
+
/** Delete file/chat messages by id (and their on-disk files). */
|
|
80
|
+
fileDelete: (peer: string, ids: string[]) => Promise<Record<string, unknown>>;
|
|
79
81
|
/** Mark a conversation read up to `ts` (defaults to now) — clears unread. */
|
|
80
82
|
chatMarkRead: (userid: string, ts?: number) => Promise<void>;
|
|
81
83
|
/** Re-read proxy allowlist from config and apply it to the running
|
|
@@ -100,7 +102,7 @@ export interface IpcHandlers {
|
|
|
100
102
|
selfRestart: () => Promise<Record<string, unknown>>;
|
|
101
103
|
}
|
|
102
104
|
export interface IpcRequest {
|
|
103
|
-
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" | "chat-mark-read" | "subscribe" | "sign" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
105
|
+
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" | "chat-mark-read" | "subscribe" | "sign" | "proxy-reload" | "proxy-access" | "self-restart";
|
|
104
106
|
address?: string;
|
|
105
107
|
hello?: string;
|
|
106
108
|
userid?: string;
|
|
@@ -113,6 +115,7 @@ export interface IpcRequest {
|
|
|
113
115
|
limit?: number;
|
|
114
116
|
ts?: number;
|
|
115
117
|
enabled?: boolean;
|
|
118
|
+
ids?: string[];
|
|
116
119
|
}
|
|
117
120
|
export interface IpcResponseOk {
|
|
118
121
|
ok: true;
|
package/dist/daemon/ipc.js
CHANGED
|
@@ -220,6 +220,11 @@ export class IpcServer {
|
|
|
220
220
|
throw new Error("path is required");
|
|
221
221
|
return await this.handlers.fileSend(req.userid, req.path);
|
|
222
222
|
}
|
|
223
|
+
case "file-delete": {
|
|
224
|
+
if (!req.userid)
|
|
225
|
+
throw new Error("userid is required");
|
|
226
|
+
return await this.handlers.fileDelete(req.userid, req.ids ?? []);
|
|
227
|
+
}
|
|
223
228
|
case "chat-mark-read": {
|
|
224
229
|
if (!req.userid)
|
|
225
230
|
throw new Error("userid is required");
|
|
@@ -32,6 +32,7 @@ export interface ChatMessage {
|
|
|
32
32
|
sent?: number;
|
|
33
33
|
durationMs?: number;
|
|
34
34
|
avgKbps?: number;
|
|
35
|
+
kbps?: number;
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
export declare class MessageStore {
|
|
@@ -69,7 +70,11 @@ export declare class MessageStore {
|
|
|
69
70
|
sent?: number;
|
|
70
71
|
durationMs?: number;
|
|
71
72
|
avgKbps?: number;
|
|
73
|
+
kbps?: number;
|
|
72
74
|
}): boolean;
|
|
75
|
+
/** Remove messages by id. Returns the removed ones so the caller can clean up
|
|
76
|
+
* any on-disk file the chip pointed at. */
|
|
77
|
+
deleteMessages(peer: string, ids: string[]): ChatMessage[];
|
|
73
78
|
private push;
|
|
74
79
|
/**
|
|
75
80
|
* Return history. With no peer, returns every peer's full thread (the legacy
|
|
@@ -89,6 +89,24 @@ export class MessageStore {
|
|
|
89
89
|
this.scheduleSave();
|
|
90
90
|
return true;
|
|
91
91
|
}
|
|
92
|
+
/** Remove messages by id. Returns the removed ones so the caller can clean up
|
|
93
|
+
* any on-disk file the chip pointed at. */
|
|
94
|
+
deleteMessages(peer, ids) {
|
|
95
|
+
const arr = this.byPeer.get(peer);
|
|
96
|
+
if (!arr || !ids.length)
|
|
97
|
+
return [];
|
|
98
|
+
const want = new Set(ids);
|
|
99
|
+
const removed = [];
|
|
100
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
101
|
+
if (want.has(arr[i].id)) {
|
|
102
|
+
removed.push(arr[i]);
|
|
103
|
+
arr.splice(i, 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (removed.length)
|
|
107
|
+
this.scheduleSave();
|
|
108
|
+
return removed;
|
|
109
|
+
}
|
|
92
110
|
push(peer, msg) {
|
|
93
111
|
let arr = this.byPeer.get(peer);
|
|
94
112
|
if (!arr) {
|
package/dist/daemon/server.js
CHANGED
|
@@ -470,6 +470,29 @@ export class DaemonServer {
|
|
|
470
470
|
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
471
471
|
return { fileId, name, size: data.length };
|
|
472
472
|
},
|
|
473
|
+
fileDelete: async (userid, ids) => {
|
|
474
|
+
const fs = await import("fs/promises");
|
|
475
|
+
const removed = this.messageStore?.deleteMessages(userid, ids) ?? [];
|
|
476
|
+
const dir = resolve(this.configDir, "downloads");
|
|
477
|
+
let files = 0;
|
|
478
|
+
for (const m of removed) {
|
|
479
|
+
if (!m.file)
|
|
480
|
+
continue;
|
|
481
|
+
// Drop the tracking entry for any still-active send so its later
|
|
482
|
+
// progress events don't try to patch a now-deleted chip.
|
|
483
|
+
for (const [fid, v] of this.activeSends)
|
|
484
|
+
if (v.msgId === m.id)
|
|
485
|
+
this.activeSends.delete(fid);
|
|
486
|
+
try {
|
|
487
|
+
await fs.unlink(resolve(dir, sanitizeFileName(m.file.name)));
|
|
488
|
+
files++;
|
|
489
|
+
}
|
|
490
|
+
catch { /* not on disk (e.g. incoming still in .partial) */ }
|
|
491
|
+
}
|
|
492
|
+
this.ipcEvents.emit("event", { type: "chat", userid, dir: "out" });
|
|
493
|
+
this.logger.info(`Deleted ${removed.length} message(s), ${files} file(s) for ${userid.slice(0, 8)}`);
|
|
494
|
+
return { deleted: removed.length, files };
|
|
495
|
+
},
|
|
473
496
|
chatMarkRead: async (userid, ts) => {
|
|
474
497
|
this.friendMeta?.markRead(userid, ts);
|
|
475
498
|
},
|
|
@@ -786,7 +809,16 @@ export class DaemonServer {
|
|
|
786
809
|
const t = this.activeSends.get(p.fileId);
|
|
787
810
|
if (!t)
|
|
788
811
|
return;
|
|
789
|
-
|
|
812
|
+
// Live speed = Δbytes / Δtime between progress events, lightly smoothed so
|
|
813
|
+
// the UI shows a steady KB/s next to the percentage instead of a jumpy one.
|
|
814
|
+
const now = Date.now();
|
|
815
|
+
if (t.lastSent !== undefined && t.lastMs !== undefined && now > t.lastMs && p.received >= t.lastSent) {
|
|
816
|
+
const kbps = (p.received - t.lastSent) / 1024 / ((now - t.lastMs) / 1000);
|
|
817
|
+
t.emaKbps = t.emaKbps === undefined ? kbps : 0.6 * t.emaKbps + 0.4 * kbps;
|
|
818
|
+
}
|
|
819
|
+
t.lastSent = p.received;
|
|
820
|
+
t.lastMs = now;
|
|
821
|
+
this.messageStore?.patchFile(t.peer, t.msgId, { sent: p.received, kbps: t.emaKbps !== undefined ? Math.round(t.emaKbps) : undefined });
|
|
790
822
|
this.ipcEvents.emit("event", { type: "chat", userid: t.peer, dir: "out" });
|
|
791
823
|
});
|
|
792
824
|
this.peerManager.on("file-cancel", (p) => {
|
|
@@ -111,8 +111,9 @@ const SPEED_EWMA_KEEP = 0.6; // new = keep*old + (1-keep)*sample
|
|
|
111
111
|
// flaky node never stalls the whole experience.
|
|
112
112
|
const STALL_IDLE_MS = 6000; // no bytes for this long on an open tunnel = stalled → tear down
|
|
113
113
|
const STALL_MIN_OPEN_MS = 4000; // only a tunnel open at least this long can count as a stall
|
|
114
|
-
const STALL_MIN_BYTES = 16 * 1024; // delivered
|
|
115
|
-
const
|
|
114
|
+
const STALL_MIN_BYTES = 16 * 1024; // delivered at least this = a healthy, real segment
|
|
115
|
+
const STALL_DEAD_BYTES = 1024; // relayed LESS than this = the exit is genuinely dead (only THIS counts as a stall); a small-but-real request (playlist/key/keep-alive, 1-16 KB) is NEUTRAL, so it can't flap a healthy exit out of the pool
|
|
116
|
+
const TRIP_AFTER_STALLS = 3; // consecutive DEAD tunnels before an exit is tripped out (hysteresis)
|
|
116
117
|
const TRIP_COOLDOWN_MS = 30_000; // how long a tripped exit stays excluded before it's retried
|
|
117
118
|
// Measures peak short-window throughput of a byte stream. Feed it every chunk
|
|
118
119
|
// length via add(); read peakBps at the end. Peak (not average) shrugs off TCP
|
|
@@ -454,10 +455,10 @@ export function startMultiExitRouter(opts) {
|
|
|
454
455
|
// re-arms on every I/O, so it only fires on genuine inactivity.
|
|
455
456
|
let stalledOut = false;
|
|
456
457
|
up.setTimeout(STALL_IDLE_MS, () => {
|
|
457
|
-
if (meter.total >=
|
|
458
|
+
if (meter.total >= STALL_DEAD_BYTES) {
|
|
458
459
|
up.setTimeout(0);
|
|
459
460
|
return;
|
|
460
|
-
} // real
|
|
461
|
+
} // relayed real data → alive, just idle (keep-alive) — don't tear down
|
|
461
462
|
stalledOut = true;
|
|
462
463
|
up.destroy();
|
|
463
464
|
});
|
|
@@ -472,7 +473,12 @@ export function startMultiExitRouter(opts) {
|
|
|
472
473
|
// Circuit-breaker classification: a tunnel that stayed open a while but
|
|
473
474
|
// delivered almost nothing is a stall (dead/flaky exit); a tunnel that
|
|
474
475
|
// moved real data proves the exit is healthy.
|
|
475
|
-
|
|
476
|
+
// Only a tunnel that relayed ~NOTHING (dead exit) counts as a stall. A
|
|
477
|
+
// small-but-real request (1-16 KB: playlist, key, keep-alive) is neutral
|
|
478
|
+
// — that's what stops healthy exits flapping out of the pool on normal
|
|
479
|
+
// CCTV/HLS chatter. A real segment (≥16 KB) proves the exit healthy.
|
|
480
|
+
const relayedNothing = meter.total < STALL_DEAD_BYTES;
|
|
481
|
+
if (relayedNothing && (stalledOut || durMs >= STALL_MIN_OPEN_MS)) {
|
|
476
482
|
recordStall(exit, stalledOut ? "idle stall" : "no data");
|
|
477
483
|
}
|
|
478
484
|
else if (meter.total >= STALL_MIN_BYTES) {
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -140,6 +140,13 @@ function dkFileSize(n) {
|
|
|
140
140
|
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + " MB";
|
|
141
141
|
return (n / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
|
142
142
|
}
|
|
143
|
+
function dkSpeed(kbps) {
|
|
144
|
+
if (kbps == null || !(kbps >= 0)) return "";
|
|
145
|
+
return kbps >= 1024 ? (kbps / 1024).toFixed(2) + " MB/s" : Math.round(kbps) + " KB/s";
|
|
146
|
+
}
|
|
147
|
+
function dkPct(p) {
|
|
148
|
+
return p == null ? "\u2026" : (p >= 100 ? "100" : p.toFixed(2)) + "%";
|
|
149
|
+
}
|
|
143
150
|
function dkFileUrl(name) {
|
|
144
151
|
return "/api/file-download?name=" + encodeURIComponent(name);
|
|
145
152
|
}
|
|
@@ -208,6 +215,7 @@ const dkApi = {
|
|
|
208
215
|
remove: (userid) => dkPost("/api/friend-remove", { userid }),
|
|
209
216
|
alias: (userid, alias) => dkPost("/api/friend-alias", { userid, alias }),
|
|
210
217
|
markRead: (userid) => dkPost("/api/chat-mark-read", { userid }),
|
|
218
|
+
delFiles: (userid, ids) => dkPost("/api/file-delete", { userid, ids }),
|
|
211
219
|
setProfile: (name, description) => dkPost("/api/set-profile", { name, description }),
|
|
212
220
|
sendFile: async (userid, file) => {
|
|
213
221
|
try {
|
|
@@ -276,6 +284,7 @@ function useDaemonData() {
|
|
|
276
284
|
const d = await dkGet("/api/chat-history?peer=" + encodeURIComponent(peerId));
|
|
277
285
|
const arr = d.chats && d.chats[peerId] || [];
|
|
278
286
|
const msgs = arr.map((m) => ({
|
|
287
|
+
id: m.id,
|
|
279
288
|
from: m.dir === "out" ? "me" : "them",
|
|
280
289
|
time: dkClock(m.ts),
|
|
281
290
|
text: m.text,
|
|
@@ -285,7 +294,8 @@ function useDaemonData() {
|
|
|
285
294
|
dir: m.dir,
|
|
286
295
|
media: dkFileMediaKind(m.file.name),
|
|
287
296
|
status: m.file.status,
|
|
288
|
-
pct: m.file.status === "sending" && m.file.size ? Math.min(100,
|
|
297
|
+
pct: m.file.status === "sending" && m.file.size ? Math.min(100, (m.file.sent || 0) / m.file.size * 100) : void 0,
|
|
298
|
+
kbps: m.file.kbps
|
|
289
299
|
} : void 0,
|
|
290
300
|
status: m.dir === "out" ? m.status === "queued" ? "queued" : "read" : void 0
|
|
291
301
|
}));
|
|
@@ -1044,114 +1054,155 @@ const mediaBtnStyle = {
|
|
|
1044
1054
|
justifyContent: "center",
|
|
1045
1055
|
lineHeight: 0
|
|
1046
1056
|
};
|
|
1047
|
-
function Msg({ m, peer, T, onTheater }) {
|
|
1057
|
+
function Msg({ m, peer, T, onTheater, onDelete, selMode, selected, onToggleSel }) {
|
|
1048
1058
|
const mine = m.from === "me";
|
|
1049
|
-
return /* @__PURE__ */ React.createElement(
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
// Received media → inline preview / player, with name, size, download.
|
|
1055
|
-
/* @__PURE__ */ React.createElement("div", { style: {
|
|
1056
|
-
display: "flex",
|
|
1057
|
-
flexDirection: "column",
|
|
1058
|
-
gap: 6,
|
|
1059
|
-
maxWidth: m.file.media === "image" || m.file.media === "video" ? "100%" : 300,
|
|
1060
|
-
width: m.file.media === "image" || m.file.media === "video" ? "100%" : void 0,
|
|
1061
|
-
background: "var(--bub-them)",
|
|
1062
|
-
border: "1px solid var(--line)",
|
|
1063
|
-
borderRadius: 12,
|
|
1064
|
-
padding: 6
|
|
1065
|
-
} }, (m.file.media === "image" || m.file.media === "video") && /* @__PURE__ */ React.createElement("div", { "data-media": true, style: { position: "relative", lineHeight: 0 } }, m.file.media === "image" ? /* @__PURE__ */ React.createElement(
|
|
1066
|
-
"img",
|
|
1067
|
-
{
|
|
1068
|
-
src: dkFileUrl(m.file.name),
|
|
1069
|
-
alt: m.file.name,
|
|
1070
|
-
style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain" }
|
|
1071
|
-
}
|
|
1072
|
-
) : /* @__PURE__ */ React.createElement(
|
|
1073
|
-
"video",
|
|
1074
|
-
{
|
|
1075
|
-
src: dkFileUrl(m.file.name),
|
|
1076
|
-
controls: true,
|
|
1077
|
-
preload: "metadata",
|
|
1078
|
-
style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain", background: "#000" }
|
|
1079
|
-
}
|
|
1080
|
-
), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", top: 8, right: 8, display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(
|
|
1081
|
-
"button",
|
|
1082
|
-
{
|
|
1083
|
-
onClick: () => onTheater({ kind: m.file.media, url: dkFileUrl(m.file.name), name: m.file.name }),
|
|
1084
|
-
title: "theater / \u7F51\u9875\u5168\u5C4F",
|
|
1085
|
-
style: mediaBtnStyle
|
|
1086
|
-
},
|
|
1087
|
-
/* @__PURE__ */ React.createElement(Icon, { name: "theater", size: 16, stroke: 2, color: "#fff" })
|
|
1088
|
-
), /* @__PURE__ */ React.createElement(
|
|
1089
|
-
"button",
|
|
1090
|
-
{
|
|
1091
|
-
onClick: (e) => {
|
|
1092
|
-
const w = e.currentTarget.closest("[data-media]");
|
|
1093
|
-
const el = w && w.querySelector("video,img");
|
|
1094
|
-
if (el && el.requestFullscreen) el.requestFullscreen();
|
|
1095
|
-
},
|
|
1096
|
-
title: "fullscreen / \u5168\u5C4F",
|
|
1097
|
-
style: mediaBtnStyle
|
|
1098
|
-
},
|
|
1099
|
-
/* @__PURE__ */ React.createElement(Icon, { name: "maximize", size: 16, stroke: 2, color: "#fff" })
|
|
1100
|
-
))), m.file.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: dkFileUrl(m.file.name), controls: true, style: { width: "100%" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, m.file.size), /* @__PURE__ */ React.createElement("a", { href: dkFileDownloadUrl(m.file.name), download: m.file.name, title: "download", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "var(--accent)" }))))
|
|
1101
|
-
) : (() => {
|
|
1102
|
-
const cardStyle = {
|
|
1059
|
+
return /* @__PURE__ */ React.createElement(
|
|
1060
|
+
"div",
|
|
1061
|
+
{
|
|
1062
|
+
onClick: selMode ? () => onToggleSel(m.id) : void 0,
|
|
1063
|
+
style: {
|
|
1103
1064
|
display: "flex",
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
cursor: mine ? "default" : "pointer"
|
|
1113
|
-
};
|
|
1114
|
-
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 }));
|
|
1115
|
-
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 sending ${m.file.pct != null ? m.file.pct + "%" : "\u2026"}` : m.file.status === "failed" ? `${m.file.size} \xB7 failed` : mine ? `${m.file.size} \xB7 sent` : `${m.file.size} \xB7 ${T.open || "open"}`));
|
|
1116
|
-
if (mine) {
|
|
1117
|
-
return /* @__PURE__ */ React.createElement("div", { style: cardStyle }, icon, meta, 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)" } }, m.file.pct != null ? m.file.pct + "%" : "\u2026") : 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)" }));
|
|
1065
|
+
justifyContent: mine ? "flex-end" : "flex-start",
|
|
1066
|
+
alignItems: "flex-end",
|
|
1067
|
+
gap: 8,
|
|
1068
|
+
margin: "3px 0",
|
|
1069
|
+
cursor: selMode ? "pointer" : "default",
|
|
1070
|
+
borderRadius: 8,
|
|
1071
|
+
background: selMode && selected ? "rgba(91,140,255,0.12)" : "transparent",
|
|
1072
|
+
padding: selMode ? "2px 4px" : 0
|
|
1118
1073
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1074
|
+
},
|
|
1075
|
+
selMode && /* @__PURE__ */ React.createElement("div", { style: {
|
|
1076
|
+
alignSelf: "center",
|
|
1077
|
+
width: 18,
|
|
1078
|
+
height: 18,
|
|
1079
|
+
borderRadius: 5,
|
|
1080
|
+
flexShrink: 0,
|
|
1081
|
+
border: "1.5px solid " + (selected ? "var(--accent)" : "var(--line)"),
|
|
1082
|
+
background: selected ? "var(--accent)" : "transparent",
|
|
1083
|
+
display: "flex",
|
|
1084
|
+
alignItems: "center",
|
|
1085
|
+
justifyContent: "center"
|
|
1086
|
+
} }, selected && /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 3, color: "#fff" })),
|
|
1087
|
+
!mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }),
|
|
1088
|
+
/* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.media === "video") ? "min(680px, 88%)" : "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, m.file ? (
|
|
1089
|
+
// Inline preview/player. Received media: as soon as it's saved.
|
|
1090
|
+
// Sent media: once it's fully delivered ('sent') — the daemon keeps a
|
|
1091
|
+
// local copy so it plays on the sender's side too.
|
|
1092
|
+
(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") ? (
|
|
1093
|
+
// Received media → inline preview / player, with name, size, download.
|
|
1094
|
+
/* @__PURE__ */ React.createElement("div", { style: {
|
|
1095
|
+
display: "flex",
|
|
1096
|
+
flexDirection: "column",
|
|
1097
|
+
gap: 6,
|
|
1098
|
+
maxWidth: m.file.media === "image" || m.file.media === "video" ? "100%" : 300,
|
|
1099
|
+
width: m.file.media === "image" || m.file.media === "video" ? "100%" : void 0,
|
|
1100
|
+
background: "var(--bub-them)",
|
|
1101
|
+
border: "1px solid var(--line)",
|
|
1102
|
+
borderRadius: 12,
|
|
1103
|
+
padding: 6
|
|
1104
|
+
} }, (m.file.media === "image" || m.file.media === "video") && /* @__PURE__ */ React.createElement("div", { "data-media": true, style: { position: "relative", lineHeight: 0 } }, m.file.media === "image" ? /* @__PURE__ */ React.createElement(
|
|
1105
|
+
"img",
|
|
1106
|
+
{
|
|
1107
|
+
src: dkFileUrl(m.file.name),
|
|
1108
|
+
alt: m.file.name,
|
|
1109
|
+
style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain" }
|
|
1110
|
+
}
|
|
1111
|
+
) : /* @__PURE__ */ React.createElement(
|
|
1112
|
+
"video",
|
|
1113
|
+
{
|
|
1114
|
+
src: dkFileUrl(m.file.name),
|
|
1115
|
+
controls: true,
|
|
1116
|
+
preload: "metadata",
|
|
1117
|
+
style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain", background: "#000" }
|
|
1118
|
+
}
|
|
1119
|
+
), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", top: 8, right: 8, display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(
|
|
1120
|
+
"button",
|
|
1130
1121
|
{
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
onClick: (e) => e.stopPropagation(),
|
|
1135
|
-
style: { display: "inline-flex", flexShrink: 0 }
|
|
1122
|
+
onClick: () => onTheater({ kind: m.file.media, url: dkFileUrl(m.file.name), name: m.file.name }),
|
|
1123
|
+
title: "theater / \u7F51\u9875\u5168\u5C4F",
|
|
1124
|
+
style: mediaBtnStyle
|
|
1136
1125
|
},
|
|
1137
|
-
/* @__PURE__ */ React.createElement(Icon, { name: "
|
|
1138
|
-
)
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1126
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "theater", size: 16, stroke: 2, color: "#fff" })
|
|
1127
|
+
), /* @__PURE__ */ React.createElement(
|
|
1128
|
+
"button",
|
|
1129
|
+
{
|
|
1130
|
+
onClick: (e) => {
|
|
1131
|
+
const w = e.currentTarget.closest("[data-media]");
|
|
1132
|
+
const el = w && w.querySelector("video,img");
|
|
1133
|
+
if (el && el.requestFullscreen) el.requestFullscreen();
|
|
1134
|
+
},
|
|
1135
|
+
title: "fullscreen / \u5168\u5C4F",
|
|
1136
|
+
style: mediaBtnStyle
|
|
1137
|
+
},
|
|
1138
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "maximize", size: 16, stroke: 2, color: "#fff" })
|
|
1139
|
+
))), m.file.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: dkFileUrl(m.file.name), controls: true, style: { width: "100%" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px" } }, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, m.file.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, m.file.size), /* @__PURE__ */ React.createElement("a", { href: dkFileDownloadUrl(m.file.name), download: m.file.name, title: "download", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "var(--accent)" }))))
|
|
1140
|
+
) : (() => {
|
|
1141
|
+
const cardStyle = {
|
|
1142
|
+
display: "flex",
|
|
1143
|
+
alignItems: "center",
|
|
1144
|
+
gap: 10,
|
|
1145
|
+
padding: "10px 12px",
|
|
1146
|
+
borderRadius: 10,
|
|
1147
|
+
background: mine ? "var(--bub-me)" : "var(--bub-them)",
|
|
1148
|
+
border: "1px solid " + (mine ? "transparent" : "var(--line)"),
|
|
1149
|
+
minWidth: 200,
|
|
1150
|
+
textDecoration: "none",
|
|
1151
|
+
cursor: mine ? "default" : "pointer"
|
|
1152
|
+
};
|
|
1153
|
+
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 }));
|
|
1154
|
+
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"}`));
|
|
1155
|
+
if (mine) {
|
|
1156
|
+
return /* @__PURE__ */ React.createElement("div", { style: cardStyle }, icon, meta, 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)" }));
|
|
1157
|
+
}
|
|
1158
|
+
return /* @__PURE__ */ React.createElement(
|
|
1159
|
+
"div",
|
|
1160
|
+
{
|
|
1161
|
+
style: cardStyle,
|
|
1162
|
+
title: "open",
|
|
1163
|
+
onClick: () => window.open(dkFileUrl(m.file.name), "_blank", "noopener")
|
|
1164
|
+
},
|
|
1165
|
+
icon,
|
|
1166
|
+
meta,
|
|
1167
|
+
/* @__PURE__ */ React.createElement(
|
|
1168
|
+
"a",
|
|
1169
|
+
{
|
|
1170
|
+
href: dkFileDownloadUrl(m.file.name),
|
|
1171
|
+
download: m.file.name,
|
|
1172
|
+
title: "download",
|
|
1173
|
+
onClick: (e) => e.stopPropagation(),
|
|
1174
|
+
style: { display: "inline-flex", flexShrink: 0 }
|
|
1175
|
+
},
|
|
1176
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "download", size: 16, stroke: 2, color: "var(--accent)" })
|
|
1177
|
+
)
|
|
1178
|
+
);
|
|
1179
|
+
})()
|
|
1180
|
+
) : /* @__PURE__ */ React.createElement("div", { style: {
|
|
1181
|
+
padding: "8px 12px",
|
|
1182
|
+
borderRadius: 12,
|
|
1183
|
+
borderBottomRightRadius: mine ? 4 : 12,
|
|
1184
|
+
borderBottomLeftRadius: mine ? 12 : 4,
|
|
1185
|
+
background: mine ? "var(--bub-me)" : "var(--bub-them)",
|
|
1186
|
+
color: mine ? "#fff" : "var(--text)",
|
|
1187
|
+
border: "1px solid " + (mine ? "transparent" : "var(--line)"),
|
|
1188
|
+
fontFamily: "var(--ui)",
|
|
1189
|
+
fontSize: 13.5,
|
|
1190
|
+
lineHeight: 1.4,
|
|
1191
|
+
letterSpacing: -0.1,
|
|
1192
|
+
wordBreak: "break-word"
|
|
1193
|
+
} }, m.text), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), !selMode && m.id && /* @__PURE__ */ React.createElement(
|
|
1194
|
+
"button",
|
|
1195
|
+
{
|
|
1196
|
+
title: T.delete || "delete",
|
|
1197
|
+
onClick: (e) => {
|
|
1198
|
+
e.stopPropagation();
|
|
1199
|
+
onDelete(m.id);
|
|
1200
|
+
},
|
|
1201
|
+
style: { background: "none", border: "none", cursor: "pointer", padding: 0, marginLeft: 2, display: "inline-flex", opacity: 0.55 }
|
|
1202
|
+
},
|
|
1203
|
+
/* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 11, stroke: 2, color: "var(--faint)" })
|
|
1204
|
+
), 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)" })))
|
|
1205
|
+
);
|
|
1155
1206
|
}
|
|
1156
1207
|
function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onAlias, onRemove, onOpenNet }) {
|
|
1157
1208
|
const scrollRef = React.useRef(null);
|
|
@@ -1161,6 +1212,33 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1161
1212
|
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
1162
1213
|
}, [peer.id, threadProp]);
|
|
1163
1214
|
const [menu, setMenu] = React.useState(false);
|
|
1215
|
+
const [selMode, setSelMode] = React.useState(false);
|
|
1216
|
+
const [sel, setSel] = React.useState(() => /* @__PURE__ */ new Set());
|
|
1217
|
+
const [hidden, setHidden] = React.useState(() => /* @__PURE__ */ new Set());
|
|
1218
|
+
React.useEffect(() => {
|
|
1219
|
+
setSelMode(false);
|
|
1220
|
+
setSel(/* @__PURE__ */ new Set());
|
|
1221
|
+
setHidden(/* @__PURE__ */ new Set());
|
|
1222
|
+
}, [peer.id]);
|
|
1223
|
+
const toggleSel = (id) => setSel((s) => {
|
|
1224
|
+
const n = new Set(s);
|
|
1225
|
+
n.has(id) ? n.delete(id) : n.add(id);
|
|
1226
|
+
return n;
|
|
1227
|
+
});
|
|
1228
|
+
const doDelete = (ids) => {
|
|
1229
|
+
ids = ids.filter(Boolean);
|
|
1230
|
+
if (!ids.length) return;
|
|
1231
|
+
if (!window.confirm(ids.length === 1 ? T.delete1 || "Delete this message and its file?" : (T.deleteN || "Delete {n} selected?").replace("{n}", ids.length))) return;
|
|
1232
|
+
setHidden((h) => {
|
|
1233
|
+
const n = new Set(h);
|
|
1234
|
+
ids.forEach((i) => n.add(i));
|
|
1235
|
+
return n;
|
|
1236
|
+
});
|
|
1237
|
+
dkApi.delFiles(peer.userId, ids).catch(() => {
|
|
1238
|
+
});
|
|
1239
|
+
setSel(/* @__PURE__ */ new Set());
|
|
1240
|
+
setSelMode(false);
|
|
1241
|
+
};
|
|
1164
1242
|
const [draft, setDraft] = React.useState("");
|
|
1165
1243
|
const [dragOver, setDragOver] = React.useState(false);
|
|
1166
1244
|
const [sending, setSending] = React.useState(null);
|
|
@@ -1215,11 +1293,31 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
1215
1293
|
/* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--accent)", display: "inline-flex", alignItems: "center", gap: 4 } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 }), " ", peer.ip))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(Btn, { icon: "more", onClick: () => setMenu((v) => !v) }), menu && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { onClick: () => setMenu(false), style: { position: "fixed", inset: 0, zIndex: 40 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 0, top: 36, zIndex: 50, width: 180, background: "var(--panel-2)", border: "1px solid var(--line)", borderRadius: 9, padding: 6, boxShadow: "0 14px 40px rgba(0,0,0,0.4)" } }, /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
|
|
1216
1294
|
setMenu(false);
|
|
1217
1295
|
onAlias(peer);
|
|
1296
|
+
} }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "check", label: T.selectDelete || "Select & delete", onClick: () => {
|
|
1297
|
+
setMenu(false);
|
|
1298
|
+
setSelMode(true);
|
|
1218
1299
|
} }), /* @__PURE__ */ React.createElement("div", { style: { height: 1, background: "var(--line)", margin: "5px 4px" } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "trash", label: T.remove, danger: true, onClick: () => {
|
|
1219
1300
|
setMenu(false);
|
|
1220
1301
|
onRemove(peer);
|
|
1221
1302
|
} }))))),
|
|
1222
|
-
/* @__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.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(
|
|
1303
|
+
/* @__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(
|
|
1304
|
+
Msg,
|
|
1305
|
+
{
|
|
1306
|
+
key: m.id || i,
|
|
1307
|
+
m,
|
|
1308
|
+
peer,
|
|
1309
|
+
T,
|
|
1310
|
+
onTheater: setTheater,
|
|
1311
|
+
onDelete: (id) => doDelete([id]),
|
|
1312
|
+
selMode,
|
|
1313
|
+
selected: sel.has(m.id),
|
|
1314
|
+
onToggleSel: toggleSel
|
|
1315
|
+
}
|
|
1316
|
+
)))),
|
|
1317
|
+
selMode && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "10px 16px", background: "var(--panel)", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, sel.size, " ", T.selected || "selected"), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: () => {
|
|
1318
|
+
setSelMode(false);
|
|
1319
|
+
setSel(/* @__PURE__ */ new Set());
|
|
1320
|
+
} }, T.cancel || "Cancel"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", icon: "trash", onClick: () => doDelete([...sel]) }, T.delete || "Delete")),
|
|
1223
1321
|
/* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } }, 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(
|
|
1224
1322
|
"input",
|
|
1225
1323
|
{
|
package/dist/ui/server.js
CHANGED
|
@@ -258,6 +258,19 @@ export function startFriendUi(opts) {
|
|
|
258
258
|
});
|
|
259
259
|
return;
|
|
260
260
|
}
|
|
261
|
+
// Delete file/chat messages by id (removes their on-disk files too).
|
|
262
|
+
if (req.method === "POST" && url === "/api/file-delete") {
|
|
263
|
+
const body = await readBody(req);
|
|
264
|
+
const userid = typeof body.userid === "string" ? body.userid : "";
|
|
265
|
+
const ids = Array.isArray(body.ids) ? body.ids.filter((x) => typeof x === "string") : [];
|
|
266
|
+
if (!userid || !ids.length) {
|
|
267
|
+
sendJson(res, 400, { ok: false, error: "userid and ids required" });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const r = await opts.call({ op: "file-delete", userid, ids });
|
|
271
|
+
sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, ...(r.data ?? {}) } : { ok: false, error: r.error });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
261
274
|
// ── File transfer ──────────────────────────────────────────────────
|
|
262
275
|
// Upload: the browser POSTs raw file bytes with ?userid=&name= in the
|
|
263
276
|
// query. We stream them to a temp file (no in-memory buffering of large
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.146",
|
|
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",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@decentnetwork/dora": "^0.1.11",
|
|
82
|
-
"@decentnetwork/peer": "^0.1.
|
|
82
|
+
"@decentnetwork/peer": "^0.1.78",
|
|
83
83
|
"ink": "^5.2.1",
|
|
84
84
|
"js-yaml": "^4.1.0",
|
|
85
85
|
"node-forge": "^1.4.0",
|