@onekeyfe/react-native-image 3.0.104 → 3.0.106

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.
@@ -0,0 +1,217 @@
1
+ // OneKey patch: Share local avatar work and persistent SDWebImage caching across
2
+ // render/preload requests, including managers isolated for HTTP headers.
3
+ import Foundation
4
+ import SDWebImage
5
+ import UIKit
6
+
7
+ final class OneKeyAvatarImageLoader: NSObject, SDImageLoader {
8
+ static let shared = OneKeyAvatarImageLoader()
9
+ static let cache: SDImageCache = {
10
+ let config = SDImageCacheConfig()
11
+ config.maxDiskAge = 30 * 24 * 60 * 60
12
+ config.maxDiskSize = 32 * 1024 * 1024
13
+ config.maxMemoryCost = 8 * 1024 * 1024
14
+ config.maxMemoryCount = 128
15
+ return SDImageCache(namespace: "onekey-avatar-blockie-v1", diskCacheDirectory: nil, config: config)
16
+ }()
17
+
18
+ private final class Subscription: NSObject, SDWebImageOperation {
19
+ let id = UUID()
20
+ let options: SDWebImage.SDWebImageOptions
21
+ let context: [SDWebImageContextOption: Any]?
22
+ private let lock = NSLock()
23
+ private var completion: SDImageLoaderCompletedBlock?
24
+ private var cancellation: (() -> Void)?
25
+ private var terminal = false
26
+
27
+ init(options: SDWebImage.SDWebImageOptions, context: [SDWebImageContextOption: Any]?,
28
+ completion: SDImageLoaderCompletedBlock?) {
29
+ self.options = options; self.context = context; self.completion = completion
30
+ }
31
+
32
+ var isTerminal: Bool {
33
+ lock.lock(); defer { lock.unlock() }; return terminal
34
+ }
35
+
36
+ func onCancel(_ block: @escaping () -> Void) {
37
+ lock.lock()
38
+ let alreadyTerminal = terminal
39
+ if !alreadyTerminal { cancellation = block }
40
+ lock.unlock()
41
+ if alreadyTerminal { block() }
42
+ }
43
+
44
+ func finish(image: UIImage?, data: Data?, error: Error?) {
45
+ lock.lock()
46
+ guard !terminal else { lock.unlock(); return }
47
+ terminal = true
48
+ let callback = completion
49
+ completion = nil; cancellation = nil
50
+ lock.unlock()
51
+ callback?(image, data, error, true)
52
+ }
53
+
54
+ func cancel() {
55
+ lock.lock()
56
+ guard !terminal else { lock.unlock(); return }
57
+ terminal = true
58
+ let callback = completion, cancel = cancellation
59
+ completion = nil; cancellation = nil
60
+ lock.unlock()
61
+ cancel?()
62
+ // Preload's checked continuation must also terminate on cancellation.
63
+ DispatchQueue.global(qos: .userInitiated).async {
64
+ callback?(nil, nil, URLError(.cancelled), true)
65
+ }
66
+ }
67
+ }
68
+
69
+ private final class Flight {
70
+ let id = UUID()
71
+ let descriptor: OneKeyBlockieDescriptor
72
+ let url: URL
73
+ var subscriptions: [UUID: Subscription] = [:]
74
+ var operation: BlockOperation?
75
+ init(descriptor: OneKeyBlockieDescriptor, url: URL) {
76
+ self.descriptor = descriptor; self.url = url
77
+ }
78
+ }
79
+
80
+ private let state = DispatchQueue(label: "onekey.avatar.state")
81
+ private let workers: OperationQueue = {
82
+ let queue = OperationQueue()
83
+ queue.name = "onekey.avatar.generate"
84
+ queue.qualityOfService = .userInitiated
85
+ queue.maxConcurrentOperationCount = 2
86
+ return queue
87
+ }()
88
+ private var flights: [String: Flight] = [:]
89
+ private var diskWrites = 0
90
+
91
+ func canRequestImage(for url: URL?) -> Bool {
92
+ // Own invalid local-avatar URLs too: fail locally instead of using HTTP.
93
+ url?.scheme == "onekey-avatar"
94
+ }
95
+
96
+ func shouldBlockFailedURL(with url: URL, error: Error) -> Bool { false }
97
+
98
+ func requestImage(with url: URL?, options: SDWebImage.SDWebImageOptions,
99
+ context: [SDWebImageContextOption: Any]?, progress: SDImageLoaderProgressBlock?,
100
+ completed: SDImageLoaderCompletedBlock?) -> SDWebImageOperation? {
101
+ let subscriber = Subscription(options: options, context: context, completion: completed)
102
+ state.async {
103
+ guard !subscriber.isTerminal else { return }
104
+ guard let url, let descriptor = OneKeyBlockieDescriptor(url: url) else {
105
+ subscriber.finish(image: nil, data: nil, error: URLError(.badURL))
106
+ return
107
+ }
108
+ let key = descriptor.cacheKey
109
+ let flight = self.flights[key] ?? Flight(descriptor: descriptor, url: url)
110
+ self.flights[key] = flight
111
+ flight.subscriptions[subscriber.id] = subscriber
112
+ subscriber.onCancel { [weak self, weak flight] in
113
+ guard let self, let flight else { return }
114
+ self.state.async {
115
+ guard self.flights[key] === flight else { return }
116
+ flight.subscriptions.removeValue(forKey: subscriber.id)
117
+ if flight.subscriptions.isEmpty {
118
+ self.flights.removeValue(forKey: key)
119
+ flight.operation?.cancel()
120
+ }
121
+ }
122
+ }
123
+ guard flight.operation == nil else { return }
124
+ let operation = BlockOperation { [weak self, weak flight] in
125
+ guard let self, let flight else { return }
126
+ self.load(flight)
127
+ }
128
+ flight.operation = operation
129
+ self.workers.addOperation(operation)
130
+ }
131
+ return subscriber
132
+ }
133
+
134
+ private func load(_ flight: Flight) {
135
+ let key = flight.descriptor.cacheKey
136
+ let cancelled = { flight.operation?.isCancelled != false }
137
+ guard !cancelled() else { return }
138
+ let queryTypes = state.sync { cacheTypes(flight, field: .originalQueryCacheType) }
139
+ // Query again inside the merged worker to close the manager-cache-query vs
140
+ // completed previous flight race. These disk operations never run on UI.
141
+ var data: Data?
142
+ if queryTypes.memory, let image = Self.cache.imageFromMemoryCache(forKey: key) {
143
+ data = image.pngData()
144
+ }
145
+ if data == nil, queryTypes.disk { data = Self.cache.diskImageData(forKey: key) }
146
+ var original = data.flatMap { UIImage(data: $0) }
147
+ // A damaged persistent entry is a local cache miss, not a permanent avatar failure.
148
+ if original == nil {
149
+ data = OneKeyBlockie.png(seed: flight.descriptor.seed, isCancelled: cancelled)
150
+ original = data.flatMap { UIImage(data: $0) }
151
+ }
152
+ guard !cancelled() else { return }
153
+ guard let data, let original else {
154
+ finish(flight, data: nil, error: URLError(.cannotDecodeContentData)); return
155
+ }
156
+ // Synchronous store on this worker seals the cache-fill window before any
157
+ // subscriber (or a new isolated manager) can observe a completed flight.
158
+ var storedMemory = false, storedDisk = false
159
+ while !cancelled() {
160
+ var subscribers: [Subscription]?
161
+ let storeTypes = state.sync { () -> (memory: Bool, disk: Bool) in
162
+ guard flights[key] === flight else { subscribers = []; return (false, false) }
163
+ let required = cacheTypes(flight, field: .originalStoreCacheType)
164
+ if (!required.memory || storedMemory) && (!required.disk || storedDisk) {
165
+ flights.removeValue(forKey: key)
166
+ subscribers = Array(flight.subscriptions.values)
167
+ }
168
+ return required
169
+ }
170
+ if let subscribers { deliver(subscribers, flight: flight, data: data, error: nil); return }
171
+ if storeTypes.memory && !storedMemory {
172
+ Self.cache.storeImage(toMemory: original, forKey: key)
173
+ storedMemory = true
174
+ }
175
+ if storeTypes.disk && !storedDisk {
176
+ Self.cache.storeImageData(toDisk: data, forKey: key)
177
+ storedDisk = true
178
+ state.async {
179
+ self.diskWrites += 1
180
+ // SDWebImage also cleans on background/termination; bound active-session
181
+ // growth without scanning the directory after every small PNG write.
182
+ if self.diskWrites % 128 == 0 { Self.cache.deleteOldFiles(completionBlock: nil) }
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ private func cacheTypes(_ flight: Flight, field: SDWebImageContextOption) -> (memory: Bool, disk: Bool) {
189
+ var memory = false, disk = false
190
+ for subscriber in flight.subscriptions.values where !subscriber.isTerminal {
191
+ let raw = (subscriber.context?[field] as? NSNumber)?.intValue ?? SDImageCacheType.all.rawValue
192
+ memory = memory || raw == SDImageCacheType.memory.rawValue || raw == SDImageCacheType.all.rawValue
193
+ disk = disk || raw == SDImageCacheType.disk.rawValue || raw == SDImageCacheType.all.rawValue
194
+ }
195
+ return (memory, disk)
196
+ }
197
+
198
+ private func finish(_ flight: Flight, data: Data?, error: Error?) {
199
+ let subscribers: [Subscription] = state.sync {
200
+ guard flights[flight.descriptor.cacheKey] === flight else { return [] }
201
+ flights.removeValue(forKey: flight.descriptor.cacheKey)
202
+ return Array(flight.subscriptions.values)
203
+ }
204
+ deliver(subscribers, flight: flight, data: data, error: error)
205
+ }
206
+
207
+ private func deliver(_ subscribers: [Subscription], flight: Flight, data: Data?, error: Error?) {
208
+ for subscriber in subscribers where !subscriber.isTerminal {
209
+ let image: UIImage? = data.flatMap {
210
+ SDWebImage.SDImageLoaderDecodeImageData($0, flight.url,
211
+ .init(rawValue: subscriber.options.rawValue), subscriber.context)
212
+ }
213
+ subscriber.finish(image: image, data: data,
214
+ error: error ?? (image == nil ? URLError(.cannotDecodeContentData) : nil))
215
+ }
216
+ }
217
+ }
@@ -0,0 +1,126 @@
1
+ // OneKey patch: Render the versioned local avatar URI without a JS PNG payload.
2
+ // Algorithm ported from ethereum-blockies-base64 1.0.2 by MyCrypto (MIT):
3
+ // https://github.com/MyCryptoHQ/ethereum-blockies-base64
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ // The above copyright notice and this permission notice shall be included in
11
+ // all copies or substantial portions of the Software.
12
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
18
+ // THE SOFTWARE.
19
+
20
+ import CoreGraphics
21
+ import Foundation
22
+ import ImageIO
23
+
24
+ struct OneKeyBlockieDescriptor {
25
+ let seed: String
26
+ let cacheKey: String
27
+
28
+ init?(url: URL) {
29
+ guard let parts = URLComponents(url: url, resolvingAgainstBaseURL: false),
30
+ parts.scheme == "onekey-avatar", parts.host == "blockie",
31
+ parts.user == nil, parts.password == nil, parts.port == nil,
32
+ parts.query == nil, parts.fragment == nil,
33
+ parts.percentEncodedPath.hasPrefix("/v1/")
34
+ else { return nil }
35
+ let encoded = String(parts.percentEncodedPath.dropFirst(4))
36
+ guard !encoded.contains("/"), let seed = encoded.removingPercentEncoding,
37
+ !seed.isEmpty
38
+ else { return nil }
39
+ // The caller already applied JS lowercase. ASCII keys preserve distinct
40
+ // UTF16 seeds that Swift String otherwise compares as canonically equal.
41
+ let allowed = CharacterSet(charactersIn:
42
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()")
43
+ guard let canonical = seed.addingPercentEncoding(withAllowedCharacters: allowed) else { return nil }
44
+ self.seed = seed
45
+ cacheKey = "onekey-avatar://blockie/v1/\(canonical)"
46
+ }
47
+ }
48
+
49
+ enum OneKeyBlockie {
50
+ static let pixelSize = 128
51
+
52
+ private struct Random {
53
+ var state = [Int32](repeating: 0, count: 4)
54
+
55
+ init(seed: String) {
56
+ for (index, unit) in seed.utf16.enumerated() {
57
+ let slot = index % 4
58
+ state[slot] = (state[slot] &<< 5) &- state[slot] &+ Int32(unit)
59
+ }
60
+ }
61
+
62
+ mutating func next() -> Double {
63
+ let t = state[0] ^ (state[0] &<< 11)
64
+ state[0] = state[1]; state[1] = state[2]; state[2] = state[3]
65
+ state[3] = state[3] ^ (state[3] >> 19) ^ t ^ (t >> 8)
66
+ return Double(UInt32(bitPattern: state[3])) / 2_147_483_648
67
+ }
68
+
69
+ mutating func color() -> [UInt8] {
70
+ let h = floor(next() * 360) / 360
71
+ let s = (next() * 60 + 40) / 100
72
+ let l = ((next() + next() + next() + next()) * 25) / 100
73
+ let q = l < 0.5 ? l * (1 + s) : l + s - l * s
74
+ let p = 2 * l - q
75
+ func channel(_ value: Double) -> UInt8 {
76
+ var t = value
77
+ if t < 0 { t += 1 }
78
+ if t > 1 { t -= 1 }
79
+ let value: Double
80
+ if t < 1.0 / 6 { value = p + (q - p) * 6 * t }
81
+ else if t < 1.0 / 2 { value = q }
82
+ else if t < 2.0 / 3 { value = p + (q - p) * (2.0 / 3 - t) * 6 }
83
+ else { value = p }
84
+ return UInt8(truncatingIfNeeded: Int(floor(value * 255 + 0.5)))
85
+ }
86
+ return [channel(h + 1.0 / 3), channel(h), channel(h - 1.0 / 3), 255]
87
+ }
88
+ }
89
+
90
+ static func rgba(seed: String, isCancelled: () -> Bool = { false }) -> Data? {
91
+ var random = Random(seed: seed)
92
+ let foreground = random.color(), background = random.color(), spot = random.color()
93
+ var pixels = [UInt8](repeating: 0, count: pixelSize * pixelSize * 4)
94
+ for row in 0..<8 {
95
+ guard !isCancelled() else { return nil }
96
+ let half = (0..<4).map { _ in Int(floor(random.next() * 2.3)) }
97
+ let cells = half + half.reversed()
98
+ for column in 0..<8 {
99
+ let color = cells[column] == 0 ? background : cells[column] == 1 ? foreground : spot
100
+ for y in (row * 16)..<((row + 1) * 16) {
101
+ for x in (column * 16)..<((column + 1) * 16) {
102
+ let offset = (y * pixelSize + x) * 4
103
+ for channel in 0..<4 { pixels[offset + channel] = color[channel] }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return Data(pixels)
109
+ }
110
+
111
+ static func png(seed: String, isCancelled: () -> Bool = { false }) -> Data? {
112
+ guard let data = rgba(seed: seed, isCancelled: isCancelled), !isCancelled(),
113
+ let provider = CGDataProvider(data: data as CFData),
114
+ let image = CGImage(width: pixelSize, height: pixelSize, bitsPerComponent: 8,
115
+ bitsPerPixel: 32, bytesPerRow: pixelSize * 4, space: CGColorSpaceCreateDeviceRGB(),
116
+ bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue),
117
+ provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)
118
+ else { return nil }
119
+ let output = NSMutableData()
120
+ guard let destination = CGImageDestinationCreateWithData(output, "public.png" as CFString, 1, nil)
121
+ else { return nil }
122
+ CGImageDestinationAddImage(destination, image, nil)
123
+ guard CGImageDestinationFinalize(destination), !isCancelled() else { return nil }
124
+ return output as Data
125
+ }
126
+ }
@@ -313,7 +313,10 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
313
313
  cachePolicy: cachePolicy ?? .memoryDisk,
314
314
  thumbnailPixelSize: thumbnailPixelSize,
315
315
  safetyTracker: safetyHandle.tracker,
316
- manager: safetyHandle.manager
316
+ // OneKey patch: Rendering and preload use the same local-avatar loader.
317
+ // manager: safetyHandle.manager
318
+ manager: safetyHandle.manager,
319
+ url: url
317
320
  )
318
321
  hostView.sd_setImage(
319
322
  with: url,
@@ -74,6 +74,8 @@ final class HybridOneKeyImageCache: HybridOneKeyImageCacheSpec {
74
74
  func clearMemory() throws -> Promise<Void> {
75
75
  Promise.async {
76
76
  SDImageCache.shared.clearMemory()
77
+ // OneKey patch: Clear the dedicated local-avatar cache with public cache operations.
78
+ OneKeyAvatarImageLoader.cache.clearMemory()
77
79
  }
78
80
  }
79
81
 
@@ -82,11 +84,16 @@ final class HybridOneKeyImageCache: HybridOneKeyImageCacheSpec {
82
84
  await withCheckedContinuation { continuation in
83
85
  SDImageCache.shared.clearDisk { continuation.resume() }
84
86
  }
87
+ await withCheckedContinuation { continuation in
88
+ OneKeyAvatarImageLoader.cache.clearDisk { continuation.resume() }
89
+ }
85
90
  }
86
91
  }
87
92
 
88
93
  func clearAll() throws -> Promise<Void> {
89
94
  SDImageCache.shared.clearMemory()
95
+ // OneKey patch: The avatar cache is shared by render and preload requests.
96
+ OneKeyAvatarImageLoader.cache.clearMemory()
90
97
  return try clearDisk()
91
98
  }
92
99
 
@@ -154,7 +161,10 @@ final class HybridOneKeyImageCache: HybridOneKeyImageCacheSpec {
154
161
  cachePolicy: source.cachePolicy ?? .memoryDisk,
155
162
  thumbnailPixelSize: thumbnailPixelSize,
156
163
  safetyTracker: safetyHandle.tracker,
157
- manager: safetyHandle.manager
164
+ // OneKey patch: Rendering and preload use the same local-avatar loader.
165
+ // manager: safetyHandle.manager
166
+ manager: safetyHandle.manager,
167
+ url: url
158
168
  )
159
169
  return await load(url: url, context: context, safetyHandle: safetyHandle)
160
170
  }
@@ -0,0 +1,14 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ // Keep optional coder modules out of the Swift compilation unit so SDWebImage
6
+ // Objective-C types are imported under a single Swift module identity.
7
+ @interface OneKeyImageCoderBridge : NSObject
8
+
9
+ + (void)addCodersToManager:(id)manager NS_SWIFT_NAME(addCoders(to:));
10
+ + (void)ensureWebPCoderRegistered;
11
+
12
+ @end
13
+
14
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,25 @@
1
+ #import "OneKeyImageCoderBridge.h"
2
+
3
+ #import <SDWebImage/SDWebImage.h>
4
+ #import <SDWebImageSVGCoder/SDImageSVGCoder.h>
5
+ #import <SDWebImageWebPCoder/SDImageWebPCoder.h>
6
+
7
+ @implementation OneKeyImageCoderBridge
8
+
9
+ + (void)addCodersToManager:(id)manager {
10
+ SDImageCodersManager *coderManager = (SDImageCodersManager *)manager;
11
+ [coderManager addCoder:SDImageSVGCoder.sharedCoder];
12
+ [coderManager addCoder:SDImageWebPCoder.sharedCoder];
13
+ }
14
+
15
+ + (void)ensureWebPCoderRegistered {
16
+ SDImageCodersManager *manager = SDImageCodersManager.sharedManager;
17
+ for (id<SDImageCoder> coder in manager.coders) {
18
+ if (coder == SDImageWebPCoder.sharedCoder) {
19
+ return;
20
+ }
21
+ }
22
+ [manager addCoder:SDImageWebPCoder.sharedCoder];
23
+ }
24
+
25
+ @end
@@ -2,8 +2,6 @@ import CryptoKit
2
2
  import Foundation
3
3
  import ImageIO
4
4
  import SDWebImage
5
- import SDWebImageSVGCoder
6
- import SDWebImageWebPCoder
7
5
  import UIKit
8
6
 
9
7
  enum OneKeyImageSafetyViolation: LocalizedError, Equatable, Sendable {
@@ -467,28 +465,18 @@ enum OneKeyImageSafetyPolicy {
467
465
  }
468
466
 
469
467
  enum OneKeyImageCoderRegistry {
470
- private static let svgCoder = SDImageSVGCoder.shared
471
- private static let webPCoder = SDImageWebPCoder.shared
472
-
473
468
  static let coder: SDImageCodersManager = {
474
469
  // Keep OneKey's decoder set independent from Expo's global registrations.
475
470
  // SDImageCodersManager starts with ImageIO, GIF and APNG coders.
476
471
  let manager = SDImageCodersManager()
477
- manager.addCoder(svgCoder)
478
- manager.addCoder(webPCoder)
472
+ OneKeyImageCoderBridge.addCoders(to: manager)
479
473
  return manager
480
474
  }()
481
475
 
482
476
  private static let globalRegistration: Void = {
483
477
  // SDAnimatedImage resolves its animated coder through the global manager,
484
478
  // even when a request-local coder is provided in the SDWebImage context.
485
- let global = SDImageCodersManager.shared
486
- let isAlreadyRegistered = (global.coders ?? []).contains {
487
- ($0 as AnyObject) === webPCoder
488
- }
489
- if !isAlreadyRegistered {
490
- global.addCoder(webPCoder)
491
- }
479
+ OneKeyImageCoderBridge.ensureWebPCoderRegistered()
492
480
  }()
493
481
 
494
482
  static func ensureWebPRegistered() {
@@ -697,7 +685,10 @@ enum OneKeyImageRequestContext {
697
685
  cachePolicy: OneKeyImageCachePolicy,
698
686
  thumbnailPixelSize: CGSize?,
699
687
  safetyTracker: OneKeyImageSafetyTracker?,
700
- manager: SDWebImageManager
688
+ // OneKey patch: Scope local avatar routing to this request, preserving HTTP managers.
689
+ // manager: SDWebImageManager
690
+ manager: SDWebImageManager,
691
+ url: URL? = nil
701
692
  ) -> [SDWebImageContextOption: Any] {
702
693
  var context = baseContext
703
694
  context[.customManager] = manager
@@ -728,6 +719,14 @@ enum OneKeyImageRequestContext {
728
719
  context[.storeCacheType] = cacheType.rawValue
729
720
  context[.originalQueryCacheType] = cacheType.rawValue
730
721
  context[.originalStoreCacheType] = cacheType.rawValue
722
+ if url?.scheme == "onekey-avatar" {
723
+ context[.imageLoader] = OneKeyAvatarImageLoader.shared
724
+ context[.imageCache] = OneKeyAvatarImageLoader.cache
725
+ context[.originalImageCache] = OneKeyAvatarImageLoader.cache
726
+ context[.cacheKeyFilter] = SDWebImageCacheKeyFilter { url in
727
+ OneKeyBlockieDescriptor(url: url)?.cacheKey ?? url.absoluteString
728
+ }
729
+ }
731
730
  return context
732
731
  }
733
732
 
@@ -0,0 +1,57 @@
1
+ import UIKit
2
+
3
+ /// A native-only OneKeyImage host for reusable container views such as list cells.
4
+ public final class OneKeyImageReusableView: UIView {
5
+ private let image = HybridOneKeyImage()
6
+
7
+ public override init(frame: CGRect) {
8
+ super.init(frame: frame)
9
+ let imageView = image.view
10
+ imageView.frame = bounds
11
+ imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
12
+ addSubview(imageView)
13
+ clipsToBounds = true
14
+ }
15
+
16
+ public required init?(coder: NSCoder) {
17
+ fatalError("init(coder:) has not been implemented")
18
+ }
19
+
20
+ public func configure(
21
+ sourceUri: String?,
22
+ sourceHeadersJson: String?,
23
+ variant: String,
24
+ contentFit: String,
25
+ cachePolicy: String,
26
+ autoplay: Bool,
27
+ recyclingKey: String,
28
+ optimizeTos: Bool,
29
+ overscan: Double,
30
+ loadingStrategy: String,
31
+ onLoad: (() -> Void)? = nil,
32
+ onError: (() -> Void)? = nil
33
+ ) {
34
+ // OneKey patch: Let reusable cells display their own success and fallback visuals.
35
+ image.onLoad = { _, _, _ in onLoad?() }
36
+ image.onError = { _ in onError?() }
37
+ image.sourceHeadersJson = sourceHeadersJson
38
+ image.variant = OneKeyImageVariant(fromString: variant) ?? .generic
39
+ image.contentFit = OneKeyImageContentFit(fromString: contentFit) ?? .cover
40
+ image.cachePolicy = OneKeyImageCachePolicy(fromString: cachePolicy) ?? .memoryDisk
41
+ image.autoplay = autoplay
42
+ image.recyclingKey = recyclingKey
43
+ image.optimizeTos = optimizeTos
44
+ image.overscan = overscan
45
+ image.loadingStrategy = OneKeyImageLoadingStrategy(fromString: loadingStrategy) ?? .static
46
+ image.sourceUri = sourceUri
47
+ image.afterUpdate()
48
+ }
49
+
50
+ public func prepareForReuse() {
51
+ image.prepareForRecycle()
52
+ }
53
+
54
+ deinit {
55
+ image.onDropView()
56
+ }
57
+ }
@@ -0,0 +1,49 @@
1
+ // OneKey patch: Protect request-local avatar routing and exact UTF16 identities.
2
+ import Foundation
3
+ import SDWebImage
4
+ import XCTest
5
+
6
+ @testable import OneKeyImage
7
+
8
+ final class OneKeyAvatarImageTests: XCTestCase {
9
+ func testAvatarCacheIsSharedWithoutReplacingIsolatedHTTPManagers() throws {
10
+ let url = try XCTUnwrap(URL(string: "onekey-avatar://blockie/v1/synthetic-avatar"))
11
+ let first = OneKeyImagePipeline.makeIsolatedManager()
12
+ let second = OneKeyImagePipeline.makeIsolatedManager()
13
+ func context(_ manager: SDWebImageManager, headers: String) -> [SDWebImageContextOption: Any] {
14
+ OneKeyImageRequestContext.make(headersJson: headers, cachePolicy: .memoryDisk,
15
+ thumbnailPixelSize: CGSize(width: 96, height: 96), safetyTracker: nil,
16
+ manager: manager, url: url)
17
+ }
18
+ let a = context(first, headers: "{\"X-Test\":\"a\"}")
19
+ let b = context(second, headers: "{\"X-Test\":\"b\"}")
20
+ XCTAssertTrue(a[.customManager] as? SDWebImageManager === first)
21
+ XCTAssertTrue(b[.customManager] as? SDWebImageManager === second)
22
+ XCTAssertTrue(a[.imageLoader] as? OneKeyAvatarImageLoader === OneKeyAvatarImageLoader.shared)
23
+ XCTAssertTrue(b[.imageLoader] as? OneKeyAvatarImageLoader === OneKeyAvatarImageLoader.shared)
24
+ XCTAssertTrue(a[.imageCache] as? SDImageCache === b[.imageCache] as? SDImageCache)
25
+ XCTAssertTrue(a[.originalImageCache] as? SDImageCache === OneKeyAvatarImageLoader.cache)
26
+ let firstFilter = try XCTUnwrap(a[.cacheKeyFilter] as? SDWebImageCacheKeyFilter)
27
+ let secondFilter = try XCTUnwrap(b[.cacheKeyFilter] as? SDWebImageCacheKeyFilter)
28
+ XCTAssertEqual(firstFilter.cacheKey(for: url), secondFilter.cacheKey(for: url))
29
+ let remote = OneKeyImageRequestContext.make(headersJson: nil, cachePolicy: .memoryDisk,
30
+ thumbnailPixelSize: nil, safetyTracker: nil, manager: first,
31
+ url: URL(string: "https://example.com/image.png"))
32
+ XCTAssertNil(remote[.imageLoader])
33
+ XCTAssertNil(remote[.imageCache])
34
+ XCTAssertNil(remote[.originalImageCache])
35
+ }
36
+
37
+ func testURIDecodesExactlyOnceAndPreservesUTF16SeedIdentity() throws {
38
+ func descriptor(_ suffix: String) throws -> OneKeyBlockieDescriptor {
39
+ try XCTUnwrap(OneKeyBlockieDescriptor(url:
40
+ XCTUnwrap(URL(string: "onekey-avatar://blockie/v1/" + suffix))))
41
+ }
42
+ XCTAssertEqual(try descriptor("%2561").seed, "%61")
43
+ XCTAssertEqual(try descriptor("%61").cacheKey, try descriptor("a").cacheKey)
44
+ XCTAssertNotEqual(try descriptor("%C3%A9").cacheKey, try descriptor("e%CC%81").cacheKey)
45
+ XCTAssertEqual(try descriptor("%C4%B0").seed.utf16.count, 1)
46
+ XCTAssertNotEqual(OneKeyBlockie.rgba(seed: "é"), OneKeyBlockie.rgba(seed: "e\u{301}"))
47
+ XCTAssertNil(OneKeyBlockie.png(seed: "synthetic", isCancelled: { true }))
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-image",
3
- "version": "3.0.104",
3
+ "version": "3.0.106",
4
4
  "description": "High-performance native image view for OneKey",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -81,7 +81,7 @@
81
81
  "typescript": "^5.9.2"
82
82
  },
83
83
  "peerDependencies": {
84
- "@onekeyfe/react-native-skeleton": "3.0.104",
84
+ "@onekeyfe/react-native-skeleton": "3.0.106",
85
85
  "react": "*",
86
86
  "react-native": "*",
87
87
  "react-native-nitro-modules": "0.37.0"