@decentnetwork/beagle 0.1.36 → 0.1.38

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.
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.35";
1
+ window.__DK_UI_VERSION="0.1.37";
2
2
  const ICON_PATHS = {
3
3
  // ---- tab bar (the four must feel like one set) ----
4
4
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
@@ -1622,7 +1622,7 @@ function DkGroupSender({ grp, T, avatar }) {
1622
1622
  title: grp.sender,
1623
1623
  style: { display: "inline-flex", alignItems: "center", gap: 5, background: "none", border: "none", padding: 0, cursor: "pointer" }
1624
1624
  },
1625
- avatar ? /* @__PURE__ */ React.createElement(DkIdenticon, { seed, size: 24, radius: 6 }) : /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, grp.sender)
1625
+ avatar ? /* @__PURE__ */ React.createElement(DkEnsAvatar, { userid: grp.senderId, fallbackSeed: seed, size: 24, radius: 6 }) : /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, grp.sender)
1626
1626
  ), open && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { onClick: () => setOpen(false), style: { position: "fixed", inset: 0, zIndex: 40 } }), /* @__PURE__ */ React.createElement("span", { style: {
1627
1627
  position: "absolute",
1628
1628
  left: 0,
@@ -1637,7 +1637,7 @@ function DkGroupSender({ grp, T, avatar }) {
1637
1637
  display: "flex",
1638
1638
  flexDirection: "column",
1639
1639
  gap: 8
1640
- } }, /* @__PURE__ */ React.createElement("span", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 700, color: "var(--text)", wordBreak: "break-all" } }, grp.sender)), grp.senderId ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Mono, { size: 10.5, dim: true, copy: grp.senderId, title: grp.senderId }, shortKey(grp.senderId, 12, 8)), /* @__PURE__ */ React.createElement(
1640
+ } }, /* @__PURE__ */ React.createElement("span", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkEnsAvatar, { userid: grp.senderId, fallbackSeed: seed, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 700, color: "var(--text)", wordBreak: "break-all" } }, grp.sender)), grp.senderId ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Mono, { size: 10.5, dim: true, copy: grp.senderId, title: grp.senderId }, shortKey(grp.senderId, 12, 8)), /* @__PURE__ */ React.createElement(
1641
1641
  "button",
1642
1642
  {
1643
1643
  onClick: addFriend,
@@ -2056,33 +2056,68 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onC
2056
2056
  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"}`));
2057
2057
  if (mine) {
2058
2058
  const btnBusy = !!busy;
2059
- 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(
2060
- "button",
2059
+ return /* @__PURE__ */ React.createElement(
2060
+ "div",
2061
2061
  {
2062
- title: T.cancel || "cancel",
2063
- disabled: btnBusy,
2064
- onClick: (e) => {
2065
- e.stopPropagation();
2066
- if (!btnBusy)
2067
- onCancel(m.id);
2068
- },
2069
- 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 }
2062
+ style: { ...cardStyle, cursor: "pointer", opacity: btnBusy ? 0.85 : 1 },
2063
+ title: T.open || "open",
2064
+ onClick: () => window.open(dkFileUrl(m.file.name), "_blank", "noopener")
2070
2065
  },
2071
- /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 13, stroke: 2.4, color: "#fff" })
2072
- ), m.file.status !== "sent" && m.id && /* @__PURE__ */ React.createElement(
2073
- "button",
2074
- {
2075
- title: T.retry || "retry",
2076
- disabled: btnBusy,
2077
- onClick: (e) => {
2078
- e.stopPropagation();
2079
- if (!btnBusy)
2080
- onRetry(m.id);
2066
+ icon,
2067
+ meta,
2068
+ onReveal && /* @__PURE__ */ React.createElement(
2069
+ "button",
2070
+ {
2071
+ title: T.revealInFinder || "\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u663E\u793A / Show in file manager",
2072
+ onClick: (e) => {
2073
+ e.stopPropagation();
2074
+ onReveal(m.file.name);
2075
+ },
2076
+ style: { background: "none", border: "none", cursor: "pointer", padding: 0, display: "inline-flex", flexShrink: 0 }
2081
2077
  },
2082
- 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 }
2083
- },
2084
- /* @__PURE__ */ React.createElement(Icon, { name: "refresh", size: 13, stroke: 2.2, color: "#fff" })
2085
- ));
2078
+ /* @__PURE__ */ React.createElement(Icon, { name: "folderOpen", size: 15, stroke: 2, color: "rgba(255,255,255,0.85)" })
2079
+ ),
2080
+ /* @__PURE__ */ React.createElement(
2081
+ "a",
2082
+ {
2083
+ href: dkFileDownloadUrl(m.file.name),
2084
+ download: m.file.name,
2085
+ title: "download",
2086
+ onClick: (e) => e.stopPropagation(),
2087
+ style: { display: "inline-flex", flexShrink: 0 }
2088
+ },
2089
+ /* @__PURE__ */ React.createElement(Icon, { name: "download", size: 15, stroke: 2, color: "rgba(255,255,255,0.85)" })
2090
+ ),
2091
+ 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)" }),
2092
+ (m.file.status === "sending" || m.file.status === "queued" || busy === "cancel") && m.id && /* @__PURE__ */ React.createElement(
2093
+ "button",
2094
+ {
2095
+ title: T.cancel || "cancel",
2096
+ disabled: btnBusy,
2097
+ onClick: (e) => {
2098
+ e.stopPropagation();
2099
+ if (!btnBusy)
2100
+ onCancel(m.id);
2101
+ },
2102
+ 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 }
2103
+ },
2104
+ /* @__PURE__ */ React.createElement(Icon, { name: "x", size: 13, stroke: 2.4, color: "#fff" })
2105
+ ),
2106
+ m.file.status !== "sent" && m.id && /* @__PURE__ */ React.createElement(
2107
+ "button",
2108
+ {
2109
+ title: T.retry || "retry",
2110
+ disabled: btnBusy,
2111
+ onClick: (e) => {
2112
+ e.stopPropagation();
2113
+ if (!btnBusy)
2114
+ onRetry(m.id);
2115
+ },
2116
+ 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 }
2117
+ },
2118
+ /* @__PURE__ */ React.createElement(Icon, { name: "refresh", size: 13, stroke: 2.2, color: "#fff" })
2119
+ )
2120
+ );
2086
2121
  }
