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/CHANGELOG.md +37 -1
- package/CONTRIBUTORS.md +16 -4
- package/README.en.md +19 -13
- package/README.md +15 -11
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/apps/mobile/README.md +15 -7
- package/apps/mobile/README.zh-CN.md +15 -7
- package/apps/mobile/release.json +3 -3
- package/assets/brand/plugin-icon.png +0 -0
- package/docs/ATTACH_EXISTING_FRPS.en.md +2 -2
- package/docs/CLIENT_MODULES.en.md +1 -1
- package/docs/CLIENT_MODULES.md +1 -1
- package/docs/CPOLAR_SUBDOMAIN.en.md +1 -1
- package/docs/CPOLAR_SUBDOMAIN.md +1 -1
- package/docs/IROH.en.md +5 -5
- package/docs/IROH.md +6 -6
- package/docs/MOBILE_COMPOSER_TOOLBAR.en.md +2 -2
- package/docs/MOBILE_COMPOSER_TOOLBAR.md +2 -2
- package/docs/MOBILE_PLUGIN_ADAPTATION.en.md +9 -0
- package/docs/MOBILE_PLUGIN_ADAPTATION.md +9 -0
- package/docs/MOBILE_PLUGIN_BASELINE.en.md +1 -1
- package/docs/MOBILE_PLUGIN_BASELINE.md +1 -1
- package/docs/README.md +1 -1
- package/docs/WEB_PUSH.en.md +21 -5
- package/docs/WEB_PUSH.md +21 -5
- package/index.mjs +6 -0
- package/lib/cli.js +104 -9
- package/lib/cli.js.map +1 -1
- package/lib/client.js +406 -16
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +40 -1
- package/lib/index.mjs +404 -35
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +177 -0
- package/lib/mobile-layout.js.map +1 -1
- package/native/iroh-host/README.md +10 -4
- package/package.json +8 -4
- package/packages/question-fixes/package.json +1 -1
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
|
-
/**
|
|
3795
|
-
function
|
|
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
|
-
|
|
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(
|
|
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(
|
|
9929
|
-
params.delete(
|
|
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;
|