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

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.
@@ -94,6 +94,8 @@ class NativeListView(
94
94
  private var configuredTopPaddingPx = 0
95
95
  private var configuredBottomPaddingPx = 0
96
96
  private val refreshLayout = SwipeRefreshLayout(context)
97
+ private val refreshIndicatorTravelPx = refreshLayout.progressViewEndOffset
98
+ private var refreshIndicatorOffsetPx = 0
97
99
  private val contentContainer = FrameLayout(context)
98
100
  private val adapter = NativeListAdapter(reactContext)
99
101
  private val layoutManager = GridLayoutManager(context, 1)
@@ -251,6 +253,11 @@ class NativeListView(
251
253
  JSONObject().put("actionKey", "nativeList.refresh"),
252
254
  )
253
255
  }
256
+ recyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
257
+ // OneKey patch: a collapsible pager owns the extra padding above the rows.
258
+ // Keep the refresh control below that header without moving ordinary lists.
259
+ updateRefreshIndicatorOffset((recyclerView.paddingTop - configuredTopPaddingPx).coerceAtLeast(0))
260
+ }
254
261
 
255
262
  recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
256
263
  override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
@@ -274,6 +281,12 @@ class NativeListView(
274
281
 
275
282
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
276
283
  super.onLayout(changed, left, top, right, bottom)
284
+ val first = config?.items?.firstOrNull()
285
+ if (recyclerView.isLayoutRequested && (first?.type == "market" || first?.json?.optString("presentation") == "market")) {
286
+ // A retained Market page can keep cached parent measurements after its diff.
287
+ // Drain the child's pending layout so its committed rows replace the old cells.
288
+ relayoutRecyclerViewImmediately()
289
+ }
277
290
  val nextWidth = right - left
278
291
  val nextHeight = bottom - top
279
292
  if (
@@ -288,6 +301,28 @@ class NativeListView(
288
301
  performPendingScrollIfNeeded()
289
302
  }
290
303
 
304
+ private fun updateRefreshIndicatorOffset(offset: Int) {
305
+ if (refreshIndicatorOffsetPx == offset) return
306
+ refreshIndicatorOffsetPx = offset
307
+ val refreshing = refreshLayout.isRefreshing
308
+ val start = offset - refreshLayout.progressCircleDiameter
309
+ refreshLayout.setProgressViewOffset(false, start, start + refreshIndicatorTravelPx)
310
+ refreshLayout.isRefreshing = refreshing
311
+ }
312
+
313
+ override fun onAttachedToWindow() {
314
+ super.onAttachedToWindow()
315
+ val first = config?.items?.firstOrNull()
316
+ if (recyclerView.isLayoutRequested && (first?.type == "market" || first?.json?.optString("presentation") == "market")) {
317
+ relayoutContents()
318
+ }
319
+ }
320
+
321
+ override fun onDetachedFromWindow() {
322
+ updateRefreshIndicatorOffset(0)
323
+ super.onDetachedFromWindow()
324
+ }
325
+
291
326
  fun applySnapshot(snapshotJson: String) {
292
327
  val next = try {
293
328
  NativeListConfig.parse(snapshotJson)
@@ -1121,7 +1156,12 @@ class NativeListView(
1121
1156
  .put("source", origin.source)
1122
1157
  .put("generation", generation)
1123
1158
  .put("layoutDirection", if (origin.sourceView.layoutDirection == LAYOUT_DIRECTION_RTL) "rtl" else "ltr")
1124
- .also { anchor -> origin.slot?.let { anchor.put("slot", it) } }
1159
+ .also { anchor ->
1160
+ origin.slot?.let { anchor.put("slot", it) }
1161
+ origin.windowPointPixels?.let { point ->
1162
+ anchor.put("windowPoint", JSONObject().put("x", point.x / density).put("y", point.y / density))
1163
+ }
1164
+ }
1125
1165
  }
1126
1166
 
1127
1167
  private fun isOriginValid(origin: NativeListActionOrigin): Boolean =
@@ -79,6 +79,7 @@ final class NativeListActionOrigin {
79
79
  let slot: Int?
80
80
  // OneKey patch: expose the layout slot while preserving the larger hit target.
81
81
  let anchorInset: CGFloat
82
+ var windowPoint: CGPoint?
82
83
 
83
84
  init(
84
85
  sourceView: UIView,
@@ -443,12 +444,17 @@ final class NativeListCell: UICollectionViewCell {
443
444
  private let titleRowStack = UIStackView()
444
445
  private let titleLabel = NativeListDottedUnderlineLabel()
445
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()
446
450
  private let tertiaryLabel = UILabel()
447
451
  private let statusLabel = NativeListInsetLabel()
448
452
  private let metricSubtitleLabel = UILabel()
449
453
  private let metricCompositeStack = UIStackView()
450
454
  private let badgeLabel = NativeListInsetLabel()
451
- private let marketBadgeButtons = (0..<3).map { _ in UIButton(type: .system) }
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) }
452
458
  private let marketBadgeImages = (0..<3).map { _ in OneKeyImageReusableView(frame: .zero) }
453
459
  private let actionStack = UIStackView()
454
460
  private let actionButtons = (0..<3).map { _ in UIButton(type: .system) }
@@ -797,6 +803,19 @@ final class NativeListCell: UICollectionViewCell {
797
803
 
798
804
  override func layoutSubviews() {
799
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
+ }
800
819
  // OneKey patch: extend only the background across the section list outer inset.
801
820
  if currentItem?.data.bool("backgroundFullWidth") == true {
802
821
  selectorFullWidthBackground.frame = CGRect(x: -frame.minX, y: 0, width: superview?.bounds.width ?? bounds.width, height: bounds.height)
@@ -1140,6 +1159,7 @@ final class NativeListCell: UICollectionViewCell {
1140
1159
  }
1141
1160
 
1142
1161
  private func reset() {
1162
+ titleLabel.transform = .identity
1143
1163
  restoreSelectorTypography()
1144
1164
  // OneKey patch: remove selector decorations before rebinding recycled cells.
1145
1165
  selectorViews.forEach { $0.removeFromSuperview() }
@@ -1202,6 +1222,15 @@ final class NativeListCell: UICollectionViewCell {
1202
1222
  headerValueIconImageView.removeFromSuperview()
1203
1223
  headerValueIconImageView.image = nil
1204
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()
1205
1234
  mediaMetadataStack.removeArrangedSubview(subtitleLabel)
1206
1235
  mediaMetadataStack.removeArrangedSubview(mediaNetworkImage)
1207
1236
  mediaMetadataStack.removeFromSuperview()
@@ -1211,6 +1240,7 @@ final class NativeListCell: UICollectionViewCell {
1211
1240
  subtitleLabel.removeFromSuperview()
1212
1241
  mainStack.insertArrangedSubview(titleRowStack, at: 0)
1213
1242
  mainStack.insertArrangedSubview(subtitleLabel, at: 1)
1243
+ mainStack.insertArrangedSubview(tertiaryLabel, at: 2)
1214
1244
  leadingWidth.constant = 40
1215
1245
  leadingHeight.constant = 40
1216
1246
  leadingIconWidth.constant = 18
@@ -1246,6 +1276,8 @@ final class NativeListCell: UICollectionViewCell {
1246
1276
  leadingActionButton.isHidden = true
1247
1277
  leadingActionButton.setImage(nil, for: .normal)
1248
1278
  leadingActionButton.setImage(nil, for: .disabled)
1279
+ leadingActionButton.accessibilityIdentifier = nil
1280
+ leadingActionButton.accessibilityLabel = nil
1249
1281
  unreadDot.isHidden = true
1250
1282
  mediaBadgeLabel.isHidden = true
1251
1283
  mediaBadgeLabel.text = nil
@@ -1265,6 +1297,13 @@ final class NativeListCell: UICollectionViewCell {
1265
1297
  titleLabel.textAlignment = .natural
1266
1298
  subtitleLabel.text = nil
1267
1299
  subtitleLabel.lineBreakMode = .byTruncatingTail
1300
+ subtitleLabel.attributedText = nil
1301
+ subtitleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
1302
+ subtitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
1303
+ tertiaryLabel.attributedText = nil
1304
+ tertiaryLabel.lineBreakMode = .byTruncatingTail
1305
+ tertiaryLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
1306
+ tertiaryLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
1268
1307
  tertiaryLabel.text = nil
1269
1308
  statusLabel.text = nil
1270
1309
  statusLabel.topInset = 0
@@ -1291,6 +1330,15 @@ final class NativeListCell: UICollectionViewCell {
1291
1330
  $0.isHidden = true
1292
1331
  $0.isEnabled = true
1293
1332
  $0.isUserInteractionEnabled = false
1333
+ // OneKey patch: a recycled badge must not retain an attributed title or font.
1334
+ $0.setAttributedTitle(nil, for: .normal)
1335
+ $0.marketLineHeight = nil
1336
+ $0.selectorSummaryLineHeight = nil
1337
+ $0.titleLabel?.font = nativeListFont(ofSize: 11, weight: .medium)
1338
+ $0.titleLabel?.numberOfLines = 1
1339
+ $0.contentHorizontalAlignment = .center
1340
+ $0.accessibilityLabel = nil
1341
+ $0.accessibilityTraits = .staticText
1294
1342
  $0.setTitle(nil, for: .normal)
1295
1343
  $0.setImage(nil, for: .normal)
1296
1344
  $0.setTitleColor(nil, for: .normal)
@@ -2266,6 +2314,28 @@ final class NativeListCell: UICollectionViewCell {
2266
2314
  rootTopConstraint.constant = verticalPadding
2267
2315
  rootBottomConstraint.constant = -verticalPadding
2268
2316
  rootStack.spacing = CGFloat(style?.double("leadingGap", default: variant == "perp" ? 8 : 14) ?? (variant == "perp" ? 8 : 14))
2317
+ if let leadingAction = item.data.dictionary("leadingAction") {
2318
+ leadingActionButton.isHidden = false
2319
+ let tintColor = UIColor(
2320
+ nativeListHex: leadingAction.string("tintColor", default: "#646464"),
2321
+ fallback: .darkGray
2322
+ )
2323
+ leadingActionButton.tintColor = tintColor
2324
+ if let image = nativeListIcon(named: leadingAction.string("name")) {
2325
+ leadingActionButton.setImage(image, for: .normal)
2326
+ leadingActionButton.setImage(
2327
+ image.withTintColor(tintColor, renderingMode: .alwaysOriginal),
2328
+ for: .disabled
2329
+ )
2330
+ }
2331
+ leadingActionButton.isEnabled = !leadingAction.bool("disabled")
2332
+ leadingActionButton.alpha = leadingActionButton.isEnabled ? 1 : 0.4
2333
+ leadingActionButton.accessibilityIdentifier = leadingAction["testID"] as? String
2334
+ leadingActionButton.accessibilityLabel = leadingAction["accessibilityLabel"] as? String
2335
+ leadingActionKey = leadingAction.string("actionKey")
2336
+ rootStack.addArrangedSubview(leadingActionButton)
2337
+ rootStack.setCustomSpacing(5, after: leadingActionButton)
2338
+ }
2269
2339
  leadingWidth.constant = imageWidth
2270
2340
  leadingHeight.constant = imageHeight
2271
2341
  if let visual = marketLeading(item, style: style) {
@@ -2295,15 +2365,29 @@ final class NativeListCell: UICollectionViewCell {
2295
2365
  }
2296
2366
  }
2297
2367
  rootStack.addArrangedSubview(mainStack)
2298
- rootStack.setCustomSpacing(0, after: mainStack)
2368
+ // OneKey patch: opt in to the source Market row's content gap.
2369
+ // rootStack.setCustomSpacing(0, after: mainStack)
2370
+ rootStack.setCustomSpacing(CGFloat(style?.double("contentTrailingGap", default: 0) ?? 0), after: mainStack)
2299
2371
  mainStack.spacing = CGFloat(style?.double("lineGap", default: 0) ?? 0)
2300
2372
  titleRowStack.spacing = CGFloat(style?.double("titleBadgeGap", default: 4) ?? 4)
2373
+ // OneKey patch: opt in without changing the other row templates' filled layout.
2374
+ if style?.string("titleBadgeLayout") == "inline" {
2375
+ mainStack.alignment = .leading
2376
+ titleRowStack.setContentHuggingPriority(.required, for: .horizontal)
2377
+ }
2301
2378
  show(titleLabel, item.data.string("title"), lines: style?.dictionary("title")?.int("lines", default: 1) ?? 1)
2302
2379
  applyMarketTextStyle(titleLabel, data: style?.dictionary("title"), theme: theme, defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, defaultColor: nativeListColor(theme, "primaryText", "#202020"))
2303
2380
  let badges = Array(item.data.dictionaries("badges").prefix(marketBadgeButtons.count))
2304
2381
  marketBadgeActionKeys = badges.map { $0["actionKey"] as? String }
2305
2382
  for (index, badge) in badges.enumerated() {
2306
2383
  let button = marketBadgeButtons[index]
2384
+ let badgeStyle = badge.dictionary("style")
2385
+ let badgeFontSize = CGFloat(badgeStyle?.double("fontSize", default: 11) ?? 11)
2386
+ let badgeFontWeight = marketFontWeight(badgeStyle?.string("fontWeight") ?? "", fallback: .medium)
2387
+ // OneKey patch: SizableText supplies tabular numerals for explicit Market metrics.
2388
+ let badgeFont = badgeStyle == nil
2389
+ ? nativeListFont(ofSize: badgeFontSize, weight: badgeFontWeight)
2390
+ : nativeListTabularFont(ofSize: badgeFontSize, weight: badgeFontWeight)
2307
2391
  let hasBuiltInIcon = badge.string("iconName") == "verified"
2308
2392
  let hasRemoteIcon = badge.dictionary("icon") != nil
2309
2393
  let hasIcon = hasBuiltInIcon || hasRemoteIcon
@@ -2326,6 +2410,12 @@ final class NativeListCell: UICollectionViewCell {
2326
2410
  button.accessibilityTraits = button.isUserInteractionEnabled ? .button : .staticText
2327
2411
  button.setTitle(text, for: .normal)
2328
2412
  button.setTitleColor(foreground, for: .normal)
2413
+ button.titleLabel?.font = badgeFont
2414
+ if let lineHeight = badgeStyle?["lineHeight"] as? Double {
2415
+ setButtonLine(button, text: text, font: badgeFont, color: foreground, lineHeight: CGFloat(lineHeight))
2416
+ // The explicit text-only line box must not cover an adjacent icon.
2417
+ if hasIcon { button.marketLineHeight = nil }
2418
+ }
2329
2419
  button.tintColor = foreground
2330
2420
  button.backgroundColor = UIColor(
2331
2421
  nativeListHex: badge.string("backgroundColor", default: ""),
@@ -2335,7 +2425,12 @@ final class NativeListCell: UICollectionViewCell {
2335
2425
  )
2336
2426
  let iconOnly = hasIcon && text.isEmpty
2337
2427
  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)
2428
+ // OneKey patch: preserve the native defaults unless the caller supplies padding.
2429
+ let padding = CGFloat(badgeStyle?.double("horizontalPadding", default: 5) ?? 5)
2430
+ let hasCustomPadding = badgeStyle?["horizontalPadding"] != nil
2431
+ let leftPadding = hasCustomPadding ? padding + (hasRemoteIcon ? iconSize + 2 : 0) : hasRemoteIcon ? 20 : hasIcon ? 3 : 5
2432
+ // button.contentEdgeInsets = UIEdgeInsets(top: 0, left: iconOnly ? 0 : hasRemoteIcon ? 20 : hasIcon ? 3 : 5, bottom: 0, right: iconOnly ? 0 : 5)
2433
+ button.contentEdgeInsets = UIEdgeInsets(top: 0, left: iconOnly ? 0 : leftPadding, bottom: 0, right: iconOnly ? 0 : padding)
2339
2434
  button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: text.isEmpty ? 0 : 3)
2340
2435
  button.titleEdgeInsets = .zero
2341
2436
  button.imageView?.contentMode = .scaleAspectFit
@@ -2345,11 +2440,15 @@ final class NativeListCell: UICollectionViewCell {
2345
2440
  for: .normal
2346
2441
  )
2347
2442
  }
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))
2443
+ // OneKey patch: match source badge metrics at physical-pixel precision.
2444
+ // let height = button.heightAnchor.constraint(equalToConstant: 18)
2445
+ let height = button.heightAnchor.constraint(equalToConstant: CGFloat(badgeStyle?.double("height", default: 18) ?? 18))
2446
+ let textWidth = (text as NSString).size(withAttributes: [.font: badgeFont]).width
2447
+ let scale = max(1, traitCollection.displayScale)
2448
+ let roundedTextWidth = badgeStyle == nil ? ceil(textWidth) : ceil(textWidth * scale) / scale
2449
+ let extraWidth = hasCustomPadding ? padding * 2 + (hasIcon ? iconSize + 2 : 0) : hasIcon ? iconSize + 11 : 10
2450
+ // let width = button.widthAnchor.constraint(equalToConstant: iconOnly ? iconSize : ceil(textWidth) + (hasIcon ? iconSize + 11 : 10))
2451
+ let width = button.widthAnchor.constraint(equalToConstant: iconOnly ? iconSize : roundedTextWidth + extraWidth)
2353
2452
  NSLayoutConstraint.activate([width, height])
2354
2453
  selectorConstraints.append(contentsOf: [width, height])
2355
2454
  if let icon = badge.dictionary("icon") {
@@ -2370,6 +2469,41 @@ final class NativeListCell: UICollectionViewCell {
2370
2469
  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
2470
  }
2372
2471
  }
2472
+ // OneKey patch: preserve volume width while the localized name truncates.
2473
+ let subtitlePrefix = item.data.dictionary("subtitlePrefix")
2474
+ let subtitlePadding = CGFloat(style?.double("subtitleTrailingPadding", default: 0) ?? 0)
2475
+ if subtitlePrefix != nil || subtitlePadding > 0 {
2476
+ mainStack.removeArrangedSubview(subtitleLabel)
2477
+ subtitleLabel.removeFromSuperview()
2478
+ mainStack.removeArrangedSubview(tertiaryLabel)
2479
+ tertiaryLabel.removeFromSuperview()
2480
+ marketSubtitleStack.axis = .horizontal
2481
+ marketSubtitleStack.alignment = .center
2482
+ marketSubtitleStack.spacing = 0
2483
+ marketSubtitleStack.clipsToBounds = true
2484
+ show(tertiaryLabel, subtitlePrefix?.string("text") ?? "", lines: 1)
2485
+ applyMarketTextStyle(tertiaryLabel, data: subtitlePrefix?.dictionary("style"), theme: theme, defaultSize: 12, defaultLineHeight: 16, defaultWeight: .regular, defaultColor: nativeListColor(theme, "secondaryText", "#646464"))
2486
+ tertiaryLabel.setContentHuggingPriority(.required, for: .horizontal)
2487
+ tertiaryLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
2488
+ subtitleLabel.setContentHuggingPriority(.required, for: .horizontal)
2489
+ subtitleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
2490
+ marketSubtitleStack.addArrangedSubview(tertiaryLabel)
2491
+ marketSubtitleStack.addArrangedSubview(subtitleLabel)
2492
+ marketSubtitleStack.addArrangedSubview(marketSubtitleSpacer)
2493
+ if !tertiaryLabel.isHidden && !subtitleLabel.isHidden {
2494
+ marketSubtitleStack.setCustomSpacing(CGFloat(subtitlePrefix?.double("gap", default: 4) ?? 4), after: tertiaryLabel)
2495
+ }
2496
+ mainStack.insertArrangedSubview(marketSubtitleStack, at: 1)
2497
+ let width = marketSubtitleStack.widthAnchor.constraint(equalTo: mainStack.widthAnchor, constant: -subtitlePadding)
2498
+ width.isActive = true
2499
+ selectorConstraints.append(width)
2500
+ if let maxWidth = subtitlePrefix?["maxWidth"] as? Double {
2501
+ let limit = tertiaryLabel.widthAnchor.constraint(lessThanOrEqualToConstant: CGFloat(maxWidth))
2502
+ limit.isActive = true
2503
+ selectorConstraints.append(limit)
2504
+ }
2505
+ marketSubtitleStack.isHidden = tertiaryLabel.isHidden && subtitleLabel.isHidden
2506
+ }
2373
2507
  rootStack.addArrangedSubview(trailingStack)
2374
2508
  trailingStack.axis = .horizontal
2375
2509
  trailingStack.alignment = .center
@@ -3039,6 +3173,41 @@ final class NativeListCell: UICollectionViewCell {
3039
3173
  rootTopConstraint.constant = 12
3040
3174
  rootBottomConstraint.constant = -12
3041
3175
  }
3176
+ if isMarket && variant == "retry" {
3177
+ let message = item.data.string("message")
3178
+ let text = item.data.string("actionText", default: "Retry")
3179
+ rootStack.axis = .vertical
3180
+ // The source tertiary Button has -5 vertical margins around its 30pt frame.
3181
+ rootStack.spacing = 7
3182
+ rootLeadingConstraint.constant = 32
3183
+ rootTrailingConstraint.constant = -32
3184
+ let height = CGFloat(item.data.double("height", default: message.isEmpty ? 52 : 120))
3185
+ let top = message.isEmpty ? 11 : max(32, (height - 56) / 2)
3186
+ rootTopConstraint.constant = top
3187
+ rootBottomConstraint.constant = -(height - top - (message.isEmpty ? 30 : 61))
3188
+ if !message.isEmpty {
3189
+ show(titleLabel, message, lines: 2)
3190
+ titleLabel.font = nativeListTabularFont(ofSize: 16)
3191
+ titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
3192
+ titleLabel.textAlignment = .center
3193
+ setLineHeight(titleLabel, text: message, lineHeight: 24)
3194
+ rootStack.addArrangedSubview(mainStack)
3195
+ }
3196
+ showAccessory(0, text, action: (item.data.string("actionKey"), nil))
3197
+ let button = accessoryButtons[0]
3198
+ button.backgroundColor = .clear
3199
+ button.layer.cornerRadius = 15
3200
+ setButtonLine(button, text: text, font: nativeListTabularFont(ofSize: 14, weight: .medium),
3201
+ color: nativeListColor(theme, "secondaryText", "#646464"), lineHeight: 20)
3202
+ let textWidth = button.intrinsicContentSize.width
3203
+ selectorConstraints.append(contentsOf: [
3204
+ button.widthAnchor.constraint(equalToConstant: textWidth + 18),
3205
+ button.heightAnchor.constraint(equalToConstant: 30),
3206
+ ])
3207
+ NSLayoutConstraint.activate(selectorConstraints)
3208
+ rootStack.addArrangedSubview(trailingStack)
3209
+ return
3210
+ }
3042
3211
  if variant == "loading" && item.data.string("loadingStyle") == "skeleton" {
3043
3212
  rootLeadingConstraint.constant = 20
3044
3213
  rootTrailingConstraint.constant = -20
@@ -3065,12 +3234,13 @@ final class NativeListCell: UICollectionViewCell {
3065
3234
  return
3066
3235
  }
3067
3236
  if isMarket && variant == "noMatch" {
3068
- rootTopConstraint.constant = 32
3069
- rootBottomConstraint.constant = -32
3237
+ let padding = max(32, (CGFloat(item.data.double("height", default: 88)) - 24) / 2)
3238
+ rootTopConstraint.constant = padding
3239
+ rootBottomConstraint.constant = -padding
3070
3240
  rootStack.addArrangedSubview(mainStack)
3071
3241
  mainStack.alignment = .center
3072
3242
  mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3073
- titleLabel.font = nativeListFont(ofSize: 16)
3243
+ titleLabel.font = nativeListTabularFont(ofSize: 16)
3074
3244
  titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
3075
3245
  titleLabel.textAlignment = .center
3076
3246
  show(titleLabel, item.data.string("message"), lines: 1)
@@ -3080,6 +3250,8 @@ final class NativeListCell: UICollectionViewCell {
3080
3250
  if isMarket && variant == "end" {
3081
3251
  rootTopConstraint.constant = 16
3082
3252
  rootBottomConstraint.constant = -16
3253
+ // OneKey patch: an empty title stack must not consume the dot's line height.
3254
+ titleRowStack.isHidden = true
3083
3255
  rootStack.addArrangedSubview(mainStack)
3084
3256
  mainStack.alignment = .center
3085
3257
  mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
@@ -3558,7 +3730,7 @@ final class NativeListCell: UICollectionViewCell {
3558
3730
  .foregroundColor: label.textColor as Any,
3559
3731
  .paragraphStyle: paragraphStyle,
3560
3732
  ]
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" {
3733
+ 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" {
3562
3734
  // OneKey patch: React Native centers font metrics inside explicit line heights.
3563
3735
  let baselineOffset = max(0, (lineHeight - label.font.lineHeight) / 2)
3564
3736
  // OneKey patch: TextKit's 14/20 headings align their baseline to the upper physical pixel.
@@ -3566,7 +3738,7 @@ final class NativeListCell: UICollectionViewCell {
3566
3738
  let scale = window?.screen.scale ?? traitCollection.displayScale
3567
3739
  attributes[.baselineOffset] = isSelectorHeading && scale > 0 ? ceil(baselineOffset * scale) / scale : baselineOffset
3568
3740
  }
3569
- if letterSpacing != 0 || currentItem?.type == "market" { attributes[.kern] = letterSpacing }
3741
+ if letterSpacing != 0 || currentItem?.type == "market" || currentItem?.data.string("presentation") == "market" { attributes[.kern] = letterSpacing }
3570
3742
  label.attributedText = NSAttributedString(string: text, attributes: attributes)
3571
3743
  }
3572
3744
 
@@ -3581,13 +3753,14 @@ final class NativeListCell: UICollectionViewCell {
3581
3753
  let paragraphStyle = NSMutableParagraphStyle()
3582
3754
  paragraphStyle.minimumLineHeight = lineHeight
3583
3755
  paragraphStyle.maximumLineHeight = lineHeight
3756
+ let isMarketText = currentItem?.type == "market" || currentItem?.type == "system" && currentItem?.data.string("presentation") == "market" && currentItem?.data.string("variant") == "retry"
3584
3757
  let isSelectorValue = currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") != "summary"
3585
3758
  let isSelectorSummary = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") == "summary"
3586
3759
  (button as? NativeListAccessoryButton)?.selectorSummaryLineHeight = isSelectorSummary ? lineHeight : nil
3587
- (button as? NativeListAccessoryButton)?.marketLineHeight = currentItem?.type == "market" ? lineHeight : nil
3760
+ (button as? NativeListAccessoryButton)?.marketLineHeight = isMarketText ? lineHeight : nil
3588
3761
  // OneKey patch: summary text uses its source line box; currency retains trailing alignment.
3589
3762
  // Market's line box already handles alignment; source text starts at its origin.
3590
- paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary || currentItem?.type == "market" ? .natural : .center
3763
+ paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary || isMarketText ? .natural : .center
3591
3764
  if isSelectorSummary {
3592
3765
  button.contentHorizontalAlignment = .leading
3593
3766
  button.titleLabel?.textAlignment = .natural
@@ -3596,14 +3769,14 @@ final class NativeListCell: UICollectionViewCell {
3596
3769
  button.contentHorizontalAlignment = .trailing
3597
3770
  button.titleLabel?.textAlignment = .right
3598
3771
  }
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
3772
+ let baselineOffset: CGFloat = isMarketText || currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil ? max(0, (lineHeight - font.lineHeight) / 2) : 0
3600
3773
  var attributes: [NSAttributedString.Key: Any] = [
3601
3774
  .font: font,
3602
3775
  .foregroundColor: color,
3603
3776
  .paragraphStyle: paragraphStyle,
3604
- .baselineOffset: currentItem?.type == "market" && lineHeight == 20 && font.pointSize == 14 ? ceil(baselineOffset * max(1, traitCollection.displayScale)) / max(1, traitCollection.displayScale) : baselineOffset,
3777
+ .baselineOffset: isMarketText && lineHeight == 20 && font.pointSize == 14 ? ceil(baselineOffset * max(1, traitCollection.displayScale)) / max(1, traitCollection.displayScale) : baselineOffset,
3605
3778
  ]
3606
- if currentItem?.type == "market" { attributes[.kern] = 0 }
3779
+ if isMarketText { attributes[.kern] = 0 }
3607
3780
  button.setAttributedTitle(
3608
3781
  NSAttributedString(string: text, attributes: attributes),
3609
3782
  for: .normal
@@ -1052,6 +1052,7 @@ final class NativeListView: UIView {
1052
1052
  let actionKey = item.data.string("longPressActionKey")
1053
1053
  guard !actionKey.isEmpty else { return }
1054
1054
  let origin = (collectionView.cellForItem(at: indexPath) as? NativeListCell)?.rowActionOrigin()
1055
+ origin?.windowPoint = gesture.location(in: window)
1055
1056
  handleAction(item: item, actionKey: actionKey, target: nil, origin: origin)
1056
1057
  }
1057
1058
 
@@ -1478,6 +1479,9 @@ final class NativeListView: UIView {
1478
1479
  ? "rtl"
1479
1480
  : "ltr",
1480
1481
  ]
1482
+ if let point = origin.windowPoint {
1483
+ anchor["windowPoint"] = ["x": point.x, "y": point.y]
1484
+ }
1481
1485
  if let slot = origin.slot { anchor["slot"] = slot }
1482
1486
  return anchor
1483
1487
  }
@@ -84,9 +84,13 @@ function assertMarketTextStyle(style, path) {
84
84
  }
85
85
  function assertMarketStyle(style, path) {
86
86
  if (!style) return;
87
- for (const field of ['horizontalPadding', 'verticalPadding', 'leadingGap', 'titleBadgeGap', 'trailingGap']) {
87
+ for (const field of ['horizontalPadding', 'verticalPadding', 'leadingGap', 'titleBadgeGap', 'trailingGap', 'contentTrailingGap', 'subtitleTrailingPadding']) {
88
88
  assertBoundedStyleNumber(style[field], `${path}.${field}`, 0, 64);
89
89
  }
90
+ // OneKey patch: validate the opt-in Market badge layout.
91
+ if (style.titleBadgeLayout !== undefined && style.titleBadgeLayout !== 'inline') {
92
+ fail(`${path}.titleBadgeLayout`, 'must be inline when provided');
93
+ }
90
94
  assertBoundedStyleNumber(style.lineGap, `${path}.lineGap`, 0, 16);
91
95
  assertBoundedStyleNumber(style.changeWidth, `${path}.changeWidth`, 1, 160);
92
96
  assertBoundedStyleNumber(style.changeHeight, `${path}.changeHeight`, 1, 160);
@@ -111,8 +115,22 @@ function assertMarketRow(row, path) {
111
115
  }
112
116
  assertText(row.title, `${path}.title`);
113
117
  assertText(row.subtitle, `${path}.subtitle`);
118
+ // OneKey patch: validate the independently laid out Market name.
119
+ if (row.subtitlePrefix) {
120
+ assertText(row.subtitlePrefix.text, `${path}.subtitlePrefix.text`);
121
+ assertBoundedStyleNumber(row.subtitlePrefix.gap, `${path}.subtitlePrefix.gap`, 0, 64);
122
+ assertBoundedStyleNumber(row.subtitlePrefix.maxWidth, `${path}.subtitlePrefix.maxWidth`, 1, 320);
123
+ assertMarketTextStyle(row.subtitlePrefix.style, `${path}.subtitlePrefix.style`);
124
+ }
114
125
  assertText(row.price, `${path}.price`);
115
126
  assertText(row.change.text, `${path}.change.text`);
127
+ assertTrailingAccessories(row.leadingAction ? [row.leadingAction] : undefined, `${path}.leadingAction`);
128
+ if (row.leadingAction) {
129
+ assertText(row.leadingAction.name, `${path}.leadingAction.name`);
130
+ if (row.leadingAction.actionKey !== undefined) {
131
+ assertKey(row.leadingAction.actionKey, `${path}.leadingAction.actionKey`);
132
+ }
133
+ }
116
134
  assertLeadingVisual(row.leading, `${path}.leading`);
117
135
  const assertSegments = (segments, segmentPath) => {
118
136
  segments?.forEach((segment, index) => {
@@ -140,6 +158,10 @@ function assertMarketRow(row, path) {
140
158
  }
141
159
  badgeKeys.add(badge.key);
142
160
  assertText(badge.text, `${badgePath}.text`);
161
+ // OneKey patch: share typography bounds with Market text styles.
162
+ assertMarketTextStyle(badge.style, `${badgePath}.style`);
163
+ assertBoundedStyleNumber(badge.style?.height, `${badgePath}.style.height`, 1, 64);
164
+ assertBoundedStyleNumber(badge.style?.horizontalPadding, `${badgePath}.style.horizontalPadding`, 0, 32);
143
165
  if (badge.iconName !== undefined && badge.iconName !== 'verified') {
144
166
  fail(`${badgePath}.iconName`, 'must be verified when provided');
145
167
  }
@@ -451,6 +473,7 @@ function assertRow(row, index, path = `rows[${index}]`) {
451
473
  }
452
474
  if (row.variant === 'retry') {
453
475
  assertKey(row.actionKey, `${path}.actionKey`);
476
+ assertText(row.actionText, `${path}.actionText`);
454
477
  }
455
478
  break;
456
479
  }
@@ -1747,6 +1747,17 @@ function createMarketRow(context, row) {
1747
1747
  const layoutStyle = resolveWebMarketLayoutStyle(row);
1748
1748
  body.style.padding = String(layoutStyle.verticalPadding) + 'px ' + String(layoutStyle.horizontalPadding) + 'px';
1749
1749
  body.style.gap = '0px';
1750
+ if (row.leadingAction) {
1751
+ const action = createIconAction(context, row.leadingAction.name, row.leadingAction.actionKey, row.leadingAction.disabled, row.leadingAction.tintColor);
1752
+ action.style.flex = '0 0 36px';
1753
+ action.style.width = '36px';
1754
+ action.style.height = '36px';
1755
+ action.style.marginRight = '5px';
1756
+ setData(action, 'testid', row.leadingAction.testID);
1757
+ if (row.leadingAction.accessibilityLabel) action.setAttribute('aria-label', row.leadingAction.accessibilityLabel);
1758
+ markActionAnchorSource(action, 'leadingAction');
1759
+ body.appendChild(action);
1760
+ }
1750
1761
  const visual = createVisual(context, row.leading);
1751
1762
  if (visual) {
1752
1763
  const width = layoutStyle.imageWidth;
@@ -50,7 +50,12 @@ export type MarketRowStyle = Readonly<{
50
50
  /** Space between title and subtitle; 0 by default, bounded to 0..16. */
51
51
  lineGap?: number;
52
52
  titleBadgeGap?: number;
53
+ /** OneKey patch: keep badges next to the intrinsic title width. */
54
+ titleBadgeLayout?: 'inline';
53
55
  trailingGap?: number;
56
+ /** OneKey patch: preserve separate Market content and subtitle insets. */
57
+ contentTrailingGap?: number;
58
+ subtitleTrailingPadding?: number;
54
59
  image?: MarketImageStyle;
55
60
  title?: MarketTextStyle;
56
61
  subtitle?: MarketTextStyle;
@@ -71,6 +76,14 @@ export type MarketBadgeModel = Readonly<{
71
76
  backgroundColor?: string;
72
77
  actionKey?: string;
73
78
  accessibilityLabel?: string;
79
+ /** OneKey patch: optional Market badge metrics; legacy native defaults remain unchanged. */
80
+ style?: Readonly<{
81
+ fontSize?: number;
82
+ fontWeight?: MarketTextStyle['fontWeight'];
83
+ lineHeight?: number;
84
+ height?: number;
85
+ horizontalPadding?: number;
86
+ }>;
74
87
  }>;
75
88
  export type MarketChangeModel = Readonly<{
76
89
  text: string;
@@ -316,9 +329,19 @@ export type DataRow = RowBase & Readonly<{
316
329
  export type MarketRow = RowBase & Readonly<{
317
330
  type: 'market';
318
331
  variant: 'token' | 'stock' | 'perp';
332
+ leadingAction?: Extract<TrailingAccessory, {
333
+ kind: 'icon';
334
+ }>;
319
335
  leading: LeadingVisual;
320
336
  title: string;
321
337
  subtitle?: string;
338
+ /** OneKey patch: localized name shrinks independently of the volume. */
339
+ subtitlePrefix?: Readonly<{
340
+ text: string;
341
+ gap?: number;
342
+ maxWidth?: number;
343
+ style?: MarketTextStyle;
344
+ }>;
322
345
  subtitleSegments?: readonly ValueTextSegment[];
323
346
  price: string;
324
347
  priceSegments?: readonly ValueTextSegment[];
@@ -415,6 +438,7 @@ export type SystemRow = RowBase & (Readonly<{
415
438
  presentation?: 'market';
416
439
  message: string;
417
440
  actionKey: string;
441
+ actionText?: string;
418
442
  }> | Readonly<{
419
443
  type: 'system';
420
444
  variant: 'warning';
@@ -524,7 +548,7 @@ export type RowPatch = Readonly<{
524
548
  }> | Readonly<{
525
549
  type: 'market';
526
550
  key: string;
527
- changes: Partial<Pick<MarketRow, CommonPatchFields | 'leading' | 'title' | 'subtitle' | 'subtitleSegments' | 'price' | 'priceSegments' | 'change' | 'badges' | 'pressActionKey' | 'pressInActionKey' | 'longPressActionKey' | 'diagnostics' | 'style'>>;
551
+ changes: Partial<Pick<MarketRow, CommonPatchFields | 'leadingAction' | 'leading' | 'title' | 'subtitle' | 'subtitleSegments' | 'price' | 'priceSegments' | 'change' | 'badges' | 'pressActionKey' | 'pressInActionKey' | 'longPressActionKey' | 'diagnostics' | 'style'>>;
528
552
  }> | Readonly<{
529
553
  type: 'mediaTile';
530
554
  key: string;
@@ -562,6 +586,11 @@ export type NativeListActionAnchor = Readonly<{
562
586
  token: string;
563
587
  /** Window-relative logical units: CSS px on Web, points on iOS, dp on Android. */
564
588
  windowRect: NativeListWindowRect;
589
+ /** Actual long-press point, in the same logical units as windowRect. */
590
+ windowPoint?: Readonly<{
591
+ x: number;
592
+ y: number;
593
+ }>;
565
594
  source: NativeListActionSource;
566
595
  slot?: number;
567
596
  generation: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-list",
3
- "version": "3.0.115",
3
+ "version": "3.0.117",
4
4
  "description": "Template-driven native RecyclerView and UICollectionView for React Native",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",
@@ -83,8 +83,8 @@
83
83
  "typescript": "^5.9.2"
84
84
  },
85
85
  "peerDependencies": {
86
- "@onekeyfe/react-native-image": "3.0.115",
87
- "@onekeyfe/react-native-native-logger": "3.0.115",
86
+ "@onekeyfe/react-native-image": "3.0.117",
87
+ "@onekeyfe/react-native-native-logger": "3.0.117",
88
88
  "react": "*",
89
89
  "react-native": "*",
90
90
  "react-native-nitro-modules": "0.37.0"