dsh-mobile 0.8.6 → 0.8.8
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 +27 -2
- package/CONTRIBUTORS.md +12 -0
- package/README.en.md +20 -14
- package/README.md +17 -13
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/apps/mobile/README.md +21 -7
- package/apps/mobile/README.zh-CN.md +21 -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/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/WEB_PUSH.en.md +2 -2
- package/docs/WEB_PUSH.md +2 -2
- package/index.mjs +6 -0
- package/lib/client.js +359 -12
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +25 -3
- package/lib/index.mjs +319 -107
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +489 -3
- package/lib/mobile-layout.js.map +1 -1
- package/native/iroh-host/README.md +10 -4
- package/package.json +9 -4
- package/packages/question-fixes/package.json +1 -1
package/lib/mobile-layout.js
CHANGED
|
@@ -24,19 +24,438 @@ window.__ModuleLoader__.load({
|
|
|
24
24
|
it: Object.freeze({
|
|
25
25
|
closePanels: "Chiudi pannelli",
|
|
26
26
|
workspaceNavigation: "Navigazione area di lavoro e sessioni",
|
|
27
|
-
backToConversation: "Torna alla conversazione"
|
|
27
|
+
backToConversation: "Torna alla conversazione",
|
|
28
|
+
composerRestore: "Mostra il campo di scrittura"
|
|
28
29
|
}),
|
|
29
30
|
en: Object.freeze({
|
|
30
31
|
closePanels: "Close panels",
|
|
31
32
|
workspaceNavigation: "Workspace and session navigation",
|
|
32
|
-
backToConversation: "Back to conversation"
|
|
33
|
+
backToConversation: "Back to conversation",
|
|
34
|
+
composerRestore: "Show composer"
|
|
33
35
|
}),
|
|
34
36
|
zh: Object.freeze({
|
|
35
37
|
closePanels: "关闭浮层",
|
|
36
38
|
workspaceNavigation: "工作区与会话导航",
|
|
37
|
-
backToConversation: "返回会话"
|
|
39
|
+
backToConversation: "返回会话",
|
|
40
|
+
composerRestore: "展开输入框"
|
|
38
41
|
})
|
|
39
42
|
});
|
|
43
|
+
// src/composer-collapse.ts
|
|
44
|
+
const COMPOSER_COLLAPSE_DEFAULTS = {
|
|
45
|
+
enabled: false,
|
|
46
|
+
collapseThresholdPx: 480,
|
|
47
|
+
reverseResetPx: 80
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Track cumulative reading progress across scroll events. This is the only
|
|
51
|
+
* collapse policy: a decision table plus accumulated progress, kept out of the
|
|
52
|
+
* component so it stays testable without a DOM.
|
|
53
|
+
*/
|
|
54
|
+
var ComposerCollapseTracker = class {
|
|
55
|
+
readingProgress = 0;
|
|
56
|
+
reverseProgress = 0;
|
|
57
|
+
state = "expanded";
|
|
58
|
+
config;
|
|
59
|
+
constructor(config) {
|
|
60
|
+
this.config = config;
|
|
61
|
+
}
|
|
62
|
+
/** Whether the opt-in switch is currently on. */
|
|
63
|
+
get enabled() {
|
|
64
|
+
return this.config.enabled;
|
|
65
|
+
}
|
|
66
|
+
/** Update whether collapse is enabled (the user's settings toggle). */
|
|
67
|
+
setEnabled(enabled) {
|
|
68
|
+
this.config = {
|
|
69
|
+
...this.config,
|
|
70
|
+
enabled
|
|
71
|
+
};
|
|
72
|
+
if (!enabled) this.reset();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Feed one scroll sample; returns the resulting presentation state.
|
|
76
|
+
*
|
|
77
|
+
* reverseResetPx is a deliberate dead zone: a collapsed composer stays
|
|
78
|
+
* collapsed until the user scrolls back up at least that far, so a 1px jitter
|
|
79
|
+
* does not flip the layout on every frame.
|
|
80
|
+
*/
|
|
81
|
+
sample(deltaY) {
|
|
82
|
+
if (!this.config.enabled) {
|
|
83
|
+
this.reset();
|
|
84
|
+
return this.state;
|
|
85
|
+
}
|
|
86
|
+
if (this.state === "collapsed") {
|
|
87
|
+
if (deltaY > 0) {
|
|
88
|
+
this.reverseProgress = 0;
|
|
89
|
+
return this.state;
|
|
90
|
+
}
|
|
91
|
+
this.reverseProgress += -deltaY;
|
|
92
|
+
if (this.reverseProgress >= Math.max(this.config.reverseResetPx, 1)) this.reset();
|
|
93
|
+
return this.state;
|
|
94
|
+
}
|
|
95
|
+
if (deltaY < 0) {
|
|
96
|
+
this.readingProgress = Math.max(0, this.readingProgress + deltaY);
|
|
97
|
+
this.reverseProgress = 0;
|
|
98
|
+
return this.state;
|
|
99
|
+
}
|
|
100
|
+
this.readingProgress += deltaY;
|
|
101
|
+
this.reverseProgress = 0;
|
|
102
|
+
if (this.readingProgress >= this.config.collapseThresholdPx) this.state = "collapsed";
|
|
103
|
+
return this.state;
|
|
104
|
+
}
|
|
105
|
+
/** User interacted with the composer (focus, tap, keyboard, draft): expand. */
|
|
106
|
+
expand() {
|
|
107
|
+
this.reset();
|
|
108
|
+
}
|
|
109
|
+
get stateValue() {
|
|
110
|
+
return this.state;
|
|
111
|
+
}
|
|
112
|
+
/** @internal exposed for tests */
|
|
113
|
+
progress() {
|
|
114
|
+
return this.readingProgress;
|
|
115
|
+
}
|
|
116
|
+
reset() {
|
|
117
|
+
this.state = "expanded";
|
|
118
|
+
this.readingProgress = 0;
|
|
119
|
+
this.reverseProgress = 0;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const COMPOSER_COLLAPSE_EVENT = "dsh-mobile:composer-collapse-change";
|
|
123
|
+
/** Read the persisted opt-in. Defaults to off; unreadable storage keeps it off. */
|
|
124
|
+
function readComposerCollapseEnabled(storage) {
|
|
125
|
+
try {
|
|
126
|
+
const stored = (storage === void 0 ? globalThis.localStorage : storage)?.getItem("dsh-mobile.composer-collapse.enabled") ?? null;
|
|
127
|
+
if (stored !== null) return stored === "true";
|
|
128
|
+
return storage === void 0 ? false : false;
|
|
129
|
+
} catch {
|
|
130
|
+
return storage === void 0 ? false : false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Subscribe to opt-in changes coming from either bundle. The listener is not
|
|
135
|
+
* called with the initial value; read it once with readComposerCollapseEnabled().
|
|
136
|
+
*/
|
|
137
|
+
function subscribeComposerCollapseEnabled(listener) {
|
|
138
|
+
const onStorage = (event) => {
|
|
139
|
+
if (event.key !== null && event.key !== "dsh-mobile.composer-collapse.enabled") return;
|
|
140
|
+
listener(readComposerCollapseEnabled());
|
|
141
|
+
};
|
|
142
|
+
const onCustom = (event) => {
|
|
143
|
+
const detail = event.detail;
|
|
144
|
+
listener(detail?.enabled ?? readComposerCollapseEnabled());
|
|
145
|
+
};
|
|
146
|
+
globalThis.addEventListener?.("storage", onStorage);
|
|
147
|
+
globalThis.addEventListener?.(COMPOSER_COLLAPSE_EVENT, onCustom);
|
|
148
|
+
return () => {
|
|
149
|
+
globalThis.removeEventListener?.("storage", onStorage);
|
|
150
|
+
globalThis.removeEventListener?.(COMPOSER_COLLAPSE_EVENT, onCustom);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** Apply the persisted opt-in onto a base config. */
|
|
154
|
+
function composerCollapseConfigFrom(enabled, base = COMPOSER_COLLAPSE_DEFAULTS) {
|
|
155
|
+
return {
|
|
156
|
+
...base,
|
|
157
|
+
enabled
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 软键盘开合监听。
|
|
162
|
+
* 键盘弹起(visualViewport 缩小)意味着用户要输入,应展开收起态。
|
|
163
|
+
* 封装在这里而不是 layout,是为了让 layout 源码不含 visualViewport——
|
|
164
|
+
* 那里只用 window.innerWidth 判宽屏,否则软键盘缩小可视视口会把手机误判成宽屏。
|
|
165
|
+
*/
|
|
166
|
+
function onViewportResize(handler) {
|
|
167
|
+
const viewport = globalThis.visualViewport;
|
|
168
|
+
if (viewport === null || viewport === void 0) return () => {};
|
|
169
|
+
viewport.addEventListener("resize", handler);
|
|
170
|
+
return () => {
|
|
171
|
+
viewport.removeEventListener("resize", handler);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const COLLAPSED_ATTRIBUTE = "data-dsh-mobile-composer-collapsed";
|
|
175
|
+
const RESTORE_LABEL_PROPERTY = "--dshm-composer-restore-label";
|
|
176
|
+
const CONVERSATION_SCROLL_SELECTOR = "[data-conversation-scroll]";
|
|
177
|
+
const COMPOSER_CARD_SELECTOR$1 = "[data-composer-card]";
|
|
178
|
+
const COMPOSER_EDITOR_SELECTOR$1 = `${COMPOSER_CARD_SELECTOR$1} :is([contenteditable="true"],[contenteditable="plaintext-only"],[contenteditable=""],textarea)`;
|
|
179
|
+
/**
|
|
180
|
+
* Install the reading-mode collapse. Opt-in through the persisted switch, off by
|
|
181
|
+
* default; the composer and its draft stay owned by DSH, this only toggles a
|
|
182
|
+
* presentation attribute on the document element.
|
|
183
|
+
*/
|
|
184
|
+
function installComposerReadingMode(options) {
|
|
185
|
+
const doc = options.doc ?? globalThis.document;
|
|
186
|
+
const root = doc.documentElement;
|
|
187
|
+
const tracker = new ComposerCollapseTracker(composerCollapseConfigFrom(readComposerCollapseEnabled()));
|
|
188
|
+
let collapsed = false;
|
|
189
|
+
const view = doc.defaultView;
|
|
190
|
+
const apply = (next) => {
|
|
191
|
+
if (collapsed === next) return;
|
|
192
|
+
collapsed = next;
|
|
193
|
+
root.setAttribute(COLLAPSED_ATTRIBUTE, next ? "true" : "false");
|
|
194
|
+
};
|
|
195
|
+
const expand = () => {
|
|
196
|
+
tracker.expand();
|
|
197
|
+
apply(false);
|
|
198
|
+
};
|
|
199
|
+
root.style.setProperty(RESTORE_LABEL_PROPERTY, JSON.stringify(options.restoreLabel));
|
|
200
|
+
const scroller = () => {
|
|
201
|
+
const found = doc.querySelector(CONVERSATION_SCROLL_SELECTOR);
|
|
202
|
+
return found instanceof (view?.HTMLElement ?? Object) ? found : void 0;
|
|
203
|
+
};
|
|
204
|
+
let lastTop = scroller()?.scrollTop ?? 0;
|
|
205
|
+
let rafPending = false;
|
|
206
|
+
const sample = () => {
|
|
207
|
+
rafPending = false;
|
|
208
|
+
const port = scroller();
|
|
209
|
+
if (port === void 0) return;
|
|
210
|
+
const top = port.scrollTop;
|
|
211
|
+
const delta = top - lastTop;
|
|
212
|
+
lastTop = top;
|
|
213
|
+
if (busy()) {
|
|
214
|
+
expand();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
apply(tracker.sample(delta) === "collapsed");
|
|
218
|
+
};
|
|
219
|
+
const onScroll = (event) => {
|
|
220
|
+
const target = event.target;
|
|
221
|
+
if (target === null || !target.hasAttribute?.(CONVERSATION_SCROLL_SELECTOR.slice(1, -1))) return;
|
|
222
|
+
if (rafPending) return;
|
|
223
|
+
rafPending = true;
|
|
224
|
+
if (view === null) {
|
|
225
|
+
sample();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
view.requestAnimationFrame(sample);
|
|
229
|
+
};
|
|
230
|
+
const busy = () => {
|
|
231
|
+
if (options.isRunning()) return true;
|
|
232
|
+
const card = doc.querySelector(COMPOSER_CARD_SELECTOR$1);
|
|
233
|
+
const editor = card?.querySelector(COMPOSER_EDITOR_SELECTOR$1) ?? null;
|
|
234
|
+
if (editor === null) return false;
|
|
235
|
+
const phase = editor.dataset.phase ?? "";
|
|
236
|
+
if (phase !== "" && phase !== "plain") return true;
|
|
237
|
+
const focused = doc.activeElement;
|
|
238
|
+
if (focused !== null && card !== null && card.contains(focused)) return true;
|
|
239
|
+
return (editor.value ?? editor.textContent ?? "").trim() !== "";
|
|
240
|
+
};
|
|
241
|
+
const guard = () => {
|
|
242
|
+
if (busy()) expand();
|
|
243
|
+
};
|
|
244
|
+
const onPointerDown = (event) => {
|
|
245
|
+
if (!collapsed) return;
|
|
246
|
+
const target = event.target;
|
|
247
|
+
if (target === null || !(target instanceof (view?.Element ?? Object))) return;
|
|
248
|
+
if (target.closest(COMPOSER_CARD_SELECTOR$1) === null) return;
|
|
249
|
+
expand();
|
|
250
|
+
};
|
|
251
|
+
const onFocusIn = () => {
|
|
252
|
+
expand();
|
|
253
|
+
};
|
|
254
|
+
let observed = [];
|
|
255
|
+
const observeCards = () => {
|
|
256
|
+
const cards = [...doc.querySelectorAll(COMPOSER_CARD_SELECTOR$1)];
|
|
257
|
+
if (cards.length === observed.length && cards.every((card, index) => card === observed[index])) return;
|
|
258
|
+
observer.disconnect();
|
|
259
|
+
for (const card of cards) observer.observe(card, {
|
|
260
|
+
childList: true,
|
|
261
|
+
subtree: true,
|
|
262
|
+
characterData: true,
|
|
263
|
+
attributes: true,
|
|
264
|
+
attributeFilter: ["data-phase"]
|
|
265
|
+
});
|
|
266
|
+
observed = cards;
|
|
267
|
+
};
|
|
268
|
+
const observer = new ((view?.MutationObserver) ?? MutationObserver)(() => {
|
|
269
|
+
observeCards();
|
|
270
|
+
guard();
|
|
271
|
+
});
|
|
272
|
+
observeCards();
|
|
273
|
+
doc.addEventListener("scroll", onScroll, {
|
|
274
|
+
passive: true,
|
|
275
|
+
capture: true
|
|
276
|
+
});
|
|
277
|
+
doc.addEventListener("pointerdown", onPointerDown, true);
|
|
278
|
+
doc.addEventListener("focusin", onFocusIn, true);
|
|
279
|
+
const stopPreference = subscribeComposerCollapseEnabled((enabled) => {
|
|
280
|
+
tracker.setEnabled(enabled);
|
|
281
|
+
if (!enabled) expand();
|
|
282
|
+
});
|
|
283
|
+
const stopViewport = onViewportResize(onFocusIn);
|
|
284
|
+
return {
|
|
285
|
+
expand,
|
|
286
|
+
dispose() {
|
|
287
|
+
doc.removeEventListener("scroll", onScroll, { capture: true });
|
|
288
|
+
doc.removeEventListener("pointerdown", onPointerDown, true);
|
|
289
|
+
doc.removeEventListener("focusin", onFocusIn, true);
|
|
290
|
+
stopViewport();
|
|
291
|
+
stopPreference();
|
|
292
|
+
observer.disconnect();
|
|
293
|
+
root.style.removeProperty(RESTORE_LABEL_PROPERTY);
|
|
294
|
+
apply(false);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Whether the active session is streaming a reply. A running reply is not a
|
|
300
|
+
* composer phase: `data-phase` returns to `plain` as soon as the submission is
|
|
301
|
+
* accepted, so this flag is the only truthful signal for the streaming window.
|
|
302
|
+
*/
|
|
303
|
+
function selectActiveSessionRunning(state) {
|
|
304
|
+
const current = state.current;
|
|
305
|
+
return current !== void 0 && state.byId[current]?.running === true;
|
|
306
|
+
}
|
|
307
|
+
// src/session-prefetch.ts
|
|
308
|
+
const SESSION_PREFETCH_DEFAULTS = {
|
|
309
|
+
enabled: false,
|
|
310
|
+
settleMs: 1500,
|
|
311
|
+
cooldownMs: 6e4,
|
|
312
|
+
timeoutMs: 2e4,
|
|
313
|
+
maxTargetsPerSwitch: 1
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* Storage key for the opt-in switch.
|
|
317
|
+
*
|
|
318
|
+
* The setting row lives in the client bundle while the tracker lives in the
|
|
319
|
+
* layout bundle (tsdown.config.ts emits them separately and inlines this module
|
|
320
|
+
* into each), so a module-level singleton cannot carry the value across. The
|
|
321
|
+
* layout bundle owns the persisted state instead; the row notifies it through
|
|
322
|
+
* both the storage event (other tabs) and a custom event (same page).
|
|
323
|
+
*/
|
|
324
|
+
const SESSION_PREFETCH_STORAGE_KEY = "dsh-mobile.session-prefetch.enabled";
|
|
325
|
+
const SESSION_PREFETCH_EVENT = "dsh-mobile:session-prefetch-change";
|
|
326
|
+
/** Read the persisted opt-in. Defaults to off; unreadable storage keeps it off. */
|
|
327
|
+
function readSessionPrefetchEnabled(storage) {
|
|
328
|
+
try {
|
|
329
|
+
return (storage ?? globalThis.localStorage)?.getItem(SESSION_PREFETCH_STORAGE_KEY) === "true";
|
|
330
|
+
} catch {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Subscribe to opt-in changes coming from either bundle. The listener is not
|
|
336
|
+
* called with the initial value; read it once with readSessionPrefetchEnabled().
|
|
337
|
+
*/
|
|
338
|
+
function subscribeSessionPrefetchEnabled(listener) {
|
|
339
|
+
const onStorage = (event) => {
|
|
340
|
+
if (event.key !== null && event.key !== "dsh-mobile.session-prefetch.enabled") return;
|
|
341
|
+
listener(readSessionPrefetchEnabled());
|
|
342
|
+
};
|
|
343
|
+
const onCustom = (event) => {
|
|
344
|
+
const detail = event.detail;
|
|
345
|
+
listener(detail?.enabled ?? readSessionPrefetchEnabled());
|
|
346
|
+
};
|
|
347
|
+
globalThis.addEventListener?.("storage", onStorage);
|
|
348
|
+
globalThis.addEventListener?.(SESSION_PREFETCH_EVENT, onCustom);
|
|
349
|
+
return () => {
|
|
350
|
+
globalThis.removeEventListener?.("storage", onStorage);
|
|
351
|
+
globalThis.removeEventListener?.(SESSION_PREFETCH_EVENT, onCustom);
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/** Apply the persisted opt-in onto a base config. */
|
|
355
|
+
function sessionPrefetchConfigFrom(enabled, base = SESSION_PREFETCH_DEFAULTS) {
|
|
356
|
+
return {
|
|
357
|
+
...base,
|
|
358
|
+
enabled
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/** In-memory per-session cooldown ledger plus MRU switch tracking (pure). */
|
|
362
|
+
var SessionPrefetchTracker = class {
|
|
363
|
+
config;
|
|
364
|
+
clock;
|
|
365
|
+
cooldownUntil = /* @__PURE__ */ new Map();
|
|
366
|
+
previousSession;
|
|
367
|
+
lastSwitchAt = 0;
|
|
368
|
+
constructor(config, clock = { now: () => Date.now() }) {
|
|
369
|
+
this.config = config;
|
|
370
|
+
this.clock = clock;
|
|
371
|
+
}
|
|
372
|
+
/** The effective opt-in this tracker was built with (read-only, for rebuilds). */
|
|
373
|
+
get enabled() {
|
|
374
|
+
return this.config.enabled;
|
|
375
|
+
}
|
|
376
|
+
/** Record that the UI switched to `sessionId`; returns the switch outcome. */
|
|
377
|
+
switchTo(sessionId) {
|
|
378
|
+
if (sessionId === "") return {
|
|
379
|
+
action: "skip",
|
|
380
|
+
reason: "no-previous"
|
|
381
|
+
};
|
|
382
|
+
const previous = this.previousSession;
|
|
383
|
+
this.previousSession = sessionId;
|
|
384
|
+
this.lastSwitchAt = this.clock.now();
|
|
385
|
+
if (previous === void 0) return {
|
|
386
|
+
action: "skip",
|
|
387
|
+
reason: "no-previous"
|
|
388
|
+
};
|
|
389
|
+
if (previous === sessionId) return {
|
|
390
|
+
action: "skip",
|
|
391
|
+
reason: "same-session"
|
|
392
|
+
};
|
|
393
|
+
if (!this.config.enabled) return {
|
|
394
|
+
action: "skip",
|
|
395
|
+
reason: "disabled"
|
|
396
|
+
};
|
|
397
|
+
const cooldownUntil = this.cooldownUntil.get(previous) ?? 0;
|
|
398
|
+
if (this.clock.now() < cooldownUntil) return {
|
|
399
|
+
action: "skip",
|
|
400
|
+
reason: "cooldown"
|
|
401
|
+
};
|
|
402
|
+
this.cooldownUntil.set(previous, this.clock.now() + this.config.cooldownMs);
|
|
403
|
+
return {
|
|
404
|
+
action: "warm",
|
|
405
|
+
target: previous
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/** Whether the switch already passed the settle window (pure predicate). */
|
|
409
|
+
settled(now = this.clock.now()) {
|
|
410
|
+
return now - this.lastSwitchAt >= this.config.settleMs;
|
|
411
|
+
}
|
|
412
|
+
/** Forget state (used when the client unmounts). */
|
|
413
|
+
reset() {
|
|
414
|
+
this.cooldownUntil.clear();
|
|
415
|
+
this.previousSession = void 0;
|
|
416
|
+
this.lastSwitchAt = 0;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* Fire a read-only `session/projections` prefetch, silently ignoring failures.
|
|
421
|
+
*
|
|
422
|
+
* The upstream RPC takes a single `request` parameter and the gateway validates
|
|
423
|
+
* wire names exactly (`assertExactArguments`), so the envelope must be
|
|
424
|
+
* `payload.args.request.sessionId` — the same shape every other call site in
|
|
425
|
+
* this repo uses (scripts/smoke-client-module-settings.mjs, smoke-draft-transfer.mjs).
|
|
426
|
+
* A bare `args.sessionId` is rejected with `gateway/arguments-invalid`.
|
|
427
|
+
*/
|
|
428
|
+
function prefetchSession(request, sessionId, timeoutMs) {
|
|
429
|
+
const controller = new AbortController();
|
|
430
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
431
|
+
(async () => {
|
|
432
|
+
try {
|
|
433
|
+
if (!(await request("/api/session/projections", {
|
|
434
|
+
method: "POST",
|
|
435
|
+
headers: { "content-type": "application/json" },
|
|
436
|
+
body: JSON.stringify({
|
|
437
|
+
type: "client-request",
|
|
438
|
+
rpcId: prefetchRpcId(),
|
|
439
|
+
method: "session/projections",
|
|
440
|
+
payload: { args: { request: { sessionId } } }
|
|
441
|
+
}),
|
|
442
|
+
signal: controller.signal
|
|
443
|
+
})).ok) return;
|
|
444
|
+
} catch {} finally {
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
}
|
|
447
|
+
})();
|
|
448
|
+
return () => {
|
|
449
|
+
controller.abort();
|
|
450
|
+
clearTimeout(timer);
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/** Unique per attempt; the upstream does not document a format, so prefer a real UUID. */
|
|
454
|
+
function prefetchRpcId() {
|
|
455
|
+
const cryptoApi = globalThis.crypto;
|
|
456
|
+
if (cryptoApi?.randomUUID !== void 0) return cryptoApi.randomUUID();
|
|
457
|
+
return `dsh-mobile-prefetch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
458
|
+
}
|
|
40
459
|
// src/mobile-layout.ts
|
|
41
460
|
/** Whether an optional service value supports the public right-Sidebar controls. */
|
|
42
461
|
function isSidebarRightControl(value) {
|
|
@@ -587,6 +1006,17 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
|
|
|
587
1006
|
.dshm-main[data-dsh-panel="1"] [data-slot="main"]>:not([data-dsh-adapt="off"]) :is(button,[role="button"],select,input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=hidden]):not([type=color]):not([type=file]):not([type=image])):not([role="switch"],[role="checkbox"],[role="radio"],[role="slider"],button[class*="_cardTitle"][class*="_cardOpen"],[data-dsh-adapt="off"],[data-dsh-adapt="off"] *){min-width:44px!important;min-height:44px!important}
|
|
588
1007
|
.dshm-main[data-dsh-panel="1"] [data-slot="main"]>:not([data-dsh-adapt="off"]) :is(button,[role="button"],a):not([data-dsh-adapt="off"],[data-dsh-adapt="off"] *){touch-action:manipulation}
|
|
589
1008
|
}
|
|
1009
|
+
/* Reading mode (issue #194): after scrolling down a long conversation the
|
|
1010
|
+
composer editor collapses so the reading area is unobstructed. The card stays
|
|
1011
|
+
and keeps the upstream Send/Stop row; its generated label is the way back, and
|
|
1012
|
+
the text comes from --dshm-composer-restore-label so nothing is injected into
|
|
1013
|
+
the React-managed card. Opt-in, off by default. */
|
|
1014
|
+
[data-dsh-mobile-composer-collapsed="true"] [data-composer-card] :is([contenteditable="true"],[contenteditable="plaintext-only"],[contenteditable=""],textarea){display:none!important}
|
|
1015
|
+
[data-dsh-mobile-composer-collapsed="true"] [data-composer-card]{
|
|
1016
|
+
padding-top:6px!important;padding-bottom:6px!important;min-height:0!important;cursor:pointer}
|
|
1017
|
+
[data-dsh-mobile-composer-collapsed="true"] [data-composer-card]::before{
|
|
1018
|
+
content:var(--dshm-composer-restore-label, "");display:flex;align-items:center;
|
|
1019
|
+
justify-content:center;min-height:44px;font-size:13px;opacity:.75;pointer-events:none}
|
|
590
1020
|
`;
|
|
591
1021
|
function MobileAppFrame(props) {
|
|
592
1022
|
const state = (0, react.useSyncExternalStore)(props.controller.subscribe, props.controller.getSnapshot);
|
|
@@ -642,6 +1072,59 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
|
|
|
642
1072
|
return current !== void 0 && session.byId[current]?.blank === false ? current : void 0;
|
|
643
1073
|
});
|
|
644
1074
|
const hasSession = activeSessionId !== void 0;
|
|
1075
|
+
const prefetchEnabled = (0, react.useSyncExternalStore)(subscribeSessionPrefetchEnabled, readSessionPrefetchEnabled, () => false);
|
|
1076
|
+
const prefetchTracker = (0, react.useRef)(void 0);
|
|
1077
|
+
const prefetchTimer = (0, react.useRef)(void 0);
|
|
1078
|
+
const prefetchDispose = (0, react.useRef)(void 0);
|
|
1079
|
+
if (prefetchTracker.current === void 0 || prefetchTracker.current.enabled !== prefetchEnabled) prefetchTracker.current = new SessionPrefetchTracker(sessionPrefetchConfigFrom(prefetchEnabled));
|
|
1080
|
+
(0, react.useEffect)(() => {
|
|
1081
|
+
if (activeSessionId === void 0) return;
|
|
1082
|
+
const decision = prefetchTracker.current.switchTo(activeSessionId);
|
|
1083
|
+
if (decision.action !== "warm") return;
|
|
1084
|
+
const target = decision.target;
|
|
1085
|
+
if (prefetchTimer.current !== void 0) clearTimeout(prefetchTimer.current);
|
|
1086
|
+
prefetchTimer.current = window.setTimeout(() => {
|
|
1087
|
+
prefetchTimer.current = void 0;
|
|
1088
|
+
prefetchDispose.current?.();
|
|
1089
|
+
prefetchDispose.current = prefetchSession(fetch, target, SESSION_PREFETCH_DEFAULTS.timeoutMs);
|
|
1090
|
+
}, SESSION_PREFETCH_DEFAULTS.settleMs);
|
|
1091
|
+
return () => {
|
|
1092
|
+
if (prefetchTimer.current !== void 0) {
|
|
1093
|
+
clearTimeout(prefetchTimer.current);
|
|
1094
|
+
prefetchTimer.current = void 0;
|
|
1095
|
+
}
|
|
1096
|
+
prefetchDispose.current?.();
|
|
1097
|
+
prefetchDispose.current = void 0;
|
|
1098
|
+
};
|
|
1099
|
+
}, [activeSessionId, prefetchEnabled]);
|
|
1100
|
+
const collapseEnabled = (0, react.useSyncExternalStore)(subscribeComposerCollapseEnabled, readComposerCollapseEnabled, () => false);
|
|
1101
|
+
const activeRunning = props.useSessions(selectActiveSessionRunning);
|
|
1102
|
+
const runningRef = (0, react.useRef)(activeRunning);
|
|
1103
|
+
runningRef.current = activeRunning;
|
|
1104
|
+
const active = collapseEnabled && hasSession;
|
|
1105
|
+
const readingMode = (0, react.useRef)(void 0);
|
|
1106
|
+
(0, react.useEffect)(() => {
|
|
1107
|
+
if (!active) return;
|
|
1108
|
+
const installed = installComposerReadingMode({
|
|
1109
|
+
restoreLabel: messages.composerRestore,
|
|
1110
|
+
isRunning: () => runningRef.current
|
|
1111
|
+
});
|
|
1112
|
+
readingMode.current = installed;
|
|
1113
|
+
if (runningRef.current) installed.expand();
|
|
1114
|
+
return () => {
|
|
1115
|
+
readingMode.current = void 0;
|
|
1116
|
+
installed.dispose();
|
|
1117
|
+
};
|
|
1118
|
+
}, [
|
|
1119
|
+
active,
|
|
1120
|
+
activeSessionId,
|
|
1121
|
+
messages.composerRestore
|
|
1122
|
+
]);
|
|
1123
|
+
(0, react.useEffect)(() => {
|
|
1124
|
+
if (!activeRunning) return;
|
|
1125
|
+
readingMode.current?.expand();
|
|
1126
|
+
document.documentElement.setAttribute("data-dsh-mobile-composer-collapsed", "false");
|
|
1127
|
+
}, [activeRunning]);
|
|
645
1128
|
(0, react.useEffect)(() => {
|
|
646
1129
|
const observer = new MutationObserver(() => {
|
|
647
1130
|
setDocumentLanguage(document.documentElement.lang);
|
|
@@ -925,6 +1408,7 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
|
|
|
925
1408
|
exports.apply = apply;
|
|
926
1409
|
exports.closeDetailsFromScrim = closeDetailsFromScrim;
|
|
927
1410
|
exports.inject = inject;
|
|
1411
|
+
exports.installComposerReadingMode = installComposerReadingMode;
|
|
928
1412
|
exports.installComposerSendKeyboardPolicy = installComposerSendKeyboardPolicy;
|
|
929
1413
|
exports.isComposerActionTap = isComposerActionTap;
|
|
930
1414
|
exports.isComposerOwnedFocus = isComposerOwnedFocus;
|
|
@@ -932,10 +1416,12 @@ body:has(.dshm-shell) :is(input,textarea,[contenteditable=true],[contenteditable
|
|
|
932
1416
|
exports.isSessionRowNavigation = isSessionRowNavigation;
|
|
933
1417
|
exports.isSidebarRightControl = isSidebarRightControl;
|
|
934
1418
|
exports.isWideViewportLayout = isWideViewportLayout;
|
|
1419
|
+
exports.readComposerCollapseEnabled = readComposerCollapseEnabled;
|
|
935
1420
|
exports.resolveComposerImePolicy = resolveComposerImePolicy;
|
|
936
1421
|
exports.resolveDeviceShortSide = resolveDeviceShortSide;
|
|
937
1422
|
exports.resolveMobileLayoutLanguage = resolveMobileLayoutLanguage;
|
|
938
1423
|
exports.resolveMobileRightbarLayout = resolveMobileRightbarLayout;
|
|
1424
|
+
exports.selectActiveSessionRunning = selectActiveSessionRunning;
|
|
939
1425
|
return module.exports;
|
|
940
1426
|
}
|
|
941
1427
|
});
|