@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.
Files changed (45) hide show
  1. package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +16 -3
  2. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +6 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +671 -52
  4. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +282 -54
  5. package/ios/HybridNativeList.swift +8 -2
  6. package/ios/NativeListCell.swift +630 -31
  7. package/ios/NativeListDesignAssets.swift +24 -1
  8. package/ios/RNCNativeListView.swift +216 -42
  9. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/Contents.json +15 -0
  10. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/icon.svg +14 -0
  11. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/Contents.json +15 -0
  12. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/icon.svg +12 -0
  13. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/Contents.json +15 -0
  14. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/icon.svg +6 -0
  15. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/Contents.json +15 -0
  16. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/icon.svg +15 -0
  17. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/Contents.json +15 -0
  18. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/icon.svg +3 -0
  19. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/Contents.json +15 -0
  20. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/icon.svg +7 -0
  21. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/Contents.json +15 -0
  22. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/icon.svg +18 -0
  23. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/Contents.json +15 -0
  24. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/icon.svg +7 -0
  25. package/lib/module/NativeList.js +102 -8
  26. package/lib/module/NativeList.web.js +20 -2
  27. package/lib/module/avatarPrefetch.js +174 -0
  28. package/lib/module/validation.js +45 -2
  29. package/lib/module/web/NativeListAvatarWorker.js +542 -0
  30. package/lib/module/web/NativeListWebAvatarCache.js +267 -0
  31. package/lib/module/web/NativeListWebEngine.js +958 -47
  32. package/lib/typescript/src/NativeList.web.d.ts +2 -2
  33. package/lib/typescript/src/avatarPrefetch.d.ts +28 -0
  34. package/lib/typescript/src/models.d.ts +72 -4
  35. package/lib/typescript/src/web/NativeListWebAvatarCache.d.ts +6 -0
  36. package/lib/typescript/src/web/NativeListWebEngine.d.ts +10 -1
  37. package/package.json +2 -2
  38. package/src/NativeList.tsx +149 -13
  39. package/src/NativeList.web.tsx +30 -3
  40. package/src/avatarPrefetch.ts +236 -0
  41. package/src/models.ts +98 -7
  42. package/src/validation.ts +131 -2
  43. package/src/web/NativeListAvatarWorker.js +567 -0
  44. package/src/web/NativeListWebAvatarCache.ts +314 -0
  45. package/src/web/NativeListWebEngine.ts +1409 -60
@@ -1,11 +1,11 @@
1
1
  import React from 'react';
2
2
  import type { NativeListProps } from './NativeList.types';
3
- import type { NativeListSnapshot } from './models';
3
+ import type { NativeListSnapshot, RowPatch } from './models';
4
4
  export type { NativeListProps, NativeListRef } from './NativeList.types';
5
5
  export type { ActionAnchorState, ScrollAlignment, ScrollPositionOptions, ScrollToEndParams, ScrollToIndexFailedInfo, ScrollToIndexParams, ScrollToItemParams, ScrollToKeyParams, ScrollToLocationParams, ScrollToOffsetParams, } from './NativeList.types';
