@onekeyfe/react-native-native-list 3.0.111 → 3.0.113

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,281 @@ 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.setTitle(item.data.string("price"), for: .normal)
2390
+ applyMarketButtonStyle(price, data: style?.dictionary("price"), defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, color: nativeListColor(theme, "primaryText", "#202020"), defaultAlignment: .trailing)
2391
+ if !item.data.dictionaries("priceSegments").isEmpty {
2392
+ 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)
2393
+ }
2394
+ let changeData = item.data.dictionary("change") ?? [:]
2395
+ let change = accessoryButtons[1]
2396
+ change.isUserInteractionEnabled = false
2397
+ change.isHidden = false
2398
+ change.setTitle(changeData.string("text"), for: .normal)
2399
+ let defaultChangeColor = nativeListColor(theme, "inverseText", "#FFFFFF")
2400
+ applyMarketButtonStyle(change, data: style?.dictionary("change"), defaultSize: 14, defaultLineHeight: 20, defaultWeight: .medium, color: UIColor(nativeListHex: changeData.string("textColor", default: "#FFFFFF"), fallback: defaultChangeColor), defaultAlignment: .center)
2401
+ if !changeData.dictionaries("textSegments").isEmpty {
2402
+ let changeTextColor = UIColor(nativeListHex: changeData.string("textColor", default: ""), fallback: defaultChangeColor)
2403
+ 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)
2404
+ }
2405
+ let toneKey = changeData.string("tone") == "positive" ? "positive" : changeData.string("tone") == "negative" ? "negative" : "secondaryText"
2406
+ change.backgroundColor = UIColor(nativeListHex: changeData.string("backgroundColor", default: ""), fallback: nativeListColor(theme, toneKey, changeData.string("tone") == "positive" ? "#218358" : changeData.string("tone") == "negative" ? "#CE2C31" : "#8D8D8D"))
2407
+ change.layer.cornerRadius = CGFloat(style?.double("changeCornerRadius", default: 8) ?? 8)
2408
+ change.clipsToBounds = true
2409
+ let width = change.widthAnchor.constraint(equalToConstant: CGFloat(style?.double("changeWidth", default: 80) ?? 80))
2410
+ let height = change.heightAnchor.constraint(equalToConstant: CGFloat(style?.double("changeHeight", default: 32) ?? 32))
2411
+ width.isActive = true
2412
+ height.isActive = true
2413
+ accessorySizeConstraints.append(contentsOf: [width, height])
2414
+ 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: ", "))
2415
+ }
2416
+
2017
2417
  private func bindMediaTile(_ item: NativeListItem, theme: [String: Any]?) {
2018
2418
  rootStack.axis = .vertical
2019
2419
  rootStack.alignment = .fill
@@ -2630,6 +3030,76 @@ final class NativeListCell: UICollectionViewCell {
2630
3030
  rootStack.alignment = .center
2631
3031
  rootStack.distribution = .fill
2632
3032
  let variant = item.data.string("variant")
3033
+ let isMarket = item.data.string("presentation") == "market"
3034
+ if isMarket {
3035
+ rootLeadingConstraint.constant = 20
3036
+ rootTrailingConstraint.constant = -20
3037
+ rootTopConstraint.constant = 12
3038
+ rootBottomConstraint.constant = -12
3039
+ }
3040
+ if variant == "loading" && item.data.string("loadingStyle") == "skeleton" {
3041
+ rootLeadingConstraint.constant = 20
3042
+ rootTrailingConstraint.constant = -20
3043
+ rootTopConstraint.constant = 12
3044
+ rootBottomConstraint.constant = -12
3045
+ let skeleton = NativeListMarketSkeleton(background: nativeListColor(theme, "background", "#FFFFFF"))
3046
+ skeleton.translatesAutoresizingMaskIntoConstraints = false
3047
+ skeleton.heightAnchor.constraint(equalToConstant: 32).isActive = true
3048
+ rootStack.addArrangedSubview(skeleton)
3049
+ selectorViews.append(skeleton)
3050
+ return
3051
+ }
3052
+ if variant == "loading" && item.data.string("loadingStyle") == "spinner" {
3053
+ rootTopConstraint.constant = 16
3054
+ rootBottomConstraint.constant = -16
3055
+ rootStack.addArrangedSubview(mainStack)
3056
+ mainStack.alignment = .center
3057
+ mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3058
+ let indicator = UIActivityIndicatorView(style: .medium)
3059
+ indicator.color = nativeListColor(theme, "icon", "#0000009B")
3060
+ indicator.startAnimating()
3061
+ mainStack.addArrangedSubview(indicator)
3062
+ selectorViews.append(indicator)
3063
+ return
3064
+ }
3065
+ if isMarket && variant == "noMatch" {
3066
+ rootTopConstraint.constant = 32
3067
+ rootBottomConstraint.constant = -32
3068
+ rootStack.addArrangedSubview(mainStack)
3069
+ mainStack.alignment = .center
3070
+ mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3071
+ titleLabel.font = nativeListFont(ofSize: 16)
3072
+ titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
3073
+ titleLabel.textAlignment = .center
3074
+ show(titleLabel, item.data.string("message"), lines: 1)
3075
+ setLineHeight(titleLabel, text: item.data.string("message"), lineHeight: 24)
3076
+ return
3077
+ }
3078
+ if isMarket && variant == "end" {
3079
+ rootTopConstraint.constant = 16
3080
+ rootBottomConstraint.constant = -16
3081
+ rootStack.addArrangedSubview(mainStack)
3082
+ mainStack.alignment = .center
3083
+ mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3084
+ let indicator = UIStackView()
3085
+ indicator.axis = .horizontal
3086
+ indicator.alignment = .center
3087
+ indicator.spacing = 8
3088
+ for width in [CGFloat(80), 4, 80] {
3089
+ let mark = UIView()
3090
+ mark.translatesAutoresizingMaskIntoConstraints = false
3091
+ mark.backgroundColor = nativeListColor(theme, "separator", "#0000001F")
3092
+ mark.layer.cornerRadius = width == 4 ? 2 : 0
3093
+ NSLayoutConstraint.activate([
3094
+ mark.widthAnchor.constraint(equalToConstant: width),
3095
+ mark.heightAnchor.constraint(equalToConstant: width == 4 ? 4 : 1),
3096
+ ])
3097
+ indicator.addArrangedSubview(mark)
3098
+ }
3099
+ mainStack.addArrangedSubview(indicator)
3100
+ selectorViews.append(indicator)
3101
+ return
3102
+ }
2633
3103
  // OneKey patch: deprecated-wallet warnings stay inside the scrolling list.
2634
3104
  if variant == "warning" {
2635
3105
  rootStack.addArrangedSubview(mainStack)
@@ -2661,10 +3131,10 @@ final class NativeListCell: UICollectionViewCell {
2661
3131
  return
2662
3132
  }
2663
3133
  if variant == "loading" {
2664
- leadingWidth.constant = 40
2665
- leadingHeight.constant = 40
3134
+ leadingWidth.constant = isMarket ? 32 : 40
3135
+ leadingHeight.constant = isMarket ? 32 : 40
2666
3136
  leadingContainer.backgroundColor = nativeListColor(theme, "strongBackground", "#F0F0F0")
2667
- leadingContainer.layer.cornerRadius = 20
3137
+ leadingContainer.layer.cornerRadius = isMarket ? 16 : 20
2668
3138
  rootStack.addArrangedSubview(leadingContainer)
2669
3139
  configureSkeleton(skeletonPrimary, width: 120, height: 12, theme: theme)
2670
3140
  configureSkeleton(skeletonSecondary, width: 80, height: 12, theme: theme)
@@ -3076,8 +3546,8 @@ final class NativeListCell: UICollectionViewCell {
3076
3546
  let paragraphStyle = NSMutableParagraphStyle()
3077
3547
  paragraphStyle.minimumLineHeight = lineHeight
3078
3548
  paragraphStyle.maximumLineHeight = lineHeight
3079
- if currentItem?.data.string("presentation") == "walletSidebar" {
3080
- // OneKey patch: attributed paragraphs must preserve wallet name alignment and tail ellipsis.
3549
+ if currentItem?.type == "market" || currentItem?.data.string("presentation") == "walletSidebar" {
3550
+ // Attributed paragraphs must preserve the label alignment and tail ellipsis.
3081
3551
  paragraphStyle.alignment = label.textAlignment
3082
3552
  paragraphStyle.lineBreakMode = label.lineBreakMode
3083
3553
  }
@@ -3086,15 +3556,15 @@ final class NativeListCell: UICollectionViewCell {
3086
3556
  .foregroundColor: label.textColor as Any,
3087
3557
  .paragraphStyle: paragraphStyle,
3088
3558
  ]
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" {
3559
+ 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
3560
  // OneKey patch: React Native centers font metrics inside explicit line heights.
3091
3561
  let baselineOffset = max(0, (lineHeight - label.font.lineHeight) / 2)
3092
3562
  // 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
3563
+ let isSelectorHeading = lineHeight == 20 && (currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" || currentItem?.type == "market" && label.font.pointSize == 14)
3094
3564
  let scale = window?.screen.scale ?? traitCollection.displayScale
3095
3565
  attributes[.baselineOffset] = isSelectorHeading && scale > 0 ? ceil(baselineOffset * scale) / scale : baselineOffset
3096
3566
  }
3097
- if letterSpacing != 0 { attributes[.kern] = letterSpacing }
3567
+ if letterSpacing != 0 || currentItem?.type == "market" { attributes[.kern] = letterSpacing }
3098
3568
  label.attributedText = NSAttributedString(string: text, attributes: attributes)
3099
3569
  }
3100
3570
 
@@ -3112,8 +3582,10 @@ final class NativeListCell: UICollectionViewCell {
3112
3582
  let isSelectorValue = currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") != "summary"
3113
3583
  let isSelectorSummary = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") == "summary"
3114
3584
  (button as? NativeListAccessoryButton)?.selectorSummaryLineHeight = isSelectorSummary ? lineHeight : nil
3585
+ (button as? NativeListAccessoryButton)?.marketLineHeight = currentItem?.type == "market" ? lineHeight : nil
3115
3586
  // OneKey patch: summary text uses its source line box; currency retains trailing alignment.
3116
- paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary ? .natural : .center
3587
+ // Market's line box already handles alignment; source text starts at its origin.
3588
+ paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary || currentItem?.type == "market" ? .natural : .center
3117
3589
  if isSelectorSummary {
3118
3590
  button.contentHorizontalAlignment = .leading
3119
3591
  button.titleLabel?.textAlignment = .natural
@@ -3122,17 +3594,16 @@ final class NativeListCell: UICollectionViewCell {
3122
3594
  button.contentHorizontalAlignment = .trailing
3123
3595
  button.titleLabel?.textAlignment = .right
3124
3596
  }
3125
- let baselineOffset: CGFloat = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil ? max(0, (lineHeight - font.lineHeight) / 2) : 0
3597
+ 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
3598
+ var attributes: [NSAttributedString.Key: Any] = [
3599
+ .font: font,
3600
+ .foregroundColor: color,
3601
+ .paragraphStyle: paragraphStyle,
3602
+ .baselineOffset: currentItem?.type == "market" && lineHeight == 20 && font.pointSize == 14 ? ceil(baselineOffset * max(1, traitCollection.displayScale)) / max(1, traitCollection.displayScale) : baselineOffset,
3603
+ ]
3604
+ if currentItem?.type == "market" { attributes[.kern] = 0 }
3126
3605
  button.setAttributedTitle(
3127
- NSAttributedString(
3128
- string: text,
3129
- attributes: [
3130
- .font: font,
3131
- .foregroundColor: color,
3132
- .paragraphStyle: paragraphStyle,
3133
- .baselineOffset: baselineOffset,
3134
- ]
3135
- ),
3606
+ NSAttributedString(string: text, attributes: attributes),
3136
3607
  for: .normal
3137
3608
  )
3138
3609
  }
@@ -3394,6 +3865,19 @@ final class NativeListCell: UICollectionViewCell {
3394
3865
  )
3395
3866
  }
3396
3867
 
3868
+ @objc private func marketBadgePressed(_ sender: UIButton) {
3869
+ guard let item = currentItem,
3870
+ marketBadgeActionKeys.indices.contains(sender.tag),
3871
+ let actionKey = marketBadgeActionKeys[sender.tag],
3872
+ !actionKey.isEmpty else { return }
3873
+ onAction?(
3874
+ item,
3875
+ actionKey,
3876
+ nil,
3877
+ actionOrigin(sourceView: sender, source: "marketBadge", slot: sender.tag)
3878
+ )
3879
+ }
3880
+
3397
3881
  @objc private func footerActionPressed(_ sender: UIButton) {
3398
3882
  guard let item = currentItem, footerActionKeys.indices.contains(sender.tag) else { return }
3399
3883
  onAction?(