@onekeyfe/react-native-native-list 6.7.0-alpha.375 → 6.7.0-alpha.382

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.
Files changed (116) hide show
  1. package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +14 -1
  2. package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
  3. package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
  4. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
  5. package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
  6. package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
  7. package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
  8. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
  9. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
  10. package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
  11. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
  12. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
  13. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
  14. package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
  15. package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
  16. package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
  17. package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
  18. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
  19. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
  20. package/docs/ROW_TEMPLATES.md +60 -9
  21. package/docs/SPEC.md +450 -5
  22. package/docs/STYLE_SPEC.md +6 -0
  23. package/ios/HybridNativeList.swift +21 -2
  24. package/ios/NativeListAccessoryStack.swift +227 -39
  25. package/ios/NativeListActivityDetailsView.swift +18 -8
  26. package/ios/NativeListActivityRenderer.swift +8 -2
  27. package/ios/NativeListButtonGroupRenderer.swift +89 -0
  28. package/ios/NativeListDataRowRenderer.swift +155 -27
  29. package/ios/NativeListDesignAssets.swift +3 -0
  30. package/ios/NativeListIdentityRenderer.swift +166 -30
  31. package/ios/NativeListLeadingVisual.swift +49 -14
  32. package/ios/NativeListMessageRenderer.swift +34 -1
  33. package/ios/NativeListModels.swift +42 -2
  34. package/ios/NativeListRendererRegistry.swift +1 -0
  35. package/ios/NativeListResolvedText.swift +2 -1
  36. package/ios/NativeListRowHost.swift +14 -0
  37. package/ios/NativeListSectionHeaderRenderer.swift +88 -0
  38. package/ios/RNCNativeListView.swift +1013 -55
  39. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
  40. package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
  41. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
  42. package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
  43. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
  44. package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
  45. package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
  46. package/ios/tests/NativeListLifecycleTests.swift +2720 -3
  47. package/lib/module/NativeList.js +175 -28
  48. package/lib/module/NativeList.web.js +129 -30
  49. package/lib/module/index.js +1 -1
  50. package/lib/module/models.js +1 -0
  51. package/lib/module/selection.js +1 -1
  52. package/lib/module/validation.js +421 -8
  53. package/lib/module/web/NativeListWebEngine.js +507 -19
  54. package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
  55. package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
  56. package/lib/module/web/templates/DataRowRenderer.js +40 -5
  57. package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
  58. package/lib/module/web/templates/MessageRowRenderer.js +18 -2
  59. package/lib/module/web/templates/RowContainerStyle.js +1 -0
  60. package/lib/module/web/templates/RowRendererRegistry.js +3 -0
  61. package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
  62. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  63. package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  64. package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  65. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  66. package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  67. package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  68. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  69. package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  70. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  71. package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  72. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  73. package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  74. package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  75. package/lib/typescript/src/NativeList.d.ts +1 -1
  76. package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
  77. package/lib/typescript/src/NativeList.types.d.ts +18 -0
  78. package/lib/typescript/src/index.d.ts +2 -2
  79. package/lib/typescript/src/models.d.ts +105 -9
  80. package/lib/typescript/src/validation.d.ts +12 -0
  81. package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
  82. package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
  83. package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
  84. package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
  85. package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
  86. package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
  87. package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
  88. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
  89. package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
  90. package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
  91. package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
  92. package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
  93. package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
  94. package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
  95. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
  96. package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
  97. package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
  98. package/package.json +3 -3
  99. package/react-native-native-list.podspec +1 -0
  100. package/src/NativeList.nitro.ts +2 -0
  101. package/src/NativeList.tsx +313 -44
  102. package/src/NativeList.types.ts +22 -0
  103. package/src/NativeList.web.tsx +204 -48
  104. package/src/index.ts +2 -0
  105. package/src/models.ts +117 -3
  106. package/src/selection.ts +1 -0
  107. package/src/validation.ts +852 -6
  108. package/src/web/NativeListWebEngine.ts +803 -20
  109. package/src/web/templates/ActivityRowRenderer.ts +14 -6
  110. package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
  111. package/src/web/templates/DataRowRenderer.ts +93 -20
  112. package/src/web/templates/IdentityRowRenderer.ts +150 -5
  113. package/src/web/templates/MessageRowRenderer.ts +23 -7
  114. package/src/web/templates/RowContainerStyle.ts +2 -0
  115. package/src/web/templates/RowRendererRegistry.ts +11 -1
  116. package/src/web/templates/SectionHeaderRowRenderer.ts +47 -4
@@ -22,6 +22,7 @@ internal class NativeListDataRowView(context: ThemedReactContext, fonts: NativeL
22
22
  gravity = Gravity.CENTER_VERTICAL
23
23
  }
24
24
  private val cells = List(4) { NativeListTableColumnView(context, fonts) }
25
+ private val trailingAccessories = NativeListAccessoryStack(context, fonts)
25
26
 
