@auxilium/datalynk-client 1.6.3 → 1.6.4

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.
package/dist/chat.cjs CHANGED
@@ -1394,6 +1394,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1394
1394
  __publicField(this, "socketUnsubscribe");
1395
1395
  __publicField(this, "typingTimers", /* @__PURE__ */ new Map());
1396
1396
  __publicField(this, "initialized", false);
1397
+ __publicField(this, "resyncing");
1398
+ __publicField(this, "readVisibility", () => false);
1399
+ __publicField(this, "directorySeq", 0);
1397
1400
  this.api = api;
1398
1401
  }
1399
1402
  async initialize(force = false) {
@@ -1419,14 +1422,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1419
1422
  }
1420
1423
  async loadDirectory(search = "", cursor) {
1421
1424
  const query = search.trim();
1422
- if (query.length < 2) {
1423
- this.users$.next([]);
1424
- return [];
1425
- }
1425
+ const seq = ++this.directorySeq;
1426
1426
  const response = await this.run({
1427
- "$/chat/directory": { query, cursor, limit: 25 }
1427
+ "$/chat/directory": { query, cursor, limit: 10 }
1428
1428
  });
1429
1429
  const users = response.users || [];
1430
+ if (seq !== this.directorySeq) return users;
1430
1431
  this.users$.next(users);
1431
1432
  const selected = this.selectedUser$.value;
1432
1433
  const replacement = selected && users.find((user) => user.id === selected.id);
@@ -1456,7 +1457,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1456
1457
  "$/chat/history": { userId, before, limit: 75 }
1457
1458
  });
1458
1459
  const messages = (response.messages || []).map((message) => ({ ...message, status: "sent" }));
1459
- this.messages$.next(before ? this.mergeMessages(messages, this.messages$.value) : messages);
1460
+ if (this.selectedUser$.value && this.selectedUser$.value.id !== userId) return messages;
1461
+ this.messages$.next(this.sortMessages(this.mergeMessages(messages, this.messages$.value)));
1460
1462
  this.hasMore$.next(Boolean(response.hasMore));
1461
1463
  return messages;
1462
1464
  } finally {
@@ -1499,7 +1501,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1499
1501
  async setTyping(userId, typing) {
1500
1502
  await this.run({ "$/chat/typing": { userId, typing } }, true);
1501
1503
  }
1504
+ /** Only the visible chat window may acknowledge messages as seen. */
1505
+ setReadVisibility(visible) {
1506
+ this.readVisibility = visible;
1507
+ return () => {
1508
+ if (this.readVisibility === visible) this.readVisibility = () => false;
1509
+ };
1510
+ }
1502
1511
  async markLatestRead() {
1512
+ if (!this.readVisibility()) return;
1503
1513
  const peer = this.selectedUser$.value;
1504
1514
  const messages = this.messages$.value;
1505
1515
  const latest = [...messages].reverse().find((message) => message.senderId === (peer == null ? void 0 : peer.id) && !message.id.startsWith("pending:"));
@@ -1524,17 +1534,43 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1524
1534
  () => this.api.socket.send({ chat: { subscribe: true } })
1525
1535
  );
1526
1536
  }
1537
+ /**
1538
+ * Catch up after every (re)subscribe. Events published while the socket was down
1539
+ * (up to the reconnect delay plus ticket fetch) are never replayed, so reload the
1540
+ * inbox and the newest page of the open thread from the API.
1541
+ */
1542
+ resync() {
1543
+ if (this.resyncing) return this.resyncing;
1544
+ this.resyncing = (async () => {
1545
+ var _a;
1546
+ try {
1547
+ await this.loadConversations();
1548
+ const peer = this.selectedUser$.value;
1549
+ if (!peer) return;
1550
+ const response = await this.run({ "$/chat/history": { userId: peer.id, limit: 75 } }, true);
1551
+ if (((_a = this.selectedUser$.value) == null ? void 0 : _a.id) !== peer.id) return;
1552
+ const latest = (response.messages || []).map((message) => ({ ...message, status: "sent" }));
1553
+ this.messages$.next(this.sortMessages(this.mergeMessages(latest, this.messages$.value)));
1554
+ await this.markLatestRead();
1555
+ } catch {
1556
+ } finally {
1557
+ this.resyncing = void 0;
1558
+ }
1559
+ })();
1560
+ return this.resyncing;
1561
+ }
1527
1562
  onSocketEvent(event) {
1528
1563
  var _a, _b, _c, _d, _e;
1529
1564
  if (!(event == null ? void 0 : event.type) || event.type.indexOf("chat.") !== 0) return;
1530
- if (event.type === "chat.message") {
1565
+ if (event.type === "chat.subscribed") {
1566
+ void this.resync();
1567
+ } else if (event.type === "chat.message") {
1531
1568
  const message = (_a = event.data) == null ? void 0 : _a.message;
1532
1569
  const selected = this.selectedUser$.value;
1533
1570
  if (message && selected && (message.senderId === selected.id || message.recipientId === selected.id)) {
1534
1571
  this.upsertMessage({ ...message, status: "sent" });
1535
- if (message.senderId === selected.id) void this.markLatestRead();
1536
1572
  }
1537
- void this.loadConversations();
1573
+ void this.loadConversations().then(() => this.markLatestRead()).catch(() => void 0);
1538
1574
  } else if (event.type === "chat.read") {
1539
1575
  this.applyReadReceipt(event.data);
1540
1576
  void this.loadConversations();
@@ -1588,6 +1624,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1588
1624
  this.messages$.next(next);
1589
1625
  }
1590
1626
  }
1627
+ /** Stream order for persisted messages; optimistic/failed sends stay at the end in send order. */
1628
+ sortMessages(messages) {
1629
+ const persisted = messages.filter((message) => !message.id.startsWith("pending:"));
1630
+ const pending = messages.filter((message) => message.id.startsWith("pending:"));
1631
+ return [...persisted.sort((a, b) => this.compareStreamIds(a.id, b.id)), ...pending];
1632
+ }
1591
1633
  mergeMessages(first, second) {
1592
1634
  const out = [];
1593
1635
  const seen = /* @__PURE__ */ new Set();
@@ -1626,16 +1668,23 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1626
1668
  }
1627
1669
  }
1628
1670
  const icon = (path) => `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${path}"/></svg>`;
