@onekeyfe/react-native-native-list 3.0.105 → 3.0.106
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,236 @@
|
|
|
1
|
+
// OneKey patch: share a bounded URI-only window across renderers. No bitmap or
|
|
2
|
+
// account-specific data enters list snapshots or the UI runtime.
|
|
3
|
+
import type { ImageSource, LeadingVisual, RowModel, RowPatch } from './models';
|
|
4
|
+
|
|
5
|
+
export type AvatarCandidate = Readonly<{
|
|
6
|
+
source: ImageSource;
|
|
7
|
+
priority: 0 | 1 | 2;
|
|
8
|
+
}>;
|
|
9
|
+
const PREFIX = 'onekey-avatar://blockie/v1/';
|
|
10
|
+
const MAX_CANDIDATES = 64;
|
|
11
|
+
const IMAGE_PATCH_FIELDS = [
|
|
12
|
+
'leading',
|
|
13
|
+
'secondaryLeading',
|
|
14
|
+
'image',
|
|
15
|
+
'networkImage',
|
|
16
|
+
'thumbnail',
|
|
17
|
+
'visual',
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export function avatarPrefetchWindow(
|
|
21
|
+
rows: readonly RowModel[],
|
|
22
|
+
first: number,
|
|
23
|
+
last: number,
|
|
24
|
+
direction: number,
|
|
25
|
+
resolveRow?: (row: RowModel) => RowModel
|
|
26
|
+
): AvatarCandidate[] {
|
|
27
|
+
if (first < 0 || last < first || first >= rows.length) return [];
|
|
28
|
+
last = Math.min(last, rows.length - 1);
|
|
29
|
+
const result: AvatarCandidate[] = [];
|
|
30
|
+
const seen = new Set<string>();
|
|
31
|
+
let limit = MAX_CANDIDATES;
|
|
32
|
+
const add = (source: ImageSource | undefined, priority: 0 | 1 | 2) => {
|
|
33
|
+
if (
|
|
34
|
+
!source?.uri.startsWith(PREFIX) ||
|
|
35
|
+
source.cachePolicy === 'none' ||
|
|
36
|
+
seen.has(source.uri) ||
|
|
37
|
+
result.length >= limit
|
|
38
|
+
)
|
|
39
|
+
return;
|
|
40
|
+
seen.add(source.uri);
|
|
41
|
+
result.push({ source, priority });
|
|
42
|
+
};
|
|
43
|
+
const visual = (value: LeadingVisual | undefined, priority: 0 | 1 | 2) => {
|
|
44
|
+
if (!value) return;
|
|
45
|
+
if ('image' in value) add(value.image, priority);
|
|
46
|
+
if ('networkImage' in value) add(value.networkImage, priority);
|
|
47
|
+
if ('images' in value)
|
|
48
|
+
value.images.forEach((image) => add(image, priority));
|
|
49
|
+
if ('overlays' in value)
|
|
50
|
+
value.overlays?.forEach((overlay) => add(overlay.image, priority));
|
|
51
|
+
};
|
|
52
|
+
const row = (value: RowModel, priority: 0 | 1 | 2) => {
|
|
53
|
+
value = resolveRow?.(value) ?? value;
|
|
54
|
+
if ('leading' in value) visual(value.leading, priority);
|
|
55
|
+
if ('secondaryLeading' in value) visual(value.secondaryLeading, priority);
|
|
56
|
+
if ('image' in value) add(value.image, priority);
|
|
57
|
+
if ('networkImage' in value) add(value.networkImage, priority);
|
|
58
|
+
if ('thumbnail' in value) add(value.thumbnail, priority);
|
|
59
|
+
if (value.type === 'walletGroup') {
|
|
60
|
+
visual(value.parent.leading, priority);
|
|
61
|
+
for (const child of value.children.slice(0, MAX_CANDIDATES)) {
|
|
62
|
+
if (result.length >= limit) break;
|
|
63
|
+
visual(child.leading, priority);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
// Bound row examination too: a giant non-avatar group must not scan all data.
|
|
68
|
+
for (let index = first; index <= last && index < first + 64; index += 1)
|
|
69
|
+
row(rows[index], 0);
|
|
70
|
+
const span = Math.min(24, (last - first + 1) * 2);
|
|
71
|
+
const step = direction < 0 ? -1 : 1;
|
|
72
|
+
const aheadStart = step > 0 ? last + 1 : first - 1;
|
|
73
|
+
const aheadLimit = Math.min(MAX_CANDIDATES, result.length + 24);
|
|
74
|
+
limit = aheadLimit;
|
|
75
|
+
for (
|
|
76
|
+
let distance = 0;
|
|
77
|
+
distance < span && result.length < aheadLimit;
|
|
78
|
+
distance += 1
|
|
79
|
+
) {
|
|
80
|
+
const index = aheadStart + distance * step;
|
|
81
|
+
if (index < 0 || index >= rows.length) break;
|
|
82
|
+
row(rows[index], 1);
|
|
83
|
+
}
|
|
84
|
+
const behindStart = step > 0 ? first - 1 : last + 1;
|
|
85
|
+
const behindLimit = Math.min(MAX_CANDIDATES, result.length + 8);
|
|
86
|
+
limit = behindLimit;
|
|
87
|
+
for (
|
|
88
|
+
let distance = 0;
|
|
89
|
+
distance < Math.min(8, span) && result.length < behindLimit;
|
|
90
|
+
distance += 1
|
|
91
|
+
) {
|
|
92
|
+
const index = behindStart - distance * step;
|
|
93
|
+
if (index < 0 || index >= rows.length) break;
|
|
94
|
+
row(rows[index], 2);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// OneKey patch: imperative image patches update a sparse prefetch overlay, not
|
|
100
|
+
// the readonly prop or a full cloned snapshot. The key index is rebuilt only for
|
|
101
|
+
// a complete snapshot; ordinary balance patches retain no extra row copies.
|
|
102
|
+
export class NativeAvatarPrefetchModel {
|
|
103
|
+
private rows: readonly RowModel[];
|
|
104
|
+
private rowByKey: Map<string, RowModel>;
|
|
105
|
+
private readonly imageRows = new Map<string, RowModel>();
|
|
106
|
+
|
|
107
|
+
constructor(rows: readonly RowModel[]) {
|
|
108
|
+
this.rows = rows;
|
|
109
|
+
this.rowByKey = new Map(rows.map((row) => [row.key, row]));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
replaceSnapshot(rows: readonly RowModel[]) {
|
|
113
|
+
this.rows = rows;
|
|
114
|
+
this.rowByKey = new Map(rows.map((row) => [row.key, row]));
|
|
115
|
+
this.imageRows.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
applyPatches(patches: readonly RowPatch[], dispatch?: () => void): boolean {
|
|
119
|
+
// Native methods have no acknowledgement. Mirror only dispatched batches;
|
|
120
|
+
// a missing host or a synchronous serialization/dispatch failure changes nothing.
|
|
121
|
+
if (!dispatch) return false;
|
|
122
|
+
dispatch();
|
|
123
|
+
const seen = new Set<string>();
|
|
124
|
+
for (const patch of patches) {
|
|
125
|
+
const row = this.rowByKey.get(patch.key);
|
|
126
|
+
// Both native renderers reject the whole batch for an unknown key/type.
|
|
127
|
+
if (!row || row.type !== patch.type || seen.has(patch.key)) return false;
|
|
128
|
+
seen.add(patch.key);
|
|
129
|
+
}
|
|
130
|
+
let changed = false;
|
|
131
|
+
for (const patch of patches) {
|
|
132
|
+
const changes: Readonly<Record<string, unknown>> = patch.changes;
|
|
133
|
+
let imageChanges: Record<string, unknown> | undefined;
|
|
134
|
+
for (const key of IMAGE_PATCH_FIELDS) {
|
|
135
|
+
if (changes[key] !== undefined) {
|
|
136
|
+
imageChanges ??= {};
|
|
137
|
+
imageChanges[key] = changes[key];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// JSON.stringify omits undefined top-level fields. Native therefore keeps
|
|
141
|
+
// them unchanged; an explicit replacement visual without image clears it.
|
|
142
|
+
if (!imageChanges) continue;
|
|
143
|
+
const row =
|
|
144
|
+
this.imageRows.get(patch.key) ?? this.rowByKey.get(patch.key)!;
|
|
145
|
+
// Preserve the validated row discriminant, matching applyRowPatches' shallow merge.
|
|
146
|
+
this.imageRows.set(patch.key, {
|
|
147
|
+
...row,
|
|
148
|
+
...imageChanges,
|
|
149
|
+
key: row.key,
|
|
150
|
+
type: row.type,
|
|
151
|
+
} as RowModel);
|
|
152
|
+
changed = true;
|
|
153
|
+
}
|
|
154
|
+
return changed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
window(first: number, last: number, direction: number): AvatarCandidate[] {
|
|
158
|
+
return avatarPrefetchWindow(
|
|
159
|
+
this.rows,
|
|
160
|
+
first,
|
|
161
|
+
last,
|
|
162
|
+
direction,
|
|
163
|
+
(row) => this.imageRows.get(row.key) ?? row
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Native preload has no cancellation token. One in-flight source is the bound;
|
|
169
|
+
// direction changes replace all not-yet-started work rather than appending it.
|
|
170
|
+
export class NativeAvatarPrefetchQueue {
|
|
171
|
+
private pending: ImageSource[] = [];
|
|
172
|
+
private activeUri: string | undefined;
|
|
173
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
174
|
+
private disposed = false;
|
|
175
|
+
private readonly recent = new Map<string, number>();
|
|
176
|
+
|
|
177
|
+
constructor(
|
|
178
|
+
private readonly preload: (source: ImageSource) => Promise<boolean>
|
|
179
|
+
) {}
|
|
180
|
+
|
|
181
|
+
update(candidates: readonly AvatarCandidate[]) {
|
|
182
|
+
if (this.disposed) return;
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
this.pending = candidates
|
|
185
|
+
.filter(
|
|
186
|
+
({ source, priority }) =>
|
|
187
|
+
priority !== 0 &&
|
|
188
|
+
source.uri !== this.activeUri &&
|
|
189
|
+
now - (this.recent.get(source.uri) ?? 0) > 30_000
|
|
190
|
+
)
|
|
191
|
+
.map(({ source }) => source);
|
|
192
|
+
this.schedule();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
dispose() {
|
|
196
|
+
this.disposed = true;
|
|
197
|
+
this.pending = [];
|
|
198
|
+
this.recent.clear();
|
|
199
|
+
if (this.timer !== undefined) clearTimeout(this.timer);
|
|
200
|
+
this.timer = undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private schedule() {
|
|
204
|
+
if (
|
|
205
|
+
this.disposed ||
|
|
206
|
+
this.activeUri ||
|
|
207
|
+
this.timer !== undefined ||
|
|
208
|
+
!this.pending.length
|
|
209
|
+
)
|
|
210
|
+
return;
|
|
211
|
+
// Yield between short batches without imposing a frame-per-image throughput cap.
|
|
212
|
+
this.timer = setTimeout(() => {
|
|
213
|
+
this.timer = undefined;
|
|
214
|
+
const source = this.pending.shift();
|
|
215
|
+
if (!source || this.disposed) return;
|
|
216
|
+
this.activeUri = source.uri;
|
|
217
|
+
Promise.resolve()
|
|
218
|
+
.then(() => this.preload(source))
|
|
219
|
+
.then((success) => {
|
|
220
|
+
if (success && !this.disposed) {
|
|
221
|
+
this.recent.delete(source.uri);
|
|
222
|
+
this.recent.set(source.uri, Date.now());
|
|
223
|
+
if (this.recent.size > 128)
|
|
224
|
+
this.recent.delete(this.recent.keys().next().value as string);
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
.catch(() => {
|
|
228
|
+
/* Visible loading retains its own failure/retry behavior. */
|
|
229
|
+
})
|
|
230
|
+
.finally(() => {
|
|
231
|
+
this.activeUri = undefined;
|
|
232
|
+
this.schedule();
|
|
233
|
+
});
|
|
234
|
+
}, 0);
|
|
235
|
+
}
|
|
236
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -22,6 +22,9 @@ export type ImageSource = Readonly<{
|
|
|
22
22
|
optimizeTos?: boolean;
|
|
23
23
|
overscan?: number;
|
|
24
24
|
loadingStrategy?: ImageLoadingStrategy;
|
|
25
|
+
// OneKey patch: fallbackUri is Web-only; retryTimes opts all platforms into terminal retries.
|
|
26
|
+
fallbackUri?: string;
|
|
27
|
+
retryTimes?: number;
|
|
25
28
|
}>;
|
|
26
29
|
|
|
27
30
|
export type BadgeModel = Readonly<{
|
|
@@ -30,7 +33,34 @@ export type BadgeModel = Readonly<{
|
|
|
30
33
|
tone?: 'neutral' | 'info' | 'success' | 'warning' | 'danger';
|
|
31
34
|
}>;
|
|
32
35
|
|
|
36
|
+
// OneKey patch: keep selector decoration and text data serializable.
|
|
37
|
+
export type SelectorTextSegment = Readonly<{
|
|
38
|
+
text: string;
|
|
39
|
+
textSegments?: readonly ValueTextSegment[];
|
|
40
|
+
tone?: TextTone | 'disabled' | 'caution';
|
|
41
|
+
separatorBefore?: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
export type VisualOverlay = Readonly<{
|
|
44
|
+
position: 'topLeft' | 'bottomRight';
|
|
45
|
+
size?: number;
|
|
46
|
+
width?: number;
|
|
47
|
+
height?: number;
|
|
48
|
+
padding?: number;
|
|
49
|
+
offset?: number;
|
|
50
|
+
offsetX?: number;
|
|
51
|
+
offsetY?: number;
|
|
52
|
+
image?: ImageSource;
|
|
53
|
+
name?: string;
|
|
54
|
+
text?: string;
|
|
55
|
+
tintColor?: string;
|
|
56
|
+
backgroundColor?: string;
|
|
57
|
+
}>;
|
|
58
|
+
|
|
33
59
|
type VisualWithImage = Readonly<{
|
|
60
|
+
fallbackIcon?: Readonly<{ name: string; tintColor?: string }>;
|
|
61
|
+
overlays?: readonly VisualOverlay[];
|
|
62
|
+
borderStyle?: 'dashed';
|
|
63
|
+
borderColor?: string;
|
|
34
64
|
image?: ImageSource;
|
|
35
65
|
fallbackText?: string;
|
|
36
66
|
backgroundColor?: string;
|
|
@@ -70,8 +100,16 @@ export type SelectionTarget =
|
|
|
70
100
|
| Readonly<{ scope: 'section'; sectionKey: string }>
|
|
71
101
|
| Readonly<{ scope: 'list' }>;
|
|
72
102
|
|
|
103
|
+
// OneKey patch: preserve compact very-small-balance digit runs.
|
|
104
|
+
export type ValueTextSegment = Readonly<{ text: string; style?: 'subscript' }>;
|
|
105
|
+
|
|
73
106
|
export type TrailingAccessory =
|
|
74
|
-
| Readonly<{
|
|
107
|
+
| Readonly<{
|
|
108
|
+
kind: 'value';
|
|
109
|
+
text: string;
|
|
110
|
+
secondary?: boolean;
|
|
111
|
+
textSegments?: readonly ValueTextSegment[];
|
|
112
|
+
}>
|
|
75
113
|
| Readonly<{
|
|
76
114
|
kind: 'valuePair';
|
|
77
115
|
primary: string;
|
|
@@ -108,6 +146,9 @@ export type TrailingAccessory =
|
|
|
108
146
|
tintColor?: string;
|
|
109
147
|
disabled?: boolean;
|
|
110
148
|
actionKey?: string;
|
|
149
|
+
testID?: string;
|
|
150
|
+
hoverActionKey?: string;
|
|
151
|
+
accessibilityLabel?: string;
|
|
111
152
|
}>
|
|
112
153
|
| Readonly<{ kind: 'spinner' }>
|
|
113
154
|
| Readonly<{ kind: 'progress'; value: number }>;
|
|
@@ -120,6 +161,15 @@ export type FooterAction = Readonly<{
|
|
|
120
161
|
}>;
|
|
121
162
|
|
|
122
163
|
export type RowBase = Readonly<{
|
|
164
|
+
// OneKey patch: preserve measured selector geometry without changing defaults.
|
|
165
|
+
height?: number;
|
|
166
|
+
// OneKey patch: retain the source Android list's measured fractional-DP rounding.
|
|
167
|
+
heightRounding?: 'floor' | 'nearest';
|
|
168
|
+
testID?: string;
|
|
169
|
+
opacity?: number;
|
|
170
|
+
backgroundColor?: string;
|
|
171
|
+
backgroundFullWidth?: boolean;
|
|
172
|
+
pressDisabled?: boolean;
|
|
123
173
|
key: string;
|
|
124
174
|
revision?: number;
|
|
125
175
|
sectionKey?: string;
|
|
@@ -136,6 +186,11 @@ export type RowBase = Readonly<{
|
|
|
136
186
|
export type IdentityRow = RowBase &
|
|
137
187
|
Readonly<{
|
|
138
188
|
type: 'identity';
|
|
189
|
+
// OneKey patch: UTF-16 ranges match Fuse indices and native attributed strings.
|
|
190
|
+
titleMatch?: readonly Readonly<{ start: number; end: number }>[];
|
|
191
|
+
titleActionKey?: string;
|
|
192
|
+
titleActionOnHover?: boolean;
|
|
193
|
+
subtitleSegments?: readonly SelectorTextSegment[];
|
|
139
194
|
presentation?: 'walletSidebar' | 'accountSelector' | 'networkSelector';
|
|
140
195
|
leading: LeadingVisual;
|
|
141
196
|
leadingAction?: Extract<TrailingAccessory, { kind: 'icon' }>;
|
|
@@ -257,6 +312,12 @@ export type MetricCardRow = RowBase &
|
|
|
257
312
|
export type SectionHeaderRow = RowBase &
|
|
258
313
|
Readonly<{
|
|
259
314
|
type: 'sectionHeader';
|
|
315
|
+
// OneKey patch: title help stays independent of section selection.
|
|
316
|
+
sticky?: boolean;
|
|
317
|
+
valueActionTestID?: string;
|
|
318
|
+
valueSegments?: readonly ValueTextSegment[];
|
|
319
|
+
titleActionKey?: string;
|
|
320
|
+
titleActionOnHover?: boolean;
|
|
260
321
|
sectionKey: string;
|
|
261
322
|
presentation?: 'networkSelector';
|
|
262
323
|
indexTitle?: string;
|
|
@@ -291,6 +352,13 @@ export type SystemRow = RowBase &
|
|
|
291
352
|
message: string;
|
|
292
353
|
actionKey: string;
|
|
293
354
|
}>
|
|
355
|
+
| Readonly<{
|
|
356
|
+
type: 'system';
|
|
357
|
+
variant: 'warning';
|
|
358
|
+
title: string;
|
|
359
|
+
message: string;
|
|
360
|
+
borderColor?: string;
|
|
361
|
+
}>
|
|
294
362
|
| Readonly<{ type: 'system'; variant: 'noMatch'; message: string }>
|
|
295
363
|
| Readonly<{ type: 'system'; variant: 'end'; message?: string }>
|
|
296
364
|
| Readonly<{ type: 'system'; variant: 'spacer'; height: number }>
|
|
@@ -310,6 +378,11 @@ export type RowModel =
|
|
|
310
378
|
| SystemRow;
|
|
311
379
|
|
|
312
380
|
export type NativeListTheme = Readonly<{
|
|
381
|
+
// OneKey patch: explicit selector controls use the original semantic theme tokens.
|
|
382
|
+
checkboxBackground?: string;
|
|
383
|
+
checkboxBorder?: string;
|
|
384
|
+
checkboxIcon?: string;
|
|
385
|
+
cautionBackground?: string;
|
|
313
386
|
background: string;
|
|
314
387
|
rowBackground: string;
|
|
315
388
|
rowSelectedBackground: string;
|
|
@@ -329,6 +402,8 @@ export type NativeListTheme = Readonly<{
|
|
|
329
402
|
inverseBackground?: string;
|
|
330
403
|
inverseText?: string;
|
|
331
404
|
info?: string;
|
|
405
|
+
// OneKey patch: match the existing account warning address tone.
|
|
406
|
+
caution?: string;
|
|
332
407
|
}>;
|
|
333
408
|
|
|
334
409
|
export type SectionIndexConfig = Readonly<{
|
|
@@ -369,7 +444,20 @@ export type NativeListSnapshot = Readonly<{
|
|
|
369
444
|
theme?: NativeListTheme;
|
|
370
445
|
}>;
|
|
371
446
|
|
|
372
|
-
|
|
447
|
+
// OneKey patch: include selector-only mutable presentation fields.
|
|
448
|
+
// type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator';
|
|
449
|
+
// OneKey patch: balance patches also refresh the existing row's spoken content.
|
|
450
|
+
// type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator' | 'height' | 'heightRounding' | 'opacity' | 'pressDisabled';
|
|
451
|
+
type CommonPatchFields =
|
|
452
|
+
| 'revision'
|
|
453
|
+
| 'disabled'
|
|
454
|
+
| 'selected'
|
|
455
|
+
| 'separator'
|
|
456
|
+
| 'height'
|
|
457
|
+
| 'heightRounding'
|
|
458
|
+
| 'opacity'
|
|
459
|
+
| 'pressDisabled'
|
|
460
|
+
| 'accessibilityLabel';
|
|
373
461
|
|
|
374
462
|
export type RowPatch =
|
|
375
463
|
| Readonly<{
|
|
@@ -387,6 +475,10 @@ export type RowPatch =
|
|
|
387
475
|
| 'trailing'
|
|
388
476
|
| 'leading'
|
|
389
477
|
| 'leadingAction'
|
|
478
|
+
| 'titleMatch'
|
|
479
|
+
| 'titleActionKey'
|
|
480
|
+
| 'titleActionOnHover'
|
|
481
|
+
| 'subtitleSegments'
|
|
390
482
|
>
|
|
391
483
|
>;
|
|
392
484
|
}>
|
|
@@ -501,6 +593,8 @@ export type RowPatch =
|
|
|
501
593
|
| 'subtitle'
|
|
502
594
|
| 'value'
|
|
503
595
|
| 'valueActionKey'
|
|
596
|
+
| 'titleActionKey'
|
|
597
|
+
| 'titleActionOnHover'
|
|
504
598
|
| 'titleIcon'
|
|
505
599
|
| 'valueIcon'
|
|
506
600
|
| 'checkbox'
|
|
@@ -565,11 +659,8 @@ export type RowActionEvent = Readonly<{
|
|
|
565
659
|
}>;
|
|
566
660
|
|
|
567
661
|
export type ActionAnchorInvalidationReason =
|
|
568
|
-
|
|
569
|
-
| 'rebind'
|
|
570
|
-
| 'snapshot'
|
|
571
|
-
| 'layout'
|
|
572
|
-
| 'destroy';
|
|
662
|
+
// OneKey patch: close web title tooltips when their native list target is left.
|
|
663
|
+
'pointerLeave' | 'scroll' | 'rebind' | 'snapshot' | 'layout' | 'destroy';
|
|
573
664
|
|
|
574
665
|
export type ActionAnchorInvalidatedEvent = Readonly<{
|
|
575
666
|
token: string;
|
package/src/validation.ts
CHANGED
|
@@ -214,6 +214,69 @@ function assertLeadingVisual(
|
|
|
214
214
|
assertImage(visual.image, `${path}.image`);
|
|
215
215
|
assertVisualShape(visual.shape, `${path}.shape`);
|
|
216
216
|
assertText(visual.cornerIcon?.name, `${path}.cornerIcon.name`);
|
|
217
|
+
// OneKey patch: validate optional selector overlays at the JSON boundary.
|
|
218
|
+
assertText(visual.fallbackIcon?.name, `${path}.fallbackIcon.name`);
|
|
219
|
+
if ((visual.overlays?.length ?? 0) > 2)
|
|
220
|
+
fail(`${path}.overlays`, 'supports at most two overlays');
|
|
221
|
+
visual.overlays?.forEach((overlay, index) => {
|
|
222
|
+
if (!['topLeft', 'bottomRight'].includes(overlay.position))
|
|
223
|
+
fail(
|
|
224
|
+
`${path}.overlays[${index}].position`,
|
|
225
|
+
'must be topLeft or bottomRight'
|
|
226
|
+
);
|
|
227
|
+
if (
|
|
228
|
+
overlay.size !== undefined &&
|
|
229
|
+
(overlay.size <= 0 || overlay.size > 40)
|
|
230
|
+
)
|
|
231
|
+
fail(`${path}.overlays[${index}].size`, 'must be within 1...40');
|
|
232
|
+
if (
|
|
233
|
+
overlay.padding !== undefined &&
|
|
234
|
+
(!Number.isFinite(overlay.padding) ||
|
|
235
|
+
overlay.padding < 0 ||
|
|
236
|
+
overlay.padding * 2 >= (overlay.size ?? 20))
|
|
237
|
+
)
|
|
238
|
+
fail(
|
|
239
|
+
`${path}.overlays[${index}].padding`,
|
|
240
|
+
'must fit inside the overlay'
|
|
241
|
+
);
|
|
242
|
+
if (
|
|
243
|
+
overlay.offset !== undefined &&
|
|
244
|
+
(!Number.isFinite(overlay.offset) ||
|
|
245
|
+
overlay.offset < 0 ||
|
|
246
|
+
overlay.offset > 20)
|
|
247
|
+
)
|
|
248
|
+
fail(`${path}.overlays[${index}].offset`, 'must be within 0...20');
|
|
249
|
+
for (const field of ['width', 'height'] as const) {
|
|
250
|
+
const value = overlay[field];
|
|
251
|
+
if (
|
|
252
|
+
value !== undefined &&
|
|
253
|
+
(!Number.isFinite(value) || value <= 0 || value > 40)
|
|
254
|
+
)
|
|
255
|
+
fail(`${path}.overlays[${index}].${field}`, 'must be within 1...40');
|
|
256
|
+
}
|
|
257
|
+
for (const field of ['offsetX', 'offsetY'] as const) {
|
|
258
|
+
const value = overlay[field];
|
|
259
|
+
if (
|
|
260
|
+
value !== undefined &&
|
|
261
|
+
(!Number.isFinite(value) || value < 0 || value > 20)
|
|
262
|
+
)
|
|
263
|
+
fail(`${path}.overlays[${index}].${field}`, 'must be within 0...20');
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
overlay.padding !== undefined &&
|
|
267
|
+
overlay.padding * 2 >=
|
|
268
|
+
Math.min(
|
|
269
|
+
overlay.width ?? overlay.size ?? 20,
|
|
270
|
+
overlay.height ?? overlay.size ?? 20
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
fail(
|
|
274
|
+
`${path}.overlays[${index}].padding`,
|
|
275
|
+
'must fit inside the overlay'
|
|
276
|
+
);
|
|
277
|
+
assertImage(overlay.image, `${path}.overlays[${index}].image`);
|
|
278
|
+
assertText(overlay.text, `${path}.overlays[${index}].text`);
|
|
279
|
+
});
|
|
217
280
|
if (visual.kind === 'token') {
|
|
218
281
|
assertImage(visual.networkImage, `${path}.networkImage`);
|
|
219
282
|
}
|
|
@@ -288,6 +351,20 @@ function assertRow(
|
|
|
288
351
|
path = `rows[${index}]`
|
|
289
352
|
): void {
|
|
290
353
|
assertKey(row.key, `${path}.key`);
|
|
354
|
+
// OneKey patch: explicit dimensions and opacity cannot corrupt list layout.
|
|
355
|
+
if (row.height !== undefined && (row.height < 0 || row.height > 4096))
|
|
356
|
+
fail(`${path}.height`, 'must be within 0...4096');
|
|
357
|
+
if (
|
|
358
|
+
row.heightRounding !== undefined &&
|
|
359
|
+
(row.height === undefined ||
|
|
360
|
+
!['floor', 'nearest'].includes(row.heightRounding))
|
|
361
|
+
)
|
|
362
|
+
fail(
|
|
363
|
+
`${path}.heightRounding`,
|
|
364
|
+
'requires an explicit height and must be floor or nearest'
|
|
365
|
+
);
|
|
366
|
+
if (row.opacity !== undefined && (row.opacity < 0 || row.opacity > 1))
|
|
367
|
+
fail(`${path}.opacity`, 'must be within 0...1');
|
|
291
368
|
if (row.groupId && !row.groupPosition) {
|
|
292
369
|
fail(`${path}.groupPosition`, 'is required when groupId is present');
|
|
293
370
|
}
|
|
@@ -314,6 +391,43 @@ function assertRow(
|
|
|
314
391
|
}
|
|
315
392
|
assertText(row.title, `${path}.title`);
|
|
316
393
|
assertText(row.subtitle, `${path}.subtitle`);
|
|
394
|
+
// OneKey patch: selector text segments truncate independently.
|
|
395
|
+
row.subtitleSegments?.forEach((segment, segmentIndex) => {
|
|
396
|
+
assertText(
|
|
397
|
+
segment.text,
|
|
398
|
+
`${path}.subtitleSegments[${segmentIndex}].text`
|
|
399
|
+
);
|
|
400
|
+
if (
|
|
401
|
+
segment.tone !== undefined &&
|
|
402
|
+
![
|
|
403
|
+
'primary',
|
|
404
|
+
'secondary',
|
|
405
|
+
'disabled',
|
|
406
|
+
'caution',
|
|
407
|
+
'positive',
|
|
408
|
+
'negative',
|
|
409
|
+
].includes(segment.tone)
|
|
410
|
+
)
|
|
411
|
+
fail(
|
|
412
|
+
`${path}.subtitleSegments[${segmentIndex}].tone`,
|
|
413
|
+
'invalid selector text tone'
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
let previousMatchEnd = 0;
|
|
417
|
+
row.titleMatch?.forEach((match) => {
|
|
418
|
+
if (
|
|
419
|
+
!Number.isInteger(match.start) ||
|
|
420
|
+
!Number.isInteger(match.end) ||
|
|
421
|
+
match.start < previousMatchEnd ||
|
|
422
|
+
match.end <= match.start ||
|
|
423
|
+
match.end > row.title.length
|
|
424
|
+
)
|
|
425
|
+
fail(
|
|
426
|
+
`${path}.titleMatch`,
|
|
427
|
+
'must contain ordered, non-overlapping UTF-16 ranges inside title'
|
|
428
|
+
);
|
|
429
|
+
previousMatchEnd = match.end;
|
|
430
|
+
});
|
|
317
431
|
assertText(row.tertiary, `${path}.tertiary`);
|
|
318
432
|
if (
|
|
319
433
|
row.tertiaryTone !== undefined &&
|
|
@@ -470,13 +584,17 @@ function assertRow(
|
|
|
470
584
|
break;
|
|
471
585
|
case 'system':
|
|
472
586
|
if (
|
|
473
|
-
|
|
587
|
+
// OneKey patch: deprecated-wallet warnings retain the original scrolling semantics.
|
|
588
|
+
!['loading', 'retry', 'noMatch', 'end', 'spacer', 'warning'].includes(
|
|
589
|
+
row.variant
|
|
590
|
+
)
|
|
474
591
|
) {
|
|
475
592
|
fail(
|
|
476
593
|
`${path}.variant`,
|
|
477
|
-
'must be loading, retry, noMatch, end, or
|
|
594
|
+
'must be loading, retry, noMatch, end, spacer, or warning'
|
|
478
595
|
);
|
|
479
596
|
}
|
|
597
|
+
if (row.variant === 'warning') assertText(row.title, `${path}.title`);
|
|
480
598
|
if (row.variant !== 'spacer') {
|
|
481
599
|
assertText(row.message, `${path}.message`);
|
|
482
600
|
}
|
|
@@ -649,6 +767,17 @@ export function validateSnapshot(
|
|
|
649
767
|
|
|
650
768
|
function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
651
769
|
const path = `patches[${index}].changes`;
|
|
770
|
+
// OneKey patch: partial balance updates retain a valid, current accessibility label.
|
|
771
|
+
if ('accessibilityLabel' in patch.changes) {
|
|
772
|
+
assertText(patch.changes.accessibilityLabel, `${path}.accessibilityLabel`);
|
|
773
|
+
}
|
|
774
|
+
// OneKey patch: partial updates may refer to an existing height but still require a valid policy.
|
|
775
|
+
if (
|
|
776
|
+
'heightRounding' in patch.changes &&
|
|
777
|
+
patch.changes.heightRounding !== undefined &&
|
|
778
|
+
!['floor', 'nearest'].includes(patch.changes.heightRounding)
|
|
779
|
+
)
|
|
780
|
+
fail(`${path}.heightRounding`, 'must be floor or nearest');
|
|
652
781
|
if (
|
|
653
782
|
patch.changes.revision !== undefined &&
|
|
654
783
|
(!Number.isSafeInteger(patch.changes.revision) ||
|