@decentnetwork/beagle 0.1.96 → 0.1.98

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.96";
1
+ window.__DK_UI_VERSION="0.1.98";
2
2
  "use strict";
3
3
  (() => {
4
4
  // packages/beagle-ui/dist/beagle-ui.js
@@ -516,7 +516,7 @@ window.__DK_UI_VERSION="0.1.96";
516
516
  whiteSpace: "nowrap",
517
517
  overflow: "hidden",
518
518
  textOverflow: "ellipsis"
519
- } }, peer.alias || shortKey(peer.userId, 8, 5)), peer.agent && /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: 13, color: "var(--faint)", stroke: 2 }), muted && /* @__PURE__ */ React.createElement(Icon, { name: "bellOff", size: 12, color: "var(--faint)", stroke: 2, title: T && T.mutedTag || "sound muted" }), peer.wire === "64" && /* @__PURE__ */ React.createElement(Tag, { tone: "off", style: { height: 15, fontSize: 9.5, padding: "0 4px" } }, "w64")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 2 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, peer.ip), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)", fontSize: 11 } }, "\xB7"), /* @__PURE__ */ React.createElement("span", { style: {
519
+ } }, peer.alias || shortKey(peer.userId, 8, 5)), peer.agent && /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: 13, color: "var(--faint)", stroke: 2 }), muted && /* @__PURE__ */ React.createElement(Icon, { name: "bellOff", size: 12, color: "var(--faint)", stroke: 2, title: T && T.mutedTag || "sound muted" }), peer.wire === "64" && /* @__PURE__ */ React.createElement(Tag, { tone: "off", style: { height: 15, fontSize: 9.5, padding: "0 4px" } }, "w64")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 2 } }, peer.ip ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, peer.ip), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)", fontSize: 11 } }, "\xB7")) : null, /* @__PURE__ */ React.createElement("span", { style: {
520
520
  fontSize: 11,
521
521
  color: "var(--dim)",
522
522
  whiteSpace: "nowrap",
@@ -524,12 +524,12 @@ window.__DK_UI_VERSION="0.1.96";
524
524
  textOverflow: "ellipsis",
525
525
  flex: 1,
526
526
  minWidth: 0
527
- } }, dkContactPreview(peer.lastMsg) || peer.lastMsg))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, peer.lastTime), peer.unread ? /* @__PURE__ */ React.createElement(Unread, { n: peer.unread }) : peer.pending ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--warn, #d29922)", title: "friend request pending" }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
527
+ } }, dkRowPreview(peer.lastMsg)))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, peer.lastTime), peer.unread ? /* @__PURE__ */ React.createElement(Unread, { n: peer.unread }) : peer.pending ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--warn, #d29922)", title: "friend request pending" }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
528
528
  }
529
529
  function dkActivityTs(p) {
530
530
  return Math.max(p.lastTs || 0, p.acceptedAt || 0, p.requestedAt || 0);
531
531
  }
532
- function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted }) {
532
+ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide }) {
533
533
  const [q, setQ] = React.useState("");
534
534
  const [addr, setAddr] = React.useState("");
535
535
  const [addState, setAddState] = React.useState(null);
@@ -578,7 +578,7 @@ window.__DK_UI_VERSION="0.1.96";
578
578
  return String(a.alias || a.userId || "").localeCompare(String(b.alias || b.userId || ""));
579
579
  });
580
580
  const online = peers.filter((p) => p.online).length;
581
- return /* @__PURE__ */ React.createElement("div", { style: { width: 320, flexShrink: 0, borderRight: "1px solid var(--line)", display: "flex", flexDirection: "column", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { padding: "12px 12px 10px", borderBottom: "1px solid var(--line)", display: "flex", flexDirection: "column", gap: 8 } }, fromDeepLink && /* @__PURE__ */ React.createElement("div", { style: {
581
+ return /* @__PURE__ */ React.createElement("div", { style: wide ? { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column", background: "var(--panel)" } : { width: 320, flexShrink: 0, borderRight: "1px solid var(--line)", display: "flex", flexDirection: "column", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { padding: "12px 12px 10px", borderBottom: "1px solid var(--line)", display: "flex", flexDirection: "column", gap: 8 } }, fromDeepLink && /* @__PURE__ */ React.createElement("div", { style: {
582
582
  fontFamily: "var(--ui)",
583
583
  fontSize: 12,
584
584
  lineHeight: 1.4,
@@ -771,6 +771,17 @@ window.__DK_UI_VERSION="0.1.96";
771
771
  const link = "\n\n[Open name card](https://app.beagle.chat/#/chat?address=" + encodeURIComponent(p.address || "") + ")";
772
772
  return head + bio + link + "\n\n```beagle-contact\n" + JSON.stringify(j) + "\n```\n";
773
773
  }
774
+ function dkRowPreview(text) {
775
+ const card = dkContactPreview(text);
776
+ if (card)
777
+ return card;
778
+ const s = String(text == null ? "" : text);
779
+ const mine = /^you:\s*/i.test(s);
780
+ const cgp = dkParseCGP1(s.replace(/^you:\s*/i, ""));
781
+ if (cgp)
782
+ return (mine ? "you: " : "") + cgp.sender + ": " + cgp.body.replace(/\s+/g, " ");
783
+ return s;
784
+ }
774
785
  function dkContactPreview(text) {
775
786
  const s = String(text == null ? "" : text);
776
787
  if (!/beagle-contact|BCR1\s*\{/i.test(s))
@@ -1413,6 +1424,8 @@ window.__DK_UI_VERSION="0.1.96";
1413
1424
  background: mine ? "var(--bub-me)" : "var(--bub-them)",
1414
1425
  border: "1px solid " + (mine ? "transparent" : "var(--line)"),
1415
1426
  minWidth: 200,
1427
+ maxWidth: "100%",
1428
+ boxSizing: "border-box",
1416
1429
  textDecoration: "none",
1417
1430
  cursor: mine ? "default" : "pointer"
1418
1431
  };
@@ -1596,7 +1609,7 @@ window.__DK_UI_VERSION="0.1.96";
1596
1609
  ) : mine && m.status === "sent" && m.confirmed === false ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "var(--faint)", title: T.sentUnconfirmed || "sent \u2014 not confirmed by the peer" }) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
1597
1610
  );
1598
1611
  }
1599
- function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted, onToggleMute }) {
1612
+ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted, onToggleMute, phone, onBack }) {
1600
1613
  const scrollRef = React.useRef(null);
1601
1614
  const isGroup = React.useMemo(() => dkThreadIsGroup(threadProp), [threadProp]);
1602
1615
  const followScrollRef = React.useRef(true);
@@ -1869,7 +1882,16 @@ window.__DK_UI_VERSION="0.1.96";
1869
1882
  onDrop
1870
1883
  },
1871
1884
  dragOver && /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", inset: 0, zIndex: 30, background: "rgba(91,140,255,0.10)", border: "2px dashed var(--accent)", borderRadius: 12, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none", fontFamily: "var(--mono)", fontSize: 14, color: "var(--accent)" } }, lang === "zh" ? "\u677E\u5F00\u53D1\u9001\u6587\u4EF6" : "Drop to send file"),
1872
- /* @__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("div", { onClick: () => setPubProfile(true), title: T && T.pubOpen || "View public profile", style: { cursor: "pointer", flexShrink: 0 } }, /* @__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", { onClick: () => setPubProfile(true), style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)", cursor: "pointer" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), isGroup && /* @__PURE__ */ React.createElement(Tag, null, T && T.groupTag || "group"), /* @__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), title: T && T.openNet || "Network view", style: { background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--accent)", display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.ip, title: peer.ip }, peer.ip), peer.address && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement(
1885
+ /* @__PURE__ */ React.createElement("div", { style: { height: phone ? 56 : 60, boxSizing: "content-box", paddingTop: phone ? "env(safe-area-inset-top, 0px)" : 0, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: phone ? 8 : 12, paddingLeft: phone ? 4 : 16, paddingRight: phone ? 8 : 16, background: "var(--panel)" } }, phone && onBack && /* @__PURE__ */ React.createElement(
1886
+ "button",
1887
+ {
1888
+ onClick: onBack,
1889
+ title: T && T.back || "Back",
1890
+ "aria-label": T && T.back || "Back",
1891
+ style: { width: 40, height: 44, flexShrink: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", padding: 0 }
1892
+ },
1893
+ /* @__PURE__ */ React.createElement(Icon, { name: "chevronLeft", size: 26, stroke: 2.2, color: "var(--accent)" })
1894
+ ), /* @__PURE__ */ React.createElement("div", { onClick: () => setPubProfile(true), title: T && T.pubOpen || "View public profile", style: { cursor: "pointer", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 })), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: phone ? 1 : void 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 } }, /* @__PURE__ */ React.createElement("span", { onClick: () => setPubProfile(true), style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)", cursor: "pointer", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 } }, peer.alias || shortKey(peer.userId, phone ? 6 : 10, phone ? 4 : 6)), peer.agent && !phone && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), isGroup && !phone && /* @__PURE__ */ React.createElement(Tag, null, T && T.groupTag || "group"), !phone && /* @__PURE__ */ React.createElement(RouteTag, { peer })), phone ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, display: "flex" } }, /* @__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), title: T && T.openNet || "Network view", style: { background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--accent)", display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.ip, title: peer.ip }, peer.ip), peer.address && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement(
1873
1895
  Mono,
1874
1896
  {
1875
1897
  size: 11.5,
@@ -1880,7 +1902,7 @@ window.__DK_UI_VERSION="0.1.96";
1880
1902
  ${peer.address}`
1881
1903
  },
1882
1904
  shortKey(peer.address, 8, 6)
1883
- )))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), onToggleMute && /* @__PURE__ */ React.createElement(
1905
+ )))), !phone && /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), onToggleMute && !phone && /* @__PURE__ */ React.createElement(
1884
1906
  Btn,
1885
1907
  {
1886
1908
  icon: muted ? "bellOff" : "bell",
@@ -1898,7 +1920,16 @@ ${peer.address}`
1898
1920
  e.target.value = "";
1899
1921
  }
1900
1922
  }
