@decentnetwork/beagle 0.1.113 → 0.1.114

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 +180 -19
  2. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.113";
1
+ window.__DK_UI_VERSION="0.1.114";
2
2
  "use strict";
3
3
  (() => {
4
4
  // packages/beagle-ui/dist/beagle-ui.js
@@ -114,6 +114,8 @@ window.__DK_UI_VERSION="0.1.113";
114
114
  plug: '<path d="M9 2v6M15 2v6"/><path d="M7 8h10v3a5 5 0 0 1-10 0V8Z"/><path d="M12 16v6"/>',
115
115
  // A form with a filled-in field: the glyph for "Fill with Beagle" and any
116
116
  // other host app that puts your identity into someone else's page.
117
+ // A pane pinned to the right edge — "keep this beside the page".
118
+ dock: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M14 4v16"/><path d="M17 9.5h1.5M17 13h1.5"/>',
117
119
  form: '<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h5"/><path d="M8 16h3"/><path d="m14.5 17.5 1.6 1.6 3.4-3.6"/>'
118
120
  });
119
121
  var H = {};
@@ -126,12 +128,16 @@ window.__DK_UI_VERSION="0.1.113";
126
128
  var hostHideApps = null;
127
129
  var hostApps = null;
128
130
  var hostProfileSections = null;
131
+ var hostHeaderActions = null;
132
+ var hostIsProtocolMessage = null;
129
133
  function setUiHost(host2) {
130
134
  H = host2 || {};
131
135
  hostTabs = Array.isArray(H.hostTabs) ? H.hostTabs : null;
132
136
  hostHideApps = Array.isArray(H.hideApps) ? H.hideApps : null;
133
137
  hostApps = Array.isArray(H.hostApps) ? H.hostApps : null;
134
138
  hostProfileSections = Array.isArray(H.hostProfileSections) ? H.hostProfileSections : null;
139
+ hostHeaderActions = Array.isArray(H.hostHeaderActions) ? H.hostHeaderActions : null;
140
+ hostIsProtocolMessage = typeof H.isProtocolMessage === "function" ? H.isProtocolMessage : null;
135
141
  hostNeedsWelcome = !!H.hostNeedsWelcome;
136
142
  hostPrefersRtcFile = !!H.hostPrefersRtcFile;
137
143
  hostHasKeyBackup = !!H.hostHasKeyBackup;
@@ -1487,6 +1493,98 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1487
1493
  }, [append, lang, onReloadFriends]);
1488
1494
  return { thread, handle, removeFriends, adoptServicePeers };
1489
1495
  }