1671
+ const SOUND_ON_ICON = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05A4.47 4.47 0 0 0 16.5 12zM14 3.23v2.06a7 7 0 0 1 0 13.42v2.06A9 9 0 0 0 14 3.23z";
1672
+ const SOUND_OFF_ICON = "M16.5 12A4.5 4.5 0 0 0 14 7.97v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.8 8.8 0 0 0 21 12a9 9 0 0 0-7-8.77v2.06A7 7 0 0 1 19 12zM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a9 9 0 0 0 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4 9.91 6.09 12 8.18V4z";
1629
1673
  const CHAT_HTML = `
1630
1674
  <section class="dl-chat" role="dialog" aria-label="Datalynk Chat">
1631
1675
  <header class="dl-chat__titlebar">
1632
- <div class="dl-chat__brand"><span class="dl-chat__mark">${icon("M4 5h16v11H9l-5 4V5zm3 4v2h2V9H7zm4 0v2h2V9h-2zm4 0v2h2V9h-2z")}</span><strong>Chat</strong></div>
1676
+ <div class="dl-chat__brand"><span class="dl-chat__mark">${icon("M4 5h16v11H9l-5 4V5zm3 4v2h2V9H7zm4 0v2h2V9h-2zm4 0v2h2V9h-2z")}</span><strong>Chat</strong><span class="dl-chat__title-unread" aria-live="polite" hidden></span></div>
1633
1677
  <div class="dl-chat__window-actions">
1678
+ <button class="dl-chat__icon-button dl-chat__sound" type="button" aria-pressed="false" aria-label="Mute message sound" title="Mute message sound">${icon(SOUND_ON_ICON)}</button>
1634
1679
  <button class="dl-chat__icon-button dl-chat__popout" type="button" aria-label="Open chat in a separate window" title="Open in new window">${icon("M14 3h7v7h-2V6.41l-9.29 9.3-1.42-1.42 9.3-9.29H14V3zM5 5h6v2H5v12h12v-6h2v8H3V5h2z")}</button>
1635
1680
  <button class="dl-chat__icon-button dl-chat__minimize" type="button" aria-label="Minimize chat" title="Minimize">${icon("M5 11h14v2H5z")}</button>
1636
1681
  <button class="dl-chat__icon-button dl-chat__close" type="button" aria-label="Close chat" title="Close">${icon("M6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19 5 17.6 10.6 12 5 6.4z")}</button>
1637
1682
  </div>
1638
1683
  </header>
1684
+ <button class="dl-chat__restore" type="button" aria-label="Restore chat" hidden>
1685
+ <span class="dl-chat__mark">${icon("M4 5h16v11H9l-5 4V5zm3 4v2h2V9H7zm4 0v2h2V9h-2zm4 0v2h2V9h-2z")}</span>
1686
+ <span><strong>Chat</strong><small>Open conversations</small></span>
1687
+ </button>
1639
1688
  <div class="dl-chat__connection" role="status" hidden></div>
1640
1689
  <div class="dl-chat__error" role="alert" hidden></div>
1641
1690
  <div class="dl-chat__body">
@@ -1681,25 +1730,39 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1681
1730
  :host{all:initial;position:fixed;inset:0;z-index:2147483000;pointer-events:none;color:#17242b;font-family:"Segoe UI",Tahoma,Arial,sans-serif}
1682
1731
  *{box-sizing:border-box}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
1683
1732
  button,input,textarea{font:inherit}
1684
- .dl-chat{--teal:#3f6f85;--teal-deep:#2f596d;--teal-soft:#e9f1f4;--red:#c42d27;--green:#599e21;--ink:#17242b;--muted:#60717a;--line:#d7e0e4;position:fixed;right:22px;bottom:22px;width:min(720px,calc(100vw - 32px));height:min(640px,calc(100dvh - 32px));min-width:360px;min-height:460px;display:flex;flex-direction:column;overflow:hidden;resize:both;pointer-events:auto;background:#fff;border:1px solid #c7d2d7;border-radius:10px;box-shadow:0 18px 54px rgba(23,36,43,.24),0 3px 10px rgba(23,36,43,.12);container-type:inline-size}
1733
+ .dl-chat{--teal:var(--primary,#3f6f85);--teal-fore:var(--primary-fore,#fff);--teal-deep:color-mix(in srgb,var(--teal) 78%,#000);--teal-soft:color-mix(in srgb,var(--teal) 12%,#fff);--teal-wash:color-mix(in srgb,var(--teal) 5%,#fff);--teal-hover:color-mix(in srgb,var(--teal) 10%,#fff);--teal-selected:color-mix(in srgb,var(--teal) 17%,#fff);--red:var(--accent,#c42d27);--red-fore:var(--accentText,#fff);--green:#599e21;--ink:#17242b;--muted:#60717a;--line:#d7e0e4;position:fixed;right:22px;bottom:22px;width:min(720px,calc(100vw - 32px));height:min(640px,calc(100dvh - 32px));min-width:360px;min-height:460px;display:flex;flex-direction:column;overflow:hidden;resize:both;pointer-events:auto;background:#fff;border:1px solid #c7d2d7;border-radius:10px;box-shadow:0 18px 54px rgba(23,36,43,.24),0 3px 10px rgba(23,36,43,.12);container-type:inline-size}
1685
1734
  .dl-chat.is-popout{inset:0;width:100%;height:100dvh;min-width:0;min-height:0;resize:none;border:0;border-radius:0;box-shadow:none}.dl-chat.is-popout .dl-chat__minimize{display:none}
1686
- .dl-chat__titlebar{height:46px;flex:0 0 46px;display:flex;align-items:center;justify-content:space-between;padding:0 8px 0 14px;color:#fff;background:var(--teal);cursor:move;touch-action:none;user-select:none}
1735
+ .dl-chat__titlebar{height:46px;flex:0 0 46px;display:flex;align-items:center;justify-content:space-between;padding:0 8px 0 14px;color:var(--teal-fore);background:var(--teal);cursor:move;touch-action:none;user-select:none}
1687
1736
  .dl-chat__brand{display:flex;align-items:center;gap:9px;font-size:15px;letter-spacing:.01em}.dl-chat__mark{display:grid;place-items:center;width:24px;height:24px}.dl-chat__mark svg{width:22px;height:22px;fill:currentColor}
1737
+ .dl-chat__title-unread{min-width:20px;height:20px;padding:0 6px;border-radius:10px;font-size:12px;font-weight:700;line-height:20px;text-align:center;color:var(--red-fore);background:var(--red);box-shadow:0 0 0 2px rgba(255,255,255,.85)}.dl-chat__title-unread[hidden]{display:none}
1688
1738
  .dl-chat__window-actions{display:flex;gap:2px}.dl-chat__icon-button{display:grid;place-items:center;width:34px;height:34px;padding:0;border:0;border-radius:5px;color:inherit;background:transparent;cursor:pointer;transition:background-color 120ms ease-out,color 120ms ease-out}.dl-chat__icon-button:hover{background:rgba(255,255,255,.14)}.dl-chat__icon-button:focus-visible,.dl-chat button:focus-visible,.dl-chat input:focus-visible,.dl-chat textarea:focus-visible{outline:2px solid #f2c94c;outline-offset:2px}.dl-chat__icon-button svg{width:18px;height:18px;fill:currentColor}
1689
1739
  .dl-chat__connection,.dl-chat__error{padding:7px 14px;font-size:12px;line-height:1.35}.dl-chat__connection{color:#654d00;background:#fff4c7;border-bottom:1px solid #e8d68c}.dl-chat__error{color:#7b201c;background:#fbe9e8;border-bottom:1px solid #efbfbc}
1690
- .dl-chat__body{min-height:0;flex:1;display:grid;grid-template-columns:260px minmax(0,1fr)}.dl-chat__sidebar{min-width:0;display:flex;flex-direction:column;background:#f4f7f8;border-right:1px solid var(--line)}
1691
- .dl-chat__search-wrap{position:relative;display:block;margin:14px 12px 9px}.dl-chat__search-wrap>svg{position:absolute;left:10px;top:50%;width:17px;height:17px;fill:#70818a;transform:translateY(-50%);pointer-events:none}.dl-chat__search{width:100%;height:36px;padding:0 10px 0 34px;color:var(--ink);background:#fff;border:1px solid #c7d2d7;border-radius:6px;outline:0}.dl-chat__search:focus,.dl-chat__composer:focus{border-color:var(--teal);box-shadow:0 0 0 2px rgba(63,111,133,.15)}
1692
- .dl-chat__scroll{min-height:0;overflow:auto;padding:0 8px 16px}.dl-chat__scroll h2{margin:14px 8px 6px;color:#63757e;font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase}
1693
- .dl-chat__person{position:relative;width:100%;display:grid;grid-template-columns:38px minmax(0,1fr) 8px;align-items:center;gap:10px;padding:8px;border:0;border-radius:7px;text-align:left;color:var(--ink);background:transparent;cursor:pointer;transition:background-color 120ms ease-out}.dl-chat__person:hover{background:#e5edef}.dl-chat__person.is-selected{background:#dce8ed}.dl-chat__avatar{display:grid;place-items:center;width:38px;height:38px;border-radius:50%;color:#fff;background:var(--teal);font-size:12px;font-weight:700;letter-spacing:.03em}.dl-chat__person-copy{min-width:0}.dl-chat__person-top,.dl-chat__person-bottom{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0}.dl-chat__person-top strong,.dl-chat__person-bottom>span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-chat__person-top strong{font-size:13px;font-weight:600}.dl-chat__person-top time{flex:0 0 auto;color:#778891;font-size:10px}.dl-chat__person-bottom>span:first-child{color:var(--muted);font-size:11px}.dl-chat__presence{width:8px;height:8px;border:2px solid #87969d;border-radius:50%}.dl-chat__presence.is-online{border-color:var(--green);background:var(--green)}.dl-chat__unread{display:grid;place-items:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;color:#fff!important;background:var(--red);font-size:10px!important;font-weight:700}.dl-chat__list-empty{margin:6px 8px 12px;color:var(--muted);font-size:12px;line-height:1.45}
1694
- .dl-chat__main{min-width:0;min-height:0}.dl-chat__empty{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:36px;text-align:center}.dl-chat__empty-icon{display:grid;place-items:center;width:58px;height:58px;color:var(--teal);background:var(--teal-soft);border-radius:50%}.dl-chat__empty-icon svg{width:27px;height:27px;fill:currentColor}.dl-chat__empty h2{margin:17px 0 5px;font-size:17px}.dl-chat__empty p{max-width:280px;margin:0;color:var(--muted);font-size:13px;line-height:1.5}
1695
- .dl-chat__thread{height:100%;display:grid;grid-template-rows:auto auto minmax(0,1fr) auto auto}.dl-chat__thread[hidden],.dl-chat__empty[hidden]{display:none}.dl-chat__thread-header{height:62px;display:flex;align-items:center;gap:10px;padding:0 16px;border-bottom:1px solid var(--line)}.dl-chat__thread-header .dl-chat__avatar{width:36px;height:36px;flex:0 0 36px}.dl-chat__thread-header>div{min-width:0;display:flex;flex-direction:column}.dl-chat__thread-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}.dl-chat__thread-status{position:relative;padding-left:11px;color:var(--muted);font-size:11px}.dl-chat__thread-status:before{content:"";position:absolute;left:0;top:50%;width:7px;height:7px;border:1.5px solid #87969d;border-radius:50%;transform:translateY(-50%)}.dl-chat__thread-status.is-online:before{border-color:var(--green);background:var(--green)}.dl-chat__back{display:none;color:var(--teal)}
1696
- .dl-chat__older{justify-self:center;margin:7px 0 0;padding:4px 8px;border:0;color:var(--teal-deep);background:transparent;font-size:11px;cursor:pointer}.dl-chat__older:hover{text-decoration:underline}.dl-chat__messages{min-height:0;overflow-y:auto;padding:12px 18px 18px;scroll-behavior:smooth}.dl-chat__date{display:flex;align-items:center;gap:10px;margin:12px 0;color:#71838c;font-size:10px}.dl-chat__date:before,.dl-chat__date:after{content:"";height:1px;flex:1;background:#e3e9ec}.dl-chat__message{display:flex;margin:4px 0}.dl-chat__message.is-mine{justify-content:flex-end}.dl-chat__bubble{max-width:min(76%,430px);padding:8px 10px 6px;color:var(--ink);background:#edf2f4;border-radius:5px 11px 11px 11px;font-size:13px;line-height:1.42;overflow-wrap:anywhere}.dl-chat__message.is-mine .dl-chat__bubble{color:#fff;background:var(--teal);border-radius:11px 5px 11px 11px}.dl-chat__bubble>span{white-space:pre-wrap}.dl-chat__bubble time{display:block;margin-top:3px;color:#6c7e87;font-size:9px;text-align:right}.dl-chat__message.is-mine time{color:rgba(255,255,255,.72)}.dl-chat__retry{display:block;margin:5px 0 0 auto;padding:2px 7px;border:1px solid rgba(255,255,255,.65);border-radius:4px;color:#fff;background:transparent;font-size:10px;cursor:pointer}.dl-chat__retry:hover{background:rgba(255,255,255,.12)}.dl-chat__thread-empty{max-width:300px;margin:auto;padding:24px;color:var(--muted);font-size:12px;line-height:1.5;text-align:center}.dl-chat__typing{height:20px;padding:0 18px;color:var(--muted);font-size:11px;font-style:italic}
1697
- .dl-chat__compose{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 38px;gap:8px;padding:10px 14px 22px;border-top:1px solid var(--line)}.dl-chat__composer{width:100%;min-height:38px;max-height:92px;resize:none;padding:9px 11px;color:var(--ink);border:1px solid #bdcbd1;border-radius:7px;line-height:1.35;outline:0}.dl-chat__send{display:grid;place-items:center;width:38px;height:38px;padding:0;border:0;border-radius:7px;color:#fff;background:var(--teal);cursor:pointer;transition:background-color 120ms ease-out}.dl-chat__send:hover{background:var(--teal-deep)}.dl-chat__send:disabled{cursor:not-allowed;opacity:.45}.dl-chat__send svg{width:19px;height:19px;fill:currentColor}.dl-chat__hint{position:absolute;left:15px;bottom:5px;color:#829098;font-size:9px}
1698
- .dl-chat.is-minimized{left:auto!important;top:auto!important;right:22px!important;bottom:0!important;width:280px!important;height:46px!important;min-height:46px;resize:none;border-radius:9px 9px 0 0}.dl-chat.is-minimized .dl-chat__titlebar{cursor:pointer}.dl-chat.is-minimized .dl-chat__body,.dl-chat.is-minimized .dl-chat__connection,.dl-chat.is-minimized .dl-chat__error{display:none}
1699
- @container (max-width:520px){.dl-chat__body{grid-template-columns:1fr}.dl-chat__main{display:none}.dl-chat.has-selection .dl-chat__sidebar{display:none}.dl-chat.has-selection .dl-chat__main{display:block}.dl-chat__back{display:grid}.dl-chat__hint{display:none}.dl-chat__compose{padding-bottom:10px}}
1700
- @media(max-width:600px){.dl-chat{inset:0;width:100%;height:100dvh;min-width:0;min-height:0;border:0;border-radius:0;resize:none}.dl-chat.is-minimized{inset:auto 12px 0 auto!important;width:min(280px,calc(100vw - 24px))!important;border:1px solid #c7d2d7;border-bottom:0}.dl-chat__titlebar{cursor:default}.dl-chat__popout{display:none}}
1740
+ .dl-chat__body{min-height:0;flex:1;display:grid;grid-template-columns:260px minmax(0,1fr);grid-template-rows:minmax(0,1fr)}.dl-chat__sidebar{min-width:0;min-height:0;display:flex;flex-direction:column;background:var(--teal-wash);border-right:1px solid var(--line)}
1741
+ .dl-chat__search-wrap{position:relative;display:block;margin:14px 12px 9px}.dl-chat__search-wrap>svg{position:absolute;left:10px;top:50%;width:17px;height:17px;fill:#70818a;transform:translateY(-50%);pointer-events:none}.dl-chat__search{width:100%;height:36px;padding:0 10px 0 34px;color:var(--ink);background:#fff;border:1px solid #c7d2d7;border-radius:6px;outline:0}.dl-chat__search:focus,.dl-chat__composer:focus{border-color:var(--teal);box-shadow:0 0 0 2px color-mix(in srgb,var(--teal) 15%,transparent)}
1742
+ .dl-chat__scroll{flex:1 1 auto;min-height:0;overflow:auto;padding:0 8px 16px}.dl-chat__scroll h2{margin:14px 8px 6px;color:#63757e;font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase}
1743
+ .dl-chat__person{position:relative;width:100%;display:grid;grid-template-columns:38px minmax(0,1fr) 8px;align-items:center;gap:10px;padding:8px;border:0;border-radius:7px;text-align:left;color:var(--ink);background:transparent;cursor:pointer;transition:background-color 120ms ease-out}.dl-chat__person:hover{background:var(--teal-hover)}.dl-chat__person.is-selected{background:var(--teal-selected)}.dl-chat__avatar{display:grid;place-items:center;width:38px;height:38px;border-radius:50%;color:var(--teal-fore);background:var(--teal);font-size:12px;font-weight:700;letter-spacing:.03em}.dl-chat__person-copy{min-width:0}.dl-chat__person-top,.dl-chat__person-bottom{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0}.dl-chat__person-top strong,.dl-chat__person-bottom>span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-chat__person-top strong{font-size:13px;font-weight:600}.dl-chat__person-top time{flex:0 0 auto;color:#778891;font-size:10px}.dl-chat__person-bottom>span:first-child{color:var(--muted);font-size:11px}.dl-chat__presence{width:8px;height:8px;border:2px solid #87969d;border-radius:50%}.dl-chat__presence.is-online{border-color:var(--green);background:var(--green)}.dl-chat__unread{display:grid;place-items:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;color:var(--red-fore)!important;background:var(--red);font-size:10px!important;font-weight:700}.dl-chat__list-empty{margin:6px 8px 12px;color:var(--muted);font-size:12px;line-height:1.45}
1744
+ .dl-chat__main{min-width:0;min-height:0;overflow:hidden;height:100%;display:flex;flex-direction:column}.dl-chat__empty{flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:36px;text-align:center}.dl-chat__empty-icon{display:grid;place-items:center;width:58px;height:58px;color:var(--teal);background:var(--teal-soft);border-radius:50%}.dl-chat__empty-icon svg{width:27px;height:27px;fill:currentColor}.dl-chat__empty h2{margin:17px 0 5px;font-size:17px}.dl-chat__empty p{max-width:280px;margin:0;color:var(--muted);font-size:13px;line-height:1.5}
1745
+ .dl-chat__thread{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}.dl-chat__thread[hidden],.dl-chat__empty[hidden]{display:none}.dl-chat__thread-header{flex:0 0 auto;height:62px;display:flex;align-items:center;gap:10px;padding:0 16px;border-bottom:1px solid var(--line)}.dl-chat__thread-header .dl-chat__avatar{width:36px;height:36px;flex:0 0 36px}.dl-chat__thread-header>div{min-width:0;display:flex;flex-direction:column}.dl-chat__thread-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}.dl-chat__thread-status{position:relative;padding-left:11px;color:var(--muted);font-size:11px}.dl-chat__thread-status:before{content:"";position:absolute;left:0;top:50%;width:7px;height:7px;border:1.5px solid #87969d;border-radius:50%;transform:translateY(-50%)}.dl-chat__thread-status.is-online:before{border-color:var(--green);background:var(--green)}.dl-chat__back{display:none;color:var(--teal)}
1746
+ .dl-chat__older{flex:0 0 auto;align-self:center;margin:7px 0 0;padding:4px 8px;border:0;color:var(--teal-deep);background:transparent;font-size:11px;cursor:pointer}.dl-chat__older:hover{text-decoration:underline}.dl-chat__messages{flex:1 1 auto;min-height:0;overflow-y:auto;padding:12px 18px 18px;scroll-behavior:smooth}.dl-chat__date{display:flex;align-items:center;gap:10px;margin:12px 0;color:#71838c;font-size:10px}.dl-chat__date:before,.dl-chat__date:after{content:"";height:1px;flex:1;background:#e3e9ec}.dl-chat__message{display:flex;margin:4px 0}.dl-chat__message.is-mine{justify-content:flex-end}.dl-chat__bubble{max-width:min(76%,430px);padding:8px 10px 6px;color:var(--ink);background:#edf2f4;border-radius:5px 11px 11px 11px;font-size:13px;line-height:1.42;overflow-wrap:anywhere}.dl-chat__message.is-mine .dl-chat__bubble{color:var(--teal-fore);background:var(--teal);border-radius:11px 5px 11px 11px}.dl-chat__bubble>span{white-space:pre-wrap}.dl-chat__bubble time{display:block;margin-top:3px;color:#6c7e87;font-size:9px;text-align:right}.dl-chat__message.is-mine time{color:color-mix(in srgb,var(--teal-fore) 72%,transparent)}.dl-chat__retry{display:block;margin:5px 0 0 auto;padding:2px 7px;border:1px solid rgba(255,255,255,.65);border-radius:4px;color:var(--teal-fore);background:transparent;font-size:10px;cursor:pointer}.dl-chat__retry:hover{background:rgba(255,255,255,.12)}.dl-chat__thread-empty{max-width:300px;margin:auto;padding:24px;color:var(--muted);font-size:12px;line-height:1.5;text-align:center}.dl-chat__typing{flex:0 0 auto;height:20px;padding:0 18px;color:var(--muted);font-size:11px;font-style:italic}
1747
+ .dl-chat__compose{flex:0 0 auto;position:relative;display:grid;grid-template-columns:minmax(0,1fr) 38px;gap:8px;padding:10px 14px 22px;border-top:1px solid var(--line)}.dl-chat__composer{width:100%;min-height:38px;max-height:92px;overflow-y:auto;resize:none;box-sizing:border-box;padding:9px 11px;color:var(--ink);border:1px solid #bdcbd1;border-radius:7px;line-height:1.35;outline:0}.dl-chat__send{display:grid;place-items:center;width:38px;height:38px;padding:0;border:0;border-radius:7px;color:var(--teal-fore);background:var(--teal);cursor:pointer;transition:background-color 120ms ease-out}.dl-chat__send:hover{background:var(--teal-deep)}.dl-chat__send:disabled{cursor:not-allowed;opacity:.45}.dl-chat__send svg{width:19px;height:19px;fill:currentColor}.dl-chat__hint{position:absolute;left:15px;bottom:5px;color:#829098;font-size:9px}
1748
+ .dl-chat__restore{display:none}
1749
+ .dl-chat.is-minimized{left:auto!important;top:auto!important;right:max(22px,env(safe-area-inset-right))!important;bottom:max(22px,env(safe-area-inset-bottom))!important;width:min(310px,calc(100vw - 32px))!important;min-width:0;height:68px!important;min-height:68px;resize:none;border:1px solid var(--teal-deep);border-radius:12px;background:var(--teal);color:var(--teal-fore);box-shadow:0 6px 24px rgba(23,36,43,.24)}
1750
+ .dl-chat.is-minimized .dl-chat__titlebar{position:absolute;inset:0 0 0 auto;height:68px;padding:0 8px;cursor:default;background:transparent;touch-action:auto}
1751
+ .dl-chat.is-minimized .dl-chat__brand{display:contents}
1752
+ .dl-chat.is-minimized .dl-chat__brand>.dl-chat__mark,.dl-chat.is-minimized .dl-chat__brand>strong,.dl-chat.is-minimized .dl-chat__sound,.dl-chat.is-minimized .dl-chat__popout,.dl-chat.is-minimized .dl-chat__minimize{display:none}
1753
+ .dl-chat.is-minimized .dl-chat__title-unread{margin-right:10px}
1754
+ .dl-chat.is-minimized .dl-chat__close{width:44px;height:44px;border:1px solid rgba(255,255,255,.4);border-radius:7px}
1755
+ .dl-chat.is-minimized .dl-chat__restore{display:flex;align-items:center;gap:12px;flex:1;width:100%;padding:10px 110px 10px 16px;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer;touch-action:manipulation}
1756
+ .dl-chat.is-minimized .dl-chat__restore:hover{background:rgba(255,255,255,.1)}
1757
+ .dl-chat__restore .dl-chat__mark{flex:none;width:32px;height:32px}.dl-chat__restore .dl-chat__mark svg{width:28px;height:28px}
1758
+ .dl-chat__restore strong,.dl-chat__restore small{display:block;white-space:nowrap}.dl-chat__restore strong{font-size:15px}.dl-chat__restore small{margin-top:3px;font-size:11px}
1759
+ .dl-chat.is-minimized .dl-chat__body,.dl-chat.is-minimized .dl-chat__connection,.dl-chat.is-minimized .dl-chat__error{display:none}
1760
+ @container (max-width:520px){.dl-chat__body{grid-template-columns:1fr}.dl-chat__main{display:none}.dl-chat.has-selection .dl-chat__sidebar{display:none}.dl-chat.has-selection .dl-chat__main{display:flex}.dl-chat__back{display:grid}.dl-chat__hint{display:none}.dl-chat__compose{padding-bottom:10px}}
1761
+ @media(max-width:600px){.dl-chat{inset:0;width:100%;height:100dvh;min-width:0;min-height:0;border:0;border-radius:0;resize:none}.dl-chat.is-minimized{right:max(12px,env(safe-area-inset-right))!important;bottom:max(12px,env(safe-area-inset-bottom))!important;width:min(310px,calc(100vw - 24px))!important}.dl-chat__titlebar{cursor:default}.dl-chat__popout{display:none}}
1762
+ @supports not (color:color-mix(in srgb,red,blue)){.dl-chat{--teal-deep:#2f596d;--teal-soft:#e9f1f4;--teal-wash:#f4f7f8;--teal-hover:#e5edef;--teal-selected:#dce8ed}}
1701
1763
  @media(prefers-reduced-motion:reduce){.dl-chat *{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important}}
1702
1764
  `;
