stream-chat-react-native 9.0.2-beta.2 → 9.1.0

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,69 @@
1
+ import Foundation
2
+
3
+ struct StreamMultipartUploadRequest {
4
+ let headers: [String: String]
5
+ let method: String
6
+ let parts: [StreamMultipartUploadPart]
7
+ let progress: StreamMultipartUploadProgressOptions?
8
+ let timeoutMs: TimeInterval?
9
+ let uploadId: String
10
+ let url: URL
11
+ }
12
+
13
+ enum StreamMultipartUploadPart {
14
+ case file(StreamMultipartFilePart)
15
+ case text(StreamMultipartTextPart)
16
+ }
17
+
18
+ struct StreamMultipartFilePart {
19
+ let fieldName: String
20
+ let fileName: String
21
+ let mimeType: String?
22
+ let uri: String
23
+ }
24
+
25
+ struct StreamMultipartTextPart {
26
+ let fieldName: String
27
+ let value: String
28
+ }
29
+
30
+ struct StreamMultipartUploadProgressOptions {
31
+ let count: Int?
32
+ let intervalMs: TimeInterval?
33
+ }
34
+
35
+ struct StreamMultipartUploadResponse {
36
+ let body: String
37
+ let headers: [String: String]
38
+ let status: Int
39
+ let statusText: String?
40
+ }
41
+
42
+ enum StreamMultipartUploadError: LocalizedError {
43
+ case cancelled
44
+ case invalidRequest(String)
45
+ case invalidURL(String)
46
+ case missingHTTPResponse
47
+ case responseBodyTooLarge(Int)
48
+ case unreadableFile(String)
49
+ case unsupportedSource(String)
50
+
51
+ var errorDescription: String? {
52
+ switch self {
53
+ case .cancelled:
54
+ return "Request aborted"
55
+ case .invalidRequest(let message):
56
+ return message
57
+ case .invalidURL(let value):
58
+ return "Invalid upload URL: \(value)"
59
+ case .missingHTTPResponse:
60
+ return "Upload completed without an HTTP response"
61
+ case .responseBodyTooLarge(let maxBytes):
62
+ return "Upload response body exceeded \(maxBytes) bytes"
63
+ case .unreadableFile(let path):
64
+ return "Unable to read upload file: \(path)"
65
+ case .unsupportedSource(let uri):
66
+ return "Unsupported upload URI: \(uri)"
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,48 @@
1
+ import Foundation
2
+
3
+ final class StreamMultipartUploadProgressThrottler {
4
+ private let count: Int
5
+ private let intervalMs: TimeInterval
6
+ private let onProgress: (Int64, Int64?) -> Void
7
+ private var emittedBucket = -1
8
+ private var lastEventAt: TimeInterval = 0
9
+
10
+ init(
11
+ options: StreamMultipartUploadProgressOptions?,
12
+ onProgress: @escaping (Int64, Int64?) -> Void
13
+ ) {
14
+ self.count = min(max(options?.count ?? 20, 1), 100)
15
+ self.intervalMs = min(max(options?.intervalMs ?? 16, 16), 1_000)
16
+ self.onProgress = onProgress
17
+ }
18
+
19
+ func dispatch(loaded: Int64, total: Int64?) {
20
+ if let total, loaded >= total {
21
+ onProgress(loaded, total)
22
+ return
23
+ }
24
+
25
+ let now = Date().timeIntervalSince1970 * 1000
26
+ let passesInterval = now - lastEventAt >= intervalMs
27
+ let passesCount: Bool
28
+
29
+ if count > 0, let total = total, total > 0 {
30
+ let nextBucket = Int(floor((Double(loaded) / Double(total)) * Double(count)))
31
+ if nextBucket > emittedBucket {
32
+ emittedBucket = nextBucket
33
+ passesCount = true
34
+ } else {
35
+ passesCount = false
36
+ }
37
+ } else {
38
+ passesCount = true
39
+ }
40
+
41
+ guard passesInterval, passesCount else {
42
+ return
43
+ }
44
+
45
+ lastEventAt = now
46
+ onProgress(loaded, total)
47
+ }
48
+ }
@@ -0,0 +1,391 @@
1
+ import AVFoundation
2
+ import Foundation
3
+ import MobileCoreServices
4
+ import Photos
5
+ import UniformTypeIdentifiers
6
+
7
+ private final class StreamPhotoRequestBox {
8
+ private let lock = NSLock()
9
+ private var isCancelled = false
10
+ private var requestId: PHImageRequestID = PHInvalidImageRequestID
11
+
12
+ func set(_ requestId: PHImageRequestID) {
13
+ let shouldCancel: Bool
14
+
15
+ lock.lock()
16
+ if isCancelled {
17
+ shouldCancel = true
18
+ } else {
19
+ self.requestId = requestId
20
+ shouldCancel = false
21
+ }
22
+ lock.unlock()
23
+
24
+ if shouldCancel, requestId != PHInvalidImageRequestID {
25
+ PHImageManager.default().cancelImageRequest(requestId)
26
+ }
27
+ }
28
+
29
+ func cancel() {
30
+ lock.lock()
31
+ isCancelled = true
32
+ let requestId = self.requestId
33
+ lock.unlock()
34
+
35
+ if requestId != PHInvalidImageRequestID {
36
+ PHImageManager.default().cancelImageRequest(requestId)
37
+ }
38
+ }
39
+ }
40
+
41
+ private final class StreamContentEditingInputRequestBox {
42
+ private let lock = NSLock()
43
+ private weak var asset: PHAsset?
44
+ private var isCancelled = false
45
+ private var requestId: PHContentEditingInputRequestID = 0
46
+
47
+ init(asset: PHAsset) {
48
+ self.asset = asset
49
+ }
50
+
51
+ func set(_ requestId: PHContentEditingInputRequestID) {
52
+ let asset: PHAsset?
53
+ let shouldCancel: Bool
54
+
55
+ lock.lock()
56
+ asset = self.asset
57
+ if isCancelled {
58
+ shouldCancel = true
59
+ } else {
60
+ self.requestId = requestId
61
+ shouldCancel = false
62
+ }
63
+ lock.unlock()
64
+
65
+ if shouldCancel, requestId != 0 {
66
+ asset?.cancelContentEditingInputRequest(requestId)
67
+ }
68
+ }
69
+
70
+ func cancel() {
71
+ lock.lock()
72
+ isCancelled = true
73
+ let requestId = self.requestId
74
+ let asset = self.asset
75
+ lock.unlock()
76
+
77
+ if requestId != 0 {
78
+ asset?.cancelContentEditingInputRequest(requestId)
79
+ }
80
+ }
81
+ }
82
+
83
+ private final class StreamMultipartContinuationBox<Value> {
84
+ private let lock = NSLock()
85
+ private var continuation: CheckedContinuation<Value, Error>?
86
+ private var pendingResult: Result<Value, Error>?
87
+ private var hasResumed = false
88
+
89
+ func set(_ continuation: CheckedContinuation<Value, Error>) {
90
+ let result: Result<Value, Error>?
91
+
92
+ lock.lock()
93
+ if let pendingResult {
94
+ self.pendingResult = nil
95
+ result = pendingResult
96
+ } else if hasResumed {
97
+ result = nil
98
+ } else {
99
+ self.continuation = continuation
100
+ result = nil
101
+ }
102
+ lock.unlock()
103
+
104
+ if let result {
105
+ resume(continuation, with: result)
106
+ }
107
+ }
108
+
109
+ func resume(returning value: Value) {
110
+ resume(with: .success(value))
111
+ }
112
+
113
+ func resume(throwing error: Error) {
114
+ resume(with: .failure(error))
115
+ }
116
+
117
+ private func resume(with result: Result<Value, Error>) {
118
+ let continuationToResume: CheckedContinuation<Value, Error>?
119
+
120
+ lock.lock()
121
+ if hasResumed {
122
+ continuationToResume = nil
123
+ } else if let continuation {
124
+ self.continuation = nil
125
+ hasResumed = true
126
+ continuationToResume = continuation
127
+ } else {
128
+ pendingResult = result
129
+ hasResumed = true
130
+ continuationToResume = nil
131
+ }
132
+ lock.unlock()
133
+
134
+ if let continuationToResume {
135
+ resume(continuationToResume, with: result)
136
+ }
137
+ }
138
+
139
+ private func resume(
140
+ _ continuation: CheckedContinuation<Value, Error>,
141
+ with result: Result<Value, Error>
142
+ ) {
143
+ switch result {
144
+ case .success(let value):
145
+ continuation.resume(returning: value)
146
+ case .failure(let error):
147
+ continuation.resume(throwing: error)
148
+ }
149
+ }
150
+ }
151
+
152
+ struct StreamMultipartResolvedFilePart {
153
+ let fieldName: String
154
+ let fileName: String
155
+ let fileURL: URL
156
+ let mimeType: String
157
+ let size: Int64?
158
+ }
159
+
160
+ enum StreamMultipartUploadSourceResolver {
161
+ static func resolve(_ part: StreamMultipartFilePart) async throws -> StreamMultipartResolvedFilePart {
162
+ try Task.checkCancellation()
163
+ let fileURL = sanitizeFileURL(try await resolveFileURL(from: part.uri))
164
+ try Task.checkCancellation()
165
+ let mimeType = part.mimeType ?? guessMimeType(fileURL: fileURL, fallbackFileName: part.fileName)
166
+ let size = fileSize(url: fileURL)
167
+
168
+ return StreamMultipartResolvedFilePart(
169
+ fieldName: part.fieldName,
170
+ fileName: part.fileName,
171
+ fileURL: fileURL,
172
+ mimeType: mimeType,
173
+ size: size
174
+ )
175
+ }
176
+
177
+ private static func resolveFileURL(from uri: String) async throws -> URL {
178
+ if uri.lowercased().hasPrefix("ph://") {
179
+ return try await resolvePhotoLibraryURL(from: uri)
180
+ }
181
+
182
+ if uri.lowercased().hasPrefix("assets-library://") {
183
+ return try await resolveAssetsLibraryURL(from: uri)
184
+ }
185
+
186
+ if uri.hasPrefix("/") {
187
+ return URL(fileURLWithPath: uri)
188
+ }
189
+
190
+ guard let parsedURL = URL(string: uri) else {
191
+ throw StreamMultipartUploadError.unsupportedSource(uri)
192
+ }
193
+
194
+ if parsedURL.isFileURL {
195
+ return parsedURL
196
+ }
197
+
198
+ throw StreamMultipartUploadError.unsupportedSource(uri)
199
+ }
200
+
201
+ private static func sanitizeFileURL(_ url: URL) -> URL {
202
+ guard url.isFileURL else {
203
+ return url
204
+ }
205
+
206
+ guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
207
+ return url
208
+ }
209
+
210
+ components.fragment = nil
211
+ components.query = nil
212
+
213
+ return components.url ?? url
214
+ }
215
+
216
+ private static func resolvePhotoLibraryURL(from uri: String) async throws -> URL {
217
+ let identifier = photoLibraryIdentifier(from: uri)
218
+ guard !identifier.isEmpty else {
219
+ throw StreamMultipartUploadError.unsupportedSource(uri)
220
+ }
221
+
222
+ let result = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil)
223
+ guard let asset = result.firstObject else {
224
+ throw StreamMultipartUploadError.unsupportedSource(uri)
225
+ }
226
+
227
+ return try await resolveAssetURL(asset)
228
+ }
229
+
230
+ @available(iOS, deprecated: 11.0)
231
+ private static func resolveAssetsLibraryURL(from uri: String) async throws -> URL {
232
+ guard let assetURL = URL(string: uri) else {
233
+ throw StreamMultipartUploadError.unsupportedSource(uri)
234
+ }
235
+
236
+ let result = PHAsset.fetchAssets(withALAssetURLs: [assetURL], options: nil)
237
+ guard let asset = result.firstObject else {
238
+ throw StreamMultipartUploadError.unsupportedSource(uri)
239
+ }
240
+
241
+ return try await resolveAssetURL(asset)
242
+ }
243
+
244
+ private static func resolveAssetURL(_ asset: PHAsset) async throws -> URL {
245
+ switch asset.mediaType {
246
+ case .video:
247
+ return try await requestVideoAssetURL(asset)
248
+ case .image:
249
+ return try await requestImageAssetURL(asset)
250
+ default:
251
+ throw StreamMultipartUploadError.unsupportedSource(asset.localIdentifier)
252
+ }
253
+ }
254
+
255
+ private static func requestImageAssetURL(_ asset: PHAsset) async throws -> URL {
256
+ let options = PHContentEditingInputRequestOptions()
257
+ options.isNetworkAccessAllowed = true
258
+ let requestBox = StreamContentEditingInputRequestBox(asset: asset)
259
+ let continuationBox = StreamMultipartContinuationBox<URL>()
260
+
261
+ return try await withTaskCancellationHandler {
262
+ try await withCheckedThrowingContinuation { continuation in
263
+ continuationBox.set(continuation)
264
+ if Task.isCancelled {
265
+ continuationBox.resume(throwing: StreamMultipartUploadError.cancelled)
266
+ return
267
+ }
268
+
269
+ let requestId = asset.requestContentEditingInput(with: options) { input, _ in
270
+ if Task.isCancelled {
271
+ continuationBox.resume(throwing: StreamMultipartUploadError.cancelled)
272
+ return
273
+ }
274
+
275
+ if let url = input?.fullSizeImageURL {
276
+ continuationBox.resume(returning: url)
277
+ return
278
+ }
279
+
280
+ continuationBox.resume(
281
+ throwing: StreamMultipartUploadError.unsupportedSource(asset.localIdentifier)
282
+ )
283
+ }
284
+ requestBox.set(requestId)
285
+ }
286
+ } onCancel: {
287
+ requestBox.cancel()
288
+ continuationBox.resume(throwing: StreamMultipartUploadError.cancelled)
289
+ }
290
+ }
291
+
292
+ private static func requestVideoAssetURL(_ asset: PHAsset) async throws -> URL {
293
+ let options = PHVideoRequestOptions()
294
+ options.deliveryMode = .highQualityFormat
295
+ options.isNetworkAccessAllowed = true
296
+ options.version = .current
297
+ let requestBox = StreamPhotoRequestBox()
298
+ let continuationBox = StreamMultipartContinuationBox<URL>()
299
+
300
+ return try await withTaskCancellationHandler {
301
+ try await withCheckedThrowingContinuation { continuation in
302
+ continuationBox.set(continuation)
303
+ if Task.isCancelled {
304
+ continuationBox.resume(throwing: StreamMultipartUploadError.cancelled)
305
+ return
306
+ }
307
+
308
+ let requestId = PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { avAsset, _, info in
309
+ if Task.isCancelled {
310
+ continuationBox.resume(throwing: StreamMultipartUploadError.cancelled)
311
+ return
312
+ }
313
+
314
+ if let isCancelled = (info?[PHImageCancelledKey] as? NSNumber)?.boolValue, isCancelled {
315
+ continuationBox.resume(throwing: StreamMultipartUploadError.cancelled)
316
+ return
317
+ }
318
+
319
+ if let error = info?[PHImageErrorKey] as? Error {
320
+ continuationBox.resume(throwing: error)
321
+ return
322
+ }
323
+
324
+ if let url = (avAsset as? AVURLAsset)?.url {
325
+ continuationBox.resume(returning: url)
326
+ return
327
+ }
328
+
329
+ continuationBox.resume(
330
+ throwing: StreamMultipartUploadError.unsupportedSource(asset.localIdentifier)
331
+ )
332
+ }
333
+ requestBox.set(requestId)
334
+ }
335
+ } onCancel: {
336
+ requestBox.cancel()
337
+ continuationBox.resume(throwing: StreamMultipartUploadError.cancelled)
338
+ }
339
+ }
340
+
341
+ private static func guessMimeType(fileURL: URL, fallbackFileName: String) -> String {
342
+ if #available(iOS 14.0, *), let type = UTType(filenameExtension: fileURL.pathExtension) {
343
+ return type.preferredMIMEType ?? "application/octet-stream"
344
+ }
345
+
346
+ let fileName = fileURL.lastPathComponent.isEmpty ? fallbackFileName : fileURL.lastPathComponent
347
+ return mimeTypeFromExtension(fileName) ?? "application/octet-stream"
348
+ }
349
+
350
+ private static func mimeTypeFromExtension(_ fileName: String) -> String? {
351
+ let pathExtension = (fileName as NSString).pathExtension
352
+ guard !pathExtension.isEmpty else {
353
+ return nil
354
+ }
355
+
356
+ if let unmanaged = UTTypeCreatePreferredIdentifierForTag(
357
+ kUTTagClassFilenameExtension,
358
+ pathExtension as CFString,
359
+ nil
360
+ )?.takeRetainedValue(),
361
+ let mime = UTTypeCopyPreferredTagWithClass(unmanaged, kUTTagClassMIMEType)?.takeRetainedValue()
362
+ {
363
+ return mime as String
364
+ }
365
+
366
+ return nil
367
+ }
368
+
369
+ private static func fileSize(url: URL) -> Int64? {
370
+ let values = try? url.resourceValues(forKeys: [.fileSizeKey])
371
+ guard let fileSize = values?.fileSize else {
372
+ return nil
373
+ }
374
+ return Int64(fileSize)
375
+ }
376
+
377
+ private static func photoLibraryIdentifier(from url: String) -> String {
378
+ guard let parsedURL = URL(string: url), parsedURL.scheme?.lowercased() == "ph" else {
379
+ return url
380
+ .replacingOccurrences(of: "ph://", with: "", options: [.caseInsensitive])
381
+ .removingPercentEncoding?
382
+ .trimmingCharacters(in: CharacterSet(charactersIn: "/")) ?? ""
383
+ }
384
+
385
+ let host = parsedURL.host ?? ""
386
+ let path = parsedURL.path
387
+ let combined = host.isEmpty ? path : "\(host)\(path)"
388
+ return combined.removingPercentEncoding?
389
+ .trimmingCharacters(in: CharacterSet(charactersIn: "/")) ?? ""
390
+ }
391
+ }
@@ -0,0 +1,16 @@
1
+ #ifdef RCT_NEW_ARCH_ENABLED
2
+
3
+ #import <React/RCTEventEmitter.h>
4
+
5
+ #if __has_include("StreamChatReactNativeSpec.h")
6
+ #import "StreamChatReactNativeSpec.h"
7
+ #elif __has_include("StreamChatExpoSpec.h")
8
+ #import "StreamChatExpoSpec.h"
9
+ #else
10
+ #error "Unable to find generated codegen spec header for StreamMultipartUploader."
11
+ #endif
12
+
13
+ @interface StreamMultipartUploader : RCTEventEmitter <NativeStreamMultipartUploaderSpec>
14
+ @end
15
+
16
+ #endif
@@ -0,0 +1,109 @@
1
+ #import "StreamMultipartUploader.h"
2
+
3
+ #ifdef RCT_NEW_ARCH_ENABLED
4
+
5
+ #if __has_include(<stream_chat_react_native/stream_chat_react_native-Swift.h>)
6
+ #import <stream_chat_react_native/stream_chat_react_native-Swift.h>
7
+ #elif __has_include(<stream_chat_expo/stream_chat_expo-Swift.h>)
8
+ #import <stream_chat_expo/stream_chat_expo-Swift.h>
9
+ #elif __has_include("stream_chat_react_native-Swift.h")
10
+ #import "stream_chat_react_native-Swift.h"
11
+ #elif __has_include("stream_chat_expo-Swift.h")
12
+ #import "stream_chat_expo-Swift.h"
13
+ #else
14
+ #error "Unable to import generated Swift header for StreamMultipartUploader."
15
+ #endif
16
+
17
+ static NSString *const StreamMultipartUploadProgressEventName = @"streamMultipartUploadProgress";
18
+
19
+ static NSDictionary<NSString *, id> *StreamMultipartUploadProgressDictionary(
20
+ const JS::NativeStreamMultipartUploader::UploadProgressConfig &progress)
21
+ {
22
+ NSMutableDictionary<NSString *, id> *payload = [NSMutableDictionary dictionaryWithCapacity:2];
23
+
24
+ if (progress.count().has_value()) {
25
+ payload[@"count"] = @(progress.count().value());
26
+ }
27
+
28
+ if (progress.intervalMs().has_value()) {
29
+ payload[@"intervalMs"] = @(progress.intervalMs().value());
30
+ }
31
+
32
+ return payload;
33
+ }
34
+
35
+ @implementation StreamMultipartUploader
36
+
37
+ RCT_EXPORT_MODULE(StreamMultipartUploader)
38
+
39
+ + (BOOL)requiresMainQueueSetup
40
+ {
41
+ return NO;
42
+ }
43
+
44
+ - (NSArray<NSString *> *)supportedEvents
45
+ {
46
+ return @[ StreamMultipartUploadProgressEventName ];
47
+ }
48
+
49
+ - (void)uploadMultipart:(NSString *)uploadId
50
+ url:(NSString *)url
51
+ method:(NSString *)method
52
+ headers:(NSArray<NSDictionary<NSString *, NSString *> *> *)headers
53
+ parts:(NSArray<NSDictionary<NSString *, id> *> *)parts
54
+ progress:(JS::NativeStreamMultipartUploader::UploadProgressConfig &)progress
55
+ timeoutMs:(NSNumber *)timeoutMs
56
+ resolve:(RCTPromiseResolveBlock)resolve
57
+ reject:(RCTPromiseRejectBlock)reject
58
+ {
59
+ __weak __typeof__(self) weakSelf = self;
60
+ NSDictionary<NSString *, id> *progressOptions = StreamMultipartUploadProgressDictionary(progress);
61
+
62
+ [StreamMultipartUploaderBridge uploadMultipartWithUploadId:uploadId
63
+ url:url
64
+ method:method
65
+ headers:headers
66
+ parts:parts
67
+ progress:progressOptions
68
+ timeoutMs:timeoutMs
69
+ onProgress:^(NSNumber *loaded, NSNumber * _Nullable total) {
70
+ __strong __typeof__(weakSelf) strongSelf = weakSelf;
71
+ if (strongSelf == nil) {
72
+ return;
73
+ }
74
+
75
+ dispatch_async(dispatch_get_main_queue(), ^{
76
+ NSMutableDictionary<NSString *, id> *payload = [NSMutableDictionary dictionaryWithCapacity:3];
77
+ payload[@"uploadId"] = uploadId;
78
+ payload[@"loaded"] = loaded;
79
+ payload[@"total"] = total ?: [NSNull null];
80
+ [strongSelf sendEventWithName:StreamMultipartUploadProgressEventName body:payload];
81
+ });
82
+ }
83
+ completion:^(NSDictionary * _Nullable response, NSError * _Nullable error) {
84
+ if (error != nil) {
85
+ reject(@"stream_multipart_upload_error", error.localizedDescription, error);
86
+ return;
87
+ }
88
+
89
+ resolve(response ?: @{});
90
+ }];
91
+ }
92
+
93
+ - (void)cancelUpload:(NSString *)uploadId
94
+ resolve:(RCTPromiseResolveBlock)resolve
95
+ reject:(RCTPromiseRejectBlock)reject
96
+ {
97
+ [StreamMultipartUploaderBridge cancelUploadWithUploadId:uploadId];
98
+ resolve(nil);
99
+ }
100
+
101
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
102
+ (const facebook::react::ObjCTurboModule::InitParams &)params
103
+ {
104
+ return std::make_shared<facebook::react::NativeStreamMultipartUploaderSpecJSI>(params);
105
+ }
106
+
107
+ @end
108
+
109
+ #endif