@decentnetwork/lan 0.1.90 → 0.1.92

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.
Binary file
Binary file
Binary file
Binary file
@@ -942,7 +942,7 @@ function Msg({ m, peer, T }) {
942
942
  wordBreak: "break-word"
943
943
  } }, m.text), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, margin: "3px 3px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.time), mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" }))));
944
944
  }
945
- function Conversation({ T, peer, lang, thread: threadProp, onSend, onAlias, onRemove, onBlock, onOpenNet, onCall }) {
945
+ function Conversation({ T, peer, lang, thread: threadProp, onSend, onAlias, onRemove, onOpenNet }) {
946
946
  const scrollRef = React.useRef(null);
947
947
  const thread = threadProp && threadProp.length ? threadProp : [{ day: "Today" }, { from: "them", time: "\u2014", text: lang === "zh" ? "\u6682\u65E0\u6D88\u606F\u8BB0\u5F55\uFF0C\u53D1\u4E2A\u6D88\u606F\u6253\u4E2A\u62DB\u547C\u5427\u3002" : "No messages yet. Say hi." }];
948
948
  React.useEffect(() => {
@@ -959,19 +959,13 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onAlias, onRe
959
959
  setDraft("");
960
960
  }
961
961
  };
962
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--accent)", display: "inline-flex", alignItems: "center", gap: 4 } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 }), " ", peer.ip))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { icon: "phone", title: T.call, onClick: () => onCall(peer) }), /* @__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: "phone", label: T.call, onClick: () => {
963
- setMenu(false);
964
- onCall(peer);
965
- } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
962
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 60, flexShrink: 0, borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)" } }, peer.alias || shortKey(peer.userId, 10, 6)), peer.agent && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), /* @__PURE__ */ React.createElement(RouteTag, { peer })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--accent)", display: "inline-flex", alignItems: "center", gap: 4 } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 }), " ", peer.ip))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(Btn, { icon: "more", onClick: () => setMenu((v) => !v) }), menu && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { onClick: () => setMenu(false), style: { position: "fixed", inset: 0, zIndex: 40 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 0, top: 36, zIndex: 50, width: 180, background: "var(--panel-2)", border: "1px solid var(--line)", borderRadius: 9, padding: 6, boxShadow: "0 14px 40px rgba(0,0,0,0.4)" } }, /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
966
963
  setMenu(false);
967
964
  onAlias(peer);
968
- } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "file", label: T.sendFile, onClick: () => setMenu(false) }), /* @__PURE__ */ React.createElement("div", { style: { height: 1, background: "var(--line)", margin: "5px 4px" } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "ban", label: T.block, danger: true, onClick: () => {
969
- setMenu(false);
970
- onBlock(peer);
971
- } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "trash", label: T.remove, danger: true, onClick: () => {
965
+ } }), /* @__PURE__ */ React.createElement("div", { style: { height: 1, background: "var(--line)", margin: "5px 4px" } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "trash", label: T.remove, danger: true, onClick: () => {
972
966
  setMenu(false);
973
967
  onRemove(peer);
974
- } }))))), /* @__PURE__ */ React.createElement("div", { ref: scrollRef, style: { flex: 1, overflow: "auto", padding: "18px 22px" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto" } }, thread.map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(Msg, { key: i, m, peer, T })))), /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(Btn, { icon: "paperclip", title: T.attach }), /* @__PURE__ */ React.createElement(
968
+ } }))))), /* @__PURE__ */ React.createElement("div", { ref: scrollRef, style: { flex: 1, overflow: "auto", padding: "18px 22px" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto" } }, thread.map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(Msg, { key: i, m, peer, T })))), /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, borderTop: "1px solid var(--line)", padding: "12px 16px", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React.createElement(
975
969
  "input",
976
970
  {
977
971
  value: draft,
@@ -1008,9 +1002,9 @@ function MenuItem({ icon, label, onClick, danger }) {
1008
1002
  function ChatEmpty({ T }) {
1009
1003
  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));
1010
1004
  }
1011
- function ChatTab({ T, lang, peers, requests, activeId, onSelect, onAct, onRemove, onBlock, onOpenNet, onCall }) {
1005
+ function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onAlias, onRemove, onOpenNet }) {
1012
1006
  const peer = peers.find((p) => p.id === activeId);
1013
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, onRemove, onBlock, onOpenNet, onCall }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
1007
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onAlias, onRemove, onOpenNet }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
1014
1008
  }
