@onekeyfe/react-native-native-list 3.0.134 → 3.0.136

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.
@@ -103,4 +103,5 @@ dependencies {
103
103
  implementation project(":react-native-nitro-modules")
104
104
  implementation project(":onekeyfe_react-native-native-logger")
105
105
  implementation project(":onekeyfe_react-native-image")
106
+ testImplementation "junit:junit:4.13.2"
106
107
  }
@@ -3,6 +3,25 @@ package com.margelo.nitro.nativelist
3
3
  import org.json.JSONArray
4
4
  import org.json.JSONObject
5
5
 
6
+ internal fun isNativeListRowPressEnabled(
7
+ type: String,
8
+ variant: String,
9
+ disabled: Boolean,
10
+ pressDisabled: Boolean,
11
+ ): Boolean = !disabled && !pressDisabled && (type != "system" || variant == "retry")
12
+
13
+ internal fun isNativeListWholeRowInteractive(
14
+ type: String,
15
+ variant: String,
16
+ disabled: Boolean,
17
+ pressDisabled: Boolean,
18
+ ): Boolean = type != "walletGroup" && isNativeListRowPressEnabled(
19
+ type = type,
20
+ variant = variant,
21
+ disabled = disabled,
22
+ pressDisabled = pressDisabled,
23
+ )
24
+
6
25
  internal data class NativeListItem(
7
26
  val key: String,
8
27
  val type: String,
@@ -22,9 +41,20 @@ internal data class NativeListItem(
22
41
  get() = !json.optBoolean("disabled", false) && type in SELECTABLE_TYPES
23
42
 
24
43
  val isRowPressEnabled: Boolean
25
- get() = !json.optBoolean("disabled", false) &&
26
- !json.optBoolean("pressDisabled", false) &&
27
- (type != "system" || json.optString("variant") == "retry")
44
+ get() = isNativeListRowPressEnabled(
45
+ type = type,
46
+ variant = json.optString("variant"),
47
+ disabled = json.optBoolean("disabled", false),
48
+ pressDisabled = json.optBoolean("pressDisabled", false),
49
+ )
50
+
51
+ val isWholeRowInteractive: Boolean
52
+ get() = isNativeListWholeRowInteractive(
53
+ type = type,
54
+ variant = json.optString("variant"),
55
+ disabled = json.optBoolean("disabled", false),
56
+ pressDisabled = json.optBoolean("pressDisabled", false),
57
+ )
28
58
 
29
59
  val isReorderable: Boolean
30
60
  get() {
@@ -395,6 +395,30 @@ private class NativeListTableColumnView(context: android.content.Context) : Line
395
395
  private fun sp(value: Float): Float = NativeListScale.font(resources, value)
396
396
  }
397
397
 
398
+ private object NativeListSourceFallbackState {
399
+ private const val CACHE_LIMIT = 128
400
+ private val sources = LinkedHashMap<String, Unit>(CACHE_LIMIT, 0.75f, true)
401
+
402
+ fun has(key: String): Boolean = synchronized(sources) {
403
+ sources[key] != null
404
+ }
405
+
406
+ fun remember(key: String) {
407
+ synchronized(sources) {
408
+ sources[key] = Unit
409
+ while (sources.size > CACHE_LIMIT) {
410
+ sources.remove(sources.entries.first().key)
411
+ }
412
+ }
413
+ }
414
+
415
+ fun forget(key: String) {
416
+ synchronized(sources) {
417
+ sources.remove(key)
418
+ }
419
+ }
420
+ }
421
+
398
422
  internal class NativeListRowView(
399
423
  private val reactContext: ThemedReactContext,
400
424
  ) : LinearLayout(reactContext) {
@@ -985,6 +1009,11 @@ internal class NativeListRowView(
985
1009
  "action" -> bindAction(item, theme, checkboxState)
986
1010
  "system" -> bindSystem(item, theme)
987
1011
  }
1012
+ // Keep passive rows out of accessibility and keyboard focus while allowing
1013
+ // their independently bound accessory controls to remain interactive.
1014
+ val wholeRowPressEnabled = item.isWholeRowInteractive
1015
+ isClickable = wholeRowPressEnabled
1016
+ isFocusable = wholeRowPressEnabled
988
1017
  applySize(item)
989
1018
  if (item.type == "system" && item.json.optString("variant") == "warning") {
990
1019
  title.typeface = NativeListFonts.medium(context)
@@ -1367,6 +1396,7 @@ internal class NativeListRowView(
1367
1396
  leadingIcon.visibility = GONE
1368
1397
  leadingIcon.iconName = ""
1369
1398
  leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(18), dp(18), Gravity.CENTER)
1399
+ leadingIcon.glyphSizeDp = null
1370
1400
  favoriteIcon.visibility = GONE
1371
1401
  favoriteIcon.iconName = ""
1372
1402
  headerTitleIcon.visibility = GONE
@@ -3209,13 +3239,27 @@ internal class NativeListRowView(
3209
3239
  val cornerIconData = visual.optJSONObject("cornerIcon")
3210
3240
  val fallback = visual.optString("fallbackText").take(2)
3211
3241
  val fallbackIconData = visual.optJSONObject("fallbackIcon")
3242
+ val fallbackIconSizeDp = fallbackIconData?.let {
3243
+ val slotSizeDp = minOf(sizeDp, heightDp)
3244
+ if (it.optString("name") == "GlobusOutline") {
3245
+ (slotSizeDp * 1.2f).roundToInt()
3246
+ } else {
3247
+ slotSizeDp
3248
+ }
3249
+ }
3212
3250
  val sourceLoadingStrategy = sources.firstOrNull()?.first?.optString("loadingStrategy", "none") ?: "none"
3213
3251
  val showsSourcePlaceholder = !isIcon && sources.isNotEmpty() && sourceLoadingStrategy != "none"
3214
3252
  val handlesSourceFallback =
3215
3253
  !isIcon && sources.isNotEmpty() &&
3216
3254
  showsSourcePlaceholder &&
3217
3255
  (visual.has("fallbackText") || fallbackIconData != null)
3218
- leadingFallback.text = if (handlesSourceFallback) "" else fallback
3256
+ val sourceFallbackKey = if (handlesSourceFallback) {
3257
+ sources.firstOrNull()?.first?.let(::sourceFallbackStateKey)
3258
+ } else null
3259
+ val restoresSourceFallback =
3260
+ sourceFallbackKey?.let(NativeListSourceFallbackState::has) == true
3261
+ leadingFallback.text =
3262
+ if (handlesSourceFallback && !restoresSourceFallback) "" else fallback
3219
3263
  leadingFallback.setTextColor(parseNativeListColor("#00000072"))
3220
3264
  val visualBackground = safeColor(
3221
3265
  visual.optString("backgroundColor"),
@@ -3234,6 +3278,23 @@ internal class NativeListRowView(
3234
3278
  )
3235
3279
  leadingFallback.visibility =
3236
3280
  if (!isIcon && (sources.isEmpty() || handlesSourceFallback)) VISIBLE else GONE
3281
+ if (handlesSourceFallback && fallbackIconData != null) {
3282
+ leadingIcon.iconName = fallbackIconData.optString("name")
3283
+ leadingIcon.tintColor = safeColor(
3284
+ fallbackIconData.optString("tintColor"),
3285
+ parseNativeListColor("#0000009B"),
3286
+ )
3287
+ leadingIcon.layoutParams = FrameLayout.LayoutParams(
3288
+ dp(fallbackIconSizeDp ?: sizeDp),
3289
+ dp(fallbackIconSizeDp ?: heightDp),
3290
+ Gravity.CENTER,
3291
+ )
3292
+ leadingIcon.glyphSizeDp = fallbackIconSizeDp
3293
+ // Keep the fallback measured while the source loads so an asynchronous
3294
+ // failure can reveal it without waiting for another RecyclerView layout.
3295
+ leadingFallback.visibility = if (restoresSourceFallback) GONE else VISIBLE
3296
+ leadingIcon.visibility = if (restoresSourceFallback) VISIBLE else INVISIBLE
3297
+ }
3237
3298
  if (isIcon) {
3238
3299
  leadingFrame.background = GradientDrawable().apply {
3239
3300
  setColor(visualBackground)
@@ -3253,6 +3314,12 @@ internal class NativeListRowView(
3253
3314
  fallbackIconData.optString("tintColor"),
3254
3315
  parseNativeListColor("#0000009B"),
3255
3316
  )
3317
+ leadingIcon.layoutParams = FrameLayout.LayoutParams(
3318
+ dp(fallbackIconSizeDp ?: sizeDp),
3319
+ dp(fallbackIconSizeDp ?: heightDp),
3320
+ Gravity.CENTER,
3321
+ )
3322
+ leadingIcon.glyphSizeDp = fallbackIconSizeDp
3256
3323
  leadingIcon.visibility = VISIBLE
3257
3324
  }
3258
3325
  val visibleSources = sources.take(leadingImages.size)
@@ -3351,6 +3418,7 @@ internal class NativeListRowView(
3351
3418
  bindImage(source, image, boundKey ?: "", index, variant,
3352
3419
  onLoad = if (!ownsSourceFallback) null else ({
3353
3420
  if (bindingEpoch == expectedEpoch) {
3421
+ sourceFallbackKey?.let(NativeListSourceFallbackState::forget)
3354
3422
  image.visibility = VISIBLE
3355
3423
  leadingFallback.visibility = GONE
3356
3424
  leadingIcon.visibility = GONE
@@ -3358,6 +3426,7 @@ internal class NativeListRowView(
3358
3426
  }),
3359
3427
  onError = if (!ownsSourceFallback) null else ({
3360
3428
  if (bindingEpoch == expectedEpoch) {
3429
+ sourceFallbackKey?.let(NativeListSourceFallbackState::remember)
3361
3430
  image.visibility = GONE
3362
3431
  if (fallbackIcon != null) {
3363
3432
  leadingFallback.visibility = GONE
@@ -3468,6 +3537,11 @@ internal class NativeListRowView(
3468
3537
  }
3469
3538
  }
3470
3539
 
3540
+ private fun sourceFallbackStateKey(source: JSONObject): String? =
3541
+ source.optString("uri").trim().takeIf(String::isNotEmpty)?.let { uri ->
3542
+ "$uri\u0000${source.optJSONObject("headers")?.toString().orEmpty()}"
3543
+ }
3544
+
3471
3545
  private fun leadingImageLayout(
3472
3546
  index: Int,
3473
3547
  count: Int,
@@ -4154,8 +4228,10 @@ internal class NativeListRowView(
4154
4228
  val uri = source.optString("uri").trim().takeIf(String::isNotEmpty)
4155
4229
  val sourceHeadersJson = source.optJSONObject("headers")?.toString()
4156
4230
  val recyclingKey = if (retryAttempt == 0) "$token:$slot" else "$token:$slot:retry:$retryAttempt"
4157
- if (hideUntilLoaded && !imageView.isDisplaying(uri, sourceHeadersJson, recyclingKey)) {
4158
- imageView.visibility = INVISIBLE
4231
+ if (hideUntilLoaded) {
4232
+ imageView.visibility = if (
4233
+ imageView.isDisplaying(uri, sourceHeadersJson, recyclingKey)
4234
+ ) VISIBLE else INVISIBLE
4159
4235
  }
4160
4236
  val handleLoad: (() -> Unit)? = if (hideUntilLoaded) ({
4161
4237
  if (bindingEpoch == expectedEpoch) {
@@ -4189,8 +4265,8 @@ internal class NativeListRowView(
4189
4265
  }),
4190
4266
  onError = if (retryLimit == 0) handleError else ({
4191
4267
  if (bindingEpoch == expectedEpoch) {
4192
- if (retryAttempt >= retryLimit) handleError?.invoke()
4193
- else if (!selectorImageRetries.containsKey(imageView)) {
4268
+ handleError?.invoke()
4269
+ if (retryAttempt < retryLimit && !selectorImageRetries.containsKey(imageView)) {
4194
4270
  val retry = Runnable {
4195
4271
  if (bindingEpoch == expectedEpoch) {
4196
4272
  selectorImageRetries.remove(imageView)
@@ -53,6 +53,24 @@ import kotlin.math.roundToInt
53
53
  import kotlin.math.sin
54
54
  import kotlin.math.sqrt
55
55
 
56
+ private class NativeListGridLayoutManager(
57
+ context: Context,
58
+ spanCount: Int,
59
+ ) : GridLayoutManager(context, spanCount) {
60
+ override fun removeAndRecycleViewAt(index: Int, recycler: RecyclerView.Recycler) {
61
+ val child = getChildAt(index) ?: return
62
+ removeViewAt(index)
63
+ // An index-based removal can leave the selected child attached during rapid
64
+ // nested scrolling. Detach that exact view before giving it to RecyclerView.
65
+ if (child.parent != null) removeView(child)
66
+ if (child.parent == null) {
67
+ recycler.recycleView(child)
68
+ } else {
69
+ requestLayout()
70
+ }
71
+ }
72
+ }
73
+
56
74
  class NativeListView(
57
75
  private val reactContext: ThemedReactContext,
58
76
  ) : LinearLayout(reactContext) {
@@ -110,7 +128,7 @@ class NativeListView(
110
128
  private var refreshIndicatorOffsetPx = 0
111
129
  private val contentContainer = FrameLayout(context)
112
130
  private val adapter = NativeListAdapter(reactContext)
113
- private val layoutManager = GridLayoutManager(context, 1)
131
+ private val layoutManager = NativeListGridLayoutManager(context, 1)
114
132
  // OneKey patch: vertical lists retain vertical drags and leave horizontal drags to a parent pager.
115
133
  private val pagerGestureTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
116
134
  private var pagerGestureIsVertical = false
@@ -0,0 +1,43 @@
1
+ package com.margelo.nitro.nativelist
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Test
5
+
6
+ class NativeListPressPolicyTest {
7
+ @Test
8
+ fun wholeRowInteractionMatchesPassiveAndAccessoryOnlyRows() {
9
+ data class Case(
10
+ val type: String,
11
+ val variant: String = "",
12
+ val disabled: Boolean = false,
13
+ val pressDisabled: Boolean = false,
14
+ val expected: Boolean,
15
+ )
16
+
17
+ val cases = listOf(
18
+ Case(type = "identity", expected = true),
19
+ Case(type = "identity", pressDisabled = true, expected = false),
20
+ Case(type = "identity", disabled = true, expected = false),
21
+ Case(type = "system", variant = "retry", expected = true),
22
+ Case(type = "system", variant = "retry", pressDisabled = true, expected = false),
23
+ Case(type = "system", variant = "loading", expected = false),
24
+ Case(type = "system", variant = "noMatch", expected = false),
25
+ Case(type = "system", variant = "warning", expected = false),
26
+ Case(type = "system", variant = "end", expected = false),
27
+ Case(type = "system", variant = "spacer", expected = false),
28
+ Case(type = "walletGroup", expected = false),
29
+ )
30
+
31
+ assertEquals(
32
+ cases.map(Case::expected),
33
+ cases.map { case ->
34
+ isNativeListWholeRowInteractive(
35
+ type = case.type,
36
+ variant = case.variant,
37
+ disabled = case.disabled,
38
+ pressDisabled = case.pressDisabled,
39
+ )
40
+ },
41
+ )
42
+ }
43
+ }
@@ -4,6 +4,26 @@ import CoreText
4
4
  import OneKeyImage
5
5
  import UIKit
6
6
 
7
+ private enum NativeListSourceFallbackState {
8
+ private static let sources: NSCache<NSString, NSNumber> = {
9
+ let cache = NSCache<NSString, NSNumber>()
10
+ cache.countLimit = 128
11
+ return cache
12
+ }()
13
+
14
+ static func has(_ key: String) -> Bool {
15
+ sources.object(forKey: key as NSString) != nil
16
+ }
17
+
18
+ static func remember(_ key: String) {
19
+ sources.setObject(NSNumber(value: true), forKey: key as NSString)
20
+ }
21
+
22
+ static func forget(_ key: String) {
23
+ sources.removeObject(forKey: key as NSString)
24
+ }
25
+ }
26
+
7
27
  // Market/TokenListSkeleton: source geometry and the native Skeleton's 3s shimmer.
8
28
  private final class NativeListMarketSkeleton: UIView {
9
29
  private let marks = (0..<5).map { _ in UIView() }
@@ -3408,12 +3428,20 @@ final class NativeListCell: UICollectionViewCell {
3408
3428
  let cornerIcon = visual.dictionary("cornerIcon")
3409
3429
  let fallbackText = String(visual.string("fallbackText").prefix(2))
3410
3430
  let fallbackIconData = visual.dictionary("fallbackIcon")
3431
+ let fallbackIconSize = fallbackIconData.map {
3432
+ let slotSize = min(leadingWidth.constant, leadingHeight.constant)
3433
+ return $0.string("name") == "GlobusOutline" ? slotSize * 1.2 : slotSize
3434
+ }
3411
3435
  let sourceLoadingStrategy = sources.first?.data.string("loadingStrategy", default: "none") ?? "none"
3412
3436
  let showsSourcePlaceholder = !isIcon && !sources.isEmpty && sourceLoadingStrategy != "none"
3413
3437
  let handlesSourceFallback = !isIcon && !sources.isEmpty &&
3414
3438
  showsSourcePlaceholder &&
3415
3439
  (visual["fallbackText"] != nil || fallbackIconData != nil)
3416
- fallbackLabel.text = handlesSourceFallback ? nil : fallbackText
3440
+ let sourceFallbackKey = handlesSourceFallback
3441
+ ? sources.first.flatMap { sourceFallbackStateKey($0.data) }
3442
+ : nil
3443
+ let restoresSourceFallback = sourceFallbackKey.map(NativeListSourceFallbackState.has) ?? false
3444
+ fallbackLabel.text = handlesSourceFallback && !restoresSourceFallback ? nil : fallbackText
3417
3445
  let sourceFallbackBackground = currentTheme?["strongBackground"] as? String ?? "#0000000F"
3418
3446
  // OneKey patch: source-backed visuals default to no placeholder/background.
3419
3447
  // A non-none image loadingStrategy opts back into the themed placeholder.
@@ -3451,6 +3479,20 @@ final class NativeListCell: UICollectionViewCell {
3451
3479
  leadingIconImageView.image = nativeListIcon(named: fallbackIconData.string("name"))
3452
3480
  leadingIconImageView.contentMode = .scaleAspectFit
3453
3481
  }
3482
+ if handlesSourceFallback, let fallbackIconData {
3483
+ fallbackLabel.isHidden = true
3484
+ leadingIconImageView.image = nativeListIcon(named: fallbackIconData.string("name"))
3485
+ leadingIconImageView.tintColor = UIColor(
3486
+ nativeListHex: fallbackIconData.string("tintColor", default: "#646464"),
3487
+ fallback: .darkGray
3488
+ )
3489
+ leadingIconImageView.contentMode = .scaleAspectFit
3490
+ leadingIconWidth.constant = fallbackIconSize ?? leadingWidth.constant
3491
+ leadingIconHeight.constant = fallbackIconSize ?? leadingHeight.constant
3492
+ leadingIconImageView.isHidden = !restoresSourceFallback
3493
+ } else if handlesSourceFallback {
3494
+ fallbackLabel.isHidden = !restoresSourceFallback
3495
+ }
3454
3496
  let visibleSources = Array(sources.prefix(leadingImages.count))
3455
3497
  let tokenPair = kind == "token" && visibleSources.count > 1
3456
3498
  leadingContainer.clipsToBounds = !tokenPair && cornerIcon == nil
@@ -3506,6 +3548,9 @@ final class NativeListCell: UICollectionViewCell {
3506
3548
  bindImage(source.data, into: imageView, token: key, slot: index, variant: source.variant,
3507
3549
  onLoad: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
3508
3550
  guard let self, self.bindingEpoch == expectedEpoch else { return }
3551
+ if let sourceFallbackKey {
3552
+ NativeListSourceFallbackState.forget(sourceFallbackKey)
3553
+ }
3509
3554
  imageView?.isHidden = false
3510
3555
  self.fallbackLabel.isHidden = true
3511
3556
  self.leadingIconImageView.isHidden = true
@@ -3513,6 +3558,9 @@ final class NativeListCell: UICollectionViewCell {
3513
3558
  },
3514
3559
  onError: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
3515
3560
  guard let self, self.bindingEpoch == expectedEpoch else { return }
3561
+ if let sourceFallbackKey {
3562
+ NativeListSourceFallbackState.remember(sourceFallbackKey)
3563
+ }
3516
3564
  imageView?.isHidden = true
3517
3565
  if let fallbackIcon {
3518
3566
  self.fallbackLabel.isHidden = true
@@ -3643,6 +3691,20 @@ final class NativeListCell: UICollectionViewCell {
3643
3691
  return sources
3644
3692
  }
3645
3693
 
3694
+ private func sourceFallbackStateKey(_ source: [String: Any]) -> String? {
3695
+ let uri = source.string("uri").trimmingCharacters(in: .whitespacesAndNewlines)
3696
+ guard !uri.isEmpty else { return nil }
3697
+ let headersJson: String
3698
+ if let headers = source.dictionary("headers"),
3699
+ JSONSerialization.isValidJSONObject(headers),
3700
+ let data = try? JSONSerialization.data(withJSONObject: headers, options: [.sortedKeys]) {
3701
+ headersJson = String(data: data, encoding: .utf8) ?? ""
3702
+ } else {
3703
+ headersJson = ""
3704
+ }
3705
+ return "\(uri)\u{0}\(headersJson)"
3706
+ }
3707
+
3646
3708
  private func leadingConstraints(
3647
3709
  _ imageView: UIView,
3648
3710
  index: Int,
@@ -115,6 +115,7 @@ final class NativeListView: UIView {
115
115
  private let reorderStartFeedback = UIImpactFeedbackGenerator(style: .medium)
116
116
  private let reorderMoveFeedback = UISelectionFeedbackGenerator()
117
117
  private var interactiveReorderFeedbackIndex: Int?
118
+ private var interactiveMovementKeys: [String]?
118
119
  private var actionAnchor: ActionAnchorRecord?
119
120
  private let actionAnchorInstanceID = UUID().uuidString
120
121
  private var actionAnchorCounter = 0
@@ -731,6 +732,9 @@ final class NativeListView: UIView {
731
732
  collectionView.layoutIfNeeded()
732
733
  return
733
734
  }
735
+ if !interactiveReorderUsesAtomicTargeting {
736
+ interactiveMovementKeys = dataSource.snapshot().itemIdentifiers
737
+ }
734
738
  interactiveReorderFeedbackIndex = indexPath.item
735
739
  reorderStartFeedback.impactOccurred(intensity: 0.7)
736
740
  reorderStartFeedback.prepare()
@@ -838,9 +842,11 @@ final class NativeListView: UIView {
838
842
  interactiveReorderAnimator?.stopAnimation(true)
839
843
  interactiveReorderFeedbackIndex = nil
840
844
  if cancelled {
845
+ interactiveMovementKeys = nil
841
846
  collectionView.cancelInteractiveMovement()
842
847
  } else {
843
848
  collectionView.endInteractiveMovement()
849
+ interactiveMovementKeys = nil
844
850
  }
845
851
  let animator = UIViewPropertyAnimator(
846
852
  duration: ReorderAnimation.duration,
@@ -874,6 +880,7 @@ final class NativeListView: UIView {
874
880
  guard interactiveReorderSource != nil else { return }
875
881
  interactiveReorderAnimator?.stopAnimation(true)
876
882
  interactiveReorderAnimator = nil
883
+ interactiveMovementKeys = nil
877
884
  collectionView.cancelInteractiveMovement()
878
885
  interactiveReorderCell?.setPressed(false)
879
886
  if interactiveReorderCompactKey != nil {
@@ -978,6 +985,23 @@ final class NativeListView: UIView {
978
985
  return collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? NativeListCell
979
986
  }
980
987
 
988
+ // OneKey patch: flow layout sizes an interactive move in UIKit's in-flight order,
989
+ // while the diffable snapshot and cell index paths keep the pre-drag order until
990
+ // the move commits. Resolving sizes from the snapshot swaps row heights.
991
+ private func layoutItem(at indexPath: IndexPath) -> NativeListItem? {
992
+ guard let keys = interactiveMovementKeys else { return item(at: indexPath) }
993
+ return keys[safe: indexPath.item].flatMap { itemsByKey[$0] }
994
+ }
995
+
996
+ private func updateInteractiveMovementKeys(movingFrom source: IndexPath, to target: IndexPath) {
997
+ guard interactiveMovementKeys != nil else { return }
998
+ var keys = dataSource.snapshot().itemIdentifiers
999
+ guard keys.indices.contains(source.item) else { return }
1000
+ let key = keys.remove(at: source.item)
1001
+ keys.insert(key, at: min(max(target.item, 0), keys.count))
1002
+ interactiveMovementKeys = keys
1003
+ }
1004
+
981
1005
  private func item(at indexPath: IndexPath) -> NativeListItem? {
982
1006
  if let key = dataSource.itemIdentifier(for: indexPath), let item = itemsByKey[key] {
983
1007
  return item
@@ -2049,7 +2073,9 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
2049
2073
  atCurrentIndexPath currentIndexPath: IndexPath,
2050
2074
  toProposedIndexPath proposedIndexPath: IndexPath
2051
2075
  ) -> IndexPath {
2052
- interactiveReorderUsesAtomicTargeting ? currentIndexPath : proposedIndexPath
2076
+ guard !interactiveReorderUsesAtomicTargeting else { return currentIndexPath }
2077
+ updateInteractiveMovementKeys(movingFrom: originalIndexPath, to: proposedIndexPath)
2078
+ return proposedIndexPath
2053
2079
  }
2054
2080
 
2055
2081
  func collectionView(
@@ -2057,7 +2083,9 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
2057
2083
  targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath,
2058
2084
  toProposedIndexPath proposedIndexPath: IndexPath
2059
2085
  ) -> IndexPath {
2060
- interactiveReorderUsesAtomicTargeting ? originalIndexPath : proposedIndexPath
2086
+ guard !interactiveReorderUsesAtomicTargeting else { return originalIndexPath }
2087
+ updateInteractiveMovementKeys(movingFrom: originalIndexPath, to: proposedIndexPath)
2088
+ return proposedIndexPath
2061
2089
  }
2062
2090
 
2063
2091
  func collectionView(
@@ -2065,7 +2093,7 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
2065
2093
  layout collectionViewLayout: UICollectionViewLayout,
2066
2094
  sizeForItemAt indexPath: IndexPath
2067
2095
  ) -> CGSize {
2068
- guard let config, let item = item(at: indexPath) else { return .zero }
2096
+ guard let config, let item = layoutItem(at: indexPath) else { return .zero }
2069
2097
  let insets = flowLayout.sectionInset
2070
2098
  if config.orientation == "horizontal" {
2071
2099
  let width: CGFloat = item.type == "rail" ? railWidth(item) : item.type == "mediaTile" ? 200 : 280
@@ -537,6 +537,10 @@ export const WEB_LIST_CSS = `
537
537
  .ok-native-list-account-row[data-native-list-selector="accountSelector"]>.ok-native-list-accessories[data-native-list-account-control="createAddress"]{position:absolute;top:18px;right:12px}
538
538
  .ok-native-list-account-row[data-native-list-selector="accountSelector"] .ok-native-list-accessories>[data-native-list-account-control="createAddress"]{flex-basis:36px;width:36px;height:36px;padding:6px;border-radius:8px}
539
539
  .ok-native-list-account-action-row{padding-left:12px;padding-right:12px}.ok-native-list-account-action-row .ok-native-list-action-title{font-size:16px;line-height:24px;font-weight:400}.ok-native-list-account-action-row .ok-native-list-action-title[data-tone="primary"]{color:var(--nl-primary)}
540
+ /* OneKey patch: account selector wallets and accounts keep the default cursor instead of pointer or grab affordances. */
541
+ .ok-native-list-root .ok-native-list-item>.ok-native-list-wallet-row,.ok-native-list-root .ok-native-list-wallet-member>.ok-native-list-wallet-row,.ok-native-list-root .ok-native-list-item>.ok-native-list-account-row,.ok-native-list-root .ok-native-list-item>.ok-native-list-account-action-row,.ok-native-list-wallet-member,.ok-native-list-wallet-row *,.ok-native-list-account-row *,.ok-native-list-account-action-row *{cursor:default}
542
+ /* OneKey patch: Add account hover and pressed backgrounds keep the account row corner radius. */
543
+ .ok-native-list-account-action-row{border-radius:12px}
540
544
  `;
541
545
  function createElement(document, tag, className, text) {
542
546
  const element = document.createElement(tag);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-list",
3
- "version": "3.0.134",
3
+ "version": "3.0.136",
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.134",
87
- "@onekeyfe/react-native-native-logger": "3.0.134",
86
+ "@onekeyfe/react-native-image": "3.0.136",
87
+ "@onekeyfe/react-native-native-logger": "3.0.136",
88
88
  "react": "*",
89
89
  "react-native": "*",
90
90
  "react-native-nitro-modules": "0.37.0"
@@ -965,6 +965,10 @@ export const WEB_LIST_CSS = `
965
965
  .ok-native-list-account-row[data-native-list-selector="accountSelector"]>.ok-native-list-accessories[data-native-list-account-control="createAddress"]{position:absolute;top:18px;right:12px}
966
966
  .ok-native-list-account-row[data-native-list-selector="accountSelector"] .ok-native-list-accessories>[data-native-list-account-control="createAddress"]{flex-basis:36px;width:36px;height:36px;padding:6px;border-radius:8px}
967
967
  .ok-native-list-account-action-row{padding-left:12px;padding-right:12px}.ok-native-list-account-action-row .ok-native-list-action-title{font-size:16px;line-height:24px;font-weight:400}.ok-native-list-account-action-row .ok-native-list-action-title[data-tone="primary"]{color:var(--nl-primary)}
968
+ /* OneKey patch: account selector wallets and accounts keep the default cursor instead of pointer or grab affordances. */
969
+ .ok-native-list-root .ok-native-list-item>.ok-native-list-wallet-row,.ok-native-list-root .ok-native-list-wallet-member>.ok-native-list-wallet-row,.ok-native-list-root .ok-native-list-item>.ok-native-list-account-row,.ok-native-list-root .ok-native-list-item>.ok-native-list-account-action-row,.ok-native-list-wallet-member,.ok-native-list-wallet-row *,.ok-native-list-account-row *,.ok-native-list-account-action-row *{cursor:default}
970
+ /* OneKey patch: Add account hover and pressed backgrounds keep the account row corner radius. */
971
+ .ok-native-list-account-action-row{border-radius:12px}
968
972
  `;
969
973
 
970
974
  function createElement(