dsh-mobile 0.7.1 → 0.8.0
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/CHANGELOG.md +20 -0
- package/CONTRIBUTORS.md +2 -2
- package/README.en.md +33 -12
- package/README.md +33 -12
- package/SECURITY.md +6 -0
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/apps/mobile/README.md +10 -6
- package/apps/mobile/README.zh-CN.md +10 -6
- package/apps/mobile/release.json +3 -3
- package/assets/mobile-push/icon-180.png +0 -0
- package/assets/mobile-push/icon-192.png +0 -0
- package/assets/mobile-push/icon-512.png +0 -0
- package/docs/README.md +2 -0
- package/docs/SLOW_CONNECTIONS.en.md +7 -3
- package/docs/SLOW_CONNECTIONS.md +7 -3
- package/docs/WEB_PUSH.en.md +118 -0
- package/docs/WEB_PUSH.md +118 -0
- package/lib/client.js +1084 -10
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +124 -1
- package/lib/index.mjs +3669 -86
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +12 -2
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +17 -5
- package/packages/question-fixes/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -3634,6 +3634,163 @@ window.__ModuleLoader__.load({
|
|
|
3634
3634
|
});
|
|
3635
3635
|
}
|
|
3636
3636
|
//#endregion
|
|
3637
|
+
//#region src/mobile-sidebar-scroll.ts
|
|
3638
|
+
function sidebarRestoreTop(snapshot, anchorTop) {
|
|
3639
|
+
return Math.max(0, anchorTop === void 0 || snapshot.offset === void 0 ? snapshot.top : anchorTop - snapshot.offset);
|
|
3640
|
+
}
|
|
3641
|
+
const keyedRows = "[data-row-key]";
|
|
3642
|
+
const hasSuffix = (element, suffix) => [...element.classList].some((token) => token.endsWith(suffix));
|
|
3643
|
+
function visible(element) {
|
|
3644
|
+
return element.isConnected && element.clientHeight > 0 && element.getClientRects().length > 0;
|
|
3645
|
+
}
|
|
3646
|
+
function captureSidebarScroll(element) {
|
|
3647
|
+
const top = element.scrollTop;
|
|
3648
|
+
const bounds = element.getBoundingClientRect();
|
|
3649
|
+
const row = [...element.querySelectorAll(keyedRows)].find((candidate) => {
|
|
3650
|
+
const rect = candidate.getBoundingClientRect();
|
|
3651
|
+
return rect.height > 0 && rect.bottom > bounds.top && rect.top < bounds.bottom;
|
|
3652
|
+
});
|
|
3653
|
+
return row === void 0 ? { top } : {
|
|
3654
|
+
top,
|
|
3655
|
+
anchor: row.getAttribute("data-row-key") ?? "",
|
|
3656
|
+
offset: row.getBoundingClientRect().top - bounds.top
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* Uses browser scrollports/keyed rows only; no React internals, DOM reordering,
|
|
3661
|
+
* forced group expansion or persistent storage of session identities. Absolute
|
|
3662
|
+
* position seeds a virtual window before a mounted keyed anchor refines it.
|
|
3663
|
+
*/
|
|
3664
|
+
function installMobileSidebarScrollMemory() {
|
|
3665
|
+
const memories = /* @__PURE__ */ new Map();
|
|
3666
|
+
const stockViewport = window.matchMedia("(max-width:720px)");
|
|
3667
|
+
const dedicatedViewport = window.matchMedia("(max-width:899px)");
|
|
3668
|
+
let root;
|
|
3669
|
+
let open = false;
|
|
3670
|
+
let ports = /* @__PURE__ */ new Map();
|
|
3671
|
+
let pending = /* @__PURE__ */ new Map();
|
|
3672
|
+
let frame = 0;
|
|
3673
|
+
let deadline = 0;
|
|
3674
|
+
let disposed = false;
|
|
3675
|
+
const isOpen = () => root !== void 0 && visible(root) && !hasSuffix(root, "_collapsed") && root.closest("[data-open=\"false\"]") === null && root.querySelector("[data-dsh-mobile-settings]") === null;
|
|
3676
|
+
const cancel = () => {
|
|
3677
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
3678
|
+
frame = 0;
|
|
3679
|
+
pending.clear();
|
|
3680
|
+
};
|
|
3681
|
+
const remember = () => {
|
|
3682
|
+
if (!open || !isOpen()) return;
|
|
3683
|
+
for (const [port, key] of ports) if (!pending.has(port) && visible(port)) memories.set(key, port === root ? { top: port.scrollTop } : captureSidebarScroll(port));
|
|
3684
|
+
};
|
|
3685
|
+
const restore = () => {
|
|
3686
|
+
frame = 0;
|
|
3687
|
+
if (disposed || !open || !isOpen()) return;
|
|
3688
|
+
for (const [port, state] of pending) {
|
|
3689
|
+
if (!visible(port)) continue;
|
|
3690
|
+
const snapshot = state.snapshot;
|
|
3691
|
+
if (snapshot === void 0) {
|
|
3692
|
+
if (port === root) {
|
|
3693
|
+
pending.delete(port);
|
|
3694
|
+
continue;
|
|
3695
|
+
}
|
|
3696
|
+
const selected = port.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]");
|
|
3697
|
+
if (selected !== null && visible(selected)) {
|
|
3698
|
+
const bounds = port.getBoundingClientRect(), rect = selected.getBoundingClientRect();
|
|
3699
|
+
if (rect.top < bounds.top || rect.bottom > bounds.bottom) port.scrollTop += rect.top - bounds.top - (port.clientHeight - rect.height) / 2;
|
|
3700
|
+
pending.delete(port);
|
|
3701
|
+
} else if (port.querySelector(keyedRows) !== null) pending.delete(port);
|
|
3702
|
+
continue;
|
|
3703
|
+
}
|
|
3704
|
+
const anchor = [...port.querySelectorAll(keyedRows)].find((row) => row.dataset.rowKey === snapshot.anchor);
|
|
3705
|
+
const target = sidebarRestoreTop(snapshot, anchor === void 0 ? void 0 : port.scrollTop + anchor.getBoundingClientRect().top - port.getBoundingClientRect().top);
|
|
3706
|
+
const before = port.scrollTop;
|
|
3707
|
+
port.scrollTop = target;
|
|
3708
|
+
if (Math.abs(port.scrollTop - target) <= 1 && Math.abs(before - port.scrollTop) <= 1) state.stable++;
|
|
3709
|
+
else state.stable = 0;
|
|
3710
|
+
if (state.stable >= 2) pending.delete(port);
|
|
3711
|
+
}
|
|
3712
|
+
if (pending.size > 0 && performance.now() < deadline) frame = requestAnimationFrame(restore);
|
|
3713
|
+
else pending.clear();
|
|
3714
|
+
};
|
|
3715
|
+
const sync = () => {
|
|
3716
|
+
if (disposed) return;
|
|
3717
|
+
const next = document.querySelector("[data-dsh-mobile-sidebar-root]") ?? void 0;
|
|
3718
|
+
const mobile = next?.closest("[data-pane=\"sidebar\"]") != null ? dedicatedViewport.matches : stockViewport.matches;
|
|
3719
|
+
if (next !== root) {
|
|
3720
|
+
cancel();
|
|
3721
|
+
ports.clear();
|
|
3722
|
+
root = next;
|
|
3723
|
+
open = false;
|
|
3724
|
+
}
|
|
3725
|
+
if (!(mobile && isOpen())) {
|
|
3726
|
+
cancel();
|
|
3727
|
+
open = false;
|
|
3728
|
+
ports.clear();
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
const reopening = !open;
|
|
3732
|
+
open = true;
|
|
3733
|
+
if (root === void 0) return;
|
|
3734
|
+
const candidates = [root, ...root.querySelectorAll("[class*=\"_regionArea\"] [class*=\"_list\"]")].filter((port) => port === root || hasSuffix(port, "_list"));
|
|
3735
|
+
const nextPorts = /* @__PURE__ */ new Map();
|
|
3736
|
+
for (const port of candidates) {
|
|
3737
|
+
const key = port === root ? "outer" : `workspace:${[...port.classList].sort().join(" ")}:${root.querySelector("[class*=\"_regionArea\"] input")?.value ?? ""}`;
|
|
3738
|
+
nextPorts.set(port, key);
|
|
3739
|
+
if (reopening || ports.get(port) !== key) {
|
|
3740
|
+
pending.set(port, {
|
|
3741
|
+
snapshot: memories.get(key),
|
|
3742
|
+
stable: 0
|
|
3743
|
+
});
|
|
3744
|
+
deadline = performance.now() + 1200;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
for (const port of pending.keys()) if (!nextPorts.has(port)) pending.delete(port);
|
|
3748
|
+
ports = nextPorts;
|
|
3749
|
+
if (pending.size > 0 && frame === 0) frame = requestAnimationFrame(restore);
|
|
3750
|
+
};
|
|
3751
|
+
const onScroll = (event) => {
|
|
3752
|
+
const port = event.target;
|
|
3753
|
+
if (!(port instanceof HTMLElement) || !open || !isOpen() || pending.has(port) || !visible(port)) return;
|
|
3754
|
+
const key = ports.get(port);
|
|
3755
|
+
if (key !== void 0) memories.set(key, port === root ? { top: port.scrollTop } : captureSidebarScroll(port));
|
|
3756
|
+
};
|
|
3757
|
+
const onIntent = (event) => {
|
|
3758
|
+
if (!(event.target instanceof Node) || !root?.contains(event.target)) return;
|
|
3759
|
+
cancel();
|
|
3760
|
+
remember();
|
|
3761
|
+
};
|
|
3762
|
+
stockViewport.addEventListener("change", sync);
|
|
3763
|
+
dedicatedViewport.addEventListener("change", sync);
|
|
3764
|
+
document.addEventListener("scroll", onScroll, true);
|
|
3765
|
+
for (const type of [
|
|
3766
|
+
"pointerdown",
|
|
3767
|
+
"touchstart",
|
|
3768
|
+
"wheel",
|
|
3769
|
+
"keydown",
|
|
3770
|
+
"click"
|
|
3771
|
+
]) document.addEventListener(type, onIntent, true);
|
|
3772
|
+
return {
|
|
3773
|
+
sync,
|
|
3774
|
+
dispose: () => {
|
|
3775
|
+
disposed = true;
|
|
3776
|
+
cancel();
|
|
3777
|
+
stockViewport.removeEventListener("change", sync);
|
|
3778
|
+
dedicatedViewport.removeEventListener("change", sync);
|
|
3779
|
+
document.removeEventListener("scroll", onScroll, true);
|
|
3780
|
+
for (const type of [
|
|
3781
|
+
"pointerdown",
|
|
3782
|
+
"touchstart",
|
|
3783
|
+
"wheel",
|
|
3784
|
+
"keydown",
|
|
3785
|
+
"click"
|
|
3786
|
+
]) document.removeEventListener(type, onIntent, true);
|
|
3787
|
+
ports.clear();
|
|
3788
|
+
memories.clear();
|
|
3789
|
+
root = void 0;
|
|
3790
|
+
}
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
//#endregion
|
|
3637
3794
|
//#region src/native-mobile.ts
|
|
3638
3795
|
/**
|
|
3639
3796
|
* Viewport below which the injected chrome becomes an overlay drawer: the
|
|
@@ -3680,10 +3837,30 @@ window.__ModuleLoader__.load({
|
|
|
3680
3837
|
.dsh-mobile-settings_fontFamily > span { flex:1 1 100px; }
|
|
3681
3838
|
.dsh-mobile-settings_fontFamily input { box-sizing:border-box; flex:1 1 180px; width:100%; min-width:0; min-height:48px; padding:8px 12px; border:0.5px solid var(--dsw-alias-border-l2); border-radius:10px; background:var(--dsw-alias-bg-module-platform); color:var(--dsw-alias-label-primary); font:inherit; }
|
|
3682
3839
|
.dsh-mobile-settings_fontFamily input:focus-visible,.dsh-mobile-settings_fontDetails > summary:focus-visible { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
|
|
3840
|
+
/* Push category rows: compact label-left / native-Switch-right rows. The control is
|
|
3841
|
+
the actual public @deepseek-ai/dsh-client-ui-primitives Switch provided by the DSH
|
|
3842
|
+
web shell: its track/thumb paint, dimensions, focus ring and motion are owned by
|
|
3843
|
+
the shell stylesheet and are never re-created, resized or overridden here. Each
|
|
3844
|
+
row is a label associated with its switch button, so the full 48px row remains
|
|
3845
|
+
one tap target while the button keeps native keyboard semantics. Category names
|
|
3846
|
+
match the General settings row title typography (14px/20px, regular weight). */
|
|
3847
|
+
.dsh-mobile-push-categories { display:flex; flex-direction:column; width:100%; margin:8px 0 4px; border-top:0.5px solid var(--dsw-alias-border-l2,#e8ebef); }
|
|
3848
|
+
.dsh-mobile-push-category { display:flex; align-items:center; justify-content:space-between; gap:12px; box-sizing:border-box; width:100%; min-height:48px; padding:6px 0; border-bottom:0.5px solid var(--dsw-alias-border-l2,#e8ebef); cursor:pointer; }
|
|
3849
|
+
.dsh-mobile-push-categoryName { flex:1 1 auto; min-width:0; color:var(--dsw-alias-label-primary,#171a21); font-size:14px; line-height:20px; font-weight:400; overflow-wrap:anywhere; }
|
|
3683
3850
|
html.dsh-native-mobile-active [data-dsh-mobile-settings-header] { box-sizing:border-box !important; height:56px !important; min-height:56px !important; padding:4px 12px !important; }
|
|
3684
3851
|
html.dsh-native-mobile-active [data-dsh-mobile-settings-header] [class*="_close"] { box-sizing:border-box !important; flex:none !important; width:48px !important; min-width:48px !important; height:48px !important; min-height:48px !important; }
|
|
3685
3852
|
/* The mobile row replaces the Host-backed font control, not the theme selector. */
|
|
3686
3853
|
html.dsh-native-mobile-active [data-slot="settings.general.item"]:has([data-mobile-font-setting]) > :not([data-mobile-font-setting])[class*="_row"]:has([class*="_control"] > [class*="_stepper"] > [class*="_arrows"]) { display:none !important; }
|
|
3854
|
+
/* The injected attachment actions exist on every viewport, not only under the
|
|
3855
|
+
narrow overlay: a tablet shows the same Add menu, and with no native item to
|
|
3856
|
+
mirror they fell back to an unstyled UA button box (bordered, centred). Keep
|
|
3857
|
+
this fallback outside the overlay query so the rows read like native ones. */
|
|
3858
|
+
.dsh-mobile-media-action { box-sizing:border-box; display:flex; align-items:center; justify-content:flex-start; gap:8px; width:100%; min-width:0; min-height:40px; padding:8px 10px; border:0; border-radius:10px; background:transparent; color:var(--dsw-alias-label-primary,inherit); cursor:pointer; font:inherit; font-size:14px; line-height:22px; text-align:left; touch-action:manipulation; }
|
|
3859
|
+
.dsh-mobile-media-action:active { opacity:.72; }
|
|
3860
|
+
.dsh-mobile-media-action:focus-visible { outline:2px solid var(--dsw-alias-state-business-primary,#4c82f7); outline-offset:1px; }
|
|
3861
|
+
.dsh-mobile-media-action:disabled { cursor:default; opacity:.38; }
|
|
3862
|
+
.dsh-mobile-media-action svg { flex:none; width:16px; height:16px; color:var(--dsw-alias-label-tertiary,currentColor); }
|
|
3863
|
+
.dsh-mobile-media-action span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
3687
3864
|
@media ${NATIVE_MOBILE_OVERLAY_QUERY} {
|
|
3688
3865
|
html.dsh-native-mobile-active,html.dsh-native-mobile-active body { width:100%; height:100%; overflow:hidden; }
|
|
3689
3866
|
html.dsh-native-mobile-active { --dsh-mobile-motion-duration:200ms; --dsh-mobile-motion-ease:cubic-bezier(.22,1,.36,1); }
|
|
@@ -3869,14 +4046,6 @@ window.__ModuleLoader__.load({
|
|
|
3869
4046
|
[data-dsh-mobile-center] [role="dialog"][aria-label*="Context"] :is(dt,dd) { white-space:nowrap !important; word-break:keep-all !important; }
|
|
3870
4047
|
.dsh-mobile-branch-toast,.dsh-mobile-media-toast { display:block; position:fixed; z-index:330; top:max(12px,env(safe-area-inset-top)); left:50%; max-width:calc(100vw - 32px); box-sizing:border-box; padding:7px 14px; border:1px solid rgb(15 23 42 / 10%); border-radius:999px; background:rgb(15 23 42 / 92%); color:#fff; font-size:13px; line-height:20px; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; opacity:0; pointer-events:none; transform:translate(-50%,-8px); transition:opacity 160ms ease,transform 160ms ease; }
|
|
3871
4048
|
.dsh-mobile-branch-toast[data-visible="true"],.dsh-mobile-media-toast[data-visible="true"] { opacity:1; transform:translate(-50%,0); }
|
|
3872
|
-
/* Fallback only: when the native Add group has no item to clone, keep the
|
|
3873
|
-
camera action on the same row metrics and tokens as the native menu. */
|
|
3874
|
-
.dsh-mobile-media-action { box-sizing:border-box; display:flex; align-items:center; justify-content:flex-start; gap:8px; width:100%; min-width:0; min-height:40px; padding:8px 10px; border:0; border-radius:10px; background:transparent; color:var(--dsw-alias-label-primary,inherit); cursor:pointer; font:inherit; font-size:14px; line-height:22px; text-align:left; touch-action:manipulation; }
|
|
3875
|
-
.dsh-mobile-media-action:active { opacity:.72; }
|
|
3876
|
-
.dsh-mobile-media-action:focus-visible { outline:2px solid var(--dsw-alias-state-business-primary,#4c82f7); outline-offset:1px; }
|
|
3877
|
-
.dsh-mobile-media-action:disabled { cursor:default; opacity:.38; }
|
|
3878
|
-
.dsh-mobile-media-action svg { flex:none; width:16px; height:16px; color:var(--dsw-alias-label-tertiary,currentColor); }
|
|
3879
|
-
.dsh-mobile-media-action span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
3880
4049
|
[data-dsh-mobile-center] [class*="_composer"] { padding-left:8px !important; padding-right:8px !important; padding-bottom:max(8px,env(safe-area-inset-bottom)) !important; }
|
|
3881
4050
|
/* Collapse the scroll viewport only; the editor, draft and attachments stay
|
|
3882
4051
|
owned by DSH. Toolbar focus keeps the card expanded during menu taps. */
|
|
@@ -3961,6 +4130,19 @@ window.__ModuleLoader__.load({
|
|
|
3961
4130
|
return classToken(candidate, "_frame") && firstByClassSuffix(candidate, "_sidebarCol") !== void 0 && firstByClassSuffix(candidate, "_centerCol") !== void 0;
|
|
3962
4131
|
});
|
|
3963
4132
|
}
|
|
4133
|
+
/**
|
|
4134
|
+
* Mirror the classes of the stock Add-menu item on an injected attachment action.
|
|
4135
|
+
*
|
|
4136
|
+
* An empty class name is not nullish: trusting it would strip the plugin's own
|
|
4137
|
+
* fallback class and leave a bordered, centred UA button on that Host version.
|
|
4138
|
+
* A stock item that does carry classes still wins, so the injected action keeps
|
|
4139
|
+
* following the native menu's spacing, colours and dark-mode states.
|
|
4140
|
+
*/
|
|
4141
|
+
function mediaActionClassName(nativeItem) {
|
|
4142
|
+
const nativeClass = nativeItem?.className;
|
|
4143
|
+
const trimmed = typeof nativeClass === "string" ? nativeClass.trim() : "";
|
|
4144
|
+
return trimmed === "" ? "dsh-mobile-media-action" : trimmed;
|
|
4145
|
+
}
|
|
3964
4146
|
/** Mark every mounted settings dialog so its mobile layout does not depend on the conversation shell. */
|
|
3965
4147
|
function markNativeMobileSettings(root) {
|
|
3966
4148
|
let marked = 0;
|
|
@@ -4886,7 +5068,7 @@ window.__ModuleLoader__.load({
|
|
|
4886
5068
|
}
|
|
4887
5069
|
})();
|
|
4888
5070
|
for (const button of [cameraButton, galleryButton]) {
|
|
4889
|
-
const className = nativeItem
|
|
5071
|
+
const className = mediaActionClassName(nativeItem);
|
|
4890
5072
|
if (button.className !== className) button.className = className;
|
|
4891
5073
|
const icon = button.firstElementChild;
|
|
4892
5074
|
const caption = button.lastElementChild;
|
|
@@ -5092,6 +5274,7 @@ window.__ModuleLoader__.load({
|
|
|
5092
5274
|
document.addEventListener("focusin", onStripFocus, true);
|
|
5093
5275
|
document.addEventListener("click", onStripClickCapture, true);
|
|
5094
5276
|
document.addEventListener("click", onBranchClick, true);
|
|
5277
|
+
const sidebarScrollMemory = installMobileSidebarScrollMemory();
|
|
5095
5278
|
let frame;
|
|
5096
5279
|
let sidebar;
|
|
5097
5280
|
let sidebarRoot;
|
|
@@ -5324,7 +5507,10 @@ window.__ModuleLoader__.load({
|
|
|
5324
5507
|
const lastColumn = frame?.style.gridTemplateColumns.trim().split(/\s+/).at(-1);
|
|
5325
5508
|
details.dataset.open = String(lastColumn !== void 0 && lastColumn !== "0px" && lastColumn !== "0");
|
|
5326
5509
|
}
|
|
5327
|
-
if (sidebar === void 0)
|
|
5510
|
+
if (sidebar === void 0) {
|
|
5511
|
+
sidebarScrollMemory.sync();
|
|
5512
|
+
return;
|
|
5513
|
+
}
|
|
5328
5514
|
sidebar.dataset.dshMobileSidebar = "true";
|
|
5329
5515
|
toggle = firstByClassSuffix(sidebar, "_toggle");
|
|
5330
5516
|
let candidate = toggle?.parentElement;
|
|
@@ -5336,6 +5522,7 @@ window.__ModuleLoader__.load({
|
|
|
5336
5522
|
if (toggle !== void 0) toggle.dataset.dshMobileToggle = "true";
|
|
5337
5523
|
if (frame !== void 0) sidebar.dataset.open = String(!classToken(sidebarRoot, "_collapsed"));
|
|
5338
5524
|
backdrop.hidden = frame === void 0 || !drawerScrimVisible(classToken(sidebarRoot, "_collapsed"), overlayQuery.matches);
|
|
5525
|
+
sidebarScrollMemory.sync();
|
|
5339
5526
|
};
|
|
5340
5527
|
const schedule = () => {
|
|
5341
5528
|
if (scheduled === 0) scheduled = requestAnimationFrame(sync);
|
|
@@ -5400,6 +5587,7 @@ window.__ModuleLoader__.load({
|
|
|
5400
5587
|
return () => {
|
|
5401
5588
|
disposed = true;
|
|
5402
5589
|
stopStockBack();
|
|
5590
|
+
sidebarScrollMemory.dispose();
|
|
5403
5591
|
disposeTaskWatcher();
|
|
5404
5592
|
mediaRequestGeneration += 1;
|
|
5405
5593
|
mediaPickerAbortController.abort();
|
|
@@ -8506,6 +8694,779 @@ window.__ModuleLoader__.load({
|
|
|
8506
8694
|
};
|
|
8507
8695
|
}
|
|
8508
8696
|
//#endregion
|
|
8697
|
+
//#region src/mobile-push/browser-api.ts
|
|
8698
|
+
function apiError(code) {
|
|
8699
|
+
return Object.assign(/* @__PURE__ */ new Error(code === "stale_generation" ? "Push settings changed elsewhere." : "Push request failed."), { code });
|
|
8700
|
+
}
|
|
8701
|
+
async function check(response) {
|
|
8702
|
+
if (response.ok) return;
|
|
8703
|
+
let code = "request_failed";
|
|
8704
|
+
if (response.status === 409) try {
|
|
8705
|
+
if ((await response.json())?.error === "stale_generation") code = "stale_generation";
|
|
8706
|
+
} catch {}
|
|
8707
|
+
throw apiError(code);
|
|
8708
|
+
}
|
|
8709
|
+
async function json(response) {
|
|
8710
|
+
try {
|
|
8711
|
+
return await response.json();
|
|
8712
|
+
} catch {
|
|
8713
|
+
throw apiError("request_failed");
|
|
8714
|
+
}
|
|
8715
|
+
}
|
|
8716
|
+
async function requestOnce(request, path, init) {
|
|
8717
|
+
try {
|
|
8718
|
+
return await request(path, init);
|
|
8719
|
+
} catch {
|
|
8720
|
+
throw apiError("request_failed");
|
|
8721
|
+
}
|
|
8722
|
+
}
|
|
8723
|
+
function postJson(request, path, value) {
|
|
8724
|
+
return requestOnce(request, path, {
|
|
8725
|
+
method: "POST",
|
|
8726
|
+
headers: { "content-type": "application/json" },
|
|
8727
|
+
body: JSON.stringify(value)
|
|
8728
|
+
});
|
|
8729
|
+
}
|
|
8730
|
+
/** Build the direct authenticated browser API over mobileRequest (single-shot, same-origin, CSRF-aware). */
|
|
8731
|
+
function createMobilePushBrowserApi(request) {
|
|
8732
|
+
return {
|
|
8733
|
+
async getConfig(installationId) {
|
|
8734
|
+
const response = await requestOnce(request, `/mobile-access/push/config?installationId=${encodeURIComponent(installationId)}`, { method: "GET" });
|
|
8735
|
+
await check(response);
|
|
8736
|
+
return json(response);
|
|
8737
|
+
},
|
|
8738
|
+
async enroll(value) {
|
|
8739
|
+
const response = await postJson(request, "/mobile-access/push/subscription", value);
|
|
8740
|
+
await check(response);
|
|
8741
|
+
return json(response);
|
|
8742
|
+
},
|
|
8743
|
+
async updatePreferences(value) {
|
|
8744
|
+
await check(await postJson(request, "/mobile-access/push/preferences", value));
|
|
8745
|
+
},
|
|
8746
|
+
async disable(value) {
|
|
8747
|
+
await check(await postJson(request, "/mobile-access/push/disable", value));
|
|
8748
|
+
},
|
|
8749
|
+
async sendTest(value) {
|
|
8750
|
+
const response = await postJson(request, "/mobile-access/push/test", { installationId: value.installationId });
|
|
8751
|
+
if (response.status === 429) throw apiError("rate_limited");
|
|
8752
|
+
await check(response);
|
|
8753
|
+
},
|
|
8754
|
+
async resolve(reference) {
|
|
8755
|
+
const response = await postJson(request, "/mobile-access/push/resolve", { reference });
|
|
8756
|
+
await check(response);
|
|
8757
|
+
const value = await json(response);
|
|
8758
|
+
if (typeof value !== "object" || value === null || !("sessionId" in value) || typeof value.sessionId !== "string" || value.sessionId === "" || !("interaction" in value) || value.interaction !== "current" && value.interaction !== "resolved") return void 0;
|
|
8759
|
+
return value;
|
|
8760
|
+
}
|
|
8761
|
+
};
|
|
8762
|
+
}
|
|
8763
|
+
//#endregion
|
|
8764
|
+
//#region src/mobile-push/installation-id.ts
|
|
8765
|
+
/** Browser-safe installation-id validation shared by the browser controller and the Gateway. */
|
|
8766
|
+
const INSTALLATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
8767
|
+
/** True only for a UUID-shaped installation id with an allowed version and variant nibble. */
|
|
8768
|
+
function isPushInstallationId(value) {
|
|
8769
|
+
return typeof value === "string" && INSTALLATION_ID.test(value);
|
|
8770
|
+
}
|
|
8771
|
+
//#endregion
|
|
8772
|
+
//#region src/mobile-push/client.ts
|
|
8773
|
+
const ID_KEY = "dsh-mobile-push-installation";
|
|
8774
|
+
const defaults = Object.freeze({
|
|
8775
|
+
question: true,
|
|
8776
|
+
plan: true,
|
|
8777
|
+
approval: true,
|
|
8778
|
+
response: true,
|
|
8779
|
+
goal: true,
|
|
8780
|
+
failure: true
|
|
8781
|
+
});
|
|
8782
|
+
function decodeKey(encoded) {
|
|
8783
|
+
const normalized = encoded.replace(/-/gu, "+").replace(/_/gu, "/");
|
|
8784
|
+
const raw = atob(normalized + "=".repeat((4 - normalized.length % 4) % 4));
|
|
8785
|
+
return Uint8Array.from(raw, (char) => char.charCodeAt(0));
|
|
8786
|
+
}
|
|
8787
|
+
function safeError(error) {
|
|
8788
|
+
return (error && typeof error === "object" && "code" in error ? String(error.code) : "") === "stale_generation" ? "push.error.stale" : "push.error.generic";
|
|
8789
|
+
}
|
|
8790
|
+
function sameKey(subscription, key) {
|
|
8791
|
+
const raw = subscription.options.applicationServerKey;
|
|
8792
|
+
if (!raw) return false;
|
|
8793
|
+
const bytes = new Uint8Array(raw instanceof ArrayBuffer ? raw : raw.buffer, raw instanceof ArrayBuffer ? 0 : raw.byteOffset, raw instanceof ArrayBuffer ? raw.byteLength : raw.byteLength);
|
|
8794
|
+
return bytes.length === key.length && bytes.every((value, index) => value === key[index]);
|
|
8795
|
+
}
|
|
8796
|
+
/** Construct with a caller-owned authenticated API adapter and an explicit public window environment. */
|
|
8797
|
+
function createMobilePushController(api, environment) {
|
|
8798
|
+
let snapshot = Object.freeze({ state: "unsupported" });
|
|
8799
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
8800
|
+
let active = false;
|
|
8801
|
+
let generation = 0;
|
|
8802
|
+
let config;
|
|
8803
|
+
let preparedKey;
|
|
8804
|
+
let existing = null;
|
|
8805
|
+
let preparedInstallId = "";
|
|
8806
|
+
let memoryInstallId = "";
|
|
8807
|
+
let mutationBusy = false;
|
|
8808
|
+
let testInFlight = false;
|
|
8809
|
+
let testOwner = 0;
|
|
8810
|
+
let attempt = 0;
|
|
8811
|
+
let ownedSubscription;
|
|
8812
|
+
let currentHostSubscription;
|
|
8813
|
+
const consentedSubscriptions = /* @__PURE__ */ new WeakSet();
|
|
8814
|
+
const pendingEnrollments = /* @__PURE__ */ new WeakMap();
|
|
8815
|
+
const trackPendingEnrollment = (subscription, operation) => {
|
|
8816
|
+
let pending = pendingEnrollments.get(subscription);
|
|
8817
|
+
if (!pending) {
|
|
8818
|
+
pending = /* @__PURE__ */ new Set();
|
|
8819
|
+
pendingEnrollments.set(subscription, pending);
|
|
8820
|
+
}
|
|
8821
|
+
pending.add(operation);
|
|
8822
|
+
};
|
|
8823
|
+
const finishPendingEnrollment = (subscription, operation) => {
|
|
8824
|
+
const pending = pendingEnrollments.get(subscription);
|
|
8825
|
+
pending?.delete(operation);
|
|
8826
|
+
if (pending?.size === 0) pendingEnrollments.delete(subscription);
|
|
8827
|
+
};
|
|
8828
|
+
const mayUnsubscribeCreatedSubscription = (subscription) => !consentedSubscriptions.has(subscription) && (pendingEnrollments.get(subscription)?.size ?? 0) === 0;
|
|
8829
|
+
const publish = (value) => {
|
|
8830
|
+
snapshot = Object.freeze({
|
|
8831
|
+
...value,
|
|
8832
|
+
testInFlight
|
|
8833
|
+
});
|
|
8834
|
+
for (const listener of listeners) listener();
|
|
8835
|
+
};
|
|
8836
|
+
const current = (token) => active && generation === token;
|
|
8837
|
+
const getInstallationId = () => {
|
|
8838
|
+
if (memoryInstallId) return memoryInstallId;
|
|
8839
|
+
try {
|
|
8840
|
+
const saved = environment.storage?.getItem(ID_KEY);
|
|
8841
|
+
if (isPushInstallationId(saved)) return memoryInstallId = saved;
|
|
8842
|
+
} catch {}
|
|
8843
|
+
const fresh = environment.randomUUID?.() ?? globalThis.crypto?.randomUUID?.();
|
|
8844
|
+
if (!isPushInstallationId(fresh)) throw new Error("uuid unavailable");
|
|
8845
|
+
memoryInstallId = fresh;
|
|
8846
|
+
try {
|
|
8847
|
+
environment.storage?.setItem(ID_KEY, fresh);
|
|
8848
|
+
} catch {}
|
|
8849
|
+
return fresh;
|
|
8850
|
+
};
|
|
8851
|
+
const detect = () => {
|
|
8852
|
+
if (!environment.secureContext || !environment.isTopLevel || !environment.pushManager || environment.permission === "unsupported") return "unsupported";
|
|
8853
|
+
if (!environment.standalone) return "install";
|
|
8854
|
+
if (environment.permission === "denied") return "denied";
|
|
8855
|
+
};
|
|
8856
|
+
const refresh = async (token) => {
|
|
8857
|
+
const blocked = detect();
|
|
8858
|
+
if (blocked) {
|
|
8859
|
+
if (current(token)) publish({ state: blocked });
|
|
8860
|
+
return false;
|
|
8861
|
+
}
|
|
8862
|
+
try {
|
|
8863
|
+
const requestedInstallId = getInstallationId();
|
|
8864
|
+
const [nextConfig, subscription] = await Promise.all([api.getConfig(requestedInstallId), environment.pushManager.getSubscription()]);
|
|
8865
|
+
if (!current(token)) return false;
|
|
8866
|
+
config = nextConfig;
|
|
8867
|
+
existing = subscription;
|
|
8868
|
+
preparedInstallId = nextConfig.installationId || requestedInstallId;
|
|
8869
|
+
memoryInstallId = preparedInstallId;
|
|
8870
|
+
try {
|
|
8871
|
+
environment.storage?.setItem(ID_KEY, preparedInstallId);
|
|
8872
|
+
} catch {}
|
|
8873
|
+
preparedKey = nextConfig.publicKey ? decodeKey(nextConfig.publicKey) : void 0;
|
|
8874
|
+
if (!nextConfig.available || !preparedKey) publish({
|
|
8875
|
+
state: "unsupported",
|
|
8876
|
+
preferences: nextConfig.preferences
|
|
8877
|
+
});
|
|
8878
|
+
else if (nextConfig.status === "enabled") {
|
|
8879
|
+
if (subscription) {
|
|
8880
|
+
currentHostSubscription = subscription;
|
|
8881
|
+
consentedSubscriptions.add(subscription);
|
|
8882
|
+
}
|
|
8883
|
+
publish({
|
|
8884
|
+
state: nextConfig.status,
|
|
8885
|
+
preferences: nextConfig.preferences
|
|
8886
|
+
});
|
|
8887
|
+
} else if (nextConfig.status === "suspended") {
|
|
8888
|
+
if (subscription) {
|
|
8889
|
+
currentHostSubscription = subscription;
|
|
8890
|
+
consentedSubscriptions.add(subscription);
|
|
8891
|
+
}
|
|
8892
|
+
publish({
|
|
8893
|
+
state: "suspended",
|
|
8894
|
+
preferences: nextConfig.preferences
|
|
8895
|
+
});
|
|
8896
|
+
} else {
|
|
8897
|
+
if (nextConfig.status === "disabled" && currentHostSubscription) {
|
|
8898
|
+
consentedSubscriptions.delete(currentHostSubscription);
|
|
8899
|
+
currentHostSubscription = void 0;
|
|
8900
|
+
}
|
|
8901
|
+
publish({
|
|
8902
|
+
state: "ready",
|
|
8903
|
+
preferences: nextConfig.preferences
|
|
8904
|
+
});
|
|
8905
|
+
}
|
|
8906
|
+
return true;
|
|
8907
|
+
} catch (error) {
|
|
8908
|
+
if (current(token)) publish({
|
|
8909
|
+
state: "failed",
|
|
8910
|
+
error: safeError(error)
|
|
8911
|
+
});
|
|
8912
|
+
return false;
|
|
8913
|
+
}
|
|
8914
|
+
};
|
|
8915
|
+
const refreshNow = () => {
|
|
8916
|
+
if (active) refresh(generation);
|
|
8917
|
+
};
|
|
8918
|
+
return {
|
|
8919
|
+
start() {
|
|
8920
|
+
active = true;
|
|
8921
|
+
generation += 1;
|
|
8922
|
+
config = void 0;
|
|
8923
|
+
ownedSubscription = void 0;
|
|
8924
|
+
publish({ state: "pending" });
|
|
8925
|
+
refreshNow();
|
|
8926
|
+
},
|
|
8927
|
+
stop() {
|
|
8928
|
+
active = false;
|
|
8929
|
+
generation += 1;
|
|
8930
|
+
attempt += 1;
|
|
8931
|
+
mutationBusy = false;
|
|
8932
|
+
testOwner += 1;
|
|
8933
|
+
if (testInFlight) {
|
|
8934
|
+
testInFlight = false;
|
|
8935
|
+
publish({ ...snapshot });
|
|
8936
|
+
}
|
|
8937
|
+
config = void 0;
|
|
8938
|
+
preparedKey = void 0;
|
|
8939
|
+
existing = null;
|
|
8940
|
+
ownedSubscription = void 0;
|
|
8941
|
+
},
|
|
8942
|
+
refresh: refreshNow,
|
|
8943
|
+
async enableFromGesture() {
|
|
8944
|
+
if (!active || mutationBusy || snapshot.state !== "ready" && snapshot.state !== "failed" && snapshot.state !== "disabled" || !config || !preparedKey || !environment.pushManager) return;
|
|
8945
|
+
const token = generation, operation = ++attempt, base = config;
|
|
8946
|
+
mutationBusy = true;
|
|
8947
|
+
publish({
|
|
8948
|
+
state: "pending",
|
|
8949
|
+
preferences: base.preferences
|
|
8950
|
+
});
|
|
8951
|
+
let subscription = existing;
|
|
8952
|
+
let created = false;
|
|
8953
|
+
let pendingSubscription;
|
|
8954
|
+
try {
|
|
8955
|
+
if (subscription && !sameKey(subscription, preparedKey)) {
|
|
8956
|
+
publish({
|
|
8957
|
+
state: "failed",
|
|
8958
|
+
error: "push.error.subscriptionConflict",
|
|
8959
|
+
preferences: base.preferences
|
|
8960
|
+
});
|
|
8961
|
+
return;
|
|
8962
|
+
}
|
|
8963
|
+
if (!subscription) {
|
|
8964
|
+
subscription = await environment.pushManager.subscribe({
|
|
8965
|
+
userVisibleOnly: true,
|
|
8966
|
+
applicationServerKey: preparedKey
|
|
8967
|
+
});
|
|
8968
|
+
created = true;
|
|
8969
|
+
}
|
|
8970
|
+
if (created) ownedSubscription = subscription;
|
|
8971
|
+
if (!current(token) || operation !== attempt) {
|
|
8972
|
+
if (created && mayUnsubscribeCreatedSubscription(subscription)) {
|
|
8973
|
+
if (await subscription.unsubscribe().catch(() => false) && ownedSubscription === subscription) ownedSubscription = void 0;
|
|
8974
|
+
}
|
|
8975
|
+
return;
|
|
8976
|
+
}
|
|
8977
|
+
trackPendingEnrollment(subscription, operation);
|
|
8978
|
+
pendingSubscription = subscription;
|
|
8979
|
+
const browserSubscription = subscription.toJSON();
|
|
8980
|
+
const result = await api.enroll({
|
|
8981
|
+
installationId: preparedInstallId || base.installationId,
|
|
8982
|
+
subscription: {
|
|
8983
|
+
endpoint: browserSubscription.endpoint,
|
|
8984
|
+
keys: {
|
|
8985
|
+
p256dh: browserSubscription.keys.p256dh,
|
|
8986
|
+
auth: browserSubscription.keys.auth
|
|
8987
|
+
}
|
|
8988
|
+
},
|
|
8989
|
+
locale: environment.locale ?? "en",
|
|
8990
|
+
preferences: base.preferences ?? defaults,
|
|
8991
|
+
keyFingerprint: base.keyFingerprint ?? "",
|
|
8992
|
+
ownerGeneration: base.ownerGeneration,
|
|
8993
|
+
registrationGeneration: base.registrationGeneration
|
|
8994
|
+
});
|
|
8995
|
+
finishPendingEnrollment(subscription, operation);
|
|
8996
|
+
pendingSubscription = void 0;
|
|
8997
|
+
if (!current(token) || operation !== attempt) {
|
|
8998
|
+
if (result.mutationGeneration !== void 0) await api.disable({
|
|
8999
|
+
installationId: result.registration.installationId,
|
|
9000
|
+
ifGeneration: result.mutationGeneration
|
|
9001
|
+
}).catch(() => void 0);
|
|
9002
|
+
if (created && mayUnsubscribeCreatedSubscription(subscription)) {
|
|
9003
|
+
if (await subscription.unsubscribe().catch(() => false) && ownedSubscription === subscription) ownedSubscription = void 0;
|
|
9004
|
+
}
|
|
9005
|
+
return;
|
|
9006
|
+
}
|
|
9007
|
+
if (created) ownedSubscription = subscription;
|
|
9008
|
+
consentedSubscriptions.add(subscription);
|
|
9009
|
+
currentHostSubscription = subscription;
|
|
9010
|
+
if (result.registration.origin === environment.origin && result.registration.installationId) {
|
|
9011
|
+
memoryInstallId = result.registration.installationId;
|
|
9012
|
+
preparedInstallId = memoryInstallId;
|
|
9013
|
+
try {
|
|
9014
|
+
environment.storage?.setItem(ID_KEY, memoryInstallId);
|
|
9015
|
+
} catch {}
|
|
9016
|
+
}
|
|
9017
|
+
config = {
|
|
9018
|
+
...base,
|
|
9019
|
+
installationId: preparedInstallId || result.registration.installationId,
|
|
9020
|
+
registrationGeneration: result.registration.generation,
|
|
9021
|
+
status: result.registration.health === "suspended" ? "suspended" : "enabled",
|
|
9022
|
+
preferences: result.registration.preferences
|
|
9023
|
+
};
|
|
9024
|
+
publish({
|
|
9025
|
+
state: result.registration.health === "suspended" ? "suspended" : "enabled",
|
|
9026
|
+
preferences: result.registration.preferences
|
|
9027
|
+
});
|
|
9028
|
+
} catch (error) {
|
|
9029
|
+
if (pendingSubscription) finishPendingEnrollment(pendingSubscription, operation);
|
|
9030
|
+
if (created && subscription && mayUnsubscribeCreatedSubscription(subscription)) {
|
|
9031
|
+
if (await subscription.unsubscribe().catch(() => false) && ownedSubscription === subscription) ownedSubscription = void 0;
|
|
9032
|
+
}
|
|
9033
|
+
if (current(token) && operation === attempt) {
|
|
9034
|
+
const isStale = safeError(error) === "push.error.stale";
|
|
9035
|
+
publish({
|
|
9036
|
+
state: "failed",
|
|
9037
|
+
error: safeError(error),
|
|
9038
|
+
preferences: base.preferences
|
|
9039
|
+
});
|
|
9040
|
+
if (isStale) refresh(token);
|
|
9041
|
+
}
|
|
9042
|
+
} finally {
|
|
9043
|
+
if (current(token) && operation === attempt) mutationBusy = false;
|
|
9044
|
+
}
|
|
9045
|
+
},
|
|
9046
|
+
async disable() {
|
|
9047
|
+
if (!active || !config) return;
|
|
9048
|
+
const token = generation, operation = ++attempt, base = config;
|
|
9049
|
+
mutationBusy = true;
|
|
9050
|
+
publish({
|
|
9051
|
+
state: "pending",
|
|
9052
|
+
preferences: base.preferences
|
|
9053
|
+
});
|
|
9054
|
+
try {
|
|
9055
|
+
await api.disable({ installationId: preparedInstallId || base.installationId });
|
|
9056
|
+
if (!current(token) || operation !== attempt) return;
|
|
9057
|
+
config = {
|
|
9058
|
+
...base,
|
|
9059
|
+
status: "disabled"
|
|
9060
|
+
};
|
|
9061
|
+
publish({
|
|
9062
|
+
state: "disabled",
|
|
9063
|
+
preferences: base.preferences
|
|
9064
|
+
});
|
|
9065
|
+
if (currentHostSubscription) consentedSubscriptions.delete(currentHostSubscription);
|
|
9066
|
+
currentHostSubscription = void 0;
|
|
9067
|
+
const sub = ownedSubscription;
|
|
9068
|
+
if (sub) {
|
|
9069
|
+
if (await sub.unsubscribe().catch(() => false) && ownedSubscription === sub) ownedSubscription = void 0;
|
|
9070
|
+
}
|
|
9071
|
+
if (await refresh(token) && current(token) && operation === attempt && config?.status === "disabled") publish({
|
|
9072
|
+
state: "disabled",
|
|
9073
|
+
preferences: config.preferences
|
|
9074
|
+
});
|
|
9075
|
+
} catch (error) {
|
|
9076
|
+
if (current(token) && operation === attempt) {
|
|
9077
|
+
publish({
|
|
9078
|
+
state: "failed",
|
|
9079
|
+
error: safeError(error),
|
|
9080
|
+
preferences: base.preferences
|
|
9081
|
+
});
|
|
9082
|
+
if (safeError(error) === "push.error.stale") refresh(token);
|
|
9083
|
+
}
|
|
9084
|
+
} finally {
|
|
9085
|
+
if (current(token) && operation === attempt) mutationBusy = false;
|
|
9086
|
+
}
|
|
9087
|
+
},
|
|
9088
|
+
async updatePreferences(value) {
|
|
9089
|
+
if (!active || !config || mutationBusy) return;
|
|
9090
|
+
const token = generation, operation = ++attempt, base = config;
|
|
9091
|
+
mutationBusy = true;
|
|
9092
|
+
publish({
|
|
9093
|
+
state: "pending",
|
|
9094
|
+
preferences: value
|
|
9095
|
+
});
|
|
9096
|
+
try {
|
|
9097
|
+
await api.updatePreferences({
|
|
9098
|
+
installationId: preparedInstallId || base.installationId,
|
|
9099
|
+
ownerGeneration: base.ownerGeneration,
|
|
9100
|
+
registrationGeneration: base.registrationGeneration,
|
|
9101
|
+
preferences: value
|
|
9102
|
+
});
|
|
9103
|
+
if (current(token) && operation === attempt) {
|
|
9104
|
+
config = {
|
|
9105
|
+
...base,
|
|
9106
|
+
preferences: value
|
|
9107
|
+
};
|
|
9108
|
+
publish({
|
|
9109
|
+
state: base.status === "enabled" ? "enabled" : "ready",
|
|
9110
|
+
preferences: value
|
|
9111
|
+
});
|
|
9112
|
+
await refresh(token);
|
|
9113
|
+
}
|
|
9114
|
+
} catch (error) {
|
|
9115
|
+
if (current(token) && operation === attempt) {
|
|
9116
|
+
publish({
|
|
9117
|
+
state: "failed",
|
|
9118
|
+
error: safeError(error),
|
|
9119
|
+
preferences: base.preferences
|
|
9120
|
+
});
|
|
9121
|
+
if (safeError(error) === "push.error.stale") refresh(token);
|
|
9122
|
+
}
|
|
9123
|
+
} finally {
|
|
9124
|
+
if (current(token) && operation === attempt) mutationBusy = false;
|
|
9125
|
+
}
|
|
9126
|
+
},
|
|
9127
|
+
async sendTest() {
|
|
9128
|
+
if (!active || !config || config.status !== "enabled" || mutationBusy || testInFlight) return;
|
|
9129
|
+
const token = generation, operation = attempt, base = config;
|
|
9130
|
+
const owner = ++testOwner;
|
|
9131
|
+
testInFlight = true;
|
|
9132
|
+
publish({
|
|
9133
|
+
state: "enabled",
|
|
9134
|
+
preferences: base.preferences
|
|
9135
|
+
});
|
|
9136
|
+
try {
|
|
9137
|
+
await api.sendTest({
|
|
9138
|
+
installationId: preparedInstallId || base.installationId,
|
|
9139
|
+
ownerGeneration: base.ownerGeneration,
|
|
9140
|
+
registrationGeneration: base.registrationGeneration
|
|
9141
|
+
});
|
|
9142
|
+
if (current(token) && operation === attempt) publish({
|
|
9143
|
+
state: "enabled",
|
|
9144
|
+
error: "push.test.accepted",
|
|
9145
|
+
preferences: base.preferences
|
|
9146
|
+
});
|
|
9147
|
+
} catch (error) {
|
|
9148
|
+
if (!current(token) || operation !== attempt) return;
|
|
9149
|
+
const message = error?.code === "rate_limited" ? "push.test.rateLimited" : "push.test.failed";
|
|
9150
|
+
const refreshed = await refresh(token);
|
|
9151
|
+
if (!current(token) || operation !== attempt) return;
|
|
9152
|
+
if (refreshed && config?.status === "enabled") publish({
|
|
9153
|
+
state: "enabled",
|
|
9154
|
+
error: message,
|
|
9155
|
+
preferences: config.preferences
|
|
9156
|
+
});
|
|
9157
|
+
} finally {
|
|
9158
|
+
if (owner === testOwner) {
|
|
9159
|
+
testInFlight = false;
|
|
9160
|
+
publish({ ...snapshot });
|
|
9161
|
+
}
|
|
9162
|
+
}
|
|
9163
|
+
},
|
|
9164
|
+
getSnapshot: () => snapshot,
|
|
9165
|
+
subscribe(listener) {
|
|
9166
|
+
listeners.add(listener);
|
|
9167
|
+
return () => {
|
|
9168
|
+
listeners.delete(listener);
|
|
9169
|
+
};
|
|
9170
|
+
}
|
|
9171
|
+
};
|
|
9172
|
+
}
|
|
9173
|
+
/** Adapter factory uses only the page's public PushManager; it deliberately has no service-worker path. */
|
|
9174
|
+
function browserPushEnvironment(view = window) {
|
|
9175
|
+
const nav = view.navigator;
|
|
9176
|
+
const pushManager = view.pushManager;
|
|
9177
|
+
const storage = (() => {
|
|
9178
|
+
try {
|
|
9179
|
+
return view.localStorage;
|
|
9180
|
+
} catch {
|
|
9181
|
+
return;
|
|
9182
|
+
}
|
|
9183
|
+
})();
|
|
9184
|
+
const isInstalled = () => {
|
|
9185
|
+
try {
|
|
9186
|
+
return nav.standalone === true || view.matchMedia("(display-mode: standalone)").matches || view.matchMedia("(display-mode: fullscreen)").matches;
|
|
9187
|
+
} catch {
|
|
9188
|
+
return nav.standalone === true;
|
|
9189
|
+
}
|
|
9190
|
+
};
|
|
9191
|
+
return {
|
|
9192
|
+
secureContext: view.isSecureContext,
|
|
9193
|
+
isTopLevel: view.top === view.self,
|
|
9194
|
+
get standalone() {
|
|
9195
|
+
return isInstalled();
|
|
9196
|
+
},
|
|
9197
|
+
get permission() {
|
|
9198
|
+
return "Notification" in view ? view.Notification.permission : "unsupported";
|
|
9199
|
+
},
|
|
9200
|
+
pushManager,
|
|
9201
|
+
storage,
|
|
9202
|
+
origin: view.location.origin,
|
|
9203
|
+
randomUUID: () => view.crypto.randomUUID(),
|
|
9204
|
+
locale: view.document.documentElement.lang.toLowerCase().startsWith("zh") ? "zh-CN" : view.document.documentElement.lang.toLowerCase().startsWith("it") ? "it" : "en"
|
|
9205
|
+
};
|
|
9206
|
+
}
|
|
9207
|
+
//#endregion
|
|
9208
|
+
//#region src/mobile-push/switch-component.ts
|
|
9209
|
+
/**
|
|
9210
|
+
* Lazily require the public Switch from the DSH module loader at category render time.
|
|
9211
|
+
*
|
|
9212
|
+
* The plugin client bundle is a `window.__ModuleLoader__.load` factory whose `require` parameter
|
|
9213
|
+
* resolves seeded shell modules synchronously (`rc.2` and `rc.3` both seed the primitives namespace
|
|
9214
|
+
* unconditionally). Requiring lazily — inside the render that needs it, never at module scope —
|
|
9215
|
+
* keeps every Node test and provider-free fixture that mounts the settings row but not the push
|
|
9216
|
+
* toggles free of any browser-only module dependency. Outside a DSH shell there is no loader
|
|
9217
|
+
* `require`, so the categories are withdrawn instead of rendering a broken stand-in; native
|
|
9218
|
+
* rendering itself is verified by the strict packed smoke against the real mounted shell.
|
|
9219
|
+
*/
|
|
9220
|
+
function requirePublicMobilePushSwitch() {
|
|
9221
|
+
if (typeof window === "undefined" || typeof require !== "function") return void 0;
|
|
9222
|
+
try {
|
|
9223
|
+
return require("@deepseek-ai/dsh-client-ui-primitives").Switch;
|
|
9224
|
+
} catch {
|
|
9225
|
+
return;
|
|
9226
|
+
}
|
|
9227
|
+
}
|
|
9228
|
+
//#endregion
|
|
9229
|
+
//#region src/mobile-push/settings.ts
|
|
9230
|
+
const MOBILE_PUSH_SETTINGS_COPY = {
|
|
9231
|
+
en: {
|
|
9232
|
+
title: "Web push notifications",
|
|
9233
|
+
desc: "Receive notifications when this page is closed.",
|
|
9234
|
+
enable: "Enable",
|
|
9235
|
+
disable: "Disable",
|
|
9236
|
+
test: "Send test",
|
|
9237
|
+
testBusy: "Sending…",
|
|
9238
|
+
accepted: "Accepted by the push provider; lock-screen delivery is not confirmed.",
|
|
9239
|
+
install: "Add this site to your Home Screen to enable push.",
|
|
9240
|
+
denied: "Notifications are blocked in browser settings.",
|
|
9241
|
+
unsupported: "Web push is unavailable in this context.",
|
|
9242
|
+
error: "Could not update push settings. Try again.",
|
|
9243
|
+
testFailed: "The test notification could not be sent. Your notification settings are unchanged.",
|
|
9244
|
+
testRateLimited: "Too many test notifications; wait a moment before trying again.",
|
|
9245
|
+
conflict: "This device already has a push subscription for an earlier server key, so enabling cannot complete. Retrying will not fix this; the server operator must check the push key state first.",
|
|
9246
|
+
busy: "Working…",
|
|
9247
|
+
categories: [
|
|
9248
|
+
"Questions",
|
|
9249
|
+
"Plans",
|
|
9250
|
+
"Approvals",
|
|
9251
|
+
"Responses",
|
|
9252
|
+
"Goals",
|
|
9253
|
+
"Failures"
|
|
9254
|
+
]
|
|
9255
|
+
},
|
|
9256
|
+
zh: {
|
|
9257
|
+
title: "网页推送通知",
|
|
9258
|
+
desc: "关闭此页面后仍可接收通知。",
|
|
9259
|
+
enable: "启用",
|
|
9260
|
+
disable: "停用",
|
|
9261
|
+
test: "发送测试",
|
|
9262
|
+
testBusy: "发送中…",
|
|
9263
|
+
accepted: "推送服务商已接受;不代表锁屏一定送达。",
|
|
9264
|
+
install: "请将此网站添加到主屏幕以启用推送。",
|
|
9265
|
+
denied: "浏览器设置已阻止通知。",
|
|
9266
|
+
unsupported: "此环境不支持网页推送。",
|
|
9267
|
+
error: "无法更新推送设置,请重试。",
|
|
9268
|
+
testFailed: "测试通知未能发送。通知设置保持不变。",
|
|
9269
|
+
testRateLimited: "测试通知发送过于频繁,请稍后再试。",
|
|
9270
|
+
conflict: "此设备已存在与旧服务器密钥绑定的推送订阅,因此无法完成启用。重试无法解决;请先由服务器管理员检查推送密钥状态。",
|
|
9271
|
+
busy: "处理中…",
|
|
9272
|
+
categories: [
|
|
9273
|
+
"问题",
|
|
9274
|
+
"计划",
|
|
9275
|
+
"审批",
|
|
9276
|
+
"回复",
|
|
9277
|
+
"目标",
|
|
9278
|
+
"失败"
|
|
9279
|
+
]
|
|
9280
|
+
},
|
|
9281
|
+
it: {
|
|
9282
|
+
title: "Notifiche Web push",
|
|
9283
|
+
desc: "Ricevi notifiche quando la pagina è chiusa.",
|
|
9284
|
+
enable: "Attiva",
|
|
9285
|
+
disable: "Disattiva",
|
|
9286
|
+
test: "Invia test",
|
|
9287
|
+
testBusy: "Invio…",
|
|
9288
|
+
accepted: "Accettata dal provider; la consegna sulla schermata di blocco non è confermata.",
|
|
9289
|
+
install: "Aggiungi il sito alla schermata Home per attivare il push.",
|
|
9290
|
+
denied: "Le notifiche sono bloccate nelle impostazioni del browser.",
|
|
9291
|
+
unsupported: "Web push non disponibile in questo contesto.",
|
|
9292
|
+
error: "Impossibile aggiornare le impostazioni push. Riprova.",
|
|
9293
|
+
testFailed: "Impossibile inviare la notifica di test. Le impostazioni delle notifiche restano invariate.",
|
|
9294
|
+
testRateLimited: "Troppe notifiche di test; attendi qualche istante prima di riprovare.",
|
|
9295
|
+
conflict: "Questo dispositivo ha già una sottoscrizione push associata a una chiave server precedente, quindi l’attivazione non può completarsi. Riprovare non risolve il problema; l’operatore del server deve prima controllare lo stato della chiave push.",
|
|
9296
|
+
busy: "Operazione…",
|
|
9297
|
+
categories: [
|
|
9298
|
+
"Domande",
|
|
9299
|
+
"Piani",
|
|
9300
|
+
"Approvazioni",
|
|
9301
|
+
"Risposte",
|
|
9302
|
+
"Obiettivi",
|
|
9303
|
+
"Errori"
|
|
9304
|
+
]
|
|
9305
|
+
}
|
|
9306
|
+
};
|
|
9307
|
+
const KEYS = [
|
|
9308
|
+
"question",
|
|
9309
|
+
"plan",
|
|
9310
|
+
"approval",
|
|
9311
|
+
"response",
|
|
9312
|
+
"goal",
|
|
9313
|
+
"failure"
|
|
9314
|
+
];
|
|
9315
|
+
function message(snapshot, copy) {
|
|
9316
|
+
if (snapshot.state === "install") return copy.install;
|
|
9317
|
+
if (snapshot.state === "denied") return copy.denied;
|
|
9318
|
+
if (snapshot.state === "unsupported") return copy.unsupported;
|
|
9319
|
+
if (snapshot.state === "enabled" && snapshot.error === "push.test.accepted") return copy.accepted;
|
|
9320
|
+
if (snapshot.state === "enabled" && snapshot.error === "push.test.rateLimited") return copy.testRateLimited;
|
|
9321
|
+
if (snapshot.state === "enabled" && snapshot.error === "push.test.failed") return copy.testFailed;
|
|
9322
|
+
if (snapshot.state === "failed" && snapshot.error === "push.error.subscriptionConflict") return copy.conflict;
|
|
9323
|
+
return snapshot.state === "failed" ? copy.error : copy.desc;
|
|
9324
|
+
}
|
|
9325
|
+
/** Browser-only settings row; mounting it never requests permission or creates a subscription. */
|
|
9326
|
+
function MobilePushSettings({ controller, locale, switchComponent }) {
|
|
9327
|
+
const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
9328
|
+
const copy = MOBILE_PUSH_SETTINGS_COPY[locale];
|
|
9329
|
+
if (snapshot.state === "unsupported") return null;
|
|
9330
|
+
const prefs = snapshot.preferences;
|
|
9331
|
+
const Switch = switchComponent ?? requirePublicMobilePushSwitch();
|
|
9332
|
+
const toggle = (key, checked) => {
|
|
9333
|
+
if (!prefs) return;
|
|
9334
|
+
controller.updatePreferences({
|
|
9335
|
+
...prefs,
|
|
9336
|
+
[key]: checked
|
|
9337
|
+
});
|
|
9338
|
+
};
|
|
9339
|
+
return (0, react.createElement)("div", {
|
|
9340
|
+
className: "dsh-mobile-settings_row",
|
|
9341
|
+
style: { flexWrap: "wrap" },
|
|
9342
|
+
lang: locale === "zh" ? "zh-CN" : locale
|
|
9343
|
+
}, (0, react.createElement)("div", {
|
|
9344
|
+
className: "dsh-mobile-settings_rowText",
|
|
9345
|
+
style: { paddingRight: 8 }
|
|
9346
|
+
}, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
|
|
9347
|
+
className: "dsh-mobile-settings_desc",
|
|
9348
|
+
"aria-live": "polite"
|
|
9349
|
+
}, message(snapshot, copy)), prefs && Switch !== void 0 ? (0, react.createElement)("div", { className: "dsh-mobile-push-categories" }, ...KEYS.map((key, index) => {
|
|
9350
|
+
const name = copy.categories[index];
|
|
9351
|
+
if (name === void 0) return null;
|
|
9352
|
+
return (0, react.createElement)("label", {
|
|
9353
|
+
key,
|
|
9354
|
+
className: "dsh-mobile-push-category"
|
|
9355
|
+
}, (0, react.createElement)("span", { className: "dsh-mobile-push-categoryName" }, name), (0, react.createElement)(Switch, {
|
|
9356
|
+
checked: prefs[key],
|
|
9357
|
+
label: name,
|
|
9358
|
+
onChange: (next) => {
|
|
9359
|
+
toggle(key, next);
|
|
9360
|
+
}
|
|
9361
|
+
}));
|
|
9362
|
+
})) : null), (0, react.createElement)("div", {
|
|
9363
|
+
className: "dsh-mobile-push-actions",
|
|
9364
|
+
style: {
|
|
9365
|
+
display: "flex",
|
|
9366
|
+
flexWrap: "wrap",
|
|
9367
|
+
gap: 8
|
|
9368
|
+
}
|
|
9369
|
+
}, snapshot.state === "enabled" ? (0, react.createElement)("button", {
|
|
9370
|
+
type: "button",
|
|
9371
|
+
className: "dsh-mobile-settings_selector",
|
|
9372
|
+
disabled: snapshot.testInFlight === true,
|
|
9373
|
+
"aria-busy": snapshot.testInFlight === true ? "true" : void 0,
|
|
9374
|
+
onClick: () => void controller.sendTest()
|
|
9375
|
+
}, snapshot.testInFlight ? copy.testBusy : copy.test) : null, snapshot.state === "enabled" || snapshot.state === "suspended" ? (0, react.createElement)("button", {
|
|
9376
|
+
type: "button",
|
|
9377
|
+
className: "dsh-mobile-settings_selector",
|
|
9378
|
+
onClick: () => void controller.disable()
|
|
9379
|
+
}, copy.disable) : snapshot.state === "ready" || snapshot.state === "failed" || snapshot.state === "disabled" ? (0, react.createElement)("button", {
|
|
9380
|
+
type: "button",
|
|
9381
|
+
className: "dsh-mobile-settings_selector",
|
|
9382
|
+
onClick: () => void controller.enableFromGesture()
|
|
9383
|
+
}, copy.enable) : (0, react.createElement)("button", {
|
|
9384
|
+
type: "button",
|
|
9385
|
+
className: "dsh-mobile-settings_selector",
|
|
9386
|
+
disabled: true
|
|
9387
|
+
}, copy.busy)));
|
|
9388
|
+
}
|
|
9389
|
+
//#endregion
|
|
9390
|
+
//#region src/mobile-push/navigation.ts
|
|
9391
|
+
const REF_PARAMETER = "mobile-push";
|
|
9392
|
+
/**
|
|
9393
|
+
* Consume one authenticated tap reference after the public Workspace and Session
|
|
9394
|
+
* list baselines are ready. Return the disposal function for list subscriptions.
|
|
9395
|
+
*/
|
|
9396
|
+
function installPushNavigation(ctx, options) {
|
|
9397
|
+
const context = ctx;
|
|
9398
|
+
const browser = options.browser ?? {
|
|
9399
|
+
location: window.location,
|
|
9400
|
+
history: window.history
|
|
9401
|
+
};
|
|
9402
|
+
const params = new URLSearchParams(browser.location.search);
|
|
9403
|
+
const reference = params.get(REF_PARAMETER);
|
|
9404
|
+
let disposed = false;
|
|
9405
|
+
let consumed = false;
|
|
9406
|
+
let resolving = false;
|
|
9407
|
+
let opened = false;
|
|
9408
|
+
const clearReference = () => {
|
|
9409
|
+
if (!params.has(REF_PARAMETER)) return;
|
|
9410
|
+
params.delete(REF_PARAMETER);
|
|
9411
|
+
const query = params.toString();
|
|
9412
|
+
const url = `${browser.location.pathname}${query ? `?${query}` : ""}${browser.location.hash}`;
|
|
9413
|
+
browser.history.replaceState(null, "", url);
|
|
9414
|
+
};
|
|
9415
|
+
const neutral = (key = "session-unavailable") => {
|
|
9416
|
+
if (disposed || consumed) return;
|
|
9417
|
+
consumed = true;
|
|
9418
|
+
clearReference();
|
|
9419
|
+
options.notify(key);
|
|
9420
|
+
};
|
|
9421
|
+
const check = () => {
|
|
9422
|
+
if (disposed || consumed || resolving) return;
|
|
9423
|
+
if (!reference) {
|
|
9424
|
+
neutral();
|
|
9425
|
+
return;
|
|
9426
|
+
}
|
|
9427
|
+
const sessions = context.sessions.list.getSnapshot();
|
|
9428
|
+
const workspaces = context.workspaces.list.getSnapshot();
|
|
9429
|
+
if (sessions.phase !== "pending" && sessions.phase !== "ready" || workspaces.phase !== "pending" && workspaces.phase !== "ready") {
|
|
9430
|
+
neutral();
|
|
9431
|
+
return;
|
|
9432
|
+
}
|
|
9433
|
+
if (sessions.phase !== "ready" || workspaces.phase !== "ready") return;
|
|
9434
|
+
if (!reference) {
|
|
9435
|
+
neutral();
|
|
9436
|
+
return;
|
|
9437
|
+
}
|
|
9438
|
+
resolving = true;
|
|
9439
|
+
consumed = true;
|
|
9440
|
+
clearReference();
|
|
9441
|
+
options.resolve(reference).then((target) => {
|
|
9442
|
+
if (disposed) return;
|
|
9443
|
+
if (!target || target.interaction !== "current" && target.interaction !== "resolved" || !sessionsAvailable(context, target.sessionId)) {
|
|
9444
|
+
options.notify("session-unavailable");
|
|
9445
|
+
return;
|
|
9446
|
+
}
|
|
9447
|
+
if (opened) return;
|
|
9448
|
+
opened = true;
|
|
9449
|
+
context.uiWorkspace.openSession(target.sessionId);
|
|
9450
|
+
if (target.interaction === "resolved") options.notify("session-resolved");
|
|
9451
|
+
}).catch(() => {
|
|
9452
|
+
if (!disposed) options.notify("session-unavailable");
|
|
9453
|
+
});
|
|
9454
|
+
};
|
|
9455
|
+
const unsubscribeSessions = context.sessions.list.subscribe(check);
|
|
9456
|
+
const unsubscribeWorkspaces = context.workspaces.list.subscribe(check);
|
|
9457
|
+
check();
|
|
9458
|
+
return () => {
|
|
9459
|
+
if (disposed) return;
|
|
9460
|
+
disposed = true;
|
|
9461
|
+
unsubscribeSessions();
|
|
9462
|
+
unsubscribeWorkspaces();
|
|
9463
|
+
};
|
|
9464
|
+
}
|
|
9465
|
+
function sessionsAvailable(ctx, sessionId) {
|
|
9466
|
+
const snapshot = ctx.sessions.list.getSnapshot();
|
|
9467
|
+
return snapshot.phase === "ready" && snapshot.ids.includes(sessionId) && snapshot.byId[sessionId] !== void 0;
|
|
9468
|
+
}
|
|
9469
|
+
//#endregion
|
|
8509
9470
|
//#region src/mobile-directory-browser.ts
|
|
8510
9471
|
/** Expected Gateway or response-validation failure for one read-only listing. */
|
|
8511
9472
|
var MobileDirectoryReadError = class extends Error {
|
|
@@ -9579,6 +10540,12 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
9579
10540
|
function parseMobileRemoteProvider(value) {
|
|
9580
10541
|
return value === "cpolar" || value === "cloudflared" || value === "frp" || value === "origin" || value === "iroh" ? value : "tailscale";
|
|
9581
10542
|
}
|
|
10543
|
+
/** Public client services the authenticated tap consumer follows (scoped child lifecycle, never a required plugin dependency). */
|
|
10544
|
+
const mobilePushNavigationServices = [
|
|
10545
|
+
"sessions",
|
|
10546
|
+
"workspaces",
|
|
10547
|
+
"uiWorkspace"
|
|
10548
|
+
];
|
|
9582
10549
|
/**
|
|
9583
10550
|
* Set a surface's visibility; opening a page or overlay brings it to the front.
|
|
9584
10551
|
* @param container - the registered surface element.
|
|
@@ -15738,6 +16705,107 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15738
16705
|
.dsh-mobile-control__remote-workspace .dsh-mobile-control__details{border:0;border-radius:0;background:transparent;box-shadow:none}
|
|
15739
16706
|
.dsh-mobile-control__self-hosted{border:0;border-radius:0;background:transparent;box-shadow:none}
|
|
15740
16707
|
` + EXTENSION_RECOVERY_STYLES;
|
|
16708
|
+
/** Resolve safe, locale-owned copy for tap-navigation results. */
|
|
16709
|
+
function mobilePushNavigationNotice(locale, key) {
|
|
16710
|
+
return {
|
|
16711
|
+
en: {
|
|
16712
|
+
"session-unavailable": "This conversation is no longer available.",
|
|
16713
|
+
"session-resolved": "Opened the conversation."
|
|
16714
|
+
},
|
|
16715
|
+
it: {
|
|
16716
|
+
"session-unavailable": "Questa conversazione non è più disponibile.",
|
|
16717
|
+
"session-resolved": "Conversazione aperta."
|
|
16718
|
+
},
|
|
16719
|
+
zh: {
|
|
16720
|
+
"session-unavailable": "此会话已不可用。",
|
|
16721
|
+
"session-resolved": "已打开会话。"
|
|
16722
|
+
}
|
|
16723
|
+
}[locale][key];
|
|
16724
|
+
}
|
|
16725
|
+
function showMobilePushNavigationNotice(page, view, key) {
|
|
16726
|
+
const node = page.createElement("div");
|
|
16727
|
+
node.className = "dsh-mobile-push-navigation-notice";
|
|
16728
|
+
node.setAttribute("role", "status");
|
|
16729
|
+
node.setAttribute("aria-live", "polite");
|
|
16730
|
+
node.lang = selectedMobileControlLocale();
|
|
16731
|
+
node.textContent = mobilePushNavigationNotice(selectedMobileControlLocale(), key);
|
|
16732
|
+
node.style.cssText = "position:fixed;z-index:1201;left:50%;bottom:max(24px,env(safe-area-inset-bottom));transform:translateX(-50%);max-width:calc(100vw - 32px);padding:10px 16px;border-radius:12px;background:var(--dsw-alias-bg-layer-3,#20242b);color:var(--dsw-alias-label-primary,#fff);font:14px/1.45 system-ui,sans-serif;box-shadow:0 8px 24px #0003;pointer-events:none";
|
|
16733
|
+
page.body.append(node);
|
|
16734
|
+
view.setTimeout(() => node.remove(), 3e3);
|
|
16735
|
+
}
|
|
16736
|
+
/** Own the browser-only push controller, settings row, and optional authenticated tap navigation. */
|
|
16737
|
+
function installMobilePushBrowserClient(ctx, runtime) {
|
|
16738
|
+
const { page, view } = runtime;
|
|
16739
|
+
if (view.__DSH_MOBILE_FRONTEND__ !== "dedicated" || view.__DSH_MOBILE_NATIVE__ !== void 0 || page.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") === null) return () => void 0;
|
|
16740
|
+
const api = runtime.api ?? createMobilePushBrowserApi(mobileRequest);
|
|
16741
|
+
const controller = createMobilePushController(api, runtime.environment ?? browserPushEnvironment(view));
|
|
16742
|
+
controller.start();
|
|
16743
|
+
const removeSettings = ctx.slots.inject("settings.general.item", () => ctx.slots.register({
|
|
16744
|
+
name: "settings.general.item",
|
|
16745
|
+
id: "dsh-mobile-push-browser",
|
|
16746
|
+
order: 120
|
|
16747
|
+
}, () => (0, react.createElement)(MobilePushSettings, {
|
|
16748
|
+
controller,
|
|
16749
|
+
locale: selectedMobileControlLocale()
|
|
16750
|
+
})));
|
|
16751
|
+
const refresh = () => {
|
|
16752
|
+
controller.refresh();
|
|
16753
|
+
};
|
|
16754
|
+
const onVisible = () => {
|
|
16755
|
+
if (page.visibilityState === "visible") refresh();
|
|
16756
|
+
};
|
|
16757
|
+
view.addEventListener("focus", refresh);
|
|
16758
|
+
page.addEventListener("visibilitychange", onVisible);
|
|
16759
|
+
const connection = ctx.get("connection");
|
|
16760
|
+
const unbindConnection = connection?.state?.subscribe(() => {
|
|
16761
|
+
if (connection.state?.getSnapshot() === "connected") refresh();
|
|
16762
|
+
}) ?? (() => void 0);
|
|
16763
|
+
let removeNavigation = () => void 0;
|
|
16764
|
+
let disposeNavigationLifecycle = () => void 0;
|
|
16765
|
+
const reference = new URLSearchParams(view.location.search).get("mobile-push");
|
|
16766
|
+
if (reference !== null && reference !== "" && ctx.inject !== void 0) {
|
|
16767
|
+
let navigationClosed = false;
|
|
16768
|
+
const navigationFiber = ctx.inject(mobilePushNavigationServices, (navigationContext) => {
|
|
16769
|
+
if (navigationClosed) return () => void 0;
|
|
16770
|
+
const pending = new URLSearchParams(view.location.search).get("mobile-push");
|
|
16771
|
+
if (pending === null || pending === "") return () => void 0;
|
|
16772
|
+
const remove = installPushNavigation(navigationContext, {
|
|
16773
|
+
resolve: (value) => api.resolve(value),
|
|
16774
|
+
notify: (key) => {
|
|
16775
|
+
if (key !== "session-unavailable" && key !== "session-resolved") return;
|
|
16776
|
+
if (runtime.notify) runtime.notify(key);
|
|
16777
|
+
else showMobilePushNavigationNotice(page, view, key);
|
|
16778
|
+
},
|
|
16779
|
+
browser: {
|
|
16780
|
+
location: view.location,
|
|
16781
|
+
history: view.history
|
|
16782
|
+
}
|
|
16783
|
+
});
|
|
16784
|
+
removeNavigation = remove;
|
|
16785
|
+
return () => {
|
|
16786
|
+
if (removeNavigation === remove) removeNavigation = () => void 0;
|
|
16787
|
+
remove();
|
|
16788
|
+
};
|
|
16789
|
+
});
|
|
16790
|
+
disposeNavigationLifecycle = () => {
|
|
16791
|
+
if (navigationClosed) return;
|
|
16792
|
+
navigationClosed = true;
|
|
16793
|
+
removeNavigation();
|
|
16794
|
+
navigationFiber.dispose();
|
|
16795
|
+
};
|
|
16796
|
+
}
|
|
16797
|
+
let disposed = false;
|
|
16798
|
+
return () => {
|
|
16799
|
+
if (disposed) return;
|
|
16800
|
+
disposed = true;
|
|
16801
|
+
view.removeEventListener("focus", refresh);
|
|
16802
|
+
page.removeEventListener("visibilitychange", onVisible);
|
|
16803
|
+
unbindConnection();
|
|
16804
|
+
disposeNavigationLifecycle();
|
|
16805
|
+
removeSettings();
|
|
16806
|
+
controller.stop();
|
|
16807
|
+
};
|
|
16808
|
+
}
|
|
15741
16809
|
/** Mount the desktop control or mobile feature enhancements. */
|
|
15742
16810
|
function apply(ctx) {
|
|
15743
16811
|
let gatewayRejectionSink;
|
|
@@ -15772,6 +16840,10 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15772
16840
|
return recovery();
|
|
15773
16841
|
};
|
|
15774
16842
|
}, "dsh-mobile: connection recovery");
|
|
16843
|
+
ctx.effect(() => installMobilePushBrowserClient(ctx, {
|
|
16844
|
+
page: document,
|
|
16845
|
+
view: window
|
|
16846
|
+
}), "dsh-mobile: authenticated browser push lifecycle");
|
|
15775
16847
|
ctx.effect(() => {
|
|
15776
16848
|
const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
|
|
15777
16849
|
const gatewayCustomization = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
|
|
@@ -15940,8 +17012,10 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15940
17012
|
exports.installCustomAssets = installCustomAssets;
|
|
15941
17013
|
exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
|
|
15942
17014
|
exports.installMobileNativeBack = installMobileNativeBack;
|
|
17015
|
+
exports.installMobilePushBrowserClient = installMobilePushBrowserClient;
|
|
15943
17016
|
exports.localizedDiagnosticEntryCopy = localizedDiagnosticEntryCopy;
|
|
15944
17017
|
exports.mobileBackBlockedMessage = mobileBackBlockedMessage;
|
|
17018
|
+
exports.mobilePushNavigationNotice = mobilePushNavigationNotice;
|
|
15945
17019
|
exports.normalizeDiagnosticOverall = normalizeDiagnosticOverall;
|
|
15946
17020
|
exports.normalizeDiagnosticStatus = normalizeDiagnosticStatus;
|
|
15947
17021
|
exports.parseMobileExtensionManifest = parseMobileExtensionManifest;
|