stream-chat-react-native 9.0.2-beta.1 → 9.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +5 -4
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +20 -4
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadFileRequestBody.kt +25 -0
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadModels.kt +39 -0
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadProgress.kt +80 -0
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadRequestParser.kt +110 -0
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadSourceResolver.kt +99 -0
- package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploader.kt +138 -0
- package/android/src/newarch/com/streamchatreactnative/StreamMultipartUploaderModule.kt +122 -0
- package/ios/shared/StreamMultipartUploadBodyStream.swift +254 -0
- package/ios/shared/StreamMultipartUploadManager.swift +462 -0
- package/ios/shared/StreamMultipartUploadModels.swift +69 -0
- package/ios/shared/StreamMultipartUploadProgress.swift +48 -0
- package/ios/shared/StreamMultipartUploadSourceResolver.swift +391 -0
- package/ios/shared/StreamMultipartUploader.h +16 -0
- package/ios/shared/StreamMultipartUploader.mm +109 -0
- package/ios/shared/StreamMultipartUploaderBridge.swift +145 -0
- package/ios/shared/StreamShimmerView.swift +180 -77
- package/ios/shared/StreamVideoThumbnailGenerator.swift +13 -2
- package/package.json +3 -2
- package/src/handlers/index.ts +1 -0
- package/src/handlers/multipartUpload.ts +9 -0
- package/src/index.js +2 -1
- package/src/native/NativeStreamMultipartUploader.ts +52 -0
- package/src/native/multipartUploader.ts +5 -0
- package/src/optionalDependencies/__tests__/pickDocument.test.ts +86 -0
- package/src/optionalDependencies/pickDocument.ts +24 -6
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
private final class StreamMultipartUploadBridgeTaskBox {
|
|
4
|
+
private let lock = NSLock()
|
|
5
|
+
private var isCancelled = false
|
|
6
|
+
private var task: Task<Void, Never>?
|
|
7
|
+
|
|
8
|
+
func setTask(_ task: Task<Void, Never>) {
|
|
9
|
+
lock.lock()
|
|
10
|
+
if isCancelled {
|
|
11
|
+
lock.unlock()
|
|
12
|
+
task.cancel()
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
self.task = task
|
|
17
|
+
lock.unlock()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func cancel() {
|
|
21
|
+
lock.lock()
|
|
22
|
+
isCancelled = true
|
|
23
|
+
let task = self.task
|
|
24
|
+
lock.unlock()
|
|
25
|
+
|
|
26
|
+
task?.cancel()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@objcMembers
|
|
31
|
+
public final class StreamMultipartUploaderBridge: NSObject {
|
|
32
|
+
private static let taskLock = NSLock()
|
|
33
|
+
private static var tasksByUploadId = [String: StreamMultipartUploadBridgeTaskBox]()
|
|
34
|
+
|
|
35
|
+
@objc(uploadMultipartWithUploadId:url:method:headers:parts:progress:timeoutMs:onProgress:completion:)
|
|
36
|
+
public static func uploadMultipart(
|
|
37
|
+
uploadId: String,
|
|
38
|
+
url: String,
|
|
39
|
+
method: String,
|
|
40
|
+
headers: [[String: String]],
|
|
41
|
+
parts: [[String: Any]],
|
|
42
|
+
progress: [String: Any]?,
|
|
43
|
+
timeoutMs: NSNumber?,
|
|
44
|
+
onProgress: @escaping (NSNumber, NSNumber?) -> Void,
|
|
45
|
+
completion: @escaping (NSDictionary?, NSError?) -> Void
|
|
46
|
+
) {
|
|
47
|
+
let taskBox = StreamMultipartUploadBridgeTaskBox()
|
|
48
|
+
var replacedTaskBox: StreamMultipartUploadBridgeTaskBox?
|
|
49
|
+
|
|
50
|
+
taskLock.lock()
|
|
51
|
+
replacedTaskBox = tasksByUploadId[uploadId]
|
|
52
|
+
tasksByUploadId[uploadId] = taskBox
|
|
53
|
+
taskLock.unlock()
|
|
54
|
+
if replacedTaskBox != nil {
|
|
55
|
+
replacedTaskBox?.cancel()
|
|
56
|
+
StreamMultipartUploadManager.shared.cancelInFlight(uploadId: uploadId)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let task = Task(priority: .userInitiated) {
|
|
60
|
+
defer {
|
|
61
|
+
taskLock.lock()
|
|
62
|
+
if tasksByUploadId[uploadId] === taskBox {
|
|
63
|
+
tasksByUploadId.removeValue(forKey: uploadId)
|
|
64
|
+
}
|
|
65
|
+
taskLock.unlock()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
do {
|
|
69
|
+
let response = try await StreamMultipartUploadManager.shared.uploadMultipart(
|
|
70
|
+
uploadId: uploadId,
|
|
71
|
+
url: url,
|
|
72
|
+
method: method,
|
|
73
|
+
headers: dictionary(from: headers),
|
|
74
|
+
parts: parts,
|
|
75
|
+
progress: progress,
|
|
76
|
+
timeoutMs: timeoutMs?.doubleValue,
|
|
77
|
+
onProgress: { loaded, total in
|
|
78
|
+
onProgress(NSNumber(value: loaded), total.map { NSNumber(value: $0) })
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
let payload = NSMutableDictionary(capacity: 4)
|
|
83
|
+
payload["body"] = response.body
|
|
84
|
+
payload["headers"] = headerEntries(from: response.headers)
|
|
85
|
+
payload["status"] = NSNumber(value: response.status)
|
|
86
|
+
payload["statusText"] = response.statusText ?? NSNull()
|
|
87
|
+
|
|
88
|
+
completion(payload, nil)
|
|
89
|
+
} catch {
|
|
90
|
+
completion(nil, error.asStreamMultipartNSError())
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
taskBox.setTask(task)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@objc(cancelUploadWithUploadId:)
|
|
98
|
+
public static func cancelUpload(uploadId: String) {
|
|
99
|
+
taskLock.lock()
|
|
100
|
+
let taskBox = tasksByUploadId.removeValue(forKey: uploadId)
|
|
101
|
+
taskLock.unlock()
|
|
102
|
+
|
|
103
|
+
taskBox?.cancel()
|
|
104
|
+
StreamMultipartUploadManager.shared.cancel(uploadId: uploadId)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private static func dictionary(from headers: [[String: String]]) -> [String: String] {
|
|
108
|
+
headers.reduce(into: [String: String]()) { result, header in
|
|
109
|
+
guard let name = header["name"], let value = header["value"] else {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
result[name] = value
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private static func headerEntries(from headers: [String: String]) -> [[String: String]] {
|
|
117
|
+
headers.map { name, value in
|
|
118
|
+
["name": name, "value": value]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private extension Error {
|
|
124
|
+
func asStreamMultipartNSError() -> NSError {
|
|
125
|
+
if self is CancellationError {
|
|
126
|
+
return NSError(
|
|
127
|
+
domain: "StreamMultipartUploader",
|
|
128
|
+
code: 2,
|
|
129
|
+
userInfo: [NSLocalizedDescriptionKey: StreamMultipartUploadError.cancelled.localizedDescription]
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let nsError = self as NSError
|
|
134
|
+
|
|
135
|
+
if nsError.domain != NSCocoaErrorDomain || nsError.code != 0 {
|
|
136
|
+
return nsError
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return NSError(
|
|
140
|
+
domain: "StreamMultipartUploader",
|
|
141
|
+
code: 1,
|
|
142
|
+
userInfo: [NSLocalizedDescriptionKey: localizedDescription]
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
import QuartzCore
|
|
2
2
|
import UIKit
|
|
3
3
|
|
|
4
|
+
private protocol StreamShimmerAppLifecycleObserving: AnyObject {
|
|
5
|
+
func shimmerAppLifecycleDidChange(isActive: Bool)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
private final class StreamShimmerAppLifecycleCoordinator: NSObject {
|
|
9
|
+
static let shared = StreamShimmerAppLifecycleCoordinator()
|
|
10
|
+
|
|
11
|
+
private let observers = NSHashTable<AnyObject>.weakObjects()
|
|
12
|
+
|
|
13
|
+
private(set) var isAppActive: Bool
|
|
14
|
+
|
|
15
|
+
private init(notificationCenter: NotificationCenter = .default) {
|
|
16
|
+
isAppActive = Self.currentAppActiveState()
|
|
17
|
+
super.init()
|
|
18
|
+
|
|
19
|
+
notificationCenter.addObserver(
|
|
20
|
+
self,
|
|
21
|
+
selector: #selector(handleWillEnterForeground),
|
|
22
|
+
name: UIApplication.willEnterForegroundNotification,
|
|
23
|
+
object: nil
|
|
24
|
+
)
|
|
25
|
+
notificationCenter.addObserver(
|
|
26
|
+
self,
|
|
27
|
+
selector: #selector(handleDidEnterBackground),
|
|
28
|
+
name: UIApplication.didEnterBackgroundNotification,
|
|
29
|
+
object: nil
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func addObserver(_ observer: StreamShimmerAppLifecycleObserving) {
|
|
34
|
+
observers.add(observer as AnyObject)
|
|
35
|
+
observer.shimmerAppLifecycleDidChange(isActive: isAppActive)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func removeObserver(_ observer: StreamShimmerAppLifecycleObserving) {
|
|
39
|
+
observers.remove(observer as AnyObject)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@objc
|
|
43
|
+
private func handleWillEnterForeground() {
|
|
44
|
+
broadcastAppState(isActive: true)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@objc
|
|
48
|
+
private func handleDidEnterBackground() {
|
|
49
|
+
broadcastAppState(isActive: false)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private func broadcastAppState(isActive: Bool) {
|
|
53
|
+
self.isAppActive = isActive
|
|
54
|
+
|
|
55
|
+
for case let observer as StreamShimmerAppLifecycleObserving in observers.allObjects {
|
|
56
|
+
observer.shimmerAppLifecycleDidChange(isActive: isActive)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private static func currentAppActiveState() -> Bool {
|
|
61
|
+
switch UIApplication.shared.applicationState {
|
|
62
|
+
case .active, .inactive:
|
|
63
|
+
return true
|
|
64
|
+
case .background:
|
|
65
|
+
return false
|
|
66
|
+
@unknown default:
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
4
72
|
/// Native shimmer view used by the Fabric component view.
|
|
5
73
|
///
|
|
6
74
|
/// It renders a base layer and a moving gradient highlight entirely in native code, so shimmer
|
|
@@ -8,14 +76,16 @@ import UIKit
|
|
|
8
76
|
/// stops animation when it is not drawable (backgrounded, detached, hidden, or zero sized).
|
|
9
77
|
@objcMembers
|
|
10
78
|
public final class StreamShimmerView: UIView {
|
|
11
|
-
private static let edgeHighlightAlpha: CGFloat = 0.1
|
|
12
79
|
private static let softHighlightAlpha: CGFloat = 0.24
|
|
13
|
-
private static let midHighlightAlpha: CGFloat = 0.48
|
|
14
|
-
private static let innerHighlightAlpha: CGFloat = 0.72
|
|
15
80
|
private static let defaultHighlightAlpha: CGFloat = 0.35
|
|
16
81
|
private static let defaultShimmerDuration: CFTimeInterval = 1.2
|
|
17
82
|
private static let shimmerStripWidthRatio: CGFloat = 1.25
|
|
18
83
|
private static let shimmerAnimationKey = "stream_shimmer_translate_x"
|
|
84
|
+
private static let gradientLocations: [NSNumber] = [0.0, 0.35, 0.5, 0.65, 1.0]
|
|
85
|
+
private static let gradientAlphaFactors: [CGFloat] = [0, softHighlightAlpha, 1, softHighlightAlpha, 0]
|
|
86
|
+
private static var animationDistanceTolerance: CGFloat {
|
|
87
|
+
1 / max(UIScreen.main.scale, 1)
|
|
88
|
+
}
|
|
19
89
|
|
|
20
90
|
private let baseLayer = CALayer()
|
|
21
91
|
private let shimmerLayer = CAGradientLayer()
|
|
@@ -25,23 +95,37 @@ public final class StreamShimmerView: UIView {
|
|
|
25
95
|
private var enabled = false
|
|
26
96
|
private var shimmerDuration: CFTimeInterval = defaultShimmerDuration
|
|
27
97
|
private var lastAnimatedDuration: CFTimeInterval = 0
|
|
28
|
-
private var
|
|
29
|
-
private var isAppActive =
|
|
98
|
+
private var lastAnimatedTravelDistance: CGFloat = 0
|
|
99
|
+
private var isAppActive = StreamShimmerAppLifecycleCoordinator.shared.isAppActive
|
|
100
|
+
private var needsBaseColorUpdate = true
|
|
101
|
+
private var needsGradientColorUpdate = true
|
|
102
|
+
|
|
103
|
+
public override var isHidden: Bool {
|
|
104
|
+
didSet {
|
|
105
|
+
updateLayersForCurrentState()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public override var alpha: CGFloat {
|
|
110
|
+
didSet {
|
|
111
|
+
updateLayersForCurrentState()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
30
114
|
|
|
31
115
|
public override init(frame: CGRect) {
|
|
32
116
|
super.init(frame: frame)
|
|
33
117
|
setupLayers()
|
|
34
|
-
|
|
118
|
+
StreamShimmerAppLifecycleCoordinator.shared.addObserver(self)
|
|
35
119
|
}
|
|
36
120
|
|
|
37
121
|
public required init?(coder: NSCoder) {
|
|
38
122
|
super.init(coder: coder)
|
|
39
123
|
setupLayers()
|
|
40
|
-
|
|
124
|
+
StreamShimmerAppLifecycleCoordinator.shared.addObserver(self)
|
|
41
125
|
}
|
|
42
126
|
|
|
43
127
|
deinit {
|
|
44
|
-
|
|
128
|
+
StreamShimmerAppLifecycleCoordinator.shared.removeObserver(self)
|
|
45
129
|
}
|
|
46
130
|
|
|
47
131
|
public override func layoutSubviews() {
|
|
@@ -69,6 +153,7 @@ public final class StreamShimmerView: UIView {
|
|
|
69
153
|
{
|
|
70
154
|
// In current usage, colors are typically driven by JS props. We still refresh on trait
|
|
71
155
|
// changes so dynamically resolved native colors remain correct if that path is used later.
|
|
156
|
+
invalidateResolvedColors()
|
|
72
157
|
updateLayersForCurrentState()
|
|
73
158
|
}
|
|
74
159
|
}
|
|
@@ -79,17 +164,34 @@ public final class StreamShimmerView: UIView {
|
|
|
79
164
|
durationMilliseconds: Double,
|
|
80
165
|
enabled: Bool
|
|
81
166
|
) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
167
|
+
let normalizedDuration = Self.normalizedDuration(milliseconds: durationMilliseconds)
|
|
168
|
+
let baseColorChanged = !self.baseColor.isEqual(baseColor)
|
|
169
|
+
let gradientColorChanged = !self.gradientColor.isEqual(gradientColor)
|
|
170
|
+
let durationChanged = shimmerDuration != normalizedDuration
|
|
171
|
+
let enabledChanged = self.enabled != enabled
|
|
172
|
+
|
|
173
|
+
if baseColorChanged {
|
|
174
|
+
self.baseColor = baseColor
|
|
175
|
+
needsBaseColorUpdate = true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if gradientColorChanged {
|
|
179
|
+
self.gradientColor = gradientColor
|
|
180
|
+
needsGradientColorUpdate = true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
shimmerDuration = normalizedDuration
|
|
85
184
|
self.enabled = enabled
|
|
86
|
-
|
|
185
|
+
|
|
186
|
+
if baseColorChanged || gradientColorChanged || durationChanged || enabledChanged {
|
|
187
|
+
updateLayersForCurrentState()
|
|
188
|
+
}
|
|
87
189
|
}
|
|
88
190
|
|
|
89
191
|
public func stopAnimation() {
|
|
90
192
|
shimmerLayer.removeAnimation(forKey: Self.shimmerAnimationKey)
|
|
91
193
|
lastAnimatedDuration = 0
|
|
92
|
-
|
|
194
|
+
lastAnimatedTravelDistance = 0
|
|
93
195
|
}
|
|
94
196
|
|
|
95
197
|
private func setupLayers() {
|
|
@@ -99,86 +201,73 @@ public final class StreamShimmerView: UIView {
|
|
|
99
201
|
shimmerLayer.allowsEdgeAntialiasing = true
|
|
100
202
|
shimmerLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
|
101
203
|
shimmerLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
|
102
|
-
shimmerLayer.locations =
|
|
204
|
+
shimmerLayer.locations = Self.gradientLocations
|
|
103
205
|
|
|
104
206
|
layer.addSublayer(baseLayer)
|
|
105
207
|
layer.addSublayer(shimmerLayer)
|
|
106
208
|
}
|
|
107
209
|
|
|
108
|
-
private func setupLifecycleObservers() {
|
|
109
|
-
NotificationCenter.default.addObserver(
|
|
110
|
-
self,
|
|
111
|
-
selector: #selector(handleWillEnterForeground),
|
|
112
|
-
name: UIApplication.willEnterForegroundNotification,
|
|
113
|
-
object: nil
|
|
114
|
-
)
|
|
115
|
-
NotificationCenter.default.addObserver(
|
|
116
|
-
self,
|
|
117
|
-
selector: #selector(handleDidEnterBackground),
|
|
118
|
-
name: UIApplication.didEnterBackgroundNotification,
|
|
119
|
-
object: nil
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
@objc
|
|
124
|
-
private func handleWillEnterForeground() {
|
|
125
|
-
// iOS can drop active layer animations while the app is backgrounded. We explicitly rerun
|
|
126
|
-
// a state update on foreground so shimmer reliably restarts when returning to the app.
|
|
127
|
-
isAppActive = true
|
|
128
|
-
updateLayersForCurrentState()
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@objc
|
|
132
|
-
private func handleDidEnterBackground() {
|
|
133
|
-
isAppActive = false
|
|
134
|
-
stopAnimation()
|
|
135
|
-
}
|
|
136
|
-
|
|
137
210
|
private func updateLayersForCurrentState() {
|
|
138
211
|
let bounds = self.bounds
|
|
212
|
+
let shouldHideShimmer = !enabled || bounds.isEmpty || isHidden || alpha <= 0.01
|
|
213
|
+
|
|
214
|
+
shimmerLayer.isHidden = shouldHideShimmer
|
|
215
|
+
|
|
139
216
|
guard !bounds.isEmpty else {
|
|
140
217
|
stopAnimation()
|
|
141
218
|
return
|
|
142
219
|
}
|
|
143
220
|
|
|
144
221
|
baseLayer.frame = bounds
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
222
|
+
updateBaseLayerColorIfNeeded()
|
|
223
|
+
updateShimmerGeometry(for: bounds)
|
|
224
|
+
updateShimmerColorsIfNeeded()
|
|
148
225
|
updateShimmerAnimation(for: bounds)
|
|
149
226
|
}
|
|
150
227
|
|
|
151
|
-
private func
|
|
152
|
-
|
|
153
|
-
|
|
228
|
+
private func updateBaseLayerColorIfNeeded() {
|
|
229
|
+
guard needsBaseColorUpdate else { return }
|
|
230
|
+
baseLayer.backgroundColor = baseColor.resolvedColor(with: traitCollection).cgColor
|
|
231
|
+
needsBaseColorUpdate = false
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private func updateShimmerGeometry(for bounds: CGRect) {
|
|
154
235
|
let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1)
|
|
155
|
-
let transparentHighlight = color(gradientColor, alphaFactor: 0)
|
|
156
236
|
shimmerLayer.frame = CGRect(x: -shimmerWidth, y: 0, width: shimmerWidth, height: bounds.height)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
color(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
color(gradientColor, alphaFactor: Self.edgeHighlightAlpha).cgColor,
|
|
168
|
-
transparentHighlight.cgColor,
|
|
169
|
-
]
|
|
170
|
-
shimmerLayer.isHidden = !enabled
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func updateShimmerColorsIfNeeded() {
|
|
240
|
+
guard needsGradientColorUpdate else { return }
|
|
241
|
+
|
|
242
|
+
let resolvedGradientColor = gradientColor.resolvedColor(with: traitCollection)
|
|
243
|
+
shimmerLayer.colors = Self.gradientAlphaFactors.map {
|
|
244
|
+
color(resolvedGradientColor, alphaFactor: $0).cgColor
|
|
245
|
+
}
|
|
246
|
+
needsGradientColorUpdate = false
|
|
171
247
|
}
|
|
172
248
|
|
|
173
249
|
private func updateShimmerAnimation(for bounds: CGRect) {
|
|
174
|
-
guard
|
|
250
|
+
guard
|
|
251
|
+
enabled,
|
|
252
|
+
isAppActive,
|
|
253
|
+
window != nil,
|
|
254
|
+
!isHidden,
|
|
255
|
+
alpha > 0.01,
|
|
256
|
+
bounds.width > 0,
|
|
257
|
+
bounds.height > 0
|
|
258
|
+
else {
|
|
175
259
|
stopAnimation()
|
|
176
260
|
return
|
|
177
261
|
}
|
|
178
262
|
|
|
179
|
-
|
|
263
|
+
let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1)
|
|
264
|
+
let animationTravelDistance = bounds.width + shimmerWidth
|
|
265
|
+
|
|
266
|
+
// If an animation already exists for the same travel distance, keep it running instead of
|
|
267
|
+
// restarting. Fabric can relayout the view for height-only or subpixel changes that do not
|
|
268
|
+
// require a new horizontal sweep.
|
|
180
269
|
if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil,
|
|
181
|
-
|
|
270
|
+
abs(lastAnimatedTravelDistance - animationTravelDistance) <= Self.animationDistanceTolerance,
|
|
182
271
|
lastAnimatedDuration == shimmerDuration
|
|
183
272
|
{
|
|
184
273
|
return
|
|
@@ -187,17 +276,16 @@ public final class StreamShimmerView: UIView {
|
|
|
187
276
|
stopAnimation()
|
|
188
277
|
|
|
189
278
|
// Start just outside the left edge and sweep fully past the right edge for a clean pass.
|
|
190
|
-
let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1)
|
|
191
279
|
let animation = CABasicAnimation(keyPath: "transform.translation.x")
|
|
192
280
|
animation.fromValue = 0
|
|
193
|
-
animation.toValue =
|
|
281
|
+
animation.toValue = animationTravelDistance
|
|
194
282
|
animation.duration = shimmerDuration
|
|
195
283
|
animation.repeatCount = .infinity
|
|
196
284
|
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
|
197
285
|
animation.isRemovedOnCompletion = true
|
|
198
286
|
shimmerLayer.add(animation, forKey: Self.shimmerAnimationKey)
|
|
199
287
|
lastAnimatedDuration = shimmerDuration
|
|
200
|
-
|
|
288
|
+
lastAnimatedTravelDistance = animationTravelDistance
|
|
201
289
|
}
|
|
202
290
|
|
|
203
291
|
private static func normalizedDuration(milliseconds: Double) -> CFTimeInterval {
|
|
@@ -205,28 +293,30 @@ public final class StreamShimmerView: UIView {
|
|
|
205
293
|
return milliseconds / 1000
|
|
206
294
|
}
|
|
207
295
|
|
|
208
|
-
private func
|
|
209
|
-
|
|
210
|
-
|
|
296
|
+
private func invalidateResolvedColors() {
|
|
297
|
+
needsBaseColorUpdate = true
|
|
298
|
+
needsGradientColorUpdate = true
|
|
299
|
+
}
|
|
211
300
|
|
|
301
|
+
private func color(_ color: UIColor, alphaFactor: CGFloat) -> UIColor {
|
|
212
302
|
var red: CGFloat = 0
|
|
213
303
|
var green: CGFloat = 0
|
|
214
304
|
var blue: CGFloat = 0
|
|
215
305
|
var alpha: CGFloat = 0
|
|
216
306
|
|
|
217
|
-
if
|
|
307
|
+
if color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
|
|
218
308
|
return UIColor(red: red, green: green, blue: blue, alpha: alpha * alphaFactor)
|
|
219
309
|
}
|
|
220
310
|
|
|
221
311
|
guard
|
|
222
|
-
let converted =
|
|
312
|
+
let converted = color.cgColor.converted(
|
|
223
313
|
to: CGColorSpace(name: CGColorSpace.extendedSRGB)!,
|
|
224
314
|
intent: .defaultIntent,
|
|
225
315
|
options: nil
|
|
226
316
|
),
|
|
227
317
|
let components = converted.components
|
|
228
318
|
else {
|
|
229
|
-
return
|
|
319
|
+
return color.withAlphaComponent(color.cgColor.alpha * alphaFactor)
|
|
230
320
|
}
|
|
231
321
|
|
|
232
322
|
switch components.count {
|
|
@@ -243,7 +333,20 @@ public final class StreamShimmerView: UIView {
|
|
|
243
333
|
alpha: components[3] * alphaFactor
|
|
244
334
|
)
|
|
245
335
|
default:
|
|
246
|
-
return
|
|
336
|
+
return color.withAlphaComponent(color.cgColor.alpha * alphaFactor)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
extension StreamShimmerView: StreamShimmerAppLifecycleObserving {
|
|
342
|
+
func shimmerAppLifecycleDidChange(isActive: Bool) {
|
|
343
|
+
// iOS can drop active layer animations while the app is backgrounded. We explicitly rerun
|
|
344
|
+
// a state update on foreground so shimmer reliably restarts when returning to the app.
|
|
345
|
+
self.isAppActive = isActive
|
|
346
|
+
if isActive {
|
|
347
|
+
updateLayersForCurrentState()
|
|
348
|
+
} else {
|
|
349
|
+
stopAnimation()
|
|
247
350
|
}
|
|
248
351
|
}
|
|
249
352
|
}
|
|
@@ -314,13 +314,24 @@ public final class StreamVideoThumbnailGenerator: NSObject {
|
|
|
314
314
|
private static func normalizeLocalURL(_ url: String) -> URL? {
|
|
315
315
|
if let parsedURL = URL(string: url), let scheme = parsedURL.scheme?.lowercased() {
|
|
316
316
|
if scheme == "file" {
|
|
317
|
-
return parsedURL
|
|
317
|
+
return sanitizedFileURL(parsedURL)
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
return nil
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
return URL(fileURLWithPath: url)
|
|
323
|
+
return sanitizedFileURL(URL(fileURLWithPath: url))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private static func sanitizedFileURL(_ url: URL) -> URL {
|
|
327
|
+
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
|
328
|
+
return url
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
components.fragment = nil
|
|
332
|
+
components.query = nil
|
|
333
|
+
|
|
334
|
+
return components.url ?? url
|
|
324
335
|
}
|
|
325
336
|
|
|
326
337
|
private static func thumbnailError(
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stream-chat-react-native",
|
|
3
3
|
"description": "The official React Native SDK for Stream Chat, a service for building chat applications",
|
|
4
|
-
"version": "9.0
|
|
4
|
+
"version": "9.1.0-beta.1",
|
|
5
5
|
"homepage": "https://www.npmjs.com/package/stream-chat-react-native",
|
|
6
6
|
"author": {
|
|
7
7
|
"company": "Stream.io Inc",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"es6-symbol": "^3.1.3",
|
|
32
32
|
"mime": "^4.0.7",
|
|
33
|
-
"stream-chat-react-native-core": "9.0
|
|
33
|
+
"stream-chat-react-native-core": "9.1.0-beta.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@react-native-camera-roll/camera-roll": ">=7.9.0",
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
"ios": {
|
|
95
95
|
"modulesProvider": {
|
|
96
96
|
"StreamChatReactNative": "StreamChatReactNative",
|
|
97
|
+
"StreamMultipartUploader": "StreamMultipartUploader",
|
|
97
98
|
"StreamVideoThumbnail": "StreamVideoThumbnail"
|
|
98
99
|
},
|
|
99
100
|
"componentProvider": {
|
package/src/handlers/index.ts
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createNativeMultipartUpload } from 'stream-chat-react-native-core';
|
|
2
|
+
|
|
3
|
+
import { uploadMultipart } from '../native/multipartUploader';
|
|
4
|
+
import { getLocalAssetUri } from '../optionalDependencies/getLocalAssetUri';
|
|
5
|
+
|
|
6
|
+
export const multipartUpload = createNativeMultipartUpload({
|
|
7
|
+
getLocalAssetUri,
|
|
8
|
+
uploadMultipart,
|
|
9
|
+
});
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Platform } from 'react-native';
|
|
|
2
2
|
|
|
3
3
|
import { registerNativeHandlers } from 'stream-chat-react-native-core';
|
|
4
4
|
|
|
5
|
-
import { compressImage } from './handlers';
|
|
5
|
+
import { compressImage, multipartUpload } from './handlers';
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
Audio,
|
|
@@ -33,6 +33,7 @@ registerNativeHandlers({
|
|
|
33
33
|
getLocalAssetUri,
|
|
34
34
|
getPhotos,
|
|
35
35
|
iOS14RefreshGallerySelection,
|
|
36
|
+
multipartUpload,
|
|
36
37
|
NativeShimmerView,
|
|
37
38
|
oniOS14GalleryLibrarySelectionChange,
|
|
38
39
|
overrideAudioRecordingConfiguration,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
4
|
+
|
|
5
|
+
export type UploadHeader = {
|
|
6
|
+
name: string;
|
|
7
|
+
value: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type UploadPart = {
|
|
11
|
+
fieldName: string;
|
|
12
|
+
fileName?: string;
|
|
13
|
+
kind: string;
|
|
14
|
+
mimeType?: string;
|
|
15
|
+
uri?: string;
|
|
16
|
+
value?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type UploadProgressConfig = {
|
|
20
|
+
count?: number;
|
|
21
|
+
intervalMs?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type UploadProgressEvent = {
|
|
25
|
+
loaded: number;
|
|
26
|
+
total?: number;
|
|
27
|
+
uploadId: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type UploadResponse = {
|
|
31
|
+
body: string;
|
|
32
|
+
headers?: ReadonlyArray<UploadHeader>;
|
|
33
|
+
status: number;
|
|
34
|
+
statusText?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface Spec extends TurboModule {
|
|
38
|
+
addListener(eventType: string): void;
|
|
39
|
+
cancelUpload(uploadId: string): Promise<void>;
|
|
40
|
+
removeListeners(count: number): void;
|
|
41
|
+
uploadMultipart(
|
|
42
|
+
uploadId: string,
|
|
43
|
+
url: string,
|
|
44
|
+
method: string,
|
|
45
|
+
headers: ReadonlyArray<UploadHeader>,
|
|
46
|
+
parts: ReadonlyArray<UploadPart>,
|
|
47
|
+
progress?: UploadProgressConfig,
|
|
48
|
+
timeoutMs?: number | null,
|
|
49
|
+
): Promise<UploadResponse>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default TurboModuleRegistry.get<Spec>('StreamMultipartUploader');
|