@onekeyfe/react-native-native-list 6.7.0-alpha.404 → 6.7.0-alpha.406
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.
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +17 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +15 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +92 -28
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +2 -0
- package/docs/ROW_TEMPLATES.md +6 -2
- package/docs/SPEC.md +97 -3
- package/ios/NativeListActivityDetailsView.swift +13 -4
- package/ios/NativeListMarketRenderer.swift +113 -18
- package/ios/RNCNativeListView.swift +15 -2
- package/ios/tests/NativeListActivityPresentationTests.swift +97 -0
- package/ios/tests/NativeListMarketMetricsTests.swift +117 -0
- package/lib/module/NativeList.js +25 -6
- package/lib/module/validation.js +74 -6
- package/lib/typescript/src/models.d.ts +3 -1
- package/package.json +3 -3
- package/src/NativeList.tsx +30 -10
- package/src/models.ts +14 -0
- package/src/validation.ts +143 -3
|
@@ -16,6 +16,7 @@ internal class NativeListActivityDetailsView(private val reactContext: ThemedRea
|
|
|
16
16
|
var onAction: ((String, View, String, Int) -> Unit)? = null
|
|
17
17
|
private var sourceScale = false
|
|
18
18
|
private val main = LinearLayout(context).apply { orientation = HORIZONTAL; gravity = Gravity.CENTER_VERTICAL }
|
|
19
|
+
private val identityColumn = LinearLayout(context).apply { orientation = HORIZONTAL; gravity = Gravity.CENTER_VERTICAL }
|
|
19
20
|
private val visual = NativeListLeadingVisual(reactContext, fonts)
|
|
20
21
|
private val identity = LinearLayout(context).apply { orientation = VERTICAL }
|
|
21
22
|
private val titleLine = LinearLayout(context).apply { orientation = HORIZONTAL; gravity = Gravity.CENTER_VERTICAL }
|
|
@@ -40,8 +41,9 @@ internal class NativeListActivityDetailsView(private val reactContext: ThemedRea
|
|
|
40
41
|
identity.addView(status)
|
|
41
42
|
feeLine.addView(feePrimary); feeLine.addView(feeSecondary)
|
|
42
43
|
fee.addView(feeLabel); fee.addView(feeLine)
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
identityColumn.addView(visual)
|
|
45
|
+
identityColumn.addView(identity, LayoutParams(0, -2, 1f))
|
|
46
|
+
main.addView(identityColumn, LayoutParams(0, -2, 1f))
|
|
45
47
|
main.addView(amounts)
|
|
46
48
|
main.addView(fee)
|
|
47
49
|
addView(main, LayoutParams(-1, -2))
|
|
@@ -110,7 +112,9 @@ internal class NativeListActivityDetailsView(private val reactContext: ThemedRea
|
|
|
110
112
|
val feeData = data.optJSONObject("fee")
|
|
111
113
|
fee.visibility = if (feeData == null) GONE else VISIBLE
|
|
112
114
|
fee.alpha = if (feeData?.optBoolean("hidden") == true) 0f else 1f
|
|
113
|
-
fee.layoutParams = LayoutParams(-2, -2).apply { marginStart = gap }
|
|
115
|
+
fee.layoutParams = (if (table) LayoutParams(0, -2, 0.6f) else LayoutParams(-2, -2)).apply { marginStart = gap }
|
|
116
|
+
feeLabel.layoutParams.width = if (table) LayoutParams.WRAP_CONTENT else LayoutParams.MATCH_PARENT
|
|
117
|
+
feeLine.layoutParams.width = if (table) LayoutParams.WRAP_CONTENT else LayoutParams.MATCH_PARENT
|
|
114
118
|
text(feeLabel, feeData?.optString("label") ?: "", style.optJSONObject("secondaryAmount"), 12f, color("secondary", theme), 16)
|
|
115
119
|
text(feePrimary, feeData?.optString("primary") ?: "", style.optJSONObject("secondaryAmount"), 14f, color("primary", theme), 20)
|
|
116
120
|
text(feeSecondary, feeData?.optString("secondary") ?: "", style.optJSONObject("secondaryAmount"), 12f, color("secondary", theme), 16)
|
|
@@ -240,9 +240,10 @@ internal class NativeListAdapter(
|
|
|
240
240
|
|
|
241
241
|
private fun isMarketQuoteUpdate(oldItem: NativeListItem, newItem: NativeListItem): Boolean {
|
|
242
242
|
if (oldItem.type != "market" || newItem.type != "market") return false
|
|
243
|
+
if (!hasSameMarketMetricStructure(oldItem, newItem)) return false
|
|
243
244
|
fun stableContent(item: NativeListItem): String {
|
|
244
245
|
val value = JSONObject(item.json.toString())
|
|
245
|
-
listOf("revision", "price", "priceSegments", "change", "accessibilityLabel")
|
|
246
|
+
listOf("revision", "price", "priceSegments", "change", "accessibilityLabel", "metrics")
|
|
246
247
|
.forEach(value::remove)
|
|
247
248
|
return value.toString()
|
|
248
249
|
}
|
|
@@ -256,3 +257,18 @@ internal const val SELECTION_PAYLOAD = "selection"
|
|
|
256
257
|
// OneKey patch: internal payload, never exposed through the serialized row contract.
|
|
257
258
|
internal const val SELECTION_ECHO_PAYLOAD = "selectionEcho"
|
|
258
259
|
internal const val MARKET_QUOTE_PAYLOAD = "marketQuote"
|
|
260
|
+
|
|
261
|
+
internal fun hasSameMarketMetricStructure(before: NativeListItem, after: NativeListItem): Boolean {
|
|
262
|
+
val old = before.json.optJSONArray("metrics")
|
|
263
|
+
val next = after.json.optJSONArray("metrics")
|
|
264
|
+
val oldCount = old?.length() ?: 0
|
|
265
|
+
if (oldCount != (next?.length() ?: 0)) return false
|
|
266
|
+
for (index in 0 until oldCount) {
|
|
267
|
+
val previous = old?.optJSONObject(index) ?: return false
|
|
268
|
+
val current = next?.optJSONObject(index) ?: return false
|
|
269
|
+
if (previous.optString("key") != current.optString("key") ||
|
|
270
|
+
previous.optInt("weight", 1) != current.optInt("weight", 1) ||
|
|
271
|
+
previous.optString("alignment", "end") != current.optString("alignment", "end")) return false
|
|
272
|
+
}
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
@@ -292,12 +292,14 @@ private class NativeListTableColumnView(context: android.content.Context, privat
|
|
|
292
292
|
|
|
293
293
|
fun reset() {
|
|
294
294
|
primaryLine.gravity = Gravity.CENTER_VERTICAL
|
|
295
|
+
secondaryLine.gravity = Gravity.CENTER_VERTICAL
|
|
295
296
|
for (icon in listOf(titleIcon, secondaryTitleIcon)) {
|
|
296
297
|
icon.visibility = GONE
|
|
297
298
|
icon.iconName = ""
|
|
298
299
|
icon.contentDescription = null
|
|
299
300
|
icon.setOnClickListener(null)
|
|
300
301
|
}
|
|
302
|
+
(primaryLine.layoutParams as LayoutParams).gravity = -1
|
|
301
303
|
primaryLine.layoutParams.width = LayoutParams.WRAP_CONTENT
|
|
302
304
|
primaryLine.layoutParams.height = LayoutParams.WRAP_CONTENT
|
|
303
305
|
secondaryLine.layoutParams.height = LayoutParams.WRAP_CONTENT
|
|
@@ -357,6 +359,17 @@ private class NativeListTableColumnView(context: android.content.Context, privat
|
|
|
357
359
|
"end" -> Gravity.END
|
|
358
360
|
else -> Gravity.START
|
|
359
361
|
}
|
|
362
|
+
fun horizontalGravity(alignment: String) = when (alignment) {
|
|
363
|
+
"center" -> Gravity.CENTER_HORIZONTAL
|
|
364
|
+
"end" -> Gravity.END
|
|
365
|
+
else -> Gravity.START
|
|
366
|
+
}
|
|
367
|
+
val primaryAlignment = style.optJSONObject("columns")?.optString("alignment")
|
|
368
|
+
?.takeIf(String::isNotEmpty) ?: column.optString("alignment", "start")
|
|
369
|
+
(primaryLine.layoutParams as LayoutParams).gravity = horizontalGravity(primaryAlignment)
|
|
370
|
+
val secondaryAlignment = style.optJSONObject("columnSecondary")?.optString("alignment")
|
|
371
|
+
?.takeIf(String::isNotEmpty) ?: column.optString("alignment", "start")
|
|
372
|
+
secondaryLine.gravity = Gravity.CENTER_VERTICAL or horizontalGravity(secondaryAlignment)
|
|
360
373
|
fun bindText(
|
|
361
374
|
view: NativeListTextView,
|
|
362
375
|
value: String,
|
|
@@ -433,6 +446,8 @@ private class NativeListTableColumnView(context: android.content.Context, privat
|
|
|
433
446
|
secondaryLine.visibility = VISIBLE
|
|
434
447
|
}
|
|
435
448
|
if (!fixedTableGeometry) applyStyle(style)
|
|
449
|
+
(secondary.layoutParams as LayoutParams).marginStart =
|
|
450
|
+
if (secondaryLeading.visibility == VISIBLE) dp(4) else 0
|
|
436
451
|
if (fixedTableGeometry) {
|
|
437
452
|
// Legacy table lines: fixed 20dp primary and 16dp secondary/badge boxes.
|
|
438
453
|
primaryLine.layoutParams.height = dp(20)
|
|
@@ -38,6 +38,9 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
38
38
|
private val marketSubtitleLine = PackedTitleLineLayout(context)
|
|
39
39
|
private val trailingColumn = LinearLayout(context)
|
|
40
40
|
private val trailingViews = List(2) { NativeListTextView(context) }
|
|
41
|
+
private val metricColumns = List(3) { LinearLayout(context) }
|
|
42
|
+
private val metricPrimaryViews = List(3) { NativeListTextView(context) }
|
|
43
|
+
private val metricSecondaryViews = List(3) { NativeListTextView(context) }
|
|
41
44
|
private val marketBadgeViews = List(3) { LinearLayout(context) }
|
|
42
45
|
private val marketBadgeLabels = List(3) { TextView(context) }
|
|
43
46
|
private val badgeSlots = List(3) { NativeListImageSlot(context) }
|
|
@@ -88,6 +91,12 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
88
91
|
mainColumn.addView(subtitle)
|
|
89
92
|
mainColumn.addView(tertiary)
|
|
90
93
|
trailingViews.forEach(trailingColumn::addView)
|
|
94
|
+
metricColumns.forEachIndexed { index, column ->
|
|
95
|
+
column.orientation = VERTICAL
|
|
96
|
+
column.gravity = Gravity.CENTER_VERTICAL
|
|
97
|
+
column.addView(metricPrimaryViews[index])
|
|
98
|
+
column.addView(metricSecondaryViews[index])
|
|
99
|
+
}
|
|
91
100
|
leadingActionIcon.useSourceScale = true
|
|
92
101
|
marketBadgeViews.forEachIndexed { index, badge ->
|
|
93
102
|
badge.orientation = HORIZONTAL
|
|
@@ -129,6 +138,50 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
129
138
|
|
|
130
139
|
private fun weighted() = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
|
|
131
140
|
|
|
141
|
+
private fun metricColor(theme: JSONObject?, tone: String): Int =
|
|
142
|
+
when (tone) {
|
|
143
|
+
"positive" -> color(theme, "positive", "#218358")
|
|
144
|
+
"negative" -> color(theme, "negative", "#CE2C31")
|
|
145
|
+
"secondary" -> color(theme, "secondaryText", "#646464")
|
|
146
|
+
else -> color(theme, "primaryText", "#202020")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun bindMetrics(metrics: JSONArray, theme: JSONObject?, gap: Int) {
|
|
150
|
+
updateMetricLabels(metrics, theme)
|
|
151
|
+
for (index in 0 until metrics.length()) {
|
|
152
|
+
val metric = metrics.getJSONObject(index)
|
|
153
|
+
addView(
|
|
154
|
+
metricColumns[index],
|
|
155
|
+
LayoutParams(0, LayoutParams.WRAP_CONTENT, metric.optInt("weight", 1).toFloat()).apply {
|
|
156
|
+
marginStart = dp(gap)
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private fun updateMetricLabels(metrics: JSONArray, theme: JSONObject?) {
|
|
163
|
+
for (index in 0 until metrics.length()) {
|
|
164
|
+
val metric = metrics.getJSONObject(index)
|
|
165
|
+
val alignment = metric.optString("alignment", "end")
|
|
166
|
+
val primary = metricPrimaryViews[index]
|
|
167
|
+
val secondary = metricSecondaryViews[index]
|
|
168
|
+
applyMarketTextStyle(
|
|
169
|
+
primary, null, 14f, 20, "medium",
|
|
170
|
+
metricColor(theme, metric.optString("tone")), alignment,
|
|
171
|
+
)
|
|
172
|
+
primary.text = marketText(metric.optString("text"), metric.optJSONArray("textSegments"), 14f)
|
|
173
|
+
primary.visibility = VISIBLE
|
|
174
|
+
applyMarketTextStyle(
|
|
175
|
+
secondary, null, 12f, 16, "regular",
|
|
176
|
+
metricColor(theme, metric.optString("secondaryTone", "secondary")), alignment,
|
|
177
|
+
)
|
|
178
|
+
secondary.text = marketText(
|
|
179
|
+
metric.optString("secondaryText"), metric.optJSONArray("secondaryTextSegments"), 12f,
|
|
180
|
+
)
|
|
181
|
+
secondary.visibility = if (secondary.text.isEmpty()) GONE else VISIBLE
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
132
185
|
private fun showText(view: TextView, text: String, lines: Int) {
|
|
133
186
|
view.text = text
|
|
134
187
|
view.maxLines = lines
|
|
@@ -146,18 +199,27 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
146
199
|
emitAction(key, view, source, slot, target)
|
|
147
200
|
}
|
|
148
201
|
|
|
149
|
-
override fun accessibilityText(item: NativeListItem)
|
|
150
|
-
item.json.
|
|
151
|
-
"accessibilityLabel"
|
|
202
|
+
override fun accessibilityText(item: NativeListItem): String {
|
|
203
|
+
if (item.json.has("accessibilityLabel") && !item.json.isNull("accessibilityLabel")) {
|
|
204
|
+
return item.json.optString("accessibilityLabel")
|
|
205
|
+
}
|
|
206
|
+
val metrics = item.json.optJSONArray("metrics")?.takeIf { it.length() in 2..3 }
|
|
207
|
+
val values = if (metrics != null) {
|
|
208
|
+
(0 until metrics.length()).flatMap { index ->
|
|
209
|
+
listOf(metricPrimaryViews[index], metricSecondaryViews[index])
|
|
210
|
+
.filter { it.visibility == VISIBLE }
|
|
211
|
+
.map { it.text.toString() }
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
152
214
|
listOf(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
215
|
+
item.json.optString("price"),
|
|
216
|
+
item.json.optJSONObject("change")?.optString("text") ?: "",
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
return (listOf(item.json.optString("title"), item.json.optString("subtitle")) + values)
|
|
220
|
+
.filter(String::isNotEmpty)
|
|
221
|
+
.joinToString(", ")
|
|
222
|
+
}
|
|
161
223
|
|
|
162
224
|
override fun bindContent(
|
|
163
225
|
item: NativeListItem,
|
|
@@ -183,7 +245,8 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
183
245
|
if (item.json.optJSONArray("badges")?.optJSONObject(index)?.optJSONObject("icon") == null)
|
|
184
246
|
badgeSlots[index].recycle()
|
|
185
247
|
}
|
|
186
|
-
for (view in listOf(title, subtitle, tertiary) + trailingViews
|
|
248
|
+
for (view in listOf(title, subtitle, tertiary) + trailingViews +
|
|
249
|
+
metricPrimaryViews + metricSecondaryViews) {
|
|
187
250
|
view.text = ""
|
|
188
251
|
view.visibility = GONE
|
|
189
252
|
view.maxLines = 1
|
|
@@ -217,7 +280,13 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
217
280
|
}
|
|
218
281
|
|
|
219
282
|
override fun bindMarketQuote(item: NativeListItem, theme: JSONObject?) {
|
|
220
|
-
if (retainBoundItem(item))
|
|
283
|
+
if (retainBoundItem(item)) {
|
|
284
|
+
item.json.optJSONArray("metrics")?.takeIf { it.length() in 2..3 }?.let {
|
|
285
|
+
updateMetricLabels(it, theme)
|
|
286
|
+
}
|
|
287
|
+
bindQuote(item, theme)
|
|
288
|
+
contentDescription = accessibilityText(item)
|
|
289
|
+
}
|
|
221
290
|
}
|
|
222
291
|
|
|
223
292
|
private fun cancelMarketHold() {
|
|
@@ -304,6 +373,7 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
304
373
|
longPressFired = false
|
|
305
374
|
leadingFrame.recycle()
|
|
306
375
|
badgeSlots.forEach { it.recycle() }
|
|
376
|
+
(metricPrimaryViews + metricSecondaryViews).forEach { it.text = "" }
|
|
307
377
|
imageSignature = ""
|
|
308
378
|
}
|
|
309
379
|
|
|
@@ -561,10 +631,12 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
561
631
|
)
|
|
562
632
|
// OneKey patch: retain the source gap without changing other templates.
|
|
563
633
|
// addView(mainColumn, weighted())
|
|
634
|
+
val metrics = item.json.optJSONArray("metrics")?.takeIf { it.length() in 2..3 }
|
|
564
635
|
addView(
|
|
565
636
|
mainColumn,
|
|
566
637
|
weighted().apply {
|
|
567
|
-
|
|
638
|
+
if (metrics != null) weight = 2f
|
|
639
|
+
else marginEnd = dp(style?.optDouble("contentTrailingGap", 0.0)?.roundToInt() ?: 0)
|
|
568
640
|
},
|
|
569
641
|
)
|
|
570
642
|
titleLine.packsChildrenAtStart = true
|
|
@@ -748,7 +820,11 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
748
820
|
}
|
|
749
821
|
trailingColumn.orientation = HORIZONTAL
|
|
750
822
|
trailingColumn.gravity = Gravity.END or Gravity.CENTER_VERTICAL
|
|
751
|
-
|
|
823
|
+
if (metrics != null) {
|
|
824
|
+
bindMetrics(metrics, theme, style?.optDouble("trailingGap", 8.0)?.roundToInt() ?: 8)
|
|
825
|
+
} else {
|
|
826
|
+
addView(trailingColumn, wrap())
|
|
827
|
+
}
|
|
752
828
|
bindQuote(item, theme)
|
|
753
829
|
val nextImageSignature =
|
|
754
830
|
nativeListCanonical(
|
|
@@ -861,18 +937,6 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
861
937
|
)
|
|
862
938
|
}
|
|
863
939
|
}
|
|
864
|
-
contentDescription =
|
|
865
|
-
item.json.optString(
|
|
866
|
-
"accessibilityLabel",
|
|
867
|
-
listOf(
|
|
868
|
-
item.json.optString("title"),
|
|
869
|
-
item.json.optString("subtitle"),
|
|
870
|
-
item.json.optString("price"),
|
|
871
|
-
changeData.optString("text"),
|
|
872
|
-
)
|
|
873
|
-
.filter(String::isNotEmpty)
|
|
874
|
-
.joinToString(", "),
|
|
875
|
-
)
|
|
876
940
|
}
|
|
877
941
|
|
|
878
942
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
@@ -912,7 +976,7 @@ internal class NativeListMarketRowView(context: ThemedReactContext, fonts: Nativ
|
|
|
912
976
|
?.has("contentVerticalAlignment") == true
|
|
913
977
|
if (item.type == "market" && item.json.optJSONObject("style") != null) {
|
|
914
978
|
// OneKey patch: preserve Yoga's half-pixel centering for Market columns and badges.
|
|
915
|
-
for (column in listOf(leadingFrame, mainColumn, trailingColumn)) {
|
|
979
|
+
for (column in listOf(leadingFrame, mainColumn, trailingColumn) + metricColumns) {
|
|
916
980
|
if (!hasContainerAlignment && column.parent === this && column.visibility != GONE) {
|
|
917
981
|
column.offsetTopAndBottom((height - column.height + 1) / 2 - column.top)
|
|
918
982
|
}
|
|
@@ -2943,6 +2943,7 @@ class NativeListView(
|
|
|
2943
2943
|
previous.type == "market" &&
|
|
2944
2944
|
changes.keys().asSequence().all(MARKET_QUOTE_FIELDS::contains)
|
|
2945
2945
|
val merged = NativeListItem.parse(mergeRow(previous.json, changes))
|
|
2946
|
+
marketQuoteOnly = marketQuoteOnly && hasSameMarketMetricStructure(previous, merged)
|
|
2946
2947
|
nextItems[index] = merged
|
|
2947
2948
|
if (changes.has("selected")) {
|
|
2948
2949
|
if (changes.optBoolean("selected")) selected.add(merged.key) else selected.remove(merged.key)
|
|
@@ -4838,6 +4839,7 @@ class NativeListView(
|
|
|
4838
4839
|
"priceSegments",
|
|
4839
4840
|
"change",
|
|
4840
4841
|
"accessibilityLabel",
|
|
4842
|
+
"metrics",
|
|
4841
4843
|
)
|
|
4842
4844
|
private const val SECTION_INDEX_CONTENT_INSET_DP = 16
|
|
4843
4845
|
private const val SECTION_INDEX_RAIL_WIDTH_DP = 32
|
package/docs/ROW_TEMPLATES.md
CHANGED
|
@@ -182,8 +182,12 @@ separately from source implementation in STYLE_SPEC §9.
|
|
|
182
182
|
with at most 32 unique keyed entries. Each entry supports a leading visual,
|
|
183
183
|
tone, primary/secondary text and subscript segments. The title supports at
|
|
184
184
|
most four keyed badges. `descriptionActionKey` emits a description action.
|
|
185
|
-
`presentation: 'table'` allocates
|
|
186
|
-
optional fee
|
|
185
|
+
`presentation: 'table'` allocates identity (including the leading visual and
|
|
186
|
+
its gap), amounts and optional fee in a stable `1:1:0.6` ratio after column
|
|
187
|
+
gaps. Without fee, identity and amounts have equal widths. Amount content
|
|
188
|
+
stays start-aligned; native fee content stays end-aligned, while Web retains its
|
|
189
|
+
existing start alignment. Fee text length and leading visibility do not move
|
|
190
|
+
the amount column. `stacked` keeps right-aligned amounts beside identity.
|
|
187
191
|
`fee.hidden` reserves its column while hiding its content. The Web rich
|
|
188
192
|
Activity renderer uses these fields and the same `hideLeading` visibility.
|
|
189
193
|
|
package/docs/SPEC.md
CHANGED
|
@@ -375,6 +375,33 @@ migration itself does not change any other protocol field.
|
|
|
375
375
|
|
|
376
376
|
## Lifecycle and concurrency
|
|
377
377
|
|
|
378
|
+
### Updates before native host binding
|
|
379
|
+
|
|
380
|
+
Status: implemented in local source, unreleased. The delayed-host regression fails
|
|
381
|
+
on the previous wrapper and passes with this change; 438 package JS/Web tests and
|
|
382
|
+
the package type check pass. This local JavaScript revision is verified with real
|
|
383
|
+
iOS Fabric/UICollectionView rendering on an iPad simulator: delayed-ref,
|
|
384
|
+
parent-layout-before-ref without an artificial delay, and host-ready control
|
|
385
|
+
scenarios all retain the three complete rows after a later single-row update.
|
|
386
|
+
The actual Home list also recovered all six rows after the task-specific Metro
|
|
387
|
+
reload. Earlier natural re-entry evidence included development Metro bundle
|
|
388
|
+
re-requests, so it does not establish a menu-only trigger or a Sui-specific natural
|
|
389
|
+
reproduction. Android device acceptance remains unverified.
|
|
390
|
+
|
|
391
|
+
Valid snapshots and row patches accepted before the native host is available MUST
|
|
392
|
+
retain the latest owned logical state. When that host binds, its rows MUST reflect
|
|
393
|
+
all accepted updates that are eligible for presentation, including updates to rows
|
|
394
|
+
that do not change again afterward. Later single-row updates MUST NOT leave other
|
|
395
|
+
rows at their initial placeholders. Existing disclosure measurement gates and
|
|
396
|
+
snapshot replacement semantics still apply. Once the host is ready, ordinary row
|
|
397
|
+
patches MUST remain incremental rather than requiring a complete snapshot for each
|
|
398
|
+
patch. This contract does not add a public prop, method, or Nitro ABI field.
|
|
399
|
+
|
|
400
|
+
Conformance covers multiple pre-bind patch batches, newer complete prop and
|
|
401
|
+
imperative snapshots, rejected updates, equivalent prop clones after slot layout,
|
|
402
|
+
selection state, repeated callbacks, distinct replacement hosts, and incremental
|
|
403
|
+
single-row updates after binding.
|
|
404
|
+
|
|
378
405
|
Full binding must work without a preceding recycle callback, including
|
|
379
406
|
style set/change/clear and content A → B. A binding epoch invalidates stale
|
|
380
407
|
image callbacks and action anchors. Recycling/disposal releases pending work
|
|
@@ -553,6 +580,14 @@ existing placeholders, and positive title-image dimensions are clamped to a
|
|
|
553
580
|
square of 1...40 logical units. Image headers are string dictionaries, never
|
|
554
581
|
interpreted as geometry. Unsafe object graphs are rejected before normalization.
|
|
555
582
|
|
|
583
|
+
Shared image URI validation accepts the entire `onekey-avatar:` scheme,
|
|
584
|
+
case-insensitively, in snapshots and row patches on iOS, Android and Web. It does
|
|
585
|
+
not restrict the host, version, path, query string or fragment, and preserves the
|
|
586
|
+
URI without decoding. Existing image URI schemes retain their acceptance rules.
|
|
587
|
+
Unsupported image URIs fail development assertions and are sanitized out in
|
|
588
|
+
production. This is validation acceptance only; avatar decoding and loading
|
|
589
|
+
remain owned by the platform image loader or Web avatar worker.
|
|
590
|
+
|
|
556
591
|
A complete snapshot remains atomic after decoration sanitization: invalid
|
|
557
592
|
schema, duplicate/missing identities, required business fields or wallet-group
|
|
558
593
|
parent/children relationships reject the snapshot. No row is silently removed.
|
|
@@ -1434,9 +1469,14 @@ secondary text and subscript text segments. Segments are caller-formatted and
|
|
|
1434
1469
|
never interpreted as numbers by NativeList. Omission preserves the legacy tree.
|
|
1435
1470
|
|
|
1436
1471
|
`presentation: stacked` places the amount lines after the identity column and
|
|
1437
|
-
right-aligns them
|
|
1438
|
-
|
|
1439
|
-
|
|
1472
|
+
right-aligns them. In `table`, the identity column includes its leading visual
|
|
1473
|
+
and gap; identity, amounts and an optional fee use stable widths in the ratio
|
|
1474
|
+
`1:1:0.6` after inter-column gaps. Without fee, identity and amounts split the
|
|
1475
|
+
remaining width equally. Amount content stays start-aligned within its column;
|
|
1476
|
+
native fee content stays end-aligned; Web retains its existing start alignment.
|
|
1477
|
+
Fee text length and leading visibility cannot move the amount column. These
|
|
1478
|
+
native widths match the existing Web table layout. Fee supports label, primary
|
|
1479
|
+
and secondary text with their own segments; `hidden` preserves its column width
|
|
1440
1480
|
while hiding its contents. Up to four uniquely keyed title badges use declared
|
|
1441
1481
|
tones. `descriptionActionKey` makes only the description emit that action with
|
|
1442
1482
|
source `description`; footer actions retain their existing source/keys and
|
|
@@ -1457,6 +1497,15 @@ baseline, matching NumberSizeableText), badge tone/clear, hidden fee, disabled
|
|
|
1457
1497
|
actions, description anchors, dark/RTL/font scaling and explicit/automatic height.
|
|
1458
1498
|
|
|
1459
1499
|
|
|
1500
|
+
Table regression acceptance additionally measures amount-column start, width and
|
|
1501
|
+
center across short/long/hidden fee, leading visibility, 1/2/6/32 amount lines and
|
|
1502
|
+
table/stacked reuse. Android DataRow primary and secondary groups honor their
|
|
1503
|
+
resolved text-role alignment (role style first, then column alignment), including
|
|
1504
|
+
start/center/end and styled/unstyled reuse. The secondary-prefix gap is present
|
|
1505
|
+
only when that prefix is visible; legacy linear rendering is unchanged. The UIKit
|
|
1506
|
+
regression explicitly forces each renderer view's direction; React Native/Fabric
|
|
1507
|
+
host direction propagation and real App RTL remain unverified.
|
|
1508
|
+
|
|
1460
1509
|
### Android ancestor scroll distance
|
|
1461
1510
|
|
|
1462
1511
|
Threshold hysteresis combines the primary RecyclerView's actual consumed scroll
|
|
@@ -1634,6 +1683,51 @@ row/presentation recycling, strict validation, native dictionary safe-ignore,
|
|
|
1634
1683
|
header/body geometry, 100 quote ticks preserving nodes with zero geometry reads,
|
|
1635
1684
|
then a real complex Market table before expanding to all five families.
|
|
1636
1685
|
|
|
1686
|
+
### Native Market metric columns
|
|
1687
|
+
|
|
1688
|
+
Status: implemented in local source, unreleased; native runtime verification
|
|
1689
|
+
pending. A `market` row may supply two or three ordered, uniquely
|
|
1690
|
+
keyed text columns through `metrics`, typed as a `Pick<DataColumn, ...>` of
|
|
1691
|
+
the supported text fields. Each column uses
|
|
1692
|
+
the text subset of `DataColumn`: key, primary text and segments, optional
|
|
1693
|
+
secondary text and segments, weight, alignment, and primary/secondary tone.
|
|
1694
|
+
Icon and secondary-leading fields are rejected in development and stripped by
|
|
1695
|
+
the existing production decoration sanitizer.
|
|
1696
|
+
Keys are nonempty and callers format all text. The row keeps its required
|
|
1697
|
+
`price` and `change` fields as compact fallbacks. When `metrics` is present,
|
|
1698
|
+
iOS and Android display the existing leading action, visual, title, subtitle,
|
|
1699
|
+
and Market badges, then the metric columns in place of the compact price and
|
|
1700
|
+
change block. The metrics are authoritative for this native presentation; the
|
|
1701
|
+
SDK does not infer their values from the fallback fields or product data.
|
|
1702
|
+
The default native accessibility label includes the title and subtitle followed
|
|
1703
|
+
by the displayed metric text, primary then secondary, in column order. It
|
|
1704
|
+
updates with each quote. An explicit row accessibility label takes precedence.
|
|
1705
|
+
The metrics use their own text tones and alignment, with both lines aligned
|
|
1706
|
+
the same way; omitted alignment defaults to `end`. Existing change block
|
|
1707
|
+
width, height, and corner radius do not
|
|
1708
|
+
apply to metric columns. The title content receives a relative weight of two;
|
|
1709
|
+
metric columns receive their declared `weight` (1...3, default one) after the
|
|
1710
|
+
row's existing action, visual, padding, and gaps take their space. The SDK
|
|
1711
|
+
does not choose a mode from screen width. Without `metrics`, all platforms
|
|
1712
|
+
retain their existing compact Market presentation. Web ignores `metrics` and
|
|
1713
|
+
continues using `webTable` when supplied.
|
|
1714
|
+
|
|
1715
|
+
Metric values travel in the existing row snapshot JSON and do not change the
|
|
1716
|
+
Nitro ABI, row key, action keys, badge anchors, or scroll ownership. Full
|
|
1717
|
+
snapshots may add, replace, or omit `metrics`; omission restores compact mode.
|
|
1718
|
+
An explicit patch with a valid two- or three-column array replaces metrics,
|
|
1719
|
+
while an omitted patch field leaves them unchanged. A full snapshot is used
|
|
1720
|
+
to remove metrics from a wide row; this contract adds no null patch meaning.
|
|
1721
|
+
Binding and recycling clear inactive metric and compact views in both
|
|
1722
|
+
directions, including changes between two and three columns, so reused rows
|
|
1723
|
+
cannot show stale text or targets. Metric fields contain no new action target;
|
|
1724
|
+
Market badge, leading action, press, press-in, and long-press behavior remains
|
|
1725
|
+
owned by the existing Market row. Conformance requires validation and patch
|
|
1726
|
+
tests, native bind/reuse and action-anchor checks, then consuming App device
|
|
1727
|
+
checks at narrow and wide list widths. Source and unit tests alone do not
|
|
1728
|
+
establish native layout acceptance.
|
|
1729
|
+
|
|
1730
|
+
|
|
1637
1731
|
|
|
1638
1732
|
### Web grid fractional column geometry
|
|
1639
1733
|
|
|
@@ -114,8 +114,9 @@ final class NativeListActivityDetailsView: UIStackView {
|
|
|
114
114
|
let feeColumn = data.dictionary("fee") == nil ? 0 : gap + feeWidth
|
|
115
115
|
let identityWidth: CGFloat
|
|
116
116
|
if table {
|
|
117
|
-
let
|
|
118
|
-
|
|
117
|
+
let hasFee = data.dictionary("fee") != nil
|
|
118
|
+
let columnWidth = max(0, mainWidth - gap * (hasFee ? 2 : 1)) / (hasFee ? 2.6 : 2)
|
|
119
|
+
identityWidth = columnWidth - leadingWidth
|
|
119
120
|
} else {
|
|
120
121
|
identityWidth = mainWidth - leadingWidth - gap - feeColumn - min(amountsWidth, mainWidth * 0.5)
|
|
121
122
|
}
|
|
@@ -163,9 +164,17 @@ final class NativeListActivityDetailsView: UIStackView {
|
|
|
163
164
|
dimensions = [
|
|
164
165
|
visualWidth,
|
|
165
166
|
visual.heightAnchor.constraint(equalToConstant: CGFloat(image.double("height", default: 40))),
|
|
166
|
-
amounts.widthAnchor.constraint(lessThanOrEqualTo: main.widthAnchor, multiplier: table ? 0.42 : 0.5),
|
|
167
167
|
]
|
|
168
|
-
if table {
|
|
168
|
+
if table {
|
|
169
|
+
// The leading visual belongs to identity, so its visibility cannot shift amounts.
|
|
170
|
+
let leadingWidth = hideLeading ? 0 : CGFloat(image.double("width", default: 40)) + gap
|
|
171
|
+
dimensions.append(amounts.widthAnchor.constraint(equalTo: identity.widthAnchor, constant: leadingWidth))
|
|
172
|
+
if data.dictionary("fee") != nil {
|
|
173
|
+
dimensions.append(fee.widthAnchor.constraint(equalTo: amounts.widthAnchor, multiplier: 0.6))
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
dimensions.append(amounts.widthAnchor.constraint(lessThanOrEqualTo: main.widthAnchor, multiplier: 0.5))
|
|
177
|
+
}
|
|
169
178
|
NSLayoutConstraint.activate(dimensions)
|
|
170
179
|
visual.isHidden = hideLeading
|
|
171
180
|
if hideLeading {
|