@decentnetwork/beagle 0.1.88 → 0.1.89

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.
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.87";
1
+ window.__DK_UI_VERSION="0.1.89";
2
2
  "use strict";
3
3
  (() => {
4
4
  // packages/beagle-ui/dist/beagle-ui.js
@@ -521,6 +521,9 @@ window.__DK_UI_VERSION="0.1.87";
521
521
  minWidth: 0
522
522
  } }, dkContactPreview(peer.lastMsg) || 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 })));
523
523
  }
524
+ function dkActivityTs(p) {
525
+ return Math.max(p.lastTs || 0, p.acceptedAt || 0, p.requestedAt || 0);
526
+ }
524
527
  function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }) {
525
528
  const [q, setQ] = React.useState("");
526
529
  const [addr, setAddr] = React.useState("");
@@ -558,12 +561,9 @@ window.__DK_UI_VERSION="0.1.87";
558
561
  const s = (p.alias || "") + " " + p.userId + " " + p.ip;
559
562
  return s.toLowerCase().includes(q.toLowerCase());
560
563
  }).slice().sort((a, b) => {
561
- const ta = a.lastTs || 0, tb = b.lastTs || 0;
564
+ const ta = dkActivityTs(a), tb = dkActivityTs(b);
562
565
  if (ta !== tb)
563
566
  return tb - ta;
564
- const aa = a.acceptedAt || 0, ba = b.acceptedAt || 0;
565
- if (aa !== ba)
566
- return ba - aa;
567
567
  return String(a.alias || a.userId || "").localeCompare(String(b.alias || b.userId || ""));
568
568
  });
569
569
  const online = peers.filter((p) => p.online).length;
@@ -2720,6 +2720,35 @@ ${peer.address}`
2720
2720
  )));
2721
2721
  return /* @__PURE__ */ React.createElement(Card, { label: zh ? "\u663E\u793A" : "Display" }, /* @__PURE__ */ React.createElement("div", { style: { ...row, borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement("span", { style: name }, zh ? "\u8BED\u8A00" : "Language"), seg("lang", t.lang, (v) => setTweak("lang", v), [["en", "EN"], ["zh", "\u4E2D\u6587"]])), /* @__PURE__ */ React.createElement("div", { style: row }, /* @__PURE__ */ React.createElement("span", { style: name }, zh ? "\u4E3B\u9898" : "Theme"), seg("theme", t.theme, (v) => setTweak("theme", v), [["dark", zh ? "\u6DF1\u8272" : "Dark"], ["light", zh ? "\u6D45\u8272" : "Light"]])));
2722
2722
  }
2723
+ function DkAlertSettings({ t, setTweak }) {
2724
+ const zh = t.lang === "zh";
2725
+ const toggle = (key, label, sub, last) => /* @__PURE__ */ React.createElement("label", { style: {
2726
+ display: "flex",
2727
+ alignItems: "center",
2728
+ gap: 13,
2729
+ padding: "13px 16px",
2730
+ cursor: "pointer",
2731
+ borderBottom: last ? "none" : "1px solid var(--line)"
2732
+ } }, /* @__PURE__ */ React.createElement(
2733
+ "input",
2734
+ {
2735
+ type: "checkbox",
2736
+ checked: t[key] !== false,
2737
+ onChange: (e) => setTweak(key, e.target.checked),
2738
+ style: { width: 16, height: 16, accentColor: "var(--accent)" }
2739
+ }
2740
+ ), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, label), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", marginTop: 2 } }, sub)));
2741
+ return /* @__PURE__ */ React.createElement(Card, { label: zh ? "\u63D0\u9192" : "Alerts" }, toggle(
2742
+ "alertSound",
2743
+ zh ? "\u65B0\u6D88\u606F\u63D0\u793A\u97F3" : "Sound for new messages",
2744
+ zh ? "\u6536\u5230\u65B0\u6D88\u606F\u6216\u597D\u53CB\u8BF7\u6C42\u65F6\u64AD\u653E\u77ED\u4FC3\u63D0\u793A\u97F3\u3002\u6B63\u5728\u770B\u7684\u5F53\u524D\u4F1A\u8BDD\u4E0D\u4F1A\u54CD\u3002" : "A short chime when a message or friend request arrives. Silent for the conversation you are reading."
2745
+ ), toggle(
2746
+ "alertFlash",
2747
+ zh ? "\u6807\u7B7E\u9875\u95EA\u70C1" : "Flash the tab",
2748
+ zh ? "\u7A97\u53E3\u4E0D\u5728\u524D\u53F0\u65F6\uFF0C\u6807\u7B7E\u6807\u9898\u548C\u56FE\u6807\u95EA\u70C1\u63D0\u793A\uFF0C\u56DE\u5230\u7A97\u53E3\u5373\u505C\u6B62\u3002" : "While this window is in the background, the tab title and icon blink until you come back.",
2749
+ true
2750
+ ));
2751
+ }
2723
2752
  function ProfileTab({ T, me, onEdit, t, setTweak }) {
2724
2753
  var _a;
2725
2754
  const [qr, setQr] = React.useState(null);
@@ -2741,7 +2770,7 @@ ${peer.address}`
2741
2770
  },