1901
- ), /* @__PURE__ */ React.createElement(Btn, { icon: "file", title: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => rtcFileRef.current && rtcFileRef.current.click() })), /* @__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: () => {
1923
+ ), !phone && /* @__PURE__ */ React.createElement(Btn, { icon: "file", title: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => rtcFileRef.current && rtcFileRef.current.click() })), /* @__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)" } }, phone && onToggleMute && /* @__PURE__ */ React.createElement(MenuItem, { icon: muted ? "bell" : "bellOff", label: muted ? T && T.unmuteSound || "Turn sound back on" : T && T.muteSound || "Mute sound", onClick: () => {
1924
+ setMenu(false);
1925
+ onToggleMute(peer.id);
1926
+ } }), phone && onSendRtcFile && /* @__PURE__ */ React.createElement(MenuItem, { icon: "file", label: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => {
1927
+ setMenu(false);
1928
+ rtcFileRef.current && rtcFileRef.current.click();
1929
+ } }), phone && /* @__PURE__ */ React.createElement(MenuItem, { icon: "network", label: T && T.openNet || "Network view", onClick: () => {
1930
+ setMenu(false);
1931
+ onOpenNet(peer);
1932
+ } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
1902
1933
  setMenu(false);
1903
1934
  onAlias(peer);
1904
1935
  } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "check", label: T.selectDelete || "Select & delete", onClick: () => {
@@ -1919,7 +1950,7 @@ ${peer.address}`
1919
1950
  fontSize: 12.5,
1920
1951
  color: "var(--text)"
1921
1952
  } }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 13, stroke: 2.2, color: "#d29922" }), /* @__PURE__ */ React.createElement("span", null, T.pendingFriend || "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.")),
1922
- /* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: "18px 22px" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, 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(
1953
+ /* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: phone ? "12px 10px" : "18px 22px", WebkitOverflowScrolling: "touch" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, 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(
1923
1954
  Msg,
1924
1955
  {
1925
1956
  key: m.id || i,
@@ -1948,7 +1979,7 @@ ${peer.address}`
1948
1979
  setSelMode(false);
1949
1980
  setSel(/* @__PURE__ */ new Set());
1950
1981
  } }, T.cancel || "Cancel"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", icon: "trash", onClick: () => doDelete([...sel]) }, T.delete || "Delete")),
1951
- /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } }, flash && /* @__PURE__ */ React.createElement("div", { style: {
1982
+ /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: phone ? "8px 8px calc(8px + env(safe-area-inset-bottom, 0px))" : "12px 16px", background: "var(--panel)" } }, flash && /* @__PURE__ */ React.createElement("div", { style: {
1952
1983
  maxWidth: 1200,
1953
1984
  margin: "0 auto 8px",
1954
1985
  display: "flex",
@@ -1965,7 +1996,7 @@ ${peer.address}`
1965
1996
  stroke: 2.2,
1966
1997
  color: flash.kind === "err" ? "var(--danger)" : "var(--accent)"
1967
1998
  }
1968
- ), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, 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.via === "webrtc" ? "WebRTC \xB7 " : "") + sending.name + (sending.size ? " \xB7 " + dkFileSize(sending.sent || 0) + " / " + dkFileSize(sending.size) : "") + (sending.pct != null ? " \xB7 " + Math.min(100, Math.max(0, sending.pct)).toFixed(1) + "%" : "") + (sending.kbps != null ? " \xB7 " + dkSpeed(sending.kbps) : "")), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
1999
+ ), flash.msg), sending && /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, 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.via === "webrtc" ? "WebRTC \xB7 " : "") + sending.name + (sending.size ? " \xB7 " + dkFileSize(sending.sent || 0) + " / " + dkFileSize(sending.size) : "") + (sending.pct != null ? " \xB7 " + Math.min(100, Math.max(0, sending.pct)).toFixed(1) + "%" : "") + (sending.kbps != null ? " \xB7 " + dkSpeed(sending.kbps) : "")), /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, margin: "0 auto", display: "flex", alignItems: "center", gap: phone ? 6 : 10 } }, /* @__PURE__ */ React.createElement(
1969
2000
  "input",
1970
2001
  {
1971
2002
  ref: fileRef,
@@ -1987,7 +2018,7 @@ ${peer.address}`
1987
2018
  sendDraft();
1988
2019
  }
1989
2020
  },
1990
- placeholder: `${T.message} ${peer.alias || shortKey(peer.userId, 6, 4)}\u2026`,
2021
+ placeholder: phone ? `${T.message}\u2026` : `${T.message} ${peer.alias || shortKey(peer.userId, 6, 4)}\u2026`,
1991
2022
  style: { flex: 1, height: 38, borderRadius: 9, border: "1px solid var(--line)", background: "var(--panel-2)", color: "var(--text)", fontFamily: "var(--ui)", fontSize: 13.5, padding: "0 14px", outline: "none", minWidth: 0 }
1992
2023
  }
1993
2024
  ), /* @__PURE__ */ React.createElement(Btn, { tone: "solid", icon: "arrowUp", title: T.send, onClick: sendDraft }))),
@@ -2079,8 +2110,11 @@ ${peer.address}`
2079
2110
  function ChatEmpty({ T }) {
2080
2111
  return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, color: "var(--faint)", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "message", size: 40, stroke: 1.4, color: "var(--line)" }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, T.pickPeer));
2081
2112
  }
2082
- function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, prefillAddr, onPrefillConsumed, muted, onToggleMute }) {
2113
+ function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
2083
2114
  const peer = peers.find((p) => p.id === activeId);
2115
+ if (phone) {
2116
+ return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide: true });
2117
+ }
2084
2118
  return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
2085
2119
  }
2086
2120
  Object.assign(window, { ChatTab });
@@ -2117,14 +2151,14 @@ ${peer.address}`
2117
2151
  ));
2118
2152
  }