1765
+ const SOUND_KEY = "datalynk-chat-sound";
1703
1766
  const windows = /* @__PURE__ */ new WeakMap();
1704
1767
  function openChat(chat, options = {}) {
1705
1768
  const doc = options.document || document;
@@ -1730,6 +1793,8 @@ button,input,textarea{font:inherit}
1730
1793
  __publicField(this, "resizeObserver");
1731
1794
  __publicField(this, "renderedMessageIds", "");
1732
1795
  __publicField(this, "destroyed", false);
1796
+ __publicField(this, "stopReadVisibility");
1797
+ __publicField(this, "onVisibilityChange", () => void this.chat.markLatestRead().catch(() => void 0));
1733
1798
  __publicField(this, "onDestroyed");
1734
1799
  this.chat = chat;
1735
1800
  this.options = options;
@@ -1740,9 +1805,11 @@ button,input,textarea{font:inherit}
1740
1805
  this.root.innerHTML = `<style>${CHAT_CSS}</style>${CHAT_HTML}`;
1741
1806
  this.doc.body.appendChild(this.host);
1742
1807
  this.bind();
1808
+ this.stopReadVisibility = this.chat.setReadVisibility(() => this.canReadSelectedThread());
1743
1809
  this.restoreGeometry();
1744
1810
  this.subscribe();
1745
1811
  this.restoreMinimized();
1812
+ this.renderSound();
1746
1813
  void this.initializeGui();
1747
1814
  this.searchInput.focus();
1748
1815
  }
