@onekeyfe/react-native-native-list 3.0.105 → 3.0.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -76,8 +76,31 @@ func nativeListTabularFont(
76
76
  }
77
77
 
78
78
  func nativeListIcon(named name: String) -> UIImage? {
79
+ // OneKey patch: the connection indicator is a solid circle at its requested size.
80
+ if name == "Circle" {
81
+ return UIGraphicsImageRenderer(size: CGSize(width: 24, height: 24)).image { _ in
82
+ UIColor.black.setFill()
83
+ UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 24, height: 24)).fill()
84
+ }.withRenderingMode(.alwaysTemplate)
85
+ }
79
86
  let assetName: String
80
87
  switch name {
88
+ // OneKey patch: official selector icon geometry.
89
+ case "AccountErrorCustom": assetName = "onekey_selector_account_error_custom"
90
+ // OneKey patch: official selector icon geometry.
91
+ case "CrossedSmallSolid": assetName = "onekey_selector_crossed_small_solid"
92
+ // OneKey patch: official selector icon geometry.
93
+ case "AllNetworksSolid": assetName = "onekey_selector_all_networks_solid"
94
+ // OneKey patch: official selector icon geometry.
95
+ case "BotIllus": assetName = "onekey_selector_bot_illus"
96
+ // OneKey patch: official selector icon geometry.
97
+ case "AppleBrand": assetName = "onekey_selector_apple_brand"
98
+ // OneKey patch: official selector icon geometry.
99
+ case "GoogleIllus": assetName = "onekey_selector_google_illus"
100
+ // OneKey patch: official selector icon geometry.
101
+ case "LockSolid": assetName = "onekey_selector_lock_solid"
102
+ // OneKey patch: official selector icon geometry.
103
+ case "GlobusOutline": assetName = "onekey_selector_globus_outline"
81
104
  case "ArrowBottomOutline": assetName = "onekey_arrow_bottom"
82
105
  case "ArrowTopOutline": assetName = "onekey_arrow_top"
83
106
  case "ChartTrendingUpOutline": assetName = "onekey_chart_trending_up"
@@ -106,5 +129,5 @@ func nativeListIcon(named name: String) -> UIImage? {
106
129
  default: return nil
107
130
  }
108
131
  return UIImage(named: assetName, in: NativeListResources.bundle, compatibleWith: nil)?
109
- .withRenderingMode(.alwaysTemplate)
132
+ .withRenderingMode(["GoogleIllus", "BotIllus", "AccountErrorCustom"].contains(name) ? .alwaysOriginal : .alwaysTemplate)
110
133
  }
@@ -1,4 +1,5 @@
1
1
  import Foundation
2
+ import CoreFoundation
2
3
  import UIKit
3
4
  import UniformTypeIdentifiers
4
5
 