2119
2153
  function FieldRow({ T, label, value, mono = true, copy, qr, onQr, last }) {
2120
- return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14, padding: "13px 16px", borderBottom: last ? "none" : "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, label), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: mono ? "var(--mono)" : "var(--ui)", fontSize: 13.5, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, value), copy && /* @__PURE__ */ React.createElement(CopyBtn, { value, copiedText: T.copied, copyFailedText: T.copyFailed, copyTitle: T.copy }), qr && /* @__PURE__ */ React.createElement(Btn, { icon: "qr", size: "sm", onClick: () => onQr && onQr(value, label) }));
2154
+ return /* @__PURE__ */ React.createElement("div", { className: "dk-row", style: { display: "flex", alignItems: "center", gap: 14, padding: "13px 16px", borderBottom: last ? "none" : "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("span", { className: "dk-lbl", style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, label), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: mono ? "var(--mono)" : "var(--ui)", fontSize: 13.5, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, value), copy && /* @__PURE__ */ React.createElement(CopyBtn, { value, copiedText: T.copied, copyFailedText: T.copyFailed, copyTitle: T.copy }), qr && /* @__PURE__ */ React.createElement(Btn, { icon: "qr", size: "sm", onClick: () => onQr && onQr(value, label) }));
2121
2155
  }
2122
2156
  function DkVersionRow({ T, me }) {
2123
2157
  const unsure = me.verFromBackend === false;
2124
2158
  const pill = (name, ver, mark) => /* @__PURE__ */ React.createElement("span", { key: name, style: { display: "inline-flex", alignItems: "baseline", gap: 5 } }, /* @__PURE__ */ React.createElement("span", { style: { color: "var(--faint)" } }, name), /* @__PURE__ */ React.createElement("span", { title: mark && unsure ? T.verInstalledHint : void 0 }, ver || "\u2014", mark && unsure && ver ? /* @__PURE__ */ React.createElement("span", { style: { color: "var(--faint)" } }, " ?") : null));
2125
2159
  const line = (label, items) => /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "baseline", gap: 10, flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("span", { style: { width: 52, flexShrink: 0, fontSize: 11, color: "var(--faint)" } }, label), /* @__PURE__ */ React.createElement("span", { style: { display: "flex", gap: 12, flexWrap: "wrap" } }, items));
2126
2160
  const embedded = me.backend === "embedded";
2127
- return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "flex-start", gap: 14, padding: "13px 16px", borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.version), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 6, fontFamily: "var(--mono)", fontSize: 13, color: "var(--text)" } }, line(T.verApp || "app", [
2161
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "flex-start", gap: 14, padding: "13px 16px", borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("span", { className: "dk-lbl", style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.version), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 6, fontFamily: "var(--mono)", fontSize: 13, color: "var(--text)" } }, line(T.verApp || "app", [
2128
2162
  pill("beagle", me.beagleVer),
2129
2163
  pill("peer-webrtc", me.webrtcVer),
2130
2164
  ...embedded ? [pill("peer", me.peerVer)] : []
@@ -2428,6 +2462,47 @@ ${peer.address}`
2428
2462
  recState[p.id] === "sent" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--online)" } }, T.pubRecSent || "sent \u2713") : recState[p.id] === "busy" ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, "\u2026") : recState[p.id] ? /* @__PURE__ */ React.createElement("span", { title: recState[p.id], style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--warn, #f59e0b)" } }, T.addFailed || "failed") : /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--accent)" } }, T.send || "Send")
2429
2463
  ))))));
2430
2464
  }
2465
+ function dkAvatarDataUrl(file) {
2466
+ return new Promise((resolve, reject) => {
2467
+ const img = new Image();
2468
+ const url = URL.createObjectURL(file);
2469
+ img.onload = () => {
2470
+ URL.revokeObjectURL(url);
2471
+ try {
2472
+ const S = 256;
2473
+ const c = document.createElement("canvas");
2474
+ c.width = S;
2475
+ c.height = S;
2476
+ const ctx = c.getContext("2d");
2477
+ const side = Math.min(img.width, img.height);
2478
+ ctx.drawImage(img, (img.width - side) / 2, (img.height - side) / 2, side, side, 0, 0, S, S);
2479
+ const enc = (q) => {
2480
+ let d = c.toDataURL("image/webp", q);
2481
+ if (!d.startsWith("data:image/webp"))
2482
+ d = c.toDataURL("image/jpeg", q);
2483
+ return d;
2484
+ };
2485
+ let dataUrl = null;
2486
+ for (const q of [0.9, 0.75, 0.55, 0.4]) {
2487
+ dataUrl = enc(q);
2488
+ if (dataUrl.length < 13e4)
2489
+ break;
2490
+ }
2491
+ if (!dataUrl || dataUrl.length >= 13e4)
2492
+ reject(new Error("too-big"));
2493
+ else
2494
+ resolve(dataUrl);
2495
+ } catch (err) {
2496
+ reject(err);
2497
+ }
2498
+ };
2499
+ img.onerror = () => {
2500
+ URL.revokeObjectURL(url);
2501
+ reject(new Error("bad-image"));
2502
+ };
2503
+ img.src = url;
2504
+ });
2505
+ }
2431
2506
  function DkEnsCard({ T, me, onRecord }) {
2432
2507
  const [st, setSt] = React.useState({ loading: true });
2433
2508
  const [label, setLabel] = React.useState("");
@@ -2510,7 +2585,7 @@ ${peer.address}`
2510
2585
  var _a;
2511
2586
  const saved = (st.record && st.record.texts || {})["com." + k] || "";
2512
2587
  const cur = (_a = draft[k]) != null ? _a : "";
2513
- return /* @__PURE__ */ React.createElement("div", { key: k, style: { ...row, borderBottom: last ? "none" : row.borderBottom } }, /* @__PURE__ */ React.createElement("span", { style: rowLbl }, k), /* @__PURE__ */ React.createElement(
2588
+ return /* @__PURE__ */ React.createElement("div", { key: k, className: "dk-row", style: { ...row, borderBottom: last ? "none" : row.borderBottom } }, /* @__PURE__ */ React.createElement("span", { className: "dk-lbl", style: rowLbl }, k), /* @__PURE__ */ React.createElement(
2514
2589
  "input",
2515
2590
  {
2516
2591
  value: cur,
@@ -2553,51 +2628,14 @@ ${peer.address}`
2553
2628
  return;
2554
2629
  setBusy(true);
2555
2630
  setMsg(null);
2556
- const img = new Image();
2557
- const url = URL.createObjectURL(f);
2558
- const fail = (text) => {
2631
+ dkAvatarDataUrl(f).then((dataUrl) => post("/api/ens-avatar-upload", { dataUrl })).catch((err) => {
2559
2632
  setBusy(false);
2560
- setMsg({ tone: "err", text });
2561
- };
2562
- img.onload = () => {
2563
- URL.revokeObjectURL(url);
2564
- try {
2565
- const S = 256;
2566
- const c = document.createElement("canvas");
2567
- c.width = S;
2568
- c.height = S;
2569
- const ctx = c.getContext("2d");
2570
- const side = Math.min(img.width, img.height);
2571
- ctx.drawImage(img, (img.width - side) / 2, (img.height - side) / 2, side, side, 0, 0, S, S);
2572
- const enc = (q) => {
2573
- let d = c.toDataURL("image/webp", q);
2574
- if (!d.startsWith("data:image/webp"))
2575
- d = c.toDataURL("image/jpeg", q);
2576
- return d;
2577
- };
2578
- let dataUrl = null;
2579
- for (const q of [0.9, 0.75, 0.55, 0.4]) {
2580
- dataUrl = enc(q);
2581
- if (dataUrl.length < 13e4)
2582
- break;
2583
- }
2584
- if (!dataUrl || dataUrl.length >= 13e4) {
2585
- fail(T.ensTooBig || "image too large even after scaling");
2586
- return;
2587
- }
2588
- post("/api/ens-avatar-upload", { dataUrl });
2589
- } catch (err) {
2590
- fail(String(err && err.message || err));
2591
- }
2592
- };
2593
- img.onerror = () => {
2594
- URL.revokeObjectURL(url);
2595
- fail(T.ensBadImage || "cannot read that image");
2596
- };
2597
- img.src = url;
2633
+ const m = err && err.message;
2634
+ setMsg({ tone: "err", text: m === "too-big" ? T.ensTooBig || "image too large even after scaling" : m === "bad-image" ? T.ensBadImage || "cannot read that image" : String(m || err) });
2635
+ });
2598
2636
  };
2599
- const walletRow = (lbl, chain, bound, onBind, last) => /* @__PURE__ */ React.createElement("div", { style: { ...row, borderBottom: last ? "none" : row.borderBottom } }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, lbl), bound ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, bound), /* @__PURE__ */ React.createElement(CopyBtn, { value: bound, copiedText: T.copied, copyFailedText: T.copyFailed, copyTitle: T.copy }), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => bind(chain, "") }, T.ensUnbind || "unbind")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.ensNotBound || "not bound"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: onBind }, T.ensBind || "bind")));
2600
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Card, { label: T.ensCard || "Name \xB7 beagles.eth" }, st.loading ? /* @__PURE__ */ React.createElement("div", { style: { padding: "14px 16px", fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.ensName || "name"), /* @__PURE__ */ React.createElement(
2637
+ const walletRow = (lbl, chain, bound, onBind, last) => /* @__PURE__ */ React.createElement("div", { className: "dk-row", style: { ...row, borderBottom: last ? "none" : row.borderBottom } }, /* @__PURE__ */ React.createElement("span", { className: "dk-lbl", style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, lbl), bound ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, bound), /* @__PURE__ */ React.createElement(CopyBtn, { value: bound, copiedText: T.copied, copyFailedText: T.copyFailed, copyTitle: T.copy }), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => bind(chain, "") }, T.ensUnbind || "unbind")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.ensNotBound || "not bound"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: onBind }, T.ensBind || "bind")));
2638
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Card, { label: T.ensCard || "Name \xB7 beagles.eth" }, st.loading ? /* @__PURE__ */ React.createElement("div", { style: { padding: "14px 16px", fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "dk-row dk-wrap", style: row }, /* @__PURE__ */ React.createElement("span", { className: "dk-lbl", style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.ensName || "name"), /* @__PURE__ */ React.createElement(
2601
2639
  "input",
2602
2640
  {
2603
2641
  value: label,
@@ -2608,9 +2646,9 @@ ${peer.address}`
2608
2646
  if (e.key === "Enter")
2609
2647
  register();
2610
2648
  },
2611
- style: field
2649
+ style: { ...field, minWidth: 110 }
2612
2650
  }
2613
- ), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--dim)", flexShrink: 0 } }, ".beagles.eth"), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", disabled: busy || !label.trim(), onClick: register }, busy ? "\u2026" : st.registered && st.mineOwned ? T.ensUpdate || "update" : T.ensRegister || "register")), st.registered && !st.mineOwned && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", borderBottom: "1px solid var(--line)", fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, (T.ensWalletOwned || "registered via your mobile wallet as") + " ", /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)" } }, rec && rec.name)), st.registered && /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.ensAvatar || "avatar"), upAvatar ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("img", { src: upAvatar, alt: "", width: 34, height: 34, style: { width: 34, height: 34, borderRadius: 9, objectFit: "cover", flexShrink: 0, background: "var(--panel-2)" } }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--text)" } }, T.ensCustom || "custom image"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => post("/api/ens-avatar", { nftid: null }) }, T.ensClear || "clear")) : punkId != null ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: punkId, size: 34, radius: 9, fallbackSeed: me.userId }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, `CryptoPunk #${punkId}`), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => post("/api/ens-avatar", { nftid: null }) }, T.ensClear || "clear")) : /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.ensNotSet || "not set"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: () => fileRef.current && fileRef.current.click() }, T.ensUpload || "upload"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: () => setPicking(true) }, T.ensChoose || "choose"), /* @__PURE__ */ React.createElement("input", { ref: fileRef, type: "file", accept: "image/*", onChange: onUploadFile, style: { display: "none" } })), walletRow(T.ensEth || "ethereum", "eth", boundEth, bindEth, false), walletRow(T.ensSol || "solana", "sol", boundSol, bindSol, !msg), msg && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", fontFamily: "var(--ui)", fontSize: 12, color: msg.tone === "ok" ? "var(--online)" : "var(--danger)" } }, msg.text)), picking && /* @__PURE__ */ React.createElement(DkPunkPicker, { T, onPick: pickPunk, onClose: () => setPicking(false) })), !st.loading && st.registered && /* @__PURE__ */ React.createElement(Card, { label: T.linksCard || "Links" }, SOCIALS.map((s, i) => socialRow(s, i === SOCIALS.length - 1))));
2651
+ ), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--dim)", flexShrink: 0 } }, ".beagles.eth"), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", disabled: busy || !label.trim(), onClick: register }, busy ? "\u2026" : st.registered && st.mineOwned ? T.ensUpdate || "update" : T.ensRegister || "register")), st.registered && !st.mineOwned && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", borderBottom: "1px solid var(--line)", fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)" } }, (T.ensWalletOwned || "registered via your mobile wallet as") + " ", /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)" } }, rec && rec.name)), st.registered && /* @__PURE__ */ React.createElement("div", { className: "dk-row dk-wrap", style: row }, /* @__PURE__ */ React.createElement("span", { className: "dk-lbl", style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.ensAvatar || "avatar"), upAvatar ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("img", { src: upAvatar, alt: "", width: 34, height: 34, style: { width: 34, height: 34, borderRadius: 9, objectFit: "cover", flexShrink: 0, background: "var(--panel-2)" } }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--text)" } }, T.ensCustom || "custom image"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => post("/api/ens-avatar", { nftid: null }) }, T.ensClear || "clear")) : punkId != null ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: punkId, size: 34, radius: 9, fallbackSeed: me.userId }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, `CryptoPunk #${punkId}`), /* @__PURE__ */ React.createElement(Btn, { size: "sm", tone: "danger", disabled: busy || !st.mineOwned, onClick: () => post("/api/ens-avatar", { nftid: null }) }, T.ensClear || "clear")) : /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.ensNotSet || "not set"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: () => fileRef.current && fileRef.current.click() }, T.ensUpload || "upload"), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: busy || !st.mineOwned, onClick: () => setPicking(true) }, T.ensChoose || "choose"), /* @__PURE__ */ React.createElement("input", { ref: fileRef, type: "file", accept: "image/*", onChange: onUploadFile, style: { display: "none" } })), walletRow(T.ensEth || "ethereum", "eth", boundEth, bindEth, false), walletRow(T.ensSol || "solana", "sol", boundSol, bindSol, !msg), msg && /* @__PURE__ */ React.createElement("div", { style: { padding: "10px 16px", fontFamily: "var(--ui)", fontSize: 12, color: msg.tone === "ok" ? "var(--online)" : "var(--danger)" } }, msg.text)), picking && /* @__PURE__ */ React.createElement(DkPunkPicker, { T, onPick: pickPunk, onClose: () => setPicking(false) })), !st.loading && st.registered && /* @__PURE__ */ React.createElement(Card, { label: T.linksCard || "Links" }, SOCIALS.map((s, i) => socialRow(s, i === SOCIALS.length - 1))));
2614
2652
  }
2615
2653
  function DkUpdateCheck({ T }) {
2616
2654
  const [st, setSt] = React.useState("idle");
@@ -2626,7 +2664,7 @@ ${peer.address}`
2626
2664
  setSt("latest");
2627
2665
  }).catch(() => setSt("idle"));
2628
2666
  };
2629
- return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14, padding: "13px 16px" } }, /* @__PURE__ */ React.createElement("span", { style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.updCheckLabel || "updates"), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: st === "latest" ? "var(--online)" : "var(--faint)" } }, st === "latest" ? T.updLatest || "up to date \u2713" : ""), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: st === "busy", onClick: check }, st === "busy" ? "\u2026" : T.updCheck || "check for updates"), info && /* @__PURE__ */ React.createElement(DkUpdateModal, { T, info, onClose: () => setInfo(null) }));
2667
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14, padding: "13px 16px" } }, /* @__PURE__ */ React.createElement("span", { className: "dk-lbl", style: { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 } }, T.updCheckLabel || "updates"), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--ui)", fontSize: 12, color: st === "latest" ? "var(--online)" : "var(--faint)" } }, st === "latest" ? T.updLatest || "up to date \u2713" : ""), /* @__PURE__ */ React.createElement(Btn, { size: "sm", disabled: st === "busy", onClick: check }, st === "busy" ? "\u2026" : T.updCheck || "check for updates"), info && /* @__PURE__ */ React.createElement(DkUpdateModal, { T, info, onClose: () => setInfo(null) }));
2630
2668
  }
2631
2669
  function DkKeyBackup({ t }) {
2632
2670
  const zh = t && t.lang === "zh";
@@ -2739,7 +2777,7 @@ ${peer.address}`
2739
2777
  },
2740
2778
  label
2741
2779
  )));
