@onekeyfe/react-native-native-list 6.7.0-alpha.397 → 6.7.0-alpha.399

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.
Files changed (40) hide show
  1. package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +23 -0
  2. package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +43 -15
  3. package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +88 -19
  4. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +94 -1
  5. package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +29 -0
  6. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListActivityHostClipPolicyTest.kt +29 -0
  7. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListIdentityTrailingAxisTest.kt +36 -0
  8. package/android/src/test/java/com/margelo/nitro/nativelist/NativeListLeadingSlotFrameTest.kt +68 -0
  9. package/docs/ROW_TEMPLATES.md +4 -0
  10. package/docs/SPEC.md +180 -37
  11. package/ios/NativeListDesignAssets.swift +6 -0
  12. package/ios/NativeListIdentityRenderer.swift +76 -4
  13. package/ios/NativeListLeadingVisual.swift +70 -8
  14. package/ios/RNCNativeListView.swift +6 -1
  15. package/ios/Resources/NativeListIcons.xcassets/onekey_document2.imageset/Contents.json +1 -0
  16. package/ios/Resources/NativeListIcons.xcassets/onekey_document2.imageset/icon.svg +1 -0
  17. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_ver_solid.imageset/Contents.json +1 -0
  18. package/ios/Resources/NativeListIcons.xcassets/onekey_dot_ver_solid.imageset/icon.svg +1 -0
  19. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_large.imageset/Contents.json +1 -0
  20. package/ios/Resources/NativeListIcons.xcassets/onekey_plus_large.imageset/icon.svg +1 -0
  21. package/ios/Resources/NativeListIcons.xcassets/onekey_ticket.imageset/Contents.json +1 -0
  22. package/ios/Resources/NativeListIcons.xcassets/onekey_ticket.imageset/icon.svg +1 -0
  23. package/ios/Resources/NativeListIcons.xcassets/onekey_wallet.imageset/Contents.json +1 -0
  24. package/ios/Resources/NativeListIcons.xcassets/onekey_wallet.imageset/icon.svg +1 -0
  25. package/ios/tests/NativeListIdentityTitleIconLayoutTests.swift +81 -0
  26. package/ios/tests/NativeListLeadingVisualPairBackdropTests.swift +50 -0
  27. package/ios/tests/NativeListLifecycleTests.swift +60 -0
  28. package/lib/module/NativeList.web.js +9 -12
  29. package/lib/module/models.js +2 -0
  30. package/lib/module/validation.js +627 -114
  31. package/lib/module/web/NativeListWebEngine.js +49 -9
  32. package/lib/module/web/templates/IdentityRowRenderer.js +58 -4
  33. package/lib/typescript/src/models.d.ts +25 -14
  34. package/lib/typescript/src/validation.d.ts +7 -2
  35. package/package.json +3 -3
  36. package/src/NativeList.web.tsx +10 -14
  37. package/src/models.ts +144 -153
  38. package/src/validation.ts +1617 -167
  39. package/src/web/NativeListWebEngine.ts +62 -10
  40. package/src/web/templates/IdentityRowRenderer.ts +70 -4
