@onekeyfe/react-native-native-list 3.0.134 → 3.0.136
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/build.gradle +1 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +81 -5
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +19 -1
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +43 -0
- package/ios/NativeListCell.swift +63 -1
- package/ios/RNCNativeListView.swift +31 -3
- package/lib/module/web/NativeListWebEngine.js +4 -0
- package/package.json +3 -3
- package/src/web/NativeListWebEngine.ts +4 -0
package/android/build.gradle
CHANGED
|
@@ -3,6 +3,25 @@ package com.margelo.nitro.nativelist
|
|
|
3
3
|
import org.json.JSONArray
|
|
4
4
|
import org.json.JSONObject
|
|
5
5
|
|
|
6
|
+
internal fun isNativeListRowPressEnabled(
|
|
7
|
+
type: String,
|
|
8
|
+
variant: String,
|
|
9
|
+
disabled: Boolean,
|
|
10
|
+
pressDisabled: Boolean,
|
|
11
|
+
): Boolean = !disabled && !pressDisabled && (type != "system" || variant == "retry")
|
|
12
|
+
|
|
13
|
+
internal fun isNativeListWholeRowInteractive(
|
|
14
|
+
type: String,
|
|
15
|
+
variant: String,
|
|
16
|
+
disabled: Boolean,
|
|
17
|
+
pressDisabled: Boolean,
|
|
18
|
+
): Boolean = type != "walletGroup" && isNativeListRowPressEnabled(
|
|
19
|
+
type = type,
|
|
20
|
+
variant = variant,
|
|
21
|
+
disabled = disabled,
|
|
22
|
+
pressDisabled = pressDisabled,
|
|
23
|
+
)
|
|
24
|
+
|
|
6
25
|
internal data class NativeListItem(
|
|
7
26
|
val key: String,
|
|
8
27
|
val type: String,
|
|
@@ -22,9 +41,20 @@ internal data class NativeListItem(
|
|
|
22
41
|
get() = !json.optBoolean("disabled", false) && type in SELECTABLE_TYPES
|
|
23
42
|
|
|
24
43
|
val isRowPressEnabled: Boolean
|
|
25
|
-
get() =
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
get() = isNativeListRowPressEnabled(
|
|
45
|
+
type = type,
|
|
46
|
+
variant = json.optString("variant"),
|
|
47
|
+
disabled = json.optBoolean("disabled", false),
|
|
48
|
+
pressDisabled = json.optBoolean("pressDisabled", false),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
val isWholeRowInteractive: Boolean
|
|
52
|
+
get() = isNativeListWholeRowInteractive(
|
|
53
|
+
type = type,
|
|
54
|
+
variant = json.optString("variant"),
|
|
55
|
+
disabled = json.optBoolean("disabled", false),
|
|
56
|
+
pressDisabled = json.optBoolean("pressDisabled", false),
|
|
57
|
+
)
|
|
28
58
|
|
|
29
59
|
val isReorderable: Boolean
|
|
30
60
|
get() {
|
|
@@ -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) {
|
|
@@ -985,6 +1009,11 @@ internal class NativeListRowView(
|
|
|
985
1009
|
"action" -> bindAction(item, theme, checkboxState)
|
|
986
1010
|
"system" -> bindSystem(item, theme)
|
|
987
1011
|
}
|
|
1012
|
+
// Keep passive rows out of accessibility and keyboard focus while allowing
|
|
1013
|
+
// their independently bound accessory controls to remain interactive.
|
|
1014
|
+
val wholeRowPressEnabled = item.isWholeRowInteractive
|
|
1015
|
+
isClickable = wholeRowPressEnabled
|
|
1016
|
+
isFocusable = wholeRowPressEnabled
|
|
988
1017
|
applySize(item)
|
|
989
1018
|
if (item.type == "system" && item.json.optString("variant") == "warning") {
|
|
990
1019
|
title.typeface = NativeListFonts.medium(context)
|
|
@@ -1367,6 +1396,7 @@ internal class NativeListRowView(
|
|
|
1367
1396
|
leadingIcon.visibility = GONE
|
|
1368
1397
|
leadingIcon.iconName = ""
|
|
1369
1398
|
leadingIcon.layoutParams = FrameLayout.LayoutParams(dp(18), dp(18), Gravity.CENTER)
|
|
1399
|
+
leadingIcon.glyphSizeDp = null
|
|
1370
1400
|
favoriteIcon.visibility = GONE
|
|
1371
1401
|
favoriteIcon.iconName = ""
|
|
1372
1402
|
headerTitleIcon.visibility = GONE
|
|
@@ -3209,13 +3239,27 @@ internal class NativeListRowView(
|
|
|
3209
3239
|
val cornerIconData = visual.optJSONObject("cornerIcon")
|
|
3210
3240
|
val fallback = visual.optString("fallbackText").take(2)
|
|
3211
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
|
+
}
|
|
3212
3250
|
val sourceLoadingStrategy = sources.firstOrNull()?.first?.optString("loadingStrategy", "none") ?: "none"
|
|
3213
3251
|
val showsSourcePlaceholder = !isIcon && sources.isNotEmpty() && sourceLoadingStrategy != "none"
|
|
3214
3252
|
val handlesSourceFallback =
|
|
3215
3253
|
!isIcon && sources.isNotEmpty() &&
|
|
3216
3254
|
showsSourcePlaceholder &&
|
|
3217
3255
|
(visual.has("fallbackText") || fallbackIconData != null)
|
|
3218
|
-
|
|
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
|
|
3219
3263
|
leadingFallback.setTextColor(parseNativeListColor("#00000072"))
|
|
3220
3264
|
val visualBackground = safeColor(
|
|
3221
3265
|
visual.optString("backgroundColor"),
|
|
@@ -3234,6 +3278,23 @@ internal class NativeListRowView(
|
|
|
3234
3278
|
)
|
|
3235
3279
|
leadingFallback.visibility =
|
|
3236
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
|
+
}
|
|
3237
3298
|
if (isIcon) {
|
|
3238
3299
|
leadingFrame.background = GradientDrawable().apply {
|
|
3239
3300
|
setColor(visualBackground)
|
|
@@ -3253,6 +3314,12 @@ internal class NativeListRowView(
|
|
|
3253
3314
|
fallbackIconData.optString("tintColor"),
|
|
3254
3315
|
parseNativeListColor("#0000009B"),
|
|
3255
3316
|
)
|
|
3317
|
+
leadingIcon.layoutParams = FrameLayout.LayoutParams(
|
|
3318
|
+
dp(fallbackIconSizeDp ?: sizeDp),
|
|
3319
|
+
dp(fallbackIconSizeDp ?: heightDp),
|
|
3320
|
+
Gravity.CENTER,
|
|
3321
|
+
)
|
|
3322
|
+
leadingIcon.glyphSizeDp = fallbackIconSizeDp
|
|
3256
3323
|
leadingIcon.visibility = VISIBLE
|
|
3257
3324
|
}
|
|
3258
3325
|
val visibleSources = sources.take(leadingImages.size)
|
|
@@ -3351,6 +3418,7 @@ internal class NativeListRowView(
|
|
|
3351
3418
|
bindImage(source, image, boundKey ?: "", index, variant,
|
|
3352
3419
|
onLoad = if (!ownsSourceFallback) null else ({
|
|
3353
3420
|
if (bindingEpoch == expectedEpoch) {
|
|
3421
|
+
sourceFallbackKey?.let(NativeListSourceFallbackState::forget)
|
|
3354
3422
|
image.visibility = VISIBLE
|
|
3355
3423
|
leadingFallback.visibility = GONE
|
|
3356
3424
|
leadingIcon.visibility = GONE
|
|
@@ -3358,6 +3426,7 @@ internal class NativeListRowView(
|
|
|
3358
3426
|
}),
|
|
3359
3427
|
onError = if (!ownsSourceFallback) null else ({
|
|
3360
3428
|
if (bindingEpoch == expectedEpoch) {
|
|
3429
|
+
sourceFallbackKey?.let(NativeListSourceFallbackState::remember)
|
|
3361
3430
|
image.visibility = GONE
|
|
3362
3431
|
if (fallbackIcon != null) {
|
|
3363
3432
|
leadingFallback.visibility = GONE
|
|
@@ -3468,6 +3537,11 @@ internal class NativeListRowView(
|
|
|
3468
3537
|
}
|
|
3469
3538
|
}
|
|
3470
3539
|
|
|
3540
|
+
private fun sourceFallbackStateKey(source: JSONObject): String? =
|
|
3541
|
+
source.optString("uri").trim().takeIf(String::isNotEmpty)?.let { uri ->
|
|
3542
|
+
"$uri\u0000${source.optJSONObject("headers")?.toString().orEmpty()}"
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3471
3545
|
private fun leadingImageLayout(
|
|
3472
3546
|
index: Int,
|
|
3473
3547
|
count: Int,
|
|
@@ -4154,8 +4228,10 @@ internal class NativeListRowView(
|
|
|
4154
4228
|
val uri = source.optString("uri").trim().takeIf(String::isNotEmpty)
|
|
4155
4229
|
val sourceHeadersJson = source.optJSONObject("headers")?.toString()
|
|
4156
4230
|
val recyclingKey = if (retryAttempt == 0) "$token:$slot" else "$token:$slot:retry:$retryAttempt"
|
|
4157
|
-
if (hideUntilLoaded
|
|
4158
|
-
imageView.visibility =
|
|
4231
|
+
if (hideUntilLoaded) {
|
|
4232
|
+
imageView.visibility = if (
|
|
4233
|
+
imageView.isDisplaying(uri, sourceHeadersJson, recyclingKey)
|
|
4234
|
+
) VISIBLE else INVISIBLE
|
|
4159
4235
|
}
|
|
4160
4236
|
val handleLoad: (() -> Unit)? = if (hideUntilLoaded) ({
|
|
4161
4237
|
if (bindingEpoch == expectedEpoch) {
|
|
@@ -4189,8 +4265,8 @@ internal class NativeListRowView(
|
|
|
4189
4265
|
}),
|
|
4190
4266
|
onError = if (retryLimit == 0) handleError else ({
|
|
4191
4267
|
if (bindingEpoch == expectedEpoch) {
|
|
4192
|
-
|
|
4193
|
-
|
|
4268
|
+
handleError?.invoke()
|
|
4269
|
+
if (retryAttempt < retryLimit && !selectorImageRetries.containsKey(imageView)) {
|
|
4194
4270
|
val retry = Runnable {
|
|
4195
4271
|
if (bindingEpoch == expectedEpoch) {
|
|
4196
4272
|
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
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.margelo.nitro.nativelist
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class NativeListPressPolicyTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun wholeRowInteractionMatchesPassiveAndAccessoryOnlyRows() {
|
|
9
|
+
data class Case(
|
|
10
|
+
val type: String,
|
|
11
|
+
val variant: String = "",
|
|
12
|
+
val disabled: Boolean = false,
|
|
13
|
+
val pressDisabled: Boolean = false,
|
|
14
|
+
val expected: Boolean,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
val cases = listOf(
|
|
18
|
+
Case(type = "identity", expected = true),
|
|
19
|
+
Case(type = "identity", pressDisabled = true, expected = false),
|
|
20
|
+
Case(type = "identity", disabled = true, expected = false),
|
|
21
|
+
Case(type = "system", variant = "retry", expected = true),
|
|
22
|
+
Case(type = "system", variant = "retry", pressDisabled = true, expected = false),
|
|
23
|
+
Case(type = "system", variant = "loading", expected = false),
|
|
24
|
+
Case(type = "system", variant = "noMatch", expected = false),
|
|
25
|
+
Case(type = "system", variant = "warning", expected = false),
|
|
26
|
+
Case(type = "system", variant = "end", expected = false),
|
|
27
|
+
Case(type = "system", variant = "spacer", expected = false),
|
|
28
|
+
Case(type = "walletGroup", expected = false),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
assertEquals(
|
|
32
|
+
cases.map(Case::expected),
|
|
33
|
+
cases.map { case ->
|
|
34
|
+
isNativeListWholeRowInteractive(
|
|
35
|
+
type = case.type,
|
|
36
|
+
variant = case.variant,
|
|
37
|
+
disabled = case.disabled,
|
|
38
|
+
pressDisabled = case.pressDisabled,
|
|
39
|
+
)
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
package/ios/NativeListCell.swift
CHANGED
|
@@ -4,6 +4,26 @@ import CoreText
|
|
|
4
4
|
import OneKeyImage
|
|
5
5
|
import UIKit
|
|
6
6
|
|
|
7
|
+
private enum NativeListSourceFallbackState {
|
|
8
|
+
private static let sources: NSCache<NSString, NSNumber> = {
|
|
9
|
+
let cache = NSCache<NSString, NSNumber>()
|
|
10
|
+
cache.countLimit = 128
|
|
11
|
+
return cache
|
|
12
|
+
}()
|
|
13
|
+
|
|
14
|
+
static func has(_ key: String) -> Bool {
|
|
15
|
+
sources.object(forKey: key as NSString) != nil
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static func remember(_ key: String) {
|
|
19
|
+
sources.setObject(NSNumber(value: true), forKey: key as NSString)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static func forget(_ key: String) {
|
|
23
|
+
sources.removeObject(forKey: key as NSString)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
// Market/TokenListSkeleton: source geometry and the native Skeleton's 3s shimmer.
|
|
8
28
|
private final class NativeListMarketSkeleton: UIView {
|
|
9
29
|
private let marks = (0..<5).map { _ in UIView() }
|
|
@@ -3408,12 +3428,20 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3408
3428
|
let cornerIcon = visual.dictionary("cornerIcon")
|
|
3409
3429
|
let fallbackText = String(visual.string("fallbackText").prefix(2))
|
|
3410
3430
|
let fallbackIconData = visual.dictionary("fallbackIcon")
|
|
3431
|
+
let fallbackIconSize = fallbackIconData.map {
|
|
3432
|
+
let slotSize = min(leadingWidth.constant, leadingHeight.constant)
|
|
3433
|
+
return $0.string("name") == "GlobusOutline" ? slotSize * 1.2 : slotSize
|
|
3434
|
+
}
|
|
3411
3435
|
let sourceLoadingStrategy = sources.first?.data.string("loadingStrategy", default: "none") ?? "none"
|
|
3412
3436
|
let showsSourcePlaceholder = !isIcon && !sources.isEmpty && sourceLoadingStrategy != "none"
|
|
3413
3437
|
let handlesSourceFallback = !isIcon && !sources.isEmpty &&
|
|
3414
3438
|
showsSourcePlaceholder &&
|
|
3415
3439
|
(visual["fallbackText"] != nil || fallbackIconData != nil)
|
|
3416
|
-
|
|
3440
|
+
let sourceFallbackKey = handlesSourceFallback
|
|
3441
|
+
? sources.first.flatMap { sourceFallbackStateKey($0.data) }
|
|
3442
|
+
: nil
|
|
3443
|
+
let restoresSourceFallback = sourceFallbackKey.map(NativeListSourceFallbackState.has) ?? false
|
|
3444
|
+
fallbackLabel.text = handlesSourceFallback && !restoresSourceFallback ? nil : fallbackText
|
|
3417
3445
|
let sourceFallbackBackground = currentTheme?["strongBackground"] as? String ?? "#0000000F"
|
|
3418
3446
|
// OneKey patch: source-backed visuals default to no placeholder/background.
|
|
3419
3447
|
// A non-none image loadingStrategy opts back into the themed placeholder.
|
|
@@ -3451,6 +3479,20 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3451
3479
|
leadingIconImageView.image = nativeListIcon(named: fallbackIconData.string("name"))
|
|
3452
3480
|
leadingIconImageView.contentMode = .scaleAspectFit
|
|
3453
3481
|
}
|
|
3482
|
+
if handlesSourceFallback, let fallbackIconData {
|
|
3483
|
+
fallbackLabel.isHidden = true
|
|
3484
|
+
leadingIconImageView.image = nativeListIcon(named: fallbackIconData.string("name"))
|
|
3485
|
+
leadingIconImageView.tintColor = UIColor(
|
|
3486
|
+
nativeListHex: fallbackIconData.string("tintColor", default: "#646464"),
|
|
3487
|
+
fallback: .darkGray
|
|
3488
|
+
)
|
|
3489
|
+
leadingIconImageView.contentMode = .scaleAspectFit
|
|
3490
|
+
leadingIconWidth.constant = fallbackIconSize ?? leadingWidth.constant
|
|
3491
|
+
leadingIconHeight.constant = fallbackIconSize ?? leadingHeight.constant
|
|
3492
|
+
leadingIconImageView.isHidden = !restoresSourceFallback
|
|
3493
|
+
} else if handlesSourceFallback {
|
|
3494
|
+
fallbackLabel.isHidden = !restoresSourceFallback
|
|
3495
|
+
}
|
|
3454
3496
|
let visibleSources = Array(sources.prefix(leadingImages.count))
|
|
3455
3497
|
let tokenPair = kind == "token" && visibleSources.count > 1
|
|
3456
3498
|
leadingContainer.clipsToBounds = !tokenPair && cornerIcon == nil
|
|
@@ -3506,6 +3548,9 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3506
3548
|
bindImage(source.data, into: imageView, token: key, slot: index, variant: source.variant,
|
|
3507
3549
|
onLoad: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
|
|
3508
3550
|
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
3551
|
+
if let sourceFallbackKey {
|
|
3552
|
+
NativeListSourceFallbackState.forget(sourceFallbackKey)
|
|
3553
|
+
}
|
|
3509
3554
|
imageView?.isHidden = false
|
|
3510
3555
|
self.fallbackLabel.isHidden = true
|
|
3511
3556
|
self.leadingIconImageView.isHidden = true
|
|
@@ -3513,6 +3558,9 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3513
3558
|
},
|
|
3514
3559
|
onError: !ownsSourceFallback ? nil : { [weak self, weak imageView] in
|
|
3515
3560
|
guard let self, self.bindingEpoch == expectedEpoch else { return }
|
|
3561
|
+
if let sourceFallbackKey {
|
|
3562
|
+
NativeListSourceFallbackState.remember(sourceFallbackKey)
|
|
3563
|
+
}
|
|
3516
3564
|
imageView?.isHidden = true
|
|
3517
3565
|
if let fallbackIcon {
|
|
3518
3566
|
self.fallbackLabel.isHidden = true
|
|
@@ -3643,6 +3691,20 @@ final class NativeListCell: UICollectionViewCell {
|
|
|
3643
3691
|
return sources
|
|
3644
3692
|
}
|
|
3645
3693
|
|
|
3694
|
+
private func sourceFallbackStateKey(_ source: [String: Any]) -> String? {
|
|
3695
|
+
let uri = source.string("uri").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
3696
|
+
guard !uri.isEmpty else { return nil }
|
|
3697
|
+
let headersJson: String
|
|
3698
|
+
if let headers = source.dictionary("headers"),
|
|
3699
|
+
JSONSerialization.isValidJSONObject(headers),
|
|
3700
|
+
let data = try? JSONSerialization.data(withJSONObject: headers, options: [.sortedKeys]) {
|
|
3701
|
+
headersJson = String(data: data, encoding: .utf8) ?? ""
|
|
3702
|
+
} else {
|
|
3703
|
+
headersJson = ""
|
|
3704
|
+
}
|
|
3705
|
+
return "\(uri)\u{0}\(headersJson)"
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3646
3708
|
private func leadingConstraints(
|
|
3647
3709
|
_ imageView: UIView,
|
|
3648
3710
|
index: Int,
|
|
@@ -115,6 +115,7 @@ final class NativeListView: UIView {
|
|
|
115
115
|
private let reorderStartFeedback = UIImpactFeedbackGenerator(style: .medium)
|
|
116
116
|
private let reorderMoveFeedback = UISelectionFeedbackGenerator()
|
|
117
117
|
private var interactiveReorderFeedbackIndex: Int?
|
|
118
|
+
private var interactiveMovementKeys: [String]?
|
|
118
119
|
private var actionAnchor: ActionAnchorRecord?
|
|
119
120
|
private let actionAnchorInstanceID = UUID().uuidString
|
|
120
121
|
private var actionAnchorCounter = 0
|
|
@@ -731,6 +732,9 @@ final class NativeListView: UIView {
|
|
|
731
732
|
collectionView.layoutIfNeeded()
|
|
732
733
|
return
|
|
733
734
|
}
|
|
735
|
+
if !interactiveReorderUsesAtomicTargeting {
|
|
736
|
+
interactiveMovementKeys = dataSource.snapshot().itemIdentifiers
|
|
737
|
+
}
|
|
734
738
|
interactiveReorderFeedbackIndex = indexPath.item
|
|
735
739
|
reorderStartFeedback.impactOccurred(intensity: 0.7)
|
|
736
740
|
reorderStartFeedback.prepare()
|
|
@@ -838,9 +842,11 @@ final class NativeListView: UIView {
|
|
|
838
842
|
interactiveReorderAnimator?.stopAnimation(true)
|
|
839
843
|
interactiveReorderFeedbackIndex = nil
|
|
840
844
|
if cancelled {
|
|
845
|
+
interactiveMovementKeys = nil
|
|
841
846
|
collectionView.cancelInteractiveMovement()
|
|
842
847
|
} else {
|
|
843
848
|
collectionView.endInteractiveMovement()
|
|
849
|
+
interactiveMovementKeys = nil
|
|
844
850
|
}
|
|
845
851
|
let animator = UIViewPropertyAnimator(
|
|
846
852
|
duration: ReorderAnimation.duration,
|
|
@@ -874,6 +880,7 @@ final class NativeListView: UIView {
|
|
|
874
880
|
guard interactiveReorderSource != nil else { return }
|
|
875
881
|
interactiveReorderAnimator?.stopAnimation(true)
|
|
876
882
|
interactiveReorderAnimator = nil
|
|
883
|
+
interactiveMovementKeys = nil
|
|
877
884
|
collectionView.cancelInteractiveMovement()
|
|
878
885
|
interactiveReorderCell?.setPressed(false)
|
|
879
886
|
if interactiveReorderCompactKey != nil {
|
|
@@ -978,6 +985,23 @@ final class NativeListView: UIView {
|
|
|
978
985
|
return collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? NativeListCell
|
|
979
986
|
}
|
|
980
987
|
|
|
988
|
+
// OneKey patch: flow layout sizes an interactive move in UIKit's in-flight order,
|
|
989
|
+
// while the diffable snapshot and cell index paths keep the pre-drag order until
|
|
990
|
+
// the move commits. Resolving sizes from the snapshot swaps row heights.
|
|
991
|
+
private func layoutItem(at indexPath: IndexPath) -> NativeListItem? {
|
|
992
|
+
guard let keys = interactiveMovementKeys else { return item(at: indexPath) }
|
|
993
|
+
return keys[safe: indexPath.item].flatMap { itemsByKey[$0] }
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private func updateInteractiveMovementKeys(movingFrom source: IndexPath, to target: IndexPath) {
|
|
997
|
+
guard interactiveMovementKeys != nil else { return }
|
|
998
|
+
var keys = dataSource.snapshot().itemIdentifiers
|
|
999
|
+
guard keys.indices.contains(source.item) else { return }
|
|
1000
|
+
let key = keys.remove(at: source.item)
|
|
1001
|
+
keys.insert(key, at: min(max(target.item, 0), keys.count))
|
|
1002
|
+
interactiveMovementKeys = keys
|
|
1003
|
+
}
|
|
1004
|
+
|
|
981
1005
|
private func item(at indexPath: IndexPath) -> NativeListItem? {
|
|
982
1006
|
if let key = dataSource.itemIdentifier(for: indexPath), let item = itemsByKey[key] {
|
|
983
1007
|
return item
|
|
@@ -2049,7 +2073,9 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
|
|
|
2049
2073
|
atCurrentIndexPath currentIndexPath: IndexPath,
|
|
2050
2074
|
toProposedIndexPath proposedIndexPath: IndexPath
|
|
2051
2075
|
) -> IndexPath {
|
|
2052
|
-
interactiveReorderUsesAtomicTargeting
|
|
2076
|
+
guard !interactiveReorderUsesAtomicTargeting else { return currentIndexPath }
|
|
2077
|
+
updateInteractiveMovementKeys(movingFrom: originalIndexPath, to: proposedIndexPath)
|
|
2078
|
+
return proposedIndexPath
|
|
2053
2079
|
}
|
|
2054
2080
|
|
|
2055
2081
|
func collectionView(
|
|
@@ -2057,7 +2083,9 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
|
|
|
2057
2083
|
targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath,
|
|
2058
2084
|
toProposedIndexPath proposedIndexPath: IndexPath
|
|
2059
2085
|
) -> IndexPath {
|
|
2060
|
-
interactiveReorderUsesAtomicTargeting
|
|
2086
|
+
guard !interactiveReorderUsesAtomicTargeting else { return originalIndexPath }
|
|
2087
|
+
updateInteractiveMovementKeys(movingFrom: originalIndexPath, to: proposedIndexPath)
|
|
2088
|
+
return proposedIndexPath
|
|
2061
2089
|
}
|
|
2062
2090
|
|
|
2063
2091
|
func collectionView(
|
|
@@ -2065,7 +2093,7 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
|
|
|
2065
2093
|
layout collectionViewLayout: UICollectionViewLayout,
|
|
2066
2094
|
sizeForItemAt indexPath: IndexPath
|
|
2067
2095
|
) -> CGSize {
|
|
2068
|
-
guard let config, let item =
|
|
2096
|
+
guard let config, let item = layoutItem(at: indexPath) else { return .zero }
|
|
2069
2097
|
let insets = flowLayout.sectionInset
|
|
2070
2098
|
if config.orientation == "horizontal" {
|
|
2071
2099
|
let width: CGFloat = item.type == "rail" ? railWidth(item) : item.type == "mediaTile" ? 200 : 280
|
|
@@ -537,6 +537,10 @@ 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
|
+
.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,.ok-native-list-wallet-row *,.ok-native-list-account-row *,.ok-native-list-account-action-row *{cursor:default}
|
|
542
|
+
/* OneKey patch: Add account hover and pressed backgrounds keep the account row corner radius. */
|
|
543
|
+
.ok-native-list-account-action-row{border-radius:12px}
|
|
540
544
|
`;
|
|
541
545
|
function createElement(document, tag, className, text) {
|
|
542
546
|
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.136",
|
|
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.136",
|
|
87
|
+
"@onekeyfe/react-native-native-logger": "3.0.136",
|
|
88
88
|
"react": "*",
|
|
89
89
|
"react-native": "*",
|
|
90
90
|
"react-native-nitro-modules": "0.37.0"
|
|
@@ -965,6 +965,10 @@ 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
|
+
.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,.ok-native-list-wallet-row *,.ok-native-list-account-row *,.ok-native-list-account-action-row *{cursor:default}
|
|
970
|
+
/* OneKey patch: Add account hover and pressed backgrounds keep the account row corner radius. */
|
|
971
|
+
.ok-native-list-account-action-row{border-radius:12px}
|
|
968
972
|
`;
|
|
969
973
|
|
|
970
974
|
function createElement(
|