@decentnetwork/beagle 0.1.43 → 0.1.45

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.
Files changed (2) hide show
  1. package/dist/desktop/app.js +237 -43
  2. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.42";
1
+ window.__DK_UI_VERSION="0.1.44";
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"/>',
@@ -1317,7 +1317,7 @@ function PeerRow({ peer, T, active, onClick }) {
1317
1317
  textOverflow: "ellipsis",
1318
1318
  flex: 1,
1319
1319
  minWidth: 0
1320
- } }, 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 })));
1320
+ } }, 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 })));
1321
1321
  }
1322
1322
  function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }) {
1323
1323
  const [q, setQ] = React.useState("");
@@ -1494,6 +1494,108 @@ function dkInlineMarkdown(s) {
1494
1494
  out += dkHtmlEscape(text.slice(last));
1495
1495
  return out;
1496
1496
  }
1497
+ function dkParseBeagleContact(raw) {
1498
+ const text = String(raw == null ? "" : raw);
1499
+ const fence = /```\s*beagle-contact\s*\n([\s\S]*?)\n```/i.exec(text);
1500
+ if (fence) {
1501
+ try {
1502
+ const j = JSON.parse(fence[1].trim());
1503
+ if (j && (j.type === "beagle_contact" || j.type === "beagle-contact")) {
1504
+ return {
1505
+ name: j.name || "",
1506
+ ens: j.ens || "",
1507
+ userid: j.userid || j.userId || "",
1508
+ address: j.address || "",
1509
+ avatar: j.avatar || j.avatarUrl || "",
1510
+ description: j.description || ""
1511
+ };
1512
+ }
1513
+ } catch (e) {
1514
+ }
1515
+ }
1516
+ if (/^\s*BCR1\s+/i.test(text)) {
1517
+ try {
1518
+ const j = JSON.parse(text.replace(/^\s*BCR1\s+/i, "").trim());
1519
+ if (j && j.type === "beagle_contact") {
1520
+ return {
1521
+ name: j.name || "",
1522
+ ens: j.ens || "",
1523
+ userid: j.userid || j.userId || "",
1524
+ address: j.address || "",
1525
+ avatar: j.avatar || "",
1526
+ description: j.description || ""
1527
+ };
1528
+ }
1529
+ } catch (e) {
1530
+ }
1531
+ }
1532
+ return null;
1533
+ }
1534
+ function dkEncodeBeagleContact(p) {
1535
+ const j = {
1536
+ v: 1,
1537
+ type: "beagle_contact",
1538
+ name: p.name || "",
1539
+ ens: p.ens || "",
1540
+ userid: p.userid || "",
1541
+ address: p.address || "",
1542
+ avatar: p.avatar || "",
1543
+ description: p.description || ""
1544
+ };
1545
+ const head = "**" + (p.name || p.ens || "Beagle user") + "**" + (p.ens ? " \xB7 `" + p.ens + "`" : "");
1546
+ const bio = p.description ? "\n\n" + p.description : "";
1547
+ const link = "\n\n[Open name card](https://app.beagle.chat/#/chat?address=" + encodeURIComponent(p.address || "") + ")";
1548
+ return head + bio + link + "\n\n```beagle-contact\n" + JSON.stringify(j) + "\n```\n";
1549
+ }
1550
+ function dkContactPreview(text) {
1551
+ const s = String(text == null ? "" : text);
1552
+ if (!/beagle-contact|BCR1\s*\{/i.test(s))
1553
+ return null;
1554
+ const mine = /^you:\s*/i.test(s);
1555
+ const card = dkParseBeagleContact(s.replace(/^you:\s*/i, ""));
1556
+ const label = "\u{1F4C7} " + (card && (card.name || card.ens) || "name card");
1557
+ return mine ? "you: " + label : label;
1558
+ }
1559
+ function DkContactCardMsg({ card, mine, T, onNameCard }) {
1560
+ return /* @__PURE__ */ React.createElement(
1561
+ "button",
1562
+ {
1563
+ onClick: () => onNameCard && onNameCard(card),
1564
+ style: {
1565
+ display: "flex",
1566
+ alignItems: "center",
1567
+ gap: 11,
1568
+ padding: "10px 13px",
1569
+ minWidth: 220,
1570
+ maxWidth: 320,
1571
+ borderRadius: 12,
1572
+ border: "1px solid var(--line)",
1573
+ cursor: "pointer",
1574
+ textAlign: "left",
1575
+ background: mine ? "var(--bub-me)" : "var(--bub-them)"
1576
+ }
1577
+ },
1578
+ card.avatar ? /* @__PURE__ */ React.createElement(DkImgAvatar, { url: card.avatar, seed: card.userid || card.address || card.name, size: 40, radius: 10 }) : /* @__PURE__ */ React.createElement(DkEnsAvatar, { userid: card.userid || card.address, fallbackSeed: card.userid || card.address || card.name, size: 40, radius: 10 }),
1579
+ /* @__PURE__ */ React.createElement("span", { style: { minWidth: 0, display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 13.5, fontWeight: 700, color: mine ? "#fff" : "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, card.name || card.ens || shortKey(card.userid || card.address, 8, 6)), card.ens && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: mine ? "rgba(255,255,255,0.8)" : "var(--accent)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, card.ens), card.description && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 11, color: mine ? "rgba(255,255,255,0.7)" : "var(--dim)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, card.description), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: mine ? "rgba(255,255,255,0.6)" : "var(--faint)", marginTop: 2, textTransform: "uppercase", letterSpacing: 0.5 } }, T && T.nameCard || "name card", " \u203A"))
1580
+ );
1581
+ }
1582
+ function dkAddressFromHref(href) {
1583
+ try {
1584
+ const u = new URL(String(href || ""), window.location.href);
1585
+ const a = u.searchParams.get("address");
1586
+ if (a && a.trim())
1587
+ return a.trim();
1588
+ const h = u.hash || "";
1589
+ const qi = h.indexOf("?");
1590
+ if (qi >= 0) {
1591
+ const ha = new URLSearchParams(h.slice(qi + 1)).get("address");
1592
+ if (ha && ha.trim())
1593
+ return ha.trim();
1594
+ }
1595
+ } catch (e) {
1596
+ }
1597
+ return "";
1598
+ }
1497
1599
  function dkMarkdownHtml(src) {
1498
1600
  const lines = String(src == null ? "" : src).replace(/\r\n?/g, "\n").split("\n");
1499
1601
  let html = "";
@@ -1506,6 +1608,7 @@ function dkMarkdownHtml(src) {
1506
1608
  continue;
1507
1609
  }
1508
1610
  if (/^```/.test(line)) {
1611
+ const lang = line.replace(/^```/, "").trim().toLowerCase();
1509
1612
  i += 1;
1510
1613
  const code = [];
1511
1614
  while (i < lines.length && !/^```/.test(lines[i])) {
@@ -1514,6 +1617,8 @@ function dkMarkdownHtml(src) {
1514
1617
  }
1515
1618
  if (i < lines.length)
1516
1619
  i += 1;
1620
+ if (lang === "beagle-contact")
1621
+ continue;
1517
1622
  html += "<pre><code>" + dkHtmlEscape(code.join("\n")) + "</code></pre>";
1518
1623
  continue;
1519
1624
  }
@@ -1709,8 +1814,23 @@ function dkNativeMediaFromText(text) {
1709
1814
  const name = String(j.fileName || "file") + ext;
1710
1815
  return { sender, kind, name, url: "data:" + mime + ";base64," + j.data.replace(/\s+/g, "") };
1711
1816
  }
1712
- function MarkdownText({ text }) {
1713
- return /* @__PURE__ */ React.createElement("div", { className: "dk-md", dangerouslySetInnerHTML: { __html: dkMarkdownHtml(text) } });
1817
+ function MarkdownText({ text, onNameCard }) {
1818
+ const onClick = (e) => {
1819
+ const a = e.target && e.target.closest && e.target.closest("a");
1820
+ if (!a)
1821
+ return;
1822
+ const addr = dkAddressFromHref(a.getAttribute("href") || "");
1823
+ if (!addr)
1824
+ return;
1825
+ e.preventDefault();
1826
+ e.stopPropagation();
1827
+ const card = dkParseBeagleContact(text) || { address: addr };
1828
+ if (!card.address)
1829
+ card.address = addr;
1830
+ if (onNameCard)
1831
+ onNameCard(card);
1832
+ };
1833
+ return /* @__PURE__ */ React.createElement("div", { className: "dk-md", onClick, dangerouslySetInnerHTML: { __html: dkMarkdownHtml(text) } });
1714
1834
  }
1715
1835
  function DkChatForm({ form, submitted, peer, onSubmit }) {
1716
1836
  const items = form && form.components || [];
@@ -1882,7 +2002,7 @@ function DkChatForm({ form, submitted, peer, onSubmit }) {
1882
2002
  return null;
1883
2003
  }), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
1884
2004
  }
1885
- function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onOpenFile, onCall, onFormSubmit, answered, selMode, selected, onToggleSel, busy, isGroup }) {
2005
+ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, answered, selMode, selected, onToggleSel, busy, isGroup }) {
1886
2006
  const mine = m.from === "me";
1887
2007
  const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
1888
2008
  const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
@@ -1920,7 +2040,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onO
1920
2040
  justifyContent: "center"
1921
2041
  } }, selected && /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 3, color: "#fff" })),
1922
2042
  !mine && (grpSender ? /* @__PURE__ */ React.createElement(DkGroupSender, { grp: grpSender, T, avatar: true }) : /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false })),
1923
- /* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.media === "video") || rtcFile && (rtcFile.media === "image" || rtcFile.media === "video") ? "min(680px, 88%)" : "64%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, callRec ? /* @__PURE__ */ React.createElement(
2043
+ /* @__PURE__ */ React.createElement("div", { style: { maxWidth: m.file && (m.file.media === "image" || m.file.media === "video") || rtcFile && (rtcFile.media === "image" || rtcFile.media === "video") ? "min(680px, 88%)" : "75%", display: "flex", flexDirection: "column", alignItems: mine ? "flex-end" : "flex-start" } }, callRec ? /* @__PURE__ */ React.createElement(
1924
2044
  "button",
1925
2045
  {
1926
2046
  onClick: () => onCall && onCall(peer.userId, callRec.kind === "video"),
@@ -2168,23 +2288,26 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onO
2168
2288
  }
2169
2289
  }
2170
2290
  }
2171
- ) : nativeMedia.kind === "audio" ? /* @__PURE__ */ React.createElement("audio", { controls: true, src: nativeMedia.url, style: { maxWidth: "100%" } }) : /* @__PURE__ */ React.createElement("video", { controls: true, src: nativeMedia.url, style: { maxWidth: "100%", borderRadius: 12, border: "1px solid var(--line)" } })) : /* @__PURE__ */ React.createElement(React.Fragment, null, grp && /* @__PURE__ */ React.createElement(DkGroupSender, { grp, T }), /* @__PURE__ */ React.createElement("div", { style: {
2172
- padding: "8px 12px",
2173
- borderRadius: 12,
2174
- borderBottomRightRadius: mine ? 4 : 12,
2175
- borderBottomLeftRadius: mine ? 12 : 4,
2176
- // Incoming messages are tinted by delivery path so the split is
2177
- // visible: amber = arrived via the express relay (offline), plain =
2178
- // live online session. Outgoing keep the accent bubble.
2179
- background: mine ? "var(--bub-me)" : m.via === "offline" ? "rgba(245,158,11,0.16)" : "var(--bub-them)",
2180
- color: mine ? "#fff" : "var(--text)",
2181
- border: "1px solid " + (mine ? "transparent" : m.via === "offline" ? "rgba(245,158,11,0.55)" : "var(--line)"),
2182
- fontFamily: "var(--ui)",
2183
- fontSize: 13.5,
2184
- lineHeight: 1.4,
2185
- letterSpacing: -0.1,
2186
- wordBreak: "break-word"
2187
- } }, /* @__PURE__ */ React.createElement(MarkdownText, { text: read.text }))), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), /* @__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.via && /* @__PURE__ */ React.createElement(
2291
+ ) : nativeMedia.kind === "audio" ? /* @__PURE__ */ React.createElement("audio", { controls: true, src: nativeMedia.url, style: { maxWidth: "100%" } }) : /* @__PURE__ */ React.createElement("video", { controls: true, src: nativeMedia.url, style: { maxWidth: "100%", borderRadius: 12, border: "1px solid var(--line)" } })) : (() => {
2292
+ const contact = !read.form ? dkParseBeagleContact(read.text) : null;
2293
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, grp && /* @__PURE__ */ React.createElement(DkGroupSender, { grp, T }), contact && (contact.address || contact.userid) ? /* @__PURE__ */ React.createElement(DkContactCardMsg, { card: contact, mine, T, onNameCard }) : /* @__PURE__ */ React.createElement("div", { style: {
2294
+ padding: "8px 12px",
2295
+ borderRadius: 12,
2296
+ borderBottomRightRadius: mine ? 4 : 12,
2297
+ borderBottomLeftRadius: mine ? 12 : 4,
2298
+ // Incoming messages are tinted by delivery path so the split is
2299
+ // visible: amber = arrived via the express relay (offline), plain =
2300
+ // live online session. Outgoing keep the accent bubble.
2301
+ background: mine ? "var(--bub-me)" : m.via === "offline" ? "rgba(245,158,11,0.16)" : "var(--bub-them)",
2302
+ color: mine ? "#fff" : "var(--text)",
2303
+ border: "1px solid " + (mine ? "transparent" : m.via === "offline" ? "rgba(245,158,11,0.55)" : "var(--line)"),
2304
+ fontFamily: "var(--ui)",
2305
+ fontSize: 13.5,
2306
+ lineHeight: 1.4,
2307
+ letterSpacing: -0.1,
2308
+ wordBreak: "break-word"
2309
+ } }, /* @__PURE__ */ React.createElement(MarkdownText, { text: read.text, onNameCard })));
2310
+ })(), read.form && /* @__PURE__ */ React.createElement("div", { style: { marginTop: 6 } }, /* @__PURE__ */ React.createElement(DkChatForm, { form: read.form, submitted, peer, onSubmit: onFormSubmit })), /* @__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.via && /* @__PURE__ */ React.createElement(
2188
2311
  "span",
2189
2312
  {
2190
2313
  title: m.via === "offline" ? "delivered via express relay (offline)" : "delivered over a live session (online)",
@@ -2213,7 +2336,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onO
2213
2336
  ) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
2214
2337
  );
2215
2338
  }
2216
- function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
2339
+ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
2217
2340
  const scrollRef = React.useRef(null);
2218
2341
  const isGroup = React.useMemo(() => dkThreadIsGroup(threadProp), [threadProp]);
2219
2342
  const followScrollRef = React.useRef(true);
@@ -2221,6 +2344,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
2221
2344
  const rtcFileRef = React.useRef(null);
2222
2345
  const [menu, setMenu] = React.useState(false);
2223
2346
  const [pubProfile, setPubProfile] = React.useState(false);
2347
+ const [nameCardProf, setNameCardProf] = React.useState(null);
2224
2348
  const [selMode, setSelMode] = React.useState(false);
2225
2349
  const [sel, setSel] = React.useState(() => /* @__PURE__ */ new Set());
2226
2350
  const [hidden, setHidden] = React.useState(() => /* @__PURE__ */ new Set());
@@ -2539,6 +2663,7 @@ ${peer.address}`
2539
2663
  onOpenFile: doOpenFile,
2540
2664
  onCall,
2541
2665
  onFormSubmit: () => onReloadThread && onReloadThread(),
2666
+ onNameCard: (card) => setNameCardProf(card),
2542
2667
  answered,
2543
2668
  busy: m.id ? fileBusy[m.id] : void 0,
2544
2669
  selMode,
@@ -2640,7 +2765,24 @@ ${peer.address}`
2640
2765
  style: { maxWidth: "100%", maxHeight: "100%", objectFit: "contain", borderRadius: 6 }
2641
2766
  }
2642
2767
  ))),
2643
- pubProfile && /* @__PURE__ */ React.createElement(DkPublicProfile, { T, userid: peer.userId, fallbackName: peer.alias, isFriend: true, onClose: () => setPubProfile(false) })
2768
+ pubProfile && /* @__PURE__ */ React.createElement(DkPublicProfile, { T, userid: peer.userId, fallbackName: peer.alias, isFriend: true, onClose: () => setPubProfile(false) }),
2769
+ nameCardProf && (() => {
2770
+ const cardFriend = (peers || []).find((p) => nameCardProf.userid && p.userId === nameCardProf.userid || nameCardProf.address && p.address === nameCardProf.address);
2771
+ return /* @__PURE__ */ React.createElement(
2772
+ DkPublicProfile,
2773
+ {
2774
+ T,
2775
+ userid: cardFriend && cardFriend.userId || nameCardProf.userid || nameCardProf.address,
2776
+ fallbackName: nameCardProf.name || nameCardProf.ens,
2777
+ isFriend: !!cardFriend,
2778
+ onMessage: cardFriend && onOpenChat ? () => {
2779
+ onOpenChat(cardFriend.id);
2780
+ setNameCardProf(null);
2781
+ } : void 0,
2782
+ onClose: () => setNameCardProf(null)
2783
+ }
2784
+ );
2785
+ })()
2644
2786
  );
2645
2787
  }
2646
2788
  function MenuItem({ icon, label, onClick, danger }) {
@@ -2666,7 +2808,7 @@ function ChatEmpty({ T }) {
2666
2808
  }
2667
2809
  function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, prefillAddr, onPrefillConsumed }) {
2668
2810
  const peer = peers.find((p) => p.id === activeId);
2669
- 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 }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
2811
+ 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 }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
2670
2812
  }
2671
2813
  Object.assign(window, { ChatTab });
2672
2814
  function StatTile({ label, value, sub, tone }) {
@@ -3008,9 +3150,34 @@ function DkPunkPicker({ T, onPick, onClose }) {
3008
3150
  /* @__PURE__ */ React.createElement("img", { src: p.image, alt: `punk #${p.id}`, style: { width: "100%", height: "100%", imageRendering: "pixelated", display: "block" } })
3009
3151
  ))))));
