@decentnetwork/beagle-ui 0.2.27 → 0.2.29

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 +268 -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.29";
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;
@@ -160,6 +166,142 @@ function DkBrowserNotice(props) {
160
166
  return H.DkBrowserNotice ? /* @__PURE__ */ React.createElement(H.DkBrowserNotice, { ...props }) : null;
161
167
  }
162
168
 
169
+ // src/host-messages.js
170
+ function protocolTest(rule) {
171
+ if (typeof rule !== "function")
172
+ return () => false;
173
+ return (msg) => {
174
+ if (!msg)
175
+ return false;
176
+ try {
177
+ const v = rule(msg);
178
+ return typeof v === "string" ? v || true : !!v;
179
+ } catch (e) {
180
+ return false;
181
+ }
182
+ };
183
+ }
184
+ function stripOwnPrefix(text) {
185
+ return String(text == null ? "" : text).replace(/^you:\s*/i, "");
186
+ }
187
+ function lastMessageOf(peer) {
188
+ const raw = String(peer && peer.lastMsg || "");
189
+ return {
190
+ text: stripOwnPrefix(raw),
191
+ dir: peer && peer.lastDir || (/^you:\s*/i.test(raw) ? "out" : "in"),
192
+ ts: peer && peer.lastTs || 0
193
+ };
194
+ }
195
+ function lastIsProtocol(peer, isProto) {
196
+ const last = lastMessageOf(peer);
197
+ if (!last.text)
198
+ return false;
199
+ return isProto(last);
200
+ }
201
+ function newestHumanMessage(msgs, isProto) {
202
+ const arr = msgs || [];
203
+ for (let i = arr.length - 1; i >= 0; i--) {
204
+ const m = arr[i];
205
+ if (!m)
206
+ continue;
207
+ if (m.file)
208
+ return m;
209
+ if (!m.text)
210
+ continue;
211
+ if (isProto({ text: m.text, dir: m.dir, ts: m.ts }))
212
+ continue;
213
+ return m;
214
+ }
215
+ return null;
216
+ }
217
+ function humanUnread(msgs, unread, isProto) {
218
+ const want = Number(unread) || 0;
219
+ if (want <= 0)
220
+ return { count: 0, exact: true };
221
+ const arr = msgs || [];
222
+ const window2 = arr.slice(Math.max(0, arr.length - want));
223
+ let n = 0;
224
+ for (const m of window2) {
225
+ if (!m || m.dir !== "in")
226
+ continue;
227
+ if (m.text && !m.file && isProto({ text: m.text, dir: m.dir, ts: m.ts }))
228
+ continue;
229
+ n++;
230
+ }
231
+ return { count: n, exact: window2.length >= want };
232
+ }
233
+ function rosterRow(peer, isProto, fix) {
234
+ const hidden = lastIsProtocol(peer, isProto);
235
+ if (!hidden) {
236
+ return {
237
+ preview: peer && peer.lastMsg || "",
238
+ lastTime: peer && peer.lastTime || "",
239
+ unread: Number(peer && peer.unread || 0),
240
+ activityTs: Number(peer && peer.lastTs || 0),
241
+ hidden: false
242
+ };
243
+ }
244
+ return {
245
+ preview: fix && fix.preview || "",
246
+ lastTime: fix && fix.lastTime || "",
247
+ unread: fix && fix.unreadExact ? fix.unread : 0,
248
+ activityTs: fix && fix.ts || 0,
249
+ hidden: true,
250
+ pending: !fix
251
+ };
252
+ }
253
+ function rosterUnreadTotal(peers, isProto, fixes) {
254
+ return (peers || []).reduce((n, p) => n + rosterRow(p, isProto, fixes && fixes[p.id]).unread, 0);
255
+ }
256
+ function collapseProtocol(thread, isProto, lang) {
257
+ const out = [];
258
+ let run = null;
259
+ const flush = () => {
260
+ if (!run)
261
+ return;
262
+ out.push({
263
+ protocol: true,
264
+ id: `proto-${run.first.id || run.startIndex}`,
265
+ count: run.items.length,
266
+ label: run.label,
267
+ dir: run.first.from === "me" ? "out" : "in",
268
+ text: run.items.map((m) => m.text).join("\n"),
269
+ line: protocolLine(run, lang)
270
+ });
271
+ run = null;
272
+ };
273
+ (thread || []).forEach((m, i) => {
274
+ const hit = m && !m.day ? isProto({ text: m.text, dir: m && m.from === "me" ? "out" : "in", ts: m && m.ts }) : false;
275
+ if (!hit || m && m.file) {
276
+ flush();
277
+ out.push(m);
278
+ return;
279
+ }
280
+ if (!run)
281
+ run = { first: m, startIndex: i, items: [], label: typeof hit === "string" ? hit : "" };
282
+ if (!run.label && typeof hit === "string")
283
+ run.label = hit;
284
+ run.items.push(m);
285
+ });
286
+ flush();
287
+ return out;
288
+ }
289
+ function protocolLine(run, lang) {
290
+ const arrow = run.first.from === "me" ? "\u2192" : "\u2190";
291
+ const n = run.items.length;
292
+ if (run.label) {
293
+ return `\u2699 ${arrow} ${run.label}${n > 1 ? ` \xB7 ${n}` : ""}`;
294
+ }
295
+ if (n > 1) {
296
+ return lang === "zh" ? `\u2699 ${arrow} ${n} \u6761\u534F\u8BAE\u6D88\u606F` : `\u2699 ${arrow} ${n} protocol messages`;
297
+ }
298
+ return `\u2699 ${arrow} ${protocolSummary(run.first.text)}`;
299
+ }
300
+ function protocolSummary(text, max = 72) {
301
+ const s = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
302
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
303
+ }
304
+
163
305
  // src/data-ipc.jsx
