@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.
Files changed (199) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/ChatPlatformSdk.podspec +20 -0
  3. package/README.md +315 -0
  4. package/android/build.gradle +54 -0
  5. package/android/src/main/AndroidManifest.xml +18 -0
  6. package/android/src/main/java/com/chatplatform/sdk/ChatSdkDownloaderModule.kt +240 -0
  7. package/android/src/main/java/com/chatplatform/sdk/ChatSdkFilePickerModule.kt +165 -0
  8. package/android/src/main/java/com/chatplatform/sdk/ChatSdkPackage.kt +15 -0
  9. package/android/src/main/res/xml/chat_sdk_file_paths.xml +7 -0
  10. package/ios/ChatSdkDownloader.m +10 -0
  11. package/ios/ChatSdkDownloader.swift +141 -0
  12. package/ios/ChatSdkFilePicker.m +9 -0
  13. package/ios/ChatSdkFilePicker.swift +161 -0
  14. package/lib/commonjs/ChatSDK.js +193 -0
  15. package/lib/commonjs/ChatSDK.js.map +1 -0
  16. package/lib/commonjs/api.js +195 -0
  17. package/lib/commonjs/api.js.map +1 -0
  18. package/lib/commonjs/attachmentUtils.js +39 -0
  19. package/lib/commonjs/attachmentUtils.js.map +1 -0
  20. package/lib/commonjs/components/AttachmentGallery.js +367 -0
  21. package/lib/commonjs/components/AttachmentGallery.js.map +1 -0
  22. package/lib/commonjs/components/ChatScreen.js +286 -0
  23. package/lib/commonjs/components/ChatScreen.js.map +1 -0
  24. package/lib/commonjs/components/MessageBubble.js +227 -0
  25. package/lib/commonjs/components/MessageBubble.js.map +1 -0
  26. package/lib/commonjs/components/MessageInput.js +273 -0
  27. package/lib/commonjs/components/MessageInput.js.map +1 -0
  28. package/lib/commonjs/components/SurveyOverlay.js +499 -0
  29. package/lib/commonjs/components/SurveyOverlay.js.map +1 -0
  30. package/lib/commonjs/downloaders/defaultAttachmentDownloader.js +28 -0
  31. package/lib/commonjs/downloaders/defaultAttachmentDownloader.js.map +1 -0
  32. package/lib/commonjs/filePicker.js +25 -0
  33. package/lib/commonjs/filePicker.js.map +1 -0
  34. package/lib/commonjs/index.js +27 -0
  35. package/lib/commonjs/index.js.map +1 -0
  36. package/lib/commonjs/native/NativeChatSdkDownloader.js +28 -0
  37. package/lib/commonjs/native/NativeChatSdkDownloader.js.map +1 -0
  38. package/lib/commonjs/native/NativeChatSdkFilePicker.js +17 -0
  39. package/lib/commonjs/native/NativeChatSdkFilePicker.js.map +1 -0
  40. package/lib/commonjs/package.json +1 -0
  41. package/lib/commonjs/realtime.js +242 -0
  42. package/lib/commonjs/realtime.js.map +1 -0
  43. package/lib/commonjs/safeArea.js +18 -0
  44. package/lib/commonjs/safeArea.js.map +1 -0
  45. package/lib/commonjs/session.js +159 -0
  46. package/lib/commonjs/session.js.map +1 -0
  47. package/lib/commonjs/surveyCache.js +30 -0
  48. package/lib/commonjs/surveyCache.js.map +1 -0
  49. package/lib/commonjs/theme.js +29 -0
  50. package/lib/commonjs/theme.js.map +1 -0
  51. package/lib/commonjs/types.js +2 -0
  52. package/lib/commonjs/types.js.map +1 -0
  53. package/lib/commonjs/useChat.js +145 -0
  54. package/lib/commonjs/useChat.js.map +1 -0
  55. package/lib/module/ChatSDK.js +189 -0
  56. package/lib/module/ChatSDK.js.map +1 -0
  57. package/lib/module/api.js +190 -0
  58. package/lib/module/api.js.map +1 -0
  59. package/lib/module/attachmentUtils.js +33 -0
  60. package/lib/module/attachmentUtils.js.map +1 -0
  61. package/lib/module/components/AttachmentGallery.js +362 -0
  62. package/lib/module/components/AttachmentGallery.js.map +1 -0
  63. package/lib/module/components/ChatScreen.js +281 -0
  64. package/lib/module/components/ChatScreen.js.map +1 -0
  65. package/lib/module/components/MessageBubble.js +222 -0
  66. package/lib/module/components/MessageBubble.js.map +1 -0
  67. package/lib/module/components/MessageInput.js +268 -0
  68. package/lib/module/components/MessageInput.js.map +1 -0
  69. package/lib/module/components/SurveyOverlay.js +494 -0
  70. package/lib/module/components/SurveyOverlay.js.map +1 -0
  71. package/lib/module/downloaders/defaultAttachmentDownloader.js +22 -0
  72. package/lib/module/downloaders/defaultAttachmentDownloader.js.map +1 -0
  73. package/lib/module/filePicker.js +20 -0
  74. package/lib/module/filePicker.js.map +1 -0
  75. package/lib/module/index.js +6 -0
  76. package/lib/module/index.js.map +1 -0
  77. package/lib/module/native/NativeChatSdkDownloader.js +23 -0
  78. package/lib/module/native/NativeChatSdkDownloader.js.map +1 -0
  79. package/lib/module/native/NativeChatSdkFilePicker.js +13 -0
  80. package/lib/module/native/NativeChatSdkFilePicker.js.map +1 -0
  81. package/lib/module/package.json +1 -0
  82. package/lib/module/realtime.js +236 -0
  83. package/lib/module/realtime.js.map +1 -0
  84. package/lib/module/safeArea.js +14 -0
  85. package/lib/module/safeArea.js.map +1 -0
  86. package/lib/module/session.js +154 -0
  87. package/lib/module/session.js.map +1 -0
  88. package/lib/module/surveyCache.js +23 -0
  89. package/lib/module/surveyCache.js.map +1 -0
  90. package/lib/module/theme.js +25 -0
  91. package/lib/module/theme.js.map +1 -0
  92. package/lib/module/types.js +2 -0
  93. package/lib/module/types.js.map +1 -0
  94. package/lib/module/useChat.js +141 -0
  95. package/lib/module/useChat.js.map +1 -0
  96. package/lib/typescript/commonjs/ChatSDK.d.ts +49 -0
  97. package/lib/typescript/commonjs/ChatSDK.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/api.d.ts +31 -0
  99. package/lib/typescript/commonjs/api.d.ts.map +1 -0
  100. package/lib/typescript/commonjs/attachmentUtils.d.ts +12 -0
  101. package/lib/typescript/commonjs/attachmentUtils.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/components/AttachmentGallery.d.ts +16 -0
  103. package/lib/typescript/commonjs/components/AttachmentGallery.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/components/ChatScreen.d.ts +16 -0
  105. package/lib/typescript/commonjs/components/ChatScreen.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/components/MessageBubble.d.ts +12 -0
  107. package/lib/typescript/commonjs/components/MessageBubble.d.ts.map +1 -0
  108. package/lib/typescript/commonjs/components/MessageInput.d.ts +14 -0
  109. package/lib/typescript/commonjs/components/MessageInput.d.ts.map +1 -0
  110. package/lib/typescript/commonjs/components/SurveyOverlay.d.ts +13 -0
  111. package/lib/typescript/commonjs/components/SurveyOverlay.d.ts.map +1 -0
  112. package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts +3 -0
  113. package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/filePicker.d.ts +4 -0
  115. package/lib/typescript/commonjs/filePicker.d.ts.map +1 -0
  116. package/lib/typescript/commonjs/index.d.ts +7 -0
  117. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts +24 -0
  119. package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts +17 -0
  121. package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/package.json +1 -0
  123. package/lib/typescript/commonjs/realtime.d.ts +42 -0
  124. package/lib/typescript/commonjs/realtime.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/safeArea.d.ts +4 -0
  126. package/lib/typescript/commonjs/safeArea.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/session.d.ts +45 -0
  128. package/lib/typescript/commonjs/session.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/surveyCache.d.ts +5 -0
  130. package/lib/typescript/commonjs/surveyCache.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/theme.d.ts +21 -0
  132. package/lib/typescript/commonjs/theme.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/types.d.ts +156 -0
  134. package/lib/typescript/commonjs/types.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/useChat.d.ts +16 -0
  136. package/lib/typescript/commonjs/useChat.d.ts.map +1 -0
  137. package/lib/typescript/module/ChatSDK.d.ts +49 -0
  138. package/lib/typescript/module/ChatSDK.d.ts.map +1 -0
  139. package/lib/typescript/module/api.d.ts +31 -0
  140. package/lib/typescript/module/api.d.ts.map +1 -0
  141. package/lib/typescript/module/attachmentUtils.d.ts +12 -0
  142. package/lib/typescript/module/attachmentUtils.d.ts.map +1 -0
  143. package/lib/typescript/module/components/AttachmentGallery.d.ts +16 -0
  144. package/lib/typescript/module/components/AttachmentGallery.d.ts.map +1 -0
  145. package/lib/typescript/module/components/ChatScreen.d.ts +16 -0
  146. package/lib/typescript/module/components/ChatScreen.d.ts.map +1 -0
  147. package/lib/typescript/module/components/MessageBubble.d.ts +12 -0
  148. package/lib/typescript/module/components/MessageBubble.d.ts.map +1 -0
  149. package/lib/typescript/module/components/MessageInput.d.ts +14 -0
  150. package/lib/typescript/module/components/MessageInput.d.ts.map +1 -0
  151. package/lib/typescript/module/components/SurveyOverlay.d.ts +13 -0
  152. package/lib/typescript/module/components/SurveyOverlay.d.ts.map +1 -0
  153. package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts +3 -0
  154. package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
  155. package/lib/typescript/module/filePicker.d.ts +4 -0
  156. package/lib/typescript/module/filePicker.d.ts.map +1 -0
  157. package/lib/typescript/module/index.d.ts +7 -0
  158. package/lib/typescript/module/index.d.ts.map +1 -0
  159. package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts +24 -0
  160. package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts.map +1 -0
  161. package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts +17 -0
  162. package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts.map +1 -0
  163. package/lib/typescript/module/package.json +1 -0
  164. package/lib/typescript/module/realtime.d.ts +42 -0
  165. package/lib/typescript/module/realtime.d.ts.map +1 -0
  166. package/lib/typescript/module/safeArea.d.ts +4 -0
  167. package/lib/typescript/module/safeArea.d.ts.map +1 -0
  168. package/lib/typescript/module/session.d.ts +45 -0
  169. package/lib/typescript/module/session.d.ts.map +1 -0
  170. package/lib/typescript/module/surveyCache.d.ts +5 -0
  171. package/lib/typescript/module/surveyCache.d.ts.map +1 -0
  172. package/lib/typescript/module/theme.d.ts +21 -0
  173. package/lib/typescript/module/theme.d.ts.map +1 -0
  174. package/lib/typescript/module/types.d.ts +156 -0
  175. package/lib/typescript/module/types.d.ts.map +1 -0
  176. package/lib/typescript/module/useChat.d.ts +16 -0
  177. package/lib/typescript/module/useChat.d.ts.map +1 -0
  178. package/package.json +75 -0
  179. package/react-native.config.js +10 -0
  180. package/src/ChatSDK.ts +237 -0
  181. package/src/api.ts +228 -0
  182. package/src/attachmentUtils.ts +49 -0
  183. package/src/components/AttachmentGallery.tsx +363 -0
  184. package/src/components/ChatScreen.tsx +267 -0
  185. package/src/components/MessageBubble.tsx +208 -0
  186. package/src/components/MessageInput.tsx +280 -0
  187. package/src/components/SurveyOverlay.tsx +469 -0
  188. package/src/downloaders/defaultAttachmentDownloader.ts +27 -0
  189. package/src/filePicker.ts +22 -0
  190. package/src/index.ts +30 -0
  191. package/src/native/NativeChatSdkDownloader.ts +49 -0
  192. package/src/native/NativeChatSdkFilePicker.ts +30 -0
  193. package/src/realtime.ts +278 -0
  194. package/src/safeArea.ts +8 -0
  195. package/src/session.ts +196 -0
  196. package/src/surveyCache.ts +24 -0
  197. package/src/theme.ts +49 -0
  198. package/src/types.ts +199 -0
  199. 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,9 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(ChatSdkFilePicker, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(pick:(NSDictionary *)options
6
+ resolver:(RCTPromiseResolveBlock)resolve
7
+ rejecter:(RCTPromiseRejectBlock)reject)
8
+
9
+ @end
@@ -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
+ }