dsh-mobile 0.7.1 → 0.8.1
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 +29 -0
- package/CONTRIBUTORS.md +2 -2
- package/README.en.md +35 -12
- package/README.md +37 -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/MOBILE_PLUGIN_ADAPTATION.en.md +18 -0
- package/docs/MOBILE_PLUGIN_ADAPTATION.md +18 -0
- package/docs/MOBILE_PLUGIN_BASELINE.en.md +117 -0
- package/docs/MOBILE_PLUGIN_BASELINE.md +117 -0
- package/docs/README.md +3 -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/cli.js.map +1 -1
- package/lib/client.js +1250 -17
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +129 -1
- package/lib/index.mjs +3854 -106
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +23 -4
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +19 -5
- package/packages/question-fixes/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -803,7 +803,22 @@ window.__ModuleLoader__.load({
|
|
|
803
803
|
lanSetupConfiguring: "Creating private certificates and local network rules…",
|
|
804
804
|
lanSetupRequired: "Local network setup has not been completed. Choose a network above to continue.",
|
|
805
805
|
lanSetupRestart: "Setup complete for {origin}. Restart DSH once to start local network access.",
|
|
806
|
-
lanSetupFailed: "Setup was not completed. Confirm the network selection and approve the Windows prompt, then try again."
|
|
806
|
+
lanSetupFailed: "Setup was not completed. Confirm the network selection and approve the Windows prompt, then try again.",
|
|
807
|
+
lanChangeTitle: "Change network",
|
|
808
|
+
lanChangeIntro: "Move local network access to another interface. The private CA and paired devices are kept; connected phones reconnect with the new address.",
|
|
809
|
+
lanChangeSelect: "New network",
|
|
810
|
+
lanChangeRefresh: "Scan again",
|
|
811
|
+
lanChangeConfirm: "Change network",
|
|
812
|
+
lanChangeBusy: "Changing network…",
|
|
813
|
+
lanChangeOk: "Network saved. If local access is stopped, enable it before connecting to the new address.",
|
|
814
|
+
lanChangeOkDisconnected: "Network changed and the listener restarted. Connected phones were disconnected; generate a new pairing QR to reconnect.",
|
|
815
|
+
lanChangeCurrent: "current",
|
|
816
|
+
lanChangeUnchanged: "That is already the current network.",
|
|
817
|
+
lanChangeUnavailable: "That network is no longer available. Scan again.",
|
|
818
|
+
lanChangeUnsupported: "This setup uses a fixed address, so it cannot change networks here.",
|
|
819
|
+
lanChangeFailed: "The change did not complete. Refresh the status before retrying; the previous configuration may have been restored.",
|
|
820
|
+
lanChangeRefreshFailed: "Could not refresh network status. A submitted change may have completed; check the current status before retrying.",
|
|
821
|
+
lanChangeRecoveryRequired: "The previous network could not be restored. Check local network configuration before enabling access again."
|
|
807
822
|
});
|
|
808
823
|
Object.assign(MOBILE_CONTROL_MESSAGES.it, {
|
|
809
824
|
lanSetupTitle: "Completa la configurazione della rete locale",
|
|
@@ -818,7 +833,22 @@ window.__ModuleLoader__.load({
|
|
|
818
833
|
lanSetupConfiguring: "Creazione dei certificati privati e delle regole di rete locale…",
|
|
819
834
|
lanSetupRequired: "La configurazione della rete locale non è completa. Scegli una rete qui sopra per continuare.",
|
|
820
835
|
lanSetupRestart: "Configurazione completata per {origin}. Riavvia DSH una volta per attivare l’accesso locale.",
|
|
821
|
-
lanSetupFailed: "Configurazione non completata. Controlla la rete e approva la richiesta di Windows, quindi riprova."
|
|
836
|
+
lanSetupFailed: "Configurazione non completata. Controlla la rete e approva la richiesta di Windows, quindi riprova.",
|
|
837
|
+
lanChangeTitle: "Cambia rete",
|
|
838
|
+
lanChangeIntro: "Sposta l’accesso alla rete locale su un’altra interfaccia. La CA privata e i dispositivi abbinati restano; i telefoni collegati si riconnettono con il nuovo indirizzo.",
|
|
839
|
+
lanChangeSelect: "Nuova rete",
|
|
840
|
+
lanChangeRefresh: "Cerca di nuovo",
|
|
841
|
+
lanChangeConfirm: "Cambia rete",
|
|
842
|
+
lanChangeBusy: "Cambio della rete…",
|
|
843
|
+
lanChangeOk: "Rete salvata. Se l’accesso locale è disattivato, attivalo prima di collegarti al nuovo indirizzo.",
|
|
844
|
+
lanChangeOkDisconnected: "Rete cambiata e listener riavviato. I telefoni collegati sono stati disconnessi; genera un nuovo QR di abbinamento per riconnetterti.",
|
|
845
|
+
lanChangeCurrent: "attuale",
|
|
846
|
+
lanChangeUnchanged: "Questa è già la rete attuale.",
|
|
847
|
+
lanChangeUnavailable: "La rete non è più disponibile. Cerca di nuovo.",
|
|
848
|
+
lanChangeUnsupported: "Questa configurazione usa un indirizzo fisso: non può cambiare rete qui.",
|
|
849
|
+
lanChangeFailed: "Cambio non completato. Aggiorna lo stato prima di riprovare; la configurazione precedente potrebbe essere stata ripristinata.",
|
|
850
|
+
lanChangeRefreshFailed: "Impossibile aggiornare lo stato. Una modifica inviata potrebbe essere stata completata; verifica prima di riprovare.",
|
|
851
|
+
lanChangeRecoveryRequired: "Impossibile ripristinare la rete precedente. Controlla la configurazione locale prima di riattivare l’accesso."
|
|
822
852
|
});
|
|
823
853
|
Object.assign(MOBILE_CONTROL_MESSAGES.zh, {
|
|
824
854
|
lanSetupTitle: "完成局域网配置",
|
|
@@ -833,7 +863,22 @@ window.__ModuleLoader__.load({
|
|
|
833
863
|
lanSetupConfiguring: "正在生成私有证书并配置局域网规则…",
|
|
834
864
|
lanSetupRequired: "尚未完成局域网配置,请选择上方网络继续。",
|
|
835
865
|
lanSetupRestart: "已为 {origin} 完成配置。请重启一次 DSH,局域网访问会自动开启。",
|
|
836
|
-
lanSetupFailed: "配置未完成。请确认网卡选择并允许 Windows 授权提示,然后重试。"
|
|
866
|
+
lanSetupFailed: "配置未完成。请确认网卡选择并允许 Windows 授权提示,然后重试。",
|
|
867
|
+
lanChangeTitle: "更换网络",
|
|
868
|
+
lanChangeIntro: "把局域网访问切换到另一块网卡。私有 CA 与已配对设备都会保留,已连接的手机需要用新地址重新连接。",
|
|
869
|
+
lanChangeSelect: "新网络",
|
|
870
|
+
lanChangeRefresh: "重新扫描",
|
|
871
|
+
lanChangeConfirm: "更换网络",
|
|
872
|
+
lanChangeBusy: "正在更换网络…",
|
|
873
|
+
lanChangeOk: "网络配置已保存。若局域网访问已关闭,请开启后再用新地址连接。",
|
|
874
|
+
lanChangeOkDisconnected: "网络已更换并重启监听,已连接的手机已断开;请生成新的配对二维码重新连接。",
|
|
875
|
+
lanChangeCurrent: "当前",
|
|
876
|
+
lanChangeUnchanged: "这已经是当前网络。",
|
|
877
|
+
lanChangeUnavailable: "该网络已不可用,请重新扫描。",
|
|
878
|
+
lanChangeUnsupported: "当前配置使用固定地址,无法在这里更换网络。",
|
|
879
|
+
lanChangeFailed: "更换未完成。请先刷新状态再决定是否重试;先前配置可能已恢复。",
|
|
880
|
+
lanChangeRefreshFailed: "无法刷新网络状态,已提交的更换可能已完成;请先确认当前状态,不要重复提交。",
|
|
881
|
+
lanChangeRecoveryRequired: "无法恢复先前网络,请先检查局域网配置,再决定是否重新开启访问。"
|
|
837
882
|
});
|
|
838
883
|
Object.assign(MOBILE_CONTROL_MESSAGES.en, {
|
|
839
884
|
selfHostedConnections: "Self-hosted connection",
|
|
@@ -3634,6 +3679,163 @@ window.__ModuleLoader__.load({
|
|
|
3634
3679
|
});
|
|
3635
3680
|
}
|
|
3636
3681
|
//#endregion
|
|
3682
|
+
//#region src/mobile-sidebar-scroll.ts
|
|
3683
|
+
function sidebarRestoreTop(snapshot, anchorTop) {
|
|
3684
|
+
return Math.max(0, anchorTop === void 0 || snapshot.offset === void 0 ? snapshot.top : anchorTop - snapshot.offset);
|
|
3685
|
+
}
|
|
3686
|
+
const keyedRows = "[data-row-key]";
|
|
3687
|
+
const hasSuffix = (element, suffix) => [...element.classList].some((token) => token.endsWith(suffix));
|
|
3688
|
+
function visible(element) {
|
|
3689
|
+
return element.isConnected && element.clientHeight > 0 && element.getClientRects().length > 0;
|
|
3690
|
+
}
|
|
3691
|
+
function captureSidebarScroll(element) {
|
|
3692
|
+
const top = element.scrollTop;
|
|
3693
|
+
const bounds = element.getBoundingClientRect();
|
|
3694
|
+
const row = [...element.querySelectorAll(keyedRows)].find((candidate) => {
|
|
3695
|
+
const rect = candidate.getBoundingClientRect();
|
|
3696
|
+
return rect.height > 0 && rect.bottom > bounds.top && rect.top < bounds.bottom;
|
|
3697
|
+
});
|
|
3698
|
+
return row === void 0 ? { top } : {
|
|
3699
|
+
top,
|
|
3700
|
+
anchor: row.getAttribute("data-row-key") ?? "",
|
|
3701
|
+
offset: row.getBoundingClientRect().top - bounds.top
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
/**
|
|
3705
|
+
* Uses browser scrollports/keyed rows only; no React internals, DOM reordering,
|
|
3706
|
+
* forced group expansion or persistent storage of session identities. Absolute
|
|
3707
|
+
* position seeds a virtual window before a mounted keyed anchor refines it.
|
|
3708
|
+
*/
|
|
3709
|
+
function installMobileSidebarScrollMemory() {
|
|
3710
|
+
const memories = /* @__PURE__ */ new Map();
|
|
3711
|
+
const stockViewport = window.matchMedia("(max-width:720px)");
|
|
3712
|
+
const dedicatedViewport = window.matchMedia("(max-width:899px)");
|
|
3713
|
+
let root;
|
|
3714
|
+
let open = false;
|
|
3715
|
+
let ports = /* @__PURE__ */ new Map();
|
|
3716
|
+
let pending = /* @__PURE__ */ new Map();
|
|
3717
|
+
let frame = 0;
|
|
3718
|
+
let deadline = 0;
|
|
3719
|
+
let disposed = false;
|
|
3720
|
+
const isOpen = () => root !== void 0 && visible(root) && !hasSuffix(root, "_collapsed") && root.closest("[data-open=\"false\"]") === null && root.querySelector("[data-dsh-mobile-settings]") === null;
|
|
3721
|
+
const cancel = () => {
|
|
3722
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
3723
|
+
frame = 0;
|
|
3724
|
+
pending.clear();
|
|
3725
|
+
};
|
|
3726
|
+
const remember = () => {
|
|
3727
|
+
if (!open || !isOpen()) return;
|
|
3728
|
+
for (const [port, key] of ports) if (!pending.has(port) && visible(port)) memories.set(key, port === root ? { top: port.scrollTop } : captureSidebarScroll(port));
|
|
3729
|
+
};
|
|
3730
|
+
const restore = () => {
|
|
3731
|
+
frame = 0;
|
|
3732
|
+
if (disposed || !open || !isOpen()) return;
|
|
3733
|
+
for (const [port, state] of pending) {
|
|
3734
|
+
if (!visible(port)) continue;
|
|
3735
|
+
const snapshot = state.snapshot;
|
|
3736
|
+
if (snapshot === void 0) {
|
|
3737
|
+
if (port === root) {
|
|
3738
|
+
pending.delete(port);
|
|
3739
|
+
continue;
|
|
3740
|
+
}
|
|
3741
|
+
const selected = port.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]");
|
|
3742
|
+
if (selected !== null && visible(selected)) {
|
|
3743
|
+
const bounds = port.getBoundingClientRect(), rect = selected.getBoundingClientRect();
|
|
3744
|
+
if (rect.top < bounds.top || rect.bottom > bounds.bottom) port.scrollTop += rect.top - bounds.top - (port.clientHeight - rect.height) / 2;
|
|
3745
|
+
pending.delete(port);
|
|
3746
|
+
} else if (port.querySelector(keyedRows) !== null) pending.delete(port);
|
|
3747
|
+
continue;
|
|
3748
|
+
}
|
|
3749
|
+
const anchor = [...port.querySelectorAll(keyedRows)].find((row) => row.dataset.rowKey === snapshot.anchor);
|
|
3750
|
+
const target = sidebarRestoreTop(snapshot, anchor === void 0 ? void 0 : port.scrollTop + anchor.getBoundingClientRect().top - port.getBoundingClientRect().top);
|
|
3751
|
+
const before = port.scrollTop;
|
|
3752
|
+
port.scrollTop = target;
|
|
3753
|
+
if (Math.abs(port.scrollTop - target) <= 1 && Math.abs(before - port.scrollTop) <= 1) state.stable++;
|
|
3754
|
+
else state.stable = 0;
|
|
3755
|
+
if (state.stable >= 2) pending.delete(port);
|
|
3756
|
+
}
|
|
3757
|
+
if (pending.size > 0 && performance.now() < deadline) frame = requestAnimationFrame(restore);
|
|
3758
|
+
else pending.clear();
|
|
3759
|
+
};
|
|
3760
|
+
const sync = () => {
|
|
3761
|
+
if (disposed) return;
|
|
3762
|
+
const next = document.querySelector("[data-dsh-mobile-sidebar-root]") ?? void 0;
|
|
3763
|
+
const mobile = next?.closest("[data-pane=\"sidebar\"]") != null ? dedicatedViewport.matches : stockViewport.matches;
|
|
3764
|
+
if (next !== root) {
|
|
3765
|
+
cancel();
|
|
3766
|
+
ports.clear();
|
|
3767
|
+
root = next;
|
|
3768
|
+
open = false;
|
|
3769
|
+
}
|
|
3770
|
+
if (!(mobile && isOpen())) {
|
|
3771
|
+
cancel();
|
|
3772
|
+
open = false;
|
|
3773
|
+
ports.clear();
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3776
|
+
const reopening = !open;
|
|
3777
|
+
open = true;
|
|
3778
|
+
if (root === void 0) return;
|
|
3779
|
+
const candidates = [root, ...root.querySelectorAll("[class*=\"_regionArea\"] [class*=\"_list\"]")].filter((port) => port === root || hasSuffix(port, "_list"));
|
|
3780
|
+
const nextPorts = /* @__PURE__ */ new Map();
|
|
3781
|
+
for (const port of candidates) {
|
|
3782
|
+
const key = port === root ? "outer" : `workspace:${[...port.classList].sort().join(" ")}:${root.querySelector("[class*=\"_regionArea\"] input")?.value ?? ""}`;
|
|
3783
|
+
nextPorts.set(port, key);
|
|
3784
|
+
if (reopening || ports.get(port) !== key) {
|
|
3785
|
+
pending.set(port, {
|
|
3786
|
+
snapshot: memories.get(key),
|
|
3787
|
+
stable: 0
|
|
3788
|
+
});
|
|
3789
|
+
deadline = performance.now() + 1200;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
for (const port of pending.keys()) if (!nextPorts.has(port)) pending.delete(port);
|
|
3793
|
+
ports = nextPorts;
|
|
3794
|
+
if (pending.size > 0 && frame === 0) frame = requestAnimationFrame(restore);
|
|
3795
|
+
};
|
|
3796
|
+
const onScroll = (event) => {
|
|
3797
|
+
const port = event.target;
|
|
3798
|
+
if (!(port instanceof HTMLElement) || !open || !isOpen() || pending.has(port) || !visible(port)) return;
|
|
3799
|
+
const key = ports.get(port);
|
|
3800
|
+
if (key !== void 0) memories.set(key, port === root ? { top: port.scrollTop } : captureSidebarScroll(port));
|
|
3801
|
+
};
|
|
3802
|
+
const onIntent = (event) => {
|
|
3803
|
+
if (!(event.target instanceof Node) || !root?.contains(event.target)) return;
|
|
3804
|
+
cancel();
|
|
3805
|
+
remember();
|
|
3806
|
+
};
|
|
3807
|
+
stockViewport.addEventListener("change", sync);
|
|
3808
|
+
dedicatedViewport.addEventListener("change", sync);
|
|
3809
|
+
document.addEventListener("scroll", onScroll, true);
|
|
3810
|
+
for (const type of [
|
|
3811
|
+
"pointerdown",
|
|
3812
|
+
"touchstart",
|
|
3813
|
+
"wheel",
|
|
3814
|
+
"keydown",
|
|
3815
|
+
"click"
|
|
3816
|
+
]) document.addEventListener(type, onIntent, true);
|
|
3817
|
+
return {
|
|
3818
|
+
sync,
|
|
3819
|
+
dispose: () => {
|
|
3820
|
+
disposed = true;
|
|
3821
|
+
cancel();
|
|
3822
|
+
stockViewport.removeEventListener("change", sync);
|
|
3823
|
+
dedicatedViewport.removeEventListener("change", sync);
|
|
3824
|
+
document.removeEventListener("scroll", onScroll, true);
|
|
3825
|
+
for (const type of [
|
|
3826
|
+
"pointerdown",
|
|
3827
|
+
"touchstart",
|
|
3828
|
+
"wheel",
|
|
3829
|
+
"keydown",
|
|
3830
|
+
"click"
|
|
3831
|
+
]) document.removeEventListener(type, onIntent, true);
|
|
3832
|
+
ports.clear();
|
|
3833
|
+
memories.clear();
|
|
3834
|
+
root = void 0;
|
|
3835
|
+
}
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
//#endregion
|
|
3637
3839
|
//#region src/native-mobile.ts
|
|
3638
3840
|
/**
|
|
3639
3841
|
* Viewport below which the injected chrome becomes an overlay drawer: the
|
|
@@ -3680,10 +3882,30 @@ window.__ModuleLoader__.load({
|
|
|
3680
3882
|
.dsh-mobile-settings_fontFamily > span { flex:1 1 100px; }
|
|
3681
3883
|
.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
3884
|
.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; }
|
|
3885
|
+
/* Push category rows: compact label-left / native-Switch-right rows. The control is
|
|
3886
|
+
the actual public @deepseek-ai/dsh-client-ui-primitives Switch provided by the DSH
|
|
3887
|
+
web shell: its track/thumb paint, dimensions, focus ring and motion are owned by
|
|
3888
|
+
the shell stylesheet and are never re-created, resized or overridden here. Each
|
|
3889
|
+
row is a label associated with its switch button, so the full 48px row remains
|
|
3890
|
+
one tap target while the button keeps native keyboard semantics. Category names
|
|
3891
|
+
match the General settings row title typography (14px/20px, regular weight). */
|
|
3892
|
+
.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); }
|
|
3893
|
+
.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; }
|
|
3894
|
+
.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
3895
|
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
3896
|
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
3897
|
/* The mobile row replaces the Host-backed font control, not the theme selector. */
|
|
3686
3898
|
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; }
|
|
3899
|
+
/* The injected attachment actions exist on every viewport, not only under the
|
|
3900
|
+
narrow overlay: a tablet shows the same Add menu, and with no native item to
|
|
3901
|
+
mirror they fell back to an unstyled UA button box (bordered, centred). Keep
|
|
3902
|
+
this fallback outside the overlay query so the rows read like native ones. */
|
|
3903
|
+
.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; }
|
|
3904
|
+
.dsh-mobile-media-action:active { opacity:.72; }
|
|
3905
|
+
.dsh-mobile-media-action:focus-visible { outline:2px solid var(--dsw-alias-state-business-primary,#4c82f7); outline-offset:1px; }
|
|
3906
|
+
.dsh-mobile-media-action:disabled { cursor:default; opacity:.38; }
|
|
3907
|
+
.dsh-mobile-media-action svg { flex:none; width:16px; height:16px; color:var(--dsw-alias-label-tertiary,currentColor); }
|
|
3908
|
+
.dsh-mobile-media-action span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
3687
3909
|
@media ${NATIVE_MOBILE_OVERLAY_QUERY} {
|
|
3688
3910
|
html.dsh-native-mobile-active,html.dsh-native-mobile-active body { width:100%; height:100%; overflow:hidden; }
|
|
3689
3911
|
html.dsh-native-mobile-active { --dsh-mobile-motion-duration:200ms; --dsh-mobile-motion-ease:cubic-bezier(.22,1,.36,1); }
|
|
@@ -3756,7 +3978,7 @@ window.__ModuleLoader__.load({
|
|
|
3756
3978
|
[data-dsh-mobile-details] { position:fixed !important; z-index:250 !important; inset:0 0 0 auto !important; width:min(94vw,460px) !important; max-width:none !important; transform:translateX(100%); transition:transform var(--dsh-mobile-motion-duration) var(--dsh-mobile-motion-ease); background:var(--dsw-alias-bg-layer-1, #fff); box-shadow:-18px 0 46px rgb(15 23 42 / 18%); }
|
|
3757
3979
|
[data-dsh-mobile-details][data-open="true"] { transform:translateX(0); }
|
|
3758
3980
|
[data-dsh-mobile-handle] { display:none !important; }
|
|
3759
|
-
[data-dsh-mobile-settings] { flex-direction:column !important; width:100vw !important; height:100dvh !important; max-width:none !important; border-radius:0 !important; animation:dsh-mobile-panel-in var(--dsh-mobile-motion-duration) var(--dsh-mobile-motion-ease); }
|
|
3981
|
+
[data-dsh-mobile-settings="true"] { flex-direction:column !important; width:100vw !important; height:100dvh !important; max-width:none !important; border-radius:0 !important; animation:dsh-mobile-panel-in var(--dsh-mobile-motion-duration) var(--dsh-mobile-motion-ease); }
|
|
3760
3982
|
[data-dsh-mobile-settings-nav] { flex:none !important; width:100% !important; padding:max(14px,env(safe-area-inset-top)) 12px 8px !important; gap:10px !important; border-bottom:1px solid var(--dsw-alias-border-l2,#e8ebef); }
|
|
3761
3983
|
[data-dsh-mobile-settings-nav] [class*="_navTitle"] { padding:0 8px !important; font-size:18px !important; line-height:28px !important; }
|
|
3762
3984
|
[data-dsh-mobile-settings-list] { flex-direction:row !important; gap:4px !important; overflow-x:auto !important; scrollbar-width:none; }
|
|
@@ -3869,14 +4091,6 @@ window.__ModuleLoader__.load({
|
|
|
3869
4091
|
[data-dsh-mobile-center] [role="dialog"][aria-label*="Context"] :is(dt,dd) { white-space:nowrap !important; word-break:keep-all !important; }
|
|
3870
4092
|
.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
4093
|
.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
4094
|
[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
4095
|
/* Collapse the scroll viewport only; the editor, draft and attachments stay
|
|
3882
4096
|
owned by DSH. Toolbar focus keeps the card expanded during menu taps. */
|
|
@@ -3961,6 +4175,19 @@ window.__ModuleLoader__.load({
|
|
|
3961
4175
|
return classToken(candidate, "_frame") && firstByClassSuffix(candidate, "_sidebarCol") !== void 0 && firstByClassSuffix(candidate, "_centerCol") !== void 0;
|
|
3962
4176
|
});
|
|
3963
4177
|
}
|
|
4178
|
+
/**
|
|
4179
|
+
* Mirror the classes of the stock Add-menu item on an injected attachment action.
|
|
4180
|
+
*
|
|
4181
|
+
* An empty class name is not nullish: trusting it would strip the plugin's own
|
|
4182
|
+
* fallback class and leave a bordered, centred UA button on that Host version.
|
|
4183
|
+
* A stock item that does carry classes still wins, so the injected action keeps
|
|
4184
|
+
* following the native menu's spacing, colours and dark-mode states.
|
|
4185
|
+
*/
|
|
4186
|
+
function mediaActionClassName(nativeItem) {
|
|
4187
|
+
const nativeClass = nativeItem?.className;
|
|
4188
|
+
const trimmed = typeof nativeClass === "string" ? nativeClass.trim() : "";
|
|
4189
|
+
return trimmed === "" ? "dsh-mobile-media-action" : trimmed;
|
|
4190
|
+
}
|
|
3964
4191
|
/** Mark every mounted settings dialog so its mobile layout does not depend on the conversation shell. */
|
|
3965
4192
|
function markNativeMobileSettings(root) {
|
|
3966
4193
|
let marked = 0;
|
|
@@ -3968,7 +4195,17 @@ window.__ModuleLoader__.load({
|
|
|
3968
4195
|
const children = Array.from(dialog.children);
|
|
3969
4196
|
const nav = children.find((child) => classToken(child, "_nav"));
|
|
3970
4197
|
const content = children.find((child) => classToken(child, "_content"));
|
|
3971
|
-
if (nav === void 0
|
|
4198
|
+
if (nav === void 0) continue;
|
|
4199
|
+
if (content === void 0) {
|
|
4200
|
+
const header = children.find((child) => classToken(child, "_header"));
|
|
4201
|
+
const options = children.find((child) => classToken(child, "_options"));
|
|
4202
|
+
const title = children.find((child) => classToken(child, "_navTitle"));
|
|
4203
|
+
if (dialog.getAttribute("data-shortcut-modal") !== "settings" || header === void 0 || options === void 0 || title === void 0) continue;
|
|
4204
|
+
dialog.dataset.dshMobileSettings = "grid";
|
|
4205
|
+
header.setAttribute("data-dsh-mobile-settings-header", "true");
|
|
4206
|
+
marked += 1;
|
|
4207
|
+
continue;
|
|
4208
|
+
}
|
|
3972
4209
|
dialog.dataset.dshMobileSettings = "true";
|
|
3973
4210
|
nav.dataset.dshMobileSettingsNav = "true";
|
|
3974
4211
|
content.dataset.dshMobileSettingsContent = "true";
|
|
@@ -4886,7 +5123,7 @@ window.__ModuleLoader__.load({
|
|
|
4886
5123
|
}
|
|
4887
5124
|
})();
|
|
4888
5125
|
for (const button of [cameraButton, galleryButton]) {
|
|
4889
|
-
const className = nativeItem
|
|
5126
|
+
const className = mediaActionClassName(nativeItem);
|
|
4890
5127
|
if (button.className !== className) button.className = className;
|
|
4891
5128
|
const icon = button.firstElementChild;
|
|
4892
5129
|
const caption = button.lastElementChild;
|
|
@@ -5092,6 +5329,7 @@ window.__ModuleLoader__.load({
|
|
|
5092
5329
|
document.addEventListener("focusin", onStripFocus, true);
|
|
5093
5330
|
document.addEventListener("click", onStripClickCapture, true);
|
|
5094
5331
|
document.addEventListener("click", onBranchClick, true);
|
|
5332
|
+
const sidebarScrollMemory = installMobileSidebarScrollMemory();
|
|
5095
5333
|
let frame;
|
|
5096
5334
|
let sidebar;
|
|
5097
5335
|
let sidebarRoot;
|
|
@@ -5324,7 +5562,10 @@ window.__ModuleLoader__.load({
|
|
|
5324
5562
|
const lastColumn = frame?.style.gridTemplateColumns.trim().split(/\s+/).at(-1);
|
|
5325
5563
|
details.dataset.open = String(lastColumn !== void 0 && lastColumn !== "0px" && lastColumn !== "0");
|
|
5326
5564
|
}
|
|
5327
|
-
if (sidebar === void 0)
|
|
5565
|
+
if (sidebar === void 0) {
|
|
5566
|
+
sidebarScrollMemory.sync();
|
|
5567
|
+
return;
|
|
5568
|
+
}
|
|
5328
5569
|
sidebar.dataset.dshMobileSidebar = "true";
|
|
5329
5570
|
toggle = firstByClassSuffix(sidebar, "_toggle");
|
|
5330
5571
|
let candidate = toggle?.parentElement;
|
|
@@ -5336,6 +5577,7 @@ window.__ModuleLoader__.load({
|
|
|
5336
5577
|
if (toggle !== void 0) toggle.dataset.dshMobileToggle = "true";
|
|
5337
5578
|
if (frame !== void 0) sidebar.dataset.open = String(!classToken(sidebarRoot, "_collapsed"));
|
|
5338
5579
|
backdrop.hidden = frame === void 0 || !drawerScrimVisible(classToken(sidebarRoot, "_collapsed"), overlayQuery.matches);
|
|
5580
|
+
sidebarScrollMemory.sync();
|
|
5339
5581
|
};
|
|
5340
5582
|
const schedule = () => {
|
|
5341
5583
|
if (scheduled === 0) scheduled = requestAnimationFrame(sync);
|
|
@@ -5400,6 +5642,7 @@ window.__ModuleLoader__.load({
|
|
|
5400
5642
|
return () => {
|
|
5401
5643
|
disposed = true;
|
|
5402
5644
|
stopStockBack();
|
|
5645
|
+
sidebarScrollMemory.dispose();
|
|
5403
5646
|
disposeTaskWatcher();
|
|
5404
5647
|
mediaRequestGeneration += 1;
|
|
5405
5648
|
mediaPickerAbortController.abort();
|
|
@@ -8506,6 +8749,779 @@ window.__ModuleLoader__.load({
|
|
|
8506
8749
|
};
|
|
8507
8750
|
}
|
|
8508
8751
|
//#endregion
|
|
8752
|
+
//#region src/mobile-push/browser-api.ts
|
|
8753
|
+
function apiError(code) {
|
|
8754
|
+
return Object.assign(/* @__PURE__ */ new Error(code === "stale_generation" ? "Push settings changed elsewhere." : "Push request failed."), { code });
|
|
8755
|
+
}
|
|
8756
|
+
async function check(response) {
|
|
8757
|
+
if (response.ok) return;
|
|
8758
|
+
let code = "request_failed";
|
|
8759
|
+
if (response.status === 409) try {
|
|
8760
|
+
if ((await response.json())?.error === "stale_generation") code = "stale_generation";
|
|
8761
|
+
} catch {}
|
|
8762
|
+
throw apiError(code);
|
|
8763
|
+
}
|
|
8764
|
+
async function json(response) {
|
|
8765
|
+
try {
|
|
8766
|
+
return await response.json();
|
|
8767
|
+
} catch {
|
|
8768
|
+
throw apiError("request_failed");
|
|
8769
|
+
}
|
|
8770
|
+
}
|
|
8771
|
+
async function requestOnce(request, path, init) {
|
|
8772
|
+
try {
|
|
8773
|
+
return await request(path, init);
|
|
8774
|
+
} catch {
|
|
8775
|
+
throw apiError("request_failed");
|
|
8776
|
+
}
|
|
8777
|
+
}
|
|
8778
|
+
function postJson(request, path, value) {
|
|
8779
|
+
return requestOnce(request, path, {
|
|
8780
|
+
method: "POST",
|
|
8781
|
+
headers: { "content-type": "application/json" },
|
|
8782
|
+
body: JSON.stringify(value)
|
|
8783
|
+
});
|
|
8784
|
+
}
|
|
8785
|
+
/** Build the direct authenticated browser API over mobileRequest (single-shot, same-origin, CSRF-aware). */
|
|
8786
|
+
function createMobilePushBrowserApi(request) {
|
|
8787
|
+
return {
|
|
8788
|
+
async getConfig(installationId) {
|
|
8789
|
+
const response = await requestOnce(request, `/mobile-access/push/config?installationId=${encodeURIComponent(installationId)}`, { method: "GET" });
|
|
8790
|
+
await check(response);
|
|
8791
|
+
return json(response);
|
|
8792
|
+
},
|
|
8793
|
+
async enroll(value) {
|
|
8794
|
+
const response = await postJson(request, "/mobile-access/push/subscription", value);
|
|
8795
|
+
await check(response);
|
|
8796
|
+
return json(response);
|
|
8797
|
+
},
|
|
8798
|
+
async updatePreferences(value) {
|
|
8799
|
+
await check(await postJson(request, "/mobile-access/push/preferences", value));
|
|
8800
|
+
},
|
|
8801
|
+
async disable(value) {
|
|
8802
|
+
await check(await postJson(request, "/mobile-access/push/disable", value));
|
|
8803
|
+
},
|
|
8804
|
+
async sendTest(value) {
|
|
8805
|
+
const response = await postJson(request, "/mobile-access/push/test", { installationId: value.installationId });
|
|
8806
|
+
if (response.status === 429) throw apiError("rate_limited");
|
|
8807
|
+
await check(response);
|
|
8808
|
+
},
|
|
8809
|
+
async resolve(reference) {
|
|
8810
|
+
const response = await postJson(request, "/mobile-access/push/resolve", { reference });
|
|
8811
|
+
await check(response);
|
|
8812
|
+
const value = await json(response);
|
|
8813
|
+
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;
|
|
8814
|
+
return value;
|
|
8815
|
+
}
|
|
8816
|
+
};
|
|
8817
|
+
}
|
|
8818
|
+
//#endregion
|
|
8819
|
+
//#region src/mobile-push/installation-id.ts
|
|
8820
|
+
/** Browser-safe installation-id validation shared by the browser controller and the Gateway. */
|
|
8821
|
+
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;
|
|
8822
|
+
/** True only for a UUID-shaped installation id with an allowed version and variant nibble. */
|
|
8823
|
+
function isPushInstallationId(value) {
|
|
8824
|
+
return typeof value === "string" && INSTALLATION_ID.test(value);
|
|
8825
|
+
}
|
|
8826
|
+
//#endregion
|
|
8827
|
+
//#region src/mobile-push/client.ts
|
|
8828
|
+
const ID_KEY = "dsh-mobile-push-installation";
|
|
8829
|
+
const defaults = Object.freeze({
|
|
8830
|
+
question: true,
|
|
8831
|
+
plan: true,
|
|
8832
|
+
approval: true,
|
|
8833
|
+
response: true,
|
|
8834
|
+
goal: true,
|
|
8835
|
+
failure: true
|
|
8836
|
+
});
|
|
8837
|
+
function decodeKey(encoded) {
|
|
8838
|
+
const normalized = encoded.replace(/-/gu, "+").replace(/_/gu, "/");
|
|
8839
|
+
const raw = atob(normalized + "=".repeat((4 - normalized.length % 4) % 4));
|
|
8840
|
+
return Uint8Array.from(raw, (char) => char.charCodeAt(0));
|
|
8841
|
+
}
|
|
8842
|
+
function safeError(error) {
|
|
8843
|
+
return (error && typeof error === "object" && "code" in error ? String(error.code) : "") === "stale_generation" ? "push.error.stale" : "push.error.generic";
|
|
8844
|
+
}
|
|
8845
|
+
function sameKey(subscription, key) {
|
|
8846
|
+
const raw = subscription.options.applicationServerKey;
|
|
8847
|
+
if (!raw) return false;
|
|
8848
|
+
const bytes = new Uint8Array(raw instanceof ArrayBuffer ? raw : raw.buffer, raw instanceof ArrayBuffer ? 0 : raw.byteOffset, raw instanceof ArrayBuffer ? raw.byteLength : raw.byteLength);
|
|
8849
|
+
return bytes.length === key.length && bytes.every((value, index) => value === key[index]);
|
|
8850
|
+
}
|
|
8851
|
+
/** Construct with a caller-owned authenticated API adapter and an explicit public window environment. */
|
|
8852
|
+
function createMobilePushController(api, environment) {
|
|
8853
|
+
let snapshot = Object.freeze({ state: "unsupported" });
|
|
8854
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
8855
|
+
let active = false;
|
|
8856
|
+
let generation = 0;
|
|
8857
|
+
let config;
|
|
8858
|
+
let preparedKey;
|
|
8859
|
+
let existing = null;
|
|
8860
|
+
let preparedInstallId = "";
|
|
8861
|
+
let memoryInstallId = "";
|
|
8862
|
+
let mutationBusy = false;
|
|
8863
|
+
let testInFlight = false;
|
|
8864
|
+
let testOwner = 0;
|
|
8865
|
+
let attempt = 0;
|
|
8866
|
+
let ownedSubscription;
|
|
8867
|
+
let currentHostSubscription;
|
|
8868
|
+
const consentedSubscriptions = /* @__PURE__ */ new WeakSet();
|
|
8869
|
+
const pendingEnrollments = /* @__PURE__ */ new WeakMap();
|
|
8870
|
+
const trackPendingEnrollment = (subscription, operation) => {
|
|
8871
|
+
let pending = pendingEnrollments.get(subscription);
|
|
8872
|
+
if (!pending) {
|
|
8873
|
+
pending = /* @__PURE__ */ new Set();
|
|
8874
|
+
pendingEnrollments.set(subscription, pending);
|
|
8875
|
+
}
|
|
8876
|
+
pending.add(operation);
|
|
8877
|
+
};
|
|
8878
|
+
const finishPendingEnrollment = (subscription, operation) => {
|
|
8879
|
+
const pending = pendingEnrollments.get(subscription);
|
|
8880
|
+
pending?.delete(operation);
|
|
8881
|
+
if (pending?.size === 0) pendingEnrollments.delete(subscription);
|
|
8882
|
+
};
|
|
8883
|
+
const mayUnsubscribeCreatedSubscription = (subscription) => !consentedSubscriptions.has(subscription) && (pendingEnrollments.get(subscription)?.size ?? 0) === 0;
|
|
8884
|
+
const publish = (value) => {
|
|
8885
|
+
snapshot = Object.freeze({
|
|
8886
|
+
...value,
|
|
8887
|
+
testInFlight
|
|
8888
|
+
});
|
|
8889
|
+
for (const listener of listeners) listener();
|
|
8890
|
+
};
|
|
8891
|
+
const current = (token) => active && generation === token;
|
|
8892
|
+
const getInstallationId = () => {
|
|
8893
|
+
if (memoryInstallId) return memoryInstallId;
|
|
8894
|
+
try {
|
|
8895
|
+
const saved = environment.storage?.getItem(ID_KEY);
|
|
8896
|
+
if (isPushInstallationId(saved)) return memoryInstallId = saved;
|
|
8897
|
+
} catch {}
|
|
8898
|
+
const fresh = environment.randomUUID?.() ?? globalThis.crypto?.randomUUID?.();
|
|
8899
|
+
if (!isPushInstallationId(fresh)) throw new Error("uuid unavailable");
|
|
8900
|
+
memoryInstallId = fresh;
|
|
8901
|
+
try {
|
|
8902
|
+
environment.storage?.setItem(ID_KEY, fresh);
|
|
8903
|
+
} catch {}
|
|
8904
|
+
return fresh;
|
|
8905
|
+
};
|
|
8906
|
+
const detect = () => {
|
|
8907
|
+
if (!environment.secureContext || !environment.isTopLevel || !environment.pushManager || environment.permission === "unsupported") return "unsupported";
|
|
8908
|
+
if (!environment.standalone) return "install";
|
|
8909
|
+
if (environment.permission === "denied") return "denied";
|
|
8910
|
+
};
|
|
8911
|
+
const refresh = async (token) => {
|
|
8912
|
+
const blocked = detect();
|
|
8913
|
+
if (blocked) {
|
|
8914
|
+
if (current(token)) publish({ state: blocked });
|
|
8915
|
+
return false;
|
|
8916
|
+
}
|
|
8917
|
+
try {
|
|
8918
|
+
const requestedInstallId = getInstallationId();
|
|
8919
|
+
const [nextConfig, subscription] = await Promise.all([api.getConfig(requestedInstallId), environment.pushManager.getSubscription()]);
|
|
8920
|
+
if (!current(token)) return false;
|
|
8921
|
+
config = nextConfig;
|
|
8922
|
+
existing = subscription;
|
|
8923
|
+
preparedInstallId = nextConfig.installationId || requestedInstallId;
|
|
8924
|
+
memoryInstallId = preparedInstallId;
|
|
8925
|
+
try {
|
|
8926
|
+
environment.storage?.setItem(ID_KEY, preparedInstallId);
|
|
8927
|
+
} catch {}
|
|
8928
|
+
preparedKey = nextConfig.publicKey ? decodeKey(nextConfig.publicKey) : void 0;
|
|
8929
|
+
if (!nextConfig.available || !preparedKey) publish({
|
|
8930
|
+
state: "unsupported",
|
|
8931
|
+
preferences: nextConfig.preferences
|
|
8932
|
+
});
|
|
8933
|
+
else if (nextConfig.status === "enabled") {
|
|
8934
|
+
if (subscription) {
|
|
8935
|
+
currentHostSubscription = subscription;
|
|
8936
|
+
consentedSubscriptions.add(subscription);
|
|
8937
|
+
}
|
|
8938
|
+
publish({
|
|
8939
|
+
state: nextConfig.status,
|
|
8940
|
+
preferences: nextConfig.preferences
|
|
8941
|
+
});
|
|
8942
|
+
} else if (nextConfig.status === "suspended") {
|
|
8943
|
+
if (subscription) {
|
|
8944
|
+
currentHostSubscription = subscription;
|
|
8945
|
+
consentedSubscriptions.add(subscription);
|
|
8946
|
+
}
|
|
8947
|
+
publish({
|
|
8948
|
+
state: "suspended",
|
|
8949
|
+
preferences: nextConfig.preferences
|
|
8950
|
+
});
|
|
8951
|
+
} else {
|
|
8952
|
+
if (nextConfig.status === "disabled" && currentHostSubscription) {
|
|
8953
|
+
consentedSubscriptions.delete(currentHostSubscription);
|
|
8954
|
+
currentHostSubscription = void 0;
|
|
8955
|
+
}
|
|
8956
|
+
publish({
|
|
8957
|
+
state: "ready",
|
|
8958
|
+
preferences: nextConfig.preferences
|
|
8959
|
+
});
|
|
8960
|
+
}
|
|
8961
|
+
return true;
|
|
8962
|
+
} catch (error) {
|
|
8963
|
+
if (current(token)) publish({
|
|
8964
|
+
state: "failed",
|
|
8965
|
+
error: safeError(error)
|
|
8966
|
+
});
|
|
8967
|
+
return false;
|
|
8968
|
+
}
|
|
8969
|
+
};
|
|
8970
|
+
const refreshNow = () => {
|
|
8971
|
+
if (active) refresh(generation);
|
|
8972
|
+
};
|
|
8973
|
+
return {
|
|
8974
|
+
start() {
|
|
8975
|
+
active = true;
|
|
8976
|
+
generation += 1;
|
|
8977
|
+
config = void 0;
|
|
8978
|
+
ownedSubscription = void 0;
|
|
8979
|
+
publish({ state: "pending" });
|
|
8980
|
+
refreshNow();
|
|
8981
|
+
},
|
|
8982
|
+
stop() {
|
|
8983
|
+
active = false;
|
|
8984
|
+
generation += 1;
|
|
8985
|
+
attempt += 1;
|
|
8986
|
+
mutationBusy = false;
|
|
8987
|
+
testOwner += 1;
|
|
8988
|
+
if (testInFlight) {
|
|
8989
|
+
testInFlight = false;
|
|
8990
|
+
publish({ ...snapshot });
|
|
8991
|
+
}
|
|
8992
|
+
config = void 0;
|
|
8993
|
+
preparedKey = void 0;
|
|
8994
|
+
existing = null;
|
|
8995
|
+
ownedSubscription = void 0;
|
|
8996
|
+
},
|
|
8997
|
+
refresh: refreshNow,
|
|
8998
|
+
async enableFromGesture() {
|
|
8999
|
+
if (!active || mutationBusy || snapshot.state !== "ready" && snapshot.state !== "failed" && snapshot.state !== "disabled" || !config || !preparedKey || !environment.pushManager) return;
|
|
9000
|
+
const token = generation, operation = ++attempt, base = config;
|
|
9001
|
+
mutationBusy = true;
|
|
9002
|
+
publish({
|
|
9003
|
+
state: "pending",
|
|
9004
|
+
preferences: base.preferences
|
|
9005
|
+
});
|
|
9006
|
+
let subscription = existing;
|
|
9007
|
+
let created = false;
|
|
9008
|
+
let pendingSubscription;
|
|
9009
|
+
try {
|
|
9010
|
+
if (subscription && !sameKey(subscription, preparedKey)) {
|
|
9011
|
+
publish({
|
|
9012
|
+
state: "failed",
|
|
9013
|
+
error: "push.error.subscriptionConflict",
|
|
9014
|
+
preferences: base.preferences
|
|
9015
|
+
});
|
|
9016
|
+
return;
|
|
9017
|
+
}
|
|
9018
|
+
if (!subscription) {
|
|
9019
|
+
subscription = await environment.pushManager.subscribe({
|
|
9020
|
+
userVisibleOnly: true,
|
|
9021
|
+
applicationServerKey: preparedKey
|
|
9022
|
+
});
|
|
9023
|
+
created = true;
|
|
9024
|
+
}
|
|
9025
|
+
if (created) ownedSubscription = subscription;
|
|
9026
|
+
if (!current(token) || operation !== attempt) {
|
|
9027
|
+
if (created && mayUnsubscribeCreatedSubscription(subscription)) {
|
|
9028
|
+
if (await subscription.unsubscribe().catch(() => false) && ownedSubscription === subscription) ownedSubscription = void 0;
|
|
9029
|
+
}
|
|
9030
|
+
return;
|
|
9031
|
+
}
|
|
9032
|
+
trackPendingEnrollment(subscription, operation);
|
|
9033
|
+
pendingSubscription = subscription;
|
|
9034
|
+
const browserSubscription = subscription.toJSON();
|
|
9035
|
+
const result = await api.enroll({
|
|
9036
|
+
installationId: preparedInstallId || base.installationId,
|
|
9037
|
+
subscription: {
|
|
9038
|
+
endpoint: browserSubscription.endpoint,
|
|
9039
|
+
keys: {
|
|
9040
|
+
p256dh: browserSubscription.keys.p256dh,
|
|
9041
|
+
auth: browserSubscription.keys.auth
|
|
9042
|
+
}
|
|
9043
|
+
},
|
|
9044
|
+
locale: environment.locale ?? "en",
|
|
9045
|
+
preferences: base.preferences ?? defaults,
|
|
9046
|
+
keyFingerprint: base.keyFingerprint ?? "",
|
|
9047
|
+
ownerGeneration: base.ownerGeneration,
|
|
9048
|
+
registrationGeneration: base.registrationGeneration
|
|
9049
|
+
});
|
|
9050
|
+
finishPendingEnrollment(subscription, operation);
|
|
9051
|
+
pendingSubscription = void 0;
|
|
9052
|
+
if (!current(token) || operation !== attempt) {
|
|
9053
|
+
if (result.mutationGeneration !== void 0) await api.disable({
|
|
9054
|
+
installationId: result.registration.installationId,
|
|
9055
|
+
ifGeneration: result.mutationGeneration
|
|
9056
|
+
}).catch(() => void 0);
|
|
9057
|
+
if (created && mayUnsubscribeCreatedSubscription(subscription)) {
|
|
9058
|
+
if (await subscription.unsubscribe().catch(() => false) && ownedSubscription === subscription) ownedSubscription = void 0;
|
|
9059
|
+
}
|
|
9060
|
+
return;
|
|
9061
|
+
}
|
|
9062
|
+
if (created) ownedSubscription = subscription;
|
|
9063
|
+
consentedSubscriptions.add(subscription);
|
|
9064
|
+
currentHostSubscription = subscription;
|
|
9065
|
+
if (result.registration.origin === environment.origin && result.registration.installationId) {
|
|
9066
|
+
memoryInstallId = result.registration.installationId;
|
|
9067
|
+
preparedInstallId = memoryInstallId;
|
|
9068
|
+
try {
|
|
9069
|
+
environment.storage?.setItem(ID_KEY, memoryInstallId);
|
|
9070
|
+
} catch {}
|
|
9071
|
+
}
|
|
9072
|
+
config = {
|
|
9073
|
+
...base,
|
|
9074
|
+
installationId: preparedInstallId || result.registration.installationId,
|
|
9075
|
+
registrationGeneration: result.registration.generation,
|
|
9076
|
+
status: result.registration.health === "suspended" ? "suspended" : "enabled",
|
|
9077
|
+
preferences: result.registration.preferences
|
|
9078
|
+
};
|
|
9079
|
+
publish({
|
|
9080
|
+
state: result.registration.health === "suspended" ? "suspended" : "enabled",
|
|
9081
|
+
preferences: result.registration.preferences
|
|
9082
|
+
});
|
|
9083
|
+
} catch (error) {
|
|
9084
|
+
if (pendingSubscription) finishPendingEnrollment(pendingSubscription, operation);
|
|
9085
|
+
if (created && subscription && mayUnsubscribeCreatedSubscription(subscription)) {
|
|
9086
|
+
if (await subscription.unsubscribe().catch(() => false) && ownedSubscription === subscription) ownedSubscription = void 0;
|
|
9087
|
+
}
|
|
9088
|
+
if (current(token) && operation === attempt) {
|
|
9089
|
+
const isStale = safeError(error) === "push.error.stale";
|
|
9090
|
+
publish({
|
|
9091
|
+
state: "failed",
|
|
9092
|
+
error: safeError(error),
|
|
9093
|
+
preferences: base.preferences
|
|
9094
|
+
});
|
|
9095
|
+
if (isStale) refresh(token);
|
|
9096
|
+
}
|
|
9097
|
+
} finally {
|
|
9098
|
+
if (current(token) && operation === attempt) mutationBusy = false;
|
|
9099
|
+
}
|
|
9100
|
+
},
|
|
9101
|
+
async disable() {
|
|
9102
|
+
if (!active || !config) return;
|
|
9103
|
+
const token = generation, operation = ++attempt, base = config;
|
|
9104
|
+
mutationBusy = true;
|
|
9105
|
+
publish({
|
|
9106
|
+
state: "pending",
|
|
9107
|
+
preferences: base.preferences
|
|
9108
|
+
});
|
|
9109
|
+
try {
|
|
9110
|
+
await api.disable({ installationId: preparedInstallId || base.installationId });
|
|
9111
|
+
if (!current(token) || operation !== attempt) return;
|
|
9112
|
+
config = {
|
|
9113
|
+
...base,
|
|
9114
|
+
status: "disabled"
|
|
9115
|
+
};
|
|
9116
|
+
publish({
|
|
9117
|
+
state: "disabled",
|
|
9118
|
+
preferences: base.preferences
|
|
9119
|
+
});
|
|
9120
|
+
if (currentHostSubscription) consentedSubscriptions.delete(currentHostSubscription);
|
|
9121
|
+
currentHostSubscription = void 0;
|
|
9122
|
+
const sub = ownedSubscription;
|
|
9123
|
+
if (sub) {
|
|
9124
|
+
if (await sub.unsubscribe().catch(() => false) && ownedSubscription === sub) ownedSubscription = void 0;
|
|
9125
|
+
}
|
|
9126
|
+
if (await refresh(token) && current(token) && operation === attempt && config?.status === "disabled") publish({
|
|
9127
|
+
state: "disabled",
|
|
9128
|
+
preferences: config.preferences
|
|
9129
|
+
});
|
|
9130
|
+
} catch (error) {
|
|
9131
|
+
if (current(token) && operation === attempt) {
|
|
9132
|
+
publish({
|
|
9133
|
+
state: "failed",
|
|
9134
|
+
error: safeError(error),
|
|
9135
|
+
preferences: base.preferences
|
|
9136
|
+
});
|
|
9137
|
+
if (safeError(error) === "push.error.stale") refresh(token);
|
|
9138
|
+
}
|
|
9139
|
+
} finally {
|
|
9140
|
+
if (current(token) && operation === attempt) mutationBusy = false;
|
|
9141
|
+
}
|
|
9142
|
+
},
|
|
9143
|
+
async updatePreferences(value) {
|
|
9144
|
+
if (!active || !config || mutationBusy) return;
|
|
9145
|
+
const token = generation, operation = ++attempt, base = config;
|
|
9146
|
+
mutationBusy = true;
|
|
9147
|
+
publish({
|
|
9148
|
+
state: "pending",
|
|
9149
|
+
preferences: value
|
|
9150
|
+
});
|
|
9151
|
+
try {
|
|
9152
|
+
await api.updatePreferences({
|
|
9153
|
+
installationId: preparedInstallId || base.installationId,
|
|
9154
|
+
ownerGeneration: base.ownerGeneration,
|
|
9155
|
+
registrationGeneration: base.registrationGeneration,
|
|
9156
|
+
preferences: value
|
|
9157
|
+
});
|
|
9158
|
+
if (current(token) && operation === attempt) {
|
|
9159
|
+
config = {
|
|
9160
|
+
...base,
|
|
9161
|
+
preferences: value
|
|
9162
|
+
};
|
|
9163
|
+
publish({
|
|
9164
|
+
state: base.status === "enabled" ? "enabled" : "ready",
|
|
9165
|
+
preferences: value
|
|
9166
|
+
});
|
|
9167
|
+
await refresh(token);
|
|
9168
|
+
}
|
|
9169
|
+
} catch (error) {
|
|
9170
|
+
if (current(token) && operation === attempt) {
|
|
9171
|
+
publish({
|
|
9172
|
+
state: "failed",
|
|
9173
|
+
error: safeError(error),
|
|
9174
|
+
preferences: base.preferences
|
|
9175
|
+
});
|
|
9176
|
+
if (safeError(error) === "push.error.stale") refresh(token);
|
|
9177
|
+
}
|
|
9178
|
+
} finally {
|
|
9179
|
+
if (current(token) && operation === attempt) mutationBusy = false;
|
|
9180
|
+
}
|
|
9181
|
+
},
|
|
9182
|
+
async sendTest() {
|
|
9183
|
+
if (!active || !config || config.status !== "enabled" || mutationBusy || testInFlight) return;
|
|
9184
|
+
const token = generation, operation = attempt, base = config;
|
|
9185
|
+
const owner = ++testOwner;
|
|
9186
|
+
testInFlight = true;
|
|
9187
|
+
publish({
|
|
9188
|
+
state: "enabled",
|
|
9189
|
+
preferences: base.preferences
|
|
9190
|
+
});
|
|
9191
|
+
try {
|
|
9192
|
+
await api.sendTest({
|
|
9193
|
+
installationId: preparedInstallId || base.installationId,
|
|
9194
|
+
ownerGeneration: base.ownerGeneration,
|
|
9195
|
+
registrationGeneration: base.registrationGeneration
|
|
9196
|
+
});
|
|
9197
|
+
if (current(token) && operation === attempt) publish({
|
|
9198
|
+
state: "enabled",
|
|
9199
|
+
error: "push.test.accepted",
|
|
9200
|
+
preferences: base.preferences
|
|
9201
|
+
});
|
|
9202
|
+
} catch (error) {
|
|
9203
|
+
if (!current(token) || operation !== attempt) return;
|
|
9204
|
+
const message = error?.code === "rate_limited" ? "push.test.rateLimited" : "push.test.failed";
|
|
9205
|
+
const refreshed = await refresh(token);
|
|
9206
|
+
if (!current(token) || operation !== attempt) return;
|
|
9207
|
+
if (refreshed && config?.status === "enabled") publish({
|
|
9208
|
+
state: "enabled",
|
|
9209
|
+
error: message,
|
|
9210
|
+
preferences: config.preferences
|
|
9211
|
+
});
|
|
9212
|
+
} finally {
|
|
9213
|
+
if (owner === testOwner) {
|
|
9214
|
+
testInFlight = false;
|
|
9215
|
+
publish({ ...snapshot });
|
|
9216
|
+
}
|
|
9217
|
+
}
|
|
9218
|
+
},
|
|
9219
|
+
getSnapshot: () => snapshot,
|
|
9220
|
+
subscribe(listener) {
|
|
9221
|
+
listeners.add(listener);
|
|
9222
|
+
return () => {
|
|
9223
|
+
listeners.delete(listener);
|
|
9224
|
+
};
|
|
9225
|
+
}
|
|
9226
|
+
};
|
|
9227
|
+
}
|
|
9228
|
+
/** Adapter factory uses only the page's public PushManager; it deliberately has no service-worker path. */
|
|
9229
|
+
function browserPushEnvironment(view = window) {
|
|
9230
|
+
const nav = view.navigator;
|
|
9231
|
+
const pushManager = view.pushManager;
|
|
9232
|
+
const storage = (() => {
|
|
9233
|
+
try {
|
|
9234
|
+
return view.localStorage;
|
|
9235
|
+
} catch {
|
|
9236
|
+
return;
|
|
9237
|
+
}
|
|
9238
|
+
})();
|
|
9239
|
+
const isInstalled = () => {
|
|
9240
|
+
try {
|
|
9241
|
+
return nav.standalone === true || view.matchMedia("(display-mode: standalone)").matches || view.matchMedia("(display-mode: fullscreen)").matches;
|
|
9242
|
+
} catch {
|
|
9243
|
+
return nav.standalone === true;
|
|
9244
|
+
}
|
|
9245
|
+
};
|
|
9246
|
+
return {
|
|
9247
|
+
secureContext: view.isSecureContext,
|
|
9248
|
+
isTopLevel: view.top === view.self,
|
|
9249
|
+
get standalone() {
|
|
9250
|
+
return isInstalled();
|
|
9251
|
+
},
|
|
9252
|
+
get permission() {
|
|
9253
|
+
return "Notification" in view ? view.Notification.permission : "unsupported";
|
|
9254
|
+
},
|
|
9255
|
+
pushManager,
|
|
9256
|
+
storage,
|
|
9257
|
+
origin: view.location.origin,
|
|
9258
|
+
randomUUID: () => view.crypto.randomUUID(),
|
|
9259
|
+
locale: view.document.documentElement.lang.toLowerCase().startsWith("zh") ? "zh-CN" : view.document.documentElement.lang.toLowerCase().startsWith("it") ? "it" : "en"
|
|
9260
|
+
};
|
|
9261
|
+
}
|
|
9262
|
+
//#endregion
|
|
9263
|
+
//#region src/mobile-push/switch-component.ts
|
|
9264
|
+
/**
|
|
9265
|
+
* Lazily require the public Switch from the DSH module loader at category render time.
|
|
9266
|
+
*
|
|
9267
|
+
* The plugin client bundle is a `window.__ModuleLoader__.load` factory whose `require` parameter
|
|
9268
|
+
* resolves seeded shell modules synchronously (`rc.2` and `rc.3` both seed the primitives namespace
|
|
9269
|
+
* unconditionally). Requiring lazily — inside the render that needs it, never at module scope —
|
|
9270
|
+
* keeps every Node test and provider-free fixture that mounts the settings row but not the push
|
|
9271
|
+
* toggles free of any browser-only module dependency. Outside a DSH shell there is no loader
|
|
9272
|
+
* `require`, so the categories are withdrawn instead of rendering a broken stand-in; native
|
|
9273
|
+
* rendering itself is verified by the strict packed smoke against the real mounted shell.
|
|
9274
|
+
*/
|
|
9275
|
+
function requirePublicMobilePushSwitch() {
|
|
9276
|
+
if (typeof window === "undefined" || typeof require !== "function") return void 0;
|
|
9277
|
+
try {
|
|
9278
|
+
return require("@deepseek-ai/dsh-client-ui-primitives").Switch;
|
|
9279
|
+
} catch {
|
|
9280
|
+
return;
|
|
9281
|
+
}
|
|
9282
|
+
}
|
|
9283
|
+
//#endregion
|
|
9284
|
+
//#region src/mobile-push/settings.ts
|
|
9285
|
+
const MOBILE_PUSH_SETTINGS_COPY = {
|
|
9286
|
+
en: {
|
|
9287
|
+
title: "Web push notifications",
|
|
9288
|
+
desc: "Receive notifications when this page is closed.",
|
|
9289
|
+
enable: "Enable",
|
|
9290
|
+
disable: "Disable",
|
|
9291
|
+
test: "Send test",
|
|
9292
|
+
testBusy: "Sending…",
|
|
9293
|
+
accepted: "Accepted by the push provider; lock-screen delivery is not confirmed.",
|
|
9294
|
+
install: "Add this site to your Home Screen to enable push.",
|
|
9295
|
+
denied: "Notifications are blocked in browser settings.",
|
|
9296
|
+
unsupported: "Web push is unavailable in this context.",
|
|
9297
|
+
error: "Could not update push settings. Try again.",
|
|
9298
|
+
testFailed: "The test notification could not be sent. Your notification settings are unchanged.",
|
|
9299
|
+
testRateLimited: "Too many test notifications; wait a moment before trying again.",
|
|
9300
|
+
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.",
|
|
9301
|
+
busy: "Working…",
|
|
9302
|
+
categories: [
|
|
9303
|
+
"Questions",
|
|
9304
|
+
"Plans",
|
|
9305
|
+
"Approvals",
|
|
9306
|
+
"Responses",
|
|
9307
|
+
"Goals",
|
|
9308
|
+
"Failures"
|
|
9309
|
+
]
|
|
9310
|
+
},
|
|
9311
|
+
zh: {
|
|
9312
|
+
title: "网页推送通知",
|
|
9313
|
+
desc: "关闭此页面后仍可接收通知。",
|
|
9314
|
+
enable: "启用",
|
|
9315
|
+
disable: "停用",
|
|
9316
|
+
test: "发送测试",
|
|
9317
|
+
testBusy: "发送中…",
|
|
9318
|
+
accepted: "推送服务商已接受;不代表锁屏一定送达。",
|
|
9319
|
+
install: "请将此网站添加到主屏幕以启用推送。",
|
|
9320
|
+
denied: "浏览器设置已阻止通知。",
|
|
9321
|
+
unsupported: "此环境不支持网页推送。",
|
|
9322
|
+
error: "无法更新推送设置,请重试。",
|
|
9323
|
+
testFailed: "测试通知未能发送。通知设置保持不变。",
|
|
9324
|
+
testRateLimited: "测试通知发送过于频繁,请稍后再试。",
|
|
9325
|
+
conflict: "此设备已存在与旧服务器密钥绑定的推送订阅,因此无法完成启用。重试无法解决;请先由服务器管理员检查推送密钥状态。",
|
|
9326
|
+
busy: "处理中…",
|
|
9327
|
+
categories: [
|
|
9328
|
+
"问题",
|
|
9329
|
+
"计划",
|
|
9330
|
+
"审批",
|
|
9331
|
+
"回复",
|
|
9332
|
+
"目标",
|
|
9333
|
+
"失败"
|
|
9334
|
+
]
|
|
9335
|
+
},
|
|
9336
|
+
it: {
|
|
9337
|
+
title: "Notifiche Web push",
|
|
9338
|
+
desc: "Ricevi notifiche quando la pagina è chiusa.",
|
|
9339
|
+
enable: "Attiva",
|
|
9340
|
+
disable: "Disattiva",
|
|
9341
|
+
test: "Invia test",
|
|
9342
|
+
testBusy: "Invio…",
|
|
9343
|
+
accepted: "Accettata dal provider; la consegna sulla schermata di blocco non è confermata.",
|
|
9344
|
+
install: "Aggiungi il sito alla schermata Home per attivare il push.",
|
|
9345
|
+
denied: "Le notifiche sono bloccate nelle impostazioni del browser.",
|
|
9346
|
+
unsupported: "Web push non disponibile in questo contesto.",
|
|
9347
|
+
error: "Impossibile aggiornare le impostazioni push. Riprova.",
|
|
9348
|
+
testFailed: "Impossibile inviare la notifica di test. Le impostazioni delle notifiche restano invariate.",
|
|
9349
|
+
testRateLimited: "Troppe notifiche di test; attendi qualche istante prima di riprovare.",
|
|
9350
|
+
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.",
|
|
9351
|
+
busy: "Operazione…",
|
|
9352
|
+
categories: [
|
|
9353
|
+
"Domande",
|
|
9354
|
+
"Piani",
|
|
9355
|
+
"Approvazioni",
|
|
9356
|
+
"Risposte",
|
|
9357
|
+
"Obiettivi",
|
|
9358
|
+
"Errori"
|
|
9359
|
+
]
|
|
9360
|
+
}
|
|
9361
|
+
};
|
|
9362
|
+
const KEYS = [
|
|
9363
|
+
"question",
|
|
9364
|
+
"plan",
|
|
9365
|
+
"approval",
|
|
9366
|
+
"response",
|
|
9367
|
+
"goal",
|
|
9368
|
+
"failure"
|
|
9369
|
+
];
|
|
9370
|
+
function message(snapshot, copy) {
|
|
9371
|
+
if (snapshot.state === "install") return copy.install;
|
|
9372
|
+
if (snapshot.state === "denied") return copy.denied;
|
|
9373
|
+
if (snapshot.state === "unsupported") return copy.unsupported;
|
|
9374
|
+
if (snapshot.state === "enabled" && snapshot.error === "push.test.accepted") return copy.accepted;
|
|
9375
|
+
if (snapshot.state === "enabled" && snapshot.error === "push.test.rateLimited") return copy.testRateLimited;
|
|
9376
|
+
if (snapshot.state === "enabled" && snapshot.error === "push.test.failed") return copy.testFailed;
|
|
9377
|
+
if (snapshot.state === "failed" && snapshot.error === "push.error.subscriptionConflict") return copy.conflict;
|
|
9378
|
+
return snapshot.state === "failed" ? copy.error : copy.desc;
|
|
9379
|
+
}
|
|
9380
|
+
/** Browser-only settings row; mounting it never requests permission or creates a subscription. */
|
|
9381
|
+
function MobilePushSettings({ controller, locale, switchComponent }) {
|
|
9382
|
+
const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
|
|
9383
|
+
const copy = MOBILE_PUSH_SETTINGS_COPY[locale];
|
|
9384
|
+
if (snapshot.state === "unsupported") return null;
|
|
9385
|
+
const prefs = snapshot.preferences;
|
|
9386
|
+
const Switch = switchComponent ?? requirePublicMobilePushSwitch();
|
|
9387
|
+
const toggle = (key, checked) => {
|
|
9388
|
+
if (!prefs) return;
|
|
9389
|
+
controller.updatePreferences({
|
|
9390
|
+
...prefs,
|
|
9391
|
+
[key]: checked
|
|
9392
|
+
});
|
|
9393
|
+
};
|
|
9394
|
+
return (0, react.createElement)("div", {
|
|
9395
|
+
className: "dsh-mobile-settings_row",
|
|
9396
|
+
style: { flexWrap: "wrap" },
|
|
9397
|
+
lang: locale === "zh" ? "zh-CN" : locale
|
|
9398
|
+
}, (0, react.createElement)("div", {
|
|
9399
|
+
className: "dsh-mobile-settings_rowText",
|
|
9400
|
+
style: { paddingRight: 8 }
|
|
9401
|
+
}, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
|
|
9402
|
+
className: "dsh-mobile-settings_desc",
|
|
9403
|
+
"aria-live": "polite"
|
|
9404
|
+
}, message(snapshot, copy)), prefs && Switch !== void 0 ? (0, react.createElement)("div", { className: "dsh-mobile-push-categories" }, ...KEYS.map((key, index) => {
|
|
9405
|
+
const name = copy.categories[index];
|
|
9406
|
+
if (name === void 0) return null;
|
|
9407
|
+
return (0, react.createElement)("label", {
|
|
9408
|
+
key,
|
|
9409
|
+
className: "dsh-mobile-push-category"
|
|
9410
|
+
}, (0, react.createElement)("span", { className: "dsh-mobile-push-categoryName" }, name), (0, react.createElement)(Switch, {
|
|
9411
|
+
checked: prefs[key],
|
|
9412
|
+
label: name,
|
|
9413
|
+
onChange: (next) => {
|
|
9414
|
+
toggle(key, next);
|
|
9415
|
+
}
|
|
9416
|
+
}));
|
|
9417
|
+
})) : null), (0, react.createElement)("div", {
|
|
9418
|
+
className: "dsh-mobile-push-actions",
|
|
9419
|
+
style: {
|
|
9420
|
+
display: "flex",
|
|
9421
|
+
flexWrap: "wrap",
|
|
9422
|
+
gap: 8
|
|
9423
|
+
}
|
|
9424
|
+
}, snapshot.state === "enabled" ? (0, react.createElement)("button", {
|
|
9425
|
+
type: "button",
|
|
9426
|
+
className: "dsh-mobile-settings_selector",
|
|
9427
|
+
disabled: snapshot.testInFlight === true,
|
|
9428
|
+
"aria-busy": snapshot.testInFlight === true ? "true" : void 0,
|
|
9429
|
+
onClick: () => void controller.sendTest()
|
|
9430
|
+
}, snapshot.testInFlight ? copy.testBusy : copy.test) : null, snapshot.state === "enabled" || snapshot.state === "suspended" ? (0, react.createElement)("button", {
|
|
9431
|
+
type: "button",
|
|
9432
|
+
className: "dsh-mobile-settings_selector",
|
|
9433
|
+
onClick: () => void controller.disable()
|
|
9434
|
+
}, copy.disable) : snapshot.state === "ready" || snapshot.state === "failed" || snapshot.state === "disabled" ? (0, react.createElement)("button", {
|
|
9435
|
+
type: "button",
|
|
9436
|
+
className: "dsh-mobile-settings_selector",
|
|
9437
|
+
onClick: () => void controller.enableFromGesture()
|
|
9438
|
+
}, copy.enable) : (0, react.createElement)("button", {
|
|
9439
|
+
type: "button",
|
|
9440
|
+
className: "dsh-mobile-settings_selector",
|
|
9441
|
+
disabled: true
|
|
9442
|
+
}, copy.busy)));
|
|
9443
|
+
}
|
|
9444
|
+
//#endregion
|
|
9445
|
+
//#region src/mobile-push/navigation.ts
|
|
9446
|
+
const REF_PARAMETER = "mobile-push";
|
|
9447
|
+
/**
|
|
9448
|
+
* Consume one authenticated tap reference after the public Workspace and Session
|
|
9449
|
+
* list baselines are ready. Return the disposal function for list subscriptions.
|
|
9450
|
+
*/
|
|
9451
|
+
function installPushNavigation(ctx, options) {
|
|
9452
|
+
const context = ctx;
|
|
9453
|
+
const browser = options.browser ?? {
|
|
9454
|
+
location: window.location,
|
|
9455
|
+
history: window.history
|
|
9456
|
+
};
|
|
9457
|
+
const params = new URLSearchParams(browser.location.search);
|
|
9458
|
+
const reference = params.get(REF_PARAMETER);
|
|
9459
|
+
let disposed = false;
|
|
9460
|
+
let consumed = false;
|
|
9461
|
+
let resolving = false;
|
|
9462
|
+
let opened = false;
|
|
9463
|
+
const clearReference = () => {
|
|
9464
|
+
if (!params.has(REF_PARAMETER)) return;
|
|
9465
|
+
params.delete(REF_PARAMETER);
|
|
9466
|
+
const query = params.toString();
|
|
9467
|
+
const url = `${browser.location.pathname}${query ? `?${query}` : ""}${browser.location.hash}`;
|
|
9468
|
+
browser.history.replaceState(null, "", url);
|
|
9469
|
+
};
|
|
9470
|
+
const neutral = (key = "session-unavailable") => {
|
|
9471
|
+
if (disposed || consumed) return;
|
|
9472
|
+
consumed = true;
|
|
9473
|
+
clearReference();
|
|
9474
|
+
options.notify(key);
|
|
9475
|
+
};
|
|
9476
|
+
const check = () => {
|
|
9477
|
+
if (disposed || consumed || resolving) return;
|
|
9478
|
+
if (!reference) {
|
|
9479
|
+
neutral();
|
|
9480
|
+
return;
|
|
9481
|
+
}
|
|
9482
|
+
const sessions = context.sessions.list.getSnapshot();
|
|
9483
|
+
const workspaces = context.workspaces.list.getSnapshot();
|
|
9484
|
+
if (sessions.phase !== "pending" && sessions.phase !== "ready" || workspaces.phase !== "pending" && workspaces.phase !== "ready") {
|
|
9485
|
+
neutral();
|
|
9486
|
+
return;
|
|
9487
|
+
}
|
|
9488
|
+
if (sessions.phase !== "ready" || workspaces.phase !== "ready") return;
|
|
9489
|
+
if (!reference) {
|
|
9490
|
+
neutral();
|
|
9491
|
+
return;
|
|
9492
|
+
}
|
|
9493
|
+
resolving = true;
|
|
9494
|
+
consumed = true;
|
|
9495
|
+
clearReference();
|
|
9496
|
+
options.resolve(reference).then((target) => {
|
|
9497
|
+
if (disposed) return;
|
|
9498
|
+
if (!target || target.interaction !== "current" && target.interaction !== "resolved" || !sessionsAvailable(context, target.sessionId)) {
|
|
9499
|
+
options.notify("session-unavailable");
|
|
9500
|
+
return;
|
|
9501
|
+
}
|
|
9502
|
+
if (opened) return;
|
|
9503
|
+
opened = true;
|
|
9504
|
+
context.uiWorkspace.openSession(target.sessionId);
|
|
9505
|
+
if (target.interaction === "resolved") options.notify("session-resolved");
|
|
9506
|
+
}).catch(() => {
|
|
9507
|
+
if (!disposed) options.notify("session-unavailable");
|
|
9508
|
+
});
|
|
9509
|
+
};
|
|
9510
|
+
const unsubscribeSessions = context.sessions.list.subscribe(check);
|
|
9511
|
+
const unsubscribeWorkspaces = context.workspaces.list.subscribe(check);
|
|
9512
|
+
check();
|
|
9513
|
+
return () => {
|
|
9514
|
+
if (disposed) return;
|
|
9515
|
+
disposed = true;
|
|
9516
|
+
unsubscribeSessions();
|
|
9517
|
+
unsubscribeWorkspaces();
|
|
9518
|
+
};
|
|
9519
|
+
}
|
|
9520
|
+
function sessionsAvailable(ctx, sessionId) {
|
|
9521
|
+
const snapshot = ctx.sessions.list.getSnapshot();
|
|
9522
|
+
return snapshot.phase === "ready" && snapshot.ids.includes(sessionId) && snapshot.byId[sessionId] !== void 0;
|
|
9523
|
+
}
|
|
9524
|
+
//#endregion
|
|
8509
9525
|
//#region src/mobile-directory-browser.ts
|
|
8510
9526
|
/** Expected Gateway or response-validation failure for one read-only listing. */
|
|
8511
9527
|
var MobileDirectoryReadError = class extends Error {
|
|
@@ -9579,6 +10595,12 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
9579
10595
|
function parseMobileRemoteProvider(value) {
|
|
9580
10596
|
return value === "cpolar" || value === "cloudflared" || value === "frp" || value === "origin" || value === "iroh" ? value : "tailscale";
|
|
9581
10597
|
}
|
|
10598
|
+
/** Public client services the authenticated tap consumer follows (scoped child lifecycle, never a required plugin dependency). */
|
|
10599
|
+
const mobilePushNavigationServices = [
|
|
10600
|
+
"sessions",
|
|
10601
|
+
"workspaces",
|
|
10602
|
+
"uiWorkspace"
|
|
10603
|
+
];
|
|
9582
10604
|
/**
|
|
9583
10605
|
* Set a surface's visibility; opening a page or overlay brings it to the front.
|
|
9584
10606
|
* @param container - the registered surface element.
|
|
@@ -10428,6 +11450,31 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
10428
11450
|
const lanSetupStatus = element("p", "dsh-mobile-control__status");
|
|
10429
11451
|
lanSetupStatus.setAttribute("role", "status");
|
|
10430
11452
|
lanSetupStatus.setAttribute("aria-live", "polite");
|
|
11453
|
+
const lanChange = element("section", "dsh-mobile-control__lan-setup");
|
|
11454
|
+
lanChange.hidden = true;
|
|
11455
|
+
const lanChangeTitle = element("h3", "dsh-mobile-control__section-title");
|
|
11456
|
+
lanChangeTitle.textContent = t("lanChangeTitle");
|
|
11457
|
+
const lanChangeIntro = element("p", "dsh-mobile-control__intro");
|
|
11458
|
+
lanChangeIntro.textContent = t("lanChangeIntro");
|
|
11459
|
+
const lanChangeForm = element("div", "dsh-mobile-control__lan-setup-form");
|
|
11460
|
+
const lanChangeLabel = element("label", "dsh-mobile-control__field");
|
|
11461
|
+
lanChangeLabel.textContent = t("lanChangeSelect");
|
|
11462
|
+
const lanChangeSelect = element("select");
|
|
11463
|
+
lanChangeSelect.setAttribute("aria-label", t("lanChangeSelect"));
|
|
11464
|
+
lanChangeLabel.append(lanChangeSelect);
|
|
11465
|
+
const lanChangeActions = element("div", "dsh-mobile-control__lan-setup-actions");
|
|
11466
|
+
const lanChangeRefresh = element("button", "dsh-mobile-control__secondary");
|
|
11467
|
+
lanChangeRefresh.type = "button";
|
|
11468
|
+
lanChangeRefresh.textContent = t("lanChangeRefresh");
|
|
11469
|
+
const lanChangeConfirm = element("button", "dsh-mobile-control__primary");
|
|
11470
|
+
lanChangeConfirm.type = "button";
|
|
11471
|
+
lanChangeConfirm.textContent = t("lanChangeConfirm");
|
|
11472
|
+
lanChangeActions.append(lanChangeRefresh, lanChangeConfirm);
|
|
11473
|
+
const lanChangeStatus = element("p", "dsh-mobile-control__status");
|
|
11474
|
+
lanChangeStatus.setAttribute("role", "status");
|
|
11475
|
+
lanChangeStatus.setAttribute("aria-live", "polite");
|
|
11476
|
+
lanChangeForm.append(lanChangeLabel, lanChangeActions);
|
|
11477
|
+
lanChange.append(lanChangeTitle, lanChangeIntro, lanChangeForm, lanChangeStatus);
|
|
10431
11478
|
const trustedNetworks = element("details", "dsh-mobile-control__details");
|
|
10432
11479
|
const trustedNetworksSummary = element("summary");
|
|
10433
11480
|
trustedNetworksSummary.textContent = t("trustedNetworksTitle");
|
|
@@ -11763,7 +12810,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11763
12810
|
diagnosticsDetails.append(diagnosticsDetailsSummary, diagnosticsReport);
|
|
11764
12811
|
header.append(title, headerActions);
|
|
11765
12812
|
actions.append(toggle, pair, linkPair);
|
|
11766
|
-
lanView.append(lanSetup, access, qrBox, lanPairingValidity, status, extensionStatus, actions, trustedNetworks);
|
|
12813
|
+
lanView.append(lanSetup, lanChange, access, qrBox, lanPairingValidity, status, extensionStatus, actions, trustedNetworks);
|
|
11767
12814
|
const channelPane = element("div", "dsh-mobile-control__channel-pane");
|
|
11768
12815
|
channelPane.append(lanView, remoteWorkspace);
|
|
11769
12816
|
const remoteColumns = element("div", "dsh-mobile-control__remote-columns");
|
|
@@ -11778,6 +12825,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11778
12825
|
let lanConfigured = true;
|
|
11779
12826
|
let lanRestartRequired = false;
|
|
11780
12827
|
let lanSetupLoaded = false;
|
|
12828
|
+
let lanChangeLoaded = false;
|
|
12829
|
+
let lanChangeBusy = false;
|
|
12830
|
+
let lanChangeRequest;
|
|
11781
12831
|
let remoteRunning = false;
|
|
11782
12832
|
let remoteReady = false;
|
|
11783
12833
|
let remoteProvider = "tailscale";
|
|
@@ -11994,6 +13044,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
11994
13044
|
lanChoiceBadge.textContent = running ? t("lanBadgeOn") : t("lanBadgeOff");
|
|
11995
13045
|
origin = running && typeof data.origin === "string" ? data.origin : "";
|
|
11996
13046
|
lanSetup.hidden = lanConfigured && !lanRestartRequired;
|
|
13047
|
+
lanChange.hidden = !lanConfigured || lanRestartRequired || data.rebindSupported !== true;
|
|
13048
|
+
if (!lanChange.hidden && !lanChangeLoaded) loadLanChange();
|
|
11997
13049
|
lanSetupForm.hidden = lanRestartRequired;
|
|
11998
13050
|
lanSetupIntro.hidden = lanRestartRequired;
|
|
11999
13051
|
status.hidden = !lanConfigured || lanRestartRequired;
|
|
@@ -12007,7 +13059,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12007
13059
|
accessLink.textContent = origin;
|
|
12008
13060
|
accessLink.title = origin;
|
|
12009
13061
|
status.classList.toggle("is-running", running);
|
|
12010
|
-
status.textContent = !lanConfigured ? t("lanSetupRequired") : lanRestartRequired ? t("lanSetupRestart", { origin: typeof data.pendingOrigin === "string" ? data.pendingOrigin : "" }) : running ? t("lanOn") : t("lanOff");
|
|
13062
|
+
status.textContent = data.recoveryRequired === true ? t("lanChangeRecoveryRequired") : !lanConfigured ? t("lanSetupRequired") : lanRestartRequired ? t("lanSetupRestart", { origin: typeof data.pendingOrigin === "string" ? data.pendingOrigin : "" }) : running ? t("lanOn") : t("lanOff");
|
|
12011
13063
|
const extensionData = data.extensions;
|
|
12012
13064
|
if (extensionData !== null && typeof extensionData === "object") {
|
|
12013
13065
|
const loaded = typeof extensionData.loaded === "number" ? extensionData.loaded : 0;
|
|
@@ -12098,6 +13150,80 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
12098
13150
|
lanSetupConfigure.disabled = lanRestartRequired || lanSetupSelect.options.length === 0;
|
|
12099
13151
|
});
|
|
12100
13152
|
});
|
|
13153
|
+
const syncLanChange = () => {
|
|
13154
|
+
const waiting = lanChangeBusy || lanChangeRequest !== void 0;
|
|
13155
|
+
lanChangeSelect.disabled = waiting;
|
|
13156
|
+
lanChangeRefresh.disabled = waiting;
|
|
13157
|
+
lanChangeConfirm.disabled = waiting || lanChangeSelect.options.length === 0;
|
|
13158
|
+
};
|
|
13159
|
+
const renderLanChange = (data) => {
|
|
13160
|
+
const networks = Array.isArray(data.networks) ? data.networks.filter((entry) => typeof entry === "object" && entry !== null) : [];
|
|
13161
|
+
lanChangeSelect.replaceChildren();
|
|
13162
|
+
for (const network of networks) {
|
|
13163
|
+
const name = typeof network.name === "string" ? network.name : "";
|
|
13164
|
+
const address = typeof network.address === "string" ? network.address : "";
|
|
13165
|
+
if (name === "") continue;
|
|
13166
|
+
const option = element("option");
|
|
13167
|
+
option.value = name;
|
|
13168
|
+
option.textContent = `${name} · ${address}${network.current === true ? ` · ${t("lanChangeCurrent")}` : ""}`;
|
|
13169
|
+
if (network.current === true) option.selected = true;
|
|
13170
|
+
lanChangeSelect.append(option);
|
|
13171
|
+
}
|
|
13172
|
+
syncLanChange();
|
|
13173
|
+
};
|
|
13174
|
+
const loadLanChange = () => {
|
|
13175
|
+
if (lanChangeRequest !== void 0) return lanChangeRequest;
|
|
13176
|
+
lanChangeLoaded = true;
|
|
13177
|
+
lanChangeRequest = Promise.all([controlRequestJson("/api/mobile-access/lan/networks"), controlRequestJson("/api/mobile-access/lan/control")]).then(([data, control]) => {
|
|
13178
|
+
if (!lifecycle.signal.aborted) {
|
|
13179
|
+
renderLanChange(data);
|
|
13180
|
+
render(control);
|
|
13181
|
+
}
|
|
13182
|
+
}, () => {
|
|
13183
|
+
if (!lifecycle.signal.aborted) lanChangeStatus.textContent = t("lanChangeRefreshFailed");
|
|
13184
|
+
}).finally(() => {
|
|
13185
|
+
lanChangeRequest = void 0;
|
|
13186
|
+
if (!lifecycle.signal.aborted) syncLanChange();
|
|
13187
|
+
});
|
|
13188
|
+
syncLanChange();
|
|
13189
|
+
return lanChangeRequest;
|
|
13190
|
+
};
|
|
13191
|
+
lanChangeRefresh.addEventListener("click", () => {
|
|
13192
|
+
loadLanChange();
|
|
13193
|
+
});
|
|
13194
|
+
lanChangeSelect.addEventListener("change", syncLanChange);
|
|
13195
|
+
lanChangeConfirm.addEventListener("click", () => {
|
|
13196
|
+
if (lanChangeSelect.value === "" || lanChangeBusy || lanChangeRequest !== void 0) return;
|
|
13197
|
+
lanChangeBusy = true;
|
|
13198
|
+
syncLanChange();
|
|
13199
|
+
lanChangeConfirm.textContent = t("lanChangeBusy");
|
|
13200
|
+
lanChangeStatus.textContent = t("lanChangeBusy");
|
|
13201
|
+
controlRequestJson("/api/mobile-access/lan/rebind", {
|
|
13202
|
+
method: "POST",
|
|
13203
|
+
body: JSON.stringify({ networkInterface: lanChangeSelect.value })
|
|
13204
|
+
}, LONG_CONTROL_REQUEST_TIMEOUT_MS).then(async (data) => {
|
|
13205
|
+
if (lifecycle.signal.aborted) return;
|
|
13206
|
+
lanChangeStatus.textContent = data.disconnected === true && data.ready === true ? t("lanChangeOkDisconnected") : t("lanChangeOk");
|
|
13207
|
+
try {
|
|
13208
|
+
const control = await controlRequestJson("/api/mobile-access/lan/control");
|
|
13209
|
+
if (lifecycle.signal.aborted) return;
|
|
13210
|
+
render(control);
|
|
13211
|
+
await loadLanChange();
|
|
13212
|
+
if (!lifecycle.signal.aborted && data.ready === true) openPairing("key");
|
|
13213
|
+
} catch {
|
|
13214
|
+
if (!lifecycle.signal.aborted) lanChangeStatus.textContent = t("lanChangeRefreshFailed");
|
|
13215
|
+
}
|
|
13216
|
+
}, (error) => {
|
|
13217
|
+
if (lifecycle.signal.aborted) return;
|
|
13218
|
+
const text = String(error);
|
|
13219
|
+
lanChangeStatus.textContent = text.includes("lan_rebind_unchanged") ? t("lanChangeUnchanged") : text.includes("lan_rebind_network_unavailable") ? t("lanChangeUnavailable") : text.includes("lan_rebind_unsupported") ? t("lanChangeUnsupported") : text.includes("lan_rebind_recovery_required") ? t("lanChangeRecoveryRequired") : t("lanChangeFailed");
|
|
13220
|
+
}).finally(() => {
|
|
13221
|
+
lanChangeBusy = false;
|
|
13222
|
+
if (lifecycle.signal.aborted) return;
|
|
13223
|
+
lanChangeConfirm.textContent = t("lanChangeConfirm");
|
|
13224
|
+
syncLanChange();
|
|
13225
|
+
});
|
|
13226
|
+
});
|
|
12101
13227
|
const clearQrTarget = (target) => {
|
|
12102
13228
|
if (typeof target.replaceChildren === "function") {
|
|
12103
13229
|
target.replaceChildren();
|
|
@@ -15738,6 +16864,107 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15738
16864
|
.dsh-mobile-control__remote-workspace .dsh-mobile-control__details{border:0;border-radius:0;background:transparent;box-shadow:none}
|
|
15739
16865
|
.dsh-mobile-control__self-hosted{border:0;border-radius:0;background:transparent;box-shadow:none}
|
|
15740
16866
|
` + EXTENSION_RECOVERY_STYLES;
|
|
16867
|
+
/** Resolve safe, locale-owned copy for tap-navigation results. */
|
|
16868
|
+
function mobilePushNavigationNotice(locale, key) {
|
|
16869
|
+
return {
|
|
16870
|
+
en: {
|
|
16871
|
+
"session-unavailable": "This conversation is no longer available.",
|
|
16872
|
+
"session-resolved": "Opened the conversation."
|
|
16873
|
+
},
|
|
16874
|
+
it: {
|
|
16875
|
+
"session-unavailable": "Questa conversazione non è più disponibile.",
|
|
16876
|
+
"session-resolved": "Conversazione aperta."
|
|
16877
|
+
},
|
|
16878
|
+
zh: {
|
|
16879
|
+
"session-unavailable": "此会话已不可用。",
|
|
16880
|
+
"session-resolved": "已打开会话。"
|
|
16881
|
+
}
|
|
16882
|
+
}[locale][key];
|
|
16883
|
+
}
|
|
16884
|
+
function showMobilePushNavigationNotice(page, view, key) {
|
|
16885
|
+
const node = page.createElement("div");
|
|
16886
|
+
node.className = "dsh-mobile-push-navigation-notice";
|
|
16887
|
+
node.setAttribute("role", "status");
|
|
16888
|
+
node.setAttribute("aria-live", "polite");
|
|
16889
|
+
node.lang = selectedMobileControlLocale();
|
|
16890
|
+
node.textContent = mobilePushNavigationNotice(selectedMobileControlLocale(), key);
|
|
16891
|
+
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";
|
|
16892
|
+
page.body.append(node);
|
|
16893
|
+
view.setTimeout(() => node.remove(), 3e3);
|
|
16894
|
+
}
|
|
16895
|
+
/** Own the browser-only push controller, settings row, and optional authenticated tap navigation. */
|
|
16896
|
+
function installMobilePushBrowserClient(ctx, runtime) {
|
|
16897
|
+
const { page, view } = runtime;
|
|
16898
|
+
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;
|
|
16899
|
+
const api = runtime.api ?? createMobilePushBrowserApi(mobileRequest);
|
|
16900
|
+
const controller = createMobilePushController(api, runtime.environment ?? browserPushEnvironment(view));
|
|
16901
|
+
controller.start();
|
|
16902
|
+
const removeSettings = ctx.slots.inject("settings.general.item", () => ctx.slots.register({
|
|
16903
|
+
name: "settings.general.item",
|
|
16904
|
+
id: "dsh-mobile-push-browser",
|
|
16905
|
+
order: 120
|
|
16906
|
+
}, () => (0, react.createElement)(MobilePushSettings, {
|
|
16907
|
+
controller,
|
|
16908
|
+
locale: selectedMobileControlLocale()
|
|
16909
|
+
})));
|
|
16910
|
+
const refresh = () => {
|
|
16911
|
+
controller.refresh();
|
|
16912
|
+
};
|
|
16913
|
+
const onVisible = () => {
|
|
16914
|
+
if (page.visibilityState === "visible") refresh();
|
|
16915
|
+
};
|
|
16916
|
+
view.addEventListener("focus", refresh);
|
|
16917
|
+
page.addEventListener("visibilitychange", onVisible);
|
|
16918
|
+
const connection = ctx.get("connection");
|
|
16919
|
+
const unbindConnection = connection?.state?.subscribe(() => {
|
|
16920
|
+
if (connection.state?.getSnapshot() === "connected") refresh();
|
|
16921
|
+
}) ?? (() => void 0);
|
|
16922
|
+
let removeNavigation = () => void 0;
|
|
16923
|
+
let disposeNavigationLifecycle = () => void 0;
|
|
16924
|
+
const reference = new URLSearchParams(view.location.search).get("mobile-push");
|
|
16925
|
+
if (reference !== null && reference !== "" && ctx.inject !== void 0) {
|
|
16926
|
+
let navigationClosed = false;
|
|
16927
|
+
const navigationFiber = ctx.inject(mobilePushNavigationServices, (navigationContext) => {
|
|
16928
|
+
if (navigationClosed) return () => void 0;
|
|
16929
|
+
const pending = new URLSearchParams(view.location.search).get("mobile-push");
|
|
16930
|
+
if (pending === null || pending === "") return () => void 0;
|
|
16931
|
+
const remove = installPushNavigation(navigationContext, {
|
|
16932
|
+
resolve: (value) => api.resolve(value),
|
|
16933
|
+
notify: (key) => {
|
|
16934
|
+
if (key !== "session-unavailable" && key !== "session-resolved") return;
|
|
16935
|
+
if (runtime.notify) runtime.notify(key);
|
|
16936
|
+
else showMobilePushNavigationNotice(page, view, key);
|
|
16937
|
+
},
|
|
16938
|
+
browser: {
|
|
16939
|
+
location: view.location,
|
|
16940
|
+
history: view.history
|
|
16941
|
+
}
|
|
16942
|
+
});
|
|
16943
|
+
removeNavigation = remove;
|
|
16944
|
+
return () => {
|
|
16945
|
+
if (removeNavigation === remove) removeNavigation = () => void 0;
|
|
16946
|
+
remove();
|
|
16947
|
+
};
|
|
16948
|
+
});
|
|
16949
|
+
disposeNavigationLifecycle = () => {
|
|
16950
|
+
if (navigationClosed) return;
|
|
16951
|
+
navigationClosed = true;
|
|
16952
|
+
removeNavigation();
|
|
16953
|
+
navigationFiber.dispose();
|
|
16954
|
+
};
|
|
16955
|
+
}
|
|
16956
|
+
let disposed = false;
|
|
16957
|
+
return () => {
|
|
16958
|
+
if (disposed) return;
|
|
16959
|
+
disposed = true;
|
|
16960
|
+
view.removeEventListener("focus", refresh);
|
|
16961
|
+
page.removeEventListener("visibilitychange", onVisible);
|
|
16962
|
+
unbindConnection();
|
|
16963
|
+
disposeNavigationLifecycle();
|
|
16964
|
+
removeSettings();
|
|
16965
|
+
controller.stop();
|
|
16966
|
+
};
|
|
16967
|
+
}
|
|
15741
16968
|
/** Mount the desktop control or mobile feature enhancements. */
|
|
15742
16969
|
function apply(ctx) {
|
|
15743
16970
|
let gatewayRejectionSink;
|
|
@@ -15772,6 +16999,10 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15772
16999
|
return recovery();
|
|
15773
17000
|
};
|
|
15774
17001
|
}, "dsh-mobile: connection recovery");
|
|
17002
|
+
ctx.effect(() => installMobilePushBrowserClient(ctx, {
|
|
17003
|
+
page: document,
|
|
17004
|
+
view: window
|
|
17005
|
+
}), "dsh-mobile: authenticated browser push lifecycle");
|
|
15775
17006
|
ctx.effect(() => {
|
|
15776
17007
|
const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
|
|
15777
17008
|
const gatewayCustomization = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
|
|
@@ -15940,8 +17171,10 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
|
|
|
15940
17171
|
exports.installCustomAssets = installCustomAssets;
|
|
15941
17172
|
exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
|
|
15942
17173
|
exports.installMobileNativeBack = installMobileNativeBack;
|
|
17174
|
+
exports.installMobilePushBrowserClient = installMobilePushBrowserClient;
|
|
15943
17175
|
exports.localizedDiagnosticEntryCopy = localizedDiagnosticEntryCopy;
|
|
15944
17176
|
exports.mobileBackBlockedMessage = mobileBackBlockedMessage;
|
|
17177
|
+
exports.mobilePushNavigationNotice = mobilePushNavigationNotice;
|
|
15945
17178
|
exports.normalizeDiagnosticOverall = normalizeDiagnosticOverall;
|
|
15946
17179
|
exports.normalizeDiagnosticStatus = normalizeDiagnosticStatus;
|
|
15947
17180
|
exports.parseMobileExtensionManifest = parseMobileExtensionManifest;
|