@decentnetwork/beagle 0.1.12 → 0.1.14

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 +167 -6
  2. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.12";
1
+ window.__DK_UI_VERSION="0.1.14";
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"/>',
@@ -1528,6 +1528,144 @@ function dkMarkdownHtml(src) {
1528
1528
  }
1529
1529
  return html || dkHtmlEscape(src);
1530
1530
  }
1531
+ const DK_NATIVE_MEDIA_MIME = {
1532
+ ".jpg": "image/jpeg",
1533
+ ".jpeg": "image/jpeg",
1534
+ ".png": "image/png",
1535
+ ".gif": "image/gif",
1536
+ ".webp": "image/webp",
1537
+ ".heic": "image/heic",
1538
+ ".m4a": "audio/mp4",
1539
+ ".aac": "audio/aac",
1540
+ ".mp3": "audio/mpeg",
1541
+ ".wav": "audio/wav",
1542
+ ".caf": "audio/x-caf",
1543
+ ".mp4": "video/mp4",
1544
+ ".mov": "video/quicktime",
1545
+ ".m4v": "video/x-m4v"
1546
+ };
1547
+ const DK_GROUP_TS_RE = /\n?\[(20\d{2}-\d{1,2}-\d{1,2}\s+\d{1,2}:\d{2}:\d{2})\]\s*$/;
1548
+ function dkStripGroupTs(text) {
1549
+ const m = DK_GROUP_TS_RE.exec(text);
1550
+ return m ? { body: text.slice(0, m.index), ts: m[1] } : { body: text, ts: null };
1551
+ }
1552
+ function dkSplitGroupSender(text) {
1553
+ const m = /^([^:\n]{1,48}):\s([\s\S]+)$/.exec(text);
1554
+ return m ? { sender: m[1], body: m[2] } : null;
1555
+ }
1556
+ function dkParseCGP1(text) {
1557
+ const t = String(text || "").trim();
1558
+ if (!t.startsWith("CGP1 "))
1559
+ return null;
1560
+ let j = null;
1561
+ try {
1562
+ j = JSON.parse(t.slice(5));
1563
+ } catch (e) {
1564
+ return null;
1565
+ }
1566
+ if (!j || j.type !== "carrier_group_message" || j.chat_type !== "group")
1567
+ return null;
1568
+ const origin = j.origin || {};
1569
+ const senderId = origin.userid || origin.friendid || null;
1570
+ const sender = origin.nickname || origin.user_info && origin.user_info.name || (senderId ? senderId.slice(0, 10) : "member");
1571
+ const body = j.message && j.message.text || j.render && j.render.plain || "";
1572
+ return { sender, senderId, body: String(body), ts: null, strong: true };
1573
+ }
1574
+ function dkGroupParse(text) {
1575
+ const cgp = dkParseCGP1(text);
1576
+ if (cgp)
1577
+ return cgp;
1578
+ const t = String(text || "");
1579
+ const { body: noTs, ts } = dkStripGroupTs(t);
1580
+ const split = dkSplitGroupSender(noTs.trim());
1581
+ if (!split)
1582
+ return null;
1583
+ return { sender: split.sender, senderId: null, body: split.body.trim(), ts, strong: !!ts };
1584
+ }
1585
+ function DkGroupSender({ grp, T }) {
1586
+ const [open, setOpen] = React.useState(false);
1587
+ const [busy, setBusy] = React.useState(false);
1588
+ const [done, setDone] = React.useState(null);
1589
+ const seed = grp.senderId || grp.sender;
1590
+ const addFriend = () => {
1591
+ setBusy(true);
1592
+ fetch("/api/add", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ address: grp.senderId }) }).then((r) => r.json()).then((r) => setDone(r.ok ? T && T.addSent || "request sent" : r.error || "failed")).catch((e) => setDone(String(e && e.message || e))).finally(() => setBusy(false));
1593
+ };
1594
+ return /* @__PURE__ */ React.createElement("span", { style: { position: "relative", display: "inline-flex", alignItems: "center", gap: 5, margin: "0 3px 2px" } }, /* @__PURE__ */ React.createElement(
1595
+ "button",
1596
+ {
1597
+ onClick: () => setOpen((v) => !v),
1598
+ style: { display: "inline-flex", alignItems: "center", gap: 5, background: "none", border: "none", padding: 0, cursor: "pointer" }
1599
+ },
1600
+ /* @__PURE__ */ React.createElement(DkIdenticon, { seed, size: 16, radius: 4 }),
1601
+ /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, grp.sender)
1602
+ ), open && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { onClick: () => setOpen(false), style: { position: "fixed", inset: 0, zIndex: 40 } }), /* @__PURE__ */ React.createElement("span", { style: {
1603
+ position: "absolute",
1604
+ left: 0,
1605
+ top: 20,
1606
+ zIndex: 50,
1607
+ width: 240,
1608
+ padding: 12,
1609
+ borderRadius: 10,
1610
+ background: "var(--panel-2)",
1611
+ border: "1px solid var(--line)",
1612
+ boxShadow: "0 14px 40px rgba(0,0,0,0.4)",
1613
+ display: "flex",
1614
+ flexDirection: "column",
1615
+ gap: 8
1616
+ } }, /* @__PURE__ */ React.createElement("span", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed, size: 34, radius: 8 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 700, color: "var(--text)", wordBreak: "break-all" } }, grp.sender)), grp.senderId ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Mono, { size: 10.5, dim: true, copy: grp.senderId, title: grp.senderId }, shortKey(grp.senderId, 12, 8)), /* @__PURE__ */ React.createElement(
1617
+ "button",
1618
+ {
1619
+ onClick: addFriend,
1620
+ disabled: busy || !!done,
1621
+ style: {
1622
+ padding: "6px 10px",
1623
+ borderRadius: 8,
1624
+ border: "none",
1625
+ background: "var(--accent)",
1626
+ color: "#fff",
1627
+ fontFamily: "var(--ui)",
1628
+ fontSize: 12,
1629
+ cursor: busy || done ? "default" : "pointer",
1630
+ opacity: busy ? 0.6 : 1
1631
+ }
1632
+ },
1633
+ done || (T && T.add || "Add friend")
1634
+ )) : /* @__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."))));
1635
+ }
1636
+ function dkThreadIsGroup(thread) {
1637
+ return (thread || []).some((m) => !m.day && m.dir !== "out" && !m.file && (dkGroupParse(m.text) || {}).strong);
1638
+ }
1639
+ function dkNativeMediaFromText(text) {
1640
+ let body = dkStripGroupTs(String(text || "")).body.trim();
1641
+ if (body.length < 32)
1642
+ return null;
1643
+ let sender = null;
1644
+ if (!body.startsWith("{")) {
1645
+ const split = dkSplitGroupSender(body);
1646
+ if (!split || !split.body.startsWith("{"))
1647
+ return null;
1648
+ sender = split.sender;
1649
+ body = split.body.trim();
1650
+ }
1651
+ if (!body.startsWith("{") || !body.endsWith("}"))
1652
+ return null;
1653
+ let j = null;
1654
+ try {
1655
+ j = JSON.parse(body);
1656
+ } catch (e) {
1657
+ return null;
1658
+ }
1659
+ if (!j || typeof j !== "object" || typeof j.data !== "string" || j.data.length < 16)
1660
+ return null;
1661
+ const kind = j.type === "image" ? "image" : j.type === "audio" || j.type === "voice" ? "audio" : j.type === "video" ? "video" : null;
1662
+ if (!kind)
1663
+ return null;
1664
+ const ext = String(j.fileExtension || "").toLowerCase();
1665
+ const mime = DK_NATIVE_MEDIA_MIME[ext] || kind + "/*";
1666
+ const name = String(j.fileName || "file") + ext;
1667
+ return { sender, kind, name, url: "data:" + mime + ";base64," + j.data.replace(/\s+/g, "") };
1668
+ }
1531
1669
  function MarkdownText({ text }) {
1532
1670
  return /* @__PURE__ */ React.createElement("div", { className: "dk-md", dangerouslySetInnerHTML: { __html: dkMarkdownHtml(text) } });
