@onekeyfe/react-native-native-list 3.0.114 → 3.0.116

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.
@@ -4,6 +4,73 @@ import CoreText
4
4
  import OneKeyImage
5
5
  import UIKit
6
6
 
7
+ // Market/TokenListSkeleton: source geometry and the native Skeleton's 3s shimmer.
8
+ private final class NativeListMarketSkeleton: UIView {
9
+ private let marks = (0..<5).map { _ in UIView() }
10
+ private let gradients = (0..<5).map { _ in CAGradientLayer() }
11
+
12
+ init(background: UIColor) {
13
+ super.init(frame: .zero)
14
+ var white: CGFloat = 1
15
+ background.getWhite(&white, alpha: nil)
16
+ let base = UIColor(nativeListHex: white < 0.5 ? "#111111" : "#FAFAFA", fallback: .white)
17
+ let highlight = UIColor(nativeListHex: white < 0.5 ? "#333333" : "#CDCDCD", fallback: .lightGray)
18
+ for (index, mark) in marks.enumerated() {
19
+ mark.backgroundColor = base
20
+ mark.clipsToBounds = true
21
+ mark.layer.cornerRadius = index == 0 ? 16 : 8
22
+ let gradient = gradients[index]
23
+ gradient.cornerRadius = mark.layer.cornerRadius
24
+ gradient.colors = [base.cgColor, highlight.cgColor, base.cgColor]
25
+ gradient.locations = [0, 0.5, 1]
26
+ gradient.startPoint = CGPoint(x: 0, y: 0.5)
27
+ gradient.endPoint = CGPoint(x: 1, y: 0.5)
28
+ mark.layer.addSublayer(gradient)
29
+ addSubview(mark)
30
+ }
31
+ }
32
+
33
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
34
+
35
+ override func layoutSubviews() {
36
+ super.layoutSubviews()
37
+ let frames = [
38
+ CGRect(x: 0, y: 0, width: 32, height: 32),
39
+ CGRect(x: 44, y: 0, width: 80, height: 16),
40
+ CGRect(x: 44, y: 20, width: 60, height: 12),
41
+ CGRect(x: bounds.width - 168, y: 7, width: 80, height: 18),
42
+ CGRect(x: bounds.width - 80, y: 7, width: 80, height: 18),
43
+ ]
44
+ CATransaction.begin()
45
+ CATransaction.setDisableActions(true)
46
+ for (index, frame) in frames.enumerated() {
47
+ marks[index].frame = frame
48
+ gradients[index].frame = marks[index].bounds
49
+ }
50
+ CATransaction.commit()
51
+ updateAnimation()
52
+ }
53
+
54
+ override func didMoveToWindow() {
55
+ super.didMoveToWindow()
56
+ updateAnimation()
57
+ }
58
+
59
+ private func updateAnimation() {
60
+ for gradient in gradients {
61
+ guard window != nil else { gradient.removeAllAnimations(); continue }
62
+ guard gradient.bounds.width > 0, gradient.animation(forKey: "shimmer") == nil else { continue }
63
+ let animation = CABasicAnimation(keyPath: "transform.translation.x")
64
+ animation.fromValue = -gradient.bounds.width
65
+ animation.toValue = gradient.bounds.width
66
+ animation.duration = 3
67
+ animation.repeatCount = .infinity
68
+ animation.timingFunction = CAMediaTimingFunction(name: .linear)
69
+ gradient.add(animation, forKey: "shimmer")
70
+ }
71
+ }
72
+ }
73
+
7
74
  final class NativeListActionOrigin {
8
75
  weak var sourceView: UIView?
9
76
  weak var ownerCell: NativeListCell?
@@ -12,6 +79,7 @@ final class NativeListActionOrigin {
12
79
  let slot: Int?
13
80
  // OneKey patch: expose the layout slot while preserving the larger hit target.
14
81
  let anchorInset: CGFloat
82
+ var windowPoint: CGPoint?
15
83
 
16
84
  init(
17
85
  sourceView: UIView,
@@ -36,13 +104,17 @@ private final class NativeListAccessoryButton: UIButton {
36
104
  didSet { invalidateIntrinsicContentSize(); setNeedsLayout() }
37
105
  }
38
106
 
107
+ var marketLineHeight: CGFloat? {
108
+ didSet { invalidateIntrinsicContentSize(); setNeedsLayout() }
109
+ }
110
+
39
111
  private var sourcePixelScale: CGFloat {
40
112
  max(1, window?.screen.scale ?? traitCollection.displayScale)
41
113
  }
42
114
 
43
115
  override var intrinsicContentSize: CGSize {
44
116
  var size = super.intrinsicContentSize
45
- guard selectorSummaryLineHeight != nil, let title = attributedTitle(for: .normal) else { return size }
117
+ guard selectorSummaryLineHeight != nil || marketLineHeight != nil, let title = attributedTitle(for: .normal) else { return size }
46
118
  let width = title.boundingRect(
47
119
  with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
48
120
  options: [.usesLineFragmentOrigin, .usesFontLeading],
@@ -54,6 +126,21 @@ private final class NativeListAccessoryButton: UIButton {
54
126
 
55
127
  override func layoutSubviews() {
56
128
  super.layoutSubviews()
129
+ if let lineHeight = marketLineHeight, let titleLabel, let title = attributedTitle(for: .normal) {
130
+ let measured = title.boundingRect(
131
+ with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
132
+ options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil
133
+ ).width
134
+ let width = min(bounds.width, ceil(measured * sourcePixelScale) / sourcePixelScale)
135
+ let x = contentHorizontalAlignment == .trailing ? bounds.width - width
136
+ : contentHorizontalAlignment == .leading ? 0 : (bounds.width - width) / 2
137
+ titleLabel.frame = CGRect(
138
+ x: floor(x * sourcePixelScale) / sourcePixelScale,
139
+ y: (bounds.height - lineHeight) / 2,
140
+ width: width, height: lineHeight
141
+ )
142
+ return
143
+ }
57
144
  guard let lineHeight = selectorSummaryLineHeight, let titleLabel else { return }
58
145
  // OneKey patch: position the final source line box after UIKit has measured the button.
59
146
  let top = ceil((bounds.height - lineHeight) / 2 * sourcePixelScale) / sourcePixelScale
@@ -357,11 +444,18 @@ final class NativeListCell: UICollectionViewCell {
357
444
  private let titleRowStack = UIStackView()
358
445
  private let titleLabel = NativeListDottedUnderlineLabel()
359
446
  private let subtitleLabel = UILabel()
447
+ // OneKey patch: keep Market name and volume in independent line boxes.
448
+ private let marketSubtitleStack = UIStackView()
449
+ private let marketSubtitleSpacer = UIView()
360
450
  private let tertiaryLabel = UILabel()
361
451
  private let statusLabel = NativeListInsetLabel()
362
452
  private let metricSubtitleLabel = UILabel()
363
453
  private let metricCompositeStack = UIStackView()
364
454
  private let badgeLabel = NativeListInsetLabel()
455
+ // OneKey patch: reuse the existing explicit line-box layout for styled Market badges.
456
+ // private let marketBadgeButtons = (0..<3).map { _ in UIButton(type: .system) }
457
+ private let marketBadgeButtons = (0..<3).map { _ in NativeListAccessoryButton(type: .system) }
458
+ private let marketBadgeImages = (0..<3).map { _ in OneKeyImageReusableView(frame: .zero) }
365
459
  private let actionStack = UIStackView()
366
460
  private let actionButtons = (0..<3).map { _ in UIButton(type: .system) }
367
461
  private let trailingStack = UIStackView()
@@ -412,6 +506,7 @@ final class NativeListCell: UICollectionViewCell {
412
506
  private var boundCheckboxData: [String: Any]?
413
507
  private var boundCheckboxTarget: NativeSelectionTarget?
414
508
  private var leadingActionKey: String?
509
+ private var marketBadgeActionKeys: [String?] = []
415
510
  private var restingBackgroundColor: UIColor = .clear
416
511
  private var pressedBackgroundColor = UIColor(
417
512
  nativeListHex: "#E8E8E8",
@@ -612,6 +707,23 @@ final class NativeListCell: UICollectionViewCell {
612
707
  titleRowStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
613
708
  titleRowStack.addArrangedSubview(titleLabel)
614
709
  titleRowStack.addArrangedSubview(badgeLabel)
710
+ marketBadgeButtons.enumerated().forEach { index, button in
711
+ button.tag = index
712
+ button.addTarget(self, action: #selector(marketBadgePressed(_:)), for: .touchUpInside)
713
+ button.titleLabel?.font = nativeListFont(ofSize: 11, weight: .medium)
714
+ button.layer.cornerRadius = 4
715
+ button.clipsToBounds = true
716
+ let image = marketBadgeImages[index]
717
+ image.isUserInteractionEnabled = false
718
+ image.translatesAutoresizingMaskIntoConstraints = false
719
+ button.addSubview(image)
720
+ NSLayoutConstraint.activate([
721
+ image.centerYAnchor.constraint(equalTo: button.centerYAnchor),
722
+ image.widthAnchor.constraint(equalToConstant: 14),
723
+ image.heightAnchor.constraint(equalToConstant: 14),
724
+ ])
725
+ titleRowStack.addArrangedSubview(button)
726
+ }
615
727
  titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
616
728
  titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
617
729
  badgeLabel.setContentHuggingPriority(.required, for: .horizontal)
@@ -691,6 +803,19 @@ final class NativeListCell: UICollectionViewCell {
691
803
 
692
804
  override func layoutSubviews() {
693
805
  super.layoutSubviews()
806
+ if let item = currentItem, item.type == "system", item.data.string("presentation") == "market",
807
+ ["noMatch", "retry"].contains(item.data.string("variant")), !titleLabel.isHidden {
808
+ // The cell content view owns the root constraints and must settle before reading descendants.
809
+ contentView.layoutIfNeeded()
810
+ // React Native floors text origins to physical pixels after centering the line box.
811
+ titleLabel.transform = .identity
812
+ let scale = max(1, window?.screen.scale ?? traitCollection.displayScale)
813
+ let origin = titleLabel.convert(titleLabel.bounds, to: contentView).origin
814
+ let x = floor((contentView.bounds.width - titleLabel.bounds.width) / 2 * scale) / scale
815
+ let contentHeight: CGFloat = item.data.string("variant") == "retry" ? 56 : 24
816
+ let y = floor(max(32, (contentView.bounds.height - contentHeight) / 2) * scale) / scale
817
+ titleLabel.transform = CGAffineTransform(translationX: x - origin.x, y: y - origin.y)
818
+ }
694
819
  // OneKey patch: extend only the background across the section list outer inset.
695
820
  if currentItem?.data.bool("backgroundFullWidth") == true {
696
821
  selectorFullWidthBackground.frame = CGRect(x: -frame.minX, y: 0, width: superview?.bounds.width ?? bounds.width, height: bounds.height)
@@ -843,6 +968,7 @@ final class NativeListCell: UICollectionViewCell {
843
968
  case "activity": bindActivity(item, theme: theme)
844
969
  case "message": bindMessage(item, theme: theme)
845
970
  case "dataRow": bindDataRow(item, theme: theme, checkboxState)
971
+ case "market": bindMarket(item, theme: theme)
846
972
  case "mediaTile": bindMediaTile(item, theme: theme)
847
973
  case "metricCard": bindMetricCard(item, theme: theme)
848
974
  case "sectionHeader": bindSectionHeader(item, theme: theme, layout: layout, checkboxState)
@@ -1033,6 +1159,7 @@ final class NativeListCell: UICollectionViewCell {
1033
1159
  }
1034
1160
 
1035
1161
  private func reset() {
1162
+ titleLabel.transform = .identity
1036
1163
  restoreSelectorTypography()
1037
1164
  // OneKey patch: remove selector decorations before rebinding recycled cells.
1038
1165
  selectorViews.forEach { $0.removeFromSuperview() }
@@ -1041,6 +1168,7 @@ final class NativeListCell: UICollectionViewCell {
1041
1168
  selectorConstraints.removeAll()
1042
1169
  selectorImages.forEach { $0.prepareForReuse() }
1043
1170
  selectorImages.removeAll()
1171
+ marketBadgeImages.forEach { $0.prepareForReuse(); $0.isHidden = true }
1044
1172
  selectorFullWidthBackground.removeFromSuperlayer()
1045
1173
  selectorBorder?.removeFromSuperlayer()
1046
1174
  selectorBorder = nil
@@ -1094,6 +1222,15 @@ final class NativeListCell: UICollectionViewCell {
1094
1222
  headerValueIconImageView.removeFromSuperview()
1095
1223
  headerValueIconImageView.image = nil
1096
1224
  headerValueIconImageView.isHidden = true
1225
+ // OneKey patch: restore the shared labels before any recycled row binds.
1226
+ marketSubtitleStack.arrangedSubviews.forEach {
1227
+ marketSubtitleStack.removeArrangedSubview($0)
1228
+ $0.removeFromSuperview()
1229
+ }
1230
+ mainStack.removeArrangedSubview(marketSubtitleStack)
1231
+ marketSubtitleStack.removeFromSuperview()
1232
+ mainStack.removeArrangedSubview(tertiaryLabel)
1233
+ tertiaryLabel.removeFromSuperview()
1097
1234
  mediaMetadataStack.removeArrangedSubview(subtitleLabel)
1098
1235
  mediaMetadataStack.removeArrangedSubview(mediaNetworkImage)
1099
1236
  mediaMetadataStack.removeFromSuperview()
@@ -1103,6 +1240,7 @@ final class NativeListCell: UICollectionViewCell {
1103
1240
  subtitleLabel.removeFromSuperview()
1104
1241
  mainStack.insertArrangedSubview(titleRowStack, at: 0)
1105
1242
  mainStack.insertArrangedSubview(subtitleLabel, at: 1)
1243
+ mainStack.insertArrangedSubview(tertiaryLabel, at: 2)
1106
1244
  leadingWidth.constant = 40
1107
1245
  leadingHeight.constant = 40
1108
1246
  leadingIconWidth.constant = 18
@@ -1123,6 +1261,7 @@ final class NativeListCell: UICollectionViewCell {
1123
1261
  $0.isHidden = true
1124
1262
  $0.alpha = 1
1125
1263
  $0.layer.cornerRadius = 0
1264
+ $0.layer.mask = nil
1126
1265
  }
1127
1266
  leadingContainer.clipsToBounds = true
1128
1267
  leadingContainer.layer.borderWidth = 0
@@ -1156,6 +1295,13 @@ final class NativeListCell: UICollectionViewCell {
1156
1295
  titleLabel.textAlignment = .natural
1157
1296
  subtitleLabel.text = nil
1158
1297
  subtitleLabel.lineBreakMode = .byTruncatingTail
1298
+ subtitleLabel.attributedText = nil
1299
+ subtitleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
1300
+ subtitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
1301
+ tertiaryLabel.attributedText = nil
1302
+ tertiaryLabel.lineBreakMode = .byTruncatingTail
1303
+ tertiaryLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
1304
+ tertiaryLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
1159
1305
  tertiaryLabel.text = nil
1160
1306
  statusLabel.text = nil
1161
1307
  statusLabel.topInset = 0
@@ -1178,6 +1324,28 @@ final class NativeListCell: UICollectionViewCell {
1178
1324
  badgeLabel.backgroundColor = .clear
1179
1325
  badgeLabel.layer.cornerRadius = 0
1180
1326
  badgeLabel.clipsToBounds = false
1327
+ marketBadgeButtons.forEach {
1328
+ $0.isHidden = true
1329
+ $0.isEnabled = true
1330
+ $0.isUserInteractionEnabled = false
1331
+ // OneKey patch: a recycled badge must not retain an attributed title or font.
1332
+ $0.setAttributedTitle(nil, for: .normal)
1333
+ $0.marketLineHeight = nil
1334
+ $0.selectorSummaryLineHeight = nil
1335
+ $0.titleLabel?.font = nativeListFont(ofSize: 11, weight: .medium)
1336
+ $0.titleLabel?.numberOfLines = 1
1337
+ $0.contentHorizontalAlignment = .center
1338
+ $0.accessibilityLabel = nil
1339
+ $0.accessibilityTraits = .staticText
1340
+ $0.setTitle(nil, for: .normal)
1341
+ $0.setImage(nil, for: .normal)
1342
+ $0.setTitleColor(nil, for: .normal)
1343
+ $0.tintColor = nil
1344
+ $0.backgroundColor = .clear
1345
+ $0.contentEdgeInsets = .zero
1346
+ $0.imageEdgeInsets = .zero
1347
+ $0.titleEdgeInsets = .zero
1348
+ }
1181
1349
  [titleLabel, subtitleLabel, tertiaryLabel, statusLabel, metricSubtitleLabel, badgeLabel, actionStack]
1182
1350
  .forEach { $0.isHidden = true }
1183
1351
  separatorView.isHidden = true
@@ -1188,7 +1356,9 @@ final class NativeListCell: UICollectionViewCell {
1188
1356
  $0.backgroundColor = .clear
1189
1357
  }
1190
1358
  accessoryButtons.enumerated().forEach { index, button in
1359
+ button.isUserInteractionEnabled = true
1191
1360
  button.selectorSummaryLineHeight = nil
1361
+ button.marketLineHeight = nil
1192
1362
  button.titleLabel?.font = nativeListFont(
1193
1363
  ofSize: index == 0 ? 16 : 14,
1194
1364
  weight: index == 0 ? .medium : .regular
@@ -1233,6 +1403,7 @@ final class NativeListCell: UICollectionViewCell {
1233
1403
  boundCheckboxData = nil
1234
1404
  boundCheckboxTarget = nil
1235
1405
  leadingActionKey = nil
1406
+ marketBadgeActionKeys = []
1236
1407
  layer.maskedCorners = []
1237
1408
  layer.cornerRadius = 0
1238
1409
  layer.cornerCurve = .circular
@@ -2014,6 +2185,347 @@ final class NativeListCell: UICollectionViewCell {
2014
2185
  }
2015
2186
  }
2016
2187
 
2188
+ private func marketFontWeight(_ value: String, fallback: NativeListFontWeight) -> NativeListFontWeight {
2189
+ switch value {
2190
+ case "regular": return .regular
2191
+ case "semibold": return .semibold
2192
+ case "bold": return .bold
2193
+ case "medium": return .medium
2194
+ default: return fallback
2195
+ }
2196
+ }
2197
+
2198
+ private func marketTextAlignment(_ value: String) -> NSTextAlignment {
2199
+ switch value {
2200
+ case "center": return .center
2201
+ case "start": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .right : .left
2202
+ case "end": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .left : .right
2203
+ default: return .natural
2204
+ }
2205
+ }
2206
+
2207
+ private func applyMarketTextStyle(
2208
+ _ label: UILabel,
2209
+ data: [String: Any]?,
2210
+ theme: [String: Any]?,
2211
+ defaultSize: CGFloat,
2212
+ defaultLineHeight: CGFloat,
2213
+ defaultWeight: NativeListFontWeight,
2214
+ defaultColor: UIColor,
2215
+ defaultAlignment: NSTextAlignment = .natural
2216
+ ) {
2217
+ let size = CGFloat(data?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
2218
+ let lineHeight = CGFloat(data?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
2219
+ label.font = nativeListTabularFont(ofSize: size, weight: marketFontWeight(data?.string("fontWeight") ?? "", fallback: defaultWeight))
2220
+ label.textColor = data?["color"].flatMap { $0 as? String }.map { UIColor(nativeListHex: $0, fallback: defaultColor) } ?? defaultColor
2221
+ label.textAlignment = data?["alignment"] == nil
2222
+ ? defaultAlignment
2223
+ : marketTextAlignment(data?.string("alignment") ?? "")
2224
+ label.numberOfLines = min(2, max(1, data?.int("lines", default: 1) ?? 1))
2225
+ setLineHeight(label, text: label.text ?? "", lineHeight: lineHeight)
2226
+ }
2227
+
2228
+ private func applyMarketButtonStyle(
2229
+ _ button: UIButton,
2230
+ data: [String: Any]?,
2231
+ defaultSize: CGFloat,
2232
+ defaultLineHeight: CGFloat,
2233
+ defaultWeight: NativeListFontWeight,
2234
+ color: UIColor,
2235
+ defaultAlignment: UIControl.ContentHorizontalAlignment
2236
+ ) {
2237
+ let size = CGFloat(data?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
2238
+ let lineHeight = CGFloat(data?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
2239
+ setButtonLine(
2240
+ button,
2241
+ text: button.title(for: .normal) ?? "",
2242
+ font: nativeListTabularFont(ofSize: size, weight: marketFontWeight(data?.string("fontWeight") ?? "", fallback: defaultWeight)),
2243
+ color: data?["color"].flatMap { $0 as? String }.map { UIColor(nativeListHex: $0, fallback: color) } ?? color,
2244
+ lineHeight: lineHeight
2245
+ )
2246
+ if data?["alignment"] == nil {
2247
+ button.contentHorizontalAlignment = defaultAlignment
2248
+ } else {
2249
+ button.contentHorizontalAlignment = data?.string("alignment") == "start"
2250
+ ? .leading
2251
+ : data?.string("alignment") == "end" ? .trailing : .center
2252
+ }
2253
+ button.titleLabel?.numberOfLines = min(2, max(1, data?.int("lines", default: 1) ?? 1))
2254
+ }
2255
+
2256
+ private func marketAttributedText(
2257
+ _ text: String,
2258
+ segments: [[String: Any]],
2259
+ style: [String: Any]?,
2260
+ defaultSize: CGFloat,
2261
+ defaultLineHeight: CGFloat,
2262
+ defaultWeight: NativeListFontWeight,
2263
+ color: UIColor,
2264
+ defaultAlignment: NSTextAlignment
2265
+ ) -> NSAttributedString {
2266
+ let size = CGFloat(style?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
2267
+ let lineHeight = CGFloat(style?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
2268
+ let paragraph = NSMutableParagraphStyle()
2269
+ paragraph.minimumLineHeight = lineHeight
2270
+ paragraph.maximumLineHeight = lineHeight
2271
+ paragraph.alignment = style?["alignment"] == nil
2272
+ ? defaultAlignment
2273
+ : marketTextAlignment(style?.string("alignment") ?? "")
2274
+ let weight = marketFontWeight(style?.string("fontWeight") ?? "", fallback: defaultWeight)
2275
+ let resolvedColor = style?["color"].flatMap { $0 as? String }.map { UIColor(nativeListHex: $0, fallback: color) } ?? color
2276
+ let result = NSMutableAttributedString(string: "")
2277
+ let source = segments.isEmpty ? [["text": text]] : segments
2278
+ for segment in source {
2279
+ let segmentSize = segment.string("style") == "subscript" ? ceil(size * 0.6) : size
2280
+ result.append(NSAttributedString(string: segment.string("text"), attributes: [
2281
+ .font: nativeListTabularFont(ofSize: segmentSize, weight: weight),
2282
+ .foregroundColor: resolvedColor,
2283
+ .kern: 0,
2284
+ .paragraphStyle: paragraph,
2285
+ .baselineOffset: max(0, (lineHeight - nativeListTabularFont(ofSize: size, weight: weight).lineHeight) / 2),
2286
+ ]))
2287
+ }
2288
+ return result
2289
+ }
2290
+
2291
+ private func marketLeading(_ item: NativeListItem, style: [String: Any]?) -> [String: Any]? {
2292
+ guard var visual = item.data.dictionary("leading") else { return nil }
2293
+ guard let imageStyle = style?.dictionary("image") else { return visual }
2294
+ if let shape = imageStyle["shape"] as? String { visual["shape"] = shape }
2295
+ if let contentFit = imageStyle["contentFit"] as? String, var image = visual.dictionary("image") {
2296
+ image["contentFit"] = contentFit
2297
+ visual["image"] = image
2298
+ }
2299
+ return visual
2300
+ }
2301
+
2302
+ private func bindMarket(_ item: NativeListItem, theme: [String: Any]?) {
2303
+ let variant = item.data.string("variant")
2304
+ let style = item.data.dictionary("style")
2305
+ let imageStyle = style?.dictionary("image")
2306
+ let imageWidth = CGFloat(imageStyle?.double("width", default: variant == "stock" ? 40 : 32) ?? (variant == "stock" ? 40 : 32))
2307
+ let imageHeight = CGFloat(imageStyle?.double("height", default: variant == "stock" ? 40 : 32) ?? (variant == "stock" ? 40 : 32))
2308
+ let horizontalPadding = CGFloat(style?.double("horizontalPadding", default: variant == "perp" ? 16 : 20) ?? (variant == "perp" ? 16 : 20))
2309
+ let verticalPadding = CGFloat(style?.double("verticalPadding", default: 12) ?? 12)
2310
+ rootLeadingConstraint.constant = horizontalPadding
2311
+ rootTrailingConstraint.constant = -horizontalPadding
2312
+ rootTopConstraint.constant = verticalPadding
2313
+ rootBottomConstraint.constant = -verticalPadding
2314
+ rootStack.spacing = CGFloat(style?.double("leadingGap", default: variant == "perp" ? 8 : 14) ?? (variant == "perp" ? 8 : 14))
2315
+ leadingWidth.constant = imageWidth
2316
+ leadingHeight.constant = imageHeight
2317
+ if let visual = marketLeading(item, style: style) {
2318
+ addLeading(visual, key: item.key)
2319
+ let radius = CGFloat(imageStyle?.double("cornerRadius", default: imageStyle?.string("shape") == "square" ? 0 : imageStyle?.string("shape") == "rounded" ? 8 : Double(min(imageWidth, imageHeight) / 2)) ?? Double(min(imageWidth, imageHeight) / 2))
2320
+ leadingContainer.layer.cornerRadius = radius
2321
+ leadingImages.first?.layer.cornerRadius = radius
2322
+ if let image = leadingImages.first, !visual.string("borderColor").isEmpty {
2323
+ // Match Token's border box: the bitmap occupies the one-point inset.
2324
+ leadingContainer.layer.borderWidth = 1
2325
+ leadingContainer.layer.borderColor = UIColor(nativeListHex: visual.string("borderColor"), fallback: .clear).cgColor
2326
+ for constraint in leadingSlotConstraints where constraint.firstItem === image {
2327
+ switch constraint.firstAttribute {
2328
+ case .leading, .top: constraint.constant = 1
2329
+ case .trailing, .bottom: constraint.constant = -1
2330
+ default: break
2331
+ }
2332
+ }
2333
+ image.layer.cornerRadius = 0
2334
+ let mask = CAShapeLayer()
2335
+ mask.path = UIBezierPath(roundedRect: CGRect(x: -1, y: -1, width: imageWidth, height: imageHeight), cornerRadius: radius).cgPath
2336
+ image.layer.mask = mask
2337
+ }
2338
+ if let diagnostic = item.data.dictionary("diagnostics")?.string("imageBindActionKey"), !diagnostic.isEmpty,
2339
+ visual.dictionary("image") != nil || visual.dictionary("networkImage") != nil {
2340
+ onAction?(item, diagnostic, nil, nil)
2341
+ }
2342
+ }
2343
+ rootStack.addArrangedSubview(mainStack)
2344
+ // OneKey patch: opt in to the source Market row's content gap.
2345
+ // rootStack.setCustomSpacing(0, after: mainStack)
2346
+ rootStack.setCustomSpacing(CGFloat(style?.double("contentTrailingGap", default: 0) ?? 0), after: mainStack)
2347
+ mainStack.spacing = CGFloat(style?.double("lineGap", default: 0) ?? 0)
2348
+ titleRowStack.spacing = CGFloat(style?.double("titleBadgeGap", default: 4) ?? 4)
2349
+ // OneKey patch: opt in without changing the other row templates' filled layout.
2350
+ if style?.string("titleBadgeLayout") == "inline" {
2351
+ mainStack.alignment = .leading
2352
+ titleRowStack.setContentHuggingPriority(.required, for: .horizontal)
2353
+ }
2354
+ show(titleLabel, item.data.string("title"), lines: style?.dictionary("title")?.int("lines", default: 1) ?? 1)
2355
+ applyMarketTextStyle(titleLabel, data: style?.dictionary("title"), theme: theme, defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, defaultColor: nativeListColor(theme, "primaryText", "#202020"))
2356
+ let badges = Array(item.data.dictionaries("badges").prefix(marketBadgeButtons.count))
2357
+ marketBadgeActionKeys = badges.map { $0["actionKey"] as? String }
2358
+ for (index, badge) in badges.enumerated() {
2359
+ let button = marketBadgeButtons[index]
2360
+ let badgeStyle = badge.dictionary("style")
2361
+ let badgeFontSize = CGFloat(badgeStyle?.double("fontSize", default: 11) ?? 11)
2362
+ let badgeFontWeight = marketFontWeight(badgeStyle?.string("fontWeight") ?? "", fallback: .medium)
2363
+ // OneKey patch: SizableText supplies tabular numerals for explicit Market metrics.
2364
+ let badgeFont = badgeStyle == nil
2365
+ ? nativeListFont(ofSize: badgeFontSize, weight: badgeFontWeight)
2366
+ : nativeListTabularFont(ofSize: badgeFontSize, weight: badgeFontWeight)
2367
+ let hasBuiltInIcon = badge.string("iconName") == "verified"
2368
+ let hasRemoteIcon = badge.dictionary("icon") != nil
2369
+ let hasIcon = hasBuiltInIcon || hasRemoteIcon
2370
+ let text = badge.string("text")
2371
+ let toneColor: UIColor
2372
+ switch badge.string("tone") {
2373
+ case "success": toneColor = nativeListColor(theme, "positive", "#218358")
2374
+ case "danger": toneColor = nativeListColor(theme, "negative", "#CE2C31")
2375
+ case "info": toneColor = nativeListColor(theme, "info", "#0D74CE")
2376
+ case "warning": toneColor = nativeListColor(theme, "primaryText", "#202020")
2377
+ default: toneColor = nativeListColor(theme, "secondaryText", "#646464")
2378
+ }
2379
+ let foreground = UIColor(
2380
+ nativeListHex: badge.string("textColor", default: ""),
2381
+ fallback: toneColor
2382
+ )
2383
+ button.isHidden = false
2384
+ button.isEnabled = true
2385
+ button.isUserInteractionEnabled = !badge.string("actionKey").isEmpty
2386
+ button.accessibilityTraits = button.isUserInteractionEnabled ? .button : .staticText
2387
+ button.setTitle(text, for: .normal)
2388
+ button.setTitleColor(foreground, for: .normal)
2389
+ button.titleLabel?.font = badgeFont
2390
+ if let lineHeight = badgeStyle?["lineHeight"] as? Double {
2391
+ setButtonLine(button, text: text, font: badgeFont, color: foreground, lineHeight: CGFloat(lineHeight))
2392
+ // The explicit text-only line box must not cover an adjacent icon.
2393
+ if hasIcon { button.marketLineHeight = nil }
2394
+ }
2395
+ button.tintColor = foreground
2396
+ button.backgroundColor = UIColor(
2397
+ nativeListHex: badge.string("backgroundColor", default: ""),
2398
+ fallback: hasIcon && text.isEmpty
2399
+ ? .clear
2400
+ : nativeListColor(theme, "strongBackground", "#0000000F")
2401
+ )
2402
+ let iconOnly = hasIcon && text.isEmpty
2403
+ let iconSize: CGFloat = hasBuiltInIcon ? 16 : 14
2404
+ // OneKey patch: preserve the native defaults unless the caller supplies padding.
2405
+ let padding = CGFloat(badgeStyle?.double("horizontalPadding", default: 5) ?? 5)
2406
+ let hasCustomPadding = badgeStyle?["horizontalPadding"] != nil
2407
+ let leftPadding = hasCustomPadding ? padding + (hasRemoteIcon ? iconSize + 2 : 0) : hasRemoteIcon ? 20 : hasIcon ? 3 : 5
2408
+ // button.contentEdgeInsets = UIEdgeInsets(top: 0, left: iconOnly ? 0 : hasRemoteIcon ? 20 : hasIcon ? 3 : 5, bottom: 0, right: iconOnly ? 0 : 5)
2409
+ button.contentEdgeInsets = UIEdgeInsets(top: 0, left: iconOnly ? 0 : leftPadding, bottom: 0, right: iconOnly ? 0 : padding)
2410
+ button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: text.isEmpty ? 0 : 3)
2411
+ button.titleEdgeInsets = .zero
2412
+ button.imageView?.contentMode = .scaleAspectFit
2413
+ if hasBuiltInIcon {
2414
+ button.setImage(
2415
+ nativeListIcon(named: "BadgeVerifiedSolid", size: CGSize(width: iconSize, height: iconSize)),
2416
+ for: .normal
2417
+ )
2418
+ }
2419
+ // OneKey patch: match source badge metrics at physical-pixel precision.
2420
+ // let height = button.heightAnchor.constraint(equalToConstant: 18)
2421
+ let height = button.heightAnchor.constraint(equalToConstant: CGFloat(badgeStyle?.double("height", default: 18) ?? 18))
2422
+ let textWidth = (text as NSString).size(withAttributes: [.font: badgeFont]).width
2423
+ let scale = max(1, traitCollection.displayScale)
2424
+ let roundedTextWidth = badgeStyle == nil ? ceil(textWidth) : ceil(textWidth * scale) / scale
2425
+ let extraWidth = hasCustomPadding ? padding * 2 + (hasIcon ? iconSize + 2 : 0) : hasIcon ? iconSize + 11 : 10
2426
+ // let width = button.widthAnchor.constraint(equalToConstant: iconOnly ? iconSize : ceil(textWidth) + (hasIcon ? iconSize + 11 : 10))
2427
+ let width = button.widthAnchor.constraint(equalToConstant: iconOnly ? iconSize : roundedTextWidth + extraWidth)
2428
+ NSLayoutConstraint.activate([width, height])
2429
+ selectorConstraints.append(contentsOf: [width, height])
2430
+ if let icon = badge.dictionary("icon") {
2431
+ marketBadgeImages[index].isHidden = false
2432
+ marketBadgeImages[index].layer.cornerRadius = 7
2433
+ marketBadgeImages[index].clipsToBounds = true
2434
+ let imageLeading = marketBadgeImages[index].leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: text.isEmpty ? 0 : 4)
2435
+ imageLeading.isActive = true
2436
+ selectorConstraints.append(imageLeading)
2437
+ bindImage(icon, into: marketBadgeImages[index], token: item.key, slot: 20 + index, variant: "generic")
2438
+ }
2439
+ button.accessibilityLabel = badge.string("accessibilityLabel", default: badge.string("text"))
2440
+ }
2441
+ if !item.data.string("subtitle").isEmpty || !item.data.dictionaries("subtitleSegments").isEmpty {
2442
+ show(subtitleLabel, item.data.string("subtitle"), lines: style?.dictionary("subtitle")?.int("lines", default: 1) ?? 1)
2443
+ applyMarketTextStyle(subtitleLabel, data: style?.dictionary("subtitle"), theme: theme, defaultSize: 14, defaultLineHeight: 20, defaultWeight: .regular, defaultColor: nativeListColor(theme, "secondaryText", "#646464"))
2444
+ if !item.data.dictionaries("subtitleSegments").isEmpty {
2445
+ subtitleLabel.attributedText = marketAttributedText(item.data.string("subtitle"), segments: item.data.dictionaries("subtitleSegments"), style: style?.dictionary("subtitle"), defaultSize: 14, defaultLineHeight: 20, defaultWeight: .regular, color: nativeListColor(theme, "secondaryText", "#646464"), defaultAlignment: .natural)
2446
+ }
2447
+ }
2448
+ // OneKey patch: preserve volume width while the localized name truncates.
2449
+ let subtitlePrefix = item.data.dictionary("subtitlePrefix")
2450
+ let subtitlePadding = CGFloat(style?.double("subtitleTrailingPadding", default: 0) ?? 0)
2451
+ if subtitlePrefix != nil || subtitlePadding > 0 {
2452
+ mainStack.removeArrangedSubview(subtitleLabel)
2453
+ subtitleLabel.removeFromSuperview()
2454
+ mainStack.removeArrangedSubview(tertiaryLabel)
2455
+ tertiaryLabel.removeFromSuperview()
2456
+ marketSubtitleStack.axis = .horizontal
2457
+ marketSubtitleStack.alignment = .center
2458
+ marketSubtitleStack.spacing = 0
2459
+ marketSubtitleStack.clipsToBounds = true
2460
+ show(tertiaryLabel, subtitlePrefix?.string("text") ?? "", lines: 1)
2461
+ applyMarketTextStyle(tertiaryLabel, data: subtitlePrefix?.dictionary("style"), theme: theme, defaultSize: 12, defaultLineHeight: 16, defaultWeight: .regular, defaultColor: nativeListColor(theme, "secondaryText", "#646464"))
2462
+ tertiaryLabel.setContentHuggingPriority(.required, for: .horizontal)
2463
+ tertiaryLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
2464
+ subtitleLabel.setContentHuggingPriority(.required, for: .horizontal)
2465
+ subtitleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
2466
+ marketSubtitleStack.addArrangedSubview(tertiaryLabel)
2467
+ marketSubtitleStack.addArrangedSubview(subtitleLabel)
2468
+ marketSubtitleStack.addArrangedSubview(marketSubtitleSpacer)
2469
+ if !tertiaryLabel.isHidden && !subtitleLabel.isHidden {
2470
+ marketSubtitleStack.setCustomSpacing(CGFloat(subtitlePrefix?.double("gap", default: 4) ?? 4), after: tertiaryLabel)
2471
+ }
2472
+ mainStack.insertArrangedSubview(marketSubtitleStack, at: 1)
2473
+ let width = marketSubtitleStack.widthAnchor.constraint(equalTo: mainStack.widthAnchor, constant: -subtitlePadding)
2474
+ width.isActive = true
2475
+ selectorConstraints.append(width)
2476
+ if let maxWidth = subtitlePrefix?["maxWidth"] as? Double {
2477
+ let limit = tertiaryLabel.widthAnchor.constraint(lessThanOrEqualToConstant: CGFloat(maxWidth))
2478
+ limit.isActive = true
2479
+ selectorConstraints.append(limit)
2480
+ }
2481
+ marketSubtitleStack.isHidden = tertiaryLabel.isHidden && subtitleLabel.isHidden
2482
+ }
2483
+ rootStack.addArrangedSubview(trailingStack)
2484
+ trailingStack.axis = .horizontal
2485
+ trailingStack.alignment = .center
2486
+ trailingStack.spacing = CGFloat(style?.double("trailingGap", default: 8) ?? 8)
2487
+ updateMarketQuote(item, theme: theme)
2488
+ }
2489
+
2490
+ func updateMarketQuote(_ item: NativeListItem, theme: [String: Any]?) {
2491
+ guard currentItem?.key == item.key, item.type == "market" else { return }
2492
+ currentItem = item
2493
+ NSLayoutConstraint.deactivate(accessorySizeConstraints)
2494
+ accessorySizeConstraints.removeAll()
2495
+ let style = item.data.dictionary("style")
2496
+ let price = accessoryButtons[0]
2497
+ price.isUserInteractionEnabled = false
2498
+ price.isHidden = false
2499
+ price.setAttributedTitle(nil, for: .normal)
2500
+ price.setTitle(item.data.string("price"), for: .normal)
2501
+ applyMarketButtonStyle(price, data: style?.dictionary("price"), defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, color: nativeListColor(theme, "primaryText", "#202020"), defaultAlignment: .trailing)
2502
+ if !item.data.dictionaries("priceSegments").isEmpty {
2503
+ price.setAttributedTitle(marketAttributedText(item.data.string("price"), segments: item.data.dictionaries("priceSegments"), style: style?.dictionary("price"), defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, color: nativeListColor(theme, "primaryText", "#202020"), defaultAlignment: marketTextAlignment("end")), for: .normal)
2504
+ }
2505
+ let changeData = item.data.dictionary("change") ?? [:]
2506
+ let change = accessoryButtons[1]
2507
+ change.isUserInteractionEnabled = false
2508
+ change.isHidden = false
2509
+ change.setAttributedTitle(nil, for: .normal)
2510
+ change.setTitle(changeData.string("text"), for: .normal)
2511
+ let defaultChangeColor = nativeListColor(theme, "inverseText", "#FFFFFF")
2512
+ applyMarketButtonStyle(change, data: style?.dictionary("change"), defaultSize: 14, defaultLineHeight: 20, defaultWeight: .medium, color: UIColor(nativeListHex: changeData.string("textColor", default: "#FFFFFF"), fallback: defaultChangeColor), defaultAlignment: .center)
2513
+ if !changeData.dictionaries("textSegments").isEmpty {
2514
+ let changeTextColor = UIColor(nativeListHex: changeData.string("textColor", default: ""), fallback: defaultChangeColor)
2515
+ change.setAttributedTitle(marketAttributedText(changeData.string("text"), segments: changeData.dictionaries("textSegments"), style: style?.dictionary("change"), defaultSize: 14, defaultLineHeight: 20, defaultWeight: .medium, color: changeTextColor, defaultAlignment: .center), for: .normal)
2516
+ }
2517
+ let toneKey = changeData.string("tone") == "positive" ? "positive" : changeData.string("tone") == "negative" ? "negative" : "secondaryText"
2518
+ change.backgroundColor = UIColor(nativeListHex: changeData.string("backgroundColor", default: ""), fallback: nativeListColor(theme, toneKey, changeData.string("tone") == "positive" ? "#218358" : changeData.string("tone") == "negative" ? "#CE2C31" : "#8D8D8D"))
2519
+ change.layer.cornerRadius = CGFloat(style?.double("changeCornerRadius", default: 8) ?? 8)
2520
+ change.clipsToBounds = true
2521
+ let width = change.widthAnchor.constraint(equalToConstant: CGFloat(style?.double("changeWidth", default: 80) ?? 80))
2522
+ let height = change.heightAnchor.constraint(equalToConstant: CGFloat(style?.double("changeHeight", default: 32) ?? 32))
2523
+ width.isActive = true
2524
+ height.isActive = true
2525
+ accessorySizeConstraints.append(contentsOf: [width, height])
2526
+ accessibilityLabel = item.data.string("accessibilityLabel", default: [item.data.string("title"), item.data.string("subtitle"), item.data.string("price"), changeData.string("text")].filter { !$0.isEmpty }.joined(separator: ", "))
2527
+ }
2528
+
2017
2529
  private func bindMediaTile(_ item: NativeListItem, theme: [String: Any]?) {
2018
2530
  rootStack.axis = .vertical
2019
2531
  rootStack.alignment = .fill
@@ -2630,6 +3142,114 @@ final class NativeListCell: UICollectionViewCell {
2630
3142
  rootStack.alignment = .center
2631
3143
  rootStack.distribution = .fill
2632
3144
  let variant = item.data.string("variant")
3145
+ let isMarket = item.data.string("presentation") == "market"
3146
+ if isMarket {
3147
+ rootLeadingConstraint.constant = 20
3148
+ rootTrailingConstraint.constant = -20
3149
+ rootTopConstraint.constant = 12
3150
+ rootBottomConstraint.constant = -12
3151
+ }
3152
+ if isMarket && variant == "retry" {
3153
+ let message = item.data.string("message")
3154
+ let text = item.data.string("actionText", default: "Retry")
3155
+ rootStack.axis = .vertical
3156
+ // The source tertiary Button has -5 vertical margins around its 30pt frame.
3157
+ rootStack.spacing = 7
3158
+ rootLeadingConstraint.constant = 32
3159
+ rootTrailingConstraint.constant = -32
3160
+ let height = CGFloat(item.data.double("height", default: message.isEmpty ? 52 : 120))
3161
+ let top = message.isEmpty ? 11 : max(32, (height - 56) / 2)
3162
+ rootTopConstraint.constant = top
3163
+ rootBottomConstraint.constant = -(height - top - (message.isEmpty ? 30 : 61))
3164
+ if !message.isEmpty {
3165
+ show(titleLabel, message, lines: 2)
3166
+ titleLabel.font = nativeListTabularFont(ofSize: 16)
3167
+ titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
3168
+ titleLabel.textAlignment = .center
3169
+ setLineHeight(titleLabel, text: message, lineHeight: 24)
3170
+ rootStack.addArrangedSubview(mainStack)
3171
+ }
3172
+ showAccessory(0, text, action: (item.data.string("actionKey"), nil))
3173
+ let button = accessoryButtons[0]
3174
+ button.backgroundColor = .clear
3175
+ button.layer.cornerRadius = 15
3176
+ setButtonLine(button, text: text, font: nativeListTabularFont(ofSize: 14, weight: .medium),
3177
+ color: nativeListColor(theme, "secondaryText", "#646464"), lineHeight: 20)
3178
+ let textWidth = button.intrinsicContentSize.width
3179
+ selectorConstraints.append(contentsOf: [
3180
+ button.widthAnchor.constraint(equalToConstant: textWidth + 18),
3181
+ button.heightAnchor.constraint(equalToConstant: 30),
3182
+ ])
3183
+ NSLayoutConstraint.activate(selectorConstraints)
3184
+ rootStack.addArrangedSubview(trailingStack)
3185
+ return
3186
+ }
3187
+ if variant == "loading" && item.data.string("loadingStyle") == "skeleton" {
3188
+ rootLeadingConstraint.constant = 20
3189
+ rootTrailingConstraint.constant = -20
3190
+ rootTopConstraint.constant = 12
3191
+ rootBottomConstraint.constant = -12
3192
+ let skeleton = NativeListMarketSkeleton(background: nativeListColor(theme, "background", "#FFFFFF"))
3193
+ skeleton.translatesAutoresizingMaskIntoConstraints = false
3194
+ skeleton.heightAnchor.constraint(equalToConstant: 32).isActive = true
3195
+ rootStack.addArrangedSubview(skeleton)
3196
+ selectorViews.append(skeleton)
3197
+ return
3198
+ }
3199
+ if variant == "loading" && item.data.string("loadingStyle") == "spinner" {
3200
+ rootTopConstraint.constant = 16
3201
+ rootBottomConstraint.constant = -16
3202
+ rootStack.addArrangedSubview(mainStack)
3203
+ mainStack.alignment = .center
3204
+ mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3205
+ let indicator = UIActivityIndicatorView(style: .medium)
3206
+ indicator.color = nativeListColor(theme, "icon", "#0000009B")
3207
+ indicator.startAnimating()
3208
+ mainStack.addArrangedSubview(indicator)
3209
+ selectorViews.append(indicator)
3210
+ return
3211
+ }
3212
+ if isMarket && variant == "noMatch" {
3213
+ let padding = max(32, (CGFloat(item.data.double("height", default: 88)) - 24) / 2)
3214
+ rootTopConstraint.constant = padding
3215
+ rootBottomConstraint.constant = -padding
3216
+ rootStack.addArrangedSubview(mainStack)
3217
+ mainStack.alignment = .center
3218
+ mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3219
+ titleLabel.font = nativeListTabularFont(ofSize: 16)
3220
+ titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
3221
+ titleLabel.textAlignment = .center
3222
+ show(titleLabel, item.data.string("message"), lines: 1)
3223
+ setLineHeight(titleLabel, text: item.data.string("message"), lineHeight: 24)
3224
+ return
3225
+ }
3226
+ if isMarket && variant == "end" {
3227
+ rootTopConstraint.constant = 16
3228
+ rootBottomConstraint.constant = -16
3229
+ // OneKey patch: an empty title stack must not consume the dot's line height.
3230
+ titleRowStack.isHidden = true
3231
+ rootStack.addArrangedSubview(mainStack)
3232
+ mainStack.alignment = .center
3233
+ mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3234
+ let indicator = UIStackView()
3235
+ indicator.axis = .horizontal
3236
+ indicator.alignment = .center
3237
+ indicator.spacing = 8
3238
+ for width in [CGFloat(80), 4, 80] {
3239
+ let mark = UIView()
3240
+ mark.translatesAutoresizingMaskIntoConstraints = false
3241
+ mark.backgroundColor = nativeListColor(theme, "separator", "#0000001F")
3242
+ mark.layer.cornerRadius = width == 4 ? 2 : 0
3243
+ NSLayoutConstraint.activate([
3244
+ mark.widthAnchor.constraint(equalToConstant: width),
3245
+ mark.heightAnchor.constraint(equalToConstant: width == 4 ? 4 : 1),
3246
+ ])
3247
+ indicator.addArrangedSubview(mark)
3248
+ }
3249
+ mainStack.addArrangedSubview(indicator)
3250
+ selectorViews.append(indicator)
3251
+ return
3252
+ }
2633
3253
  // OneKey patch: deprecated-wallet warnings stay inside the scrolling list.
2634
3254
  if variant == "warning" {
2635
3255
  rootStack.addArrangedSubview(mainStack)
@@ -2661,10 +3281,10 @@ final class NativeListCell: UICollectionViewCell {
2661
3281
  return
2662
3282
  }
2663
3283
  if variant == "loading" {
2664
- leadingWidth.constant = 40
2665
- leadingHeight.constant = 40
3284
+ leadingWidth.constant = isMarket ? 32 : 40
3285
+ leadingHeight.constant = isMarket ? 32 : 40
2666
3286
  leadingContainer.backgroundColor = nativeListColor(theme, "strongBackground", "#F0F0F0")
2667
- leadingContainer.layer.cornerRadius = 20
3287
+ leadingContainer.layer.cornerRadius = isMarket ? 16 : 20
2668
3288
  rootStack.addArrangedSubview(leadingContainer)
2669
3289
  configureSkeleton(skeletonPrimary, width: 120, height: 12, theme: theme)
2670
3290
  configureSkeleton(skeletonSecondary, width: 80, height: 12, theme: theme)
@@ -3076,8 +3696,8 @@ final class NativeListCell: UICollectionViewCell {
3076
3696
  let paragraphStyle = NSMutableParagraphStyle()
3077
3697
  paragraphStyle.minimumLineHeight = lineHeight
3078
3698
  paragraphStyle.maximumLineHeight = lineHeight
3079
- if currentItem?.data.string("presentation") == "walletSidebar" {
3080
- // OneKey patch: attributed paragraphs must preserve wallet name alignment and tail ellipsis.
3699
+ if currentItem?.type == "market" || currentItem?.data.string("presentation") == "walletSidebar" {
3700
+ // Attributed paragraphs must preserve the label alignment and tail ellipsis.
3081
3701
  paragraphStyle.alignment = label.textAlignment
3082
3702
  paragraphStyle.lineBreakMode = label.lineBreakMode
3083
3703
  }
@@ -3086,15 +3706,15 @@ final class NativeListCell: UICollectionViewCell {
3086
3706
  .foregroundColor: label.textColor as Any,
3087
3707
  .paragraphStyle: paragraphStyle,
3088
3708
  ]
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" {
3709
+ if currentItem?.type == "market" || currentItem?.type == "system" && currentItem?.data.string("presentation") == "market" && currentItem?.data.string("variant") == "noMatch" || (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
3710
  // OneKey patch: React Native centers font metrics inside explicit line heights.
3091
3711
  let baselineOffset = max(0, (lineHeight - label.font.lineHeight) / 2)
3092
3712
  // 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
3713
+ let isSelectorHeading = lineHeight == 20 && (currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" || currentItem?.type == "market" && label.font.pointSize == 14)
3094
3714
  let scale = window?.screen.scale ?? traitCollection.displayScale
3095
3715
  attributes[.baselineOffset] = isSelectorHeading && scale > 0 ? ceil(baselineOffset * scale) / scale : baselineOffset
3096
3716
  }
3097
- if letterSpacing != 0 { attributes[.kern] = letterSpacing }
3717
+ if letterSpacing != 0 || currentItem?.type == "market" || currentItem?.data.string("presentation") == "market" { attributes[.kern] = letterSpacing }
3098
3718
  label.attributedText = NSAttributedString(string: text, attributes: attributes)
3099
3719
  }
3100
3720
 
@@ -3109,11 +3729,14 @@ final class NativeListCell: UICollectionViewCell {
3109
3729
  let paragraphStyle = NSMutableParagraphStyle()
3110
3730
  paragraphStyle.minimumLineHeight = lineHeight
3111
3731
  paragraphStyle.maximumLineHeight = lineHeight
3732
+ let isMarketText = currentItem?.type == "market" || currentItem?.type == "system" && currentItem?.data.string("presentation") == "market" && currentItem?.data.string("variant") == "retry"
3112
3733
  let isSelectorValue = currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") != "summary"
3113
3734
  let isSelectorSummary = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") == "summary"
3114
3735
  (button as? NativeListAccessoryButton)?.selectorSummaryLineHeight = isSelectorSummary ? lineHeight : nil
3736
+ (button as? NativeListAccessoryButton)?.marketLineHeight = isMarketText ? lineHeight : nil
3115
3737
  // OneKey patch: summary text uses its source line box; currency retains trailing alignment.
3116
- paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary ? .natural : .center
3738
+ // Market's line box already handles alignment; source text starts at its origin.
3739
+ paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary || isMarketText ? .natural : .center
3117
3740
  if isSelectorSummary {
3118
3741
  button.contentHorizontalAlignment = .leading
3119
3742
  button.titleLabel?.textAlignment = .natural
@@ -3122,17 +3745,16 @@ final class NativeListCell: UICollectionViewCell {
3122
3745
  button.contentHorizontalAlignment = .trailing
3123
3746
  button.titleLabel?.textAlignment = .right
3124
3747
  }
3125
- let baselineOffset: CGFloat = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil ? max(0, (lineHeight - font.lineHeight) / 2) : 0
3748
+ let baselineOffset: CGFloat = isMarketText || currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil ? max(0, (lineHeight - font.lineHeight) / 2) : 0
3749
+ var attributes: [NSAttributedString.Key: Any] = [
3750
+ .font: font,
3751
+ .foregroundColor: color,
3752
+ .paragraphStyle: paragraphStyle,
3753
+ .baselineOffset: isMarketText && lineHeight == 20 && font.pointSize == 14 ? ceil(baselineOffset * max(1, traitCollection.displayScale)) / max(1, traitCollection.displayScale) : baselineOffset,
3754
+ ]
3755
+ if isMarketText { attributes[.kern] = 0 }
3126
3756
  button.setAttributedTitle(
3127
- NSAttributedString(
3128
- string: text,
3129
- attributes: [
3130
- .font: font,
3131
- .foregroundColor: color,
3132
- .paragraphStyle: paragraphStyle,
3133
- .baselineOffset: baselineOffset,
3134
- ]
3135
- ),
3757
+ NSAttributedString(string: text, attributes: attributes),
3136
3758
  for: .normal
3137
3759
  )
3138
3760
  }
@@ -3394,6 +4016,19 @@ final class NativeListCell: UICollectionViewCell {
3394
4016
  )
3395
4017
  }
3396
4018
 
4019
+ @objc private func marketBadgePressed(_ sender: UIButton) {
4020
+ guard let item = currentItem,
4021
+ marketBadgeActionKeys.indices.contains(sender.tag),
4022
+ let actionKey = marketBadgeActionKeys[sender.tag],
4023
+ !actionKey.isEmpty else { return }
4024
+ onAction?(
4025
+ item,
4026
+ actionKey,
4027
+ nil,
4028
+ actionOrigin(sourceView: sender, source: "marketBadge", slot: sender.tag)
4029
+ )
4030
+ }
4031
+
3397
4032
  @objc private func footerActionPressed(_ sender: UIButton) {
3398
4033
  guard let item = currentItem, footerActionKeys.indices.contains(sender.tag) else { return }
3399
4034
  onAction?(