@onekeyfe/react-native-native-list 3.0.117 → 3.0.119

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/README.md CHANGED
@@ -240,6 +240,12 @@ const avatar = {
240
240
  } as const;
241
241
  ```
242
242
 
243
+ NativeList defaults `loadingStrategy` to `none`, so source-backed images render
244
+ without a loading background or terminal fallback. Set it to `static` or
245
+ `skeleton` to opt into loading UI; a configured `fallbackText` or
246
+ `fallbackIcon` is shown after a terminal load failure only when loading UI is
247
+ enabled.
248
+
243
249
  Each cell owns a fixed pool of native `OneKeyImageReusableView` slots. On bind,
244
250
  NativeList forwards the URI, headers, content fit, cache policy, autoplay, TOS
245
251
  options, overscan, and loading strategy. A stable `rowKey:slot` recycling key
@@ -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,19 @@ 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 sourceLoadingStrategy = sources.firstOrNull()?.first?.optString("loadingStrategy", "none") ?: "none"
3193
+ val showsSourcePlaceholder = !isIcon && sources.isNotEmpty() && sourceLoadingStrategy != "none"
3194
+ val handlesSourceFallback =
3195
+ !isIcon && sources.isNotEmpty() &&
3196
+ showsSourcePlaceholder &&
3197
+ (visual.has("fallbackText") || fallbackIconData != null)
3198
+ leadingFallback.text = if (handlesSourceFallback) "" else fallback
3188
3199
  leadingFallback.setTextColor(parseNativeListColor("#00000072"))
3189
3200
  val visualBackground = safeColor(
3190
3201
  visual.optString("backgroundColor"),
3191
- parseNativeListColor(if (isIcon) "#0000000F" else "#E0E0E0"),
3202
+ // OneKey patch: source-backed visuals default to no placeholder/background.
3203
+ if (!isIcon && sources.isNotEmpty()) Color.TRANSPARENT else parseNativeListColor(imagePlaceholderColor),
3192
3204
  )
3193
3205
  if (!isIcon && visual.optString("backgroundColor").isNotEmpty()) {
3194
3206
  leadingFrame.background = roundedFill(
@@ -3197,10 +3209,11 @@ internal class NativeListRowView(
3197
3209
  )
3198
3210
  }
3199
3211
  leadingFallback.background = roundedFill(
3200
- visualBackground,
3212
+ if (handlesSourceFallback) parseNativeListColor(imagePlaceholderColor) else visualBackground,
3201
3213
  cornerRadiusDp ?: leadingCornerRadius(shape, minOf(sizeDp, heightDp)),
3202
3214
  )
3203
- leadingFallback.visibility = if (!isIcon && sources.isEmpty()) VISIBLE else GONE
3215
+ leadingFallback.visibility =
3216
+ if (!isIcon && (sources.isEmpty() || handlesSourceFallback)) VISIBLE else GONE
3204
3217
  if (isIcon) {
3205
3218
  leadingFrame.background = GradientDrawable().apply {
3206
3219
  setColor(visualBackground)
@@ -3213,6 +3226,14 @@ internal class NativeListRowView(
3213
3226
  parseNativeListColor("#0000009B"),
3214
3227
  )
3215
3228
  leadingIcon.visibility = VISIBLE
3229
+ } else if (sources.isEmpty() && fallbackIconData != null) {
3230
+ leadingFallback.visibility = GONE
3231
+ leadingIcon.iconName = fallbackIconData.optString("name")
3232
+ leadingIcon.tintColor = safeColor(
3233
+ fallbackIconData.optString("tintColor"),
3234
+ parseNativeListColor("#0000009B"),
3235
+ )
3236
+ leadingIcon.visibility = VISIBLE
3216
3237
  }
3217
3238
  val visibleSources = sources.take(leadingImages.size)
3218
3239
  val tokenPair = kind == "token" && visibleSources.size > 1
@@ -3265,7 +3286,10 @@ internal class NativeListRowView(
3265
3286
  }
3266
3287
  visibleSources.forEachIndexed { index, (source, variant) ->
3267
3288
  val image = leadingImages[index]
3268
- image.visibility = VISIBLE
3289
+ // OneKey patch: Decorative badges and source-owned fallbacks stay hidden until loaded.
3290
+ // image.visibility = VISIBLE
3291
+ val ownsSourceFallback = index == 0 && handlesSourceFallback
3292
+ image.visibility = if (index > 0 || ownsSourceFallback) INVISIBLE else VISIBLE
3269
3293
  image.layoutParams = leadingImageLayout(
3270
3294
  index = index,
3271
3295
  count = visibleSources.size,
@@ -3302,21 +3326,32 @@ internal class NativeListRowView(
3302
3326
  marketLeadingUsesSourceClip = true
3303
3327
  image.clipToOutline = false
3304
3328
  }
3305
- val fallbackIcon = if (index == 0) visual.optJSONObject("fallbackIcon") else null
3329
+ val fallbackIcon = if (index == 0) fallbackIconData else null
3306
3330
  val expectedEpoch = bindingEpoch
3307
3331
  bindImage(source, image, boundKey ?: "", index, variant,
3308
- onLoad = if (fallbackIcon == null) null else ({
3309
- if (bindingEpoch == expectedEpoch) { image.visibility = VISIBLE; leadingIcon.visibility = GONE }
3332
+ onLoad = if (!ownsSourceFallback) null else ({
3333
+ if (bindingEpoch == expectedEpoch) {
3334
+ image.visibility = VISIBLE
3335
+ leadingFallback.visibility = GONE
3336
+ leadingIcon.visibility = GONE
3337
+ }
3310
3338
  }),
3311
- onError = if (fallbackIcon == null) null else ({
3339
+ onError = if (!ownsSourceFallback) null else ({
3312
3340
  if (bindingEpoch == expectedEpoch) {
3313
3341
  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
3342
+ if (fallbackIcon != null) {
3343
+ leadingFallback.visibility = GONE
3344
+ leadingIcon.iconName = fallbackIcon.optString("name")
3345
+ leadingIcon.tintColor = safeColor(fallbackIcon.optString("tintColor"), parseNativeListColor("#0000009B"))
3346
+ leadingIcon.visibility = VISIBLE
3347
+ } else {
3348
+ leadingIcon.visibility = GONE
3349
+ leadingFallback.text = fallback
3350
+ leadingFallback.visibility = VISIBLE
3351
+ }
3318
3352
  }
3319
3353
  }),
3354
+ hideUntilLoaded = index > 0 || ownsSourceFallback,
3320
3355
  )
3321
3356
  }
3322
3357
  // OneKey patch: wallet overlays retain source images, provider colors and QR text.
@@ -3339,7 +3374,7 @@ internal class NativeListRowView(
3339
3374
  val image = overlay.optJSONObject("image")
3340
3375
  val view = when {
3341
3376
  image != null -> OneKeyImageReusableView(reactContext).also {
3342
- bindImage(image, it, boundKey ?: "", 10 + index, "generic")
3377
+ bindImage(image, it, boundKey ?: "", 10 + index, "generic", hideUntilLoaded = true)
3343
3378
  selectorImages.add(it)
3344
3379
  }
3345
3380
  overlay.optString("text").isNotEmpty() -> TextView(context).apply {
@@ -4086,12 +4121,26 @@ internal class NativeListRowView(
4086
4121
  variant: String,
4087
4122
  onLoad: (() -> Unit)? = null,
4088
4123
  onError: (() -> Unit)? = null,
4124
+ hideUntilLoaded: Boolean = false,
4089
4125
  retryAttempt: Int = 0,
4090
4126
  ) {
4091
4127
  selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
4092
4128
  val expectedEpoch = bindingEpoch
4093
4129
  val retryLimit = source.optInt("retryTimes", 0).coerceAtLeast(0)
4094
4130
  val uri = source.optString("uri").trim().takeIf(String::isNotEmpty)
4131
+ if (hideUntilLoaded) imageView.visibility = INVISIBLE
4132
+ val handleLoad: (() -> Unit)? = if (hideUntilLoaded) ({
4133
+ if (bindingEpoch == expectedEpoch) {
4134
+ imageView.visibility = VISIBLE
4135
+ onLoad?.invoke()
4136
+ }
4137
+ }) else onLoad
4138
+ val handleError: (() -> Unit)? = if (hideUntilLoaded) ({
4139
+ if (bindingEpoch == expectedEpoch) {
4140
+ imageView.visibility = INVISIBLE
4141
+ onError?.invoke()
4142
+ }
4143
+ }) else onError
4095
4144
  imageView.configure(
4096
4145
  sourceUri = uri,
4097
4146
  sourceHeadersJson = source.optJSONObject("headers")?.toString(),
@@ -4102,21 +4151,22 @@ internal class NativeListRowView(
4102
4151
  recyclingKey = if (retryAttempt == 0) "$token:$slot" else "$token:$slot:retry:$retryAttempt",
4103
4152
  optimizeTos = retryAttempt == 0 && source.optBoolean("optimizeTos", true),
4104
4153
  overscan = source.optDouble("overscan", 1.1),
4105
- loadingStrategy = source.optString("loadingStrategy", "static"),
4106
- onLoad = if (retryLimit == 0) onLoad else ({
4154
+ loadingStrategy = source.optString("loadingStrategy", "none"),
4155
+ placeholderColor = imagePlaceholderColor,
4156
+ onLoad = if (retryLimit == 0) handleLoad else ({
4107
4157
  if (bindingEpoch == expectedEpoch) {
4108
4158
  selectorImageRetries.remove(imageView)?.let(imageView::removeCallbacks)
4109
- onLoad?.invoke()
4159
+ handleLoad?.invoke()
4110
4160
  }
4111
4161
  }),
4112
- onError = if (retryLimit == 0) onError else ({
4162
+ onError = if (retryLimit == 0) handleError else ({
4113
4163
  if (bindingEpoch == expectedEpoch) {
4114
- if (retryAttempt >= retryLimit) onError?.invoke()
4164
+ if (retryAttempt >= retryLimit) handleError?.invoke()
4115
4165
  else if (!selectorImageRetries.containsKey(imageView)) {
4116
4166
  val retry = Runnable {
4117
4167
  if (bindingEpoch == expectedEpoch) {
4118
4168
  selectorImageRetries.remove(imageView)
4119
- bindImage(source, imageView, token, slot, variant, onLoad, onError, retryAttempt + 1)
4169
+ bindImage(source, imageView, token, slot, variant, onLoad, onError, hideUntilLoaded, retryAttempt + 1)
4120
4170
  }
4121
4171
  }
4122
4172
  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,28 @@ 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))
3371
- leadingContainer.backgroundColor = UIColor(
3372
- nativeListHex: visual.string("backgroundColor", default: isIcon ? "#F0F0F0" : "#E0E0E0"),
3373
- fallback: .gray
3378
+ let fallbackText = String(visual.string("fallbackText").prefix(2))
3379
+ let fallbackIconData = visual.dictionary("fallbackIcon")
3380
+ let sourceLoadingStrategy = sources.first?.data.string("loadingStrategy", default: "none") ?? "none"
3381
+ let showsSourcePlaceholder = !isIcon && !sources.isEmpty && sourceLoadingStrategy != "none"
3382
+ let handlesSourceFallback = !isIcon && !sources.isEmpty &&
3383
+ showsSourcePlaceholder &&
3384
+ (visual["fallbackText"] != nil || fallbackIconData != nil)
3385
+ fallbackLabel.text = handlesSourceFallback ? nil : fallbackText
3386
+ let sourceFallbackBackground = currentTheme?["strongBackground"] as? String ?? "#0000000F"
3387
+ // OneKey patch: source-backed visuals default to no placeholder/background.
3388
+ // A non-none image loadingStrategy opts back into the themed placeholder.
3389
+ let visualBackground = visual.string(
3390
+ "backgroundColor",
3391
+ default: !isIcon && !sources.isEmpty ? "#00000000" : sourceFallbackBackground
3374
3392
  )
3393
+ let visualBackgroundColor = UIColor(nativeListHex: visualBackground, fallback: .clear)
3394
+ leadingContainer.backgroundColor = handlesSourceFallback
3395
+ ? UIColor(nativeListHex: sourceFallbackBackground, fallback: .clear)
3396
+ : visualBackgroundColor
3375
3397
  leadingContainer.layer.cornerRadius = leadingCornerRadius(shape: shape)
3376
3398
  leadingContainer.clipsToBounds = true
3377
- fallbackLabel.isHidden = isIcon || !sources.isEmpty
3399
+ fallbackLabel.isHidden = isIcon || (!sources.isEmpty && !handlesSourceFallback)
3378
3400
  if isIcon {
3379
3401
  leadingContainer.layer.borderWidth = 1 / UIScreen.main.scale
3380
3402
  leadingContainer.layer.borderColor = UIColor(
@@ -3388,6 +3410,15 @@ final class NativeListCell: UICollectionViewCell {
3388
3410
  )
3389
3411
  leadingIconImageView.image = nativeListIcon(named: visual.string("name"))
3390
3412
  leadingIconImageView.contentMode = .scaleAspectFit
3413
+ } else if sources.isEmpty, let fallbackIconData {
3414
+ fallbackLabel.isHidden = true
3415
+ leadingIconImageView.isHidden = false
3416
+ leadingIconImageView.tintColor = UIColor(
3417
+ nativeListHex: fallbackIconData.string("tintColor", default: "#646464"),
3418
+ fallback: .darkGray
3419
+ )
3420
+ leadingIconImageView.image = nativeListIcon(named: fallbackIconData.string("name"))
3421
+ leadingIconImageView.contentMode = .scaleAspectFit
3391
3422
  }
3392
3423
  let visibleSources = Array(sources.prefix(leadingImages.count))
3393
3424
  let tokenPair = kind == "token" && visibleSources.count > 1
@@ -3422,7 +3453,10 @@ final class NativeListCell: UICollectionViewCell {
3422
3453
  }
3423
3454
  for (index, source) in visibleSources.enumerated() {
3424
3455
  let imageView = leadingImages[index]
3425
- imageView.isHidden = false
3456
+ // OneKey patch: Decorative badges and source-owned fallbacks stay hidden until loaded.
3457
+ // imageView.isHidden = false
3458
+ let ownsSourceFallback = index == 0 && handlesSourceFallback
3459
+ imageView.isHidden = index > 0 || ownsSourceFallback
3426
3460
  imageView.clipsToBounds = true
3427
3461
  leadingSlotConstraints.append(contentsOf: leadingConstraints(
3428
3462
  imageView,
@@ -3436,22 +3470,31 @@ final class NativeListCell: UICollectionViewCell {
3436
3470
  imageView.layer.cornerRadius = 0
3437
3471
  imageView.clipsToBounds = false
3438
3472
  }
3439
- let fallbackIcon = index == 0 ? visual.dictionary("fallbackIcon") : nil
3473
+ let fallbackIcon = index == 0 ? fallbackIconData : nil
3440
3474
  let expectedEpoch = bindingEpoch
3441
3475
  bindImage(source.data, into: imageView, token: key, slot: index, variant: source.variant,
3442
- onLoad: fallbackIcon == nil ? nil : { [weak self, weak imageView] in
3476
+ onLoad: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
3443
3477
  guard let self, self.bindingEpoch == expectedEpoch else { return }
3444
3478
  imageView?.isHidden = false
3479
+ self.fallbackLabel.isHidden = true
3445
3480
  self.leadingIconImageView.isHidden = true
3481
+ self.leadingContainer.backgroundColor = visualBackgroundColor
3446
3482
  },
3447
- onError: fallbackIcon == nil ? nil : { [weak self, weak imageView] in
3448
- guard let self, self.bindingEpoch == expectedEpoch, let fallbackIcon else { return }
3483
+ onError: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
3484
+ guard let self, self.bindingEpoch == expectedEpoch else { return }
3449
3485
  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
- })
3486
+ if let fallbackIcon {
3487
+ self.fallbackLabel.isHidden = true
3488
+ self.leadingIconImageView.image = nativeListIcon(named: fallbackIcon.string("name"))
3489
+ self.leadingIconImageView.tintColor = UIColor(nativeListHex: fallbackIcon.string("tintColor", default: "#646464"), fallback: .darkGray)
3490
+ self.leadingIconImageView.isHidden = false
3491
+ } else {
3492
+ self.leadingIconImageView.isHidden = true
3493
+ self.fallbackLabel.text = fallbackText
3494
+ self.fallbackLabel.isHidden = false
3495
+ }
3496
+ },
3497
+ hideUntilLoaded: index > 0 || ownsSourceFallback)
3455
3498
  }
3456
3499
  // OneKey patch: source-derived wallet decorations may occupy both corners.
3457
3500
  let overlays = visual.dictionaries("overlays")
@@ -3468,7 +3511,15 @@ final class NativeListCell: UICollectionViewCell {
3468
3511
  let width = CGFloat(overlay.double("width", default: isWalletText ? Double(naturalTextWidth) : Double(size)))
3469
3512
  let frame = UIView()
3470
3513
  frame.translatesAutoresizingMaskIntoConstraints = false
3471
- frame.backgroundColor = UIColor(nativeListHex: overlay.string("backgroundColor", default: "#FFFFFF"), fallback: .clear)
3514
+ // OneKey patch: The row theme, not a light-only white, owns badge backing.
3515
+ // frame.backgroundColor = UIColor(nativeListHex: overlay.string("backgroundColor", default: "#FFFFFF"), fallback: .clear)
3516
+ frame.backgroundColor = UIColor(
3517
+ nativeListHex: overlay.string(
3518
+ "backgroundColor",
3519
+ default: currentTheme?["rowBackground"] as? String ?? "#00000000"
3520
+ ),
3521
+ fallback: .clear
3522
+ )
3472
3523
  frame.layer.cornerRadius = min(width, height) / 2
3473
3524
  frame.clipsToBounds = true
3474
3525
  leadingContainer.addSubview(frame)
@@ -3483,7 +3534,14 @@ final class NativeListCell: UICollectionViewCell {
3483
3534
  let content: UIView
3484
3535
  if let image = overlay.dictionary("image") {
3485
3536
  let imageView = OneKeyImageReusableView(frame: .zero)
3486
- bindImage(image, into: imageView, token: key, slot: 10 + index, variant: "generic")
3537
+ bindImage(
3538
+ image,
3539
+ into: imageView,
3540
+ token: key,
3541
+ slot: 10 + index,
3542
+ variant: "generic",
3543
+ hideUntilLoaded: true
3544
+ )
3487
3545
  selectorImages.append(imageView)
3488
3546
  content = imageView
3489
3547
  } else if !overlay.string("text").isEmpty {
@@ -3943,6 +4001,7 @@ final class NativeListCell: UICollectionViewCell {
3943
4001
  variant: String,
3944
4002
  onLoad: (() -> Void)? = nil,
3945
4003
  onError: (() -> Void)? = nil,
4004
+ hideUntilLoaded: Bool = false,
3946
4005
  retryAttempt: Int = 0
3947
4006
  ) {
3948
4007
  let imageID = ObjectIdentifier(imageView)
@@ -3957,6 +4016,17 @@ final class NativeListCell: UICollectionViewCell {
3957
4016
  } else {
3958
4017
  headersJson = nil
3959
4018
  }
4019
+ if hideUntilLoaded { imageView.isHidden = true }
4020
+ let handleLoad: (() -> Void)? = hideUntilLoaded ? { [weak self, weak imageView] in
4021
+ guard let self, self.bindingEpoch == expectedEpoch else { return }
4022
+ imageView?.isHidden = false
4023
+ onLoad?()
4024
+ } : onLoad
4025
+ let handleError: (() -> Void)? = hideUntilLoaded ? { [weak self, weak imageView] in
4026
+ guard let self, self.bindingEpoch == expectedEpoch else { return }
4027
+ imageView?.isHidden = true
4028
+ onError?()
4029
+ } : onError
3960
4030
  imageView.configure(
3961
4031
  sourceUri: source.string("uri").trimmingCharacters(in: .whitespacesAndNewlines),
3962
4032
  sourceHeadersJson: headersJson,
@@ -3967,20 +4037,21 @@ final class NativeListCell: UICollectionViewCell {
3967
4037
  recyclingKey: retryAttempt == 0 ? "\(token):\(slot)" : "\(token):\(slot):retry:\(retryAttempt)",
3968
4038
  optimizeTos: retryAttempt == 0 && (source["optimizeTos"] == nil || source.bool("optimizeTos")),
3969
4039
  overscan: source["overscan"] == nil ? 1.1 : source.double("overscan"),
3970
- loadingStrategy: source.string("loadingStrategy", default: "static"),
3971
- onLoad: retryLimit == 0 ? onLoad : { [weak self] in
4040
+ loadingStrategy: source.string("loadingStrategy", default: "none"),
4041
+ placeholderColor: currentTheme?["strongBackground"] as? String ?? "#0000000F",
4042
+ onLoad: retryLimit == 0 ? handleLoad : { [weak self] in
3972
4043
  guard let self, self.bindingEpoch == expectedEpoch else { return }
3973
4044
  self.selectorImageRetries.removeValue(forKey: imageID)?.cancel()
3974
- onLoad?()
4045
+ handleLoad?()
3975
4046
  },
3976
- onError: retryLimit == 0 ? onError : { [weak self, weak imageView] in
4047
+ onError: retryLimit == 0 ? handleError : { [weak self, weak imageView] in
3977
4048
  guard let self, self.bindingEpoch == expectedEpoch, let imageView else { return }
3978
- guard retryAttempt < retryLimit else { onError?(); return }
4049
+ guard retryAttempt < retryLimit else { handleError?(); return }
3979
4050
  guard self.selectorImageRetries[imageID] == nil else { return }
3980
4051
  let retry = DispatchWorkItem { [weak self, weak imageView] in
3981
4052
  guard let self, self.bindingEpoch == expectedEpoch, let imageView else { return }
3982
4053
  self.selectorImageRetries.removeValue(forKey: imageID)
3983
- self.bindImage(source, into: imageView, token: token, slot: slot, variant: variant, onLoad: onLoad, onError: onError, retryAttempt: retryAttempt + 1)
4054
+ self.bindImage(source, into: imageView, token: token, slot: slot, variant: variant, onLoad: onLoad, onError: onError, hideUntilLoaded: hideUntilLoaded, retryAttempt: retryAttempt + 1)
3984
4055
  }
3985
4056
  self.selectorImageRetries[imageID] = retry
3986
4057
  DispatchQueue.main.asyncAfter(deadline: .now() + Double(Int.random(in: 0...2)), execute: retry)