2742
- return /* @__PURE__ */ React.createElement(Card, { label: zh ? "\u663E\u793A" : "Display" }, /* @__PURE__ */ React.createElement("div", { style: { ...row, borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("span", { style: name }, zh ? "\u8BED\u8A00" : "Language"), seg("lang", t.lang, (v) => setTweak("lang", v), [["en", "EN"], ["zh", "\u4E2D\u6587"]])), /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: name }, zh ? "\u4E3B\u9898" : "Theme"), seg("theme", t.theme, (v) => setTweak("theme", v), [["dark", zh ? "\u6DF1\u8272" : "Dark"], ["light", zh ? "\u6D45\u8272" : "Light"]])));
2780
+ return /* @__PURE__ */ React.createElement(Card, { label: zh ? "\u663E\u793A" : "Display" }, /* @__PURE__ */ React.createElement("div", { style: { ...row, borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("span", { style: name }, zh ? "\u8BED\u8A00" : "Language"), seg("lang", t.lang, (v) => setTweak("lang", v), [["en", "EN"], ["zh", "\u4E2D\u6587"]])), /* @__PURE__ */ React.createElement("div", { className: "dk-row dk-wrap", style: row }, /* @__PURE__ */ React.createElement("span", { style: name }, zh ? "\u4E3B\u9898" : "Theme"), seg("theme", t.theme, (v) => setTweak("theme", v), [["dark", zh ? "\u6DF1\u8272" : "Dark"], ["light", zh ? "\u6D45\u8272" : "Light"]])));
2743
2781
  }
2744
2782
  function DkAlertSettings({ t, setTweak }) {
2745
2783
  const zh = t.lang === "zh";
@@ -2770,8 +2808,67 @@ ${peer.address}`
2770
2808
  true
2771
2809
  ));
2772
2810
  }
2773
- function ProfileTab({ T, me, onEdit, t, setTweak }) {
2811
+ function DkAvatarSheet({ T, me, zh, onSet, onClose }) {
2812
+ const [picking, setPicking] = React.useState(false);
2813
+ const [busy, setBusy] = React.useState(false);
2814
+ const [err, setErr] = React.useState(null);
2815
+ const fileRef = React.useRef(null);
2816
+ const canUpload = Object.prototype.hasOwnProperty.call(me, "avatarDataUrl");
2817
+ const save = (p) => {
2818
+ setBusy(true);
2819
+ setErr(null);
2820
+ return Promise.resolve(onSet(p)).then((r) => {
2821
+ setBusy(false);
2822
+ if (r && r.ok === false)
2823
+ setErr(r.error || "failed");
2824
+ else
2825
+ onClose();
2826
+ }).catch((e) => {
2827
+ setBusy(false);
2828
+ setErr(String(e && e.message || e));
2829
+ });
2830
+ };
2831
+ const pickPunk = (id) => {
2832
+ setPicking(false);
2833
+ save(canUpload ? { punkId: id, avatarDataUrl: null } : { punkId: id });
2834
+ };
2835
+ const onFile = (e) => {
2836
+ const f = e.target.files && e.target.files[0];
2837
+ e.target.value = "";
2838
+ if (!f)
2839
+ return;
2840
+ setBusy(true);
2841
+ setErr(null);
2842
+ dkAvatarDataUrl(f).then((dataUrl) => save({ avatarDataUrl: dataUrl })).catch((x) => {
2843
+ setBusy(false);
2844
+ const m = x && x.message;
2845
+ setErr(m === "too-big" ? T.ensTooBig || "image too large even after scaling" : m === "bad-image" ? T.ensBadImage || "cannot read that image" : String(m || x));
2846
+ });
2847
+ };
2848
+ const has = me.punkId != null || !!(canUpload && me.avatarDataUrl);
2849
+ const item = (icon, label, onClick, danger) => /* @__PURE__ */ React.createElement("button", { onClick, disabled: busy, style: {
2850
+ display: "flex",
2851
+ alignItems: "center",
2852
+ gap: 12,
2853
+ width: "100%",
2854
+ padding: "13px 14px",
2855
+ border: "none",
2856
+ borderRadius: 10,
2857
+ background: "var(--panel-2)",
2858
+ cursor: busy ? "default" : "pointer",
2859
+ opacity: busy ? 0.6 : 1,
2860
+ fontFamily: "var(--ui)",
2861
+ fontSize: 14,
2862
+ color: danger ? "var(--danger)" : "var(--text)",
2863
+ textAlign: "left"
2864
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 18, stroke: 2, color: danger ? "var(--danger)" : "var(--accent)" }), label);
2865
+ if (picking)
2866
+ return /* @__PURE__ */ React.createElement(DkPunkPicker, { T, onPick: pickPunk, onClose: () => setPicking(false) });
2867
+ return /* @__PURE__ */ React.createElement("div", { onClick: onClose, style: { position: "fixed", inset: 0, zIndex: 90, background: "color-mix(in oklab, #000, transparent 38%)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { width: 360, maxWidth: "100%", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: 16, display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 4 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 40, radius: 10, dot: false }), /* @__PURE__ */ React.createElement("span", { style: { flex: 1, fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 700, color: "var(--text)" } }, zh ? "\u66F4\u6362\u5934\u50CF" : "Change avatar"), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), item("sparkles", zh ? "\u9009\u4E00\u4E2A CryptoPunk" : "Choose a CryptoPunk", () => setPicking(true)), canUpload && item("image", zh ? "\u4E0A\u4F20\u7167\u7247" : "Upload a photo", () => fileRef.current && fileRef.current.click()), has && item("trash", zh ? "\u79FB\u9664\u5934\u50CF" : "Remove avatar", () => save(canUpload ? { punkId: null, avatarDataUrl: null } : { punkId: null }), true), /* @__PURE__ */ React.createElement("input", { ref: fileRef, type: "file", accept: "image/*", onChange: onFile, style: { display: "none" } }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--faint)", lineHeight: 1.5, marginTop: 2 } }, zh ? "\u4E0D\u9700\u8981\u6CE8\u518C beagles.eth \u540D\u5B57\u3002\u597D\u53CB\u4F1A\u5728\u4E0B\u6B21\u6536\u5230\u4F60\u7684\u8D44\u6599\u65F6\u770B\u5230\u65B0\u5934\u50CF\u3002" : "No beagles.eth name needed. Friends see it the next time your profile reaches them."), err && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--danger)" } }, err)));
2868
+ }
2869
+ function ProfileTab({ T, me, onEdit, t, setTweak, onSetAvatar }) {
2774
2870
  var _a;
2871
+ const [avatarSheet, setAvatarSheet] = React.useState(false);
2775
2872
  const [qr, setQr] = React.useState(null);
2776
2873
  const [editing, setEditing] = React.useState(false);
2777
2874
  const [ensRec, setEnsRec] = React.useState(null);
@@ -2779,7 +2876,17 @@ ${peer.address}`
2779
2876
  const pubAvatar = ensRec && ensRec.texts && ensRec.texts.avatar || null;
2780
2877
  const punkId = (_a = me.punkId) != null ? _a : pubPunk;
2781
2878
  const upAvatar = me.avatarUrl || pubAvatar;
2782
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 24 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 18, padding: "20px 22px", borderRadius: 14, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, upAvatar ? /* @__PURE__ */ React.createElement("img", { src: upAvatar, alt: "", width: 68, height: 68, style: { width: 68, height: 68, borderRadius: 16, objectFit: "cover", background: "var(--panel-2)" } }) : punkId != null ? /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: punkId, size: 68, radius: 16, fallbackSeed: me.userId }) : /* @__PURE__ */ React.createElement(DkIdenticon, { seed: me.userId, size: 68, radius: 16 }), /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -3, bottom: -3, width: 18, height: 18, borderRadius: 999, background: "var(--online)", border: "3px solid var(--panel)" } })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, letterSpacing: -0.5, color: "var(--text)" } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: "ok" }, "online"), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13, color: "var(--dim)", marginTop: 4 } }, me.handle), me.description ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 3 } }, me.description) : null), /* @__PURE__ */ React.createElement(Btn, { icon: "edit", onClick: () => setEditing(true) }, T.editProfile)), /* @__PURE__ */ React.createElement(Card, { label: T.identity }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.userId, value: me.userId, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.carrierAddr, value: me.carrier, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.netKey, value: me.netKey, copy: true, last: true })), /* @__PURE__ */ React.createElement(DkEnsCard, { T, me, onRecord: setEnsRec }), typeof me.autoAccept === "boolean" && /* @__PURE__ */ React.createElement(Card, { label: T && T.friendsSettings || "Friends" }, /* @__PURE__ */ React.createElement("label", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px", cursor: "pointer" } }, /* @__PURE__ */ React.createElement(
2879
+ const canSetAvatar = !!onSetAvatar && !ensRec;
2880
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { className: "dk-page", style: { maxWidth: 760, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 24 } }, /* @__PURE__ */ React.createElement("div", { className: "dk-wrap", style: { display: "flex", alignItems: "center", gap: 18, rowGap: 12, padding: "20px 22px", borderRadius: 14, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement(
2881
+ "div",
2882
+ {
2883
+ style: { position: "relative", cursor: canSetAvatar ? "pointer" : "default" },
2884
+ onClick: canSetAvatar ? () => setAvatarSheet(true) : void 0,
2885
+ title: canSetAvatar ? t && t.lang === "zh" ? "\u66F4\u6362\u5934\u50CF" : "Change avatar" : void 0
2886
+ },
2887
+ upAvatar ? /* @__PURE__ */ React.createElement("img", { src: upAvatar, alt: "", width: 68, height: 68, style: { width: 68, height: 68, borderRadius: 16, objectFit: "cover", background: "var(--panel-2)" } }) : punkId != null ? /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: punkId, size: 68, radius: 16, fallbackSeed: me.userId }) : /* @__PURE__ */ React.createElement(DkIdenticon, { seed: me.userId, size: 68, radius: 16 }),
2888
+ canSetAvatar ? /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -5, bottom: -5, width: 24, height: 24, borderRadius: 999, background: "var(--accent)", border: "3px solid var(--panel)", display: "flex", alignItems: "center", justifyContent: "center", boxSizing: "border-box" } }, /* @__PURE__ */ React.createElement(Icon, { name: "camera", size: 12, stroke: 2.2, color: "#fff" })) : /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -3, bottom: -3, width: 18, height: 18, borderRadius: 999, background: "var(--online)", border: "3px solid var(--panel)" } })
2889
+ ), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { className: "dk-wrap", style: { display: "flex", alignItems: "center", gap: 10, rowGap: 4 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, letterSpacing: -0.5, color: "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0, maxWidth: "100%" } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: "ok" }, "online"), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13, color: "var(--dim)", marginTop: 4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, me.handle), me.description ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", marginTop: 3 } }, me.description) : null), /* @__PURE__ */ React.createElement(Btn, { icon: "edit", onClick: () => setEditing(true) }, T.editProfile)), /* @__PURE__ */ React.createElement(Card, { label: T.identity }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.userId, value: me.userId, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.carrierAddr, value: me.carrier, copy: true, qr: true, onQr: (v, l) => setQr({ value: v, label: l }) }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.netKey, value: me.netKey, copy: true, last: true })), /* @__PURE__ */ React.createElement(DkEnsCard, { T, me, onRecord: setEnsRec }), typeof me.autoAccept === "boolean" && /* @__PURE__ */ React.createElement(Card, { label: T && T.friendsSettings || "Friends" }, /* @__PURE__ */ React.createElement("label", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px", cursor: "pointer" } }, /* @__PURE__ */ React.createElement(
2783
2890
  "input",
2784
2891
  {
2785
2892
  type: "checkbox",
@@ -2791,13 +2898,50 @@ ${peer.address}`
2791
2898
  },
2792
2899
  style: { width: 16, height: 16, accentColor: "var(--accent)" }
2793
2900
  }
2794
- ), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, T && T.autoAcceptLabel || "Auto-accept friend requests"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", marginTop: 2 } }, T && T.autoAcceptSub || "Off: requests wait in the sidebar for your approval.")))), hostHasKeyBackup && /* @__PURE__ */ React.createElement(DkKeyBackup, { t: t || {} }), t && setTweak && /* @__PURE__ */ React.createElement(DkAlertSettings, { t, setTweak }), t && setTweak && /* @__PURE__ */ React.createElement(DkDisplaySettings, { t, setTweak }), /* @__PURE__ */ React.createElement(Card, { label: T.network }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.virtualIp, value: me.ip, copy: true }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.wireLabel, value: `${me.wire} \xB7 lossless`, mono: false }), /* @__PURE__ */ React.createElement(DkVersionRow, { T, me }), /* @__PURE__ */ React.createElement(DkUpdateCheck, { T })), /* @__PURE__ */ React.createElement(Card, { label: T.dangerZone }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 8, flexShrink: 0, background: "color-mix(in oklab, var(--danger), transparent 86%)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--danger)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 17, stroke: 2 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--danger)" } }, T.deleteNode), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)", marginTop: 1 } }, T.deleteSub)), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", size: "sm" }, T.delete)))), qr && /* @__PURE__ */ React.createElement(DkQrModal, { T, value: qr.value, label: qr.label, onClose: () => setQr(null) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
2901
+ ), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, T && T.autoAcceptLabel || "Auto-accept friend requests"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", marginTop: 2 } }, T && T.autoAcceptSub || "Off: requests wait in the sidebar for your approval.")))), hostHasKeyBackup && /* @__PURE__ */ React.createElement(DkKeyBackup, { t: t || {} }), t && setTweak && /* @__PURE__ */ React.createElement(DkAlertSettings, { t, setTweak }), t && setTweak && /* @__PURE__ */ React.createElement(DkDisplaySettings, { t, setTweak }), /* @__PURE__ */ React.createElement(Card, { label: T.network }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.virtualIp, value: me.ip, copy: true }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.wireLabel, value: `${me.wire} \xB7 lossless`, mono: false }), /* @__PURE__ */ React.createElement(DkVersionRow, { T, me }), /* @__PURE__ */ React.createElement(DkUpdateCheck, { T })), /* @__PURE__ */ React.createElement(Card, { label: T.dangerZone }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 8, flexShrink: 0, background: "color-mix(in oklab, var(--danger), transparent 86%)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--danger)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 17, stroke: 2 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--danger)" } }, T.deleteNode), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)", marginTop: 1 } }, T.deleteSub)), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", size: "sm" }, T.delete)))), qr && /* @__PURE__ */ React.createElement(DkQrModal, { T, value: qr.value, label: qr.label, onClose: () => setQr(null) }), avatarSheet && /* @__PURE__ */ React.createElement(DkAvatarSheet, { T, me, zh: t && t.lang === "zh", onSet: onSetAvatar, onClose: () => setAvatarSheet(false) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
2795
2902
  if (onEdit)
2796
2903
  onEdit(name, description);
2797
2904
  setEditing(false);
2798
2905
  } }));
2799
2906
  }
2800
2907
  Object.assign(window, { ProfileTab, DkPunkAvatar, DkEnsAvatar, DkPublicProfile });
2908
+ var DK_PHONE_MQ = "(max-width: 700px)";
2909
+ function useIsPhone() {
2910
+ const mq = React.useMemo(() => typeof window !== "undefined" && window.matchMedia ? window.matchMedia(DK_PHONE_MQ) : null, []);
2911
+ const [phone, setPhone] = React.useState(() => !!(mq && mq.matches));
2912
+ React.useEffect(() => {
2913
+ if (!mq)
2914
+ return void 0;
2915
+ const on = () => setPhone(mq.matches);
2916
+ on();
2917
+ if (mq.addEventListener)
2918
+ mq.addEventListener("change", on);
2919
+ else
2920
+ mq.addListener(on);
2921
+ return () => {
2922
+ if (mq.removeEventListener)
2923
+ mq.removeEventListener("change", on);
2924
+ else
2925
+ mq.removeListener(on);
2926
+ };
2927
+ }, [mq]);
2928
+ return phone;
2929
+ }
2930
+ var DK_PHONE_CSS = `
2931
+ @media ${DK_PHONE_MQ} {
2932
+ .dk-page { padding: 14px 12px 40px !important; gap: 18px !important; }
2933
+ .dk-lbl { width: 78px !important; font-size: 10px !important; letter-spacing: 0.3px !important; }
2934
+ .dk-row { padding: 11px 12px !important; gap: 9px !important; }
2935
+ .dk-wrap { flex-wrap: wrap !important; }
2936
+ .dk-grid2 { grid-template-columns: 1fr 1fr !important; }
2937
+ .dk-grid1 { grid-template-columns: 1fr !important; }
2938
+ .dk-no-phone { display: none !important; }
2939
+ /* iOS zooms the page into any focused field under 16px and never zooms back. */
2940
+ input:not([type=checkbox]):not([type=radio]), textarea, select { font-size: 16px !important; }
2941
+ }`;
2942
+ function DkPhoneStyles() {
2943
+ return /* @__PURE__ */ React.createElement("style", null, DK_PHONE_CSS);
2944
+ }
2801
2945
  function DkIdenticon({ seed, size = 30, radius = 7 }) {
2802
2946
  const { cells, hue } = dkIdenticon(seed);
2803
2947
  const cell = size / 5;
@@ -2971,7 +3115,7 @@ ${peer.address}`
2971
3115
  function Section({ label, count, trailing, style }) {
2972
3116
  return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "0 2px", ...style } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform: "uppercase", color: "var(--faint)" } }, label), count != null && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, color: "var(--faint)" } }, count), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, height: 1, background: "var(--line)" } }), trailing);