2742
2771
  style: { width: 16, height: 16, accentColor: "var(--accent)" }
2743
2772
  }
2744
- ), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, T && T.autoAcceptLabel || "Auto-accept friend requests"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", marginTop: 2 } }, T && T.autoAcceptSub || "Off: requests wait in the sidebar for your approval.")))), hostHasKeyBackup && /* @__PURE__ */ React.createElement(DkKeyBackup, { t: t || {} }), t && setTweak && /* @__PURE__ */ React.createElement(DkDisplaySettings, { t, setTweak }), /* @__PURE__ */ React.createElement(Card, { label: T.network }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.virtualIp, value: me.ip, copy: true }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.wireLabel, value: `${me.wire} \xB7 lossless`, mono: false }), /* @__PURE__ */ React.createElement(DkVersionRow, { T, me }), /* @__PURE__ */ React.createElement(DkUpdateCheck, { T })), /* @__PURE__ */ React.createElement(Card, { label: T.dangerZone }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 8, flexShrink: 0, background: "color-mix(in oklab, var(--danger), transparent 86%)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--danger)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 17, stroke: 2 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--danger)" } }, T.deleteNode), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)", marginTop: 1 } }, T.deleteSub)), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", size: "sm" }, T.delete)))), qr && /* @__PURE__ */ React.createElement(DkQrModal, { T, value: qr.value, label: qr.label, onClose: () => setQr(null) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
2773
+ ), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, T && T.autoAcceptLabel || "Auto-accept friend requests"), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11.5, color: "var(--dim)", marginTop: 2 } }, T && T.autoAcceptSub || "Off: requests wait in the sidebar for your approval.")))), hostHasKeyBackup && /* @__PURE__ */ React.createElement(DkKeyBackup, { t: t || {} }), t && setTweak && /* @__PURE__ */ React.createElement(DkAlertSettings, { t, setTweak }), t && setTweak && /* @__PURE__ */ React.createElement(DkDisplaySettings, { t, setTweak }), /* @__PURE__ */ React.createElement(Card, { label: T.network }, /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.virtualIp, value: me.ip, copy: true }), /* @__PURE__ */ React.createElement(FieldRow, { T, label: T.wireLabel, value: `${me.wire} \xB7 lossless`, mono: false }), /* @__PURE__ */ React.createElement(DkVersionRow, { T, me }), /* @__PURE__ */ React.createElement(DkUpdateCheck, { T })), /* @__PURE__ */ React.createElement(Card, { label: T.dangerZone }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 13, padding: "14px 16px" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 34, height: 34, borderRadius: 8, flexShrink: 0, background: "color-mix(in oklab, var(--danger), transparent 86%)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--danger)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 17, stroke: 2 })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 600, color: "var(--danger)" } }, T.deleteNode), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--faint)", marginTop: 1 } }, T.deleteSub)), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", size: "sm" }, T.delete)))), qr && /* @__PURE__ */ React.createElement(DkQrModal, { T, value: qr.value, label: qr.label, onClose: () => setQr(null) }), editing && /* @__PURE__ */ React.createElement(DkEditModal, { T, me, onClose: () => setEditing(false), onSave: (name, description) => {
2745
2774
  if (onEdit)