2087
2122
  return /* @__PURE__ */ React.createElement(
2088
2123
  "div",
@@ -2874,6 +2909,39 @@ function DkPunkAvatar({ id, size, radius, fallbackSeed }) {
2874
2909
  }
2875
2910
  );
2876
2911
  }
2912
+ const dkEnsAvCache = /* @__PURE__ */ new Map();
2913
+ function DkEnsAvatar({ userid, fallbackSeed, size, radius }) {
2914
+ const [pub, setPub] = React.useState(() => userid && dkEnsAvCache.has(userid) ? dkEnsAvCache.get(userid) : void 0);
2915
+ React.useEffect(() => {
2916
+ let dead = false;
2917
+ if (!userid) {
2918
+ setPub(null);
2919
+ return void 0;
2920
+ }
2921
+ if (dkEnsAvCache.has(userid)) {
2922
+ setPub(dkEnsAvCache.get(userid));
2923
+ return void 0;
2924
+ }
2925
+ fetch(`/api/ens-profile?userid=${encodeURIComponent(userid)}`).then((r) => r.json()).then((d) => {
2926
+ var _a;
2927
+ const v = d.ok && d.profile ? { avatarUrl: d.profile.avatarUrl || null, punkId: (_a = d.profile.punkId) != null ? _a : null } : null;
2928
+ dkEnsAvCache.set(userid, v);
2929
+ if (!dead)
2930
+ setPub(v);
2931
+ }).catch(() => {
2932
+ if (!dead)
2933
+ setPub(null);
2934
+ });
2935
+ return () => {
2936
+ dead = true;
2937
+ };
2938
+ }, [userid]);
2939
+ if (pub && pub.avatarUrl)
2940
+ return /* @__PURE__ */ React.createElement(DkImgAvatar, { url: pub.avatarUrl, seed: fallbackSeed || userid, size, radius });
2941
+ if (pub && pub.punkId != null)
2942
+ return /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: pub.punkId, size, radius, fallbackSeed: fallbackSeed || userid });
2943
+ return /* @__PURE__ */ React.createElement(DkIdenticon, { seed: fallbackSeed || userid || "x", size, radius });
2944
+ }
2877
2945
  function DkPunkPicker({ T, onPick, onClose }) {
2878
2946
  const TYPES = ["any", "female", "male", "zombie", "ape", "alien"];
2879
2947
  const tLabel = (t) => T && T["pt_" + t] || t;
@@ -3211,7 +3279,7 @@ function ProfileTab({ T, me, onEdit }) {
3211
3279
  setEditing(false);
3212
3280
  } }));
