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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,8 @@ import com.facebook.react.uimanager.ThemedReactContext
10
10
  import org.json.JSONObject
11
11
  import java.util.Collections
12
12
  import java.util.WeakHashMap
13
+ import kotlin.math.ceil
14
+ import kotlin.math.roundToInt
13
15
 
14
16
  internal class NativeListAdapter(
15
17
  private val context: ThemedReactContext,
@@ -39,6 +41,8 @@ internal class NativeListAdapter(
39
41
  )
40
42
  val currentList: List<NativeListItem>
41
43
  get() = differ.currentList
44
+ private var marketSourceItems: List<NativeListItem>? = null
45
+ private var marketSourceEdges = DoubleArray(0)
42
46
  private val createdRows = Collections.newSetFromMap(
43
47
  WeakHashMap<NativeListRowView, Boolean>(),
44
48
  )
@@ -122,6 +126,30 @@ internal class NativeListAdapter(
122
126
  fun itemAt(position: Int): NativeListItem? =
123
127
  reorderItems?.getOrNull(position) ?: differ.currentList.getOrNull(position)
124
128
 
129
+ // OneKey patch: Yoga rounds badge dimensions from unrounded cumulative row edges.
130
+ fun marketSourceEdgePx(position: Int): Double {
131
+ val items = reorderItems ?: differ.currentList
132
+ if (marketSourceItems !== items) {
133
+ val density = context.resources.displayMetrics.density.toDouble()
134
+ val edges = DoubleArray(items.size + 1)
135
+ items.forEachIndexed { index, item ->
136
+ val style = item.json.optJSONObject("style")
137
+ val height = item.json.optDouble("height", 0.0)
138
+ val sourceHeight = if (item.type == "market" && style != null) {
139
+ val titleHeight = ceil((style.optJSONObject("title")?.optDouble("lineHeight", 24.0) ?: 24.0) * density) / density
140
+ val subtitleHeight = if (item.json.optString("subtitle").isNotEmpty() || item.json.optJSONObject("subtitlePrefix") != null) {
141
+ ceil((style.optJSONObject("subtitle")?.optDouble("lineHeight", 20.0) ?: 20.0) * density) / density + style.optDouble("lineGap", 0.0)
142
+ } else 0.0
143
+ maxOf(height.roundToInt().toDouble(), titleHeight + subtitleHeight + style.optDouble("verticalPadding", 12.0) * 2).toFloat().toDouble()
144
+ } else height
145
+ edges[index + 1] = edges[index] + sourceHeight * density
146
+ }
147
+ marketSourceItems = items
148
+ marketSourceEdges = edges
149
+ }
150
+ return marketSourceEdges.getOrElse(position) { 0.0 }
151
+ }
152
+
125
153
  fun positionOfKey(key: String): Int =
126
154
  (reorderItems ?: differ.currentList).indexOfFirst { it.key == key }
127
155
 
@@ -169,6 +197,8 @@ internal class NativeListAdapter(
169
197
  }
170
198
 
171
199
  fun dispose() {
200
+ marketSourceItems = null
201
+ marketSourceEdges = DoubleArray(0)
172
202
  reorderItems = null
173
203
  suppressDifferUpdates = false
174
204
  createdRows.forEach(NativeListRowView::dispose)
@@ -45,6 +45,7 @@ import com.facebook.react.uimanager.style.LogicalEdge
45
45
  import com.margelo.nitro.onekeyimage.OneKeyImageReusableView
46
46
  import androidx.core.graphics.PathParser
47
47
  import androidx.core.widget.TextViewCompat
48
+ import androidx.recyclerview.widget.RecyclerView
48
49
  import org.json.JSONArray
49
50
  import org.json.JSONObject
50
51
  import kotlin.math.roundToInt
@@ -119,6 +120,7 @@ internal data class NativeListActionOrigin(
119
120
  val source: String,
120
121
  val slot: Int? = null,
121
122
  val anchorInsetPixels: Int = 0,
123
+ val windowPointPixels: android.graphics.PointF? = null,
122
124
  )
123
125
 
124
126
  /** React Native color strings use CSS #RRGGBBAA ordering; Android expects #AARRGGBB. */
@@ -209,6 +211,8 @@ private class DottedUnderlineTextView(context: android.content.Context) : TextVi
209
211
 
210
212
  private class PackedTitleLineLayout(context: android.content.Context) : LinearLayout(context) {
211
213
  var packsChildrenAtStart = false
214
+ // OneKey patch: optional cap for the Market subtitle's localized name.
215
+ var leadingTextMaxWidth = Int.MAX_VALUE
212
216
 
213
217
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
214
218
  if (!packsChildrenAtStart || childCount < 2) {
@@ -236,7 +240,7 @@ private class PackedTitleLineLayout(context: android.content.Context) : LinearLa
236
240
  val titleMargins = title.layoutParams as MarginLayoutParams
237
241
  if (widthMode != MeasureSpec.UNSPECIFIED) {
238
242
  (title as TextView).maxWidth = (widthSize - paddingLeft - paddingRight - accessoryWidth -
239
- titleMargins.leftMargin - titleMargins.rightMargin).coerceAtLeast(0)
243
+ titleMargins.leftMargin - titleMargins.rightMargin).coerceAtLeast(0).coerceAtMost(leadingTextMaxWidth)
240
244
  }
241
245
  (title.layoutParams as LayoutParams).apply {
242
246
  width = LayoutParams.WRAP_CONTENT
@@ -393,7 +397,16 @@ private class NativeListTableColumnView(context: android.content.Context) : Line
393
397
  internal class NativeListRowView(
394
398
  private val reactContext: ThemedReactContext,
395
399
  ) : LinearLayout(reactContext) {
396
- private val leadingFrame = FrameLayout(context)
400
+ private var marketLeadingUsesSourceClip = false
401
+ private val leadingFrame = object : FrameLayout(context) {
402
+ override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
403
+ if (!marketLeadingUsesSourceClip || child !== leadingImages[0]) return super.drawChild(canvas, child, drawingTime)
404
+ // OneKey patch: clip only the Market avatar to the source border's padding box.
405
+ val saved = canvas.save()
406
+ BackgroundStyleApplicator.clipToPaddingBox(this, canvas)
407
+ return try { super.drawChild(canvas, child, drawingTime) } finally { canvas.restoreToCount(saved) }
408
+ }
409
+ }
397
410
  // OneKey patch: reset selector fragments and corner decorations on every bind.
398
411
  private val selectorViews = mutableListOf<View>()
399
412
  private var selectorUsesSourceScale = false
@@ -405,6 +418,7 @@ internal class NativeListRowView(
405
418
  }
406
419
  private val selectorOriginalFontFeatures = mutableMapOf<TextView, String?>()
407
420
  private val selectorOriginalPaintFlags = mutableMapOf<TextView, Int>()
421
+ private val marketOriginalPaintFlags = mutableMapOf<TextView, Int>()
408
422
  private val selectorLineHeights = mutableMapOf<TextView, Int>()
409
423
  private val selectorFontSizes = mutableMapOf<TextView, Float>()
410
424
  private val selectorImages = mutableListOf<OneKeyImageReusableView>()
@@ -427,6 +441,8 @@ internal class NativeListRowView(
427
441
  private val titleLine = PackedTitleLineLayout(context)
428
442
  private val title = DottedUnderlineTextView(context)
429
443
  private val subtitle = TextView(context)
444
+ // OneKey patch: the first text shrinks while the volume retains its width.
445
+ private val marketSubtitleLine = PackedTitleLineLayout(context)
430
446
  private val tertiary = TextView(context)
431
447
  private val status = TextView(context)
432
448
  private val metricSubtitle = TextView(context)
@@ -479,6 +495,8 @@ internal class NativeListRowView(
479
495
  private var marketLongPressRunnable: Runnable? = null
480
496
  private var marketTouchStartX = 0f
481
497
  private var marketTouchStartY = 0f
498
+ private var marketTouchX = 0f
499
+ private var marketTouchY = 0f
482
500
  private var marketLongPressFired = false
483
501
  private var reorderActive = false
484
502
  private var checkboxCheckedColor = Color.rgb(32, 32, 32)
@@ -610,6 +628,8 @@ internal class NativeListRowView(
610
628
  if (item?.type == "market") {
611
629
  marketTouchStartX = event.x
612
630
  marketTouchStartY = event.y
631
+ marketTouchX = event.x
632
+ marketTouchY = event.y
613
633
  item.json.optString("pressInActionKey").takeIf(String::isNotEmpty)?.let { actionKey ->
614
634
  emitAction(item, actionKey, null, this, "row")
615
635
  }
@@ -620,7 +640,11 @@ internal class NativeListRowView(
620
640
  if (touchPressed && current?.key == expectedKey && current.type == "market") {
621
641
  marketLongPressRunnable = null
622
642
  marketLongPressFired = true
623
- emitAction(current, actionKey, null, this, "row")
643
+ val location = IntArray(2)
644
+ getLocationInWindow(location)
645
+ onAction?.invoke(current, actionKey, null, actionOrigin(this, "row").copy(
646
+ windowPointPixels = android.graphics.PointF(location[0] + marketTouchX, location[1] + marketTouchY),
647
+ ))
624
648
  }
625
649
  }
626
650
  marketLongPressRunnable = runnable
@@ -634,6 +658,8 @@ internal class NativeListRowView(
634
658
  }
635
659
  }
636
660
  MotionEvent.ACTION_MOVE -> {
661
+ marketTouchX = event.x
662
+ marketTouchY = event.y
637
663
  val outside = event.x < 0 || event.y < 0 || event.x >= width || event.y >= height
638
664
  val movedMarket = (tag as? NativeListItem)?.type == "market" &&
639
665
  (kotlin.math.abs(event.x - marketTouchStartX) > dp(10) ||
@@ -713,6 +739,22 @@ internal class NativeListRowView(
713
739
  }
714
740
 
715
741
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
742
+ val marketItem = (tag as? NativeListItem)?.takeIf { it.type == "market" }
743
+ val marketStyle = marketItem?.json?.optJSONObject("style")
744
+ if (marketStyle?.has("horizontalPadding") == true) {
745
+ // OneKey patch: Yoga rounds the two absolute edges, not both padding values.
746
+ val sourcePadding = marketStyle.optDouble("horizontalPadding") * resources.displayMetrics.density
747
+ val width = MeasureSpec.getSize(widthMeasureSpec)
748
+ val rowHeight = selectorHeight
749
+ val sourceVerticalPadding = marketStyle.takeIf { it.has("verticalPadding") && rowHeight != null }
750
+ ?.optDouble("verticalPadding")?.times(resources.displayMetrics.density)
751
+ setPadding(
752
+ sourcePadding.roundToInt(),
753
+ sourceVerticalPadding?.roundToInt() ?: paddingTop,
754
+ width - (width - sourcePadding).roundToInt(),
755
+ if (sourceVerticalPadding != null && rowHeight != null) rowHeight - (rowHeight - sourceVerticalPadding).roundToInt() else paddingBottom,
756
+ )
757
+ }
716
758
  if (isMediaTile) {
717
759
  val availableWidth = (MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight)
718
760
  .coerceAtLeast(0)
@@ -737,6 +779,63 @@ internal class NativeListRowView(
737
779
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
738
780
  super.onLayout(changed, left, top, right, bottom)
739
781
  val item = tag as? NativeListItem ?: return
782
+ if (item.type == "market" && item.json.optJSONObject("style") != null) {
783
+ // OneKey patch: preserve Yoga's half-pixel centering for Market columns and badges.
784
+ for (column in listOf(leadingFrame, mainColumn, trailingColumn)) {
785
+ if (column.parent === this && column.visibility != GONE) {
786
+ column.offsetTopAndBottom((height - column.height + 1) / 2 - column.top)
787
+ }
788
+ }
789
+ // OneKey patch: token avatars stay centered in the row when text rounding adds a pixel.
790
+ if (item.json.optString("variant") != "token" && leadingFrame.parent === this && mainColumn.parent === this) {
791
+ leadingFrame.offsetTopAndBottom(mainColumn.top + (mainColumn.height - leadingFrame.height + 1) / 2 - leadingFrame.top)
792
+ }
793
+ // OneKey patch: preserve Yoga's absolute-edge rounding for the Market network badge.
794
+ if (leadingOverlayBackground.visibility == VISIBLE && item.json.optString("variant") == "token") {
795
+ val recycler = parent as? RecyclerView
796
+ val adapter = recycler?.adapter as? NativeListAdapter
797
+ val position = recycler?.getChildAdapterPosition(this) ?: RecyclerView.NO_POSITION
798
+ if (recycler != null && adapter != null && position != RecyclerView.NO_POSITION && adapter.itemAt(position)?.key == item.key) {
799
+ val density = resources.displayMetrics.density.toDouble()
800
+ val rowTop = adapter.marketSourceEdgePx(position)
801
+ val rowHeight = adapter.marketSourceEdgePx(position + 1) - rowTop
802
+ val imageHeight = item.json.optJSONObject("style")?.optJSONObject("image")?.optDouble("height", 32.0) ?: 32.0
803
+ val contentTop = (recycler.paddingTop / density).roundToInt() * density
804
+ val badgeTop = contentTop + rowTop + (rowHeight - imageHeight * density) / 2 + (imageHeight - 16) * density
805
+ val badgeHeight = (badgeTop + 20 * density).roundToInt() - badgeTop.roundToInt()
806
+ val top = ((imageHeight - 16) * density).roundToInt()
807
+ leadingOverlayBackground.layout(leadingOverlayBackground.left, top, leadingOverlayBackground.right, top + badgeHeight)
808
+ }
809
+ }
810
+ for (badge in marketBadgeViews) {
811
+ if (badge.parent === titleLine && badge.visibility != GONE) {
812
+ badge.offsetTopAndBottom((titleLine.height - badge.height + 1) / 2 - badge.top)
813
+ }
814
+ }
815
+ // OneKey patch: preserve fractional text advances and gaps until the final edge.
816
+ val style = item.json.optJSONObject("style")
817
+ for ((line, gap) in listOf(
818
+ titleLine to style.optDouble("titleBadgeGap", 4.0),
819
+ marketSubtitleLine to (item.json.optJSONObject("subtitlePrefix")?.optDouble("gap", 4.0) ?: 0.0),
820
+ )) {
821
+ var cursor = 0f
822
+ var hasPrevious = false
823
+ for (index in 0 until line.childCount) {
824
+ val child = line.getChildAt(index)
825
+ if (child.visibility == GONE) continue
826
+ if (hasPrevious) {
827
+ cursor += (gap * resources.displayMetrics.density).toFloat()
828
+ child.offsetLeftAndRight(cursor.roundToInt() - child.left)
829
+ }
830
+ val advance = if (index == 0 && child is TextView) {
831
+ val textWidth = if (line === titleLine) child.paint.measureText(child.text.toString()) else child.layout?.getLineWidth(0)
832
+ textWidth?.coerceAtMost(child.width.toFloat()) ?: child.width.toFloat()
833
+ } else child.width.toFloat()
834
+ cursor = if (hasPrevious) cursor + advance else child.left + advance
835
+ hasPrevious = true
836
+ }
837
+ }
838
+ }
740
839
  val accessory = item.json.optJSONArray("trailing")?.optJSONObject(0)
741
840
  if (item.type == "identity" && item.json.has("height") && item.json.optString("presentation") == "accountSelector" && accessory?.optString("kind") == "icon" && accessory.optString("name") == "PlusSmallOutline") {
742
841
  // OneKey patch: the borderless Plus retains the source's fixed top18/negative7 slot.
@@ -785,7 +884,7 @@ internal class NativeListRowView(
785
884
  touchPressed = shouldRestorePressed
786
885
  currentLayout = layout
787
886
  tag = item
788
- selectorUsesSourceScale = item.type == "market" || item.usesSelectorSourceScale || useSourceScale
887
+ selectorUsesSourceScale = item.type == "market" || item.type == "system" && item.json.optString("presentation") == "market" && item.json.optString("variant") == "retry" || item.usesSelectorSourceScale || useSourceScale
789
888
  reorderActive = false
790
889
  leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
791
890
  secondaryImage.prepareForReuse()
@@ -1079,6 +1178,8 @@ internal class NativeListRowView(
1079
1178
  selectorOriginalFontFeatures.clear()
1080
1179
  selectorOriginalPaintFlags.forEach { (view, flags) -> view.paintFlags = flags }
1081
1180
  selectorOriginalPaintFlags.clear()
1181
+ marketOriginalPaintFlags.forEach { (view, flags) -> view.paintFlags = flags }
1182
+ marketOriginalPaintFlags.clear()
1082
1183
  selectorLineHeights.clear()
1083
1184
  selectorFontSizes.clear()
1084
1185
  // OneKey patch: selector-only views cannot survive a recycled binding.
@@ -1095,7 +1196,17 @@ internal class NativeListRowView(
1095
1196
  badge.setOnClickListener(null)
1096
1197
  badge.contentDescription = null
1097
1198
  marketBadgeLabels[index].apply {
1199
+ // OneKey patch: restore every optional Market badge typography property on reuse.
1098
1200
  text = ""
1201
+ layoutParams = wrap()
1202
+ fontFeatureSettings = null
1203
+ textSize = sp(11f)
1204
+ typeface = NativeListFonts.medium(context)
1205
+ setLineSpacing(0f, 1f)
1206
+ includeFontPadding = false
1207
+ maxLines = 1
1208
+ ellipsize = TextUtils.TruncateAt.END
1209
+ gravity = Gravity.START or Gravity.CENTER_VERTICAL
1099
1210
  setTextColor(color(null, "secondaryText", "#0000009B"))
1100
1211
  }
1101
1212
  marketBadgeImages[index].prepareForReuse()
@@ -1140,6 +1251,9 @@ internal class NativeListRowView(
1140
1251
  boundCheckboxData = null
1141
1252
  mainColumn.orientation = VERTICAL
1142
1253
  mainColumn.gravity = Gravity.CENTER_VERTICAL
1254
+ // OneKey patch: remove the Market group before restoring shared labels.
1255
+ marketSubtitleLine.removeAllViews()
1256
+ mainColumn.removeView(marketSubtitleLine)
1143
1257
  mediaMetadataRow.removeView(subtitle)
1144
1258
  mediaMetadataRow.removeView(mediaNetworkImage)
1145
1259
  mainColumn.removeView(mediaMetadataRow)
@@ -1165,6 +1279,10 @@ internal class NativeListRowView(
1165
1279
  badgeLine.text = ""
1166
1280
  title.setLineSpacing(0f, 1f)
1167
1281
  subtitle.setLineSpacing(0f, 1f)
1282
+ tertiary.setLineSpacing(0f, 1f)
1283
+ tertiary.maxWidth = Int.MAX_VALUE
1284
+ tertiary.ellipsize = TextUtils.TruncateAt.END
1285
+ tertiary.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
1168
1286
  title.letterSpacing = 0f
1169
1287
  title.showsDottedUnderline = false
1170
1288
  title.setPadding(0, 0, 0, 0)
@@ -1218,6 +1336,7 @@ internal class NativeListRowView(
1218
1336
  spinner.visibility = GONE
1219
1337
  spinner.layoutParams = LayoutParams(dp(20), dp(20)).apply { gravity = Gravity.END }
1220
1338
  spinner.alpha = 1f
1339
+ marketLeadingUsesSourceClip = false
1221
1340
  leadingFrame.visibility = GONE
1222
1341
  leadingFrame.background = null
1223
1342
  leadingFrame.clipChildren = false
@@ -1932,6 +2051,25 @@ internal class NativeListRowView(
1932
2051
  )
1933
2052
  }
1934
2053
 
2054
+ // OneKey patch: opt-in Market text uses the same pixel rounding and line box as RN.
2055
+ private fun applyMarketTextMetrics(view: TextView, style: JSONObject?) {
2056
+ if (style == null || view.visibility != VISIBLE || view.text.isEmpty()) return
2057
+ marketOriginalPaintFlags.putIfAbsent(view, view.paintFlags)
2058
+ view.paintFlags = view.paintFlags or Paint.SUBPIXEL_TEXT_FLAG or Paint.LINEAR_TEXT_FLAG
2059
+ if (style.has("fontSize")) {
2060
+ val sourceSize = sp(style.optDouble("fontSize").toFloat()) * resources.displayMetrics.density
2061
+ view.setTextSize(TypedValue.COMPLEX_UNIT_PX, kotlin.math.ceil(sourceSize.toDouble()).toFloat())
2062
+ }
2063
+ val text = SpannableStringBuilder(view.text)
2064
+ text.getSpans(0, text.length, SelectorLineHeightSpan::class.java).forEach(text::removeSpan)
2065
+ if (style.has("lineHeight")) {
2066
+ val lineHeight = kotlin.math.ceil(style.optDouble("lineHeight") * (if (selectorUsesSourceScale) 1f else NativeListScale.factor(resources)) * resources.displayMetrics.density).toInt()
2067
+ text.setSpan(SelectorLineHeightSpan(lineHeight), 0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
2068
+ view.setLineSpacing(0f, 1f)
2069
+ }
2070
+ view.text = text
2071
+ }
2072
+
1935
2073
  private fun marketText(value: String, segments: JSONArray?, fontSize: Float): CharSequence {
1936
2074
  if (segments == null || segments.length() == 0) return value
1937
2075
  val result = SpannableStringBuilder()
@@ -1989,7 +2127,11 @@ internal class NativeListRowView(
1989
2127
  cornerRadiusDp = cornerRadius,
1990
2128
  )
1991
2129
 
1992
- addView(mainColumn, weighted())
2130
+ // OneKey patch: retain the source gap without changing other templates.
2131
+ // addView(mainColumn, weighted())
2132
+ addView(mainColumn, weighted().apply {
2133
+ marginEnd = dp(style?.optDouble("contentTrailingGap", 0.0)?.roundToInt() ?: 0)
2134
+ })
1993
2135
  titleLine.packsChildrenAtStart = true
1994
2136
  showText(title, item.json.optString("title"), style?.optJSONObject("title")?.optInt("lines", 1) ?: 1)
1995
2137
  applyMarketTextStyle(
@@ -2008,6 +2150,7 @@ internal class NativeListRowView(
2008
2150
  val badge = badges.getJSONObject(index)
2009
2151
  val badgeView = marketBadgeViews[index]
2010
2152
  val badgeLabel = marketBadgeLabels[index]
2153
+ val badgeStyle = badge.optJSONObject("style")
2011
2154
  val hasGlyph = badge.optString("iconName") == "verified"
2012
2155
  val remoteIcon = badge.optJSONObject("icon")
2013
2156
  val hasIcon = hasGlyph || remoteIcon != null
@@ -2022,15 +2165,28 @@ internal class NativeListRowView(
2022
2165
  val foreground = safeColor(badge.optString("textColor"), toneColor)
2023
2166
  badgeView.visibility = VISIBLE
2024
2167
  val iconOnly = hasIcon && text.isEmpty()
2025
- badgeView.setPadding(dp(if (iconOnly) 0 else if (hasIcon) 2 else 5), 0, dp(if (iconOnly) 0 else 5), 0)
2168
+ // OneKey patch: Market callers can request the original badge padding.
2169
+ val padding = badgeStyle?.optDouble("horizontalPadding", 5.0)?.roundToInt() ?: 5
2170
+ val leftPadding = if (badgeStyle?.has("horizontalPadding") == true) padding else if (hasIcon) 2 else 5
2171
+ // badgeView.setPadding(dp(if (iconOnly) 0 else if (hasIcon) 2 else 5), 0, dp(if (iconOnly) 0 else 5), 0)
2172
+ badgeView.setPadding(dp(if (iconOnly) 0 else leftPadding), 0, dp(if (iconOnly) 0 else padding), 0)
2026
2173
  badgeView.background = roundedFill(
2027
2174
  safeColor(
2028
2175
  badge.optString("backgroundColor"),
2029
2176
  if (hasIcon && text.isEmpty()) Color.TRANSPARENT else color(theme, "strongBackground", "#0000000F"),
2030
2177
  ),
2031
2178
  4f,
2032
- )
2179
+ ).apply {
2180
+ if (badgeStyle != null) this.cornerRadius = 4f * resources.displayMetrics.density
2181
+ }
2033
2182
  badgeLabel.text = text
2183
+ // OneKey patch: match SizableText's tabular numerals only for explicit Market metrics.
2184
+ badgeLabel.fontFeatureSettings = if (badgeStyle != null) "tnum" else null
2185
+ badgeLabel.textSize = sp(badgeStyle?.optDouble("fontSize", 11.0)?.toFloat() ?: 11f)
2186
+ badgeLabel.typeface = marketTypeface(badgeStyle?.optString("fontWeight").orEmpty(), "medium")
2187
+ if (badgeStyle?.has("lineHeight") == true) {
2188
+ TextViewCompat.setLineHeight(badgeLabel, dp(badgeStyle.optDouble("lineHeight").roundToInt()))
2189
+ }
2034
2190
  badgeLabel.setTextColor(foreground)
2035
2191
  badgeLabel.visibility = if (text.isEmpty()) GONE else VISIBLE
2036
2192
  if (hasGlyph) {
@@ -2056,7 +2212,9 @@ internal class NativeListRowView(
2056
2212
  badgeView.contentDescription = badge.optString("accessibilityLabel", text)
2057
2213
  titleLine.addView(
2058
2214
  badgeView,
2059
- LayoutParams(LayoutParams.WRAP_CONTENT, dp(18)).apply { marginStart = dp(titleBadgeGap) },
2215
+ // OneKey patch: preserve 18dp unless the Market row opts into source metrics.
2216
+ // LayoutParams(LayoutParams.WRAP_CONTENT, dp(18)).apply { marginStart = dp(titleBadgeGap) },
2217
+ LayoutParams(LayoutParams.WRAP_CONTENT, dp(badgeStyle?.optDouble("height", 18.0)?.roundToInt() ?: 18)).apply { marginStart = dp(titleBadgeGap) },
2060
2218
  )
2061
2219
  }
2062
2220
  }
@@ -2082,6 +2240,31 @@ internal class NativeListRowView(
2082
2240
  style?.optJSONObject("subtitle")?.optDouble("fontSize", 14.0)?.toFloat() ?: 14f,
2083
2241
  )
2084
2242
  }
2243
+ // OneKey patch: preserve independent name metrics, truncation, and volume.
2244
+ val subtitlePrefix = item.json.optJSONObject("subtitlePrefix")
2245
+ val subtitlePadding = style?.optDouble("subtitleTrailingPadding", 0.0)?.roundToInt() ?: 0
2246
+ if (subtitlePrefix != null || subtitlePadding > 0) {
2247
+ mainColumn.removeView(subtitle)
2248
+ mainColumn.removeView(tertiary)
2249
+ marketSubtitleLine.orientation = HORIZONTAL
2250
+ marketSubtitleLine.gravity = Gravity.CENTER_VERTICAL
2251
+ marketSubtitleLine.packsChildrenAtStart = true
2252
+ marketSubtitleLine.leadingTextMaxWidth = subtitlePrefix?.takeIf { it.has("maxWidth") }
2253
+ ?.optDouble("maxWidth")?.roundToInt()?.let(::dp) ?: Int.MAX_VALUE
2254
+ marketSubtitleLine.setPadding(0, 0, dp(subtitlePadding), 0)
2255
+ showText(tertiary, subtitlePrefix?.optString("text") ?: "", 1)
2256
+ applyMarketTextStyle(tertiary, subtitlePrefix?.optJSONObject("style"), 12f, 16, "regular", color(theme, "secondaryText", "#646464"), "start")
2257
+ marketSubtitleLine.addView(tertiary, wrap())
2258
+ marketSubtitleLine.addView(subtitle, wrap().apply {
2259
+ if (tertiary.visibility == VISIBLE && subtitle.visibility == VISIBLE) {
2260
+ marginStart = dp(subtitlePrefix?.optDouble("gap", 4.0)?.roundToInt() ?: 4)
2261
+ }
2262
+ })
2263
+ mainColumn.addView(marketSubtitleLine, 1, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
2264
+ topMargin = dp(style?.optDouble("lineGap", 0.0)?.roundToInt() ?: 0)
2265
+ })
2266
+ marketSubtitleLine.visibility = if (tertiary.visibility == VISIBLE || subtitle.visibility == VISIBLE) VISIBLE else GONE
2267
+ }
2085
2268
  trailingColumn.orientation = HORIZONTAL
2086
2269
  trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
2087
2270
  addView(trailingColumn, wrap())
@@ -2150,6 +2333,24 @@ internal class NativeListRowView(
2150
2333
  dp(style?.optDouble("changeWidth", 80.0)?.roundToInt() ?: 80),
2151
2334
  dp(style?.optDouble("changeHeight", 32.0)?.roundToInt() ?: 32),
2152
2335
  )
2336
+ applyMarketTextMetrics(title, style?.optJSONObject("title"))
2337
+ applyMarketTextMetrics(subtitle, style?.optJSONObject("subtitle"))
2338
+ applyMarketTextMetrics(tertiary, item.json.optJSONObject("subtitlePrefix")?.optJSONObject("style"))
2339
+ applyMarketTextMetrics(price, priceStyle)
2340
+ applyMarketTextMetrics(change, changeStyle)
2341
+ val badges = item.json.optJSONArray("badges")
2342
+ marketBadgeLabels.forEachIndexed { index, label ->
2343
+ val badge = badges?.optJSONObject(index)
2344
+ val badgeStyle = badge?.optJSONObject("style")
2345
+ applyMarketTextMetrics(label, badgeStyle)
2346
+ if (badge != null && label.visibility == VISIBLE && badgeStyle?.has("horizontalPadding") == true &&
2347
+ badge.optJSONObject("icon") == null && badge.optString("iconName").isEmpty()) {
2348
+ // OneKey patch: match RN's intrinsic text width and one rounded padding pair.
2349
+ label.layoutParams = wrap().apply { width = label.paint.measureText(label.text.toString()).roundToInt() }
2350
+ val padding = badgeStyle.optDouble("horizontalPadding") * resources.displayMetrics.density
2351
+ marketBadgeViews[index].setPadding(kotlin.math.floor(padding).toInt(), 0, (padding * 2).roundToInt() - kotlin.math.floor(padding).toInt(), 0)
2352
+ }
2353
+ }
2153
2354
  contentDescription = item.json.optString(
2154
2355
  "accessibilityLabel",
2155
2356
  listOf(
@@ -2817,16 +3018,41 @@ internal class NativeListRowView(
2817
3018
  )
2818
3019
  return
2819
3020
  }
3021
+ if (isMarket && variant == "retry") {
3022
+ val message = item.json.optString("message")
3023
+ orientation = VERTICAL
3024
+ gravity = Gravity.CENTER
3025
+ setPadding(dp(32), dp(if (message.isEmpty()) 11 else 32), dp(32), dp(if (message.isEmpty()) 11 else 27))
3026
+ if (message.isNotEmpty()) {
3027
+ showText(title, message, 2)
3028
+ val style = JSONObject().put("fontSize", 16).put("lineHeight", 24)
3029
+ applyMarketTextStyle(title, style, 16f, 24, "regular", color(theme, "secondaryText", "#0000009B"), "center")
3030
+ applyMarketTextMetrics(title, style)
3031
+ addView(mainColumn, wrap().apply { bottomMargin = kotlin.math.ceil(7.0 * resources.displayMetrics.density).toInt() })
3032
+ }
3033
+ showTrailing(0, item.json.optString("actionText", "Retry"), true, item.json.optString("actionKey"))
3034
+ val button = trailingViews[0]
3035
+ val style = JSONObject().put("fontSize", 14).put("lineHeight", 20)
3036
+ applyMarketTextStyle(button, style, 14f, 20, "medium", color(theme, "secondaryText", "#0000009B"), "center")
3037
+ applyMarketTextMetrics(button, style)
3038
+ button.background = null
3039
+ // Match Yoga's text rounding before adding the tertiary Button's border/padding.
3040
+ val density = resources.displayMetrics.density
3041
+ val buttonWidth = (kotlin.math.ceil(button.paint.measureText(button.text.toString()).toDouble()) + 18 * density).roundToInt()
3042
+ val buttonHeight = kotlin.math.ceil(kotlin.math.ceil(20.0 * density) + 10 * density).toInt()
3043
+ button.setPadding(0, 0, 0, 0)
3044
+ button.layoutParams = LayoutParams(buttonWidth, buttonHeight)
3045
+ addView(trailingColumn, wrap())
3046
+ return
3047
+ }
2820
3048
  if (isMarket && variant == "noMatch") {
2821
3049
  gravity = Gravity.CENTER
2822
3050
  setPadding(dp(32), dp(32), dp(32), dp(32))
2823
- title.textSize = sp(16f)
2824
- title.typeface = NativeListFonts.regular(context)
2825
- title.gravity = Gravity.CENTER
2826
- title.setTextColor(color(theme, "secondaryText", "#0000009B"))
2827
- TextViewCompat.setLineHeight(title, dp(24))
2828
3051
  showText(title, item.json.optString("message"), 1)
2829
- addView(mainColumn, weighted())
3052
+ val style = JSONObject().put("fontSize", 16).put("lineHeight", 24)
3053
+ applyMarketTextStyle(title, style, 16f, 24, "regular", color(theme, "secondaryText", "#0000009B"), "center")
3054
+ applyMarketTextMetrics(title, style)
3055
+ addView(mainColumn, wrap())
2830
3056
  return
2831
3057
  }
2832
3058
  // OneKey patch: warning title/description wrap inside the actual scroll content.
@@ -2972,12 +3198,19 @@ internal class NativeListRowView(
2972
3198
  if (tokenPair) {
2973
3199
  leadingOverlayBackground.visibility = VISIBLE
2974
3200
  leadingOverlayBackground.background = roundedFill(visualBackdropColor, 10f)
3201
+ val item = tag as? NativeListItem
3202
+ val marketPadding = if (item?.type == "market") item.json.optJSONObject("style")?.optDouble("horizontalPadding", 20.0) ?: 20.0 else null
3203
+ val density = resources.displayMetrics.density
3204
+ // Yoga rounds the absolute horizontal edges, including the avatar's fractional origin.
3205
+ val badgeRight = marketPadding?.let { ((it + sizeDp + 4) * density).roundToInt() }
3206
+ val badgeLeft = marketPadding?.let { ((it + sizeDp - 16) * density).roundToInt() }
3207
+ val avatarRight = marketPadding?.let { ((it + sizeDp) * density).roundToInt() }
2975
3208
  leadingOverlayBackground.layoutParams = FrameLayout.LayoutParams(
2976
- dp(20),
3209
+ if (badgeRight != null && badgeLeft != null) badgeRight - badgeLeft else dp(20),
2977
3210
  dp(20),
2978
3211
  Gravity.END or Gravity.BOTTOM,
2979
3212
  ).apply {
2980
- marginEnd = -dp(4)
3213
+ marginEnd = if (badgeRight != null && avatarRight != null) avatarRight - badgeRight else -dp(4)
2981
3214
  bottomMargin = -dp(4)
2982
3215
  }
2983
3216
  }
@@ -3019,21 +3252,26 @@ internal class NativeListRowView(
3019
3252
  image.clipToOutline = true
3020
3253
  if ((tag as? NativeListItem)?.type == "market" && index == 0 && visual.optString("borderColor").isNotEmpty()) {
3021
3254
  val inset = dp(1)
3022
- val radius = scaledDp(cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)))
3023
- leadingFrame.background = GradientDrawable().apply {
3024
- setColor(visualBackground)
3025
- setStroke(inset, safeColor(visual.optString("borderColor"), Color.TRANSPARENT))
3026
- this.cornerRadius = radius
3255
+ val density = resources.displayMetrics.density
3256
+ val sourceRadius = cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp))
3257
+ // OneKey patch: retain the source's fractional border and background rendering.
3258
+ leadingFrame.background = null
3259
+ BackgroundStyleApplicator.setBackgroundColor(leadingFrame, visualBackground)
3260
+ // Match the source's physical edges: React Native rasterizes ALL as a different stroke path.
3261
+ for (edge in arrayOf(LogicalEdge.LEFT, LogicalEdge.TOP, LogicalEdge.RIGHT, LogicalEdge.BOTTOM)) {
3262
+ BackgroundStyleApplicator.setBorderWidth(leadingFrame, edge, 1f)
3263
+ BackgroundStyleApplicator.setBorderColor(leadingFrame, edge, safeColor(visual.optString("borderColor"), Color.TRANSPARENT))
3027
3264
  }
3028
- image.layoutParams = FrameLayout.LayoutParams(dp(sizeDp) - inset * 2, dp(heightDp) - inset * 2).apply {
3265
+ BackgroundStyleApplicator.setBorderRadius(leadingFrame, BorderRadiusProp.BORDER_RADIUS, LengthPercentage(sourceRadius, LengthPercentageType.POINT))
3266
+ // OneKey patch: round the source image's absolute edges inside its border.
3267
+ val sourceLeft = ((tag as? NativeListItem)?.json?.optJSONObject("style")?.optDouble("horizontalPadding", 20.0) ?: 20.0) * density
3268
+ val imageWidth = (sourceLeft + (sizeDp - 1) * density).roundToInt() - (sourceLeft + density).roundToInt()
3269
+ image.layoutParams = FrameLayout.LayoutParams(imageWidth, dp(heightDp - 2)).apply {
3029
3270
  leftMargin = inset
3030
3271
  topMargin = inset
3031
3272
  }
3032
- image.outlineProvider = object : ViewOutlineProvider() {
3033
- override fun getOutline(view: View, outline: Outline) {
3034
- outline.setRoundRect(-inset, -inset, view.width + inset, view.height + inset, radius)
3035
- }
3036
- }
3273
+ marketLeadingUsesSourceClip = true
3274
+ image.clipToOutline = false
3037
3275
  }
3038
3276
  val fallbackIcon = if (index == 0) visual.optJSONObject("fallbackIcon") else null
3039
3277
  val expectedEpoch = bindingEpoch
@@ -3497,6 +3735,13 @@ internal class NativeListRowView(
3497
3735
  }
3498
3736
 
3499
3737
  private fun applySize(item: NativeListItem) {
3738
+ if (item.type == "system" && item.json.optString("presentation") == "market" &&
3739
+ item.json.optString("variant") in setOf("retry", "noMatch")) {
3740
+ val defaultHeight = if (item.json.optString("variant") == "noMatch") 88.0 else if (item.json.optString("message").isEmpty()) 52.0 else 120.0
3741
+ minimumHeight = (item.json.optDouble("height", defaultHeight).toFloat() * resources.displayMetrics.density).roundToInt()
3742
+ selectorHeight = if (item.json.has("height")) minimumHeight else null
3743
+ return
3744
+ }
3500
3745
  if (item.type == "market") {
3501
3746
  val style = item.json.optJSONObject("style")
3502
3747
  val imageHeight = style?.optJSONObject("image")?.optDouble(
@@ -3505,12 +3750,15 @@ internal class NativeListRowView(
3505
3750
  ) ?: if (item.json.optString("variant") == "stock") 40.0 else 32.0
3506
3751
  val verticalPadding = style?.optDouble("verticalPadding", 12.0) ?: 12.0
3507
3752
  val defaultHeight = if (item.json.optString("variant") == "stock") 72.0 else 68.0
3508
- minimumHeight = dp(
3509
- item.json.optDouble(
3510
- "height",
3511
- maxOf(defaultHeight, imageHeight + verticalPadding * 2),
3512
- ).roundToInt(),
3753
+ // OneKey patch: preserve fractional DP until the final physical pixel edge.
3754
+ val rowHeight = item.json.optDouble(
3755
+ "height",
3756
+ maxOf(defaultHeight, imageHeight + verticalPadding * 2),
3513
3757
  )
3758
+ val sourceScale = if (selectorUsesSourceScale) 1f else NativeListScale.factor(resources)
3759
+ minimumHeight = (rowHeight * sourceScale * resources.displayMetrics.density).roundToInt()
3760
+ // OneKey patch: explicit Market row heights must not grow after child pixel rounding.
3761
+ selectorHeight = if (item.json.has("height")) minimumHeight else null
3514
3762
  return
3515
3763
  }
3516
3764
  val isWalletSidebar =
@@ -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
@@ -1265,6 +1295,13 @@ final class NativeListCell: UICollectionViewCell {
1265
1295
  titleLabel.textAlignment = .natural
1266
1296
  subtitleLabel.text = nil
1267
1297
  subtitleLabel.lineBreakMode = .byTruncatingTail
1298
+ subtitleLabel.attributedText = nil
1299
+ subtitleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
1300
+ subtitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
1301
+ tertiaryLabel.attributedText = nil
1302
+ tertiaryLabel.lineBreakMode = .byTruncatingTail
1303
+ tertiaryLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
1304
+ tertiaryLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
1268
1305
  tertiaryLabel.text = nil
1269
1306
  statusLabel.text = nil
1270
1307
  statusLabel.topInset = 0
@@ -1291,6 +1328,15 @@ final class NativeListCell: UICollectionViewCell {
1291
1328
  $0.isHidden = true
1292
1329
  $0.isEnabled = true
1293
1330
  $0.isUserInteractionEnabled = false
1331
+ // OneKey patch: a recycled badge must not retain an attributed title or font.
1332
+ $0.setAttributedTitle(nil, for: .normal)
1333
+ $0.marketLineHeight = nil
1334
+ $0.selectorSummaryLineHeight = nil
1335
+ $0.titleLabel?.font = nativeListFont(ofSize: 11, weight: .medium)
1336
+ $0.titleLabel?.numberOfLines = 1
1337
+ $0.contentHorizontalAlignment = .center
1338
+ $0.accessibilityLabel = nil
1339
+ $0.accessibilityTraits = .staticText
1294
1340
  $0.setTitle(nil, for: .normal)
1295
1341
  $0.setImage(nil, for: .normal)
1296
1342
  $0.setTitleColor(nil, for: .normal)
@@ -2295,15 +2341,29 @@ final class NativeListCell: UICollectionViewCell {
2295
2341
  }
2296
2342
  }
2297
2343
  rootStack.addArrangedSubview(mainStack)
2298
- rootStack.setCustomSpacing(0, after: mainStack)
2344
+ // OneKey patch: opt in to the source Market row's content gap.
2345
+ // rootStack.setCustomSpacing(0, after: mainStack)
2346
+ rootStack.setCustomSpacing(CGFloat(style?.double("contentTrailingGap", default: 0) ?? 0), after: mainStack)
2299
2347
  mainStack.spacing = CGFloat(style?.double("lineGap", default: 0) ?? 0)
2300
2348
  titleRowStack.spacing = CGFloat(style?.double("titleBadgeGap", default: 4) ?? 4)
2349
+ // OneKey patch: opt in without changing the other row templates' filled layout.
2350
+ if style?.string("titleBadgeLayout") == "inline" {
2351
+ mainStack.alignment = .leading
2352
+ titleRowStack.setContentHuggingPriority(.required, for: .horizontal)
2353
+ }
2301
2354
  show(titleLabel, item.data.string("title"), lines: style?.dictionary("title")?.int("lines", default: 1) ?? 1)
2302
2355
  applyMarketTextStyle(titleLabel, data: style?.dictionary("title"), theme: theme, defaultSize: 16, defaultLineHeight: 24, defaultWeight: .medium, defaultColor: nativeListColor(theme, "primaryText", "#202020"))
2303
2356
  let badges = Array(item.data.dictionaries("badges").prefix(marketBadgeButtons.count))
2304
2357
  marketBadgeActionKeys = badges.map { $0["actionKey"] as? String }
2305
2358
  for (index, badge) in badges.enumerated() {
2306
2359
  let button = marketBadgeButtons[index]
2360
+ let badgeStyle = badge.dictionary("style")
2361
+ let badgeFontSize = CGFloat(badgeStyle?.double("fontSize", default: 11) ?? 11)
2362
+ let badgeFontWeight = marketFontWeight(badgeStyle?.string("fontWeight") ?? "", fallback: .medium)
2363
+ // OneKey patch: SizableText supplies tabular numerals for explicit Market metrics.
2364
+ let badgeFont = badgeStyle == nil
2365
+ ? nativeListFont(ofSize: badgeFontSize, weight: badgeFontWeight)
2366
+ : nativeListTabularFont(ofSize: badgeFontSize, weight: badgeFontWeight)
2307
2367
  let hasBuiltInIcon = badge.string("iconName") == "verified"
2308
2368
  let hasRemoteIcon = badge.dictionary("icon") != nil
2309
2369
  let hasIcon = hasBuiltInIcon || hasRemoteIcon
@@ -2326,6 +2386,12 @@ final class NativeListCell: UICollectionViewCell {
2326
2386
  button.accessibilityTraits = button.isUserInteractionEnabled ? .button : .staticText
2327
2387
  button.setTitle(text, for: .normal)
2328
2388
  button.setTitleColor(foreground, for: .normal)
2389
+ button.titleLabel?.font = badgeFont
2390
+ if let lineHeight = badgeStyle?["lineHeight"] as? Double {
2391
+ setButtonLine(button, text: text, font: badgeFont, color: foreground, lineHeight: CGFloat(lineHeight))
2392
+ // The explicit text-only line box must not cover an adjacent icon.
2393
+ if hasIcon { button.marketLineHeight = nil }
2394
+ }
2329
2395
  button.tintColor = foreground
2330
2396
  button.backgroundColor = UIColor(
2331
2397
  nativeListHex: badge.string("backgroundColor", default: ""),
@@ -2335,7 +2401,12 @@ final class NativeListCell: UICollectionViewCell {
2335
2401
  )
2336
2402
  let iconOnly = hasIcon && text.isEmpty
2337
2403
  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)
2404
+ // OneKey patch: preserve the native defaults unless the caller supplies padding.
2405
+ let padding = CGFloat(badgeStyle?.double("horizontalPadding", default: 5) ?? 5)
2406
+ let hasCustomPadding = badgeStyle?["horizontalPadding"] != nil
2407
+ let leftPadding = hasCustomPadding ? padding + (hasRemoteIcon ? iconSize + 2 : 0) : hasRemoteIcon ? 20 : hasIcon ? 3 : 5
2408
+ // button.contentEdgeInsets = UIEdgeInsets(top: 0, left: iconOnly ? 0 : hasRemoteIcon ? 20 : hasIcon ? 3 : 5, bottom: 0, right: iconOnly ? 0 : 5)
2409
+ button.contentEdgeInsets = UIEdgeInsets(top: 0, left: iconOnly ? 0 : leftPadding, bottom: 0, right: iconOnly ? 0 : padding)
2339
2410
  button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: text.isEmpty ? 0 : 3)
2340
2411
  button.titleEdgeInsets = .zero
2341
2412
  button.imageView?.contentMode = .scaleAspectFit
@@ -2345,11 +2416,15 @@ final class NativeListCell: UICollectionViewCell {
2345
2416
  for: .normal
2346
2417
  )
2347
2418
  }
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))
2419
+ // OneKey patch: match source badge metrics at physical-pixel precision.
2420
+ // let height = button.heightAnchor.constraint(equalToConstant: 18)
2421
+ let height = button.heightAnchor.constraint(equalToConstant: CGFloat(badgeStyle?.double("height", default: 18) ?? 18))
2422
+ let textWidth = (text as NSString).size(withAttributes: [.font: badgeFont]).width
2423
+ let scale = max(1, traitCollection.displayScale)
2424
+ let roundedTextWidth = badgeStyle == nil ? ceil(textWidth) : ceil(textWidth * scale) / scale
2425
+ let extraWidth = hasCustomPadding ? padding * 2 + (hasIcon ? iconSize + 2 : 0) : hasIcon ? iconSize + 11 : 10
2426
+ // let width = button.widthAnchor.constraint(equalToConstant: iconOnly ? iconSize : ceil(textWidth) + (hasIcon ? iconSize + 11 : 10))
2427
+ let width = button.widthAnchor.constraint(equalToConstant: iconOnly ? iconSize : roundedTextWidth + extraWidth)
2353
2428
  NSLayoutConstraint.activate([width, height])
2354
2429
  selectorConstraints.append(contentsOf: [width, height])
2355
2430
  if let icon = badge.dictionary("icon") {
@@ -2370,6 +2445,41 @@ final class NativeListCell: UICollectionViewCell {
2370
2445
  subtitleLabel.attributedText = marketAttributedText(item.data.string("subtitle"), segments: item.data.dictionaries("subtitleSegments"), style: style?.dictionary("subtitle"), defaultSize: 14, defaultLineHeight: 20, defaultWeight: .regular, color: nativeListColor(theme, "secondaryText", "#646464"), defaultAlignment: .natural)
2371
2446
  }
2372
2447
  }
2448
+ // OneKey patch: preserve volume width while the localized name truncates.
2449
+ let subtitlePrefix = item.data.dictionary("subtitlePrefix")
2450
+ let subtitlePadding = CGFloat(style?.double("subtitleTrailingPadding", default: 0) ?? 0)
2451
+ if subtitlePrefix != nil || subtitlePadding > 0 {
2452
+ mainStack.removeArrangedSubview(subtitleLabel)
2453
+ subtitleLabel.removeFromSuperview()
2454
+ mainStack.removeArrangedSubview(tertiaryLabel)
2455
+ tertiaryLabel.removeFromSuperview()
2456
+ marketSubtitleStack.axis = .horizontal
2457
+ marketSubtitleStack.alignment = .center
2458
+ marketSubtitleStack.spacing = 0
2459
+ marketSubtitleStack.clipsToBounds = true
2460
+ show(tertiaryLabel, subtitlePrefix?.string("text") ?? "", lines: 1)
2461
+ applyMarketTextStyle(tertiaryLabel, data: subtitlePrefix?.dictionary("style"), theme: theme, defaultSize: 12, defaultLineHeight: 16, defaultWeight: .regular, defaultColor: nativeListColor(theme, "secondaryText", "#646464"))
2462
+ tertiaryLabel.setContentHuggingPriority(.required, for: .horizontal)
2463
+ tertiaryLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
2464
+ subtitleLabel.setContentHuggingPriority(.required, for: .horizontal)
2465
+ subtitleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
2466
+ marketSubtitleStack.addArrangedSubview(tertiaryLabel)
2467
+ marketSubtitleStack.addArrangedSubview(subtitleLabel)
2468
+ marketSubtitleStack.addArrangedSubview(marketSubtitleSpacer)
2469
+ if !tertiaryLabel.isHidden && !subtitleLabel.isHidden {
2470
+ marketSubtitleStack.setCustomSpacing(CGFloat(subtitlePrefix?.double("gap", default: 4) ?? 4), after: tertiaryLabel)
2471
+ }
2472
+ mainStack.insertArrangedSubview(marketSubtitleStack, at: 1)
2473
+ let width = marketSubtitleStack.widthAnchor.constraint(equalTo: mainStack.widthAnchor, constant: -subtitlePadding)
2474
+ width.isActive = true
2475
+ selectorConstraints.append(width)
2476
+ if let maxWidth = subtitlePrefix?["maxWidth"] as? Double {
2477
+ let limit = tertiaryLabel.widthAnchor.constraint(lessThanOrEqualToConstant: CGFloat(maxWidth))
2478
+ limit.isActive = true
2479
+ selectorConstraints.append(limit)
2480
+ }
2481
+ marketSubtitleStack.isHidden = tertiaryLabel.isHidden && subtitleLabel.isHidden
2482
+ }
2373
2483
  rootStack.addArrangedSubview(trailingStack)
2374
2484
  trailingStack.axis = .horizontal
2375
2485
  trailingStack.alignment = .center
@@ -3039,6 +3149,41 @@ final class NativeListCell: UICollectionViewCell {
3039
3149
  rootTopConstraint.constant = 12
3040
3150
  rootBottomConstraint.constant = -12
3041
3151
  }
3152
+ if isMarket && variant == "retry" {
3153
+ let message = item.data.string("message")
3154
+ let text = item.data.string("actionText", default: "Retry")
3155
+ rootStack.axis = .vertical
3156
+ // The source tertiary Button has -5 vertical margins around its 30pt frame.
3157
+ rootStack.spacing = 7
3158
+ rootLeadingConstraint.constant = 32
3159
+ rootTrailingConstraint.constant = -32
3160
+ let height = CGFloat(item.data.double("height", default: message.isEmpty ? 52 : 120))
3161
+ let top = message.isEmpty ? 11 : max(32, (height - 56) / 2)
3162
+ rootTopConstraint.constant = top
3163
+ rootBottomConstraint.constant = -(height - top - (message.isEmpty ? 30 : 61))
3164
+ if !message.isEmpty {
3165
+ show(titleLabel, message, lines: 2)
3166
+ titleLabel.font = nativeListTabularFont(ofSize: 16)
3167
+ titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
3168
+ titleLabel.textAlignment = .center
3169
+ setLineHeight(titleLabel, text: message, lineHeight: 24)
3170
+ rootStack.addArrangedSubview(mainStack)
3171
+ }
3172
+ showAccessory(0, text, action: (item.data.string("actionKey"), nil))
3173
+ let button = accessoryButtons[0]
3174
+ button.backgroundColor = .clear
3175
+ button.layer.cornerRadius = 15
3176
+ setButtonLine(button, text: text, font: nativeListTabularFont(ofSize: 14, weight: .medium),
3177
+ color: nativeListColor(theme, "secondaryText", "#646464"), lineHeight: 20)
3178
+ let textWidth = button.intrinsicContentSize.width
3179
+ selectorConstraints.append(contentsOf: [
3180
+ button.widthAnchor.constraint(equalToConstant: textWidth + 18),
3181
+ button.heightAnchor.constraint(equalToConstant: 30),
3182
+ ])
3183
+ NSLayoutConstraint.activate(selectorConstraints)
3184
+ rootStack.addArrangedSubview(trailingStack)
3185
+ return
3186
+ }
3042
3187
  if variant == "loading" && item.data.string("loadingStyle") == "skeleton" {
3043
3188
  rootLeadingConstraint.constant = 20
3044
3189
  rootTrailingConstraint.constant = -20
@@ -3065,12 +3210,13 @@ final class NativeListCell: UICollectionViewCell {
3065
3210
  return
3066
3211
  }
3067
3212
  if isMarket && variant == "noMatch" {
3068
- rootTopConstraint.constant = 32
3069
- rootBottomConstraint.constant = -32
3213
+ let padding = max(32, (CGFloat(item.data.double("height", default: 88)) - 24) / 2)
3214
+ rootTopConstraint.constant = padding
3215
+ rootBottomConstraint.constant = -padding
3070
3216
  rootStack.addArrangedSubview(mainStack)
3071
3217
  mainStack.alignment = .center
3072
3218
  mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
3073
- titleLabel.font = nativeListFont(ofSize: 16)
3219
+ titleLabel.font = nativeListTabularFont(ofSize: 16)
3074
3220
  titleLabel.textColor = nativeListColor(theme, "secondaryText", "#646464")
3075
3221
  titleLabel.textAlignment = .center
3076
3222
  show(titleLabel, item.data.string("message"), lines: 1)
@@ -3080,6 +3226,8 @@ final class NativeListCell: UICollectionViewCell {
3080
3226
  if isMarket && variant == "end" {
3081
3227
  rootTopConstraint.constant = 16
3082
3228
  rootBottomConstraint.constant = -16
3229
+ // OneKey patch: an empty title stack must not consume the dot's line height.
3230
+ titleRowStack.isHidden = true
3083
3231
  rootStack.addArrangedSubview(mainStack)
3084
3232
  mainStack.alignment = .center
3085
3233
  mainStack.setContentHuggingPriority(.defaultLow, for: .horizontal)
@@ -3558,7 +3706,7 @@ final class NativeListCell: UICollectionViewCell {
3558
3706
  .foregroundColor: label.textColor as Any,
3559
3707
  .paragraphStyle: paragraphStyle,
3560
3708
  ]
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" {
3709
+ if currentItem?.type == "market" || currentItem?.type == "system" && currentItem?.data.string("presentation") == "market" && currentItem?.data.string("variant") == "noMatch" || (currentItem?.data["height"] != nil && (["accountSelector", "walletSidebar"].contains(currentItem?.data.string("presentation") ?? "") || currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector")) || currentItem?.type == "system" && currentItem?.data.string("variant") == "warning" {
3562
3710
  // OneKey patch: React Native centers font metrics inside explicit line heights.
3563
3711
  let baselineOffset = max(0, (lineHeight - label.font.lineHeight) / 2)
3564
3712
  // OneKey patch: TextKit's 14/20 headings align their baseline to the upper physical pixel.
@@ -3566,7 +3714,7 @@ final class NativeListCell: UICollectionViewCell {
3566
3714
  let scale = window?.screen.scale ?? traitCollection.displayScale
3567
3715
  attributes[.baselineOffset] = isSelectorHeading && scale > 0 ? ceil(baselineOffset * scale) / scale : baselineOffset
3568
3716
  }
3569
- if letterSpacing != 0 || currentItem?.type == "market" { attributes[.kern] = letterSpacing }
3717
+ if letterSpacing != 0 || currentItem?.type == "market" || currentItem?.data.string("presentation") == "market" { attributes[.kern] = letterSpacing }
3570
3718
  label.attributedText = NSAttributedString(string: text, attributes: attributes)
3571
3719
  }
3572
3720
 
@@ -3581,13 +3729,14 @@ final class NativeListCell: UICollectionViewCell {
3581
3729
  let paragraphStyle = NSMutableParagraphStyle()
3582
3730
  paragraphStyle.minimumLineHeight = lineHeight
3583
3731
  paragraphStyle.maximumLineHeight = lineHeight
3732
+ let isMarketText = currentItem?.type == "market" || currentItem?.type == "system" && currentItem?.data.string("presentation") == "market" && currentItem?.data.string("variant") == "retry"
3584
3733
  let isSelectorValue = currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") != "summary"
3585
3734
  let isSelectorSummary = currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil && currentItem?.data.string("variant") == "summary"
3586
3735
  (button as? NativeListAccessoryButton)?.selectorSummaryLineHeight = isSelectorSummary ? lineHeight : nil
3587
- (button as? NativeListAccessoryButton)?.marketLineHeight = currentItem?.type == "market" ? lineHeight : nil
3736
+ (button as? NativeListAccessoryButton)?.marketLineHeight = isMarketText ? lineHeight : nil
3588
3737
  // OneKey patch: summary text uses its source line box; currency retains trailing alignment.
3589
3738
  // Market's line box already handles alignment; source text starts at its origin.
3590
- paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary || currentItem?.type == "market" ? .natural : .center
3739
+ paragraphStyle.alignment = isSelectorValue ? .right : isSelectorSummary || isMarketText ? .natural : .center
3591
3740
  if isSelectorSummary {
3592
3741
  button.contentHorizontalAlignment = .leading
3593
3742
  button.titleLabel?.textAlignment = .natural
@@ -3596,14 +3745,14 @@ final class NativeListCell: UICollectionViewCell {
3596
3745
  button.contentHorizontalAlignment = .trailing
3597
3746
  button.titleLabel?.textAlignment = .right
3598
3747
  }
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
3748
+ let baselineOffset: CGFloat = isMarketText || currentItem?.type == "sectionHeader" && currentItem?.data.string("presentation") == "networkSelector" && currentItem?.data["height"] != nil ? max(0, (lineHeight - font.lineHeight) / 2) : 0
3600
3749
  var attributes: [NSAttributedString.Key: Any] = [
3601
3750
  .font: font,
3602
3751
  .foregroundColor: color,
3603
3752
  .paragraphStyle: paragraphStyle,
3604
- .baselineOffset: currentItem?.type == "market" && lineHeight == 20 && font.pointSize == 14 ? ceil(baselineOffset * max(1, traitCollection.displayScale)) / max(1, traitCollection.displayScale) : baselineOffset,
3753
+ .baselineOffset: isMarketText && lineHeight == 20 && font.pointSize == 14 ? ceil(baselineOffset * max(1, traitCollection.displayScale)) / max(1, traitCollection.displayScale) : baselineOffset,
3605
3754
  ]
3606
- if currentItem?.type == "market" { attributes[.kern] = 0 }
3755
+ if isMarketText { attributes[.kern] = 0 }
3607
3756
  button.setAttributedTitle(
3608
3757
  NSAttributedString(string: text, attributes: attributes),
3609
3758
  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,6 +115,13 @@ 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`);
116
127
  assertLeadingVisual(row.leading, `${path}.leading`);
@@ -140,6 +151,10 @@ function assertMarketRow(row, path) {
140
151
  }
141
152
  badgeKeys.add(badge.key);
142
153
  assertText(badge.text, `${badgePath}.text`);
154
+ // OneKey patch: share typography bounds with Market text styles.
155
+ assertMarketTextStyle(badge.style, `${badgePath}.style`);
156
+ assertBoundedStyleNumber(badge.style?.height, `${badgePath}.style.height`, 1, 64);
157
+ assertBoundedStyleNumber(badge.style?.horizontalPadding, `${badgePath}.style.horizontalPadding`, 0, 32);
143
158
  if (badge.iconName !== undefined && badge.iconName !== 'verified') {
144
159
  fail(`${badgePath}.iconName`, 'must be verified when provided');
145
160
  }
@@ -451,6 +466,7 @@ function assertRow(row, index, path = `rows[${index}]`) {
451
466
  }
452
467
  if (row.variant === 'retry') {
453
468
  assertKey(row.actionKey, `${path}.actionKey`);
469
+ assertText(row.actionText, `${path}.actionText`);
454
470
  }
455
471
  break;
456
472
  }
@@ -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;
@@ -319,6 +332,13 @@ export type MarketRow = RowBase & Readonly<{
319
332
  leading: LeadingVisual;
320
333
  title: string;
321
334
  subtitle?: string;
335
+ /** OneKey patch: localized name shrinks independently of the volume. */
336
+ subtitlePrefix?: Readonly<{
337
+ text: string;
338
+ gap?: number;
339
+ maxWidth?: number;
340
+ style?: MarketTextStyle;
341
+ }>;
322
342
  subtitleSegments?: readonly ValueTextSegment[];
323
343
  price: string;
324
344
  priceSegments?: readonly ValueTextSegment[];
@@ -415,6 +435,7 @@ export type SystemRow = RowBase & (Readonly<{
415
435
  presentation?: 'market';
416
436
  message: string;
417
437
  actionKey: string;
438
+ actionText?: string;
418
439
  }> | Readonly<{
419
440
  type: 'system';
420
441
  variant: 'warning';
@@ -562,6 +583,11 @@ export type NativeListActionAnchor = Readonly<{
562
583
  token: string;
563
584
  /** Window-relative logical units: CSS px on Web, points on iOS, dp on Android. */
564
585
  windowRect: NativeListWindowRect;
586
+ /** Actual long-press point, in the same logical units as windowRect. */
587
+ windowPoint?: Readonly<{
588
+ x: number;
589
+ y: number;
590
+ }>;
565
591
  source: NativeListActionSource;
566
592
  slot?: number;
567
593
  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.116",
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.116",
87
+ "@onekeyfe/react-native-native-logger": "3.0.116",
88
88
  "react": "*",
89
89
  "react-native": "*",
90
90
  "react-native-nitro-modules": "0.37.0"
package/src/models.ts CHANGED
@@ -57,7 +57,12 @@ export type MarketRowStyle = Readonly<{
57
57
  /** Space between title and subtitle; 0 by default, bounded to 0..16. */
58
58
  lineGap?: number;
59
59
  titleBadgeGap?: number;
60
+ /** OneKey patch: keep badges next to the intrinsic title width. */
61
+ titleBadgeLayout?: 'inline';
60
62
  trailingGap?: number;
63
+ /** OneKey patch: preserve separate Market content and subtitle insets. */
64
+ contentTrailingGap?: number;
65
+ subtitleTrailingPadding?: number;
61
66
  image?: MarketImageStyle;
62
67
  title?: MarketTextStyle;
63
68
  subtitle?: MarketTextStyle;
@@ -79,6 +84,14 @@ export type MarketBadgeModel = Readonly<{
79
84
  backgroundColor?: string;
80
85
  actionKey?: string;
81
86
  accessibilityLabel?: string;
87
+ /** OneKey patch: optional Market badge metrics; legacy native defaults remain unchanged. */
88
+ style?: Readonly<{
89
+ fontSize?: number;
90
+ fontWeight?: MarketTextStyle['fontWeight'];
91
+ lineHeight?: number;
92
+ height?: number;
93
+ horizontalPadding?: number;
94
+ }>;
82
95
  }>;
83
96
 
84
97
  export type MarketChangeModel = Readonly<{
@@ -336,6 +349,13 @@ export type MarketRow = RowBase &
336
349
  leading: LeadingVisual;
337
350
  title: string;
338
351
  subtitle?: string;
352
+ /** OneKey patch: localized name shrinks independently of the volume. */
353
+ subtitlePrefix?: Readonly<{
354
+ text: string;
355
+ gap?: number;
356
+ maxWidth?: number;
357
+ style?: MarketTextStyle;
358
+ }>;
339
359
  subtitleSegments?: readonly ValueTextSegment[];
340
360
  price: string;
341
361
  priceSegments?: readonly ValueTextSegment[];
@@ -434,6 +454,7 @@ export type SystemRow = RowBase &
434
454
  presentation?: 'market';
435
455
  message: string;
436
456
  actionKey: string;
457
+ actionText?: string;
437
458
  }>
438
459
  | Readonly<{
439
460
  type: 'system';
@@ -764,6 +785,8 @@ export type NativeListActionAnchor = Readonly<{
764
785
  token: string;
765
786
  /** Window-relative logical units: CSS px on Web, points on iOS, dp on Android. */
766
787
  windowRect: NativeListWindowRect;
788
+ /** Actual long-press point, in the same logical units as windowRect. */
789
+ windowPoint?: Readonly<{ x: number; y: number }>;
767
790
  source: NativeListActionSource;
768
791
  slot?: number;
769
792
  generation: number;
package/src/validation.ts CHANGED
@@ -162,9 +162,18 @@ function assertMarketStyle(
162
162
  'leadingGap',
163
163
  'titleBadgeGap',
164
164
  'trailingGap',
165
+ 'contentTrailingGap',
166
+ 'subtitleTrailingPadding',
165
167
  ] as const) {
166
168
  assertBoundedStyleNumber(style[field], `${path}.${field}`, 0, 64);
167
169
  }
170
+ // OneKey patch: validate the opt-in Market badge layout.
171
+ if (
172
+ style.titleBadgeLayout !== undefined &&
173
+ style.titleBadgeLayout !== 'inline'
174
+ ) {
175
+ fail(`${path}.titleBadgeLayout`, 'must be inline when provided');
176
+ }
168
177
  assertBoundedStyleNumber(style.lineGap, `${path}.lineGap`, 0, 16);
169
178
  assertBoundedStyleNumber(style.changeWidth, `${path}.changeWidth`, 1, 160);
170
179
  assertBoundedStyleNumber(style.changeHeight, `${path}.changeHeight`, 1, 160);
@@ -211,6 +220,26 @@ function assertMarketRow(row: MarketRow, path: string): void {
211
220
  }
212
221
  assertText(row.title, `${path}.title`);
213
222
  assertText(row.subtitle, `${path}.subtitle`);
223
+ // OneKey patch: validate the independently laid out Market name.
224
+ if (row.subtitlePrefix) {
225
+ assertText(row.subtitlePrefix.text, `${path}.subtitlePrefix.text`);
226
+ assertBoundedStyleNumber(
227
+ row.subtitlePrefix.gap,
228
+ `${path}.subtitlePrefix.gap`,
229
+ 0,
230
+ 64
231
+ );
232
+ assertBoundedStyleNumber(
233
+ row.subtitlePrefix.maxWidth,
234
+ `${path}.subtitlePrefix.maxWidth`,
235
+ 1,
236
+ 320
237
+ );
238
+ assertMarketTextStyle(
239
+ row.subtitlePrefix.style,
240
+ `${path}.subtitlePrefix.style`
241
+ );
242
+ }
214
243
  assertText(row.price, `${path}.price`);
215
244
  assertText(row.change.text, `${path}.change.text`);
216
245
  assertLeadingVisual(row.leading, `${path}.leading`);
@@ -246,6 +275,20 @@ function assertMarketRow(row: MarketRow, path: string): void {
246
275
  }
247
276
  badgeKeys.add(badge.key);
248
277
  assertText(badge.text, `${badgePath}.text`);
278
+ // OneKey patch: share typography bounds with Market text styles.
279
+ assertMarketTextStyle(badge.style, `${badgePath}.style`);
280
+ assertBoundedStyleNumber(
281
+ badge.style?.height,
282
+ `${badgePath}.style.height`,
283
+ 1,
284
+ 64
285
+ );
286
+ assertBoundedStyleNumber(
287
+ badge.style?.horizontalPadding,
288
+ `${badgePath}.style.horizontalPadding`,
289
+ 0,
290
+ 32
291
+ );
249
292
  if (badge.iconName !== undefined && badge.iconName !== 'verified') {
250
293
  fail(`${badgePath}.iconName`, 'must be verified when provided');
251
294
  }
@@ -797,6 +840,7 @@ function assertRow(
797
840
  }
798
841
  if (row.variant === 'retry') {
799
842
  assertKey(row.actionKey, `${path}.actionKey`);
843
+ assertText(row.actionText, `${path}.actionText`);
800
844
  }
801
845
  break;
802
846
  }