1533
1671
  }
@@ -1701,9 +1839,11 @@ function DkChatForm({ form, submitted, peer, onSubmit }) {
1701
1839
  return null;
1702
1840
  }), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
1703
1841
  }
1704
- function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onCall, onFormSubmit, answered, selMode, selected, onToggleSel, busy }) {
1842
+ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onCall, onFormSubmit, answered, selMode, selected, onToggleSel, busy, isGroup }) {
1705
1843
  const mine = m.from === "me";
1706
- const read = window.ChatComponents ? window.ChatComponents.readMessage(m.text || "") : { text: m.text, form: null, interaction: null };
1844
+ const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
1845
+ const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
1846
+ const read = window.ChatComponents ? window.ChatComponents.readMessage((grp ? grp.body : m.text) || "") : { text: grp ? grp.body : m.text, form: null, interaction: null };
1707
1847
  const submitted = read.form && answered ? answered.get(read.form.custom_id) : null;
1708
1848
  const rtcFile = !mine && !m.file ? dkRtcFileFromText(peer.userId || peer.id, m.text) : null;
1709
1849
  const callRec = !m.file ? dkCallFromText(m.text) : null;
@@ -1934,7 +2074,22 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onC
1934
2074
  )
1935
2075
  );
1936
2076
  })()
1937
- ) : /* @__PURE__ */ React.createElement("div", { style: {
2077
+ ) : nativeMedia ? /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 4, maxWidth: "min(420px, 88%)" } }, nativeMedia.sender && /* @__PURE__ */ React.createElement(DkGroupSender, { grp: { sender: nativeMedia.sender, senderId: null }, T }), nativeMedia.kind === "image" ? /* @__PURE__ */ React.createElement(
2078
+ "img",
2079
+ {
2080
+ src: nativeMedia.url,
2081
+ alt: nativeMedia.name,
2082
+ style: { maxWidth: "100%", borderRadius: 12, border: "1px solid var(--line)", cursor: "pointer" },
2083
+ onClick: () => {
2084
+ try {
2085
+ const w = window.open();
2086
+ if (w)
2087
+ w.document.write('<img src="' + nativeMedia.url + '" style="max-width:100%">');
2088
+ } catch (e) {
2089
+ }
2090
+ }
2091
+ }
2092
+ ) : 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: {
1938
2093
  padding: "8px 12px",
1939
2094
  borderRadius: 12,
1940
2095
  borderBottomRightRadius: mine ? 4 : 12,
@@ -1950,7 +2105,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onC
1950
2105
  lineHeight: 1.4,
1951
2106
  letterSpacing: -0.1,
1952
2107
  wordBreak: "break-word"
1953
- } }, /* @__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(
2108
+ } }, /* @__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(
1954
2109
  "span",
1955
2110
  {
1956
2111
  title: m.via === "offline" ? "delivered via express relay (offline)" : "delivered over a live session (online)",
@@ -1981,6 +2136,7 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onC
1981
2136
  }
1982
2137
  function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
1983
2138
  const scrollRef = React.useRef(null);
2139
+ const isGroup = React.useMemo(() => dkThreadIsGroup(threadProp), [threadProp]);
1984
2140
  const followScrollRef = React.useRef(true);
1985
2141
  const fileRef = React.useRef(null);
1986
2142
  const rtcFileRef = React.useRef(null);
@@ -2233,7 +2389,7 @@ function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, o
2233
2389
  onDrop
2234
2390
  },
2235
2391
  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"),
2236
- /* @__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), 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(
2392
+ /* @__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(
2237
2393
  Mono,
2238
2394
  {
2239
2395
  size: 11.5,
@@ -2283,6 +2439,7 @@ ${peer.address}`
2283
2439
  m,
2284
2440
  peer,
2285
2441
  T,
2442
+ isGroup,
2286
2443
  onTheater: setTheater,
2287
2444
  onDelete: (id) => doDelete([id]),
2288
2445
  onCancel: doCancel,
@@ -3493,9 +3650,11 @@ const STR = {
3493
3650
  pickPeer: "Select a peer to open the conversation",
3494
3651
  queued: "queued",
3495
3652
  sendingMsg: "sending",
3653
+ groupTag: "group",
3496
3654
  open: "open",
3497
3655
  retry: "Retry",
3498
3656
  cancel: "Cancel",
3657
+ 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.",
3499
3658
  pendingFriend: "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.",
3500
3659
  cancelling: "Cancelling\u2026",
3501
3660
  cancelled: "Transfer cancelled",
@@ -3589,9 +3748,11 @@ const STR = {
3589
3748
  pickPeer: "\u9009\u62E9\u4E00\u4F4D\u597D\u53CB\u5F00\u59CB\u4F1A\u8BDD",
3590
3749
  queued: "\u5F85\u53D1\u9001",
3591
3750
  sendingMsg: "\u53D1\u9001\u4E2D",
3751
+ groupTag: "\u7FA4",
3592
3752
  open: "\u6253\u5F00",
3593
3753
  retry: "\u91CD\u8BD5",
3594
3754
  cancel: "\u53D6\u6D88",
3755
+ groupSenderUnknown: "\u7FA4\u670D\u52A1\u53EA\u8F6C\u53D1\u4E86\u663E\u793A\u540D\uFF0C\u62FF\u4E0D\u5230\u8BE5\u6210\u5458\u7684\u5B8C\u6574\u8EAB\u4EFD\u3002\u5728\u7FA4\u91CC\u6267\u884C /agent add <beagle \u5730\u5740> \u628A Beagle \u6CE8\u518C\u4E3A\u7FA4 agent \u540E\uFF0C\u5373\u53EF\u6536\u5230\u5B8C\u6574\u8EAB\u4EFD\u5E76\u4E00\u952E\u52A0\u597D\u53CB\u3002",
3595
3756
  pendingFriend: "\u7B49\u5F85\u5BF9\u65B9\u63A5\u53D7\u597D\u53CB\u8BF7\u6C42 \u2014 \u6D88\u606F\u4F1A\u5148\u6392\u961F\uFF0C\u5BF9\u65B9\u63A5\u53D7\u540E\u81EA\u52A8\u9001\u8FBE\u3002",
3596
3757
  cancelling: "\u6B63\u5728\u53D6\u6D88\u2026",
3597
3758
  cancelled: "\u5DF2\u53D6\u6D88\u4F20\u8F93",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
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",