@developer_tribe/react-native-comnyx 0.13.13 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/android/generated/RCTAppDependencyProvider.h +25 -0
  2. package/android/generated/RCTAppDependencyProvider.mm +55 -0
  3. package/android/generated/RCTModulesConformingToProtocolsProvider.h +18 -0
  4. package/android/generated/RCTModulesConformingToProtocolsProvider.mm +33 -0
  5. package/android/generated/RCTThirdPartyComponentsProvider.h +16 -0
  6. package/android/generated/RCTThirdPartyComponentsProvider.mm +23 -0
  7. package/android/generated/ReactAppDependencyProvider.podspec +34 -0
  8. package/android/generated/jni/CMakeLists.txt +36 -0
  9. package/android/generated/jni/RNComnyxSpec-generated.cpp +22 -0
  10. package/android/generated/jni/RNComnyxSpec.h +24 -0
  11. package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI-generated.cpp +17 -0
  12. package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI.h +19 -0
  13. package/android/src/main/AndroidManifest.xml +15 -0
  14. package/android/src/main/AndroidManifestNew.xml +4 -0
  15. package/android/src/main/java/com/comnyx/ComnyxMediaPickerModule.kt +347 -0
  16. package/android/src/main/java/com/comnyx/ComnyxPackage.kt +1 -1
  17. package/android/src/main/java/com/comnyx/VideoPlayerActivity.kt +91 -0
  18. package/ios/ComnyxMediaPicker.m +29 -0
  19. package/ios/ComnyxMediaPicker.swift +436 -0
  20. package/lib/commonjs/NativeComnyxMediaPicker.js +83 -0
  21. package/lib/commonjs/NativeComnyxMediaPicker.js.map +1 -0
  22. package/lib/commonjs/api/index.js +19 -0
  23. package/lib/commonjs/api/index.js.map +1 -1
  24. package/lib/commonjs/api/media.js +76 -0
  25. package/lib/commonjs/api/media.js.map +1 -0
  26. package/lib/commonjs/assets/attachment-01.png +0 -0
  27. package/lib/commonjs/assets/gallery.png +0 -0
  28. package/lib/commonjs/assets/video-play.png +0 -0
  29. package/lib/commonjs/assets/x-circle.png +0 -0
  30. package/lib/commonjs/components/ChatList.js +48 -22
  31. package/lib/commonjs/components/ChatList.js.map +1 -1
  32. package/lib/commonjs/components/LinkifyText.js +5 -1
  33. package/lib/commonjs/components/LinkifyText.js.map +1 -1
  34. package/lib/commonjs/components/MediaMessageItem.js +333 -0
  35. package/lib/commonjs/components/MediaMessageItem.js.map +1 -0
  36. package/lib/commonjs/components/MediaPickerButton.js +244 -0
  37. package/lib/commonjs/components/MediaPickerButton.js.map +1 -0
  38. package/lib/commonjs/components/MediaViewerModal.js +164 -0
  39. package/lib/commonjs/components/MediaViewerModal.js.map +1 -0
  40. package/lib/commonjs/components/MessageInput.js +344 -73
  41. package/lib/commonjs/components/MessageInput.js.map +1 -1
  42. package/lib/commonjs/components/MessageItem.js +17 -8
  43. package/lib/commonjs/components/MessageItem.js.map +1 -1
  44. package/lib/commonjs/constants/translations.js +174 -29
  45. package/lib/commonjs/constants/translations.js.map +1 -1
  46. package/lib/commonjs/data/fake/media.js +105 -0
  47. package/lib/commonjs/data/fake/media.js.map +1 -0
  48. package/lib/commonjs/types/MediaTypes.js +2 -0
  49. package/lib/commonjs/types/MediaTypes.js.map +1 -0
  50. package/lib/commonjs/version.js +1 -1
  51. package/lib/commonjs/version.js.map +1 -1
  52. package/lib/module/NativeComnyxMediaPicker.js +73 -0
  53. package/lib/module/NativeComnyxMediaPicker.js.map +1 -0
  54. package/lib/module/api/index.js +1 -0
  55. package/lib/module/api/index.js.map +1 -1
  56. package/lib/module/api/media.js +70 -0
  57. package/lib/module/api/media.js.map +1 -0
  58. package/lib/module/assets/attachment-01.png +0 -0
  59. package/lib/module/assets/gallery.png +0 -0
  60. package/lib/module/assets/video-play.png +0 -0
  61. package/lib/module/assets/x-circle.png +0 -0
  62. package/lib/module/components/ChatList.js +48 -22
  63. package/lib/module/components/ChatList.js.map +1 -1
  64. package/lib/module/components/LinkifyText.js +5 -1
  65. package/lib/module/components/LinkifyText.js.map +1 -1
  66. package/lib/module/components/MediaMessageItem.js +330 -0
  67. package/lib/module/components/MediaMessageItem.js.map +1 -0
  68. package/lib/module/components/MediaPickerButton.js +240 -0
  69. package/lib/module/components/MediaPickerButton.js.map +1 -0
  70. package/lib/module/components/MediaViewerModal.js +160 -0
  71. package/lib/module/components/MediaViewerModal.js.map +1 -0
  72. package/lib/module/components/MessageInput.js +347 -75
  73. package/lib/module/components/MessageInput.js.map +1 -1
  74. package/lib/module/components/MessageItem.js +17 -8
  75. package/lib/module/components/MessageItem.js.map +1 -1
  76. package/lib/module/constants/translations.js +174 -29
  77. package/lib/module/constants/translations.js.map +1 -1
  78. package/lib/module/data/fake/media.js +99 -0
  79. package/lib/module/data/fake/media.js.map +1 -0
  80. package/lib/module/types/MediaTypes.js +2 -0
  81. package/lib/module/types/MediaTypes.js.map +1 -0
  82. package/lib/module/version.js +1 -1
  83. package/lib/module/version.js.map +1 -1
  84. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts +9 -0
  85. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts.map +1 -0
  86. package/lib/typescript/src/api/index.d.ts +1 -0
  87. package/lib/typescript/src/api/index.d.ts.map +1 -1
  88. package/lib/typescript/src/api/media.d.ts +7 -0
  89. package/lib/typescript/src/api/media.d.ts.map +1 -0
  90. package/lib/typescript/src/components/ChatList.d.ts.map +1 -1
  91. package/lib/typescript/src/components/LinkifyText.d.ts.map +1 -1
  92. package/lib/typescript/src/components/MediaMessageItem.d.ts +6 -0
  93. package/lib/typescript/src/components/MediaMessageItem.d.ts.map +1 -0
  94. package/lib/typescript/src/components/MediaPickerButton.d.ts +5 -0
  95. package/lib/typescript/src/components/MediaPickerButton.d.ts.map +1 -0
  96. package/lib/typescript/src/components/MediaViewerModal.d.ts +8 -0
  97. package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -0
  98. package/lib/typescript/src/components/MessageInput.d.ts.map +1 -1
  99. package/lib/typescript/src/components/MessageItem.d.ts.map +1 -1
  100. package/lib/typescript/src/constants/translations.d.ts.map +1 -1
  101. package/lib/typescript/src/data/fake/media.d.ts +6 -0
  102. package/lib/typescript/src/data/fake/media.d.ts.map +1 -0
  103. package/lib/typescript/src/types/Conversation.d.ts +19 -0
  104. package/lib/typescript/src/types/Conversation.d.ts.map +1 -1
  105. package/lib/typescript/src/types/LocalizationKeys.d.ts +5 -0
  106. package/lib/typescript/src/types/LocalizationKeys.d.ts.map +1 -1
  107. package/lib/typescript/src/types/MediaTypes.d.ts +26 -0
  108. package/lib/typescript/src/types/MediaTypes.d.ts.map +1 -0
  109. package/lib/typescript/src/version.d.ts +1 -1
  110. package/lib/typescript/src/version.d.ts.map +1 -1
  111. package/package.json +1 -1
  112. package/src/NativeComnyxMediaPicker.ts +83 -0
  113. package/src/api/index.ts +1 -0
  114. package/src/api/media.ts +116 -0
  115. package/src/assets/attachment-01.png +0 -0
  116. package/src/assets/gallery.png +0 -0
  117. package/src/assets/video-play.png +0 -0
  118. package/src/assets/x-circle.png +0 -0
  119. package/src/components/ChatList.tsx +81 -24
  120. package/src/components/CustomerForm.tsx +1 -1
  121. package/src/components/LinkifyText.tsx +3 -2
  122. package/src/components/MediaMessageItem.tsx +390 -0
  123. package/src/components/MediaPickerButton.tsx +269 -0
  124. package/src/components/MediaViewerModal.tsx +168 -0
  125. package/src/components/MessageInput.tsx +396 -84
  126. package/src/components/MessageItem.tsx +19 -5
  127. package/src/constants/translations.ts +145 -0
  128. package/src/data/fake/media.ts +110 -0
  129. package/src/types/Conversation.ts +20 -0
  130. package/src/types/LocalizationKeys.ts +5 -0
  131. package/src/types/MediaTypes.ts +27 -0
  132. package/src/version.ts +1 -1