2973
3117
  }
2974
- Object.assign(window, { DkIdenticon, DkBot, DkAvatar, DkImgAvatar, StatusDot, Mono, shortKey, RouteTag, Tag, Unread, Btn, Section });
3118
+ Object.assign(window, { useIsPhone, DkPhoneStyles, DkIdenticon, DkBot, DkAvatar, DkImgAvatar, StatusDot, Mono, shortKey, RouteTag, Tag, Unread, Btn, Section });
2975
3119
  var APPS = [
2976
3120
  {
2977
3121
  id: "meet",
@@ -3813,9 +3957,9 @@ ${peer.address}`
3813
3957
  value: q,
3814
3958
  onChange: (e) => setQ(e.target.value),
3815
3959
  placeholder: T.dirSearch || "search name / userid",
3816
- style: { flex: 1, background: "transparent", border: "none", outline: "none", color: "var(--text)", fontFamily: "var(--ui)", fontSize: 13 }
3960
+ style: { flex: 1, minWidth: 0, background: "transparent", border: "none", outline: "none", color: "var(--text)", fontFamily: "var(--ui)", fontSize: 13 }
3817
3961
  }
3818
- ), kind === "bridge" && (meta == null ? void 0 : meta.bridge) && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, String(meta.bridge).replace(/^https?:\/\//, ""), meta.online ? ` \xB7 ${meta.online} online` : ""), list && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, shown.length, "/", list.length)), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minHeight: 0, overflowY: "auto" } }, err ? /* @__PURE__ */ React.createElement("div", { style: { padding: 22, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, (T.dirError || "Could not load the directory:") + " " + err) : !list ? /* @__PURE__ */ React.createElement("div", { style: { padding: 22, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : shown.length === 0 ? /* @__PURE__ */ React.createElement("div", { style: { padding: 22, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, kind === "bridge" && (meta == null ? void 0 : meta.pending) ? T.bridgePending || "Not verified on this bridge yet \u2014 it may still be running the older protocol." : kind === "bridge" ? T.bridgeEmpty || "Nobody else is listed on this bridge yet." : T.dirEmpty || "nobody here") : shown.map((p) => /* @__PURE__ */ React.createElement(
3962
+ ), kind === "bridge" && (meta == null ? void 0 : meta.bridge) && /* @__PURE__ */ React.createElement("span", { className: "dk-no-phone", style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)", whiteSpace: "nowrap" } }, String(meta.bridge).replace(/^https?:\/\//, ""), meta.online ? ` \xB7 ${meta.online} online` : ""), list && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)", whiteSpace: "nowrap", flexShrink: 0 } }, shown.length, "/", list.length)), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minHeight: 0, overflowY: "auto" } }, err ? /* @__PURE__ */ React.createElement("div", { style: { padding: 22, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, (T.dirError || "Could not load the directory:") + " " + err) : !list ? /* @__PURE__ */ React.createElement("div", { style: { padding: 22, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : shown.length === 0 ? /* @__PURE__ */ React.createElement("div", { style: { padding: 22, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, kind === "bridge" && (meta == null ? void 0 : meta.pending) ? T.bridgePending || "Not verified on this bridge yet \u2014 it may still be running the older protocol." : kind === "bridge" ? T.bridgeEmpty || "Nobody else is listed on this bridge yet." : T.dirEmpty || "nobody here") : shown.map((p) => /* @__PURE__ */ React.createElement(
3819
3963
  DkDirRow,
3820
3964
  {
3821
3965
  key: p.userid,
@@ -4204,14 +4348,20 @@ ${peer.address}`
4204
4348
  }
4205
4349
  Object.assign(window, { useRtcFileController, RtcFileInbox });
4206
4350
  function StatTile({ label, value, sub, tone }) {
4207
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, padding: "13px 15px", borderRadius: 10, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 700, letterSpacing: 1, textTransform: "uppercase", color: "var(--faint)" } }, label), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 21, fontWeight: 700, letterSpacing: -0.5, marginTop: 5, color: tone || "var(--text)" } }, value), sub && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--dim)", marginTop: 2 } }, sub));
4351
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, padding: "13px 15px", borderRadius: 10, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 700, letterSpacing: 1, textTransform: "uppercase", color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, label), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 21, fontWeight: 700, letterSpacing: -0.5, marginTop: 5, color: tone || "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, value), sub && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--dim)", marginTop: 2 } }, sub));
4208
4352
  }
