@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,208 @@
1
+ import React from 'react'
2
+ import { Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
3
+ import { attachmentDisplayName } from '../attachmentUtils'
4
+ import type { ChatAttachment, ChatButton, ChatMessage } from '../types'
5
+ import type { ChatTheme } from '../theme'
6
+
7
+ interface Props {
8
+ message: ChatMessage
9
+ theme: ChatTheme
10
+ onButtonPress?: (callbackData: string, serverMessageId: number) => void
11
+ onAttachmentPress?: (attachment: ChatAttachment) => void
12
+ }
13
+
14
+ export function MessageBubble({ message, theme, onButtonPress, onAttachmentPress }: Props) {
15
+ if (message.type === 'system' || message.type === 'event') {
16
+ return (
17
+ <View style={styles.systemRow}>
18
+ <Text style={[styles.systemText, { color: theme.systemText }]}>
19
+ {message.text}
20
+ </Text>
21
+ </View>
22
+ )
23
+ }
24
+
25
+ const isOutbound = message.type === 'contact'
26
+
27
+ return (
28
+ <View style={[styles.row, isOutbound ? styles.rowRight : styles.rowLeft]}>
29
+ <View
30
+ style={[
31
+ styles.bubble,
32
+ isOutbound
33
+ ? [styles.outbound, { backgroundColor: theme.outboundBg }]
34
+ : [styles.inbound, { backgroundColor: theme.inboundBg }],
35
+ ]}
36
+ >
37
+ {!isOutbound && message.sender?.name ? (
38
+ <Text style={[styles.senderName, { color: theme.primaryColor }]}>
39
+ {message.sender.name}
40
+ </Text>
41
+ ) : null}
42
+
43
+ {message.attachments && message.attachments.length > 0 && (
44
+ <View style={styles.attachments}>
45
+ {message.attachments.map((att, i) => (
46
+ <AttachmentView
47
+ key={att.id !== 0 ? att.id : `temp-${i}`}
48
+ attachment={att}
49
+ isOutbound={isOutbound}
50
+ theme={theme}
51
+ onPress={att.id > 0 ? () => onAttachmentPress?.(att) : undefined}
52
+ />
53
+ ))}
54
+ </View>
55
+ )}
56
+
57
+ {message.text ? (
58
+ <Text
59
+ style={[
60
+ styles.text,
61
+ { color: isOutbound ? theme.outboundText : theme.inboundText },
62
+ ]}
63
+ >
64
+ {message.text}
65
+ </Text>
66
+ ) : null}
67
+
68
+ {message.buttons && message.buttons.length > 0 && (
69
+ <View style={styles.buttons}>
70
+ {message.buttons.map((btn, i) => (
71
+ <BotButton
72
+ key={i}
73
+ button={btn}
74
+ theme={theme}
75
+ onPress={() =>
76
+ onButtonPress?.(btn.callback_data, message.serverMessageId ?? 0)
77
+ }
78
+ />
79
+ ))}
80
+ </View>
81
+ )}
82
+
83
+ <Text
84
+ style={[
85
+ styles.time,
86
+ { color: isOutbound ? 'rgba(255,255,255,0.65)' : theme.systemText },
87
+ ]}
88
+ >
89
+ {message.time}
90
+ </Text>
91
+ </View>
92
+ </View>
93
+ )
94
+ }
95
+
96
+ function formatBytes(bytes: number): string {
97
+ if (bytes <= 0) return ''
98
+ if (bytes < 1024) return `${bytes} Б`
99
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} КБ`
100
+ return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`
101
+ }
102
+
103
+ function AttachmentView({
104
+ attachment,
105
+ isOutbound,
106
+ theme,
107
+ onPress,
108
+ }: {
109
+ attachment: ChatAttachment
110
+ isOutbound: boolean
111
+ theme: ChatTheme
112
+ onPress?: () => void
113
+ }) {
114
+ if (attachment.type === 'image') {
115
+ return (
116
+ <TouchableOpacity onPress={onPress} disabled={!onPress} activeOpacity={0.85}>
117
+ <Image
118
+ source={{ uri: attachment.url }}
119
+ style={styles.image}
120
+ resizeMode="cover"
121
+ />
122
+ </TouchableOpacity>
123
+ )
124
+ }
125
+
126
+ const nameColor = isOutbound ? theme.outboundText : theme.inboundText
127
+ const sizeColor = isOutbound ? 'rgba(255,255,255,0.65)' : theme.systemText
128
+ const displayName = attachmentDisplayName(attachment)
129
+
130
+ return (
131
+ <TouchableOpacity
132
+ style={styles.fileBlock}
133
+ onPress={onPress}
134
+ disabled={!onPress}
135
+ activeOpacity={0.75}
136
+ >
137
+ <Text style={[styles.fileName, { color: nameColor }]} numberOfLines={2}>
138
+ {displayName}
139
+ </Text>
140
+ {attachment.size > 0 && (
141
+ <Text style={[styles.fileSize, { color: sizeColor }]}>
142
+ {formatBytes(attachment.size)}
143
+ </Text>
144
+ )}
145
+ </TouchableOpacity>
146
+ )
147
+ }
148
+
149
+ function BotButton({
150
+ button,
151
+ theme,
152
+ onPress,
153
+ }: {
154
+ button: ChatButton
155
+ theme: ChatTheme
156
+ onPress: () => void
157
+ }) {
158
+ return (
159
+ <TouchableOpacity
160
+ style={[styles.botButton, { borderColor: theme.primaryColor }]}
161
+ onPress={onPress}
162
+ >
163
+ <Text style={[styles.botButtonText, { color: theme.primaryColor }]}>
164
+ {button.text}
165
+ </Text>
166
+ </TouchableOpacity>
167
+ )
168
+ }
169
+
170
+ const BUBBLE_MAX_WIDTH = Dimensions.get('window').width * 0.78
171
+
172
+ const styles = StyleSheet.create({
173
+ row: { flexDirection: 'row', marginVertical: 3, paddingHorizontal: 16 },
174
+ rowRight: { justifyContent: 'flex-end' },
175
+ rowLeft: { justifyContent: 'flex-start' },
176
+ bubble: {
177
+ maxWidth: BUBBLE_MAX_WIDTH,
178
+ minWidth: 56,
179
+ borderRadius: 16,
180
+ paddingHorizontal: 14,
181
+ paddingTop: 10,
182
+ paddingBottom: 8,
183
+ },
184
+ outbound: { borderBottomRightRadius: 4 },
185
+ inbound: { borderBottomLeftRadius: 4 },
186
+ senderName: { fontSize: 12, fontWeight: '600', marginBottom: 4 },
187
+ text: { fontSize: 15, lineHeight: 21 },
188
+ time: { fontSize: 11, lineHeight: 14, alignSelf: 'flex-end', marginTop: 6 },
189
+ systemRow: { alignItems: 'center', marginVertical: 8, paddingHorizontal: 16 },
190
+ systemText: { fontSize: 12, textAlign: 'center' },
191
+ attachments: { gap: 6, marginBottom: 4 },
192
+ image: { width: 200, height: 150, borderRadius: 8 },
193
+ fileBlock: {
194
+ gap: 2,
195
+ minWidth: 120,
196
+ },
197
+ fileName: { fontSize: 14, fontWeight: '500', lineHeight: 19 },
198
+ fileSize: { fontSize: 11, lineHeight: 15 },
199
+ buttons: { gap: 6, marginTop: 8 },
200
+ botButton: {
201
+ borderWidth: 1.5,
202
+ borderRadius: 8,
203
+ paddingVertical: 8,
204
+ paddingHorizontal: 12,
205
+ alignItems: 'center',
206
+ },
207
+ botButtonText: { fontSize: 14, fontWeight: '500' },
208
+ })
@@ -0,0 +1,280 @@
1
+ import React, { useRef, useState } from 'react'
2
+ import {
3
+ ActivityIndicator,
4
+ Image,
5
+ Platform,
6
+ SafeAreaView,
7
+ ScrollView,
8
+ StyleSheet,
9
+ Text,
10
+ TextInput,
11
+ TouchableOpacity,
12
+ View,
13
+ } from 'react-native'
14
+ import { INPUT_BOTTOM_PADDING } from '../safeArea'
15
+ import { pickFiles as defaultPickFiles } from '../filePicker'
16
+ import type { ChatTheme } from '../theme'
17
+ import type { AttachmentInput, ChatStrings } from '../types'
18
+
19
+ interface Props {
20
+ theme: ChatTheme
21
+ isSending: boolean
22
+ onSend: (text: string, files: AttachmentInput[]) => void
23
+ /** Переопределяет встроенный пикер (@react-native-documents/picker). */
24
+ onPickFiles?: () => Promise<AttachmentInput[] | null>
25
+ strings?: Pick<ChatStrings, 'inputPlaceholder' | 'sendingText'>
26
+ }
27
+
28
+ function docIcon(mime: string): string {
29
+ if (mime === 'application/pdf') return '📄'
30
+ if (mime.includes('word') || mime.includes('document')) return '📝'
31
+ if (mime.includes('sheet') || mime.includes('excel')) return '📊'
32
+ if (mime.startsWith('audio/')) return '🎵'
33
+ if (mime.startsWith('video/')) return '🎬'
34
+ return '📎'
35
+ }
36
+
37
+ export function MessageInput({ theme, isSending, onSend, onPickFiles, strings }: Props) {
38
+ const [text, setText] = useState('')
39
+ const [files, setFiles] = useState<AttachmentInput[]>([])
40
+ const [picking, setPicking] = useState(false)
41
+ const inputRef = useRef<TextInput>(null)
42
+
43
+ const canSend = (text.trim().length > 0 || files.length > 0) && !isSending
44
+ const placeholder = strings?.inputPlaceholder ?? 'Сообщение…'
45
+ const picker = onPickFiles ?? defaultPickFiles
46
+
47
+ const handlePickFiles = async () => {
48
+ if (picking) return
49
+ setPicking(true)
50
+ try {
51
+ const picked = await picker()
52
+ if (picked && picked.length > 0) {
53
+ setFiles((prev) => [...prev, ...picked])
54
+ }
55
+ } finally {
56
+ setPicking(false)
57
+ }
58
+ }
59
+
60
+ const removeFile = (index: number) => {
61
+ setFiles((prev) => prev.filter((_, i) => i !== index))
62
+ }
63
+
64
+ const handleSend = () => {
65
+ if (!canSend) return
66
+ const trimmed = text.trim()
67
+ onSend(trimmed, files)
68
+ setText('')
69
+ setFiles([])
70
+ }
71
+
72
+ const inputBar = (
73
+ <View
74
+ style={[
75
+ styles.container,
76
+ {
77
+ backgroundColor: theme.background,
78
+ borderTopColor: theme.inputBorder,
79
+ paddingBottom: INPUT_BOTTOM_PADDING,
80
+ },
81
+ ]}
82
+ >
83
+ {files.length > 0 && (
84
+ <ScrollView
85
+ horizontal
86
+ showsHorizontalScrollIndicator={false}
87
+ style={styles.filesStrip}
88
+ contentContainerStyle={styles.filesStripContent}
89
+ >
90
+ {files.map((file, i) => (
91
+ <View key={i} style={styles.filePreview}>
92
+ {file.type.startsWith('image/') ? (
93
+ <Image source={{ uri: file.uri }} style={styles.previewImage} />
94
+ ) : (
95
+ <View style={[styles.previewDoc, { backgroundColor: theme.inputBg, borderColor: theme.inputBorder }]}>
96
+ <Text style={styles.previewDocIcon}>{docIcon(file.type)}</Text>
97
+ <Text style={[styles.previewDocName, { color: theme.inputText }]} numberOfLines={2}>
98
+ {file.name}
99
+ </Text>
100
+ </View>
101
+ )}
102
+ <TouchableOpacity
103
+ style={styles.removeBtn}
104
+ onPress={() => removeFile(i)}
105
+ hitSlop={6}
106
+ >
107
+ <Text style={styles.removeBtnText}>✕</Text>
108
+ </TouchableOpacity>
109
+ </View>
110
+ ))}
111
+ </ScrollView>
112
+ )}
113
+
114
+ <View style={styles.inputRow}>
115
+ <TouchableOpacity
116
+ style={[styles.attachBtn, { backgroundColor: theme.inputBg, borderColor: theme.inputBorder }]}
117
+ onPress={handlePickFiles}
118
+ disabled={picking || isSending}
119
+ activeOpacity={0.7}
120
+ >
121
+ {picking ? (
122
+ <ActivityIndicator color={theme.systemText} size="small" />
123
+ ) : (
124
+ <Text style={[styles.attachIcon, { color: theme.primaryColor }]}>⊕</Text>
125
+ )}
126
+ </TouchableOpacity>
127
+
128
+ <View style={[styles.inputWrap, { backgroundColor: theme.inputBg, borderColor: theme.inputBorder }]}>
129
+ <TextInput
130
+ ref={inputRef}
131
+ style={[styles.input, { color: theme.inputText }]}
132
+ placeholder={placeholder}
133
+ placeholderTextColor={theme.systemText}
134
+ multiline
135
+ maxLength={4000}
136
+ value={text}
137
+ onChangeText={setText}
138
+ onSubmitEditing={handleSend}
139
+ returnKeyType="send"
140
+ blurOnSubmit={false}
141
+ />
142
+ </View>
143
+
144
+ <TouchableOpacity
145
+ style={[
146
+ styles.sendBtn,
147
+ { backgroundColor: canSend ? theme.sendButtonBg : theme.inputBorder },
148
+ ]}
149
+ onPress={handleSend}
150
+ disabled={!canSend}
151
+ activeOpacity={0.7}
152
+ >
153
+ {isSending ? (
154
+ <ActivityIndicator color="#fff" size="small" />
155
+ ) : (
156
+ <Text style={styles.sendIcon}>↑</Text>
157
+ )}
158
+ </TouchableOpacity>
159
+ </View>
160
+ </View>
161
+ )
162
+
163
+ if (Platform.OS === 'ios') {
164
+ return (
165
+ <SafeAreaView style={{ backgroundColor: theme.background }}>
166
+ {inputBar}
167
+ </SafeAreaView>
168
+ )
169
+ }
170
+
171
+ return inputBar
172
+ }
173
+
174
+ const styles = StyleSheet.create({
175
+ container: {
176
+ borderTopWidth: StyleSheet.hairlineWidth,
177
+ },
178
+ filesStrip: {
179
+ maxHeight: 110,
180
+ borderBottomWidth: StyleSheet.hairlineWidth,
181
+ borderBottomColor: 'rgba(0,0,0,0.07)',
182
+ },
183
+ filesStripContent: {
184
+ paddingHorizontal: 12,
185
+ paddingVertical: 10,
186
+ gap: 10,
187
+ },
188
+ filePreview: {
189
+ position: 'relative',
190
+ },
191
+ previewImage: {
192
+ width: 80,
193
+ height: 80,
194
+ borderRadius: 8,
195
+ },
196
+ previewDoc: {
197
+ width: 80,
198
+ height: 80,
199
+ borderRadius: 8,
200
+ borderWidth: 1,
201
+ alignItems: 'center',
202
+ justifyContent: 'center',
203
+ padding: 6,
204
+ gap: 4,
205
+ },
206
+ previewDocIcon: {
207
+ fontSize: 24,
208
+ },
209
+ previewDocName: {
210
+ fontSize: 9,
211
+ textAlign: 'center',
212
+ lineHeight: 12,
213
+ },
214
+ removeBtn: {
215
+ position: 'absolute',
216
+ top: -6,
217
+ right: -6,
218
+ width: 20,
219
+ height: 20,
220
+ borderRadius: 10,
221
+ backgroundColor: '#333',
222
+ alignItems: 'center',
223
+ justifyContent: 'center',
224
+ },
225
+ removeBtnText: {
226
+ color: '#fff',
227
+ fontSize: 9,
228
+ lineHeight: 10,
229
+ fontWeight: 'bold',
230
+ },
231
+ inputRow: {
232
+ flexDirection: 'row',
233
+ alignItems: 'flex-end',
234
+ paddingHorizontal: 12,
235
+ paddingTop: 10,
236
+ paddingBottom: 0,
237
+ gap: 8,
238
+ },
239
+ attachBtn: {
240
+ width: 40,
241
+ height: 40,
242
+ borderRadius: 20,
243
+ borderWidth: 1,
244
+ alignItems: 'center',
245
+ justifyContent: 'center',
246
+ flexShrink: 0,
247
+ },
248
+ attachIcon: {
249
+ fontSize: 22,
250
+ lineHeight: 24,
251
+ },
252
+ inputWrap: {
253
+ flex: 1,
254
+ borderWidth: 1,
255
+ borderRadius: 20,
256
+ paddingHorizontal: 14,
257
+ paddingVertical: 8,
258
+ minHeight: 40,
259
+ maxHeight: 120,
260
+ },
261
+ input: {
262
+ fontSize: 15,
263
+ lineHeight: 20,
264
+ padding: 0,
265
+ },
266
+ sendBtn: {
267
+ width: 40,
268
+ height: 40,
269
+ borderRadius: 20,
270
+ alignItems: 'center',
271
+ justifyContent: 'center',
272
+ flexShrink: 0,
273
+ },
274
+ sendIcon: {
275
+ color: '#fff',
276
+ fontSize: 18,
277
+ fontWeight: 'bold',
278
+ lineHeight: 20,
279
+ },
280
+ })