@pubuduth-aplicy/chat-ui 2.1.51 → 2.1.53
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/package.json +1 -1
- package/src/components/messages/Message.tsx +175 -54
- package/src/components/messages/MessageContainer.tsx +81 -11
- package/src/components/messages/MessageInput.tsx +328 -269
- package/src/components/messages/Messages.tsx +17 -2
- package/src/components/sidebar/Conversation.tsx +1 -0
- package/src/components/sidebar/Conversations.tsx +26 -2
- package/src/components/sidebar/SearchInput.tsx +14 -2
- package/src/components/sidebar/Sidebar.tsx +4 -5
- package/src/service/messageService.ts +6 -2
- package/src/stores/Zustant.ts +19 -2
- package/src/style/style.css +265 -6
- package/src/types/type.ts +1 -1
|
@@ -8,20 +8,11 @@ import paperplane from "../../assets/icons8-send-50.png";
|
|
|
8
8
|
// import useSendMessage from '../../hooks/useSendMessage'; // Importing the useSendMessage hook
|
|
9
9
|
import { FilePreview, FileType } from "../common/FilePreview";
|
|
10
10
|
import { getApiClient } from "../../lib/api/apiClient";
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"image/webp",
|
|
17
|
-
];
|
|
18
|
-
const ACCEPTED_VIDEO_TYPES = ["video/mp4", "video/webm", "video/ogg"];
|
|
19
|
-
const ACCEPTED_DOCUMENT_TYPES = [
|
|
20
|
-
"application/pdf",
|
|
21
|
-
"application/msword",
|
|
22
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
23
|
-
"text/plain",
|
|
24
|
-
];
|
|
11
|
+
import { MessageStatus } from "../../types/type";
|
|
12
|
+
const MAX_FILE_SIZE_MB = 25; // 10MB max file size
|
|
13
|
+
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp','video/mp4', 'video/webm', 'video/ogg'];
|
|
14
|
+
const ACCEPTED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/ogg'];
|
|
15
|
+
const ACCEPTED_DOCUMENT_TYPES = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'];
|
|
25
16
|
|
|
26
17
|
interface Attachment {
|
|
27
18
|
file: File;
|
|
@@ -35,15 +26,26 @@ const MessageInput = () => {
|
|
|
35
26
|
const apiClient = getApiClient();
|
|
36
27
|
const { socket } = useChatContext();
|
|
37
28
|
const { userId } = useChatContext();
|
|
38
|
-
const { selectedConversation } = useChatUIStore();
|
|
39
|
-
const [message, setMessage] = useState("");
|
|
40
|
-
|
|
41
|
-
const mutation = useMessageMutation(); // useMutation hook to send message
|
|
29
|
+
const { selectedConversation,setMessages } = useChatUIStore();
|
|
30
|
+
const [message, setMessage] = useState("");
|
|
31
|
+
const mutation = useMessageMutation();
|
|
42
32
|
const [typingUser, setTypingUser] = useState<string | null>(null);
|
|
43
33
|
const [isSending, setIsSending] = useState(false);
|
|
44
34
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
45
|
-
const [isTyping, setIsTyping] = useState(false);
|
|
35
|
+
const [isTyping, setIsTyping] = useState(false);
|
|
46
36
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
|
37
|
+
const [showAttachmentOptions, setShowAttachmentOptions] = useState(false);
|
|
38
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
39
|
+
const attachmentsRef = useRef<Attachment[]>([]);
|
|
40
|
+
const [tempMessageId, setTempMessageId] = useState<string | null>(null);
|
|
41
|
+
|
|
42
|
+
const generateTempId = () => `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
43
|
+
|
|
44
|
+
// Function to auto-resize the textarea
|
|
45
|
+
const autoResizeTextarea = (element: HTMLTextAreaElement) => {
|
|
46
|
+
element.style.height = "auto"
|
|
47
|
+
element.style.height = Math.min(150, element.scrollHeight) + "px"
|
|
48
|
+
}
|
|
47
49
|
|
|
48
50
|
useEffect(() => {
|
|
49
51
|
if (selectedConversation?._id) {
|
|
@@ -72,28 +74,17 @@ const MessageInput = () => {
|
|
|
72
74
|
return () => clearTimeout(typingTimeout);
|
|
73
75
|
}, [message, socket, selectedConversation?._id, userId]);
|
|
74
76
|
|
|
77
|
+
|
|
75
78
|
useEffect(() => {
|
|
76
79
|
if (!socket || !selectedConversation?._id) return;
|
|
77
80
|
|
|
78
|
-
const handleTyping = ({
|
|
79
|
-
userId,
|
|
80
|
-
chatId,
|
|
81
|
-
}: {
|
|
82
|
-
userId: string;
|
|
83
|
-
chatId: string;
|
|
84
|
-
}) => {
|
|
81
|
+
const handleTyping = ({ userId, chatId }: { userId: string; chatId: string }) => {
|
|
85
82
|
if (chatId === selectedConversation._id) {
|
|
86
83
|
setTypingUser(userId);
|
|
87
84
|
}
|
|
88
85
|
};
|
|
89
86
|
|
|
90
|
-
const handleStopTyping = ({
|
|
91
|
-
userId,
|
|
92
|
-
chatId,
|
|
93
|
-
}: {
|
|
94
|
-
userId: string;
|
|
95
|
-
chatId: string;
|
|
96
|
-
}) => {
|
|
87
|
+
const handleStopTyping = ({ userId, chatId }: { userId: string; chatId: string }) => {
|
|
97
88
|
if (chatId === selectedConversation._id) {
|
|
98
89
|
setTypingUser((prev) => (prev === userId ? null : prev));
|
|
99
90
|
}
|
|
@@ -109,114 +100,213 @@ const MessageInput = () => {
|
|
|
109
100
|
}, [socket, selectedConversation?._id]);
|
|
110
101
|
|
|
111
102
|
const getFileType = (mimeType: string): FileType | null => {
|
|
112
|
-
if (ACCEPTED_IMAGE_TYPES.includes(mimeType)) return
|
|
113
|
-
if (ACCEPTED_VIDEO_TYPES.includes(mimeType)) return
|
|
114
|
-
if (ACCEPTED_DOCUMENT_TYPES.includes(mimeType)) return
|
|
103
|
+
if (ACCEPTED_IMAGE_TYPES.includes(mimeType)) return 'image';
|
|
104
|
+
if (ACCEPTED_VIDEO_TYPES.includes(mimeType)) return 'video';
|
|
105
|
+
if (ACCEPTED_DOCUMENT_TYPES.includes(mimeType)) return 'document';
|
|
115
106
|
return null;
|
|
116
107
|
};
|
|
117
108
|
|
|
118
|
-
const uploadToS3 = async (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
109
|
+
const uploadToS3 = async (file: File, onProgress?:(progress:number)=> void): Promise<{ url: string, name: string, size: number, type: FileType }> => {
|
|
110
|
+
const response = await apiClient.post('api/chat/generatePresignedUrl', {
|
|
111
|
+
fileName: file.name,
|
|
112
|
+
fileType: file.type,
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const { signedUrl, fileUrl } = await response.data;
|
|
117
|
+
|
|
118
|
+
// const uploadResponse = await fetch(signedUrl, {
|
|
119
|
+
// method: 'PUT',
|
|
120
|
+
// body: file,
|
|
121
|
+
// headers: {
|
|
122
|
+
// 'Content-Type': file.type,
|
|
123
|
+
// // 'x-amz-acl': 'public-read'
|
|
124
|
+
// },
|
|
125
|
+
// });
|
|
126
|
+
|
|
127
|
+
const xhr = new XMLHttpRequest();
|
|
128
|
+
xhr.open('PUT', signedUrl, true);
|
|
129
|
+
xhr.setRequestHeader('Content-Type', file.type);
|
|
130
|
+
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
xhr.upload.onprogress = (event) => {
|
|
133
|
+
if (event.lengthComputable && onProgress) {
|
|
134
|
+
const progress = Math.round((event.loaded / event.total) * 100);
|
|
135
|
+
onProgress(progress);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
xhr.onload = () => {
|
|
140
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
141
|
+
resolve({
|
|
142
|
+
url: fileUrl,
|
|
143
|
+
name: file.name,
|
|
144
|
+
size: file.size,
|
|
145
|
+
type: getFileType(file.type),
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
reject(new Error('Upload failed'));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
xhr.onerror = () => reject(new Error('Upload failed'));
|
|
153
|
+
xhr.send(file);
|
|
136
154
|
});
|
|
137
|
-
|
|
138
|
-
if (!uploadResponse.ok) {
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
};
|
|
155
|
+
|
|
156
|
+
// if (!uploadResponse.ok) {
|
|
157
|
+
// throw new Error('Upload failed');
|
|
158
|
+
// }
|
|
159
|
+
|
|
160
|
+
// return {
|
|
161
|
+
// url: fileUrl,
|
|
162
|
+
// name: file.name,
|
|
163
|
+
// size: file.size,
|
|
164
|
+
// type: getFileType(file.type),
|
|
165
|
+
// };
|
|
148
166
|
};
|
|
149
167
|
|
|
150
168
|
const handleSubmit = useCallback(
|
|
151
169
|
async (e: any) => {
|
|
152
170
|
e.preventDefault();
|
|
153
|
-
|
|
154
|
-
|
|
171
|
+
if (!message && attachmentsRef.current.length === 0 || isSending) return;
|
|
155
172
|
setIsSending(true);
|
|
173
|
+
|
|
174
|
+
const tempId = generateTempId();
|
|
175
|
+
setTempMessageId(tempId);
|
|
176
|
+
|
|
177
|
+
const optimisticMessage = {
|
|
178
|
+
_id: tempId,
|
|
179
|
+
text: message, // Added text property to match the expected type
|
|
180
|
+
message: message,
|
|
181
|
+
senderId: userId,
|
|
182
|
+
status: 'sending' as MessageStatus,
|
|
183
|
+
createdAt: new Date().toISOString(),
|
|
184
|
+
media: attachmentsRef.current.map(att => ({
|
|
185
|
+
type: att.type,
|
|
186
|
+
url: att.previewUrl,
|
|
187
|
+
name: att.file.name,
|
|
188
|
+
size: att.file.size,
|
|
189
|
+
uploadProgress: 0,
|
|
190
|
+
uploadError: "",
|
|
191
|
+
})),
|
|
192
|
+
isUploading: true,
|
|
193
|
+
isOptimistic: true
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
setMessages(prev => [...prev, optimisticMessage]);
|
|
197
|
+
|
|
156
198
|
try {
|
|
199
|
+
|
|
157
200
|
const uploadedFiles = await Promise.all(
|
|
158
|
-
|
|
201
|
+
attachmentsRef.current.map(async (attachment,index) => {
|
|
159
202
|
try {
|
|
160
|
-
const result = await uploadToS3(attachment.file)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
203
|
+
const result = await uploadToS3(attachment.file,(progress)=>{
|
|
204
|
+
setMessages(prev => prev.map(msg =>{
|
|
205
|
+
if(msg._id === tempId){
|
|
206
|
+
const updatedMedia = [...msg.media!];
|
|
207
|
+
updatedMedia[index] = {
|
|
208
|
+
...updatedMedia[index],
|
|
209
|
+
uploadProgress: progress
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
...msg,
|
|
213
|
+
media: updatedMedia
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return msg;
|
|
217
|
+
}))
|
|
218
|
+
});
|
|
219
|
+
console.log("Uploaded file:", result);
|
|
220
|
+
return result;
|
|
221
|
+
// return {
|
|
222
|
+
// type: attachment.type,
|
|
223
|
+
// url: result.url,
|
|
224
|
+
// name: result.name,
|
|
225
|
+
// size: result.size
|
|
226
|
+
// };
|
|
167
227
|
} catch (error) {
|
|
168
|
-
console.error(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
228
|
+
console.error(`Error uploading file ${attachment.file.name}:`, error);
|
|
229
|
+
setMessages(prev => prev.map(msg => {
|
|
230
|
+
if(msg._id === tempId){
|
|
231
|
+
const updatedMedia = [...msg.media!];
|
|
232
|
+
updatedMedia[index] = {
|
|
233
|
+
...updatedMedia[index],
|
|
234
|
+
uploadError: "Upload failed"
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
...msg,
|
|
238
|
+
media: updatedMedia
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return msg;
|
|
242
|
+
}))
|
|
172
243
|
return null;
|
|
173
244
|
}
|
|
174
245
|
})
|
|
175
246
|
);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const successfulUploads = uploadedFiles.filter(
|
|
179
|
-
|
|
180
|
-
console.log("📤 Sending message:",
|
|
181
|
-
mutation.mutate(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
const successfulUploads = uploadedFiles.filter(file => file !== null);
|
|
250
|
+
|
|
251
|
+
console.log("📤 Sending message:", successfulUploads);
|
|
252
|
+
mutation.mutate({
|
|
253
|
+
chatId: selectedConversation?.participantDetails._id,
|
|
254
|
+
senderId: userId,
|
|
255
|
+
message,
|
|
256
|
+
attachments: successfulUploads,
|
|
257
|
+
}, {
|
|
258
|
+
onSuccess: (data) => {
|
|
259
|
+
console.log('Response from sendMessage:', data);
|
|
260
|
+
|
|
261
|
+
socket.emit("sendMessage", {
|
|
262
|
+
chatId: selectedConversation?._id,
|
|
263
|
+
message,
|
|
264
|
+
messageId: data[1]._id,
|
|
265
|
+
attachments: successfulUploads,
|
|
266
|
+
senderId: userId,
|
|
267
|
+
receiverId: selectedConversation?.participantDetails._id,
|
|
268
|
+
});
|
|
269
|
+
setMessages(prev => {
|
|
270
|
+
console.log("Removing optimistic message:", prev);
|
|
271
|
+
|
|
272
|
+
// Definitely remove the optimistic message
|
|
273
|
+
const filtered = prev.filter(msg => msg._id !== tempMessageId);
|
|
274
|
+
// Add the real message from server
|
|
275
|
+
console.log("Adding real message:", filtered);
|
|
276
|
+
|
|
277
|
+
return [...filtered, {
|
|
278
|
+
...data[1],
|
|
279
|
+
isUploading: false,
|
|
280
|
+
isOptimistic: false}
|
|
281
|
+
];
|
|
282
|
+
});
|
|
186
283
|
},
|
|
187
|
-
{
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
senderId: userId,
|
|
196
|
-
receiverId: selectedConversation?.participantDetails._id,
|
|
197
|
-
});
|
|
198
|
-
},
|
|
199
|
-
onError: (error) => {
|
|
200
|
-
console.error("❌ Error in sending message:", error);
|
|
201
|
-
},
|
|
202
|
-
}
|
|
203
|
-
);
|
|
284
|
+
onError: (error) => {
|
|
285
|
+
console.error("❌ Error in sending message:", error);
|
|
286
|
+
setMessages(prev => prev.map(msg =>
|
|
287
|
+
msg._id === tempId ? { ...msg, status: 'failed' } : msg
|
|
288
|
+
));
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
204
292
|
} catch (error) {
|
|
205
293
|
console.error("❌ Error sending message:", error);
|
|
294
|
+
setMessages(prev => prev.map(msg =>
|
|
295
|
+
msg._id === tempId ? { ...msg, status: 'failed' } : msg
|
|
296
|
+
));
|
|
206
297
|
} finally {
|
|
207
298
|
setIsSending(false);
|
|
208
299
|
setMessage("");
|
|
300
|
+
setAttachments([]);
|
|
301
|
+
setTempMessageId(null);
|
|
209
302
|
}
|
|
210
303
|
},
|
|
211
304
|
[message, selectedConversation, userId, isSending]
|
|
212
305
|
);
|
|
213
306
|
|
|
214
|
-
const [showAttachmentOptions, setShowAttachmentOptions] = useState(false);
|
|
215
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
216
|
-
|
|
217
307
|
useEffect(() => {
|
|
218
308
|
return () => {
|
|
219
|
-
attachments.forEach(
|
|
309
|
+
attachments.forEach(attachment => {
|
|
220
310
|
URL.revokeObjectURL(attachment.previewUrl);
|
|
221
311
|
});
|
|
222
312
|
};
|
|
@@ -236,17 +326,22 @@ const MessageInput = () => {
|
|
|
236
326
|
|
|
237
327
|
const getAcceptString = (type: FileType): string => {
|
|
238
328
|
switch (type) {
|
|
239
|
-
case
|
|
240
|
-
return ACCEPTED_IMAGE_TYPES.join(
|
|
241
|
-
case
|
|
242
|
-
return ACCEPTED_VIDEO_TYPES.join(
|
|
243
|
-
case
|
|
244
|
-
return ACCEPTED_DOCUMENT_TYPES.join(
|
|
329
|
+
case 'image':
|
|
330
|
+
return ACCEPTED_IMAGE_TYPES.join(',');
|
|
331
|
+
case 'video':
|
|
332
|
+
return ACCEPTED_VIDEO_TYPES.join(',');
|
|
333
|
+
case 'document':
|
|
334
|
+
return ACCEPTED_DOCUMENT_TYPES.join(',');
|
|
245
335
|
default:
|
|
246
|
-
return
|
|
336
|
+
return '*';
|
|
247
337
|
}
|
|
248
338
|
};
|
|
249
339
|
|
|
340
|
+
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
attachmentsRef.current = attachments;
|
|
343
|
+
}, [attachments]);
|
|
344
|
+
|
|
250
345
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
251
346
|
const files = e.target.files;
|
|
252
347
|
if (!files || files.length === 0) return;
|
|
@@ -257,39 +352,37 @@ const MessageInput = () => {
|
|
|
257
352
|
const file = files[i];
|
|
258
353
|
const fileType = getFileType(file.type);
|
|
259
354
|
|
|
260
|
-
// Validate file type
|
|
261
355
|
if (!fileType) {
|
|
262
356
|
console.error(`Unsupported file type: ${file.type}`);
|
|
263
357
|
continue;
|
|
264
358
|
}
|
|
265
359
|
|
|
266
|
-
// Validate file size
|
|
267
360
|
if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) {
|
|
268
361
|
console.error(`File too large: ${file.name}`);
|
|
269
362
|
continue;
|
|
270
363
|
}
|
|
271
364
|
|
|
272
|
-
|
|
273
|
-
const previewUrl =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
: URL.createObjectURL(file);
|
|
365
|
+
|
|
366
|
+
const previewUrl = fileType === 'document'
|
|
367
|
+
? URL.createObjectURL(new Blob([''], { type: 'application/pdf' })) // Placeholder for documents
|
|
368
|
+
: URL.createObjectURL(file);
|
|
277
369
|
|
|
278
370
|
newAttachments.push({
|
|
279
371
|
file,
|
|
280
372
|
type: fileType,
|
|
281
|
-
previewUrl
|
|
373
|
+
previewUrl
|
|
282
374
|
});
|
|
283
375
|
}
|
|
284
376
|
|
|
285
|
-
setAttachments(
|
|
377
|
+
setAttachments(prev => [...prev, ...newAttachments]);
|
|
286
378
|
if (fileInputRef.current) {
|
|
287
|
-
fileInputRef.current.value =
|
|
379
|
+
fileInputRef.current.value = '';
|
|
288
380
|
}
|
|
289
381
|
};
|
|
382
|
+
|
|
290
383
|
|
|
291
384
|
const removeAttachment = (index: number) => {
|
|
292
|
-
setAttachments(
|
|
385
|
+
setAttachments(prev => {
|
|
293
386
|
const newAttachments = [...prev];
|
|
294
387
|
URL.revokeObjectURL(newAttachments[index].previewUrl);
|
|
295
388
|
newAttachments.splice(index, 1);
|
|
@@ -299,154 +392,120 @@ const MessageInput = () => {
|
|
|
299
392
|
|
|
300
393
|
return (
|
|
301
394
|
<div className="message-input-container">
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
/>
|
|
313
|
-
))}
|
|
314
|
-
</div>
|
|
315
|
-
)}
|
|
316
|
-
|
|
317
|
-
<form className="chatMessageInputform" onSubmit={handleSubmit}>
|
|
318
|
-
<div className="chatMessageInputdiv">
|
|
319
|
-
{/* Hidden file input */}
|
|
320
|
-
<input
|
|
321
|
-
type="file"
|
|
322
|
-
ref={fileInputRef}
|
|
323
|
-
style={{ display: "none" }}
|
|
324
|
-
onChange={handleFileChange}
|
|
325
|
-
multiple
|
|
326
|
-
/>
|
|
327
|
-
|
|
328
|
-
{/* Attachment button and options */}
|
|
329
|
-
<div
|
|
330
|
-
className="attachment-container"
|
|
331
|
-
style={{ position: "relative" }}
|
|
332
|
-
>
|
|
333
|
-
<button
|
|
334
|
-
type="button"
|
|
335
|
-
className="attachment-button"
|
|
336
|
-
onClick={handleAttachmentClick}
|
|
337
|
-
style={{
|
|
338
|
-
background: "none",
|
|
339
|
-
border: "none",
|
|
340
|
-
cursor: "pointer",
|
|
341
|
-
padding: "8px",
|
|
342
|
-
}}
|
|
343
|
-
>
|
|
344
|
-
<div className="attachment-icon">
|
|
345
|
-
<svg
|
|
346
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
347
|
-
width="18"
|
|
348
|
-
height="18"
|
|
349
|
-
viewBox="0 0 24 24"
|
|
350
|
-
fill="none"
|
|
351
|
-
stroke="currentColor"
|
|
352
|
-
strokeWidth="2"
|
|
353
|
-
strokeLinecap="round"
|
|
354
|
-
strokeLinejoin="round"
|
|
355
|
-
>
|
|
356
|
-
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
|
|
357
|
-
</svg>
|
|
358
|
-
</div>
|
|
359
|
-
</button>
|
|
360
|
-
|
|
361
|
-
{showAttachmentOptions && (
|
|
362
|
-
<div className="attachment-options">
|
|
363
|
-
<button type="button" onClick={() => handleFileSelect("image")}>
|
|
364
|
-
<div className="icon">
|
|
365
|
-
<svg
|
|
366
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
367
|
-
width="18"
|
|
368
|
-
height="18"
|
|
369
|
-
viewBox="0 0 24 24"
|
|
370
|
-
fill="none"
|
|
371
|
-
stroke="currentColor"
|
|
372
|
-
strokeWidth="2"
|
|
373
|
-
strokeLinecap="round"
|
|
374
|
-
strokeLinejoin="round"
|
|
375
|
-
>
|
|
376
|
-
<rect
|
|
377
|
-
x="3"
|
|
378
|
-
y="3"
|
|
379
|
-
width="18"
|
|
380
|
-
height="18"
|
|
381
|
-
rx="2"
|
|
382
|
-
ry="2"
|
|
383
|
-
></rect>
|
|
384
|
-
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
|
385
|
-
<polyline points="21 15 16 10 5 21"></polyline>
|
|
386
|
-
</svg>
|
|
387
|
-
</div>
|
|
388
|
-
<span>Photos & videos</span>
|
|
389
|
-
</button>
|
|
390
|
-
<button
|
|
391
|
-
type="button"
|
|
392
|
-
onClick={() => handleFileSelect("document")}
|
|
393
|
-
>
|
|
394
|
-
<div className="icon">
|
|
395
|
-
<svg
|
|
396
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
397
|
-
width="18"
|
|
398
|
-
height="18"
|
|
399
|
-
viewBox="0 0 24 24"
|
|
400
|
-
fill="none"
|
|
401
|
-
stroke="currentColor"
|
|
402
|
-
strokeWidth="2"
|
|
403
|
-
strokeLinecap="round"
|
|
404
|
-
strokeLinejoin="round"
|
|
405
|
-
>
|
|
406
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
407
|
-
<polyline points="14 2 14 8 20 8"></polyline>
|
|
408
|
-
<line x1="16" y1="13" x2="8" y2="13"></line>
|
|
409
|
-
<line x1="16" y1="17" x2="8" y2="17"></line>
|
|
410
|
-
<polyline points="10 9 9 9 8 9"></polyline>
|
|
411
|
-
</svg>
|
|
412
|
-
</div>
|
|
413
|
-
<span>Document</span>
|
|
414
|
-
</button>
|
|
415
|
-
</div>
|
|
416
|
-
)}
|
|
417
|
-
</div>
|
|
418
|
-
|
|
419
|
-
<input
|
|
420
|
-
type="text"
|
|
421
|
-
className="chatMessageInput"
|
|
422
|
-
placeholder="Send a message"
|
|
423
|
-
value={message}
|
|
424
|
-
onChange={(e) => setMessage(e.target.value)}
|
|
395
|
+
{/* Preview area for attachments */}
|
|
396
|
+
{attachments.length > 0 && (
|
|
397
|
+
<div className="attachments-preview">
|
|
398
|
+
{attachments.map((attachment, index) => (
|
|
399
|
+
<FilePreview
|
|
400
|
+
key={index}
|
|
401
|
+
file={attachment.file}
|
|
402
|
+
type={attachment.type}
|
|
403
|
+
previewUrl={attachment.previewUrl}
|
|
404
|
+
onRemove={() => removeAttachment(index)}
|
|
425
405
|
/>
|
|
406
|
+
))}
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
<form className="chatMessageInputform" onSubmit={handleSubmit}>
|
|
411
|
+
<div className="chatMessageInputdiv">
|
|
412
|
+
{/* Hidden file input */}
|
|
413
|
+
<input
|
|
414
|
+
type="file"
|
|
415
|
+
ref={fileInputRef}
|
|
416
|
+
style={{ display: 'none' }}
|
|
417
|
+
onChange={handleFileChange}
|
|
418
|
+
multiple
|
|
419
|
+
/>
|
|
420
|
+
|
|
421
|
+
{/* Attachment button and options */}
|
|
422
|
+
<div className="attachment-container" style={{ position: 'relative' }}>
|
|
426
423
|
<button
|
|
427
|
-
type="
|
|
428
|
-
className="
|
|
429
|
-
|
|
424
|
+
type="button"
|
|
425
|
+
className="attachment-button"
|
|
426
|
+
onClick={handleAttachmentClick}
|
|
427
|
+
style={{
|
|
428
|
+
background: 'none',
|
|
429
|
+
border: 'none',
|
|
430
|
+
cursor: 'pointer',
|
|
431
|
+
padding: '8px',
|
|
432
|
+
}}
|
|
430
433
|
>
|
|
431
|
-
<
|
|
434
|
+
<div className="attachment-icon">
|
|
435
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
436
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
|
|
437
|
+
</svg>
|
|
438
|
+
</div>
|
|
432
439
|
</button>
|
|
433
|
-
</div>
|
|
434
440
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
<div className="
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
441
|
+
{showAttachmentOptions && (
|
|
442
|
+
<div className="attachment-options">
|
|
443
|
+
<button
|
|
444
|
+
type="button"
|
|
445
|
+
onClick={() => handleFileSelect('image')}
|
|
446
|
+
>
|
|
447
|
+
<div className="icon">
|
|
448
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
449
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
450
|
+
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
|
451
|
+
<polyline points="21 15 16 10 5 21"></polyline>
|
|
452
|
+
</svg>
|
|
453
|
+
</div>
|
|
454
|
+
<span>Photos & videos</span>
|
|
455
|
+
</button>
|
|
456
|
+
<button
|
|
457
|
+
type="button"
|
|
458
|
+
onClick={() => handleFileSelect('document')}
|
|
459
|
+
>
|
|
460
|
+
<div className="icon">
|
|
461
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
462
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
463
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
464
|
+
<line x1="16" y1="13" x2="8" y2="13"></line>
|
|
465
|
+
<line x1="16" y1="17" x2="8" y2="17"></line>
|
|
466
|
+
<polyline points="10 9 9 9 8 9"></polyline>
|
|
467
|
+
</svg>
|
|
468
|
+
</div>
|
|
469
|
+
<span>Document</span>
|
|
470
|
+
</button>
|
|
446
471
|
</div>
|
|
447
472
|
)}
|
|
448
|
-
|
|
449
|
-
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<textarea
|
|
476
|
+
className="chatMessageInput"
|
|
477
|
+
placeholder="Send a message"
|
|
478
|
+
value={message}
|
|
479
|
+
onChange={(e) => {
|
|
480
|
+
setMessage(e.target.value)
|
|
481
|
+
autoResizeTextarea(e.target)
|
|
482
|
+
}}
|
|
483
|
+
rows={1}
|
|
484
|
+
style={{ resize: "none" }}
|
|
485
|
+
onKeyDown={(e) => {
|
|
486
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
487
|
+
e.preventDefault()
|
|
488
|
+
handleSubmit(e)
|
|
489
|
+
}
|
|
490
|
+
}}
|
|
491
|
+
/>
|
|
492
|
+
<button type="submit" className="chatMessageInputSubmit" disabled={isSending}>
|
|
493
|
+
<img width={10} height={10} src={paperplane} alt="send" />
|
|
494
|
+
</button>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
{typingUser && typingUser !== userId && typingUser === selectedConversation?.participantDetails?._id && !isSending && (
|
|
498
|
+
<div className="typingIndicator">
|
|
499
|
+
<div className="loader">
|
|
500
|
+
<div className="ball" />
|
|
501
|
+
<div className="ball" />
|
|
502
|
+
<div className="ball" />
|
|
503
|
+
typing
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
)}
|
|
507
|
+
</form>
|
|
508
|
+
</div>
|
|
450
509
|
);
|
|
451
510
|
};
|
|
452
511
|
|