stream-chat-react-native 9.0.2-beta.2 → 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.
Files changed (27) hide show
  1. package/android/build.gradle +5 -4
  2. package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +20 -4
  3. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadFileRequestBody.kt +25 -0
  4. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadModels.kt +39 -0
  5. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadProgress.kt +80 -0
  6. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadRequestParser.kt +110 -0
  7. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadSourceResolver.kt +99 -0
  8. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploader.kt +138 -0
  9. package/android/src/newarch/com/streamchatreactnative/StreamMultipartUploaderModule.kt +122 -0
  10. package/ios/shared/StreamMultipartUploadBodyStream.swift +254 -0
  11. package/ios/shared/StreamMultipartUploadManager.swift +462 -0
  12. package/ios/shared/StreamMultipartUploadModels.swift +69 -0
  13. package/ios/shared/StreamMultipartUploadProgress.swift +48 -0
  14. package/ios/shared/StreamMultipartUploadSourceResolver.swift +391 -0
  15. package/ios/shared/StreamMultipartUploader.h +16 -0
  16. package/ios/shared/StreamMultipartUploader.mm +109 -0
  17. package/ios/shared/StreamMultipartUploaderBridge.swift +145 -0
  18. package/ios/shared/StreamShimmerView.swift +180 -77
  19. package/ios/shared/StreamVideoThumbnailGenerator.swift +13 -2
  20. package/package.json +3 -2
  21. package/src/handlers/index.ts +1 -0
  22. package/src/handlers/multipartUpload.ts +9 -0
  23. package/src/index.js +2 -1
  24. package/src/native/NativeStreamMultipartUploader.ts +52 -0
  25. package/src/native/multipartUploader.ts +5 -0
  26. package/src/optionalDependencies/__tests__/pickDocument.test.ts +86 -0
  27. 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 lastAnimatedSize: CGSize = .zero
29
- private var isAppActive = true
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
- setupLifecycleObservers()
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
- setupLifecycleObservers()
124
+ StreamShimmerAppLifecycleCoordinator.shared.addObserver(self)
41
125
  }
42
126
 
43
127
  deinit {
44
- NotificationCenter.default.removeObserver(self)
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
- self.baseColor = baseColor
83
- self.gradientColor = gradientColor
84
- shimmerDuration = Self.normalizedDuration(milliseconds: durationMilliseconds)
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
- updateLayersForCurrentState()
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
- lastAnimatedSize = .zero
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 = [0.0, 0.08, 0.2, 0.32, 0.4, 0.5, 0.6, 0.68, 0.8, 0.92, 1.0]
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
- baseLayer.backgroundColor = baseColor.cgColor
146
-
147
- updateShimmerLayer(for: bounds)
222
+ updateBaseLayerColorIfNeeded()
223
+ updateShimmerGeometry(for: bounds)
224
+ updateShimmerColorsIfNeeded()
148
225
  updateShimmerAnimation(for: bounds)
149
226
  }
150
227
 
151
- private func updateShimmerLayer(for bounds: CGRect) {
152
- // Rebuild the shimmer gradient for current width/colors. Keep this tied to real state changes
153
- // such as layout/prop updates, not continuous per frame calls.
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
- shimmerLayer.colors = [
158
- transparentHighlight.cgColor,
159
- color(gradientColor, alphaFactor: Self.edgeHighlightAlpha).cgColor,
160
- color(gradientColor, alphaFactor: Self.softHighlightAlpha).cgColor,
161
- color(gradientColor, alphaFactor: Self.midHighlightAlpha).cgColor,
162
- color(gradientColor, alphaFactor: Self.innerHighlightAlpha).cgColor,
163
- gradientColor.cgColor,
164
- color(gradientColor, alphaFactor: Self.innerHighlightAlpha).cgColor,
165
- color(gradientColor, alphaFactor: Self.midHighlightAlpha).cgColor,
166
- color(gradientColor, alphaFactor: Self.softHighlightAlpha).cgColor,
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 enabled, isAppActive, window != nil, bounds.width > 0, bounds.height > 0 else {
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
- // If an animation already exists for the same size, keep it running instead of restarting.
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
- lastAnimatedSize == bounds.size,
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 = bounds.width + shimmerWidth
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
- lastAnimatedSize = bounds.size
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 color(_ color: UIColor, alphaFactor: CGFloat) -> UIColor {
209
- // Preserve the resolved color channels and shape only alpha for smooth highlight falloff.
210
- let resolvedColor = color.resolvedColor(with: traitCollection)
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 resolvedColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
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 = resolvedColor.cgColor.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 resolvedColor.withAlphaComponent(resolvedColor.cgColor.alpha * alphaFactor)
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 resolvedColor.withAlphaComponent(resolvedColor.cgColor.alpha * alphaFactor)
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.2-beta.2",
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.2-beta.2"
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": {
@@ -1 +1,2 @@
1
1
  export * from './compressImage';
2
+ export * from './multipartUpload';
@@ -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');
@@ -0,0 +1,5 @@
1
+ import { createNativeMultipartUploader } from 'stream-chat-react-native-core';
2
+
3
+ import NativeStreamMultipartUploader from './NativeStreamMultipartUploader';
4
+
5
+ export const uploadMultipart = createNativeMultipartUploader(NativeStreamMultipartUploader);