@decentnetwork/beagle-ui 0.2.28 → 0.2.30

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 +224 -111
  2. package/package.json +1 -1
package/dist/beagle-ui.js CHANGED
@@ -1,4 +1,4 @@
1
- export const BEAGLE_UI_VERSION = "0.2.28";
1
+ export const BEAGLE_UI_VERSION = "0.2.30";
2
2
  // src/icons.jsx
3
3
  var ICON_PATHS = {
4
4
  // ---- tab bar (the four must feel like one set) ----
@@ -166,6 +166,142 @@ function DkBrowserNotice(props) {
166
166
  return H.DkBrowserNotice ? /* @__PURE__ */ React.createElement(H.DkBrowserNotice, { ...props }) : null;
167
167
  }
168
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
+
169
305
  // src/data-ipc.jsx
170
306
  function dkHash(s) {
171
307
  let h = 2166136261;
@@ -398,6 +534,48 @@ function dkThreadMsg(m) {
398
534
  components: m.components
399
535
  };
400
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
+ }
401
579
  function useDaemonData() {
402
580
  const [snap, setSnap] = React.useState({
403
581
  me: DK_ME_FALLBACK,
@@ -1157,6 +1335,20 @@ async function loadServicePeers() {
1157
1335
  return service_peers_default;
1158
1336
  }
1159
1337
  }
1338
+ var ASSISTANT_TS_KEY = "beagle.assistant.lastTs";
1339
+ function loadAssistantTs() {
1340
+ try {
1341
+ return Number(localStorage.getItem(ASSISTANT_TS_KEY)) || 0;
1342
+ } catch (e) {
1343
+ return 0;
1344
+ }
1345
+ }
1346
+ function saveAssistantTs(ts) {
1347
+ try {
1348
+ localStorage.setItem(ASSISTANT_TS_KEY, String(ts));
1349
+ } catch (e) {
1350
+ }
1351
+ }
1160
1352
  var ASSISTANT_PEER = {
1161
1353
  id: ASSISTANT_ID,
1162
1354
  userId: ASSISTANT_ID,
@@ -1164,7 +1356,6 @@ var ASSISTANT_PEER = {
1164
1356
  address: "",
1165
1357
  online: true,
1166
1358
  app: true,
1167
- pinned: true,
1168
1359
  agent: false,
1169
1360
  pending: false,
1170
1361
  via: null,
@@ -1269,9 +1460,18 @@ function AssistantPanel({ m, T, lang, onRemove, onAdopt }) {
1269
1460
  }).finally(() => setBusy(false));
1270
1461
  } }, busy ? T.working || "Working\u2026" : label), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 12, color: "var(--faint)" } }, adopt ? lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u5373\u4FDD\u6301\u4E3A\u597D\u53CB" : "Untick any you want to keep as a friend" : lang === "zh" ? "\u53D6\u6D88\u52FE\u9009\u53EF\u4FDD\u7559" : "Untick anyone you want to keep"))));
1271
1462
  }