@@ -0,0 +1 @@
1
+ {"images":[{"filename":"icon.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000"><path d="M13 11h8v2h-8v8h-2v-8H3v-2h8V3h2z"/></svg>
@@ -0,0 +1 @@
1
+ {"images":[{"filename":"icon.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000"><path d="M16 17.01h-2V15h2zm0-4h-2V11h2zm0-4h-2V7h2z"/><path fill-rule="evenodd" clip-rule="evenodd" d="m22 9.927-.629.252c-1.661.665-1.661 2.977 0 3.642l.629.252V20H2v-5.927l.629-.252c1.661-.665 1.661-2.977 0-3.642L2 9.927V4h20zM4 8.636c2.5 1.52 2.499 5.207 0 6.727V18h16v-2.637c-2.499-1.52-2.5-5.208 0-6.727V6H4z"/></svg>
@@ -0,0 +1 @@
1
+ {"images":[{"filename":"icon.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000"><path d="M15.5 13.25a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M17 8h4v13H7a4 4 0 0 1-4-4V6.5A3.5 3.5 0 0 1 6.5 3H17zM5 17a2 2 0 0 0 2 2h12v-9H6.5c-.537 0-1.045-.12-1.5-.337zM6.5 5a1.5 1.5 0 1 0 0 3H15V5z"/></svg>
@@ -0,0 +1,81 @@
1
+ import UIKit
2
+ import XCTest
3
+ @testable import NativeListModule
4
+
5
+ @MainActor
6
+ final class NativeListIdentityTitleIconLayoutTests: XCTestCase {
7
+ private func child<T>(_ cell: NativeListIdentityCell, _ label: String, as type: T.Type) throws -> T {
8
+ try XCTUnwrap(Mirror(reflecting: cell).children.first { $0.label == label }?.value as? T)
9
+ }
10
+
11
+ // OK-66041: the Home native-token row carries a GasSolid `titleIcon`. The
12
+ // glyph must sit right after the title text (4pt gap), as Android's
13
+ // PackedTitleLineLayout and the Web flex row already do. iOS let the title
14
+ // label stretch across the whole title row, so the icon landed next to the
15
+ // trailing value column instead of beside "ETH".
16
+ func testTitleIconFollowsTheTitleTextInsteadOfTheRowEnd() throws {
17
+ let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 390, height: 60))
18
+ let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 200))
19
+ window.addSubview(cell)
20
+ let titleIcon = try child(cell, "titleIconView", as: UIImageView.self)
21
+ let titleLabel = try child(cell, "titleLabel", as: UILabel.self)
22
+ let titleStack = try child(cell, "titleRowStack", as: UIStackView.self)
23
+ let item = try NativeListItem(data: [
24
+ "key": "eth", "type": "identity", "title": "ETH", "height": 60,
25
+ "leading": ["kind": "token", "fallbackIcon": ["name": "CryptoCoinOutline"]],
26
+ "titleIcon": ["name": "GasSolid"],
27
+ "subtitleSegments": [
28
+ ["text": "$2,330.23", "tone": "secondary"],
29
+ ["text": "+0.23%", "tone": "positive", "separatorBefore": true],
30
+ ],
31
+ "trailing": [["kind": "valuePair", "primary": "0.00001417", "secondary": "$0.03"]],
32
+ ])
33
+ cell.bind(item: item, theme: nil, layout: "linear", selected: false,
34
+ checkboxState: { _, _, fallback in fallback }, fonts: NativeListFontFamilies())
35
+ cell.layoutIfNeeded()
36
+
37
+ XCTAssertFalse(titleIcon.isHidden)
38
+ // The title keeps its intrinsic width; the slack goes to the end of the row.
39
+ XCTAssertEqual(
40
+ titleLabel.bounds.width, titleLabel.intrinsicContentSize.width, accuracy: 1,
41
+ "title label must not stretch across the title row")
42
+ let labelFrame = titleLabel.convert(titleLabel.bounds, to: cell)
43
+ let iconFrame = titleIcon.convert(titleIcon.bounds, to: cell)
44
+ XCTAssertEqual(iconFrame.minX, labelFrame.maxX + 4, accuracy: 1,
45
+ "gas icon must follow the title text with the 4pt gap")
46
+ let iconInRow = titleIcon.convert(titleIcon.bounds, to: titleStack)
47
+ XCTAssertLessThan(iconInRow.maxX, titleStack.bounds.width / 2,
48
+ "gas icon must stay in the leading half of the title row, not at the value column")
49
+ }
50
+
51
+ // A long title still truncates in front of the icon instead of pushing it
52
+ // out of the row, and the badge keeps following the icon.
53
+ func testLongTitleTruncatesAndKeepsIconAndBadgeAdjacent() throws {
54
+ let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 320, height: 60))
55
+ let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
56
+ window.addSubview(cell)
57
+ let titleIcon = try child(cell, "titleIconView", as: UIImageView.self)
58
+ let titleLabel = try child(cell, "titleLabel", as: UILabel.self)
59
+ let badge = try child(cell, "badgeLabel", as: UILabel.self)
60
+ let item = try NativeListItem(data: [
61
+ "key": "long", "type": "identity",
62
+ "title": "A very long token name that cannot possibly fit on one line", "height": 60,
63
+ "leading": ["kind": "token", "fallbackIcon": ["name": "CryptoCoinOutline"]],
64
+ "titleIcon": ["name": "GasSolid"],
65
+ "badges": [["key": "native", "text": "Native"]],
66
+ "trailing": [["kind": "valuePair", "primary": "1.00", "secondary": "$1.00"]],
67
+ ])
68
+ cell.bind(item: item, theme: nil, layout: "linear", selected: false,
69
+ checkboxState: { _, _, fallback in fallback }, fonts: NativeListFontFamilies())
70
+ cell.layoutIfNeeded()
71
+
72
+ let labelFrame = titleLabel.convert(titleLabel.bounds, to: cell)
73
+ let iconFrame = titleIcon.convert(titleIcon.bounds, to: cell)
74
+ let badgeFrame = badge.convert(badge.bounds, to: cell)
75
+ XCTAssertEqual(titleIcon.bounds.width, 20, accuracy: 0.5)
76
+ XCTAssertEqual(iconFrame.minX, labelFrame.maxX + 4, accuracy: 1)
77
+ XCTAssertFalse(badge.isHidden)
78
+ XCTAssertGreaterThan(badgeFrame.minX, iconFrame.maxX)
79
+ XCTAssertLessThanOrEqual(badgeFrame.maxX, cell.bounds.width)
80
+ }
81
+ }
@@ -0,0 +1,50 @@
1
+ import UIKit
2
+ import XCTest
3
+ @testable import NativeListModule
4
+
5
+ @MainActor
6
+ final class NativeListLeadingVisualPairBackdropTests: XCTestCase {
7
+ private func child<T>(_ visual: NativeListLeadingVisual, _ label: String, as type: T.Type) throws -> T {
8
+ try XCTUnwrap(Mirror(reflecting: visual).children.first { $0.label == label }?.value as? T)
9
+ }
10
+
11
+ private func token(_ uri: String) -> [String: Any] {
12
+ [
13
+ "kind": "token", "shape": "circle",
14
+ "image": ["uri": uri, "width": 40, "height": 40],
15
+ "fallbackIcon": ["name": "CryptoCoinOutline"],
16
+ ]
17
+ }
18
+
19
+ // PR #185 review: the pair ring sits above the primary token. While the
20
+ // secondary image has not loaded (or failed), its slot is hidden, so the ring
21
+ // must be hidden too; otherwise a blank row-background disc covers the
22
+ // primary's lower-right corner.
23
+ func testPairRingStaysHiddenWhileTheSecondaryImageIsNotDisplayed() throws {
24
+ let visual = NativeListLeadingVisual(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
25
+ let ring = try child(visual, "pairBackdrop", as: UIView.self)
26
+ visual.bind(
27
+ token("https://example.invalid/primary.png"), style: [:], key: "pair", theme: nil,
28
+ isUnread: false, secondaryVisual: token("https://example.invalid/secondary.png"),
29
+ fonts: NativeListFontFamilies())
30
+ visual.layoutIfNeeded()
31
+ // `slots` is a value-type array: read it after bind has created the slots.
32
+ let slots = try child(visual, "slots", as: [NativeListImageSlot].self)
33
+ XCTAssertEqual(slots.count, 2)
34
+ XCTAssertTrue(slots[1].view.isHidden, "secondary slot is hidden until its image loads")
35
+ XCTAssertTrue(ring.isHidden, "ring must follow the hidden secondary slot")
36
+
37
+ visual.recycle()
38
+ XCTAssertTrue(ring.isHidden)
39
+ }
40
+
41
+ func testNoPairMeansNoRing() throws {
42
+ let visual = NativeListLeadingVisual(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
43
+ let ring = try child(visual, "pairBackdrop", as: UIView.self)
44
+ visual.bind(
45
+ token("https://example.invalid/primary.png"), style: [:], key: "single", theme: nil,
46
+ isUnread: false, fonts: NativeListFontFamilies())
47
+ visual.layoutIfNeeded()
48
+ XCTAssertTrue(ring.isHidden)
49
+ }
50
+ }
@@ -4,6 +4,25 @@ import XCTest
4
4
 
5
5
  @MainActor
6
6
  final class NativeListLifecycleTests: XCTestCase {
7
+ func testCanonicalNullPatchRemovesStoredOptionalFields() throws {
8
+ let view = NativeListView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
9
+ let row: [String: Any] = ["key": "token", "type": "identity", "title": "Token",
10
+ "leading": ["kind": "token"], "subtitle": "Old",
11
+ "titleImage": ["image": ["uri": "data:image/png;base64,AA==", "width": 14, "height": 14]]]
12
+ let snapshot: [String: Any] = ["schemaVersion": 1, "generation": 1,
13
+ "layout": ["kind": "linear"], "rows": [row]]
14
+ view.applySnapshotJson(String(decoding: try JSONSerialization.data(withJSONObject: snapshot), as: UTF8.self))
15
+ let patches: [[String: Any]] = [["key": "token", "type": "identity",
16
+ "changes": ["title": "New", "subtitle": NSNull(), "titleImage": NSNull()]]]
17
+ view.applyPatchesJson(String(decoding: try JSONSerialization.data(withJSONObject: patches), as: UTF8.self))
18
+ let items = try XCTUnwrap(Mirror(reflecting: view).descendant("itemsByKey") as? [String: NativeListItem])
19
+ let accepted = try XCTUnwrap(items["token"])
20
+ XCTAssertEqual(accepted.data.string("title"), "New")
21
+ XCTAssertNil(accepted.data["subtitle"])
22
+ XCTAssertNil(accepted.data["titleImage"])
23
+ XCTAssertEqual(accepted.key, "token")
24
+ }
25
+
7
26
  func testAcceptedRowIndexReusesStableShapeAndRejectsLateInvalidPatch() throws {
8
27
  let view = NativeListView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
9
28
  func snapshot(_ keys: [String], generation: Int, title: String = "Old") throws -> String {
@@ -229,6 +248,47 @@ final class NativeListLifecycleTests: XCTestCase {
229
248
  XCTAssertNotEqual(cell.bindingEpoch, changedEpoch)
230
249
  }
231
250
 
251
+ func testIdentityTitleImageRetainsRequestAndClearsOnProductionCellReuse() throws {
252
+ let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 320, height: 60))
253
+ let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
254
+ window.addSubview(cell)
255
+ func bind(_ descriptor: [String: Any]?, title: String = "STOCK") throws {
256
+ var data: [String: Any] = ["key": "stock", "type": "identity", "title": title,
257
+ "height": 60, "leading": ["kind": "token", "fallbackIcon": ["name": "CryptoCoinOutline"]]]
258
+ if let descriptor { data["titleImage"] = descriptor }
259
+ cell.bind(item: try NativeListItem(data: data), theme: nil, layout: "linear", selected: false,
260
+ checkboxState: { _, _, fallback in fallback }, fonts: NativeListFontFamilies())
261
+ cell.layoutIfNeeded()
262
+ }
263
+ let descriptor: [String: Any] = ["image": ["uri": "data:image/png;base64,AA==", "width": 14, "height": 14],
264
+ "accessibilityLabel": "Issuer", "hoverActionKey": "issuer-info"]
265
+ try bind(descriptor)
266
+ let slot = try XCTUnwrap(Mirror(reflecting: cell).children.first {
267
+ $0.label == "titleImageSlot"
268
+ }?.value as? NativeListImageSlot)
269
+ XCTAssertFalse(slot.view.isHidden)
270
+ XCTAssertEqual(slot.view.bounds.width, 14, accuracy: 0.5)
271
+ XCTAssertEqual(slot.view.bounds.height, 14, accuracy: 0.5)
272
+ XCTAssertEqual(slot.view.layer.cornerRadius, 7, accuracy: 0.1)
273
+ XCTAssertEqual(slot.view.accessibilityLabel, "Issuer")
274
+ let identity = Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String
275
+ XCTAssertNotNil(identity)
276
+ try bind(descriptor, title: "Changed title")
277
+ XCTAssertEqual(Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String, identity)
278
+ try bind(["image": ["uri": "data:image/png;base64,AQ==", "width": 20, "height": 20]])
279
+ XCTAssertEqual(slot.view.bounds.width, 20, accuracy: 0.5)
280
+ XCTAssertEqual(slot.view.layer.cornerRadius, 10, accuracy: 0.1)
281
+ XCTAssertNil(slot.view.accessibilityLabel)
282
+ XCTAssertNotEqual(Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String, identity)
283
+ try bind(nil)
284
+ XCTAssertTrue(slot.view.isHidden)
285
+ XCTAssertNil(Mirror(reflecting: slot).children.first { $0.label == "identity" }?.value as? String)
286
+ try bind(descriptor)
287
+ cell.prepareForReuse()
288
+ XCTAssertTrue(slot.view.isHidden)
289
+ XCTAssertNil(slot.view.accessibilityLabel)
290
+ }
291
+
232
292
  func testRiskIdentityTitleIconAndButtonResetOnProductionCellBind() throws {
233
293
  let cell = NativeListIdentityCell(frame: CGRect(x: 0, y: 0, width: 320, height: 56))
234
294
  let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
@@ -4,7 +4,7 @@ import { useNativeListFontFamiliesJson } from "./fonts.js";
4
4
  import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
5
5
  import { View } from 'react-native';
6
6
  import { normalizeIndexScroll, normalizeKeyScroll, normalizePositionScroll, resolveLocationIndex, scrollFailure, validateOffset } from "./scrolling.js";
7
- import { applyRowPatches, containNativeListInput, emptyNativeListSnapshot, validatePatches, validateSnapshot } from "./validation.js";
7
+ import { applyRowPatches, containNativeListInput, emptyNativeListSnapshot, validateSnapshot } from "./validation.js";
8
8
  import { NativeListWebEngine } from "./web/NativeListWebEngine.js";
9
9
  import { acquireNativeListAvatar, canonicalNativeListAvatarUri } from "./web/NativeListWebAvatarCache.js";
10
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -255,18 +255,15 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
255
255
  synchronizeSlots(next);
256
256
  },
257
257
  applyPatches(patches) {
258
- const accepted = containNativeListInput(() => validatePatches(patches));
259
- if (accepted && accepted.length > 0) {
260
- const engine = engineRef.current;
261
- if (engine) {
262
- engine.applyPatches(accepted);
263
- snapshotRef.current = engine.currentSnapshot();
264
- } else {
265
- const next = containNativeListInput(() => applyRowPatches(snapshotRef.current, accepted));
266
- if (next) snapshotRef.current = next;
267
- }
268
- acceptedSnapshotRef.current = snapshotRef.current;
258
+ const engine = engineRef.current;
259
+ if (engine) {
260
+ engine.applyPatches(patches);
261
+ snapshotRef.current = engine.currentSnapshot();
262
+ } else {
263
+ const next = containNativeListInput(() => applyRowPatches(snapshotRef.current, patches));
264
+ if (next) snapshotRef.current = next;
269
265
  }
266
+ acceptedSnapshotRef.current = snapshotRef.current;
270
267
  },
271
268
  reconcileSelection(selectedKeys) {
272
269
  engineRef.current?.reconcileSelection(selectedKeys);
@@ -54,4 +54,6 @@ export const ROW_BOX_STYLE_KEYS_BY_TYPE = {
54
54
  // type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator';
55
55
  // OneKey patch: balance patches also refresh the existing row's spoken content.
56
56
  // type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator' | 'height' | 'heightRounding' | 'opacity' | 'pressDisabled';
57
+
58
+ /** Null removes optional fields; undefined leaves the existing value unchanged. */
57
59
  //# sourceMappingURL=models.js.map