@ion299/sdk-react-native 0.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/CHANGELOG.md +17 -0
- package/ChatPlatformSdk.podspec +20 -0
- package/README.md +315 -0
- package/android/build.gradle +54 -0
- package/android/src/main/AndroidManifest.xml +18 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkDownloaderModule.kt +240 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkFilePickerModule.kt +165 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkPackage.kt +15 -0
- package/android/src/main/res/xml/chat_sdk_file_paths.xml +7 -0
- package/ios/ChatSdkDownloader.m +10 -0
- package/ios/ChatSdkDownloader.swift +141 -0
- package/ios/ChatSdkFilePicker.m +9 -0
- package/ios/ChatSdkFilePicker.swift +161 -0
- package/lib/commonjs/ChatSDK.js +193 -0
- package/lib/commonjs/ChatSDK.js.map +1 -0
- package/lib/commonjs/api.js +195 -0
- package/lib/commonjs/api.js.map +1 -0
- package/lib/commonjs/attachmentUtils.js +39 -0
- package/lib/commonjs/attachmentUtils.js.map +1 -0
- package/lib/commonjs/components/AttachmentGallery.js +367 -0
- package/lib/commonjs/components/AttachmentGallery.js.map +1 -0
- package/lib/commonjs/components/ChatScreen.js +286 -0
- package/lib/commonjs/components/ChatScreen.js.map +1 -0
- package/lib/commonjs/components/MessageBubble.js +227 -0
- package/lib/commonjs/components/MessageBubble.js.map +1 -0
- package/lib/commonjs/components/MessageInput.js +273 -0
- package/lib/commonjs/components/MessageInput.js.map +1 -0
- package/lib/commonjs/components/SurveyOverlay.js +499 -0
- package/lib/commonjs/components/SurveyOverlay.js.map +1 -0
- package/lib/commonjs/downloaders/defaultAttachmentDownloader.js +28 -0
- package/lib/commonjs/downloaders/defaultAttachmentDownloader.js.map +1 -0
- package/lib/commonjs/filePicker.js +25 -0
- package/lib/commonjs/filePicker.js.map +1 -0
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/native/NativeChatSdkDownloader.js +28 -0
- package/lib/commonjs/native/NativeChatSdkDownloader.js.map +1 -0
- package/lib/commonjs/native/NativeChatSdkFilePicker.js +17 -0
- package/lib/commonjs/native/NativeChatSdkFilePicker.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/realtime.js +242 -0
- package/lib/commonjs/realtime.js.map +1 -0
- package/lib/commonjs/safeArea.js +18 -0
- package/lib/commonjs/safeArea.js.map +1 -0
- package/lib/commonjs/session.js +159 -0
- package/lib/commonjs/session.js.map +1 -0
- package/lib/commonjs/surveyCache.js +30 -0
- package/lib/commonjs/surveyCache.js.map +1 -0
- package/lib/commonjs/theme.js +29 -0
- package/lib/commonjs/theme.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useChat.js +145 -0
- package/lib/commonjs/useChat.js.map +1 -0
- package/lib/module/ChatSDK.js +189 -0
- package/lib/module/ChatSDK.js.map +1 -0
- package/lib/module/api.js +190 -0
- package/lib/module/api.js.map +1 -0
- package/lib/module/attachmentUtils.js +33 -0
- package/lib/module/attachmentUtils.js.map +1 -0
- package/lib/module/components/AttachmentGallery.js +362 -0
- package/lib/module/components/AttachmentGallery.js.map +1 -0
- package/lib/module/components/ChatScreen.js +281 -0
- package/lib/module/components/ChatScreen.js.map +1 -0
- package/lib/module/components/MessageBubble.js +222 -0
- package/lib/module/components/MessageBubble.js.map +1 -0
- package/lib/module/components/MessageInput.js +268 -0
- package/lib/module/components/MessageInput.js.map +1 -0
- package/lib/module/components/SurveyOverlay.js +494 -0
- package/lib/module/components/SurveyOverlay.js.map +1 -0
- package/lib/module/downloaders/defaultAttachmentDownloader.js +22 -0
- package/lib/module/downloaders/defaultAttachmentDownloader.js.map +1 -0
- package/lib/module/filePicker.js +20 -0
- package/lib/module/filePicker.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/NativeChatSdkDownloader.js +23 -0
- package/lib/module/native/NativeChatSdkDownloader.js.map +1 -0
- package/lib/module/native/NativeChatSdkFilePicker.js +13 -0
- package/lib/module/native/NativeChatSdkFilePicker.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/realtime.js +236 -0
- package/lib/module/realtime.js.map +1 -0
- package/lib/module/safeArea.js +14 -0
- package/lib/module/safeArea.js.map +1 -0
- package/lib/module/session.js +154 -0
- package/lib/module/session.js.map +1 -0
- package/lib/module/surveyCache.js +23 -0
- package/lib/module/surveyCache.js.map +1 -0
- package/lib/module/theme.js +25 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useChat.js +141 -0
- package/lib/module/useChat.js.map +1 -0
- package/lib/typescript/commonjs/ChatSDK.d.ts +49 -0
- package/lib/typescript/commonjs/ChatSDK.d.ts.map +1 -0
- package/lib/typescript/commonjs/api.d.ts +31 -0
- package/lib/typescript/commonjs/api.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachmentUtils.d.ts +12 -0
- package/lib/typescript/commonjs/attachmentUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/AttachmentGallery.d.ts +16 -0
- package/lib/typescript/commonjs/components/AttachmentGallery.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/ChatScreen.d.ts +16 -0
- package/lib/typescript/commonjs/components/ChatScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/MessageBubble.d.ts +12 -0
- package/lib/typescript/commonjs/components/MessageBubble.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/MessageInput.d.ts +14 -0
- package/lib/typescript/commonjs/components/MessageInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/SurveyOverlay.d.ts +13 -0
- package/lib/typescript/commonjs/components/SurveyOverlay.d.ts.map +1 -0
- package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts +3 -0
- package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
- package/lib/typescript/commonjs/filePicker.d.ts +4 -0
- package/lib/typescript/commonjs/filePicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +7 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts +24 -0
- package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts +17 -0
- package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/realtime.d.ts +42 -0
- package/lib/typescript/commonjs/realtime.d.ts.map +1 -0
- package/lib/typescript/commonjs/safeArea.d.ts +4 -0
- package/lib/typescript/commonjs/safeArea.d.ts.map +1 -0
- package/lib/typescript/commonjs/session.d.ts +45 -0
- package/lib/typescript/commonjs/session.d.ts.map +1 -0
- package/lib/typescript/commonjs/surveyCache.d.ts +5 -0
- package/lib/typescript/commonjs/surveyCache.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme.d.ts +21 -0
- package/lib/typescript/commonjs/theme.d.ts.map +1 -0
- package/lib/typescript/commonjs/types.d.ts +156 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/useChat.d.ts +16 -0
- package/lib/typescript/commonjs/useChat.d.ts.map +1 -0
- package/lib/typescript/module/ChatSDK.d.ts +49 -0
- package/lib/typescript/module/ChatSDK.d.ts.map +1 -0
- package/lib/typescript/module/api.d.ts +31 -0
- package/lib/typescript/module/api.d.ts.map +1 -0
- package/lib/typescript/module/attachmentUtils.d.ts +12 -0
- package/lib/typescript/module/attachmentUtils.d.ts.map +1 -0
- package/lib/typescript/module/components/AttachmentGallery.d.ts +16 -0
- package/lib/typescript/module/components/AttachmentGallery.d.ts.map +1 -0
- package/lib/typescript/module/components/ChatScreen.d.ts +16 -0
- package/lib/typescript/module/components/ChatScreen.d.ts.map +1 -0
- package/lib/typescript/module/components/MessageBubble.d.ts +12 -0
- package/lib/typescript/module/components/MessageBubble.d.ts.map +1 -0
- package/lib/typescript/module/components/MessageInput.d.ts +14 -0
- package/lib/typescript/module/components/MessageInput.d.ts.map +1 -0
- package/lib/typescript/module/components/SurveyOverlay.d.ts +13 -0
- package/lib/typescript/module/components/SurveyOverlay.d.ts.map +1 -0
- package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts +3 -0
- package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
- package/lib/typescript/module/filePicker.d.ts +4 -0
- package/lib/typescript/module/filePicker.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +7 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts +24 -0
- package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts.map +1 -0
- package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts +17 -0
- package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/realtime.d.ts +42 -0
- package/lib/typescript/module/realtime.d.ts.map +1 -0
- package/lib/typescript/module/safeArea.d.ts +4 -0
- package/lib/typescript/module/safeArea.d.ts.map +1 -0
- package/lib/typescript/module/session.d.ts +45 -0
- package/lib/typescript/module/session.d.ts.map +1 -0
- package/lib/typescript/module/surveyCache.d.ts +5 -0
- package/lib/typescript/module/surveyCache.d.ts.map +1 -0
- package/lib/typescript/module/theme.d.ts +21 -0
- package/lib/typescript/module/theme.d.ts.map +1 -0
- package/lib/typescript/module/types.d.ts +156 -0
- package/lib/typescript/module/types.d.ts.map +1 -0
- package/lib/typescript/module/useChat.d.ts +16 -0
- package/lib/typescript/module/useChat.d.ts.map +1 -0
- package/package.json +75 -0
- package/react-native.config.js +10 -0
- package/src/ChatSDK.ts +237 -0
- package/src/api.ts +228 -0
- package/src/attachmentUtils.ts +49 -0
- package/src/components/AttachmentGallery.tsx +363 -0
- package/src/components/ChatScreen.tsx +267 -0
- package/src/components/MessageBubble.tsx +208 -0
- package/src/components/MessageInput.tsx +280 -0
- package/src/components/SurveyOverlay.tsx +469 -0
- package/src/downloaders/defaultAttachmentDownloader.ts +27 -0
- package/src/filePicker.ts +22 -0
- package/src/index.ts +30 -0
- package/src/native/NativeChatSdkDownloader.ts +49 -0
- package/src/native/NativeChatSdkFilePicker.ts +30 -0
- package/src/realtime.ts +278 -0
- package/src/safeArea.ts +8 -0
- package/src/session.ts +196 -0
- package/src/surveyCache.ts +24 -0
- package/src/theme.ts +49 -0
- package/src/types.ts +199 -0
- package/src/useChat.ts +190 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
package com.chatplatform.sdk
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.database.Cursor
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import android.provider.OpenableColumns
|
|
9
|
+
import com.facebook.react.bridge.ActivityEventListener
|
|
10
|
+
import com.facebook.react.bridge.Arguments
|
|
11
|
+
import com.facebook.react.bridge.BaseActivityEventListener
|
|
12
|
+
import com.facebook.react.bridge.Promise
|
|
13
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
14
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
15
|
+
import com.facebook.react.bridge.ReactMethod
|
|
16
|
+
import com.facebook.react.bridge.ReadableArray
|
|
17
|
+
import com.facebook.react.bridge.ReadableMap
|
|
18
|
+
import com.facebook.react.bridge.WritableArray
|
|
19
|
+
import com.facebook.react.bridge.WritableNativeArray
|
|
20
|
+
import java.io.File
|
|
21
|
+
import java.io.FileOutputStream
|
|
22
|
+
import java.util.UUID
|
|
23
|
+
|
|
24
|
+
class ChatSdkFilePickerModule(reactContext: ReactApplicationContext) :
|
|
25
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
26
|
+
|
|
27
|
+
override fun getName(): String = NAME
|
|
28
|
+
|
|
29
|
+
private var pendingPromise: Promise? = null
|
|
30
|
+
|
|
31
|
+
private val activityEventListener = object : BaseActivityEventListener() {
|
|
32
|
+
override fun onActivityResult(
|
|
33
|
+
activity: Activity?,
|
|
34
|
+
requestCode: Int,
|
|
35
|
+
resultCode: Int,
|
|
36
|
+
data: Intent?,
|
|
37
|
+
) {
|
|
38
|
+
if (requestCode != REQUEST_CODE) return
|
|
39
|
+
val promise = pendingPromise ?: return
|
|
40
|
+
pendingPromise = null
|
|
41
|
+
|
|
42
|
+
if (resultCode != Activity.RESULT_OK || data == null) {
|
|
43
|
+
promise.resolve(null)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
val context = reactApplicationContext
|
|
49
|
+
val uris = collectUris(data)
|
|
50
|
+
if (uris.isEmpty()) {
|
|
51
|
+
promise.resolve(null)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
val result: WritableArray = WritableNativeArray()
|
|
55
|
+
for (uri in uris) {
|
|
56
|
+
val copy = copyToCache(context, uri) ?: continue
|
|
57
|
+
val map = Arguments.createMap()
|
|
58
|
+
map.putString("uri", Uri.fromFile(copy.file).toString())
|
|
59
|
+
map.putString("name", copy.name)
|
|
60
|
+
map.putString("mime", copy.mime)
|
|
61
|
+
map.putDouble("size", copy.size.toDouble())
|
|
62
|
+
result.pushMap(map)
|
|
63
|
+
}
|
|
64
|
+
promise.resolve(result)
|
|
65
|
+
} catch (e: Throwable) {
|
|
66
|
+
promise.reject("PICKER_ERROR", e.message ?: "Не удалось обработать выбор файлов", e)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
init {
|
|
72
|
+
reactContext.addActivityEventListener(activityEventListener)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ReactMethod
|
|
76
|
+
fun pick(options: ReadableMap, promise: Promise) {
|
|
77
|
+
val activity = currentActivity
|
|
78
|
+
if (activity == null) {
|
|
79
|
+
promise.reject("NO_ACTIVITY", "Нет активной Activity")
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (pendingPromise != null) {
|
|
83
|
+
promise.reject("ALREADY_PICKING", "Пикер файлов уже открыт")
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val multiple = if (options.hasKey("multiple")) options.getBoolean("multiple") else false
|
|
88
|
+
val mimeFilter: Array<String> = readMimeFilter(options)
|
|
89
|
+
|
|
90
|
+
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
91
|
+
addCategory(Intent.CATEGORY_OPENABLE)
|
|
92
|
+
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
|
|
93
|
+
type = if (mimeFilter.size == 1) mimeFilter[0] else "*/*"
|
|
94
|
+
if (mimeFilter.size > 1) putExtra(Intent.EXTRA_MIME_TYPES, mimeFilter)
|
|
95
|
+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
pendingPromise = promise
|
|
99
|
+
try {
|
|
100
|
+
activity.startActivityForResult(intent, REQUEST_CODE)
|
|
101
|
+
} catch (e: Throwable) {
|
|
102
|
+
pendingPromise = null
|
|
103
|
+
promise.reject("PICKER_LAUNCH_FAILED", e.message ?: "Не удалось открыть пикер", e)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun readMimeFilter(options: ReadableMap): Array<String> {
|
|
108
|
+
if (!options.hasKey("mimeFilter")) return arrayOf("*/*")
|
|
109
|
+
val arr: ReadableArray = options.getArray("mimeFilter") ?: return arrayOf("*/*")
|
|
110
|
+
if (arr.size() == 0) return arrayOf("*/*")
|
|
111
|
+
return Array(arr.size()) { i -> arr.getString(i) ?: "*/*" }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private fun collectUris(data: Intent): List<Uri> {
|
|
115
|
+
val clip = data.clipData
|
|
116
|
+
if (clip != null && clip.itemCount > 0) {
|
|
117
|
+
return (0 until clip.itemCount).mapNotNull { clip.getItemAt(it).uri }
|
|
118
|
+
}
|
|
119
|
+
return listOfNotNull(data.data)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private data class CachedFile(val file: File, val name: String, val mime: String, val size: Long)
|
|
123
|
+
|
|
124
|
+
private fun copyToCache(context: Context, uri: Uri): CachedFile? {
|
|
125
|
+
val resolver = context.contentResolver
|
|
126
|
+
val (name, declaredSize) = queryNameAndSize(resolver, uri)
|
|
127
|
+
val mime = resolver.getType(uri) ?: "application/octet-stream"
|
|
128
|
+
val safeName = sanitize(name)
|
|
129
|
+
val targetDir = File(context.cacheDir, "chat-sdk-picker").apply { mkdirs() }
|
|
130
|
+
val target = File(targetDir, "${UUID.randomUUID()}_$safeName")
|
|
131
|
+
|
|
132
|
+
resolver.openInputStream(uri).use { input ->
|
|
133
|
+
if (input == null) return null
|
|
134
|
+
FileOutputStream(target).use { output -> input.copyTo(output) }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
val size = if (declaredSize > 0) declaredSize else target.length()
|
|
138
|
+
return CachedFile(target, safeName, mime, size)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private fun queryNameAndSize(resolver: android.content.ContentResolver, uri: Uri): Pair<String, Long> {
|
|
142
|
+
var name = "file"
|
|
143
|
+
var size = 0L
|
|
144
|
+
val cursor: Cursor? = resolver.query(uri, null, null, null, null)
|
|
145
|
+
cursor?.use { c ->
|
|
146
|
+
if (c.moveToFirst()) {
|
|
147
|
+
val nameIdx = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
148
|
+
val sizeIdx = c.getColumnIndex(OpenableColumns.SIZE)
|
|
149
|
+
if (nameIdx >= 0 && !c.isNull(nameIdx)) name = c.getString(nameIdx) ?: name
|
|
150
|
+
if (sizeIdx >= 0 && !c.isNull(sizeIdx)) size = c.getLong(sizeIdx)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return name to size
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private fun sanitize(name: String): String {
|
|
157
|
+
val cleaned = name.replace(Regex("[/\\\\?%*:|\"<>]"), "_").trim()
|
|
158
|
+
return if (cleaned.isEmpty()) "file" else cleaned
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
companion object {
|
|
162
|
+
const val NAME = "ChatSdkFilePicker"
|
|
163
|
+
private const val REQUEST_CODE = 0x6C1C
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.chatplatform.sdk
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class ChatSdkPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = listOf(
|
|
10
|
+
ChatSdkFilePickerModule(reactContext),
|
|
11
|
+
ChatSdkDownloaderModule(reactContext),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
<cache-path name="chat_sdk_cache" path="." />
|
|
4
|
+
<files-path name="chat_sdk_files" path="." />
|
|
5
|
+
<external-files-path name="chat_sdk_external_files" path="." />
|
|
6
|
+
<external-cache-path name="chat_sdk_external_cache" path="." />
|
|
7
|
+
</paths>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_MODULE(ChatSdkDownloader, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(download:(NSDictionary *)request
|
|
7
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
8
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
9
|
+
|
|
10
|
+
@end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import React
|
|
4
|
+
|
|
5
|
+
@objc(ChatSdkDownloader)
|
|
6
|
+
class ChatSdkDownloader: RCTEventEmitter {
|
|
7
|
+
|
|
8
|
+
private var listenerCount: Int = 0
|
|
9
|
+
private var sessions: [String: URLSessionDownloadTask] = [:]
|
|
10
|
+
|
|
11
|
+
override init() {
|
|
12
|
+
super.init()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@objc override static func requiresMainQueueSetup() -> Bool { return false }
|
|
16
|
+
override func supportedEvents() -> [String]! { return ["ChatSdkDownloadProgress"] }
|
|
17
|
+
override func startObserving() { listenerCount += 1 }
|
|
18
|
+
override func stopObserving() { listenerCount = max(0, listenerCount - 1) }
|
|
19
|
+
|
|
20
|
+
@objc(download:resolver:rejecter:)
|
|
21
|
+
func download(_ request: NSDictionary,
|
|
22
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
23
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
24
|
+
guard let urlString = request["url"] as? String, let url = URL(string: urlString) else {
|
|
25
|
+
reject("INVALID_URL", "URL не задан", nil)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
let filename = sanitize((request["filename"] as? String) ?? "file")
|
|
29
|
+
let mime = (request["mime"] as? String) ?? "application/octet-stream"
|
|
30
|
+
let headers = (request["headers"] as? [String: String]) ?? [:]
|
|
31
|
+
let id = UUID().uuidString
|
|
32
|
+
|
|
33
|
+
var req = URLRequest(url: url)
|
|
34
|
+
for (k, v) in headers { req.setValue(v, forHTTPHeaderField: k) }
|
|
35
|
+
|
|
36
|
+
let delegate = DownloadDelegate(id: id, filename: filename, mime: mime, owner: self) { result in
|
|
37
|
+
switch result {
|
|
38
|
+
case .success(let targetUrl):
|
|
39
|
+
resolve(["id": id, "uri": targetUrl.absoluteString])
|
|
40
|
+
case .failure(let error):
|
|
41
|
+
reject("DOWNLOAD_FAILED", error.localizedDescription, error)
|
|
42
|
+
}
|
|
43
|
+
self.sessions.removeValue(forKey: id)
|
|
44
|
+
}
|
|
45
|
+
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
|
|
46
|
+
let task = session.downloadTask(with: req)
|
|
47
|
+
sessions[id] = task
|
|
48
|
+
task.resume()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fileprivate func emitProgress(id: String, written: Int64, total: Int64) {
|
|
52
|
+
guard listenerCount > 0 else { return }
|
|
53
|
+
sendEvent(withName: "ChatSdkDownloadProgress", body: [
|
|
54
|
+
"id": id,
|
|
55
|
+
"bytesWritten": NSNumber(value: written),
|
|
56
|
+
"totalBytes": NSNumber(value: total),
|
|
57
|
+
])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fileprivate func presentShareSheet(for url: URL) {
|
|
61
|
+
DispatchQueue.main.async {
|
|
62
|
+
guard let root = Self.topViewController() else { return }
|
|
63
|
+
let activity = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
|
64
|
+
if let pop = activity.popoverPresentationController {
|
|
65
|
+
pop.sourceView = root.view
|
|
66
|
+
pop.sourceRect = CGRect(x: root.view.bounds.midX, y: root.view.bounds.midY, width: 0, height: 0)
|
|
67
|
+
pop.permittedArrowDirections = []
|
|
68
|
+
}
|
|
69
|
+
root.present(activity, animated: true)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private func sanitize(_ name: String) -> String {
|
|
74
|
+
let invalid = CharacterSet(charactersIn: "/\\?%*:|\"<>")
|
|
75
|
+
let cleaned = name.components(separatedBy: invalid).joined(separator: "_")
|
|
76
|
+
let trimmed = cleaned.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
77
|
+
return trimmed.isEmpty ? "file" : trimmed
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static func topViewController() -> UIViewController? {
|
|
81
|
+
for scene in UIApplication.shared.connectedScenes {
|
|
82
|
+
if let windowScene = scene as? UIWindowScene {
|
|
83
|
+
for window in windowScene.windows where window.isKeyWindow {
|
|
84
|
+
var top = window.rootViewController
|
|
85
|
+
while let presented = top?.presentedViewController { top = presented }
|
|
86
|
+
return top
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return nil
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private final class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
95
|
+
enum DownloadResult { case success(URL); case failure(Error) }
|
|
96
|
+
|
|
97
|
+
let id: String
|
|
98
|
+
let filename: String
|
|
99
|
+
let mime: String
|
|
100
|
+
weak var owner: ChatSdkDownloader?
|
|
101
|
+
let completion: (DownloadResult) -> Void
|
|
102
|
+
|
|
103
|
+
init(id: String, filename: String, mime: String, owner: ChatSdkDownloader, completion: @escaping (DownloadResult) -> Void) {
|
|
104
|
+
self.id = id
|
|
105
|
+
self.filename = filename
|
|
106
|
+
self.mime = mime
|
|
107
|
+
self.owner = owner
|
|
108
|
+
self.completion = completion
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func urlSession(_ session: URLSession,
|
|
112
|
+
downloadTask: URLSessionDownloadTask,
|
|
113
|
+
didWriteData bytesWritten: Int64,
|
|
114
|
+
totalBytesWritten: Int64,
|
|
115
|
+
totalBytesExpectedToWrite: Int64) {
|
|
116
|
+
owner?.emitProgress(id: id, written: totalBytesWritten, total: totalBytesExpectedToWrite)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func urlSession(_ session: URLSession,
|
|
120
|
+
downloadTask: URLSessionDownloadTask,
|
|
121
|
+
didFinishDownloadingTo location: URL) {
|
|
122
|
+
do {
|
|
123
|
+
let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
124
|
+
let dir = cache.appendingPathComponent("chat-sdk-downloads", isDirectory: true)
|
|
125
|
+
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
126
|
+
let target = dir.appendingPathComponent(filename)
|
|
127
|
+
if FileManager.default.fileExists(atPath: target.path) {
|
|
128
|
+
try FileManager.default.removeItem(at: target)
|
|
129
|
+
}
|
|
130
|
+
try FileManager.default.moveItem(at: location, to: target)
|
|
131
|
+
owner?.presentShareSheet(for: target)
|
|
132
|
+
completion(.success(target))
|
|
133
|
+
} catch {
|
|
134
|
+
completion(.failure(error))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
139
|
+
if let error = error { completion(.failure(error)) }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import MobileCoreServices
|
|
4
|
+
import UniformTypeIdentifiers
|
|
5
|
+
|
|
6
|
+
@objc(ChatSdkFilePicker)
|
|
7
|
+
class ChatSdkFilePicker: NSObject {
|
|
8
|
+
|
|
9
|
+
private var pendingResolve: RCTPromiseResolveBlock?
|
|
10
|
+
private var pendingReject: RCTPromiseRejectBlock?
|
|
11
|
+
private var holder: PickerHolder?
|
|
12
|
+
|
|
13
|
+
@objc static func requiresMainQueueSetup() -> Bool { return true }
|
|
14
|
+
|
|
15
|
+
@objc(pick:resolver:rejecter:)
|
|
16
|
+
func pick(_ options: NSDictionary,
|
|
17
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
18
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
19
|
+
if pendingResolve != nil {
|
|
20
|
+
reject("ALREADY_PICKING", "Пикер файлов уже открыт", nil)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let multiple = (options["multiple"] as? Bool) ?? false
|
|
25
|
+
let mimeFilter = (options["mimeFilter"] as? [String]) ?? []
|
|
26
|
+
|
|
27
|
+
DispatchQueue.main.async {
|
|
28
|
+
guard let root = Self.topViewController() else {
|
|
29
|
+
reject("NO_VIEW_CONTROLLER", "Нет корневого view controller", nil)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
self.pendingResolve = resolve
|
|
34
|
+
self.pendingReject = reject
|
|
35
|
+
|
|
36
|
+
let picker: UIDocumentPickerViewController
|
|
37
|
+
if #available(iOS 14.0, *) {
|
|
38
|
+
let types = Self.contentTypes(from: mimeFilter)
|
|
39
|
+
picker = UIDocumentPickerViewController(forOpeningContentTypes: types, asCopy: true)
|
|
40
|
+
} else {
|
|
41
|
+
let types = mimeFilter.isEmpty ? [String(kUTTypeItem)] : mimeFilter
|
|
42
|
+
picker = UIDocumentPickerViewController(documentTypes: types, in: .import)
|
|
43
|
+
}
|
|
44
|
+
picker.allowsMultipleSelection = multiple
|
|
45
|
+
|
|
46
|
+
let holder = PickerHolder(owner: self)
|
|
47
|
+
self.holder = holder
|
|
48
|
+
picker.delegate = holder
|
|
49
|
+
root.present(picker, animated: true)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fileprivate func finishCancelled() {
|
|
54
|
+
let resolve = pendingResolve
|
|
55
|
+
pendingResolve = nil
|
|
56
|
+
pendingReject = nil
|
|
57
|
+
holder = nil
|
|
58
|
+
resolve?(NSNull())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fileprivate func finishSuccess(urls: [URL]) {
|
|
62
|
+
let resolve = pendingResolve
|
|
63
|
+
let reject = pendingReject
|
|
64
|
+
pendingResolve = nil
|
|
65
|
+
pendingReject = nil
|
|
66
|
+
holder = nil
|
|
67
|
+
|
|
68
|
+
do {
|
|
69
|
+
let items = try urls.map { try Self.copyAndDescribe($0) }
|
|
70
|
+
resolve?(items)
|
|
71
|
+
} catch {
|
|
72
|
+
reject?("PICKER_ERROR", error.localizedDescription, error)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@available(iOS 14.0, *)
|
|
77
|
+
private static func contentTypes(from mimeFilter: [String]) -> [UTType] {
|
|
78
|
+
if mimeFilter.isEmpty { return [.item] }
|
|
79
|
+
let types = mimeFilter.compactMap { UTType(mimeType: $0) }
|
|
80
|
+
return types.isEmpty ? [.item] : types
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static func copyAndDescribe(_ url: URL) throws -> [String: Any] {
|
|
84
|
+
let needsScope = url.startAccessingSecurityScopedResource()
|
|
85
|
+
defer { if needsScope { url.stopAccessingSecurityScopedResource() } }
|
|
86
|
+
|
|
87
|
+
let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
88
|
+
let dir = cache.appendingPathComponent("chat-sdk-picker", isDirectory: true)
|
|
89
|
+
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
90
|
+
|
|
91
|
+
let safeName = sanitize(url.lastPathComponent)
|
|
92
|
+
let target = dir.appendingPathComponent("\(UUID().uuidString)_\(safeName)")
|
|
93
|
+
if FileManager.default.fileExists(atPath: target.path) {
|
|
94
|
+
try FileManager.default.removeItem(at: target)
|
|
95
|
+
}
|
|
96
|
+
try FileManager.default.copyItem(at: url, to: target)
|
|
97
|
+
|
|
98
|
+
let attrs = try FileManager.default.attributesOfItem(atPath: target.path)
|
|
99
|
+
let size = (attrs[.size] as? NSNumber)?.int64Value ?? 0
|
|
100
|
+
|
|
101
|
+
return [
|
|
102
|
+
"uri": target.absoluteString,
|
|
103
|
+
"name": safeName,
|
|
104
|
+
"mime": mimeType(for: url.pathExtension),
|
|
105
|
+
"size": NSNumber(value: size),
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static func sanitize(_ name: String) -> String {
|
|
110
|
+
let invalid = CharacterSet(charactersIn: "/\\?%*:|\"<>")
|
|
111
|
+
let cleaned = name.components(separatedBy: invalid).joined(separator: "_")
|
|
112
|
+
let trimmed = cleaned.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
113
|
+
return trimmed.isEmpty ? "file" : trimmed
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private static func mimeType(for ext: String) -> String {
|
|
117
|
+
if ext.isEmpty { return "application/octet-stream" }
|
|
118
|
+
if #available(iOS 14.0, *) {
|
|
119
|
+
if let t = UTType(filenameExtension: ext), let m = t.preferredMIMEType {
|
|
120
|
+
return m
|
|
121
|
+
}
|
|
122
|
+
return "application/octet-stream"
|
|
123
|
+
}
|
|
124
|
+
let tagClass = kUTTagClassFilenameExtension
|
|
125
|
+
guard
|
|
126
|
+
let uti = UTTypeCreatePreferredIdentifierForTag(tagClass, ext as CFString, nil)?.takeRetainedValue(),
|
|
127
|
+
let mime = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() as String?
|
|
128
|
+
else { return "application/octet-stream" }
|
|
129
|
+
return mime
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private static func topViewController() -> UIViewController? {
|
|
133
|
+
let scenes = UIApplication.shared.connectedScenes
|
|
134
|
+
for scene in scenes {
|
|
135
|
+
if let windowScene = scene as? UIWindowScene {
|
|
136
|
+
for window in windowScene.windows where window.isKeyWindow {
|
|
137
|
+
var top = window.rootViewController
|
|
138
|
+
while let presented = top?.presentedViewController { top = presented }
|
|
139
|
+
return top
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return nil
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private final class PickerHolder: NSObject, UIDocumentPickerDelegate {
|
|
148
|
+
weak var owner: ChatSdkFilePicker?
|
|
149
|
+
|
|
150
|
+
init(owner: ChatSdkFilePicker) {
|
|
151
|
+
self.owner = owner
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
|
155
|
+
owner?.finishSuccess(urls: urls)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
|
159
|
+
owner?.finishCancelled()
|
|
160
|
+
}
|
|
161
|
+
}
|