@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
@@ -1,4 +1,6 @@
1
1
  import Foundation
2
+ // OneKey patch: preserve native font faces while enabling tabular number features.
3
+ import CoreText
2
4
  import OneKeyImage
3
5
  import UIKit
4
6
 
@@ -8,19 +10,56 @@ final class NativeListActionOrigin {
8
10
  let bindingEpoch: Int
9
11
  let source: String
10
12
  let slot: Int?
13
+ // OneKey patch: expose the layout slot while preserving the larger hit target.
14
+ let anchorInset: CGFloat
11
15
 
12
16
  init(
13
17
  sourceView: UIView,
14
18
  ownerCell: NativeListCell,
15
19
  bindingEpoch: Int,
16
20
  source: String,
17
- slot: Int? = nil
21
+ slot: Int? = nil,
22
+ anchorInset: CGFloat = 0
18
23
  ) {
19
24
  self.sourceView = sourceView
20
25
  self.ownerCell = ownerCell
21
26
  self.bindingEpoch = bindingEpoch
22
27
  self.source = source
23
28
  self.slot = slot
29
+ self.anchorInset = anchorInset
30
+ }
31
+ }
32
+
33
+ // OneKey patch: explicit summary actions use the source text's physical-pixel line box.
34
+ private final class NativeListAccessoryButton: UIButton {
35
+ var selectorSummaryLineHeight: CGFloat? {
36
+ didSet { invalidateIntrinsicContentSize(); setNeedsLayout() }
37
+ }
38
+
39
+ private var sourcePixelScale: CGFloat {
40
+ max(1, window?.screen.scale ?? traitCollection.displayScale)
41
+ }
42
+
43
+ override var intrinsicContentSize: CGSize {
44
+ var size = super.intrinsicContentSize
45
+ guard selectorSummaryLineHeight != nil, let title = attributedTitle(for: .normal) else { return size }
46
+ let width = title.boundingRect(
47
+ with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
48
+ options: [.usesLineFragmentOrigin, .usesFontLeading],
49
+ context: nil
50
+ ).width
51
+ size.width = ceil(width * sourcePixelScale) / sourcePixelScale
52
+ return size
53
+ }
54
+
55
+ override func layoutSubviews() {
56
+ super.layoutSubviews()
57
+ guard let lineHeight = selectorSummaryLineHeight, let titleLabel else { return }
58
+ // OneKey patch: position the final source line box after UIKit has measured the button.
59
+ let top = ceil((bounds.height - lineHeight) / 2 * sourcePixelScale) / sourcePixelScale
60
+ var frame = titleLabel.frame
61
+ frame.origin.y = top
62
+ titleLabel.frame = frame
24
63
  }
25
64
  }
26
65
 
@@ -54,6 +93,24 @@ private final class NativeListDottedUnderlineLabel: UILabel {
54
93
  didSet { setNeedsLayout() }
55
94
  }
56
95
 