1496
+ function protocolTest(rule) {
1497
+ if (typeof rule !== "function")
1498
+ return () => false;
1499
+ return (msg) => {
1500
+ if (!msg)
1501
+ return false;
1502
+ try {
1503
+ const v = rule(msg);
1504
+ return typeof v === "string" ? v || true : !!v;
1505
+ } catch (e) {
1506
+ return false;
1507
+ }
1508
+ };
1509
+ }
1510
+ function stripOwnPrefix(text) {
1511
+ return String(text == null ? "" : text).replace(/^you:\s*/i, "");
1512
+ }
1513
+ function lastMessageOf(peer) {
1514
+ const raw = String(peer && peer.lastMsg || "");
1515
+ return {
1516
+ text: stripOwnPrefix(raw),
1517
+ dir: peer && peer.lastDir || (/^you:\s*/i.test(raw) ? "out" : "in"),
1518
+ ts: peer && peer.lastTs || 0
1519
+ };
1520
+ }
1521
+ function lastIsProtocol(peer, isProto) {
1522
+ const last = lastMessageOf(peer);
1523
+ if (!last.text)
1524
+ return false;
1525
+ return isProto(last);
1526
+ }
1527
+ function rosterRow(peer, isProto) {
1528
+ const hidden = lastIsProtocol(peer, isProto);
1529
+ return {
1530
+ preview: hidden ? "" : peer && peer.lastMsg || "",
1531
+ lastTime: hidden ? "" : peer && peer.lastTime || "",
1532
+ unread: hidden ? 0 : Number(peer && peer.unread || 0),
1533
+ activityTs: hidden ? 0 : Number(peer && peer.lastTs || 0),
1534
+ hidden
1535
+ };
1536
+ }
1537
+ function rosterUnreadTotal(peers, isProto) {
1538
+ return (peers || []).reduce((n, p) => n + rosterRow(p, isProto).unread, 0);
1539
+ }
1540
+ function collapseProtocol(thread, isProto, lang) {
1541
+ const out = [];
1542
+ let run = null;
1543
+ const flush = () => {
1544
+ if (!run)
1545
+ return;
1546
+ out.push({
1547
+ protocol: true,
1548
+ id: `proto-${run.first.id || run.startIndex}`,
1549
+ count: run.items.length,
1550
+ label: run.label,
1551
+ dir: run.first.from === "me" ? "out" : "in",
1552
+ text: run.items.map((m) => m.text).join("\n"),
1553
+ line: protocolLine(run, lang)
1554
+ });
1555
+ run = null;
1556
+ };
1557
+ (thread || []).forEach((m, i) => {
1558
+ const hit = m && !m.day ? isProto({ text: m.text, dir: m && m.from === "me" ? "out" : "in", ts: m && m.ts }) : false;
1559
+ if (!hit || m && m.file) {
1560
+ flush();
1561
+ out.push(m);
1562
+ return;
1563
+ }
1564
+ if (!run)
1565
+ run = { first: m, startIndex: i, items: [], label: typeof hit === "string" ? hit : "" };
1566
+ if (!run.label && typeof hit === "string")
1567
+ run.label = hit;
1568
+ run.items.push(m);
1569
+ });
1570
+ flush();
1571
+ return out;
1572
+ }
1573
+ function protocolLine(run, lang) {
1574
+ const arrow = run.first.from === "me" ? "\u2192" : "\u2190";
1575
+ const n = run.items.length;
1576
+ if (run.label) {
1577
+ return `\u2699 ${arrow} ${run.label}${n > 1 ? ` \xB7 ${n}` : ""}`;
1578
+ }
1579
+ if (n > 1) {
1580
+ return lang === "zh" ? `\u2699 ${arrow} ${n} \u6761\u534F\u8BAE\u6D88\u606F` : `\u2699 ${arrow} ${n} protocol messages`;
1581
+ }
1582
+ return `\u2699 ${arrow} ${protocolSummary(run.first.text)}`;
1583
+ }
1584
+ function protocolSummary(text, max = 72) {
1585
+ const s = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
1586
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1587
+ }
1490
1588
  var DK_CMD_HIST_KEY = "beagle.assistant.history";
1491
1589
  var DK_CMD_HIST_MAX = 50;
1492
1590
  function RequestsBlock({ T, requests, onAct }) {
@@ -1547,8 +1645,9 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1547
1645
  paddingLeft: 8
1548
1646
  } }, r.descr), failed[r.id] && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--danger)" } }, failed[r.id]), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => act(r, "accept"), style: { flex: 1 } }, T.accept), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "x", size: "sm", onClick: () => act(r, "reject"), style: { flex: 1 } }, T.reject))))));
1549
1647
  }
1550
- function PeerRow({ peer, T, active, onClick, muted }) {
1648
+ function PeerRow({ peer, T, active, onClick, muted, isProtocol }) {
1551
1649
  const name = peer.alias || peer.userId;
1650
+ const row = rosterRow(peer, isProtocol || (() => false));
1552
1651
  return /* @__PURE__ */ React.createElement("button", { onClick, style: {
1553
1652
  display: "flex",
1554
1653
  alignItems: "center",
@@ -1577,12 +1676,13 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1577
1676
  textOverflow: "ellipsis",
1578
1677
  flex: 1,
1579
1678
  minWidth: 0
1580
- } }, dkRowPreview(peer.lastMsg)))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, peer.lastTime), peer.unread ? /* @__PURE__ */ React.createElement(Unread, { n: peer.unread }) : peer.pending ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--warn, #d29922)", title: "friend request pending" }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
1679
+ } }, dkRowPreview(row.preview)))), /* @__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)" } }, row.lastTime), row.unread ? /* @__PURE__ */ React.createElement(Unread, { n: row.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 })));
1581
1680
  }