3010
3152
  }
3011
- function DkPublicProfile({ T, userid, fallbackName, onClose, isFriend, isMe }) {
3153
+ function DkPublicProfile({ T, userid, fallbackName, onClose, isFriend, isMe, onMessage }) {
3012
3154
  const [st, setSt] = React.useState({ loading: true });
3013
3155
  const [addState, setAddState] = React.useState(null);
3156
+ const [recOpen, setRecOpen] = React.useState(false);
3157
+ const [recPeers, setRecPeers] = React.useState(null);
3158
+ const [recState, setRecState] = React.useState({});
3159
+ const openRecommend = () => {
3160
+ setRecOpen(true);
3161
+ if (recPeers)
3162
+ return;
3163
+ fetch("/api/desktop").then((r) => r.json()).then((d) => {
3164
+ const list = (d && d.peers || []).filter((p) => p.userId && p.userId !== userid && !p.pending);
3165
+ setRecPeers(list);
3166
+ }).catch(() => setRecPeers([]));
3167
+ };
3168
+ const recommendTo = (p) => {
3169
+ const prof2 = st.prof || {};
3170
+ const text = dkEncodeBeagleContact({
3171
+ name: prof2.displayName || fallbackName || "",
3172
+ ens: prof2.ens || "",
3173
+ userid,
3174
+ address: prof2.address || "",
3175
+ avatar: prof2.avatarUrl || "",
3176
+ description: prof2.description || ""
3177
+ });
3178
+ setRecState((s) => ({ ...s, [p.id]: "busy" }));
3179
+ fetch("/api/chat-send", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ userid: p.userId, text }) }).then((r) => r.json()).then((r) => setRecState((s) => ({ ...s, [p.id]: r.ok !== false ? "sent" : r.error || "failed" }))).catch((e) => setRecState((s) => ({ ...s, [p.id]: String(e && e.message || e) })));
3180
+ };
3014
3181
  const addFriend = (address) => {
3015
3182
  setAddState("busy");
3016
3183
  fetch("/api/add", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ address }) }).then((r) => r.json()).then((r) => setAddState(r.ok !== false ? "sent" : r.error || T.addFailed || "failed")).catch((e) => setAddState(String(e && e.message || e)));
