@decentnetwork/beagle-ui 0.2.27 → 0.2.28

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/beagle-ui.js +182 -19
  2. package/package.json +1 -1
package/dist/beagle-ui.js CHANGED
@@ -1,4 +1,4 @@
1
- export const BEAGLE_UI_VERSION = "0.2.27";
1
+ export const BEAGLE_UI_VERSION = "0.2.28";
2
2
  // src/icons.jsx
3
3
  var ICON_PATHS = {
4
4
  // ---- tab bar (the four must feel like one set) ----
@@ -114,6 +114,8 @@ Object.assign(ICON_PATHS, {
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
 
@@ -128,12 +130,16 @@ var hostTabs = null;
128
130
  var hostHideApps = null;
129
131
  var hostApps = null;
130
132
  var hostProfileSections = null;
133
+ var hostHeaderActions = null;
134
+ var hostIsProtocolMessage = null;
131
135
  function setUiHost(host) {
132
136
  H = host || {};
133
137
  hostTabs = Array.isArray(H.hostTabs) ? H.hostTabs : null;
134
138
  hostHideApps = Array.isArray(H.hideApps) ? H.hideApps : null;
135
139
  hostApps = Array.isArray(H.hostApps) ? H.hostApps : null;
136
140
  hostProfileSections = Array.isArray(H.hostProfileSections) ? H.hostProfileSections : null;
141
+ hostHeaderActions = Array.isArray(H.hostHeaderActions) ? H.hostHeaderActions : null;
142
+ hostIsProtocolMessage = typeof H.isProtocolMessage === "function" ? H.isProtocolMessage : null;
137
143
  hostNeedsWelcome = !!H.hostNeedsWelcome;
138
144
  hostPrefersRtcFile = !!H.hostPrefersRtcFile;
139
145
  hostHasKeyBackup = !!H.hostHasKeyBackup;
@@ -1500,6 +1506,100 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1500
1506
  return { thread, handle, removeFriends, adoptServicePeers };
1501
1507
  }
1502
1508
 
1509
+ // src/host-messages.js
1510
+ function protocolTest(rule) {
1511
+ if (typeof rule !== "function")
1512
+ return () => false;
1513
+ return (msg) => {
1514
+ if (!msg)
1515
+ return false;
1516
+ try {
1517
+ const v = rule(msg);
1518
+ return typeof v === "string" ? v || true : !!v;
1519
+ } catch (e) {
1520
+ return false;
1521
+ }
1522
+ };
1523
+ }
1524
+ function stripOwnPrefix(text) {
1525
+ return String(text == null ? "" : text).replace(/^you:\s*/i, "");
1526
+ }
1527
+ function lastMessageOf(peer) {
1528
+ const raw = String(peer && peer.lastMsg || "");
1529
+ return {
1530
+ text: stripOwnPrefix(raw),
1531
+ dir: peer && peer.lastDir || (/^you:\s*/i.test(raw) ? "out" : "in"),
1532
+ ts: peer && peer.lastTs || 0
1533
+ };
1534
+ }
1535
+ function lastIsProtocol(peer, isProto) {
1536
+ const last = lastMessageOf(peer);
1537
+ if (!last.text)
1538
+ return false;
1539
+ return isProto(last);
1540
+ }
1541
+ function rosterRow(peer, isProto) {
1542
+ const hidden = lastIsProtocol(peer, isProto);
1543
+ return {
1544
+ preview: hidden ? "" : peer && peer.lastMsg || "",
1545
+ lastTime: hidden ? "" : peer && peer.lastTime || "",
1546
+ unread: hidden ? 0 : Number(peer && peer.unread || 0),
1547
+ activityTs: hidden ? 0 : Number(peer && peer.lastTs || 0),
1548
+ hidden
1549
+ };
1550
+ }
1551
+ function rosterUnreadTotal(peers, isProto) {
1552
+ return (peers || []).reduce((n, p) => n + rosterRow(p, isProto).unread, 0);
1553
+ }
1554
+ function collapseProtocol(thread, isProto, lang) {
1555
+ const out = [];
1556
+ let run = null;
1557
+ const flush = () => {
1558
+ if (!run)
1559
+ return;
1560
+ out.push({
1561
+ protocol: true,
1562
+ id: `proto-${run.first.id || run.startIndex}`,
1563
+ count: run.items.length,
1564
+ label: run.label,
1565
+ dir: run.first.from === "me" ? "out" : "in",
1566
+ text: run.items.map((m) => m.text).join("\n"),
1567
+ line: protocolLine(run, lang)
1568
+ });
1569
+ run = null;
1570
+ };
1571
+ (thread || []).forEach((m, i) => {
1572
+ const hit = m && !m.day ? isProto({ text: m.text, dir: m && m.from === "me" ? "out" : "in", ts: m && m.ts }) : false;
1573
+ if (!hit || m && m.file) {
1574
+ flush();
1575
+ out.push(m);
1576
+ return;
1577
+ }
1578
+ if (!run)
1579
+ run = { first: m, startIndex: i, items: [], label: typeof hit === "string" ? hit : "" };
1580
+ if (!run.label && typeof hit === "string")
1581
+ run.label = hit;
1582
+ run.items.push(m);
1583
+ });
1584
+ flush();
1585
+ return out;
1586
+ }
1587
+ function protocolLine(run, lang) {
1588
+ const arrow = run.first.from === "me" ? "\u2192" : "\u2190";
1589
+ const n = run.items.length;
1590
+ if (run.label) {
1591
+ return `\u2699 ${arrow} ${run.label}${n > 1 ? ` \xB7 ${n}` : ""}`;
1592
+ }
1593
+ if (n > 1) {
1594
+ return lang === "zh" ? `\u2699 ${arrow} ${n} \u6761\u534F\u8BAE\u6D88\u606F` : `\u2699 ${arrow} ${n} protocol messages`;
1595
+ }
1596
+ return `\u2699 ${arrow} ${protocolSummary(run.first.text)}`;
1597
+ }
1598
+ function protocolSummary(text, max = 72) {
1599
+ const s = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
1600
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1601
+ }
1602
+
1503
1603
  // src/chat.jsx
1504
1604
  var DK_CMD_HIST_KEY = "beagle.assistant.history";
1505
1605
  var DK_CMD_HIST_MAX = 50;
@@ -1561,8 +1661,9 @@ function RequestsBlock({ T, requests, onAct }) {
1561
1661
  paddingLeft: 8
1562
1662
  } }, 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))))));
