stream-chat-expo 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 (28) hide show
  1. package/android/build.gradle +5 -4
  2. package/android/src/main/java/com/streamchatexpo/StreamChatExpoPackage.java +18 -3
  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/streamchatexpo/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 +88 -0
  27. package/src/optionalDependencies/getPhotos.ts +8 -5
  28. package/src/optionalDependencies/pickDocument.ts +28 -12
@@ -29,8 +29,9 @@ if (isNewArchitectureEnabled()) {
29
29
  def getExtOrIntegerDefault(name) {
30
30
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["StreamChatExpo_" + name]).toInteger()
31
31
  }
32
+ def canonicalProjectDir = projectDir.getCanonicalFile()
32
33
  def localSharedNativeRootDir = new File(projectDir, "src/main/java/com/streamchatreactnative/shared")
33
- def sharedNativeRootDir = new File(projectDir, "../../shared-native/android")
34
+ def sharedNativeRootDir = new File(canonicalProjectDir, "../../shared-native/android")
34
35
  def hasNativeSources = { File dir ->
35
36
  dir.exists() && !fileTree(dir).matching { include "**/*.kt"; include "**/*.java" }.files.isEmpty()
36
37
  }
@@ -88,10 +89,10 @@ tasks.register("syncSharedShimmerSources") {
88
89
  outputs.upToDateWhen { false }
89
90
  doLast {
90
91
  def sourceRootDir = null
91
- if (hasNativeSources(localSharedNativeRootDir)) {
92
- sourceRootDir = localSharedNativeRootDir
93
- } else if (hasNativeSources(sharedNativeRootDir)) {
92
+ if (hasNativeSources(sharedNativeRootDir)) {
94
93
  sourceRootDir = sharedNativeRootDir
94
+ } else if (hasNativeSources(localSharedNativeRootDir)) {
95
+ sourceRootDir = localSharedNativeRootDir
95
96
  }
96
97
 
97
98
  if (sourceRootDir == null) {
@@ -14,12 +14,17 @@ import java.util.List;
14
14
  import java.util.Map;
15
15
 
16
16
  public class StreamChatExpoPackage 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
20
21
  @Override
21
22
  public NativeModule getModule(String name, ReactApplicationContext reactContext) {
22
- if (name.equals(STREAM_VIDEO_THUMBNAIL_MODULE) && BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
23
+ if (name.equals(STREAM_MULTIPART_UPLOADER_MODULE)) {
24
+ return createNewArchModule("com.streamchatexpo.StreamMultipartUploaderModule", reactContext);
25
+ }
26
+
27
+ if (name.equals(STREAM_VIDEO_THUMBNAIL_MODULE)) {
23
28
  return createNewArchModule("com.streamchatexpo.StreamVideoThumbnailModule", reactContext);
24
29
  }
25
30
 
@@ -30,7 +35,17 @@ public class StreamChatExpoPackage extends TurboReactPackage {
30
35
  public ReactModuleInfoProvider getReactModuleInfoProvider() {
31
36
  return () -> {
32
37
  final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
33
- boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
38
+ moduleInfos.put(
39
+ STREAM_MULTIPART_UPLOADER_MODULE,
40
+ new ReactModuleInfo(
41
+ STREAM_MULTIPART_UPLOADER_MODULE,
42
+ STREAM_MULTIPART_UPLOADER_MODULE,
43
+ false, // canOverrideExistingModule
44
+ false, // needsEagerInit
45
+ false, // hasConstants
46
+ false, // isCxxModule
47
+ true // isTurboModule
48
+ ));
34
49
  moduleInfos.put(
35
50
  STREAM_VIDEO_THUMBNAIL_MODULE,
36
51
  new ReactModuleInfo(
@@ -40,7 +55,7 @@ public class StreamChatExpoPackage extends TurboReactPackage {
40
55
  false, // needsEagerInit
41
56
  false, // hasConstants
42
57
  false, // isCxxModule
43
- isTurboModule // isTurboModule
58
+ true // isTurboModule
44
59
  ));
45
60
  return moduleInfos;
46
61
  };
@@ -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.streamchatexpo
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
+ }