@@ -1,14 +1,25 @@
1
- import { TextInput, View, Image, TouchableOpacity } from 'react-native';
2
- import { useState } from 'react';
1
+ import {
2
+ TextInput,
3
+ View,
4
+ Image,
5
+ TouchableOpacity,
6
+ ScrollView,
7
+ } from 'react-native';
8
+ import { useState, useCallback } from 'react';
3
9
  import { sendCustomerMessage } from '../api';
10
+ import { getUploadUrl, uploadFileToS3, sendMediaMessage } from '../api';
11
+ import { deleteTempFile } from '../NativeComnyxMediaPicker';
4
12
  import { useThemeColors } from '../hooks/useThemeColors';
5
13
  import { ScaledSheet } from './ScaledSheet';
6
14
  import { useLocalize } from '../hooks/useLocalize';
7
15
  import { activeOpacity } from '../constants/activeOpacity';
8
16
  import { useIsRtl } from '../hooks/isRtl';
9
17
  import { useAppStore } from '../store/store';
18
+ import { MediaPickerButton } from './MediaPickerButton';
19
+ import type { MediaAsset } from '../types/MediaTypes';
10
20
 
11
21
  const sendDark = require('../assets/arrow-right.png');
22
+ const circleXIcon = require('../assets/x-circle.png');
12
23
 
