@onekeyfe/react-native-native-list 3.0.135 → 3.0.137
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/NativeListModels.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +79 -5
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +19 -1
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSourceFallbackStateKeyTest.kt +43 -0
- package/ios/NativeListCell.swift +74 -3
- package/lib/module/web/NativeListWebEngine.js +6 -0
- package/package.json +3 -3
- package/src/web/NativeListWebEngine.ts +6 -0
|
@@ -2,6 +2,9 @@ package com.margelo.nitro.nativelist
|
|
|
2
2
|
|
|
3
3
|
import org.json.JSONArray
|
|
4
4
|
import org.json.JSONObject
|
|
5
|
+
import java.nio.ByteBuffer
|
|
6
|
+
import java.security.MessageDigest
|
|
7
|
+
import java.util.Locale
|
|
5
8
|
|
|
6
9
|
internal fun isNativeListRowPressEnabled(
|
|
7
10
|
type: String,
|
|
@@ -22,6 +25,28 @@ internal fun isNativeListWholeRowInteractive(
|
|
|
22
25
|
pressDisabled = pressDisabled,
|
|
23
26
|
)
|
|
24
27
|
|
|
28
|
+
// The image fallback-state cache is process-wide, so it is keyed by a digest of the request
|
|
29
|
+
// identity instead of the raw headers, which can carry credentials such as Authorization.
|
|
30
|
+
internal fun nativeListSourceFallbackStateKey(uri: String, headers: Map<String, String>): String? {
|
|
31
|
+
val trimmedUri = uri.trim().takeIf(String::isNotEmpty) ?: return null
|
|
32
|
+
val digest = MessageDigest.getInstance("SHA-256")
|
|
33
|
+
updateLengthPrefixed(digest, trimmedUri)
|
|
34
|
+
headers.entries
|
|
35
|
+
.map { it.key.lowercase(Locale.ROOT) to it.value }
|
|
36
|
+
.sortedWith(compareBy<Pair<String, String>>({ it.first }, { it.second }))
|
|
37
|
+
.forEach { (name, value) ->
|
|
38
|
+
updateLengthPrefixed(digest, name)
|
|
39
|
+
updateLengthPrefixed(digest, value)
|
|
40
|
+
}
|
|
41
|
+
return digest.digest().joinToString("") { "%02x".format(Locale.ROOT, it.toInt() and 0xff) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun updateLengthPrefixed(digest: MessageDigest, value: String) {
|
|
45
|
+
val bytes = value.toByteArray(Charsets.UTF_8)
|
|
46
|
+
digest.update(ByteBuffer.allocate(Int.SIZE_BYTES).putInt(bytes.size).array())
|
|
47
|
+
digest.update(bytes)
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
internal data class NativeListItem(
|
|
26
51
|
val key: String,
|
|
27
52
|
val type: String,
|
|
@@ -395,6 +395,30 @@ private class NativeListTableColumnView(context: android.content.Context) : Line
|
|
|
395
395
|
private fun sp(value: Float): Float = NativeListScale.font(resources, value)
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
private object NativeListSourceFallbackState {
|
|
399
|
+
private const val CACHE_LIMIT = 128
|
|
400
|
+
private val sources = LinkedHashMap<String, Unit>(CACHE_LIMIT, 0.75f, true)
|
|
401
|
+
|
|
402
|
+
fun has(key: String): Boolean = synchronized(sources) {
|
|
403
|
+
sources[key] != null
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
fun remember(key: String) {
|
|
407
|
+
synchronized(sources) {
|
|
408
|
+
sources[key] = Unit
|
|
409
|
+
while (sources.size > CACHE_LIMIT) {
|
|
410
|
+
sources.remove(sources.entries.first().key)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
fun forget(key: String) {
|
|
416
|
+
synchronized(sources) {
|
|
417
|
+
sources.remove(key)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
398
422
|
internal class NativeListRowView(
|
|
399
423
|
private val reactContext: ThemedReactContext,
|
|
400
424
|
) : LinearLayout(reactContext) {
|
|
@@ -1372,6 +1396,7 @@ internal class NativeListRowView(
|
|
|
1372
1396
|
leadingIcon.visibility = GONE
|
|
1373
1397
|
leadingIcon.iconName = ""
|
|
1374
1398
|
leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(18), dp(18), Gravity.CENTER)
|
|
1399
|
+
leadingIcon.glyphSizeDp = null
|
|
1375
1400
|
favoriteIcon.visibility = GONE
|
|
1376
1401
|
favoriteIcon.iconName = ""
|
|
1377
1402
|
headerTitleIcon.visibility = GONE
|
|
@@ -3214,13 +3239,27 @@ internal class NativeListRowView(
|
|
|
3214
3239
|
val cornerIconData = visual.optJSONObject("cornerIcon")
|
|
3215
3240
|
val fallback = visual.optString("fallbackText").take(2)
|
|
3216
3241
|
val fallbackIconData = visual.optJSONObject("fallbackIcon")
|
|
3242
|
+
val fallbackIconSizeDp = fallbackIconData?.let {
|
|
3243
|
+
val slotSizeDp = minOf(sizeDp, heightDp)
|
|
3244
|
+
if (it.optString("name") == "GlobusOutline") {
|
|
3245
|
+
(slotSizeDp * 1.2f).roundToInt()
|
|
3246
|
+
} else {
|
|
3247
|
+
slotSizeDp
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3217
3250
|
val sourceLoadingStrategy = sources.firstOrNull()?.first?.optString("loadingStrategy", "none") ?: "none"
|
|
3218
3251
|
val showsSourcePlaceholder = !isIcon && sources.isNotEmpty() && sourceLoadingStrategy != "none"
|
|
3219
3252
|
val handlesSourceFallback =
|
|
3220
3253
|
!isIcon && sources.isNotEmpty() &&
|
|
3221
3254
|
showsSourcePlaceholder &&
|
|
3222
3255
|
(visual.has("fallbackText") || fallbackIconData != null)
|
|
3223
|
-
|
|
3256
|
+
val sourceFallbackKey = if (handlesSourceFallback) {
|
|
3257
|
+
sources.firstOrNull()?.first?.let(::sourceFallbackStateKey)
|
|
3258
|
+
} else null
|
|
3259
|
+
val restoresSourceFallback =
|
|
3260
|
+
sourceFallbackKey?.let(NativeListSourceFallbackState::has) == true
|
|
3261
|
+
leadingFallback.text =
|
|
3262
|
+
if (handlesSourceFallback && !restoresSourceFallback) "" else fallback
|
|
3224
3263
|
leadingFallback.setTextColor(parseNativeListColor("#00000072"))
|
|
3225
3264
|
val visualBackground = safeColor(
|
|
3226
3265
|
visual.optString("backgroundColor"),
|
|
@@ -3239,6 +3278,23 @@ internal class NativeListRowView(
|
|
|
3239
3278
|
)
|
|
3240
3279
|
leadingFallback.visibility =
|
|
3241
3280
|
if (!isIcon && (sources.isEmpty() || handlesSourceFallback)) VISIBLE else GONE
|
|
3281
|
+
if (handlesSourceFallback && fallbackIconData != null) {
|
|
3282
|
+
leadingIcon.iconName = fallbackIconData.optString("name")
|
|
3283
|
+
leadingIcon.tintColor = safeColor(
|
|
3284
|
+
fallbackIconData.optString("tintColor"),
|
|
3285
|
+
parseNativeListColor("#0000009B"),
|
|
3286
|
+
)
|
|
3287
|
+
leadingIcon.layoutParams = FrameLayout.LayoutParams(
|
|
3288
|
+
dp(fallbackIconSizeDp ?: sizeDp),
|
|
3289
|
+
dp(fallbackIconSizeDp ?: heightDp),
|
|
3290
|
+
Gravity.CENTER,
|
|
3291
|
+
)
|
|
3292
|
+
leadingIcon.glyphSizeDp = fallbackIconSizeDp
|
|
3293
|
+
// Keep the fallback measured while the source loads so an asynchronous
|
|
3294
|
+
// failure can reveal it without waiting for another RecyclerView layout.
|
|
3295
|
+
leadingFallback.visibility = if (restoresSourceFallback) GONE else VISIBLE
|
|
3296
|
+
leadingIcon.visibility = if (restoresSourceFallback) VISIBLE else INVISIBLE
|
|
3297
|
+
}
|
|
3242
3298
|
if (isIcon) {
|
|
3243
3299
|
leadingFrame.background = GradientDrawable().apply {
|
|
3244
3300
|
setColor(visualBackground)
|
|
@@ -3258,6 +3314,12 @@ internal class NativeListRowView(
|
|
|
3258
3314
|
fallbackIconData.optString("tintColor"),
|
|
3259
3315
|
parseNativeListColor("#0000009B"),
|
|
3260
3316
|
)
|
|
3317
|
+
leadingIcon.layoutParams = FrameLayout.LayoutParams(
|
|
3318
|
+
dp(fallbackIconSizeDp ?: sizeDp),
|
|
3319
|
+
dp(fallbackIconSizeDp ?: heightDp),
|
|
3320
|
+
Gravity.CENTER,
|
|
3321
|
+
)
|
|
3322
|
+
leadingIcon.glyphSizeDp = fallbackIconSizeDp
|
|
3261
3323
|
leadingIcon.visibility = VISIBLE
|
|
3262
3324
|
}
|
|
3263
3325
|
val visibleSources = sources.take(leadingImages.size)
|
|
@@ -3356,6 +3418,7 @@ internal class NativeListRowView(
|
|
|
3356
3418
|
bindImage(source, image, boundKey ?: "", index, variant,
|
|
3357
3419
|
onLoad = if (!ownsSourceFallback) null else ({
|
|
3358
3420
|
if (bindingEpoch == expectedEpoch) {
|
|
3421
|
+
sourceFallbackKey?.let(NativeListSourceFallbackState::forget)
|
|
3359
3422
|
image.visibility = VISIBLE
|
|
3360
3423
|
leadingFallback.visibility = GONE
|
|
3361
3424
|
leadingIcon.visibility = GONE
|
|
@@ -3363,6 +3426,7 @@ internal class NativeListRowView(
|
|
|
3363
3426
|
}),
|
|
3364
3427
|
onError = if (!ownsSourceFallback) null else ({
|
|
3365
3428
|
if (bindingEpoch == expectedEpoch) {
|
|
3429
|
+
sourceFallbackKey?.let(NativeListSourceFallbackState::remember)
|
|
3366
3430
|
image.visibility = GONE
|
|
3367
3431
|
if (fallbackIcon != null) {
|
|
3368
3432
|
leadingFallback.visibility = GONE
|
|
@@ -3473,6 +3537,14 @@ internal class NativeListRowView(
|
|
|
3473
3537
|
}
|
|
3474
3538
|
}
|
|
3475
3539
|
|
|
3540
|
+
private fun sourceFallbackStateKey(source: JSONObject): String? =
|
|
3541
|
+
nativeListSourceFallbackStateKey(
|
|
3542
|
+
uri = source.optString("uri"),
|
|
3543
|
+
headers = source.optJSONObject("headers")?.let { headers ->
|
|
3544
|
+
headers.keys().asSequence().associateWith { headers.optString(it) }
|
|
3545
|
+
}.orEmpty(),
|
|
3546
|
+
)
|
|
3547
|
+
|
|
3476
3548
|
private fun leadingImageLayout(
|
|
3477
3549
|
index: Int,
|
|
3478
3550
|
count: Int,
|
|
@@ -4159,8 +4231,10 @@ internal class NativeListRowView(
|
|
|
4159
4231
|
val uri = source.optString("uri").trim().takeIf(String::isNotEmpty)
|
|
4160
4232
|
val sourceHeadersJson = source.optJSONObject("headers")?.toString()
|
|
4161
4233
|
val recyclingKey = if (retryAttempt == 0) "$token:$slot" else "$token:$slot:retry:$retryAttempt"
|
|
4162
|
-
if (hideUntilLoaded
|
|
4163
|
-
imageView.visibility =
|
|
4234
|
+
if (hideUntilLoaded) {
|
|
4235
|
+
imageView.visibility = if (
|
|
4236
|
+
imageView.isDisplaying(uri, sourceHeadersJson, recyclingKey)
|
|
4237
|
+
) VISIBLE else INVISIBLE
|
|
4164
4238
|
}
|
|
4165
4239
|
val handleLoad: (() -> Unit)? = if (hideUntilLoaded) ({
|
|
4166
4240
|
if (bindingEpoch == expectedEpoch) {
|
|
@@ -4194,8 +4268,8 @@ internal class NativeListRowView(
|
|
|
4194
4268
|
}),
|
|
4195
4269
|
onError = if (retryLimit == 0) handleError else ({
|
|
4196
4270
|
if (bindingEpoch == expectedEpoch) {
|
|
4197
|
-
|
|
4198
|
-
|
|
4271
|
+
handleError?.invoke()
|
|
4272
|
+
if (retryAttempt < retryLimit && !selectorImageRetries.containsKey(imageView)) {
|
|
4199
4273
|
val retry = Runnable {
|
|
4200
4274
|
if (bindingEpoch == expectedEpoch) {
|
|
4201
4275
|
selectorImageRetries.remove(imageView)
|
|
@@ -53,6 +53,24 @@ import kotlin.math.roundToInt
|
|
|
53
53
|
import kotlin.math.sin
|
|
54
54
|
import kotlin.math.sqrt
|
|
55
55
|
|
|
56
|
+
private class NativeListGridLayoutManager(
|
|
57
|
+
context: Context,
|
|
58
|
+
spanCount: Int,
|
|
59
|
+
) : GridLayoutManager(context, spanCount) {
|
|
60
|
+
override fun removeAndRecycleViewAt(index: Int, recycler: RecyclerView.Recycler) {
|
|
61
|
+
val child = getChildAt(index) ?: return
|
|
62
|
+
removeViewAt(index)
|
|
63
|
+
// An index-based removal can leave the selected child attached during rapid
|
|
64
|
+
// nested scrolling. Detach that exact view before giving it to RecyclerView.
|
|
65
|
+
if (child.parent != null) removeView(child)
|
|
66
|
+
if (child.parent == null) {
|
|
67
|
+
recycler.recycleView(child)
|
|
68
|
+
} else {
|
|
69
|
+
requestLayout()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
class NativeListView(
|
|
57
75
|
private val reactContext: ThemedReactContext,
|
|
58
76
|
) : LinearLayout(reactContext) {
|
|
@@ -110,7 +128,7 @@ class NativeListView(
|
|
|
110
128
|
private var refreshIndicatorOffsetPx = 0
|
|
111
129
|
private val contentContainer = FrameLayout(context)
|
|
112
130
|
private val adapter = NativeListAdapter(reactContext)
|
|
113
|
-
private val layoutManager =
|
|
131
|
+
private val layoutManager = NativeListGridLayoutManager(context, 1)
|
|
114
132
|
// OneKey patch: vertical lists retain vertical drags and leave horizontal drags to a parent pager.
|
|
115
133
|
private val pagerGestureTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
116
134
|
private var pagerGestureIsVertical = false
|
package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSourceFallbackStateKeyTest.kt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertNull
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
class NativeListSourceFallbackStateKeyTest {
|
|
10
|
+
private val uri = "https://example.com/token.png"
|
|
11
|
+
|
|
12
|
+
@Test
|
|
13
|
+
fun keyIsADigestThatDoesNotRetainHeaderValues() {
|
|
14
|
+
val key = nativeListSourceFallbackStateKey(uri, mapOf("Authorization" to "Bearer secret-token"))
|
|
15
|
+
|
|
16
|
+
assertTrue(key.orEmpty().matches(Regex("[0-9a-f]{64}")))
|
|
17
|
+
assertFalse(key.orEmpty().contains("secret-token"))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Test
|
|
21
|
+
fun keyIgnoresHeaderOrderAndNameCase() {
|
|
22
|
+
assertEquals(
|
|
23
|
+
nativeListSourceFallbackStateKey(uri, linkedMapOf("Authorization" to "Bearer a", "X-Trace" to "1")),
|
|
24
|
+
nativeListSourceFallbackStateKey(uri, linkedMapOf("x-trace" to "1", "authorization" to "Bearer a")),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test
|
|
29
|
+
fun keyDistinguishesSourcesThatDifferOnlyByHeaders() {
|
|
30
|
+
val keys = listOf(
|
|
31
|
+
nativeListSourceFallbackStateKey(uri, emptyMap()),
|
|
32
|
+
nativeListSourceFallbackStateKey(uri, mapOf("Authorization" to "Bearer a")),
|
|
33
|
+
nativeListSourceFallbackStateKey(uri, mapOf("Authorization" to "Bearer b")),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
assertEquals(keys.size, keys.toSet().size)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
fun blankUriHasNoKey() {
|
|
41
|
+
assertNull(nativeListSourceFallbackStateKey(" ", mapOf("Authorization" to "Bearer a")))
|
|
42
|
+
}
|
|
43
|
+
}
|
package/ios/NativeListCell.swift
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
// OneKey patch: preserve native font faces while enabling tabular number features.
|
|
3
3
|
import CoreText
|
|
4
|
+
import CryptoKit
|
|
4
5
|
import OneKeyImage
|
|
5
6
|
import UIKit
|
|
6
7
|
|
|
8
|
+
private enum NativeListSourceFallbackState {
|
|
9
|
+
private static let sources: NSCache<NSString, NSNumber> = {
|
|
10
|
+
let cache = NSCache<NSString, NSNumber>()
|
|
11
|
+
cache.countLimit = 128
|
|
12
|
+
return cache
|
|
13
|
+
}()
|
|
14
|
+
|
|
15
|
+
static func has(_ key: String) -> Bool {
|
|
16
|
+
sources.object(forKey: key as NSString) != nil
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static func remember(_ key: String) {
|
|
20
|
+
sources.setObject(NSNumber(value: true), forKey: key as NSString)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func forget(_ key: String) {
|
|
24
|
+
sources.removeObject(forKey: key as NSString)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
// Market/TokenListSkeleton: source geometry and the native Skeleton's 3s shimmer.
|
|
8
29
|
private final class NativeListMarketSkeleton: UIView {
|
|
9
30
|
private let marks = (0..<5).map { _ in UIView() }
|
|
@@ -3408,12 +3429,20 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3408
3429
|
let cornerIcon = visual.dictionary("cornerIcon")
|
|
3409
3430
|
let fallbackText = String(visual.string("fallbackText").prefix(2))
|
|
3410
3431
|
let fallbackIconData = visual.dictionary("fallbackIcon")
|
|
3432
|
+
let fallbackIconSize = fallbackIconData.map {
|
|
3433
|
+
let slotSize = min(leadingWidth.constant, leadingHeight.constant)
|
|
3434
|
+
return $0.string("name") == "GlobusOutline" ? slotSize * 1.2 : slotSize
|
|
3435
|
+
}
|
|
3411
3436
|
let sourceLoadingStrategy = sources.first?.data.string("loadingStrategy", default: "none") ?? "none"
|
|
3412
3437
|
let showsSourcePlaceholder = !isIcon && !sources.isEmpty && sourceLoadingStrategy != "none"
|
|
3413
3438
|
let handlesSourceFallback = !isIcon && !sources.isEmpty &&
|
|
3414
3439
|
showsSourcePlaceholder &&
|
|
3415
3440
|
(visual["fallbackText"] != nil || fallbackIconData != nil)
|
|
3416
|
-
|
|
3441
|
+
let sourceFallbackKey = handlesSourceFallback
|
|
3442
|
+
? sources.first.flatMap { sourceFallbackStateKey($0.data) }
|
|
3443
|
+
: nil
|
|
3444
|
+
let restoresSourceFallback = sourceFallbackKey.map(NativeListSourceFallbackState.has) ?? false
|
|
3445
|
+
fallbackLabel.text = handlesSourceFallback && !restoresSourceFallback ? nil : fallbackText
|
|
3417
3446
|
let sourceFallbackBackground = currentTheme?["strongBackground"] as? String ?? "#0000000F"
|
|
3418
3447
|
// OneKey patch: source-backed visuals default to no placeholder/background.
|
|
3419
3448
|
// A non-none image loadingStrategy opts back into the themed placeholder.
|
|
@@ -3451,6 +3480,20 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3451
3480
|
leadingIconImageView.image = nativeListIcon(named: fallbackIconData.string("name"))
|
|
3452
3481
|
leadingIconImageView.contentMode = .scaleAspectFit
|
|
3453
3482
|
}
|
|
3483
|
+
if handlesSourceFallback, let fallbackIconData {
|
|
3484
|
+
fallbackLabel.isHidden = true
|
|
3485
|
+
leadingIconImageView.image = nativeListIcon(named: fallbackIconData.string("name"))
|
|
3486
|
+
leadingIconImageView.tintColor = UIColor(
|
|
3487
|
+
nativeListHex: fallbackIconData.string("tintColor", default: "#646464"),
|
|
3488
|
+
fallback: .darkGray
|
|
3489
|
+
)
|
|
3490
|
+
leadingIconImageView.contentMode = .scaleAspectFit
|
|
3491
|
+
leadingIconWidth.constant = fallbackIconSize ?? leadingWidth.constant
|
|
3492
|
+
leadingIconHeight.constant = fallbackIconSize ?? leadingHeight.constant
|
|
3493
|
+
leadingIconImageView.isHidden = !restoresSourceFallback
|
|
3494
|
+
} else if handlesSourceFallback {
|
|
3495
|
+
fallbackLabel.isHidden = !restoresSourceFallback
|
|
3496
|
+
}
|
|
3454
3497
|
let visibleSources = Array(sources.prefix(leadingImages.count))
|
|
3455
3498
|
let tokenPair = kind == "token" && visibleSources.count > 1
|
|
3456
3499
|
leadingContainer.clipsToBounds = !tokenPair && cornerIcon == nil
|
|
@@ -3506,6 +3549,9 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3506
3549
|
bindImage(source.data, into: imageView, token: key, slot: index, variant: source.variant,
|
|
3507
3550
|
onLoad: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
|
|
3508
3551
|
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
3552
|
+
if let sourceFallbackKey {
|
|
3553
|
+
NativeListSourceFallbackState.forget(sourceFallbackKey)
|
|
3554
|
+
}
|
|
3509
3555
|
imageView?.isHidden = false
|
|
3510
3556
|
self.fallbackLabel.isHidden = true
|
|
3511
3557
|
self.leadingIconImageView.isHidden = true
|
|
@@ -3513,6 +3559,9 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3513
3559
|
},
|
|
3514
3560
|
onError: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
|
|
3515
3561
|
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
3562
|
+
if let sourceFallbackKey {
|
|
3563
|
+
NativeListSourceFallbackState.remember(sourceFallbackKey)
|
|
3564
|
+
}
|
|
3516
3565
|
imageView?.isHidden = true
|
|
3517
3566
|
if let fallbackIcon {
|
|
3518
3567
|
self.fallbackLabel.isHidden = true
|
|
@@ -3643,6 +3692,26 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3643
3692
|
return sources
|
|
3644
3693
|
}
|
|
3645
3694
|
|
|
3695
|
+
private func sourceFallbackStateKey(_ source: [String: Any]) -> String? {
|
|
3696
|
+
let uri = source.string("uri").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
3697
|
+
guard !uri.isEmpty else { return nil }
|
|
3698
|
+
// The fallback-state cache is process-wide, so key it by a digest of the request identity
|
|
3699
|
+
// instead of retaining raw header values, which can carry credentials such as Authorization.
|
|
3700
|
+
let headers = source.dictionary("headers") ?? [:]
|
|
3701
|
+
let fields: [(name: String, value: String)] = headers.map { key, value in
|
|
3702
|
+
(name: key.lowercased(), value: String(describing: value))
|
|
3703
|
+
}
|
|
3704
|
+
let sortedFields = fields.sorted { lhs, rhs in
|
|
3705
|
+
lhs.name == rhs.name ? lhs.value < rhs.value : lhs.name < rhs.name
|
|
3706
|
+
}
|
|
3707
|
+
var canonicalValue = "\(uri.utf8.count):\(uri)"
|
|
3708
|
+
for field in sortedFields {
|
|
3709
|
+
canonicalValue += "\(field.name.utf8.count):\(field.name)\(field.value.utf8.count):\(field.value)"
|
|
3710
|
+
}
|
|
3711
|
+
let digest = SHA256.hash(data: Data(canonicalValue.utf8))
|
|
3712
|
+
return digest.map { String(format: "%02x", $0) }.joined()
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3646
3715
|
private func leadingConstraints(
|
|
3647
3716
|
_ imageView: UIView,
|
|
3648
3717
|
index: Int,
|
|
@@ -4085,8 +4154,10 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
4085
4154
|
},
|
|
4086
4155
|
onError: retryLimit == 0 ? handleError : { [weak self, weak imageView] in
|
|
4087
4156
|
guard let self, self.bindingEpoch == expectedEpoch, let imageView else { return }
|
|
4088
|
-
|
|
4089
|
-
|
|
4157
|
+
// Report every failed attempt: a rebind during the retry delay cancels the pending retry,
|
|
4158
|
+
// so deferring to the last attempt would never record the source fallback state.
|
|
4159
|
+
handleError?()
|
|
4160
|
+
guard retryAttempt < retryLimit, self.selectorImageRetries[imageID] == nil else { return }
|
|
4090
4161
|
let retry = DispatchWorkItem { [weak self, weak imageView] in
|
|
4091
4162
|
guard let self, self.bindingEpoch == expectedEpoch, let imageView else { return }
|
|
4092
4163
|
self.selectorImageRetries.removeValue(forKey: imageID)
|
|
@@ -537,6 +537,12 @@ export const WEB_LIST_CSS = `
|
|
|
537
537
|
.ok-native-list-account-row[data-native-list-selector="accountSelector"]>.ok-native-list-accessories[data-native-list-account-control="createAddress"]{position:absolute;top:18px;right:12px}
|
|
538
538
|
.ok-native-list-account-row[data-native-list-selector="accountSelector"] .ok-native-list-accessories>[data-native-list-account-control="createAddress"]{flex-basis:36px;width:36px;height:36px;padding:6px;border-radius:8px}
|
|
539
539
|
.ok-native-list-account-action-row{padding-left:12px;padding-right:12px}.ok-native-list-account-action-row .ok-native-list-action-title{font-size:16px;line-height:24px;font-weight:400}.ok-native-list-account-action-row .ok-native-list-action-title[data-tone="primary"]{color:var(--nl-primary)}
|
|
540
|
+
/* OneKey patch: account selector wallets and accounts keep the default cursor instead of pointer or grab affordances.
|
|
541
|
+
Only the row surface is matched: cursor inherits, so plain children follow the row, while icon buttons,
|
|
542
|
+
checkboxes, and action buttons keep advertising themselves with their own pointer cursor. */
|
|
543
|
+
.ok-native-list-root .ok-native-list-item>.ok-native-list-wallet-row,.ok-native-list-root .ok-native-list-wallet-member>.ok-native-list-wallet-row,.ok-native-list-root .ok-native-list-item>.ok-native-list-account-row,.ok-native-list-root .ok-native-list-item>.ok-native-list-account-action-row,.ok-native-list-wallet-member{cursor:default}
|
|
544
|
+
/* OneKey patch: Add account hover and pressed backgrounds keep the account row corner radius. */
|
|
545
|
+
.ok-native-list-account-action-row{border-radius:12px}
|
|
540
546
|
`;
|
|
541
547
|
function createElement(document, tag, className, text) {
|
|
542
548
|
const element = document.createElement(tag);
|
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.137",
|
|
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.137",
|
|
87
|
+
"@onekeyfe/react-native-native-logger": "3.0.137",
|
|
88
88
|
"react": "*",
|
|
89
89
|
"react-native": "*",
|
|
90
90
|
"react-native-nitro-modules": "0.37.0"
|
|
@@ -965,6 +965,12 @@ export const WEB_LIST_CSS = `
|
|
|
965
965
|
.ok-native-list-account-row[data-native-list-selector="accountSelector"]>.ok-native-list-accessories[data-native-list-account-control="createAddress"]{position:absolute;top:18px;right:12px}
|
|
966
966
|
.ok-native-list-account-row[data-native-list-selector="accountSelector"] .ok-native-list-accessories>[data-native-list-account-control="createAddress"]{flex-basis:36px;width:36px;height:36px;padding:6px;border-radius:8px}
|
|
967
967
|
.ok-native-list-account-action-row{padding-left:12px;padding-right:12px}.ok-native-list-account-action-row .ok-native-list-action-title{font-size:16px;line-height:24px;font-weight:400}.ok-native-list-account-action-row .ok-native-list-action-title[data-tone="primary"]{color:var(--nl-primary)}
|
|
968
|
+
/* OneKey patch: account selector wallets and accounts keep the default cursor instead of pointer or grab affordances.
|
|
969
|
+
Only the row surface is matched: cursor inherits, so plain children follow the row, while icon buttons,
|
|
970
|
+
checkboxes, and action buttons keep advertising themselves with their own pointer cursor. */
|
|
971
|
+
.ok-native-list-root .ok-native-list-item>.ok-native-list-wallet-row,.ok-native-list-root .ok-native-list-wallet-member>.ok-native-list-wallet-row,.ok-native-list-root .ok-native-list-item>.ok-native-list-account-row,.ok-native-list-root .ok-native-list-item>.ok-native-list-account-action-row,.ok-native-list-wallet-member{cursor:default}
|
|
972
|
+
/* OneKey patch: Add account hover and pressed backgrounds keep the account row corner radius. */
|
|
973
|
+
.ok-native-list-account-action-row{border-radius:12px}
|
|
968
974
|
`;
|
|
969
975
|
|
|
970
976
|
function createElement(
|