@decentnetwork/beagle 0.1.113 → 0.1.115

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 +266 -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.115";
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;
@@ -157,6 +163,140 @@ window.__DK_UI_VERSION="0.1.113";
157
163
  function DkBrowserNotice(props) {
158
164
  return H.DkBrowserNotice ? /* @__PURE__ */ React.createElement(H.DkBrowserNotice, { ...props }) : null;
159
165
  }
166
+ function protocolTest(rule) {
167
+ if (typeof rule !== "function")
168
+ return () => false;
169
+ return (msg) => {
170
+ if (!msg)
171
+ return false;
172
+ try {
173
+ const v = rule(msg);
174
+ return typeof v === "string" ? v || true : !!v;
175
+ } catch (e) {
176
+ return false;
177
+ }
178
+ };
179
+ }
180
+ function stripOwnPrefix(text) {
181
+ return String(text == null ? "" : text).replace(/^you:\s*/i, "");
182
+ }
183
+ function lastMessageOf(peer) {
184
+ const raw = String(peer && peer.lastMsg || "");
185
+ return {
186
+ text: stripOwnPrefix(raw),
187
+ dir: peer && peer.lastDir || (/^you:\s*/i.test(raw) ? "out" : "in"),
188
+ ts: peer && peer.lastTs || 0
189
+ };
190
+ }
191
+ function lastIsProtocol(peer, isProto) {
192
+ const last = lastMessageOf(peer);
193
+ if (!last.text)
194
+ return false;
195
+ return isProto(last);
196
+ }
197
+ function newestHumanMessage(msgs, isProto) {
198
+ const arr = msgs || [];
199
+ for (let i = arr.length - 1; i >= 0; i--) {
200
+ const m = arr[i];
201
+ if (!m)
202
+ continue;
203
+ if (m.file)
204
+ return m;
205
+ if (!m.text)
206
+ continue;
207
+ if (isProto({ text: m.text, dir: m.dir, ts: m.ts }))
208
+ continue;
209
+ return m;
210
+ }
211
+ return null;
212
+ }
213
+ function humanUnread(msgs, unread, isProto) {
214
+ const want = Number(unread) || 0;
215
+ if (want <= 0)
216
+ return { count: 0, exact: true };
217
+ const arr = msgs || [];
218
+ const window2 = arr.slice(Math.max(0, arr.length - want));
219
+ let n = 0;
220
+ for (const m of window2) {
221
+ if (!m || m.dir !== "in")
222
+ continue;
223
+ if (m.text && !m.file && isProto({ text: m.text, dir: m.dir, ts: m.ts }))
224
+ continue;
225
+ n++;
226
+ }
227
+ return { count: n, exact: window2.length >= want };
228
+ }
229
+ function rosterRow(peer, isProto, fix) {
230
+ const hidden = lastIsProtocol(peer, isProto);
231
+ if (!hidden) {
232
+ return {
233
+ preview: peer && peer.lastMsg || "",
234
+ lastTime: peer && peer.lastTime || "",
235
+ unread: Number(peer && peer.unread || 0),
236
+ activityTs: Number(peer && peer.lastTs || 0),
237
+ hidden: false
238
+ };
239
+ }
240
+ return {
241
+ preview: fix && fix.preview || "",
242
+ lastTime: fix && fix.lastTime || "",
243
+ unread: fix && fix.unreadExact ? fix.unread : 0,
244
+ activityTs: fix && fix.ts || 0,
245
+ hidden: true,
246
+ pending: !fix
247
+ };
248
+ }
249
+ function rosterUnreadTotal(peers, isProto, fixes) {
250
+ return (peers || []).reduce((n, p) => n + rosterRow(p, isProto, fixes && fixes[p.id]).unread, 0);
251
+ }
252
+ function collapseProtocol(thread, isProto, lang) {
253
+ const out = [];
254
+ let run = null;
255
+ const flush = () => {
256
+ if (!run)
257
+ return;
258
+ out.push({
259
+ protocol: true,
260
+ id: `proto-${run.first.id || run.startIndex}`,
261
+ count: run.items.length,
262
+ label: run.label,
263
+ dir: run.first.from === "me" ? "out" : "in",
264
+ text: run.items.map((m) => m.text).join("\n"),
265
+ line: protocolLine(run, lang)
266
+ });
267
+ run = null;
268
+ };
269
+ (thread || []).forEach((m, i) => {
270
+ const hit = m && !m.day ? isProto({ text: m.text, dir: m && m.from === "me" ? "out" : "in", ts: m && m.ts }) : false;
271
+ if (!hit || m && m.file) {
272
+ flush();
273
+ out.push(m);
274
+ return;
275
+ }
276
+ if (!run)
277
+ run = { first: m, startIndex: i, items: [], label: typeof hit === "string" ? hit : "" };
278
+ if (!run.label && typeof hit === "string")
279
+ run.label = hit;
280
+ run.items.push(m);
281
+ });
282
+ flush();
283
+ return out;
284
+ }
285
+ function protocolLine(run, lang) {
286
+ const arrow = run.first.from === "me" ? "\u2192" : "\u2190";
287
+ const n = run.items.length;
288
+ if (run.label) {
289
+ return `\u2699 ${arrow} ${run.label}${n > 1 ? ` \xB7 ${n}` : ""}`;
290
+ }
291
+ if (n > 1) {
292
+ return lang === "zh" ? `\u2699 ${arrow} ${n} \u6761\u534F\u8BAE\u6D88\u606F` : `\u2699 ${arrow} ${n} protocol messages`;
293
+ }
294
+ return `\u2699 ${arrow} ${protocolSummary(run.first.text)}`;
295
+ }
296
+ function protocolSummary(text, max = 72) {
297
+ const s = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
298
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
299
+ }
160
300
  function dkHash(s) {
161
301
  let h = 2166136261;
162
302
  for (let i = 0; i < s.length; i++) {
@@ -388,6 +528,48 @@ window.__DK_UI_VERSION="0.1.113";
388
528
  components: m.components
389
529
  };
390
530
  }
