@onekeyfe/react-native-native-list 3.0.157-alpha.249 → 3.0.157-alpha.251
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 +21 -5
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
- package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
- package/docs/DESIGN.md +518 -9
- package/docs/MIGRATION_BASELINE.md +117 -0
- package/docs/ROW_TEMPLATES.md +359 -0
- package/docs/SPEC.md +358 -0
- package/docs/STYLE_SPEC.md +978 -0
- package/docs/images/list-capabilities.svg +39 -0
- package/docs/images/row-action.svg +17 -0
- package/docs/images/row-activity.svg +24 -0
- package/docs/images/row-data.svg +24 -0
- package/docs/images/row-identity.svg +25 -0
- package/docs/images/row-market.svg +23 -0
- package/docs/images/row-media-tile.svg +26 -0
- package/docs/images/row-message.svg +20 -0
- package/docs/images/row-metric-card.svg +33 -0
- package/docs/images/row-rail.svg +18 -0
- package/docs/images/row-section-header.svg +19 -0
- package/docs/images/row-style-regions.svg +30 -0
- package/docs/images/row-system.svg +30 -0
- package/docs/images/row-wallet-group.svg +26 -0
- package/ios/NativeListAccessoryStack.swift +680 -0
- package/ios/NativeListActionRenderer.swift +108 -0
- package/ios/NativeListActivityRenderer.swift +179 -0
- package/ios/NativeListDataRowRenderer.swift +444 -0
- package/ios/NativeListIdentityRenderer.swift +685 -0
- package/ios/NativeListImageSlot.swift +140 -0
- package/ios/NativeListLeadingVisual.swift +372 -0
- package/ios/NativeListMarketRenderer.swift +698 -0
- package/ios/NativeListMediaTileRenderer.swift +184 -0
- package/ios/NativeListMessageRenderer.swift +168 -0
- package/ios/NativeListMetricCardRenderer.swift +420 -0
- package/ios/NativeListModels.swift +32 -0
- package/ios/NativeListRailRenderer.swift +152 -0
- package/ios/NativeListRendererCell.swift +311 -0
- package/ios/NativeListRendererRegistry.swift +50 -0
- package/ios/NativeListResolvedText.swift +122 -0
- package/ios/NativeListRowHost.swift +63 -0
- package/ios/NativeListSectionHeaderRenderer.swift +708 -0
- package/ios/NativeListSystemRenderer.swift +568 -0
- package/ios/NativeListTextLabel.swift +42 -0
- package/ios/NativeListTextStyles.swift +236 -0
- package/ios/NativeListWalletGroupRenderer.swift +255 -0
- package/ios/RNCNativeListView.swift +111 -180
- package/lib/module/models.js +54 -0
- package/lib/module/validation.js +324 -26
- package/lib/module/web/NativeListWebEngine.js +148 -936
- package/lib/module/web/templates/ActionRowRenderer.js +97 -0
- package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
- package/lib/module/web/templates/DataRowRenderer.js +145 -0
- package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
- package/lib/module/web/templates/MarketRowRenderer.js +310 -0
- package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
- package/lib/module/web/templates/MessageRowRenderer.js +165 -0
- package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
- package/lib/module/web/templates/RailRowRenderer.js +98 -0
- package/lib/module/web/templates/RowContainerStyle.js +100 -0
- package/lib/module/web/templates/RowElements.js +93 -0
- package/lib/module/web/templates/RowRendererRegistry.js +70 -0
- package/lib/module/web/templates/RowText.js +121 -0
- package/lib/module/web/templates/RowVisual.js +43 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
- package/lib/module/web/templates/SystemRowRenderer.js +161 -0
- package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
- package/lib/typescript/src/models.d.ts +166 -13
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
- package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
- package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
- package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
- package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
- package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
- package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
- package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
- package/package.json +3 -3
- package/src/models.ts +281 -19
- package/src/validation.ts +501 -46
- package/src/web/NativeListWebEngine.ts +361 -1932
- package/src/web/templates/ActionRowRenderer.ts +123 -0
- package/src/web/templates/ActivityRowRenderer.ts +159 -0
- package/src/web/templates/DataRowRenderer.ts +212 -0
- package/src/web/templates/IdentityRowRenderer.ts +527 -0
- package/src/web/templates/MarketRowRenderer.ts +467 -0
- package/src/web/templates/MediaTileRowRenderer.ts +182 -0
- package/src/web/templates/MessageRowRenderer.ts +306 -0
- package/src/web/templates/MetricCardRowRenderer.ts +337 -0
- package/src/web/templates/RailRowRenderer.ts +136 -0
- package/src/web/templates/RowContainerStyle.ts +131 -0
- package/src/web/templates/RowElements.ts +160 -0
- package/src/web/templates/RowRendererRegistry.ts +111 -0
- package/src/web/templates/RowText.ts +181 -0
- package/src/web/templates/RowVisual.ts +115 -0
- package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
- package/src/web/templates/SystemRowRenderer.ts +282 -0
- package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
- package/ios/NativeListCell.swift +0 -4300
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import OneKeyImage
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
// Request lifetime is independent of row action epochs. An unchanged descriptor
|
|
6
|
+
// can outlive a text-only rebind; a new row/slot/source always invalidates it.
|
|
7
|
+
final class NativeListImageSlot {
|
|
8
|
+
let view = OneKeyImageReusableView(frame: .zero)
|
|
9
|
+
private var identity: String?
|
|
10
|
+
private var epoch = 0
|
|
11
|
+
private var retry: DispatchWorkItem?
|
|
12
|
+
private var completion: ((Bool) -> Void)?
|
|
13
|
+
private var result: Bool?
|
|
14
|
+
|
|
15
|
+
static func signature(_ value: [String: Any]) -> String {
|
|
16
|
+
guard let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys])
|
|
17
|
+
else { return "" }
|
|
18
|
+
return SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func bind(
|
|
22
|
+
_ source: [String: Any], key: String, variant: String = "generic",
|
|
23
|
+
fit: String? = nil, placeholder: String = "#0000000F", completion: ((Bool) -> Void)? = nil
|
|
24
|
+
) {
|
|
25
|
+
self.completion = completion
|
|
26
|
+
let next = Self.signature([
|
|
27
|
+
"source": source, "key": key, "variant": variant,
|
|
28
|
+
"fit": fit ?? source.string("contentFit", default: "cover"), "placeholder": placeholder,
|
|
29
|
+
])
|
|
30
|
+
if identity == next {
|
|
31
|
+
if let result { completion?(result) }
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
recycle()
|
|
35
|
+
self.completion = completion
|
|
36
|
+
identity = next
|
|
37
|
+
configure(
|
|
38
|
+
source, key: key, variant: variant, fit: fit, placeholder: placeholder, attempt: 0,
|
|
39
|
+
token: epoch)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private func configure(
|
|
43
|
+
_ source: [String: Any], key: String, variant: String, fit: String?, placeholder: String,
|
|
44
|
+
attempt: Int, token: Int
|
|
45
|
+
) {
|
|
46
|
+
guard token == self.epoch else { return }
|
|
47
|
+
var completed = false
|
|
48
|
+
let headers = source.dictionary("headers").flatMap {
|
|
49
|
+
try? JSONSerialization.data(withJSONObject: $0, options: [.sortedKeys])
|
|
50
|
+
}
|
|
51
|
+
self.view.configure(
|
|
52
|
+
sourceUri: source.string("uri").trimmingCharacters(in: .whitespacesAndNewlines),
|
|
53
|
+
sourceHeadersJson: headers.flatMap { String(data: $0, encoding: .utf8) },
|
|
54
|
+
variant: variant, contentFit: fit ?? source.string("contentFit", default: "cover"),
|
|
55
|
+
cachePolicy: source.string("cachePolicy", default: "memory-disk"),
|
|
56
|
+
autoplay: source.bool("autoplay"),
|
|
57
|
+
recyclingKey: attempt == 0 ? key : "\(key):retry:\(attempt)",
|
|
58
|
+
optimizeTos: attempt == 0 && source.bool("optimizeTos", default: true),
|
|
59
|
+
overscan: source.double("overscan", default: 1.1),
|
|
60
|
+
loadingStrategy: source.string("loadingStrategy", default: "none"),
|
|
61
|
+
placeholderColor: placeholder,
|
|
62
|
+
onLoad: { [weak self] in
|
|
63
|
+
guard let self, self.epoch == token, !completed else { return }
|
|
64
|
+
completed = true
|
|
65
|
+
self.retry?.cancel()
|
|
66
|
+
self.retry = nil
|
|
67
|
+
self.result = true
|
|
68
|
+
self.completion?(true)
|
|
69
|
+
},
|
|
70
|
+
onError: { [weak self] in
|
|
71
|
+
guard let self, self.epoch == token, !completed else { return }
|
|
72
|
+
completed = true
|
|
73
|
+
self.result = false
|
|
74
|
+
self.completion?(false)
|
|
75
|
+
guard attempt < source.int("retryTimes"), self.retry == nil else { return }
|
|
76
|
+
let work = DispatchWorkItem { [weak self] in
|
|
77
|
+
guard let self, self.epoch == token else { return }
|
|
78
|
+
self.retry = nil
|
|
79
|
+
self.configure(
|
|
80
|
+
source, key: key, variant: variant, fit: fit, placeholder: placeholder,
|
|
81
|
+
attempt: attempt + 1, token: token)
|
|
82
|
+
}
|
|
83
|
+
self.retry = work
|
|
84
|
+
DispatchQueue.main.asyncAfter(
|
|
85
|
+
deadline: .now() + Double(Int.random(in: 0...2)), execute: work)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func recycle() {
|
|
90
|
+
epoch &+= 1
|
|
91
|
+
retry?.cancel()
|
|
92
|
+
retry = nil
|
|
93
|
+
completion = nil
|
|
94
|
+
identity = nil
|
|
95
|
+
result = nil
|
|
96
|
+
view.prepareForReuse()
|
|
97
|
+
}
|
|
98
|
+
deinit { retry?.cancel() }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
enum NativeListSourceFallbackState {
|
|
102
|
+
private static let sources: NSCache<NSString, NSNumber> = {
|
|
103
|
+
let cache = NSCache<NSString, NSNumber>()
|
|
104
|
+
cache.countLimit = 128
|
|
105
|
+
return cache
|
|
106
|
+
}()
|
|
107
|
+
|
|
108
|
+
static func has(_ key: String) -> Bool {
|
|
109
|
+
sources.object(forKey: key as NSString) != nil
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static func remember(_ key: String) {
|
|
113
|
+
sources.setObject(NSNumber(value: true), forKey: key as NSString)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static func forget(_ key: String) {
|
|
117
|
+
sources.removeObject(forKey: key as NSString)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func nativeListSourceFallbackStateKey(_ source: [String: Any]) -> String? {
|
|
122
|
+
let uri = source.string("uri").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
123
|
+
guard !uri.isEmpty else { return nil }
|
|
124
|
+
// The fallback-state cache is process-wide, so key it by a digest of the request identity
|
|
125
|
+
// instead of retaining raw header values, which can carry credentials such as Authorization.
|
|
126
|
+
let headers = source.dictionary("headers") ?? [:]
|
|
127
|
+
let fields: [(name: String, value: String)] = headers.map { key, value in
|
|
128
|
+
(name: key.lowercased(), value: String(describing: value))
|
|
129
|
+
}
|
|
130
|
+
let sortedFields = fields.sorted { lhs, rhs in
|
|
131
|
+
lhs.name == rhs.name ? lhs.value < rhs.value : lhs.name < rhs.name
|
|
132
|
+
}
|
|
133
|
+
var canonicalValue = "\(uri.utf8.count):\(uri)"
|
|
134
|
+
for field in sortedFields {
|
|
135
|
+
canonicalValue +=
|
|
136
|
+
"\(field.name.utf8.count):\(field.name)\(field.value.utf8.count):\(field.value)"
|
|
137
|
+
}
|
|
138
|
+
let digest = SHA256.hash(data: Data(canonicalValue.utf8))
|
|
139
|
+
return digest.map { String(format: "%02x", $0) }.joined()
|
|
140
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
// Bounded visual primitive: three source slots, two optional overlays. It knows
|
|
4
|
+
// visual descriptors and request lifetimes, never row types or business keys.
|
|
5
|
+
final class NativeListLeadingVisual: UIView {
|
|
6
|
+
var glyphSize: CGFloat = 18
|
|
7
|
+
var bitmapBorderWidth: CGFloat = 0
|
|
8
|
+
private let fallback = NativeListTextLabel()
|
|
9
|
+
var fallbackTextView: UILabel { fallback }
|
|
10
|
+
var singleOuterMask = false
|
|
11
|
+
var alwaysClips = false
|
|
12
|
+
var dashedBorderWidth: CGFloat = 2
|
|
13
|
+
var overlayTextFontSize: CGFloat = 10
|
|
14
|
+
var overlayTextLineHeight: CGFloat?
|
|
15
|
+
var overlayTextHorizontalInset: CGFloat?
|
|
16
|
+
private let icon = UIImageView()
|
|
17
|
+
private let unread = UIView()
|
|
18
|
+
private let networkBackdrop = UIView()
|
|
19
|
+
private let corner = UIImageView()
|
|
20
|
+
private let cornerBackdrop = UIView()
|
|
21
|
+
private var slots: [NativeListImageSlot] = []
|
|
22
|
+
private var overlaySlots: [NativeListImageSlot] = []
|
|
23
|
+
private var overlays: [(UIView, [String: Any])] = []
|
|
24
|
+
private let dashedBorder = CAShapeLayer()
|
|
25
|
+
private var visual: [String: Any] = [:]
|
|
26
|
+
private var imageStyle: [String: Any] = [:]
|
|
27
|
+
private var sources: [([String: Any], String)] = []
|
|
28
|
+
|
|
29
|
+
override init(frame: CGRect) {
|
|
30
|
+
super.init(frame: frame)
|
|
31
|
+
[fallback, icon, networkBackdrop, cornerBackdrop, corner, unread].forEach(addSubview)
|
|
32
|
+
fallback.textAlignment = .center
|
|
33
|
+
fallback.font = nativeListFont(ofSize: 13, weight: .bold)
|
|
34
|
+
icon.contentMode = .scaleAspectFit
|
|
35
|
+
corner.contentMode = .scaleAspectFit
|
|
36
|
+
unread.backgroundColor = UIColor(nativeListHex: "#E5484D", fallback: .red)
|
|
37
|
+
unread.layer.cornerRadius = 4
|
|
38
|
+
networkBackdrop.layer.cornerRadius = 10
|
|
39
|
+
cornerBackdrop.layer.cornerRadius = 10
|
|
40
|
+
layer.addSublayer(dashedBorder)
|
|
41
|
+
}
|
|
42
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
43
|
+
|
|
44
|
+
func bind(
|
|
45
|
+
_ visual: [String: Any], style: [String: Any], key: String, theme: [String: Any]?,
|
|
46
|
+
isUnread: Bool, secondaryVisual: [String: Any]? = nil
|
|
47
|
+
) {
|
|
48
|
+
self.visual = visual
|
|
49
|
+
imageStyle = style
|
|
50
|
+
let kind = visual.string("kind")
|
|
51
|
+
let variant =
|
|
52
|
+
kind == "token"
|
|
53
|
+
? "token"
|
|
54
|
+
: kind == "network" ? "network" : ["account", "wallet"].contains(kind) ? "avatar" : "generic"
|
|
55
|
+
sources =
|
|
56
|
+
kind == "stackedImages"
|
|
57
|
+
? visual.dictionaries("images").prefix(3).map { ($0, "generic") }
|
|
58
|
+
: kind == "icon" ? [] : visual.dictionary("image").map { [($0, variant)] } ?? []
|
|
59
|
+
if kind == "token", !sources.isEmpty, let network = visual.dictionary("networkImage") {
|
|
60
|
+
sources.append((network, "network"))
|
|
61
|
+
}
|
|
62
|
+
if let secondaryVisual {
|
|
63
|
+
let secondaryKind = secondaryVisual.string("kind")
|
|
64
|
+
let source =
|
|
65
|
+
secondaryKind == "stackedImages"
|
|
66
|
+
? secondaryVisual.dictionaries("images").first
|
|
67
|
+
: secondaryKind == "icon" ? nil : secondaryVisual.dictionary("image")
|
|
68
|
+
if let source {
|
|
69
|
+
sources.append(
|
|
70
|
+
(
|
|
71
|
+
source,
|
|
72
|
+
secondaryKind == "token"
|
|
73
|
+
? "token"
|
|
74
|
+
: secondaryKind == "network"
|
|
75
|
+
? "network" : ["account", "wallet"].contains(secondaryKind) ? "avatar" : "generic"
|
|
76
|
+
))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
while slots.count < sources.count {
|
|
80
|
+
let slot = NativeListImageSlot()
|
|
81
|
+
slots.append(slot)
|
|
82
|
+
insertSubview(slot.view, belowSubview: slots.count == 1 ? networkBackdrop : cornerBackdrop)
|
|
83
|
+
}
|
|
84
|
+
let placeholder = theme?.string("strongBackground", default: "#0000000F") ?? "#0000000F"
|
|
85
|
+
let fallbackIcon = visual.dictionary("fallbackIcon")
|
|
86
|
+
let sourceFallback =
|
|
87
|
+
kind != "icon" && !sources.isEmpty
|
|
88
|
+
&& sources[0].0.string("loadingStrategy", default: "none") != "none"
|
|
89
|
+
&& (visual["fallbackText"] != nil || fallbackIcon != nil)
|
|
90
|
+
let fallbackKey = sourceFallback ? nativeListSourceFallbackStateKey(sources[0].0) : nil
|
|
91
|
+
let restored = fallbackKey.map(NativeListSourceFallbackState.has) ?? false
|
|
92
|
+
let fallbackData = kind == "icon" ? visual : fallbackIcon
|
|
93
|
+
fallback.text = String(visual.string("fallbackText").prefix(2))
|
|
94
|
+
fallback.textColor = UIColor(nativeListHex: "#8D8D8D", fallback: .gray)
|
|
95
|
+
icon.image = fallbackData.map { nativeListIcon(named: $0.string("name")) } ?? nil
|
|
96
|
+
icon.tintColor = UIColor(
|
|
97
|
+
nativeListHex: fallbackData?.string("tintColor", default: "#646464") ?? "#646464",
|
|
98
|
+
fallback: .darkGray)
|
|
99
|
+
fallback.isHidden = kind == "icon" || fallbackIcon != nil || !sources.isEmpty && !restored
|
|
100
|
+
icon.isHidden = !(kind == "icon" || (sources.isEmpty || restored) && fallbackIcon != nil)
|
|
101
|
+
let visualBackground = UIColor(
|
|
102
|
+
nativeListHex: visual.string(
|
|
103
|
+
"backgroundColor", default: sources.isEmpty ? placeholder : "#00000000"), fallback: .clear)
|
|
104
|
+
backgroundColor =
|
|
105
|
+
sourceFallback ? UIColor(nativeListHex: placeholder, fallback: .clear) : visualBackground
|
|
106
|
+
layer.borderWidth = kind == "icon" ? 1 / UIScreen.main.scale : 0
|
|
107
|
+
layer.borderColor = UIColor(nativeListHex: "#0000001F", fallback: .clear).cgColor
|
|
108
|
+
if bitmapBorderWidth > 0 {
|
|
109
|
+
layer.borderWidth = bitmapBorderWidth
|
|
110
|
+
layer.borderColor =
|
|
111
|
+
UIColor(nativeListHex: visual.string("borderColor"), fallback: .clear).cgColor
|
|
112
|
+
}
|
|
113
|
+
for (index, slot) in slots.enumerated() {
|
|
114
|
+
guard index < sources.count else {
|
|
115
|
+
slot.recycle()
|
|
116
|
+
slot.view.isHidden = true
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
let source = sources[index]
|
|
120
|
+
slot.view.isHidden = index > 0 || sourceFallback
|
|
121
|
+
slot.bind(
|
|
122
|
+
source.0, key: "\(key):leading:\(index)", variant: source.1,
|
|
123
|
+
fit: index == 0 ? style["contentFit"] as? String : nil, placeholder: placeholder
|
|
124
|
+
) { [weak self, weak slot] loaded in
|
|
125
|
+
slot?.view.isHidden = !loaded && (index > 0 || sourceFallback)
|
|
126
|
+
guard let self, index == 0, sourceFallback else { return }
|
|
127
|
+
if let fallbackKey {
|
|
128
|
+
if loaded {
|
|
129
|
+
NativeListSourceFallbackState.forget(fallbackKey)
|
|
130
|
+
} else {
|
|
131
|
+
NativeListSourceFallbackState.remember(fallbackKey)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
self.backgroundColor =
|
|
135
|
+
loaded ? visualBackground : UIColor(nativeListHex: placeholder, fallback: .clear)
|
|
136
|
+
self.fallback.isHidden = loaded || fallbackIcon != nil
|
|
137
|
+
self.icon.isHidden = loaded || fallbackIcon == nil
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
networkBackdrop.isHidden = !(kind == "token" && sources.count > 1)
|
|
141
|
+
networkBackdrop.backgroundColor = nativeListColor(theme, "rowBackground", "#FFFFFF")
|
|
142
|
+
let badge = visual.dictionary("cornerIcon")
|
|
143
|
+
corner.isHidden = badge == nil
|
|
144
|
+
cornerBackdrop.isHidden = badge == nil
|
|
145
|
+
corner.image = badge.flatMap { nativeListIcon(named: $0.string("name")) }
|
|
146
|
+
corner.tintColor = UIColor(
|
|
147
|
+
nativeListHex: badge?.string("tintColor", default: "#646464") ?? "#646464",
|
|
148
|
+
fallback: .darkGray)
|
|
149
|
+
cornerBackdrop.backgroundColor = UIColor(
|
|
150
|
+
nativeListHex: badge?.string("backgroundColor", default: "#FFFFFF") ?? "#FFFFFF",
|
|
151
|
+
fallback: .white)
|
|
152
|
+
unread.isHidden = !isUnread
|
|
153
|
+
let previousOverlays = overlays
|
|
154
|
+
overlays.removeAll(keepingCapacity: true)
|
|
155
|
+
let descriptors = Array(visual.dictionaries("overlays").prefix(2))
|
|
156
|
+
while overlaySlots.count < descriptors.count { overlaySlots.append(NativeListImageSlot()) }
|
|
157
|
+
for (index, data) in descriptors.enumerated() {
|
|
158
|
+
let frame = index < previousOverlays.count ? previousOverlays[index].0 : UIView()
|
|
159
|
+
frame.clipsToBounds = true
|
|
160
|
+
frame.backgroundColor = UIColor(
|
|
161
|
+
nativeListHex: data.string(
|
|
162
|
+
"backgroundColor",
|
|
163
|
+
default: theme?.string("rowBackground", default: "#00000000") ?? "#00000000"),
|
|
164
|
+
fallback: .clear)
|
|
165
|
+
let child: UIView
|
|
166
|
+
if let image = data.dictionary("image") {
|
|
167
|
+
let slot = overlaySlots[index]
|
|
168
|
+
child = slot.view
|
|
169
|
+
child.isHidden = true
|
|
170
|
+
slot.bind(image, key: "\(key):overlay:\(index)", placeholder: placeholder) {
|
|
171
|
+
[weak child] loaded in child?.isHidden = !loaded
|
|
172
|
+
}
|
|
173
|
+
} else if !data.string("text").isEmpty {
|
|
174
|
+
overlaySlots[index].recycle()
|
|
175
|
+
let label = frame.subviews.first as? UILabel ?? UILabel()
|
|
176
|
+
label.text = data.string("text")
|
|
177
|
+
label.textAlignment = .center
|
|
178
|
+
// A line-height box is the legacy wallet text badge: regular weight, centered metrics.
|
|
179
|
+
label.font = nativeListFont(
|
|
180
|
+
ofSize: overlayTextFontSize, weight: overlayTextLineHeight == nil ? .medium : .regular)
|
|
181
|
+
if let line = overlayTextLineHeight {
|
|
182
|
+
let paragraph = NSMutableParagraphStyle()
|
|
183
|
+
paragraph.minimumLineHeight = line
|
|
184
|
+
paragraph.maximumLineHeight = line
|
|
185
|
+
paragraph.alignment = .center
|
|
186
|
+
label.attributedText = NSAttributedString(
|
|
187
|
+
string: data.string("text"),
|
|
188
|
+
attributes: [
|
|
189
|
+
.font: label.font as Any, .paragraphStyle: paragraph,
|
|
190
|
+
.baselineOffset: max(0, (line - label.font.lineHeight) / 2),
|
|
191
|
+
])
|
|
192
|
+
}
|
|
193
|
+
label.textColor = UIColor(
|
|
194
|
+
nativeListHex: data.string("tintColor", default: "#646464"), fallback: .darkGray)
|
|
195
|
+
child = label
|
|
196
|
+
} else {
|
|
197
|
+
overlaySlots[index].recycle()
|
|
198
|
+
let glyph = frame.subviews.first as? UIImageView ?? UIImageView()
|
|
199
|
+
glyph.image = nativeListIcon(named: data.string("name"))
|
|
200
|
+
glyph.contentMode = .scaleAspectFit
|
|
201
|
+
glyph.tintColor = UIColor(
|
|
202
|
+
nativeListHex: data.string("tintColor", default: "#646464"), fallback: .darkGray)
|
|
203
|
+
child = glyph
|
|
204
|
+
}
|
|
205
|
+
if child.superview !== frame {
|
|
206
|
+
frame.subviews.forEach { $0.removeFromSuperview() }
|
|
207
|
+
frame.addSubview(child)
|
|
208
|
+
}
|
|
209
|
+
if frame.superview == nil { addSubview(frame) }
|
|
210
|
+
overlays.append((frame, data))
|
|
211
|
+
}
|
|
212
|
+
previousOverlays.dropFirst(descriptors.count).forEach { $0.0.removeFromSuperview() }
|
|
213
|
+
for slot in overlaySlots.dropFirst(descriptors.count) { slot.recycle() }
|
|
214
|
+
bringSubviewToFront(unread)
|
|
215
|
+
setNeedsLayout()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
override func layoutSubviews() {
|
|
219
|
+
super.layoutSubviews()
|
|
220
|
+
let kind = visual.string("kind")
|
|
221
|
+
let rtl = effectiveUserInterfaceLayoutDirection == .rightToLeft
|
|
222
|
+
func logical(_ rect: CGRect) -> CGRect {
|
|
223
|
+
rtl
|
|
224
|
+
? CGRect(x: bounds.width - rect.maxX, y: rect.minY, width: rect.width, height: rect.height)
|
|
225
|
+
: rect
|
|
226
|
+
}
|
|
227
|
+
let shape = imageStyle.string(
|
|
228
|
+
"shape", default: visual.string("shape", default: kind == "image" ? "rounded" : "circle"))
|
|
229
|
+
let radius =
|
|
230
|
+
imageStyle["cornerRadius"] != nil
|
|
231
|
+
? CGFloat(imageStyle.double("cornerRadius"))
|
|
232
|
+
: shape == "square"
|
|
233
|
+
? 0
|
|
234
|
+
: shape == "rounded"
|
|
235
|
+
? (imageStyle["shape"] != nil ? 10 : min(10, bounds.height / 4))
|
|
236
|
+
: min(bounds.width, bounds.height) / 2
|
|
237
|
+
layer.cornerRadius = radius
|
|
238
|
+
clipsToBounds =
|
|
239
|
+
alwaysClips
|
|
240
|
+
|| sources.count < 2 && overlays.isEmpty && visual.dictionary("cornerIcon") == nil
|
|
241
|
+
let ellipse = imageStyle.string("shape") == "circle" && imageStyle["cornerRadius"] == nil
|
|
242
|
+
if ellipse && clipsToBounds {
|
|
243
|
+
let mask = layer.mask as? CAShapeLayer ?? CAShapeLayer()
|
|
244
|
+
mask.path = UIBezierPath(ovalIn: bounds).cgPath
|
|
245
|
+
layer.mask = mask
|
|
246
|
+
} else {
|
|
247
|
+
layer.mask = nil
|
|
248
|
+
}
|
|
249
|
+
fallback.frame = bounds
|
|
250
|
+
let fallbackIcon = visual.dictionary("fallbackIcon")
|
|
251
|
+
let iconSize: CGFloat =
|
|
252
|
+
!sources.isEmpty && fallbackIcon != nil
|
|
253
|
+
? min(bounds.width, bounds.height)
|
|
254
|
+
* (fallbackIcon?.string("name") == "GlobusOutline" ? 1.2 : 1) : glyphSize
|
|
255
|
+
icon.frame = CGRect(
|
|
256
|
+
x: (bounds.width - iconSize) / 2, y: (bounds.height - iconSize) / 2, width: iconSize,
|
|
257
|
+
height: iconSize)
|
|
258
|
+
let tokenPair = kind == "token" && sources.count > 1
|
|
259
|
+
for (index, slot) in slots.prefix(sources.count).enumerated() {
|
|
260
|
+
let frame: CGRect
|
|
261
|
+
if sources.count == 1 || tokenPair && index == 0 {
|
|
262
|
+
frame = bounds
|
|
263
|
+
} else if tokenPair {
|
|
264
|
+
frame = CGRect(x: bounds.width - 14, y: bounds.height - 14, width: 16, height: 16)
|
|
265
|
+
} else {
|
|
266
|
+
let side = bounds.height * 0.72
|
|
267
|
+
frame = CGRect(
|
|
268
|
+
x: CGFloat(index) * (bounds.width - side) / CGFloat(max(1, sources.count - 1)),
|
|
269
|
+
y: (bounds.height - side) / 2, width: side, height: side)
|
|
270
|
+
}
|
|
271
|
+
slot.view.frame = logical(
|
|
272
|
+
index == 0 ? frame.insetBy(dx: bitmapBorderWidth, dy: bitmapBorderWidth) : frame)
|
|
273
|
+
slot.view.clipsToBounds =
|
|
274
|
+
!(singleOuterMask && sources.count == 1 && overlays.isEmpty
|
|
275
|
+
&& visual.dictionary("cornerIcon") == nil)
|
|
276
|
+
slot.view.layer.cornerRadius =
|
|
277
|
+
index == 0 && singleOuterMask && sources.count == 1 && overlays.isEmpty
|
|
278
|
+
&& visual.dictionary("cornerIcon") == nil
|
|
279
|
+
? 0 : index == 0 && (sources.count == 1 || tokenPair) ? radius : frame.height / 2
|
|
280
|
+
}
|
|
281
|
+
for (index, slot) in slots.enumerated() {
|
|
282
|
+
if index == 0 && bitmapBorderWidth > 0 {
|
|
283
|
+
let mask = slot.view.layer.mask as? CAShapeLayer ?? CAShapeLayer()
|
|
284
|
+
mask.path =
|
|
285
|
+
UIBezierPath(
|
|
286
|
+
roundedRect: slot.view.bounds.insetBy(dx: -bitmapBorderWidth, dy: -bitmapBorderWidth),
|
|
287
|
+
cornerRadius: radius
|
|
288
|
+
).cgPath
|
|
289
|
+
slot.view.layer.mask = mask
|
|
290
|
+
slot.view.layer.cornerRadius = 0
|
|
291
|
+
} else if ellipse && index == 0 {
|
|
292
|
+
let mask = slot.view.layer.mask as? CAShapeLayer ?? CAShapeLayer()
|
|
293
|
+
mask.path = UIBezierPath(ovalIn: slot.view.bounds).cgPath
|
|
294
|
+
slot.view.layer.mask = mask
|
|
295
|
+
} else {
|
|
296
|
+
slot.view.layer.mask = nil
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
networkBackdrop.frame = logical(
|
|
300
|
+
CGRect(x: bounds.width - 16, y: bounds.height - 16, width: 20, height: 20))
|
|
301
|
+
cornerBackdrop.frame = networkBackdrop.frame
|
|
302
|
+
corner.frame = cornerBackdrop.frame.insetBy(dx: 1, dy: 1)
|
|
303
|
+
unread.frame = logical(CGRect(x: bounds.width - 8, y: 0, width: 8, height: 8))
|
|
304
|
+
for (frame, data) in overlays {
|
|
305
|
+
let size = CGFloat(data.double("size", default: 20))
|
|
306
|
+
// Legacy wallet text badges size to their text (tabular 12pt + 4) at the line height.
|
|
307
|
+
let textBadgeLine =
|
|
308
|
+
data.dictionary("image") == nil && !data.string("text").isEmpty
|
|
309
|
+
&& data.string("name").isEmpty ? overlayTextLineHeight : nil
|
|
310
|
+
let naturalWidth = textBadgeLine.map { _ -> CGFloat in
|
|
311
|
+
let scale = max(1, window?.screen.scale ?? traitCollection.displayScale)
|
|
312
|
+
let textWidth = (data.string("text") as NSString).size(withAttributes: [
|
|
313
|
+
.font: nativeListTabularFont(ofSize: overlayTextFontSize), .kern: 0,
|
|
314
|
+
]).width
|
|
315
|
+
return ceil(textWidth * scale) / scale + 4
|
|
316
|
+
}
|
|
317
|
+
let width = CGFloat(data.double("width", default: Double(naturalWidth ?? size)))
|
|
318
|
+
let height = CGFloat(data.double("height", default: Double(textBadgeLine ?? size)))
|
|
319
|
+
let offset = data.double("offset", default: 2)
|
|
320
|
+
let x = CGFloat(data.double("offsetX", default: offset))
|
|
321
|
+
let y = CGFloat(data.double("offsetY", default: offset))
|
|
322
|
+
frame.frame = logical(
|
|
323
|
+
CGRect(
|
|
324
|
+
x: data.string("position") == "topLeft" ? -x : bounds.width - width + x,
|
|
325
|
+
y: data.string("position") == "topLeft" ? -y : bounds.height - height + y, width: width,
|
|
326
|
+
height: height))
|
|
327
|
+
frame.layer.cornerRadius = min(width, height) / 2
|
|
328
|
+
let inset = CGFloat(data.double("padding", default: 0))
|
|
329
|
+
frame.subviews.first?.frame = frame.bounds.insetBy(
|
|
330
|
+
dx: data["text"] != nil ? overlayTextHorizontalInset ?? inset : inset,
|
|
331
|
+
dy: data["text"] != nil && overlayTextHorizontalInset != nil ? 0 : inset)
|
|
332
|
+
}
|
|
333
|
+
dashedBorder.isHidden = visual.string("borderStyle") != "dashed"
|
|
334
|
+
dashedBorder.strokeColor =
|
|
335
|
+
UIColor(nativeListHex: visual.string("borderColor", default: "#8D8D8D"), fallback: .gray)
|
|
336
|
+
.cgColor
|
|
337
|
+
dashedBorder.fillColor = UIColor.clear.cgColor
|
|
338
|
+
dashedBorder.lineWidth = dashedBorderWidth
|
|
339
|
+
dashedBorder.lineDashPattern = [4, 4]
|
|
340
|
+
dashedBorder.path =
|
|
341
|
+
UIBezierPath(ovalIn: bounds.insetBy(dx: dashedBorderWidth / 2, dy: dashedBorderWidth / 2))
|
|
342
|
+
.cgPath
|
|
343
|
+
}
|
|
344
|
+
func recycle() {
|
|
345
|
+
slots.forEach { $0.recycle() }
|
|
346
|
+
overlaySlots.forEach { $0.recycle() }
|
|
347
|
+
}
|
|
348
|
+
/// Empty slot (legacy leading container without a visual): no image, fallback, fill,
|
|
349
|
+
/// border or overlays from a previous binding.
|
|
350
|
+
func clear() {
|
|
351
|
+
recycle()
|
|
352
|
+
visual = [:]
|
|
353
|
+
sources = []
|
|
354
|
+
slots.forEach { $0.view.isHidden = true }
|
|
355
|
+
overlays.forEach { $0.0.removeFromSuperview() }
|
|
356
|
+
overlays.removeAll()
|
|
357
|
+
fallback.text = nil
|
|
358
|
+
fallback.isHidden = true
|
|
359
|
+
icon.image = nil
|
|
360
|
+
icon.isHidden = true
|
|
361
|
+
networkBackdrop.isHidden = true
|
|
362
|
+
corner.isHidden = true
|
|
363
|
+
cornerBackdrop.isHidden = true
|
|
364
|
+
unread.isHidden = true
|
|
365
|
+
backgroundColor = .clear
|
|
366
|
+
layer.borderWidth = 0
|
|
367
|
+
layer.mask = nil
|
|
368
|
+
dashedBorder.isHidden = true
|
|
369
|
+
dashedBorder.path = nil
|
|
370
|
+
setNeedsLayout()
|
|
371
|
+
}
|
|
372
|
+
}
|