@@ -1753,20 +1820,24 @@ button,input,textarea{font:inherit}
1753
1820
  this.host.hidden = false;
1754
1821
  this.setMinimized(false);
1755
1822
  this.searchInput.focus();
1823
+ this.onVisibilityChange();
1756
1824
  }
1757
1825
  destroy() {
1758
- var _a, _b, _c, _d, _e, _f;
1826
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1759
1827
  if (this.destroyed) return;
1760
1828
  this.destroyed = true;
1829
+ (_a = this.stopReadVisibility) == null ? void 0 : _a.call(this);
1830
+ this.doc.removeEventListener("visibilitychange", this.onVisibilityChange);
1831
+ (_b = this.doc.defaultView) == null ? void 0 : _b.removeEventListener("focus", this.onVisibilityChange);
1761
1832
  this.subscriptions.forEach((subscription) => subscription.unsubscribe());
1762
- (_a = this.resizeObserver) == null ? void 0 : _a.disconnect();
1833
+ (_c = this.resizeObserver) == null ? void 0 : _c.disconnect();
1763
1834
  clearTimeout(this.searchTimer);
1764
1835
  clearTimeout(this.typingTimer);
1765
1836
  if (this.typingSent && this.selected) void this.chat.setTyping(this.selected.id, false).catch(() => void 0);
1766
1837
  this.host.remove();
1767
- (_c = (_b = this.options).onClose) == null ? void 0 : _c.call(_b);
1768
- (_d = this.onDestroyed) == null ? void 0 : _d.call(this);
1769
- if (this.options.popout && !((_e = this.doc.defaultView) == null ? void 0 : _e.closed)) (_f = this.doc.defaultView) == null ? void 0 : _f.close();
1838
+ (_e = (_d = this.options).onClose) == null ? void 0 : _e.call(_d);
1839
+ (_f = this.onDestroyed) == null ? void 0 : _f.call(this);
1840
+ if (this.options.popout && !((_g = this.doc.defaultView) == null ? void 0 : _g.closed)) (_h = this.doc.defaultView) == null ? void 0 : _h.close();
1770
1841
  }