531
+ var DK_TAIL_MIN = 30;
532
+ var DK_TAIL_MAX = 200;
533
+ function useProtocolTails(peers, isProtocol, enabled) {
534
+ const [fixes, setFixes] = React.useState({});
535
+ const cache = React.useRef(/* @__PURE__ */ new Map());
536
+ const inFlight = React.useRef(/* @__PURE__ */ new Set());
537
+ React.useEffect(() => {
538
+ if (!enabled)
539
+ return;
540
+ for (const p of peers || []) {
541
+ if (!p || !lastIsProtocol(p, isProtocol))
542
+ continue;
543
+ const unread = Number(p.unread) || 0;
544
+ const key = `${p.lastTs || 0}:${unread}`;
545
+ const had = cache.current.get(p.id);
546
+ if (had && had.key === key)
547
+ continue;
548
+ if (inFlight.current.has(p.id))
549
+ continue;
550
+ const limit = Math.min(DK_TAIL_MAX, Math.max(DK_TAIL_MIN, unread + 5));
551
+ inFlight.current.add(p.id);
552
+ dkGet("/api/chat-history?peer=" + encodeURIComponent(p.id) + "&limit=" + limit).then((d) => {
553
+ const msgs = d && d.chats && d.chats[p.id] || [];
554
+ const human = newestHumanMessage(msgs, isProtocol);
555
+ const count = humanUnread(msgs, unread, isProtocol);
556
+ const fix = {
557
+ preview: human ? (human.dir === "out" ? "you: " : "") + (human.file ? human.file.name : human.text || "") : "",
558
+ lastTime: human ? dkClock(human.ts) : "",
559
+ ts: human ? human.ts : 0,
560
+ unread: count.count,
561
+ unreadExact: count.exact
562
+ };
563
+ cache.current.set(p.id, { key, fix });
564
+ setFixes((f) => Object.assign({}, f, { [p.id]: fix }));
565
+ }).catch(() => {
566
+ }).finally(() => {
567
+ inFlight.current.delete(p.id);
568
+ });
569
+ }
570
+ }, [peers, isProtocol, enabled]);
571
+ return fixes;
572
+ }
391
573
  function useDaemonData() {
392
574
  const [snap, setSnap] = React.useState({
393
575
  me: DK_ME_FALLBACK,
@@ -1547,8 +1729,9 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1547
1729
  paddingLeft: 8
1548
1730
  } }, 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
1731
  }
1550
- function PeerRow({ peer, T, active, onClick, muted }) {
1732
+ function PeerRow({ peer, T, active, onClick, muted, isProtocol, fix }) {
1551
1733
  const name = peer.alias || peer.userId;
1734
+ const row = rosterRow(peer, isProtocol || (() => false), fix);
1552
1735
  return /* @__PURE__ */ React.createElement("button", { onClick, style: {
1553
1736
  display: "flex",
1554
1737
  alignItems: "center",
@@ -1577,12 +1760,13 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1577
1760
  textOverflow: "ellipsis",
1578
1761
  flex: 1,
1579
1762
  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 })));
1763
+ } }, 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
1764
  }