164
306
  function dkHash(s) {
165
307
  let h = 2166136261;
@@ -392,6 +534,48 @@ function dkThreadMsg(m) {
392
534
  components: m.components
393
535
  };
394
536
  }
537
+ var DK_TAIL_MIN = 30;
538
+ var DK_TAIL_MAX = 200;
539
+ function useProtocolTails(peers, isProtocol, enabled) {
540
+ const [fixes, setFixes] = React.useState({});
541
+ const cache = React.useRef(/* @__PURE__ */ new Map());
542
+ const inFlight = React.useRef(/* @__PURE__ */ new Set());
543
+ React.useEffect(() => {
544
+ if (!enabled)
545
+ return;
546
+ for (const p of peers || []) {
547
+ if (!p || !lastIsProtocol(p, isProtocol))
548
+ continue;
549
+ const unread = Number(p.unread) || 0;
550
+ const key = `${p.lastTs || 0}:${unread}`;
551
+ const had = cache.current.get(p.id);
552
+ if (had && had.key === key)
553
+ continue;
554
+ if (inFlight.current.has(p.id))
555
+ continue;
556
+ const limit = Math.min(DK_TAIL_MAX, Math.max(DK_TAIL_MIN, unread + 5));
557
+ inFlight.current.add(p.id);
558
+ dkGet("/api/chat-history?peer=" + encodeURIComponent(p.id) + "&limit=" + limit).then((d) => {
559
+ const msgs = d && d.chats && d.chats[p.id] || [];
560
+ const human = newestHumanMessage(msgs, isProtocol);
561
+ const count = humanUnread(msgs, unread, isProtocol);
562
+ const fix = {
563
+ preview: human ? (human.dir === "out" ? "you: " : "") + (human.file ? human.file.name : human.text || "") : "",
564
+ lastTime: human ? dkClock(human.ts) : "",
565
+ ts: human ? human.ts : 0,
566
+ unread: count.count,
567
+ unreadExact: count.exact
568
+ };
569
+ cache.current.set(p.id, { key, fix });
570
+ setFixes((f) => Object.assign({}, f, { [p.id]: fix }));
571
+ }).catch(() => {
572
+ }).finally(() => {
573
+ inFlight.current.delete(p.id);
574
+ });
575
+ }
576
+ }, [peers, isProtocol, enabled]);
577
+ return fixes;
578
+ }
395
579
  function useDaemonData() {
396
580
  const [snap, setSnap] = React.useState({
397
581
  me: DK_ME_FALLBACK,
@@ -1561,8 +1745,9 @@ function RequestsBlock({ T, requests, onAct }) {
1561
1745
  paddingLeft: 8
1562
1746
  } }, 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
1747
  }
1564
- function PeerRow({ peer, T, active, onClick, muted }) {
1748
+ function PeerRow({ peer, T, active, onClick, muted, isProtocol, fix }) {
1565
1749
  const name = peer.alias || peer.userId;
1750
+ const row = rosterRow(peer, isProtocol || (() => false), fix);
1566
1751
  return /* @__PURE__ */ React.createElement("button", { onClick, style: {
1567
1752
  display: "flex",
1568
1753
  alignItems: "center",
@@ -1591,12 +1776,13 @@ function PeerRow({ peer, T, active, onClick, muted }) {
1591
1776
  textOverflow: "ellipsis",
1592
1777
  flex: 1,
1593
1778
  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 })));
1779
+ } }, 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
1780
  }
