@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.
- package/OneKeyImage.podspec +2 -1
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyBlockieAvatar.kt +264 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyBlockieAvatarLoader.kt +173 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImage.kt +25 -13
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageCache.kt +2 -1
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageGlideRegistry.kt +11 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageModel.kt +1 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageReusableView.kt +76 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyBlockieAvatarLoaderTest.kt +327 -0
- package/android/src/test/java/com/margelo/nitro/onekeyimage/OneKeyBlockieAvatarTest.kt +214 -0
- package/ios/OneKeyAvatarImageLoader.swift +217 -0
- package/ios/OneKeyBlockie.swift +126 -0
- package/ios/OneKeyImage.swift +4 -1
- package/ios/OneKeyImageCache.swift +11 -1
- package/ios/OneKeyImageCoderBridge.h +14 -0
- package/ios/OneKeyImageCoderBridge.m +25 -0
- package/ios/OneKeyImageRequestContext.swift +14 -15
- package/ios/OneKeyImageReusableView.swift +57 -0
- package/ios/tests/OneKeyAvatarImageTests.swift +49 -0
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/ios/OneKeyImage.swift
CHANGED
|
@@ -313,7 +313,10 @@ final class HybridOneKeyImage: HybridOneKeyImageSpec, RecyclableView {
|
|
|
313
313
|
cachePolicy: cachePolicy ?? .memoryDisk,
|
|
314
314
|
thumbnailPixelSize: thumbnailPixelSize,
|
|
315
315
|
safetyTracker: safetyHandle.tracker,
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
84
|
+
"@onekeyfe/react-native-skeleton": "3.0.106",
|
|
85
85
|
"react": "*",
|
|
86
86
|
"react-native": "*",
|
|
87
87
|
"react-native-nitro-modules": "0.37.0"
|