@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,363 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
2
+ import {
3
+ ActivityIndicator,
4
+ Alert,
5
+ Dimensions,
6
+ FlatList,
7
+ Modal,
8
+ Platform,
9
+ SafeAreaView,
10
+ StatusBar,
11
+ StyleSheet,
12
+ Text,
13
+ TouchableOpacity,
14
+ View,
15
+ Image,
16
+ } from 'react-native'
17
+ import { attachmentDisplayName, type AttachmentDownloadHandler } from '../attachmentUtils'
18
+ import type { GalleryAttachment } from '../types'
19
+ import type { ChatTheme } from '../theme'
20
+
21
+ const { width: SW } = Dimensions.get('window')
22
+
23
+ interface Props {
24
+ attachments: GalleryAttachment[]
25
+ initialIndex: number
26
+ visible: boolean
27
+ onClose: () => void
28
+ downloadLabel?: string
29
+ onDownloadAttachment: AttachmentDownloadHandler
30
+ theme: ChatTheme
31
+ }
32
+
33
+ function formatBytes(bytes: number): string {
34
+ if (bytes <= 0) return ''
35
+ if (bytes < 1024) return `${bytes} Б`
36
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ`
37
+ return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`
38
+ }
39
+
40
+ function GallerySlide({ item, isVisible }: { item: GalleryAttachment; isVisible: boolean }) {
41
+ const [loading, setLoading] = useState(true)
42
+ const displayName = attachmentDisplayName(item)
43
+
44
+ if (item.type === 'image') {
45
+ return (
46
+ <View style={slide.root}>
47
+ {loading && isVisible && (
48
+ <ActivityIndicator color="#fff" size="large" style={StyleSheet.absoluteFill} />
49
+ )}
50
+ <Image
51
+ source={{ uri: item.url }}
52
+ style={slide.image}
53
+ resizeMode="contain"
54
+ onLoadStart={() => setLoading(true)}
55
+ onLoadEnd={() => setLoading(false)}
56
+ />
57
+ </View>
58
+ )
59
+ }
60
+
61
+ return (
62
+ <View style={[slide.root, slide.doc]}>
63
+ <Text style={slide.docName} numberOfLines={6}>{displayName}</Text>
64
+ {item.size > 0 && (
65
+ <Text style={slide.docSize}>{formatBytes(item.size)}</Text>
66
+ )}
67
+ </View>
68
+ )
69
+ }
70
+
71
+ export function AttachmentGallery({
72
+ attachments,
73
+ initialIndex,
74
+ visible,
75
+ onClose,
76
+ downloadLabel = 'Скачать',
77
+ onDownloadAttachment,
78
+ theme,
79
+ }: Props) {
80
+ const listRef = useRef<FlatList<GalleryAttachment>>(null)
81
+ const [currentIndex, setCurrentIndex] = useState(initialIndex)
82
+ const [isDownloading, setIsDownloading] = useState(false)
83
+
84
+ const current = attachments[currentIndex]
85
+
86
+ useEffect(() => {
87
+ if (visible) {
88
+ setCurrentIndex(initialIndex)
89
+ }
90
+ }, [visible, initialIndex])
91
+
92
+ const handleViewableChange = useCallback(
93
+ ({ viewableItems }: { viewableItems: Array<{ index: number | null }> }) => {
94
+ if (viewableItems[0]?.index != null) {
95
+ setCurrentIndex(viewableItems[0].index)
96
+ }
97
+ },
98
+ [],
99
+ )
100
+
101
+ const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 50 })
102
+
103
+ const scrollTo = (index: number) => {
104
+ listRef.current?.scrollToIndex({ index, animated: true })
105
+ setCurrentIndex(index)
106
+ }
107
+
108
+ const handleDownload = async () => {
109
+ if (!current || isDownloading) return
110
+
111
+ setIsDownloading(true)
112
+ try {
113
+ await onDownloadAttachment(current)
114
+ } catch (e) {
115
+ const message = e instanceof Error ? e.message : 'Не удалось скачать файл'
116
+ Alert.alert('Ошибка', message)
117
+ } finally {
118
+ setIsDownloading(false)
119
+ }
120
+ }
121
+
122
+ const renderItem = ({ item, index }: { item: GalleryAttachment; index: number }) => (
123
+ <GallerySlide item={item} isVisible={index === currentIndex} />
124
+ )
125
+
126
+ const counterText = attachments.length > 1 ? `${currentIndex + 1} / ${attachments.length}` : ''
127
+
128
+ return (
129
+ <Modal
130
+ visible={visible}
131
+ transparent={false}
132
+ animationType="fade"
133
+ onRequestClose={onClose}
134
+ statusBarTranslucent
135
+ >
136
+ <View style={styles.root}>
137
+ {Platform.OS === 'android' && (
138
+ <StatusBar barStyle="light-content" backgroundColor="#000" translucent />
139
+ )}
140
+
141
+ <SafeAreaView style={styles.headerSafe}>
142
+ <View style={styles.header}>
143
+ <View style={styles.headerSide} />
144
+ <Text style={styles.counter}>{counterText}</Text>
145
+ <TouchableOpacity style={styles.closeBtn} onPress={onClose} hitSlop={12}>
146
+ <Text style={styles.closeIcon}>✕</Text>
147
+ </TouchableOpacity>
148
+ </View>
149
+ </SafeAreaView>
150
+
151
+ <View style={styles.listWrap}>
152
+ <FlatList
153
+ ref={listRef}
154
+ data={attachments}
155
+ keyExtractor={(a) => String(a.id)}
156
+ renderItem={renderItem}
157
+ horizontal
158
+ pagingEnabled
159
+ showsHorizontalScrollIndicator={false}
160
+ initialScrollIndex={initialIndex}
161
+ getItemLayout={(_, index) => ({ length: SW, offset: SW * index, index })}
162
+ onViewableItemsChanged={handleViewableChange}
163
+ viewabilityConfig={viewabilityConfig.current}
164
+ style={styles.list}
165
+ />
166
+
167
+ {attachments.length > 1 && currentIndex > 0 && (
168
+ <TouchableOpacity
169
+ style={[styles.navBtn, styles.navLeft]}
170
+ onPress={() => scrollTo(currentIndex - 1)}
171
+ activeOpacity={0.7}
172
+ >
173
+ <Text style={styles.navIcon}>‹</Text>
174
+ </TouchableOpacity>
175
+ )}
176
+ {attachments.length > 1 && currentIndex < attachments.length - 1 && (
177
+ <TouchableOpacity
178
+ style={[styles.navBtn, styles.navRight]}
179
+ onPress={() => scrollTo(currentIndex + 1)}
180
+ activeOpacity={0.7}
181
+ >
182
+ <Text style={styles.navIcon}>›</Text>
183
+ </TouchableOpacity>
184
+ )}
185
+ </View>
186
+
187
+ {current && (
188
+ <SafeAreaView style={styles.footerSafe}>
189
+ <View style={styles.footer}>
190
+ <View style={styles.meta}>
191
+ <Text style={styles.metaName} numberOfLines={1}>
192
+ {attachmentDisplayName(current)}
193
+ </Text>
194
+ <Text style={styles.metaInfo}>
195
+ {[formatBytes(current.size), current.messageTime].filter(Boolean).join(' · ')}
196
+ </Text>
197
+ </View>
198
+ <TouchableOpacity
199
+ style={[styles.downloadBtn, { backgroundColor: theme.primaryColor }]}
200
+ onPress={() => void handleDownload()}
201
+ activeOpacity={0.85}
202
+ disabled={isDownloading}
203
+ >
204
+ {isDownloading ? (
205
+ <ActivityIndicator color="#fff" size="small" />
206
+ ) : (
207
+ <>
208
+ <Text style={styles.downloadIcon}>↓</Text>
209
+ <Text style={styles.downloadText}>{downloadLabel}</Text>
210
+ </>
211
+ )}
212
+ </TouchableOpacity>
213
+ </View>
214
+ </SafeAreaView>
215
+ )}
216
+ </View>
217
+ </Modal>
218
+ )
219
+ }
220
+
221
+ const slide = StyleSheet.create({
222
+ root: {
223
+ width: SW,
224
+ flex: 1,
225
+ alignItems: 'center',
226
+ justifyContent: 'center',
227
+ },
228
+ image: {
229
+ width: SW,
230
+ height: '100%',
231
+ },
232
+ doc: {
233
+ paddingHorizontal: 32,
234
+ gap: 12,
235
+ },
236
+ docName: {
237
+ color: '#fff',
238
+ fontSize: 20,
239
+ fontWeight: '600',
240
+ textAlign: 'center',
241
+ lineHeight: 28,
242
+ },
243
+ docSize: {
244
+ color: 'rgba(255,255,255,0.7)',
245
+ fontSize: 15,
246
+ textAlign: 'center',
247
+ },
248
+ })
249
+
250
+ const styles = StyleSheet.create({
251
+ root: {
252
+ flex: 1,
253
+ backgroundColor: '#000',
254
+ },
255
+ headerSafe: {
256
+ backgroundColor: 'rgba(0,0,0,0.85)',
257
+ borderBottomWidth: StyleSheet.hairlineWidth,
258
+ borderBottomColor: 'rgba(255,255,255,0.15)',
259
+ },
260
+ header: {
261
+ flexDirection: 'row',
262
+ alignItems: 'center',
263
+ paddingHorizontal: 16,
264
+ paddingVertical: 10,
265
+ },
266
+ headerSide: {
267
+ width: 40,
268
+ },
269
+ counter: {
270
+ flex: 1,
271
+ color: '#fff',
272
+ fontSize: 16,
273
+ fontWeight: '600',
274
+ textAlign: 'center',
275
+ },
276
+ closeBtn: {
277
+ width: 40,
278
+ height: 40,
279
+ borderRadius: 20,
280
+ backgroundColor: '#fff',
281
+ alignItems: 'center',
282
+ justifyContent: 'center',
283
+ },
284
+ closeIcon: {
285
+ color: '#111',
286
+ fontSize: 18,
287
+ fontWeight: '700',
288
+ lineHeight: 20,
289
+ },
290
+ listWrap: {
291
+ flex: 1,
292
+ },
293
+ list: {
294
+ flex: 1,
295
+ },
296
+ navBtn: {
297
+ position: 'absolute',
298
+ top: 0,
299
+ bottom: 0,
300
+ width: 56,
301
+ alignItems: 'center',
302
+ justifyContent: 'center',
303
+ },
304
+ navLeft: { left: 0 },
305
+ navRight: { right: 0 },
306
+ navIcon: {
307
+ color: '#fff',
308
+ fontSize: 40,
309
+ lineHeight: 44,
310
+ textShadowColor: 'rgba(0,0,0,0.8)',
311
+ textShadowOffset: { width: 0, height: 1 },
312
+ textShadowRadius: 6,
313
+ },
314
+ footerSafe: {
315
+ backgroundColor: 'rgba(0,0,0,0.9)',
316
+ borderTopWidth: StyleSheet.hairlineWidth,
317
+ borderTopColor: 'rgba(255,255,255,0.15)',
318
+ },
319
+ footer: {
320
+ flexDirection: 'row',
321
+ alignItems: 'center',
322
+ paddingHorizontal: 20,
323
+ paddingVertical: 16,
324
+ gap: 12,
325
+ },
326
+ meta: {
327
+ flex: 1,
328
+ gap: 4,
329
+ },
330
+ metaName: {
331
+ color: '#fff',
332
+ fontSize: 15,
333
+ fontWeight: '600',
334
+ lineHeight: 20,
335
+ },
336
+ metaInfo: {
337
+ color: 'rgba(255,255,255,0.7)',
338
+ fontSize: 13,
339
+ lineHeight: 18,
340
+ },
341
+ downloadBtn: {
342
+ flexDirection: 'row',
343
+ alignItems: 'center',
344
+ justifyContent: 'center',
345
+ minWidth: 110,
346
+ borderRadius: 20,
347
+ paddingHorizontal: 18,
348
+ paddingVertical: 10,
349
+ gap: 6,
350
+ flexShrink: 0,
351
+ },
352
+ downloadIcon: {
353
+ color: '#fff',
354
+ fontSize: 16,
355
+ fontWeight: 'bold',
356
+ lineHeight: 18,
357
+ },
358
+ downloadText: {
359
+ color: '#fff',
360
+ fontSize: 14,
361
+ fontWeight: '600',
362
+ },
363
+ })
@@ -0,0 +1,267 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
+ import {
3
+ ActivityIndicator,
4
+ FlatList,
5
+ Image,
6
+ KeyboardAvoidingView,
7
+ Platform,
8
+ SafeAreaView,
9
+ StyleSheet,
10
+ Text,
11
+ TouchableOpacity,
12
+ View,
13
+ } from 'react-native'
14
+ import { getAndroidStatusBarHeight } from '../safeArea'
15
+ import { ChatSDK } from '../ChatSDK'
16
+ import { buildTheme, type ChatTheme } from '../theme'
17
+ import { useChat } from '../useChat'
18
+ import type { AttachmentDownloadHandler } from '../attachmentUtils'
19
+ import { defaultAttachmentDownloader } from '../downloaders/defaultAttachmentDownloader'
20
+ import type { AttachmentInput, ChatAttachment, ChatMessage, ChatStrings, GalleryAttachment } from '../types'
21
+ import { MessageBubble } from './MessageBubble'
22
+ import { MessageInput } from './MessageInput'
23
+ import { SurveyOverlay } from './SurveyOverlay'
24
+ import { AttachmentGallery } from './AttachmentGallery'
25
+
26
+ interface Props {
27
+ onClose?: () => void
28
+ theme?: Partial<ChatTheme>
29
+ strings?: ChatStrings
30
+ /** Переопределяет встроенный пикер файлов (react-native-document-picker). */
31
+ onPickFiles?: () => Promise<AttachmentInput[] | null>
32
+ /** Переопределяет встроенное скачивание вложений из галереи. */
33
+ onDownloadAttachment?: AttachmentDownloadHandler
34
+ }
35
+
36
+ const DEFAULT_STRINGS: Required<ChatStrings> = {
37
+ headerTitle: 'Чат',
38
+ emptyStateText: 'Начните диалог — оператор ответит в ближайшее время',
39
+ inputPlaceholder: 'Сообщение…',
40
+ sendingText: 'Отправка…',
41
+ errorRetry: 'Повторить',
42
+ surveyTitle: 'Оцените качество поддержки',
43
+ surveySubmit: 'Отправить',
44
+ surveySkip: 'Пропустить',
45
+ surveyClose: 'Закрыть',
46
+ galleryDownload: 'Скачать',
47
+ }
48
+
49
+ export function ChatScreen({
50
+ onClose,
51
+ theme: themeOverride,
52
+ strings,
53
+ onPickFiles,
54
+ onDownloadAttachment,
55
+ }: Props) {
56
+ const config = ChatSDK.getSDKConfig()
57
+ const baseTheme = buildTheme(config)
58
+ const theme = themeOverride ? { ...baseTheme, ...themeOverride } : baseTheme
59
+ const listRef = useRef<FlatList<ChatMessage>>(null)
60
+
61
+ const t = { ...DEFAULT_STRINGS, ...strings }
62
+
63
+ const {
64
+ messages,
65
+ isLoading,
66
+ isSending,
67
+ error,
68
+ survey,
69
+ sendMessage,
70
+ sendCallback,
71
+ submitSurvey,
72
+ dismissSurvey,
73
+ retry,
74
+ } = useChat()
75
+
76
+ const [galleryIndex, setGalleryIndex] = useState(0)
77
+ const [galleryVisible, setGalleryVisible] = useState(false)
78
+
79
+ const galleryAttachments = useMemo<GalleryAttachment[]>(
80
+ () =>
81
+ messages.flatMap((m) =>
82
+ (m.attachments ?? [])
83
+ .filter((a) => a.id > 0)
84
+ .map((a): GalleryAttachment => ({ ...a, messageTime: m.time })),
85
+ ),
86
+ [messages],
87
+ )
88
+
89
+ const openGallery = (attachment: ChatAttachment) => {
90
+ const idx = galleryAttachments.findIndex((a) => a.id === attachment.id)
91
+ setGalleryIndex(Math.max(0, idx))
92
+ setGalleryVisible(true)
93
+ }
94
+
95
+ const showInitialLoader = isLoading && messages.length === 0
96
+ const title = strings?.headerTitle ?? config?.widgetTitle ?? DEFAULT_STRINGS.headerTitle
97
+
98
+ useEffect(() => {
99
+ if (messages.length > 0) {
100
+ setTimeout(() => listRef.current?.scrollToEnd({ animated: true }), 100)
101
+ }
102
+ }, [messages.length])
103
+
104
+ const headerContent = (
105
+ <>
106
+ {onClose && (
107
+ <TouchableOpacity onPress={onClose} style={styles.backBtn} hitSlop={8}>
108
+ <Text style={[styles.backIcon, { color: theme.headerText }]}>‹</Text>
109
+ </TouchableOpacity>
110
+ )}
111
+ {config?.imageLogo ? (
112
+ <Image source={{ uri: config.imageLogo }} style={styles.logo} />
113
+ ) : null}
114
+ <Text
115
+ style={[styles.headerTitle, { color: theme.headerText }]}
116
+ numberOfLines={2}
117
+ ellipsizeMode="tail"
118
+ >
119
+ {title}
120
+ </Text>
121
+ </>
122
+ )
123
+
124
+ return (
125
+ <View style={[styles.root, { backgroundColor: theme.background }]}>
126
+ <KeyboardAvoidingView
127
+ style={styles.flex}
128
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
129
+ >
130
+ {Platform.OS === 'ios' ? (
131
+ <SafeAreaView style={{ backgroundColor: theme.headerBg }}>
132
+ <View style={styles.header}>{headerContent}</View>
133
+ </SafeAreaView>
134
+ ) : (
135
+ <View
136
+ style={[
137
+ styles.header,
138
+ {
139
+ backgroundColor: theme.headerBg,
140
+ paddingTop: getAndroidStatusBarHeight() + 8,
141
+ },
142
+ ]}
143
+ >
144
+ {headerContent}
145
+ </View>
146
+ )}
147
+
148
+ {error && (
149
+ <View style={styles.errorBanner}>
150
+ <Text style={styles.errorText}>{error}</Text>
151
+ <TouchableOpacity onPress={retry}>
152
+ <Text style={[styles.retryText, { color: theme.primaryColor }]}>
153
+ {t.errorRetry}
154
+ </Text>
155
+ </TouchableOpacity>
156
+ </View>
157
+ )}
158
+
159
+ {showInitialLoader ? (
160
+ <View style={styles.loader}>
161
+ <ActivityIndicator color={theme.primaryColor} size="large" />
162
+ </View>
163
+ ) : (
164
+ <FlatList
165
+ ref={listRef}
166
+ style={styles.list}
167
+ data={messages}
168
+ keyExtractor={(m) => m.id}
169
+ renderItem={({ item }) => (
170
+ <MessageBubble
171
+ message={item}
172
+ theme={theme}
173
+ onButtonPress={(cbData, serverId) => void sendCallback(serverId, cbData)}
174
+ onAttachmentPress={openGallery}
175
+ />
176
+ )}
177
+ contentContainerStyle={[
178
+ styles.messageList,
179
+ messages.length === 0 && styles.messageListEmpty,
180
+ ]}
181
+ showsVerticalScrollIndicator={false}
182
+ ListEmptyComponent={
183
+ <View style={styles.emptyWrap}>
184
+ <Text style={[styles.emptyText, { color: theme.systemText }]}>
185
+ {t.emptyStateText}
186
+ </Text>
187
+ </View>
188
+ }
189
+ />
190
+ )}
191
+
192
+ <MessageInput
193
+ theme={theme}
194
+ isSending={isSending}
195
+ onSend={(text, files) => void sendMessage(text, files)}
196
+ onPickFiles={onPickFiles}
197
+ strings={t}
198
+ />
199
+ </KeyboardAvoidingView>
200
+
201
+ {survey && (
202
+ <SurveyOverlay
203
+ config={survey}
204
+ theme={theme}
205
+ strings={t}
206
+ onSubmit={submitSurvey}
207
+ onDismiss={dismissSurvey}
208
+ />
209
+ )}
210
+
211
+ {galleryAttachments.length > 0 && (
212
+ <AttachmentGallery
213
+ attachments={galleryAttachments}
214
+ initialIndex={galleryIndex}
215
+ visible={galleryVisible}
216
+ onClose={() => setGalleryVisible(false)}
217
+ downloadLabel={t.galleryDownload}
218
+ onDownloadAttachment={onDownloadAttachment ?? defaultAttachmentDownloader}
219
+ theme={theme}
220
+ />
221
+ )}
222
+ </View>
223
+ )
224
+ }
225
+
226
+ const styles = StyleSheet.create({
227
+ root: { flex: 1 },
228
+ flex: { flex: 1 },
229
+ list: { flex: 1 },
230
+ header: {
231
+ flexDirection: 'row',
232
+ alignItems: 'center',
233
+ paddingHorizontal: 16,
234
+ paddingBottom: 14,
235
+ gap: 10,
236
+ elevation: 2,
237
+ shadowColor: '#000',
238
+ shadowOffset: { width: 0, height: 1 },
239
+ shadowOpacity: 0.12,
240
+ shadowRadius: 3,
241
+ },
242
+ backBtn: { padding: 4, flexShrink: 0 },
243
+ backIcon: { fontSize: 28, lineHeight: 28, fontWeight: '300' },
244
+ logo: { width: 32, height: 32, borderRadius: 16, flexShrink: 0 },
245
+ headerTitle: { fontSize: 17, fontWeight: '600', flex: 1, flexShrink: 1, minWidth: 0 },
246
+ messageList: { paddingTop: 12, paddingBottom: 8, flexGrow: 1 },
247
+ messageListEmpty: { justifyContent: 'center' },
248
+ loader: { flex: 1, alignItems: 'center', justifyContent: 'center' },
249
+ emptyWrap: {
250
+ alignItems: 'center',
251
+ justifyContent: 'center',
252
+ paddingHorizontal: 32,
253
+ paddingVertical: 60,
254
+ },
255
+ emptyText: { fontSize: 14, textAlign: 'center', lineHeight: 20 },
256
+ errorBanner: {
257
+ flexDirection: 'row',
258
+ alignItems: 'center',
259
+ justifyContent: 'space-between',
260
+ backgroundColor: '#fff3f3',
261
+ paddingHorizontal: 16,
262
+ paddingVertical: 8,
263
+ gap: 8,
264
+ },
265
+ errorText: { color: '#cc2222', fontSize: 13, flex: 1 },
266
+ retryText: { fontSize: 13, fontWeight: '500' },
267
+ })