1563
1663
  }
1564
- function PeerRow({ peer, T, active, onClick, muted }) {
1664
+ function PeerRow({ peer, T, active, onClick, muted, isProtocol }) {
1565
1665
  const name = peer.alias || peer.userId;
1666
+ const row = rosterRow(peer, isProtocol || (() => false));
1566
1667
  return /* @__PURE__ */ React.createElement("button", { onClick, style: {
1567
1668
  display: "flex",
1568
1669
  alignItems: "center",
@@ -1591,12 +1692,13 @@ function PeerRow({ peer, T, active, onClick, muted }) {
1591
1692
  textOverflow: "ellipsis",
1592
1693
  flex: 1,
1593
1694
  minWidth: 0
1594
- } }, 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 })));
1695
+ } }, 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 })));
1595
1696
  }
1596
- function dkActivityTs(p) {
1597
- return Math.max(p.lastTs || 0, p.acceptedAt || 0, p.requestedAt || 0);
1697
+ function dkActivityTs(p, isProtocol) {
1698
+ const row = rosterRow(p, isProtocol || (() => false));
1699
+ return Math.max(row.activityTs, p.acceptedAt || 0, p.requestedAt || 0);
1598
1700
  }
1599
- function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide }) {
1701
+ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide, isProtocol }) {
1600
1702
  const [q, setQ] = React.useState("");
1601
1703
  const [addr, setAddr] = React.useState("");
1602
1704
  const [addState, setAddState] = React.useState(null);
@@ -1641,7 +1743,7 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, pre
1641
1743
  }).slice().sort((a, b) => {
1642
1744
  if (!!a.pinned !== !!b.pinned)
1643
1745
  return a.pinned ? -1 : 1;
1644
- const ta = dkActivityTs(a), tb = dkActivityTs(b);
1746
+ const ta = dkActivityTs(a, isProtocol), tb = dkActivityTs(b, isProtocol);
1645
1747
  if (ta !== tb)
1646
1748
  return tb - ta;
1647
1749
  return String(a.alias || a.userId || "").localeCompare(String(b.alias || b.userId || ""));
@@ -1680,7 +1782,7 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, pre
1680
1782
  fontSize: 11.5,
1681
1783
  lineHeight: 1.35,
1682
1784
  color: addState.kind === "err" ? "var(--bad, #e5484d)" : addState.kind === "ok" ? "var(--good, #46a758)" : "var(--faint)"
1683
- } }, 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"))));
1785
+ } }, 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"))));
1684
1786
  }
