dsh-mobile 0.6.10 → 0.6.12

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 (41) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/CONTRIBUTORS.md +24 -6
  3. package/README.en.md +22 -19
  4. package/README.md +22 -19
  5. package/SECURITY.md +9 -0
  6. package/apps/mobile/README.md +71 -90
  7. package/apps/mobile/README.zh-CN.md +131 -0
  8. package/apps/mobile/release.json +3 -3
  9. package/docs/ATTACH_EXISTING_FRPS.en.md +1 -1
  10. package/docs/ATTACH_EXISTING_FRPS.md +1 -1
  11. package/docs/CADDY_MANAGED.en.md +7 -7
  12. package/docs/CADDY_MANAGED.md +7 -7
  13. package/docs/CLIENT_MODULES.en.md +2 -0
  14. package/docs/CLIENT_MODULES.md +2 -0
  15. package/docs/CLOUDFLARE_TUNNEL.en.md +1 -1
  16. package/docs/CLOUDFLARE_TUNNEL.md +1 -1
  17. package/docs/CPOLAR_SUBDOMAIN.en.md +1 -1
  18. package/docs/CPOLAR_SUBDOMAIN.md +1 -1
  19. package/docs/DSH_0.1.5_LAN.md +1 -1
  20. package/docs/EXTENSION_WORKERS.en.md +2 -2
  21. package/docs/EXTENSION_WORKERS.md +2 -2
  22. package/docs/MOBILE_COMPOSER_TOOLBAR.en.md +10 -4
  23. package/docs/MOBILE_COMPOSER_TOOLBAR.md +10 -4
  24. package/docs/README.md +4 -2
  25. package/docs/SELF_HOSTED_FRP.en.md +2 -2
  26. package/docs/SELF_HOSTED_FRP.md +2 -2
  27. package/docs/SELF_HOSTED_ORIGIN.en.md +3 -1
  28. package/docs/SELF_HOSTED_ORIGIN.md +3 -1
  29. package/docs/SKIN_COMPATIBILITY.en.md +1 -0
  30. package/docs/SKIN_COMPATIBILITY.md +1 -0
  31. package/docs/SLOW_CONNECTIONS.en.md +12 -0
  32. package/docs/SLOW_CONNECTIONS.md +12 -0
  33. package/lib/client.js +234 -37
  34. package/lib/client.js.map +1 -1
  35. package/lib/index.d.mts +34 -3
  36. package/lib/index.mjs +402 -209
  37. package/lib/index.mjs.map +1 -1
  38. package/lib/mobile-compat.js +5 -1
  39. package/lib/mobile-compat.js.map +1 -1
  40. package/package.json +6 -1
  41. package/packages/question-fixes/package.json +1 -1