6
6
  export declare const NativeList: React.ForwardRefExoticComponent<NativeListProps & React.RefAttributes<Readonly<{
7
7
  applySnapshot(snapshot: NativeListSnapshot): void;
8
- applyPatches(patches: readonly import("./models").RowPatch[]): void;
8
+ applyPatches(patches: readonly RowPatch[]): void;
9
9
  reconcileSelection(selectedKeys: readonly string[]): void;
10
10
  scrollToKey: (paramsOrKey: import("./NativeList.types").ScrollToKeyParams | string, animated?: boolean, alignment?: import("./NativeList.types").ScrollAlignment) => void;
11
11
  scrollToIndex: (paramsOrIndex: import("./NativeList.types").ScrollToIndexParams | number, animated?: boolean, alignment?: import("./NativeList.types").ScrollAlignment) => void;
@@ -0,0 +1,28 @@
1
+ import type { ImageSource, RowModel, RowPatch } from './models';
2
+ export type AvatarCandidate = Readonly<{
3
+ source: ImageSource;
4
+ priority: 0 | 1 | 2;
5
+ }>;
6
+ export declare function avatarPrefetchWindow(rows: readonly RowModel[], first: number, last: number, direction: number, resolveRow?: (row: RowModel) => RowModel): AvatarCandidate[];
7
+ export declare class NativeAvatarPrefetchModel {
8
+ private rows;
9
+ private rowByKey;
10
+ private readonly imageRows;
11
+ constructor(rows: readonly RowModel[]);
12
+ replaceSnapshot(rows: readonly RowModel[]): void;
13
+ applyPatches(patches: readonly RowPatch[], dispatch?: () => void): boolean;
14
+ window(first: number, last: number, direction: number): AvatarCandidate[];
15
+ }
16
+ export declare class NativeAvatarPrefetchQueue {
17
+ private readonly preload;
18
+ private pending;
19
+ private activeUri;
20
+ private timer;
21
+ private disposed;
22
+ private readonly recent;
23
+ constructor(preload: (source: ImageSource) => Promise<boolean>);
24
+ update(candidates: readonly AvatarCandidate[]): void;
25
+ dispose(): void;
26
+ private schedule;
27
+ }
28
+ //# sourceMappingURL=avatarPrefetch.d.ts.map
@@ -20,13 +20,43 @@ export type ImageSource = Readonly<{
20
20
  optimizeTos?: boolean;
21
21
  overscan?: number;
22
22
  loadingStrategy?: ImageLoadingStrategy;
23
+ fallbackUri?: string;
24
+ retryTimes?: number;
23
25
  }>;
24
26
  export type BadgeModel = Readonly<{
25
27
  key: string;
26
28
  text: string;
27
29
  tone?: 'neutral' | 'info' | 'success' | 'warning' | 'danger';
28
30
  }>;
31
+ export type SelectorTextSegment = Readonly<{
32
+ text: string;
33
+ textSegments?: readonly ValueTextSegment[];
34
+ tone?: TextTone | 'disabled' | 'caution';
35
+ separatorBefore?: boolean;
36
+ }>;
37
+ export type VisualOverlay = Readonly<{
38
+ position: 'topLeft' | 'bottomRight';
39
+ size?: number;
40
+ width?: number;
41
+ height?: number;
42
+ padding?: number;
43
+ offset?: number;
44
+ offsetX?: number;
45
+ offsetY?: number;
46
+ image?: ImageSource;
47
+ name?: string;
48
+ text?: string;
49
+ tintColor?: string;
50
+ backgroundColor?: string;
51
+ }>;
29
52
  type VisualWithImage = Readonly<{
53
+ fallbackIcon?: Readonly<{
54
+ name: string;
55
+ tintColor?: string;
56
+ }>;
57
+ overlays?: readonly VisualOverlay[];
58
+ borderStyle?: 'dashed';
59
+ borderColor?: string;
30
60
  image?: ImageSource;
31
61
  fallbackText?: string;
32
62
  backgroundColor?: string;
@@ -67,10 +97,15 @@ export type SelectionTarget = Readonly<{
67
97
  }> | Readonly<{
68
98
  scope: 'list';
69
99
  }>;
100
+ export type ValueTextSegment = Readonly<{
101
+ text: string;
102
+ style?: 'subscript';
103
+ }>;
70
104
  export type TrailingAccessory = Readonly<{
71
105
  kind: 'value';
72
106
  text: string;
73
107
  secondary?: boolean;
108
+ textSegments?: readonly ValueTextSegment[];
74
109
  }> | Readonly<{
75
110
  kind: 'valuePair';
76
111
  primary: string;
@@ -109,6 +144,9 @@ export type TrailingAccessory = Readonly<{
109
144
  tintColor?: string;
110
145
  disabled?: boolean;
111
146
  actionKey?: string;
147
+ testID?: string;
148
+ hoverActionKey?: string;
149
+ accessibilityLabel?: string;
112
150
  }> | Readonly<{
113
151
  kind: 'spinner';
114
152
  }> | Readonly<{
@@ -122,6 +160,13 @@ export type FooterAction = Readonly<{
122
160
  disabled?: boolean;
123
161
  }>;
124
162
  export type RowBase = Readonly<{
163
+ height?: number;
164
+ heightRounding?: 'floor' | 'nearest';
165
+ testID?: string;
166
+ opacity?: number;
167
+ backgroundColor?: string;
168
+ backgroundFullWidth?: boolean;
169
+ pressDisabled?: boolean;
125
170
  key: string;
126
171
  revision?: number;
127
172
  sectionKey?: string;
@@ -136,6 +181,13 @@ export type RowBase = Readonly<{
136
181
  }>;
137
182
  export type IdentityRow = RowBase & Readonly<{
138
183
  type: 'identity';
184
+ titleMatch?: readonly Readonly<{
185
+ start: number;
186
+ end: number;
187
+ }>[];
188
+ titleActionKey?: string;
189
+ titleActionOnHover?: boolean;
190
+ subtitleSegments?: readonly SelectorTextSegment[];
139
191
  presentation?: 'walletSidebar' | 'accountSelector' | 'networkSelector';
140
192
  leading: LeadingVisual;
141
193
  leadingAction?: Extract<TrailingAccessory, {
@@ -243,6 +295,11 @@ export type MetricCardRow = RowBase & Readonly<{
243
295
  }>;
244
296
  export type SectionHeaderRow = RowBase & Readonly<{
245
297
  type: 'sectionHeader';
298
+ sticky?: boolean;
299
+ valueActionTestID?: string;
300
+ valueSegments?: readonly ValueTextSegment[];
301
+ titleActionKey?: string;
302
+ titleActionOnHover?: boolean;
246
303
  sectionKey: string;
247
304
  presentation?: 'networkSelector';
248
305
  indexTitle?: string;
@@ -284,6 +341,12 @@ export type SystemRow = RowBase & (Readonly<{
284
341
  variant: 'retry';
285
342
  message: string;
286
343
  actionKey: string;
344
+ }> | Readonly<{
345
+ type: 'system';
346
+ variant: 'warning';
347
+ title: string;
348
+ message: string;
349
+ borderColor?: string;
287
350
  }> | Readonly<{
288
351
  type: 'system';
289
352
  variant: 'noMatch';
@@ -299,6 +362,10 @@ export type SystemRow = RowBase & (Readonly<{
299
362
  }>);
300
363
  export type RowModel = IdentityRow | WalletGroupRow | RailRow | ActivityRow | MessageRow | DataRow | MediaTileRow | MetricCardRow | SectionHeaderRow | ActionRow | SystemRow;
301
364
  export type NativeListTheme = Readonly<{
365
+ checkboxBackground?: string;
366
+ checkboxBorder?: string;
367
+ checkboxIcon?: string;
368
+ cautionBackground?: string;
302
369
  background: string;
303
370
  rowBackground: string;
304
371
  rowSelectedBackground: string;
@@ -318,6 +385,7 @@ export type NativeListTheme = Readonly<{
318
385
  inverseBackground?: string;
319
386
  inverseText?: string;
320
387
  info?: string;
388
+ caution?: string;
321
389
  }>;
322
390
  export type SectionIndexConfig = Readonly<{
323
391
  enabled: boolean;
@@ -355,11 +423,11 @@ export type NativeListSnapshot = Readonly<{
355
423
  fixedFooter?: ActionRow | SystemRow;
356
424
  theme?: NativeListTheme;
357
425
  }>;
358
- type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator';
426
+ type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator' | 'height' | 'heightRounding' | 'opacity' | 'pressDisabled' | 'accessibilityLabel';
359
427
  export type RowPatch = Readonly<{
360
428
  type: 'identity';
361
429
  key: string;
362
- changes: Partial<Pick<IdentityRow, CommonPatchFields | 'title' | 'subtitle' | 'tertiary' | 'tertiaryTone' | 'badges' | 'trailing' | 'leading' | 'leadingAction'>>;
430
+ changes: Partial<Pick<IdentityRow, CommonPatchFields | 'title' | 'subtitle' | 'tertiary' | 'tertiaryTone' | 'badges' | 'trailing' | 'leading' | 'leadingAction' | 'titleMatch' | 'titleActionKey' | 'titleActionOnHover' | 'subtitleSegments'>>;
363
431
  }> | Readonly<{
364
432
  type: 'rail';
365
433
  key: string;
@@ -387,7 +455,7 @@ export type RowPatch = Readonly<{
387
455
  }> | Readonly<{
388
456
  type: 'sectionHeader';
389
457
  key: string;
390
- changes: Partial<Pick<SectionHeaderRow, CommonPatchFields | 'variant' | 'title' | 'subtitle' | 'value' | 'valueActionKey' | 'titleIcon' | 'valueIcon' | 'checkbox'>>;
458
+ changes: Partial<Pick<SectionHeaderRow, CommonPatchFields | 'variant' | 'title' | 'subtitle' | 'value' | 'valueActionKey' | 'titleActionKey' | 'titleActionOnHover' | 'titleIcon' | 'valueIcon' | 'checkbox'>>;
391
459
  }> | Readonly<{
392
460
  type: 'action';
393
461
  key: string;
@@ -424,7 +492,7 @@ export type RowActionEvent = Readonly<{
424
492
  sectionKey?: string;
425
493
  anchor?: NativeListActionAnchor;
426
494
  }>;
427
- export type ActionAnchorInvalidationReason = 'scroll' | 'rebind' | 'snapshot' | 'layout' | 'destroy';
495
+ export type ActionAnchorInvalidationReason = 'pointerLeave' | 'scroll' | 'rebind' | 'snapshot' | 'layout' | 'destroy';
428
496
  export type ActionAnchorInvalidatedEvent = Readonly<{
429
497
  token: string;
430
498
  reason: ActionAnchorInvalidationReason;
@@ -0,0 +1,6 @@
1
+ export type AvatarLease = (() => void) & {
2
+ setPriority: (priority: number) => void;
3
+ };
4
+ export declare function canonicalNativeListAvatarUri(uri: string): string | undefined;
5
+ export declare function acquireNativeListAvatar(document: Document, uri: string, resolve: (url: string) => void, reject: () => void, priority?: number): AvatarLease;
6
+ //# sourceMappingURL=NativeListWebAvatarCache.d.ts.map
@@ -77,9 +77,13 @@ export declare class NativeListWebEngine {
77
77
  private resizeObserver;
78
78
  private pendingScroll;
79
79
  private lastVisibleSignature;
80
+ private readonly avatarLeases;
81
+ private avatarOffset;
82
+ private avatarDirection;
80
83
  private reachedGeneration;
81
84
  private stickyKey;
82
85
  private previewTimer;
86
+ private sectionIndexEntries;
83
87
  private pointerReorder;
84
88
  private keyboardReorder;
85
89
  private reorderMoveFrame;
@@ -100,6 +104,7 @@ export declare class NativeListWebEngine {
100
104
  private lastViewportWidth;
101
105
  private lastViewportHeight;
102
106
  private destroyed;
107
+ private measuredWarningHeights;
103
108
  constructor(host: HTMLElement, snapshot: NativeListSnapshot, callbacks: NativeListWebCallbacks, virtualizationEnabled?: boolean);
104
109
  updateCallbacks(callbacks: NativeListWebCallbacks): void;
105
110
  setVirtualizationEnabled(enabled: boolean): void;
@@ -117,10 +122,12 @@ export declare class NativeListWebEngine {
117
122
  private setSnapshot;
118
123
  private applyTheme;
119
124
  private recomputeLayout;
125
+ private updateAvatarWindow;
120
126
  private renderWindow;
121
127
  private positionElement;
122
128
  private renderElement;
123
129
  private renderFooter;
130
+ private sectionIndexVisibleEntryIndices;
124
131
  private renderSectionIndex;
125
132
  private updateVisibleSelection;
126
133
  private updateVisibleState;
@@ -145,6 +152,8 @@ export declare class NativeListWebEngine {
145
152
  private invalidateActionAnchor;
146
153
  private emitActionAnchorInvalidated;
147
154
  private handleRowPress;
155
+ private handleTitlePointerOver;
156
+ private handleTitlePointerOut;
148
157
  private handleClick;
149
158
  private handleKeyDown;
150
159
  private startKeyboardReorder;
@@ -157,7 +166,7 @@ export declare class NativeListWebEngine {
157
166
  private requestFrame;
158
167
  private cancelFrame;
159
168
  private selectIndexPosition;
160
- private indexButtonAtEvent;
169
+ private sectionIndexEntryAtEvent;
161
170
  private handleIndexPointer;
162
171
  private handleIndexClick;
163
172
  private handlePullStart;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-list",
3
- "version": "3.0.105",
3
+ "version": "3.0.106",
4
4
  "description": "Template-driven native RecyclerView and UICollectionView for React Native",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",
@@ -82,7 +82,7 @@
82
82
  "typescript": "^5.9.2"
83
83
  },
84
84
  "peerDependencies": {
85
- "@onekeyfe/react-native-image": "3.0.105",
85
+ "@onekeyfe/react-native-image": "3.0.106",
86
86
  "react": "*",
87
87
  "react-native": "*",
88
88
  "react-native-nitro-modules": "0.37.0"
@@ -1,4 +1,18 @@
1
- import React, { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
1
+ import React, {
2
+ forwardRef,
3
+ useEffect,
4
+ useImperativeHandle,
5
+ useMemo,
6
+ useRef,
7
+ } from 'react';
8
+ import {
9
+ OneKeyImageCache,
10
+ OneKeyImageCachePolicy,
11
+ } from '@onekeyfe/react-native-image';
12
+ import {
13
+ NativeAvatarPrefetchModel,
14
+ NativeAvatarPrefetchQueue,
15
+ } from './avatarPrefetch';
2
16
  import { callback, getHostComponent } from 'react-native-nitro-modules';
3
17
  import type {
4
18
  NativeListMethods,
@@ -13,6 +27,7 @@ import type {
13
27
  VisibleRangeChangedEvent,
14
28
  } from './models';
15
29
  import type { NativeListProps, NativeListRef } from './NativeList.types';
30
+ import { isSelectableRow } from './selection';
16
31
  import {
17
32
  normalizeIndexScroll,
18
33
  normalizeKeyScroll,
@@ -22,7 +37,11 @@ import {
22
37
  validateOffset,
23
38
  type NormalizedPositionScroll,
24
39
  } from './scrolling';
25
- import { serializePatches, serializeSnapshot } from './validation';
40
+ import {
41
+ applyRowPatches,
42
+ serializePatches,
43
+ serializeSnapshot,
44
+ } from './validation';
26
45
 
27
46
  export type { NativeListProps, NativeListRef } from './NativeList.types';
28
47
  export type {
@@ -91,8 +110,74 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
91
110
  onRefresh,
92
111
  onScrollToIndexFailed,
93
112
  };
113
+ const snapshotJson = useMemo(() => serializeSnapshot(snapshot), [snapshot]);
94
114
  const snapshotRef = useRef(snapshot);
95
- snapshotRef.current = snapshot;
115
+ const previousSnapshotJsonRef = useRef(snapshotJson);
116
+ const snapshotChanged = previousSnapshotJsonRef.current !== snapshotJson;
117
+ if (snapshotChanged) {
118
+ snapshotRef.current = snapshot;
119
+ previousSnapshotJsonRef.current = snapshotJson;
120
+ }
121
+ // OneKey patch: range events already cross the bridge only when row indices
122
+ // change. Prefetch is optional work; scrolling and visible loads stay native.
123
+ const avatarQueueRef = useRef<NativeAvatarPrefetchQueue | null>(null);
124
+ const avatarRangeRef = useRef<
125
+ { first: number; last: number; direction: number } | undefined
126
+ >(undefined);
127
+ // OneKey patch: a changed prop is a complete snapshot; unrelated renders must
128
+ // not erase image patches already dispatched through the imperative handle.
129
+ // const avatarPrefetchPaused = useRef(false);
130
+ const avatarModelRef = useRef<NativeAvatarPrefetchModel | null>(null);
131
+ const avatarLifecycle = useRef<'pending' | 'mounted' | 'unmounted'>(
132
+ 'pending'
133
+ );
134
+ if (!avatarModelRef.current)
135
+ avatarModelRef.current = new NativeAvatarPrefetchModel(snapshot.rows);
136
+ else if (snapshotChanged)
137
+ avatarModelRef.current.replaceSnapshot(snapshot.rows);
138
+ const updateAvatarPrefetch = () => {
139
+ const range = avatarRangeRef.current;
140
+ if (!range) return;
141
+ avatarQueueRef.current?.update(
142
+ avatarModelRef.current?.window(
143
+ range.first,
144
+ range.last,
145
+ range.direction
146
+ ) ?? []
147
+ );
148
+ };
149
+ const updateAvatarPrefetchRef = useRef(updateAvatarPrefetch);
150
+ updateAvatarPrefetchRef.current = updateAvatarPrefetch;
151
+ useEffect(() => {
152
+ avatarLifecycle.current = 'mounted';
153
+ const queue = new NativeAvatarPrefetchQueue((source) =>
154
+ OneKeyImageCache.preload([
155
+ {
156
+ uri: source.uri,
157
+ headers: source.headers,
158
+ resizeWidth: source.width,
159
+ resizeHeight: source.height,
160
+ optimizeTos: false,
161
+ cachePolicy:
162
+ source.cachePolicy === 'memory'
163
+ ? OneKeyImageCachePolicy.MEMORY
164
+ : source.cachePolicy === 'disk'
165
+ ? OneKeyImageCachePolicy.DISK
166
+ : OneKeyImageCachePolicy.MEMORY_DISK,
167
+ },
168
+ ])
169
+ );
170
+ avatarQueueRef.current = queue;
171
+ updateAvatarPrefetchRef.current();
172
+ return () => {
173
+ queue.dispose();
174
+ avatarQueueRef.current = null;
175
+ avatarLifecycle.current = 'unmounted';
176
+ };
177
+ }, []);
178
+ useEffect(() => {
179
+ updateAvatarPrefetchRef.current();
180
+ }, [snapshot]);
96
181
  const initialScrollRef = useRef<
97
182
  | Readonly<{
98
183
  index?: number;
@@ -123,7 +208,6 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
123
208
  }
124
209
  );
125
210
  const didApplyInitialScroll = useRef(false);
126
- const snapshotJson = useMemo(() => serializeSnapshot(snapshot), [snapshot]);
127
211
 
128
212
  const emitIndexFailure = (
129
213
  index: number,
@@ -179,15 +263,46 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
179
263
 
180
264
  useImperativeHandle(forwardedRef, () => ({
181
265
  applySnapshot(nextSnapshot) {
266
+ const nextSnapshotJson = serializeSnapshot(nextSnapshot);
267
+ nativeRef.current?.applySnapshot(nextSnapshotJson);
182
268
  snapshotRef.current = nextSnapshot;
183
- nativeRef.current?.applySnapshot(serializeSnapshot(nextSnapshot));
269
+ if (nativeRef.current && avatarLifecycle.current !== 'unmounted') {
270
+ avatarModelRef.current?.replaceSnapshot(nextSnapshot.rows);
271
+ updateAvatarPrefetchRef.current();
272
+ }
184
273
  },
185
274
  applyPatches(patches) {
186
- if (patches.length > 0)
187
- nativeRef.current?.applyPatches(serializePatches(patches));
275
+ if (patches.length === 0) return;
276
+ const nextSnapshot = applyRowPatches(snapshotRef.current, patches);
277
+ const native = nativeRef.current;
278
+ const dispatch = native
279
+ ? () => {
280
+ native.applyPatches(serializePatches(patches));
281
+ snapshotRef.current = nextSnapshot;
282
+ }
283
+ : undefined;
284
+ if (avatarLifecycle.current === 'unmounted') {
285
+ dispatch?.();
286
+ return;
287
+ }
288
+ if (avatarModelRef.current?.applyPatches(patches, dispatch))
289
+ updateAvatarPrefetchRef.current();
188
290
  },
189
291
  reconcileSelection(selectedKeys) {
190
292
  nativeRef.current?.reconcileSelection(JSON.stringify(selectedKeys));
293
+ const current = snapshotRef.current;
294
+ if (
295
+ current.selection &&
296
+ (current.selection.mode !== 'single' || selectedKeys.length <= 1) &&
297
+ selectedKeys.every((key) =>
298
+ current.rows.some((row) => row.key === key && isSelectableRow(row))
299
+ )
300
+ ) {
301
+ snapshotRef.current = {
302
+ ...current,
303
+ selection: { ...current.selection, selectedKeys },
304
+ };
305
+ }
191
306
  },
192
307
  scrollToKey(paramsOrKey, animated, alignment) {
193
308
  const { key, scroll } = normalizeKeyScroll(
@@ -264,9 +379,21 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
264
379
  );
265
380
  }),
266
381
  onSelectionDelta: callback((payloadJson: string) => {
267
- callbacksRef.current.onSelectionDelta?.(
268
- parsePayload<SelectionDeltaEvent>(payloadJson)
269
- );
382
+ const payload = parsePayload<SelectionDeltaEvent>(payloadJson);
383
+ const current = snapshotRef.current;
384
+ if (current.selection) {
385
+ const selectedKeys = new Set(current.selection.selectedKeys);
386
+ payload.removedKeys.forEach((key) => selectedKeys.delete(key));
387
+ payload.addedKeys.forEach((key) => selectedKeys.add(key));
388
+ snapshotRef.current = {
389
+ ...current,
390
+ selection: {
391
+ ...current.selection,
392
+ selectedKeys: [...selectedKeys],
393
+ },
394
+ };
395
+ }
396
+ callbacksRef.current.onSelectionDelta?.(payload);
270
397
  }),
271
398
  onReorder: callback((payloadJson: string) => {
272
399
  callbacksRef.current.onReorder?.(
@@ -279,9 +406,18 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
279
406
  );
280
407
  }),
281
408
  onVisibleRangeChanged: callback((payloadJson: string) => {
282
- callbacksRef.current.onVisibleRangeChanged?.(
283
- parsePayload<VisibleRangeChangedEvent>(payloadJson)
284
- );
409
+ const payload = parsePayload<VisibleRangeChangedEvent>(payloadJson);
410
+ const previous = avatarRangeRef.current;
411
+ avatarRangeRef.current = {
412
+ first: payload.firstIndex,
413
+ last: payload.lastIndex,
414
+ direction:
415
+ previous && payload.firstIndex !== previous.first
416
+ ? Math.sign(payload.firstIndex - previous.first)
417
+ : previous?.direction ?? 1,
418
+ };
419
+ updateAvatarPrefetchRef.current();
420
+ callbacksRef.current.onVisibleRangeChanged?.(payload);
285
421
  }),
286
422
  }),
287
423
  []
@@ -9,7 +9,7 @@ import React, {
9
9
  } from 'react';
10
10
  import { View } from 'react-native';
11
11
  import type { NativeListProps, NativeListRef } from './NativeList.types';
12
- import type { NativeListSnapshot } from './models';
12
+ import type { NativeListSnapshot, RowPatch } from './models';
13
13
  import {
14
14
  normalizeIndexScroll,
15
15
  normalizeKeyScroll,
@@ -69,7 +69,20 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
69
69
  [snapshot]
70
70
  );
71
71
  const snapshotRef = useRef(validatedSnapshot);
72
- snapshotRef.current = validatedSnapshot;
72
+ const snapshotPropRef = useRef(validatedSnapshot);
73
+ // An imperative snapshot survives host mounting until the snapshot prop changes.
74
+ if (snapshotPropRef.current !== validatedSnapshot) {
75
+ snapshotPropRef.current = validatedSnapshot;
76
+ snapshotRef.current = validatedSnapshot;
77
+ }
78
+ // OneKey patch: React refs can be ready before the DOM engine is mounted.
79
+ const pendingPatchesRef = useRef<
80
+ | {
81
+ snapshot: NativeListSnapshot;
82
+ batches: Array<readonly RowPatch[]>;
83
+ }
84
+ | undefined
85
+ >(undefined);
73
86
  const appliedSnapshotRef = useRef<NativeListSnapshot | undefined>(
74
87
  undefined
75
88
  );
@@ -125,6 +138,10 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
125
138
  );
126
139
  engineRef.current = engine;
127
140
  appliedSnapshotRef.current = snapshotRef.current;
141
+ const pending = pendingPatchesRef.current;
142
+ pendingPatchesRef.current = undefined;
143
+ if (pending?.snapshot === snapshotRef.current)
144
+ pending.batches.forEach((patches) => engine.applyPatches(patches));
128
145
  const initial = initialScrollRef.current;
129
146
  if (initial && !didApplyInitialScroll.current) {
130
147
  didApplyInitialScroll.current = true;
@@ -166,6 +183,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
166
183
  useImperativeHandle(forwardedRef, () => ({
167
184
  applySnapshot(nextSnapshot) {
168
185
  const next = validateSnapshot(nextSnapshot);
186
+ pendingPatchesRef.current = undefined;
169
187
  snapshotRef.current = next;
170
188
  appliedSnapshotRef.current = next;
171
189
  engineRef.current?.applySnapshot(next);
@@ -173,7 +191,16 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
173
191
  applyPatches(patches) {
174
192
  if (patches.length > 0) {
175
193
  serializePatches(patches);
176
- engineRef.current?.applyPatches(patches);
194
+ // engineRef.current?.applyPatches(patches);
195
+ if (engineRef.current) engineRef.current.applyPatches(patches);
196
+ else {
197
+ if (pendingPatchesRef.current?.snapshot !== snapshotRef.current)
198
+ pendingPatchesRef.current = {
199
+ snapshot: snapshotRef.current,
200
+ batches: [],
201
+ };
202
+ pendingPatchesRef.current.batches.push(patches);
203
+ }
177
204
  }
178
205
  },
179
206
  reconcileSelection(selectedKeys) {