1582
- function dkActivityTs(p) {
1583
- return Math.max(p.lastTs || 0, p.acceptedAt || 0, p.requestedAt || 0);
1681
+ function dkActivityTs(p, isProtocol) {
1682
+ const row = rosterRow(p, isProtocol || (() => false));
1683
+ return Math.max(row.activityTs, p.acceptedAt || 0, p.requestedAt || 0);
1584
1684
  }
1585
- function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide }) {
1685
+ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide, isProtocol }) {
1586
1686
  const [q, setQ] = React.useState("");
1587
1687
  const [addr, setAddr] = React.useState("");
1588
1688
  const [addState, setAddState] = React.useState(null);
@@ -1627,7 +1727,7 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1627
1727
  }).slice().sort((a, b) => {
1628
1728
  if (!!a.pinned !== !!b.pinned)
1629
1729
  return a.pinned ? -1 : 1;
1630
- const ta = dkActivityTs(a), tb = dkActivityTs(b);
1730
+ const ta = dkActivityTs(a, isProtocol), tb = dkActivityTs(b, isProtocol);
1631
1731
  if (ta !== tb)
1632
1732
  return tb - ta;
1633
1733
  return String(a.alias || a.userId || "").localeCompare(String(b.alias || b.userId || ""));
@@ -1666,7 +1766,7 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1666
1766
  fontSize: 11.5,
1667
1767
  lineHeight: 1.35,
1668
1768
  color: addState.kind === "err" ? "var(--bad, #e5484d)" : addState.kind === "ok" ? "var(--good, #46a758)" : "var(--faint)"
1669
- } }, addState.msg), /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "search", size: 15, color: "var(--faint)", stroke: 2, style: { position: "absolute", left: 9 } }), /* @__PURE__ */ React.createElement("input", { value: q, onChange: (e) => setQ(e.target.value), placeholder: T.search, style: { ...inputStyle, paddingLeft: 30 } }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", padding: "10px 8px 16px" } }, /* @__PURE__ */ React.createElement(RequestsBlock, { T, requests, onAct }), /* @__PURE__ */ React.createElement(Section, { label: T.peers, count: `${online}/${peers.length}`, style: { margin: "6px 4px 8px" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((p) => /* @__PURE__ */ React.createElement(PeerRow, { key: p.id, peer: p, T, active: p.id === activeId, onClick: () => onSelect(p.id), muted: !!muted && muted.includes(p.id) })), !filtered.length && /* @__PURE__ */ React.createElement("div", { style: { padding: 16, textAlign: "center", fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "no matches"))));
1769
+ } }, addState.msg), /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "search", size: 15, color: "var(--faint)", stroke: 2, style: { position: "absolute", left: 9 } }), /* @__PURE__ */ React.createElement("input", { value: q, onChange: (e) => setQ(e.target.value), placeholder: T.search, style: { ...inputStyle, paddingLeft: 30 } }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", padding: "10px 8px 16px" } }, /* @__PURE__ */ React.createElement(RequestsBlock, { T, requests, onAct }), /* @__PURE__ */ React.createElement(Section, { label: T.peers, count: `${online}/${peers.length}`, style: { margin: "6px 4px 8px" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((p) => /* @__PURE__ */ React.createElement(PeerRow, { key: p.id, peer: p, T, active: p.id === activeId, onClick: () => onSelect(p.id), muted: !!muted && muted.includes(p.id), isProtocol })), !filtered.length && /* @__PURE__ */ React.createElement("div", { style: { padding: 16, textAlign: "center", fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "no matches"))));
1670
1770
  }
1671
1771
  var inputStyle = {
1672
1772
  flex: 1,
@@ -2666,7 +2766,7 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
2666
2766
  ) : mine && m.via === "offline" && m.status !== "queued" && m.status !== "sending" ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "#f59e0b", title: T.viaOutOffline || "posted to the offline relay \u2014 they get it when they come online" }) : mine && m.status === "sent" && m.confirmed === false ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "var(--faint)", title: T.sentUnconfirmed || "sent \u2014 not confirmed by the peer" }) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
2667
2767
  );
2668
2768
  }
