@decentnetwork/lan 0.1.225 → 0.1.227

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.
@@ -1181,8 +1181,18 @@ const mediaBtnStyle = {
1181
1181
  justifyContent: "center",
1182
1182
  lineHeight: 0
1183
1183
  };
1184
+ function dkRtcFileFromText(peerId, text) {
1185
+ const m = /^received file via WebRTC: (.+) \(([^)]+)\)$/.exec(String(text || ""));
1186
+ if (!m || !window.__dkRtcFiles) return null;
1187
+ const name = m[1];
1188
+ for (const item of window.__dkRtcFiles.values()) {
1189
+ if (item && item.peerId === peerId && item.name === name) return item;
1190
+ }
1191
+ return null;
1192
+ }
1184
1193
  function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, selected, onToggleSel, busy }) {
1185
1194
  const mine = m.from === "me";
1195
+ const rtcFile = !mine && !m.file ? dkRtcFileFromText(peer.userId || peer.id, m.text) : null;
1186
1196
  return /* @__PURE__ */ React.createElement(
1187
1197
  "div",
1188
1198
  {
@@ -1212,7 +1222,52 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, selMode, sele
1212
1222
  justifyContent: "center"
1213
1223
  } }, selected && /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 3, color: "#fff" })),
1214
1224
  !mine && /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }),
