dsh-mobile 0.6.10 → 0.6.12
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 +25 -0
- package/CONTRIBUTORS.md +24 -6
- package/README.en.md +22 -19
- package/README.md +22 -19
- package/SECURITY.md +9 -0
- package/apps/mobile/README.md +71 -90
- package/apps/mobile/README.zh-CN.md +131 -0
- package/apps/mobile/release.json +3 -3
- package/docs/ATTACH_EXISTING_FRPS.en.md +1 -1
- package/docs/ATTACH_EXISTING_FRPS.md +1 -1
- package/docs/CADDY_MANAGED.en.md +7 -7
- package/docs/CADDY_MANAGED.md +7 -7
- package/docs/CLIENT_MODULES.en.md +2 -0
- package/docs/CLIENT_MODULES.md +2 -0
- package/docs/CLOUDFLARE_TUNNEL.en.md +1 -1
- package/docs/CLOUDFLARE_TUNNEL.md +1 -1
- package/docs/CPOLAR_SUBDOMAIN.en.md +1 -1
- package/docs/CPOLAR_SUBDOMAIN.md +1 -1
- package/docs/DSH_0.1.5_LAN.md +1 -1
- package/docs/EXTENSION_WORKERS.en.md +2 -2
- package/docs/EXTENSION_WORKERS.md +2 -2
- package/docs/MOBILE_COMPOSER_TOOLBAR.en.md +10 -4
- package/docs/MOBILE_COMPOSER_TOOLBAR.md +10 -4
- package/docs/README.md +4 -2
- package/docs/SELF_HOSTED_FRP.en.md +2 -2
- package/docs/SELF_HOSTED_FRP.md +2 -2
- package/docs/SELF_HOSTED_ORIGIN.en.md +3 -1
- package/docs/SELF_HOSTED_ORIGIN.md +3 -1
- package/docs/SKIN_COMPATIBILITY.en.md +1 -0
- package/docs/SKIN_COMPATIBILITY.md +1 -0
- package/docs/SLOW_CONNECTIONS.en.md +12 -0
- package/docs/SLOW_CONNECTIONS.md +12 -0
- package/lib/client.js +234 -37
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +34 -3
- package/lib/index.mjs +402 -209
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-compat.js +5 -1
- package/lib/mobile-compat.js.map +1 -1
- package/package.json +6 -1
- package/packages/question-fixes/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -3484,9 +3484,10 @@ window.__ModuleLoader__.load({
|
|
|
3484
3484
|
[data-dsh-mobile-center] pre { max-width:100%; overflow-x:auto; }
|
|
3485
3485
|
[data-dsh-mobile-center] :is(img,video,canvas,svg) { max-width:100%; }
|
|
3486
3486
|
[data-dsh-mobile-message-scroll] { box-sizing:border-box !important; width:100% !important; padding:8px 10px 20px !important; }
|
|
3487
|
-
[data-dsh-mobile-history-loader] { position:relative !important; min-height:
|
|
3488
|
-
[data-dsh-mobile-history-loader] button
|
|
3489
|
-
[data-dsh-mobile-history-loader]
|
|
3487
|
+
[data-dsh-mobile-history-loader] { position:relative !important; display:grid !important; grid-template-columns:minmax(0,1fr) !important; justify-items:center !important; flex:0 0 auto !important; min-height:48px !important; font-size:var(--dsh-content-font-size-secondary,13px); line-height:1.5; }
|
|
3488
|
+
[data-dsh-mobile-history-loader]::before,[data-dsh-mobile-history-loader] button { box-sizing:border-box !important; grid-area:1/1 !important; min-height:48px !important; max-width:100% !important; padding:8px 12px !important; font:inherit !important; white-space:normal !important; overflow-wrap:anywhere; }
|
|
3489
|
+
[data-dsh-mobile-history-loader]::before { content:attr(data-dsh-mobile-history-label); visibility:hidden; pointer-events:none; }
|
|
3490
|
+
[data-dsh-mobile-history-loader] button { min-width:48px !important; touch-action:manipulation; }
|
|
3490
3491
|
/* DSH's sibling margins exclude hidden and empty Chat Node Seats. Keep that
|
|
3491
3492
|
spacing rule so searchable hidden seats contribute no folded-turn gaps. */
|
|
3492
3493
|
[data-dsh-mobile-message-column] { box-sizing:border-box !important; width:100% !important; max-width:none !important; margin:0 !important; padding:0 !important; gap:0 !important; --dsh-chat-flow-gap:10px; }
|
|
@@ -3849,6 +3850,158 @@ window.__ModuleLoader__.load({
|
|
|
3849
3850
|
return currentTop <= AUTO_HISTORY_THRESHOLD_PX && currentTop < previousTop - .5;
|
|
3850
3851
|
}
|
|
3851
3852
|
/**
|
|
3853
|
+
* Retain the native idle label's wrapping space while its button displays a shorter busy label.
|
|
3854
|
+
* @param loader - the row whose hidden sizing text shares the button's grid cell.
|
|
3855
|
+
* @param button - the upstream control which owns the localized idle and busy labels.
|
|
3856
|
+
*/
|
|
3857
|
+
function syncHistoryLoaderLabel(loader, button) {
|
|
3858
|
+
if (button.disabled) return;
|
|
3859
|
+
const label = button.textContent?.trim() ?? "";
|
|
3860
|
+
if (loader.dataset.dshMobileHistoryLabel !== label) loader.dataset.dshMobileHistoryLabel = label;
|
|
3861
|
+
}
|
|
3862
|
+
/**
|
|
3863
|
+
* Maximum gap between input or scrolling events before an unused loading intent expires.
|
|
3864
|
+
* A held finger may re-arm on its next owned move; an undelivered touch end cannot keep the lease alive.
|
|
3865
|
+
*/
|
|
3866
|
+
const HISTORY_SCROLL_QUIET_MS = 1600;
|
|
3867
|
+
/**
|
|
3868
|
+
* One shared options object for every intent listener, so a listener is removed by
|
|
3869
|
+
* exactly the registration it came from. Capture keeps `wheel` and `touchmove`
|
|
3870
|
+
* visible even where DSH stops propagation, and is harmless for the scroller's own
|
|
3871
|
+
* `scroll`/`scrollend`.
|
|
3872
|
+
*/
|
|
3873
|
+
const HISTORY_INTENT_LISTENER_OPTIONS = {
|
|
3874
|
+
passive: true,
|
|
3875
|
+
capture: true
|
|
3876
|
+
};
|
|
3877
|
+
/**
|
|
3878
|
+
* The events that carry a history-loading intent. The scroller is not focusable, so
|
|
3879
|
+
* there is no `keydown`: a keyboard reader scrolls it through the page instead.
|
|
3880
|
+
*/
|
|
3881
|
+
const HISTORY_INTENT_EVENTS = [
|
|
3882
|
+
"touchstart",
|
|
3883
|
+
"touchmove",
|
|
3884
|
+
"wheel",
|
|
3885
|
+
"scroll",
|
|
3886
|
+
"scrollend",
|
|
3887
|
+
"touchend",
|
|
3888
|
+
"touchcancel"
|
|
3889
|
+
];
|
|
3890
|
+
/**
|
|
3891
|
+
* The history gate's session identity for one sync pass. `composer` must be the
|
|
3892
|
+
* composer this pass resolved itself: the media binding captures the composer
|
|
3893
|
+
* only later in the same sync, so resolving through that capture lags one pass
|
|
3894
|
+
* behind, and a session switch that reuses the scroller node while replacing
|
|
3895
|
+
* the composer would still report the previous session's id, skip the reset,
|
|
3896
|
+
* and leave the old arm live inside the new session.
|
|
3897
|
+
*/
|
|
3898
|
+
function resolveHistoryGateIdentity(input) {
|
|
3899
|
+
const { sessionId } = resolveComposerSessionOrigin(input.composer, input.selectedRow, input.selectedRowToken);
|
|
3900
|
+
return {
|
|
3901
|
+
sessionId,
|
|
3902
|
+
sessionChanged: sessionId !== input.previousSessionId
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* Decide whether a scroll near the top of the history is the user's, because DSH
|
|
3907
|
+
* writes `scrollTop` itself while it prepends a loaded page. An ungated listener read
|
|
3908
|
+
* that compensation as another upward fling, clicked "Load earlier" again, and shook
|
|
3909
|
+
* the view in a chain. Only fresh input arms the gate, one arm buys at most one
|
|
3910
|
+
* successful load, and an arm with no further input expires quietly.
|
|
3911
|
+
*/
|
|
3912
|
+
function createHistoryAutoLoadGate(hooks) {
|
|
3913
|
+
let armed = false;
|
|
3914
|
+
const touchIds = /* @__PURE__ */ new Set();
|
|
3915
|
+
let disposed = false;
|
|
3916
|
+
let previousTop = hooks.getTop();
|
|
3917
|
+
let quietPending = false;
|
|
3918
|
+
let quietId;
|
|
3919
|
+
const cancelQuiet = () => {
|
|
3920
|
+
if (!quietPending) return;
|
|
3921
|
+
quietPending = false;
|
|
3922
|
+
hooks.cancel(quietId);
|
|
3923
|
+
quietId = void 0;
|
|
3924
|
+
};
|
|
3925
|
+
const onQuiet = () => {
|
|
3926
|
+
quietPending = false;
|
|
3927
|
+
quietId = void 0;
|
|
3928
|
+
armed = false;
|
|
3929
|
+
};
|
|
3930
|
+
const restartQuiet = () => {
|
|
3931
|
+
cancelQuiet();
|
|
3932
|
+
if (disposed) return;
|
|
3933
|
+
quietPending = true;
|
|
3934
|
+
quietId = hooks.schedule(onQuiet, HISTORY_SCROLL_QUIET_MS);
|
|
3935
|
+
};
|
|
3936
|
+
const arm = () => {
|
|
3937
|
+
armed = true;
|
|
3938
|
+
restartQuiet();
|
|
3939
|
+
};
|
|
3940
|
+
const onScroll = () => {
|
|
3941
|
+
const top = hooks.getTop();
|
|
3942
|
+
const eligible = armed && hooks.shouldLoad(previousTop, top);
|
|
3943
|
+
previousTop = top;
|
|
3944
|
+
if (eligible && hooks.tryLoad()) {
|
|
3945
|
+
armed = false;
|
|
3946
|
+
cancelQuiet();
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
if (armed) restartQuiet();
|
|
3950
|
+
};
|
|
3951
|
+
return {
|
|
3952
|
+
handleEvent: (event) => {
|
|
3953
|
+
if (disposed) return;
|
|
3954
|
+
switch (event.type) {
|
|
3955
|
+
case "touchstart":
|
|
3956
|
+
for (const touch of Array.from(event.changedTouches ?? [])) touchIds.add(touch.identifier);
|
|
3957
|
+
if ((event.changedTouches?.length ?? 0) > 0) arm();
|
|
3958
|
+
break;
|
|
3959
|
+
case "touchmove":
|
|
3960
|
+
if (Array.from(event.changedTouches ?? []).some((touch) => touchIds.has(touch.identifier))) arm();
|
|
3961
|
+
break;
|
|
3962
|
+
case "wheel":
|
|
3963
|
+
arm();
|
|
3964
|
+
break;
|
|
3965
|
+
case "touchend":
|
|
3966
|
+
case "touchcancel": {
|
|
3967
|
+
let endedOwnedTouch = false;
|
|
3968
|
+
for (const touch of Array.from(event.changedTouches ?? [])) if (touchIds.delete(touch.identifier)) endedOwnedTouch = true;
|
|
3969
|
+
if (endedOwnedTouch) restartQuiet();
|
|
3970
|
+
break;
|
|
3971
|
+
}
|
|
3972
|
+
case "scrollend":
|
|
3973
|
+
armed = false;
|
|
3974
|
+
cancelQuiet();
|
|
3975
|
+
break;
|
|
3976
|
+
case "scroll": onScroll();
|
|
3977
|
+
}
|
|
3978
|
+
},
|
|
3979
|
+
reset: (top) => {
|
|
3980
|
+
armed = false;
|
|
3981
|
+
touchIds.clear();
|
|
3982
|
+
previousTop = top;
|
|
3983
|
+
cancelQuiet();
|
|
3984
|
+
},
|
|
3985
|
+
dispose: () => {
|
|
3986
|
+
disposed = true;
|
|
3987
|
+
armed = false;
|
|
3988
|
+
touchIds.clear();
|
|
3989
|
+
cancelQuiet();
|
|
3990
|
+
},
|
|
3991
|
+
state: () => ({
|
|
3992
|
+
armed,
|
|
3993
|
+
previousTop
|
|
3994
|
+
})
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
/** Attach a gate to one scroller; the disposer removes exactly the listeners it added. */
|
|
3998
|
+
function bindHistoryAutoLoadGate(scroller, gate) {
|
|
3999
|
+
for (const type of HISTORY_INTENT_EVENTS) scroller.addEventListener(type, gate.handleEvent, HISTORY_INTENT_LISTENER_OPTIONS);
|
|
4000
|
+
return () => {
|
|
4001
|
+
for (const type of HISTORY_INTENT_EVENTS) scroller.removeEventListener(type, gate.handleEvent, HISTORY_INTENT_LISTENER_OPTIONS);
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
/**
|
|
3852
4005
|
* Whether the overlay scrim belongs on screen. The scrim exists for the
|
|
3853
4006
|
* slide-in drawer only — it dims the page and catches the tap that closes the
|
|
3854
4007
|
* drawer — so it is shown while that drawer is open *and* the overlay query is
|
|
@@ -4270,16 +4423,17 @@ window.__ModuleLoader__.load({
|
|
|
4270
4423
|
let disposeEnterBinding;
|
|
4271
4424
|
const sessionTokens = /* @__PURE__ */ new WeakMap();
|
|
4272
4425
|
let nextSessionToken = 0;
|
|
4426
|
+
const sessionRowToken = (row) => {
|
|
4427
|
+
let token = sessionTokens.get(row);
|
|
4428
|
+
if (token === void 0) {
|
|
4429
|
+
token = `stock-${String(++nextSessionToken)}`;
|
|
4430
|
+
sessionTokens.set(row, token);
|
|
4431
|
+
}
|
|
4432
|
+
return token;
|
|
4433
|
+
};
|
|
4273
4434
|
const currentSessionOrigin = () => {
|
|
4274
4435
|
const selectedRow = document.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]");
|
|
4275
|
-
return resolveComposerSessionOrigin(boundComposer, selectedRow,
|
|
4276
|
-
let token = sessionTokens.get(row);
|
|
4277
|
-
if (token === void 0) {
|
|
4278
|
-
token = `stock-${String(++nextSessionToken)}`;
|
|
4279
|
-
sessionTokens.set(row, token);
|
|
4280
|
-
}
|
|
4281
|
-
return token;
|
|
4282
|
-
});
|
|
4436
|
+
return resolveComposerSessionOrigin(boundComposer, selectedRow, sessionRowToken);
|
|
4283
4437
|
};
|
|
4284
4438
|
const disposeBrowserComposerSoftEnter = bindBrowserComposerSoftEnter(document, (editor, composerCard) => ({
|
|
4285
4439
|
appBridge: nativeAppOwnsComposerEnter(window),
|
|
@@ -4524,26 +4678,68 @@ window.__ModuleLoader__.load({
|
|
|
4524
4678
|
let transitionTimer = 0;
|
|
4525
4679
|
let transitionTarget;
|
|
4526
4680
|
let historyScroller;
|
|
4527
|
-
let
|
|
4681
|
+
let historySessionId = null;
|
|
4682
|
+
let disposeHistoryGate;
|
|
4683
|
+
let historyLoaderBinding;
|
|
4684
|
+
const historyLabelObserver = new MutationObserver(() => schedule());
|
|
4685
|
+
const bindHistoryLoader = (next) => {
|
|
4686
|
+
if (historyLoaderBinding?.element === next) return;
|
|
4687
|
+
historyLabelObserver.disconnect();
|
|
4688
|
+
if (historyLoaderBinding !== void 0) {
|
|
4689
|
+
const { element, originalLabel } = historyLoaderBinding;
|
|
4690
|
+
if (originalLabel === void 0) delete element.dataset.dshMobileHistoryLabel;
|
|
4691
|
+
else element.dataset.dshMobileHistoryLabel = originalLabel;
|
|
4692
|
+
}
|
|
4693
|
+
historyLoaderBinding = next === void 0 ? void 0 : {
|
|
4694
|
+
element: next,
|
|
4695
|
+
originalLabel: next.dataset.dshMobileHistoryLabel
|
|
4696
|
+
};
|
|
4697
|
+
if (next !== void 0) historyLabelObserver.observe(next, {
|
|
4698
|
+
childList: true,
|
|
4699
|
+
characterData: true,
|
|
4700
|
+
subtree: true
|
|
4701
|
+
});
|
|
4702
|
+
};
|
|
4528
4703
|
const historyLoadButton = () => {
|
|
4529
4704
|
return (historyScroller === void 0 ? void 0 : firstByClassSuffix(historyScroller, "_older"))?.querySelector("button") ?? void 0;
|
|
4530
4705
|
};
|
|
4531
|
-
const
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4706
|
+
const historyAutoLoadGate = createHistoryAutoLoadGate({
|
|
4707
|
+
getTop: () => Math.max(0, historyScroller?.scrollTop ?? 0),
|
|
4708
|
+
shouldLoad: shouldAutoLoadEarlier,
|
|
4709
|
+
tryLoad: () => {
|
|
4710
|
+
const button = historyLoadButton();
|
|
4711
|
+
if (button === void 0 || button.disabled || button.getAttribute("aria-disabled") === "true") return false;
|
|
4712
|
+
button.click();
|
|
4713
|
+
return true;
|
|
4714
|
+
},
|
|
4715
|
+
schedule: (fn, ms) => window.setTimeout(fn, ms),
|
|
4716
|
+
cancel: (id) => {
|
|
4717
|
+
window.clearTimeout(id);
|
|
4718
|
+
}
|
|
4719
|
+
});
|
|
4720
|
+
/**
|
|
4721
|
+
* Rebind the gate to the conversation scroller, and drop an arm that belongs to a
|
|
4722
|
+
* session the reader has already left: DSH can reuse the same scroller node, so the
|
|
4723
|
+
* session identity has to be checked even when the node did not change. The
|
|
4724
|
+
* composer is this pass's own resolution, never the media binding's capture.
|
|
4725
|
+
*/
|
|
4726
|
+
const bindHistoryScroller = (next, composer) => {
|
|
4727
|
+
if (historyScroller !== next) {
|
|
4728
|
+
disposeHistoryGate?.();
|
|
4729
|
+
disposeHistoryGate = void 0;
|
|
4730
|
+
historyScroller = next;
|
|
4731
|
+
historyAutoLoadGate.reset(next?.scrollTop ?? 0);
|
|
4732
|
+
if (next !== void 0) disposeHistoryGate = bindHistoryAutoLoadGate(next, historyAutoLoadGate);
|
|
4733
|
+
}
|
|
4734
|
+
const identity = resolveHistoryGateIdentity({
|
|
4735
|
+
previousSessionId: historySessionId,
|
|
4736
|
+
composer,
|
|
4737
|
+
selectedRow: document.querySelector("[role=\"treeitem\"][aria-selected=\"true\"]"),
|
|
4738
|
+
selectedRowToken: sessionRowToken
|
|
4739
|
+
});
|
|
4740
|
+
historySessionId = identity.sessionId;
|
|
4741
|
+
if (!identity.sessionChanged) return;
|
|
4742
|
+
historyAutoLoadGate.reset(historyScroller?.scrollTop ?? 0);
|
|
4547
4743
|
};
|
|
4548
4744
|
const animateNavigation = (event) => {
|
|
4549
4745
|
if (!(event.target instanceof Element)) return;
|
|
@@ -4614,7 +4810,8 @@ window.__ModuleLoader__.load({
|
|
|
4614
4810
|
if (center === void 0) {
|
|
4615
4811
|
syncPanHeader(void 0);
|
|
4616
4812
|
syncComposerEnterNewline(null);
|
|
4617
|
-
bindHistoryScroller(void 0);
|
|
4813
|
+
bindHistoryScroller(void 0, null);
|
|
4814
|
+
bindHistoryLoader(void 0);
|
|
4618
4815
|
syncMediaBinding(null);
|
|
4619
4816
|
setCameraActionDisabled(true, label("Apri prima una sessione", "Open a session first", "请先打开会话"));
|
|
4620
4817
|
cameraButton.remove();
|
|
@@ -4628,17 +4825,15 @@ window.__ModuleLoader__.load({
|
|
|
4628
4825
|
viewArea = firstByClassSuffix(center, "_viewArea");
|
|
4629
4826
|
if (viewArea !== void 0) viewArea.dataset.dshMobileView = "true";
|
|
4630
4827
|
const conversation = center.querySelector("[data-conversation-scroll]");
|
|
4631
|
-
|
|
4828
|
+
const composerCard = center.querySelector("[data-composer-card]");
|
|
4829
|
+
bindHistoryScroller(conversation ?? void 0, composerCard ?? null);
|
|
4632
4830
|
const historyLoader = conversation === null ? void 0 : firstByClassSuffix(conversation, "_older");
|
|
4831
|
+
bindHistoryLoader(historyLoader);
|
|
4633
4832
|
if (historyLoader !== void 0) {
|
|
4634
4833
|
historyLoader.dataset.dshMobileHistoryLoader = "true";
|
|
4635
4834
|
historyLoader.setAttribute("aria-live", "polite");
|
|
4636
4835
|
const button = historyLoader.querySelector("button");
|
|
4637
|
-
if (button !== null)
|
|
4638
|
-
button.tabIndex = -1;
|
|
4639
|
-
if (button.disabled) button.removeAttribute("aria-hidden");
|
|
4640
|
-
else button.setAttribute("aria-hidden", "true");
|
|
4641
|
-
}
|
|
4836
|
+
if (button !== null) syncHistoryLoaderLabel(historyLoader, button);
|
|
4642
4837
|
}
|
|
4643
4838
|
const messageColumn = conversation === null ? void 0 : firstByClassSuffix(conversation, "_column");
|
|
4644
4839
|
const messageScroll = messageColumn?.parentElement;
|
|
@@ -4653,7 +4848,6 @@ window.__ModuleLoader__.load({
|
|
|
4653
4848
|
table.before(wrapper);
|
|
4654
4849
|
wrapper.append(table);
|
|
4655
4850
|
}
|
|
4656
|
-
const composerCard = center.querySelector("[data-composer-card]");
|
|
4657
4851
|
const composerRow = composerCard?.querySelector(":scope > [data-input-scroll]")?.nextElementSibling;
|
|
4658
4852
|
if (!(composerRow instanceof HTMLElement)) {
|
|
4659
4853
|
syncComposerEnterNewline(null);
|
|
@@ -4805,7 +4999,10 @@ window.__ModuleLoader__.load({
|
|
|
4805
4999
|
if (transitionRestartFrame !== 0) cancelAnimationFrame(transitionRestartFrame);
|
|
4806
5000
|
if (transitionTimer !== 0) clearTimeout(transitionTimer);
|
|
4807
5001
|
transitionTarget?.removeAttribute("data-dsh-mobile-view-transition");
|
|
4808
|
-
|
|
5002
|
+
disposeHistoryGate?.();
|
|
5003
|
+
disposeHistoryGate = void 0;
|
|
5004
|
+
historyAutoLoadGate.dispose();
|
|
5005
|
+
bindHistoryLoader(void 0);
|
|
4809
5006
|
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
4810
5007
|
document.removeEventListener("keydown", onKeyDown, true);
|
|
4811
5008
|
document.removeEventListener("pointerdown", onTogglePointerDown, true);
|