package/lib/client.js CHANGED
@@ -3484,9 +3484,10 @@ window.__ModuleLoader__.load({
3484
3484
  [data-dsh-mobile-center] pre { max-width:100%; overflow-x:auto; }
3485
3485
  [data-dsh-mobile-center] :is(img,video,canvas,svg) { max-width:100%; }
3486
3486
  [data-dsh-mobile-message-scroll] { box-sizing:border-box !important; width:100% !important; padding:8px 10px 20px !important; }
3487
- [data-dsh-mobile-history-loader] { position:relative !important; min-height:1px !important; }
3488
- [data-dsh-mobile-history-loader] button:not(:disabled) { position:absolute !important; width:1px !important; height:1px !important; margin:-1px !important; padding:0 !important; clip-path:inset(50%) !important; opacity:0 !important; overflow:hidden !important; pointer-events:none !important; }
3489
- [data-dsh-mobile-history-loader] button:disabled { min-height:28px !important; padding:4px 12px !important; }
3487
+ [data-dsh-mobile-history-loader] { position:relative !important; display:grid !important; grid-template-columns:minmax(0,1fr) !important; justify-items:center !important; flex:0 0 auto !important; min-height:48px !important; font-size:var(--dsh-content-font-size-secondary,13px); line-height:1.5; }
3488
+ [data-dsh-mobile-history-loader]::before,[data-dsh-mobile-history-loader] button { box-sizing:border-box !important; grid-area:1/1 !important; min-height:48px !important; max-width:100% !important; padding:8px 12px !important; font:inherit !important; white-space:normal !important; overflow-wrap:anywhere; }
3489
+ [data-dsh-mobile-history-loader]::before { content:attr(data-dsh-mobile-history-label); visibility:hidden; pointer-events:none; }
3490
+ [data-dsh-mobile-history-loader] button { min-width:48px !important; touch-action:manipulation; }
3490
3491
  /* DSH's sibling margins exclude hidden and empty Chat Node Seats. Keep that
3491
3492
  spacing rule so searchable hidden seats contribute no folded-turn gaps. */
3492
3493
  [data-dsh-mobile-message-column] { box-sizing:border-box !important; width:100% !important; max-width:none !important; margin:0 !important; padding:0 !important; gap:0 !important; --dsh-chat-flow-gap:10px; }
@@ -3849,6 +3850,158 @@ window.__ModuleLoader__.load({
3849
3850
  return currentTop <= AUTO_HISTORY_THRESHOLD_PX && currentTop < previousTop - .5;
3850
3851
  }
3851
3852
  /**
3853
+ * Retain the native idle label's wrapping space while its button displays a shorter busy label.
3854
+ * @param loader - the row whose hidden sizing text shares the button's grid cell.
3855
+ * @param button - the upstream control which owns the localized idle and busy labels.
3856
+ */
3857
+ function syncHistoryLoaderLabel(loader, button) {
3858
+ if (button.disabled) return;
3859
+ const label = button.textContent?.trim() ?? "";
3860
+ if (loader.dataset.dshMobileHistoryLabel !== label) loader.dataset.dshMobileHistoryLabel = label;
3861
+ }
3862
+ /**
3863
+ * Maximum gap between input or scrolling events before an unused loading intent expires.
3864
+ * A held finger may re-arm on its next owned move; an undelivered touch end cannot keep the lease alive.
3865
+ */
3866
+ const HISTORY_SCROLL_QUIET_MS = 1600;
3867
+ /**
3868
+ * One shared options object for every intent listener, so a listener is removed by
3869
+ * exactly the registration it came from. Capture keeps `wheel` and `touchmove`
3870
+ * visible even where DSH stops propagation, and is harmless for the scroller's own
3871
+ * `scroll`/`scrollend`.
3872
+ */
3873
+ const HISTORY_INTENT_LISTENER_OPTIONS = {
3874
+ passive: true,
3875
+ capture: true
3876
+ };
3877
+ /**
3878
+ * The events that carry a history-loading intent. The scroller is not focusable, so
3879
+ * there is no `keydown`: a keyboard reader scrolls it through the page instead.
3880
+ */
3881
+ const HISTORY_INTENT_EVENTS = [
3882
+ "touchstart",
3883
+ "touchmove",
3884
+ "wheel",
3885
+ "scroll",
3886
+ "scrollend",
3887
+ "touchend",
3888
+ "touchcancel"
3889
+ ];
3890
+ /**
3891
+ * The history gate's session identity for one sync pass. `composer` must be the
3892
+ * composer this pass resolved itself: the media binding captures the composer
3893
+ * only later in the same sync, so resolving through that capture lags one pass
3894
+ * behind, and a session switch that reuses the scroller node while replacing
3895
+ * the composer would still report the previous session's id, skip the reset,
3896
+ * and leave the old arm live inside the new session.
3897
+ */
3898
+ function resolveHistoryGateIdentity(input) {
3899
+ const { sessionId } = resolveComposerSessionOrigin(input.composer, input.selectedRow, input.selectedRowToken);
3900
+ return {
3901
+ sessionId,
3902
+ sessionChanged: sessionId !== input.previousSessionId
3903
+ };
3904
+ }
3905
+ /**
3906
+ * Decide whether a scroll near the top of the history is the user's, because DSH
3907
+ * writes `scrollTop` itself while it prepends a loaded page. An ungated listener read
3908
+ * that compensation as another upward fling, clicked "Load earlier" again, and shook
3909
+ * the view in a chain. Only fresh input arms the gate, one arm buys at most one
3910
+ * successful load, and an arm with no further input expires quietly.
3911
+ */
3912
+ function createHistoryAutoLoadGate(hooks) {
3913
+ let armed = false;
3914
+ const touchIds = /* @__PURE__ */ new Set();
3915
+ let disposed = false;
3916
+ let previousTop = hooks.getTop();
3917
+ let quietPending = false;
3918
+ let quietId;
3919
+ const cancelQuiet = () => {
3920
+ if (!quietPending) return;
3921
+ quietPending = false;
3922
+ hooks.cancel(quietId);
3923
+ quietId = void 0;
3924
+ };
3925
+ const onQuiet = () => {
3926
+ quietPending = false;
3927
+ quietId = void 0;
3928
+ armed = false;
3929
+ };
3930
+ const restartQuiet = () => {
3931
+ cancelQuiet();
3932
+ if (disposed) return;
3933
+ quietPending = true;
3934
+ quietId = hooks.schedule(onQuiet, HISTORY_SCROLL_QUIET_MS);
3935
+ };
3936
+ const arm = () => {
3937
+ armed = true;
3938
+ restartQuiet();
3939
+ };
3940
+ const onScroll = () => {
3941
+ const top = hooks.getTop();
3942
+ const eligible = armed && hooks.shouldLoad(previousTop, top);
3943
+ previousTop = top;
3944
+ if (eligible && hooks.tryLoad()) {
3945
+ armed = false;
3946
+ cancelQuiet();
3947
+ return;
3948
+ }
3949
+ if (armed) restartQuiet();
3950
+ };
3951
+ return {
3952
+ handleEvent: (event) => {
3953
+ if (disposed) return;
3954
+ switch (event.type) {
3955
+ case "touchstart":
3956
+ for (const touch of Array.from(event.changedTouches ?? [])) touchIds.add(touch.identifier);
3957
+ if ((event.changedTouches?.length ?? 0) > 0) arm();
3958
+ break;
3959
+ case "touchmove":
3960
+ if (Array.from(event.changedTouches ?? []).some((touch) => touchIds.has(touch.identifier))) arm();
3961
+ break;
3962
+ case "wheel":
3963
+ arm();
3964
+ break;
3965
+ case "touchend":
3966
+ case "touchcancel": {
3967
+ let endedOwnedTouch = false;
3968
+ for (const touch of Array.from(event.changedTouches ?? [])) if (touchIds.delete(touch.identifier)) endedOwnedTouch = true;
3969
+ if (endedOwnedTouch) restartQuiet();
3970
+ break;
3971
+ }
3972
+ case "scrollend":
3973
+ armed = false;
3974
+ cancelQuiet();
3975
+ break;
3976
+ case "scroll": onScroll();
3977
+ }
3978
+ },
3979
+ reset: (top) => {
3980
+ armed = false;
3981
+ touchIds.clear();
3982
+ previousTop = top;
3983
+ cancelQuiet();
3984
+ },
3985
+ dispose: () => {
3986
+ disposed = true;
3987
+ armed = false;
3988
+ touchIds.clear();
3989
+ cancelQuiet();
3990
+ },
3991
+ state: () => ({
3992
+ armed,
3993
+ previousTop
3994
+ })
3995
+ };
3996
+ }
3997
+ /** Attach a gate to one scroller; the disposer removes exactly the listeners it added. */
3998
+ function bindHistoryAutoLoadGate(scroller, gate) {
3999
+ for (const type of HISTORY_INTENT_EVENTS) scroller.addEventListener(type, gate.handleEvent, HISTORY_INTENT_LISTENER_OPTIONS);
4000
+ return () => {
4001
+ for (const type of HISTORY_INTENT_EVENTS) scroller.removeEventListener(type, gate.handleEvent, HISTORY_INTENT_LISTENER_OPTIONS);
4002
+ };
4003
+ }
4004
+ /**
3852
4005
  * Whether the overlay scrim belongs on screen. The scrim exists for the
3853
4006
  * slide-in drawer only — it dims the page and catches the tap that closes the
3854
4007
  * drawer — so it is shown while that drawer is open *and* the overlay query is
@@ -4270,16 +4423,17 @@ window.__ModuleLoader__.load({
4270
4423
  let disposeEnterBinding;
4271
4424
  const sessionTokens = /* @__PURE__ */ new WeakMap();
4272
4425
  let nextSessionToken = 0;
4426
+ const sessionRowToken = (row) => {
4427
+ let token = sessionTokens.get(row);
4428
+ if (token === void 0) {
4429
+ token = `stock-${String(++nextSessionToken)}`;
4430
+ sessionTokens.set(row, token);
4431
+ }
4432
+ return token;
4433
+ };
4273
4434
  const currentSessionOrigin = () => {
4274
4435
  const selectedRow = document.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]");
4275
- return resolveComposerSessionOrigin(boundComposer, selectedRow, (row) => {
4276
- let token = sessionTokens.get(row);
4277
- if (token === void 0) {
4278
- token = `stock-${String(++nextSessionToken)}`;
4279
- sessionTokens.set(row, token);
4280
- }
4281
- return token;
4282
- });
4436
+ return resolveComposerSessionOrigin(boundComposer, selectedRow, sessionRowToken);
4283
4437
  };
4284
4438
  const disposeBrowserComposerSoftEnter = bindBrowserComposerSoftEnter(document, (editor, composerCard) => ({
4285
4439
  appBridge: nativeAppOwnsComposerEnter(window),
@@ -4524,26 +4678,68 @@ window.__ModuleLoader__.load({
4524
4678
  let transitionTimer = 0;
4525
4679
  let transitionTarget;
4526
4680
  let historyScroller;
4527
- let historyPreviousTop = 0;
4681
+ let historySessionId = null;
4682
+ let disposeHistoryGate;
4683
+ let historyLoaderBinding;
4684
+ const historyLabelObserver = new MutationObserver(() => schedule());
4685
+ const bindHistoryLoader = (next) => {
4686
+ if (historyLoaderBinding?.element === next) return;
4687
+ historyLabelObserver.disconnect();
4688
+ if (historyLoaderBinding !== void 0) {
4689
+ const { element, originalLabel } = historyLoaderBinding;
4690
+ if (originalLabel === void 0) delete element.dataset.dshMobileHistoryLabel;
4691
+ else element.dataset.dshMobileHistoryLabel = originalLabel;
4692
+ }
4693
+ historyLoaderBinding = next === void 0 ? void 0 : {
4694
+ element: next,
4695
+ originalLabel: next.dataset.dshMobileHistoryLabel
4696
+ };
4697
+ if (next !== void 0) historyLabelObserver.observe(next, {
4698
+ childList: true,
4699
+ characterData: true,
4700
+ subtree: true
4701
+ });
4702
+ };
4528
4703
  const historyLoadButton = () => {
4529
4704
  return (historyScroller === void 0 ? void 0 : firstByClassSuffix(historyScroller, "_older"))?.querySelector("button") ?? void 0;
4530
4705
  };
4531
- const onHistoryScroll = () => {
4532
- if (historyScroller === void 0) return;
4533
- const currentTop = Math.max(0, historyScroller.scrollTop);
4534
- const shouldLoad = shouldAutoLoadEarlier(historyPreviousTop, currentTop);
4535
- historyPreviousTop = currentTop;
4536
- if (!shouldLoad) return;
4537
- const button = historyLoadButton();
4538
- if (button === void 0 || button.disabled || button.getAttribute("aria-disabled") === "true") return;
4539
- button.click();
4540
- };
4541
- const bindHistoryScroller = (next) => {
4542
- if (historyScroller === next) return;
4543
- historyScroller?.removeEventListener("scroll", onHistoryScroll);
4544
- historyScroller = next;
4545
- historyPreviousTop = next?.scrollTop ?? 0;
4546
- historyScroller?.addEventListener("scroll", onHistoryScroll, { passive: true });
4706
+ const historyAutoLoadGate = createHistoryAutoLoadGate({
4707
+ getTop: () => Math.max(0, historyScroller?.scrollTop ?? 0),
4708
+ shouldLoad: shouldAutoLoadEarlier,
4709
+ tryLoad: () => {
4710
+ const button = historyLoadButton();
4711
+ if (button === void 0 || button.disabled || button.getAttribute("aria-disabled") === "true") return false;
4712
+ button.click();
4713
+ return true;
4714
+ },
4715
+ schedule: (fn, ms) => window.setTimeout(fn, ms),
4716
+ cancel: (id) => {
4717
+ window.clearTimeout(id);
4718
+ }
4719
+ });
4720
+ /**
4721
+ * Rebind the gate to the conversation scroller, and drop an arm that belongs to a
4722
+ * session the reader has already left: DSH can reuse the same scroller node, so the
4723
+ * session identity has to be checked even when the node did not change. The
4724
+ * composer is this pass's own resolution, never the media binding's capture.
4725
+ */
4726
+ const bindHistoryScroller = (next, composer) => {
4727
+ if (historyScroller !== next) {
4728
+ disposeHistoryGate?.();
4729
+ disposeHistoryGate = void 0;
4730
+ historyScroller = next;
4731
+ historyAutoLoadGate.reset(next?.scrollTop ?? 0);
4732
+ if (next !== void 0) disposeHistoryGate = bindHistoryAutoLoadGate(next, historyAutoLoadGate);
4733
+ }
4734
+ const identity = resolveHistoryGateIdentity({
4735
+ previousSessionId: historySessionId,
4736
+ composer,
4737
+ selectedRow: document.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]"),
4738
+ selectedRowToken: sessionRowToken
4739
+ });
4740
+ historySessionId = identity.sessionId;
4741
+ if (!identity.sessionChanged) return;
4742
+ historyAutoLoadGate.reset(historyScroller?.scrollTop ?? 0);
4547
4743
  };
4548
4744
  const animateNavigation = (event) => {
4549
4745
  if (!(event.target instanceof Element)) return;
@@ -4614,7 +4810,8 @@ window.__ModuleLoader__.load({
4614
4810
  if (center === void 0) {
4615
4811
  syncPanHeader(void 0);
4616
4812
  syncComposerEnterNewline(null);
4617
- bindHistoryScroller(void 0);
4813
+ bindHistoryScroller(void 0, null);
4814
+ bindHistoryLoader(void 0);
4618
4815
  syncMediaBinding(null);
4619
4816
  setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
4620
4817
  cameraButton.remove();
@@ -4628,17 +4825,15 @@ window.__ModuleLoader__.load({
4628
4825
  viewArea = firstByClassSuffix(center, "_viewArea");
4629
4826
  if (viewArea !== void 0) viewArea.dataset.dshMobileView = "true";
4630
4827
  const conversation = center.querySelector("[data-conversation-scroll]");
4631
- bindHistoryScroller(conversation ?? void 0);
4828
+ const composerCard = center.querySelector("[data-composer-card]");
4829
+ bindHistoryScroller(conversation ?? void 0, composerCard ?? null);
4632
4830
  const historyLoader = conversation === null ? void 0 : firstByClassSuffix(conversation, "_older");
4831
+ bindHistoryLoader(historyLoader);
4633
4832
  if (historyLoader !== void 0) {
4634
4833
  historyLoader.dataset.dshMobileHistoryLoader = "true";
4635
4834
  historyLoader.setAttribute("aria-live", "polite");
4636
4835
  const button = historyLoader.querySelector("button");
4637
- if (button !== null) {
4638
- button.tabIndex = -1;
4639
- if (button.disabled) button.removeAttribute("aria-hidden");
4640
- else button.setAttribute("aria-hidden", "true");
4641
- }
4836
+ if (button !== null) syncHistoryLoaderLabel(historyLoader, button);
4642
4837
  }
4643
4838
  const messageColumn = conversation === null ? void 0 : firstByClassSuffix(conversation, "_column");
4644
4839
  const messageScroll = messageColumn?.parentElement;
@@ -4653,7 +4848,6 @@ window.__ModuleLoader__.load({
4653
4848
  table.before(wrapper);
4654
4849
  wrapper.append(table);
4655
4850
  }
4656
- const composerCard = center.querySelector("[data-composer-card]");
4657
4851
  const composerRow = composerCard?.querySelector(":scope > [data-input-scroll]")?.nextElementSibling;
4658
4852
  if (!(composerRow instanceof HTMLElement)) {
4659
4853
  syncComposerEnterNewline(null);
@@ -4805,7 +4999,10 @@ window.__ModuleLoader__.load({
4805
4999
  if (transitionRestartFrame !== 0) cancelAnimationFrame(transitionRestartFrame);
4806
5000
  if (transitionTimer !== 0) clearTimeout(transitionTimer);
4807
5001
  transitionTarget?.removeAttribute("data-dsh-mobile-view-transition");
4808
- historyScroller?.removeEventListener("scroll", onHistoryScroll);
5002
+ disposeHistoryGate?.();
5003
+ disposeHistoryGate = void 0;
5004
+ historyAutoLoadGate.dispose();
5005
+ bindHistoryLoader(void 0);
4809
5006
  document.removeEventListener("pointerdown", onPointerDown, true);
4810
5007
  document.removeEventListener("keydown", onKeyDown, true);
4811
5008
  document.removeEventListener("pointerdown", onTogglePointerDown, true);