26
27
  override fun unselectedBackground(
27
28
  item: NativeListItem,
@@ -54,16 +55,20 @@ internal class NativeListDataRowView(context: ThemedReactContext, fonts: NativeL
54
55
  addView(visual)
55
56
  addView(accessories)
56
57
  addView(columns, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
58
+ addView(trailingAccessories)
57
59
  accessories.onAction = { key, view, slot, target ->
58
60
  emitAction(key, view, "trailingAccessory", slot, target)
59
61
  }
62
+ trailingAccessories.onAction = { key, view, slot, target ->
63
+ emitAction(key, view, "trailingAccessory", slot, target)
64
+ }
60
65
  }
61
66
 
62
67
  override fun defaultHeight(item: NativeListItem, layout: String): Int {
63
68
  val values = item.json.optJSONArray("columns") ?: JSONArray()
64
69
  val secondary =
65
70
  (0 until values.length()).any {
66
- values.getJSONObject(it).optString("secondaryText").isNotEmpty()
71
+ nativeListDataColumnText(values.getJSONObject(it), "secondaryText").isNotEmpty()
67
72
  }
68
73
  return if (layout == "table") if (secondary) 60 else 52 else if (secondary) 64 else 56
69
74
  }
@@ -127,6 +132,9 @@ internal class NativeListDataRowView(context: ThemedReactContext, fonts: NativeL
127
132
  val accessoryStyle = JSONObject().put("value", style.optJSONObject("index"))
128
133
  accessories.bind(item, descriptors, theme, accessoryStyle, sourceScale, checkboxState)
129
134
  accessories.visibility = if (descriptors.length() == 0) GONE else VISIBLE
135
+ val trailing = data.optJSONArray("trailing") ?: JSONArray()
136
+ trailingAccessories.bind(item, trailing, theme, style, sourceScale, checkboxState)
137
+ trailingAccessories.visibility = if (trailing.length() == 0) GONE else VISIBLE
130
138
  accessories.layoutParams =
131
139
  LayoutParams(dp(32), LayoutParams.WRAP_CONTENT).apply { marginEnd = dp(10) }
132
140
  val values = data.getJSONArray("columns")
@@ -159,6 +167,7 @@ internal class NativeListDataRowView(context: ThemedReactContext, fonts: NativeL
159
167
  style,
160
168
  fixedTableGeometry = unstyled,
161
169
  )
170
+ cell.bindTitleIcons(value, theme, sourceScale)
162
171
  // Legacy table default: every column is a fixed 40dp, top-aligned block.
163
172
  cell.layoutParams =
164
173
  LayoutParams(
@@ -194,17 +203,20 @@ internal class NativeListDataRowView(context: ThemedReactContext, fonts: NativeL
194
203
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
195
204
  ) {
196
205
  accessories.updateSelection(item, checkboxState)
206
+ trailingAccessories.updateSelection(item, checkboxState)
197
207
  }
198
208
 
199
209
  override fun recycleContent() {
200
210
  visual.recycle()
201
211
  accessories.reset()
212
+ trailingAccessories.reset()
202
213
  cells.forEach { it.reset() }
203
214
  }
204
215
 
205
216
  override fun disposeContent() {
206
217
  visual.dispose()
207
218
  accessories.reset()
219
+ trailingAccessories.reset()
208
220
  }
209
221
 
210
222
  private companion object {
@@ -215,6 +227,8 @@ internal class NativeListDataRowView(context: ThemedReactContext, fonts: NativeL
215
227
  private class NativeListTableColumnView(context: android.content.Context, private val fonts: NativeListFonts) : LinearLayout(context) {
216
228
  private val primaryLine = LinearLayout(context)
217
229
  private val primary = NativeListTextView(context)
230
+ private val titleIcon = OneKeyIconView(context)
231
+ private val secondaryTitleIcon = OneKeyIconView(context)
218
232
  private val badges = LinearLayout(context)
219
233
  private val secondaryLine = LinearLayout(context)
220
234
  private val secondaryLeading = NativeListTextView(context)
@@ -256,6 +270,13 @@ private class NativeListTableColumnView(context: android.content.Context, privat
256
270
  },
257
271
  )
258
272
  primaryLine.addView(primary, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT))
273
+ for (icon in listOf(titleIcon, secondaryTitleIcon)) {
274
+ icon.visibility = GONE
275
+ primaryLine.addView(
276
+ icon,
277
+ LayoutParams(dp(20), dp(20)).apply { marginStart = dp(4) },
278
+ )
279
+ }
259
280
  primaryLine.addView(
260
281
  badges,
261
282
  LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply {
@@ -270,6 +291,13 @@ private class NativeListTableColumnView(context: android.content.Context, privat
270
291
  }
271
292
 
272
293
  fun reset() {
294
+ primaryLine.gravity = Gravity.CENTER_VERTICAL
295
+ for (icon in listOf(titleIcon, secondaryTitleIcon)) {
296
+ icon.visibility = GONE
297
+ icon.iconName = ""
298
+ icon.contentDescription = null
299
+ icon.setOnClickListener(null)
300
+ }
273
301
  primaryLine.layoutParams.width = LayoutParams.WRAP_CONTENT
274
302
  primaryLine.layoutParams.height = LayoutParams.WRAP_CONTENT
275
303
  secondaryLine.layoutParams.height = LayoutParams.WRAP_CONTENT
@@ -292,6 +320,26 @@ private class NativeListTableColumnView(context: android.content.Context, privat
292
320
  badges.visibility = GONE
293
321
  }
294
322
 
323
+ fun bindTitleIcons(column: JSONObject, theme: JSONObject?, sourceScale: Boolean) {
324
+ for ((field, icon) in listOf(
325
+ "titleIcon" to titleIcon,
326
+ "secondaryTitleIcon" to secondaryTitleIcon,
327
+ )) {
328
+ val descriptor = column.optJSONObject(field) ?: continue
329
+ icon.iconName = descriptor.optString("name")
330
+ icon.glyphSizeDp = 20
331
+ icon.useSourceScale = sourceScale
332
+ icon.tintColor = parseNativeListColor(
333
+ descriptor.optString("tintColor").takeIf(String::isNotEmpty)
334
+ ?: theme?.optString("iconSubdued", "#00000072") ?: "#00000072"
335
+ )
336
+ icon.contentDescription = descriptor.optString("accessibilityLabel").takeIf(String::isNotEmpty)
337
+ icon.visibility = VISIBLE
338
+ }
339
+ if (primary.maxLines > 1 && (titleIcon.visibility == VISIBLE || secondaryTitleIcon.visibility == VISIBLE))
340
+ primaryLine.gravity = Gravity.TOP
341
+ }
342
+
295
343
  fun bind(
296
344
  column: JSONObject,
297
345
  rowBadges: JSONArray?,
@@ -326,7 +374,7 @@ private class NativeListTableColumnView(context: android.content.Context, privat
326
374
  .bind(view)
327
375
  if (resolved.optString("truncate") != "clip") view.ellipsize = TextUtils.TruncateAt.END
328
376
  }
329
- bindText(primary, column.optString("text"), "columns", primarySize, "medium", primaryColor, 20)
377
+ bindText(primary, nativeListDataColumnText(column, "text"), "columns", primarySize, "medium", primaryColor, 20)
330
378
  bindText(
331
379
  secondaryLeading,
332
380
  column.optString("secondaryLeadingText"),
@@ -338,13 +386,15 @@ private class NativeListTableColumnView(context: android.content.Context, privat
338
386
  )
339
387
  bindText(
340
388
  secondary,
341
- column.optString("secondaryText"),
389
+ nativeListDataColumnText(column, "secondaryText"),
342
390
  "columnSecondary",
343
391
  12f,
344
392
  "regular",
345
393
  secondaryColor,
346
394
  16,
347
395
  )
396
+ primary.text = dataSegments(primary.text, column.optJSONArray("textSegments"), primary.textSize)
397
+ secondary.text = dataSegments(secondary.text, column.optJSONArray("secondaryTextSegments"), secondary.textSize)
348
398
  if (rowBadges != null && rowBadges.length() > 0) {
349
399
  badges.visibility = VISIBLE
350
400
  for (index in 0 until minOf(2, rowBadges.length())) {
@@ -377,7 +427,7 @@ private class NativeListTableColumnView(context: android.content.Context, privat
377
427
  secondaryLeading.visibility = VISIBLE
378
428
  secondaryLine.visibility = VISIBLE
379
429
  }
380
- val secondaryText = column.optString("secondaryText")
430
+ val secondaryText = nativeListDataColumnText(column, "secondaryText")
381
431
  if (secondaryText.isNotEmpty()) {
382
432
  secondary.visibility = VISIBLE
383
433
  secondaryLine.visibility = VISIBLE
@@ -404,7 +454,9 @@ private class NativeListTableColumnView(context: android.content.Context, privat
404
454
  ) {
405
455
  reset()
406
456
  gravity = Gravity.START
407
- val value = android.text.SpannableStringBuilder(column.optString("text"))
457
+ val value = android.text.SpannableStringBuilder(dataSegments(
458
+ nativeListDataColumnText(column, "text"), column.optJSONArray("textSegments"),
459
+ sp(16f) * resources.displayMetrics.scaledDensity))
408
460
  value.setSpan(
409
461
  android.text.style.ForegroundColorSpan(primaryColor),
410
462
  0,
@@ -428,10 +480,12 @@ private class NativeListTableColumnView(context: android.content.Context, privat
428
480
  android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
429
481
  )
430
482
  }
431
- val secondaryText = column.optString("secondaryText")
483
+ val secondaryText = nativeListDataColumnText(column, "secondaryText")
432
484
  if (secondaryText.isNotEmpty()) {
433
485
  val start = value.length
434
- value.append("\n$secondaryText")
486
+ value.append("\n")
487
+ value.append(dataSegments(secondaryText, column.optJSONArray("secondaryTextSegments"),
488
+ sp(16f) * resources.displayMetrics.scaledDensity))
435
489
  value.setSpan(
436
490
  android.text.style.ForegroundColorSpan(secondaryColor),
437
491
  start,
@@ -461,6 +515,24 @@ private class NativeListTableColumnView(context: android.content.Context, privat
461
515
  primary.visibility = VISIBLE
462
516
  }
463
517
 
518
+ private fun dataSegments(fallback: CharSequence, segments: JSONArray?, fontSizePx: Float): CharSequence {
519
+ if (segments == null || segments.length() == 0) return fallback
520
+ val value = android.text.SpannableStringBuilder(fallback)
521
+ var offset = 0
522
+ for (index in 0 until segments.length()) {
523
+ val segment = segments.getJSONObject(index)
524
+ val end = offset + segment.optString("text").length
525
+ if (segment.optString("style") == "subscript" && end > offset) {
526
+ val density = resources.displayMetrics.density
527
+ val size = (kotlin.math.ceil(fontSizePx / density * 0.6f) * density).roundToInt()
528
+ value.setSpan(android.text.style.AbsoluteSizeSpan(size), offset, end,
529
+ android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
530
+ }
531
+ offset = end
532
+ }
533
+ return value
534
+ }
535
+
464
536
  private fun applyStyle(style: JSONObject) {
465
537
  style.optJSONObject("columns")?.let {
466
538
  if (it.has("alignment")) {
@@ -485,3 +557,11 @@ private class NativeListTableColumnView(context: android.content.Context, privat
485
557
 
486
558
  private fun sp(value: Float): Float = NativeListScale.font(resources, value)
487
559
  }
560
+
561
+ private fun nativeListDataColumnText(column: JSONObject, field: String): String {
562
+ val segments = column.optJSONArray("${field}Segments")
563
+ if (segments == null || segments.length() == 0) return column.optString(field)
564
+ return buildString {
565
+ for (index in 0 until segments.length()) append(segments.getJSONObject(index).optString("text"))
566
+ }
567
+ }
@@ -1,5 +1,6 @@
1
1
  package com.margelo.nitro.nativelist
2
2
 
3
+ import android.graphics.PointF
3
4
  import android.graphics.drawable.GradientDrawable
4
5
  import android.text.Spannable
5
6
  import android.text.SpannableStringBuilder
@@ -8,6 +9,7 @@ import android.text.style.AbsoluteSizeSpan
8
9
  import android.text.style.ForegroundColorSpan
9
10
  import android.util.TypedValue
10
11
  import android.view.Gravity
12
+ import android.view.MotionEvent
11
13
  import android.view.View
12
14
  import android.view.ViewGroup
13
15
  import android.widget.LinearLayout
@@ -28,17 +30,41 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
28
30
  private val mainColumn = LinearLayout(context)
29
31
  private val titleLine = PackedTitleLineLayout(context)
30
32
  private val title = NativeListTextView(context)
33
+ private val titleIcon = OneKeyIconView(context)
31
34
  private val subtitle = NativeListTextView(context)
32
35
  private val tertiary = NativeListTextView(context)
33
36
  private val badgeLine = NativeListTextView(context)
37
+ private val titleSuffixLabels = mutableListOf<NativeListTextView>()
38
+ private val titleSuffixMaxWidths = mutableListOf<Int>()
34
39
  private val trailingColumn = NativeListAccessoryStack(context, fonts)
35
40
  private val semanticBadgeLabels = mutableListOf<TextView>()
36
41
  private val semanticSubtitleLabels = mutableListOf<TextView>()
37
42
  private var walletBadgeLine: View? = null
38
43
  private var currentItem: NativeListItem? = null
39
44
  private var currentTheme: JSONObject? = null
45
+ private var longPressPoint: PointF? = null
46
+ private var longPressPointEpoch = -1L
40
47
  private val paintDefaults = mutableMapOf<TextView, Int>()
41
48
 
49
+ override fun onRowTouch(event: MotionEvent) {
50
+ when (event.actionMasked) {
51
+ MotionEvent.ACTION_DOWN -> {
52
+ longPressPoint = if (currentItem?.isRowPressEnabled == true) PointF(event.x, event.y) else null
53
+ longPressPointEpoch = bindingEpoch
54
+ }
55
+ MotionEvent.ACTION_MOVE -> {
56
+ if (longPressPointEpoch == bindingEpoch && longPressPoint != null &&
57
+ event.x >= 0 && event.y >= 0 && event.x < width && event.y < height
58
+ ) longPressPoint?.set(event.x, event.y)
59
+ else longPressPoint = null
60
+ }
61
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
62
+ longPressPoint = null
63
+ longPressPointEpoch = -1L
64
+ }
65
+ }
66
+ }
67
+
42
68
  override fun backgroundGroupPosition(
43
69
  item: NativeListItem,
44
70
  layout: String,
@@ -83,6 +109,7 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
83
109
  titleLine.orientation = HORIZONTAL
84
110
  titleLine.gravity = Gravity.CENTER_VERTICAL
85
111
  titleLine.addView(title, LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
112
+ titleLine.addView(titleIcon, LayoutParams(dp(20), dp(20)).apply { marginStart = dp(4) })
86
113
  titleLine.addView(badgeLine)
87
114
  trailingColumn.onAction = { key, view, slot, target ->
88
115
  emitAction(key, view, "trailingAccessory", slot, target, trailingColumn.anchorInset(view))
@@ -111,7 +138,7 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
111
138
 
112
139
  private fun showText(view: TextView, text: String, lines: Int) {
113
140
  view.text = text
114
- view.maxLines = lines
141
+ view.maxLines = if (lines == 0) Int.MAX_VALUE else lines
115
142
  view.visibility = if (text.isEmpty()) GONE else VISIBLE
116
143
  }
117
144
 
@@ -135,10 +162,42 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
135
162
  layout: String,
136
163
  checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
137
164
  ) {
165
+ cancelLongPress()
166
+ longPressPoint = null
167
+ longPressPointEpoch = -1L
138
168
  currentItem = item
169
+ val longPressActionKey = item.json.optString("longPressActionKey")
170
+ val boundEpoch = bindingEpoch
171
+ setOnLongClickListener(
172
+ if (longPressActionKey.isNotEmpty())
173
+ OnLongClickListener {
174
+ val point = longPressPoint
175
+ if (!isEnabled || !item.isRowPressEnabled || currentItem?.key != item.key ||
176
+ bindingEpoch != boundEpoch ||
177
+ (longPressPointEpoch == boundEpoch && point == null)) false
178
+ else {
179
+ val windowPoint = point?.takeIf { longPressPointEpoch == boundEpoch }?.let {
180
+ val location = IntArray(2)
181
+ getLocationInWindow(location)
182
+ PointF(location[0] + it.x, location[1] + it.y)
183
+ }
184
+ onAction?.invoke(item, longPressActionKey, null,
185
+ NativeListActionOrigin(this, this, bindingEpoch, "row",
186
+ windowPointPixels = windowPoint))
187
+ longPressPoint = null
188
+ longPressPointEpoch = -1L
189
+ true
190
+ }
191
+ }
192
+ else null
193
+ )
194
+ isLongClickable = longPressActionKey.isNotEmpty()
139
195
  currentTheme = theme
140
196
  removeAllViews()
141
197
  mainColumn.removeAllViews()
198
+ titleSuffixLabels.forEach(titleLine::removeView)
199
+ titleSuffixLabels.clear()
200
+ titleSuffixMaxWidths.clear()
142
201
  semanticBadgeLabels.clear()
143
202
  semanticSubtitleLabels.clear()
144
203
  walletBadgeLine = null
@@ -157,8 +216,12 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
157
216
  mainColumn.gravity = Gravity.CENTER_VERTICAL
158
217
  titleLine.gravity = Gravity.CENTER_VERTICAL
159
218
  titleLine.packsChildrenAtStart = false
219
+ titleLine.leadingTextMaxWidth = Int.MAX_VALUE
160
220
  titleLine.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
161
221
  title.layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
222
+ titleIcon.visibility = GONE
223
+ titleIcon.contentDescription = null
224
+ titleIcon.setOnClickListener(null)
162
225
  for (view in listOf(title, subtitle, tertiary, badgeLine)) {
163
226
  view.text = ""
164
227
  view.visibility = GONE
@@ -294,6 +357,19 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
294
357
  private fun addLeading(visual: JSONObject?, sizeDp: Int, spacingDp: Int = 12) {
295
358
  // Legacy: a missing visual still reserves an empty, transparent leading slot.
296
359
  val descriptor = visual ?: JSONObject().put("backgroundColor", "#00000000")
360
+ val overlays = descriptor.optJSONArray("overlays")
361
+ if (overlays != null && (0 until minOf(2, overlays.length())).any { index ->
362
+ val overlay = overlays.optJSONObject(index) ?: return@any false
363
+ overlay.optInt("offsetX", overlay.optInt("offset", 2)) > 0 ||
364
+ overlay.optInt("offsetY", overlay.optInt("offset", 2)) > 0
365
+ }) {
366
+ // The badge extends beyond the avatar and the row's padded content box.
367
+ clipChildren = false
368
+ clipToPadding = false
369
+ }
370
+ leadingFrame.bitmapBorderWidth =
371
+ if (descriptor.optString("borderColor").isNotEmpty() &&
372
+ descriptor.optString("borderStyle") != "dashed") dp(1) else 0
297
373
  if (visual == null) leadingFrame.recycle()
298
374
  val item = currentItem!!
299
375
  val style = item.json.optJSONObject("style")?.optJSONObject("image") ?: JSONObject()
@@ -366,7 +442,58 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
366
442
  }
367
443
 
368
444
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
445
+ val ratio = currentItem?.json?.optJSONObject("style")
446
+ ?.optDouble("leadingContentMaxWidthRatio", Double.NaN) ?: Double.NaN
447
+ titleLine.leadingTextMaxWidth =
448
+ if (ratio.isFinite() && ratio > 0.0 && ratio <= 1.0 && leadingFrame.parent === this) {
449
+ val inner = (MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight)
450
+ .coerceAtLeast(0)
451
+ val leading = leadingFrame.layoutParams as LayoutParams
452
+ val icon = if (titleIcon.visibility == VISIBLE) dp(24) else 0
453
+ (inner * ratio).roundToInt()
454
+ .minus(leading.width + leading.marginEnd + icon)
455
+ .coerceAtLeast(0)
456
+ } else Int.MAX_VALUE
457
+ titleSuffixLabels.forEachIndexed { index, label ->
458
+ label.maxWidth = titleSuffixMaxWidths[index]
459
+ }
369
460
  super.onMeasure(widthMeasureSpec, heightMeasureSpec)
461
+ if (titleSuffixLabels.isNotEmpty()) {
462
+ val fixedWidth =
463
+ (1 until titleLine.childCount).sumOf { index ->
464
+ val view = titleLine.getChildAt(index)
465
+ if (view.visibility == GONE || titleSuffixLabels.any { it === view }) 0
466
+ else {
467
+ val margins = view.layoutParams as MarginLayoutParams
468
+ view.measuredWidth + margins.leftMargin + margins.rightMargin
469
+ }
470
+ }
471
+ val suffixMargins = titleSuffixLabels.sumOf {
472
+ val margins = it.layoutParams as MarginLayoutParams
473
+ margins.leftMargin + margins.rightMargin
474
+ }
475
+ val available =
476
+ (titleLine.measuredWidth - fixedWidth - suffixMargins - dp(80)).coerceAtLeast(0)
477
+ val desired = titleSuffixLabels.mapIndexed { index, label ->
478
+ label.paint.measureText(label.text.toString()).roundToInt()
479
+ .coerceAtMost(titleSuffixMaxWidths[index])
480
+ }
481
+ val total = desired.sum()
482
+ var remaining = available
483
+ var changed = false
484
+ titleSuffixLabels.forEachIndexed { index, label ->
485
+ val share =
486
+ if (total <= available) desired[index]
487
+ else if (index == titleSuffixLabels.lastIndex) remaining
488
+ else (desired[index].toLong() * available / total).toInt()
489
+ remaining -= share
490
+ if (label.maxWidth != share) {
491
+ label.maxWidth = share
492
+ changed = true
493
+ }
494
+ }
495
+ if (changed) super.onMeasure(widthMeasureSpec, heightMeasureSpec)
496
+ }
370
497
  val item = currentItem ?: return
371
498
  if (
372
499
  item.hasExplicitHeight &&
@@ -424,6 +551,9 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
424
551
  }
425
552
 
426
553
  override fun recycleContent() {
554
+ cancelLongPress()
555
+ longPressPoint = null
556
+ longPressPointEpoch = -1L
427
557
  leadingFrame.recycle()
428
558
  trailingColumn.reset()
429
559
  currentItem = null
@@ -592,6 +722,48 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
592
722
  titleLine.packsChildrenAtStart = true
593
723
  title.ellipsize = TextUtils.TruncateAt.END
594
724
  showText(title, item.json.optString("title"), item.json.optInt("titleLines", 1))
725
+ item.json.optJSONArray("titleSuffixes")?.let { suffixes ->
726
+ for (index in 0 until suffixes.length()) {
727
+ val suffix = suffixes.optJSONObject(index) ?: continue
728
+ val label = NativeListTextView(context).apply {
729
+ text = suffix.optString("text")
730
+ typeface = fonts.regular(context)
731
+ textSize = sp(12f)
732
+ includeFontPadding = false
733
+ maxLines = 1
734
+ ellipsize = TextUtils.TruncateAt.END
735
+ setTextColor(
736
+ color(
737
+ theme,
738
+ if (suffix.optString("tone") == "disabled") "disabledText" else "secondaryText",
739
+ "#0000009B",
740
+ )
741
+ )
742
+ }
743
+ TextViewCompat.setLineHeight(label, dp(16))
744
+ val maxWidth =
745
+ if (suffix.has("maxWidth")) stylePx(suffix.optDouble("maxWidth")) else Int.MAX_VALUE
746
+ label.maxWidth = maxWidth
747
+ titleSuffixMaxWidths.add(maxWidth)
748
+ titleSuffixLabels.add(label)
749
+ titleLine.addView(
750
+ label,
751
+ titleLine.childCount - 1,
752
+ LayoutParams(LayoutParams.WRAP_CONTENT, dp(16)).apply { marginStart = dp(8) },
753
+ )
754
+ }
755
+ }
756
+ item.json.optJSONObject("titleIcon")?.let { icon ->
757
+ titleIcon.iconName = icon.optString("name")
758
+ titleIcon.glyphSizeDp = 20
759
+ titleIcon.useSourceScale = sourceScale
760
+ titleIcon.tintColor = safeColor(
761
+ icon.optString("tintColor").takeIf(String::isNotEmpty),
762
+ color(theme, "iconSubdued", "#00000072"),
763
+ )
764
+ titleIcon.contentDescription = icon.optString("accessibilityLabel").takeIf(String::isNotEmpty)
765
+ titleIcon.visibility = VISIBLE
766
+ }
595
767
  if (item.json.optString("presentation") == "accountSelector") {
596
768
  title.typeface = fonts.regular(context)
597
769
  }
@@ -711,6 +883,10 @@ internal class NativeListIdentityRowView(context: ThemedReactContext, fonts: Nat
711
883
  .let { kinds ->
712
884
  kinds.containsAll(listOf("value", "checkbox")) ||
713
885
  kinds == listOf("valuePair", "menu") ||
886
+ kinds == listOf("valuePair", "chevron") ||
887
+ (kinds == listOf("icon", "chevron") &&
888
+ accessories.getJSONObject(0).optString("name") == "RefreshCcwOutline" &&
889
+ accessories.getJSONObject(0).optString("actionKey").isEmpty()) ||
714
890
  (kinds == listOf("icon", "icon") &&
715
891
  accessories.getJSONObject(0).optString("name") == "PencilOutline" &&
716
892
  accessories.getJSONObject(1).optString("name") == "DragOutline")
@@ -27,6 +27,7 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
27
27
  private val cornerBackdrop = View(context)
28
28
  private val slots = mutableListOf<NativeListImageSlot>()
29
29
  private val overlaySlots = mutableListOf<NativeListImageSlot>()
30
+ private val overlayFallbackIcons = mutableListOf<OneKeyIconView>()
30
31
  private val overlays = mutableListOf<Pair<FrameLayout, JSONObject>>()
31
32
  private var visual = JSONObject()
32
33
  private var style = JSONObject()
@@ -159,6 +160,8 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
159
160
  }
160
161
  val placeholder = theme?.optString("strongBackground", "#0000000F") ?: "#0000000F"
161
162
  val fallbackIcon = visual.optJSONObject("fallbackIcon")
163
+ val fallbackBackground = fallbackIcon?.optString("backgroundColor")
164
+ ?.takeIf(String::isNotEmpty) ?: placeholder
162
165
  val sourceFallback =
163
166
  sources.firstOrNull()?.first?.optString("loadingStrategy", "none")?.let { it != "none" } ==
164
167
  true && (visual.has("fallbackText") || fallbackIcon != null)
@@ -179,7 +182,7 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
179
182
  val restored = fallbackKey?.let(NativeListSourceFallbackState::has) == true
180
183
  fallback.textSize = if (sourceScale) 13f else NativeListScale.font(resources, 13f)
181
184
  fallback.text = visual.optString("fallbackText").take(2)
182
- fallback.setTextColor(color("#00000072"))
185
+ fallback.setTextColor(color(visual.optString("fallbackTextColor", "#00000072")))
183
186
  fallback.visibility =
184
187
  if (kind != "icon" && fallbackIcon == null && (sources.isEmpty() || restored)) VISIBLE
185
188
  else GONE
@@ -193,7 +196,9 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
193
196
  color(
194
197
  visual.optString("backgroundColor", if (sources.isEmpty()) placeholder else "#00000000")
195
198
  )
196
- val backgroundColor = if (sourceFallback) color(placeholder) else visualBackground
199
+ val backgroundColor =
200
+ if (sources.isEmpty() && fallbackIcon?.has("backgroundColor") == true || restored) color(fallbackBackground)
201
+ else if (sourceFallback) color(placeholder) else visualBackground
197
202
  background =
198
203
  fill(backgroundColor, 0f).apply {
199
204
  if (kind == "icon" && iconBorder) setStroke(1, color("#0000001F"))
@@ -210,7 +215,7 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
210
215
  slot.recycle()
211
216
  slot.view.visibility = GONE
212
217
  } else {
213
- slot.view.visibility = if (index > 0 || sourceFallback) INVISIBLE else VISIBLE
218
+ slot.view.visibility = if (index > 0 || restored) INVISIBLE else VISIBLE
214
219
  slot.bind(
215
220
  sources[index].first,
216
221
  "$key:leading:$index",
@@ -229,7 +234,7 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
229
234
  else NativeListSourceFallbackState.remember(it)
230
235
  }
231
236
  (background as? GradientDrawable)?.setColor(
232
- if (loaded) visualBackground else color(placeholder)
237
+ if (loaded) visualBackground else color(fallbackBackground)
233
238
  )
234
239
  fallback.visibility = if (!loaded && fallbackIcon == null) VISIBLE else GONE
235
240
  icon.visibility = if (!loaded && fallbackIcon != null) VISIBLE else GONE
@@ -272,8 +277,18 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
272
277
  overlayCount = descriptors.size,
273
278
  )
274
279
  while (overlaySlots.size < descriptors.size) overlaySlots.add(NativeListImageSlot(reactContext))
280
+ while (overlayFallbackIcons.size < descriptors.size)
281
+ overlayFallbackIcons.add(OneKeyIconView(context))
275
282
  descriptors.forEachIndexed { index, data ->
276
283
  val frame = previousOverlays.getOrNull(index)?.first ?: FrameLayout(context)
284
+ val image = data.optJSONObject("image")
285
+ val fallbackData = data.optJSONObject("fallbackIcon")
286
+ val fallbackGlyph = overlayFallbackIcons[index].apply {
287
+ visibility = GONE
288
+ iconName = fallbackData?.optString("name") ?: ""
289
+ tintColor = color(fallbackData?.optString("tintColor", "#0000009B") ?: "#0000009B")
290
+ useSourceScale = sourceScale
291
+ }
277
292
  frame.background =
278
293
  fill(
279
294
  color(data.optString("backgroundColor", "#00000000")),
@@ -285,11 +300,15 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
285
300
  frame.outlineProvider = ViewOutlineProvider.BACKGROUND
286
301
  frame.clipToOutline = true
287
302
  val child: View =
288
- data.optJSONObject("image")?.let { image ->
303
+ image?.let { source ->
289
304
  val slot = overlaySlots[index]
290
305
  slot.view.visibility = INVISIBLE
291
- slot.bind(image, "$key:overlay:$index", placeholder = placeholder) {
306
+ val circularImage = data.optInt("padding", 0) > 0 &&
307
+ data.optInt("width", data.optInt("size", 20)) == overlayHeight(data)
308
+ slot.bind(source, "$key:overlay:$index", placeholder = placeholder,
309
+ round = circularImage) {
292
310
  slot.view.visibility = if (it) VISIBLE else INVISIBLE
311
+ fallbackGlyph.visibility = if (!it && fallbackData != null) VISIBLE else GONE
293
312
  }
294
313
  slot.view
295
314
  }
@@ -306,8 +325,13 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
306
325
  if (walletText) androidx.core.widget.TextViewCompat.setLineHeight(this, dp(16))
307
326
  else setLineSpacing(0f, 1f)
308
327
  typeface =
309
- if (isWalletText(data)) fonts.regular(context)
310
- else fonts.medium(context)
328
+ when (data.optString("fontWeight")) {
329
+ "regular" -> fonts.regular(context)
330
+ "medium" -> fonts.medium(context)
331
+ "semibold" -> fonts.semibold(context)
332
+ "bold" -> fonts.bold(context)
333
+ else -> if (isWalletText(data)) fonts.regular(context) else fonts.medium(context)
334
+ }
311
335
  setTextColor(color(data.optString("tintColor", "#0000009B")))
312
336
  }
313
337
  } else {
@@ -321,11 +345,13 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
321
345
  val inset = data.optInt("padding", 0)
322
346
  if (isWalletText(data)) frame.setPadding(dp(2), 0, dp(2), 0)
323
347
  else frame.setPadding(dp(inset), dp(inset), dp(inset), dp(inset))
324
- if (child.parent !== frame) {
325
- frame.removeAllViews()
326
- // A reused slot view may still belong to a frame dropped by an earlier bind.
327
- (child.parent as? android.view.ViewGroup)?.removeView(child)
328
- frame.addView(child, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
348
+ frame.removeAllViews()
349
+ // A reused slot view may still belong to a frame dropped by an earlier bind.
350
+ (child.parent as? android.view.ViewGroup)?.removeView(child)
351
+ frame.addView(child, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
352
+ if (image != null && fallbackData != null) {
353
+ (fallbackGlyph.parent as? android.view.ViewGroup)?.removeView(fallbackGlyph)
354
+ frame.addView(fallbackGlyph, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
329
355
  }
330
356
  if (frame.parent == null) addView(frame)
331
357
  overlays.add(frame to data)
@@ -383,8 +409,11 @@ internal class NativeListLeadingVisual(private val reactContext: ThemedReactCont
383
409
  )
384
410
  }
385
411
  place(fallback, 0, 0, w, h)
412
+ val explicitFallbackSize = visual.optJSONObject("fallbackIcon")
413
+ ?.optDouble("size", Double.NaN)?.takeIf { it.isFinite() && it > 0.0 }
386
414
  val iconSize =
387
- if (sources.isNotEmpty() && visual.optJSONObject("fallbackIcon") != null)
415
+ if (explicitFallbackSize != null) dp(explicitFallbackSize.roundToInt())
416
+ else if (sources.isNotEmpty() && visual.optJSONObject("fallbackIcon") != null)
388
417
  (minOf(w, h) *
389
418
  if (visual.optJSONObject("fallbackIcon")?.optString("name") == "GlobusOutline") 1.2f
390
419
  else 1f)