96
+ // OneKey patch: migrated section titles include the source 3-point underline box.
97
+ var reservesDottedUnderlineSpace = false {
98
+ didSet { invalidateIntrinsicContentSize(); setNeedsLayout(); setNeedsDisplay() }
99
+ }
100
+
101
+ override var intrinsicContentSize: CGSize {
102
+ var size = super.intrinsicContentSize
103
+ if reservesDottedUnderlineSpace && showsDottedUnderline { size.height += 3 }
104
+ return size
105
+ }
106
+
107
+ override func drawText(in rect: CGRect) {
108
+ let textRect = reservesDottedUnderlineSpace && showsDottedUnderline
109
+ ? CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: max(0, rect.height - 3))
110
+ : rect
111
+ super.drawText(in: textRect)
112
+ }
113
+
57
114
  var dottedUnderlineColor: UIColor = .clear {
58
115
  didSet {
59
116
  dottedUnderlineLayer.strokeColor = dottedUnderlineColor.cgColor
@@ -101,7 +158,9 @@ private final class NativeListDottedUnderlineLabel: UILabel {
101
158
  width: bounds.width,
102
159
  height: bounds.height + 2 + dottedUnderlineVerticalOffset
103
160
  )
104
- let y = bounds.height + 1 + dottedUnderlineVerticalOffset
161
+ // OneKey patch: explicit header underline occupies the reserved final two points.
162
+ // let y = bounds.height + 1 + dottedUnderlineVerticalOffset
163
+ let y = reservesDottedUnderlineSpace ? bounds.height - 1 : bounds.height + 1 + dottedUnderlineVerticalOffset
105
164
  let path = UIBezierPath()
106
165
  path.move(to: CGPoint(x: 1, y: y))
107
166
  path.addLine(to: CGPoint(x: max(1, textWidth - 1), y: y))
@@ -257,6 +316,9 @@ private final class NativeListTableColumnView: UIStackView {
257
316
 
258
317
  private func textColor(_ tone: String, theme: [String: Any]?) -> UIColor {
259
318
  switch tone {
319
+ // OneKey patch: account warnings and hidden balances use existing theme tokens.
320
+ case "disabled": return nativeListColor(theme, "disabledText", "#8D8D8D")
321
+ case "caution": return nativeListColor(theme, "caution", "#AB6400")
260
322
  case "secondary": return nativeListColor(theme, "secondaryText", "#646464")
261
323
  case "positive": return nativeListColor(theme, "positive", "#218358")
262
324
  case "negative": return nativeListColor(theme, "negative", "#CE2C31")
@@ -273,6 +335,13 @@ final class NativeListCell: UICollectionViewCell {
273
335
  private let leadingOverlayBackground = UIView()
274
336
  private let leadingCornerIconBackground = UIView()
275
337
  private let leadingCornerIconImageView = UIImageView()
338
+ // OneKey patch: selector-only views are reset on every native cell binding.
339
+ private var selectorViews: [UIView] = []
340
+ private var selectorConstraints: [NSLayoutConstraint] = []
341
+ private var selectorImages: [OneKeyImageReusableView] = []
342
+ private var selectorBorder: CAShapeLayer?
343
+ private let selectorFullWidthBackground = CALayer()
344
+ private lazy var selectorTitleTap = UITapGestureRecognizer(target: self, action: #selector(selectorTitlePressed))
276
345
  private let secondaryImage = OneKeyImageReusableView(frame: .zero)
277
346
  private let mediaNetworkImage = OneKeyImageReusableView(frame: .zero)
278
347
  private let fallbackLabel = UILabel()
@@ -296,7 +365,9 @@ final class NativeListCell: UICollectionViewCell {
296
365
  private let actionStack = UIStackView()
297
366
  private let actionButtons = (0..<3).map { _ in UIButton(type: .system) }
298
367
  private let trailingStack = UIStackView()
299
- private let accessoryButtons = (0..<2).map { _ in UIButton(type: .system) }
368
+ // OneKey patch: summary actions opt into source typography while other buttons keep UIKit layout.
369
+ // private let accessoryButtons = (0..<2).map { _ in UIButton(type: .system) }
370
+ private let accessoryButtons = (0..<2).map { _ in NativeListAccessoryButton(type: .system) }
300
371
  private let checkboxButton = UIButton(type: .system)
301
372
  private let spinner = UIActivityIndicatorView(style: .medium)
302
373
  private let dataStack = UIStackView()
@@ -332,6 +403,8 @@ final class NativeListCell: UICollectionViewCell {
332
403
  private var leadingSlotConstraints: [NSLayoutConstraint] = []
333
404
  private var dataWeightConstraints: [NSLayoutConstraint] = []
334
405
  private var accessorySizeConstraints: [NSLayoutConstraint] = []
406
+ // OneKey patch: restore selector-only font features before a cell is reused.
407
+ private var selectorTypographyRestorers: [() -> Void] = []
335
408
  private var currentItem: NativeListItem?
336
409
  private var accessoryActions: [(String, NativeSelectionTarget?)] = []
337
410
  private var footerActionKeys: [String] = []
@@ -345,12 +418,15 @@ final class NativeListCell: UICollectionViewCell {
345
418
  fallback: .lightGray
346
419
  )
347
420
  private var checkboxCheckedColor = UIColor(nativeListHex: "#202020", fallback: .black)
421
+ private var checkboxIconColor = UIColor.white
348
422
  private var checkboxUncheckedColor = UIColor(nativeListHex: "#FCFCFC", fallback: .white)
349
423
  private var checkboxBorderColor = UIColor(nativeListHex: "#CECECE", fallback: .lightGray)
350
424
  private var visualBackdropColor = UIColor.white
351
425
  private var currentLayout = "linear"
352
426
  private var currentTheme: [String: Any]?
353
427
  private var currentItemIndex: Int?
428
+ // OneKey patch: delayed image retries belong to the current reusable cell binding.
429
+ private var selectorImageRetries: [ObjectIdentifier: DispatchWorkItem] = [:]
354
430
  private(set) var bindingEpoch = 0
355
431
 
356
432
  var onAction: ((NativeListItem, String, NativeSelectionTarget?, NativeListActionOrigin?) -> Void)?
@@ -615,12 +691,41 @@ final class NativeListCell: UICollectionViewCell {
615
691
 
616
692
  override func layoutSubviews() {
617
693
  super.layoutSubviews()
694
+ // OneKey patch: extend only the background across the section list outer inset.
695
+ if currentItem?.data.bool("backgroundFullWidth") == true {
696
+ selectorFullWidthBackground.frame = CGRect(x: -frame.minX, y: 0, width: superview?.bounds.width ?? bounds.width, height: bounds.height)
697
+ }
618
698
  if currentItem?.type == "mediaTile" {
619
699
  mediaHeight.constant = max(0, contentView.bounds.width - 20)
620
700
  }
701
+ if currentItem?.type == "identity", currentItem?.data["height"] != nil,
702
+ currentItem?.data.string("presentation") == "accountSelector",
703
+ let accessory = currentItem?.data.dictionaries("trailing").first,
704
+ accessory.string("kind") == "icon", accessory.string("name") == "PlusSmallOutline" {
705
+ // OneKey patch: PlusButton's fixed top18 slot and negative7 margin place its frame at11.
706
+ let button = accessoryButtons[0]
707
+ button.transform = .identity
708
+ let origin = button.convert(button.bounds, to: contentView).minY
709
+ button.transform = CGAffineTransform(translationX: 0, y: 11 - origin)
710
+ }
711
+ }
712
+
713
+ // OneKey patch: trim only detached/recycled members, never a live compact
714
+ // drag proxy or an expanding group. Small groups retain eight reusable cells.
715
+ private func trimWalletGroupCells(keeping required: Int) {
716
+ let retained = max(8, required)
717
+ while walletGroupCells.count > retained {
718
+ let cell = walletGroupCells.removeLast()
719
+ rootStack.removeArrangedSubview(cell)
720
+ cell.removeFromSuperview()
721
+ cell.prepareForReuse()
722
+ cell.onAction = nil
723
+ cell.onBindingInvalidated = nil
724
+ }
621
725
  }
622
726
 
623
727
  override func prepareForReuse() {
728
+ let canTrimMembers = !walletGroupCompactAppearanceActive && (rootStack.layer.animationKeys()?.isEmpty ?? true)
624
729
  super.prepareForReuse()
625
730
  invalidateCurrentBinding()
626
731
  isHighlighted = false
@@ -632,6 +737,8 @@ final class NativeListCell: UICollectionViewCell {
632
737
  walletGroupCompactContainer.alpha = 1
633
738
  walletGroupCompactCell?.prepareForReuse()
634
739
  walletGroupCells.forEach { $0.prepareForReuse() }
740
+ walletGroupMembers.removeAll()
741
+ if canTrimMembers { trimWalletGroupCells(keeping: 0) }
635
742
  leadingImages.forEach { $0.prepareForReuse() }
636
743
  secondaryImage.prepareForReuse()
637
744
  mediaNetworkImage.prepareForReuse()
@@ -645,6 +752,8 @@ final class NativeListCell: UICollectionViewCell {
645
752
  selected: Bool,
646
753
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
647
754
  ) {
755
+ // OneKey patch: a same-row snapshot refresh must not clear a touch that is still held.
756
+ let shouldRestoreHighlight = isHighlighted && currentItem?.key == item.key
648
757
  invalidateCurrentBinding()
649
758
  bindingEpoch &+= 1
650
759
  currentLayout = layout
@@ -664,6 +773,14 @@ final class NativeListCell: UICollectionViewCell {
664
773
  // Checkbox uses the literal neutral7 alpha token. Applying opacity to the
665
774
  // opaque primary text color produces a different RGB result.
666
775
  checkboxBorderColor = UIColor(nativeListHex: "#00000031", fallback: .lightGray)
776
+ checkboxIconColor = checkboxUncheckedColor
777
+ if item.data.string("presentation") == "networkSelector" {
778
+ checkboxCheckedColor = nativeListColor(theme, "checkboxBackground", "#202020")
779
+ checkboxBorderColor = nativeListColor(theme, "checkboxBorder", "#00000031")
780
+ checkboxIconColor = nativeListColor(theme, "checkboxIcon", "#FFFFFF")
781
+ // OneKey patch: the V1 checkbox fills even its unchecked body with iconInverse.
782
+ checkboxUncheckedColor = checkboxIconColor
783
+ }
667
784
  visualBackdropColor = nativeListColor(theme, "rowBackground", "#FFFFFF")
668
785
  titleLabel.textColor = primary
669
786
  subtitleLabel.textColor = secondary
@@ -693,6 +810,11 @@ final class NativeListCell: UICollectionViewCell {
693
810
  pressedBackgroundColor = restingBackgroundColor
694
811
  }
695
812
  updateBackgroundColor()
813
+ if item.data.bool("backgroundFullWidth"), let background = item.data["backgroundColor"] as? String {
814
+ selectorFullWidthBackground.backgroundColor = UIColor(nativeListHex: background, fallback: .clear).cgColor
815
+ contentView.layer.insertSublayer(selectorFullWidthBackground, at: 0)
816
+ clipsToBounds = false
817
+ }
696
818
  if layout == "table" {
697
819
  if item.type == "dataRow" {
698
820
  rootLeadingConstraint.constant = 20
@@ -707,8 +829,12 @@ final class NativeListCell: UICollectionViewCell {
707
829
  }
708
830
  applyGroupPosition(item.data.string("groupPosition"))
709
831
  isUserInteractionEnabled = !item.data.bool("disabled")
710
- contentView.alpha = isUserInteractionEnabled ? 1 : 0.5
832
+ // OneKey patch: deprecated wallets remain interactive while dimmed.
833
+ // contentView.alpha = isUserInteractionEnabled ? 1 : 0.5
834
+ contentView.alpha = CGFloat(item.data.double("opacity", default: 1)) * (isUserInteractionEnabled ? 1 : 0.5)
711
835
  accessibilityLabel = item.data.string("accessibilityLabel", default: item.data.string("title"))
836
+ // OneKey patch: keep existing selector automation identifiers.
837
+ accessibilityIdentifier = item.data["testID"] as? String
712
838
 
713
839
  switch item.type {
714
840
  case "walletGroup": bindWalletGroup(item, theme: theme, layout: layout, checkboxState)
@@ -724,6 +850,17 @@ final class NativeListCell: UICollectionViewCell {
724
850
  case "system": bindSystem(item, theme: theme)
725
851
  default: break
726
852
  }
853
+ applySelectorTypography(item)
854
+ if shouldRestoreHighlight && isUserInteractionEnabled {
855
+ isHighlighted = true
856
+ }
857
+ if item.type == "sectionHeader", item.data.string("presentation") == "networkSelector", item.data["height"] != nil, item.data.dictionary("checkbox") != nil, !item.data.string("value").isEmpty {
858
+ // OneKey patch: UIKit must reserve only the total's intrinsic width before the checkbox.
859
+ let valueWidth = accessoryButtons[0].intrinsicContentSize.width
860
+ let width = trailingStack.widthAnchor.constraint(equalToConstant: valueWidth + 12 + 20)
861
+ width.isActive = true
862
+ selectorConstraints.append(width)
863
+ }
727
864
  }
728
865
 
729
866
  func updateSelection(
@@ -732,6 +869,8 @@ final class NativeListCell: UICollectionViewCell {
732
869
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
733
870
  ) {
734
871
  guard currentItem?.key == item.key else { return }
872
+ restoreSelectorTypography()
873
+ defer { applySelectorTypography(item) }
735
874
  if item.type == "walletGroup" {
736
875
  currentItem = item
737
876
  let memberData = [item.data.dictionary("parent")].compactMap { $0 }
@@ -777,6 +916,13 @@ final class NativeListCell: UICollectionViewCell {
777
916
  selected ? "#FFFFFFED" : "#FFFFFFAF"
778
917
  )
779
918
  }
919
+ // OneKey patch: retain the latest descriptor when a controlled echo avoids full binding.
920
+ if boundCheckboxData != nil {
921
+ let latestCheckbox = item.type == "identity"
922
+ ? item.data.dictionaries("trailing").last { $0.string("kind") == "checkbox" }
923
+ : item.data.dictionary("checkbox")
924
+ boundCheckboxData = latestCheckbox ?? boundCheckboxData
925
+ }
780
926
  guard let data = boundCheckboxData, let target = boundCheckboxTarget else { return }
781
927
  updateCheckboxPresentation(
782
928
  item,
@@ -786,12 +932,55 @@ final class NativeListCell: UICollectionViewCell {
786
932
  )
787
933
  }
788
934
 
935
+ // OneKey patch: match SizableText TABULAR_NUMS on every selector text run, retaining its face and size.
936
+ private func selectorTabularFont(_ font: UIFont) -> UIFont {
937
+ var settings = font.fontDescriptor.fontAttributes[.featureSettings] as? [[UIFontDescriptor.FeatureKey: Int]] ?? []
938
+ settings.removeAll { $0[.type] == kNumberSpacingType }
939
+ settings.append([.type: kNumberSpacingType, .selector: kMonospacedNumbersSelector])
940
+ return UIFont(descriptor: font.fontDescriptor.addingAttributes([.featureSettings: settings]), size: font.pointSize)
941
+ }
942
+
943
+ private func selectorTabularText(_ original: NSAttributedString) -> NSAttributedString {
944
+ let result = NSMutableAttributedString(attributedString: original)
945
+ // OneKey patch: body typography explicitly supplies letterSpacing=0 in Tamagui.
946
+ result.addAttribute(.kern, value: 0, range: NSRange(location: 0, length: result.length))
947
+ original.enumerateAttribute(.font, in: NSRange(location: 0, length: original.length)) { value, range, _ in
948
+ if let font = value as? UIFont { result.addAttribute(.font, value: self.selectorTabularFont(font), range: range) }
949
+ }
950
+ return result
951
+ }
952
+
953
+ private func restoreSelectorTypography() {
954
+ selectorTypographyRestorers.reversed().forEach { $0() }
955
+ selectorTypographyRestorers.removeAll()
956
+ }
957
+
958
+ private func applySelectorTypography(_ item: NativeListItem) {
959
+ guard ["accountSelector", "networkSelector", "walletSidebar"].contains(item.data.string("presentation")) || item.type == "system" && item.data.string("variant") == "warning" else { return }
960
+ func visit(_ view: UIView) {
961
+ if let button = view as? UIButton {
962
+ if let original = button.attributedTitle(for: .normal) {
963
+ selectorTypographyRestorers.append { button.setAttributedTitle(original, for: .normal) }
964
+ button.setAttributedTitle(selectorTabularText(original), for: .normal)
965
+ }
966
+ } else if let label = view as? UILabel, let font = label.font {
967
+ let original = label.attributedText
968
+ selectorTypographyRestorers.append { label.font = font; label.attributedText = original }
969
+ label.font = selectorTabularFont(font)
970
+ if let original { label.attributedText = selectorTabularText(original) }
971
+ }
972
+ for child in view.subviews { visit(child) }
973
+ }
974
+ visit(contentView)
975
+ }
976
+
789
977
  private func updateSummaryText(_ item: NativeListItem) {
790
978
  let title = item.data.string("title")
791
979
  titleLabel.isHidden = title.isEmpty
792
980
  setLineHeight(titleLabel, text: title, lineHeight: 24)
793
981
 
794
982
  let value = item.data.string("value")
983
+ let isExplicitNetworkHeader = item.data.string("presentation") == "networkSelector" && item.data["height"] != nil
795
984
  let valueButton = accessoryButtons[0]
796
985
  valueButton.isHidden = value.isEmpty
797
986
  if value.isEmpty {
@@ -801,7 +990,7 @@ final class NativeListCell: UICollectionViewCell {
801
990
  setButtonLine(
802
991
  valueButton,
803
992
  text: value,
804
- font: nativeListFont(ofSize: 16),
993
+ font: nativeListFont(ofSize: 16, weight: isExplicitNetworkHeader ? .medium : .regular),
805
994
  color: nativeListColor(currentTheme, "secondaryText", "#646464"),
806
995
  lineHeight: 24
807
996
  )
@@ -826,7 +1015,7 @@ final class NativeListCell: UICollectionViewCell {
826
1015
  // Selection is communicated by the destination state for these source
827
1016
  // components; neither has a persistent selected tile background.
828
1017
  color = nativeListColor(theme, "rowBackground", "#FFFFFF")
829
- } else if layout == "sectioned" {
1018
+ } else if layout == "sectioned", !item.data.bool("selected") {
830
1019
  // Checkbox-backed section lists in app-monorepo keep rows on $bg;
831
1020
  // selection is represented by the checkbox itself.
832
1021
  color = nativeListColor(theme, "rowBackground", "#FFFFFF")
@@ -836,10 +1025,27 @@ final class NativeListCell: UICollectionViewCell {
836
1025
  !selected {
837
1026
  color = nativeListColor(theme, "subduedBackground", "#F9F9F9")
838
1027
  }
1028
+ // OneKey patch: portfolio group headers retain their source background.
1029
+ if let backgroundColor = item.data["backgroundColor"] as? String {
1030
+ return UIColor(nativeListHex: backgroundColor, fallback: color)
1031
+ }
839
1032
  return color
840
1033
  }
841
1034
 
842
1035
  private func reset() {
1036
+ restoreSelectorTypography()
1037
+ // OneKey patch: remove selector decorations before rebinding recycled cells.
1038
+ selectorViews.forEach { $0.removeFromSuperview() }
1039
+ selectorViews.removeAll()
1040
+ NSLayoutConstraint.deactivate(selectorConstraints)
1041
+ selectorConstraints.removeAll()
1042
+ selectorImages.forEach { $0.prepareForReuse() }
1043
+ selectorImages.removeAll()
1044
+ selectorFullWidthBackground.removeFromSuperlayer()
1045
+ selectorBorder?.removeFromSuperlayer()
1046
+ selectorBorder = nil
1047
+ titleLabel.removeGestureRecognizer(selectorTitleTap)
1048
+ titleLabel.isUserInteractionEnabled = false
843
1049
  walletGroupCompactCell?.invalidateCurrentBinding()
844
1050
  walletGroupCells.forEach { $0.invalidateCurrentBinding() }
845
1051
  isHighlighted = false
@@ -848,6 +1054,10 @@ final class NativeListCell: UICollectionViewCell {
848
1054
  $0.removeFromSuperview()
849
1055
  }
850
1056
  walletGroupMembers.removeAll()
1057
+ // OneKey patch: reset has detached the old hierarchy, including on direct
1058
+ // large-to-small binds that do not pass through UICollectionView reuse.
1059
+ let requiredMembers = currentItem?.type == "walletGroup" ? (currentItem?.data.dictionaries("children").count ?? 0) + 1 : 0
1060
+ trimWalletGroupCells(keeping: requiredMembers)
851
1061
  walletGroupCompactAppearanceActive = false
852
1062
  walletGroupCompactContainer.isHidden = true
853
1063
  walletGroupCompactContainer.alpha = 1
@@ -978,11 +1188,13 @@ final class NativeListCell: UICollectionViewCell {
978
1188
  $0.backgroundColor = .clear
979
1189
  }
980
1190
  accessoryButtons.enumerated().forEach { index, button in
1191
+ button.selectorSummaryLineHeight = nil
981
1192
  button.titleLabel?.font = nativeListFont(
982
1193
  ofSize: index == 0 ? 16 : 14,
983
1194
  weight: index == 0 ? .medium : .regular
984
1195
  )
985
1196
  button.isHidden = true
1197
+ button.accessibilityIdentifier = nil
986
1198
  button.setTitle(nil, for: .normal)
987
1199
  button.setAttributedTitle(nil, for: .normal)
988
1200
  button.setImage(nil, for: .normal)
@@ -994,6 +1206,7 @@ final class NativeListCell: UICollectionViewCell {
994
1206
  button.backgroundColor = .clear
995
1207
  button.layer.cornerRadius = 0
996
1208
  button.contentEdgeInsets = .zero
1209
+ button.transform = .identity
997
1210
  }
998
1211
  checkboxButton.isHidden = true
999
1212
  checkboxButton.alpha = 1
@@ -1028,6 +1241,7 @@ final class NativeListCell: UICollectionViewCell {
1028
1241
  contentView.clipsToBounds = false
1029
1242
  leadingContainer.alpha = 1
1030
1243
  titleLabel.showsDottedUnderline = false
1244
+ titleLabel.reservesDottedUnderlineSpace = false
1031
1245
  titleLabel.dottedUnderlineVerticalOffset = 0
1032
1246
  }
1033
1247
 
@@ -1073,7 +1287,8 @@ final class NativeListCell: UICollectionViewCell {
1073
1287
  while walletGroupCells.count < walletGroupMembers.count {
1074
1288
  let memberCell = NativeListCell(frame: .zero)
1075
1289
  memberCell.translatesAutoresizingMaskIntoConstraints = false
1076
- memberCell.heightAnchor.constraint(equalToConstant: 68).isActive = true
1290
+ // OneKey patch: each member's current height is applied when bound.
1291
+ // memberCell.heightAnchor.constraint(equalToConstant: 68).isActive = true
1077
1292
  walletGroupCells.append(memberCell)
1078
1293
  }
1079
1294
  rootStack.axis = .vertical
@@ -1083,8 +1298,18 @@ final class NativeListCell: UICollectionViewCell {
1083
1298
  rootTrailingConstraint.constant = 0
1084
1299
  rootTopConstraint.constant = 0
1085
1300
  rootBottomConstraint.constant = 0
1301
+ if memberData.first?["height"] != nil {
1302
+ // OneKey patch: the source group's one-point border occupies layout space.
1303
+ rootLeadingConstraint.constant = 1
1304
+ rootTrailingConstraint.constant = -1
1305
+ rootTopConstraint.constant = 1
1306
+ rootBottomConstraint.constant = -1
1307
+ }
1086
1308
  walletGroupMembers.enumerated().forEach { index, member in
1087
1309
  let memberCell = walletGroupCells[index]
1310
+ // OneKey patch: badges add a second line within their logical wallet group.
1311
+ memberCell.constraints.filter { $0.firstAttribute == .height && $0.secondItem == nil }.forEach { $0.isActive = false }
1312
+ memberCell.heightAnchor.constraint(equalToConstant: CGFloat(member.data.double("height", default: member.data.dictionaries("badges").isEmpty ? 68 : 92))).isActive = true
1088
1313
  memberCell.onAction = { [weak self] source, action, target, origin in
1089
1314
  self?.onAction?(source, action, target, origin)
1090
1315
  }
@@ -1140,7 +1365,10 @@ final class NativeListCell: UICollectionViewCell {
1140
1365
  let point = gesture.location(in: rootStack)
1141
1366
  for (index, cell) in walletGroupCells.prefix(walletGroupMembers.count).enumerated()
1142
1367
  where cell.frame.contains(point) {
1143
- onAction?(walletGroupMembers[index], "press", nil, cell.rowActionOrigin())
1368
+ // OneKey patch: group member press gating must not disable accessory controls.
1369
+ if !walletGroupMembers[index].data.bool("pressDisabled") {
1370
+ onAction?(walletGroupMembers[index], "press", nil, cell.rowActionOrigin())
1371
+ }
1144
1372
  return
1145
1373
  }
1146
1374
  }
@@ -1269,8 +1497,14 @@ final class NativeListCell: UICollectionViewCell {
1269
1497
  contentView.clipsToBounds = true
1270
1498
  } else {
1271
1499
  applyGroupPosition(currentItem?.data.string("groupPosition") ?? "")
1272
- let restingRadius: CGFloat = currentItem?.type == "metricCard" ? 12 : 0
1500
+ // OneKey patch: explicit account and network selectors preserve ListItem radius while idle.
1501
+ // let restingRadius: CGFloat = currentItem?.type == "metricCard" ? 12 : 0
1502
+ let isSelectorIdentity = currentItem?.type == "identity" && currentItem?.data["height"] != nil
1503
+ let isAccountSelector = isSelectorIdentity && ["accountSelector", "networkSelector"].contains(currentItem?.data.string("presentation") ?? "")
1504
+ let isWalletSidebar = isSelectorIdentity && currentItem?.data.string("presentation") == "walletSidebar"
1505
+ let restingRadius: CGFloat = isWalletSidebar ? 20 : currentItem?.type == "metricCard" || isAccountSelector ? 12 : 0
1273
1506
  contentView.layer.cornerRadius = restingRadius
1507
+ contentView.layer.cornerCurve = isWalletSidebar ? .continuous : .circular
1274
1508
  contentView.clipsToBounds = restingRadius > 0
1275
1509
  }
1276
1510
  }
@@ -1299,6 +1533,17 @@ final class NativeListCell: UICollectionViewCell {
1299
1533
  fallbackLabel.font = nativeListFont(ofSize: 28)
1300
1534
  addLeading(item.data.dictionary("leading"), key: item.key)
1301
1535
  rootStack.addArrangedSubview(mainStack)
1536
+ // OneKey patch: activate width constraints only after both stacks share an ancestor.
1537
+ if item.data["height"] != nil {
1538
+ titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
1539
+ titleRowStack.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
1540
+ let width = mainStack.widthAnchor.constraint(equalTo: rootStack.widthAnchor)
1541
+ width.isActive = true
1542
+ selectorConstraints.append(width)
1543
+ let titleWidth = titleRowStack.widthAnchor.constraint(lessThanOrEqualTo: mainStack.widthAnchor)
1544
+ titleWidth.isActive = true
1545
+ selectorConstraints.append(titleWidth)
1546
+ }
1302
1547
  show(titleLabel, item.data.string("title"), lines: 1)
1303
1548
  setLineHeight(titleLabel, text: item.data.string("title"), lineHeight: 16)
1304
1549
  titleLabel.textColor = nativeListColor(
@@ -1306,6 +1551,32 @@ final class NativeListCell: UICollectionViewCell {
1306
1551
  selected ? "primaryText" : "secondaryText",
1307
1552
  selected ? "#FFFFFFED" : "#FFFFFFAF"
1308
1553
  )
1554
+ // OneKey patch: wallet tags belong below the centered name.
1555
+ let badges = item.data.dictionaries("badges")
1556
+ if !badges.isEmpty {
1557
+ let line = UIStackView()
1558
+ line.axis = .horizontal
1559
+ line.spacing = 4
1560
+ line.alignment = .center
1561
+ for badge in badges {
1562
+ let label = NativeListInsetLabel()
1563
+ let isSelector = item.data["height"] != nil
1564
+ let isWarning = badge.string("tone") == "warning"
1565
+ label.font = nativeListFont(ofSize: isSelector ? 11 : 12)
1566
+ label.textColor = nativeListColor(theme, isSelector && isWarning ? "caution" : "secondaryText", isSelector && isWarning ? "#AB6400" : "#646464")
1567
+ label.backgroundColor = nativeListColor(theme, isSelector ? (isWarning ? "cautionBackground" : "subduedBackground") : "strongBackground", isSelector && isWarning ? "#FFF8C5" : "#F0F0F0")
1568
+ label.horizontalInset = isSelector ? 6 : 4
1569
+ label.topInset = 2
1570
+ label.bottomInset = 2
1571
+ label.layer.cornerRadius = 4
1572
+ label.clipsToBounds = true
1573
+ setLineHeight(label, text: badge.string("text"), lineHeight: isSelector ? 14 : 16)
1574
+ line.addArrangedSubview(label)
1575
+ }
1576
+ mainStack.spacing = 4
1577
+ mainStack.addArrangedSubview(line)
1578
+ selectorViews.append(line)
1579
+ }
1309
1580
  return
1310
1581
  }
1311
1582
  if item.data.string("presentation") == "accountSelector" {
@@ -1344,6 +1615,12 @@ final class NativeListCell: UICollectionViewCell {
1344
1615
  rootStack.setCustomSpacing(5, after: leadingActionButton)
1345
1616
  }
1346
1617
  addLeading(item.data.dictionary("leading"), key: item.key)
1618
+ // OneKey patch: custom network initials match LetterAvatar size 32.
1619
+ if item.data.string("presentation") == "networkSelector", let leading = item.data.dictionary("leading"), leading.dictionary("image") == nil, leading.dictionary("fallbackIcon") == nil, !leading.string("fallbackText").isEmpty {
1620
+ fallbackLabel.font = nativeListFont(ofSize: 19, weight: .semibold)
1621
+ fallbackLabel.textColor = nativeListColor(theme, "inverseText", "#FCFCFC")
1622
+ setLineHeight(fallbackLabel, text: leading.string("fallbackText"), lineHeight: 27)
1623
+ }
1347
1624
  rootStack.addArrangedSubview(mainStack)
1348
1625
  show(titleLabel, item.data.string("title"), lines: item.data.int("titleLines", default: 1))
1349
1626
  show(subtitleLabel, item.data.string("subtitle"), lines: item.data.int("subtitleLines", default: 1))
@@ -1353,6 +1630,77 @@ final class NativeListCell: UICollectionViewCell {
1353
1630
  setLineHeight(titleLabel, text: item.data.string("title"), lineHeight: 24)
1354
1631
  setLineHeight(subtitleLabel, text: item.data.string("subtitle"), lineHeight: 20)
1355
1632
  }
1633
+ // OneKey patch: preserve independent balance/address truncation and warning tones.
1634
+ let segments = item.data.dictionaries("subtitleSegments")
1635
+ if !segments.isEmpty {
1636
+ subtitleLabel.isHidden = true
1637
+ mainStack.spacing = 0
1638
+ setLineHeight(titleLabel, text: item.data.string("title"), lineHeight: 24)
1639
+ let line = UIStackView()
1640
+ line.axis = .horizontal
1641
+ line.alignment = .center
1642
+ line.spacing = 0
1643
+ for segment in segments {
1644
+ if segment.bool("separatorBefore") {
1645
+ let gap = UIView()
1646
+ gap.translatesAutoresizingMaskIntoConstraints = false
1647
+ let dot = UIView()
1648
+ dot.translatesAutoresizingMaskIntoConstraints = false
1649
+ dot.backgroundColor = nativeListColor(theme, "disabledText", "#8D8D8D")
1650
+ dot.layer.cornerRadius = 2
1651
+ gap.addSubview(dot)
1652
+ NSLayoutConstraint.activate([
1653
+ gap.widthAnchor.constraint(equalToConstant: 16),
1654
+ gap.heightAnchor.constraint(equalToConstant: 20),
1655
+ dot.widthAnchor.constraint(equalToConstant: 4),
1656
+ dot.heightAnchor.constraint(equalToConstant: 4),
1657
+ dot.centerXAnchor.constraint(equalTo: gap.centerXAnchor),
1658
+ dot.centerYAnchor.constraint(equalTo: gap.centerYAnchor),
1659
+ ])
1660
+ line.addArrangedSubview(gap)
1661
+ }
1662
+ let label = UILabel()
1663
+ label.font = nativeListFont(ofSize: 14)
1664
+ label.lineBreakMode = .byTruncatingTail
1665
+ label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
1666
+ label.textColor = dataTextColor(segment.string("tone", default: "secondary"), theme: theme)
1667
+ setLineHeight(label, text: segment.string("text"), lineHeight: 20)
1668
+ let runs = segment.dictionaries("textSegments")
1669
+ if !runs.isEmpty {
1670
+ let value = NSMutableAttributedString(string: "")
1671
+ let paragraph = NSMutableParagraphStyle()
1672
+ paragraph.minimumLineHeight = 20
1673
+ paragraph.maximumLineHeight = 20
1674
+ for run in runs {
1675
+ value.append(NSAttributedString(string: run.string("text"), attributes: [
1676
+ .font: nativeListFont(ofSize: run.string("style") == "subscript" ? 9 : 14),
1677
+ .foregroundColor: label.textColor as Any,
1678
+ .paragraphStyle: paragraph,
1679
+ .baselineOffset: max(0, (20 - nativeListFont(ofSize: 14).lineHeight) / 2),
1680
+ ]))
1681
+ }
1682
+ label.attributedText = value
1683
+ }
1684
+ line.addArrangedSubview(label)
1685
+ }
1686
+ let filler = UIView()
1687
+ filler.setContentHuggingPriority(UILayoutPriority(1), for: .horizontal)
1688
+ line.addArrangedSubview(filler)
1689
+ mainStack.insertArrangedSubview(line, at: 2)
1690
+ selectorViews.append(line)
1691
+ }
1692
+ let matches = item.data.dictionaries("titleMatch")
1693
+ if !matches.isEmpty {
1694
+ let text = NSMutableAttributedString(attributedString: titleLabel.attributedText ?? NSAttributedString(string: item.data.string("title")))
1695
+ for match in matches {
1696
+ let start = match.int("start")
1697
+ let end = match.int("end")
1698
+ if start >= 0 && end > start && end <= text.length {
1699
+ text.addAttribute(.foregroundColor, value: nativeListColor(theme, "info", "#0D74CE"), range: NSRange(location: start, length: end - start))
1700
+ }
1701
+ }
1702
+ titleLabel.attributedText = text
1703
+ }
1356
1704
  tertiaryLabel.textColor = nativeListColor(
1357
1705
  theme,
1358
1706
  item.data.string("tertiaryTone") == "info" ? "info" : "secondaryText",
@@ -1997,15 +2345,29 @@ final class NativeListCell: UICollectionViewCell {
1997
2345
  ) {
1998
2346
  rootStack.addArrangedSubview(mainStack)
1999
2347
  let variant = item.data.string("variant")
2348
+ // OneKey patch: title help is a separate target from checkbox and value actions.
2349
+ if !item.data.string("titleActionKey").isEmpty {
2350
+ titleLabel.isUserInteractionEnabled = true
2351
+ titleLabel.addGestureRecognizer(selectorTitleTap)
2352
+ titleLabel.setContentHuggingPriority(.required, for: .horizontal)
2353
+ }
2000
2354
  let isSummary = variant == "summary"
2001
2355
  let isGallery = variant == "gallery"
2002
2356
  let isTable = layout == "table"
2003
2357
  let isNetworkSelector = item.data.string("presentation") == "networkSelector"
2358
+ let isExplicitNetworkHeader = isNetworkSelector && item.data["height"] != nil
2359
+ if isExplicitNetworkHeader {
2360
+ // OneKey patch: the flexible title consumes spare space before trailing totals.
2361
+ trailingStack.setContentHuggingPriority(.required, for: .horizontal)
2362
+ trailingStack.setContentCompressionResistancePriority(.required, for: .horizontal)
2363
+ }
2364
+ titleLabel.reservesDottedUnderlineSpace = isExplicitNetworkHeader && !item.data.string("titleActionKey").isEmpty
2004
2365
  let isHistory = variant == "history" ||
2005
2366
  item.key.hasPrefix("history-") ||
2006
2367
  (item.sectionKey?.hasPrefix("history-") ?? false)
2007
2368
  let headerWeight: NativeListFontWeight = isSummary
2008
2369
  ? .medium
2370
+ : isExplicitNetworkHeader && (item.data.dictionary("checkbox") != nil || item.data.string("titleActionKey").isEmpty) ? .semibold
2009
2371
  : isNetworkSelector ? .medium
2010
2372
  : isGallery || layout == "sectioned" ? .semibold : .regular
2011
2373
  titleLabel.font = nativeListFont(
@@ -2019,8 +2381,8 @@ final class NativeListCell: UICollectionViewCell {
2019
2381
  )
2020
2382
  show(titleLabel, item.data.string("title"), lines: 1)
2021
2383
  if isNetworkSelector {
2022
- titleLabel.showsDottedUnderline = true
2023
- titleLabel.dottedUnderlineVerticalOffset = 2
2384
+ titleLabel.showsDottedUnderline = !isExplicitNetworkHeader || !item.data.string("titleActionKey").isEmpty
2385
+ titleLabel.dottedUnderlineVerticalOffset = isExplicitNetworkHeader ? 1 : 2
2024
2386
  titleLabel.dottedUnderlineColor = nativeListColor(
2025
2387
  theme,
2026
2388
  "secondaryText",
@@ -2028,8 +2390,9 @@ final class NativeListCell: UICollectionViewCell {
2028
2390
  )
2029
2391
  rootLeadingConstraint.constant = 12
2030
2392
  rootTrailingConstraint.constant = -12
2031
- rootTopConstraint.constant = 12
2032
- rootBottomConstraint.constant = -15
2393
+ let isAlphabet = isExplicitNetworkHeader && item.data.string("titleActionKey").isEmpty
2394
+ rootTopConstraint.constant = isAlphabet ? 8 : 12
2395
+ rootBottomConstraint.constant = isAlphabet ? -8 : isExplicitNetworkHeader ? -12 : -15
2033
2396
  setLineHeight(titleLabel, text: item.data.string("title"), lineHeight: 20)
2034
2397
  } else if isHistory {
2035
2398
  rootLeadingConstraint.constant = 0
@@ -2095,6 +2458,7 @@ final class NativeListCell: UICollectionViewCell {
2095
2458
  rootTopConstraint.constant = 24
2096
2459
  rootBottomConstraint.constant = -20
2097
2460
  setLineHeight(titleLabel, text: item.data.string("title"), lineHeight: 24)
2461
+ accessoryButtons[0].accessibilityIdentifier = item.data["valueActionTestID"] as? String
2098
2462
  let valueActionKey = item.data.string("valueActionKey")
2099
2463
  let action: (String, NativeSelectionTarget?)? = valueActionKey.isEmpty
2100
2464
  ? nil
@@ -2105,11 +2469,11 @@ final class NativeListCell: UICollectionViewCell {
2105
2469
  action: action,
2106
2470
  color: nativeListColor(theme, "secondaryText", "#646464")
2107
2471
  )
2108
- accessoryButtons[0].titleLabel?.font = nativeListFont(ofSize: 16)
2472
+ accessoryButtons[0].titleLabel?.font = nativeListFont(ofSize: 16, weight: isExplicitNetworkHeader ? .medium : .regular)
2109
2473
  setButtonLine(
2110
2474
  accessoryButtons[0],
2111
2475
  text: item.data.string("value"),
2112
- font: nativeListFont(ofSize: 16),
2476
+ font: nativeListFont(ofSize: 16, weight: isExplicitNetworkHeader ? .medium : .regular),
2113
2477
  color: nativeListColor(theme, "secondaryText", "#646464"),
2114
2478
  lineHeight: 24
2115
2479
  )
@@ -2169,6 +2533,28 @@ final class NativeListCell: UICollectionViewCell {
2169
2533
  )
2170
2534
  }
2171
2535
  }
2536
+ applyValueSegments(item.data.dictionaries("valueSegments"), to: accessoryButtons[0], theme: theme)
2537
+ }
2538
+
2539
+ // OneKey patch: small zero-count digits remain on the regular amount baseline.
2540
+ private func applyValueSegments(_ segments: [[String: Any]], to button: UIButton, theme: [String: Any]?) {
2541
+ guard !segments.isEmpty else { return }
2542
+ let value = NSMutableAttributedString(string: "")
2543
+ let color = nativeListColor(theme, "primaryText", "#202020")
2544
+ // OneKey patch: rich currency changes font runs without dropping the established line baseline.
2545
+ let current = button.attributedTitle(for: .normal)
2546
+ var attributes = current.flatMap { $0.length > 0 ? $0.attributes(at: 0, effectiveRange: nil) : nil } ?? [:]
2547
+ attributes[.foregroundColor] = color
2548
+ if currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil {
2549
+ let paragraph = (attributes[.paragraphStyle] as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
2550
+ paragraph.alignment = .right
2551
+ attributes[.paragraphStyle] = paragraph
2552
+ }
2553
+ for segment in segments {
2554
+ attributes[.font] = nativeListTabularFont(ofSize: segment.string("style") == "subscript" ? 10 : 16, weight: .medium)
2555
+ value.append(NSAttributedString(string: segment.string("text"), attributes: attributes))
2556
+ }
2557
+ button.setAttributedTitle(value, for: .normal)
2172
2558
  }
2173
2559
 
2174
2560
  private func bindAction(
@@ -2185,6 +2571,8 @@ final class NativeListCell: UICollectionViewCell {
2185
2571
  addLeading(icon, key: item.key)
2186
2572
  if isAccountSelector {
2187
2573
  leadingContainer.layer.cornerCurve = .continuous
2574
+ leadingContainer.layer.cornerRadius = 8
2575
+ leadingContainer.layer.borderWidth = 0
2188
2576
  }
2189
2577
  if icon["backgroundColor"] == nil {
2190
2578
  leadingContainer.backgroundColor = .clear
@@ -2195,8 +2583,9 @@ final class NativeListCell: UICollectionViewCell {
2195
2583
  rootStack.addArrangedSubview(mainStack)
2196
2584
  show(titleLabel, item.data.string("title"), lines: 1)
2197
2585
  if isAccountSelector {
2198
- titleLabel.font = nativeListFont(ofSize: 16)
2199
- titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
2586
+ // OneKey patch: ListItem.Text is medium; empty-search actions use regular body text.
2587
+ titleLabel.font = nativeListFont(ofSize: 16, weight: item.data.dictionary("icon") == nil ? .regular : .medium)
2588
+ titleLabel.textColor = nativeListColor(theme, item.data.string("tone") == "primary" ? "primaryText" : "secondaryText", item.data.string("tone") == "primary" ? "#202020" : "#646464")
2200
2589
  } else if item.data.string("tone") == "danger" {
2201
2590
  titleLabel.textColor = nativeListColor(theme, "negative", "#CE2C31")
2202
2591
  }
@@ -2217,8 +2606,19 @@ final class NativeListCell: UICollectionViewCell {
2217
2606
  }
2218
2607
  }
2219
2608
 
2609
+ // OneKey patch: preserve the actual title frame for Popover placement.
2610
+ @objc private func selectorTitlePressed() {
2611
+ guard let item = currentItem else { return }
2612
+ let action = item.data.string("titleActionKey")
2613
+ guard !action.isEmpty else { return }
2614
+ onAction?(item, action, nil, actionOrigin(sourceView: titleLabel, source: "leadingAction"))
2615
+ }
2616
+
2220
2617
  private func dataTextColor(_ tone: String, theme: [String: Any]?) -> UIColor {
2221
2618
  switch tone.isEmpty ? "primary" : tone {
2619
+ // OneKey patch: account warnings and hidden balances use existing theme tokens.
2620
+ case "disabled": return nativeListColor(theme, "disabledText", "#8D8D8D")
2621
+ case "caution": return nativeListColor(theme, "caution", "#AB6400")
2222
2622
  case "secondary": return nativeListColor(theme, "secondaryText", "#646464")
2223
2623
  case "positive": return nativeListColor(theme, "positive", "#218358")
2224
2624
  case "negative": return nativeListColor(theme, "negative", "#CE2C31")
@@ -2230,6 +2630,36 @@ final class NativeListCell: UICollectionViewCell {
2230
2630
  rootStack.alignment = .center
2231
2631
  rootStack.distribution = .fill
2232
2632
  let variant = item.data.string("variant")
2633
+ // OneKey patch: deprecated-wallet warnings stay inside the scrolling list.
2634
+ if variant == "warning" {
2635
+ rootStack.addArrangedSubview(mainStack)
2636
+ rootTopConstraint.constant = 14
2637
+ rootBottomConstraint.constant = -14
2638
+ mainStack.spacing = 4
2639
+ titleLabel.font = nativeListFont(ofSize: 14, weight: .medium)
2640
+ titleLabel.numberOfLines = 0
2641
+ subtitleLabel.font = nativeListFont(ofSize: 14)
2642
+ subtitleLabel.numberOfLines = 0
2643
+ show(titleLabel, item.data.string("title"), lines: 0)
2644
+ show(subtitleLabel, item.data.string("message"), lines: 0)
2645
+ setLineHeight(titleLabel, text: item.data.string("title"), lineHeight: 20)
2646
+ setLineHeight(subtitleLabel, text: item.data.string("message"), lineHeight: 20)
2647
+ let borderColor = UIColor(nativeListHex: item.data.string("borderColor", default: "#E0E0E0"), fallback: .lightGray)
2648
+ for top in [true, false] {
2649
+ let border = UIView()
2650
+ border.translatesAutoresizingMaskIntoConstraints = false
2651
+ border.backgroundColor = borderColor
2652
+ contentView.addSubview(border)
2653
+ NSLayoutConstraint.activate([
2654
+ border.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: -8),
2655
+ border.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 8),
2656
+ border.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale),
2657
+ top ? border.topAnchor.constraint(equalTo: contentView.topAnchor) : border.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
2658
+ ])
2659
+ selectorViews.append(border)
2660
+ }
2661
+ return
2662
+ }
2233
2663
  if variant == "loading" {
2234
2664
  leadingWidth.constant = 40
2235
2665
  leadingHeight.constant = 40
@@ -2357,7 +2787,105 @@ final class NativeListCell: UICollectionViewCell {
2357
2787
  tokenPair: tokenPair,
2358
2788
  shape: shape
2359
2789
  ))
2360
- bindImage(source.data, into: imageView, token: key, slot: index, variant: source.variant)
2790
+ if currentItem?.data.string("presentation") == "networkSelector", currentItem?.data["height"] != nil, visibleSources.count == 1, cornerIcon == nil, visual.dictionaries("overlays").isEmpty {
2791
+ // OneKey patch: a single outer mask matches NetworkAvatar's edge antialiasing.
2792
+ imageView.layer.cornerRadius = 0
2793
+ imageView.clipsToBounds = false
2794
+ }
2795
+ let fallbackIcon = index == 0 ? visual.dictionary("fallbackIcon") : nil
2796
+ let expectedEpoch = bindingEpoch
2797
+ bindImage(source.data, into: imageView, token: key, slot: index, variant: source.variant,
2798
+ onLoad: fallbackIcon == nil ? nil : { [weak self, weak imageView] in
2799
+ guard let self, self.bindingEpoch == expectedEpoch else { return }
2800
+ imageView?.isHidden = false
2801
+ self.leadingIconImageView.isHidden = true
2802
+ },
2803
+ onError: fallbackIcon == nil ? nil : { [weak self, weak imageView] in
2804
+ guard let self, self.bindingEpoch == expectedEpoch, let fallbackIcon else { return }
2805
+ imageView?.isHidden = true
2806
+ self.fallbackLabel.isHidden = true
2807
+ self.leadingIconImageView.image = nativeListIcon(named: fallbackIcon.string("name"))
2808
+ self.leadingIconImageView.tintColor = UIColor(nativeListHex: fallbackIcon.string("tintColor", default: "#646464"), fallback: .darkGray)
2809
+ self.leadingIconImageView.isHidden = false
2810
+ })
2811
+ }
2812
+ // OneKey patch: source-derived wallet decorations may occupy both corners.
2813
+ let overlays = visual.dictionaries("overlays")
2814
+ if !overlays.isEmpty { leadingContainer.clipsToBounds = false }
2815
+ for (index, overlay) in overlays.enumerated() {
2816
+ let size = CGFloat(overlay.double("size", default: 20))
2817
+ let isWalletText = currentItem?.data.string("presentation") == "walletSidebar" && currentItem?.data["height"] != nil && !overlay.string("text").isEmpty && overlay.dictionary("image") == nil && overlay.string("name").isEmpty
2818
+ let inset = CGFloat(overlay.double("padding", default: 0))
2819
+ let offsetX = CGFloat(overlay.double("offsetX", default: overlay.double("offset", default: 2)))
2820
+ let offsetY = CGFloat(overlay.double("offsetY", default: overlay.double("offset", default: 2)))
2821
+ let height = CGFloat(overlay.double("height", default: isWalletText ? 16 : Double(size)))
2822
+ let textWidth = (overlay.string("text") as NSString).size(withAttributes: [.font: nativeListTabularFont(ofSize: 12), .kern: 0]).width
2823
+ let naturalTextWidth = ceil(textWidth * UIScreen.main.scale) / UIScreen.main.scale + 4
2824
+ let width = CGFloat(overlay.double("width", default: isWalletText ? Double(naturalTextWidth) : Double(size)))
2825
+ let frame = UIView()
2826
+ frame.translatesAutoresizingMaskIntoConstraints = false
2827
+ frame.backgroundColor = UIColor(nativeListHex: overlay.string("backgroundColor", default: "#FFFFFF"), fallback: .clear)
2828
+ frame.layer.cornerRadius = min(width, height) / 2
2829
+ frame.clipsToBounds = true
2830
+ leadingContainer.addSubview(frame)
2831
+ selectorViews.append(frame)
2832
+ let topLeft = overlay.string("position") == "topLeft"
2833
+ leadingSlotConstraints.append(contentsOf: [
2834
+ frame.widthAnchor.constraint(equalToConstant: width),
2835
+ frame.heightAnchor.constraint(equalToConstant: height),
2836
+ topLeft ? frame.leadingAnchor.constraint(equalTo: leadingContainer.leadingAnchor, constant: -offsetX) : frame.trailingAnchor.constraint(equalTo: leadingContainer.trailingAnchor, constant: offsetX),
2837
+ topLeft ? frame.topAnchor.constraint(equalTo: leadingContainer.topAnchor, constant: -offsetY) : frame.bottomAnchor.constraint(equalTo: leadingContainer.bottomAnchor, constant: offsetY),
2838
+ ])
2839
+ let content: UIView
2840
+ if let image = overlay.dictionary("image") {
2841
+ let imageView = OneKeyImageReusableView(frame: .zero)
2842
+ bindImage(image, into: imageView, token: key, slot: 10 + index, variant: "generic")
2843
+ selectorImages.append(imageView)
2844
+ content = imageView
2845
+ } else if !overlay.string("text").isEmpty {
2846
+ let label = UILabel()
2847
+ label.text = overlay.string("text")
2848
+ label.textAlignment = .center
2849
+ label.font = nativeListFont(ofSize: isWalletText ? 12 : 10, weight: isWalletText ? .regular : .medium)
2850
+ label.textColor = UIColor(nativeListHex: overlay.string("tintColor", default: "#646464"), fallback: .darkGray)
2851
+ if isWalletText { setLineHeight(label, text: overlay.string("text"), lineHeight: 16) }
2852
+ content = label
2853
+ } else {
2854
+ let imageView = UIImageView(image: nativeListIcon(named: overlay.string("name")))
2855
+ imageView.contentMode = .scaleAspectFit
2856
+ imageView.tintColor = UIColor(nativeListHex: overlay.string("tintColor", default: "#646464"), fallback: .darkGray)
2857
+ content = imageView
2858
+ }
2859
+ content.translatesAutoresizingMaskIntoConstraints = false
2860
+ frame.addSubview(content)
2861
+ NSLayoutConstraint.activate([
2862
+ content.leadingAnchor.constraint(equalTo: frame.leadingAnchor, constant: isWalletText ? 2 : inset),
2863
+ content.trailingAnchor.constraint(equalTo: frame.trailingAnchor, constant: isWalletText ? -2 : -inset),
2864
+ content.topAnchor.constraint(equalTo: frame.topAnchor, constant: isWalletText ? 0 : inset),
2865
+ content.bottomAnchor.constraint(equalTo: frame.bottomAnchor, constant: isWalletText ? 0 : -inset),
2866
+ ])
2867
+ }
2868
+ if let fallbackIcon = visual.dictionary("fallbackIcon"), sources.isEmpty {
2869
+ fallbackLabel.isHidden = true
2870
+ leadingIconImageView.isHidden = false
2871
+ leadingIconImageView.image = nativeListIcon(named: fallbackIcon.string("name"))
2872
+ leadingIconImageView.tintColor = UIColor(nativeListHex: fallbackIcon.string("tintColor", default: "#646464"), fallback: .darkGray)
2873
+ if currentItem?.data.string("presentation") == "walletSidebar", currentItem?.data["height"] != nil, fallbackIcon.string("name") == "LockSolid" {
2874
+ leadingIconWidth.constant = 40
2875
+ leadingIconHeight.constant = 40
2876
+ leadingContainer.clipsToBounds = false
2877
+ }
2878
+ }
2879
+ if visual.string("borderStyle") == "dashed" {
2880
+ let border = CAShapeLayer()
2881
+ border.strokeColor = UIColor(nativeListHex: visual.string("borderColor", default: "#8D8D8D"), fallback: .gray).cgColor
2882
+ border.fillColor = UIColor.clear.cgColor
2883
+ border.lineWidth = currentItem?.data.string("presentation") == "walletSidebar" && currentItem?.data["height"] != nil ? 1 : 2
2884
+ border.lineDashPattern = [4, 4]
2885
+ let borderInset = border.lineWidth / 2
2886
+ border.path = UIBezierPath(ovalIn: CGRect(x: borderInset, y: borderInset, width: leadingWidth.constant - border.lineWidth, height: leadingHeight.constant - border.lineWidth)).cgPath
2887
+ leadingContainer.layer.addSublayer(border)
2888
+ selectorBorder = border
2361
2889
  }
2362
2890
  NSLayoutConstraint.activate(leadingSlotConstraints)
2363
2891
  }
@@ -2439,6 +2967,11 @@ final class NativeListCell: UICollectionViewCell {
2439
2967
  switch accessory.string("kind") {
2440
2968
  case "value":
2441
2969
  showAccessory(textIndex, accessory.string("text"))
2970
+ if item.data.string("presentation") == "networkSelector" && item.data["height"] != nil {
2971
+ accessoryButtons[textIndex].contentHorizontalAlignment = .trailing
2972
+ accessoryButtons[textIndex].titleLabel?.textAlignment = .right
2973
+ }
2974
+ applyValueSegments(accessory.dictionaries("textSegments"), to: accessoryButtons[textIndex], theme: theme)
2442
2975
  textIndex += 1
2443
2976
  case "valuePair":
2444
2977
  showValuePairAccessory(textIndex, accessory, theme: theme)
@@ -2519,7 +3052,7 @@ final class NativeListCell: UICollectionViewCell {
2519
3052
  state == "unchecked" ? nil : nativeListIcon(named: glyphName),
2520
3053
  for: .normal
2521
3054
  )
2522
- checkboxButton.tintColor = checkboxUncheckedColor
3055
+ checkboxButton.tintColor = checkboxIconColor
2523
3056
  // A disabled ListItem already applies 0.5 to its complete content. Avoid
2524
3057
  // multiplying that opacity on the nested control a second time.
2525
3058
  checkboxButton.alpha = item.data.bool("disabled") ? 1 : accessoryDisabled ? 0.5 : 1
@@ -2543,11 +3076,24 @@ final class NativeListCell: UICollectionViewCell {
2543
3076
  let paragraphStyle = NSMutableParagraphStyle()
2544
3077
  paragraphStyle.minimumLineHeight = lineHeight
2545
3078
  paragraphStyle.maximumLineHeight = lineHeight
3079
+ if currentItem?.data.string("presentation") == "walletSidebar" {
3080
+ // OneKey patch: attributed paragraphs must preserve wallet name alignment and tail ellipsis.
3081
+ paragraphStyle.alignment = label.textAlignment
3082
+ paragraphStyle.lineBreakMode = label.lineBreakMode
3083
+ }
2546
3084
  var attributes: [NSAttributedString.Key: Any] = [
2547
3085
  .font: label.font as Any,
2548
3086
  .foregroundColor: label.textColor as Any,
2549
3087
  .paragraphStyle: paragraphStyle,
2550
3088
  ]
3089
+ if (currentItem?.data["height"] != nil && (["accountSelector", "walletSidebar"].contains(currentItem?.data.string("presentation") ?? "") || currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector")) || currentItem?.type == "system" && currentItem?.data.string("variant") == "warning" {
3090
+ // OneKey patch: React Native centers font metrics inside explicit line heights.
3091
+ let baselineOffset = max(0, (lineHeight - label.font.lineHeight) / 2)
3092
+ // OneKey patch: TextKit's 14/20 headings align their baseline to the upper physical pixel.
3093
+ let isSelectorHeading = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && lineHeight == 20
3094
+ let scale = window?.screen.scale ?? traitCollection.displayScale
3095
+ attributes[.baselineOffset] = isSelectorHeading && scale > 0 ? ceil(baselineOffset * scale) / scale : baselineOffset
3096
+ }
2551
3097
  if letterSpacing != 0 { attributes[.kern] = letterSpacing }
2552
3098
  label.attributedText = NSAttributedString(string: text, attributes: attributes)
2553
3099
  }
@@ -2563,7 +3109,20 @@ final class NativeListCell: UICollectionViewCell {
2563
3109
  let paragraphStyle = NSMutableParagraphStyle()
2564
3110
  paragraphStyle.minimumLineHeight = lineHeight
2565
3111
  paragraphStyle.maximumLineHeight = lineHeight
2566
- paragraphStyle.alignment = .center
3112
+ let isSelectorValue = currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") != "summary"
3113
+ let isSelectorSummary = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") == "summary"
3114
+ (button as? NativeListAccessoryButton)?.selectorSummaryLineHeight = isSelectorSummary ? lineHeight : nil
3115
+ // OneKey patch: summary text uses its source line box; currency retains trailing alignment.
3116
+ paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary ? .natural : .center
3117
+ if isSelectorSummary {
3118
+ button.contentHorizontalAlignment = .leading
3119
+ button.titleLabel?.textAlignment = .natural
3120
+ }
3121
+ if isSelectorValue {
3122
+ button.contentHorizontalAlignment = .trailing
3123
+ button.titleLabel?.textAlignment = .right
3124
+ }
3125
+ let baselineOffset: CGFloat = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil ? max(0, (lineHeight - font.lineHeight) / 2) : 0
2567
3126
  button.setAttributedTitle(
2568
3127
  NSAttributedString(
2569
3128
  string: text,
@@ -2571,6 +3130,7 @@ final class NativeListCell: UICollectionViewCell {
2571
3130
  .font: font,
2572
3131
  .foregroundColor: color,
2573
3132
  .paragraphStyle: paragraphStyle,
3133
+ .baselineOffset: baselineOffset,
2574
3134
  ]
2575
3135
  ),
2576
3136
  for: .normal
@@ -2671,6 +3231,9 @@ final class NativeListCell: UICollectionViewCell {
2671
3231
  switch tone.isEmpty ? defaultTone : tone {
2672
3232
  case "positive": return nativeListColor(theme, "positive", "#218358")
2673
3233
  case "negative": return nativeListColor(theme, "negative", "#CE2C31")
3234
+ // OneKey patch: account warnings and hidden balances use existing theme tokens.
3235
+ case "disabled": return nativeListColor(theme, "disabledText", "#8D8D8D")
3236
+ case "caution": return nativeListColor(theme, "caution", "#AB6400")
2674
3237
  case "secondary": return nativeListColor(theme, "secondaryText", "#646464")
2675
3238
  default: return nativeListColor(theme, "primaryText", "#202020")
2676
3239
  }
@@ -2682,10 +3245,10 @@ final class NativeListCell: UICollectionViewCell {
2682
3245
  button.isHidden = false
2683
3246
  button.isEnabled = !data.bool("disabled")
2684
3247
  button.alpha = button.isEnabled ? 1 : 0.4
2685
- let tintColor = UIColor(
2686
- nativeListHex: data.string("tintColor", default: "#646464"),
2687
- fallback: .darkGray
2688
- )
3248
+ let isAccountCreate = currentItem?.data.string("presentation") == "accountSelector" && currentItem?.data["height"] != nil && data.string("name") == "PlusSmallOutline"
3249
+ let tintColor = data["tintColor"] == nil && isAccountCreate
3250
+ ? nativeListColor(currentTheme, "iconSubdued", "#8D8D8D")
3251
+ : UIColor(nativeListHex: data.string("tintColor", default: "#646464"), fallback: .darkGray)
2689
3252
  button.tintColor = tintColor
2690
3253
  if let image = nativeListIcon(named: data.string("name")) {
2691
3254
  button.setImage(image, for: .normal)
@@ -2695,7 +3258,12 @@ final class NativeListCell: UICollectionViewCell {
2695
3258
  )
2696
3259
  }
2697
3260
  let isDrillIn = data.string("kind") == "chevron"
2698
- let size: CGFloat = isDrillIn ? 24 : 36
3261
+ let isAccountIcon = currentItem?.data.string("presentation") == "accountSelector" && !isDrillIn
3262
+ let size: CGFloat = isDrillIn ? 24 : isAccountIcon && !isAccountCreate ? 38 : 36
3263
+ if isAccountCreate { button.layer.cornerRadius = 8 }
3264
+ if isAccountIcon { rootStack.setCustomSpacing(5, after: mainStack) }
3265
+ button.accessibilityIdentifier = data["testID"] as? String
3266
+ button.accessibilityLabel = data["accessibilityLabel"] as? String
2699
3267
  if !isDrillIn, !data.string("actionKey").isEmpty {
2700
3268
  // Reproduce the trailing edge of IconButton's m=-7 while keeping its
2701
3269
  // full 36-point frame for padding/highlight behavior.
@@ -2726,8 +3294,15 @@ final class NativeListCell: UICollectionViewCell {
2726
3294
  into imageView: OneKeyImageReusableView,
2727
3295
  token: String,
2728
3296
  slot: Int,
2729
- variant: String
3297
+ variant: String,
3298
+ onLoad: (() -> Void)? = nil,
3299
+ onError: (() -> Void)? = nil,
3300
+ retryAttempt: Int = 0
2730
3301
  ) {
3302
+ let imageID = ObjectIdentifier(imageView)
3303
+ selectorImageRetries.removeValue(forKey: imageID)?.cancel()
3304
+ let expectedEpoch = bindingEpoch
3305
+ let retryLimit = max(0, source.int("retryTimes", default: 0))
2731
3306
  let headersJson: String?
2732
3307
  if let headers = source.dictionary("headers"),
2733
3308
  JSONSerialization.isValidJSONObject(headers),
@@ -2743,10 +3318,27 @@ final class NativeListCell: UICollectionViewCell {
2743
3318
  contentFit: source.string("contentFit", default: "cover"),
2744
3319
  cachePolicy: source.string("cachePolicy", default: "memory-disk"),
2745
3320
  autoplay: source.bool("autoplay"),
2746
- recyclingKey: "\(token):\(slot)",
2747
- optimizeTos: source["optimizeTos"] == nil || source.bool("optimizeTos"),
3321
+ recyclingKey: retryAttempt == 0 ? "\(token):\(slot)" : "\(token):\(slot):retry:\(retryAttempt)",
3322
+ optimizeTos: retryAttempt == 0 && (source["optimizeTos"] == nil || source.bool("optimizeTos")),
2748
3323
  overscan: source["overscan"] == nil ? 1.1 : source.double("overscan"),
2749
- loadingStrategy: source.string("loadingStrategy", default: "static")
3324
+ loadingStrategy: source.string("loadingStrategy", default: "static"),
3325
+ onLoad: retryLimit == 0 ? onLoad : { [weak self] in
3326
+ guard let self, self.bindingEpoch == expectedEpoch else { return }
3327
+ self.selectorImageRetries.removeValue(forKey: imageID)?.cancel()
3328
+ onLoad?()
3329
+ },
3330
+ onError: retryLimit == 0 ? onError : { [weak self, weak imageView] in
3331
+ guard let self, self.bindingEpoch == expectedEpoch, let imageView else { return }
3332
+ guard retryAttempt < retryLimit else { onError?(); return }
3333
+ guard self.selectorImageRetries[imageID] == nil else { return }
3334
+ let retry = DispatchWorkItem { [weak self, weak imageView] in
3335
+ guard let self, self.bindingEpoch == expectedEpoch, let imageView else { return }
3336
+ self.selectorImageRetries.removeValue(forKey: imageID)
3337
+ self.bindImage(source, into: imageView, token: token, slot: slot, variant: variant, onLoad: onLoad, onError: onError, retryAttempt: retryAttempt + 1)
3338
+ }
3339
+ self.selectorImageRetries[imageID] = retry
3340
+ DispatchQueue.main.asyncAfter(deadline: .now() + Double(Int.random(in: 0...2)), execute: retry)
3341
+ }
2750
3342
  )
2751
3343
  }
2752
3344
 
@@ -2837,12 +3429,17 @@ final class NativeListCell: UICollectionViewCell {
2837
3429
  source: String,
2838
3430
  slot: Int? = nil
2839
3431
  ) -> NativeListActionOrigin {
2840
- NativeListActionOrigin(
3432
+ let isAccountIcon = currentItem?.data.string("presentation") == "accountSelector"
3433
+ && source == "trailingAccessory"
3434
+ && accessoryButtons.contains { $0 === sourceView }
3435
+ && sourceView.bounds.width == 38
3436
+ return NativeListActionOrigin(
2841
3437
  sourceView: sourceView,
2842
3438
  ownerCell: self,
2843
3439
  bindingEpoch: bindingEpoch,
2844
3440
  source: source,
2845
- slot: slot
3441
+ slot: slot,
3442
+ anchorInset: isAccountIcon ? 7 : 0
2846
3443
  )
2847
3444
  }
2848
3445
 
@@ -2851,6 +3448,8 @@ final class NativeListCell: UICollectionViewCell {
2851
3448
  }
2852
3449
 
2853
3450
  private func invalidateCurrentBinding() {
3451
+ selectorImageRetries.values.forEach { $0.cancel() }
3452
+ selectorImageRetries.removeAll()
2854
3453
  guard currentItem != nil else { return }
2855
3454
  onBindingInvalidated?(self, bindingEpoch)
2856
3455
  bindingEpoch &+= 1