1582
- function dkActivityTs(p) {
1583
- return Math.max(p.lastTs || 0, p.acceptedAt || 0, p.requestedAt || 0);
1765
+ function dkActivityTs(p, isProtocol, fix) {
1766
+ const row = rosterRow(p, isProtocol || (() => false), fix);
1767
+ return Math.max(row.activityTs, p.acceptedAt || 0, p.requestedAt || 0);
1584
1768
  }
1585
- function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide }) {
1769
+ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide, isProtocol, protocolFix }) {
1586
1770
  const [q, setQ] = React.useState("");
1587
1771
  const [addr, setAddr] = React.useState("");
1588
1772
  const [addState, setAddState] = React.useState(null);
@@ -1627,7 +1811,8 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1627
1811
  }).slice().sort((a, b) => {
1628
1812
  if (!!a.pinned !== !!b.pinned)
1629
1813
  return a.pinned ? -1 : 1;
1630
- const ta = dkActivityTs(a), tb = dkActivityTs(b);
1814
+ const ta = dkActivityTs(a, isProtocol, protocolFix && protocolFix[a.id]);
1815
+ const tb = dkActivityTs(b, isProtocol, protocolFix && protocolFix[b.id]);
1631
1816
  if (ta !== tb)
1632
1817
  return tb - ta;
1633
1818
  return String(a.alias || a.userId || "").localeCompare(String(b.alias || b.userId || ""));
@@ -1666,7 +1851,7 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1666
1851
  fontSize: 11.5,
1667
1852
  lineHeight: 1.35,
1668
1853
  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"))));
1854
+ } }, 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"))));
1670
1855
  }
1671
1856
  var inputStyle = {
1672
1857
  flex: 1,
@@ -2666,7 +2851,7 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
2666
2851
  ) : 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
2852
  );
2668
2853
  }
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 }) {
2854
+ 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
2855
  const scrollRef = React.useRef(null);
2671
2856
  const prependRef = React.useRef(null);
2672
2857
  const [earlierBusy, setEarlierBusy] = React.useState(false);
@@ -3088,7 +3273,15 @@ ${peer.address}`
3088
3273
  fontSize: 12.5,
3089
3274
  color: "var(--text)"
3090
3275
  } }, /* @__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(
3276
+ /* @__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: {
3277
+ fontFamily: "var(--mono)",
3278
+ fontSize: 10.5,
3279
+ color: "var(--faint)",
3280
+ maxWidth: "86%",
3281
+ whiteSpace: "nowrap",
3282
+ overflow: "hidden",
3283
+ textOverflow: "ellipsis"
3284
+ } }, m.line)) : /* @__PURE__ */ React.createElement(
3092
3285
  Msg,
3093
3286
  {
3094
3287
  key: m.id || i,
@@ -3259,12 +3452,12 @@ ${peer.address}`
3259
3452
  function ChatEmpty({ T }) {
3260
3453
  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
3454
  }
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 }) {
3455
+ 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 }) {
3263
3456
  const peer = peers.find((p) => p.id === activeId);
3264
3457
  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 });
3458
+ 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 });
3266
3459
  }
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 }));
3460
+ 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 }));
3268
3461
  }
3269
3462
  Object.assign(window, { ChatTab });