2746
2775
  onEdit(name, description);
2747
2776
  setEditing(false);
@@ -4290,6 +4319,181 @@ ${peer.address}`
4290
4319
  return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 1040, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 26 } }, /* @__PURE__ */ React.createElement(MyNode, { T, me, activeExit, peers, reqCount }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.peerRouting, count: peers.length }), /* @__PURE__ */ React.createElement(PeerTable, { T, peers, onOpenChat })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.exitNodes, trailing: /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { icon: "plus", size: "sm" }, T.addExit)) }), activeExit && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderRadius: 10, background: "color-mix(in oklab, var(--warn), transparent 90%)", border: "1px solid color-mix(in oklab, var(--warn), transparent 70%)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "route", size: 17, color: "var(--warn)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, T.egressVia), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: activeExit }, activeExit), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "unlink", size: "sm", onClick: () => onSetExit(null) }, T.stopRouting)), /* @__PURE__ */ React.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } }, exits.map((r) => /* @__PURE__ */ React.createElement(ExitCard, { key: r.region, T, region: r, activeExit, onSetExit }))))));
4291
4320
  }
4292
4321
  Object.assign(window, { NetworkTab });
4322
+ var dkAudioCtx = null;
4323
+ function dkAudio() {
4324
+ if (dkAudioCtx && dkAudioCtx.state !== "closed")
4325
+ return dkAudioCtx;
4326
+ try {
4327
+ dkAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
4328
+ } catch (e) {
4329
+ dkAudioCtx = null;
4330
+ }
4331
+ return dkAudioCtx;
4332
+ }
4333
+ function dkUnlockAudioOnGesture() {
4334
+ const unlock = () => {
4335
+ const c = dkAudio();
4336
+ if (c && c.state === "suspended")
4337
+ c.resume().catch(() => {
4338
+ });
4339
+ };
4340
+ window.addEventListener("pointerdown", unlock, { passive: true });
4341
+ window.addEventListener("keydown", unlock, { passive: true });
4342
+ return () => {
4343
+ window.removeEventListener("pointerdown", unlock);
4344
+ window.removeEventListener("keydown", unlock);
4345
+ };
4346
+ }
4347
+ function dkChime(kind) {
4348
+ const c = dkAudio();
4349
+ if (!c)
4350
+ return;
4351
+ if (c.state === "suspended")
4352
+ c.resume().catch(() => {
4353
+ });
4354
+ const notes = kind === "request" ? [[660, 0], [880, 0.14]] : [[880, 0], [1175, 0.11]];
4355
+ const t0 = c.currentTime;
4356
+ for (const [freq, at] of notes) {
4357
+ const o = c.createOscillator(), g = c.createGain();
4358
+ o.type = "sine";
4359
+ o.frequency.value = freq;
4360
+ o.connect(g);
4361
+ g.connect(c.destination);
4362
+ const t = t0 + at;
4363
+ g.gain.setValueAtTime(1e-4, t);
4364
+ g.gain.exponentialRampToValueAtTime(0.18, t + 0.015);
4365
+ g.gain.exponentialRampToValueAtTime(1e-4, t + 0.17);
4366
+ o.start(t);
4367
+ o.stop(t + 0.2);
4368
+ }
4369
+ }
4370
+ var dkFlash = { timer: null, title: "", link: null, href: null, badged: null, on: false, n: 0, label: "" };
4371
+ function dkBadgedIcon(href) {
4372
+ return new Promise((res, rej) => {
4373
+ const img = new Image();
4374
+ img.onload = () => {
4375
+ try {
4376
+ if (!img.naturalWidth)
4377
+ return rej(new Error("icon has no intrinsic size"));
4378
+ const s = 64, cv = document.createElement("canvas");
4379
+ cv.width = s;
4380
+ cv.height = s;
4381
+ const cx = cv.getContext("2d");
4382
+ cx.drawImage(img, 0, 0, s, s);
4383
+ cx.beginPath();
4384
+ cx.arc(s - 14, 14, 12, 0, Math.PI * 2);
4385
+ cx.fillStyle = "#e5484d";
4386
+ cx.fill();
4387
+ cx.lineWidth = 3;
4388
+ cx.strokeStyle = "#fff";
4389
+ cx.stroke();
4390
+ res(cv.toDataURL("image/png"));
4391
+ } catch (e) {
4392
+ rej(e);
4393
+ }
4394
+ };
4395
+ img.onerror = () => rej(new Error("icon failed to load"));
4396
+ img.src = href;
4397
+ });
4398
+ }
4399
+ function dkFlashTick() {
4400
+ dkFlash.on = !dkFlash.on;
4401
+ document.title = `(${dkFlash.n}) ${dkFlash.on ? dkFlash.label : dkFlash.title}`;
4402
+ }
4403
+ function dkFlashStart(label, count) {
4404
+ dkFlash.n += count || 1;
4405
+ dkFlash.label = label;
4406
+ if (dkFlash.timer)
4407
+ return;
4408
+ dkFlash.title = document.title;
4409
+ dkFlash.link = document.querySelector('link[rel~="icon"]');
4410
+ dkFlash.href = dkFlash.link ? dkFlash.link.href : null;
4411
+ dkFlash.badged = null;
4412
+ dkFlash.on = false;
4413
+ if (dkFlash.href) {
4414
+ dkBadgedIcon(dkFlash.href).then((b) => {
4415
+ dkFlash.badged = b;
4416
+ if (dkFlash.timer && dkFlash.link)
4417
+ dkFlash.link.href = b;
4418
+ }).catch(() => {
4419
+ });
4420
+ }
4421
+ dkFlashTick();
4422
+ dkFlash.timer = setInterval(dkFlashTick, 1e3);
4423
+ }
4424
+ function dkFlashStop() {
4425
+ if (!dkFlash.timer)
4426
+ return;
4427
+ clearInterval(dkFlash.timer);
4428
+ dkFlash.timer = null;
4429
+ dkFlash.n = 0;
4430
+ document.title = dkFlash.title;
4431
+ if (dkFlash.link && dkFlash.href)
4432
+ dkFlash.link.href = dkFlash.href;
4433
+ }
4434
+ function dkLooking() {
4435
+ return document.visibilityState === "visible" && document.hasFocus();
4436
+ }
4437
+ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true }) {
4438
+ const prev = React.useRef(null);
4439
+ React.useEffect(() => dkUnlockAudioOnGesture(), []);
4440
+ React.useEffect(() => {
4441
+ if (!flash)
4442
+ dkFlashStop();
4443
+ }, [flash]);
4444
+ React.useEffect(() => {
4445
+ const seen = () => {
4446
+ if (dkLooking())
4447
+ dkFlashStop();
4448
+ };
4449
+ window.addEventListener("focus", seen);
4450
+ window.addEventListener("pointerdown", seen, { passive: true });
4451
+ document.addEventListener("visibilitychange", seen);
4452
+ return () => {
4453
+ window.removeEventListener("focus", seen);
4454
+ window.removeEventListener("pointerdown", seen);
4455
+ document.removeEventListener("visibilitychange", seen);
4456
+ dkFlashStop();
4457
+ };
4458
+ }, []);
4459
+ React.useEffect(() => {
4460
+ if (!loaded)
4461
+ return;
4462
+ const byId = /* @__PURE__ */ new Map();
4463
+ for (const p of peers || []) {
4464
+ const dir = p.lastDir || (String(p.lastMsg || "").startsWith("you: ") ? "out" : "in");
4465
+ byId.set(p.id, { ts: p.lastTs || 0, dir });
4466
+ }
4467
+ const reqIds = new Set((requests || []).map((r) => r.id));
4468
+ const was = prev.current;
4469
+ prev.current = { byId, reqIds };
4470
+ if (!was)
4471
+ return;
4472
+ let msgs = 0, elsewhere = 0, reqs = 0;
4473
+ for (const [id, cur] of byId) {
4474
+ if (cur.dir !== "in" || !cur.ts)
4475
+ continue;
4476
+ const old = was.byId.get(id);
4477
+ if (old ? cur.ts <= old.ts : Date.now() - cur.ts > 3e4)
4478
+ continue;
4479
+ msgs += 1;
4480
+ if (id !== activeId)
4481
+ elsewhere += 1;
4482
+ }
4483
+ for (const id of reqIds)
4484
+ if (!was.reqIds.has(id))
4485
+ reqs += 1;
4486
+ if (!msgs && !reqs)
4487
+ return;
4488
+ const looking = dkLooking();
4489
+ if (sound && !(looking && !reqs && !elsewhere))
4490
+ dkChime(reqs && !msgs ? "request" : "message");
4491
+ if (flash && !looking) {
4492
+ const label = msgs ? T && T.alertMsg || "New message" : T && T.alertReq || "New friend request";
4493
+ dkFlashStart(label, msgs + reqs);
4494
+ }
4495
+ }, [peers, requests, loaded, activeId, T, sound, flash]);
4496
+ }
4293
4497
  var TWEAKS_STORAGE_KEY = "decentlan.tweaks";
4294
4498
  function useTweaks(defaults) {
4295
4499
  const [values, setValues] = React.useState(() => {
@@ -4347,7 +4551,9 @@ ${peer.address}`
4347
4551
  "accent": "#7B6CF6",