4209
4353
  function MyNode({ T, me, activeExit, peers, reqCount = 0 }) {
4210
4354
  const online = peers.filter((p) => p.online).length;
4211
4355
  const direct = peers.filter((p) => p.online && p.via === "direct").length;
4212
- return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14, padding: "16px 18px", borderRadius: 12, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: me.userId, size: 46, radius: 11 }), /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", right: -3, bottom: -3, width: 14, height: 14, borderRadius: 999, background: "var(--online)", border: "2.5px solid var(--panel)" } })), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 9 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 17, fontWeight: 700, color: "var(--text)" } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: "ok" }, "online"), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 5 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, T.myIp), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: me.ip }, me.ip), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, "wire ", me.wire), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, "lan ", me.lanVer, " / peer ", me.peerVer))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { icon: "copy", onClick: () => dkCopy(me.carrier) }, T.copyAddr)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 12 } }, /* @__PURE__ */ React.createElement(StatTile, { label: T.peersOnline, value: `${online}/${peers.length}`, sub: `${direct} ${T.direct}`, tone: "var(--online)" }), /* @__PURE__ */ React.createElement(StatTile, { label: T.activeEgress, value: activeExit ? activeExit : T.directEgress, sub: activeExit ? "china \xB7 cn-sh-01" : T.noProxy, tone: activeExit ? "var(--warn)" : "var(--text)" }), /* @__PURE__ */ React.createElement(StatTile, { label: T.wireLabel, value: me.wire, sub: `${me.channel} \xB7 ${T.lossless}` }), /* @__PURE__ */ React.createElement(StatTile, { label: T.reqs, value: String(reqCount), sub: T.pending, tone: reqCount ? "var(--accent)" : "var(--text)" })));
4356
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement("div", { className: "dk-wrap", style: { display: "flex", alignItems: "center", gap: 14, padding: "16px 18px", borderRadius: 12, background: "var(--panel)", border: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 46, radius: 11 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: 1 } }, /* @__PURE__ */ React.createElement("div", { className: "dk-wrap", style: { display: "flex", alignItems: "center", gap: 9 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 17, fontWeight: 700, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 } }, me.name), /* @__PURE__ */ React.createElement(Tag, { tone: me.online ? "ok" : "off" }, me.online ? "online" : "offline"), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel), me.isExit && /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, "exit", me.exitRegion ? ` \xB7 ${me.exitRegion.toUpperCase()}` : "")), /* @__PURE__ */ React.createElement("div", { className: "dk-wrap", style: { display: "flex", alignItems: "center", gap: 8, marginTop: 5, rowGap: 2 } }, me.ip ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, T.myIp), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: me.ip }, me.ip), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7")) : null, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, "wire ", me.wire), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, "lan ", me.lanVer, " / peer ", me.peerVer))), /* @__PURE__ */ React.createElement(Btn, { icon: "copy", onClick: () => dkCopy(me.carrier) }, T.copyAddr)), /* @__PURE__ */ React.createElement("div", { className: "dk-grid2", style: { display: "grid", gridTemplateColumns: "repeat(4, minmax(0, 1fr))", gap: 12 } }, /* @__PURE__ */ React.createElement(StatTile, { label: T.peersOnline, value: `${online}/${peers.length}`, sub: `${direct} ${T.direct}`, tone: "var(--online)" }), /* @__PURE__ */ React.createElement(StatTile, { label: T.activeEgress, value: activeExit ? activeExit : T.directEgress, sub: activeExit ? T.routing : T.noProxy, tone: activeExit ? "var(--warn)" : "var(--text)" }), /* @__PURE__ */ React.createElement(StatTile, { label: T.wireLabel, value: me.wire, sub: `${me.channel} \xB7 ${T.lossless}` }), /* @__PURE__ */ React.createElement(StatTile, { label: T.reqs, value: String(reqCount), sub: T.pending, tone: reqCount ? "var(--accent)" : "var(--text)" })));
4213
4357
  }
4214
4358
  function PeerTable({ T, peers, onOpenChat }) {
4359
+ const phone = useIsPhone();
4360
+ if (!peers.length)
4361
+ return null;
4362
+ if (phone) {
4363
+ return /* @__PURE__ */ React.createElement("div", { style: { borderRadius: 11, border: "1px solid var(--line)", overflow: "hidden", background: "var(--panel)" } }, peers.map((p, i) => /* @__PURE__ */ React.createElement("div", { key: p.id, onClick: () => onOpenChat(p.id), style: { display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", cursor: "pointer", borderBottom: i < peers.length - 1 ? "1px solid var(--line)" : "none" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: p, size: 30, radius: 7 }), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 3 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: p.alias ? "var(--ui)" : "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 } }, p.alias || shortKey(p.userId, 8, 5)), p.agent && /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: 12, color: "var(--faint)", stroke: 2 })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 } }, /* @__PURE__ */ React.createElement(RouteTag, { peer: p }), p.ip ? /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: p.ip }, p.ip) : null)), /* @__PURE__ */ React.createElement(Icon, { name: "chevronRight", size: 16, color: "var(--faint)", stroke: 2 }))));
4364
+ }
4215
4365
  return /* @__PURE__ */ React.createElement("div", { style: { borderRadius: 11, border: "1px solid var(--line)", overflow: "hidden", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { display: "grid", gridTemplateColumns: "1.4fr 1fr 1fr 0.8fr 90px", gap: 0, padding: "9px 16px", borderBottom: "1px solid var(--line)", background: "var(--panel-2)" } }, [T.colPeer, T.colVip, T.colPath, T.colWire, ""].map((h, i) => /* @__PURE__ */ React.createElement("span", { key: i, style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 700, letterSpacing: 0.8, textTransform: "uppercase", color: "var(--faint)", textAlign: i === 4 ? "right" : "left" } }, h))), peers.map((p, i) => /* @__PURE__ */ React.createElement("div", { key: p.id, style: {
4216
4366
  display: "grid",
4217
4367
  gridTemplateColumns: "1.4fr 1fr 1fr 0.8fr 90px",
@@ -4229,7 +4379,7 @@ ${peer.address}`
4229
4379
  })()), /* @__PURE__ */ React.createElement("div", null, region.nodes.map((n, i) => {
4230
4380
  const active = n.ip === activeExit;
4231
4381
  const stuck = n.online && !n.reachable;
4232
- return /* @__PURE__ */ React.createElement("div", { key: n.ip, style: { display: "flex", alignItems: "center", gap: 12, padding: "10px 14px", borderBottom: i < region.nodes.length - 1 ? "1px solid var(--line)" : "none", background: active ? "color-mix(in oklab, var(--accent), transparent 92%)" : "transparent" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: n.reachable }), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: n.ip }, n.ip), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, n.host), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), n.reachable ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: n.ping > 200 ? "var(--warn)" : "var(--dim)" } }, n.ping, "ms") : stuck ? /* @__PURE__ */ React.createElement(Tag, { tone: "warn", title: "Announces online but IP won't pass \u2014 session desync or NAT-blocked" }, "online \xB7 no route") : /* @__PURE__ */ React.createElement(Tag, { tone: "off" }, "offline"), active ? /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => onSetExit(null) }, T.routing) : /* @__PURE__ */ React.createElement(Btn, { size: "sm", icon: "route", onClick: () => n.reachable && onSetExit(n.ip), style: { opacity: n.reachable ? 1 : 0.4 } }, T.routeThru));
4382
+ return /* @__PURE__ */ React.createElement("div", { key: n.ip, className: "dk-wrap", style: { display: "flex", alignItems: "center", gap: 12, rowGap: 6, padding: "10px 14px", borderBottom: i < region.nodes.length - 1 ? "1px solid var(--line)" : "none", background: active ? "color-mix(in oklab, var(--accent), transparent 92%)" : "transparent" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: n.reachable }), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: n.ip }, n.ip), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, n.host), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), n.reachable ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: n.ping > 200 ? "var(--warn)" : "var(--dim)" } }, n.ping, "ms") : stuck ? /* @__PURE__ */ React.createElement(Tag, { tone: "warn", title: "Announces online but IP won't pass \u2014 session desync or NAT-blocked" }, "online \xB7 no route") : /* @__PURE__ */ React.createElement(Tag, { tone: "off" }, "offline"), active ? /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => onSetExit(null) }, T.routing) : /* @__PURE__ */ React.createElement(Btn, { size: "sm", icon: "route", onClick: () => n.reachable && onSetExit(n.ip), style: { opacity: n.reachable ? 1 : 0.4 } }, T.routeThru));
4233
4383
  })));
4234
4384
  }
4235
4385
  function LanEnableCard({ backend, onArm, onCancel }) {
@@ -4335,9 +4485,10 @@ ${peer.address}`
4335
4485
  }
4336
4486
  function NetworkTab({ T, me, peers, exits, activeExit, reqCount, onSetExit, onOpenChat, backend, onArmLan, onCancelLan }) {
4337
4487
  if (backend && backend.switchable && !backend.hasVirtualLan) {
4338
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1040, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 26 } }, /* @__PURE__ */ React.createElement(MyNode, { T, me, activeExit, peers, reqCount }), /* @__PURE__ */ React.createElement(LanEnableCard, { backend, onArm: onArmLan, onCancel: onCancelLan })));
4488
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { className: "dk-page", style: { maxWidth: 1040, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 26 } }, /* @__PURE__ */ React.createElement(MyNode, { T, me, activeExit, peers, reqCount }), /* @__PURE__ */ React.createElement(LanEnableCard, { backend, onArm: onArmLan, onCancel: onCancelLan })));
4339
4489
  }
4340
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1040, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 26 } }, /* @__PURE__ */ React.createElement(MyNode, { T, me, activeExit, peers, reqCount }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.peerRouting, count: peers.length }), /* @__PURE__ */ React.createElement(PeerTable, { T, peers, onOpenChat })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.exitNodes, trailing: /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { icon: "plus", size: "sm" }, T.addExit)) }), activeExit && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderRadius: 10, background: "color-mix(in oklab, var(--warn), transparent 90%)", border: "1px solid color-mix(in oklab, var(--warn), transparent 70%)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "route", size: 17, color: "var(--warn)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, T.egressVia), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: activeExit }, activeExit), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "unlink", size: "sm", onClick: () => onSetExit(null) }, T.stopRouting)), /* @__PURE__ */ React.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } }, exits.map((r) => /* @__PURE__ */ React.createElement(ExitCard, { key: r.region, T, region: r, activeExit, onSetExit }))))));
4490
+ const lanless = !!backend && backend.hasVirtualLan === false;
4491
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { className: "dk-page", style: { maxWidth: 1040, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 26 } }, /* @__PURE__ */ React.createElement(MyNode, { T, me, activeExit, peers, reqCount }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.peerRouting, count: peers.length }), /* @__PURE__ */ React.createElement(PeerTable, { T, peers, onOpenChat }), !peers.length && /* @__PURE__ */ React.createElement("div", { style: { padding: "14px 16px", borderRadius: 11, border: "1px dashed var(--line)", fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.noPeersYet || "No friends yet \u2014 add one from Chat and it shows up here with its route.")), lanless ? null : /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.exitNodes, trailing: /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { icon: "plus", size: "sm" }, T.addExit)) }), activeExit && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderRadius: 10, background: "color-mix(in oklab, var(--warn), transparent 90%)", border: "1px solid color-mix(in oklab, var(--warn), transparent 70%)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "route", size: 17, color: "var(--warn)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, T.egressVia), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: activeExit }, activeExit), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "unlink", size: "sm", onClick: () => onSetExit(null) }, T.stopRouting)), /* @__PURE__ */ React.createElement("div", { className: "dk-grid1", style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } }, exits.map((r) => /* @__PURE__ */ React.createElement(ExitCard, { key: r.region, T, region: r, activeExit, onSetExit }))))));
4341
4492
  }