@@ -3050,25 +3217,42 @@ function DkPublicProfile({ T, userid, fallbackName, onClose, isFriend, isMe }) {
3050
3217
  )))), (() => {
3051
3218
  const kvRow = (label, value, head = 8) => /* @__PURE__ */ React.createElement("div", { style: rowS, key: label }, /* @__PURE__ */ React.createElement("span", { style: lbl }, label), /* @__PURE__ */ React.createElement("span", { title: value, style: { flex: 1, minWidth: 0, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--dim)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, shortKey(value, head, 6)), /* @__PURE__ */ React.createElement(CopyBtn, { value, copiedText: T.copied, copyFailedText: T.copyFailed, copyTitle: T.copy }));
3052
3219
  return /* @__PURE__ */ React.createElement(React.Fragment, null, prof.eth && kvRow("ethereum", prof.eth), prof.sol && kvRow("solana", prof.sol), kvRow(T.userId || "user id", userid, 10), prof.address && kvRow(T.addr || "address", prof.address));
3053
- })(), !isMe && !isFriend && prof.address && /* @__PURE__ */ React.createElement("div", { style: { ...rowS, justifyContent: "flex-end" } }, addState === "sent" ? /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, T.dirSent || "requested") : addState && addState !== "busy" ? /* @__PURE__ */ React.createElement("span", { title: addState, style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--warn, #f59e0b)" } }, T.addFailed || "failed") : /* @__PURE__ */ React.createElement(
3220
+ })(), (() => {
3221
+ const cta = { padding: "7px 16px", borderRadius: 9, cursor: "pointer", fontFamily: "var(--ui)", fontSize: 12.5 };
3222
+ return /* @__PURE__ */ React.createElement("div", { style: { ...rowS, justifyContent: "flex-end", gap: 8 } }, !isMe && prof.address && /* @__PURE__ */ React.createElement(
3223
+ "button",
3224
+ {
3225
+ onClick: openRecommend,
3226
+ style: { ...cta, border: "1px solid var(--line)", background: "transparent", color: "var(--text)" }
3227
+ },
3228
+ T.pubRecommend || "Recommend"
3229
+ ), isFriend && onMessage && /* @__PURE__ */ React.createElement(
3230
+ "button",
3231
+ {
3232
+ onClick: onMessage,
3233
+ style: { ...cta, border: "none", background: "var(--accent)", color: "#fff" }
3234
+ },
3235
+ T.pubMessage || "Message"
3236
+ ), !isMe && !isFriend && prof.address && (addState === "sent" ? /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, T.dirSent || "requested") : addState && addState !== "busy" ? /* @__PURE__ */ React.createElement("span", { title: addState, style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--warn, #f59e0b)" } }, T.addFailed || "failed") : /* @__PURE__ */ React.createElement(
3237
+ "button",
3238
+ {
3239
+ onClick: () => addFriend(prof.address),
3240
+ disabled: addState === "busy",
3241
+ style: { ...cta, border: "none", background: "var(--accent)", color: "#fff", opacity: addState === "busy" ? 0.6 : 1 }
3242
+ },
3243
+ addState === "busy" ? "\u2026" : T.dirAdd || "Add"
3244
+ )));
3245
+ })(), recOpen && /* @__PURE__ */ React.createElement("div", { style: { borderTop: "1px solid var(--line)", paddingTop: 10, display: "flex", flexDirection: "column", gap: 4, maxHeight: 200, overflowY: "auto" } }, /* @__PURE__ */ React.createElement("span", { style: { ...lbl, width: "auto", paddingBottom: 4 } }, T.pubRecTitle || "send this card to\u2026"), !recPeers ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.dirLoading || "loading\u2026") : recPeers.length === 0 ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)" } }, T.dirEmpty || "nobody") : recPeers.map((p) => /* @__PURE__ */ React.createElement(
3054
3246
  "button",