3270
3463
  function CopyBtn({ value, copiedText = "Copied", copyFailedText = "Copy failed", copyTitle = "Copy" }) {
@@ -5888,7 +6081,7 @@ ${peer.address}`
5888
6081
  function dkLooking() {
5889
6082
  return document.visibilityState === "visible" && document.hasFocus();
5890
6083
  }
5891
- function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted }) {
6084
+ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted, isProtocol }) {
5892
6085
  const prev = React.useRef(null);
5893
6086
  const mutedRef = React.useRef(/* @__PURE__ */ new Set());
5894
6087
  mutedRef.current = new Set(muted || []);
@@ -5918,7 +6111,8 @@ ${peer.address}`
5918
6111
  const byId = /* @__PURE__ */ new Map();
5919
6112
  for (const p of peers || []) {
5920
6113
  const dir = p.lastDir || (String(p.lastMsg || "").startsWith("you: ") ? "out" : "in");
5921
- byId.set(p.id, { ts: p.lastTs || 0, dir });
6114
+ const quiet = typeof isProtocol === "function" && lastIsProtocol(p, isProtocol);
6115
+ byId.set(p.id, { ts: p.lastTs || 0, dir, quiet });
5922
6116
  }
5923
6117
  const reqIds = new Set((requests || []).map((r) => r.id));
5924
6118
  const was = prev.current;
@@ -5929,6 +6123,8 @@ ${peer.address}`
5929
6123
  for (const [id, cur] of byId) {
5930
6124
  if (cur.dir !== "in" || !cur.ts)
5931
6125
  continue;
6126
+ if (cur.quiet)
6127
+ continue;
5932
6128
  const old = was.byId.get(id);
5933
6129
  if (old ? cur.ts <= old.ts : Date.now() - cur.ts > 3e4)
5934
6130
  continue;
@@ -6772,6 +6968,8 @@ ${peer.address}`
6772
6968
  const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
6773
6969
  const mutedIds = Array.isArray(t.alertMuted) ? t.alertMuted : [];
6774
6970
  const onToggleMute = (id) => setTweak("alertMuted", mutedIds.includes(id) ? mutedIds.filter((x) => x !== id) : mutedIds.concat(id));
6971
+ const isProtocol = React.useMemo(() => protocolTest(hostIsProtocolMessage), []);
6972
+ const protocolFix = useProtocolTails(peers, isProtocol, !!hostIsProtocolMessage);
6775
6973
  useIncomingAlerts({
6776
6974
  T,
6777
6975
  peers,
@@ -6780,7 +6978,8 @@ ${peer.address}`
6780
6978
  activeId,
6781
6979
  sound: t.alertSound !== false,
6782
6980
  flash: t.alertFlash !== false,
6783
- muted: mutedIds
6981
+ muted: mutedIds,
6982
+ isProtocol
6784
6983
  });
6785
6984
  const assistant = useAssistant({
6786
6985
  lang: t.lang,
@@ -6814,6 +7013,54 @@ ${peer.address}`
6814
7013
  });
6815
7014
  const phone = useIsPhone();
6816
7015
  const [sharing, setSharing] = React.useState(false);
7016
+ const headerActions = (hostHeaderActions || []).filter((a) => a && a.id && typeof a.onClick === "function");
7017
+ const headerActionBtns = headerActions.map((a) => {
7018
+ let on = false;
7019
+ try {
7020
+ on = typeof a.active === "function" ? !!a.active() : false;
7021
+ } catch (e) {
7022
+ on = false;
7023
+ }
7024
+ const label = pickText(a.label, t.lang, a.id);
7025
+ return /* @__PURE__ */ React.createElement(
7026
+ "button",
7027
+ {
7028
+ key: a.id,
7029
+ title: label,
7030
+ "aria-label": label,
7031
+ "aria-pressed": on,
7032
+ onClick: () => {
7033
+ try {
7034
+ a.onClick({ T, t, me, activeId, phone });
7035
+ } catch (e) {
7036
+ }
7037
+ },
7038
+ style: {
7039
+ width: 32,
7040
+ height: 32,
7041
+ flexShrink: 0,
7042
+ borderRadius: 8,
7043
+ cursor: "pointer",
7044
+ padding: 0,
7045
+ border: "1px solid " + (on ? "var(--accent)" : "var(--line)"),
7046
+ background: on ? "color-mix(in oklab, var(--accent), transparent 86%)" : "transparent",
7047
+ display: "flex",
7048
+ alignItems: "center",
7049
+ justifyContent: "center"
7050
+ }
7051
+ },
7052
+ /* @__PURE__ */ React.createElement(
7053
+ Icon,
7054
+ {
7055
+ name: typeof a.icon === "string" ? a.icon : void 0,
7056
+ svgPath: a.icon && typeof a.icon === "object" ? a.icon.svgPath : void 0,
7057
+ size: 16,
7058
+ stroke: 2,
7059
+ color: on ? "var(--accent)" : "var(--dim)"
7060
+ }
7061
+ )
7062
+ );
7063
+ });
6817
7064
  const shareBtn = /* @__PURE__ */ React.createElement(
6818
7065
  "button",
6819
7066
  {
@@ -6984,7 +7231,7 @@ ${peer.address}`
6984
7231
  const lastPeople = React.useRef("here");
6985
7232
  if (peopleTab)
6986
7233
  lastPeople.current = tab;
6987
- const unreadTotal = (peers || []).reduce((n, p) => n + (Number(p.unread) || 0), 0) + (requests || []).length;
7234
+ const unreadTotal = rosterUnreadTotal(peers, isProtocol, protocolFix) + (requests || []).length;
6988
7235
  const NAV_FOR = {
6989
7236
  chat: { icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
6990
7237
  people: { icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
@@ -7028,7 +7275,7 @@ ${peer.address}`
7028
7275
  display: "flex",
7029
7276
  border: "1px solid " + (tab === "profile" ? "var(--line)" : "transparent"),
7030
7277
  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: {
7278
+ } }, /* @__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
7279
  display: "inline-flex",
7033
7280
  alignItems: "center",
7034
7281
  gap: 6,
@@ -7042,7 +7289,7 @@ ${peer.address}`
7042
7289
  fontWeight: 600,
7043
7290
  background: tab === n.id ? "var(--accent)" : "transparent",
7044
7291
  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(
7292
+ } }, /* @__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(
7046
7293
  ProfileTab,
7047
7294
  {
7048
7295
  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.115",
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.29",
39
39
  "@types/js-yaml": "^4.0.9",
40
40
  "@types/node": "^20.11.0",
41
41
  "@types/yargs": "^17.0.32",