1771
1842
  get shell() {
1772
1843
  return this.query(".dl-chat");
@@ -1786,23 +1857,49 @@ button,input,textarea{font:inherit}
1786
1857
  get sendButton() {
1787
1858
  return this.query(".dl-chat__send");
1788
1859
  }
1860
+ canReadSelectedThread() {
1861
+ return !this.destroyed && !this.host.hidden && !this.shell.classList.contains("is-minimized") && this.doc.visibilityState === "visible" && Boolean(this.doc.hasFocus()) && this.query(".dl-chat__thread").getClientRects().length > 0;
1862
+ }
1789
1863
  query(selector) {
1790
1864
  const node = this.root.querySelector(selector);
1791
1865
  if (!node) throw new Error(`Missing chat element: ${selector}`);
1792
1866
  return node;
1793
1867
  }
1794
1868
  bind() {
1795
- var _a;
1869
+ var _a, _b;
1796
1870
  this.query(".dl-chat__close").addEventListener("click", () => this.destroy());
1871
+ this.query(".dl-chat__restore").addEventListener("click", () => this.setMinimized(false));
1797
1872
  this.query(".dl-chat__minimize").addEventListener("click", () => this.setMinimized(!this.shell.classList.contains("is-minimized")));
1798
1873
  this.query(".dl-chat__popout").addEventListener("click", () => this.popout());
1874
+ this.query(".dl-chat__sound").addEventListener("click", () => this.setMuted(!this.isMuted()));
1799
1875
  this.query(".dl-chat__back").addEventListener("click", () => {
1800
1876
  this.shell.classList.remove("has-selection");
1801
1877
  this.searchInput.focus();
1802
1878
  });
1803
1879
  this.searchInput.addEventListener("input", () => {
1880
+ this.renderPeople(this.chat.users$.value);
1804
1881
  clearTimeout(this.searchTimer);
1805
- this.searchTimer = setTimeout(() => void this.chat.loadDirectory(this.searchInput.value).catch(() => void 0), 220);
1882
+ this.searchTimer = setTimeout(() => void this.chat.loadDirectory(this.searchInput.value).catch(() => void 0), 150);
1883
+ });
1884
+ this.searchInput.addEventListener("keydown", (event) => {
1885
+ const first = this.query(".dl-chat__people").querySelector(".dl-chat__person");
1886
+ if (event.key === "Enter" && first) {
1887
+ event.preventDefault();
1888
+ first.click();
1889
+ } else if (event.key === "ArrowDown" && first) {
1890
+ event.preventDefault();
1891
+ first.focus();
1892
+ }
1893
+ });
1894
+ this.query(".dl-chat__people").addEventListener("keydown", (event) => {
1895
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
1896
+ const buttons = Array.from(this.query(".dl-chat__people").querySelectorAll(".dl-chat__person"));
1897
+ const index = buttons.indexOf(this.doc.activeElement);
1898
+ if (index < 0) return;
1899
+ event.preventDefault();
1900
+ if (event.key === "ArrowDown") buttons[Math.min(index + 1, buttons.length - 1)].focus();
1901
+ else if (index === 0) this.searchInput.focus();
1902
+ else buttons[index - 1].focus();
1806
1903
  });
1807
1904
  this.composer.addEventListener("input", () => this.onComposerInput());
1808
1905
  this.composer.addEventListener("keydown", (event) => {
@@ -1822,6 +1919,8 @@ button,input,textarea{font:inherit}
1822
1919
  this.resizeObserver.observe(this.shell);
1823
1920
  }
1824
1921
  (_a = this.doc.defaultView) == null ? void 0 : _a.addEventListener("beforeunload", () => this.destroy(), { once: true });
1922
+ this.doc.addEventListener("visibilitychange", this.onVisibilityChange);
1923
+ (_b = this.doc.defaultView) == null ? void 0 : _b.addEventListener("focus", this.onVisibilityChange);
1825
1924
  }
1826
1925
  subscribe() {
1827
1926
  this.subscriptions.push(
@@ -1838,10 +1937,18 @@ button,input,textarea{font:inherit}
1838
1937
  this.api.status$.subscribe((status) => this.renderConnection(status)),
1839
1938
  this.chat.unread$.subscribe((count) => {
1840
1939
  var _a, _b;
1841
- return (_b = (_a = this.options).onUnreadChange) == null ? void 0 : _b.call(_a, count);
1940
+ this.renderUnread(count);
1941
+ (_b = (_a = this.options).onUnreadChange) == null ? void 0 : _b.call(_a, count);
1842
1942
  })
1843
1943
  );
1844
1944
  }
1945
+ /** Titlebar count; stays visible while the window is minimized. */
1946
+ renderUnread(count) {
1947
+ const badge = this.query(".dl-chat__title-unread");
1948
+ badge.toggleAttribute("hidden", count <= 0);
1949
+ badge.textContent = count > 99 ? "99+" : String(count);
1950
+ badge.setAttribute("aria-label", `${count} unread message${count === 1 ? "" : "s"}`);
1951
+ }
1845
1952
  renderPeople(users) {
1846
1953
  const query = this.searchInput.value.trim().toLowerCase();
1847
1954
  const conversationIds = new Set(this.chat.conversations$.value.map((row) => row.peer.id));
@@ -1851,11 +1958,11 @@ button,input,textarea{font:inherit}
1851
1958
  if (!filtered.length) {
1852
1959
  const empty = this.doc.createElement("p");
1853
1960
  empty.className = "dl-chat__list-empty";
1854
- empty.textContent = query ? "No matching people" : "Search for someone to start a conversation.";
1961
+ empty.textContent = query ? "No matching people" : "No other people to start a conversation with.";
1855
1962
  target.appendChild(empty);
1856
1963
  return;
1857
1964
  }
1858
- filtered.slice(0, query ? 30 : 8).forEach((user) => target.appendChild(this.personButton(user)));
1965
+ filtered.slice(0, 10).forEach((user) => target.appendChild(this.personButton(user)));
1859
1966
  }
1860
1967
  renderConversations(rows) {
1861
1968
  this.conversationList.replaceChildren();
@@ -1997,7 +2104,13 @@ button,input,textarea{font:inherit}
1997
2104
  node.textContent = status === "unauthorized" ? "Your session expired. Sign in again to continue chatting." : "Chat is reconnecting. Messages cannot be sent yet.";
1998
2105
  this.sendButton.disabled = status !== "online" || !this.composer.value.trim();
1999
2106
  }
2107
+ growComposer() {
2108
+ const box = this.composer;
2109
+ box.style.height = "auto";
2110
+ box.style.height = `${box.scrollHeight + box.offsetHeight - box.clientHeight}px`;
2111
+ }
2000
2112
  onComposerInput() {
2113
+ this.growComposer();
2001
2114
  if (!this.selected) return;
2002
2115
  this.writeDraft(this.selected.id, this.composer.value);
2003
2116
  this.sendButton.disabled = !this.composer.value.trim() || this.api.status !== "online";
@@ -2021,6 +2134,7 @@ button,input,textarea{font:inherit}
2021
2134
  try {
2022
2135
  await this.chat.send(peer.id, body);
2023
2136
  this.composer.value = "";
2137
+ this.growComposer();
2024
2138
  this.writeDraft(peer.id, "");
2025
2139
  if (this.typingSent) await this.chat.setTyping(peer.id, false).catch(() => void 0);
2026
2140
  this.typingSent = false;
@@ -2031,7 +2145,7 @@ button,input,textarea{font:inherit}
2031
2145
  }
2032
2146
  }
2033
2147
  popout() {
2034
- var _a;
2148
+ var _a, _b;
2035
2149
  const popup = (_a = this.doc.defaultView) == null ? void 0 : _a.open("", "datalynk-chat", "popup,width=760,height=720,resizable=yes");
2036
2150
  if (!popup) {
2037
2151
  this.chat.error$.next("Allow pop-ups to open chat in a separate window.");
@@ -2039,6 +2153,11 @@ button,input,textarea{font:inherit}
2039
2153
  }
2040
2154
  popup.document.title = this.options.title || "Datalynk Chat";
2041
2155
  popup.document.body.style.margin = "0";
2156
+ const theme = (_b = this.doc.defaultView) == null ? void 0 : _b.getComputedStyle(this.doc.documentElement);
2157
+ ["--primary", "--primary-fore", "--accent", "--accentText"].forEach((name) => {
2158
+ const value = theme == null ? void 0 : theme.getPropertyValue(name).trim();
2159
+ if (value) popup.document.documentElement.style.setProperty(name, value);
2160
+ });
2042
2161
  openChat(this.chat, { ...this.options, document: popup.document, popout: true });
2043
2162
  this.destroy();
2044
2163
  }
@@ -2084,6 +2203,7 @@ button,input,textarea{font:inherit}
2084
2203
  async initializeGui() {
2085
2204
  try {
2086
2205
  await this.chat.initialize();
2206
+ void this.chat.loadDirectory(this.searchInput.value).catch(() => void 0);
2087
2207
  const selectedId = this.readSelectedUser();
2088
2208
  if (!selectedId || this.chat.selectedUser$.value) return;
2089
2209
  const conversation = this.chat.conversations$.value.find((row) => row.peer.id === selectedId);
@@ -2094,6 +2214,30 @@ button,input,textarea{font:inherit}
2094
2214
  } catch {
2095
2215
  }
2096
2216
  }
2217
+ isMuted() {
2218
+ try {
2219
+ return localStorage.getItem(SOUND_KEY) === "off";
2220
+ } catch {
2221
+ return false;
2222
+ }
2223
+ }
2224
+ setMuted(muted) {
2225
+ try {
2226
+ if (muted) localStorage.setItem(SOUND_KEY, "off");
2227
+ else localStorage.removeItem(SOUND_KEY);
2228
+ } catch {
2229
+ }
2230
+ this.renderSound();
2231
+ }
2232
+ renderSound() {
2233
+ var _a;
2234
+ const muted = this.isMuted();
2235
+ const button = this.query(".dl-chat__sound");
2236
+ button.setAttribute("aria-pressed", String(muted));
2237
+ button.setAttribute("aria-label", muted ? "Unmute message sound" : "Mute message sound");
2238
+ button.title = muted ? "Unmute message sound" : "Mute message sound";
2239
+ (_a = button.querySelector("path")) == null ? void 0 : _a.setAttribute("d", muted ? SOUND_OFF_ICON : SOUND_ON_ICON);
2240
+ }
2097
2241
  restoreMinimized() {
2098
2242
  try {
2099
2243
  if (localStorage.getItem(this.storageKey("minimized")) === "1") this.setMinimized(true, false);
@@ -2105,11 +2249,17 @@ button,input,textarea{font:inherit}
2105
2249
  if (this.options.popout) return;
2106
2250
  if (minimized && !this.shell.classList.contains("is-minimized")) this.saveGeometry();
2107
2251
  this.shell.classList.toggle("is-minimized", minimized);
2252
+ this.query(".dl-chat__restore").hidden = !minimized;
2108
2253
  const button = this.query(".dl-chat__minimize");
2109
2254
  button.setAttribute("aria-label", minimized ? "Restore chat" : "Minimize chat");
2110
2255
  button.title = minimized ? "Restore" : "Minimize";
2111
2256
  (_a = button.querySelector("path")) == null ? void 0 : _a.setAttribute("d", minimized ? "M7.4 15.4 12 10.8l4.6 4.6L18 14l-6-6-6 6 1.4 1.4z" : "M5 11h14v2H5z");
2112
2257
  if (!minimized) this.restoreGeometry();
2258
+ if (!minimized) this.onVisibilityChange();
2259
+ if (persist) {
2260
+ if (minimized) this.query(".dl-chat__restore").focus();
2261
+ else this.searchInput.focus();
2262
+ }
2113
2263
  if (persist) this.saveMinimized(minimized);
2114
2264
  }
2115
2265
  saveMinimized(value) {
@@ -2138,22 +2288,41 @@ button,input,textarea{font:inherit}
2138
2288
  var _a, _b;
2139
2289
  if (this.options.popout) return;
2140
2290
  try {
2291
+ const viewWidth = ((_a = this.doc.defaultView) == null ? void 0 : _a.innerWidth) || 760;
2292
+ const viewHeight = ((_b = this.doc.defaultView) == null ? void 0 : _b.innerHeight) || 720;
2293
+ if (viewWidth <= 600) {
2294
+ ["width", "height", "left", "top", "right", "bottom"].forEach((key) => this.shell.style.removeProperty(key));
2295
+ return;
2296
+ }
2141
2297
  const stored = JSON.parse(localStorage.getItem(this.storageKey("geometry")) || "null");
2142
- if (!stored) return;
2143
- const maxWidth = Math.max(360, (((_a = this.doc.defaultView) == null ? void 0 : _a.innerWidth) || 760) - 16);
2144
- const maxHeight = Math.max(460, (((_b = this.doc.defaultView) == null ? void 0 : _b.innerHeight) || 720) - 16);
2145
- this.shell.style.width = Math.min(maxWidth, Math.max(360, stored.width || 720)) + "px";
2146
- this.shell.style.height = Math.min(maxHeight, Math.max(460, stored.height || 640)) + "px";
2147
- this.shell.style.left = Math.max(0, Math.min(stored.left || 8, maxWidth - 120)) + "px";
2148
- this.shell.style.top = Math.max(0, Math.min(stored.top || 8, maxHeight - 60)) + "px";
2149
- this.shell.style.right = "auto";
2150
- this.shell.style.bottom = "auto";
2298
+ const valid = stored && [stored.width, stored.height, stored.left, stored.top].every((value) => typeof value === "number" && Number.isFinite(value)) && stored.width > 0 && stored.height > 0;
2299
+ const margin = this.options.placement === "bottom-right" ? 22 : 8;
2300
+ if (valid) {
2301
+ const width = Math.min(viewWidth - margin * 2, Math.max(560, stored.width));
2302
+ const height = Math.min(viewHeight - margin * 2, Math.max(460, stored.height));
2303
+ this.shell.style.width = width + "px";
2304
+ this.shell.style.height = height + "px";
2305
+ if (this.options.placement !== "bottom-right") {
2306
+ const clamp = (value, max) => Math.max(margin, Math.min(value, max));
2307
+ this.shell.style.left = clamp(stored.left, viewWidth - width - margin) + "px";
2308
+ this.shell.style.top = clamp(stored.top, viewHeight - height - margin) + "px";
2309
+ this.shell.style.right = "auto";
2310
+ this.shell.style.bottom = "auto";
2311
+ }
2312
+ }
2313
+ if (this.options.placement === "bottom-right") {
2314
+ this.shell.style.left = "auto";
2315
+ this.shell.style.top = "auto";
2316
+ this.shell.style.right = margin + "px";
2317
+ this.shell.style.bottom = margin + "px";
2318
+ }
2151
2319
  } catch {
2152
2320
  }
2153
2321
  }
2154
2322
  saveGeometry() {
2155
2323
  if (this.options.popout || this.destroyed || this.shell.classList.contains("is-minimized")) return;
2156
2324
  const rect = this.shell.getBoundingClientRect();
2325
+ if (!this.host.isConnected || this.host.hidden || rect.width <= 0 || rect.height <= 0) return;
2157
2326
  try {
2158
2327
  localStorage.setItem(this.storageKey("geometry"), JSON.stringify({
2159
2328
  left: rect.left,
package/dist/chat.d.ts CHANGED
@@ -49,9 +49,12 @@ export declare class Chat {
49
49
  private socketUnsubscribe?;
50
50
  private typingTimers;
51
51
  private initialized;
52
+ private resyncing?;
53
+ private readVisibility;
52
54
  constructor(api: Api);
53
55
  initialize(force?: boolean): Promise<void>;
54
56
  available(): Promise<boolean>;
57
+ private directorySeq;
55
58
  loadDirectory(search?: string, cursor?: string): Promise<ChatUser[]>;
56
59
  loadConversations(): Promise<ChatConversation[]>;
57
60
  selectUser(user: ChatUser): Promise<void>;
@@ -59,12 +62,22 @@ export declare class Chat {
59
62
  send(userId: number, body: string, clientId?: string): Promise<ChatMessage>;
60
63
  retry(message: ChatMessage): Promise<ChatMessage>;
61
64
  setTyping(userId: number, typing: boolean): Promise<void>;
65
+ /** Only the visible chat window may acknowledge messages as seen. */
66
+ setReadVisibility(visible: () => boolean): () => void;
62
67
  markLatestRead(): Promise<void>;
63
68
  destroy(): void;
64
69
  private connectRealtime;
70
+ /**
71
+ * Catch up after every (re)subscribe. Events published while the socket was down
72
+ * (up to the reconnect delay plus ticket fetch) are never replayed, so reload the
73
+ * inbox and the newest page of the open thread from the API.
74
+ */
75
+ private resync;
65
76
  private onSocketEvent;
66
77
  private applyReadReceipt;
67
78
  private upsertMessage;
79
+ /** Stream order for persisted messages; optimistic/failed sends stay at the end in send order. */
80
+ private sortMessages;
68
81
  private mergeMessages;
69
82
  private run;
70
83
  private currentUserId;
@@ -1 +1 @@
1
- {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"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"}