4348
4552
  "density": "compact",
4349
4553
  "lang": "en",
4350
- "startTab": "chat"
4554
+ "startTab": "chat",
4555
+ "alertSound": true,
4556
+ "alertFlash": true
4351
4557
  }
4352
4558
  );
4353
4559
  function dkDetectLang() {
@@ -4416,6 +4622,8 @@ ${peer.address}`
4416
4622
  network: "Network",
4417
4623
  apps: "Apps",
4418
4624
  profile: "Profile",
4625
+ alertMsg: "New message",
4626
+ alertReq: "New friend request",
4419
4627
  recommended: "Discover",
4420
4628
  registered: "Names",
4421
4629
  here: "Here",
@@ -4596,6 +4804,8 @@ ${peer.address}`
4596
4804
  network: "\u7F51\u7EDC",
4597
4805
  apps: "\u5E94\u7528",
4598
4806
  profile: "\u6211\u7684",
4807
+ alertMsg: "\u65B0\u6D88\u606F",
4808
+ alertReq: "\u65B0\u7684\u597D\u53CB\u8BF7\u6C42",
4599
4809
  recommended: "\u63A8\u8350",
4600
4810
  registered: "\u540D\u5F55",
4601
4811
  here: "\u5728\u573A",
@@ -4988,6 +5198,15 @@ ${peer.address}`
4988
5198
  const T = STR[t.lang] || STR.en;
4989
5199
  const vars = dkTheme(t.theme, t.accent);
4990
5200
  const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
5201
+ useIncomingAlerts({
5202
+ T,
5203
+ peers,
5204
+ requests,
5205
+ loaded: data.loaded,
5206
+ activeId,
5207
+ sound: t.alertSound !== false,
5208
+ flash: t.alertFlash !== false
5209
+ });
4991
5210
  const callCtl = useCallController(me.userId, (peerId, video, direction) => {
4992
5211
  const text = `WebRTC ${video ? "video" : "audio"} call: ${direction}`;
4993
5212
  dkApi.logLocal(peerId, direction === "outgoing" ? "out" : "in", text).then(() => {
@@ -5129,10 +5348,16 @@ ${peer.address}`
5129
5348
  { id: "recommended", icon: "sparkles", label: T.recommended },
5130
5349
  { id: "registered", icon: "at", label: T.registered },
5131
5350
  { id: "network", icon: "network", label: T.network },
5132
- { id: "apps", icon: "grid", label: T.apps },
5133
- { id: "profile", icon: "userRound", label: T.profile }
5351
+ { id: "apps", icon: "grid", label: T.apps }
5134
5352
  ];