1272
- function useAssistant({ lang, T, muted, onToggleMute, sound, flash, setAlert, onReloadFriends }) {
1463
+ function useAssistant({ lang, T, muted, onToggleMute, sound, flash, setAlert, onReloadFriends, onActivity }) {
1273
1464
  const [thread, setThread] = React.useState(() => [{ day: (/* @__PURE__ */ new Date()).toLocaleDateString() }, asMsg("them", helpText(lang))]);
1274
- const append = React.useCallback((...ms) => setThread((t) => t.concat(ms)), []);
1465
+ const bump = React.useCallback(() => {
1466
+ const ts = Date.now();
1467
+ saveAssistantTs(ts);
1468
+ if (onActivity)
1469
+ onActivity(ts);
1470
+ }, [onActivity]);
1471
+ const append = React.useCallback((...ms) => {
1472
+ bump();
1473
+ setThread((t) => t.concat(ms));
1474
+ }, [bump]);
1275
1475
  React.useEffect(() => {
1276
1476
  setThread((t) => t.length === 2 ? [t[0], asMsg("them", helpText(lang))] : t);
1277
1477
  }, [lang]);
@@ -1506,100 +1706,6 @@ ${lines}`, { assistant: { kind: "adopt", rows: fresh } }));
1506
1706
  return { thread, handle, removeFriends, adoptServicePeers };
1507
1707
  }
1508
1708
 
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
-
1603
1709
  // src/chat.jsx
1604
1710
  var DK_CMD_HIST_KEY = "beagle.assistant.history";
1605
1711
  var DK_CMD_HIST_MAX = 50;
@@ -1661,9 +1767,9 @@ function RequestsBlock({ T, requests, onAct }) {
1661
1767
  paddingLeft: 8
1662
1768
  } }, 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))))));
1663
1769
  }
1664
- function PeerRow({ peer, T, active, onClick, muted, isProtocol }) {
1770
+ function PeerRow({ peer, T, active, onClick, muted, isProtocol, fix }) {
1665
1771
  const name = peer.alias || peer.userId;
1666
- const row = rosterRow(peer, isProtocol || (() => false));
1772
+ const row = rosterRow(peer, isProtocol || (() => false), fix);
1667
1773
  return /* @__PURE__ */ React.createElement("button", { onClick, style: {
1668
1774
  display: "flex",
1669
1775
  alignItems: "center",
@@ -1694,11 +1800,11 @@ function PeerRow({ peer, T, active, onClick, muted, isProtocol }) {
1694
1800
  minWidth: 0
1695
1801
  } }, 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 })));
1696
1802
  }
1697
- function dkActivityTs(p, isProtocol) {
1698
- const row = rosterRow(p, isProtocol || (() => false));
1803
+ function dkActivityTs(p, isProtocol, fix) {
1804
+ const row = rosterRow(p, isProtocol || (() => false), fix);
1699
1805
  return Math.max(row.activityTs, p.acceptedAt || 0, p.requestedAt || 0);
1700
1806
  }
1701
- function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide, isProtocol }) {
1807
+ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted, wide, isProtocol, protocolFix }) {
1702
1808
  const [q, setQ] = React.useState("");
1703
1809
  const [addr, setAddr] = React.useState("");
1704
1810
  const [addState, setAddState] = React.useState(null);
@@ -1743,7 +1849,8 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, pre
1743
1849
  }).slice().sort((a, b) => {
1744
1850
  if (!!a.pinned !== !!b.pinned)
1745
1851
  return a.pinned ? -1 : 1;
1746
- const ta = dkActivityTs(a, isProtocol), tb = dkActivityTs(b, isProtocol);
1852
+ const ta = dkActivityTs(a, isProtocol, protocolFix && protocolFix[a.id]);
1853
+ const tb = dkActivityTs(b, isProtocol, protocolFix && protocolFix[b.id]);
1747
1854
  if (ta !== tb)
1748
1855
  return tb - ta;
1749
1856
  return String(a.alias || a.userId || "").localeCompare(String(b.alias || b.userId || ""));
@@ -1782,7 +1889,7 @@ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, pre
1782
1889
  fontSize: 11.5,
1783
1890
  lineHeight: 1.35,
1784
1891
  color: addState.kind === "err" ? "var(--bad, #e5484d)" : addState.kind === "ok" ? "var(--good, #46a758)" : "var(--faint)"
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"))));
1892
+ } }, 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"))));
1786
1893
  }
1787
1894
  var inputStyle = {
1788
1895
  flex: 1,
@@ -3383,12 +3490,12 @@ function MenuItem({ icon, label, onClick, danger }) {
3383
3490
  function ChatEmpty({ T }) {
3384
3491
  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));
3385
3492
  }
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 }) {
3493
+ 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 }) {
3387
3494
  const peer = peers.find((p) => p.id === activeId);
3388
3495
  if (phone) {
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 });
3496
+ 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 });
3390
3497
  }
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 }));
3498
+ 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 }));
3392
3499
  }
3393
3500
  Object.assign(window, { ChatTab });
3394
3501
 
@@ -6869,7 +6976,11 @@ function DkApp() {
6869
6976
  sessionStorage.setItem(key, "1");
6870
6977
  window.location.reload();
6871
6978
  }, [me && me.beagleVer]);
6872
- const peers = React.useMemo(() => [ASSISTANT_PEER].concat(data.peers || []), [data.peers]);
6979
+ const [assistantTs, setAssistantTs] = React.useState(loadAssistantTs);
6980
+ const peers = React.useMemo(() => (data.peers || []).concat([Object.assign({}, ASSISTANT_PEER, {
6981
+ lastTs: assistantTs,
6982
+ lastTime: assistantTs ? dkClock(assistantTs) : ""
6983
+ })]), [data.peers, assistantTs]);
6873
6984
  const [pendingAddr, setPendingAddr] = React.useState(() => dkParseDeepLinkAddress());
6874
6985
  React.useEffect(() => {
6875
6986
  const onHash = () => {
@@ -6924,6 +7035,7 @@ function DkApp() {
6924
7035
  const mutedIds = Array.isArray(t.alertMuted) ? t.alertMuted : [];
6925
7036
  const onToggleMute = (id) => setTweak("alertMuted", mutedIds.includes(id) ? mutedIds.filter((x) => x !== id) : mutedIds.concat(id));
6926
7037
  const isProtocol = React.useMemo(() => protocolTest(hostIsProtocolMessage), []);
7038
+ const protocolFix = useProtocolTails(peers, isProtocol, !!hostIsProtocolMessage);
6927
7039
  useIncomingAlerts({
6928
7040
  T,
6929
7041
  peers,
@@ -6940,6 +7052,7 @@ function DkApp() {
6940
7052
  T,
6941
7053
  muted: mutedIds,
6942
7054
  onToggleMute,
7055
+ onActivity: setAssistantTs,
6943
7056
  sound: t.alertSound !== false,
6944
7057
  flash: t.alertFlash !== false,
6945
7058
  setAlert: (which, on) => setTweak(which === "sound" ? "alertSound" : "alertFlash", on),
@@ -7185,7 +7298,7 @@ function DkApp() {
7185
7298
  const lastPeople = React.useRef("here");
7186
7299
  if (peopleTab)
7187
7300
  lastPeople.current = tab;
7188
- const unreadTotal = rosterUnreadTotal(peers, isProtocol) + (requests || []).length;
7301
+ const unreadTotal = rosterUnreadTotal(peers, isProtocol, protocolFix) + (requests || []).length;
7189
7302
  const NAV_FOR = {
7190
7303
  chat: { icon: "message", label: T.chat, active: tab === "chat", badge: unreadTotal, go: () => setTab("chat") },
7191
7304
  people: { icon: "users", label: T.people, active: peopleTab, go: () => setTab(lastPeople.current) },
@@ -7243,7 +7356,7 @@ function DkApp() {
7243
7356
  fontWeight: 600,
7244
7357
  background: tab === n.id ? "var(--accent)" : "transparent",
7245
7358
  color: tab === n.id ? "#fff" : "var(--dim)"
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(
7359
+ } }, /* @__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(
7247
7360
  ProfileTab,
7248
7361
  {
7249
7362
  T,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle-ui",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
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",