@onekeyfe/react-native-native-list 3.0.116 → 3.0.118
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/NativeListRowView.kt +98 -21
- package/ios/NativeListCell.swift +112 -23
- package/ios/RNCNativeListView.swift +3 -4
- package/lib/module/NativeList.js +12 -8
- package/lib/module/NativeList.web.js +14 -0
- package/lib/module/index.js +1 -1
- package/lib/module/validation.js +7 -0
- package/lib/module/web/NativeListWebEngine.js +97 -16
- package/lib/typescript/src/NativeList.d.ts +2 -0
- package/lib/typescript/src/NativeList.web.d.ts +2 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/models.d.ts +4 -1
- package/package.json +3 -3
- package/src/NativeList.tsx +23 -15
- package/src/NativeList.web.tsx +38 -1
- package/src/index.ts +1 -1
- package/src/models.ts +2 -0
- package/src/validation.ts +10 -0
- package/src/web/NativeListWebEngine.ts +143 -26
|
@@ -506,6 +506,7 @@ internal class NativeListRowView(
|
|
|
506
506
|
private var checkboxUsesSelectorStyle = false
|
|
507
507
|
private var iconSubduedColor = Color.rgb(141, 141, 141)
|
|
508
508
|
private var visualBackdropColor = Color.WHITE
|
|
509
|
+
private var imagePlaceholderColor = "#0000000F"
|
|
509
510
|
private val circleOutlineProvider = object : ViewOutlineProvider() {
|
|
510
511
|
override fun getOutline(view: View, outline: Outline) {
|
|
511
512
|
outline.setOval(0, 0, view.width, view.height)
|
|
@@ -914,6 +915,8 @@ internal class NativeListRowView(
|
|
|
914
915
|
}
|
|
915
916
|
iconSubduedColor = color(theme, "iconSubdued", "#00000072")
|
|
916
917
|
visualBackdropColor = color(theme, "rowBackground", "#FFFFFF")
|
|
918
|
+
imagePlaceholderColor = theme?.optString("strongBackground", "#0000000F")
|
|
919
|
+
?.takeIf(String::isNotEmpty) ?: "#0000000F"
|
|
917
920
|
unreadDot.background = roundedFill(
|
|
918
921
|
parseNativeListColor("#E5484D"),
|
|
919
922
|
4f,
|
|
@@ -1367,6 +1370,8 @@ internal class NativeListRowView(
|
|
|
1367
1370
|
leadingActionIcon.iconName = ""
|
|
1368
1371
|
leadingActionIcon.glyphSizeDp = 24
|
|
1369
1372
|
leadingActionIcon.setOnClickListener(null)
|
|
1373
|
+
leadingActionIcon.setTag(com.facebook.react.R.id.react_test_id, null)
|
|
1374
|
+
leadingActionIcon.contentDescription = null
|
|
1370
1375
|
mainColumn.visibility = VISIBLE
|
|
1371
1376
|
dataContainer.visibility = GONE
|
|
1372
1377
|
dataColumns.forEach { it.visibility = GONE }
|
|
@@ -2106,6 +2111,33 @@ internal class NativeListRowView(
|
|
|
2106
2111
|
?: if (variant == "perp") 8 else 14
|
|
2107
2112
|
setPadding(dp(horizontalPadding), dp(verticalPadding), dp(horizontalPadding), dp(verticalPadding))
|
|
2108
2113
|
|
|
2114
|
+
item.json.optJSONObject("leadingAction")?.let { action ->
|
|
2115
|
+
leadingActionIcon.visibility = VISIBLE
|
|
2116
|
+
leadingActionIcon.iconName = action.optString("name")
|
|
2117
|
+
leadingActionIcon.glyphSizeDp = 24
|
|
2118
|
+
leadingActionIcon.tintColor = safeColor(
|
|
2119
|
+
action.optString("tintColor"),
|
|
2120
|
+
color(theme, "icon", "#0000009B"),
|
|
2121
|
+
)
|
|
2122
|
+
leadingActionIcon.isEnabled = !action.optBoolean("disabled", false)
|
|
2123
|
+
leadingActionIcon.alpha = if (leadingActionIcon.isEnabled) 1f else 0.4f
|
|
2124
|
+
leadingActionIcon.setTag(
|
|
2125
|
+
com.facebook.react.R.id.react_test_id,
|
|
2126
|
+
action.optString("testID").takeIf(String::isNotEmpty),
|
|
2127
|
+
)
|
|
2128
|
+
leadingActionIcon.contentDescription = action.optString("accessibilityLabel")
|
|
2129
|
+
leadingActionIcon.setOnClickListener {
|
|
2130
|
+
emitAction(
|
|
2131
|
+
item,
|
|
2132
|
+
action.optString("actionKey"),
|
|
2133
|
+
null,
|
|
2134
|
+
leadingActionIcon,
|
|
2135
|
+
"leadingAction",
|
|
2136
|
+
)
|
|
2137
|
+
}
|
|
2138
|
+
addView(leadingActionIcon, LayoutParams(dp(36), dp(36)).apply { marginEnd = dp(5) })
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2109
2141
|
val leading = JSONObject(item.json.getJSONObject("leading").toString())
|
|
2110
2142
|
imageStyle?.optString("shape")?.takeIf(String::isNotEmpty)?.let { leading.put("shape", it) }
|
|
2111
2143
|
imageStyle?.optString("contentFit")?.takeIf(String::isNotEmpty)?.let { contentFit ->
|
|
@@ -2463,11 +2495,12 @@ internal class NativeListRowView(
|
|
|
2463
2495
|
mediaMetadataRow.gravity = Gravity.CENTER_VERTICAL
|
|
2464
2496
|
mediaMetadataRow.addView(subtitle, weighted().apply { marginEnd = dp(8) })
|
|
2465
2497
|
item.json.optJSONObject("networkImage")?.let { networkImage ->
|
|
2466
|
-
|
|
2498
|
+
// OneKey patch: Preserve badge layout without exposing a loading/error tile.
|
|
2499
|
+
// mediaNetworkImage.visibility = VISIBLE
|
|
2467
2500
|
mediaNetworkImage.outlineProvider = circleOutlineProvider
|
|
2468
2501
|
mediaNetworkImage.clipToOutline = true
|
|
2469
2502
|
mediaMetadataRow.addView(mediaNetworkImage, LayoutParams(dp(14), dp(14)))
|
|
2470
|
-
bindImage(networkImage, mediaNetworkImage, item.key, 2, "network")
|
|
2503
|
+
bindImage(networkImage, mediaNetworkImage, item.key, 2, "network", hideUntilLoaded = true)
|
|
2471
2504
|
}
|
|
2472
2505
|
mainColumn.addView(mediaMetadataRow, 0)
|
|
2473
2506
|
mainColumn.addView(titleLine, 1)
|
|
@@ -3155,11 +3188,17 @@ internal class NativeListRowView(
|
|
|
3155
3188
|
val isIcon = kind == "icon"
|
|
3156
3189
|
val cornerIconData = visual.optJSONObject("cornerIcon")
|
|
3157
3190
|
val fallback = visual.optString("fallbackText").take(2)
|
|
3158
|
-
|
|
3191
|
+
val fallbackIconData = visual.optJSONObject("fallbackIcon")
|
|
3192
|
+
val handlesSourceFallback =
|
|
3193
|
+
!isIcon && sources.isNotEmpty() &&
|
|
3194
|
+
(visual.has("fallbackText") || fallbackIconData != null)
|
|
3195
|
+
leadingFallback.text = if (handlesSourceFallback) "" else fallback
|
|
3159
3196
|
leadingFallback.setTextColor(parseNativeListColor("#00000072"))
|
|
3160
3197
|
val visualBackground = safeColor(
|
|
3161
3198
|
visual.optString("backgroundColor"),
|
|
3162
|
-
|
|
3199
|
+
// OneKey patch: Visual loading and fallback use the active list theme.
|
|
3200
|
+
// parseNativeListColor(if (isIcon) "#0000000F" else "#E0E0E0"),
|
|
3201
|
+
parseNativeListColor(imagePlaceholderColor),
|
|
3163
3202
|
)
|
|
3164
3203
|
if (!isIcon && visual.optString("backgroundColor").isNotEmpty()) {
|
|
3165
3204
|
leadingFrame.background = roundedFill(
|
|
@@ -3168,10 +3207,11 @@ internal class NativeListRowView(
|
|
|
3168
3207
|
)
|
|
3169
3208
|
}
|
|
3170
3209
|
leadingFallback.background = roundedFill(
|
|
3171
|
-
visualBackground,
|
|
3210
|
+
if (handlesSourceFallback) parseNativeListColor(imagePlaceholderColor) else visualBackground,
|
|
3172
3211
|
cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)),
|
|
3173
3212
|
)
|
|
3174
|
-
leadingFallback.visibility =
|
|
3213
|
+
leadingFallback.visibility =
|
|
3214
|
+
if (!isIcon && (sources.isEmpty() || handlesSourceFallback)) VISIBLE else GONE
|
|
3175
3215
|
if (isIcon) {
|
|
3176
3216
|
leadingFrame.background = GradientDrawable().apply {
|
|
3177
3217
|
setColor(visualBackground)
|
|
@@ -3184,6 +3224,14 @@ internal class NativeListRowView(
|
|
|
3184
3224
|
parseNativeListColor("#0000009B"),
|
|
3185
3225
|
)
|
|
3186
3226
|
leadingIcon.visibility = VISIBLE
|
|
3227
|
+
} else if (sources.isEmpty() && fallbackIconData != null) {
|
|
3228
|
+
leadingFallback.visibility = GONE
|
|
3229
|
+
leadingIcon.iconName = fallbackIconData.optString("name")
|
|
3230
|
+
leadingIcon.tintColor = safeColor(
|
|
3231
|
+
fallbackIconData.optString("tintColor"),
|
|
3232
|
+
parseNativeListColor("#0000009B"),
|
|
3233
|
+
)
|
|
3234
|
+
leadingIcon.visibility = VISIBLE
|
|
3187
3235
|
}
|
|
3188
3236
|
val visibleSources = sources.take(leadingImages.size)
|
|
3189
3237
|
val tokenPair = kind == "token" && visibleSources.size > 1
|
|
@@ -3236,7 +3284,10 @@ internal class NativeListRowView(
|
|
|
3236
3284
|
}
|
|
3237
3285
|
visibleSources.forEachIndexed { index, (source, variant) ->
|
|
3238
3286
|
val image = leadingImages[index]
|
|
3239
|
-
|
|
3287
|
+
// OneKey patch: Decorative badges and source-owned fallbacks stay hidden until loaded.
|
|
3288
|
+
// image.visibility = VISIBLE
|
|
3289
|
+
val ownsSourceFallback = index == 0 && handlesSourceFallback
|
|
3290
|
+
image.visibility = if (index > 0 || ownsSourceFallback) INVISIBLE else VISIBLE
|
|
3240
3291
|
image.layoutParams = leadingImageLayout(
|
|
3241
3292
|
index = index,
|
|
3242
3293
|
count = visibleSources.size,
|
|
@@ -3273,21 +3324,32 @@ internal class NativeListRowView(
|
|
|
3273
3324
|
marketLeadingUsesSourceClip = true
|
|
3274
3325
|
image.clipToOutline = false
|
|
3275
3326
|
}
|
|
3276
|
-
val fallbackIcon = if (index == 0)
|
|
3327
|
+
val fallbackIcon = if (index == 0) fallbackIconData else null
|
|
3277
3328
|
val expectedEpoch = bindingEpoch
|
|
3278
3329
|
bindImage(source, image, boundKey ?: "", index, variant,
|
|
3279
|
-
onLoad = if (
|
|
3280
|
-
if (bindingEpoch == expectedEpoch) {
|
|
3330
|
+
onLoad = if (!ownsSourceFallback) null else ({
|
|
3331
|
+
if (bindingEpoch == expectedEpoch) {
|
|
3332
|
+
image.visibility = VISIBLE
|
|
3333
|
+
leadingFallback.visibility = GONE
|
|
3334
|
+
leadingIcon.visibility = GONE
|
|
3335
|
+
}
|
|
3281
3336
|
}),
|
|
3282
|
-
onError = if (
|
|
3337
|
+
onError = if (!ownsSourceFallback) null else ({
|
|
3283
3338
|
if (bindingEpoch == expectedEpoch) {
|
|
3284
3339
|
image.visibility = GONE
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3340
|
+
if (fallbackIcon != null) {
|
|
3341
|
+
leadingFallback.visibility = GONE
|
|
3342
|
+
leadingIcon.iconName = fallbackIcon.optString("name")
|
|
3343
|
+
leadingIcon.tintColor = safeColor(fallbackIcon.optString("tintColor"), parseNativeListColor("#0000009B"))
|
|
3344
|
+
leadingIcon.visibility = VISIBLE
|
|
3345
|
+
} else {
|
|
3346
|
+
leadingIcon.visibility = GONE
|
|
3347
|
+
leadingFallback.text = fallback
|
|
3348
|
+
leadingFallback.visibility = VISIBLE
|
|
3349
|
+
}
|
|
3289
3350
|
}
|
|
3290
3351
|
}),
|
|
3352
|
+
hideUntilLoaded = index > 0 || ownsSourceFallback,
|
|
3291
3353
|
)
|
|
3292
3354
|
}
|
|
3293
3355
|
// OneKey patch: wallet overlays retain source images, provider colors and QR text.
|
|
@@ -3310,7 +3372,7 @@ internal class NativeListRowView(
|
|
|
3310
3372
|
val image = overlay.optJSONObject("image")
|
|
3311
3373
|
val view = when {
|
|
3312
3374
|
image != null -> OneKeyImageReusableView(reactContext).also {
|
|
3313
|
-
bindImage(image, it, boundKey ?: "", 10 + index, "generic")
|
|
3375
|
+
bindImage(image, it, boundKey ?: "", 10 + index, "generic", hideUntilLoaded = true)
|
|
3314
3376
|
selectorImages.add(it)
|
|
3315
3377
|
}
|
|
3316
3378
|
overlay.optString("text").isNotEmpty() -> TextView(context).apply {
|
|
@@ -4057,12 +4119,26 @@ internal class NativeListRowView(
|
|
|
4057
4119
|
variant: String,
|
|
4058
4120
|
onLoad: (() -> Unit)? = null,
|
|
4059
4121
|
onError: (() -> Unit)? = null,
|
|
4122
|
+
hideUntilLoaded: Boolean = false,
|
|
4060
4123
|
retryAttempt: Int = 0,
|
|
4061
4124
|
) {
|
|
4062
4125
|
selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
|
|
4063
4126
|
val expectedEpoch = bindingEpoch
|
|
4064
4127
|
val retryLimit = source.optInt("retryTimes", 0).coerceAtLeast(0)
|
|
4065
4128
|
val uri = source.optString("uri").trim().takeIf(String::isNotEmpty)
|
|
4129
|
+
if (hideUntilLoaded) imageView.visibility = INVISIBLE
|
|
4130
|
+
val handleLoad: (() -> Unit)? = if (hideUntilLoaded) ({
|
|
4131
|
+
if (bindingEpoch == expectedEpoch) {
|
|
4132
|
+
imageView.visibility = VISIBLE
|
|
4133
|
+
onLoad?.invoke()
|
|
4134
|
+
}
|
|
4135
|
+
}) else onLoad
|
|
4136
|
+
val handleError: (() -> Unit)? = if (hideUntilLoaded) ({
|
|
4137
|
+
if (bindingEpoch == expectedEpoch) {
|
|
4138
|
+
imageView.visibility = INVISIBLE
|
|
4139
|
+
onError?.invoke()
|
|
4140
|
+
}
|
|
4141
|
+
}) else onError
|
|
4066
4142
|
imageView.configure(
|
|
4067
4143
|
sourceUri = uri,
|
|
4068
4144
|
sourceHeadersJson = source.optJSONObject("headers")?.toString(),
|
|
@@ -4074,20 +4150,21 @@ internal class NativeListRowView(
|
|
|
4074
4150
|
optimizeTos = retryAttempt == 0 && source.optBoolean("optimizeTos", true),
|
|
4075
4151
|
overscan = source.optDouble("overscan", 1.1),
|
|
4076
4152
|
loadingStrategy = source.optString("loadingStrategy", "static"),
|
|
4077
|
-
|
|
4153
|
+
placeholderColor = imagePlaceholderColor,
|
|
4154
|
+
onLoad = if (retryLimit == 0) handleLoad else ({
|
|
4078
4155
|
if (bindingEpoch == expectedEpoch) {
|
|
4079
4156
|
selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
|
|
4080
|
-
|
|
4157
|
+
handleLoad?.invoke()
|
|
4081
4158
|
}
|
|
4082
4159
|
}),
|
|
4083
|
-
onError = if (retryLimit == 0)
|
|
4160
|
+
onError = if (retryLimit == 0) handleError else ({
|
|
4084
4161
|
if (bindingEpoch == expectedEpoch) {
|
|
4085
|
-
if (retryAttempt >= retryLimit)
|
|
4162
|
+
if (retryAttempt >= retryLimit) handleError?.invoke()
|
|
4086
4163
|
else if (!selectorImageRetries.containsKey(imageView)) {
|
|
4087
4164
|
val retry = Runnable {
|
|
4088
4165
|
if (bindingEpoch == expectedEpoch) {
|
|
4089
4166
|
selectorImageRetries.remove(imageView)
|
|
4090
|
-
bindImage(source, imageView, token, slot, variant, onLoad, onError, retryAttempt + 1)
|
|
4167
|
+
bindImage(source, imageView, token, slot, variant, onLoad, onError, hideUntilLoaded, retryAttempt + 1)
|
|
4091
4168
|
}
|
|
4092
4169
|
}
|
|
4093
4170
|
selectorImageRetries[imageView] = retry
|
package/ios/NativeListCell.swift
CHANGED
|
@@ -1276,6 +1276,8 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1276
1276
|
leadingActionButton.isHidden = true
|
|
1277
1277
|
leadingActionButton.setImage(nil, for: .normal)
|
|
1278
1278
|
leadingActionButton.setImage(nil, for: .disabled)
|
|
1279
|
+
leadingActionButton.accessibilityIdentifier = nil
|
|
1280
|
+
leadingActionButton.accessibilityLabel = nil
|
|
1279
1281
|
unreadDot.isHidden = true
|
|
1280
1282
|
mediaBadgeLabel.isHidden = true
|
|
1281
1283
|
mediaBadgeLabel.text = nil
|
|
@@ -1833,7 +1835,13 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1833
1835
|
let label = UILabel()
|
|
1834
1836
|
label.font = nativeListFont(ofSize: 14)
|
|
1835
1837
|
label.lineBreakMode = .byTruncatingTail
|
|
1836
|
-
|
|
1838
|
+
// Match V1: the balance may shrink, but keep the shortened address intact.
|
|
1839
|
+
label.setContentCompressionResistancePriority(
|
|
1840
|
+
item.data.string("presentation") == "accountSelector" && segment.bool("separatorBefore")
|
|
1841
|
+
? .defaultHigh
|
|
1842
|
+
: .defaultLow,
|
|
1843
|
+
for: .horizontal
|
|
1844
|
+
)
|
|
1837
1845
|
label.textColor = dataTextColor(segment.string("tone", default: "secondary"), theme: theme)
|
|
1838
1846
|
setLineHeight(label, text: segment.string("text"), lineHeight: 20)
|
|
1839
1847
|
let runs = segment.dictionaries("textSegments")
|
|
@@ -2312,6 +2320,28 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
2312
2320
|
rootTopConstraint.constant = verticalPadding
|
|
2313
2321
|
rootBottomConstraint.constant = -verticalPadding
|
|
2314
2322
|
rootStack.spacing = CGFloat(style?.double("leadingGap", default: variant == "perp" ? 8 : 14) ?? (variant == "perp" ? 8 : 14))
|
|
2323
|
+
if let leadingAction = item.data.dictionary("leadingAction") {
|
|
2324
|
+
leadingActionButton.isHidden = false
|
|
2325
|
+
let tintColor = UIColor(
|
|
2326
|
+
nativeListHex: leadingAction.string("tintColor", default: "#646464"),
|
|
2327
|
+
fallback: .darkGray
|
|
2328
|
+
)
|
|
2329
|
+
leadingActionButton.tintColor = tintColor
|
|
2330
|
+
if let image = nativeListIcon(named: leadingAction.string("name")) {
|
|
2331
|
+
leadingActionButton.setImage(image, for: .normal)
|
|
2332
|
+
leadingActionButton.setImage(
|
|
2333
|
+
image.withTintColor(tintColor, renderingMode: .alwaysOriginal),
|
|
2334
|
+
for: .disabled
|
|
2335
|
+
)
|
|
2336
|
+
}
|
|
2337
|
+
leadingActionButton.isEnabled = !leadingAction.bool("disabled")
|
|
2338
|
+
leadingActionButton.alpha = leadingActionButton.isEnabled ? 1 : 0.4
|
|
2339
|
+
leadingActionButton.accessibilityIdentifier = leadingAction["testID"] as? String
|
|
2340
|
+
leadingActionButton.accessibilityLabel = leadingAction["accessibilityLabel"] as? String
|
|
2341
|
+
leadingActionKey = leadingAction.string("actionKey")
|
|
2342
|
+
rootStack.addArrangedSubview(leadingActionButton)
|
|
2343
|
+
rootStack.setCustomSpacing(5, after: leadingActionButton)
|
|
2344
|
+
}
|
|
2315
2345
|
leadingWidth.constant = imageWidth
|
|
2316
2346
|
leadingHeight.constant = imageHeight
|
|
2317
2347
|
if let visual = marketLeading(item, style: style) {
|
|
@@ -2567,13 +2597,15 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
2567
2597
|
subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
2568
2598
|
if let networkImage = item.data.dictionary("networkImage") {
|
|
2569
2599
|
mediaMetadataStack.addArrangedSubview(mediaNetworkImage)
|
|
2570
|
-
|
|
2600
|
+
// OneKey patch: Preserve badge layout without exposing a loading/error tile.
|
|
2601
|
+
// mediaNetworkImage.isHidden = false
|
|
2571
2602
|
bindImage(
|
|
2572
2603
|
networkImage,
|
|
2573
2604
|
into: mediaNetworkImage,
|
|
2574
2605
|
token: item.key,
|
|
2575
2606
|
slot: 2,
|
|
2576
|
-
variant: "network"
|
|
2607
|
+
variant: "network",
|
|
2608
|
+
hideUntilLoaded: true
|
|
2577
2609
|
)
|
|
2578
2610
|
}
|
|
2579
2611
|
mainStack.insertArrangedSubview(mediaMetadataStack, at: 0)
|
|
@@ -3343,14 +3375,23 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3343
3375
|
default: kind == "image" || mediaHeight.isActive ? "rounded" : "circle"
|
|
3344
3376
|
)
|
|
3345
3377
|
let cornerIcon = visual.dictionary("cornerIcon")
|
|
3346
|
-
|
|
3378
|
+
let fallbackText = String(visual.string("fallbackText").prefix(2))
|
|
3379
|
+
let fallbackIconData = visual.dictionary("fallbackIcon")
|
|
3380
|
+
let handlesSourceFallback = !isIcon && !sources.isEmpty &&
|
|
3381
|
+
(visual["fallbackText"] != nil || fallbackIconData != nil)
|
|
3382
|
+
fallbackLabel.text = handlesSourceFallback ? nil : fallbackText
|
|
3383
|
+
let sourceFallbackBackground = currentTheme?["strongBackground"] as? String ?? "#0000000F"
|
|
3347
3384
|
leadingContainer.backgroundColor = UIColor(
|
|
3348
|
-
|
|
3385
|
+
// OneKey patch: Visual loading and fallback use the active list theme.
|
|
3386
|
+
// nativeListHex: visual.string("backgroundColor", default: isIcon ? "#F0F0F0" : "#E0E0E0"),
|
|
3387
|
+
nativeListHex: handlesSourceFallback
|
|
3388
|
+
? sourceFallbackBackground
|
|
3389
|
+
: visual.string("backgroundColor", default: sourceFallbackBackground),
|
|
3349
3390
|
fallback: .gray
|
|
3350
3391
|
)
|
|
3351
3392
|
leadingContainer.layer.cornerRadius = leadingCornerRadius(shape: shape)
|
|
3352
3393
|
leadingContainer.clipsToBounds = true
|
|
3353
|
-
fallbackLabel.isHidden = isIcon || !sources.isEmpty
|
|
3394
|
+
fallbackLabel.isHidden = isIcon || (!sources.isEmpty && !handlesSourceFallback)
|
|
3354
3395
|
if isIcon {
|
|
3355
3396
|
leadingContainer.layer.borderWidth = 1 / UIScreen.main.scale
|
|
3356
3397
|
leadingContainer.layer.borderColor = UIColor(
|
|
@@ -3364,6 +3405,15 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3364
3405
|
)
|
|
3365
3406
|
leadingIconImageView.image = nativeListIcon(named: visual.string("name"))
|
|
3366
3407
|
leadingIconImageView.contentMode = .scaleAspectFit
|
|
3408
|
+
} else if sources.isEmpty, let fallbackIconData {
|
|
3409
|
+
fallbackLabel.isHidden = true
|
|
3410
|
+
leadingIconImageView.isHidden = false
|
|
3411
|
+
leadingIconImageView.tintColor = UIColor(
|
|
3412
|
+
nativeListHex: fallbackIconData.string("tintColor", default: "#646464"),
|
|
3413
|
+
fallback: .darkGray
|
|
3414
|
+
)
|
|
3415
|
+
leadingIconImageView.image = nativeListIcon(named: fallbackIconData.string("name"))
|
|
3416
|
+
leadingIconImageView.contentMode = .scaleAspectFit
|
|
3367
3417
|
}
|
|
3368
3418
|
let visibleSources = Array(sources.prefix(leadingImages.count))
|
|
3369
3419
|
let tokenPair = kind == "token" && visibleSources.count > 1
|
|
@@ -3398,7 +3448,10 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3398
3448
|
}
|
|
3399
3449
|
for (index, source) in visibleSources.enumerated() {
|
|
3400
3450
|
let imageView = leadingImages[index]
|
|
3401
|
-
|
|
3451
|
+
// OneKey patch: Decorative badges and source-owned fallbacks stay hidden until loaded.
|
|
3452
|
+
// imageView.isHidden = false
|
|
3453
|
+
let ownsSourceFallback = index == 0 && handlesSourceFallback
|
|
3454
|
+
imageView.isHidden = index > 0 || ownsSourceFallback
|
|
3402
3455
|
imageView.clipsToBounds = true
|
|
3403
3456
|
leadingSlotConstraints.append(contentsOf: leadingConstraints(
|
|
3404
3457
|
imageView,
|
|
@@ -3412,22 +3465,30 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3412
3465
|
imageView.layer.cornerRadius = 0
|
|
3413
3466
|
imageView.clipsToBounds = false
|
|
3414
3467
|
}
|
|
3415
|
-
let fallbackIcon = index == 0 ?
|
|
3468
|
+
let fallbackIcon = index == 0 ? fallbackIconData : nil
|
|
3416
3469
|
let expectedEpoch = bindingEpoch
|
|
3417
3470
|
bindImage(source.data, into: imageView, token: key, slot: index, variant: source.variant,
|
|
3418
|
-
onLoad:
|
|
3471
|
+
onLoad: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
|
|
3419
3472
|
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
3420
3473
|
imageView?.isHidden = false
|
|
3474
|
+
self.fallbackLabel.isHidden = true
|
|
3421
3475
|
self.leadingIconImageView.isHidden = true
|
|
3422
3476
|
},
|
|
3423
|
-
onError:
|
|
3424
|
-
guard let self, self.bindingEpoch == expectedEpoch
|
|
3477
|
+
onError: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
|
|
3478
|
+
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
3425
3479
|
imageView?.isHidden = true
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3480
|
+
if let fallbackIcon {
|
|
3481
|
+
self.fallbackLabel.isHidden = true
|
|
3482
|
+
self.leadingIconImageView.image = nativeListIcon(named: fallbackIcon.string("name"))
|
|
3483
|
+
self.leadingIconImageView.tintColor = UIColor(nativeListHex: fallbackIcon.string("tintColor", default: "#646464"), fallback: .darkGray)
|
|
3484
|
+
self.leadingIconImageView.isHidden = false
|
|
3485
|
+
} else {
|
|
3486
|
+
self.leadingIconImageView.isHidden = true
|
|
3487
|
+
self.fallbackLabel.text = fallbackText
|
|
3488
|
+
self.fallbackLabel.isHidden = false
|
|
3489
|
+
}
|
|
3490
|
+
},
|
|
3491
|
+
hideUntilLoaded: index > 0 || ownsSourceFallback)
|
|
3431
3492
|
}
|
|
3432
3493
|
// OneKey patch: source-derived wallet decorations may occupy both corners.
|
|
3433
3494
|
let overlays = visual.dictionaries("overlays")
|
|
@@ -3444,7 +3505,15 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3444
3505
|
let width = CGFloat(overlay.double("width", default: isWalletText ? Double(naturalTextWidth) : Double(size)))
|
|
3445
3506
|
let frame = UIView()
|
|
3446
3507
|
frame.translatesAutoresizingMaskIntoConstraints = false
|
|
3447
|
-
|
|
3508
|
+
// OneKey patch: The row theme, not a light-only white, owns badge backing.
|
|
3509
|
+
// frame.backgroundColor = UIColor(nativeListHex: overlay.string("backgroundColor", default: "#FFFFFF"), fallback: .clear)
|
|
3510
|
+
frame.backgroundColor = UIColor(
|
|
3511
|
+
nativeListHex: overlay.string(
|
|
3512
|
+
"backgroundColor",
|
|
3513
|
+
default: currentTheme?["rowBackground"] as? String ?? "#00000000"
|
|
3514
|
+
),
|
|
3515
|
+
fallback: .clear
|
|
3516
|
+
)
|
|
3448
3517
|
frame.layer.cornerRadius = min(width, height) / 2
|
|
3449
3518
|
frame.clipsToBounds = true
|
|
3450
3519
|
leadingContainer.addSubview(frame)
|
|
@@ -3459,7 +3528,14 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3459
3528
|
let content: UIView
|
|
3460
3529
|
if let image = overlay.dictionary("image") {
|
|
3461
3530
|
let imageView = OneKeyImageReusableView(frame: .zero)
|
|
3462
|
-
bindImage(
|
|
3531
|
+
bindImage(
|
|
3532
|
+
image,
|
|
3533
|
+
into: imageView,
|
|
3534
|
+
token: key,
|
|
3535
|
+
slot: 10 + index,
|
|
3536
|
+
variant: "generic",
|
|
3537
|
+
hideUntilLoaded: true
|
|
3538
|
+
)
|
|
3463
3539
|
selectorImages.append(imageView)
|
|
3464
3540
|
content = imageView
|
|
3465
3541
|
} else if !overlay.string("text").isEmpty {
|
|
@@ -3919,6 +3995,7 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3919
3995
|
variant: String,
|
|
3920
3996
|
onLoad: (() -> Void)? = nil,
|
|
3921
3997
|
onError: (() -> Void)? = nil,
|
|
3998
|
+
hideUntilLoaded: Bool = false,
|
|
3922
3999
|
retryAttempt: Int = 0
|
|
3923
4000
|
) {
|
|
3924
4001
|
let imageID = ObjectIdentifier(imageView)
|
|
@@ -3933,6 +4010,17 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3933
4010
|
} else {
|
|
3934
4011
|
headersJson = nil
|
|
3935
4012
|
}
|
|
4013
|
+
if hideUntilLoaded { imageView.isHidden = true }
|
|
4014
|
+
let handleLoad: (() -> Void)? = hideUntilLoaded ? { [weak self, weak imageView] in
|
|
4015
|
+
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
4016
|
+
imageView?.isHidden = false
|
|
4017
|
+
onLoad?()
|
|
4018
|
+
} : onLoad
|
|
4019
|
+
let handleError: (() -> Void)? = hideUntilLoaded ? { [weak self, weak imageView] in
|
|
4020
|
+
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
4021
|
+
imageView?.isHidden = true
|
|
4022
|
+
onError?()
|
|
4023
|
+
} : onError
|
|
3936
4024
|
imageView.configure(
|
|
3937
4025
|
sourceUri: source.string("uri").trimmingCharacters(in: .whitespacesAndNewlines),
|
|
3938
4026
|
sourceHeadersJson: headersJson,
|
|
@@ -3944,19 +4032,20 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3944
4032
|
optimizeTos: retryAttempt == 0 && (source["optimizeTos"] == nil || source.bool("optimizeTos")),
|
|
3945
4033
|
overscan: source["overscan"] == nil ? 1.1 : source.double("overscan"),
|
|
3946
4034
|
loadingStrategy: source.string("loadingStrategy", default: "static"),
|
|
3947
|
-
|
|
4035
|
+
placeholderColor: currentTheme?["strongBackground"] as? String ?? "#0000000F",
|
|
4036
|
+
onLoad: retryLimit == 0 ? handleLoad : { [weak self] in
|
|
3948
4037
|
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
3949
4038
|
self.selectorImageRetries.removeValue(forKey: imageID)?.cancel()
|
|
3950
|
-
|
|
4039
|
+
handleLoad?()
|
|
3951
4040
|
},
|
|
3952
|
-
onError: retryLimit == 0 ?
|
|
4041
|
+
onError: retryLimit == 0 ? handleError : { [weak self, weak imageView] in
|
|
3953
4042
|
guard let self, self.bindingEpoch == expectedEpoch, let imageView else { return }
|
|
3954
|
-
guard retryAttempt < retryLimit else {
|
|
4043
|
+
guard retryAttempt < retryLimit else { handleError?(); return }
|
|
3955
4044
|
guard self.selectorImageRetries[imageID] == nil else { return }
|
|
3956
4045
|
let retry = DispatchWorkItem { [weak self, weak imageView] in
|
|
3957
4046
|
guard let self, self.bindingEpoch == expectedEpoch, let imageView else { return }
|
|
3958
4047
|
self.selectorImageRetries.removeValue(forKey: imageID)
|
|
3959
|
-
self.bindImage(source, into: imageView, token: token, slot: slot, variant: variant, onLoad: onLoad, onError: onError, retryAttempt: retryAttempt + 1)
|
|
4048
|
+
self.bindImage(source, into: imageView, token: token, slot: slot, variant: variant, onLoad: onLoad, onError: onError, hideUntilLoaded: hideUntilLoaded, retryAttempt: retryAttempt + 1)
|
|
3960
4049
|
}
|
|
3961
4050
|
self.selectorImageRetries[imageID] = retry
|
|
3962
4051
|
DispatchQueue.main.asyncAfter(deadline: .now() + Double(Int.random(in: 0...2)), execute: retry)
|
|
@@ -248,12 +248,11 @@ final class NativeListView: UIView {
|
|
|
248
248
|
guard let next = try? NativeListConfig.parse(json: json) else { return }
|
|
249
249
|
invalidateActionAnchor(reason: "snapshot")
|
|
250
250
|
if let current = config, isControlledSelectionSnapshotUpdate(from: current, to: next) {
|
|
251
|
-
let changedSummaryKeys = Set(zip(current.items, next.items).compactMap { old, new in
|
|
252
|
-
old.content != new.content ? new.key : nil
|
|
253
|
-
})
|
|
254
251
|
config = next
|
|
255
252
|
itemsByKey = Dictionary(uniqueKeysWithValues: next.items.map { ($0.key, $0) })
|
|
256
|
-
|
|
253
|
+
// OneKey patch: A controlled selection echo is fully handled by the
|
|
254
|
+
// lightweight updater. Rebinding unchanged visuals clears cached images.
|
|
255
|
+
refreshVisibleSelection()
|
|
257
256
|
return
|
|
258
257
|
}
|
|
259
258
|
let oldItems = itemsByKey
|
package/lib/module/NativeList.js
CHANGED
|
@@ -10,6 +10,17 @@ import { applyRowPatches, serializePatches, serializeSnapshot } from "./validati
|
|
|
10
10
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
11
11
|
const NativeListConfig = require('../nitrogen/generated/shared/json/NativeListConfig.json');
|
|
12
12
|
const NativeListHost = getHostComponent('NativeList', () => NativeListConfig);
|
|
13
|
+
export function preloadNativeListAvatarImages(sources) {
|
|
14
|
+
return OneKeyImageCache.preload(sources.map(source => ({
|
|
15
|
+
uri: source.uri,
|
|
16
|
+
headers: source.headers,
|
|
17
|
+
resizeWidth: source.width,
|
|
18
|
+
resizeHeight: source.height,
|
|
19
|
+
optimizeTos: source.optimizeTos !== false,
|
|
20
|
+
overscan: source.overscan,
|
|
21
|
+
cachePolicy: source.cachePolicy === 'memory' ? OneKeyImageCachePolicy.MEMORY : source.cachePolicy === 'disk' ? OneKeyImageCachePolicy.DISK : OneKeyImageCachePolicy.MEMORY_DISK
|
|
22
|
+
})));
|
|
23
|
+
}
|
|
13
24
|
function parsePayload(payloadJson) {
|
|
14
25
|
return JSON.parse(payloadJson);
|
|
15
26
|
}
|
|
@@ -78,14 +89,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
|
78
89
|
updateAvatarPrefetchRef.current = updateAvatarPrefetch;
|
|
79
90
|
useEffect(() => {
|
|
80
91
|
avatarLifecycle.current = 'mounted';
|
|
81
|
-
const queue = new NativeAvatarPrefetchQueue(source =>
|
|
82
|
-
uri: source.uri,
|
|
83
|
-
headers: source.headers,
|
|
84
|
-
resizeWidth: source.width,
|
|
85
|
-
resizeHeight: source.height,
|
|
86
|
-
optimizeTos: false,
|
|
87
|
-
cachePolicy: source.cachePolicy === 'memory' ? OneKeyImageCachePolicy.MEMORY : source.cachePolicy === 'disk' ? OneKeyImageCachePolicy.DISK : OneKeyImageCachePolicy.MEMORY_DISK
|
|
88
|
-
}]));
|
|
92
|
+
const queue = new NativeAvatarPrefetchQueue(source => preloadNativeListAvatarImages([source]));
|
|
89
93
|
avatarQueueRef.current = queue;
|
|
90
94
|
updateAvatarPrefetchRef.current();
|
|
91
95
|
return () => {
|
|
@@ -5,7 +5,21 @@ import { View } from 'react-native';
|
|
|
5
5
|
import { normalizeIndexScroll, normalizeKeyScroll, normalizePositionScroll, resolveLocationIndex, scrollFailure, validateOffset } from "./scrolling.js";
|
|
6
6
|
import { serializePatches, validateSnapshot } from "./validation.js";
|
|
7
7
|
import { NativeListWebEngine } from "./web/NativeListWebEngine.js";
|
|
8
|
+
import { acquireNativeListAvatar, canonicalNativeListAvatarUri } from "./web/NativeListWebAvatarCache.js";
|
|
8
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
export function preloadNativeListAvatarImages(sources) {
|
|
11
|
+
if (typeof document === 'undefined') return Promise.resolve(false);
|
|
12
|
+
const uris = [...new Set(sources.map(source => canonicalNativeListAvatarUri(source.uri)).filter(uri => uri !== undefined))];
|
|
13
|
+
if (!uris.length) return Promise.resolve(false);
|
|
14
|
+
return Promise.all(uris.map(uri => new Promise(resolve => {
|
|
15
|
+
let release;
|
|
16
|
+
const settle = success => {
|
|
17
|
+
resolve(success);
|
|
18
|
+
queueMicrotask(() => release?.());
|
|
19
|
+
};
|
|
20
|
+
release = acquireNativeListAvatar(document, uri, () => settle(true), () => settle(false), 0);
|
|
21
|
+
}))).then(results => results.every(Boolean));
|
|
22
|
+
}
|
|
9
23
|
export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
|
|
10
24
|
snapshot,
|
|
11
25
|
webVirtualizationEnabled = true,
|
package/lib/module/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
export { NativeList } from './NativeList';
|
|
3
|
+
export { NativeList, preloadNativeListAvatarImages } from './NativeList';
|
|
4
4
|
export { applyRowPatches, serializePatches, serializeSnapshot, validatePatches, validateSnapshot } from "./validation.js";
|
|
5
5
|
export { checkboxStateForKeys, checkboxStateForSection, isSelectableRow, reduceSelection, selectionStateFromSnapshot } from "./selection.js";
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
package/lib/module/validation.js
CHANGED
|
@@ -124,6 +124,13 @@ function assertMarketRow(row, path) {
|
|
|
124
124
|
}
|
|
125
125
|
assertText(row.price, `${path}.price`);
|
|
126
126
|
assertText(row.change.text, `${path}.change.text`);
|
|
127
|
+
assertTrailingAccessories(row.leadingAction ? [row.leadingAction] : undefined, `${path}.leadingAction`);
|
|
128
|
+
if (row.leadingAction) {
|
|
129
|
+
assertText(row.leadingAction.name, `${path}.leadingAction.name`);
|
|
130
|
+
if (row.leadingAction.actionKey !== undefined) {
|
|
131
|
+
assertKey(row.leadingAction.actionKey, `${path}.leadingAction.actionKey`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
127
134
|
assertLeadingVisual(row.leading, `${path}.leading`);
|
|
128
135
|
const assertSegments = (segments, segmentPath) => {
|
|
129
136
|
segments?.forEach((segment, index) => {
|
|
@@ -628,11 +628,32 @@ function configureWebAvatar(image, source, uri) {
|
|
|
628
628
|
});
|
|
629
629
|
webAvatarCleanup.set(image, dispose);
|
|
630
630
|
}
|
|
631
|
+
const WEB_IMAGE_FADE_DELAY_MS = 100;
|
|
632
|
+
const WEB_IMAGE_FADE_DURATION_MS = 140;
|
|
633
|
+
function webImageNow(image) {
|
|
634
|
+
return image.ownerDocument.defaultView?.performance.now() ?? Date.now();
|
|
635
|
+
}
|
|
636
|
+
function revealWebImage(element, startedAt) {
|
|
637
|
+
element.getAnimations?.().forEach(animation => animation.cancel());
|
|
638
|
+
element.style.opacity = '1';
|
|
639
|
+
const view = element.ownerDocument.defaultView;
|
|
640
|
+
if (webImageNow(element) - startedAt < WEB_IMAGE_FADE_DELAY_MS || view?.matchMedia?.('(prefers-reduced-motion: reduce)').matches || typeof element.animate !== 'function') return;
|
|
641
|
+
element.animate([{
|
|
642
|
+
opacity: 0
|
|
643
|
+
}, {
|
|
644
|
+
opacity: 1
|
|
645
|
+
}], {
|
|
646
|
+
duration: WEB_IMAGE_FADE_DURATION_MS,
|
|
647
|
+
easing: 'ease-out'
|
|
648
|
+
});
|
|
649
|
+
}
|
|
631
650
|
function createImage(context, source, className) {
|
|
632
651
|
const avatarUri = canonicalNativeListAvatarUri(source.uri);
|
|
633
652
|
const uri = avatarUri ?? safeImageUri(source.uri);
|
|
634
653
|
if (!uri) return undefined;
|
|
635
654
|
const image = context.document.createElement('img');
|
|
655
|
+
const startedAt = webImageNow(image);
|
|
656
|
+
image.style.opacity = '0';
|
|
636
657
|
if (className) image.className = className;
|
|
637
658
|
// OneKey patch: consume recoverable errors before the visual's final fallback listener.
|
|
638
659
|
if (avatarUri) {
|
|
@@ -646,6 +667,13 @@ function createImage(context, source, className) {
|
|
|
646
667
|
image.loading = 'lazy';
|
|
647
668
|
image.decoding = 'async';
|
|
648
669
|
image.style.objectFit = source.contentFit === 'fill' ? 'fill' : source.contentFit ?? 'cover';
|
|
670
|
+
image.addEventListener('load', () => {
|
|
671
|
+
if (image.dataset.nativeListSelectorPaint !== 'true') revealWebImage(image, startedAt);
|
|
672
|
+
});
|
|
673
|
+
image.addEventListener('error', () => {
|
|
674
|
+
image.style.opacity = '0';
|
|
675
|
+
});
|
|
676
|
+
image.dataset.nativeListImageStartedAt = String(startedAt);
|
|
649
677
|
return image;
|
|
650
678
|
}
|
|
651
679
|
|
|
@@ -655,14 +683,18 @@ function paintSelectorImageBackground(image, frame, inset = 0) {
|
|
|
655
683
|
const paint = createElement(image.ownerDocument, 'span', 'ok-native-list-selector-image-background');
|
|
656
684
|
paint.style.cssText = 'position:absolute;pointer-events:none;border-radius:inherit;background-position:center;background-repeat:no-repeat';
|
|
657
685
|
paint.style.inset = String(inset) + 'px';
|
|
686
|
+
paint.style.opacity = '0';
|
|
658
687
|
paint.style.backgroundSize = image.style.objectFit === 'fill' ? '100% 100%' : image.style.objectFit === 'center' ? 'auto' : image.style.objectFit;
|
|
688
|
+
image.dataset.nativeListSelectorPaint = 'true';
|
|
659
689
|
image.style.opacity = '0';
|
|
660
690
|
const update = () => {
|
|
661
691
|
paint.style.backgroundImage = 'url(' + JSON.stringify(image.currentSrc || image.src) + ')';
|
|
692
|
+
revealWebImage(paint, Number(image.dataset.nativeListImageStartedAt) || webImageNow(image));
|
|
662
693
|
};
|
|
663
694
|
image.addEventListener('load', update);
|
|
664
695
|
image.addEventListener('error', () => {
|
|
665
696
|
paint.style.backgroundImage = 'none';
|
|
697
|
+
paint.style.opacity = '0';
|
|
666
698
|
});
|
|
667
699
|
frame.insertBefore(paint, image);
|
|
668
700
|
if (image.complete && image.naturalWidth > 0) update();
|
|
@@ -949,8 +981,15 @@ function createVisual(context, visual, selectorPresentation) {
|
|
|
949
981
|
frame.appendChild(fallback);
|
|
950
982
|
return frame;
|
|
951
983
|
}
|
|
952
|
-
|
|
984
|
+
|
|
985
|
+
// OneKey patch: Every image visual starts from the current theme backing.
|
|
986
|
+
// if ('backgroundColor' in visual && visual.backgroundColor)
|
|
987
|
+
// frame.style.background = visual.backgroundColor;
|
|
988
|
+
frame.style.background = 'backgroundColor' in visual && visual.backgroundColor ? visual.backgroundColor : 'var(--nl-strong)';
|
|
953
989
|
const source = visual.kind === 'image' ? visual.image : visual.image;
|
|
990
|
+
const fallbackIcon = 'fallbackIcon' in visual ? visual.fallbackIcon : undefined;
|
|
991
|
+
const handlesSourceFallback = !!source && (fallbackIcon !== undefined || 'fallbackText' in visual);
|
|
992
|
+
if (handlesSourceFallback) frame.style.background = 'var(--nl-strong)';
|
|
954
993
|
const image = source ? createImage(context, source) : undefined;
|
|
955
994
|
if (image) {
|
|
956
995
|
image.className = 'ok-native-list-visual-main';
|
|
@@ -970,17 +1009,19 @@ function createVisual(context, visual, selectorPresentation) {
|
|
|
970
1009
|
frame.appendChild(corner);
|
|
971
1010
|
}
|
|
972
1011
|
// OneKey patch: image failure uses the same source-derived fallback as v1.
|
|
973
|
-
|
|
974
|
-
|
|
1012
|
+
const hasFallbackText = 'fallbackText' in visual;
|
|
1013
|
+
if (fallbackIcon || hasFallbackText) {
|
|
975
1014
|
const showFallback = () => {
|
|
976
1015
|
if (image) {
|
|
977
1016
|
disposeWebImageRetries(image);
|
|
978
1017
|
image.remove();
|
|
979
1018
|
}
|
|
980
1019
|
frame.querySelector('.ok-native-list-visual-fallback:not(.ok-native-list-visual-corner)')?.remove();
|
|
981
|
-
const fallback = createElement(context.document, 'span', 'ok-native-list-visual-fallback');
|
|
982
|
-
|
|
983
|
-
|
|
1020
|
+
const fallback = createElement(context.document, 'span', 'ok-native-list-visual-fallback', fallbackIcon ? undefined : 'fallbackText' in visual ? visual.fallbackText ?? '' : '');
|
|
1021
|
+
if (fallbackIcon) {
|
|
1022
|
+
applySelectorIcon(fallback, fallbackIcon.name);
|
|
1023
|
+
if (fallbackIcon.tintColor) fallback.style.color = fallbackIcon.tintColor;
|
|
1024
|
+
}
|
|
984
1025
|
frame.prepend(fallback);
|
|
985
1026
|
};
|
|
986
1027
|
if (image) image.addEventListener('error', showFallback, {
|
|
@@ -1747,6 +1788,17 @@ function createMarketRow(context, row) {
|
|
|
1747
1788
|
const layoutStyle = resolveWebMarketLayoutStyle(row);
|
|
1748
1789
|
body.style.padding = String(layoutStyle.verticalPadding) + 'px ' + String(layoutStyle.horizontalPadding) + 'px';
|
|
1749
1790
|
body.style.gap = '0px';
|
|
1791
|
+
if (row.leadingAction) {
|
|
1792
|
+
const action = createIconAction(context, row.leadingAction.name, row.leadingAction.actionKey, row.leadingAction.disabled, row.leadingAction.tintColor);
|
|
1793
|
+
action.style.flex = '0 0 36px';
|
|
1794
|
+
action.style.width = '36px';
|
|
1795
|
+
action.style.height = '36px';
|
|
1796
|
+
action.style.marginRight = '5px';
|
|
1797
|
+
setData(action, 'testid', row.leadingAction.testID);
|
|
1798
|
+
if (row.leadingAction.accessibilityLabel) action.setAttribute('aria-label', row.leadingAction.accessibilityLabel);
|
|
1799
|
+
markActionAnchorSource(action, 'leadingAction');
|
|
1800
|
+
body.appendChild(action);
|
|
1801
|
+
}
|
|
1750
1802
|
const visual = createVisual(context, row.leading);
|
|
1751
1803
|
if (visual) {
|
|
1752
1804
|
const width = layoutStyle.imageWidth;
|
|
@@ -1822,16 +1874,45 @@ function createMarketRow(context, row) {
|
|
|
1822
1874
|
titleLine.appendChild(element);
|
|
1823
1875
|
});
|
|
1824
1876
|
main.appendChild(titleLine);
|
|
1825
|
-
if (row.subtitle || row.subtitleSegments?.length) {
|
|
1826
|
-
const
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1877
|
+
if (row.subtitlePrefix || row.subtitle || row.subtitleSegments?.length) {
|
|
1878
|
+
const subtitleLine = createElement(context.document, 'span', 'ok-native-list-market-subtitle-line');
|
|
1879
|
+
subtitleLine.style.display = 'flex';
|
|
1880
|
+
subtitleLine.style.alignItems = 'center';
|
|
1881
|
+
subtitleLine.style.minWidth = '0';
|
|
1882
|
+
subtitleLine.style.overflow = 'hidden';
|
|
1883
|
+
subtitleLine.style.gap = String(row.subtitlePrefix ? row.subtitlePrefix.gap ?? 4 : 0) + 'px';
|
|
1884
|
+
if (row.subtitlePrefix) {
|
|
1885
|
+
const prefix = createElement(context.document, 'span', 'ok-native-list-market-subtitle-prefix', row.subtitlePrefix.text);
|
|
1886
|
+
applyMarketTextStyle(prefix, row.subtitlePrefix.style, {
|
|
1887
|
+
fontSize: 12,
|
|
1888
|
+
lineHeight: 16,
|
|
1889
|
+
weight: 400,
|
|
1890
|
+
alignment: 'start'
|
|
1891
|
+
});
|
|
1892
|
+
prefix.style.display = 'block';
|
|
1893
|
+
prefix.style.flex = '1 1 auto';
|
|
1894
|
+
prefix.style.minWidth = '0';
|
|
1895
|
+
prefix.style.overflow = 'hidden';
|
|
1896
|
+
prefix.style.textOverflow = 'ellipsis';
|
|
1897
|
+
prefix.style.color = row.subtitlePrefix.style?.color ?? 'var(--nl-secondary)';
|
|
1898
|
+
if (row.subtitlePrefix.maxWidth !== undefined) {
|
|
1899
|
+
prefix.style.maxWidth = String(row.subtitlePrefix.maxWidth) + 'px';
|
|
1900
|
+
}
|
|
1901
|
+
subtitleLine.appendChild(prefix);
|
|
1902
|
+
}
|
|
1903
|
+
if (row.subtitle || row.subtitleSegments?.length) {
|
|
1904
|
+
const subtitle = createElement(context.document, 'span', 'ok-native-list-market-subtitle', row.subtitle);
|
|
1905
|
+
applyMarketTextStyle(subtitle, style?.subtitle, {
|
|
1906
|
+
fontSize: 14,
|
|
1907
|
+
lineHeight: 20,
|
|
1908
|
+
weight: 400,
|
|
1909
|
+
alignment: 'start'
|
|
1910
|
+
});
|
|
1911
|
+
applyValueSegments(subtitle, row.subtitleSegments, style?.subtitle?.fontSize ?? 14, style?.subtitle?.lineHeight ?? 20, marketFontWeight(style?.subtitle?.fontWeight, 400));
|
|
1912
|
+
subtitle.style.flex = row.subtitlePrefix ? '0 0 auto' : '1 1 auto';
|
|
1913
|
+
subtitleLine.appendChild(subtitle);
|
|
1914
|
+
}
|
|
1915
|
+
main.appendChild(subtitleLine);
|
|
1835
1916
|
}
|
|
1836
1917
|
body.appendChild(main);
|
|
1837
1918
|
const trailing = createElement(context.document, 'span', 'ok-native-list-market-trailing');
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { ImageSource } from './models';
|
|
2
3
|
import type { NativeListProps } from './NativeList.types';
|
|
3
4
|
export type { NativeListProps, NativeListRef } from './NativeList.types';
|
|
4
5
|
export type { ActionAnchorState, ScrollAlignment, ScrollPositionOptions, ScrollToEndParams, ScrollToIndexFailedInfo, ScrollToIndexParams, ScrollToItemParams, ScrollToKeyParams, ScrollToLocationParams, ScrollToOffsetParams, } from './NativeList.types';
|
|
6
|
+
export declare function preloadNativeListAvatarImages(sources: readonly ImageSource[]): Promise<boolean>;
|
|
5
7
|
export declare const NativeList: React.ForwardRefExoticComponent<NativeListProps & React.RefAttributes<Readonly<{
|
|
6
8
|
applySnapshot(snapshot: import("./models").NativeListSnapshot): void;
|
|
7
9
|
applyPatches(patches: readonly import("./models").RowPatch[]): void;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { NativeListProps } from './NativeList.types';
|
|
3
|
-
import type { NativeListSnapshot, RowPatch } from './models';
|
|
3
|
+
import type { ImageSource, NativeListSnapshot, RowPatch } from './models';
|
|
4
4
|
export type { NativeListProps, NativeListRef } from './NativeList.types';
|
|
5
5
|
export type { ActionAnchorState, ScrollAlignment, ScrollPositionOptions, ScrollToEndParams, ScrollToIndexFailedInfo, ScrollToIndexParams, ScrollToItemParams, ScrollToKeyParams, ScrollToLocationParams, ScrollToOffsetParams, } from './NativeList.types';
|
|
6
|
+
export declare function preloadNativeListAvatarImages(sources: readonly ImageSource[]): Promise<boolean>;
|
|
6
7
|
export declare const NativeList: React.ForwardRefExoticComponent<NativeListProps & React.RefAttributes<Readonly<{
|
|
7
8
|
applySnapshot(snapshot: NativeListSnapshot): void;
|
|
8
9
|
applyPatches(patches: readonly RowPatch[]): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { NativeList } from './NativeList';
|
|
1
|
+
export { NativeList, preloadNativeListAvatarImages } from './NativeList';
|
|
2
2
|
export type { ActionAnchorState, NativeListProps, NativeListRef, ScrollAlignment, ScrollPositionOptions, ScrollToEndParams, ScrollToIndexFailedInfo, ScrollToIndexParams, ScrollToItemParams, ScrollToKeyParams, ScrollToLocationParams, ScrollToOffsetParams, } from './NativeList';
|
|
3
3
|
export type * from './models';
|
|
4
4
|
export { applyRowPatches, serializePatches, serializeSnapshot, validatePatches, validateSnapshot, } from './validation';
|
|
@@ -329,6 +329,9 @@ export type DataRow = RowBase & Readonly<{
|
|
|
329
329
|
export type MarketRow = RowBase & Readonly<{
|
|
330
330
|
type: 'market';
|
|
331
331
|
variant: 'token' | 'stock' | 'perp';
|
|
332
|
+
leadingAction?: Extract<TrailingAccessory, {
|
|
333
|
+
kind: 'icon';
|
|
334
|
+
}>;
|
|
332
335
|
leading: LeadingVisual;
|
|
333
336
|
title: string;
|
|
334
337
|
subtitle?: string;
|
|
@@ -545,7 +548,7 @@ export type RowPatch = Readonly<{
|
|
|
545
548
|
}> | Readonly<{
|
|
546
549
|
type: 'market';
|
|
547
550
|
key: string;
|
|
548
|
-
changes: Partial<Pick<MarketRow, CommonPatchFields | 'leading' | 'title' | 'subtitle' | 'subtitleSegments' | 'price' | 'priceSegments' | 'change' | 'badges' | 'pressActionKey' | 'pressInActionKey' | 'longPressActionKey' | 'diagnostics' | 'style'>>;
|
|
551
|
+
changes: Partial<Pick<MarketRow, CommonPatchFields | 'leadingAction' | 'leading' | 'title' | 'subtitle' | 'subtitleSegments' | 'price' | 'priceSegments' | 'change' | 'badges' | 'pressActionKey' | 'pressInActionKey' | 'longPressActionKey' | 'diagnostics' | 'style'>>;
|
|
549
552
|
}> | Readonly<{
|
|
550
553
|
type: 'mediaTile';
|
|
551
554
|
key: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-native-list",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.118",
|
|
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.
|
|
87
|
-
"@onekeyfe/react-native-native-logger": "3.0.
|
|
86
|
+
"@onekeyfe/react-native-image": "3.0.118",
|
|
87
|
+
"@onekeyfe/react-native-native-logger": "3.0.118",
|
|
88
88
|
"react": "*",
|
|
89
89
|
"react-native": "*",
|
|
90
90
|
"react-native-nitro-modules": "0.37.0"
|
package/src/NativeList.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from './NativeList.nitro';
|
|
21
21
|
import type {
|
|
22
22
|
ActionAnchorInvalidatedEvent,
|
|
23
|
+
ImageSource,
|
|
23
24
|
RowActionEvent,
|
|
24
25
|
SelectionDeltaEvent,
|
|
25
26
|
ReorderEvent,
|
|
@@ -64,6 +65,27 @@ const NativeListHost = getHostComponent<
|
|
|
64
65
|
NativeListMethods
|
|
65
66
|
>('NativeList', () => NativeListConfig);
|
|
66
67
|
|
|
68
|
+
export function preloadNativeListAvatarImages(
|
|
69
|
+
sources: readonly ImageSource[]
|
|
70
|
+
): Promise<boolean> {
|
|
71
|
+
return OneKeyImageCache.preload(
|
|
72
|
+
sources.map((source) => ({
|
|
73
|
+
uri: source.uri,
|
|
74
|
+
headers: source.headers,
|
|
75
|
+
resizeWidth: source.width,
|
|
76
|
+
resizeHeight: source.height,
|
|
77
|
+
optimizeTos: source.optimizeTos !== false,
|
|
78
|
+
overscan: source.overscan,
|
|
79
|
+
cachePolicy:
|
|
80
|
+
source.cachePolicy === 'memory'
|
|
81
|
+
? OneKeyImageCachePolicy.MEMORY
|
|
82
|
+
: source.cachePolicy === 'disk'
|
|
83
|
+
? OneKeyImageCachePolicy.DISK
|
|
84
|
+
: OneKeyImageCachePolicy.MEMORY_DISK,
|
|
85
|
+
}))
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
function parsePayload<T>(payloadJson: string): T {
|
|
68
90
|
return JSON.parse(payloadJson) as T;
|
|
69
91
|
}
|
|
@@ -151,21 +173,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
151
173
|
useEffect(() => {
|
|
152
174
|
avatarLifecycle.current = 'mounted';
|
|
153
175
|
const queue = new NativeAvatarPrefetchQueue((source) =>
|
|
154
|
-
|
|
155
|
-
{
|
|
156
|
-
uri: source.uri,
|
|
157
|
-
headers: source.headers,
|
|
158
|
-
resizeWidth: source.width,
|
|
159
|
-
resizeHeight: source.height,
|
|
160
|
-
optimizeTos: false,
|
|
161
|
-
cachePolicy:
|
|
162
|
-
source.cachePolicy === 'memory'
|
|
163
|
-
? OneKeyImageCachePolicy.MEMORY
|
|
164
|
-
: source.cachePolicy === 'disk'
|
|
165
|
-
? OneKeyImageCachePolicy.DISK
|
|
166
|
-
: OneKeyImageCachePolicy.MEMORY_DISK,
|
|
167
|
-
},
|
|
168
|
-
])
|
|
176
|
+
preloadNativeListAvatarImages([source])
|
|
169
177
|
);
|
|
170
178
|
avatarQueueRef.current = queue;
|
|
171
179
|
updateAvatarPrefetchRef.current();
|
package/src/NativeList.web.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import React, {
|
|
|
9
9
|
} from 'react';
|
|
10
10
|
import { View } from 'react-native';
|
|
11
11
|
import type { NativeListProps, NativeListRef } from './NativeList.types';
|
|
12
|
-
import type { NativeListSnapshot, RowPatch } from './models';
|
|
12
|
+
import type { ImageSource, NativeListSnapshot, RowPatch } from './models';
|
|
13
13
|
import {
|
|
14
14
|
normalizeIndexScroll,
|
|
15
15
|
normalizeKeyScroll,
|
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
NativeListWebEngine,
|
|
25
25
|
type NativeListWebCallbacks,
|
|
26
26
|
} from './web/NativeListWebEngine';
|
|
27
|
+
import {
|
|
28
|
+
acquireNativeListAvatar,
|
|
29
|
+
canonicalNativeListAvatarUri,
|
|
30
|
+
} from './web/NativeListWebAvatarCache';
|
|
27
31
|
|
|
28
32
|
export type { NativeListProps, NativeListRef } from './NativeList.types';
|
|
29
33
|
export type {
|
|
@@ -39,6 +43,39 @@ export type {
|
|
|
39
43
|
ScrollToOffsetParams,
|
|
40
44
|
} from './NativeList.types';
|
|
41
45
|
|
|
46
|
+
export function preloadNativeListAvatarImages(
|
|
47
|
+
sources: readonly ImageSource[]
|
|
48
|
+
): Promise<boolean> {
|
|
49
|
+
if (typeof document === 'undefined') return Promise.resolve(false);
|
|
50
|
+
const uris = [
|
|
51
|
+
...new Set(
|
|
52
|
+
sources
|
|
53
|
+
.map((source) => canonicalNativeListAvatarUri(source.uri))
|
|
54
|
+
.filter((uri): uri is string => uri !== undefined)
|
|
55
|
+
),
|
|
56
|
+
];
|
|
57
|
+
if (!uris.length) return Promise.resolve(false);
|
|
58
|
+
return Promise.all(
|
|
59
|
+
uris.map(
|
|
60
|
+
(uri) =>
|
|
61
|
+
new Promise<boolean>((resolve) => {
|
|
62
|
+
let release: (() => void) | undefined;
|
|
63
|
+
const settle = (success: boolean) => {
|
|
64
|
+
resolve(success);
|
|
65
|
+
queueMicrotask(() => release?.());
|
|
66
|
+
};
|
|
67
|
+
release = acquireNativeListAvatar(
|
|
68
|
+
document,
|
|
69
|
+
uri,
|
|
70
|
+
() => settle(true),
|
|
71
|
+
() => settle(false),
|
|
72
|
+
0
|
|
73
|
+
);
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
).then((results) => results.every(Boolean));
|
|
77
|
+
}
|
|
78
|
+
|
|
42
79
|
export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
43
80
|
function NativeList(
|
|
44
81
|
{
|
package/src/index.ts
CHANGED
package/src/models.ts
CHANGED
|
@@ -346,6 +346,7 @@ export type MarketRow = RowBase &
|
|
|
346
346
|
Readonly<{
|
|
347
347
|
type: 'market';
|
|
348
348
|
variant: 'token' | 'stock' | 'perp';
|
|
349
|
+
leadingAction?: Extract<TrailingAccessory, { kind: 'icon' }>;
|
|
349
350
|
leading: LeadingVisual;
|
|
350
351
|
title: string;
|
|
351
352
|
subtitle?: string;
|
|
@@ -667,6 +668,7 @@ export type RowPatch =
|
|
|
667
668
|
Pick<
|
|
668
669
|
MarketRow,
|
|
669
670
|
| CommonPatchFields
|
|
671
|
+
| 'leadingAction'
|
|
670
672
|
| 'leading'
|
|
671
673
|
| 'title'
|
|
672
674
|
| 'subtitle'
|
package/src/validation.ts
CHANGED
|
@@ -242,6 +242,16 @@ function assertMarketRow(row: MarketRow, path: string): void {
|
|
|
242
242
|
}
|
|
243
243
|
assertText(row.price, `${path}.price`);
|
|
244
244
|
assertText(row.change.text, `${path}.change.text`);
|
|
245
|
+
assertTrailingAccessories(
|
|
246
|
+
row.leadingAction ? [row.leadingAction] : undefined,
|
|
247
|
+
`${path}.leadingAction`
|
|
248
|
+
);
|
|
249
|
+
if (row.leadingAction) {
|
|
250
|
+
assertText(row.leadingAction.name, `${path}.leadingAction.name`);
|
|
251
|
+
if (row.leadingAction.actionKey !== undefined) {
|
|
252
|
+
assertKey(row.leadingAction.actionKey, `${path}.leadingAction.actionKey`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
245
255
|
assertLeadingVisual(row.leading, `${path}.leading`);
|
|
246
256
|
const assertSegments = (
|
|
247
257
|
segments: MarketRow['priceSegments'],
|
|
@@ -1087,6 +1087,30 @@ function configureWebAvatar(
|
|
|
1087
1087
|
webAvatarCleanup.set(image, dispose);
|
|
1088
1088
|
}
|
|
1089
1089
|
|
|
1090
|
+
const WEB_IMAGE_FADE_DELAY_MS = 100;
|
|
1091
|
+
const WEB_IMAGE_FADE_DURATION_MS = 140;
|
|
1092
|
+
|
|
1093
|
+
function webImageNow(image: HTMLImageElement): number {
|
|
1094
|
+
return image.ownerDocument.defaultView?.performance.now() ?? Date.now();
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function revealWebImage(element: HTMLElement, startedAt: number) {
|
|
1098
|
+
element.getAnimations?.().forEach((animation) => animation.cancel());
|
|
1099
|
+
element.style.opacity = '1';
|
|
1100
|
+
const view = element.ownerDocument.defaultView;
|
|
1101
|
+
if (
|
|
1102
|
+
webImageNow(element as HTMLImageElement) - startedAt <
|
|
1103
|
+
WEB_IMAGE_FADE_DELAY_MS ||
|
|
1104
|
+
view?.matchMedia?.('(prefers-reduced-motion: reduce)').matches ||
|
|
1105
|
+
typeof element.animate !== 'function'
|
|
1106
|
+
)
|
|
1107
|
+
return;
|
|
1108
|
+
element.animate([{ opacity: 0 }, { opacity: 1 }], {
|
|
1109
|
+
duration: WEB_IMAGE_FADE_DURATION_MS,
|
|
1110
|
+
easing: 'ease-out',
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1090
1114
|
function createImage(
|
|
1091
1115
|
context: RenderContext,
|
|
1092
1116
|
source: ImageSource,
|
|
@@ -1096,6 +1120,8 @@ function createImage(
|
|
|
1096
1120
|
const uri = avatarUri ?? safeImageUri(source.uri);
|
|
1097
1121
|
if (!uri) return undefined;
|
|
1098
1122
|
const image = context.document.createElement('img');
|
|
1123
|
+
const startedAt = webImageNow(image);
|
|
1124
|
+
image.style.opacity = '0';
|
|
1099
1125
|
if (className) image.className = className;
|
|
1100
1126
|
// OneKey patch: consume recoverable errors before the visual's final fallback listener.
|
|
1101
1127
|
if (avatarUri) {
|
|
@@ -1110,6 +1136,14 @@ function createImage(
|
|
|
1110
1136
|
image.decoding = 'async';
|
|
1111
1137
|
image.style.objectFit =
|
|
1112
1138
|
source.contentFit === 'fill' ? 'fill' : source.contentFit ?? 'cover';
|
|
1139
|
+
image.addEventListener('load', () => {
|
|
1140
|
+
if (image.dataset.nativeListSelectorPaint !== 'true')
|
|
1141
|
+
revealWebImage(image, startedAt);
|
|
1142
|
+
});
|
|
1143
|
+
image.addEventListener('error', () => {
|
|
1144
|
+
image.style.opacity = '0';
|
|
1145
|
+
});
|
|
1146
|
+
image.dataset.nativeListImageStartedAt = String(startedAt);
|
|
1113
1147
|
return image;
|
|
1114
1148
|
}
|
|
1115
1149
|
|
|
@@ -1128,20 +1162,27 @@ function paintSelectorImageBackground(
|
|
|
1128
1162
|
paint.style.cssText =
|
|
1129
1163
|
'position:absolute;pointer-events:none;border-radius:inherit;background-position:center;background-repeat:no-repeat';
|
|
1130
1164
|
paint.style.inset = String(inset) + 'px';
|
|
1165
|
+
paint.style.opacity = '0';
|
|
1131
1166
|
paint.style.backgroundSize =
|
|
1132
1167
|
image.style.objectFit === 'fill'
|
|
1133
1168
|
? '100% 100%'
|
|
1134
1169
|
: image.style.objectFit === 'center'
|
|
1135
1170
|
? 'auto'
|
|
1136
1171
|
: image.style.objectFit;
|
|
1172
|
+
image.dataset.nativeListSelectorPaint = 'true';
|
|
1137
1173
|
image.style.opacity = '0';
|
|
1138
1174
|
const update = () => {
|
|
1139
1175
|
paint.style.backgroundImage =
|
|
1140
1176
|
'url(' + JSON.stringify(image.currentSrc || image.src) + ')';
|
|
1177
|
+
revealWebImage(
|
|
1178
|
+
paint,
|
|
1179
|
+
Number(image.dataset.nativeListImageStartedAt) || webImageNow(image)
|
|
1180
|
+
);
|
|
1141
1181
|
};
|
|
1142
1182
|
image.addEventListener('load', update);
|
|
1143
1183
|
image.addEventListener('error', () => {
|
|
1144
1184
|
paint.style.backgroundImage = 'none';
|
|
1185
|
+
paint.style.opacity = '0';
|
|
1145
1186
|
});
|
|
1146
1187
|
frame.insertBefore(paint, image);
|
|
1147
1188
|
if (image.complete && image.naturalWidth > 0) update();
|
|
@@ -1516,9 +1557,19 @@ function createVisual(
|
|
|
1516
1557
|
return frame;
|
|
1517
1558
|
}
|
|
1518
1559
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1560
|
+
// OneKey patch: Every image visual starts from the current theme backing.
|
|
1561
|
+
// if ('backgroundColor' in visual && visual.backgroundColor)
|
|
1562
|
+
// frame.style.background = visual.backgroundColor;
|
|
1563
|
+
frame.style.background =
|
|
1564
|
+
'backgroundColor' in visual && visual.backgroundColor
|
|
1565
|
+
? visual.backgroundColor
|
|
1566
|
+
: 'var(--nl-strong)';
|
|
1521
1567
|
const source = visual.kind === 'image' ? visual.image : visual.image;
|
|
1568
|
+
const fallbackIcon =
|
|
1569
|
+
'fallbackIcon' in visual ? visual.fallbackIcon : undefined;
|
|
1570
|
+
const handlesSourceFallback =
|
|
1571
|
+
!!source && (fallbackIcon !== undefined || 'fallbackText' in visual);
|
|
1572
|
+
if (handlesSourceFallback) frame.style.background = 'var(--nl-strong)';
|
|
1522
1573
|
const image = source ? createImage(context, source) : undefined;
|
|
1523
1574
|
if (image) {
|
|
1524
1575
|
image.className = 'ok-native-list-visual-main';
|
|
@@ -1556,8 +1607,8 @@ function createVisual(
|
|
|
1556
1607
|
frame.appendChild(corner);
|
|
1557
1608
|
}
|
|
1558
1609
|
// OneKey patch: image failure uses the same source-derived fallback as v1.
|
|
1559
|
-
|
|
1560
|
-
|
|
1610
|
+
const hasFallbackText = 'fallbackText' in visual;
|
|
1611
|
+
if (fallbackIcon || hasFallbackText) {
|
|
1561
1612
|
const showFallback = () => {
|
|
1562
1613
|
if (image) {
|
|
1563
1614
|
disposeWebImageRetries(image);
|
|
@@ -1571,10 +1622,18 @@ function createVisual(
|
|
|
1571
1622
|
const fallback = createElement(
|
|
1572
1623
|
context.document,
|
|
1573
1624
|
'span',
|
|
1574
|
-
'ok-native-list-visual-fallback'
|
|
1625
|
+
'ok-native-list-visual-fallback',
|
|
1626
|
+
fallbackIcon
|
|
1627
|
+
? undefined
|
|
1628
|
+
: 'fallbackText' in visual
|
|
1629
|
+
? visual.fallbackText ?? ''
|
|
1630
|
+
: ''
|
|
1575
1631
|
);
|
|
1576
|
-
|
|
1577
|
-
|
|
1632
|
+
if (fallbackIcon) {
|
|
1633
|
+
applySelectorIcon(fallback, fallbackIcon.name);
|
|
1634
|
+
if (fallbackIcon.tintColor)
|
|
1635
|
+
fallback.style.color = fallbackIcon.tintColor;
|
|
1636
|
+
}
|
|
1578
1637
|
frame.prepend(fallback);
|
|
1579
1638
|
};
|
|
1580
1639
|
if (image) image.addEventListener('error', showFallback, { once: true });
|
|
@@ -3123,6 +3182,24 @@ function createMarketRow(context: RenderContext, row: MarketRow): HTMLElement {
|
|
|
3123
3182
|
String(layoutStyle.horizontalPadding) +
|
|
3124
3183
|
'px';
|
|
3125
3184
|
body.style.gap = '0px';
|
|
3185
|
+
if (row.leadingAction) {
|
|
3186
|
+
const action = createIconAction(
|
|
3187
|
+
context,
|
|
3188
|
+
row.leadingAction.name,
|
|
3189
|
+
row.leadingAction.actionKey,
|
|
3190
|
+
row.leadingAction.disabled,
|
|
3191
|
+
row.leadingAction.tintColor
|
|
3192
|
+
);
|
|
3193
|
+
action.style.flex = '0 0 36px';
|
|
3194
|
+
action.style.width = '36px';
|
|
3195
|
+
action.style.height = '36px';
|
|
3196
|
+
action.style.marginRight = '5px';
|
|
3197
|
+
setData(action, 'testid', row.leadingAction.testID);
|
|
3198
|
+
if (row.leadingAction.accessibilityLabel)
|
|
3199
|
+
action.setAttribute('aria-label', row.leadingAction.accessibilityLabel);
|
|
3200
|
+
markActionAnchorSource(action, 'leadingAction');
|
|
3201
|
+
body.appendChild(action);
|
|
3202
|
+
}
|
|
3126
3203
|
const visual = createVisual(context, row.leading);
|
|
3127
3204
|
if (visual) {
|
|
3128
3205
|
const width = layoutStyle.imageWidth;
|
|
@@ -3236,27 +3313,67 @@ function createMarketRow(context: RenderContext, row: MarketRow): HTMLElement {
|
|
|
3236
3313
|
titleLine.appendChild(element);
|
|
3237
3314
|
});
|
|
3238
3315
|
main.appendChild(titleLine);
|
|
3239
|
-
if (row.subtitle || row.subtitleSegments?.length) {
|
|
3240
|
-
const
|
|
3316
|
+
if (row.subtitlePrefix || row.subtitle || row.subtitleSegments?.length) {
|
|
3317
|
+
const subtitleLine = createElement(
|
|
3241
3318
|
context.document,
|
|
3242
3319
|
'span',
|
|
3243
|
-
'ok-native-list-market-subtitle'
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3320
|
+
'ok-native-list-market-subtitle-line'
|
|
3321
|
+
);
|
|
3322
|
+
subtitleLine.style.display = 'flex';
|
|
3323
|
+
subtitleLine.style.alignItems = 'center';
|
|
3324
|
+
subtitleLine.style.minWidth = '0';
|
|
3325
|
+
subtitleLine.style.overflow = 'hidden';
|
|
3326
|
+
subtitleLine.style.gap =
|
|
3327
|
+
String(row.subtitlePrefix ? row.subtitlePrefix.gap ?? 4 : 0) + 'px';
|
|
3328
|
+
if (row.subtitlePrefix) {
|
|
3329
|
+
const prefix = createElement(
|
|
3330
|
+
context.document,
|
|
3331
|
+
'span',
|
|
3332
|
+
'ok-native-list-market-subtitle-prefix',
|
|
3333
|
+
row.subtitlePrefix.text
|
|
3334
|
+
);
|
|
3335
|
+
applyMarketTextStyle(prefix, row.subtitlePrefix.style, {
|
|
3336
|
+
fontSize: 12,
|
|
3337
|
+
lineHeight: 16,
|
|
3338
|
+
weight: 400,
|
|
3339
|
+
alignment: 'start',
|
|
3340
|
+
});
|
|
3341
|
+
prefix.style.display = 'block';
|
|
3342
|
+
prefix.style.flex = '1 1 auto';
|
|
3343
|
+
prefix.style.minWidth = '0';
|
|
3344
|
+
prefix.style.overflow = 'hidden';
|
|
3345
|
+
prefix.style.textOverflow = 'ellipsis';
|
|
3346
|
+
prefix.style.color =
|
|
3347
|
+
row.subtitlePrefix.style?.color ?? 'var(--nl-secondary)';
|
|
3348
|
+
if (row.subtitlePrefix.maxWidth !== undefined) {
|
|
3349
|
+
prefix.style.maxWidth = String(row.subtitlePrefix.maxWidth) + 'px';
|
|
3350
|
+
}
|
|
3351
|
+
subtitleLine.appendChild(prefix);
|
|
3352
|
+
}
|
|
3353
|
+
if (row.subtitle || row.subtitleSegments?.length) {
|
|
3354
|
+
const subtitle = createElement(
|
|
3355
|
+
context.document,
|
|
3356
|
+
'span',
|
|
3357
|
+
'ok-native-list-market-subtitle',
|
|
3358
|
+
row.subtitle
|
|
3359
|
+
);
|
|
3360
|
+
applyMarketTextStyle(subtitle, style?.subtitle, {
|
|
3361
|
+
fontSize: 14,
|
|
3362
|
+
lineHeight: 20,
|
|
3363
|
+
weight: 400,
|
|
3364
|
+
alignment: 'start',
|
|
3365
|
+
});
|
|
3366
|
+
applyValueSegments(
|
|
3367
|
+
subtitle,
|
|
3368
|
+
row.subtitleSegments,
|
|
3369
|
+
style?.subtitle?.fontSize ?? 14,
|
|
3370
|
+
style?.subtitle?.lineHeight ?? 20,
|
|
3371
|
+
marketFontWeight(style?.subtitle?.fontWeight, 400)
|
|
3372
|
+
);
|
|
3373
|
+
subtitle.style.flex = row.subtitlePrefix ? '0 0 auto' : '1 1 auto';
|
|
3374
|
+
subtitleLine.appendChild(subtitle);
|
|
3375
|
+
}
|
|
3376
|
+
main.appendChild(subtitleLine);
|
|
3260
3377
|
}
|
|
3261
3378
|
body.appendChild(main);
|
|
3262
3379
|
const trailing = createElement(
|