5135
- return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__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" } }, /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__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)" } }, "\xB7 ", nav.find((n) => n.id === tab).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__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)" } }), /* @__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))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr("") }), 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 }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak })), data.locked && /* @__PURE__ */ React.createElement(
5353
+ return /* @__PURE__ */ React.createElement("div", { style: { ...vars, "--row-pad": rowPad, position: "fixed", inset: 0, display: "flex", background: "var(--bg)", color: "var(--text)", fontFamily: "var(--ui)" } }, /* @__PURE__ */ React.createElement("div", { style: { width: 68, flexShrink: 0, borderRight: "1px solid var(--line)", background: "var(--rail)", display: "flex", flexDirection: "column", alignItems: "center", padding: "14px 0", gap: 8 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 38, height: 38, borderRadius: 10, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 8 } }, /* @__PURE__ */ React.createElement(Icon, { name: "terminal", size: 20, color: "#fff", stroke: 2.2 })), nav.map((n) => /* @__PURE__ */ React.createElement(RailBtn, { key: n.id, icon: n.icon, label: n.label, active: tab === n.id, soon: n.soon, onClick: () => setTab(n.id) })), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement("button", { onClick: () => setTab("profile"), title: T.profile, style: {
5354
+ padding: 4,
5355
+ borderRadius: 12,
5356
+ cursor: "pointer",
5357
+ display: "flex",
5358
+ border: "1px solid " + (tab === "profile" ? "var(--line)" : "transparent"),
5359
+ background: tab === "profile" ? "var(--panel-2)" : "transparent"
5360
+ } }, /* @__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" } }, /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__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)" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__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)" } }), /* @__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))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr("") }), 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 }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak })), data.locked && /* @__PURE__ */ React.createElement(
5136
5361
  DkLockedOut,
5137
5362
  {
5138
5363
  compact: true,
@@ -90,31 +90,30 @@ export class EmbeddedHost {
90
90
  this.#messages = new MessageStore(resolve(opts.configDir, "messages.json"));
91
91
  this.#meta = new FriendMetaStore(resolve(opts.configDir, "friends-meta.json"));
92
92
  }
93
- /** Friend-list order: pinned first, then most recent conversation, then
94
- * never-messaged contacts by name. Returns a new array — `friends()`
95
- * hands back the SDK's own list and sorting it in place would reorder
96
- * the friend store as a side effect of merely rendering the sidebar. */
93
+ /** Friend-list order: pinned first, then most recent ACTIVITY, then by
94
+ * name. Activity is the newest of the last message, the friendship being
95
+ * accepted, and our request going out — so a friend you added or accepted
96
+ * a moment ago ranks exactly as if they had just written to you. The
97
+ * earlier rule sorted by last message alone and ranked new friends only
98
+ * among the never-messaged group at the bottom, which on a long list still
99
+ * put them below the fold and made the accept look like a no-op. Returns
100
+ * a new array — `friends()` hands back the SDK's own list and sorting it
101
+ * in place would reorder the friend store as a side effect of merely
102
+ * rendering the sidebar. */
97
103
  #sortForList(friends, last) {
98
104
  const rank = (f) => [
99
105
  this.#meta.get(f.carrierId)?.pinned ? 0 : 1,
100
- // Negated so a LARGER timestamp sorts first. No conversation →
101
- // MAX_SAFE_INTEGER, which parks those entries after every real one.
102
- // Not Infinity: two never-messaged friends would then compare
103
- // Infinity - Infinity = NaN, and the tie-break would only still work
104
- // by accident of NaN being falsy.
105
- last.has(f.carrierId) ? -last.get(f.carrierId).ts : Number.MAX_SAFE_INTEGER,
106
- // Among friends you have never messaged, NEWEST FRIENDSHIP first.
107
- // Alphabetical put someone you accepted ten seconds ago wherever their
108
- // name happened to land — usually far down a 50-name list, so accepting
109
- // a request looked like nothing had happened. The one friend you are
110
- // most likely to open is the one you just added.
111
- -(f.acceptedAt ?? 0),
106
+ // Negated so a LARGER timestamp sorts first. Nothing at all → 0, which
107
+ // parks the entry after every real one; a plain number, so two such
108
+ // entries compare 0 - 0 = 0 and fall through to the name (Infinity
109
+ // would give NaN there).
110
+ -Math.max(last.get(f.carrierId)?.ts ?? 0, f.acceptedAt ?? 0, f.requestedAt ?? 0),
112
111
  (f.name || f.carrierId).toLowerCase(),
113
112
  ];
114
113
  return [...friends].sort((a, b) => {
115
- const [ap, at, aa, an] = rank(a);
116
- const [bp, bt, ba, bn] = rank(b);
117
- return ap - bp || at - bt || aa - ba || an.localeCompare(bn);
114
+ const [ap, at, an] = rank(a);
115
+ const [bp, bt, bn] = rank(b);
116
+ return ap - bp || at - bt || an.localeCompare(bn);
118
117
  });
119
118
  }
120
119
  get downloadsDir() {
@@ -459,6 +458,10 @@ export class EmbeddedHost {
459
458
  f.status === "online" ||
460
459
  this.#messages.hasInbound(f.carrierId),
461
460
  lastSeen: f.acceptedAt,
461
+ // When WE sent the request. Counts as activity for the sidebar
462
+ // order: someone you just added belongs at the top while you
463
+ // wait for them, not filed by name at the bottom.
464
+ requestedAt: f.requestedAt,
462
465
  // The avatar they advertised over Carrier (userinfo `gender`).
463
466
  // Peer-to-peer, so it works for a friend who never registered a
464
467
  // beagles.eth name — until now that meant an identicon.
package/dist/server.js CHANGED
@@ -1975,11 +1975,19 @@ export function startBeagleServer(opts) {
1975
1975
  // be ordered, which is why the sidebar never reordered on new
1976
1976
  // activity.
1977
1977
  lastTs: lm?.ts ?? 0,
1978
- // When the friendship was established. The sidebar sorts by last
1979
- // message and falls back to this, so a friend you just accepted —
1980
- // who by definition has no messages — lands at the top of the
1981
- // no-history group instead of alphabetically halfway down it.
1978
+ // Which way the last message went. The UI's new-message alert
1979
+ // needs to know an inbound stamp moved, and inferring that from
1980
+ // the "you: " prefix on the preview is a convention, not a field.
1981
+ lastDir: lm?.dir ?? null,
1982
+ // When the friendship was established, and when we asked for it.
1983
+ // The sidebar ranks a friend by the newest of these and the last
1984
+ // message, so someone you just accepted — or just added — lands
1985
+ // at the top like a fresh message would, not in a no-history
1986
+ // group at the bottom. A daemon backend reports only lastSeen
1987
+ // (its acceptedAt); requestedAt is embedded-only and reads 0
1988
+ // elsewhere.
1982
1989
  acceptedAt: f.lastSeen ?? 0,
1990
+ requestedAt: f.requestedAt ?? 0,
1983
1991
  wire: "163",
1984
1992
  avatarUrl: ensPub.get(uid)?.avatarUrl ?? null,
1985
1993
  // What they advertised over Carrier wins over the directory: it is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
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",