@onekeyfe/react-native-native-list 3.0.105 → 3.0.107
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/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +16 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +6 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +671 -52
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +282 -54
- package/ios/HybridNativeList.swift +8 -2
- package/ios/NativeListCell.swift +630 -31
- package/ios/NativeListDesignAssets.swift +24 -1
- package/ios/RNCNativeListView.swift +216 -42
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/icon.svg +14 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/icon.svg +12 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/icon.svg +6 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/icon.svg +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/icon.svg +3 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/icon.svg +7 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/icon.svg +18 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/icon.svg +7 -0
- package/lib/module/NativeList.js +102 -8
- package/lib/module/NativeList.web.js +20 -2
- package/lib/module/avatarPrefetch.js +174 -0
- package/lib/module/validation.js +45 -2
- package/lib/module/web/NativeListAvatarWorker.js +542 -0
- package/lib/module/web/NativeListWebAvatarCache.js +267 -0
- package/lib/module/web/NativeListWebEngine.js +958 -47
- package/lib/typescript/src/NativeList.web.d.ts +2 -2
- package/lib/typescript/src/avatarPrefetch.d.ts +28 -0
- package/lib/typescript/src/models.d.ts +72 -4
- package/lib/typescript/src/web/NativeListWebAvatarCache.d.ts +6 -0
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +10 -1
- package/package.json +2 -2
- package/src/NativeList.tsx +149 -13
- package/src/NativeList.web.tsx +30 -3
- package/src/avatarPrefetch.ts +236 -0
- package/src/models.ts +98 -7
- package/src/validation.ts +131 -2
- package/src/web/NativeListAvatarWorker.js +567 -0
- package/src/web/NativeListWebAvatarCache.ts +314 -0
- package/src/web/NativeListWebEngine.ts +1409 -60
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
2
|
+
<path
|
|
3
|
+
fill="#8897A5"
|
|
4
|
+
d="M11 2a1 1 0 1 1 2 0v1.8l1.6 1.6a1 1 0 1 1-1.4 1.4L12 5.6l-1.2 1.2a1 1 0 0 1-1.4-1.4L11 3.8z"
|
|
5
|
+
/>
|
|
6
|
+
<rect width="18" height="14" x="3" y="6" fill="#3FA9F5" rx="5" />
|
|
7
|
+
<rect width="3" height="6" x="1.5" y="10" fill="#8897A5" rx="1.5" />
|
|
8
|
+
<rect width="3" height="6" x="19.5" y="10" fill="#8897A5" rx="1.5" />
|
|
9
|
+
<circle cx="9" cy="12" r="1.5" fill="#10243E" />
|
|
10
|
+
<circle cx="15" cy="12" r="1.5" fill="#10243E" />
|
|
11
|
+
<path
|
|
12
|
+
fill="#10243E"
|
|
13
|
+
d="M8.5 15.4c.9.8 2.08 1.2 3.5 1.2s2.6-.4 3.5-1.2c.24-.2.6-.18.8.06.2.23.17.6-.06.8-1.14.98-2.58 1.46-4.24 1.46s-3.1-.48-4.24-1.46a.58.58 0 0 1-.06-.8c.2-.24.56-.26.8-.06"
|
|
14
|
+
/>
|
|
15
|
+
</svg>
|
package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/icon.svg
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
2
|
+
<path
|
|
3
|
+
fill-rule="evenodd"
|
|
4
|
+
d="M12 2c5.185 0 9.448 3.947 9.95 9H22v2h-.05c-.502 5.053-4.765 9-9.95 9s-9.448-3.947-9.95-9H2v-2h.05C2.552 5.947 6.815 2 12 2M9.523 13c.09 1.982.438 3.726.934 5.002.29.746.612 1.282.917 1.614.304.331.517.384.626.384s.322-.053.626-.384c.305-.332.627-.868.917-1.614.496-1.276.845-3.02.934-5.002zm-5.459 0a8 8 0 0 0 4.8 6.36 10 10 0 0 1-.271-.633C7.994 17.187 7.61 15.189 7.52 13zm12.416 0c-.09 2.189-.474 4.187-1.073 5.727a10 10 0 0 1-.271.633 8 8 0 0 0 4.8-6.36zM8.863 4.639A8 8 0 0 0 4.064 11h3.457c.09-2.189.473-4.187 1.072-5.727q.127-.327.27-.634M12 4c-.109 0-.322.053-.626.384-.305.332-.627.868-.917 1.614-.496 1.276-.844 3.02-.934 5.002h4.954c-.09-1.982-.438-3.726-.934-5.002-.29-.746-.612-1.282-.917-1.614C12.322 4.053 12.109 4 12 4m3.136.639q.144.307.271.634c.599 1.54.982 3.538 1.073 5.727h3.456a8 8 0 0 0-4.8-6.361"
|
|
5
|
+
clip-rule="evenodd"
|
|
6
|
+
/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
2
|
+
<path
|
|
3
|
+
fill="#4285F4"
|
|
4
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09"
|
|
5
|
+
/>
|
|
6
|
+
<path
|
|
7
|
+
fill="#34A853"
|
|
8
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23"
|
|
9
|
+
/>
|
|
10
|
+
<path
|
|
11
|
+
fill="#FBBC05"
|
|
12
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22z"
|
|
13
|
+
/>
|
|
14
|
+
<path
|
|
15
|
+
fill="#EA4335"
|
|
16
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53"
|
|
17
|
+
/>
|
|
18
|
+
</svg>
|
package/lib/module/NativeList.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
|
|
3
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
|
|
4
|
+
import { OneKeyImageCache, OneKeyImageCachePolicy } from '@onekeyfe/react-native-image';
|
|
5
|
+
import { NativeAvatarPrefetchModel, NativeAvatarPrefetchQueue } from "./avatarPrefetch.js";
|
|
4
6
|
import { callback, getHostComponent } from 'react-native-nitro-modules';
|
|
7
|
+
import { isSelectableRow } from "./selection.js";
|
|
5
8
|
import { normalizeIndexScroll, normalizeKeyScroll, normalizePositionScroll, resolveLocationIndex, scrollFailure, validateOffset } from "./scrolling.js";
|
|
6
|
-
import { serializePatches, serializeSnapshot } from "./validation.js";
|
|
9
|
+
import { applyRowPatches, serializePatches, serializeSnapshot } from "./validation.js";
|
|
7
10
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
11
|
const NativeListConfig = require('../nitrogen/generated/shared/json/NativeListConfig.json');
|
|
9
12
|
const NativeListHost = getHostComponent('NativeList', () => NativeListConfig);
|
|
@@ -48,8 +51,52 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
48
51
|
onRefresh,
|
|
49
52
|
onScrollToIndexFailed
|
|
50
53
|
};
|
|
54
|
+
const snapshotJson = useMemo(() => serializeSnapshot(snapshot), [snapshot]);
|
|
51
55
|
const snapshotRef = useRef(snapshot);
|
|
52
|
-
|
|
56
|
+
const previousSnapshotJsonRef = useRef(snapshotJson);
|
|
57
|
+
const snapshotChanged = previousSnapshotJsonRef.current !== snapshotJson;
|
|
58
|
+
if (snapshotChanged) {
|
|
59
|
+
snapshotRef.current = snapshot;
|
|
60
|
+
previousSnapshotJsonRef.current = snapshotJson;
|
|
61
|
+
}
|
|
62
|
+
// OneKey patch: range events already cross the bridge only when row indices
|
|
63
|
+
// change. Prefetch is optional work; scrolling and visible loads stay native.
|
|
64
|
+
const avatarQueueRef = useRef(null);
|
|
65
|
+
const avatarRangeRef = useRef(undefined);
|
|
66
|
+
// OneKey patch: a changed prop is a complete snapshot; unrelated renders must
|
|
67
|
+
// not erase image patches already dispatched through the imperative handle.
|
|
68
|
+
// const avatarPrefetchPaused = useRef(false);
|
|
69
|
+
const avatarModelRef = useRef(null);
|
|
70
|
+
const avatarLifecycle = useRef('pending');
|
|
71
|
+
if (!avatarModelRef.current) avatarModelRef.current = new NativeAvatarPrefetchModel(snapshot.rows);else if (snapshotChanged) avatarModelRef.current.replaceSnapshot(snapshot.rows);
|
|
72
|
+
const updateAvatarPrefetch = () => {
|
|
73
|
+
const range = avatarRangeRef.current;
|
|
74
|
+
if (!range) return;
|
|
75
|
+
avatarQueueRef.current?.update(avatarModelRef.current?.window(range.first, range.last, range.direction) ?? []);
|
|
76
|
+
};
|
|
77
|
+
const updateAvatarPrefetchRef = useRef(updateAvatarPrefetch);
|
|
78
|
+
updateAvatarPrefetchRef.current = updateAvatarPrefetch;
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
avatarLifecycle.current = 'mounted';
|
|
81
|
+
const queue = new NativeAvatarPrefetchQueue(source => OneKeyImageCache.preload([{
|
|
82
|
+
uri: source.uri,
|
|
83
|
+
headers: source.headers,
|
|
84
|
+
resizeWidth: source.width,
|
|
85
|
+
resizeHeight: source.height,
|
|
86
|
+
optimizeTos: false,
|
|
87
|
+
cachePolicy: source.cachePolicy === 'memory' ? OneKeyImageCachePolicy.MEMORY : source.cachePolicy === 'disk' ? OneKeyImageCachePolicy.DISK : OneKeyImageCachePolicy.MEMORY_DISK
|
|
88
|
+
}]));
|
|
89
|
+
avatarQueueRef.current = queue;
|
|
90
|
+
updateAvatarPrefetchRef.current();
|
|
91
|
+
return () => {
|
|
92
|
+
queue.dispose();
|
|
93
|
+
avatarQueueRef.current = null;
|
|
94
|
+
avatarLifecycle.current = 'unmounted';
|
|
95
|
+
};
|
|
96
|
+
}, []);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
updateAvatarPrefetchRef.current();
|
|
99
|
+
}, [snapshot]);
|
|
53
100
|
const initialScrollRef = useRef(initialScrollIndex === undefined && initialScrollKey === undefined ? undefined : initialScrollIndex !== undefined ? normalizeIndexScroll({
|
|
54
101
|
index: initialScrollIndex,
|
|
55
102
|
animated: false,
|
|
@@ -64,7 +111,6 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
64
111
|
}, 'start')
|
|
65
112
|
});
|
|
66
113
|
const didApplyInitialScroll = useRef(false);
|
|
67
|
-
const snapshotJson = useMemo(() => serializeSnapshot(snapshot), [snapshot]);
|
|
68
114
|
const emitIndexFailure = (index, reason) => {
|
|
69
115
|
callbacksRef.current.onScrollToIndexFailed?.(scrollFailure(snapshotRef.current.rows, index, reason));
|
|
70
116
|
};
|
|
@@ -89,14 +135,40 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
89
135
|
applyInitialScrollCallbackRef.current = applyInitialScroll;
|
|
90
136
|
useImperativeHandle(forwardedRef, () => ({
|
|
91
137
|
applySnapshot(nextSnapshot) {
|
|
138
|
+
const nextSnapshotJson = serializeSnapshot(nextSnapshot);
|
|
139
|
+
nativeRef.current?.applySnapshot(nextSnapshotJson);
|
|
92
140
|
snapshotRef.current = nextSnapshot;
|
|
93
|
-
nativeRef.current
|
|
141
|
+
if (nativeRef.current && avatarLifecycle.current !== 'unmounted') {
|
|
142
|
+
avatarModelRef.current?.replaceSnapshot(nextSnapshot.rows);
|
|
143
|
+
updateAvatarPrefetchRef.current();
|
|
144
|
+
}
|
|
94
145
|
},
|
|
95
146
|
applyPatches(patches) {
|
|
96
|
-
if (patches.length
|
|
147
|
+
if (patches.length === 0) return;
|
|
148
|
+
const nextSnapshot = applyRowPatches(snapshotRef.current, patches);
|
|
149
|
+
const native = nativeRef.current;
|
|
150
|
+
const dispatch = native ? () => {
|
|
151
|
+
native.applyPatches(serializePatches(patches));
|
|
152
|
+
snapshotRef.current = nextSnapshot;
|
|
153
|
+
} : undefined;
|
|
154
|
+
if (avatarLifecycle.current === 'unmounted') {
|
|
155
|
+
dispatch?.();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (avatarModelRef.current?.applyPatches(patches, dispatch)) updateAvatarPrefetchRef.current();
|
|
97
159
|
},
|
|
98
160
|
reconcileSelection(selectedKeys) {
|
|
99
161
|
nativeRef.current?.reconcileSelection(JSON.stringify(selectedKeys));
|
|
162
|
+
const current = snapshotRef.current;
|
|
163
|
+
if (current.selection && (current.selection.mode !== 'single' || selectedKeys.length <= 1) && selectedKeys.every(key => current.rows.some(row => row.key === key && isSelectableRow(row)))) {
|
|
164
|
+
snapshotRef.current = {
|
|
165
|
+
...current,
|
|
166
|
+
selection: {
|
|
167
|
+
...current.selection,
|
|
168
|
+
selectedKeys
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
100
172
|
},
|
|
101
173
|
scrollToKey(paramsOrKey, animated, alignment) {
|
|
102
174
|
const {
|
|
@@ -162,7 +234,21 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
162
234
|
callbacksRef.current.onActionAnchorInvalidated?.(parsePayload(payloadJson));
|
|
163
235
|
}),
|
|
164
236
|
onSelectionDelta: callback(payloadJson => {
|
|
165
|
-
|
|
237
|
+
const payload = parsePayload(payloadJson);
|
|
238
|
+
const current = snapshotRef.current;
|
|
239
|
+
if (current.selection) {
|
|
240
|
+
const selectedKeys = new Set(current.selection.selectedKeys);
|
|
241
|
+
payload.removedKeys.forEach(key => selectedKeys.delete(key));
|
|
242
|
+
payload.addedKeys.forEach(key => selectedKeys.add(key));
|
|
243
|
+
snapshotRef.current = {
|
|
244
|
+
...current,
|
|
245
|
+
selection: {
|
|
246
|
+
...current.selection,
|
|
247
|
+
selectedKeys: [...selectedKeys]
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
callbacksRef.current.onSelectionDelta?.(payload);
|
|
166
252
|
}),
|
|
167
253
|
onReorder: callback(payloadJson => {
|
|
168
254
|
callbacksRef.current.onReorder?.(parsePayload(payloadJson));
|
|
@@ -171,7 +257,15 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
171
257
|
callbacksRef.current.onEndReached?.(parsePayload(payloadJson));
|
|
172
258
|
}),
|
|
173
259
|
onVisibleRangeChanged: callback(payloadJson => {
|
|
174
|
-
|
|
260
|
+
const payload = parsePayload(payloadJson);
|
|
261
|
+
const previous = avatarRangeRef.current;
|
|
262
|
+
avatarRangeRef.current = {
|
|
263
|
+
first: payload.firstIndex,
|
|
264
|
+
last: payload.lastIndex,
|
|
265
|
+
direction: previous && payload.firstIndex !== previous.first ? Math.sign(payload.firstIndex - previous.first) : previous?.direction ?? 1
|
|
266
|
+
};
|
|
267
|
+
updateAvatarPrefetchRef.current();
|
|
268
|
+
callbacksRef.current.onVisibleRangeChanged?.(payload);
|
|
175
269
|
})
|
|
176
270
|
}), []);
|
|
177
271
|
return /*#__PURE__*/_jsx(NativeListHost, {
|
|
@@ -29,7 +29,14 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
29
29
|
webVirtualizationEnabledRef.current = webVirtualizationEnabled;
|
|
30
30
|
const validatedSnapshot = useMemo(() => validateSnapshot(snapshot), [snapshot]);
|
|
31
31
|
const snapshotRef = useRef(validatedSnapshot);
|
|
32
|
-
|
|
32
|
+
const snapshotPropRef = useRef(validatedSnapshot);
|
|
33
|
+
// An imperative snapshot survives host mounting until the snapshot prop changes.
|
|
34
|
+
if (snapshotPropRef.current !== validatedSnapshot) {
|
|
35
|
+
snapshotPropRef.current = validatedSnapshot;
|
|
36
|
+
snapshotRef.current = validatedSnapshot;
|
|
37
|
+
}
|
|
38
|
+
// OneKey patch: React refs can be ready before the DOM engine is mounted.
|
|
39
|
+
const pendingPatchesRef = useRef(undefined);
|
|
33
40
|
const appliedSnapshotRef = useRef(undefined);
|
|
34
41
|
const callbacksRef = useRef({});
|
|
35
42
|
callbacksRef.current = {
|
|
@@ -61,6 +68,9 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
61
68
|
const engine = new NativeListWebEngine(host, snapshotRef.current, callbacksRef.current, webVirtualizationEnabledRef.current);
|
|
62
69
|
engineRef.current = engine;
|
|
63
70
|
appliedSnapshotRef.current = snapshotRef.current;
|
|
71
|
+
const pending = pendingPatchesRef.current;
|
|
72
|
+
pendingPatchesRef.current = undefined;
|
|
73
|
+
if (pending?.snapshot === snapshotRef.current) pending.batches.forEach(patches => engine.applyPatches(patches));
|
|
64
74
|
const initial = initialScrollRef.current;
|
|
65
75
|
if (initial && !didApplyInitialScroll.current) {
|
|
66
76
|
didApplyInitialScroll.current = true;
|
|
@@ -89,6 +99,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
89
99
|
useImperativeHandle(forwardedRef, () => ({
|
|
90
100
|
applySnapshot(nextSnapshot) {
|
|
91
101
|
const next = validateSnapshot(nextSnapshot);
|
|
102
|
+
pendingPatchesRef.current = undefined;
|
|
92
103
|
snapshotRef.current = next;
|
|
93
104
|
appliedSnapshotRef.current = next;
|
|
94
105
|
engineRef.current?.applySnapshot(next);
|
|
@@ -96,7 +107,14 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
96
107
|
applyPatches(patches) {
|
|
97
108
|
if (patches.length > 0) {
|
|
98
109
|
serializePatches(patches);
|
|
99
|
-
engineRef.current?.applyPatches(patches);
|
|
110
|
+
// engineRef.current?.applyPatches(patches);
|
|
111
|
+
if (engineRef.current) engineRef.current.applyPatches(patches);else {
|
|
112
|
+
if (pendingPatchesRef.current?.snapshot !== snapshotRef.current) pendingPatchesRef.current = {
|
|
113
|
+
snapshot: snapshotRef.current,
|
|
114
|
+
batches: []
|
|
115
|
+
};
|
|
116
|
+
pendingPatchesRef.current.batches.push(patches);
|
|
117
|
+
}
|
|
100
118
|
}
|
|
101
119
|
},
|
|
102
120
|
reconcileSelection(selectedKeys) {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// OneKey patch: share a bounded URI-only window across renderers. No bitmap or
|
|
4
|
+
// account-specific data enters list snapshots or the UI runtime.
|
|
5
|
+
|
|
6
|
+
const PREFIX = 'onekey-avatar://blockie/v1/';
|
|
7
|
+
const MAX_CANDIDATES = 64;
|
|
8
|
+
const IMAGE_PATCH_FIELDS = ['leading', 'secondaryLeading', 'image', 'networkImage', 'thumbnail', 'visual'];
|
|
9
|
+
export function avatarPrefetchWindow(rows, first, last, direction, resolveRow) {
|
|
10
|
+
if (first < 0 || last < first || first >= rows.length) return [];
|
|
11
|
+
last = Math.min(last, rows.length - 1);
|
|
12
|
+
const result = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
let limit = MAX_CANDIDATES;
|
|
15
|
+
const add = (source, priority) => {
|
|
16
|
+
if (!source?.uri.startsWith(PREFIX) || source.cachePolicy === 'none' || seen.has(source.uri) || result.length >= limit) return;
|
|
17
|
+
seen.add(source.uri);
|
|
18
|
+
result.push({
|
|
19
|
+
source,
|
|
20
|
+
priority
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
const visual = (value, priority) => {
|
|
24
|
+
if (!value) return;
|
|
25
|
+
if ('image' in value) add(value.image, priority);
|
|
26
|
+
if ('networkImage' in value) add(value.networkImage, priority);
|
|
27
|
+
if ('images' in value) value.images.forEach(image => add(image, priority));
|
|
28
|
+
if ('overlays' in value) value.overlays?.forEach(overlay => add(overlay.image, priority));
|
|
29
|
+
};
|
|
30
|
+
const row = (value, priority) => {
|
|
31
|
+
value = resolveRow?.(value) ?? value;
|
|
32
|
+
if ('leading' in value) visual(value.leading, priority);
|
|
33
|
+
if ('secondaryLeading' in value) visual(value.secondaryLeading, priority);
|
|
34
|
+
if ('image' in value) add(value.image, priority);
|
|
35
|
+
if ('networkImage' in value) add(value.networkImage, priority);
|
|
36
|
+
if ('thumbnail' in value) add(value.thumbnail, priority);
|
|
37
|
+
if (value.type === 'walletGroup') {
|
|
38
|
+
visual(value.parent.leading, priority);
|
|
39
|
+
for (const child of value.children.slice(0, MAX_CANDIDATES)) {
|
|
40
|
+
if (result.length >= limit) break;
|
|
41
|
+
visual(child.leading, priority);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// Bound row examination too: a giant non-avatar group must not scan all data.
|
|
46
|
+
for (let index = first; index <= last && index < first + 64; index += 1) row(rows[index], 0);
|
|
47
|
+
const span = Math.min(24, (last - first + 1) * 2);
|
|
48
|
+
const step = direction < 0 ? -1 : 1;
|
|
49
|
+
const aheadStart = step > 0 ? last + 1 : first - 1;
|
|
50
|
+
const aheadLimit = Math.min(MAX_CANDIDATES, result.length + 24);
|
|
51
|
+
limit = aheadLimit;
|
|
52
|
+
for (let distance = 0; distance < span && result.length < aheadLimit; distance += 1) {
|
|
53
|
+
const index = aheadStart + distance * step;
|
|
54
|
+
if (index < 0 || index >= rows.length) break;
|
|
55
|
+
row(rows[index], 1);
|
|
56
|
+
}
|
|
57
|
+
const behindStart = step > 0 ? first - 1 : last + 1;
|
|
58
|
+
const behindLimit = Math.min(MAX_CANDIDATES, result.length + 8);
|
|
59
|
+
limit = behindLimit;
|
|
60
|
+
for (let distance = 0; distance < Math.min(8, span) && result.length < behindLimit; distance += 1) {
|
|
61
|
+
const index = behindStart - distance * step;
|
|
62
|
+
if (index < 0 || index >= rows.length) break;
|
|
63
|
+
row(rows[index], 2);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// OneKey patch: imperative image patches update a sparse prefetch overlay, not
|
|
69
|
+
// the readonly prop or a full cloned snapshot. The key index is rebuilt only for
|
|
70
|
+
// a complete snapshot; ordinary balance patches retain no extra row copies.
|
|
71
|
+
export class NativeAvatarPrefetchModel {
|
|
72
|
+
imageRows = new Map();
|
|
73
|
+
constructor(rows) {
|
|
74
|
+
this.rows = rows;
|
|
75
|
+
this.rowByKey = new Map(rows.map(row => [row.key, row]));
|
|
76
|
+
}
|
|
77
|
+
replaceSnapshot(rows) {
|
|
78
|
+
this.rows = rows;
|
|
79
|
+
this.rowByKey = new Map(rows.map(row => [row.key, row]));
|
|
80
|
+
this.imageRows.clear();
|
|
81
|
+
}
|
|
82
|
+
applyPatches(patches, dispatch) {
|
|
83
|
+
// Native methods have no acknowledgement. Mirror only dispatched batches;
|
|
84
|
+
// a missing host or a synchronous serialization/dispatch failure changes nothing.
|
|
85
|
+
if (!dispatch) return false;
|
|
86
|
+
dispatch();
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
for (const patch of patches) {
|
|
89
|
+
const row = this.rowByKey.get(patch.key);
|
|
90
|
+
// Both native renderers reject the whole batch for an unknown key/type.
|
|
91
|
+
if (!row || row.type !== patch.type || seen.has(patch.key)) return false;
|
|
92
|
+
seen.add(patch.key);
|
|
93
|
+
}
|
|
94
|
+
let changed = false;
|
|
95
|
+
for (const patch of patches) {
|
|
96
|
+
const changes = patch.changes;
|
|
97
|
+
let imageChanges;
|
|
98
|
+
for (const key of IMAGE_PATCH_FIELDS) {
|
|
99
|
+
if (changes[key] !== undefined) {
|
|
100
|
+
imageChanges ??= {};
|
|
101
|
+
imageChanges[key] = changes[key];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// JSON.stringify omits undefined top-level fields. Native therefore keeps
|
|
105
|
+
// them unchanged; an explicit replacement visual without image clears it.
|
|
106
|
+
if (!imageChanges) continue;
|
|
107
|
+
const row = this.imageRows.get(patch.key) ?? this.rowByKey.get(patch.key);
|
|
108
|
+
// Preserve the validated row discriminant, matching applyRowPatches' shallow merge.
|
|
109
|
+
this.imageRows.set(patch.key, {
|
|
110
|
+
...row,
|
|
111
|
+
...imageChanges,
|
|
112
|
+
key: row.key,
|
|
113
|
+
type: row.type
|
|
114
|
+
});
|
|
115
|
+
changed = true;
|
|
116
|
+
}
|
|
117
|
+
return changed;
|
|
118
|
+
}
|
|
119
|
+
window(first, last, direction) {
|
|
120
|
+
return avatarPrefetchWindow(this.rows, first, last, direction, row => this.imageRows.get(row.key) ?? row);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Native preload has no cancellation token. One in-flight source is the bound;
|
|
125
|
+
// direction changes replace all not-yet-started work rather than appending it.
|
|
126
|
+
export class NativeAvatarPrefetchQueue {
|
|
127
|
+
pending = [];
|
|
128
|
+
disposed = false;
|
|
129
|
+
recent = new Map();
|
|
130
|
+
constructor(preload) {
|
|
131
|
+
this.preload = preload;
|
|
132
|
+
}
|
|
133
|
+
update(candidates) {
|
|
134
|
+
if (this.disposed) return;
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
this.pending = candidates.filter(({
|
|
137
|
+
source,
|
|
138
|
+
priority
|
|
139
|
+
}) => priority !== 0 && source.uri !== this.activeUri && now - (this.recent.get(source.uri) ?? 0) > 30_000).map(({
|
|
140
|
+
source
|
|
141
|
+
}) => source);
|
|
142
|
+
this.schedule();
|
|
143
|
+
}
|
|
144
|
+
dispose() {
|
|
145
|
+
this.disposed = true;
|
|
146
|
+
this.pending = [];
|
|
147
|
+
this.recent.clear();
|
|
148
|
+
if (this.timer !== undefined) clearTimeout(this.timer);
|
|
149
|
+
this.timer = undefined;
|
|
150
|
+
}
|
|
151
|
+
schedule() {
|
|
152
|
+
if (this.disposed || this.activeUri || this.timer !== undefined || !this.pending.length) return;
|
|
153
|
+
// Yield between short batches without imposing a frame-per-image throughput cap.
|
|
154
|
+
this.timer = setTimeout(() => {
|
|
155
|
+
this.timer = undefined;
|
|
156
|
+
const source = this.pending.shift();
|
|
157
|
+
if (!source || this.disposed) return;
|
|
158
|
+
this.activeUri = source.uri;
|
|
159
|
+
Promise.resolve().then(() => this.preload(source)).then(success => {
|
|
160
|
+
if (success && !this.disposed) {
|
|
161
|
+
this.recent.delete(source.uri);
|
|
162
|
+
this.recent.set(source.uri, Date.now());
|
|
163
|
+
if (this.recent.size > 128) this.recent.delete(this.recent.keys().next().value);
|
|
164
|
+
}
|
|
165
|
+
}).catch(() => {
|
|
166
|
+
/* Visible loading retains its own failure/retry behavior. */
|
|
167
|
+
}).finally(() => {
|
|
168
|
+
this.activeUri = undefined;
|
|
169
|
+
this.schedule();
|
|
170
|
+
});
|
|
171
|
+
}, 0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=avatarPrefetch.js.map
|
package/lib/module/validation.js
CHANGED
|
@@ -124,6 +124,26 @@ function assertLeadingVisual(visual, path) {
|
|
|
124
124
|
assertImage(visual.image, `${path}.image`);
|
|
125
125
|
assertVisualShape(visual.shape, `${path}.shape`);
|
|
126
126
|
assertText(visual.cornerIcon?.name, `${path}.cornerIcon.name`);
|
|
127
|
+
// OneKey patch: validate optional selector overlays at the JSON boundary.
|
|
128
|
+
assertText(visual.fallbackIcon?.name, `${path}.fallbackIcon.name`);
|
|
129
|
+
if ((visual.overlays?.length ?? 0) > 2) fail(`${path}.overlays`, 'supports at most two overlays');
|
|
130
|
+
visual.overlays?.forEach((overlay, index) => {
|
|
131
|
+
if (!['topLeft', 'bottomRight'].includes(overlay.position)) fail(`${path}.overlays[${index}].position`, 'must be topLeft or bottomRight');
|
|
132
|
+
if (overlay.size !== undefined && (overlay.size <= 0 || overlay.size > 40)) fail(`${path}.overlays[${index}].size`, 'must be within 1...40');
|
|
133
|
+
if (overlay.padding !== undefined && (!Number.isFinite(overlay.padding) || overlay.padding < 0 || overlay.padding * 2 >= (overlay.size ?? 20))) fail(`${path}.overlays[${index}].padding`, 'must fit inside the overlay');
|
|
134
|
+
if (overlay.offset !== undefined && (!Number.isFinite(overlay.offset) || overlay.offset < 0 || overlay.offset > 20)) fail(`${path}.overlays[${index}].offset`, 'must be within 0...20');
|
|
135
|
+
for (const field of ['width', 'height']) {
|
|
136
|
+
const value = overlay[field];
|
|
137
|
+
if (value !== undefined && (!Number.isFinite(value) || value <= 0 || value > 40)) fail(`${path}.overlays[${index}].${field}`, 'must be within 1...40');
|
|
138
|
+
}
|
|
139
|
+
for (const field of ['offsetX', 'offsetY']) {
|
|
140
|
+
const value = overlay[field];
|
|
141
|
+
if (value !== undefined && (!Number.isFinite(value) || value < 0 || value > 20)) fail(`${path}.overlays[${index}].${field}`, 'must be within 0...20');
|
|
142
|
+
}
|
|
143
|
+
if (overlay.padding !== undefined && overlay.padding * 2 >= Math.min(overlay.width ?? overlay.size ?? 20, overlay.height ?? overlay.size ?? 20)) fail(`${path}.overlays[${index}].padding`, 'must fit inside the overlay');
|
|
144
|
+
assertImage(overlay.image, `${path}.overlays[${index}].image`);
|
|
145
|
+
assertText(overlay.text, `${path}.overlays[${index}].text`);
|
|
146
|
+
});
|
|
127
147
|
if (visual.kind === 'token') {
|
|
128
148
|
assertImage(visual.networkImage, `${path}.networkImage`);
|
|
129
149
|
}
|
|
@@ -169,6 +189,10 @@ function assertWalletGroup(row, path) {
|
|
|
169
189
|
}
|
|
170
190
|
function assertRow(row, index, path = `rows[${index}]`) {
|
|
171
191
|
assertKey(row.key, `${path}.key`);
|
|
192
|
+
// OneKey patch: explicit dimensions and opacity cannot corrupt list layout.
|
|
193
|
+
if (row.height !== undefined && (row.height < 0 || row.height > 4096)) fail(`${path}.height`, 'must be within 0...4096');
|
|
194
|
+
if (row.heightRounding !== undefined && (row.height === undefined || !['floor', 'nearest'].includes(row.heightRounding))) fail(`${path}.heightRounding`, 'requires an explicit height and must be floor or nearest');
|
|
195
|
+
if (row.opacity !== undefined && (row.opacity < 0 || row.opacity > 1)) fail(`${path}.opacity`, 'must be within 0...1');
|
|
172
196
|
if (row.groupId && !row.groupPosition) {
|
|
173
197
|
fail(`${path}.groupPosition`, 'is required when groupId is present');
|
|
174
198
|
}
|
|
@@ -186,6 +210,16 @@ function assertRow(row, index, path = `rows[${index}]`) {
|
|
|
186
210
|
}
|
|
187
211
|
assertText(row.title, `${path}.title`);
|
|
188
212
|
assertText(row.subtitle, `${path}.subtitle`);
|
|
213
|
+
// OneKey patch: selector text segments truncate independently.
|
|
214
|
+
row.subtitleSegments?.forEach((segment, segmentIndex) => {
|
|
215
|
+
assertText(segment.text, `${path}.subtitleSegments[${segmentIndex}].text`);
|
|
216
|
+
if (segment.tone !== undefined && !['primary', 'secondary', 'disabled', 'caution', 'positive', 'negative'].includes(segment.tone)) fail(`${path}.subtitleSegments[${segmentIndex}].tone`, 'invalid selector text tone');
|
|
217
|
+
});
|
|
218
|
+
let previousMatchEnd = 0;
|
|
219
|
+
row.titleMatch?.forEach(match => {
|
|
220
|
+
if (!Number.isInteger(match.start) || !Number.isInteger(match.end) || match.start < previousMatchEnd || match.end <= match.start || match.end > row.title.length) fail(`${path}.titleMatch`, 'must contain ordered, non-overlapping UTF-16 ranges inside title');
|
|
221
|
+
previousMatchEnd = match.end;
|
|
222
|
+
});
|
|
189
223
|
assertText(row.tertiary, `${path}.tertiary`);
|
|
190
224
|
if (row.tertiaryTone !== undefined && !['secondary', 'info'].includes(row.tertiaryTone)) {
|
|
191
225
|
fail(`${path}.tertiaryTone`, 'must be secondary or info');
|
|
@@ -293,9 +327,12 @@ function assertRow(row, index, path = `rows[${index}]`) {
|
|
|
293
327
|
assertTrailingAccessories(row.trailing, `${path}.trailing`);
|
|
294
328
|
break;
|
|
295
329
|
case 'system':
|
|
296
|
-
if (
|
|
297
|
-
|
|
330
|
+
if (
|
|
331
|
+
// OneKey patch: deprecated-wallet warnings retain the original scrolling semantics.
|
|
332
|
+
!['loading', 'retry', 'noMatch', 'end', 'spacer', 'warning'].includes(row.variant)) {
|
|
333
|
+
fail(`${path}.variant`, 'must be loading, retry, noMatch, end, spacer, or warning');
|
|
298
334
|
}
|
|
335
|
+
if (row.variant === 'warning') assertText(row.title, `${path}.title`);
|
|
299
336
|
if (row.variant !== 'spacer') {
|
|
300
337
|
assertText(row.message, `${path}.message`);
|
|
301
338
|
}
|
|
@@ -397,6 +434,12 @@ export function validateSnapshot(snapshot) {
|
|
|
397
434
|
}
|
|
398
435
|
function assertPatchChanges(patch, index) {
|
|
399
436
|
const path = `patches[${index}].changes`;
|
|
437
|
+
// OneKey patch: partial balance updates retain a valid, current accessibility label.
|
|
438
|
+
if ('accessibilityLabel' in patch.changes) {
|
|
439
|
+
assertText(patch.changes.accessibilityLabel, `${path}.accessibilityLabel`);
|
|
440
|
+
}
|
|
441
|
+
// OneKey patch: partial updates may refer to an existing height but still require a valid policy.
|
|
442
|
+
if ('heightRounding' in patch.changes && patch.changes.heightRounding !== undefined && !['floor', 'nearest'].includes(patch.changes.heightRounding)) fail(`${path}.heightRounding`, 'must be floor or nearest');
|
|
400
443
|
if (patch.changes.revision !== undefined && (!Number.isSafeInteger(patch.changes.revision) || patch.changes.revision < 0)) {
|
|
401
444
|
fail(`${path}.revision`, 'must be a non-negative safe integer');
|
|
402
445
|
}
|