@onekeyfe/react-native-native-list 3.0.117 → 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 +69 -21
- package/ios/NativeListCell.swift +88 -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/web/NativeListWebEngine.js +86 -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/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/web/NativeListWebEngine.ts +125 -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,
|
|
@@ -2492,11 +2495,12 @@ internal class NativeListRowView(
|
|
|
2492
2495
|
mediaMetadataRow.gravity = Gravity.CENTER_VERTICAL
|
|
2493
2496
|
mediaMetadataRow.addView(subtitle, weighted().apply { marginEnd = dp(8) })
|
|
2494
2497
|
item.json.optJSONObject("networkImage")?.let { networkImage ->
|
|
2495
|
-
|
|
2498
|
+
// OneKey patch: Preserve badge layout without exposing a loading/error tile.
|
|
2499
|
+
// mediaNetworkImage.visibility = VISIBLE
|
|
2496
2500
|
mediaNetworkImage.outlineProvider = circleOutlineProvider
|
|
2497
2501
|
mediaNetworkImage.clipToOutline = true
|
|
2498
2502
|
mediaMetadataRow.addView(mediaNetworkImage, LayoutParams(dp(14), dp(14)))
|
|
2499
|
-
bindImage(networkImage, mediaNetworkImage, item.key, 2, "network")
|
|
2503
|
+
bindImage(networkImage, mediaNetworkImage, item.key, 2, "network", hideUntilLoaded = true)
|
|
2500
2504
|
}
|
|
2501
2505
|
mainColumn.addView(mediaMetadataRow, 0)
|
|
2502
2506
|
mainColumn.addView(titleLine, 1)
|
|
@@ -3184,11 +3188,17 @@ internal class NativeListRowView(
|
|
|
3184
3188
|
val isIcon = kind == "icon"
|
|
3185
3189
|
val cornerIconData = visual.optJSONObject("cornerIcon")
|
|
3186
3190
|
val fallback = visual.optString("fallbackText").take(2)
|
|
3187
|
-
|
|
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
|
|
3188
3196
|
leadingFallback.setTextColor(parseNativeListColor("#00000072"))
|
|
3189
3197
|
val visualBackground = safeColor(
|
|
3190
3198
|
visual.optString("backgroundColor"),
|
|
3191
|
-
|
|
3199
|
+
// OneKey patch: Visual loading and fallback use the active list theme.
|
|
3200
|
+
// parseNativeListColor(if (isIcon) "#0000000F" else "#E0E0E0"),
|
|
3201
|
+
parseNativeListColor(imagePlaceholderColor),
|
|
3192
3202
|
)
|
|
3193
3203
|
if (!isIcon && visual.optString("backgroundColor").isNotEmpty()) {
|
|
3194
3204
|
leadingFrame.background = roundedFill(
|
|
@@ -3197,10 +3207,11 @@ internal class NativeListRowView(
|
|
|
3197
3207
|
)
|
|
3198
3208
|
}
|
|
3199
3209
|
leadingFallback.background = roundedFill(
|
|
3200
|
-
visualBackground,
|
|
3210
|
+
if (handlesSourceFallback) parseNativeListColor(imagePlaceholderColor) else visualBackground,
|
|
3201
3211
|
cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)),
|
|
3202
3212
|
)
|
|
3203
|
-
leadingFallback.visibility =
|
|
3213
|
+
leadingFallback.visibility =
|
|
3214
|
+
if (!isIcon && (sources.isEmpty() || handlesSourceFallback)) VISIBLE else GONE
|
|
3204
3215
|
if (isIcon) {
|
|
3205
3216
|
leadingFrame.background = GradientDrawable().apply {
|
|
3206
3217
|
setColor(visualBackground)
|
|
@@ -3213,6 +3224,14 @@ internal class NativeListRowView(
|
|
|
3213
3224
|
parseNativeListColor("#0000009B"),
|
|
3214
3225
|
)
|
|
3215
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
|
|
3216
3235
|
}
|
|
3217
3236
|
val visibleSources = sources.take(leadingImages.size)
|
|
3218
3237
|
val tokenPair = kind == "token" && visibleSources.size > 1
|
|
@@ -3265,7 +3284,10 @@ internal class NativeListRowView(
|
|
|
3265
3284
|
}
|
|
3266
3285
|
visibleSources.forEachIndexed { index, (source, variant) ->
|
|
3267
3286
|
val image = leadingImages[index]
|
|
3268
|
-
|
|
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
|
|
3269
3291
|
image.layoutParams = leadingImageLayout(
|
|
3270
3292
|
index = index,
|
|
3271
3293
|
count = visibleSources.size,
|
|
@@ -3302,21 +3324,32 @@ internal class NativeListRowView(
|
|
|
3302
3324
|
marketLeadingUsesSourceClip = true
|
|
3303
3325
|
image.clipToOutline = false
|
|
3304
3326
|
}
|
|
3305
|
-
val fallbackIcon = if (index == 0)
|
|
3327
|
+
val fallbackIcon = if (index == 0) fallbackIconData else null
|
|
3306
3328
|
val expectedEpoch = bindingEpoch
|
|
3307
3329
|
bindImage(source, image, boundKey ?: "", index, variant,
|
|
3308
|
-
onLoad = if (
|
|
3309
|
-
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
|
+
}
|
|
3310
3336
|
}),
|
|
3311
|
-
onError = if (
|
|
3337
|
+
onError = if (!ownsSourceFallback) null else ({
|
|
3312
3338
|
if (bindingEpoch == expectedEpoch) {
|
|
3313
3339
|
image.visibility = GONE
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
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
|
+
}
|
|
3318
3350
|
}
|
|
3319
3351
|
}),
|
|
3352
|
+
hideUntilLoaded = index > 0 || ownsSourceFallback,
|
|
3320
3353
|
)
|
|
3321
3354
|
}
|
|
3322
3355
|
// OneKey patch: wallet overlays retain source images, provider colors and QR text.
|
|
@@ -3339,7 +3372,7 @@ internal class NativeListRowView(
|
|
|
3339
3372
|
val image = overlay.optJSONObject("image")
|
|
3340
3373
|
val view = when {
|
|
3341
3374
|
image != null -> OneKeyImageReusableView(reactContext).also {
|
|
3342
|
-
bindImage(image, it, boundKey ?: "", 10 + index, "generic")
|
|
3375
|
+
bindImage(image, it, boundKey ?: "", 10 + index, "generic", hideUntilLoaded = true)
|
|
3343
3376
|
selectorImages.add(it)
|
|
3344
3377
|
}
|
|
3345
3378
|
overlay.optString("text").isNotEmpty() -> TextView(context).apply {
|
|
@@ -4086,12 +4119,26 @@ internal class NativeListRowView(
|
|
|
4086
4119
|
variant: String,
|
|
4087
4120
|
onLoad: (() -> Unit)? = null,
|
|
4088
4121
|
onError: (() -> Unit)? = null,
|
|
4122
|
+
hideUntilLoaded: Boolean = false,
|
|
4089
4123
|
retryAttempt: Int = 0,
|
|
4090
4124
|
) {
|
|
4091
4125
|
selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
|
|
4092
4126
|
val expectedEpoch = bindingEpoch
|
|
4093
4127
|
val retryLimit = source.optInt("retryTimes", 0).coerceAtLeast(0)
|
|
4094
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
|
|
4095
4142
|
imageView.configure(
|
|
4096
4143
|
sourceUri = uri,
|
|
4097
4144
|
sourceHeadersJson = source.optJSONObject("headers")?.toString(),
|
|
@@ -4103,20 +4150,21 @@ internal class NativeListRowView(
|
|
|
4103
4150
|
optimizeTos = retryAttempt == 0 && source.optBoolean("optimizeTos", true),
|
|
4104
4151
|
overscan = source.optDouble("overscan", 1.1),
|
|
4105
4152
|
loadingStrategy = source.optString("loadingStrategy", "static"),
|
|
4106
|
-
|
|
4153
|
+
placeholderColor = imagePlaceholderColor,
|
|
4154
|
+
onLoad = if (retryLimit == 0) handleLoad else ({
|
|
4107
4155
|
if (bindingEpoch == expectedEpoch) {
|
|
4108
4156
|
selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
|
|
4109
|
-
|
|
4157
|
+
handleLoad?.invoke()
|
|
4110
4158
|
}
|
|
4111
4159
|
}),
|
|
4112
|
-
onError = if (retryLimit == 0)
|
|
4160
|
+
onError = if (retryLimit == 0) handleError else ({
|
|
4113
4161
|
if (bindingEpoch == expectedEpoch) {
|
|
4114
|
-
if (retryAttempt >= retryLimit)
|
|
4162
|
+
if (retryAttempt >= retryLimit) handleError?.invoke()
|
|
4115
4163
|
else if (!selectorImageRetries.containsKey(imageView)) {
|
|
4116
4164
|
val retry = Runnable {
|
|
4117
4165
|
if (bindingEpoch == expectedEpoch) {
|
|
4118
4166
|
selectorImageRetries.remove(imageView)
|
|
4119
|
-
bindImage(source, imageView, token, slot, variant, onLoad, onError, retryAttempt + 1)
|
|
4167
|
+
bindImage(source, imageView, token, slot, variant, onLoad, onError, hideUntilLoaded, retryAttempt + 1)
|
|
4120
4168
|
}
|
|
4121
4169
|
}
|
|
4122
4170
|
selectorImageRetries[imageView] = retry
|
package/ios/NativeListCell.swift
CHANGED
|
@@ -1835,7 +1835,13 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
1835
1835
|
let label = UILabel()
|
|
1836
1836
|
label.font = nativeListFont(ofSize: 14)
|
|
1837
1837
|
label.lineBreakMode = .byTruncatingTail
|
|
1838
|
-
|
|
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
|
+
)
|
|
1839
1845
|
label.textColor = dataTextColor(segment.string("tone", default: "secondary"), theme: theme)
|
|
1840
1846
|
setLineHeight(label, text: segment.string("text"), lineHeight: 20)
|
|
1841
1847
|
let runs = segment.dictionaries("textSegments")
|
|
@@ -2591,13 +2597,15 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
2591
2597
|
subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
2592
2598
|
if let networkImage = item.data.dictionary("networkImage") {
|
|
2593
2599
|
mediaMetadataStack.addArrangedSubview(mediaNetworkImage)
|
|
2594
|
-
|
|
2600
|
+
// OneKey patch: Preserve badge layout without exposing a loading/error tile.
|
|
2601
|
+
// mediaNetworkImage.isHidden = false
|
|
2595
2602
|
bindImage(
|
|
2596
2603
|
networkImage,
|
|
2597
2604
|
into: mediaNetworkImage,
|
|
2598
2605
|
token: item.key,
|
|
2599
2606
|
slot: 2,
|
|
2600
|
-
variant: "network"
|
|
2607
|
+
variant: "network",
|
|
2608
|
+
hideUntilLoaded: true
|
|
2601
2609
|
)
|
|
2602
2610
|
}
|
|
2603
2611
|
mainStack.insertArrangedSubview(mediaMetadataStack, at: 0)
|
|
@@ -3367,14 +3375,23 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3367
3375
|
default: kind == "image" || mediaHeight.isActive ? "rounded" : "circle"
|
|
3368
3376
|
)
|
|
3369
3377
|
let cornerIcon = visual.dictionary("cornerIcon")
|
|
3370
|
-
|
|
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"
|
|
3371
3384
|
leadingContainer.backgroundColor = UIColor(
|
|
3372
|
-
|
|
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),
|
|
3373
3390
|
fallback: .gray
|
|
3374
3391
|
)
|
|
3375
3392
|
leadingContainer.layer.cornerRadius = leadingCornerRadius(shape: shape)
|
|
3376
3393
|
leadingContainer.clipsToBounds = true
|
|
3377
|
-
fallbackLabel.isHidden = isIcon || !sources.isEmpty
|
|
3394
|
+
fallbackLabel.isHidden = isIcon || (!sources.isEmpty && !handlesSourceFallback)
|
|
3378
3395
|
if isIcon {
|
|
3379
3396
|
leadingContainer.layer.borderWidth = 1 / UIScreen.main.scale
|
|
3380
3397
|
leadingContainer.layer.borderColor = UIColor(
|
|
@@ -3388,6 +3405,15 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3388
3405
|
)
|
|
3389
3406
|
leadingIconImageView.image = nativeListIcon(named: visual.string("name"))
|
|
3390
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
|
|
3391
3417
|
}
|
|
3392
3418
|
let visibleSources = Array(sources.prefix(leadingImages.count))
|
|
3393
3419
|
let tokenPair = kind == "token" && visibleSources.count > 1
|
|
@@ -3422,7 +3448,10 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3422
3448
|
}
|
|
3423
3449
|
for (index, source) in visibleSources.enumerated() {
|
|
3424
3450
|
let imageView = leadingImages[index]
|
|
3425
|
-
|
|
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
|
|
3426
3455
|
imageView.clipsToBounds = true
|
|
3427
3456
|
leadingSlotConstraints.append(contentsOf: leadingConstraints(
|
|
3428
3457
|
imageView,
|
|
@@ -3436,22 +3465,30 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3436
3465
|
imageView.layer.cornerRadius = 0
|
|
3437
3466
|
imageView.clipsToBounds = false
|
|
3438
3467
|
}
|
|
3439
|
-
let fallbackIcon = index == 0 ?
|
|
3468
|
+
let fallbackIcon = index == 0 ? fallbackIconData : nil
|
|
3440
3469
|
let expectedEpoch = bindingEpoch
|
|
3441
3470
|
bindImage(source.data, into: imageView, token: key, slot: index, variant: source.variant,
|
|
3442
|
-
onLoad:
|
|
3471
|
+
onLoad: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
|
|
3443
3472
|
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
3444
3473
|
imageView?.isHidden = false
|
|
3474
|
+
self.fallbackLabel.isHidden = true
|
|
3445
3475
|
self.leadingIconImageView.isHidden = true
|
|
3446
3476
|
},
|
|
3447
|
-
onError:
|
|
3448
|
-
guard let self, self.bindingEpoch == expectedEpoch
|
|
3477
|
+
onError: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
|
|
3478
|
+
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
3449
3479
|
imageView?.isHidden = true
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
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)
|
|
3455
3492
|
}
|
|
3456
3493
|
// OneKey patch: source-derived wallet decorations may occupy both corners.
|
|
3457
3494
|
let overlays = visual.dictionaries("overlays")
|
|
@@ -3468,7 +3505,15 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3468
3505
|
let width = CGFloat(overlay.double("width", default: isWalletText ? Double(naturalTextWidth) : Double(size)))
|
|
3469
3506
|
let frame = UIView()
|
|
3470
3507
|
frame.translatesAutoresizingMaskIntoConstraints = false
|
|
3471
|
-
|
|
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
|
+
)
|
|
3472
3517
|
frame.layer.cornerRadius = min(width, height) / 2
|
|
3473
3518
|
frame.clipsToBounds = true
|
|
3474
3519
|
leadingContainer.addSubview(frame)
|
|
@@ -3483,7 +3528,14 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3483
3528
|
let content: UIView
|
|
3484
3529
|
if let image = overlay.dictionary("image") {
|
|
3485
3530
|
let imageView = OneKeyImageReusableView(frame: .zero)
|
|
3486
|
-
bindImage(
|
|
3531
|
+
bindImage(
|
|
3532
|
+
image,
|
|
3533
|
+
into: imageView,
|
|
3534
|
+
token: key,
|
|
3535
|
+
slot: 10 + index,
|
|
3536
|
+
variant: "generic",
|
|
3537
|
+
hideUntilLoaded: true
|
|
3538
|
+
)
|
|
3487
3539
|
selectorImages.append(imageView)
|
|
3488
3540
|
content = imageView
|
|
3489
3541
|
} else if !overlay.string("text").isEmpty {
|
|
@@ -3943,6 +3995,7 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3943
3995
|
variant: String,
|
|
3944
3996
|
onLoad: (() -> Void)? = nil,
|
|
3945
3997
|
onError: (() -> Void)? = nil,
|
|
3998
|
+
hideUntilLoaded: Bool = false,
|
|
3946
3999
|
retryAttempt: Int = 0
|
|
3947
4000
|
) {
|
|
3948
4001
|
let imageID = ObjectIdentifier(imageView)
|
|
@@ -3957,6 +4010,17 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3957
4010
|
} else {
|
|
3958
4011
|
headersJson = nil
|
|
3959
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
|
|
3960
4024
|
imageView.configure(
|
|
3961
4025
|
sourceUri: source.string("uri").trimmingCharacters(in: .whitespacesAndNewlines),
|
|
3962
4026
|
sourceHeadersJson: headersJson,
|
|
@@ -3968,19 +4032,20 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3968
4032
|
optimizeTos: retryAttempt == 0 && (source["optimizeTos"] == nil || source.bool("optimizeTos")),
|
|
3969
4033
|
overscan: source["overscan"] == nil ? 1.1 : source.double("overscan"),
|
|
3970
4034
|
loadingStrategy: source.string("loadingStrategy", default: "static"),
|
|
3971
|
-
|
|
4035
|
+
placeholderColor: currentTheme?["strongBackground"] as? String ?? "#0000000F",
|
|
4036
|
+
onLoad: retryLimit == 0 ? handleLoad : { [weak self] in
|
|
3972
4037
|
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
3973
4038
|
self.selectorImageRetries.removeValue(forKey: imageID)?.cancel()
|
|
3974
|
-
|
|
4039
|
+
handleLoad?()
|
|
3975
4040
|
},
|
|
3976
|
-
onError: retryLimit == 0 ?
|
|
4041
|
+
onError: retryLimit == 0 ? handleError : { [weak self, weak imageView] in
|
|
3977
4042
|
guard let self, self.bindingEpoch == expectedEpoch, let imageView else { return }
|
|
3978
|
-
guard retryAttempt < retryLimit else {
|
|
4043
|
+
guard retryAttempt < retryLimit else { handleError?(); return }
|
|
3979
4044
|
guard self.selectorImageRetries[imageID] == nil else { return }
|
|
3980
4045
|
let retry = DispatchWorkItem { [weak self, weak imageView] in
|
|
3981
4046
|
guard let self, self.bindingEpoch == expectedEpoch, let imageView else { return }
|
|
3982
4047
|
self.selectorImageRetries.removeValue(forKey: imageID)
|
|
3983
|
-
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)
|
|
3984
4049
|
}
|
|
3985
4050
|
self.selectorImageRetries[imageID] = retry
|
|
3986
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
|
|
@@ -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, {
|
|
@@ -1833,16 +1874,45 @@ function createMarketRow(context, row) {
|
|
|
1833
1874
|
titleLine.appendChild(element);
|
|
1834
1875
|
});
|
|
1835
1876
|
main.appendChild(titleLine);
|
|
1836
|
-
if (row.subtitle || row.subtitleSegments?.length) {
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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);
|
|
1846
1916
|
}
|
|
1847
1917
|
body.appendChild(main);
|
|
1848
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';
|
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
|
@@ -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 });
|
|
@@ -3254,27 +3313,67 @@ function createMarketRow(context: RenderContext, row: MarketRow): HTMLElement {
|
|
|
3254
3313
|
titleLine.appendChild(element);
|
|
3255
3314
|
});
|
|
3256
3315
|
main.appendChild(titleLine);
|
|
3257
|
-
if (row.subtitle || row.subtitleSegments?.length) {
|
|
3258
|
-
const
|
|
3316
|
+
if (row.subtitlePrefix || row.subtitle || row.subtitleSegments?.length) {
|
|
3317
|
+
const subtitleLine = createElement(
|
|
3259
3318
|
context.document,
|
|
3260
3319
|
'span',
|
|
3261
|
-
'ok-native-list-market-subtitle'
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
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);
|
|
3278
3377
|
}
|
|
3279
3378
|
body.appendChild(main);
|
|
3280
3379
|
const trailing = createElement(
|