1596
- function dkActivityTs(p) {
1597
- return Math.max(p.lastTs || 0, p.acceptedAt || 0, p.requestedAt || 0);
1781
+ function dkActivityTs(p, isProtocol, fix) {
1782
+ const row = rosterRow(p, isProtocol || (() => false), fix);
1783
+ return Math.max(row.activityTs, p.acceptedAt || 0, p.requestedAt || 0);
1598
1784
  }
1599
- function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide }) {
1785
+ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide, isProtocol, protocolFix }) {
1600
1786
  const [q, setQ] = React.useState("");
1601
1787
  const [addr, setAddr] = React.useState("");
1602
1788
  const [addState, setAddState] = React.useState(null);
@@ -1641,7 +1827,8 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, pre
1641
1827
  }).slice().sort((a, b) => {
1642
1828
  if (!!a.pinned !== !!b.pinned)
1643
1829
  return a.pinned ? -1 : 1;
1644
- const ta = dkActivityTs(a), tb = dkActivityTs(b);
1830
+ const ta = dkActivityTs(a, isProtocol, protocolFix && protocolFix[a.id]);
1831
+ const tb = dkActivityTs(b, isProtocol, protocolFix && protocolFix[b.id]);
1645
1832
  if (ta !== tb)
1646
1833
  return tb - ta;
1647
1834
  return String(a.alias || a.userId || "").localeCompare(String(b.alias || b.userId || ""));
@@ -1680,7 +1867,7 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, pre
1680
1867
  fontSize: 11.5,
1681
1868
  lineHeight: 1.35,
1682
1869
  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"))));
1870
+ } }, 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, fix: protocolFix && protocolFix[p.id] })), !filtered.length && /* @__PURE__ */ React.createElement("div", { style: { padding: 16, textAlign: "center", fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "no matches"))));
1684
1871
  }
1685
1872
  var inputStyle = {
1686
1873
  flex: 1,
@@ -2680,7 +2867,7 @@ function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetry
2680
2867
  ) : 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
2868
  );