@@ -51,7 +52,12 @@ final class NativeListView: UIView {
51
52
  var onSelectionDelta: ((String) -> Void)?
52
53
  var onReorder: ((String) -> Void)?
53
54
  var onEndReached: ((String) -> Void)?
54
- var onVisibleRangeChanged: ((String) -> Void)?
55
+ var onVisibleRangeChanged: ((String) -> Void)? {
56
+ didSet {
57
+ lastVisibleRange = nil
58
+ if onVisibleRangeChanged != nil { emitVisibleRangeIfNeeded() }
59
+ }
60
+ }
55
61
 
56
62
  private let flowLayout = NativeListFlowLayout()
57
63
  private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
@@ -77,6 +83,8 @@ final class NativeListView: UIView {
77
83
  target: self,
78
84
  action: #selector(reorderLongPressChanged(_:))
79
85
  )
86
+ // OneKey patch: claim only vertical drags so held rows and ancestor pagers stay responsive.
87
+ private lazy var listBodyGestureGuard = UIPanGestureRecognizer(target: nil, action: nil)
80
88
  private var interactiveReorderSource: (key: String, index: Int)?
81
89
  private weak var interactiveReorderCell: NativeListCell?
82
90
  private var interactiveReorderCompactKey: String?
@@ -92,7 +100,10 @@ final class NativeListView: UIView {
92
100
  private let actionAnchorInstanceID = UUID().uuidString
93
101
  private var actionAnchorCounter = 0
94
102
 
95
- private static let sectionIndexGutter: CGFloat = 44
103
+ private static let sectionIndexContentInset: CGFloat = 16
104
+ private static let sectionIndexRailWidth: CGFloat = 32
105
+ private static let sectionIndexPreviewSize: CGFloat = 48
106
+ private static let sectionIndexPreviewEndMargin: CGFloat = 40
96
107
 
97
108
  override init(frame: CGRect) {
98
109
  super.init(frame: frame)
@@ -102,6 +113,11 @@ final class NativeListView: UIView {
102
113
  collectionView.dragDelegate = self
103
114
  collectionView.dropDelegate = self
104
115
  collectionView.alwaysBounceVertical = true
116
+ // OneKey patch: keep list-body drags from being claimed by an ancestor modal sheet.
117
+ listBodyGestureGuard.cancelsTouchesInView = false
118
+ listBodyGestureGuard.isEnabled = false
119
+ listBodyGestureGuard.delegate = self
120
+ collectionView.addGestureRecognizer(listBodyGestureGuard)
105
121
  reorderLongPress.minimumPressDuration = ReorderAnimation.longPressDuration
106
122
  reorderLongPress.allowableMovement = ReorderAnimation.allowableMovement
107
123
  reorderLongPress.delegate = self
@@ -136,11 +152,14 @@ final class NativeListView: UIView {
136
152
  sectionIndexView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
137
153
  sectionIndexView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
138
154
  sectionIndexView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
139
- sectionIndexView.widthAnchor.constraint(equalToConstant: Self.sectionIndexGutter),
140
- sectionIndexPreview.centerXAnchor.constraint(equalTo: collectionView.centerXAnchor),
155
+ sectionIndexView.widthAnchor.constraint(equalToConstant: Self.sectionIndexRailWidth),
156
+ sectionIndexPreview.trailingAnchor.constraint(
157
+ equalTo: safeAreaLayoutGuide.trailingAnchor,
158
+ constant: -Self.sectionIndexPreviewEndMargin
159
+ ),
141
160
  sectionIndexPreview.centerYAnchor.constraint(equalTo: collectionView.centerYAnchor),
142
- sectionIndexPreview.widthAnchor.constraint(equalToConstant: 72),
143
- sectionIndexPreview.heightAnchor.constraint(equalToConstant: 72),
161
+ sectionIndexPreview.widthAnchor.constraint(equalToConstant: Self.sectionIndexPreviewSize),
162
+ sectionIndexPreview.heightAnchor.constraint(equalToConstant: Self.sectionIndexPreviewSize),
144
163
  ])
145
164
 
146
165
  sectionIndexView.isHidden = true
@@ -152,11 +171,11 @@ final class NativeListView: UIView {
152
171
  }
153
172
  sectionIndexPreview.isHidden = true
154
173
  sectionIndexPreview.alpha = 0
155
- sectionIndexPreview.layer.cornerRadius = 16
174
+ sectionIndexPreview.layer.cornerRadius = 14
156
175
  sectionIndexPreview.layer.masksToBounds = true
157
176
  sectionIndexPreview.textAlignment = .center
158
177
  sectionIndexPreview.adjustsFontForContentSizeCategory = true
159
- sectionIndexPreview.font = nativeListFont(ofSize: 28, weight: .semibold)
178
+ sectionIndexPreview.font = nativeListFont(ofSize: 22, weight: .semibold)
160
179
  sectionIndexPreview.isAccessibilityElement = false
161
180
 
162
181
  footerCell.onAction = { [weak self] item, action, target, origin in
@@ -225,15 +244,18 @@ final class NativeListView: UIView {
225
244
  guard let next = try? NativeListConfig.parse(json: json) else { return }
226
245
  invalidateActionAnchor(reason: "snapshot")
227
246
  if let current = config, isControlledSelectionSnapshotUpdate(from: current, to: next) {
247
+ let changedSummaryKeys = Set(zip(current.items, next.items).compactMap { old, new in
248
+ old.content != new.content ? new.key : nil
249
+ })
228
250
  config = next
229
251
  itemsByKey = Dictionary(uniqueKeysWithValues: next.items.map { ($0.key, $0) })
230
- refreshVisibleSelection()
252
+ refreshVisibleSelection(changedSummaryKeys: changedSummaryKeys)
231
253
  return
232
254
  }
233
255
  let oldItems = itemsByKey
256
+ if config?.generation != next.generation { endReachedGeneration = nil }
234
257
  config = next
235
258
  itemsByKey = Dictionary(uniqueKeysWithValues: next.items.map { ($0.key, $0) })
236
- endReachedGeneration = nil
237
259
  configureSectionIndex(next)
238
260
  configureLayout(next)
239
261
  configureRefresh(next)
@@ -275,12 +297,14 @@ final class NativeListView: UIView {
275
297
  }
276
298
 
277
299
  var changedKeys: [String] = []
300
+ var sizeChanged = false
278
301
  for (index, changes) in pending {
279
302
  var merged = current.items[index].data
280
303
  changes.forEach { key, value in
281
304
  if key != "key" && key != "type" { merged[key] = value }
282
305
  }
283
306
  guard let item = try? NativeListItem(data: merged) else { return }
307
+ if rowHeight(current.items[index]) != rowHeight(item) { sizeChanged = true }
284
308
  current.items[index] = item
285
309
  changedKeys.append(item.key)
286
310
  if let selected = changes["selected"] as? Bool {
@@ -292,7 +316,13 @@ final class NativeListView: UIView {
292
316
  itemsByKey = Dictionary(uniqueKeysWithValues: current.items.map { ($0.key, $0) })
293
317
  var snapshot = dataSource.snapshot()
294
318
  snapshot.reconfigureItems(changedKeys.filter { snapshot.indexOfItem($0) != nil })
295
- dataSource.apply(snapshot, animatingDifferences: false)
319
+ dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
320
+ guard let self else { return }
321
+ if sizeChanged { self.flowLayout.invalidateLayout() }
322
+ self.collectionView.layoutIfNeeded()
323
+ self.emitVisibleRangeIfNeeded()
324
+ self.checkEndReached()
325
+ }
296
326
  configureFooter(current)
297
327
  }
298
328
 
@@ -472,7 +502,8 @@ final class NativeListView: UIView {
472
502
 
473
503
  private func configureLayout(_ config: NativeListConfig) {
474
504
  let isHorizontal = config.orientation == "horizontal"
475
- let indexGutter = sectionIndexEntries.isEmpty ? 0 : Self.sectionIndexGutter
505
+ // OneKey patch: the section index overlays rows and keeps only an accessory-safe inset.
506
+ let indexGutter = sectionIndexEntries.isEmpty ? 0 : Self.sectionIndexContentInset
476
507
  let isRightToLeft = effectiveUserInterfaceLayoutDirection == .rightToLeft
477
508
  flowLayout.scrollDirection = isHorizontal ? .horizontal : .vertical
478
509
  flowLayout.minimumLineSpacing = config.itemSpacing
@@ -485,7 +516,7 @@ final class NativeListView: UIView {
485
516
  )
486
517
  flowLayout.stickyItemIndexes = config.stickyHeaders
487
518
  ? Set(config.items.enumerated().compactMap {
488
- $0.element.type == "sectionHeader" && $0.element.data.string("variant") != "summary"
519
+ $0.element.type == "sectionHeader" && $0.element.data.bool("sticky", default: true) && $0.element.data.string("variant") != "summary"
489
520
  ? $0.offset
490
521
  : nil
491
522
  })
@@ -493,6 +524,7 @@ final class NativeListView: UIView {
493
524
  flowLayout.invalidateLayout()
494
525
  collectionView.alwaysBounceHorizontal = isHorizontal
495
526
  collectionView.alwaysBounceVertical = !isHorizontal
527
+ listBodyGestureGuard.isEnabled = !isHorizontal
496
528
  collectionView.showsVerticalScrollIndicator = sectionIndexEntries.isEmpty
497
529
  collectionView.dragInteractionEnabled = false
498
530
  lastLayoutDirection = effectiveUserInterfaceLayoutDirection
@@ -512,11 +544,12 @@ final class NativeListView: UIView {
512
544
  interactiveReorderCell = nil
513
545
  return
514
546
  }
515
- if item.type == "walletGroup", gesture.location(in: cell).y > 68 {
516
- interactiveReorderSource = nil
517
- interactiveReorderCell = nil
518
- return
519
- }
547
+ // OneKey patch: a hidden wallet child starts dragging its whole logical group.
548
+ // if item.type == "walletGroup", gesture.location(in: cell).y > 68 {
549
+ // interactiveReorderSource = nil
550
+ // interactiveReorderCell = nil
551
+ // return
552
+ // }
520
553
  interactiveReorderSource = (item.key, indexPath.item)
521
554
  interactiveReorderCell = cell
522
555
  interactiveReorderUsesAtomicTargeting = item.type == "identity" &&
@@ -838,7 +871,8 @@ final class NativeListView: UIView {
838
871
  sectionIndexView.configure(
839
872
  titles: sectionIndexEntries.map(\.title),
840
873
  textColor: nativeListColor(config.theme, "secondaryText", "#646464"),
841
- activeColor: nativeListColor(config.theme, "accent", "#108303")
874
+ activeColor: nativeListColor(config.theme, "accent", "#108303"),
875
+ activeTextColor: nativeListColor(config.theme, "inverseText", "#FCFCFC")
842
876
  )
843
877
  sectionIndexView.isHidden = sectionIndexEntries.isEmpty
844
878
  sectionIndexPreview.backgroundColor = nativeListColor(
@@ -932,7 +966,7 @@ final class NativeListView: UIView {
932
966
  theme: config.theme,
933
967
  layout: config.layout,
934
968
  itemIndex: itemIndex,
935
- selected: config.selectedKeys.contains(item.key),
969
+ selected: item.data.bool("selected") || config.selectedKeys.contains(item.key),
936
970
  checkboxState: { [weak self] item, target, fallback in
937
971
  self?.resolveCheckboxState(item: item, target: target, fallback: fallback) ?? fallback
938
972
  }
@@ -943,7 +977,8 @@ final class NativeListView: UIView {
943
977
  }
944
978
 
945
979
  private func handleRowPress(_ item: NativeListItem, origin: NativeListActionOrigin?) {
946
- guard let config, !item.data.bool("disabled") else { return }
980
+ // OneKey patch: missing-address rows keep accessory actions available.
981
+ guard let config, !item.data.bool("disabled"), !item.data.bool("pressDisabled") else { return }
947
982
  if config.rowPressToggles && item.isSelectable && config.selectionMode != "none" {
948
983
  updateSelection(target: NativeSelectionTarget(scope: "row", key: item.key), sourceKey: item.key)
949
984
  return
@@ -1026,7 +1061,7 @@ final class NativeListView: UIView {
1026
1061
  return "indeterminate"
1027
1062
  }
1028
1063
 
1029
- private func refreshVisibleSelection() {
1064
+ private func refreshVisibleSelection(changedSummaryKeys: Set<String> = []) {
1030
1065
  guard let config else { return }
1031
1066
  let checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String = {
1032
1067
  [weak self] item, target, fallback in
@@ -1035,9 +1070,13 @@ final class NativeListView: UIView {
1035
1070
  for indexPath in collectionView.indexPathsForVisibleItems {
1036
1071
  guard let item = config.items[safe: indexPath.item],
1037
1072
  let cell = collectionView.cellForItem(at: indexPath) as? NativeListCell else { continue }
1073
+ if changedSummaryKeys.contains(item.key) {
1074
+ bind(cell: cell, item: item, itemIndex: indexPath.item)
1075
+ continue
1076
+ }
1038
1077
  cell.updateSelection(
1039
1078
  item: item,
1040
- selected: config.selectedKeys.contains(item.key),
1079
+ selected: item.data.bool("selected") || config.selectedKeys.contains(item.key),
1041
1080
  checkboxState: checkboxState
1042
1081
  )
1043
1082
  }
@@ -1080,19 +1119,73 @@ final class NativeListView: UIView {
1080
1119
  return zip(current.items, next.items).allSatisfy { old, new in
1081
1120
  guard old.key == new.key, old.type == new.type else { return false }
1082
1121
  if old.content == new.content { return true }
1083
- guard old.type == "sectionHeader",
1084
- old.data.string("variant") == "summary",
1085
- new.data.string("variant") == "summary" else { return false }
1086
- var oldData = old.data
1087
- var newData = new.data
1088
- oldData.removeValue(forKey: "title")
1089
- oldData.removeValue(forKey: "value")
1090
- newData.removeValue(forKey: "title")
1091
- newData.removeValue(forKey: "value")
1122
+ // OneKey patch: controlled echoes can also update row and checkbox selection fields.
1123
+ // guard old.type == "sectionHeader",
1124
+ // old.data.string("variant") == "summary",
1125
+ // new.data.string("variant") == "summary" else { return false }
1126
+ // var oldData = old.data
1127
+ // var newData = new.data
1128
+ // oldData.removeValue(forKey: "title")
1129
+ // oldData.removeValue(forKey: "value")
1130
+ // newData.removeValue(forKey: "title")
1131
+ // newData.removeValue(forKey: "value")
1132
+ let controlled = next.selectionMode == "single" || next.selectionMode == "multiple"
1133
+ guard let oldData = selectionComparisonData(old.data, controlled: controlled),
1134
+ let newData = selectionComparisonData(new.data, controlled: controlled) else { return false }
1092
1135
  return jsonData(oldData) == jsonData(newData)
1093
1136
  }
1094
1137
  }
1095
1138
 
1139
+ // OneKey patch: remove only fields that the existing lightweight binder refreshes.
1140
+ private func selectionComparisonData(_ data: [String: Any], controlled: Bool) -> [String: Any]? {
1141
+ guard let type = data["type"] as? String else { return nil }
1142
+ var result = data
1143
+ if let selected = result["selected"] {
1144
+ guard CFGetTypeID(selected as CFTypeRef) == CFBooleanGetTypeID() else { return nil }
1145
+ result.removeValue(forKey: "selected")
1146
+ }
1147
+ if type == "walletGroup" {
1148
+ guard let parent = data["parent"] as? [String: Any], parent["type"] as? String == "identity",
1149
+ let children = data["children"] as? [[String: Any]],
1150
+ let normalizedParent = selectionComparisonData(parent, controlled: false) else { return nil }
1151
+ var normalizedChildren: [[String: Any]] = []
1152
+ for child in children {
1153
+ guard child["type"] as? String == "identity",
1154
+ let normalized = selectionComparisonData(child, controlled: false) else { return nil }
1155
+ normalizedChildren.append(normalized)
1156
+ }
1157
+ result["parent"] = normalizedParent
1158
+ result["children"] = normalizedChildren
1159
+ }
1160
+ if type == "sectionHeader", data["variant"] as? String == "summary" {
1161
+ result.removeValue(forKey: "title")
1162
+ result.removeValue(forKey: "value")
1163
+ }
1164
+ guard controlled else { return result }
1165
+ func checkboxData(_ value: Any) -> [String: Any]? {
1166
+ guard var checkbox = value as? [String: Any], checkbox["kind"] as? String == "checkbox" else { return nil }
1167
+ if let state = checkbox["state"] {
1168
+ guard let state = state as? String, ["checked", "unchecked", "indeterminate"].contains(state) else { return nil }
1169
+ }
1170
+ checkbox.removeValue(forKey: "state")
1171
+ return checkbox
1172
+ }
1173
+ if ["dataRow", "sectionHeader", "action"].contains(type), let checkbox = result["checkbox"] {
1174
+ guard let normalized = checkboxData(checkbox) else { return nil }
1175
+ result["checkbox"] = normalized
1176
+ }
1177
+ if type == "identity", let trailing = result["trailing"] {
1178
+ guard var accessories = trailing as? [[String: Any]], accessories.count <= 2,
1179
+ accessories.filter({ $0["kind"] as? String == "checkbox" }).count <= 1 else { return nil }
1180
+ for index in accessories.indices where accessories[index]["kind"] as? String == "checkbox" {
1181
+ guard let normalized = checkboxData(accessories[index]) else { return nil }
1182
+ accessories[index] = normalized
1183
+ }
1184
+ result["trailing"] = accessories
1185
+ }
1186
+ return result
1187
+ }
1188
+
1096
1189
  private func dictionariesEqual(_ lhs: [String: Any]?, _ rhs: [String: Any]?) -> Bool {
1097
1190
  switch (lhs, rhs) {
1098
1191
  case (nil, nil): return true
@@ -1107,16 +1200,36 @@ final class NativeListView: UIView {
1107
1200
  }
1108
1201
 
1109
1202
  private func rowHeight(_ item: NativeListItem) -> CGFloat {
1203
+ // OneKey patch: honor selector baseline geometry; keep compact drag sizing.
1204
+ if item.type != "walletGroup", item.data["height"] != nil { return CGFloat(item.data.double("height")) }
1110
1205
  if item.type == "system", item.data.string("variant") == "spacer" {
1111
1206
  return CGFloat(item.data.int("height"))
1112
1207
  }
1208
+ // OneKey patch: warning height follows the current native font and available width.
1209
+ if item.type == "system", item.data.string("variant") == "warning" {
1210
+ let textWidth = max(1, collectionView.bounds.width - (config?.contentPaddingHorizontal ?? 0) * 2 - 24)
1211
+ func textHeight(_ key: String, weight: NativeListFontWeight) -> CGFloat {
1212
+ let paragraph = NSMutableParagraphStyle()
1213
+ paragraph.minimumLineHeight = 20
1214
+ paragraph.maximumLineHeight = 20
1215
+ return ceil((item.data.string(key) as NSString).boundingRect(with: CGSize(width: textWidth, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: nativeListFont(ofSize: 14, weight: weight), .paragraphStyle: paragraph], context: nil).height / 20) * 20
1216
+ }
1217
+ return 32 + textHeight("title", weight: .medium) + textHeight("message", weight: .regular)
1218
+ }
1113
1219
  if item.type == "walletGroup" {
1114
1220
  if item.key == interactiveReorderCompactKey { return 68 }
1115
1221
  let childCount = item.data.dictionaries("children").count
1116
- return CGFloat((childCount + 1) * 68 + childCount * 12)
1222
+ // OneKey patch: wallet badges contribute their own member heights.
1223
+ // return CGFloat((childCount + 1) * 68 + childCount * 12)
1224
+ let members = [item.data.dictionary("parent")].compactMap { $0 } + item.data.dictionaries("children")
1225
+ return members.reduce(CGFloat(childCount * 12 + (members.first?["height"] != nil ? 2 : 0))) { total, data in
1226
+ total + CGFloat(data.double("height", default: data.dictionaries("badges").isEmpty ? 68 : 92))
1227
+ }
1117
1228
  }
1118
1229
  if item.type == "identity", item.data.string("presentation") == "walletSidebar" {
1119
- return 68
1230
+ // OneKey patch: default sidebar badge geometry is 24 points taller.
1231
+ // return 68
1232
+ return item.data.dictionaries("badges").isEmpty ? 68 : 92
1120
1233
  }
1121
1234
  if item.type == "identity", item.data.string("presentation") == "networkSelector" {
1122
1235
  return 47
@@ -1268,7 +1381,7 @@ final class NativeListView: UIView {
1268
1381
  actionAnchorCounter &+= 1
1269
1382
  let generation = config?.generation ?? 0
1270
1383
  let token = "\(actionAnchorInstanceID):\(generation):\(actionAnchorCounter):\(origin.bindingEpoch)"
1271
- let rect = sourceView.convert(sourceView.bounds, to: window)
1384
+ let rect = sourceView.convert(sourceView.bounds, to: window).insetBy(dx: origin.anchorInset, dy: origin.anchorInset)
1272
1385
  let record = ActionAnchorRecord(token: token, origin: origin)
1273
1386
  actionAnchor = record
1274
1387
  var anchor: [String: Any] = [
@@ -1341,6 +1454,7 @@ final class NativeListView: UIView {
1341
1454
  DispatchQueue.main.async { [weak self] in
1342
1455
  guard let self else { return }
1343
1456
  self.visibleEventScheduled = false
1457
+ guard let onVisibleRangeChanged = self.onVisibleRangeChanged else { return }
1344
1458
  let indexes = self.collectionView.indexPathsForVisibleItems.map(\.item).sorted()
1345
1459
  let first = indexes.first ?? -1
1346
1460
  let last = indexes.last ?? -1
@@ -1354,7 +1468,7 @@ final class NativeListView: UIView {
1354
1468
  var payload: [String: Any] = ["firstIndex": first, "lastIndex": last]
1355
1469
  if let firstKey { payload["firstKey"] = firstKey }
1356
1470
  if let lastKey { payload["lastKey"] = lastKey }
1357
- self.emit(self.onVisibleRangeChanged, payload)
1471
+ self.emit(onVisibleRangeChanged, payload)
1358
1472
  }
1359
1473
  }
1360
1474
 
@@ -1371,10 +1485,18 @@ final class NativeListView: UIView {
1371
1485
  if let lastKey = config.items.last?.key { payload["lastKey"] = lastKey }
1372
1486
  emit(onEndReached, payload)
1373
1487
  }
1488
+
1489
+ override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
1490
+ guard gestureRecognizer === listBodyGestureGuard,
1491
+ let pan = gestureRecognizer as? UIPanGestureRecognizer else { return true }
1492
+ let velocity = pan.velocity(in: collectionView)
1493
+ return abs(velocity.y) > abs(velocity.x)
1494
+ }
1374
1495
  }
1375
1496
 
1376
1497
  extension NativeListView: UIGestureRecognizerDelegate {
1377
1498
  func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
1499
+ if gestureRecognizer === listBodyGestureGuard { return true }
1378
1500
  var view = touch.view
1379
1501
  while let current = view, current !== footerCell {
1380
1502
  if current is UIControl { return false }
@@ -1387,7 +1509,26 @@ extension NativeListView: UIGestureRecognizerDelegate {
1387
1509
  _ gestureRecognizer: UIGestureRecognizer,
1388
1510
  shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
1389
1511
  ) -> Bool {
1390
- gestureRecognizer.view === footerCell || otherGestureRecognizer.view === footerCell
1512
+ if gestureRecognizer === listBodyGestureGuard {
1513
+ guard let otherView = otherGestureRecognizer.view else { return false }
1514
+ return otherView === collectionView || otherView.isDescendant(of: collectionView)
1515
+ }
1516
+ if otherGestureRecognizer === listBodyGestureGuard {
1517
+ guard let gestureView = gestureRecognizer.view else { return false }
1518
+ return gestureView === collectionView || gestureView.isDescendant(of: collectionView)
1519
+ }
1520
+ return gestureRecognizer.view === footerCell || otherGestureRecognizer.view === footerCell
1521
+ }
1522
+
1523
+ func gestureRecognizer(
1524
+ _ gestureRecognizer: UIGestureRecognizer,
1525
+ shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer
1526
+ ) -> Bool {
1527
+ guard gestureRecognizer === listBodyGestureGuard,
1528
+ otherGestureRecognizer is UIPanGestureRecognizer,
1529
+ let otherView = otherGestureRecognizer.view,
1530
+ otherView !== collectionView else { return false }
1531
+ return collectionView.isDescendant(of: otherView)
1391
1532
  }
1392
1533
  }
1393
1534
 
@@ -1604,13 +1745,16 @@ private struct NativeListSectionIndexEntry {
1604
1745
  let position: Int
1605
1746
  }
1606
1747
 
1607
- private final class NativeListSectionIndexView: UIControl {
1748
+ // OneKey patch: arbitrate index scrubbing against ancestor dismissal gestures.
1749
+ // private final class NativeListSectionIndexView: UIControl {
1750
+ private final class NativeListSectionIndexView: UIControl, UIGestureRecognizerDelegate {
1608
1751
  var onSelect: ((Int, Bool) -> Void)?
1609
1752
  var onInteractionEnded: (() -> Void)?
1610
1753
  private var titles: [String] = []
1611
1754
  private var labels: [UILabel] = []
1612
1755
  private var textColor: UIColor = .secondaryLabel
1613
1756
  private var activeColor: UIColor = .tintColor
1757
+ private var activeTextColor: UIColor = .white
1614
1758
  private var lastTouchIndex: Int?
1615
1759
  private(set) var activeIndex: Int?
1616
1760
 
@@ -1620,16 +1764,43 @@ private final class NativeListSectionIndexView: UIControl {
1620
1764
  accessibilityLabel = "Section index"
1621
1765
  accessibilityTraits = [.adjustable]
1622
1766
  isExclusiveTouch = true
1767
+
1768
+ // UIControl tracking alone cannot prevent an ancestor sheet pan from taking the touch.
1769
+ // Recognize immediately, but keep delivering touches to the existing tracking methods.
1770
+ let scrubGesture = UILongPressGestureRecognizer(target: nil, action: nil)
1771
+ scrubGesture.minimumPressDuration = 0
1772
+ scrubGesture.allowableMovement = .greatestFiniteMagnitude
1773
+ scrubGesture.cancelsTouchesInView = false
1774
+ scrubGesture.delaysTouchesEnded = false
1775
+ scrubGesture.delegate = self
1776
+ addGestureRecognizer(scrubGesture)
1777
+ }
1778
+
1779
+ func gestureRecognizer(
1780
+ _ gestureRecognizer: UIGestureRecognizer,
1781
+ shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer
1782
+ ) -> Bool {
1783
+ guard otherGestureRecognizer is UIPanGestureRecognizer,
1784
+ let otherView = otherGestureRecognizer.view,
1785
+ otherView !== self else { return false }
1786
+ // The dependency applies only to touches starting in this index, including moves outside it.
1787
+ return isDescendant(of: otherView)
1623
1788
  }
1624
1789
 
1625
1790
  required init?(coder: NSCoder) {
1626
1791
  fatalError("init(coder:) has not been implemented")
1627
1792
  }
1628
1793
 
1629
- func configure(titles: [String], textColor: UIColor, activeColor: UIColor) {
1794
+ func configure(
1795
+ titles: [String],
1796
+ textColor: UIColor,
1797
+ activeColor: UIColor,
1798
+ activeTextColor: UIColor
1799
+ ) {
1630
1800
  self.titles = titles
1631
1801
  self.textColor = textColor
1632
1802
  self.activeColor = activeColor
1803
+ self.activeTextColor = activeTextColor
1633
1804
  labels.forEach { $0.removeFromSuperview() }
1634
1805
  labels = titles.map { title in
1635
1806
  let label = UILabel()
@@ -1660,9 +1831,9 @@ private final class NativeListSectionIndexView: UIControl {
1660
1831
  let originY = (bounds.height - height * CGFloat(labels.count)) / 2
1661
1832
  for (index, label) in labels.enumerated() {
1662
1833
  label.frame = CGRect(
1663
- x: 0,
1834
+ x: 6,
1664
1835
  y: originY + CGFloat(index) * height,
1665
- width: bounds.width,
1836
+ width: 20,
1666
1837
  height: height
1667
1838
  )
1668
1839
  }
@@ -1718,7 +1889,10 @@ private final class NativeListSectionIndexView: UIControl {
1718
1889
  private func updateLabelStyles() {
1719
1890
  for (index, label) in labels.enumerated() {
1720
1891
  let active = index == activeIndex
1721
- label.textColor = active ? activeColor : textColor
1892
+ label.textColor = active ? activeTextColor : textColor
1893
+ label.backgroundColor = active ? activeColor : .clear
1894
+ label.layer.cornerRadius = 8
1895
+ label.layer.masksToBounds = active
1722
1896
  label.font = nativeListFont(ofSize: 10, weight: active ? .semibold : .medium)
1723
1897
  }
1724
1898
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "images": [
3
+ {
4
+ "filename": "icon.svg",
5
+ "idiom": "universal"
6
+ }
7
+ ],
8
+ "info": {
9
+ "author": "xcode",
10
+ "version": 1
11
+ },
12
+ "properties": {
13
+ "preserves-vector-representation": true
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" fill="currentColor">
2
+ <path
3
+ fill="#000"
4
+ fill-opacity="0.447"
5
+ d="M12.5 12.75a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5"
6
+ />
7
+ <path
8
+ fill="#000"
9
+ fill-opacity="0.447"
10
+ fill-rule="evenodd"
11
+ d="M0 3.5A3.5 3.5 0 0 1 3.5 0h8.088A2.41 2.41 0 0 1 14 2.412V5h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H4a4 4 0 0 1-4-4zm2 3.163V14a2 2 0 0 0 2 2h11a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H3.5c-.537 0-1.045-.12-1.5-.337M2 3.5A1.5 1.5 0 0 0 3.5 5H12V2.412A.41.41 0 0 0 11.588 2H3.5A1.5 1.5 0 0 0 2 3.5"
12
+ clip-rule="evenodd"
13
+ />
14
+ </svg>
@@ -0,0 +1,15 @@
1
+ {
2
+ "images": [
3
+ {
4
+ "filename": "icon.svg",
5
+ "idiom": "universal"
6
+ }
7
+ ],
8
+ "info": {
9
+ "author": "xcode",
10
+ "version": 1
11
+ },
12
+ "properties": {
13
+ "preserves-vector-representation": true
14
+ }
15
+ }
@@ -0,0 +1,12 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
2
+ <path
3
+ fill="currentColor"
4
+ d="M15.333 13.998a1.335 1.335 0 1 1 0 2.67 1.335 1.335 0 0 1 0-2.67"
5
+ />
6
+ <path
7
+ fill="currentColor"
8
+ fill-rule="evenodd"
9
+ d="M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0M8 12.668A2 2 0 0 0 6 14.666V16c0 1.103.895 1.997 1.998 1.998h1.334A2 2 0 0 0 11.33 16v-1.334a2 2 0 0 0-1.998-1.998zm7.333 0a2.665 2.665 0 1 0 0 5.33 2.665 2.665 0 0 0 0-5.33M7.999 6.001A2 2 0 0 0 6.001 8v1.334c0 1.103.895 1.998 1.998 1.998h1.334a2 2 0 0 0 1.998-1.998V7.999a2 2 0 0 0-1.998-1.998zm6.667 0A2 2 0 0 0 12.668 8v1.334c0 1.103.895 1.998 1.998 1.998H16a2 2 0 0 0 1.998-1.998V7.999A2 2 0 0 0 16 6.001z"
10
+ clip-rule="evenodd"
11
+ />
12
+ </svg>
@@ -0,0 +1,15 @@
1
+ {
2
+ "images": [
3
+ {
4
+ "filename": "icon.svg",
5
+ "idiom": "universal"
6
+ }
7
+ ],
8
+ "info": {
9
+ "author": "xcode",
10
+ "version": 1
11
+ },
12
+ "properties": {
13
+ "preserves-vector-representation": true
14
+ }
15
+ }
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 20" fill="currentColor">
2
+ <path
3
+ fill="currentColor"
4
+ d="M11.67.834c.117 1.074-.315 2.153-.955 2.928-.64.773-1.692 1.378-2.718 1.298-.14-1.054.38-2.151.971-2.836C9.63 1.45 10.746.872 11.67.834M14.994 7.093c-.176.108-1.992 1.224-1.972 3.482.025 2.769 2.428 3.693 2.46 3.705l-.004.015a10.1 10.1 0 0 1-1.264 2.593c-.764 1.116-1.556 2.229-2.806 2.254-.598.011-1-.162-1.416-.343-.437-.19-.891-.386-1.609-.386-.751 0-1.226.203-1.683.398-.397.169-.78.333-1.32.354-1.208.047-2.124-1.207-2.895-2.32C.909 14.57-.294 10.414 1.322 7.612c.803-1.395 2.237-2.275 3.794-2.298.671-.014 1.32.244 1.89.47.434.172.821.326 1.135.326.282 0 .659-.149 1.099-.323.692-.273 1.539-.607 2.41-.518.599.026 2.276.24 3.354 1.818z"
5
+ />
6
+ </svg>
@@ -0,0 +1,15 @@
1
+ {
2
+ "images": [
3
+ {
4
+ "filename": "icon.svg",
5
+ "idiom": "universal"
6
+ }
7
+ ],
8
+ "info": {
9
+ "author": "xcode",
10
+ "version": 1
11
+ },
12
+ "properties": {
13
+ "preserves-vector-representation": true
14
+ }
15
+ }