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

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?
@@ -36,13 +103,17 @@ private final class NativeListAccessoryButton: UIButton {
36
103
  didSet { invalidateIntrinsicContentSize(); setNeedsLayout() }
37
104
  }
38
105
 
106
+ var marketLineHeight: CGFloat? {
107
+ didSet { invalidateIntrinsicContentSize(); setNeedsLayout() }
108
+ }
109
+
39
110
  private var sourcePixelScale: CGFloat {
40
111
  max(1, window?.screen.scale ?? traitCollection.displayScale)
41
112
  }
42
113
 
43
114
  override var intrinsicContentSize: CGSize {
44
115
  var size = super.intrinsicContentSize
45
- guard selectorSummaryLineHeight != nil, let title = attributedTitle(for: .normal) else { return size }
116
+ guard selectorSummaryLineHeight != nil || marketLineHeight != nil, let title = attributedTitle(for: .normal) else { return size }
46
117
  let width = title.boundingRect(
47
118
  with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
48
119
  options: [.usesLineFragmentOrigin, .usesFontLeading],
@@ -54,6 +125,21 @@ private final class NativeListAccessoryButton: UIButton {
54
125
 
55
126
  override func layoutSubviews() {
56
127
  super.layoutSubviews()
128
+ if let lineHeight = marketLineHeight, let titleLabel, let title = attributedTitle(for: .normal) {
129
+ let measured = title.boundingRect(
130
+ with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
131
+ options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil
132
+ ).width
133
+ let width = min(bounds.width, ceil(measured * sourcePixelScale) / sourcePixelScale)
134
+ let x = contentHorizontalAlignment == .trailing ? bounds.width - width
135
+ : contentHorizontalAlignment == .leading ? 0 : (bounds.width - width) / 2
136
+ titleLabel.frame = CGRect(
137
+ x: floor(x * sourcePixelScale) / sourcePixelScale,
138
+ y: (bounds.height - lineHeight) / 2,
139
+ width: width, height: lineHeight
140
+ )
141
+ return
142
+ }
57
143
  guard let lineHeight = selectorSummaryLineHeight, let titleLabel else { return }
58
144
  // OneKey patch: position the final source line box after UIKit has measured the button.
59
145
  let top = ceil((bounds.height - lineHeight) / 2 * sourcePixelScale) / sourcePixelScale
@@ -362,6 +448,8 @@ final class NativeListCell: UICollectionViewCell {
362
448
  private let metricSubtitleLabel = UILabel()
363
449
  private let metricCompositeStack = UIStackView()
364
450
  private let badgeLabel = NativeListInsetLabel()
451
+ private let marketBadgeButtons = (0..<3).map { _ in UIButton(type: .system) }
452
+ private let marketBadgeImages = (0..<3).map { _ in OneKeyImageReusableView(frame: .zero) }
365
453
  private let actionStack = UIStackView()
366
454
  private let actionButtons = (0..<3).map { _ in UIButton(type: .system) }
367
455
  private let trailingStack = UIStackView()
@@ -412,6 +500,7 @@ final class NativeListCell: UICollectionViewCell {
412
500
  private var boundCheckboxData: [String: Any]?
413
501
  private var boundCheckboxTarget: NativeSelectionTarget?
414
502
  private var leadingActionKey: String?
503
+ private var marketBadgeActionKeys: [String?] = []
415
504
  private var restingBackgroundColor: UIColor = .clear
416
505
  private var pressedBackgroundColor = UIColor(
417
506
  nativeListHex: "#E8E8E8",
@@ -612,6 +701,23 @@ final class NativeListCell: UICollectionViewCell {
612
701
  titleRowStack.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
613
702
  titleRowStack.addArrangedSubview(titleLabel)
614
703
  titleRowStack.addArrangedSubview(badgeLabel)
704
+ marketBadgeButtons.enumerated().forEach { index, button in
705
+ button.tag = index
706
+ button.addTarget(self, action: #selector(marketBadgePressed(_:)), for: .touchUpInside)
707
+ button.titleLabel?.font = nativeListFont(ofSize: 11, weight: .medium)
708
+ button.layer.cornerRadius = 4
709
+ button.clipsToBounds = true
710
+ let image = marketBadgeImages[index]
711
+ image.isUserInteractionEnabled = false
712
+ image.translatesAutoresizingMaskIntoConstraints = false
713
+ button.addSubview(image)
714
+ NSLayoutConstraint.activate([
715
+ image.centerYAnchor.constraint(equalTo: button.centerYAnchor),
716
+ image.widthAnchor.constraint(equalToConstant: 14),
717
+ image.heightAnchor.constraint(equalToConstant: 14),
718
+ ])
719
+ titleRowStack.addArrangedSubview(button)
720
+ }
615
721
  titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
616
722
  titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
617
723
  badgeLabel.setContentHuggingPriority(.required, for: .horizontal)
@@ -843,6 +949,7 @@ final class NativeListCell: UICollectionViewCell {
843
949
  case "activity": bindActivity(item, theme: theme)
844
950
  case "message": bindMessage(item, theme: theme)
845
951
  case "dataRow": bindDataRow(item, theme: theme, checkboxState)
952
+ case "market": bindMarket(item, theme: theme)
846
953
  case "mediaTile": bindMediaTile(item, theme: theme)
847
954
  case "metricCard": bindMetricCard(item, theme: theme)
848
955
  case "sectionHeader": bindSectionHeader(item, theme: theme, layout: layout, checkboxState)
@@ -1041,6 +1148,7 @@ final class NativeListCell: UICollectionViewCell {
1041
1148
  selectorConstraints.removeAll()
1042
1149
  selectorImages.forEach { $0.prepareForReuse() }
1043
1150
  selectorImages.removeAll()
1151
+ marketBadgeImages.forEach { $0.prepareForReuse(); $0.isHidden = true }
1044
1152
  selectorFullWidthBackground.removeFromSuperlayer()
1045
1153
  selectorBorder?.removeFromSuperlayer()
1046
1154
  selectorBorder = nil
@@ -1123,6 +1231,7 @@ final class NativeListCell: UICollectionViewCell {
1123
1231
  $0.isHidden = true
1124
1232
  $0.alpha = 1
1125
1233
  $0.layer.cornerRadius = 0
1234
+ $0.layer.mask = nil
1126
1235
  }
1127
1236
  leadingContainer.clipsToBounds = true
1128
1237
  leadingContainer.layer.borderWidth = 0
@@ -1178,6 +1287,19 @@ final class NativeListCell: UICollectionViewCell {
1178
1287
  badgeLabel.backgroundColor = .clear
1179
1288
  badgeLabel.layer.cornerRadius = 0
1180
1289
  badgeLabel.clipsToBounds = false
1290
+ marketBadgeButtons.forEach {
1291
+ $0.isHidden = true
1292
+ $0.isEnabled = true
1293
+ $0.isUserInteractionEnabled = false
1294
+ $0.setTitle(nil, for: .normal)
1295
+ $0.setImage(nil, for: .normal)
1296
+ $0.setTitleColor(nil, for: .normal)
1297
+ $0.tintColor = nil
1298
+ $0.backgroundColor = .clear
1299
+ $0.contentEdgeInsets = .zero
1300
+ $0.imageEdgeInsets = .zero
1301
+ $0.titleEdgeInsets = .zero
1302
+ }
1181
1303
  [titleLabel, subtitleLabel, tertiaryLabel, statusLabel, metricSubtitleLabel, badgeLabel, actionStack]
1182
1304
  .forEach { $0.isHidden = true }
1183
1305
  separatorView.isHidden = true
@@ -1188,7 +1310,9 @@ final class NativeListCell: UICollectionViewCell {
1188
1310
  $0.backgroundColor = .clear
1189
1311
  }
1190
1312
  accessoryButtons.enumerated().forEach { index, button in
1313
+ button.isUserInteractionEnabled = true
1191
1314
  button.selectorSummaryLineHeight = nil
1315
+ button.marketLineHeight = nil
1192
1316
  button.titleLabel?.font = nativeListFont(
1193
1317
  ofSize: index == 0 ? 16 : 14,
1194
1318
  weight: index == 0 ? .medium : .regular
@@ -1233,6 +1357,7 @@ final class NativeListCell: UICollectionViewCell {
1233
1357
  boundCheckboxData = nil
1234
1358
  boundCheckboxTarget = nil
1235
1359
  leadingActionKey = nil
1360
+ marketBadgeActionKeys = []
1236
1361
  layer.maskedCorners = []
1237
1362
  layer.cornerRadius = 0
1238
1363
  layer.cornerCurve = .circular
@@ -2014,6 +2139,283 @@ final class NativeListCell: UICollectionViewCell {
2014
2139
  }
2015
2140
  }
2016
2141
 
2142
+ private func marketFontWeight(_ value: String, fallback: NativeListFontWeight) -> NativeListFontWeight {
2143
+ switch value {
2144
+ case "regular": return .regular
2145
+ case "semibold": return .semibold
2146
+ case "bold": return .bold
2147
+ case "medium": return .medium
2148
+ default: return fallback
2149
+ }
2150
+ }
2151
+
2152
+ private func marketTextAlignment(_ value: String) -> NSTextAlignment {
2153
+ switch value {
2154
+ case "center": return .center
2155
+ case "start": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .right : .left
2156
+ case "end": return effectiveUserInterfaceLayoutDirection == .rightToLeft ? .left : .right
2157
+ default: return .natural
2158
+ }
2159
+ }
2160
+
2161
+ private func applyMarketTextStyle(
2162
+ _ label: UILabel,
2163
+ data: [String: Any]?,
2164
+ theme: [String: Any]?,
2165
+ defaultSize: CGFloat,
2166
+ defaultLineHeight: CGFloat,
2167
+ defaultWeight: NativeListFontWeight,
2168
+ defaultColor: UIColor,
2169
+ defaultAlignment: NSTextAlignment = .natural
2170
+ ) {
2171
+ let size = CGFloat(data?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
2172
+ let lineHeight = CGFloat(data?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
2173
+ label.font = nativeListTabularFont(ofSize: size, weight: marketFontWeight(data?.string("fontWeight") ?? "", fallback: defaultWeight))
2174
+ label.textColor = data?["color"].flatMap { $0 as? String }.map { UIColor(nativeListHex: $0, fallback: defaultColor) } ?? defaultColor
2175
+ label.textAlignment = data?["alignment"] == nil
2176
+ ? defaultAlignment
2177
+ : marketTextAlignment(data?.string("alignment") ?? "")
2178
+ label.numberOfLines = min(2, max(1, data?.int("lines", default: 1) ?? 1))
2179
+ setLineHeight(label, text: label.text ?? "", lineHeight: lineHeight)
2180
+ }
2181
+
2182
+ private func applyMarketButtonStyle(
2183
+ _ button: UIButton,
2184
+ data: [String: Any]?,
2185
+ defaultSize: CGFloat,
2186
+ defaultLineHeight: CGFloat,
2187
+ defaultWeight: NativeListFontWeight,
2188
+ color: UIColor,
2189
+ defaultAlignment: UIControl.ContentHorizontalAlignment
2190
+ ) {
2191
+ let size = CGFloat(data?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
2192
+ let lineHeight = CGFloat(data?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
2193
+ setButtonLine(
2194
+ button,
2195
+ text: button.title(for: .normal) ?? "",
2196
+ font: nativeListTabularFont(ofSize: size, weight: marketFontWeight(data?.string("fontWeight") ?? "", fallback: defaultWeight)),
2197
+ color: data?["color"].flatMap { $0 as? String }.map { UIColor(nativeListHex: $0, fallback: color) } ?? color,
2198
+ lineHeight: lineHeight
2199
+ )
2200
+ if data?["alignment"] == nil {
2201
+ button.contentHorizontalAlignment = defaultAlignment
2202
+ } else {
2203
+ button.contentHorizontalAlignment = data?.string("alignment") == "start"
2204
+ ? .leading
2205
+ : data?.string("alignment") == "end" ? .trailing : .center
2206
+ }
2207
+ button.titleLabel?.numberOfLines = min(2, max(1, data?.int("lines", default: 1) ?? 1))
2208
+ }
2209
+
2210
+ private func marketAttributedText(
2211
+ _ text: String,
2212
+ segments: [[String: Any]],
2213
+ style: [String: Any]?,
2214
+ defaultSize: CGFloat,
2215
+ defaultLineHeight: CGFloat,
2216
+ defaultWeight: NativeListFontWeight,
2217
+ color: UIColor,
2218
+ defaultAlignment: NSTextAlignment
2219
+ ) -> NSAttributedString {
2220
+ let size = CGFloat(style?.double("fontSize", default: Double(defaultSize)) ?? Double(defaultSize))
2221
+ let lineHeight = CGFloat(style?.double("lineHeight", default: Double(defaultLineHeight)) ?? Double(defaultLineHeight))
2222
+ let paragraph = NSMutableParagraphStyle()
2223
+ paragraph.minimumLineHeight = lineHeight
2224
+ paragraph.maximumLineHeight = lineHeight
2225
+ paragraph.alignment = style?["alignment"] == nil
2226
+ ? defaultAlignment
2227
+ : marketTextAlignment(style?.string("alignment") ?? "")
2228
+ let weight = marketFontWeight(style?.string("fontWeight") ?? "", fallback: defaultWeight)
2229
+ let resolvedColor = style?["color"].flatMap { $0 as? String }.map { UIColor(nativeListHex: $0, fallback: color) } ?? color
2230
+ let result = NSMutableAttributedString(string: "")
2231
+ let source = segments.isEmpty ? [["text": text]] : segments
2232
+ for segment in source {
2233
+ let segmentSize = segment.string("style") == "subscript" ? ceil(size * 0.6) : size
2234
+ result.append(NSAttributedString(string: segment.string("text"), attributes: [
2235
+ .font: nativeListTabularFont(ofSize: segmentSize, weight: weight),
2236
+ .foregroundColor: resolvedColor,
2237
+ .kern: 0,
2238
+ .paragraphStyle: paragraph,
2239
+ .baselineOffset: max(0, (lineHeight - nativeListTabularFont(ofSize: size, weight: weight).lineHeight) / 2),
2240
+ ]))
2241
+ }
2242
+ return result
2243
+ }
2244
+
2245
+ private func marketLeading(_ item: NativeListItem, style: [String: Any]?) -> [String: Any]? {
2246
+ guard var visual = item.data.dictionary("leading") else { return nil }
2247
+ guard let imageStyle = style?.dictionary("image") else { return visual }
2248
+ if let shape = imageStyle["shape"] as? String { visual["shape"] = shape }
2249
+ if let contentFit = imageStyle["contentFit"] as? String, var image = visual.dictionary("image") {
2250
+ image["contentFit"] = contentFit
2251
+ visual["image"] = image
2252
+ }
2253
+ return visual
2254
+ }
2255
+
2256
+ private func bindMarket(_ item: NativeListItem, theme: [String: Any]?) {
2257
+ let variant = item.data.string("variant")
2258
+ let style = item.data.dictionary("style")
2259
+ let imageStyle = style?.dictionary("image")
2260
+ let imageWidth = CGFloat(imageStyle?.double("width", default: variant == "stock" ? 40 : 32) ?? (variant == "stock" ? 40 : 32))
2261
+ let imageHeight = CGFloat(imageStyle?.double("height", default: variant == "stock" ? 40 : 32) ?? (variant == "stock" ? 40 : 32))
2262
+ let horizontalPadding = CGFloat(style?.double("horizontalPadding", default: variant == "perp" ? 16 : 20) ?? (variant == "perp" ? 16 : 20))
2263
+ let verticalPadding = CGFloat(style?.double("verticalPadding", default: 12) ?? 12)
2264
+ rootLeadingConstraint.constant = horizontalPadding
2265
+ rootTrailingConstraint.constant = -horizontalPadding
2266
+ rootTopConstraint.constant = verticalPadding
2267
+ rootBottomConstraint.constant = -verticalPadding
2268
+ rootStack.spacing = CGFloat(style?.double("leadingGap", default: variant == "perp" ? 8 : 14) ?? (variant == "perp" ? 8 : 14))
2269
+ leadingWidth.constant = imageWidth
2270
+ leadingHeight.constant = imageHeight
2271
+ if let visual = marketLeading(item, style: style) {
2272
+ addLeading(visual, key: item.key)
2273
+ 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))
2274
+ leadingContainer.layer.cornerRadius = radius
2275
+ leadingImages.first?.layer.cornerRadius = radius
2276
+ if let image = leadingImages.first, !visual.string("borderColor").isEmpty {
2277
+ // Match Token's border box: the bitmap occupies the one-point inset.
2278
+ leadingContainer.layer.borderWidth = 1
2279
+ leadingContainer.layer.borderColor = UIColor(nativeListHex: visual.string("borderColor"), fallback: .clear).cgColor
2280
+ for constraint in leadingSlotConstraints where constraint.firstItem === image {
2281
+ switch constraint.firstAttribute {
2282
+ case .leading, .top: constraint.constant = 1
2283
+ case .trailing, .bottom: constraint.constant = -1
2284
+ default: break
2285
+ }
2286
+ }
2287
+ image.layer.cornerRadius = 0
2288
+ let mask = CAShapeLayer()
2289
+ mask.path = UIBezierPath(roundedRect: CGRect(x: -1, y: -1, width: imageWidth, height: imageHeight), cornerRadius: radius).cgPath
2290
+ image.layer.mask = mask
2291
+ }
2292
+ if let diagnostic = item.data.dictionary("diagnostics")?.string("imageBindActionKey"), !diagnostic.isEmpty,
2293
+ visual.dictionary("image") != nil || visual.dictionary("networkImage") != nil {
2294
+ onAction?(item, diagnostic, nil, nil)
2295
+ }
2296
+ }
2297
+ rootStack.addArrangedSubview(mainStack)
2298
+ rootStack.setCustomSpacing(0, after: mainStack)
2299
+ mainStack.spacing = CGFloat(style?.double("lineGap", default: 0) ?? 0)
2300
+ titleRowStack.spacing = CGFloat(style?.double("titleBadgeGap", default: 4) ?? 4)
2301
+ show(titleLabel, item.data.string("title"), lines: style?.dictionary("title")?.int("lines", default: 1) ?? 1)
2302
+ applyMarketTextStyle(titleLabel, data: style?.dictionary("title"), theme: theme, defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, defaultColor: nativeListColor(theme, "primaryText", "#202020"))
2303
+ let badges = Array(item.data.dictionaries("badges").prefix(marketBadgeButtons.count))
2304
+ marketBadgeActionKeys = badges.map { $0["actionKey"] as? String }
2305
+ for (index, badge) in badges.enumerated() {
2306
+ let button = marketBadgeButtons[index]
2307
+ let hasBuiltInIcon = badge.string("iconName") == "verified"
2308
+ let hasRemoteIcon = badge.dictionary("icon") != nil
2309
+ let hasIcon = hasBuiltInIcon || hasRemoteIcon
2310
+ let text = badge.string("text")
2311
+ let toneColor: UIColor
2312
+ switch badge.string("tone") {
2313
+ case "success": toneColor = nativeListColor(theme, "positive", "#218358")
2314
+ case "danger": toneColor = nativeListColor(theme, "negative", "#CE2C31")
2315
+ case "info": toneColor = nativeListColor(theme, "info", "#0D74CE")
2316
+ case "warning": toneColor = nativeListColor(theme, "primaryText", "#202020")
2317
+ default: toneColor = nativeListColor(theme, "secondaryText", "#646464")
2318
+ }
2319
+ let foreground = UIColor(
2320
+ nativeListHex: badge.string("textColor", default: ""),
2321
+ fallback: toneColor
2322
+ )
2323
+ button.isHidden = false
2324
+ button.isEnabled = true
2325
+ button.isUserInteractionEnabled = !badge.string("actionKey").isEmpty
2326
+ button.accessibilityTraits = button.isUserInteractionEnabled ? .button : .staticText
2327
+ button.setTitle(text, for: .normal)
2328
+ button.setTitleColor(foreground, for: .normal)
2329
+ button.tintColor = foreground
2330
+ button.backgroundColor = UIColor(
2331
+ nativeListHex: badge.string("backgroundColor", default: ""),
2332
+ fallback: hasIcon && text.isEmpty
2333
+ ? .clear
2334
+ : nativeListColor(theme, "strongBackground", "#0000000F")
2335
+ )
2336
+ let iconOnly = hasIcon && text.isEmpty
2337
+ let iconSize: CGFloat = hasBuiltInIcon ? 16 : 14
2338
+ button.contentEdgeInsets = UIEdgeInsets(top: 0, left: iconOnly ? 0 : hasRemoteIcon ? 20 : hasIcon ? 3 : 5, bottom: 0, right: iconOnly ? 0 : 5)
2339
+ button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: text.isEmpty ? 0 : 3)
2340
+ button.titleEdgeInsets = .zero
2341
+ button.imageView?.contentMode = .scaleAspectFit
2342
+ if hasBuiltInIcon {
2343
+ button.setImage(
2344
+ nativeListIcon(named: "BadgeVerifiedSolid", size: CGSize(width: iconSize, height: iconSize)),
2345
+ for: .normal
2346
+ )
2347
+ }
2348
+ let height = button.heightAnchor.constraint(equalToConstant: 18)
2349
+ let textWidth = (text as NSString).size(
2350
+ withAttributes: [.font: nativeListFont(ofSize: 11, weight: .medium)]
2351
+ ).width
2352
+ let width = button.widthAnchor.constraint(equalToConstant: iconOnly ? iconSize : ceil(textWidth) + (hasIcon ? iconSize + 11 : 10))
2353
+ NSLayoutConstraint.activate([width, height])
2354
+ selectorConstraints.append(contentsOf: [width, height])
2355
+ if let icon = badge.dictionary("icon") {
2356
+ marketBadgeImages[index].isHidden = false
2357
+ marketBadgeImages[index].layer.cornerRadius = 7
2358
+ marketBadgeImages[index].clipsToBounds = true
2359
+ let imageLeading = marketBadgeImages[index].leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: text.isEmpty ? 0 : 4)
2360
+ imageLeading.isActive = true
2361
+ selectorConstraints.append(imageLeading)
2362
+ bindImage(icon, into: marketBadgeImages[index], token: item.key, slot: 20 + index, variant: "generic")
2363
+ }
2364
+ button.accessibilityLabel = badge.string("accessibilityLabel", default: badge.string("text"))
2365
+ }
2366
+ if !item.data.string("subtitle").isEmpty || !item.data.dictionaries("subtitleSegments").isEmpty {
2367
+ show(subtitleLabel, item.data.string("subtitle"), lines: style?.dictionary("subtitle")?.int("lines", default: 1) ?? 1)
2368
+ applyMarketTextStyle(subtitleLabel, data: style?.dictionary("subtitle"), theme: theme, defaultSize: 14, defaultLineHeight: 20, defaultWeight: .regular, defaultColor: nativeListColor(theme, "secondaryText", "#646464"))
2369
+ if !item.data.dictionaries("subtitleSegments").isEmpty {
2370
+ 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)
2371
+ }
2372
+ }
2373
+ rootStack.addArrangedSubview(trailingStack)
2374
+ trailingStack.axis = .horizontal
2375
+ trailingStack.alignment = .center
2376
+ trailingStack.spacing = CGFloat(style?.double("trailingGap", default: 8) ?? 8)
2377
+ updateMarketQuote(item, theme: theme)
2378
+ }
2379
+
2380
+ func updateMarketQuote(_ item: NativeListItem, theme: [String: Any]?) {
2381
+ guard currentItem?.key == item.key, item.type == "market" else { return }
2382
+ currentItem = item
2383
+ NSLayoutConstraint.deactivate(accessorySizeConstraints)
2384
+ accessorySizeConstraints.removeAll()
2385
+ let style = item.data.dictionary("style")
2386
+ let price = accessoryButtons[0]
2387
+ price.isUserInteractionEnabled = false
2388
+ price.isHidden = false
2389
+ price.setAttributedTitle(nil, for: .normal)
2390
+ price.setTitle(item.data.string("price"), for: .normal)
2391
+ applyMarketButtonStyle(price, data: style?.dictionary("price"), defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, color: nativeListColor(theme, "primaryText", "#202020"), defaultAlignment: .trailing)
2392
+ if !item.data.dictionaries("priceSegments").isEmpty {
2393
+ 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)
2394
+ }
2395
+ let changeData = item.data.dictionary("change") ?? [:]
2396
+ let change = accessoryButtons[1]
2397
+ change.isUserInteractionEnabled = false
2398
+ change.isHidden = false
2399
+ change.setAttributedTitle(nil, for: .normal)
2400
+ change.setTitle(changeData.string("text"), for: .normal)
2401
+ let defaultChangeColor = nativeListColor(theme, "inverseText", "#FFFFFF")
2402
+ applyMarketButtonStyle(change, data: style?.dictionary("change"), defaultSize: 14, defaultLineHeight: 20, defaultWeight: .medium, color: UIColor(nativeListHex: changeData.string("textColor", default: "#FFFFFF"), fallback: defaultChangeColor), defaultAlignment: .center)
2403
+ if !changeData.dictionaries("textSegments").isEmpty {
2404
+ let changeTextColor = UIColor(nativeListHex: changeData.string("textColor", default: ""), fallback: defaultChangeColor)
2405
+ 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)
2406
+ }
2407
+ let toneKey = changeData.string("tone") == "positive" ? "positive" : changeData.string("tone") == "negative" ? "negative" : "secondaryText"
2408
+ change.backgroundColor = UIColor(nativeListHex: changeData.string("backgroundColor", default: ""), fallback: nativeListColor(theme, toneKey, changeData.string("tone") == "positive" ? "#218358" : changeData.string("tone") == "negative" ? "#CE2C31" : "#8D8D8D"))
2409
+ change.layer.cornerRadius = CGFloat(style?.double("changeCornerRadius", default: 8) ?? 8)
2410
+ change.clipsToBounds = true
2411
+ let width = change.widthAnchor.constraint(equalToConstant: CGFloat(style?.double("changeWidth", default: 80) ?? 80))
2412
+ let height = change.heightAnchor.constraint(equalToConstant: CGFloat(style?.double("changeHeight", default: 32) ?? 32))
2413
+ width.isActive = true
2414
+ height.isActive = true
2415
+ accessorySizeConstraints.append(contentsOf: [width, height])
2416
+ 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: ", "))
2417
+ }
2418
+
2017
2419
  private func bindMediaTile(_ item: NativeListItem, theme: [String: Any]?) {
2018
2420
  rootStack.axis = .vertical
2019
2421
  rootStack.alignment = .fill
@@ -2630,6 +3032,76 @@ final class NativeListCell: UICollectionViewCell {
2630
3032
  rootStack.alignment = .center
2631
3033
  rootStack.distribution = .fill
2632
3034
  let variant = item.data.string("variant")
3035
+ let isMarket = item.data.string("presentation") == "market"
3036
+ if isMarket {
3037
+ rootLeadingConstraint.constant = 20
3038
+ rootTrailingConstraint.constant = -20
3039
+ rootTopConstraint.constant = 12
3040
+ rootBottomConstraint.constant = -12
3041
+ }
3042
+ if variant == "loading" && item.data.string("loadingStyle") == "skeleton" {
3043
+ rootLeadingConstraint.constant = 20
3044
+ rootTrailingConstraint.constant = -20
3045
+ rootTopConstraint.constant = 12
3046
+ rootBottomConstraint.constant = -12
3047
+ let skeleton = NativeListMarketSkeleton(background: nativeListColor(theme, "background", "#FFFFFF"))
3048
+ skeleton.translatesAutoresizingMaskIntoConstraints = false
3049
+ skeleton.heightAnchor.constraint(equalToConstant: 32).isActive = true
3050
+ rootStack.addArrangedSubview(skeleton)
3051
+ selectorViews.append(skeleton)
3052
+ return
3053
+ }
3054
+ if variant == "loading" && item.data.string("loadingStyle") == "spinner" {
3055
+ rootTopConstraint.constant = 16
3056
+ rootBottomConstraint.constant = -16
3057
+ rootStack.addArrangedSubview(mainStack)
3058
+ mainStack.alignment = .center
3059
+ mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3060
+ let indicator = UIActivityIndicatorView(style: .medium)
3061
+ indicator.color = nativeListColor(theme, "icon", "#0000009B")
3062
+ indicator.startAnimating()
3063
+ mainStack.addArrangedSubview(indicator)
3064
+ selectorViews.append(indicator)
3065
+ return
3066
+ }
3067
+ if isMarket && variant == "noMatch" {
3068
+ rootTopConstraint.constant = 32
3069
+ rootBottomConstraint.constant = -32
3070
+ rootStack.addArrangedSubview(mainStack)
3071
+ mainStack.alignment = .center
3072
+ mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3073
+ titleLabel.font = nativeListFont(ofSize: 16)
3074
+ titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
3075
+ titleLabel.textAlignment = .center
3076
+ show(titleLabel, item.data.string("message"), lines: 1)
3077
+ setLineHeight(titleLabel, text: item.data.string("message"), lineHeight: 24)
3078
+ return
3079
+ }
3080
+ if isMarket && variant == "end" {
3081
+ rootTopConstraint.constant = 16
3082
+ rootBottomConstraint.constant = -16
3083
+ rootStack.addArrangedSubview(mainStack)
3084
+ mainStack.alignment = .center
3085
+ mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3086
+ let indicator = UIStackView()
3087
+ indicator.axis = .horizontal
3088
+ indicator.alignment = .center
3089
+ indicator.spacing = 8
3090
+ for width in [CGFloat(80), 4, 80] {
3091
+ let mark = UIView()
3092
+ mark.translatesAutoresizingMaskIntoConstraints = false
3093
+ mark.backgroundColor = nativeListColor(theme, "separator", "#0000001F")
3094
+ mark.layer.cornerRadius = width == 4 ? 2 : 0
3095
+ NSLayoutConstraint.activate([
3096
+ mark.widthAnchor.constraint(equalToConstant: width),
3097
+ mark.heightAnchor.constraint(equalToConstant: width == 4 ? 4 : 1),
3098
+ ])
3099
+ indicator.addArrangedSubview(mark)
3100
+ }
3101
+ mainStack.addArrangedSubview(indicator)
3102
+ selectorViews.append(indicator)
3103
+ return
3104
+ }
2633
3105
  // OneKey patch: deprecated-wallet warnings stay inside the scrolling list.
2634
3106
  if variant == "warning" {
2635
3107
  rootStack.addArrangedSubview(mainStack)
@@ -2661,10 +3133,10 @@ final class NativeListCell: UICollectionViewCell {
2661
3133
  return
2662
3134
  }
2663
3135
  if variant == "loading" {
2664
- leadingWidth.constant = 40
2665
- leadingHeight.constant = 40
3136
+ leadingWidth.constant = isMarket ? 32 : 40
3137
+ leadingHeight.constant = isMarket ? 32 : 40
2666
3138
  leadingContainer.backgroundColor = nativeListColor(theme, "strongBackground", "#F0F0F0")
2667
- leadingContainer.layer.cornerRadius = 20
3139
+ leadingContainer.layer.cornerRadius = isMarket ? 16 : 20
2668
3140
  rootStack.addArrangedSubview(leadingContainer)
2669
3141
  configureSkeleton(skeletonPrimary, width: 120, height: 12, theme: theme)
2670
3142
  configureSkeleton(skeletonSecondary, width: 80, height: 12, theme: theme)
@@ -3076,8 +3548,8 @@ final class NativeListCell: UICollectionViewCell {
3076
3548
  let paragraphStyle = NSMutableParagraphStyle()
3077
3549
  paragraphStyle.minimumLineHeight = lineHeight
3078
3550
  paragraphStyle.maximumLineHeight = lineHeight
3079
- if currentItem?.data.string("presentation") == "walletSidebar" {
3080
- // OneKey patch: attributed paragraphs must preserve wallet name alignment and tail ellipsis.
3551
+ if currentItem?.type == "market" || currentItem?.data.string("presentation") == "walletSidebar" {
3552
+ // Attributed paragraphs must preserve the label alignment and tail ellipsis.
3081
3553
  paragraphStyle.alignment = label.textAlignment
3082
3554
  paragraphStyle.lineBreakMode = label.lineBreakMode
3083
3555
  }
@@ -3086,15 +3558,15 @@ final class NativeListCell: UICollectionViewCell {
3086
3558
  .foregroundColor: label.textColor as Any,
3087
3559
  .paragraphStyle: paragraphStyle,
3088
3560
  ]
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" {
3561
+ if currentItem?.type == "market" || (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
3562
  // OneKey patch: React Native centers font metrics inside explicit line heights.
3091
3563
  let baselineOffset = max(0, (lineHeight - label.font.lineHeight) / 2)
3092
3564
  // 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
3565
+ let isSelectorHeading = lineHeight == 20 && (currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" || currentItem?.type == "market" && label.font.pointSize == 14)
3094
3566
  let scale = window?.screen.scale ?? traitCollection.displayScale
3095
3567
  attributes[.baselineOffset] = isSelectorHeading && scale > 0 ? ceil(baselineOffset * scale) / scale : baselineOffset
3096
3568
  }
3097
- if letterSpacing != 0 { attributes[.kern] = letterSpacing }
3569
+ if letterSpacing != 0 || currentItem?.type == "market" { attributes[.kern] = letterSpacing }
3098
3570
  label.attributedText = NSAttributedString(string: text, attributes: attributes)
3099
3571
  }
3100
3572
 
@@ -3112,8 +3584,10 @@ final class NativeListCell: UICollectionViewCell {
3112
3584
  let isSelectorValue = currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") != "summary"
3113
3585
  let isSelectorSummary = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") == "summary"
3114
3586
  (button as? NativeListAccessoryButton)?.selectorSummaryLineHeight = isSelectorSummary ? lineHeight : nil
3587
+ (button as? NativeListAccessoryButton)?.marketLineHeight = currentItem?.type == "market" ? lineHeight : nil
3115
3588
  // OneKey patch: summary text uses its source line box; currency retains trailing alignment.
3116
- paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary ? .natural : .center
3589
+ // Market's line box already handles alignment; source text starts at its origin.
3590
+ paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary || currentItem?.type == "market" ? .natural : .center
3117
3591
  if isSelectorSummary {
3118
3592
  button.contentHorizontalAlignment = .leading
3119
3593
  button.titleLabel?.textAlignment = .natural
@@ -3122,17 +3596,16 @@ final class NativeListCell: UICollectionViewCell {
3122
3596
  button.contentHorizontalAlignment = .trailing
3123
3597
  button.titleLabel?.textAlignment = .right
3124
3598
  }
3125
- let baselineOffset: CGFloat = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil ? max(0, (lineHeight - font.lineHeight) / 2) : 0
3599
+ let baselineOffset: CGFloat = currentItem?.type == "market" || currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil ? max(0, (lineHeight - font.lineHeight) / 2) : 0
3600
+ var attributes: [NSAttributedString.Key: Any] = [
3601
+ .font: font,
3602
+ .foregroundColor: color,
3603
+ .paragraphStyle: paragraphStyle,
3604
+ .baselineOffset: currentItem?.type == "market" && lineHeight == 20 && font.pointSize == 14 ? ceil(baselineOffset * max(1, traitCollection.displayScale)) / max(1, traitCollection.displayScale) : baselineOffset,
3605
+ ]
3606
+ if currentItem?.type == "market" { attributes[.kern] = 0 }
3126
3607
  button.setAttributedTitle(
3127
- NSAttributedString(
3128
- string: text,
3129
- attributes: [
3130
- .font: font,
3131
- .foregroundColor: color,
3132
- .paragraphStyle: paragraphStyle,
3133
- .baselineOffset: baselineOffset,
3134
- ]
3135
- ),
3608
+ NSAttributedString(string: text, attributes: attributes),
3136
3609
  for: .normal
3137
3610
  )
3138
3611
  }
@@ -3394,6 +3867,19 @@ final class NativeListCell: UICollectionViewCell {
3394
3867
  )
3395
3868
  }
3396
3869
 
3870
+ @objc private func marketBadgePressed(_ sender: UIButton) {
3871
+ guard let item = currentItem,
3872
+ marketBadgeActionKeys.indices.contains(sender.tag),
3873
+ let actionKey = marketBadgeActionKeys[sender.tag],
3874
+ !actionKey.isEmpty else { return }
3875
+ onAction?(
3876
+ item,
3877
+ actionKey,
3878
+ nil,
3879
+ actionOrigin(sourceView: sender, source: "marketBadge", slot: sender.tag)
3880
+ )
3881
+ }
3882
+
3397
3883
  @objc private func footerActionPressed(_ sender: UIButton) {
3398
3884
  guard let item = currentItem, footerActionKeys.indices.contains(sender.tag) else { return }
3399
3885
  onAction?(