2669
- function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted, onToggleMute, phone, onBack }) {
2769
+ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, isProtocol, muted, onToggleMute, phone, onBack }) {
2670
2770
  const scrollRef = React.useRef(null);
2671
2771
  const prependRef = React.useRef(null);
2672
2772
  const [earlierBusy, setEarlierBusy] = React.useState(false);
@@ -3088,7 +3188,15 @@ ${peer.address}`
3088
3188
  fontSize: 12.5,
3089
3189
  color: "var(--text)"
3090
3190
  } }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 13, stroke: 2.2, color: "#d29922" }), /* @__PURE__ */ React.createElement("span", null, T.pendingFriend || "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.")),
3091
- /* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: phone ? "12px 10px" : "18px 22px", WebkitOverflowScrolling: "touch" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, margin: "0 auto" } }, hasEarlier && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "center", margin: "4px 0 10px" } }, /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: loadEarlier, disabled: earlierBusy }, earlierBusy ? T.loadingEarlier || "Loading\u2026" : T.loadEarlier || "Load earlier messages")), thread.filter((m) => m.day || !hidden.has(m.id)).map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(
3191
+ /* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: phone ? "12px 10px" : "18px 22px", WebkitOverflowScrolling: "touch" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1200, margin: "0 auto" } }, hasEarlier && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "center", margin: "4px 0 10px" } }, /* @__PURE__ */ React.createElement(Btn, { size: "sm", onClick: loadEarlier, disabled: earlierBusy }, earlierBusy ? T.loadingEarlier || "Loading\u2026" : T.loadEarlier || "Load earlier messages")), collapseProtocol(thread, isProtocol || (() => false), lang).filter((m) => m.day || m.protocol || !hidden.has(m.id)).map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : m.protocol ? /* @__PURE__ */ React.createElement("div", { key: m.id || i, style: { display: "flex", justifyContent: "center", margin: "4px 0" } }, /* @__PURE__ */ React.createElement("span", { title: m.text, style: {
3192
+ fontFamily: "var(--mono)",
3193
+ fontSize: 10.5,
3194
+ color: "var(--faint)",
3195
+ maxWidth: "86%",
3196
+ whiteSpace: "nowrap",
3197
+ overflow: "hidden",
3198
+ textOverflow: "ellipsis"
3199
+ } }, m.line)) : /* @__PURE__ */ React.createElement(
3092
3200
  Msg,
3093
3201
  {
3094
3202
  key: m.id || i,
@@ -3259,12 +3367,12 @@ ${peer.address}`
3259
3367
  function ChatEmpty({ T }) {
3260
3368
  return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, color: "var(--faint)", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "message", size: 40, stroke: 1.4, color: "var(--line)" }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, T.pickPeer));
3261
3369
  }
3262
- function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
3370
+ function ChatTab({ T, lang, peers, requests, activeId, thread, isProtocol, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
3263
3371
  const peer = peers.find((p) => p.id === activeId);
3264
3372
  if (phone) {
3265
- return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide: true });
3373
+ return peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, isProtocol, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted: !!muted && muted.includes(peer.id), onToggleMute, phone: true, onBack }) : /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, isProtocol, wide: true });
3266
3374
  }
3267
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
3375
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, isProtocol }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, isProtocol, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
3268
3376
  }
3269
3377
  Object.assign(window, { ChatTab });
