@onekeyfe/react-native-native-list 3.0.118 → 3.0.119

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/README.md CHANGED
@@ -240,6 +240,12 @@ const avatar = {
240
240
  } as const;
241
241
  ```
242
242
 
243
+ NativeList defaults `loadingStrategy` to `none`, so source-backed images render
244
+ without a loading background or terminal fallback. Set it to `static` or
245
+ `skeleton` to opt into loading UI; a configured `fallbackText` or
246
+ `fallbackIcon` is shown after a terminal load failure only when loading UI is
247
+ enabled.
248
+
243
249
  Each cell owns a fixed pool of native `OneKeyImageReusableView` slots. On bind,
244
250
  NativeList forwards the URI, headers, content fit, cache policy, autoplay, TOS
245
251
  options, overscan, and loading strategy. A stable `rowKey:slot` recycling key
@@ -3189,16 +3189,18 @@ internal class NativeListRowView(
3189
3189
  val cornerIconData = visual.optJSONObject("cornerIcon")
3190
3190
  val fallback = visual.optString("fallbackText").take(2)
3191
3191
  val fallbackIconData = visual.optJSONObject("fallbackIcon")
3192
+ val sourceLoadingStrategy = sources.firstOrNull()?.first?.optString("loadingStrategy", "none") ?: "none"
3193
+ val showsSourcePlaceholder = !isIcon && sources.isNotEmpty() && sourceLoadingStrategy != "none"
3192
3194
  val handlesSourceFallback =
3193
3195
  !isIcon && sources.isNotEmpty() &&
3196
+ showsSourcePlaceholder &&
3194
3197
  (visual.has("fallbackText") || fallbackIconData != null)
3195
3198
  leadingFallback.text = if (handlesSourceFallback) "" else fallback
3196
3199
  leadingFallback.setTextColor(parseNativeListColor("#00000072"))
3197
3200
  val visualBackground = safeColor(
3198
3201
  visual.optString("backgroundColor"),
3199
- // OneKey patch: Visual loading and fallback use the active list theme.
3200
- // parseNativeListColor(if (isIcon) "#0000000F" else "#E0E0E0"),
3201
- parseNativeListColor(imagePlaceholderColor),
3202
+ // OneKey patch: source-backed visuals default to no placeholder/background.
3203
+ if (!isIcon && sources.isNotEmpty()) Color.TRANSPARENT else parseNativeListColor(imagePlaceholderColor),
3202
3204
  )
3203
3205
  if (!isIcon && visual.optString("backgroundColor").isNotEmpty()) {
3204
3206
  leadingFrame.background = roundedFill(
@@ -4149,7 +4151,7 @@ internal class NativeListRowView(
4149
4151
  recyclingKey = if (retryAttempt == 0) "$token:$slot" else "$token:$slot:retry:$retryAttempt",
4150
4152
  optimizeTos = retryAttempt == 0 && source.optBoolean("optimizeTos", true),
4151
4153
  overscan = source.optDouble("overscan", 1.1),
4152
- loadingStrategy = source.optString("loadingStrategy", "static"),
4154
+ loadingStrategy = source.optString("loadingStrategy", "none"),
4153
4155
  placeholderColor = imagePlaceholderColor,
4154
4156
  onLoad = if (retryLimit == 0) handleLoad else ({
4155
4157
  if (bindingEpoch == expectedEpoch) {
@@ -3377,18 +3377,23 @@ final class NativeListCell: UICollectionViewCell {
3377
3377
  let cornerIcon = visual.dictionary("cornerIcon")
3378
3378
  let fallbackText = String(visual.string("fallbackText").prefix(2))
3379
3379
  let fallbackIconData = visual.dictionary("fallbackIcon")
3380
+ let sourceLoadingStrategy = sources.first?.data.string("loadingStrategy", default: "none") ?? "none"
3381
+ let showsSourcePlaceholder = !isIcon && !sources.isEmpty && sourceLoadingStrategy != "none"
3380
3382
  let handlesSourceFallback = !isIcon && !sources.isEmpty &&
3383
+ showsSourcePlaceholder &&
3381
3384
  (visual["fallbackText"] != nil || fallbackIconData != nil)
3382
3385
  fallbackLabel.text = handlesSourceFallback ? nil : fallbackText
3383
3386
  let sourceFallbackBackground = currentTheme?["strongBackground"] as? String ?? "#0000000F"
3384
- leadingContainer.backgroundColor = UIColor(
3385
- // OneKey patch: Visual loading and fallback use the active list theme.
3386
- // nativeListHex: visual.string("backgroundColor", default: isIcon ? "#F0F0F0" : "#E0E0E0"),
3387
- nativeListHex: handlesSourceFallback
3388
- ? sourceFallbackBackground
3389
- : visual.string("backgroundColor", default: sourceFallbackBackground),
3390
- fallback: .gray
3387
+ // OneKey patch: source-backed visuals default to no placeholder/background.
3388
+ // A non-none image loadingStrategy opts back into the themed placeholder.
3389
+ let visualBackground = visual.string(
3390
+ "backgroundColor",
3391
+ default: !isIcon && !sources.isEmpty ? "#00000000" : sourceFallbackBackground
3391
3392
  )
3393
+ let visualBackgroundColor = UIColor(nativeListHex: visualBackground, fallback: .clear)
3394
+ leadingContainer.backgroundColor = handlesSourceFallback
3395
+ ? UIColor(nativeListHex: sourceFallbackBackground, fallback: .clear)
3396
+ : visualBackgroundColor
3392
3397
  leadingContainer.layer.cornerRadius = leadingCornerRadius(shape: shape)
3393
3398
  leadingContainer.clipsToBounds = true
3394
3399
  fallbackLabel.isHidden = isIcon || (!sources.isEmpty && !handlesSourceFallback)
@@ -3473,6 +3478,7 @@ final class NativeListCell: UICollectionViewCell {
3473
3478
  imageView?.isHidden = false
3474
3479
  self.fallbackLabel.isHidden = true
3475
3480
  self.leadingIconImageView.isHidden = true
3481
+ self.leadingContainer.backgroundColor = visualBackgroundColor
3476
3482
  },
3477
3483
  onError: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
3478
3484
  guard let self, self.bindingEpoch == expectedEpoch else { return }
@@ -4031,7 +4037,7 @@ final class NativeListCell: UICollectionViewCell {
4031
4037
  recyclingKey: retryAttempt == 0 ? "\(token):\(slot)" : "\(token):\(slot):retry:\(retryAttempt)",
4032
4038
  optimizeTos: retryAttempt == 0 && (source["optimizeTos"] == nil || source.bool("optimizeTos")),
4033
4039
  overscan: source["overscan"] == nil ? 1.1 : source.double("overscan"),
4034
- loadingStrategy: source.string("loadingStrategy", default: "static"),
4040
+ loadingStrategy: source.string("loadingStrategy", default: "none"),
4035
4041
  placeholderColor: currentTheme?["strongBackground"] as? String ?? "#0000000F",
4036
4042
  onLoad: retryLimit == 0 ? handleLoad : { [weak self] in
4037
4043
  guard let self, self.bindingEpoch == expectedEpoch else { return }
@@ -94,7 +94,10 @@ final class NativeListView: UIView {
94
94
  private var interactiveReorderCompactKey: String?
95
95
  private var interactiveReorderUsesAtomicTargeting = false
96
96
  private var interactiveReorderTargetIndex: Int?
97
+ private var interactiveReorderTargetKey: String?
98
+ private var interactiveReorderLockedCrossAxisPosition: CGFloat?
97
99
  private var interactiveReorderTransformedCells: [NativeListCell] = []
100
+ private var deferredReorderReconfigureKeys = Set<String>()
98
101
  private let interactiveReorderPlaceholder = UIView()
99
102
  private var interactiveReorderAnimator: UIViewPropertyAnimator?
100
103
  private let reorderStartFeedback = UIImpactFeedbackGenerator(style: .medium)
@@ -255,6 +258,13 @@ final class NativeListView: UIView {
255
258
  refreshVisibleSelection()
256
259
  return
257
260
  }
261
+ if let current = config, interactiveReorderSource != nil {
262
+ if canDeferSnapshotDuringInteractiveReorder(from: current, to: next) {
263
+ deferSnapshotDuringInteractiveReorder(from: current, to: next)
264
+ return
265
+ }
266
+ cancelInteractiveReorderForStructuralUpdate()
267
+ }
258
268
  let oldItems = itemsByKey
259
269
  let themeChanged = !dictionariesEqual(config?.theme, next.theme)
260
270
  if config?.generation != next.generation { endReachedGeneration = nil }
@@ -269,11 +279,13 @@ final class NativeListView: UIView {
269
279
  snapshot.appendSections([0])
270
280
  let keys = next.items.map(\.key)
271
281
  snapshot.appendItems(keys, toSection: 0)
272
- let changedKeys = keys.filter { key in
282
+ var changedKeys = Set(keys.filter { key in
273
283
  guard let old = oldItems[key], let new = itemsByKey[key] else { return false }
274
284
  return themeChanged || old.revision != new.revision || old.content != new.content
275
- }
276
- snapshot.reconfigureItems(changedKeys)
285
+ })
286
+ changedKeys.formUnion(deferredReorderReconfigureKeys)
287
+ deferredReorderReconfigureKeys.removeAll()
288
+ snapshot.reconfigureItems(changedKeys.filter { itemsByKey[$0] != nil })
277
289
  dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
278
290
  guard let self else { return }
279
291
  self.collectionView.layoutIfNeeded()
@@ -300,9 +312,10 @@ final class NativeListView: UIView {
300
312
  pending.append((index, changes))
301
313
  }
302
314
 
303
- var changedKeys: [String] = []
315
+ var changedKeys = Set<String>()
304
316
  var marketQuoteKeys = Set<String>()
305
317
  var sizeChanged = false
318
+ var reorderStructureChanged = false
306
319
  let marketQuoteFields: Set<String> = [
307
320
  "revision", "price", "priceSegments", "change", "accessibilityLabel",
308
321
  ]
@@ -313,22 +326,37 @@ final class NativeListView: UIView {
313
326
  if key != "key" && key != "type" { merged[key] = value }
314
327
  }
315
328
  guard let item = try? NativeListItem(data: merged) else { return }
316
- if rowHeight(current.items[index]) != rowHeight(item) { sizeChanged = true }
329
+ if rowHeight(previous, usingCompactReorderHeight: false) != rowHeight(item, usingCompactReorderHeight: false) ||
330
+ current.orientation == "horizontal" && previous.type == "rail" && railWidth(previous) != railWidth(item) ||
331
+ current.sectionIndexEnabled && previous.type == "sectionHeader" &&
332
+ previous.data.string("indexTitle").isEmpty != item.data.string("indexTitle").isEmpty {
333
+ sizeChanged = true
334
+ }
335
+ if previous.sectionKey != item.sectionKey ||
336
+ previous.isReorderable != item.isReorderable ||
337
+ !hasSameNestedReorderStructure(previous, item) ||
338
+ previous.type == "identity" &&
339
+ previous.data.string("presentation") != item.data.string("presentation") {
340
+ reorderStructureChanged = true
341
+ }
317
342
  current.items[index] = item
318
343
  if previous.type == "market", Set(changes.keys).isSubset(of: marketQuoteFields) {
319
344
  marketQuoteKeys.insert(item.key)
320
345
  } else {
321
- changedKeys.append(item.key)
346
+ changedKeys.insert(item.key)
322
347
  }
323
348
  if let selected = changes["selected"] as? Bool {
324
349
  if selected { current.selectedKeys.insert(item.key) } else { current.selectedKeys.remove(item.key) }
325
350
  }
326
351
  }
327
352
  if !changedKeys.isEmpty { invalidateActionAnchor(reason: "snapshot") }
353
+ if interactiveReorderSource != nil, sizeChanged || reorderStructureChanged {
354
+ cancelInteractiveReorderForStructuralUpdate()
355
+ }
328
356
  config = current
329
357
  itemsByKey = Dictionary(uniqueKeysWithValues: current.items.map { ($0.key, $0) })
330
358
  for indexPath in collectionView.indexPathsForVisibleItems {
331
- guard let item = current.items[safe: indexPath.item],
359
+ guard let item = item(at: indexPath),
332
360
  marketQuoteKeys.contains(item.key),
333
361
  let cell = collectionView.cellForItem(at: indexPath) as? NativeListCell else { continue }
334
362
  cell.updateMarketQuote(item, theme: current.theme)
@@ -339,6 +367,13 @@ final class NativeListView: UIView {
339
367
  checkEndReached()
340
368
  return
341
369
  }
370
+ if interactiveReorderSource != nil {
371
+ deferredReorderReconfigureKeys.formUnion(changedKeys)
372
+ configureFooter(current)
373
+ return
374
+ }
375
+ changedKeys.formUnion(deferredReorderReconfigureKeys)
376
+ deferredReorderReconfigureKeys.removeAll()
342
377
  var snapshot = dataSource.snapshot()
343
378
  snapshot.reconfigureItems(changedKeys.filter { snapshot.indexOfItem($0) != nil })
344
379
  dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
@@ -567,6 +602,9 @@ final class NativeListView: UIView {
567
602
  let cell = collectionView.cellForItem(at: indexPath) as? NativeListCell else {
568
603
  interactiveReorderSource = nil
569
604
  interactiveReorderCell = nil
605
+ interactiveReorderTargetIndex = nil
606
+ interactiveReorderTargetKey = nil
607
+ interactiveReorderLockedCrossAxisPosition = nil
570
608
  return
571
609
  }
572
610
  // OneKey patch: a hidden wallet child starts dragging its whole logical group.
@@ -581,6 +619,11 @@ final class NativeListView: UIView {
581
619
  item.data.string("presentation") == "walletSidebar" &&
582
620
  current.items.contains { $0.type == "walletGroup" }
583
621
  interactiveReorderTargetIndex = indexPath.item
622
+ interactiveReorderTargetKey = item.key
623
+ let sourceCenter = flowLayout.layoutAttributesForItem(at: indexPath)?.center ?? cell.center
624
+ interactiveReorderLockedCrossAxisPosition = current.orientation == "horizontal"
625
+ ? sourceCenter.y
626
+ : sourceCenter.x
584
627
  if item.type == "walletGroup" {
585
628
  interactiveReorderCompactKey = item.key
586
629
  cell.setWalletGroupReorderCompact(true)
@@ -596,6 +639,8 @@ final class NativeListView: UIView {
596
639
  interactiveReorderCell = nil
597
640
  interactiveReorderUsesAtomicTargeting = false
598
641
  interactiveReorderTargetIndex = nil
642
+ interactiveReorderTargetKey = nil
643
+ interactiveReorderLockedCrossAxisPosition = nil
599
644
  interactiveReorderFeedbackIndex = nil
600
645
  flowLayout.invalidateLayout()
601
646
  collectionView.layoutIfNeeded()
@@ -608,13 +653,20 @@ final class NativeListView: UIView {
608
653
  showInteractiveReorderPlaceholder(at: indexPath, item: item, config: current)
609
654
  case .changed:
610
655
  guard interactiveReorderSource != nil else { return }
611
- let point = gesture.location(in: collectionView)
656
+ var point = gesture.location(in: collectionView)
657
+ if let lockedPosition = interactiveReorderLockedCrossAxisPosition {
658
+ if config?.orientation == "horizontal" {
659
+ point.y = lockedPosition
660
+ } else {
661
+ point.x = lockedPosition
662
+ }
663
+ }
612
664
  collectionView.updateInteractiveMovementTargetPosition(point)
613
665
  if let indexPath = nearestReorderIndexPath(to: point),
614
666
  let item = item(at: indexPath) {
615
667
  let targetChanged = interactiveReorderFeedbackIndex != indexPath.item
616
668
  if interactiveReorderUsesAtomicTargeting {
617
- updateAtomicReorderTarget(to: indexPath.item, animated: targetChanged)
669
+ updateAtomicReorderTarget(to: indexPath.item, key: item.key, animated: targetChanged)
618
670
  }
619
671
  if targetChanged {
620
672
  interactiveReorderFeedbackIndex = indexPath.item
@@ -628,6 +680,7 @@ final class NativeListView: UIView {
628
680
  interactiveReorderCell?.setPressed(false)
629
681
  finishInteractiveReorder(cancelled: false)
630
682
  case .cancelled, .failed:
683
+ guard interactiveReorderSource != nil else { return }
631
684
  interactiveReorderCell?.setPressed(false)
632
685
  finishInteractiveReorder(cancelled: true)
633
686
  default:
@@ -636,10 +689,17 @@ final class NativeListView: UIView {
636
689
  }
637
690
 
638
691
  private func nearestReorderIndexPath(to point: CGPoint) -> IndexPath? {
639
- collectionView.indexPathsForVisibleItems.min { lhs, rhs in
692
+ let isHorizontal = config?.orientation == "horizontal"
693
+ return collectionView.indexPathsForVisibleItems.min { lhs, rhs in
640
694
  let lhsFrame = flowLayout.layoutAttributesForItem(at: lhs)?.frame ?? .zero
641
695
  let rhsFrame = flowLayout.layoutAttributesForItem(at: rhs)?.frame ?? .zero
642
- return abs(lhsFrame.midY - point.y) < abs(rhsFrame.midY - point.y)
696
+ let lhsDistance = isHorizontal
697
+ ? abs(lhsFrame.midX - point.x)
698
+ : abs(lhsFrame.midY - point.y)
699
+ let rhsDistance = isHorizontal
700
+ ? abs(rhsFrame.midX - point.x)
701
+ : abs(rhsFrame.midY - point.y)
702
+ return lhsDistance < rhsDistance
643
703
  }
644
704
  }
645
705
 
@@ -685,6 +745,7 @@ final class NativeListView: UIView {
685
745
  }
686
746
 
687
747
  private func finishInteractiveReorder(cancelled: Bool) {
748
+ interactiveReorderLockedCrossAxisPosition = nil
688
749
  if interactiveReorderUsesAtomicTargeting {
689
750
  finishAtomicInteractiveReorder(cancelled: cancelled)
690
751
  return
@@ -715,12 +776,58 @@ final class NativeListView: UIView {
715
776
  } else {
716
777
  self.interactiveReorderSource = nil
717
778
  self.interactiveReorderCell = nil
779
+ self.interactiveReorderTargetIndex = nil
780
+ self.interactiveReorderTargetKey = nil
781
+ self.scheduleDeferredReorderRefresh()
718
782
  }
719
783
  }
720
784
  interactiveReorderAnimator = animator
721
785
  animator.startAnimation()
722
786
  }
723
787
 
788
+ private func cancelInteractiveReorderForStructuralUpdate() {
789
+ guard interactiveReorderSource != nil else { return }
790
+ interactiveReorderAnimator?.stopAnimation(true)
791
+ interactiveReorderAnimator = nil
792
+ collectionView.cancelInteractiveMovement()
793
+ interactiveReorderCell?.setPressed(false)
794
+ if interactiveReorderCompactKey != nil {
795
+ interactiveReorderCell?.setWalletGroupReorderCompact(false)
796
+ interactiveReorderCompactKey = nil
797
+ flowLayout.invalidateLayout()
798
+ }
799
+ resetAtomicReorderTransforms()
800
+ interactiveReorderSource = nil
801
+ interactiveReorderCell = nil
802
+ interactiveReorderFeedbackIndex = nil
803
+ clearAtomicInteractiveReorderState()
804
+ }
805
+
806
+ private func scheduleDeferredReorderRefresh() {
807
+ guard !deferredReorderReconfigureKeys.isEmpty else { return }
808
+ DispatchQueue.main.async { [weak self] in
809
+ self?.flushDeferredReorderRefresh()
810
+ }
811
+ }
812
+
813
+ private func flushDeferredReorderRefresh() {
814
+ guard interactiveReorderSource == nil,
815
+ interactiveReorderCompactKey == nil,
816
+ interactiveReorderAnimator == nil,
817
+ !deferredReorderReconfigureKeys.isEmpty else { return }
818
+ var snapshot = dataSource.snapshot()
819
+ let keys = deferredReorderReconfigureKeys.filter { snapshot.indexOfItem($0) != nil }
820
+ deferredReorderReconfigureKeys.removeAll()
821
+ guard !keys.isEmpty else { return }
822
+ snapshot.reconfigureItems(Array(keys))
823
+ dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
824
+ guard let self else { return }
825
+ self.collectionView.layoutIfNeeded()
826
+ self.emitVisibleRangeIfNeeded()
827
+ self.checkEndReached()
828
+ }
829
+ }
830
+
724
831
  private func completeInteractiveReorder(
725
832
  _ snapshot: NSDiffableDataSourceSnapshot<Int, String>
726
833
  ) {
@@ -729,6 +836,10 @@ final class NativeListView: UIView {
729
836
  defer {
730
837
  interactiveReorderSource = nil
731
838
  interactiveReorderCell = nil
839
+ interactiveReorderTargetIndex = nil
840
+ interactiveReorderTargetKey = nil
841
+ interactiveReorderLockedCrossAxisPosition = nil
842
+ scheduleDeferredReorderRefresh()
732
843
  }
733
844
  let keys = snapshot.itemIdentifiers
734
845
  let items = keys.compactMap { itemsByKey[$0] }
@@ -768,6 +879,10 @@ final class NativeListView: UIView {
768
879
  self.walletGroupCell(for: key)?.finishWalletGroupReorderExpansion()
769
880
  self.interactiveReorderSource = nil
770
881
  self.interactiveReorderCell = nil
882
+ self.interactiveReorderTargetIndex = nil
883
+ self.interactiveReorderTargetKey = nil
884
+ self.interactiveReorderAnimator = nil
885
+ self.scheduleDeferredReorderRefresh()
771
886
  }
772
887
  interactiveReorderAnimator = animator
773
888
  animator.startAnimation()
@@ -785,9 +900,10 @@ final class NativeListView: UIView {
785
900
  return config?.items[safe: indexPath.item]
786
901
  }
787
902
 
788
- private func updateAtomicReorderTarget(to targetIndex: Int, animated: Bool) {
903
+ private func updateAtomicReorderTarget(to targetIndex: Int, key targetKey: String, animated: Bool) {
789
904
  guard let source = interactiveReorderSource else { return }
790
905
  interactiveReorderTargetIndex = targetIndex
906
+ interactiveReorderTargetKey = targetKey
791
907
  let distance = CGFloat(68) + flowLayout.minimumLineSpacing
792
908
  let updates = { [weak self] in
793
909
  guard let self else { return }
@@ -827,17 +943,28 @@ final class NativeListView: UIView {
827
943
 
828
944
  private func finishAtomicInteractiveReorder(cancelled: Bool) {
829
945
  interactiveReorderAnimator?.stopAnimation(true)
946
+ interactiveReorderAnimator = nil
830
947
  interactiveReorderFeedbackIndex = nil
831
948
  guard let source = interactiveReorderSource else { return }
832
- let targetIndex = interactiveReorderTargetIndex ?? source.index
833
949
  collectionView.cancelInteractiveMovement()
834
950
 
835
- if !cancelled, targetIndex != source.index {
951
+ if !cancelled {
836
952
  var snapshot = dataSource.snapshot()
837
953
  let keys = snapshot.itemIdentifiers
838
- if targetIndex < source.index, let targetKey = keys[safe: targetIndex] {
954
+ guard let targetKey = interactiveReorderTargetKey,
955
+ targetKey != source.key,
956
+ let sourceIndex = keys.firstIndex(of: source.key),
957
+ let targetIndex = keys.firstIndex(of: targetKey) else {
958
+ resetAtomicReorderTransforms()
959
+ interactiveReorderSource = nil
960
+ interactiveReorderCell = nil
961
+ clearAtomicInteractiveReorderState()
962
+ scheduleDeferredReorderRefresh()
963
+ return
964
+ }
965
+ if targetIndex < sourceIndex {
839
966
  snapshot.moveItem(source.key, beforeItem: targetKey)
840
- } else if let targetKey = keys[safe: targetIndex] {
967
+ } else {
841
968
  snapshot.moveItem(source.key, afterItem: targetKey)
842
969
  }
843
970
  dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
@@ -852,12 +979,15 @@ final class NativeListView: UIView {
852
979
  interactiveReorderSource = nil
853
980
  interactiveReorderCell = nil
854
981
  clearAtomicInteractiveReorderState()
982
+ scheduleDeferredReorderRefresh()
855
983
  }
856
984
  }
857
985
 
858
986
  private func clearAtomicInteractiveReorderState() {
859
987
  interactiveReorderUsesAtomicTargeting = false
860
988
  interactiveReorderTargetIndex = nil
989
+ interactiveReorderTargetKey = nil
990
+ interactiveReorderLockedCrossAxisPosition = nil
861
991
  interactiveReorderPlaceholder.isHidden = true
862
992
  interactiveReorderPlaceholder.alpha = 1
863
993
  }
@@ -1131,7 +1261,7 @@ final class NativeListView: UIView {
1131
1261
  self?.resolveCheckboxState(item: item, target: target, fallback: fallback) ?? fallback
1132
1262
  }
1133
1263
  for indexPath in collectionView.indexPathsForVisibleItems {
1134
- guard let item = config.items[safe: indexPath.item],
1264
+ guard let item = item(at: indexPath),
1135
1265
  let cell = collectionView.cellForItem(at: indexPath) as? NativeListCell else { continue }
1136
1266
  if changedSummaryKeys.contains(item.key) {
1137
1267
  bind(cell: cell, item: item, itemIndex: indexPath.item)
@@ -1200,6 +1330,101 @@ final class NativeListView: UIView {
1200
1330
  }
1201
1331
  }
1202
1332
 
1333
+ private func canDeferSnapshotDuringInteractiveReorder(
1334
+ from current: NativeListConfig,
1335
+ to next: NativeListConfig
1336
+ ) -> Bool {
1337
+ guard current.layout == next.layout,
1338
+ current.orientation == next.orientation,
1339
+ current.gridColumns == next.gridColumns,
1340
+ current.stickyHeaders == next.stickyHeaders,
1341
+ current.contentPadding == next.contentPadding,
1342
+ current.contentPaddingHorizontal == next.contentPaddingHorizontal,
1343
+ current.contentPaddingTop == next.contentPaddingTop,
1344
+ current.contentPaddingBottom == next.contentPaddingBottom,
1345
+ current.itemSpacing == next.itemSpacing,
1346
+ current.reorderable == next.reorderable,
1347
+ current.sectionIndexEnabled == next.sectionIndexEnabled,
1348
+ current.items.count == next.items.count,
1349
+ hasSameFooterGeometry(current.fixedFooter, next.fixedFooter) else { return false }
1350
+
1351
+ return zip(current.items, next.items).allSatisfy { old, new in
1352
+ guard old.key == new.key,
1353
+ old.type == new.type,
1354
+ old.sectionKey == new.sectionKey,
1355
+ old.isReorderable == new.isReorderable,
1356
+ hasSameNestedReorderStructure(old, new),
1357
+ rowHeight(old, usingCompactReorderHeight: false) == rowHeight(new, usingCompactReorderHeight: false) else {
1358
+ return false
1359
+ }
1360
+ if current.orientation == "horizontal", old.type == "rail", railWidth(old) != railWidth(new) {
1361
+ return false
1362
+ }
1363
+ if old.type == "identity", old.data.string("presentation") != new.data.string("presentation") {
1364
+ return false
1365
+ }
1366
+ if current.sectionIndexEnabled,
1367
+ old.type == "sectionHeader",
1368
+ old.data.string("indexTitle").isEmpty != new.data.string("indexTitle").isEmpty {
1369
+ return false
1370
+ }
1371
+ return true
1372
+ }
1373
+ }
1374
+
1375
+ private func hasSameNestedReorderStructure(_ current: NativeListItem, _ next: NativeListItem) -> Bool {
1376
+ guard current.type == "walletGroup" else { return true }
1377
+ guard let currentParent = current.data.dictionary("parent"),
1378
+ let nextParent = next.data.dictionary("parent") else { return false }
1379
+ let currentMembers = [currentParent] + current.data.dictionaries("children")
1380
+ let nextMembers = [nextParent] + next.data.dictionaries("children")
1381
+ guard currentMembers.count == nextMembers.count else { return false }
1382
+ return zip(currentMembers, nextMembers).allSatisfy { old, new in
1383
+ old.string("key") == new.string("key") &&
1384
+ old.string("type") == new.string("type") &&
1385
+ old.string("sectionKey") == new.string("sectionKey") &&
1386
+ old.string("presentation") == new.string("presentation")
1387
+ }
1388
+ }
1389
+
1390
+ private func hasSameFooterGeometry(_ current: NativeListItem?, _ next: NativeListItem?) -> Bool {
1391
+ switch (current, next) {
1392
+ case (nil, nil):
1393
+ return true
1394
+ case let (current?, next?):
1395
+ return current.key == next.key &&
1396
+ current.type == next.type &&
1397
+ rowHeight(current, usingCompactReorderHeight: false) == rowHeight(next, usingCompactReorderHeight: false)
1398
+ default:
1399
+ return false
1400
+ }
1401
+ }
1402
+
1403
+ private func deferSnapshotDuringInteractiveReorder(
1404
+ from current: NativeListConfig,
1405
+ to next: NativeListConfig
1406
+ ) {
1407
+ let oldItems = itemsByKey
1408
+ let themeChanged = !dictionariesEqual(current.theme, next.theme)
1409
+ let selectionChanged = current.selectionMode != next.selectionMode ||
1410
+ current.selectedKeys != next.selectedKeys
1411
+ if current.generation != next.generation { endReachedGeneration = nil }
1412
+ config = next
1413
+ itemsByKey = Dictionary(uniqueKeysWithValues: next.items.map { ($0.key, $0) })
1414
+ let changedKeys = next.items.compactMap { item -> String? in
1415
+ guard let old = oldItems[item.key] else { return nil }
1416
+ return themeChanged || selectionChanged || old.revision != item.revision || old.content != item.content
1417
+ ? item.key
1418
+ : nil
1419
+ }
1420
+ deferredReorderReconfigureKeys.formUnion(changedKeys)
1421
+ configureSectionIndex(next)
1422
+ configureRefresh(next)
1423
+ configureFooter(next)
1424
+ emitVisibleRangeIfNeeded()
1425
+ checkEndReached()
1426
+ }
1427
+
1203
1428
  // OneKey patch: remove only fields that the existing lightweight binder refreshes.
1204
1429
  private func selectionComparisonData(_ data: [String: Any], controlled: Bool) -> [String: Any]? {
1205
1430
  guard let type = data["type"] as? String else { return nil }
@@ -1263,7 +1488,10 @@ final class NativeListView: UIView {
1263
1488
  return try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys])
1264
1489
  }
1265
1490
 
1266
- private func rowHeight(_ item: NativeListItem) -> CGFloat {
1491
+ private func rowHeight(
1492
+ _ item: NativeListItem,
1493
+ usingCompactReorderHeight: Bool = true
1494
+ ) -> CGFloat {
1267
1495
  // OneKey patch: honor selector baseline geometry; keep compact drag sizing.
1268
1496
  if item.type != "walletGroup", item.data["height"] != nil { return CGFloat(item.data.double("height")) }
1269
1497
  if item.type == "system", item.data.string("variant") == "spacer" {
@@ -1281,7 +1509,7 @@ final class NativeListView: UIView {
1281
1509
  return 32 + textHeight("title", weight: .medium) + textHeight("message", weight: .regular)
1282
1510
  }
1283
1511
  if item.type == "walletGroup" {
1284
- if item.key == interactiveReorderCompactKey { return 68 }
1512
+ if usingCompactReorderHeight, item.key == interactiveReorderCompactKey { return 68 }
1285
1513
  let childCount = item.data.dictionaries("children").count
1286
1514
  // OneKey patch: wallet badges contribute their own member heights.
1287
1515
  // return CGFloat((childCount + 1) * 68 + childCount * 12)
@@ -982,20 +982,27 @@ function createVisual(context, visual, selectorPresentation) {
982
982
  return frame;
983
983
  }
984
984
 
985
- // OneKey patch: Every image visual starts from the current theme backing.
986
- // if ('backgroundColor' in visual && visual.backgroundColor)
987
- // frame.style.background = visual.backgroundColor;
988
- frame.style.background = 'backgroundColor' in visual && visual.backgroundColor ? visual.backgroundColor : 'var(--nl-strong)';
985
+ // OneKey patch: NativeList images are usually local and should not flash a
986
+ // placeholder. Callers can opt in with image.loadingStrategy.
987
+ const visualBackground = 'backgroundColor' in visual && visual.backgroundColor ? visual.backgroundColor : 'transparent';
989
988
  const source = visual.kind === 'image' ? visual.image : visual.image;
989
+ const showsSourcePlaceholder = !!source && source.loadingStrategy !== undefined && source.loadingStrategy !== 'none';
990
+ frame.style.background = showsSourcePlaceholder ? 'var(--nl-strong)' : visualBackground;
990
991
  const fallbackIcon = 'fallbackIcon' in visual ? visual.fallbackIcon : undefined;
991
- const handlesSourceFallback = !!source && (fallbackIcon !== undefined || 'fallbackText' in visual);
992
- if (handlesSourceFallback) frame.style.background = 'var(--nl-strong)';
992
+ const handlesSourceFallback = showsSourcePlaceholder && (fallbackIcon !== undefined || 'fallbackText' in visual);
993
993
  const image = source ? createImage(context, source) : undefined;
994
994
  if (image) {
995
995
  image.className = 'ok-native-list-visual-main';
996
996
  frame.appendChild(image);
997
+ if (showsSourcePlaceholder) {
998
+ const restoreBackground = () => {
999
+ frame.style.background = visualBackground;
1000
+ };
1001
+ image.addEventListener('load', restoreBackground);
1002
+ if (image.complete && image.naturalWidth > 0) restoreBackground();
1003
+ }
997
1004
  if (selectorPresentation) paintSelectorImageBackground(image, frame);
998
- } else {
1005
+ } else if (!source || showsSourcePlaceholder) {
999
1006
  frame.appendChild(createElement(context.document, 'span', 'ok-native-list-visual-fallback', 'fallbackText' in visual ? visual.fallbackText ?? '' : ''));
1000
1007
  }
1001
1008
  if (visual.kind === 'token' && visual.networkImage) {
@@ -1010,7 +1017,7 @@ function createVisual(context, visual, selectorPresentation) {
1010
1017
  }
1011
1018
  // OneKey patch: image failure uses the same source-derived fallback as v1.
1012
1019
  const hasFallbackText = 'fallbackText' in visual;
1013
- if (fallbackIcon || hasFallbackText) {
1020
+ if ((fallbackIcon || hasFallbackText) && (!source || handlesSourceFallback)) {
1014
1021
  const showFallback = () => {
1015
1022
  if (image) {
1016
1023
  disposeWebImageRetries(image);
@@ -2563,8 +2570,25 @@ export class NativeListWebEngine {
2563
2570
  sectionIndexMetrics(viewportHeight) {
2564
2571
  const availableHeight = Math.max(0, viewportHeight - SECTION_INDEX_EDGE_PADDING * 2);
2565
2572
  const trackHeight = Math.min(availableHeight, SECTION_INDEX_LABEL_SPACING * this.sectionIndexEntries.length);
2573
+ const centeredOriginY = (viewportHeight - trackHeight) / 2;
2574
+ if (!this.snapshot.capabilities?.sectionIndex?.centeredInWindow) {
2575
+ return {
2576
+ originY: centeredOriginY,
2577
+ trackHeight
2578
+ };
2579
+ }
2580
+ const view = this.document.defaultView;
2581
+ if (!view) return {
2582
+ originY: centeredOriginY,
2583
+ trackHeight
2584
+ };
2585
+ const visualViewport = view.visualViewport;
2586
+ const windowCenterY = visualViewport ? visualViewport.offsetTop + visualViewport.height / 2 : view.innerHeight / 2;
2587
+ const railTop = this.viewportFrame.getBoundingClientRect().top + (Number.parseFloat(this.indexRail.style.top) || 0);
2588
+ const minOriginY = SECTION_INDEX_EDGE_PADDING;
2589
+ const maxOriginY = Math.max(minOriginY, viewportHeight - SECTION_INDEX_EDGE_PADDING - trackHeight);
2566
2590
  return {
2567
- originY: (viewportHeight - trackHeight) / 2,
2591
+ originY: Math.min(maxOriginY, Math.max(minOriginY, windowCenterY - railTop - trackHeight / 2)),
2568
2592
  trackHeight
2569
2593
  };
2570
2594
  }
@@ -19,6 +19,7 @@ export type ImageSource = Readonly<{
19
19
  autoplay?: boolean;
20
20
  optimizeTos?: boolean;
21
21
  overscan?: number;
22
+ /** Defaults to `none` in NativeList; use `static` or `skeleton` to opt into loading UI. */
22
23
  loadingStrategy?: ImageLoadingStrategy;
23
24
  fallbackUri?: string;
24
25
  retryTimes?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-list",
3
- "version": "3.0.118",
3
+ "version": "3.0.119",
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",
@@ -83,8 +83,8 @@
83
83
  "typescript": "^5.9.2"
84
84
  },
85
85
  "peerDependencies": {
86
- "@onekeyfe/react-native-image": "3.0.118",
87
- "@onekeyfe/react-native-native-logger": "3.0.118",
86
+ "@onekeyfe/react-native-image": "3.0.119",
87
+ "@onekeyfe/react-native-native-logger": "3.0.119",
88
88
  "react": "*",
89
89
  "react-native": "*",
90
90
  "react-native-nitro-modules": "0.37.0"
package/src/models.ts CHANGED
@@ -21,6 +21,7 @@ export type ImageSource = Readonly<{
21
21
  autoplay?: boolean;
22
22
  optimizeTos?: boolean;
23
23
  overscan?: number;
24
+ /** Defaults to `none` in NativeList; use `static` or `skeleton` to opt into loading UI. */
24
25
  loadingStrategy?: ImageLoadingStrategy;
25
26
  // OneKey patch: fallbackUri is Web-only; retryTimes opts all platforms into terminal retries.
26
27
  fallbackUri?: string;
@@ -1557,25 +1557,38 @@ function createVisual(
1557
1557
  return frame;
1558
1558
  }
1559
1559
 
1560
- // OneKey patch: Every image visual starts from the current theme backing.
1561
- // if ('backgroundColor' in visual && visual.backgroundColor)
1562
- // frame.style.background = visual.backgroundColor;
1563
- frame.style.background =
1560
+ // OneKey patch: NativeList images are usually local and should not flash a
1561
+ // placeholder. Callers can opt in with image.loadingStrategy.
1562
+ const visualBackground =
1564
1563
  'backgroundColor' in visual && visual.backgroundColor
1565
1564
  ? visual.backgroundColor
1566
- : 'var(--nl-strong)';
1565
+ : 'transparent';
1567
1566
  const source = visual.kind === 'image' ? visual.image : visual.image;
1567
+ const showsSourcePlaceholder =
1568
+ !!source &&
1569
+ source.loadingStrategy !== undefined &&
1570
+ source.loadingStrategy !== 'none';
1571
+ frame.style.background = showsSourcePlaceholder
1572
+ ? 'var(--nl-strong)'
1573
+ : visualBackground;
1568
1574
  const fallbackIcon =
1569
1575
  'fallbackIcon' in visual ? visual.fallbackIcon : undefined;
1570
1576
  const handlesSourceFallback =
1571
- !!source && (fallbackIcon !== undefined || 'fallbackText' in visual);
1572
- if (handlesSourceFallback) frame.style.background = 'var(--nl-strong)';
1577
+ showsSourcePlaceholder &&
1578
+ (fallbackIcon !== undefined || 'fallbackText' in visual);
1573
1579
  const image = source ? createImage(context, source) : undefined;
1574
1580
  if (image) {
1575
1581
  image.className = 'ok-native-list-visual-main';
1576
1582
  frame.appendChild(image);
1583
+ if (showsSourcePlaceholder) {
1584
+ const restoreBackground = () => {
1585
+ frame.style.background = visualBackground;
1586
+ };
1587
+ image.addEventListener('load', restoreBackground);
1588
+ if (image.complete && image.naturalWidth > 0) restoreBackground();
1589
+ }
1577
1590
  if (selectorPresentation) paintSelectorImageBackground(image, frame);
1578
- } else {
1591
+ } else if (!source || showsSourcePlaceholder) {
1579
1592
  frame.appendChild(
1580
1593
  createElement(
1581
1594
  context.document,
@@ -1608,7 +1621,7 @@ function createVisual(
1608
1621
  }
1609
1622
  // OneKey patch: image failure uses the same source-derived fallback as v1.
1610
1623
  const hasFallbackText = 'fallbackText' in visual;
1611
- if (fallbackIcon || hasFallbackText) {
1624
+ if ((fallbackIcon || hasFallbackText) && (!source || handlesSourceFallback)) {
1612
1625
  const showFallback = () => {
1613
1626
  if (image) {
1614
1627
  disposeWebImageRetries(image);
@@ -4320,8 +4333,29 @@ export class NativeListWebEngine {
4320
4333
  availableHeight,
4321
4334
  SECTION_INDEX_LABEL_SPACING * this.sectionIndexEntries.length
4322
4335
  );
4336
+ const centeredOriginY = (viewportHeight - trackHeight) / 2;
4337
+ if (!this.snapshot.capabilities?.sectionIndex?.centeredInWindow) {
4338
+ return { originY: centeredOriginY, trackHeight };
4339
+ }
4340
+ const view = this.document.defaultView;
4341
+ if (!view) return { originY: centeredOriginY, trackHeight };
4342
+ const visualViewport = view.visualViewport;
4343
+ const windowCenterY = visualViewport
4344
+ ? visualViewport.offsetTop + visualViewport.height / 2
4345
+ : view.innerHeight / 2;
4346
+ const railTop =
4347
+ this.viewportFrame.getBoundingClientRect().top +
4348
+ (Number.parseFloat(this.indexRail.style.top) || 0);
4349
+ const minOriginY = SECTION_INDEX_EDGE_PADDING;
4350
+ const maxOriginY = Math.max(
4351
+ minOriginY,
4352
+ viewportHeight - SECTION_INDEX_EDGE_PADDING - trackHeight
4353
+ );
4323
4354
  return {
4324
- originY: (viewportHeight - trackHeight) / 2,
4355
+ originY: Math.min(
4356
+ maxOriginY,
4357
+ Math.max(minOriginY, windowCenterY - railTop - trackHeight / 2)
4358
+ ),
4325
4359
  trackHeight,
4326
4360
  };
4327
4361
  }