4342
4493
  Object.assign(window, { NetworkTab });
4343
4494
  var dkAudioCtx = null;
@@ -4658,6 +4809,8 @@ ${peer.address}`
4658
4809
  recommended: "Discover",
4659
4810
  registered: "Names",
4660
4811
  here: "Here",
4812
+ people: "People",
4813
+ back: "Back",
4661
4814
  dirAdd: "Add",
4662
4815
  dirSent: "requested",
4663
4816
  dirOpenChat: "chat",
@@ -4843,6 +4996,8 @@ ${peer.address}`
4843
4996
  recommended: "\u63A8\u8350",
4844
4997
  registered: "\u540D\u5F55",
4845
4998
  here: "\u5728\u573A",
4999
+ people: "\u53D1\u73B0",
5000
+ back: "\u8FD4\u56DE",
4846
5001
  dirAdd: "\u52A0\u597D\u53CB",
4847
5002
  dirSent: "\u5DF2\u8BF7\u6C42",
4848
5003
  dirOpenChat: "\u4F1A\u8BDD",
@@ -5033,6 +5188,24 @@ ${peer.address}`
5033
5188
  gap: 3
5034
5189
  } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 20, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 9, fontWeight: 600, letterSpacing: 0.2, color: active ? "var(--accent)" : "var(--faint)" } }, label), soon && /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 5, right: 6, width: 6, height: 6, borderRadius: 999, background: "var(--warn)" } }));
5035
5190
  }
5191
+ function TabBarBtn({ icon, label, active, badge, onClick }) {
5192
+ return /* @__PURE__ */ React.createElement("button", { onClick, style: {
5193
+ flex: 1,
5194
+ minWidth: 0,
5195
+ height: 54,
5196
+ border: "none",
5197
+ background: "transparent",
5198
+ cursor: "pointer",
5199
+ position: "relative",
5200
+ display: "flex",
5201
+ flexDirection: "column",
5202
+ alignItems: "center",
5203
+ justifyContent: "center",
5204
+ gap: 3,
5205
+ fontFamily: "var(--mono)"
5206
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 22, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 10, fontWeight: 600, color: active ? "var(--accent)" : "var(--faint)", whiteSpace: "nowrap" } }, label), badge ? /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 4, left: "50%", marginLeft: 6 } }, /* @__PURE__ */ React.createElement(Unread, { n: badge })) : null);
5207
+ }
5208
+ var DK_PEOPLE_TABS = ["here", "recommended", "registered"];
5036
5209
  function DkUpdateModal({ T, info, onClose }) {
5037
5210
  const [phase, setPhase] = React.useState("idle");
5038
5211
  const [err, setErr] = React.useState(null);
@@ -5270,10 +5443,34 @@ ${peer.address}`
5270
5443
  data.refresh();
5271
5444
  });
5272
5445
  });