1685
1787
  var inputStyle = {
1686
1788
  flex: 1,
@@ -2680,7 +2782,7 @@ function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetry
2680
2782
  ) : 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)" })))
2681
2783
  );
2682
2784
  }
2683
- 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 }) {
2785
+ 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 }) {
2684
2786
  const scrollRef = React.useRef(null);
2685
2787
  const prependRef = React.useRef(null);
2686
2788
  const [earlierBusy, setEarlierBusy] = React.useState(false);
@@ -3102,7 +3204,15 @@ ${peer.address}`
3102
3204
  fontSize: 12.5,
3103
3205
  color: "var(--text)"
3104
3206
  } }, /* @__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.")),
3105
- /* @__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(
3207
+ /* @__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: {
3208
+ fontFamily: "var(--mono)",
3209
+ fontSize: 10.5,
3210
+ color: "var(--faint)",
3211
+ maxWidth: "86%",
3212
+ whiteSpace: "nowrap",
3213
+ overflow: "hidden",
3214
+ textOverflow: "ellipsis"
3215
+ } }, m.line)) : /* @__PURE__ */ React.createElement(
3106
3216
  Msg,
3107
3217
  {
3108
3218
  key: m.id || i,
@@ -3273,12 +3383,12 @@ function MenuItem({ icon, label, onClick, danger }) {
3273
3383
  function ChatEmpty({ T }) {
3274
3384
  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));
3275
3385
  }
3276
- 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 }) {
3386
+ 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 }) {
3277
3387
  const peer = peers.find((p) => p.id === activeId);
3278
3388
  if (phone) {
3279
- 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 });
3389
+ 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 });
3280
3390
  }
3281
- 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 }));
3391
+ 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 }));
3282
3392
  }
3283
3393
  Object.assign(window, { ChatTab });
3284
3394
 
@@ -5922,7 +6032,7 @@ function dkFlashStop() {
5922
6032
  function dkLooking() {
5923
6033
  return document.visibilityState === "visible" && document.hasFocus();
5924
6034
  }
5925
- function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted }) {
6035
+ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted, isProtocol }) {
5926
6036
  const prev = React.useRef(null);
5927
6037
  const mutedRef = React.useRef(/* @__PURE__ */ new Set());
5928
6038
  mutedRef.current = new Set(muted || []);
@@ -5952,7 +6062,8 @@ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true,
5952
6062
  const byId = /* @__PURE__ */ new Map();
5953
6063
  for (const p of peers || []) {
5954
6064
  const dir = p.lastDir || (String(p.lastMsg || "").startsWith("you: ") ? "out" : "in");
5955
- byId.set(p.id, { ts: p.lastTs || 0, dir });
6065
+ const quiet = typeof isProtocol === "function" && lastIsProtocol(p, isProtocol);
6066
+ byId.set(p.id, { ts: p.lastTs || 0, dir, quiet });
5956
6067
  }
5957
6068
  const reqIds = new Set((requests || []).map((r) => r.id));
5958
6069
  const was = prev.current;
@@ -5963,6 +6074,8 @@ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true,
5963
6074
  for (const [id, cur] of byId) {
5964
6075
  if (cur.dir !== "in" || !cur.ts)
5965
6076
  continue;
6077
+ if (cur.quiet)
6078
+ continue;
5966
6079
  const old = was.byId.get(id);
5967
6080
  if (old ? cur.ts <= old.ts : Date.now() - cur.ts > 3e4)
5968
6081
  continue;
@@ -6810,6 +6923,7 @@ function DkApp() {
6810
6923
  const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
6811
6924
  const mutedIds = Array.isArray(t.alertMuted) ? t.alertMuted : [];
6812
6925
  const onToggleMute = (id) => setTweak("alertMuted", mutedIds.includes(id) ? mutedIds.filter((x) => x !== id) : mutedIds.concat(id));
6926
+ const isProtocol = React.useMemo(() => protocolTest(hostIsProtocolMessage), []);
6813
6927
  useIncomingAlerts({
6814
6928
  T,
6815
6929
  peers,
@@ -6818,7 +6932,8 @@ function DkApp() {
6818
6932
  activeId,
6819
6933
  sound: t.alertSound !== false,
6820
6934
  flash: t.alertFlash !== false,
6821
- muted: mutedIds
6935
+ muted: mutedIds,
6936
+ isProtocol
6822
6937
  });
6823
6938
  const assistant = useAssistant({
6824
6939
  lang: t.lang,
@@ -6852,6 +6967,54 @@ function DkApp() {
6852
6967
  });
6853
6968
  const phone = useIsPhone();
6854
6969
  const [sharing, setSharing] = React.useState(false);
6970
+ const headerActions = (hostHeaderActions || []).filter((a) => a && a.id && typeof a.onClick === "function");
6971
+ const headerActionBtns = headerActions.map((a) => {
6972
+ let on = false;
6973
+ try {
6974
+ on = typeof a.active === "function" ? !!a.active() : false;
6975
+ } catch (e) {
6976
+ on = false;
6977
+ }
6978
+ const label = pickText(a.label, t.lang, a.id);
6979
+ return /* @__PURE__ */ React.createElement(
6980
+ "button",
6981
+ {
6982
+ key: a.id,
6983
+ title: label,
6984
+ "aria-label": label,
6985
+ "aria-pressed": on,
6986
+ onClick: () => {
6987
+ try {
6988
+ a.onClick({ T, t, me, activeId, phone });
6989
+ } catch (e) {
6990
+ }
6991
+ },
6992
+ style: {
6993
+ width: 32,
6994
+ height: 32,
6995
+ flexShrink: 0,
6996
+ borderRadius: 8,
6997
+ cursor: "pointer",
6998
+ padding: 0,
6999
+ border: "1px solid " + (on ? "var(--accent)" : "var(--line)"),
7000
+ background: on ? "color-mix(in oklab, var(--accent), transparent 86%)" : "transparent",
7001
+ display: "flex",
7002
+ alignItems: "center",
7003
+ justifyContent: "center"
7004
+ }
7005
+ },
7006
+ /* @__PURE__ */ React.createElement(
7007
+ Icon,
7008
+ {
7009
+ name: typeof a.icon === "string" ? a.icon : void 0,
7010
+ svgPath: a.icon && typeof a.icon === "object" ? a.icon.svgPath : void 0,
7011
+ size: 16,
7012
+ stroke: 2,
7013
+ color: on ? "var(--accent)" : "var(--dim)"
7014
+ }
7015
+ )
7016
+ );
7017
+ });
6855
7018
  const shareBtn = /* @__PURE__ */ React.createElement(
6856
7019
  "button",
6857
7020
  {
@@ -7022,7 +7185,7 @@ function DkApp() {
7022
7185
  const lastPeople = React.useRef("here");
7023
7186
  if (peopleTab)
7024
7187
  lastPeople.current = tab;
7025
- const unreadTotal = (peers || []).reduce((n, p) => n + (Number(p.unread) || 0), 0) + (requests || []).length;
7188
+ const unreadTotal = rosterUnreadTotal(peers, isProtocol) + (requests || []).length;
7026
7189
  const NAV_FOR = {
7027
7190
  chat: { icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
7028
7191
  people: { icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
@@ -7066,7 +7229,7 @@ function DkApp() {
7066
7229
  display: "flex",
7067
7230
  border: "1px solid " + (tab === "profile" ? "var(--line)" : "transparent"),
7068
7231
  background: tab === "profile" ? "var(--panel-2)" : "transparent"
7069
- } }, /* @__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: {
7232
+ } }, /* @__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: {
7070
7233
  display: "inline-flex",
7071
7234
  alignItems: "center",
7072
7235
  gap: 6,
@@ -7080,7 +7243,7 @@ function DkApp() {
7080
7243
  fontWeight: 600,
7081
7244
  background: tab === n.id ? "var(--accent)" : "transparent",
7082
7245
  color: tab === n.id ? "#fff" : "var(--dim)"
7083
- } }, /* @__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(
7246
+ } }, /* @__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(
7084
7247
  ProfileTab,
7085
7248
  {
7086
7249
  T,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle-ui",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "The Beagle chat UI, minus the machine it runs on — shared by the desktop install and the browser build.",
5
5
  "license": "MIT",
6
6
  "type": "module",