2682
2869
  }
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 }) {
2870
+ 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
2871
  const scrollRef = React.useRef(null);
2685
2872
  const prependRef = React.useRef(null);
2686
2873
  const [earlierBusy, setEarlierBusy] = React.useState(false);
@@ -3102,7 +3289,15 @@ ${peer.address}`
3102
3289
  fontSize: 12.5,
3103
3290
  color: "var(--text)"
3104
3291
  } }, /* @__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(
3292
+ /* @__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: {
3293
+ fontFamily: "var(--mono)",
3294
+ fontSize: 10.5,
3295
+ color: "var(--faint)",
3296
+ maxWidth: "86%",
3297
+ whiteSpace: "nowrap",
3298
+ overflow: "hidden",
3299
+ textOverflow: "ellipsis"
3300
+ } }, m.line)) : /* @__PURE__ */ React.createElement(
3106
3301
  Msg,
3107
3302
  {
3108
3303
  key: m.id || i,
@@ -3273,12 +3468,12 @@ function MenuItem({ icon, label, onClick, danger }) {
3273
3468
  function ChatEmpty({ T }) {
3274
3469
  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
3470
  }
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 }) {
3471
+ function ChatTab({ T, lang, peers, requests, activeId, thread, isProtocol, protocolFix, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, hasEarlier, onLoadEarlier, onAssistantRemove, onAssistantAdopt, prefillAddr, onPrefillConsumed, muted, onToggleMute, phone, onBack }) {
3277
3472
  const peer = peers.find((p) => p.id === activeId);
3278
3473
  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 });
3474
+ 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, protocolFix, wide: true });
3280
3475
  }
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 }));
3476
+ 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, protocolFix }), 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
3477
  }
3283
3478
  Object.assign(window, { ChatTab });
3284
3479
 
@@ -5922,7 +6117,7 @@ function dkFlashStop() {
5922
6117
  function dkLooking() {
5923
6118
  return document.visibilityState === "visible" && document.hasFocus();
5924
6119
  }
5925
- function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted }) {
6120
+ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted, isProtocol }) {
5926
6121
  const prev = React.useRef(null);
5927
6122
  const mutedRef = React.useRef(/* @__PURE__ */ new Set());
5928
6123
  mutedRef.current = new Set(muted || []);
@@ -5952,7 +6147,8 @@ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true,
5952
6147
  const byId = /* @__PURE__ */ new Map();
5953
6148
  for (const p of peers || []) {
5954
6149
  const dir = p.lastDir || (String(p.lastMsg || "").startsWith("you: ") ? "out" : "in");
5955
- byId.set(p.id, { ts: p.lastTs || 0, dir });
6150
+ const quiet = typeof isProtocol === "function" && lastIsProtocol(p, isProtocol);
6151
+ byId.set(p.id, { ts: p.lastTs || 0, dir, quiet });
5956
6152
  }
5957
6153
  const reqIds = new Set((requests || []).map((r) => r.id));
5958
6154
  const was = prev.current;
@@ -5963,6 +6159,8 @@ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true,
5963
6159
  for (const [id, cur] of byId) {
5964
6160
  if (cur.dir !== "in" || !cur.ts)
5965
6161
  continue;
6162
+ if (cur.quiet)
6163
+ continue;
5966
6164
  const old = was.byId.get(id);
5967
6165
  if (old ? cur.ts <= old.ts : Date.now() - cur.ts > 3e4)
5968
6166
  continue;
@@ -6810,6 +7008,8 @@ function DkApp() {
6810
7008
  const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
6811
7009
  const mutedIds = Array.isArray(t.alertMuted) ? t.alertMuted : [];
6812
7010
  const onToggleMute = (id) => setTweak("alertMuted", mutedIds.includes(id) ? mutedIds.filter((x) => x !== id) : mutedIds.concat(id));
7011
+ const isProtocol = React.useMemo(() => protocolTest(hostIsProtocolMessage), []);
7012
+ const protocolFix = useProtocolTails(peers, isProtocol, !!hostIsProtocolMessage);
6813
7013
  useIncomingAlerts({
6814
7014
  T,
6815
7015
  peers,
@@ -6818,7 +7018,8 @@ function DkApp() {
6818
7018
  activeId,
6819
7019
  sound: t.alertSound !== false,
6820
7020
  flash: t.alertFlash !== false,
6821
- muted: mutedIds
7021
+ muted: mutedIds,
7022
+ isProtocol
6822
7023
  });
6823
7024
  const assistant = useAssistant({
6824
7025
  lang: t.lang,
@@ -6852,6 +7053,54 @@ function DkApp() {
6852
7053
  });
6853
7054
  const phone = useIsPhone();
6854
7055
  const [sharing, setSharing] = React.useState(false);
7056
+ const headerActions = (hostHeaderActions || []).filter((a) => a && a.id && typeof a.onClick === "function");
7057
+ const headerActionBtns = headerActions.map((a) => {
7058
+ let on = false;
7059
+ try {
7060
+ on = typeof a.active === "function" ? !!a.active() : false;
7061
+ } catch (e) {
7062
+ on = false;
7063
+ }
7064
+ const label = pickText(a.label, t.lang, a.id);
7065
+ return /* @__PURE__ */ React.createElement(
7066
+ "button",
7067
+ {
7068
+ key: a.id,
7069
+ title: label,
7070
+ "aria-label": label,
7071
+ "aria-pressed": on,
7072
+ onClick: () => {
7073
+ try {
7074
+ a.onClick({ T, t, me, activeId, phone });
7075
+ } catch (e) {
7076
+ }
7077
+ },
7078
+ style: {
7079
+ width: 32,
7080
+ height: 32,
7081
+ flexShrink: 0,
7082
+ borderRadius: 8,
7083
+ cursor: "pointer",
7084
+ padding: 0,
7085
+ border: "1px solid " + (on ? "var(--accent)" : "var(--line)"),
7086
+ background: on ? "color-mix(in oklab, var(--accent), transparent 86%)" : "transparent",
7087
+ display: "flex",
7088
+ alignItems: "center",
7089
+ justifyContent: "center"
7090
+ }
7091
+ },
7092
+ /* @__PURE__ */ React.createElement(
7093
+ Icon,
7094
+ {
7095
+ name: typeof a.icon === "string" ? a.icon : void 0,
7096
+ svgPath: a.icon && typeof a.icon === "object" ? a.icon.svgPath : void 0,
7097
+ size: 16,
7098
+ stroke: 2,
7099
+ color: on ? "var(--accent)" : "var(--dim)"
7100
+ }
7101
+ )
7102
+ );
7103
+ });
6855
7104
  const shareBtn = /* @__PURE__ */ React.createElement(
6856
7105
  "button",
6857
7106
  {
@@ -7022,7 +7271,7 @@ function DkApp() {
7022
7271
  const lastPeople = React.useRef("here");
7023
7272
  if (peopleTab)
7024
7273
  lastPeople.current = tab;
7025
- const unreadTotal = (peers || []).reduce((n, p) => n + (Number(p.unread) || 0), 0) + (requests || []).length;
7274
+ const unreadTotal = rosterUnreadTotal(peers, isProtocol, protocolFix) + (requests || []).length;
7026
7275
  const NAV_FOR = {
7027
7276
  chat: { icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
7028
7277
  people: { icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
@@ -7066,7 +7315,7 @@ function DkApp() {
7066
7315
  display: "flex",
7067
7316
  border: "1px solid " + (tab === "profile" ? "var(--line)" : "transparent"),
7068
7317
  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: {
7318
+ } }, /* @__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
7319
  display: "inline-flex",
7071
7320
  alignItems: "center",
7072
7321
  gap: 6,
@@ -7080,7 +7329,7 @@ function DkApp() {
7080
7329
  fontWeight: 600,
7081
7330
  background: tab === n.id ? "var(--accent)" : "transparent",
7082
7331
  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(
7332
+ } }, /* @__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, protocolFix, 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
7333
  ProfileTab,
7085
7334
  {
7086
7335
  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.29",
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",