13
24
  export function MessageInput({
14
25
  scrollToBottom,
@@ -17,124 +28,359 @@ export function MessageInput({
17
28
  selectedMessage?: string;
18
29
  }) {
19
30
  const [value, setValue] = useState('');
31
+ const [pendingMedia, setPendingMedia] = useState<MediaAsset[]>([]);
32
+ const [isSending, setIsSending] = useState(false);
20
33
  const customer = useAppStore((s) => s.customer!);
21
34
  const themeColors = useThemeColors();
22
35
  const localize = useLocalize();
23
36
  const isRtl = useIsRtl();
24
37
 
25
- const sendMessage = () => {
26
- if (value.trim()) {
38
+ const sendTextOnlyMessage = useCallback(() => {
39
+ if (!value.trim()) return;
40
+ const date = new Date();
41
+ const localId = `local-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
42
+ useAppStore.getState().setData((data) => [
43
+ {
44
+ id: null,
45
+ local_id: localId,
46
+ approved: false,
47
+ content: value,
48
+ created_at: date,
49
+ customer: {
50
+ name: customer.name,
51
+ profile_photo_url: null,
52
+ },
53
+ },
54
+ ...(data ?? []),
55
+ ]);
56
+ sendCustomerMessage(customer.external_id as string, value, {
57
+ fake: useAppStore.getState().fake,
58
+ })
59
+ .then((res) => {
60
+ const data = useAppStore.getState().data;
61
+ if (data) {
62
+ const itemIndex = data.findIndex((item) => item.local_id === localId);
63
+ if (itemIndex === -1) {
64
+ //TODO:??
65
+ } else {
66
+ const alteredData = [...data];
67
+ alteredData[itemIndex] = {
68
+ id: res.message.id,
69
+ content: res.message.content,
70
+ customer:
71
+ res.message.customer ?? alteredData[itemIndex]?.customer,
72
+ created_at: new Date(res.message.created_at),
73
+ approved: true,
74
+ };
75
+ useAppStore.setState({
76
+ data: alteredData,
77
+ });
78
+ scrollToBottom(false);
79
+ }
80
+ } else {
81
+ //TODO: ??
82
+ }
83
+ })
84
+ .catch(() => {
85
+ const data = useAppStore.getState().data;
86
+ if (data) {
87
+ const itemIndex = data.findIndex((item) => item.local_id === localId);
88
+ if (itemIndex === -1) {
89
+ //TODO:??
90
+ } else {
91
+ const alteredData = [...data];
92
+ alteredData[itemIndex] = {
93
+ ...alteredData[itemIndex],
94
+ id: alteredData[itemIndex]?.id ?? null,
95
+ content: alteredData[itemIndex]?.content ?? '',
96
+ created_at: alteredData[itemIndex]?.created_at ?? new Date(),
97
+ approved: alteredData[itemIndex]?.approved ?? false,
98
+ customer: alteredData[itemIndex]?.customer ?? null,
99
+ error: true,
100
+ };
101
+ useAppStore.setState({
102
+ data: alteredData,
103
+ });
104
+ scrollToBottom(false);
105
+ }
106
+ } else {
107
+ //TODO: ??
108
+ }
109
+ });
110
+ setValue('');
111
+ }, [value, customer, scrollToBottom]);
112
+
113
+ const uploadSingleMedia = useCallback(
114
+ async (
115
+ asset: MediaAsset,
116
+ isFake: boolean,
117
+ onProgress?: (percentage: number) => void
118
+ ): Promise<string> => {
119
+ const presignResponse = await getUploadUrl(
120
+ asset.fileName,
121
+ asset.mimeType,
122
+ { fake: isFake }
123
+ );
124
+ const uploadUrl = presignResponse.data.url;
125
+ const filePath = presignResponse.data.path;
126
+
127
+ await uploadFileToS3(uploadUrl, asset.uri, asset.mimeType, onProgress, {
128
+ fake: isFake,
129
+ });
130
+
131
+ return isFake ? asset.uri : filePath;
132
+ },
133
+ []
134
+ );
135
+
136
+ const sendMediaMessages = useCallback(
137
+ async (assets: MediaAsset[], content: string) => {
138
+ const isFake = useAppStore.getState().fake;
27
139
  const date = new Date();
140
+
141
+ const placeholderMediaFiles = assets.map((asset) => ({
142
+ url: '',
143
+ type: asset.type,
144
+ local_uri: asset.uri,
145
+ thumbnail_uri: asset.thumbnailUri,
146
+ }));
147
+
148
+ const localId = `local-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
28
149
  useAppStore.getState().setData((data) => [
29
150
  {
30
151
  id: null,
152
+ local_id: localId,
31
153
  approved: false,
32
- content: value,
154
+ content: content,
33
155
  created_at: date,
34
156
  customer: {
35
157
  name: customer.name,
36
158
  profile_photo_url: null,
37
159
  },
160
+ media_files: placeholderMediaFiles,
161
+ media_type: assets[0]!.type,
162
+ is_uploading: true,
163
+ upload_progress: 0,
38
164
  },
39
165
  ...(data ?? []),
40
166
  ]);
41
- sendCustomerMessage(customer.external_id as string, value, {
42
- fake: useAppStore.getState().fake,
43
- })
44
- .then((res) => {
45
- const data = useAppStore.getState().data;
46
- if (data) {
47
- const itemIndex = data.findIndex(
48
- (item) =>
49
- item.id === null &&
50
- new Date(item.created_at).getTime() === date.getTime()
167
+ scrollToBottom(false);
168
+
169
+ try {
170
+ const fileProgresses = new Array(assets.length).fill(0);
171
+ let lastProgressUpdate = 0;
172
+
173
+ const updateOverallProgress = () => {
174
+ const now = Date.now();
175
+ if (now - lastProgressUpdate < 250) return;
176
+ lastProgressUpdate = now;
177
+
178
+ const sum = fileProgresses.reduce((a, b) => a + b, 0);
179
+ const overall = Math.min(95, Math.round(sum / assets.length));
180
+ const currentData = useAppStore.getState().data;
181
+ if (currentData) {
182
+ const idx = currentData.findIndex(
183
+ (item) => item.local_id === localId
51
184
  );
52
- if (itemIndex === -1) {
53
- //TODO:??
54
- } else {
55
- const alteredData = [...data];
56
- alteredData[itemIndex] = {
57
- id: res.message.id,
58
- content: res.message.content,
59
- customer:
60
- res.message.customer ?? alteredData[itemIndex]?.customer,
61
- created_at: new Date(res.message.created_at),
62
- approved: true,
185
+ if (idx !== -1) {
186
+ const updated = [...currentData];
187
+ updated[idx] = {
188
+ ...updated[idx]!,
189
+ upload_progress: overall,
63
190
  };
64
- useAppStore.setState({
65
- data: alteredData,
66
- });
67
- scrollToBottom(false);
191
+ useAppStore.setState({ data: updated });
68
192
  }
69
- } else {
70
- //TODO: ??
71
193
  }
72
- })
73
- .catch(() => {
74
- const data = useAppStore.getState().data;
75
- if (data) {
76
- const itemIndex = data.findIndex(
77
- (item) =>
78
- item.id === null &&
79
- new Date(item.created_at).getTime() === date.getTime()
80
- );
81
- if (itemIndex === -1) {
82
- //TODO:??
83
- } else {
84
- const alteredData = [...data];
85
- alteredData[itemIndex] = {
86
- ...alteredData[itemIndex],
87
- id: alteredData[itemIndex]?.id ?? null,
88
- content: alteredData[itemIndex]?.content ?? '',
89
- created_at: alteredData[itemIndex]?.created_at ?? new Date(),
90
- approved: alteredData[itemIndex]?.approved ?? false,
91
- customer: alteredData[itemIndex]?.customer ?? null,
92
- error: true,
93
- };
94
- useAppStore.setState({
95
- data: alteredData,
96
- });
97
- scrollToBottom(false);
194
+ };
195
+
196
+ const uploadedPaths: string[] = [];
197
+ for (let i = 0; i < assets.length; i++) {
198
+ const path = await uploadSingleMedia(
199
+ assets[i]!,
200
+ isFake,
201
+ (percentage) => {
202
+ fileProgresses[i] = percentage;
203
+ updateOverallProgress();
98
204
  }
99
- } else {
100
- //TODO: ??
205
+ );
206
+ uploadedPaths.push(path);
207
+ }
208
+
209
+ const res = await sendMediaMessage(
210
+ customer.external_id as string,
211
+ uploadedPaths,
212
+ assets[0]!.type,
213
+ content,
214
+ { fake: isFake }
215
+ );
216
+
217
+ const currentData = useAppStore.getState().data;
218
+ if (currentData) {
219
+ const idx = currentData.findIndex(
220
+ (item) => item.local_id === localId
221
+ );
222
+ if (idx !== -1) {
223
+ const updated = [...currentData];
224
+ const finalMediaFiles = assets.map((asset, i) => ({
225
+ url: uploadedPaths[i]!,
226
+ type: asset.type,
227
+ local_uri: asset.uri,
228
+ thumbnail_uri: asset.thumbnailUri,
229
+ }));
230
+ updated[idx] = {
231
+ id: res.message.id,
232
+ content: res.message.content,
233
+ customer: res.message.customer ?? updated[idx]?.customer ?? null,
234
+ created_at: new Date(res.message.created_at),
235
+ approved: true,
236
+ media_files: finalMediaFiles,
237
+ media_type: assets[0]!.type,
238
+ is_uploading: false,
239
+ upload_progress: 100,
240
+ };
241
+ useAppStore.setState({ data: updated });
242
+ scrollToBottom(false);
243
+ }
244
+ }
245
+
246
+ // Temp files are NOT deleted here — they are still needed for display
247
+ // until the backend data refresh provides full S3 URLs.
248
+ // The OS cleans up temp directory files automatically.
249
+ } catch (error) {
250
+ // Handle upload error — temp files are preserved for retry
251
+ const currentData = useAppStore.getState().data;
252
+ if (currentData) {
253
+ const idx = currentData.findIndex(
254
+ (item) => item.local_id === localId
255
+ );
256
+ if (idx !== -1) {
257
+ const updated = [...currentData];
258
+ updated[idx] = {
259
+ ...updated[idx]!,
260
+ is_uploading: false,
261
+ error: true,
262
+ };
263
+ useAppStore.setState({ data: updated });
264
+ scrollToBottom(false);
101
265
  }
102
- });
266
+ }
267
+ console.error('[Comnyx] Media upload failed:', error);
268
+ }
269
+ },
270
+ [customer, scrollToBottom, uploadSingleMedia]
271
+ );
272
+
273
+ const handleMediaSelected = useCallback((assets: MediaAsset[]) => {
274
+ setPendingMedia((prev) => [...prev, ...assets]);
275
+ }, []);
276
+
277
+ const removePendingMedia = useCallback((index: number) => {
278
+ setPendingMedia((prev) => {
279
+ const removed = prev[index];
280
+ if (removed) {
281
+ deleteTempFile(removed.uri).catch((e) =>
282
+ console.warn('[Comnyx] Failed to delete temp file:', e)
283
+ );
284
+ if (removed.thumbnailUri) {
285
+ deleteTempFile(removed.thumbnailUri).catch((e) =>
286
+ console.warn('[Comnyx] Failed to delete temp thumbnail:', e)
287
+ );
288
+ }
289
+ }
290
+ return prev.filter((_, i) => i !== index);
291
+ });
292
+ }, []);
293
+
294
+ const handleSend = useCallback(async () => {
295
+ if (isSending) return;
296
+
297
+ if (pendingMedia.length > 0) {
298
+ setIsSending(true);
299
+ const content = value.trim();
300
+ const assets = [...pendingMedia];
301
+ setPendingMedia([]);
103
302
  setValue('');
303
+ await sendMediaMessages(assets, content);
304
+ setIsSending(false);
305
+ } else if (value.trim()) {
306
+ sendTextOnlyMessage();
104
307
  }
105
- };
308
+ }, [isSending, pendingMedia, value, sendMediaMessages, sendTextOnlyMessage]);
309
+
310
+ const hasPendingMedia = pendingMedia.length > 0;
106
311
 
107
312
  return (
108
313
  <View
109
314
  style={[styles.container, { backgroundColor: themeColors.background }]}
110
315
  >
111
- <TextInput
112
- value={value}
113
- onChangeText={(newValue) => setValue(newValue)}
114
- onSubmitEditing={sendMessage}
115
- returnKeyType="send"
116
- multiline={true}
117
- maxLength={1000}
118
- style={[
119
- styles.textInput,
120
- {
121
- backgroundColor: themeColors.background,
122
- color: themeColors.text,
123
- borderColor: themeColors.silver,
124
- },
125
- ]}
126
- placeholder={localize('chat.messageInput.placeholder')}
127
- placeholderTextColor={themeColors.text + '80'}
128
- />
316
+ <MediaPickerButton onMediaSelected={handleMediaSelected} />
317
+ <View style={[styles.inputWrapper, { borderColor: themeColors.silver }]}>
318
+ {hasPendingMedia && (
319
+ <ScrollView
320
+ horizontal
321
+ showsHorizontalScrollIndicator={false}
322
+ style={styles.previewScroll}
323
+ contentContainerStyle={styles.previewContent}
324
+ >
325
+ {pendingMedia.map((asset, index) => (
326
+ <View key={`${asset.uri}-${index}`} style={styles.previewItem}>
327
+ <Image
328
+ source={{
329
+ uri:
330
+ asset.type === 'video' && asset.thumbnailUri
331
+ ? asset.thumbnailUri
332
+ : asset.uri,
333
+ }}
334
+ style={styles.previewThumbnail}
335
+ />
336
+ {asset.type === 'video' && (
337
+ <View style={styles.playIconOverlay}>
338
+ <View style={styles.playIcon}>
339
+ <View style={styles.playTriangle} />
340
+ </View>
341
+ </View>
342
+ )}
343
+ <TouchableOpacity
344
+ style={styles.removeButton}
345
+ onPress={() => removePendingMedia(index)}
346
+ activeOpacity={activeOpacity}
347
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
348
+ >
349
+ <Image source={circleXIcon} style={styles.removeIcon} />
350
+ </TouchableOpacity>
351
+ </View>
352
+ ))}
353
+ </ScrollView>
354
+ )}
355
+ <TextInput
356
+ value={value}
357
+ onChangeText={(newValue) => setValue(newValue)}
358
+ onSubmitEditing={handleSend}
359
+ returnKeyType="send"
360
+ multiline={true}
361
+ maxLength={1000}
362
+ style={[
363
+ styles.textInput,
364
+ {
365
+ backgroundColor: themeColors.background,
366
+ color: themeColors.text,
367
+ },
368
+ ]}
369
+ placeholder={localize('chat.messageInput.placeholder')}
370
+ placeholderTextColor={themeColors.text + '80'}
371
+ />
372
+ </View>
129
373
  <TouchableOpacity
130
374
  style={[styles.sendButton]}
131
- onPress={sendMessage}
375
+ onPress={handleSend}
132
376
  activeOpacity={activeOpacity}
377
+ disabled={isSending}
133
378
  >
134
379
  <Image
135
380
  style={[
136
381
  styles.sendIcon,
137
382
  isRtl && { transform: [{ rotate: '180deg' }] },
383
+ isSending && { opacity: 0.4 },
138
384
  ]}
139
385
  source={sendDark}
140
386
  />
@@ -148,16 +394,81 @@ const styles = ScaledSheet.create({
148
394
  flexDirection: 'row',
149
395
  paddingHorizontal: '10@s',
150
396
  paddingVertical: '10@vs',
151
- alignItems: 'center',
397
+ alignItems: 'flex-end',
152
398
  position: 'relative',
153
399
  zIndex: 1,
154
400
  },
155
- textInput: {
401
+ inputWrapper: {
156
402
  flex: 1,
157
403
  borderWidth: 1,
158
404
  borderRadius: '20@s',
159
- paddingHorizontal: '15@s',
160
405
  marginRight: '10@s',
406
+ overflow: 'hidden',
407
+ },
408
+ previewScroll: {
409
+ maxHeight: '80@vs',
410
+ marginTop: '8@vs',
411
+ marginHorizontal: '10@s',
412
+ },
413
+ previewContent: {
414
+ flexDirection: 'row',
415
+ gap: '6@s',
416
+ },
417
+ previewItem: {
418
+ position: 'relative',
419
+ width: '60@vs',
420
+ height: '60@vs',
421
+ borderRadius: '8@s',
422
+ },
423
+ previewThumbnail: {
424
+ width: '100%',
425
+ height: '100%',
426
+ borderRadius: '8@s',
427
+ },
428
+ playIconOverlay: {
429
+ ...({
430
+ position: 'absolute',
431
+ top: 0,
432
+ left: 0,
433
+ right: 0,
434
+ bottom: 0,
435
+ justifyContent: 'center',
436
+ alignItems: 'center',
437
+ } as any),
438
+ },
439
+ playIcon: {
440
+ width: '22@vs',
441
+ height: '22@vs',
442
+ borderRadius: '11@vs',
443
+ borderWidth: 1.5,
444
+ borderColor: '#fff',
445
+ justifyContent: 'center',
446
+ alignItems: 'center',
447
+ },
448
+ playTriangle: {
449
+ width: 0,
450
+ height: 0,
451
+ borderLeftWidth: '7@vs',
452
+ borderTopWidth: '5@vs',
453
+ borderBottomWidth: '5@vs',
454
+ borderLeftColor: '#fff',
455
+ borderTopColor: 'transparent',
456
+ borderBottomColor: 'transparent',
457
+ marginLeft: '2@s',
458
+ },
459
+ removeButton: {
460
+ position: 'absolute',
461
+ top: '-1@vs',
462
+ right: 0,
463
+ zIndex: 1,
464
+ },
465
+ removeIcon: {
466
+ width: '20@vs',
467
+ height: '20@vs',
468
+ },
469
+ textInput: {
470
+ paddingHorizontal: '15@s',
471
+
161
472
  fontSize: '16@vs',
162
473
  paddingVertical: '10@vs',
163
474
  minHeight: 40,
@@ -169,6 +480,7 @@ const styles = ScaledSheet.create({
169
480
  borderRadius: '20@vs',
170
481
  justifyContent: 'center',
171
482
  alignItems: 'center',
483
+ marginBottom: 0,
172
484
  },
173
485
  sendIcon: {
174
486
  width: '40@vs',
@@ -6,6 +6,7 @@ import { useState, useRef, useEffect } from 'react';
6
6
  import { ScaledSheet } from './ScaledSheet';
7
7
  import { useAppStore } from '../store/store';
8
8
  import { LinkifyText } from './LinkifyText';
9
+ import { MediaMessageItem } from './MediaMessageItem';
9
10
 
10
11
  const clockIcon = require('../assets/iconamoon_clock-fill.png');
11
12
  const infoIcon = require('../assets/info-circle.png');
@@ -30,6 +31,23 @@ export function MessageItem({
30
31
  }: {
31
32
  item: AppConversationMessage;
32
33
  onShowPopup: () => void;
34
+ }) {
35
+ if (
36
+ item.media_url ||
37
+ item.media_local_uri ||
38
+ (item.media_files && item.media_files.length > 0)
39
+ ) {
40
+ return <MediaMessageItem item={item} onShowPopup={onShowPopup} />;
41
+ }
42
+ return <TextMessageItem item={item} onShowPopup={onShowPopup} />;
43
+ }
44
+
45
+ function TextMessageItem({
46
+ item,
47
+ onShowPopup,
48
+ }: {
49
+ item: AppConversationMessage;
50
+ onShowPopup: () => void;
33
51
  }) {
34
52
  const themeColors = useThemeColors();
35
53
  const isDeviceOwner = !item.user && !item.bot;
@@ -101,17 +119,14 @@ export function MessageItem({
101
119
  setLayoutComplete(false);
102
120
  }, [item.content]);
103
121
 
104
- // Check if there's enough space for the timestamp
105
122
  const hasSpaceInLastLine =
106
123
  layoutRef.current.lines >= 1 &&
107
124
  layoutRef.current.lastLineWidth < layoutRef.current.totalWidth * 0.8;
108
125
 
109
- // A single line message with enough space for the timestamp
110
126
  const isSingleLineWithSpace =
111
127
  layoutRef.current.lines === 1 &&
112
128
  layoutRef.current.lastLineWidth > layoutRef.current.totalWidth * 0.8;
113
129
 
114
- // For multi-line messages, check if the last line has space
115
130
  const isMultiLineWithSpace =
116
131
  layoutRef.current.lines > 1 && hasSpaceInLastLine;
117
132
 
@@ -170,7 +185,6 @@ export function MessageItem({
170
185
  /https?:\/\/(?:[-\w.])+(?::[0-9]+)?(?:\/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:#(?:[\w.])*)?)?/g,
171
186
  color: '#0066CC',
172
187
  navigate: (url) => {
173
- console.log('url', url);
174
188
  if (url) {
175
189
  Linking.openURL(url);
176
190
  }
@@ -178,7 +192,7 @@ export function MessageItem({
178
192
  },
179
193
  ]}
180
194
  >
181
- {item.content}
195
+ {typeof item.content === 'string' ? item.content : ''}
182
196
  </LinkifyText>
183
197
  </AppText>
184
198