1015
1009
  Object.assign(window, { ChatTab });
1016
1010
  function StatTile({ label, value, sub, tone }) {
@@ -1061,63 +1055,6 @@ function ProfileTab({ T, me }) {
1061
1055
  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" } }, /* @__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")), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13, color: "var(--dim)", marginTop: 4 } }, me.handle)), /* @__PURE__ */ React.createElement(Btn, { icon: "edit" }, T.editProfile)), /* @__PURE__ */ React.createElement(Card, { label: T.identity }, /* @__PURE__ */ React.createElement(FieldRow, { label: T.userId, value: me.userId, copy: true }), /* @__PURE__ */ React.createElement(FieldRow, { label: T.carrierAddr, value: me.carrier, copy: true, qr: true }), /* @__PURE__ */ React.createElement(FieldRow, { label: T.netKey, value: me.netKey, copy: true, last: true })), /* @__PURE__ */ React.createElement(Card, { label: T.network }, /* @__PURE__ */ React.createElement(FieldRow, { label: T.virtualIp, value: me.ip, copy: true }), /* @__PURE__ */ React.createElement(FieldRow, { label: T.wireLabel, value: `${me.wire} \xB7 lossless`, mono: false }), /* @__PURE__ */ React.createElement(FieldRow, { label: T.version, value: `lan ${me.lanVer} \xB7 peer ${me.peerVer} \xB7 ${me.channel}`, last: true })), /* @__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)))));
1062
1056
  }
1063
1057
  Object.assign(window, { ProfileTab });
1064
- function CallBtn({ icon, label, onClick, active, danger, wide }) {
1065
- const bg = danger ? "var(--danger)" : active ? "var(--accent)" : "var(--chip)";
1066
- const fg = danger || active ? "#fff" : "var(--text)";
1067
- return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement("button", { onClick, style: {
1068
- width: wide ? 76 : 56,
1069
- height: 56,
1070
- borderRadius: wide ? 18 : 999,
1071
- cursor: "pointer",
1072
- background: bg,
1073
- color: fg,
1074
- border: "1px solid " + (active || danger ? "transparent" : "var(--line)"),
1075
- display: "flex",
1076
- alignItems: "center",
1077
- justifyContent: "center"
1078
- } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 22, stroke: 2 })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, label));
1079
- }
1080
- function VideoTile({ peer, label, self, big }) {
1081
- return /* @__PURE__ */ React.createElement("div", { style: {
1082
- position: "relative",
1083
- width: "100%",
1084
- height: "100%",
1085
- borderRadius: big ? 14 : 12,
1086
- overflow: "hidden",
1087
- background: "repeating-linear-gradient(45deg, var(--panel) 0 14px, var(--panel-2) 14px 28px)",
1088
- border: "1px solid var(--line)",
1089
- display: "flex",
1090
- alignItems: "center",
1091
- justifyContent: "center"
1092
- } }, !self && /* @__PURE__ */ React.createElement(DkIdenticon, { seed: peer.userId, size: big ? 92 : 44, radius: big ? 22 : 12 }), self && /* @__PURE__ */ React.createElement(Icon, { name: "userRound", size: big ? 60 : 30, color: "var(--faint)", stroke: 1.4 }), /* @__PURE__ */ React.createElement("span", { style: {
1093
- position: "absolute",
1094
- left: 10,
1095
- bottom: 9,
1096
- fontFamily: "var(--mono)",
1097
- fontSize: 11,
1098
- fontWeight: 600,
1099
- color: "var(--dim)",
1100
- background: "color-mix(in oklab, var(--bg), transparent 25%)",
1101
- padding: "3px 8px",
1102
- borderRadius: 6
1103
- } }, label));
1104
- }
1105
- function CallOverlay({ T, peer, mode, onSetMode, onEnd }) {
1106
- const [secs, setSecs] = React.useState(0);
1107
- const [muted, setMuted] = React.useState(false);
1108
- const [spk, setSpk] = React.useState(true);
1109
- React.useEffect(() => {
1110
- const id = setInterval(() => setSecs((s) => s + 1), 1e3);
1111
- return () => clearInterval(id);
1112
- }, []);
1113
- const mm = String(Math.floor(secs / 60)).padStart(2, "0");
1114
- const ss = String(secs % 60).padStart(2, "0");
1115
- const video = mode === "video";
1116
- const name = peer.alias || shortKey(peer.userId, 10, 6);
1117
- const controls = /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 18, alignItems: "flex-start" } }, /* @__PURE__ */ React.createElement(CallBtn, { icon: muted ? "micFill" : "mic", label: muted ? T.unmute : T.mute, active: muted, onClick: () => setMuted((m) => !m) }), /* @__PURE__ */ React.createElement(CallBtn, { icon: "video", label: video ? T.audioOnly : T.toVideo, active: video, onClick: () => onSetMode(video ? "audio" : "video") }), /* @__PURE__ */ React.createElement(CallBtn, { icon: "speaker", label: T.speaker, active: spk, onClick: () => setSpk((s) => !s) }), /* @__PURE__ */ React.createElement(CallBtn, { icon: "phone", label: T.endCall, danger: true, wide: true, onClick: onEnd }));
1118
- return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", inset: 0, zIndex: 70, background: "var(--bg)", display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement("div", { style: { height: 52, flexShrink: 0, display: "flex", alignItems: "center", gap: 10, padding: "0 18px", borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement(Icon, { name: video ? "video" : "phone", size: 16, color: "var(--accent)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement(Tag, { tone: peer.via === "relay" ? "warn" : "ok" }, T.connected, " \xB7 ", peer.via, " ", peer.ping, "ms"), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, color: "var(--text)", letterSpacing: 1 } }, mm, ":", ss)), video ? /* @__PURE__ */ React.createElement("div", { style: { flex: 1, position: "relative", padding: 18 } }, /* @__PURE__ */ React.createElement(VideoTile, { peer, label: "video \xB7 " + name, big: true }), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 30, bottom: 30, width: 220, height: 148 } }, /* @__PURE__ */ React.createElement(VideoTile, { peer, label: T.you, self: true })), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", left: 0, right: 0, bottom: 30, display: "flex", justifyContent: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { padding: "16px 26px", borderRadius: 20, background: "color-mix(in oklab, var(--panel), transparent 8%)", border: "1px solid var(--line)" } }, controls))) : /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 22 } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", width: 150, height: 150, borderRadius: 999, border: "2px solid var(--accent)", animation: "dkpulse 2s ease-out infinite" } }), /* @__PURE__ */ React.createElement(DkIdenticon, { seed: peer.userId, size: 108, radius: 26 })), /* @__PURE__ */ React.createElement("div", { style: { textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, justifyContent: "center", marginTop: 8 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--faint)" } }, peer.ip), /* @__PURE__ */ React.createElement(Tag, { tone: peer.via === "relay" ? "warn" : "ok" }, peer.via, " \xB7 ", peer.ping, "ms"))), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 18 } }, controls)));
1119
- }
1120
- Object.assign(window, { CallOverlay });
1121
1058
  const DK_DEFAULTS = (
1122
1059
  /*EDITMODE-BEGIN*/
1123
1060
  {
@@ -1336,14 +1273,10 @@ function RailBtn({ icon, label, active, soon, onClick }) {
1336
1273
  gap: 3
1337
1274
  } }, /* @__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)" } }));
1338
1275
  }
1339
- function MeetingTab({ T, lang }) {
1340
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 18, background: "var(--bg)", padding: 24 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 72, height: 72, borderRadius: 18, background: "var(--panel)", border: "1px solid var(--line)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--accent)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "video", size: 32, stroke: 1.7 })), /* @__PURE__ */ React.createElement("div", { style: { textAlign: "center", maxWidth: 440 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 9, justifyContent: "center" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 19, fontWeight: 700, color: "var(--text)" } }, T.meetingRooms), /* @__PURE__ */ React.createElement(Tag, { tone: "warn" }, T.nextVersion)), /* @__PURE__ */ React.createElement("p", { style: { fontFamily: "var(--ui)", fontSize: 13, lineHeight: 1.55, color: "var(--dim)", marginTop: 10 } }, T.meetingDesc)), /* @__PURE__ */ React.createElement(Btn, { icon: "plus", style: { opacity: 0.5, pointerEvents: "none" } }, T.createRoom));
1341
- }
1342
1276
  function DkApp() {
1343
1277
  const [t, setTweak] = useTweaks(DK_DEFAULTS);
1344
1278
  const [tab, setTab] = React.useState(t.startTab || "chat");
1345
1279
  const [activeId, setActiveId] = React.useState(null);
1346
- const [call, setCall] = React.useState(null);
1347
1280
  const data = useDaemonData();
1348
1281
  const me = data.me;
1349
1282
  const peers = data.peers;
@@ -1392,10 +1325,9 @@ function DkApp() {
1392
1325
  const nav = [
1393
1326
  { id: "chat", icon: "message", label: T.chat },
1394
1327
  { id: "network", icon: "network", label: T.network },
1395
- { id: "meeting", icon: "video", label: T.meeting, soon: true },
1396
1328
  { id: "profile", icon: "userRound", label: T.profile }
1397
1329
  ];
1398
- 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("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column" } }, /* @__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("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "decentlan"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).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, onAlias, onRemove, onBlock: onRemove, onOpenNet, onCall: (p) => setCall({ peer: p, mode: "audio" }) }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "meeting" && /* @__PURE__ */ React.createElement(MeetingTab, { T, lang: t.lang }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me })), call && /* @__PURE__ */ React.createElement(CallOverlay, { T, peer: call.peer, mode: call.mode, onSetMode: (m) => setCall((c) => ({ ...c, mode: m })), onEnd: () => setCall(null) }), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
1330
+ 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("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column" } }, /* @__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("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "decentlan"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", nav.find((n) => n.id === tab).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, onAlias, onRemove, onOpenNet }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me })), /* @__PURE__ */ React.createElement(TweaksPanel, null, /* @__PURE__ */ React.createElement(TweakSection, { label: t.lang === "zh" ? "\u5916\u89C2" : "Appearance" }), /* @__PURE__ */ React.createElement(
1399
1331
  TweakRadio,
1400
1332
  {
1401
1333
  label: t.lang === "zh" ? "\u4E3B\u9898" : "Theme",
package/dist/ui/server.js CHANGED
@@ -17,6 +17,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { dirname, join } from "node:path";
19
19
  import yaml from "js-yaml";
20
+ import { DEFAULT_EXITS } from "../config/loader.js";
20
21
  // Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
21
22
  // scripts/build-ui.mjs emits it next to this compiled module at dist/ui/desktop/.
22
23
  const DESKTOP_DIR = join(dirname(fileURLToPath(import.meta.url)), "desktop");
@@ -194,27 +195,65 @@ export function startFriendUi(opts) {
194
195
  via: "lan",
195
196
  time: "",
196
197
  }));
197
- // Exit nodes from routes.yaml regions, online-status resolved via ipam.
198
+ // Exit nodes = the AVAILABLE exits this node knows about, grouped by
199
+ // region. The authoritative list is the shipped DEFAULT_EXITS
200
+ // (config/default-exits.yaml) — every install has it, so the panel is
201
+ // populated out-of-the-box even on a node with no routes.yaml. Any
202
+ // extra exit IPs an operator added to routes.yaml are folded in too.
203
+ // online status: is that exit a currently-connected friend (by userid,
204
+ // else by its virtual ip showing up as a live peer).
198
205
  let routes = { regions: [], default: "direct" };
199
206
  if (existsSync(opts.routesPath)) {
200
207
  routes = yaml.load(readFileSync(opts.routesPath, "utf-8")) ?? routes;
201
208
  }
209
+ const onlineUserids = new Set();
202
210
  const onlineIps = new Set();
203
- for (const [uid, ip] of ipByUserid) {
204
- const s = sessByUserid.get(uid);
205
- if (s && s.transport && s.transport !== "none")
206
- onlineIps.add(ip);
211
+ for (const f of diagFriends) {
212
+ const uid = f.carrierId || f.pubkey || "";
213
+ const s = f.session;
214
+ if (uid && s && s.transport && s.transport !== "none") {
215
+ onlineUserids.add(uid);
216
+ const ip = ipByUserid.get(uid);
217
+ if (ip)
218
+ onlineIps.add(ip);
219
+ }
207
220
  }
208
- const exits = (routes.regions ?? []).map((r) => ({
209
- region: r.name,
210
- flag: (r.name || "?").slice(0, 2).toUpperCase(),
211
- label: r.name,
212
- nodes: (r.exits ?? []).map((ip, i) => ({
213
- ip,
214
- online: onlineIps.has(ip),
221
+ const regionMeta = {
222
+ china: { flag: "CN", label: "China" },
223
+ japan: { flag: "JP", label: "Japan" },
224
+ us: { flag: "US", label: "United States" },
225
+ };
226
+ const byRegion = new Map();
227
+ for (const e of DEFAULT_EXITS) {
228
+ if (!e.virtual_ip)
229
+ continue;
230
+ const region = e.region || "other";
231
+ if (!byRegion.has(region))
232
+ byRegion.set(region, []);
233
+ byRegion.get(region).push({
234
+ ip: e.virtual_ip,
235
+ host: e.name,
236
+ online: onlineUserids.has(e.userid) || onlineIps.has(e.virtual_ip),
215
237
  ping: null,
216
- host: `${(r.name || "ex").slice(0, 2)}-${String(i + 1).padStart(2, "0")}`,
217
- })),
238
+ });
239
+ }
240
+ // Fold in any extra exit IPs from routes.yaml not already covered.
241
+ const knownIps = new Set(DEFAULT_EXITS.flatMap((e) => (e.virtual_ip ? [e.virtual_ip] : [])));
242
+ for (const r of routes.regions ?? []) {
243
+ for (const ip of r.exits ?? []) {
244
+ if (knownIps.has(ip))
245
+ continue;
246
+ knownIps.add(ip);
247
+ if (!byRegion.has(r.name))
248
+ byRegion.set(r.name, []);
249
+ byRegion.get(r.name).push({ ip, host: ip, online: onlineIps.has(ip), ping: null });
250
+ }
251
+ }
252
+ const exits = [...byRegion.entries()].map(([region, nodes]) => ({
253
+ region,
254
+ flag: regionMeta[region]?.flag ?? region.slice(0, 2).toUpperCase(),
255
+ label: regionMeta[region]?.label ?? region,
256
+ nodes,
218
257
  }));
219
258
  const activeExit = routes.default && routes.default !== "direct" ? routes.default : null;
220
259
  sendJson(res, 200, { me, peers, requests, exits, activeExit });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.90",
3
+ "version": "0.1.92",
4
4
  "description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",