3055
3247
  {
3056
- onClick: () => addFriend(prof.address),
3057
- disabled: addState === "busy",
3058
- style: {
3059
- padding: "7px 16px",
3060
- borderRadius: 9,
3061
- border: "none",
3062
- cursor: addState === "busy" ? "default" : "pointer",
3063
- background: "var(--accent)",
3064
- color: "#fff",
3065
- fontFamily: "var(--ui)",
3066
- fontSize: 12.5,
3067
- opacity: addState === "busy" ? 0.6 : 1
3068
- }
3248
+ key: p.id,
3249
+ onClick: () => !recState[p.id] && recommendTo(p),
3250
+ style: { display: "flex", alignItems: "center", gap: 9, padding: "6px 8px", borderRadius: 9, border: "none", background: "transparent", cursor: recState[p.id] ? "default" : "pointer", textAlign: "left" }
3069
3251
  },
3070
- addState === "busy" ? "\u2026" : T.dirAdd || "Add"
3071
- )))));
3252
+ /* @__PURE__ */ React.createElement(DkAvatar, { peer: p, size: 26, radius: 7, dot: false }),
3253
+ /* @__PURE__ */ React.createElement("span", { style: { flex: 1, minWidth: 0, fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, p.alias || shortKey(p.userId, 8, 6)),
3254
+ 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")
3255
+ ))))));
3072
3256
  }
