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.
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
@@ -36,8 +36,9 @@ def getExtOrDefault(name) {
36
36
  def getExtOrIntegerDefault(name) {
37
37
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ImageResizer_" + name]).toInteger()
38
38
  }
39
+ def canonicalProjectDir = projectDir.getCanonicalFile()
39
40
  def localSharedNativeRootDir = new File(projectDir, "src/main/java/com/streamchatreactnative/shared")
40
- def sharedNativeRootDir = new File(projectDir, "../../shared-native/android")
41
+ def sharedNativeRootDir = new File(canonicalProjectDir, "../../shared-native/android")
41
42
  def hasNativeSources = { File dir ->
42
43
  dir.exists() && !fileTree(dir).matching { include "**/*.kt"; include "**/*.java" }.files.isEmpty()
43
44
  }
@@ -101,10 +102,10 @@ tasks.register("syncSharedShimmerSources") {
101
102
  outputs.upToDateWhen { false }
102
103
  doLast {
103
104
  def sourceRootDir = null
104
- if (hasNativeSources(localSharedNativeRootDir)) {
105
- sourceRootDir = localSharedNativeRootDir
106
- } else if (hasNativeSources(sharedNativeRootDir)) {
105
+ if (hasNativeSources(sharedNativeRootDir)) {
107
106
  sourceRootDir = sharedNativeRootDir
107
+ } else if (hasNativeSources(localSharedNativeRootDir)) {
108
+ sourceRootDir = localSharedNativeRootDir
108
109
  }
109
110
 
110
111
  if (sourceRootDir == null) {
@@ -14,6 +14,7 @@ import java.util.List;
14
14
  import java.util.Map;
15
15
 
16
16
  public class StreamChatReactNativePackage extends TurboReactPackage {
17
+ private static final String STREAM_MULTIPART_UPLOADER_MODULE = "StreamMultipartUploader";
17
18
  private static final String STREAM_VIDEO_THUMBNAIL_MODULE = "StreamVideoThumbnail";
18
19
 
19
20
  @Nullable
@@ -21,7 +22,12 @@ public class StreamChatReactNativePackage extends TurboReactPackage {
21
22
  public NativeModule getModule(String name, ReactApplicationContext reactContext) {
22
23
  if (name.equals(StreamChatReactNativeModule.NAME)) {
23
24
  return new StreamChatReactNativeModule(reactContext);
24
- } else if (name.equals(STREAM_VIDEO_THUMBNAIL_MODULE) && BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
25
+ } else if (name.equals(STREAM_MULTIPART_UPLOADER_MODULE)) {
26
+ return createNewArchModule(
27
+ "com.streamchatreactnative.StreamMultipartUploaderModule",
28
+ reactContext
29
+ );
30
+ } else if (name.equals(STREAM_VIDEO_THUMBNAIL_MODULE)) {
25
31
  return createNewArchModule(
26
32
  "com.streamchatreactnative.StreamVideoThumbnailModule",
27
33
  reactContext
@@ -35,7 +41,6 @@ public class StreamChatReactNativePackage extends TurboReactPackage {
35
41
  public ReactModuleInfoProvider getReactModuleInfoProvider() {
36
42
  return () -> {
37
43
  final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
38
- boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
39
44
  moduleInfos.put(
40
45
  StreamChatReactNativeModule.NAME,
41
46
  new ReactModuleInfo(
@@ -45,7 +50,18 @@ public class StreamChatReactNativePackage extends TurboReactPackage {
45
50
  false, // needsEagerInit
46
51
  true, // hasConstants
47
52
  false, // isCxxModule
48
- isTurboModule // isTurboModule
53
+ true // isTurboModule
54
+ ));
55
+ moduleInfos.put(
56
+ STREAM_MULTIPART_UPLOADER_MODULE,
57
+ new ReactModuleInfo(
58
+ STREAM_MULTIPART_UPLOADER_MODULE,
59
+ STREAM_MULTIPART_UPLOADER_MODULE,
60
+ false, // canOverrideExistingModule
61
+ false, // needsEagerInit
62
+ false, // hasConstants
63
+ false, // isCxxModule
64
+ true // isTurboModule
49
65
  ));
50
66
  moduleInfos.put(
51
67
  STREAM_VIDEO_THUMBNAIL_MODULE,
@@ -56,7 +72,7 @@ public class StreamChatReactNativePackage extends TurboReactPackage {
56
72
  false, // needsEagerInit
57
73
  false, // hasConstants
58
74
  false, // isCxxModule
59
- isTurboModule // isTurboModule
75
+ true // isTurboModule
60
76
  ));
61
77
  return moduleInfos;
62
78
  };
@@ -0,0 +1,25 @@
1
+ package com.streamchatreactnative.shared.upload
2
+
3
+ import android.content.Context
4
+ import okhttp3.RequestBody
5
+ import okhttp3.MediaType.Companion.toMediaTypeOrNull
6
+ import okio.BufferedSink
7
+ import okio.source
8
+
9
+ class StreamMultipartUploadFileRequestBody(
10
+ private val context: Context,
11
+ private val filePart: StreamMultipartFilePart,
12
+ ) : RequestBody() {
13
+ private val resolvedMimeType = StreamMultipartUploadSourceResolver.mimeType(context, filePart)
14
+ private val resolvedContentLength = StreamMultipartUploadSourceResolver.contentLength(context, filePart.uri)
15
+
16
+ override fun contentLength(): Long = resolvedContentLength ?: -1L
17
+
18
+ override fun contentType() = resolvedMimeType.toMediaTypeOrNull()
19
+
20
+ override fun writeTo(sink: BufferedSink) {
21
+ StreamMultipartUploadSourceResolver.openInputStream(context, filePart.uri).use { inputStream ->
22
+ sink.writeAll(inputStream.source())
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,39 @@
1
+ package com.streamchatreactnative.shared.upload
2
+
3
+ data class StreamMultipartUploadRequest(
4
+ val headers: Map<String, String>,
5
+ val method: String,
6
+ val parts: List<StreamMultipartUploadPart>,
7
+ val progress: StreamMultipartUploadProgressOptions?,
8
+ val timeoutMs: Long?,
9
+ val uploadId: String,
10
+ val url: String,
11
+ )
12
+
13
+ sealed interface StreamMultipartUploadPart {
14
+ val fieldName: String
15
+ }
16
+
17
+ data class StreamMultipartFilePart(
18
+ override val fieldName: String,
19
+ val fileName: String,
20
+ val mimeType: String?,
21
+ val uri: String,
22
+ ) : StreamMultipartUploadPart
23
+
24
+ data class StreamMultipartTextPart(
25
+ override val fieldName: String,
26
+ val value: String,
27
+ ) : StreamMultipartUploadPart
28
+
29
+ data class StreamMultipartUploadProgressOptions(
30
+ val count: Int?,
31
+ val intervalMs: Long?,
32
+ )
33
+
34
+ data class StreamMultipartUploadResponse(
35
+ val body: String,
36
+ val headers: Map<String, String>,
37
+ val status: Int,
38
+ val statusText: String?,
39
+ )
@@ -0,0 +1,80 @@
1
+ package com.streamchatreactnative.shared.upload
2
+
3
+ import android.os.SystemClock
4
+ import okhttp3.RequestBody
5
+ import okio.Buffer
6
+ import okio.BufferedSink
7
+ import okio.ForwardingSink
8
+ import okio.Sink
9
+ import okio.buffer
10
+ import kotlin.math.floor
11
+
12
+ class StreamMultipartUploadProgressThrottler(
13
+ options: StreamMultipartUploadProgressOptions?,
14
+ private val onProgress: (loaded: Long, total: Long?) -> Unit,
15
+ ) {
16
+ private val intervalMs = (options?.intervalMs ?: 16L).coerceIn(16L, 1_000L)
17
+ private val count = (options?.count ?: 20).coerceIn(1, 100)
18
+ private var emittedBuckets = -1
19
+ private var lastEventAtMs = 0L
20
+
21
+ fun dispatch(loaded: Long, total: Long?) {
22
+ val now = SystemClock.elapsedRealtime()
23
+ val isTerminal = total != null && total >= 0 && loaded >= total
24
+
25
+ if (isTerminal) {
26
+ onProgress(loaded, total)
27
+ return
28
+ }
29
+
30
+ val passesInterval = now - lastEventAtMs >= intervalMs
31
+ val passesCount =
32
+ if (count > 0 && total != null && total > 0) {
33
+ val nextBucket = floor((loaded.toDouble() / total.toDouble()) * count.toDouble()).toInt()
34
+ if (nextBucket > emittedBuckets) {
35
+ emittedBuckets = nextBucket
36
+ true
37
+ } else {
38
+ false
39
+ }
40
+ } else {
41
+ true
42
+ }
43
+
44
+ if (!passesInterval || !passesCount) {
45
+ return
46
+ }
47
+
48
+ lastEventAtMs = now
49
+ onProgress(loaded, total)
50
+ }
51
+ }
52
+
53
+ class StreamMultipartUploadProgressRequestBody(
54
+ private val requestBody: RequestBody,
55
+ private val throttler: StreamMultipartUploadProgressThrottler,
56
+ ) : RequestBody() {
57
+ private val resolvedContentLength by lazy { requestBody.contentLength().takeIf { it >= 0L } }
58
+
59
+ override fun contentLength(): Long = requestBody.contentLength()
60
+
61
+ override fun contentType() = requestBody.contentType()
62
+
63
+ override fun writeTo(sink: BufferedSink) {
64
+ val countingSink =
65
+ object : ForwardingSink(sink as Sink) {
66
+ private var bytesWritten = 0L
67
+
68
+ override fun write(source: Buffer, byteCount: Long) {
69
+ super.write(source, byteCount)
70
+
71
+ bytesWritten += byteCount
72
+ throttler.dispatch(bytesWritten, resolvedContentLength)
73
+ }
74
+ }
75
+
76
+ val bufferedSink = countingSink.buffer()
77
+ requestBody.writeTo(bufferedSink)
78
+ bufferedSink.flush()
79
+ }
80
+ }
@@ -0,0 +1,110 @@
1
+ package com.streamchatreactnative.shared.upload
2
+
3
+ import com.facebook.react.bridge.ReadableArray
4
+ import com.facebook.react.bridge.ReadableMap
5
+ import com.facebook.react.bridge.ReadableType
6
+
7
+ object StreamMultipartUploadRequestParser {
8
+ fun parse(
9
+ uploadId: String,
10
+ url: String,
11
+ method: String,
12
+ headers: ReadableArray,
13
+ parts: ReadableArray,
14
+ progress: ReadableMap?,
15
+ timeoutMs: Double?,
16
+ ): StreamMultipartUploadRequest {
17
+ return StreamMultipartUploadRequest(
18
+ headers = headers.toStringMap(),
19
+ method = method,
20
+ parts = parts.toUploadParts(),
21
+ progress = progress?.toProgressOptions(),
22
+ timeoutMs = timeoutMs?.toLong()?.takeIf { it > 0L },
23
+ uploadId = uploadId,
24
+ url = url,
25
+ )
26
+ }
27
+
28
+ private fun ReadableArray.toUploadParts(): List<StreamMultipartUploadPart> {
29
+ val parsedParts = mutableListOf<StreamMultipartUploadPart>()
30
+
31
+ for (index in 0 until size()) {
32
+ val part = getMap(index) ?: throw IllegalArgumentException("Missing multipart part at index $index")
33
+ val fieldName =
34
+ part.getString("fieldName") ?: throw IllegalArgumentException("Multipart part $index is missing fieldName")
35
+ val kind =
36
+ part.getString("kind") ?: throw IllegalArgumentException("Multipart part $index is missing kind")
37
+
38
+ when (kind) {
39
+ "file" -> {
40
+ val uri =
41
+ part.getString("uri") ?: throw IllegalArgumentException("Multipart file part $index is missing uri")
42
+ val fileName =
43
+ part.getString("fileName")
44
+ ?: throw IllegalArgumentException("Multipart file part $index is missing fileName")
45
+
46
+ parsedParts += StreamMultipartFilePart(
47
+ fieldName = fieldName,
48
+ fileName = fileName,
49
+ mimeType = part.getString("mimeType"),
50
+ uri = uri,
51
+ )
52
+ }
53
+
54
+ "text" -> {
55
+ val value =
56
+ part.getString("value") ?: throw IllegalArgumentException("Multipart text part $index is missing value")
57
+ parsedParts += StreamMultipartTextPart(fieldName = fieldName, value = value)
58
+ }
59
+
60
+ else -> throw IllegalArgumentException("Unsupported multipart part kind: $kind")
61
+ }
62
+ }
63
+
64
+ if (parsedParts.none { it is StreamMultipartFilePart }) {
65
+ throw IllegalArgumentException("Multipart upload must contain at least one file part")
66
+ }
67
+
68
+ return parsedParts
69
+ }
70
+
71
+ private fun ReadableArray.toStringMap(): Map<String, String> {
72
+ val parsed = mutableMapOf<String, String>()
73
+
74
+ for (index in 0 until size()) {
75
+ val header = getMap(index) ?: throw IllegalArgumentException("Missing multipart header at index $index")
76
+ val name =
77
+ header.getString("name") ?: throw IllegalArgumentException("Multipart header $index is missing name")
78
+ if (header.getType("value") == ReadableType.Null) {
79
+ continue
80
+ }
81
+ val value =
82
+ header.getString("value")
83
+ ?: header.getDynamic("value").asString()
84
+ ?: throw IllegalArgumentException("Multipart header $index is missing value")
85
+ parsed[name] = value
86
+ }
87
+
88
+ return parsed
89
+ }
90
+
91
+ private fun ReadableMap.toProgressOptions(): StreamMultipartUploadProgressOptions {
92
+ val count =
93
+ if (hasKey("count") && !isNull("count")) {
94
+ getDouble("count").toInt().coerceIn(1, 100)
95
+ } else {
96
+ null
97
+ }
98
+ val intervalMs =
99
+ if (hasKey("intervalMs") && !isNull("intervalMs")) {
100
+ getDouble("intervalMs").toLong().coerceIn(16L, 1_000L)
101
+ } else {
102
+ null
103
+ }
104
+
105
+ return StreamMultipartUploadProgressOptions(
106
+ count = count,
107
+ intervalMs = intervalMs,
108
+ )
109
+ }
110
+ }
@@ -0,0 +1,99 @@
1
+ package com.streamchatreactnative.shared.upload
2
+
3
+ import android.content.Context
4
+ import android.database.Cursor
5
+ import android.net.Uri
6
+ import android.provider.OpenableColumns
7
+ import java.io.File
8
+ import java.io.FileInputStream
9
+ import java.io.InputStream
10
+ import java.net.URLConnection
11
+
12
+ object StreamMultipartUploadSourceResolver {
13
+ fun contentLength(context: Context, uriString: String): Long? {
14
+ val uri = normalizeUri(uriString)
15
+
16
+ return when (uri.scheme?.lowercase()) {
17
+ null, "file" -> {
18
+ val file = toFile(uri, uriString)
19
+ if (!file.exists()) {
20
+ throw IllegalArgumentException("File does not exist for upload: $uriString")
21
+ }
22
+ file.length()
23
+ }
24
+
25
+ "content" -> {
26
+ context.contentResolver.openAssetFileDescriptor(uri, "r")?.use { descriptor ->
27
+ descriptor.length.takeIf { it >= 0L }
28
+ } ?: queryLongColumn(context, uri, OpenableColumns.SIZE)
29
+ }
30
+
31
+ else -> throw IllegalArgumentException("Unsupported upload URI scheme: ${uri.scheme}")
32
+ }
33
+ }
34
+
35
+ fun mimeType(context: Context, part: StreamMultipartFilePart): String {
36
+ val explicitMimeType = part.mimeType?.takeIf { it.isNotBlank() }
37
+ if (explicitMimeType != null) {
38
+ return explicitMimeType
39
+ }
40
+
41
+ val uri = normalizeUri(part.uri)
42
+ val contentResolverMime = context.contentResolver.getType(uri)
43
+ if (!contentResolverMime.isNullOrBlank()) {
44
+ return contentResolverMime
45
+ }
46
+
47
+ return URLConnection.guessContentTypeFromName(part.fileName) ?: "application/octet-stream"
48
+ }
49
+
50
+ fun openInputStream(context: Context, uriString: String): InputStream {
51
+ val uri = normalizeUri(uriString)
52
+
53
+ return when (uri.scheme?.lowercase()) {
54
+ null, "file" -> FileInputStream(toFile(uri, uriString))
55
+ "content" ->
56
+ context.contentResolver.openInputStream(uri)
57
+ ?: throw IllegalArgumentException("Failed to open content URI for upload: $uriString")
58
+ else -> throw IllegalArgumentException("Unsupported upload URI scheme: ${uri.scheme}")
59
+ }
60
+ }
61
+
62
+ private fun normalizeUri(uriString: String): Uri {
63
+ if (uriString.startsWith("/")) {
64
+ return Uri.fromFile(File(uriString))
65
+ }
66
+
67
+ val parsed = Uri.parse(uriString)
68
+
69
+ if (parsed.scheme.isNullOrBlank()) {
70
+ return Uri.fromFile(File(uriString))
71
+ }
72
+
73
+ return parsed
74
+ }
75
+
76
+ private fun queryLongColumn(context: Context, uri: Uri, columnName: String): Long? {
77
+ val projection = arrayOf(columnName)
78
+ val cursor: Cursor =
79
+ context.contentResolver.query(uri, projection, null, null, null) ?: return null
80
+
81
+ cursor.use {
82
+ if (!it.moveToFirst()) {
83
+ return null
84
+ }
85
+
86
+ val columnIndex = it.getColumnIndex(columnName)
87
+ if (columnIndex == -1 || it.isNull(columnIndex)) {
88
+ return null
89
+ }
90
+
91
+ return it.getLong(columnIndex)
92
+ }
93
+ }
94
+
95
+ private fun toFile(uri: Uri, original: String): File {
96
+ val path = uri.path ?: original
97
+ return File(path)
98
+ }
99
+ }
@@ -0,0 +1,138 @@
1
+ package com.streamchatreactnative.shared.upload
2
+
3
+ import android.content.Context
4
+ import okhttp3.Call
5
+ import okhttp3.MultipartBody
6
+ import okhttp3.OkHttpClient
7
+ import okhttp3.Request
8
+ import okhttp3.RequestBody
9
+ import okhttp3.ResponseBody
10
+ import java.io.InterruptedIOException
11
+ import java.io.IOException
12
+ import java.util.concurrent.ConcurrentHashMap
13
+ import java.util.concurrent.TimeUnit
14
+
15
+ object StreamMultipartUploader {
16
+ private val client: OkHttpClient = OkHttpClient.Builder().retryOnConnectionFailure(true).build()
17
+ private const val MAX_RESPONSE_BODY_BYTES = 1_048_576L
18
+ private val cancelledUploadIds = ConcurrentHashMap.newKeySet<String>()
19
+ private val inFlightCalls = ConcurrentHashMap<String, Call>()
20
+
21
+ fun cancel(uploadId: String) {
22
+ cancelledUploadIds.add(uploadId)
23
+ inFlightCalls.remove(uploadId)?.cancel()
24
+ }
25
+
26
+ fun upload(
27
+ context: Context,
28
+ request: StreamMultipartUploadRequest,
29
+ onProgress: (loaded: Long, total: Long?) -> Unit,
30
+ ): StreamMultipartUploadResponse {
31
+ if (cancelledUploadIds.contains(request.uploadId)) {
32
+ cancelledUploadIds.remove(request.uploadId)
33
+ throw InterruptedIOException("Request aborted")
34
+ }
35
+
36
+ val httpRequest = createRequest(context, request, onProgress)
37
+ val call = clientFor(request).newCall(httpRequest)
38
+ val existingCall = inFlightCalls.putIfAbsent(request.uploadId, call)
39
+ if (existingCall != null) {
40
+ throw IllegalStateException("Upload already in flight for id: ${request.uploadId}")
41
+ }
42
+
43
+ try {
44
+ if (cancelledUploadIds.remove(request.uploadId)) {
45
+ call.cancel()
46
+ }
47
+
48
+ call.execute().use { response ->
49
+ return StreamMultipartUploadResponse(
50
+ body = readResponseBody(response.body),
51
+ headers =
52
+ response.headers.names().associateWith { name ->
53
+ response.headers(name).joinToString(", ")
54
+ },
55
+ status = response.code,
56
+ statusText = response.message,
57
+ )
58
+ }
59
+ } finally {
60
+ inFlightCalls.remove(request.uploadId, call)
61
+ cancelledUploadIds.remove(request.uploadId)
62
+ }
63
+ }
64
+
65
+ private fun clientFor(request: StreamMultipartUploadRequest): OkHttpClient {
66
+ val timeoutMs = request.timeoutMs ?: return client
67
+ return client.newBuilder()
68
+ .callTimeout(timeoutMs, TimeUnit.MILLISECONDS)
69
+ .build()
70
+ }
71
+
72
+ private fun readResponseBody(body: ResponseBody?): String {
73
+ if (body == null) {
74
+ return ""
75
+ }
76
+
77
+ val source = body.source()
78
+ source.request(MAX_RESPONSE_BODY_BYTES + 1L)
79
+ val buffer = source.buffer
80
+
81
+ if (buffer.size > MAX_RESPONSE_BODY_BYTES) {
82
+ throw IOException("Upload response body exceeded $MAX_RESPONSE_BODY_BYTES bytes")
83
+ }
84
+
85
+ return buffer.clone().readString(Charsets.UTF_8)
86
+ }
87
+
88
+ private fun createMultipartBody(
89
+ context: Context,
90
+ request: StreamMultipartUploadRequest,
91
+ onProgress: (loaded: Long, total: Long?) -> Unit,
92
+ ): RequestBody {
93
+ val multipartBodyBuilder = MultipartBody.Builder().setType(MultipartBody.FORM)
94
+
95
+ request.parts.forEach { part ->
96
+ when (part) {
97
+ is StreamMultipartFilePart -> {
98
+ multipartBodyBuilder.addFormDataPart(
99
+ part.fieldName,
100
+ part.fileName,
101
+ StreamMultipartUploadFileRequestBody(context, part),
102
+ )
103
+ }
104
+
105
+ is StreamMultipartTextPart -> {
106
+ multipartBodyBuilder.addFormDataPart(part.fieldName, part.value)
107
+ }
108
+ }
109
+ }
110
+
111
+ val multipartBody = multipartBodyBuilder.build()
112
+ val throttler = StreamMultipartUploadProgressThrottler(request.progress, onProgress)
113
+
114
+ return StreamMultipartUploadProgressRequestBody(multipartBody, throttler)
115
+ }
116
+
117
+ private fun createRequest(
118
+ context: Context,
119
+ request: StreamMultipartUploadRequest,
120
+ onProgress: (loaded: Long, total: Long?) -> Unit,
121
+ ): Request {
122
+ val requestBuilder = Request.Builder().url(request.url)
123
+
124
+ request.headers.forEach { (key, value) ->
125
+ if (
126
+ key.equals("Content-Type", ignoreCase = true) ||
127
+ key.equals("Content-Length", ignoreCase = true)
128
+ ) {
129
+ return@forEach
130
+ }
131
+
132
+ requestBuilder.header(key, value)
133
+ }
134
+
135
+ val body = createMultipartBody(context, request, onProgress)
136
+ return requestBuilder.method(request.method, body).build()
137
+ }
138
+ }
@@ -0,0 +1,122 @@
1
+ package com.streamchatreactnative
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.Promise
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReadableArray
7
+ import com.facebook.react.bridge.ReadableMap
8
+ import com.facebook.react.bridge.UiThreadUtil
9
+ import com.facebook.react.modules.core.DeviceEventManagerModule
10
+ import com.streamchatreactnative.shared.upload.StreamMultipartUploadRequestParser
11
+ import com.streamchatreactnative.shared.upload.StreamMultipartUploader
12
+ import java.util.concurrent.LinkedBlockingQueue
13
+ import java.util.concurrent.ThreadPoolExecutor
14
+ import java.util.concurrent.TimeUnit
15
+
16
+ class StreamMultipartUploaderModule(
17
+ reactContext: ReactApplicationContext,
18
+ ) : NativeStreamMultipartUploaderSpec(reactContext) {
19
+ override fun getName(): String = NAME
20
+
21
+ override fun addListener(eventType: String) = Unit
22
+
23
+ override fun removeListeners(count: Double) = Unit
24
+
25
+ override fun cancelUpload(uploadId: String, promise: Promise) {
26
+ StreamMultipartUploader.cancel(uploadId)
27
+ promise.resolve(null)
28
+ }
29
+
30
+ override fun uploadMultipart(
31
+ uploadId: String,
32
+ url: String,
33
+ method: String,
34
+ headers: ReadableArray,
35
+ parts: ReadableArray,
36
+ progress: ReadableMap?,
37
+ timeoutMs: Double?,
38
+ promise: Promise,
39
+ ) {
40
+ val request =
41
+ try {
42
+ StreamMultipartUploadRequestParser.parse(
43
+ uploadId = uploadId,
44
+ url = url,
45
+ method = method,
46
+ headers = headers,
47
+ parts = parts,
48
+ progress = progress,
49
+ timeoutMs = timeoutMs,
50
+ )
51
+ } catch (error: Throwable) {
52
+ promise.reject("stream_multipart_upload_error", error.message, error)
53
+ return
54
+ }
55
+
56
+ try {
57
+ executor.execute {
58
+ try {
59
+ val response =
60
+ StreamMultipartUploader.upload(reactApplicationContext, request) { loaded, total ->
61
+ emitProgress(uploadId, loaded, total)
62
+ }
63
+
64
+ val payload = Arguments.createMap().apply {
65
+ putString("body", response.body)
66
+ putArray("headers", Arguments.createArray().apply {
67
+ response.headers.forEach { (name, value) ->
68
+ pushMap(
69
+ Arguments.createMap().apply {
70
+ putString("name", name)
71
+ putString("value", value)
72
+ },
73
+ )
74
+ }
75
+ })
76
+ putDouble("status", response.status.toDouble())
77
+ putString("statusText", response.statusText)
78
+ }
79
+ promise.resolve(payload)
80
+ } catch (error: Throwable) {
81
+ promise.reject("stream_multipart_upload_error", error.message, error)
82
+ }
83
+ }
84
+ } catch (error: Throwable) {
85
+ promise.reject("stream_multipart_upload_error", error.message, error)
86
+ }
87
+ }
88
+
89
+ private fun emitProgress(uploadId: String, loaded: Long, total: Long?) {
90
+ UiThreadUtil.runOnUiThread {
91
+ val payload = Arguments.createMap().apply {
92
+ putDouble("loaded", loaded.toDouble())
93
+ if (total != null) {
94
+ putDouble("total", total.toDouble())
95
+ } else {
96
+ putNull("total")
97
+ }
98
+ putString("uploadId", uploadId)
99
+ }
100
+
101
+ reactApplicationContext
102
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
103
+ .emit(PROGRESS_EVENT_NAME, payload)
104
+ }
105
+ }
106
+
107
+ companion object {
108
+ const val NAME = "StreamMultipartUploader"
109
+ private const val PROGRESS_EVENT_NAME = "streamMultipartUploadProgress"
110
+ private val maxConcurrentUploads = Runtime.getRuntime().availableProcessors().coerceIn(2, 4)
111
+ private val executor =
112
+ ThreadPoolExecutor(
113
+ maxConcurrentUploads,
114
+ maxConcurrentUploads,
115
+ 30L,
116
+ TimeUnit.SECONDS,
117
+ LinkedBlockingQueue(64),
118
+ ).apply {
119
+ allowCoreThreadTimeOut(true)
120
+ }
121
+ }
122
+ }