@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.
- package/android/generated/RCTAppDependencyProvider.h +25 -0
- package/android/generated/RCTAppDependencyProvider.mm +55 -0
- package/android/generated/RCTModulesConformingToProtocolsProvider.h +18 -0
- package/android/generated/RCTModulesConformingToProtocolsProvider.mm +33 -0
- package/android/generated/RCTThirdPartyComponentsProvider.h +16 -0
- package/android/generated/RCTThirdPartyComponentsProvider.mm +23 -0
- package/android/generated/ReactAppDependencyProvider.podspec +34 -0
- package/android/generated/jni/CMakeLists.txt +36 -0
- package/android/generated/jni/RNComnyxSpec-generated.cpp +22 -0
- package/android/generated/jni/RNComnyxSpec.h +24 -0
- package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI-generated.cpp +17 -0
- package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI.h +19 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/AndroidManifestNew.xml +4 -0
- package/android/src/main/java/com/comnyx/ComnyxMediaPickerModule.kt +347 -0
- package/android/src/main/java/com/comnyx/ComnyxPackage.kt +1 -1
- package/android/src/main/java/com/comnyx/VideoPlayerActivity.kt +91 -0
- package/ios/ComnyxMediaPicker.m +29 -0
- package/ios/ComnyxMediaPicker.swift +436 -0
- package/lib/commonjs/NativeComnyxMediaPicker.js +83 -0
- package/lib/commonjs/NativeComnyxMediaPicker.js.map +1 -0
- package/lib/commonjs/api/index.js +19 -0
- package/lib/commonjs/api/index.js.map +1 -1
- package/lib/commonjs/api/media.js +76 -0
- package/lib/commonjs/api/media.js.map +1 -0
- package/lib/commonjs/assets/attachment-01.png +0 -0
- package/lib/commonjs/assets/gallery.png +0 -0
- package/lib/commonjs/assets/video-play.png +0 -0
- package/lib/commonjs/assets/x-circle.png +0 -0
- package/lib/commonjs/components/ChatList.js +48 -22
- package/lib/commonjs/components/ChatList.js.map +1 -1
- package/lib/commonjs/components/LinkifyText.js +5 -1
- package/lib/commonjs/components/LinkifyText.js.map +1 -1
- package/lib/commonjs/components/MediaMessageItem.js +333 -0
- package/lib/commonjs/components/MediaMessageItem.js.map +1 -0
- package/lib/commonjs/components/MediaPickerButton.js +244 -0
- package/lib/commonjs/components/MediaPickerButton.js.map +1 -0
- package/lib/commonjs/components/MediaViewerModal.js +164 -0
- package/lib/commonjs/components/MediaViewerModal.js.map +1 -0
- package/lib/commonjs/components/MessageInput.js +344 -73
- package/lib/commonjs/components/MessageInput.js.map +1 -1
- package/lib/commonjs/components/MessageItem.js +17 -8
- package/lib/commonjs/components/MessageItem.js.map +1 -1
- package/lib/commonjs/constants/translations.js +174 -29
- package/lib/commonjs/constants/translations.js.map +1 -1
- package/lib/commonjs/data/fake/media.js +105 -0
- package/lib/commonjs/data/fake/media.js.map +1 -0
- package/lib/commonjs/types/MediaTypes.js +2 -0
- package/lib/commonjs/types/MediaTypes.js.map +1 -0
- package/lib/commonjs/version.js +1 -1
- package/lib/commonjs/version.js.map +1 -1
- package/lib/module/NativeComnyxMediaPicker.js +73 -0
- package/lib/module/NativeComnyxMediaPicker.js.map +1 -0
- package/lib/module/api/index.js +1 -0
- package/lib/module/api/index.js.map +1 -1
- package/lib/module/api/media.js +70 -0
- package/lib/module/api/media.js.map +1 -0
- package/lib/module/assets/attachment-01.png +0 -0
- package/lib/module/assets/gallery.png +0 -0
- package/lib/module/assets/video-play.png +0 -0
- package/lib/module/assets/x-circle.png +0 -0
- package/lib/module/components/ChatList.js +48 -22
- package/lib/module/components/ChatList.js.map +1 -1
- package/lib/module/components/LinkifyText.js +5 -1
- package/lib/module/components/LinkifyText.js.map +1 -1
- package/lib/module/components/MediaMessageItem.js +330 -0
- package/lib/module/components/MediaMessageItem.js.map +1 -0
- package/lib/module/components/MediaPickerButton.js +240 -0
- package/lib/module/components/MediaPickerButton.js.map +1 -0
- package/lib/module/components/MediaViewerModal.js +160 -0
- package/lib/module/components/MediaViewerModal.js.map +1 -0
- package/lib/module/components/MessageInput.js +347 -75
- package/lib/module/components/MessageInput.js.map +1 -1
- package/lib/module/components/MessageItem.js +17 -8
- package/lib/module/components/MessageItem.js.map +1 -1
- package/lib/module/constants/translations.js +174 -29
- package/lib/module/constants/translations.js.map +1 -1
- package/lib/module/data/fake/media.js +99 -0
- package/lib/module/data/fake/media.js.map +1 -0
- package/lib/module/types/MediaTypes.js +2 -0
- package/lib/module/types/MediaTypes.js.map +1 -0
- package/lib/module/version.js +1 -1
- package/lib/module/version.js.map +1 -1
- package/lib/typescript/src/NativeComnyxMediaPicker.d.ts +9 -0
- package/lib/typescript/src/NativeComnyxMediaPicker.d.ts.map +1 -0
- package/lib/typescript/src/api/index.d.ts +1 -0
- package/lib/typescript/src/api/index.d.ts.map +1 -1
- package/lib/typescript/src/api/media.d.ts +7 -0
- package/lib/typescript/src/api/media.d.ts.map +1 -0
- package/lib/typescript/src/components/ChatList.d.ts.map +1 -1
- package/lib/typescript/src/components/LinkifyText.d.ts.map +1 -1
- package/lib/typescript/src/components/MediaMessageItem.d.ts +6 -0
- package/lib/typescript/src/components/MediaMessageItem.d.ts.map +1 -0
- package/lib/typescript/src/components/MediaPickerButton.d.ts +5 -0
- package/lib/typescript/src/components/MediaPickerButton.d.ts.map +1 -0
- package/lib/typescript/src/components/MediaViewerModal.d.ts +8 -0
- package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -0
- package/lib/typescript/src/components/MessageInput.d.ts.map +1 -1
- package/lib/typescript/src/components/MessageItem.d.ts.map +1 -1
- package/lib/typescript/src/constants/translations.d.ts.map +1 -1
- package/lib/typescript/src/data/fake/media.d.ts +6 -0
- package/lib/typescript/src/data/fake/media.d.ts.map +1 -0
- package/lib/typescript/src/types/Conversation.d.ts +19 -0
- package/lib/typescript/src/types/Conversation.d.ts.map +1 -1
- package/lib/typescript/src/types/LocalizationKeys.d.ts +5 -0
- package/lib/typescript/src/types/LocalizationKeys.d.ts.map +1 -1
- package/lib/typescript/src/types/MediaTypes.d.ts +26 -0
- package/lib/typescript/src/types/MediaTypes.d.ts.map +1 -0
- package/lib/typescript/src/version.d.ts +1 -1
- package/lib/typescript/src/version.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeComnyxMediaPicker.ts +83 -0
- package/src/api/index.ts +1 -0
- package/src/api/media.ts +116 -0
- package/src/assets/attachment-01.png +0 -0
- package/src/assets/gallery.png +0 -0
- package/src/assets/video-play.png +0 -0
- package/src/assets/x-circle.png +0 -0
- package/src/components/ChatList.tsx +81 -24
- package/src/components/CustomerForm.tsx +1 -1
- package/src/components/LinkifyText.tsx +3 -2
- package/src/components/MediaMessageItem.tsx +390 -0
- package/src/components/MediaPickerButton.tsx +269 -0
- package/src/components/MediaViewerModal.tsx +168 -0
- package/src/components/MessageInput.tsx +396 -84
- package/src/components/MessageItem.tsx +19 -5
- package/src/constants/translations.ts +145 -0
- package/src/data/fake/media.ts +110 -0
- package/src/types/Conversation.ts +20 -0
- package/src/types/LocalizationKeys.ts +5 -0
- package/src/types/MediaTypes.ts +27 -0
- package/src/version.ts +1 -1
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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={
|
|
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: '
|
|
397
|
+
alignItems: 'flex-end',
|
|
152
398
|
position: 'relative',
|
|
153
399
|
zIndex: 1,
|
|
154
400
|
},
|
|
155
|
-
|
|
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
|
|