3213
3281
  }
3214
- Object.assign(window, { ProfileTab, DkPunkAvatar, DkPublicProfile });
3282
+ Object.assign(window, { ProfileTab, DkPunkAvatar, DkEnsAvatar, DkPublicProfile });
3215
3283
  function DkDirAvatar({ p, size }) {
3216
3284
  const [broken, setBroken] = React.useState(false);
3217
3285
  if (p.punkId != null && !(p.avatar && !broken)) {
package/dist/server.js CHANGED
@@ -956,8 +956,10 @@ export function startBeagleServer(opts) {
956
956
  }
957
957
  // ── File transfer ──────────────────────────────────────────────────
958
958
  // Upload: the browser POSTs raw file bytes with ?userid=&name= in the
959
- // query. We stream them to a temp file (no in-memory buffering of large
960
- // files) and hand the daemon that path over the existing file-send IPC.
959
+ // query. The upload is saved INTO downloads/ (collision-renamed) and
960
+ // kept the sender's own chip can then open / reveal / download the
961
+ // file just like the receiver's. (It used to stream to a temp file
962
+ // deleted right after send, which made every sent chip a dead end.)
961
963
  if (req.method === "POST" && url === "/api/file-send") {
962
964
  const q = new URL(req.url || "", "http://x").searchParams;
963
965
  const userid = q.get("userid") || "";
@@ -970,17 +972,37 @@ export function startBeagleServer(opts) {
970
972
  const os = await import("node:os");
971
973
  const fs = await import("node:fs");
972
974
  const fsp = await import("node:fs/promises");
973
- const tmpDir = await fsp.mkdtemp(join(os.tmpdir(), "agentnet-up-"));
974
- const tmpPath = join(tmpDir, safe);
975
+ // Keep the copy under downloads when available (normal case); fall
976
+ // back to the old ephemeral temp only when downloads are disabled.
977
+ const keep = !!opts.downloadsDir;
978
+ let destDir;
979
+ if (keep) {
980
+ destDir = opts.downloadsDir;
981
+ await fsp.mkdir(destDir, { recursive: true });
982
+ }
983
+ else {
984
+ destDir = await fsp.mkdtemp(join(os.tmpdir(), "agentnet-up-"));
985
+ }
986
+ let destName = safe;
987
+ let destPath = join(destDir, destName);
988
+ if (keep) {
989
+ const dot = safe.lastIndexOf(".");
990
+ const base = dot > 0 ? safe.slice(0, dot) : safe;
991
+ const ext = dot > 0 ? safe.slice(dot) : "";
992
+ for (let i = 1; existsSync(destPath); i++) {
993
+ destName = `${base}-${i}${ext}`;
994
+ destPath = join(destDir, destName);
995
+ }
996
+ }
975
997
  try {
976
998
  await new Promise((resolve2, reject) => {
977
- const ws = fs.createWriteStream(tmpPath);
999
+ const ws = fs.createWriteStream(destPath);
978
1000
  req.pipe(ws);
979
1001
  req.on("error", reject);
980
1002
  ws.on("error", reject);
981
1003
  ws.on("finish", () => resolve2());
982
1004
  });
983
- const r = await opts.call({ op: "file-send", userid, path: tmpPath });
1005
+ const r = await opts.call({ op: "file-send", userid, path: destPath });
984
1006
  if (!r.ok) {
985
1007
  sendJson(res, 502, r);
986
1008
  return;
@@ -991,7 +1013,8 @@ export function startBeagleServer(opts) {
991
1013
  sendJson(res, 500, { ok: false, error: e instanceof Error ? e.message : String(e) });
992
1014
  }
993
1015
  finally {
994
- await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined);
1016
+ if (!keep)
1017
+ await fsp.rm(destDir, { recursive: true, force: true }).catch(() => undefined);
995
1018
  }
996
1019
  return;
997
1020
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",