1215
- /* @__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 ? (
1225
+ /* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.media === "video") || rtcFile && (rtcFile.media === "image" || rtcFile.media === "video") ? "min(680px, 88%)" : "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, rtcFile ? /* @__PURE__ */ React.createElement("div", { style: {
1226
+ display: "flex",
1227
+ flexDirection: "column",
1228
+ gap: 6,
1229
+ maxWidth: rtcFile.media === "image" || rtcFile.media === "video" ? "100%" : 300,
1230
+ width: rtcFile.media === "image" || rtcFile.media === "video" ? "100%" : void 0,
1231
+ background: "var(--bub-them)",
1232
+ border: "1px solid var(--line)",
1233
+ borderRadius: 12,
1234
+ padding: 6
1235
+ } }, (rtcFile.media === "image" || rtcFile.media === "video") && /* @__PURE__ */ React.createElement("div", { "data-media": true, style: { position: "relative", lineHeight: 0 } }, rtcFile.media === "image" ? /* @__PURE__ */ React.createElement(
1236
+ "img",
1237
+ {
1238
+ src: rtcFile.url,
1239
+ alt: rtcFile.name,
1240
+ style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain" }
1241
+ }
1242
+ ) : /* @__PURE__ */ React.createElement(
1243
+ "video",
1244
+ {
1245
+ src: rtcFile.url,
1246
+ controls: true,
1247
+ preload: "metadata",
1248
+ style: { display: "block", width: "100%", maxHeight: "72vh", borderRadius: 8, objectFit: "contain", background: "#000" }
1249
+ }
1250
+ ), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", top: 8, right: 8, display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(
1251
+ "button",
1252
+ {
1253
+ onClick: () => onTheater({ kind: rtcFile.media, url: rtcFile.url, name: rtcFile.name }),
1254
+ title: "theater / \u7F51\u9875\u5168\u5C4F",
1255
+ style: mediaBtnStyle
1256
+ },
1257
+ /* @__PURE__ */ React.createElement(Icon, { name: "theater", size: 16, stroke: 2, color: "#fff" })
1258
+ ), /* @__PURE__ */ React.createElement(
1259
+ "button",
1260
+ {
1261
+ onClick: (e) => {
1262
+ const w = e.currentTarget.closest("[data-media]");
1263
+ const el = w && w.querySelector("video,img");
1264
+ if (el && el.requestFullscreen) el.requestFullscreen();
1265
+ },
1266
+ title: "fullscreen / \u5168\u5C4F",
1267
+ style: mediaBtnStyle
1268
+ },
1269
+ /* @__PURE__ */ React.createElement(Icon, { name: "maximize", size: 16, stroke: 2, color: "#fff" })
1270
+ ))), rtcFile.media === "audio" && /* @__PURE__ */ React.createElement("audio", { src: rtcFile.url, 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" } }, rtcFile.name), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", flexShrink: 0 } }, dkFileSize(rtcFile.size)), /* @__PURE__ */ React.createElement("a", { href: rtcFile.url, download: rtcFile.name, title: "download", style: { display: "inline-flex", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "var(--accent)" })))) : m.file ? (
1216
1271
  // Inline preview/player. Received media: as soon as it's saved.
1217
1272
  // Sent media: once it's fully delivered ('sent') — the daemon keeps a
1218
1273
  // local copy so it plays on the sender's side too.
@@ -1947,9 +2002,19 @@ function dkFileDownload(blob, name) {
1947
2002
  a.textContent = name || "agentnet-file";
1948
2003
  return { url, name: a.download };
1949
2004
  }
2005
+ function dkRememberRtcFile(peerId, fileId, name, size, url) {
2006
+ if (!window.__dkRtcFiles) window.__dkRtcFiles = /* @__PURE__ */ new Map();
2007
+ const item = { id: fileId, peerId, name, size, url, media: dkFileMediaKind(name) };
2008
+ window.__dkRtcFiles.set(peerId + "|" + name + "|" + size, item);
2009
+ return item;
2010
+ }
1950
2011
  function useRtcFileController(selfId, onReceivedText) {
1951
2012
  const [incoming, setIncoming] = React.useState([]);
1952
2013
  const peersRef = React.useRef(/* @__PURE__ */ new Map());
2014
+ const onReceivedTextRef = React.useRef(onReceivedText);
2015
+ React.useEffect(() => {
2016
+ onReceivedTextRef.current = onReceivedText;
2017
+ }, [onReceivedText]);
1953
2018
  React.useEffect(() => {
1954
2019
  if (!selfId || !window.RTCPeerConnection || !window.dkRtcSignalBus) return;
1955
2020
  const bus = dkRtcSignalBus();
@@ -1988,8 +2053,9 @@ function useRtcFileController(selfId, onReceivedText) {
1988
2053
  const meta = sess.meta || {};
1989
2054
  const blob = new Blob(sess.chunks, { type: meta.mime || "application/octet-stream" });
1990
2055
  const dl = dkFileDownload(blob, meta.name || "agentnet-file");
2056
+ dkRememberRtcFile(peerId, fileId, dl.name, blob.size, dl.url);
1991
2057
  setIncoming((arr) => [{ id: fileId, peerId, name: dl.name, size: blob.size, url: dl.url }].concat(arr).slice(0, 8));
1992
- if (onReceivedText) onReceivedText(peerId, "received file via WebRTC: " + dl.name + " (" + dkFileSize(blob.size) + ")");
2058
+ if (onReceivedTextRef.current) onReceivedTextRef.current(peerId, "received file via WebRTC: " + dl.name + " (" + dkFileSize(blob.size) + ")");
1993
2059
  cleanup(fileId);
1994
2060
  } else if (msg.type === "cancel") {
1995
2061
  cleanup(fileId);
@@ -2071,7 +2137,7 @@ function useRtcFileController(selfId, onReceivedText) {
2071
2137
  off();
2072
2138
  for (const id of Array.from(peersRef.current.keys())) cleanup(id);
2073
2139
  };
2074
- }, [selfId, onReceivedText]);
2140
+ }, [selfId]);
2075
2141
  const sendFile = React.useCallback(async (peerId, file, onProgress) => {
2076
2142
  if (!peerId || !file || !window.RTCPeerConnection || !window.dkRtcSignalBus) {
2077
2143
  return { ok: false, fallback: true, error: "WebRTC unavailable" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.225",
3
+ "version": "0.1.227",
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",