5446
+ const phone = useIsPhone();
5273
5447
  React.useEffect(() => {
5274
- if (!activeId && peers.length)
5448
+ if (!phone && !activeId && peers.length)
5275
5449
  setActiveId(peers[0].id);
5276
- }, [peers, activeId]);
5450
+ }, [peers, activeId, phone]);
5451
+ React.useEffect(() => {
5452
+ if (!phone)
5453
+ return void 0;
5454
+ const onPop = () => setActiveId(null);
5455
+ window.addEventListener("popstate", onPop);
5456
+ return () => window.removeEventListener("popstate", onPop);
5457
+ }, [phone]);
5458
+ const openConversation = (id) => {
5459
+ setActiveId(id);
5460
+ if (phone && id) {
5461
+ try {
5462
+ window.history.pushState({ dkConv: id }, "");
5463
+ } catch (e) {
5464
+ }
5465
+ }
5466
+ };
5467
+ const closeConversation = () => {
5468
+ const st = window.history.state;
5469
+ if (st && st.dkConv)
5470
+ window.history.back();
5471
+ else
5472
+ setActiveId(null);
5473
+ };
5277
5474
  React.useEffect(() => {
5278
5475
  if (!activeId)
5279
5476
  return;
@@ -5285,7 +5482,7 @@ ${peer.address}`
5285
5482
  }, 2500);
5286
5483
  return () => clearInterval(iv);
5287
5484
  }, [activeId]);
5288
- const onSelect = (id) => setActiveId(id);
5485
+ const onSelect = (id) => openConversation(id);
5289
5486
  const onAct = (id, kind) => {
5290
5487
  const r = requests.find((x) => x.id === id);
5291
5488
  const uid = r && r.userid || id;
@@ -5381,8 +5578,8 @@ ${peer.address}`
5381
5578
  const onSetExit = () => {
5382
5579
  };
5383
5580
  const onOpenChat = (id) => {
5384
- setActiveId(id);
5385
5581
  setTab("chat");
5582
+ openConversation(id);
5386
5583
  };
5387
5584
  const onOpenNet = () => setTab("network");
5388
5585
  const nav = [
@@ -5393,14 +5590,44 @@ ${peer.address}`
5393
5590
  { id: "network", icon: "network", label: T.network },
5394
5591
  { id: "apps", icon: "grid", label: T.apps }
5395
5592
  ];
5396
- return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: {
5593
+ const peopleTab = DK_PEOPLE_TABS.includes(tab);
5594
+ const lastPeople = React.useRef("here");
5595
+ if (peopleTab)
5596
+ lastPeople.current = tab;
5597
+ const unreadTotal = (peers || []).reduce((n, p) => n + (Number(p.unread) || 0), 0) + (requests || []).length;
5598
+ const phoneNav = [
5599
+ { id: "chat", icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
5600
+ { id: "people", icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
5601
+ { id: "network", icon: "network", label: T.network, active: tab === "network", go: () => setTab("network") },
5602
+ { id: "apps", icon: "grid", label: T.apps, active: tab === "apps", go: () => setTab("apps") },
5603
+ { id: "profile", icon: "userRound", label: T.profile, active: tab === "profile", go: () => setTab("profile") }
5604
+ ];
5605
+ const inConversation = phone && tab === "chat" && !!activeId && (peers || []).some((p) => p.id === activeId);
5606
+ return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", flexDirection: phone ? "column" : "row", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement(DkPhoneStyles, null), !phone && /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: {
5397
5607
  padding: 4,
5398
5608
  borderRadius: 12,
5399
5609
  cursor: "pointer",
5400
5610
  display: "flex",
5401
5611
  border: "1px solid " + (tab === "profile" ? "var(--line)" : "transparent"),
5402
5612
  background: tab === "profile" ? "var(--panel-2)" : "transparent"
5403
- } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak })), data.locked && /* @__PURE__ */ React.createElement(
5613
+ } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, !inConversation && /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), !inConversation && /* @__PURE__ */ React.createElement("div", { style: { height: 46, boxSizing: "content-box", paddingTop: phone ? "env(safe-area-inset-top, 0px)" : 0, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: phone ? 8 : 12, paddingLeft: phone ? 12 : 16, paddingRight: phone ? 12 : 16 } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", whiteSpace: "nowrap" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), phone ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: { padding: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 28, radius: 7, dot: false }))) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name)))), phone && peopleTab && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", gap: 6, padding: "8px 12px", borderBottom: "1px solid var(--line)", background: "var(--panel)", overflowX: "auto" } }, nav.filter((n) => DK_PEOPLE_TABS.includes(n.id)).map((n) => /* @__PURE__ */ React.createElement("button", { key: n.id, onClick: () => setTab(n.id), style: {
5614
+ display: "inline-flex",
5615
+ alignItems: "center",
5616
+ gap: 6,
5617
+ padding: "6px 12px",
5618
+ borderRadius: 999,
5619
+ cursor: "pointer",
5620
+ whiteSpace: "nowrap",
5621
+ border: "1px solid var(--line)",
5622
+ fontFamily: "var(--mono)",
5623
+ fontSize: 12,
5624
+ fontWeight: 600,
5625
+ background: tab === n.id ? "var(--accent)" : "transparent",
5626
+ color: tab === n.id ? "#fff" : "var(--dim)"
5627
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, onBack: closeConversation, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak, onSetAvatar: (p) => dkApi.setProfileFull(p).then((r) => {
5628
+ data.refresh();
5629
+ return r;
5630
+ }) })), phone && !inConversation && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", borderTop: "1px solid var(--line)", background: "var(--rail)", paddingBottom: "env(safe-area-inset-bottom, 0px)" } }, phoneNav.map((n) => /* @__PURE__ */ React.createElement(TabBarBtn, { key: n.id, icon: n.icon, label: n.label, active: n.active, badge: n.badge, onClick: n.go }))), data.locked && /* @__PURE__ */ React.createElement(
5404
5631
  DkLockedOut,
5405
5632
  {
5406
5633
  compact: true,
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <title>Beagle Chat</title>
7
7
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' rx='5' fill='%230d1117'/%3E%3Cg fill='none' stroke='%233fb950' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4.5 17 6-6-6-6'/%3E%3Cpath d='M12 18.5h7.5'/%3E%3C/g%3E%3C/svg%3E" />
8
8
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
package/dist/ipc.d.ts CHANGED
@@ -44,9 +44,13 @@ export declare function ipcSocketPath(dataDir: string, platform?: NodeJS.Platfor
44
44
  * Windows named pipes, so on Windows we connect and let net report. */
45
45
  export declare function ipcSocketSupportsFsExistenceCheck(platform?: NodeJS.Platform): boolean;
46
46
  /** How long the liveness probe waits for a connect before calling it dead.
47
- * Local IPC: a listening socket accepts in microseconds, so this is a
48
- * generous ceiling that still keeps the 1500ms watch tick honest. */
49
- export declare const DAEMON_PROBE_TIMEOUT_MS = 400;
47
+ * A listening local socket accepts in well under a millisecond, so this
48
+ * budget never measured the daemon — it measured how long OUR event loop took
49
+ * to run the connect callback. At 400 ms a GC pause or a big JSON body in
50
+ * beagle read as "daemon gone": 814 false flaps on mac-dev while the daemon's
51
+ * pid never changed. One miss is not believed either; see DAEMON_MISS_STREAK
52
+ * in lan-handoff.ts. */
53
+ export declare const DAEMON_PROBE_TIMEOUT_MS = 2000;
50
54
  /**
51
55
  * Is a decentlan daemon actually LISTENING for this identity?
52
56
  *
package/dist/ipc.js CHANGED
@@ -28,9 +28,13 @@ export function ipcSocketSupportsFsExistenceCheck(platform = process.platform) {
28
28
  return platform !== "win32";
29
29
  }
30
30
  /** How long the liveness probe waits for a connect before calling it dead.
31
- * Local IPC: a listening socket accepts in microseconds, so this is a
32
- * generous ceiling that still keeps the 1500ms watch tick honest. */
33
- export const DAEMON_PROBE_TIMEOUT_MS = 400;
31
+ * A listening local socket accepts in well under a millisecond, so this
32
+ * budget never measured the daemon — it measured how long OUR event loop took
33
+ * to run the connect callback. At 400 ms a GC pause or a big JSON body in
34
+ * beagle read as "daemon gone": 814 false flaps on mac-dev while the daemon's
35
+ * pid never changed. One miss is not believed either; see DAEMON_MISS_STREAK
36
+ * in lan-handoff.ts. */
37
+ export const DAEMON_PROBE_TIMEOUT_MS = 2000;
34
38
  /**
35
39
  * Is a decentlan daemon actually LISTENING for this identity?
36
40
  *
@@ -6,6 +6,13 @@ export type HandoffState = "embedded" | "releasing" | "daemon" | "waiting";
6
6
  * gives up on it and takes the identity back. `agentnet restart` and a
7
7
  * launchd/systemd kickstart are both back well inside this. */
8
8
  export declare const DAEMON_GRACE_MS: number;
9
+ /** One missed probe is not a verdict. The probe runs on beagle's own event
10
+ * loop, so any stall here (GC, a large /api/desktop body, an npm child) fires
11
+ * its timer before the queued `connect` callback runs, and a healthy daemon
12
+ * reads as gone — 814 times on mac-dev while daemon pid 75338 never changed.
13
+ * Only a streak of misses that also spans real time is believed. */
14
+ export declare const DAEMON_MISS_STREAK = 3;
15
+ export declare const DAEMON_MISS_WINDOW_MS = 5000;
9
16
  export interface SwitchableHostOptions {
10
17
  dataDir: string;
11
18
  /** Build a fresh embedded host. Called on first start and on every fallback,
@@ -37,6 +44,9 @@ export declare class SwitchablePeerHost implements PeerHost {
37
44
  get kind(): PeerHost["kind"];
38
45
  get hasVirtualLan(): boolean;
39
46
  get state(): HandoffState;
47
+ /** The daemon's data dir (socket and pidfile), for callers that need to
48
+ * watch a daemon restart through. */
49
+ get dataDir(): string;
40
50
  /** Seconds left before `releasing` gives up; 0 when not releasing. */
41
51
  get releaseSecondsLeft(): number;
42
52
  call(req: IpcRequest): Promise<IpcResponse>;
@@ -23,6 +23,13 @@ const RELEASE_TIMEOUT_MS = 10 * 60 * 1000;
23
23
  * gives up on it and takes the identity back. `agentnet restart` and a
24
24
  * launchd/systemd kickstart are both back well inside this. */
25
25
  export const DAEMON_GRACE_MS = 60 * 1000;
26
+ /** One missed probe is not a verdict. The probe runs on beagle's own event
27
+ * loop, so any stall here (GC, a large /api/desktop body, an npm child) fires
28
+ * its timer before the queued `connect` callback runs, and a healthy daemon
29
+ * reads as gone — 814 times on mac-dev while daemon pid 75338 never changed.
30
+ * Only a streak of misses that also spans real time is believed. */
31
+ export const DAEMON_MISS_STREAK = 3;
32
+ export const DAEMON_MISS_WINDOW_MS = 5000;
26
33
  /**
27
34
  * A PeerHost whose backend can change under it.
28
35
  *
@@ -59,6 +66,11 @@ export class SwitchablePeerHost {
59
66
  get state() {
60
67
  return this.#state;
61
68
  }
69
+ /** The daemon's data dir (socket and pidfile), for callers that need to
70
+ * watch a daemon restart through. */
71
+ get dataDir() {
72
+ return this.#opts.dataDir;
73
+ }
62
74
  /** Seconds left before `releasing` gives up; 0 when not releasing. */
63
75
  get releaseSecondsLeft() {
64
76
  if (this.#state !== "releasing")
@@ -130,14 +142,33 @@ export class SwitchablePeerHost {
130
142
  }, WATCH_INTERVAL_MS);
131
143
  this.#timer.unref?.();
132
144
  }
145
+ /** Consecutive missed probes; 0 the moment the daemon answers. */
146
+ #missStreak = 0;
147
+ /** A probe can take its full 2 s timeout against a 1.5 s watch interval.
148
+ * Overlapping ticks would each act on the other's half-made decision. */
149
+ #ticking = false;
133
150
  async #tick() {
134
- if (this.#busy)
151
+ if (this.#busy || this.#ticking)
135
152
  return;
153
+ this.#ticking = true;
154
+ try {
155
+ await this.#step();
156
+ }
157
+ finally {
158
+ this.#ticking = false;
159
+ }
160
+ }
161
+ async #step() {
136
162
  const daemonUp = await this.#probe(this.#opts.dataDir);
137
- if (daemonUp)
163
+ if (daemonUp) {
138
164
  this.#daemonMissingSince = 0;
139
- else if (this.#daemonMissingSince === 0)
140
- this.#daemonMissingSince = Date.now();
165
+ this.#missStreak = 0;
166
+ }
167
+ else {
168
+ if (this.#daemonMissingSince === 0)
169
+ this.#daemonMissingSince = Date.now();
170
+ this.#missStreak++;
171
+ }
141
172
  // A tick can await; make sure nothing started while we were probing.
142
173
  if (this.#busy)
143
174
  return;
@@ -152,7 +183,14 @@ export class SwitchablePeerHost {
152
183
  // and rm the pidfile.
153
184
  if (this.#opts.force === "daemon") {
154
185
  if (this.#state === "daemon" && !daemonUp) {
155
- this.#log.info("the daemon went away — waiting for it to come back (forced daemon backend)");
186
+ // Keep serving until the absence is confirmed. Every call made during
187
+ // a false `waiting` failed with "the daemon is restarting", which the
188
+ // UI showed as an empty friend list and a hung /api/desktop.
189
+ const missedMs = Date.now() - this.#daemonMissingSince;
190
+ if (this.#missStreak < DAEMON_MISS_STREAK || missedMs < DAEMON_MISS_WINDOW_MS)
191
+ return;
192
+ this.#log.info(`the daemon went away — waiting for it to come back (forced daemon backend; ` +
193
+ `${this.#missStreak} probes missed over ${(missedMs / 1000).toFixed(1)} s)`);
156
194
  const old = this.#inner;
157
195
  this.#inner = null;
158
196
  this.#setState("waiting");
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { IpcRequest, IpcResponse } from "./ipc.js";
1
+ import { type IpcRequest, type IpcResponse } from "./ipc.js";
2
2
  /** How this copy of beagle got onto disk — decides what "update" can do.
3
3
  * npx cache paths contain _npx AND node_modules, so test _npx first. */
4
4
  export declare function installMode(): "global" | "npx" | "dev" | "unknown";
@@ -32,6 +32,8 @@ export interface BeagleServerOptions {
32
32
  readonly hasVirtualLan: boolean;
33
33
  readonly state: string;
34
34
  readonly releaseSecondsLeft: number;
35
+ /** The daemon's socket/pidfile dir, so "Update lan" can wait for a new pid. */
36
+ readonly dataDir?: string;
35
37
  armForDaemon: () => Promise<{
36
38
  state: string;
37
39
  command: string;
package/dist/server.js CHANGED
@@ -19,6 +19,7 @@ import { fileURLToPath } from "node:url";
19
19
  import { dirname, join } from "node:path";
20
20
  import { INSTALL_COMMAND as LAN_INSTALL_COMMAND } from "./lan-handoff.js";
21
21
  import yaml from "js-yaml";
22
+ import { probeDaemon } from "./ipc.js";
22
23
  import { DEFAULT_EXITS } from "./exits.js";
23
24
  import * as peerAddr from "@decentnetwork/peer";
24
25
  import { bridgeRoster } from "./bridge-roster.js";
@@ -513,10 +514,45 @@ async function npmQuery(args) {
513
514
  }
514
515
  });
515
516
  }
516
- /** Ask the running agentnet daemon to re-exec itself so it picks up the
517
- * just-installed lan version. Tries the PATH binary, then the global
518
- * install's CLI entry directly (pm2 PATH often lacks the nvm shims). */
519
- async function agentnetRestart() {
517
+ /** Ask the agentnet daemon to restart onto the lan that was just installed,
518
+ * and report success only when a NEW daemon answers. `agentnet restart`
519
+ * exiting 0 means the request was accepted, nothing more: under launchd a
520
+ * lan ≥ 0.1.302 daemon exits and launchd starts the new code, while an older
521
+ * one re-execs outside launchd — which left mac-dev's KeepAlive job failing
522
+ * every ~10 s. Either way the proof is a different pid that accepts a
523
+ * connection. Without a data dir there is nothing to watch, so the old
524
+ * answer stands. */
525
+ async function agentnetRestart(dataDir) {
526
+ const pidBefore = dataDir ? readDaemonPid(dataDir) : "";
527
+ const r = await requestAgentnetRestart();
528
+ if (!r.ok || !dataDir)
529
+ return r;
530
+ return waitForNewDaemon(dataDir, pidBefore);
531
+ }
532
+ const readDaemonPid = (dataDir) => {
533
+ try {
534
+ return readFileSync(join(dataDir, "daemon.pid"), "utf-8").trim();
535
+ }
536
+ catch {
537
+ return "";
538
+ }
539
+ };
540
+ async function waitForNewDaemon(dataDir, pidBefore) {
541
+ const deadline = Date.now() + 60_000;
542
+ while (Date.now() < deadline) {
543
+ const pid = readDaemonPid(dataDir);
544
+ if (pid && pid !== pidBefore && (await probeDaemon(dataDir)))
545
+ return { ok: true };
546
+ await new Promise((r) => setTimeout(r, 1000));
547
+ }
548
+ return {
549
+ ok: false,
550
+ error: `agentnet accepted the restart, but no new daemon answered within 60 s (pid still ${readDaemonPid(dataDir) || "none"})`,
551
+ };
552
+ }
553
+ /** Run `agentnet restart`: the PATH binary, then the global install's CLI entry
554
+ * directly (pm2's PATH often lacks the nvm shims). */
555
+ async function requestAgentnetRestart() {
520
556
  const { spawn } = await import("node:child_process");
521
557
  const cliJs = join(dirname(process.execPath), "..", "lib", "node_modules", "@decentnetwork", "lan", "dist", "cli", "index.js");
522
558
  const attempts = [
@@ -1826,7 +1862,7 @@ export function startBeagleServer(opts) {
1826
1862
  sendJson(res, 502, inst);
1827
1863
  return;
1828
1864
  }
1829
- const rr = await agentnetRestart();
1865
+ const rr = await agentnetRestart(opts.lanHost?.dataDir);
1830
1866
  updateCache = null;
1831
1867
  sendJson(res, 200, { ok: true, restarted: rr.ok, ...(rr.ok ? {} : { note: rr.error }) });
1832
1868
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
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",
@@ -35,7 +35,7 @@
35
35
  "yargs": "^17.7.2"
36
36
  },
37
37
  "devDependencies": {
38
- "@decentnetwork/beagle-ui": "^0.2.7",
38
+ "@decentnetwork/beagle-ui": "^0.2.14",
39
39
  "@types/js-yaml": "^4.0.9",
40
40
  "@types/node": "^20.11.0",
41
41
  "@types/yargs": "^17.0.32",