3073
3257
  function DkEnsCard({ T, me, onRecord }) {
3074
3258
  const [st, setSt] = React.useState({ loading: true });
@@ -4417,6 +4601,11 @@ const STR = {
4417
4601
  linkPlaceholder: "username or profile URL",
4418
4602
  save: "save",
4419
4603
  addr: "address",
4604
+ nameCard: "name card",
4605
+ pubMessage: "Message",
4606
+ pubRecommend: "Recommend",
4607
+ pubRecTitle: "send this card to\u2026",
4608
+ pubRecSent: "sent \u2713",
4420
4609
  updTitle: "Update available",
4421
4610
  updLater: "Later",
4422
4611
  updNow: "Update now",
@@ -4581,6 +4770,11 @@ const STR = {
4581
4770
  linkPlaceholder: "\u7528\u6237\u540D\u6216\u4E3B\u9875\u94FE\u63A5",
4582
4771
  save: "\u4FDD\u5B58",
4583
4772
  addr: "\u5730\u5740",
4773
+ nameCard: "\u540D\u7247",
4774
+ pubMessage: "\u53D1\u6D88\u606F",
4775
+ pubRecommend: "\u63A8\u8350",
4776
+ pubRecTitle: "\u628A\u540D\u7247\u53D1\u9001\u7ED9\u2026",
4777
+ pubRecSent: "\u5DF2\u53D1\u9001 \u2713",
4584
4778
  updTitle: "\u53D1\u73B0\u65B0\u7248\u672C",
4585
4779
  updLater: "\u7A0D\u540E",
4586
4780
  updNow: "\u7ACB\u5373\u66F4\u65B0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
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",