@decentnetwork/beagle 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/desktop/app.js +132 -9
- package/dist/server.js +121 -4
- package/package.json +1 -1
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.23";
|
|
2
2
|
const ICON_PATHS = {
|
|
3
3
|
// ---- tab bar (the four must feel like one set) ----
|
|
4
4
|
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
|
@@ -627,9 +627,25 @@ function DkBot({ seed, size = 30, radius = 7 }) {
|
|
|
627
627
|
justifyContent: "center"
|
|
628
628
|
} }, /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: size * 0.56, color: "#fff", stroke: 1.9 }));
|
|
629
629
|
}
|
|
630
|
+
function DkImgAvatar({ url, seed, size, radius }) {
|
|
631
|
+
const [broken, setBroken] = React.useState(false);
|
|
632
|
+
if (broken)
|
|
633
|
+
return /* @__PURE__ */ React.createElement(DkIdenticon, { seed, size, radius });
|
|
634
|
+
return /* @__PURE__ */ React.createElement(
|
|
635
|
+
"img",
|
|
636
|
+
{
|
|
637
|
+
src: url,
|
|
638
|
+
alt: "",
|
|
639
|
+
width: size,
|
|
640
|
+
height: size,
|
|
641
|
+
onError: () => setBroken(true),
|
|
642
|
+
style: { width: size, height: size, borderRadius: radius, objectFit: "cover", flexShrink: 0, background: "var(--panel-2)" }
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
}
|
|
630
646
|
function DkAvatar({ peer, size = 30, radius = 7, dot = true }) {
|
|
631
647
|
const d = Math.max(8, size * 0.3);
|
|
632
|
-
return /* @__PURE__ */ React.createElement("div", { style: { position: "relative", width: size, height: size, flexShrink: 0 } }, peer.agent ? /* @__PURE__ */ React.createElement(DkBot, { seed: peer.id, size, radius }) : /* @__PURE__ */ React.createElement(DkIdenticon, { seed: peer.userId || peer.id, size, radius }), dot && /* @__PURE__ */ React.createElement("span", { style: {
|
|
648
|
+
return /* @__PURE__ */ React.createElement("div", { style: { position: "relative", width: size, height: size, flexShrink: 0 } }, peer.agent ? /* @__PURE__ */ React.createElement(DkBot, { seed: peer.id, size, radius }) : peer.avatarUrl ? /* @__PURE__ */ React.createElement(DkImgAvatar, { url: peer.avatarUrl, seed: peer.userId || peer.id, size, radius }) : peer.punkId != null ? /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: peer.punkId, size, radius, fallbackSeed: peer.userId || peer.id }) : /* @__PURE__ */ React.createElement(DkIdenticon, { seed: peer.userId || peer.id, size, radius }), dot && /* @__PURE__ */ React.createElement("span", { style: {
|
|
633
649
|
position: "absolute",
|
|
634
650
|
right: -2,
|
|
635
651
|
bottom: -2,
|
|
@@ -764,7 +780,7 @@ function Btn({ children, icon, tone = "ghost", onClick, size = "md", title, styl
|
|
|
764
780
|
function Section({ label, count, trailing, style }) {
|
|
765
781
|
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);
|
|
766
782
|
}
|
|
767
|
-
Object.assign(window, { DkIdenticon, DkBot, DkAvatar, StatusDot, Mono, shortKey, RouteTag, Tag, Unread, Btn, Section });
|
|
783
|
+
Object.assign(window, { DkIdenticon, DkBot, DkAvatar, DkImgAvatar, StatusDot, Mono, shortKey, RouteTag, Tag, Unread, Btn, Section });
|
|
768
784
|
const __TWEAKS_STYLE = `
|
|
769
785
|
.twk-panel{position:fixed;right:16px;bottom:16px;z-index:2147483646;width:280px;
|
|
770
786
|
max-height:calc(100vh - 32px);display:flex;flex-direction:column;
|
|
@@ -1586,6 +1602,7 @@ function DkGroupSender({ grp, T, avatar }) {
|
|
|
1586
1602
|
const [open, setOpen] = React.useState(false);
|
|
1587
1603
|
const [busy, setBusy] = React.useState(false);
|
|
1588
1604
|
const [done, setDone] = React.useState(null);
|
|
1605
|
+
const [showProf, setShowProf] = React.useState(false);
|
|
1589
1606
|
const seed = grp.senderId || grp.sender;
|
|
1590
1607
|
const addFriend = () => {
|
|
1591
1608
|
setBusy(true);
|
|
@@ -1638,7 +1655,26 @@ function DkGroupSender({ grp, T, avatar }) {
|
|
|
1638
1655
|
}
|
|
1639
1656
|
},
|
|
1640
1657
|
done || (T && T.add || "Add friend")
|
|
1641
|
-
)
|
|
1658
|
+
), /* @__PURE__ */ React.createElement(
|
|
1659
|
+
"button",
|
|
1660
|
+
{
|
|
1661
|
+
onClick: () => {
|
|
1662
|
+
setOpen(false);
|
|
1663
|
+
setShowProf(true);
|
|
1664
|
+
},
|
|
1665
|
+
style: {
|
|
1666
|
+
padding: "6px 10px",
|
|
1667
|
+
borderRadius: 8,
|
|
1668
|
+
border: "1px solid var(--line)",
|
|
1669
|
+
background: "transparent",
|
|
1670
|
+
color: "var(--text)",
|
|
1671
|
+
fontFamily: "var(--ui)",
|
|
1672
|
+
fontSize: 12,
|
|
1673
|
+
cursor: "pointer"
|
|
1674
|
+
}
|
|
1675
|
+
},
|
|
1676
|
+
T && T.pubOpen || "View public profile"
|
|
1677
|
+
)) : /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", lineHeight: 1.45 } }, T && T.groupSenderUnknown || "The group relays only this display name. Register Beagle as a group agent (/agent add <beagle address> in the group) to receive full member identities you can add as friends."))), showProf && grp.senderId && /* @__PURE__ */ React.createElement(DkPublicProfile, { T, userid: grp.senderId, fallbackName: grp.sender, onClose: () => setShowProf(false) }));
|
|
1642
1678
|
}
|
|
1643
1679
|
function dkThreadIsGroup(thread) {
|
|
1644
1680
|
return (thread || []).some((m) => !m.day && m.dir !== "out" && !m.file && (dkGroupParse(m.text) || {}).strong);
|
|
@@ -2149,6 +2185,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
2149
2185
|
const fileRef = React.useRef(null);
|
|
2150
2186
|
const rtcFileRef = React.useRef(null);
|
|
2151
2187
|
const [menu, setMenu] = React.useState(false);
|
|
2188
|
+
const [pubProfile, setPubProfile] = React.useState(false);
|
|
2152
2189
|
const [selMode, setSelMode] = React.useState(false);
|
|
2153
2190
|
const [sel, setSel] = React.useState(() => /* @__PURE__ */ new Set());
|
|
2154
2191
|
const [hidden, setHidden] = React.useState(() => /* @__PURE__ */ new Set());
|
|
@@ -2397,7 +2434,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
|
|
|
2397
2434
|
onDrop
|
|
2398
2435
|
},
|
|
2399
2436
|
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"),
|
|
2400
|
-
/* @__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"), 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(
|
|
2437
|
+
/* @__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(
|
|
2401
2438
|
Mono,
|
|
2402
2439
|
{
|
|
2403
2440
|
size: 11.5,
|
|
@@ -2555,7 +2592,8 @@ ${peer.address}`
|
|
|
2555
2592
|
onClick: (e) => e.stopPropagation(),
|
|
2556
2593
|
style: { maxWidth: "100%", maxHeight: "100%", objectFit: "contain", borderRadius: 6 }
|
|
2557
2594
|
}
|
|
2558
|
-
)))
|
|
2595
|
+
))),
|
|
2596
|
+
pubProfile && /* @__PURE__ */ React.createElement(DkPublicProfile, { T, userid: peer.userId, fallbackName: peer.alias, onClose: () => setPubProfile(false) })
|
|
2559
2597
|
);
|
|
2560
2598
|
}
|
|
2561
2599
|
function MenuItem({ icon, label, onClick, danger }) {
|
|
@@ -2890,6 +2928,40 @@ function DkPunkPicker({ T, onPick, onClose }) {
|
|
|
2890
2928
|
/* @__PURE__ */ React.createElement("img", { src: p.image, alt: `punk #${p.id}`, style: { width: "100%", height: "100%", imageRendering: "pixelated", display: "block" } })
|
|
2891
2929
|
))))));
|
|
2892
2930
|
}
|
|
2931
|
+
function DkPublicProfile({ T, userid, fallbackName, onClose }) {
|
|
2932
|
+
const [st, setSt] = React.useState({ loading: true });
|
|
2933
|
+
React.useEffect(() => {
|
|
2934
|
+
let dead = false;
|
|
2935
|
+
setSt({ loading: true });
|
|
2936
|
+
fetch(`/api/ens-profile?userid=${encodeURIComponent(userid)}`).then((r) => r.json()).then((d) => {
|
|
2937
|
+
if (!dead)
|
|
2938
|
+
setSt({ loading: false, prof: d.ok && d.profile || null });
|
|
2939
|
+
}).catch(() => {
|
|
2940
|
+
if (!dead)
|
|
2941
|
+
setSt({ loading: false, prof: null });
|
|
2942
|
+
});
|
|
2943
|
+
return () => {
|
|
2944
|
+
dead = true;
|
|
2945
|
+
};
|
|
2946
|
+
}, [userid]);
|
|
2947
|
+
const prof = st.prof;
|
|
2948
|
+
const LINK_BASE = { twitter: "https://x.com/", linkedin: "https://www.linkedin.com/in/", github: "https://github.com/" };
|
|
2949
|
+
const links = prof ? Object.entries(prof.links || {}) : [];
|
|
2950
|
+
const lbl = { width: 110, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 };
|
|
2951
|
+
const rowS = { display: "flex", alignItems: "center", gap: 10, padding: "10px 0", borderTop: "1px solid var(--line)" };
|
|
2952
|
+
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: 24 } }, /* @__PURE__ */ React.createElement("div", { onClick: (e) => e.stopPropagation(), style: { width: 420, maxWidth: "94vw", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: "20px 22px", display: "flex", flexDirection: "column", gap: 14 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 14 } }, prof && prof.avatarUrl ? /* @__PURE__ */ React.createElement(DkImgAvatar, { url: prof.avatarUrl, seed: userid, size: 56, radius: 14 }) : prof && prof.punkId != null ? /* @__PURE__ */ React.createElement(DkPunkAvatar, { id: prof.punkId, size: 56, radius: 14, fallbackSeed: userid }) : /* @__PURE__ */ React.createElement(DkIdenticon, { seed: userid, size: 56, radius: 14 }), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 17, fontWeight: 700, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, prof && prof.displayName || fallbackName || shortKey(userid, 10, 6)), prof && prof.ens && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--accent)", marginTop: 2 } }, prof.ens)), /* @__PURE__ */ React.createElement(Btn, { icon: "x", size: "sm", onClick: onClose })), st.loading ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : !prof ? /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.pubNone || "No public profile \u2014 this user has not registered a beagles.eth name.") : /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column" } }, prof.description && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--dim)", paddingBottom: 12, lineHeight: 1.5 } }, prof.description), links.length > 0 && /* @__PURE__ */ React.createElement("div", { style: { ...rowS, gap: 8, flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("span", { style: lbl }, T.linksCard || "links"), links.map(([k, h]) => /* @__PURE__ */ React.createElement(
|
|
2953
|
+
"a",
|
|
2954
|
+
{
|
|
2955
|
+
key: k,
|
|
2956
|
+
href: (LINK_BASE[k] || "") + h,
|
|
2957
|
+
target: "_blank",
|
|
2958
|
+
rel: "noreferrer",
|
|
2959
|
+
style: { display: "inline-flex", alignItems: "center", gap: 5, padding: "4px 10px", borderRadius: 999, border: "1px solid var(--line)", textDecoration: "none", fontFamily: "var(--ui)", fontSize: 12, color: "var(--accent)" }
|
|
2960
|
+
},
|
|
2961
|
+
k,
|
|
2962
|
+
" \u2197"
|
|
2963
|
+
))), prof.eth && /* @__PURE__ */ React.createElement("div", { style: rowS }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "ethereum"), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: prof.eth, title: prof.eth }, shortKey(prof.eth, 8, 6))), prof.sol && /* @__PURE__ */ React.createElement("div", { style: rowS }, /* @__PURE__ */ React.createElement("span", { style: lbl }, "solana"), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: prof.sol, title: prof.sol }, shortKey(prof.sol, 8, 6))), /* @__PURE__ */ React.createElement("div", { style: rowS }, /* @__PURE__ */ React.createElement("span", { style: lbl }, T.userId || "user id"), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: userid, title: userid }, shortKey(userid, 10, 6))))));
|
|
2964
|
+
}
|
|
2893
2965
|
function DkEnsCard({ T, me, onRecord }) {
|
|
2894
2966
|
const [st, setSt] = React.useState({ loading: true });
|
|
2895
2967
|
const [label, setLabel] = React.useState("");
|
|
@@ -2956,6 +3028,47 @@ function DkEnsCard({ T, me, onRecord }) {
|
|
|
2956
3028
|
};
|
|
2957
3029
|
const field = { flex: 1, minWidth: 0, height: 34, borderRadius: 8, border: "1px solid var(--line)", background: "var(--panel-2)", color: "var(--text)", fontFamily: "var(--mono)", fontSize: 13, padding: "0 10px", outline: "none" };
|
|
2958
3030
|
const row = { display: "flex", alignItems: "center", gap: 10, padding: "12px 16px", borderBottom: "1px solid var(--line)" };
|
|
3031
|
+
const rowLbl = { width: 130, flexShrink: 0, fontFamily: "var(--mono)", fontSize: 11.5, fontWeight: 600, color: "var(--faint)", textTransform: "uppercase", letterSpacing: 0.5 };
|
|
3032
|
+
const SOCIALS = [
|
|
3033
|
+
{ k: "twitter", base: "https://x.com/" },
|
|
3034
|
+
{ k: "linkedin", base: "https://www.linkedin.com/in/" },
|
|
3035
|
+
{ k: "github", base: "https://github.com/" }
|
|
3036
|
+
];
|
|
3037
|
+
const [draft, setDraft] = React.useState({});
|
|
3038
|
+
React.useEffect(() => {
|
|
3039
|
+
const t = st.record && st.record.texts || {};
|
|
3040
|
+
setDraft({ twitter: t["com.twitter"] || "", linkedin: t["com.linkedin"] || "", github: t["com.github"] || "" });
|
|
3041
|
+
}, [st.record]);
|
|
3042
|
+
const saveSocial = (k) => post("/api/ens-social", { key: k, value: (draft[k] || "").trim() });
|
|
3043
|
+
const socialRow = ({ k, base }, last) => {
|
|
3044
|
+
var _a;
|
|
3045
|
+
const saved = (st.record && st.record.texts || {})["com." + k] || "";
|
|
3046
|
+
const cur = (_a = draft[k]) != null ? _a : "";
|
|
3047
|
+
return /* @__PURE__ */ React.createElement("div", { key: k, style: { ...row, borderBottom: last ? "none" : row.borderBottom } }, /* @__PURE__ */ React.createElement("span", { style: rowLbl }, k), /* @__PURE__ */ React.createElement(
|
|
3048
|
+
"input",
|
|
3049
|
+
{
|
|
3050
|
+
value: cur,
|
|
3051
|
+
onChange: (e) => setDraft({ ...draft, [k]: e.target.value }),
|
|
3052
|
+
disabled: busy || !st.mineOwned,
|
|
3053
|
+
placeholder: T.linkPlaceholder || "username or profile URL",
|
|
3054
|
+
onKeyDown: (e) => {
|
|
3055
|
+
if (e.key === "Enter")
|
|
3056
|
+
saveSocial(k);
|
|
3057
|
+
},
|
|
3058
|
+
style: field
|
|
3059
|
+
}
|
|
3060
|
+
), saved && /* @__PURE__ */ React.createElement(
|
|
3061
|
+
"a",
|
|
3062
|
+
{
|
|
3063
|
+
href: base + saved,
|
|
3064
|
+
target: "_blank",
|
|
3065
|
+
rel: "noreferrer",
|
|
3066
|
+
title: base + saved,
|
|
3067
|
+
style: { flexShrink: 0, fontFamily: "var(--mono)", fontSize: 13, color: "var(--accent)", textDecoration: "none" }
|
|
3068
|
+
},
|
|
3069
|
+
"\u2197"
|
|
3070
|
+
), /* @__PURE__ */ React.createElement(Btn, { tone: "accent", size: "sm", disabled: busy || !st.mineOwned || cur.trim() === saved, onClick: () => saveSocial(k) }, saved && !cur.trim() ? T.ensUnbind || "unbind" : T.save || "save"));
|
|
3071
|
+
};
|
|
2959
3072
|
const [picking, setPicking] = React.useState(false);
|
|
2960
3073
|
const fileRef = React.useRef(null);
|
|
2961
3074
|
const rec = st.record;
|
|
@@ -3018,7 +3131,7 @@ function DkEnsCard({ T, me, onRecord }) {
|
|
|
3018
3131
|
img.src = url;
|
|
3019
3132
|
};
|
|
3020
3133
|
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")));
|
|
3021
|
-
return /* @__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(
|
|
3134
|
+
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(
|
|
3022
3135
|
"input",
|
|
3023
3136
|
{
|
|
3024
3137
|
value: label,
|
|
@@ -3031,7 +3144,7 @@ function DkEnsCard({ T, me, onRecord }) {
|
|
|
3031
3144
|
},
|
|
3032
3145
|
style: field
|
|
3033
3146
|
}
|
|
3034
|
-
), /* @__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) }));
|
|
3147
|
+
), /* @__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))));
|
|
3035
3148
|
}
|
|
3036
3149
|
function ProfileTab({ T, me, onEdit }) {
|
|
3037
3150
|
const [qr, setQr] = React.useState(null);
|
|
@@ -3057,7 +3170,7 @@ function ProfileTab({ T, me, onEdit }) {
|
|
|
3057
3170
|
setEditing(false);
|
|
3058
3171
|
} }));
|
|
3059
3172
|
}
|
|
3060
|
-
Object.assign(window, { ProfileTab, DkPunkAvatar });
|
|
3173
|
+
Object.assign(window, { ProfileTab, DkPunkAvatar, DkPublicProfile });
|
|
3061
3174
|
function DkDirAvatar({ p, size }) {
|
|
3062
3175
|
const [broken, setBroken] = React.useState(false);
|
|
3063
3176
|
if (p.punkId != null && !(p.avatar && !broken)) {
|
|
@@ -4098,6 +4211,11 @@ const STR = {
|
|
|
4098
4211
|
pt_zombie: "zombie",
|
|
4099
4212
|
pt_ape: "ape",
|
|
4100
4213
|
pt_alien: "alien",
|
|
4214
|
+
linksCard: "Links",
|
|
4215
|
+
linkPlaceholder: "username or profile URL",
|
|
4216
|
+
save: "save",
|
|
4217
|
+
pubOpen: "View public profile",
|
|
4218
|
+
pubNone: "No public profile \u2014 this user has not registered a beagles.eth name.",
|
|
4101
4219
|
ensNoEthWallet: "no Ethereum wallet extension found (MetaMask\u2026)",
|
|
4102
4220
|
ensNoSolWallet: "no Solana wallet extension found (Phantom\u2026)",
|
|
4103
4221
|
ensWalletOwned: "registered via your mobile wallet as",
|
|
@@ -4239,6 +4357,11 @@ const STR = {
|
|
|
4239
4357
|
pt_zombie: "\u50F5\u5C38",
|
|
4240
4358
|
pt_ape: "\u733F",
|
|
4241
4359
|
pt_alien: "\u5916\u661F\u4EBA",
|
|
4360
|
+
linksCard: "\u793E\u4EA4\u8D26\u53F7",
|
|
4361
|
+
linkPlaceholder: "\u7528\u6237\u540D\u6216\u4E3B\u9875\u94FE\u63A5",
|
|
4362
|
+
save: "\u4FDD\u5B58",
|
|
4363
|
+
pubOpen: "\u67E5\u770B\u516C\u5F00\u8D44\u6599",
|
|
4364
|
+
pubNone: "\u6682\u65E0\u516C\u5F00\u8D44\u6599\u2014\u2014\u5BF9\u65B9\u8FD8\u6CA1\u6709\u6CE8\u518C beagles.eth \u540D\u5B57\u3002",
|
|
4242
4365
|
ensNoEthWallet: "\u672A\u68C0\u6D4B\u5230\u4EE5\u592A\u574A\u94B1\u5305\u63D2\u4EF6(MetaMask \u7B49)",
|
|
4243
4366
|
ensNoSolWallet: "\u672A\u68C0\u6D4B\u5230 Solana \u94B1\u5305\u63D2\u4EF6(Phantom \u7B49)",
|
|
4244
4367
|
ensWalletOwned: "\u5DF2\u901A\u8FC7\u624B\u673A\u94B1\u5305\u6CE8\u518C\u4E3A",
|
package/dist/server.js
CHANGED
|
@@ -154,6 +154,39 @@ async function ensAvatarDelete(call, fullName) {
|
|
|
154
154
|
// best-effort only
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
function ensPubFromEntry(ens, v) {
|
|
158
|
+
const t = v?.texts ?? {};
|
|
159
|
+
return {
|
|
160
|
+
ens,
|
|
161
|
+
avatarUrl: t.avatar || null,
|
|
162
|
+
punkId: v?.nft === "CryptoPunks" && typeof v?.nftid === "number" && v.nftid > 0 ? v.nftid : null,
|
|
163
|
+
displayName: t.displayName || null,
|
|
164
|
+
description: t.description || null,
|
|
165
|
+
links: {
|
|
166
|
+
...(t["com.twitter"] ? { twitter: t["com.twitter"] } : {}),
|
|
167
|
+
...(t["com.linkedin"] ? { linkedin: t["com.linkedin"] } : {}),
|
|
168
|
+
...(t["com.github"] ? { github: t["com.github"] } : {}),
|
|
169
|
+
},
|
|
170
|
+
eth: v?.addresses?.["60"] || null,
|
|
171
|
+
sol: v?.addresses?.["501"] || null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** userid → EnsPub from the cached /names listing. Bounded wait so the UI
|
|
175
|
+
* poll never blocks on a cold cache — the fetch continues in the background
|
|
176
|
+
* and the next poll finds it cached. */
|
|
177
|
+
async function ensPublicByUserid(maxWaitMs = 300) {
|
|
178
|
+
const p = fetchDiscoverJson(`${ENS_GATEWAY}/names`).catch(() => null);
|
|
179
|
+
const raw = (await Promise.race([p, new Promise((r) => setTimeout(() => r(null), maxWaitMs))]));
|
|
180
|
+
const map = new Map();
|
|
181
|
+
if (raw && typeof raw === "object") {
|
|
182
|
+
for (const [ens, v] of Object.entries(raw)) {
|
|
183
|
+
const uid = v?.texts?.carrierUserId;
|
|
184
|
+
if (uid)
|
|
185
|
+
map.set(uid, ensPubFromEntry(ens, v));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return map;
|
|
189
|
+
}
|
|
157
190
|
/** True only when the request came from the local machine. Used to gate the
|
|
158
191
|
* "Sign in with Decent" routes so binding the UI to a LAN IP can't expose
|
|
159
192
|
* identity signing to other hosts. The popup always runs in the local user's
|
|
@@ -471,16 +504,16 @@ export function startBeagleServer(opts) {
|
|
|
471
504
|
// Register/update this node's *.beagles.eth name and bind wallet
|
|
472
505
|
// addresses into the record. The write routes trigger identity
|
|
473
506
|
// signatures, so they're LOCALHOST-ONLY like /connect.
|
|
474
|
-
if (url === "/api/ens-register" || url === "/api/ens-bind-wallet" || url === "/api/ens-avatar" || url === "/api/ens-avatar-upload") {
|
|
507
|
+
if (url === "/api/ens-register" || url === "/api/ens-bind-wallet" || url === "/api/ens-avatar" || url === "/api/ens-avatar-upload" || url === "/api/ens-social") {
|
|
475
508
|
if (!isLocalRequest(req)) {
|
|
476
509
|
sendJson(res, 403, { ok: false, error: "identity signing is available only on this machine (localhost)" });
|
|
477
510
|
return;
|
|
478
511
|
}
|
|
479
512
|
}
|
|
480
513
|
// Punk directory (proxied): a random batch by type, and one punk by id.
|
|
481
|
-
if (req.method === "GET" && url
|
|
514
|
+
if (req.method === "GET" && url === "/api/punk-list") {
|
|
482
515
|
try {
|
|
483
|
-
const q = new URL(url, "http://localhost");
|
|
516
|
+
const q = new URL(req.url || "", "http://localhost");
|
|
484
517
|
const type = (q.searchParams.get("type") || "any").toLowerCase().replace(/[^a-z]/g, "") || "any";
|
|
485
518
|
const limit = Math.min(60, Math.max(1, Number(q.searchParams.get("limit")) || 24));
|
|
486
519
|
const list = await punksFetch(`/api/punks/filter/${type}/any?limit=${limit}`);
|
|
@@ -549,6 +582,59 @@ export function startBeagleServer(opts) {
|
|
|
549
582
|
sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, nftid: clear ? null : nftid } : { ok: false, error: r.error });
|
|
550
583
|
return;
|
|
551
584
|
}
|
|
585
|
+
// Bind/update/remove a social handle on the record, stored under the
|
|
586
|
+
// standard ENS text keys (com.twitter / com.linkedin / com.github) so
|
|
587
|
+
// other ENS-aware clients can read them too. Empty value = remove.
|
|
588
|
+
if (req.method === "POST" && url === "/api/ens-social") {
|
|
589
|
+
const SOCIAL_KEYS = { twitter: "com.twitter", linkedin: "com.linkedin", github: "com.github" };
|
|
590
|
+
const body = await readBody(req);
|
|
591
|
+
const key = SOCIAL_KEYS[String(body.key ?? "")];
|
|
592
|
+
if (!key) {
|
|
593
|
+
sendJson(res, 400, { ok: false, error: "key must be twitter, linkedin or github" });
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// Accept a bare handle, @handle, or a pasted profile URL — keep the handle.
|
|
597
|
+
const value = String(body.value ?? "")
|
|
598
|
+
.trim()
|
|
599
|
+
.replace(/^https?:\/\/(www\.)?(x\.com|twitter\.com|github\.com|linkedin\.com\/(in|company))\//i, "")
|
|
600
|
+
.replace(/\/+$/, "")
|
|
601
|
+
.replace(/^@/, "");
|
|
602
|
+
if (value && !/^[A-Za-z0-9_.-]{1,64}$/.test(value)) {
|
|
603
|
+
sendJson(res, 400, { ok: false, error: "handle may only contain letters, digits, _ . -" });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const diag = await opts.call({ op: "diag" });
|
|
607
|
+
const identity = (diag.ok ? diag.data?.identity : null) ?? {};
|
|
608
|
+
if (!identity.userid) {
|
|
609
|
+
sendJson(res, 502, { ok: false, error: "identity unavailable" });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const mine = await ensFetchJson(`/getAddress/${encodeURIComponent(identity.userid)}`);
|
|
613
|
+
if (!mine) {
|
|
614
|
+
sendJson(res, 404, { ok: false, error: "register a name first" });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (mine.owner !== identity.userid) {
|
|
618
|
+
sendJson(res, 403, { ok: false, error: "this name was registered by your mobile wallet — edit links there" });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const texts = { ...(mine.texts ?? {}) };
|
|
622
|
+
if (value)
|
|
623
|
+
texts[key] = value;
|
|
624
|
+
else
|
|
625
|
+
delete texts[key];
|
|
626
|
+
const r = await ensSignAndSet(opts.call, {
|
|
627
|
+
name: mine.name,
|
|
628
|
+
owner: identity.userid,
|
|
629
|
+
addresses: mine.addresses ?? {},
|
|
630
|
+
texts,
|
|
631
|
+
referee: mine.referee ?? "",
|
|
632
|
+
nft: mine.nft ?? "",
|
|
633
|
+
nftid: mine.nftid ?? 0,
|
|
634
|
+
});
|
|
635
|
+
sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, key: body.key, value: value || null } : { ok: false, error: r.error });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
552
638
|
// Upload a custom avatar image: the browser sends a pre-scaled data URL,
|
|
553
639
|
// we push the bytes to the gateway's R2 (auth = identity signature) and
|
|
554
640
|
// point texts.avatar at it.
|
|
@@ -1039,11 +1125,38 @@ export function startBeagleServer(opts) {
|
|
|
1039
1125
|
// One bootstrap call for the desktop UI: composes the design's DK_*
|
|
1040
1126
|
// shapes (me / peers / requests / exits) from diag + friends-list +
|
|
1041
1127
|
// ipam + routes, so the client data layer is a single poll.
|
|
1128
|
+
// Public profile card for any userid (friend or directory row): what
|
|
1129
|
+
// they published on their beagles.eth record. Read-only, no signing.
|
|
1130
|
+
if (req.method === "GET" && url === "/api/ens-profile") {
|
|
1131
|
+
const q = new URL(req.url || "", "http://localhost").searchParams;
|
|
1132
|
+
let userid = q.get("userid") || "";
|
|
1133
|
+
if (!userid) {
|
|
1134
|
+
sendJson(res, 400, { ok: false, error: "userid required" });
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
// A 52-char Carrier ADDRESS also works (group cards only have that):
|
|
1138
|
+
// its first 32 bytes are the pubkey, i.e. the userid.
|
|
1139
|
+
try {
|
|
1140
|
+
const bytes = peerAddr.base58ToBytes(userid);
|
|
1141
|
+
if (bytes.length === peerAddr.CARRIER_ADDRESS_SIZE)
|
|
1142
|
+
userid = peerAddr.bytesToBase58(bytes.slice(0, 32));
|
|
1143
|
+
}
|
|
1144
|
+
catch { /* not base58 — leave as-is */ }
|
|
1145
|
+
let prof = (await ensPublicByUserid(1500)).get(userid) ?? null;
|
|
1146
|
+
if (!prof) {
|
|
1147
|
+
const rec = await ensFetchJson(`/getAddress/${encodeURIComponent(userid)}`);
|
|
1148
|
+
if (rec)
|
|
1149
|
+
prof = ensPubFromEntry(rec.name, rec);
|
|
1150
|
+
}
|
|
1151
|
+
sendJson(res, 200, { ok: true, registered: !!prof, profile: prof });
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1042
1154
|
if (req.method === "GET" && url === "/api/desktop") {
|
|
1043
|
-
const [diag, pending, flist] = await Promise.all([
|
|
1155
|
+
const [diag, pending, flist, ensPub] = await Promise.all([
|
|
1044
1156
|
opts.call({ op: "diag" }),
|
|
1045
1157
|
opts.call({ op: "friends-pending" }),
|
|
1046
1158
|
opts.call({ op: "friends-list" }),
|
|
1159
|
+
ensPublicByUserid(),
|
|
1047
1160
|
]);
|
|
1048
1161
|
const d = (diag.ok ? diag.data : {}) ?? {};
|
|
1049
1162
|
const identity = d.identity ?? {};
|
|
@@ -1094,6 +1207,8 @@ export function startBeagleServer(opts) {
|
|
|
1094
1207
|
// null = backend didn't report it (daemon); the UI then hides the
|
|
1095
1208
|
// toggle rather than showing a state it cannot know.
|
|
1096
1209
|
autoAccept: typeof node.autoAccept === "boolean" ? node.autoAccept : null,
|
|
1210
|
+
avatarUrl: ensPub.get(identity.userid ?? "")?.avatarUrl ?? null,
|
|
1211
|
+
punkId: ensPub.get(identity.userid ?? "")?.punkId ?? null,
|
|
1097
1212
|
};
|
|
1098
1213
|
const fl = (flist.ok ? (flist.data?.friends ?? []) : []);
|
|
1099
1214
|
const peers = fl.map((f) => {
|
|
@@ -1124,6 +1239,8 @@ export function startBeagleServer(opts) {
|
|
|
1124
1239
|
lastMsg: lm ? (lm.dir === "out" ? "you: " : "") + (lm.text ?? "") : "",
|
|
1125
1240
|
lastTime: fmtTime(lm?.ts),
|
|
1126
1241
|
wire: "163",
|
|
1242
|
+
avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
|
|
1243
|
+
punkId: ensPub.get(uid)?.punkId ?? null,
|
|
1127
1244
|
};
|
|
1128
1245
|
});
|
|
1129
1246
|
const pend = pending.ok ? (pending.data?.pending ?? []) : [];
|
package/package.json
CHANGED