@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.
@@ -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
- mediaNetworkImage.visibility = VISIBLE
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
- leadingFallback.text = fallback
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
- parseNativeListColor(if (isIcon) "#0000000F" else "#E0E0E0"),
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 = if (!isIcon && sources.isEmpty()) VISIBLE else GONE
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
- image.visibility = VISIBLE
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) visual.optJSONObject("fallbackIcon") else null
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 (fallbackIcon == null) null else ({
3309
- if (bindingEpoch == expectedEpoch) { image.visibility = VISIBLE; leadingIcon.visibility = GONE }
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 (fallbackIcon == null) null else ({
3337
+ onError = if (!ownsSourceFallback) null else ({
3312
3338
  if (bindingEpoch == expectedEpoch) {
3313
3339
  image.visibility = GONE
3314
- leadingFallback.visibility = GONE
3315
- leadingIcon.iconName = fallbackIcon.optString("name")
3316
- leadingIcon.tintColor = safeColor(fallbackIcon.optString("tintColor"), parseNativeListColor("#0000009B"))
3317
- leadingIcon.visibility = VISIBLE
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
- onLoad = if (retryLimit == 0) onLoad else ({
4153
+ placeholderColor = imagePlaceholderColor,
4154
+ onLoad = if (retryLimit == 0) handleLoad else ({
4107
4155
  if (bindingEpoch == expectedEpoch) {
4108
4156
  selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
4109
- onLoad?.invoke()
4157
+ handleLoad?.invoke()
4110
4158
  }
4111
4159
  }),
4112
- onError = if (retryLimit == 0) onError else ({
4160
+ onError = if (retryLimit == 0) handleError else ({
4113
4161
  if (bindingEpoch == expectedEpoch) {
4114
- if (retryAttempt >= retryLimit) onError?.invoke()
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
@@ -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
- label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
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
- mediaNetworkImage.isHidden = false
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
- fallbackLabel.text = String(visual.string("fallbackText").prefix(2))
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
- nativeListHex: visual.string("backgroundColor", default: isIcon ? "#F0F0F0" : "#E0E0E0"),
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
- imageView.isHidden = false
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 ? visual.dictionary("fallbackIcon") : nil
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: fallbackIcon == nil ? nil : { [weak self, weak imageView] in
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: fallbackIcon == nil ? nil : { [weak self, weak imageView] in
3448
- guard let self, self.bindingEpoch == expectedEpoch, let fallbackIcon else { return }
3477
+ onError: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
3478
+ guard let self, self.bindingEpoch == expectedEpoch else { return }
3449
3479
  imageView?.isHidden = true
3450
- self.fallbackLabel.isHidden = true
3451
- self.leadingIconImageView.image = nativeListIcon(named: fallbackIcon.string("name"))
3452
- self.leadingIconImageView.tintColor = UIColor(nativeListHex: fallbackIcon.string("tintColor", default: "#646464"), fallback: .darkGray)
3453
- self.leadingIconImageView.isHidden = false
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
- frame.backgroundColor = UIColor(nativeListHex: overlay.string("backgroundColor", default: "#FFFFFF"), fallback: .clear)
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(image, into: imageView, token: key, slot: 10 + index, variant: "generic")
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
- onLoad: retryLimit == 0 ? onLoad : { [weak self] in
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
- onLoad?()
4039
+ handleLoad?()
3975
4040
  },
3976
- onError: retryLimit == 0 ? onError : { [weak self, weak imageView] in
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 { onError?(); return }
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
- refreshVisibleSelection(changedSummaryKeys: changedSummaryKeys)
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
@@ -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 => OneKeyImageCache.preload([{
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,
@@ -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
- if ('backgroundColor' in visual && visual.backgroundColor) frame.style.background = visual.backgroundColor;
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
- if ('fallbackIcon' in visual && visual.fallbackIcon) {
974
- const icon = visual.fallbackIcon;
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
- applySelectorIcon(fallback, icon.name);
983
- if (icon.tintColor) fallback.style.color = icon.tintColor;
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 subtitle = createElement(context.document, 'span', 'ok-native-list-market-subtitle', row.subtitle);
1838
- applyMarketTextStyle(subtitle, style?.subtitle, {
1839
- fontSize: 14,
1840
- lineHeight: 20,
1841
- weight: 400,
1842
- alignment: 'start'
1843
- });
1844
- applyValueSegments(subtitle, row.subtitleSegments, style?.subtitle?.fontSize ?? 14, style?.subtitle?.lineHeight ?? 20, marketFontWeight(style?.subtitle?.fontWeight, 400));
1845
- main.appendChild(subtitle);
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.117",
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.117",
87
- "@onekeyfe/react-native-native-logger": "3.0.117",
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"
@@ -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
- OneKeyImageCache.preload([
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();
@@ -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
@@ -1,4 +1,4 @@
1
- export { NativeList } from './NativeList';
1
+ export { NativeList, preloadNativeListAvatarImages } from './NativeList';
2
2
  export type {
3
3
  ActionAnchorState,
4
4
  NativeListProps,
@@ -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
- if ('backgroundColor' in visual && visual.backgroundColor)
1520
- frame.style.background = visual.backgroundColor;
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
- if ('fallbackIcon' in visual && visual.fallbackIcon) {
1560
- const icon = visual.fallbackIcon;
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
- applySelectorIcon(fallback, icon.name);
1577
- if (icon.tintColor) fallback.style.color = icon.tintColor;
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 subtitle = createElement(
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
- row.subtitle
3263
- );
3264
- applyMarketTextStyle(subtitle, style?.subtitle, {
3265
- fontSize: 14,
3266
- lineHeight: 20,
3267
- weight: 400,
3268
- alignment: 'start',
3269
- });
3270
- applyValueSegments(
3271
- subtitle,
3272
- row.subtitleSegments,
3273
- style?.subtitle?.fontSize ?? 14,
3274
- style?.subtitle?.lineHeight ?? 20,
3275
- marketFontWeight(style?.subtitle?.fontWeight, 400)
3276
- );
3277
- main.appendChild(subtitle);
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(