dsh-mobile 0.8.5 → 0.8.6

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,6 +3791,28 @@ 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
+ /** 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
+ }
3794
3816
  /** Parse a `task-notify` SSE payload; rejects anything malformed or empty. */
3795
3817
  function parseTaskNotifyPayload(value) {
3796
3818
  let record;
@@ -3845,7 +3867,8 @@ window.__ModuleLoader__.load({
3845
3867
  return bridge.invoke("notification.notify", {
3846
3868
  title: event.title,
3847
3869
  body: event.body,
3848
- tag: event.tag
3870
+ tag: event.tag,
3871
+ ...event.navigate === void 0 ? {} : { navigate: event.navigate }
3849
3872
  });
3850
3873
  }).then(() => void 0, (error) => {
3851
3874
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
@@ -9582,10 +9605,7 @@ window.__ModuleLoader__.load({
9582
9605
  });
9583
9606
  if (currentHostSubscription) consentedSubscriptions.delete(currentHostSubscription);
9584
9607
  currentHostSubscription = void 0;
9585
- const sub = ownedSubscription;
9586
- if (sub) {
9587
- if (await sub.unsubscribe().catch(() => false) && ownedSubscription === sub) ownedSubscription = void 0;
9588
- }
9608
+ ownedSubscription = void 0;
9589
9609
  if (await refresh(token) && current(token) && operation === attempt && config?.status === "disabled") publish({
9590
9610
  state: "disabled",
9591
9611
  preferences: config.preferences
@@ -9746,7 +9766,13 @@ window.__ModuleLoader__.load({
9746
9766
  const MOBILE_PUSH_SETTINGS_COPY = {
9747
9767
  en: {
9748
9768
  title: "Web push notifications",
9749
- desc: "Receive notifications when this page is closed.",
9769
+ desc: "Receive notifications when this page is closed. Disable stops sends but keeps the browser subscription so it can be reused safely.",
9770
+ permission: {
9771
+ granted: "System permission: granted. A reinstalled Home Screen app can inherit this site-level grant.",
9772
+ default: "System permission: not decided.",
9773
+ denied: "System permission: blocked.",
9774
+ unknown: "System permission: unavailable."
9775
+ },
9750
9776
  enable: "Enable",
9751
9777
  disable: "Disable",
9752
9778
  test: "Send test",
@@ -9771,7 +9797,13 @@ window.__ModuleLoader__.load({
9771
9797
  },
9772
9798
  zh: {
9773
9799
  title: "网页推送通知",
9774
- desc: "关闭此页面后仍可接收通知。",
9800
+ desc: "关闭此页面后仍可接收通知。停用会停止发送,但保留浏览器订阅,便于安全地重新启用。",
9801
+ permission: {
9802
+ granted: "系统权限:已允许。重新安装的主屏幕 App 可能继承站点级授权。",
9803
+ default: "系统权限:尚未选择。",
9804
+ denied: "系统权限:已阻止。",
9805
+ unknown: "系统权限:不可读取。"
9806
+ },
9775
9807
  enable: "启用",
9776
9808
  disable: "停用",
9777
9809
  test: "发送测试",
@@ -9796,7 +9828,13 @@ window.__ModuleLoader__.load({
9796
9828
  },
9797
9829
  it: {
9798
9830
  title: "Notifiche Web push",
9799
- desc: "Ricevi notifiche quando la pagina è chiusa.",
9831
+ desc: "Ricevi notifiche quando la pagina è chiusa. La disattivazione interrompe gli invii ma conserva l’iscrizione del browser per poterla riutilizzare.",
9832
+ permission: {
9833
+ granted: "Permesso di sistema: concesso. Una Web App reinstallata può ereditare questa autorizzazione del sito.",
9834
+ default: "Permesso di sistema: non ancora scelto.",
9835
+ denied: "Permesso di sistema: bloccato.",
9836
+ unknown: "Permesso di sistema: non disponibile."
9837
+ },
9800
9838
  enable: "Attiva",
9801
9839
  disable: "Disattiva",
9802
9840
  test: "Invia test",
@@ -9845,6 +9883,7 @@ window.__ModuleLoader__.load({
9845
9883
  if (snapshot.state === "unsupported") return null;
9846
9884
  const prefs = snapshot.preferences;
9847
9885
  const Switch = switchComponent ?? requirePublicMobilePushSwitch();
9886
+ const permission = typeof Notification === "undefined" ? "unknown" : Notification.permission;
9848
9887
  const toggle = (key, checked) => {
9849
9888
  if (!prefs) return;
9850
9889
  controller.updatePreferences({
@@ -9866,7 +9905,7 @@ window.__ModuleLoader__.load({
9866
9905
  }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
9867
9906
  className: "dsh-mobile-settings_desc",
9868
9907
  "aria-live": "polite"
9869
- }, message(snapshot, copy)), prefs && Switch !== void 0 ? (0, react.createElement)("div", { className: "dsh-mobile-push-categories" }, ...KEYS.map((key, index) => {
9908
+ }, 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
9909
  const name = copy.categories[index];
9871
9910
  if (name === void 0) return null;
9872
9911
  return (0, react.createElement)("label", {
@@ -9907,7 +9946,6 @@ window.__ModuleLoader__.load({
9907
9946
  }, copy.busy)));
9908
9947
  }
9909
9948
  // src/mobile-push/navigation.ts
9910
- const REF_PARAMETER = "mobile-push";
9911
9949
  /**
9912
9950
  * Consume one authenticated tap reference after the public Workspace and Session
9913
9951
  * list baselines are ready. Return the disposal function for list subscriptions.
@@ -9918,15 +9956,16 @@ window.__ModuleLoader__.load({
9918
9956
  location: window.location,
9919
9957
  history: window.history
9920
9958
  };
9959
+ const refParameter = options.parameter ?? "mobile-push";
9921
9960
  const params = new URLSearchParams(browser.location.search);
9922
- const reference = params.get(REF_PARAMETER);
9961
+ const reference = params.get(refParameter);
9923
9962
  let disposed = false;
9924
9963
  let consumed = false;
9925
9964
  let resolving = false;
9926
9965
  let opened = false;
9927
9966
  const clearReference = () => {
9928
- if (!params.has(REF_PARAMETER)) return;
9929
- params.delete(REF_PARAMETER);
9967
+ if (!params.has(refParameter)) return;
9968
+ params.delete(refParameter);
9930
9969
  const query = params.toString();
9931
9970
  const url = `${browser.location.pathname}${query ? `?${query}` : ""}${browser.location.hash}`;
9932
9971
  browser.history.replaceState(null, "", url);
@@ -11412,6 +11451,111 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
11412
11451
  onClick: invoke
11413
11452
  }, state === "busy" ? copy.busy : state === "success" ? copy.reopen : state === "error" ? copy.retry : copy.action));
11414
11453
  }
11454
+ function mobileBackgroundNotificationCopy(locale) {
11455
+ if (locale === "zh") return {
11456
+ title: "后台任务提醒",
11457
+ description: "App 页面被系统挂起后,通过可关闭的常驻连接继续接收任务完成提醒。",
11458
+ enabled: "关闭后台提醒",
11459
+ disabled: "开启后台提醒",
11460
+ busy: "正在处理…",
11461
+ error: "无法更新后台提醒;请确认任务通知权限和当前连接。"
11462
+ };
11463
+ if (locale === "it") return {
11464
+ title: "Promemoria in background",
11465
+ description: "Continua a ricevere gli avvisi quando Android sospende la pagina, tramite una connessione persistente disattivabile.",
11466
+ enabled: "Disattiva promemoria",
11467
+ disabled: "Attiva promemoria",
11468
+ busy: "Operazione…",
11469
+ error: "Impossibile aggiornare i promemoria; controlla il permesso notifiche e la connessione attiva."
11470
+ };
11471
+ return {
11472
+ title: "Background task reminders",
11473
+ description: "Keep receiving completion alerts after Android suspends the page, through a dismissible persistent connection.",
11474
+ enabled: "Disable background reminders",
11475
+ disabled: "Enable background reminders",
11476
+ busy: "Working…",
11477
+ error: "Could not update background reminders. Check notification permission and the active connection."
11478
+ };
11479
+ }
11480
+ /** Android-only opt-in for the foreground SSE fallback when the WebView is suspended. */
11481
+ function MobileBackgroundNotificationRow() {
11482
+ const [supported, setSupported] = (0, react.useState)(false);
11483
+ const [enabled, setEnabled] = (0, react.useState)(false);
11484
+ const [busy, setBusy] = (0, react.useState)(false);
11485
+ const [error, setError] = (0, react.useState)(false);
11486
+ const generation = (0, react.useRef)(0);
11487
+ (0, react.useEffect)(() => {
11488
+ let active = true;
11489
+ const refresh = () => {
11490
+ const attempt = ++generation.current;
11491
+ const bridge = window.__DSH_MOBILE_NATIVE__;
11492
+ if (bridge === void 0) {
11493
+ if (active) {
11494
+ setSupported(false);
11495
+ setError(false);
11496
+ }
11497
+ return;
11498
+ }
11499
+ Promise.resolve(bridge.capabilities()).then(async (capabilities) => {
11500
+ const available = capabilities.includes("notification.background.get") && capabilities.includes("notification.background.set");
11501
+ if (!active || attempt !== generation.current) return;
11502
+ setSupported(available);
11503
+ if (!available) return;
11504
+ const result = await bridge.invoke("notification.background.get", {});
11505
+ if (!active || attempt !== generation.current) return;
11506
+ if (typeof result?.enabled !== "boolean") throw new Error("invalid background notification status");
11507
+ setEnabled(result.enabled);
11508
+ setError(false);
11509
+ }).catch(() => {
11510
+ if (active && attempt === generation.current) {
11511
+ setSupported(true);
11512
+ setError(true);
11513
+ }
11514
+ });
11515
+ };
11516
+ refresh();
11517
+ window.addEventListener("dsh-mobile-native-ready", refresh);
11518
+ return () => {
11519
+ active = false;
11520
+ generation.current += 1;
11521
+ window.removeEventListener("dsh-mobile-native-ready", refresh);
11522
+ };
11523
+ }, []);
11524
+ if (!supported) return null;
11525
+ const locale = selectedMobileControlLocale();
11526
+ const copy = mobileBackgroundNotificationCopy(locale);
11527
+ const toggle = () => {
11528
+ const bridge = window.__DSH_MOBILE_NATIVE__;
11529
+ if (bridge === void 0 || busy) return;
11530
+ const attempt = ++generation.current;
11531
+ setBusy(true);
11532
+ setError(false);
11533
+ bridge.invoke("notification.background.set", { enabled: !enabled }).then((result) => {
11534
+ if (attempt !== generation.current) return;
11535
+ const next = result?.enabled;
11536
+ if (typeof next !== "boolean") throw new Error("invalid background notification status");
11537
+ setEnabled(next);
11538
+ }).catch(() => {
11539
+ if (attempt === generation.current) setError(true);
11540
+ }).finally(() => {
11541
+ if (attempt === generation.current) setBusy(false);
11542
+ });
11543
+ };
11544
+ return (0, react.createElement)("div", {
11545
+ className: "dsh-mobile-settings_row",
11546
+ lang: locale
11547
+ }, (0, react.createElement)("div", { className: "dsh-mobile-settings_rowText" }, (0, react.createElement)("div", { className: "dsh-mobile-settings_title" }, copy.title), (0, react.createElement)("div", {
11548
+ className: "dsh-mobile-settings_desc",
11549
+ "aria-live": "polite"
11550
+ }, error ? copy.error : copy.description)), (0, react.createElement)("button", {
11551
+ type: "button",
11552
+ className: "dsh-mobile-settings_selector",
11553
+ disabled: busy,
11554
+ "aria-busy": busy ? "true" : void 0,
11555
+ "aria-pressed": enabled ? "true" : "false",
11556
+ onClick: toggle
11557
+ }, busy ? copy.busy : enabled ? copy.enabled : copy.disabled));
11558
+ }
11415
11559
  const CONTROL_REQUEST_TIMEOUT_MS = 15e3;
11416
11560
  const LONG_CONTROL_REQUEST_TIMEOUT_MS = 21e4;
11417
11561
  const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
@@ -17252,7 +17396,8 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17252
17396
  kind: "done",
17253
17397
  title: label("Attività completata", "Task finished", "任务已完成"),
17254
17398
  body: label("Il tuo task DSH è terminato", "Your DSH task finished", "你的 DSH 任务已完成"),
17255
- tag: taskCompletionTag(parsed.sessionId, parsed.turn)
17399
+ tag: taskCompletionTag(parsed.sessionId, parsed.turn),
17400
+ navigate: taskNotificationNavigate(parsed.sessionId)
17256
17401
  });
17257
17402
  }, () => {
17258
17403
  fireDeviceRevoked();
@@ -17471,6 +17616,46 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17471
17616
  page.body.append(node);
17472
17617
  view.setTimeout(() => node.remove(), 3e3);
17473
17618
  }
17619
+ /** Consume the Android background service's opaque Session route on cold or warm launch. */
17620
+ function installTaskNotificationNavigation(ctx, page, view) {
17621
+ if (view.__DSH_MOBILE_FRONTEND__ !== "dedicated" || ctx.inject === void 0) return () => void 0;
17622
+ if (new URLSearchParams(view.location.search).get("dsh-session") === null) return () => void 0;
17623
+ let closed = false;
17624
+ let removeNavigation;
17625
+ const fiber = ctx.inject(mobilePushNavigationServices, (navigationContext) => {
17626
+ if (closed) return () => void 0;
17627
+ if (new URLSearchParams(view.location.search).get("dsh-session") === null) return () => void 0;
17628
+ const remove = installPushNavigation(navigationContext, {
17629
+ parameter: "dsh-session",
17630
+ resolve: async (value) => {
17631
+ const sessionId = decodeTaskNotificationNavigate(value);
17632
+ return sessionId === void 0 ? void 0 : {
17633
+ sessionId,
17634
+ interaction: "current"
17635
+ };
17636
+ },
17637
+ notify: (key) => {
17638
+ if (key === "session-unavailable" || key === "session-resolved") showMobilePushNavigationNotice(page, view, key);
17639
+ },
17640
+ browser: {
17641
+ location: view.location,
17642
+ history: view.history
17643
+ }
17644
+ });
17645
+ removeNavigation = remove;
17646
+ return () => {
17647
+ if (removeNavigation === remove) removeNavigation = void 0;
17648
+ remove();
17649
+ };
17650
+ });
17651
+ return () => {
17652
+ if (closed) return;
17653
+ closed = true;
17654
+ removeNavigation?.();
17655
+ removeNavigation = void 0;
17656
+ fiber.dispose();
17657
+ };
17658
+ }
17474
17659
  /** Own the browser-only push controller, settings row, and optional authenticated tap navigation. */
17475
17660
  function installMobilePushBrowserClient(ctx, runtime) {
17476
17661
  const { page, view } = runtime;
@@ -17582,6 +17767,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17582
17767
  page: document,
17583
17768
  view: window
17584
17769
  }), "dsh-mobile: authenticated browser push lifecycle");
17770
+ ctx.effect(() => installTaskNotificationNavigation(ctx, document, window), "dsh-mobile: native task notification navigation");
17585
17771
  ctx.effect(() => {
17586
17772
  const desktopAdmin = isDesktopAdminSurface(location.hostname, location.search, window.__DSH_MOBILE_FRONTEND__, location.protocol);
17587
17773
  const gatewayCustomization = ownsMobileCustomization(document.querySelector("meta[name=\"dsh-mobile-gateway\"][content=\"authenticated\"]") !== null, window.__DSH_MOBILE_FRONTEND__);
@@ -17639,7 +17825,13 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17639
17825
  id: "dsh-mobile-task-notifications",
17640
17826
  order: 110
17641
17827
  }, MobileTaskNotificationRow);
17828
+ const removeBackgroundNotifications = ctx.slots.register({
17829
+ name: "settings.general.item",
17830
+ id: "dsh-mobile-background-notifications",
17831
+ order: 111
17832
+ }, MobileBackgroundNotificationRow);
17642
17833
  return () => {
17834
+ removeBackgroundNotifications();
17643
17835
  removeTaskNotifications();
17644
17836
  removeSwitchComputer();
17645
17837
  removeDisplayScale();
@@ -17752,6 +17944,7 @@ dialog.dshm-picker[open]{display:grid;place-items:center}dialog.dshm-picker::bac
17752
17944
  exports.installDshLanguageBoundSurface = installDshLanguageBoundSurface;
17753
17945
  exports.installMobileNativeBack = installMobileNativeBack;
17754
17946
  exports.installMobilePushBrowserClient = installMobilePushBrowserClient;
17947
+ exports.installTaskNotificationNavigation = installTaskNotificationNavigation;
17755
17948
  exports.localizedDiagnosticEntryCopy = localizedDiagnosticEntryCopy;
17756
17949
  exports.mobileBackBlockedMessage = mobileBackBlockedMessage;
17757
17950
  exports.mobilePushNavigationNotice = mobilePushNavigationNotice;