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,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
|