dsh-mobile 0.8.5 → 0.8.7

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/lib/client.js CHANGED
@@ -3791,8 +3791,40 @@ window.__ModuleLoader__.load({
3791
3791
  if (bridge === void 0 || typeof bridge.capabilities !== "function" || typeof bridge.invoke !== "function") return void 0;
3792
3792
  return bridge;
3793
3793
  }
3794
- /** Parse a `task-notify` SSE payload; rejects anything malformed or empty. */
3795
- function parseTaskNotifyPayload(value) {
3794
+ /** Encode one opaque Session id for the App's internal notification route. */
3795
+ function taskNotificationNavigate(sessionId) {
3796
+ if (sessionId === "") return void 0;
3797
+ const bytes = new TextEncoder().encode(sessionId);
3798
+ let binary = "";
3799
+ for (const byte of bytes) binary += String.fromCharCode(byte);
3800
+ const route = `/?dsh-session=${globalThis.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "")}`;
3801
+ return route.length <= 270 ? route : void 0;
3802
+ }
3803
+ /** Decode only the App's bounded internal Session route. */
3804
+ function decodeTaskNotificationNavigate(value) {
3805
+ if (value === null || value === void 0 || !/^[A-Za-z0-9_-]{1,256}$/u.test(value)) return void 0;
3806
+ try {
3807
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - value.length % 4) % 4);
3808
+ const binary = globalThis.atob(padded);
3809
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
3810
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3811
+ return decoded === "" ? void 0 : decoded;
3812
+ } catch {
3813
+ return;
3814
+ }
3815
+ }
3816
+ /** Resolve the opaque Session id carried by the App's internal notification route. */
3817
+ function sessionIdFromNotificationRoute(route, base = "https://mobile.invalid/") {
3818
+ if (route === null || route === void 0) return void 0;
3819
+ let encoded;
3820
+ try {
3821
+ encoded = new URL(route, base).searchParams.get("dsh-session");
3822
+ } catch {
3823
+ return;
3824
+ }
3825
+ return decodeTaskNotificationNavigate(encoded);
3826
+ }
3827
+ /** Parse a `task-notify` SSE payload; rejects anything malformed or empty. */ function parseTaskNotifyPayload(value) {
3796
3828
  let record;
3797
3829
  if (typeof value === "string") try {
3798
3830
  const parsed = JSON.parse(value);
@@ -3845,7 +3877,8 @@ window.__ModuleLoader__.load({
3845
3877
  return bridge.invoke("notification.notify", {
3846
3878
  title: event.title,
3847
3879
  body: event.body,
3848
- tag: event.tag
3880
+ tag: event.tag,
3881
+ ...event.navigate === void 0 ? {} : { navigate: event.navigate }
3849
3882
  });
3850
3883
  }).then(() => void 0, (error) => {
3851
3884
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
@@ -4283,6 +4316,13 @@ window.__ModuleLoader__.load({
4283
4316
  .dsh-mobile-settings_selector:hover:not(:disabled) { background:var(--dsw-alias-interactive-bg-hover); }
4284
4317
  .dsh-mobile-settings_selector:active:not(:disabled) { background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover)); }
4285
4318
  .dsh-mobile-settings_selector:focus-visible { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
4319
+ /* Row switches (session prefetch, composer collapse) pair a visually-hidden
4320
+ native checkbox with an On/Off pill. Hide the checkbox so the pill is the only
4321
+ visible control, while the label keeps the 48px row a tap target and the input
4322
+ stays keyboard-focusable; the pill's :focus-visible ring is driven by the
4323
+ checkbox's focus via :has. */
4324
+ .dsh-mobile-settings_switchInput { position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0; opacity:0; }
4325
+ .dsh-mobile-settings_row:has(.dsh-mobile-settings_switchInput:focus-visible) .dsh-mobile-settings_selector { outline:2px solid var(--dsw-alias-label-primary-bluish,#2563eb); outline-offset:2px; }
4286
4326
  .dsh-mobile-settings_selector:disabled { cursor:wait; opacity:.55; }
4287
4327
  .dsh-mobile-settings_fontControls { display:flex; align-items:center; gap:8px; flex-shrink:0; }
4288
4328
  .dsh-mobile-settings_fontControls button { min-width:48px; padding:0 12px; }
@@ -9582,10 +9622,7 @@ window.__ModuleLoader__.load({
9582
9622
  });
9583
9623
  if (currentHostSubscription) consentedSubscriptions.delete(currentHostSubscription);
9584
9624
  currentHostSubscription = void 0;
9585
- const sub = ownedSubscription;
9586
- if (sub) {
9587
- if (await sub.unsubscribe().catch(() => false) && ownedSubscription === sub) ownedSubscription = void 0;
9588
- }
9625
+ ownedSubscription = void 0;
9589
9626
  if (await refresh(token) && current(token) && operation === attempt && config?.status === "disabled") publish({
9590
9627
  state: "disabled",
9591
9628
  preferences: config.preferences
@@ -9746,7 +9783,13 @@ window.__ModuleLoader__.load({
9746
9783
  const MOBILE_PUSH_SETTINGS_COPY = {
9747
9784
  en: {
9748
9785
  title: "Web push notifications",
9749
- desc: "Receive notifications when this page is closed.",
9786
+ desc: "Receive notifications when this page is closed. Disable stops sends but keeps the browser subscription so it can be reused safely.",
9787
+ permission: {
9788
+ granted: "System permission: granted. A reinstalled Home Screen app can inherit this site-level grant.",
9789
+ default: "System permission: not decided.",
9790
+ denied: "System permission: blocked.",
9791
+ unknown: "System permission: unavailable."
9792
+ },
9750
9793
  enable: "Enable",
9751
9794
  disable: "Disable",
9752
9795
  test: "Send test",
@@ -9771,7 +9814,13 @@ window.__ModuleLoader__.load({
9771
9814
  },
9772
9815
  zh: {
9773
9816
  title: "网页推送通知",
9774
- desc: "关闭此页面后仍可接收通知。",
9817
+ desc: "关闭此页面后仍可接收通知。停用会停止发送,但保留浏览器订阅,便于安全地重新启用。",
9818
+ permission: {
9819
+ granted: "系统权限:已允许。重新安装的主屏幕 App 可能继承站点级授权。",
9820
+ default: "系统权限:尚未选择。",
9821
+ denied: "系统权限:已阻止。",
9822
+ unknown: "系统权限:不可读取。"
9823
+ },
9775
9824
  enable: "启用",
9776
9825
  disable: "停用",
9777
9826
  test: "发送测试",
@@ -9796,7 +9845,13 @@ window.__ModuleLoader__.load({
9796
9845
  },
9797
9846
  it: {
9798
9847
  title: "Notifiche Web push",
9799
- desc: "Ricevi notifiche quando la pagina è chiusa.",
9848
+ desc: "Ricevi notifiche quando la pagina è chiusa. La disattivazione interrompe gli invii ma conserva l’iscrizione del browser per poterla riutilizzare.",
9849
+ permission: {
9850
+ granted: "Permesso di sistema: concesso. Una Web App reinstallata può ereditare questa autorizzazione del sito.",
9851
+ default: "Permesso di sistema: non ancora scelto.",
9852
+ denied: "Permesso di sistema: bloccato.",
9853
+ unknown: "Permesso di sistema: non disponibile."
9854
+ },
9800
9855
  enable: "Attiva",
9801
9856
  disable: "Disattiva",
9802
9857
  test: "Invia test",
@@ -9845,6 +9900,7 @@ window.__ModuleLoader__.load({
9845
9900
  if (snapshot.state === "unsupported") return null;
9846
9901
  const prefs = snapshot.preferences;
9847
9902
  const Switch = switchComponent ?? requirePublicMobilePushSwitch();
9903
+ const permission = typeof Notification === "undefined" ? "unknown" : Notification.permission;
9848
9904
  const toggle = (key, checked) => {
9849
9905
  if (!prefs) return;
9850
9906
  controller.updatePreferences({
@@ -9866,7 +9922,7 @@ window.__ModuleLoader__.load({
9866
9922
  }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
9867
9923
  className: "dsh-mobile-settings_desc",
9868
9924
  "aria-live": "polite"
9869
- }, message(snapshot, copy)), prefs && Switch !== void 0 ? (0, react.createElement)("div", { className: "dsh-mobile-push-categories" }, ...KEYS.map((key, index) => {
9925
+ }, message(snapshot, copy)), (0, react.createElement)("div", { className: "dsh-mobile-settings_desc" }, copy.permission[permission]), prefs && Switch !== void 0 ? (0, react.createElement)("div", { className: "dsh-mobile-push-categories" }, ...KEYS.map((key, index) => {
9870
9926
  const name = copy.categories[index];
9871
9927
  if (name === void 0) return null;
9872
9928
  return (0, react.createElement)("label", {
@@ -9906,8 +9962,105 @@ window.__ModuleLoader__.load({
9906
9962
  disabled: true
9907
9963
  }, copy.busy)));
9908
9964
  }
9965
+ // src/session-prefetch.ts
9966
+ /**
9967
+ * Storage key for the opt-in switch.
9968
+ *
9969
+ * The setting row lives in the client bundle while the tracker lives in the
9970
+ * layout bundle (tsdown.config.ts emits them separately and inlines this module
9971
+ * into each), so a module-level singleton cannot carry the value across. The
9972
+ * layout bundle owns the persisted state instead; the row notifies it through
9973
+ * both the storage event (other tabs) and a custom event (same page).
9974
+ */
9975
+ const SESSION_PREFETCH_STORAGE_KEY = "dsh-mobile.session-prefetch.enabled";
9976
+ const SESSION_PREFETCH_EVENT = "dsh-mobile:session-prefetch-change";
9977
+ /** Read the persisted opt-in. Defaults to off; unreadable storage keeps it off. */
9978
+ function readSessionPrefetchEnabled(storage) {
9979
+ try {
9980
+ return (storage ?? globalThis.localStorage)?.getItem(SESSION_PREFETCH_STORAGE_KEY) === "true";
9981
+ } catch {
9982
+ return false;
9983
+ }
9984
+ }
9985
+ /** Persist the opt-in and notify the layout bundle in the same page. */
9986
+ function writeSessionPrefetchEnabled(enabled, storage) {
9987
+ try {
9988
+ (storage ?? globalThis.localStorage)?.setItem(SESSION_PREFETCH_STORAGE_KEY, String(enabled));
9989
+ } catch {}
9990
+ try {
9991
+ globalThis.dispatchEvent?.(new CustomEvent(SESSION_PREFETCH_EVENT, { detail: { enabled } }));
9992
+ } catch {}
9993
+ }
9994
+ /**
9995
+ * Subscribe to opt-in changes coming from either bundle. The listener is not
9996
+ * called with the initial value; read it once with readSessionPrefetchEnabled().
9997
+ */
9998
+ function subscribeSessionPrefetchEnabled(listener) {
9999
+ const onStorage = (event) => {
10000
+ if (event.key !== null && event.key !== "dsh-mobile.session-prefetch.enabled") return;
10001
+ listener(readSessionPrefetchEnabled());
10002
+ };
10003
+ const onCustom = (event) => {
10004
+ const detail = event.detail;
10005
+ listener(detail?.enabled ?? readSessionPrefetchEnabled());
10006
+ };
10007
+ globalThis.addEventListener?.("storage", onStorage);
10008
+ globalThis.addEventListener?.(SESSION_PREFETCH_EVENT, onCustom);
10009
+ return () => {
10010
+ globalThis.removeEventListener?.("storage", onStorage);
10011
+ globalThis.removeEventListener?.(SESSION_PREFETCH_EVENT, onCustom);
10012
+ };
10013
+ }
10014
+ // src/session-prefetch-settings.ts
10015
+ const SESSION_PREFETCH_COPY = {
10016
+ en: {
10017
+ title: "Prefetch the session you just left",
10018
+ desc: "After switching sessions, warm the previous one in the background so switching back opens faster. Read-only, at most one session per switch, with a per-session cooldown.",
10019
+ on: "On",
10020
+ off: "Off"
10021
+ },
10022
+ zh: {
10023
+ title: "预取刚离开的会话",
10024
+ desc: "切换会话后在后台预热上一个会话,切回去时打开更快。只读、每次切换最多预热一个会话,并带单会话冷却。",
10025
+ on: "已开启",
10026
+ off: "已关闭"
10027
+ },
10028
+ it: {
10029
+ title: "Prefetch della sessione appena lasciata",
10030
+ desc: "Dopo un cambio di sessione riscalda in background quella precedente, così tornare indietro è più rapido. Sola lettura, al massimo una sessione per cambio, con cooldown per sessione.",
10031
+ on: "Attivo",
10032
+ off: "Disattivo"
10033
+ }
10034
+ };
10035
+ /** Browser-only settings row; toggling it only persists the flag. */
10036
+ function SessionPrefetchSettings({ locale, read, write, subscribe }) {
10037
+ const readEnabled = read ?? readSessionPrefetchEnabled;
10038
+ const writeEnabled = write ?? writeSessionPrefetchEnabled;
10039
+ const enabled = (0, react.useSyncExternalStore)(subscribe ?? subscribeSessionPrefetchEnabled, readEnabled, () => false);
10040
+ const copy = SESSION_PREFETCH_COPY[locale];
10041
+ return (0, react.createElement)("label", {
10042
+ className: "dsh-mobile-settings_row",
10043
+ lang: locale === "zh" ? "zh-CN" : locale,
10044
+ "data-dsh-mobile-session-prefetch": enabled ? "on" : "off"
10045
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
10046
+ className: "dsh-mobile-settings_desc",
10047
+ "aria-live": "polite"
10048
+ }, copy.desc)), (0, react.createElement)("input", {
10049
+ type: "checkbox",
10050
+ role: "switch",
10051
+ className: "dsh-mobile-settings_switchInput",
10052
+ checked: enabled,
10053
+ "aria-label": copy.title,
10054
+ "aria-checked": enabled ? "true" : "false",
10055
+ onChange: (event) => {
10056
+ writeEnabled(event.currentTarget.checked);
10057
+ }
10058
+ }), (0, react.createElement)("span", {
10059
+ className: "dsh-mobile-settings_selector",
10060
+ "aria-hidden": "true"
10061
+ }, enabled ? copy.on : copy.off));
10062
+ }
9909
10063
  // src/mobile-push/navigation.ts
9910
- const REF_PARAMETER = "mobile-push";
9911
10064
  /**
9912
10065
  * Consume one authenticated tap reference after the public Workspace and Session
9913
10066
  * list baselines are ready. Return the disposal function for list subscriptions.
@@ -9918,15 +10071,16 @@ window.__ModuleLoader__.load({
9918
10071
  location: window.location,
9919
10072
  history: window.history
9920
10073
  };
10074
+ const refParameter = options.parameter ?? "mobile-push";
9921
10075
  const params = new URLSearchParams(browser.location.search);
9922
- const reference = params.get(REF_PARAMETER);
10076
+ const reference = params.get(refParameter);
9923
10077
  let disposed = false;
9924
10078
  let consumed = false;
9925
10079
  let resolving = false;
9926
10080
  let opened = false;
9927
10081
  const clearReference = () => {
9928
- if (!params.has(REF_PARAMETER)) return;
9929
- params.delete(REF_PARAMETER);
10082
+ if (!params.has(refParameter)) return;
10083
+ params.delete(refParameter);
9930
10084
  const query = params.toString();
9931
10085
  const url = `${browser.location.pathname}${query ? `?${query}` : ""}${browser.location.hash}`;
9932
10086
  browser.history.replaceState(null, "", url);
@@ -11412,6 +11566,136 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11412
11566
  onClick: invoke
11413
11567
  }, state === "busy" ? copy.busy : state === "success" ? copy.reopen : state === "error" ? copy.retry : copy.action));
11414
11568
  }
11569
+ function mobileBackgroundNotificationCopy(locale) {
11570
+ if (locale === "zh") return {
11571
+ title: "后台任务提醒",
11572
+ description: "App 页面被系统挂起后,通过可关闭的常驻连接继续接收任务完成提醒。",
11573
+ enabled: "关闭后台提醒",
11574
+ disabled: "开启后台提醒",
11575
+ busy: "正在处理…",
11576
+ error: "无法更新后台提醒;请确认任务通知权限和当前连接。",
11577
+ connected: "已连接",
11578
+ disconnected: "未连接"
11579
+ };
11580
+ if (locale === "it") return {
11581
+ title: "Promemoria in background",
11582
+ description: "Continua a ricevere gli avvisi quando Android sospende la pagina, tramite una connessione persistente disattivabile.",
11583
+ enabled: "Disattiva promemoria",
11584
+ disabled: "Attiva promemoria",
11585
+ busy: "Operazione…",
11586
+ error: "Impossibile aggiornare i promemoria; controlla il permesso notifiche e la connessione attiva.",
11587
+ connected: "Connesso",
11588
+ disconnected: "Non connesso"
11589
+ };
11590
+ return {
11591
+ title: "Background task reminders",
11592
+ description: "Keep receiving completion alerts after Android suspends the page, through a dismissible persistent connection.",
11593
+ enabled: "Disable background reminders",
11594
+ disabled: "Enable background reminders",
11595
+ busy: "Working…",
11596
+ error: "Could not update background reminders. Check notification permission and the active connection.",
11597
+ connected: "Connected",
11598
+ disconnected: "Not connected"
11599
+ };
11600
+ }
11601
+ /** Render the App's real connection state when the installed App reports it. */
11602
+ function mobileBackgroundStatusText(locale, connected, lastError) {
11603
+ if (connected === void 0) return void 0;
11604
+ const copy = mobileBackgroundNotificationCopy(locale);
11605
+ if (connected) return copy.connected;
11606
+ return lastError === void 0 ? copy.disconnected : `${copy.disconnected} (${lastError})`;
11607
+ }
11608
+ /** Android-only opt-in for the foreground SSE fallback when the WebView is suspended. */
11609
+ function MobileBackgroundNotificationRow() {
11610
+ const [supported, setSupported] = (0, react.useState)(false);
11611
+ const [enabled, setEnabled] = (0, react.useState)(false);
11612
+ const [connected, setConnected] = (0, react.useState)(void 0);
11613
+ const [lastError, setLastError] = (0, react.useState)(void 0);
11614
+ const [busy, setBusy] = (0, react.useState)(false);
11615
+ const [error, setError] = (0, react.useState)(false);
11616
+ const generation = (0, react.useRef)(0);
11617
+ (0, react.useEffect)(() => {
11618
+ let active = true;
11619
+ const refresh = () => {
11620
+ const attempt = ++generation.current;
11621
+ const bridge = window.__DSH_MOBILE_NATIVE__;
11622
+ if (bridge === void 0) {
11623
+ if (active) {
11624
+ setSupported(false);
11625
+ setError(false);
11626
+ }
11627
+ return;
11628
+ }
11629
+ Promise.resolve(bridge.capabilities()).then(async (capabilities) => {
11630
+ const available = capabilities.includes("notification.background.get") && capabilities.includes("notification.background.set");
11631
+ if (!active || attempt !== generation.current) return;
11632
+ setSupported(available);
11633
+ if (!available) return;
11634
+ const result = await bridge.invoke("notification.background.get", {});
11635
+ if (!active || attempt !== generation.current) return;
11636
+ if (typeof result?.enabled !== "boolean") throw new Error("invalid background notification status");
11637
+ setEnabled(result.enabled);
11638
+ setConnected(typeof result.connected === "boolean" ? result.connected : void 0);
11639
+ setLastError(typeof result.lastError === "string" ? result.lastError : void 0);
11640
+ setError(false);
11641
+ }).catch(() => {
11642
+ if (active && attempt === generation.current) {
11643
+ setSupported(true);
11644
+ setError(true);
11645
+ }
11646
+ });
11647
+ };
11648
+ refresh();
11649
+ window.addEventListener("dsh-mobile-native-ready", refresh);
11650
+ return () => {
11651
+ active = false;
11652
+ generation.current += 1;
11653
+ window.removeEventListener("dsh-mobile-native-ready", refresh);
11654
+ };
11655
+ }, []);
11656
+ if (!supported) return null;
11657
+ const locale = selectedMobileControlLocale();
11658
+ const copy = mobileBackgroundNotificationCopy(locale);
11659
+ const statusText = enabled ? mobileBackgroundStatusText(locale, connected, lastError) : void 0;
11660
+ const toggle = () => {
11661
+ const bridge = window.__DSH_MOBILE_NATIVE__;
11662
+ if (bridge === void 0 || busy) return;
11663
+ const attempt = ++generation.current;
11664
+ setBusy(true);
11665
+ setError(false);
11666
+ bridge.invoke("notification.background.set", { enabled: !enabled }).then((result) => {
11667
+ if (attempt !== generation.current) return;
11668
+ const next = result?.enabled;
11669
+ if (typeof next !== "boolean") throw new Error("invalid background notification status");
11670
+ setEnabled(next);
11671
+ bridge.invoke("notification.background.get", {}).then((statusResult) => {
11672
+ if (attempt !== generation.current) return;
11673
+ const status = statusResult;
11674
+ if (typeof status?.enabled === "boolean") setEnabled(status.enabled);
11675
+ setConnected(typeof status?.connected === "boolean" ? status.connected : void 0);
11676
+ setLastError(typeof status?.lastError === "string" ? status.lastError : void 0);
11677
+ }).catch(() => void 0);
11678
+ }).catch(() => {
11679
+ if (attempt === generation.current) setError(true);
11680
+ }).finally(() => {
11681
+ if (attempt === generation.current) setBusy(false);
11682
+ });
11683
+ };
11684
+ return (0, react.createElement)("div", {
11685
+ className: "dsh-mobile-settings_row",
11686
+ lang: locale
11687
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
11688
+ className: "dsh-mobile-settings_desc",
11689
+ "aria-live": "polite"
11690
+ }, error ? copy.error : statusText ?? copy.description)), (0, react.createElement)("button", {
11691
+ type: "button",
11692
+ className: "dsh-mobile-settings_selector",
11693
+ disabled: busy,
11694
+ "aria-busy": busy ? "true" : void 0,
11695
+ "aria-pressed": enabled ? "true" : "false",
11696
+ onClick: toggle
11697
+ }, busy ? copy.busy : enabled ? copy.enabled : copy.disabled));
11698
+ }
11415
11699
  const CONTROL_REQUEST_TIMEOUT_MS = 15e3;
11416
11700
  const LONG_CONTROL_REQUEST_TIMEOUT_MS = 21e4;
11417
11701
  const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
@@ -17252,7 +17536,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17252
17536
  kind: "done",
17253
17537
  title: label("Attività completata", "Task finished", "任务已完成"),
17254
17538
  body: label("Il tuo task DSH è terminato", "Your DSH task finished", "你的 DSH 任务已完成"),
17255
- tag: taskCompletionTag(parsed.sessionId, parsed.turn)
17539
+ tag: taskCompletionTag(parsed.sessionId, parsed.turn),
17540
+ navigate: taskNotificationNavigate(parsed.sessionId)
17256
17541
  });
17257
17542
  }, () => {
17258
17543
  fireDeviceRevoked();
@@ -17471,6 +17756,85 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17471
17756
  page.body.append(node);
17472
17757
  view.setTimeout(() => node.remove(), 3e3);
17473
17758
  }
17759
+ /** Consume the Android background service's opaque Session route on cold or warm launch. */
17760
+ function installTaskNotificationNavigation(ctx, page, view) {
17761
+ if (view.__DSH_MOBILE_FRONTEND__ !== "dedicated" || ctx.inject === void 0) return () => void 0;
17762
+ if (new URLSearchParams(view.location.search).get("dsh-session") === null) return () => void 0;
17763
+ let closed = false;
17764
+ let removeNavigation;
17765
+ const fiber = ctx.inject(mobilePushNavigationServices, (navigationContext) => {
17766
+ if (closed) return () => void 0;
17767
+ if (new URLSearchParams(view.location.search).get("dsh-session") === null) return () => void 0;
17768
+ const remove = installPushNavigation(navigationContext, {
17769
+ parameter: "dsh-session",
17770
+ resolve: async (value) => {
17771
+ const sessionId = decodeTaskNotificationNavigate(value);
17772
+ return sessionId === void 0 ? void 0 : {
17773
+ sessionId,
17774
+ interaction: "current"
17775
+ };
17776
+ },
17777
+ notify: (key) => {
17778
+ if (key === "session-unavailable" || key === "session-resolved") showMobilePushNavigationNotice(page, view, key);
17779
+ },
17780
+ browser: {
17781
+ location: view.location,
17782
+ history: view.history
17783
+ }
17784
+ });
17785
+ removeNavigation = remove;
17786
+ return () => {
17787
+ if (removeNavigation === remove) removeNavigation = void 0;
17788
+ remove();
17789
+ };
17790
+ });
17791
+ return () => {
17792
+ if (closed) return;
17793
+ closed = true;
17794
+ removeNavigation?.();
17795
+ removeNavigation = void 0;
17796
+ fiber.dispose();
17797
+ };
17798
+ }
17799
+ /**
17800
+ * Open a Session in the live page from an Android notification tap. The App calls this through
17801
+ * `window.__dshMobileOpenSession` instead of reloading the document; returning false asks the App
17802
+ * to fall back to a route navigation (for example while the page is still booting).
17803
+ */
17804
+ function installNativeNotificationNavigation(ctx, view) {
17805
+ if (view.__DSH_MOBILE_FRONTEND__ !== "dedicated" || ctx.inject === void 0) return () => void 0;
17806
+ let closed = false;
17807
+ const target = view;
17808
+ const fiber = ctx.inject(mobilePushNavigationServices, (navigationContext) => {
17809
+ if (closed) return () => void 0;
17810
+ const context = navigationContext;
17811
+ const open = (route) => {
17812
+ if (closed || typeof route !== "string") return false;
17813
+ const sessionId = sessionIdFromNotificationRoute(route);
17814
+ if (sessionId === void 0) return false;
17815
+ const workspace = context.uiWorkspace;
17816
+ if (workspace === void 0 || typeof workspace.openSession !== "function") return false;
17817
+ const sessions = context.sessions?.list.getSnapshot();
17818
+ if (sessions !== void 0 && sessions.phase === "ready" && !sessions.ids.includes(sessionId)) return false;
17819
+ try {
17820
+ workspace.openSession(sessionId);
17821
+ return true;
17822
+ } catch {
17823
+ return false;
17824
+ }
17825
+ };
17826
+ target.__dshMobileOpenSession = open;
17827
+ return () => {
17828
+ if (target.__dshMobileOpenSession === open) delete target.__dshMobileOpenSession;
17829
+ };
17830
+ });
17831
+ return () => {
17832
+ if (closed) return;
17833
+ closed = true;
17834
+ delete target.__dshMobileOpenSession;
17835
+ fiber.dispose();
17836
+ };
17837
+ }
17474
17838
  /** Own the browser-only push controller, settings row, and optional authenticated tap navigation. */
17475
17839
  function installMobilePushBrowserClient(ctx, runtime) {
17476
17840
  const { page, view } = runtime;
@@ -17544,6 +17908,19 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17544
17908
  controller.stop();
17545
17909
  };
17546
17910
  }
17911
+ /**
17912
+ * Register the session-prefetch opt-in on every dedicated Mobile page, including the Android
17913
+ * App webview. The row used to live inside the browser-push installer, which returns early when
17914
+ * the native bridge exists, so App users never saw the switch for a layout-side feature they use.
17915
+ */
17916
+ function installSessionPrefetchSettings(ctx, view) {
17917
+ if (view.__DSH_MOBILE_FRONTEND__ !== "dedicated") return () => void 0;
17918
+ return ctx.slots.inject("settings.general.item", () => ctx.slots.register({
17919
+ name: "settings.general.item",
17920
+ id: "dsh-mobile-session-prefetch",
17921
+ order: 130
17922
+ }, () => (0, react.createElement)(SessionPrefetchSettings, { locale: selectedMobileControlLocale() })));
17923
+ }
17547
17924
  /** Mount the desktop control or mobile feature enhancements. */
17548
17925
  function apply(ctx) {
17549
17926
  let gatewayRejectionSink;
@@ -17582,6 +17959,9 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17582
17959
  page: document,
17583
17960
  view: window
17584
17961
  }), "dsh-mobile: authenticated browser push lifecycle");
17962
+ ctx.effect(() => installTaskNotificationNavigation(ctx, document, window), "dsh-mobile: native task notification navigation");
17963
+ ctx.effect(() => installNativeNotificationNavigation(ctx, window), "dsh-mobile: in-page notification navigation");
17964
+ ctx.effect(() => installSessionPrefetchSettings(ctx, window), "dsh-mobile: session prefetch settings");
17585
17965
  ctx.effect(() => {
17586
17966
  const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
17587
17967
  const gatewayCustomization = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
@@ -17639,7 +18019,13 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17639
18019
  id: "dsh-mobile-task-notifications",
17640
18020
  order: 110
17641
18021
  }, MobileTaskNotificationRow);
18022
+ const removeBackgroundNotifications = ctx.slots.register({
18023
+ name: "settings.general.item",
18024
+ id: "dsh-mobile-background-notifications",
18025
+ order: 111
18026
+ }, MobileBackgroundNotificationRow);
17642
18027
  return () => {
18028
+ removeBackgroundNotifications();
17643
18029
  removeTaskNotifications();
17644
18030
  removeSwitchComputer();
17645
18031
  removeDisplayScale();
@@ -17752,8 +18138,12 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17752
18138
  exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
17753
18139
  exports.installMobileNativeBack = installMobileNativeBack;
17754
18140
  exports.installMobilePushBrowserClient = installMobilePushBrowserClient;
18141
+ exports.installNativeNotificationNavigation = installNativeNotificationNavigation;
18142
+ exports.installSessionPrefetchSettings = installSessionPrefetchSettings;
18143
+ exports.installTaskNotificationNavigation = installTaskNotificationNavigation;
17755
18144
  exports.localizedDiagnosticEntryCopy = localizedDiagnosticEntryCopy;
17756
18145
  exports.mobileBackBlockedMessage = mobileBackBlockedMessage;
18146
+ exports.mobileBackgroundStatusText = mobileBackgroundStatusText;
17757
18147
  exports.mobilePushNavigationNotice = mobilePushNavigationNotice;
17758
18148
  exports.normalizeDiagnosticOverall = normalizeDiagnosticOverall;
17759
18149
  exports.normalizeDiagnosticStatus = normalizeDiagnosticStatus;