3270
3378
  function CopyBtn({ value, copiedText = "Copied", copyFailedText = "Copy failed", copyTitle = "Copy" }) {
@@ -5888,7 +5996,7 @@ ${peer.address}`
5888
5996
  function dkLooking() {
5889
5997
  return document.visibilityState === "visible" && document.hasFocus();
5890
5998
  }
5891
- function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted }) {
5999
+ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted, isProtocol }) {
5892
6000
  const prev = React.useRef(null);
5893
6001
  const mutedRef = React.useRef(/* @__PURE__ */ new Set());
5894
6002
  mutedRef.current = new Set(muted || []);
@@ -5918,7 +6026,8 @@ ${peer.address}`
5918
6026
  const byId = /* @__PURE__ */ new Map();
5919
6027
  for (const p of peers || []) {
5920
6028
  const dir = p.lastDir || (String(p.lastMsg || "").startsWith("you: ") ? "out" : "in");
5921
- byId.set(p.id, { ts: p.lastTs || 0, dir });
6029
+ const quiet = typeof isProtocol === "function" && lastIsProtocol(p, isProtocol);
6030
+ byId.set(p.id, { ts: p.lastTs || 0, dir, quiet });
5922
6031
  }
5923
6032
  const reqIds = new Set((requests || []).map((r) => r.id));
5924
6033
  const was = prev.current;
@@ -5929,6 +6038,8 @@ ${peer.address}`
5929
6038
  for (const [id, cur] of byId) {
5930
6039
  if (cur.dir !== "in" || !cur.ts)
5931
6040
  continue;
6041
+ if (cur.quiet)
6042
+ continue;
5932
6043
  const old = was.byId.get(id);
5933
6044
  if (old ? cur.ts <= old.ts : Date.now() - cur.ts > 3e4)
5934
6045
  continue;
@@ -6772,6 +6883,7 @@ ${peer.address}`
6772
6883
  const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
6773
6884
  const mutedIds = Array.isArray(t.alertMuted) ? t.alertMuted : [];
6774
6885
  const onToggleMute = (id) => setTweak("alertMuted", mutedIds.includes(id) ? mutedIds.filter((x) => x !== id) : mutedIds.concat(id));
6886
+ const isProtocol = React.useMemo(() => protocolTest(hostIsProtocolMessage), []);
6775
6887
  useIncomingAlerts({
6776
6888
  T,
6777
6889
  peers,
@@ -6780,7 +6892,8 @@ ${peer.address}`
6780
6892
  activeId,
6781
6893
  sound: t.alertSound !== false,
6782
6894
  flash: t.alertFlash !== false,
6783
- muted: mutedIds
6895
+ muted: mutedIds,
6896
+ isProtocol
6784
6897
  });
6785
6898
  const assistant = useAssistant({
6786
6899
  lang: t.lang,
@@ -6814,6 +6927,54 @@ ${peer.address}`
6814
6927
  });
6815
6928
  const phone = useIsPhone();
6816
6929
  const [sharing, setSharing] = React.useState(false);
6930
+ const headerActions = (hostHeaderActions || []).filter((a) => a && a.id && typeof a.onClick === "function");
6931
+ const headerActionBtns = headerActions.map((a) => {
6932
+ let on = false;
6933
+ try {
6934
+ on = typeof a.active === "function" ? !!a.active() : false;
6935
+ } catch (e) {
6936
+ on = false;
6937
+ }
6938
+ const label = pickText(a.label, t.lang, a.id);
6939
+ return /* @__PURE__ */ React.createElement(
6940
+ "button",
6941
+ {
6942
+ key: a.id,
6943
+ title: label,
6944
+ "aria-label": label,
6945
+ "aria-pressed": on,
6946
+ onClick: () => {
6947
+ try {
6948
+ a.onClick({ T, t, me, activeId, phone });
6949
+ } catch (e) {
6950
+ }
6951
+ },
6952
+ style: {
6953
+ width: 32,
6954
+ height: 32,
6955
+ flexShrink: 0,
6956
+ borderRadius: 8,
6957
+ cursor: "pointer",
6958
+ padding: 0,
6959
+ border: "1px solid " + (on ? "var(--accent)" : "var(--line)"),
6960
+ background: on ? "color-mix(in oklab, var(--accent), transparent 86%)" : "transparent",
6961
+ display: "flex",
6962
+ alignItems: "center",
6963
+ justifyContent: "center"
6964
+ }
6965
+ },
6966
+ /* @__PURE__ */ React.createElement(
6967
+ Icon,
6968
+ {
6969
+ name: typeof a.icon === "string" ? a.icon : void 0,
6970
+ svgPath: a.icon && typeof a.icon === "object" ? a.icon.svgPath : void 0,
6971
+ size: 16,
6972
+ stroke: 2,
6973
+ color: on ? "var(--accent)" : "var(--dim)"
6974
+ }
6975
+ )
6976
+ );
6977
+ });
6817
6978
  const shareBtn = /* @__PURE__ */ React.createElement(
6818
6979
  "button",
6819
6980
  {
@@ -6984,7 +7145,7 @@ ${peer.address}`
6984
7145
  const lastPeople = React.useRef("here");
6985
7146
  if (peopleTab)
6986
7147
  lastPeople.current = tab;
6987
- const unreadTotal = (peers || []).reduce((n, p) => n + (Number(p.unread) || 0), 0) + (requests || []).length;
7148
+ const unreadTotal = rosterUnreadTotal(peers, isProtocol) + (requests || []).length;
6988
7149
  const NAV_FOR = {
6989
7150
  chat: { icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
6990
7151
  people: { icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
@@ -7028,7 +7189,7 @@ ${peer.address}`
7028
7189
  display: "flex",
7029
7190
  border: "1px solid " + (tab === "profile" ? "var(--line)" : "transparent"),
7030
7191
  background: tab === "profile" ? "var(--panel-2)" : "transparent"
7031
- } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, !inConversation && /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), !inConversation && /* @__PURE__ */ React.createElement("div", { style: { height: 46, boxSizing: "content-box", paddingTop: phone ? "env(safe-area-inset-top, 0px)" : 0, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: phone ? 8 : 12, paddingLeft: phone ? 12 : 16, paddingRight: phone ? 12 : 16 } }, /* @__PURE__ */ React.createElement(Brand, { T, style: { gap: 12, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle")), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", whiteSpace: "nowrap" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), phone ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), shareBtn, /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: { padding: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 28, radius: 7, dot: false }))) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), shareBtn, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name)))), phone && peopleTab && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", gap: 6, padding: "8px 12px", borderBottom: "1px solid var(--line)", background: "var(--panel)", overflowX: "auto" } }, nav.filter((n) => DK_PEOPLE_TABS.includes(n.id)).map((n) => /* @__PURE__ */ React.createElement("button", { key: n.id, onClick: () => setTab(n.id), style: {
7192
+ } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, !inConversation && /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), !inConversation && /* @__PURE__ */ React.createElement("div", { style: { height: 46, boxSizing: "content-box", paddingTop: phone ? "env(safe-area-inset-top, 0px)" : 0, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: phone ? 8 : 12, paddingLeft: phone ? 12 : 16, paddingRight: phone ? 12 : 16 } }, /* @__PURE__ */ React.createElement(Brand, { T, style: { gap: 12, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle")), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)", whiteSpace: "nowrap" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), phone ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), headerActionBtns, shareBtn, /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: { padding: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 28, radius: 7, dot: false }))) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), headerActionBtns, shareBtn, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name)))), phone && peopleTab && /* @__PURE__ */ React.createElement("div", { style: { flexShrink: 0, display: "flex", gap: 6, padding: "8px 12px", borderBottom: "1px solid var(--line)", background: "var(--panel)", overflowX: "auto" } }, nav.filter((n) => DK_PEOPLE_TABS.includes(n.id)).map((n) => /* @__PURE__ */ React.createElement("button", { key: n.id, onClick: () => setTab(n.id), style: {
7032
7193
  display: "inline-flex",
7033
7194
  alignItems: "center",
7034
7195
  gap: 6,
@@ -7042,7 +7203,7 @@ ${peer.address}`
7042
7203
  fontWeight: 600,
7043
7204
  background: tab === n.id ? "var(--accent)" : "transparent",
7044
7205
  color: tab === n.id ? "#fff" : "var(--dim)"
7045
- } }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, onBack: closeConversation, peers, requests, activeId, thread: activeId === ASSISTANT_ID ? assistant.thread : data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), hasEarlier: !!(activeId && activeId !== ASSISTANT_ID && data.earlier[activeId]), onAssistantRemove: activeId === ASSISTANT_ID ? assistant.removeFriends : null, onAssistantAdopt: activeId === ASSISTANT_ID ? assistant.adoptServicePeers : null, onLoadEarlier: () => activeId && data.loadEarlier(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t, me, peers, phone, setTab }), tabs.map((x) => x.kind === "host" && tab === x.id ? /* @__PURE__ */ React.createElement(HostTabPane, { key: x.id, tab: x, props: { T, t, me, peers, phone, setTab }, lang: t.lang }) : null), tab === "profile" && /* @__PURE__ */ React.createElement(
7206
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, isProtocol, onBack: closeConversation, peers, requests, activeId, thread: activeId === ASSISTANT_ID ? assistant.thread : data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), hasEarlier: !!(activeId && activeId !== ASSISTANT_ID && data.earlier[activeId]), onAssistantRemove: activeId === ASSISTANT_ID ? assistant.removeFriends : null, onAssistantAdopt: activeId === ASSISTANT_ID ? assistant.adoptServicePeers : null, onLoadEarlier: () => activeId && data.loadEarlier(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t, me, peers, phone, setTab }), tabs.map((x) => x.kind === "host" && tab === x.id ? /* @__PURE__ */ React.createElement(HostTabPane, { key: x.id, tab: x, props: { T, t, me, peers, phone, setTab }, lang: t.lang }) : null), tab === "profile" && /* @__PURE__ */ React.createElement(
7046
7207
  ProfileTab,
7047
7208
  {
7048
7209
  T,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.113",
3
+ "version": "0.1.114",
4
4
  "description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,7 +35,7 @@
35
35
  "yargs": "^17.7.2"
36
36
  },
37
37
  "devDependencies": {
38
- "@decentnetwork/beagle-ui": "^0.2.27",
38
+ "@decentnetwork/beagle-ui": "^0.2.28",
39
39
  "@types/js-yaml": "^4.0.9",
40
40
  "@types/node": "^20.11.0",
41
41
  "@types/yargs": "^17.0.32",