@pubuduth-aplicy/chat-ui 2.1.50 → 2.1.52
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/common/FilePreview.tsx +91 -0
- package/src/components/messages/Message.tsx +175 -54
- package/src/components/messages/MessageContainer.tsx +81 -11
- package/src/components/messages/MessageInput.tsx +383 -15
- package/src/components/messages/Messages.tsx +17 -2
- package/src/components/sidebar/Conversation.tsx +2 -1
- package/src/components/sidebar/Conversations.tsx +26 -2
- package/src/components/sidebar/SearchInput.tsx +10 -2
- package/src/components/sidebar/Sidebar.tsx +1 -2
- package/src/service/messageService.ts +6 -2
- package/src/stores/Zustant.ts +19 -2
- package/src/style/style.css +168 -13
- package/src/types/type.ts +1 -1
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import React, { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { useMessageMutation } from "../../hooks/mutations/useSendMessage";
|
|
4
4
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
5
5
|
import useChatUIStore from "../../stores/Zustant";
|
|
6
6
|
import paperplane from "../../assets/icons8-send-50.png";
|
|
7
7
|
// import { PaperPlaneRight } from '@phosphor-icons/react'; // Assuming you're using icons from Phosphor Icons library
|
|
8
8
|
// import useSendMessage from '../../hooks/useSendMessage'; // Importing the useSendMessage hook
|
|
9
|
+
import { FilePreview, FileType } from "../common/FilePreview";
|
|
10
|
+
import { getApiClient } from "../../lib/api/apiClient";
|
|
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'];
|
|
16
|
+
|
|
17
|
+
interface Attachment {
|
|
18
|
+
file: File;
|
|
19
|
+
type: FileType;
|
|
20
|
+
previewUrl: string;
|
|
21
|
+
uploadProgress?: number;
|
|
22
|
+
uploadError?: string;
|
|
23
|
+
}
|
|
9
24
|
|
|
10
25
|
const MessageInput = () => {
|
|
26
|
+
const apiClient = getApiClient();
|
|
11
27
|
const { socket } = useChatContext();
|
|
12
28
|
const { userId } = useChatContext();
|
|
13
|
-
const { selectedConversation } = useChatUIStore();
|
|
14
|
-
const [message, setMessage] = useState("");
|
|
15
|
-
|
|
16
|
-
const mutation = useMessageMutation(); // useMutation hook to send message
|
|
29
|
+
const { selectedConversation,setMessages } = useChatUIStore();
|
|
30
|
+
const [message, setMessage] = useState("");
|
|
31
|
+
const mutation = useMessageMutation();
|
|
17
32
|
const [typingUser, setTypingUser] = useState<string | null>(null);
|
|
18
33
|
const [isSending, setIsSending] = useState(false);
|
|
19
34
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
20
|
-
const [isTyping, setIsTyping] = useState(false);
|
|
35
|
+
const [isTyping, setIsTyping] = useState(false);
|
|
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
|
+
}
|
|
21
49
|
|
|
22
50
|
useEffect(() => {
|
|
23
51
|
if (selectedConversation?._id) {
|
|
@@ -71,58 +99,398 @@ const MessageInput = () => {
|
|
|
71
99
|
};
|
|
72
100
|
}, [socket, selectedConversation?._id]);
|
|
73
101
|
|
|
102
|
+
const getFileType = (mimeType: string): FileType | null => {
|
|
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';
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
|
|
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);
|
|
154
|
+
});
|
|
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
|
+
// };
|
|
166
|
+
};
|
|
167
|
+
|
|
74
168
|
const handleSubmit = useCallback(
|
|
75
169
|
async (e: any) => {
|
|
76
170
|
e.preventDefault();
|
|
77
|
-
if (!message || isSending) return;
|
|
78
|
-
|
|
171
|
+
if (!message && attachmentsRef.current.length === 0 || isSending) return;
|
|
79
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
|
+
|
|
80
198
|
try {
|
|
81
|
-
|
|
199
|
+
|
|
200
|
+
const uploadedFiles = await Promise.all(
|
|
201
|
+
attachmentsRef.current.map(async (attachment,index) => {
|
|
202
|
+
try {
|
|
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
|
+
// };
|
|
227
|
+
} catch (error) {
|
|
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
|
+
}))
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
const successfulUploads = uploadedFiles.filter(file => file !== null);
|
|
250
|
+
|
|
251
|
+
console.log("📤 Sending message:", successfulUploads);
|
|
82
252
|
mutation.mutate({
|
|
83
253
|
chatId: selectedConversation?.participantDetails._id,
|
|
84
254
|
senderId: userId,
|
|
85
255
|
message,
|
|
256
|
+
attachments: successfulUploads,
|
|
86
257
|
}, {
|
|
87
258
|
onSuccess: (data) => {
|
|
88
259
|
console.log('Response from sendMessage:', data);
|
|
89
|
-
|
|
260
|
+
|
|
90
261
|
socket.emit("sendMessage", {
|
|
91
262
|
chatId: selectedConversation?._id,
|
|
92
263
|
message,
|
|
93
264
|
messageId: data[1]._id,
|
|
265
|
+
attachments: successfulUploads,
|
|
94
266
|
senderId: userId,
|
|
95
267
|
receiverId: selectedConversation?.participantDetails._id,
|
|
96
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
|
+
});
|
|
97
283
|
},
|
|
98
284
|
onError: (error) => {
|
|
99
285
|
console.error("❌ Error in sending message:", error);
|
|
286
|
+
setMessages(prev => prev.map(msg =>
|
|
287
|
+
msg._id === tempId ? { ...msg, status: 'failed' } : msg
|
|
288
|
+
));
|
|
100
289
|
},
|
|
101
290
|
});
|
|
102
291
|
|
|
103
292
|
} catch (error) {
|
|
104
293
|
console.error("❌ Error sending message:", error);
|
|
294
|
+
setMessages(prev => prev.map(msg =>
|
|
295
|
+
msg._id === tempId ? { ...msg, status: 'failed' } : msg
|
|
296
|
+
));
|
|
105
297
|
} finally {
|
|
106
298
|
setIsSending(false);
|
|
107
299
|
setMessage("");
|
|
300
|
+
setAttachments([]);
|
|
301
|
+
setTempMessageId(null);
|
|
108
302
|
}
|
|
109
303
|
},
|
|
110
304
|
[message, selectedConversation, userId, isSending]
|
|
111
305
|
);
|
|
112
306
|
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
return () => {
|
|
309
|
+
attachments.forEach(attachment => {
|
|
310
|
+
URL.revokeObjectURL(attachment.previewUrl);
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
}, [attachments]);
|
|
314
|
+
|
|
315
|
+
const handleAttachmentClick = () => {
|
|
316
|
+
setShowAttachmentOptions(!showAttachmentOptions);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const handleFileSelect = (type: FileType) => {
|
|
320
|
+
if (fileInputRef.current) {
|
|
321
|
+
fileInputRef.current.accept = getAcceptString(type);
|
|
322
|
+
fileInputRef.current.click();
|
|
323
|
+
}
|
|
324
|
+
setShowAttachmentOptions(false);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const getAcceptString = (type: FileType): string => {
|
|
328
|
+
switch (type) {
|
|
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(',');
|
|
335
|
+
default:
|
|
336
|
+
return '*';
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
attachmentsRef.current = attachments;
|
|
343
|
+
}, [attachments]);
|
|
344
|
+
|
|
345
|
+
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
346
|
+
const files = e.target.files;
|
|
347
|
+
if (!files || files.length === 0) return;
|
|
348
|
+
|
|
349
|
+
const newAttachments: Attachment[] = [];
|
|
350
|
+
|
|
351
|
+
for (let i = 0; i < files.length; i++) {
|
|
352
|
+
const file = files[i];
|
|
353
|
+
const fileType = getFileType(file.type);
|
|
354
|
+
|
|
355
|
+
if (!fileType) {
|
|
356
|
+
console.error(`Unsupported file type: ${file.type}`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) {
|
|
361
|
+
console.error(`File too large: ${file.name}`);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
const previewUrl = fileType === 'document'
|
|
367
|
+
? URL.createObjectURL(new Blob([''], { type: 'application/pdf' })) // Placeholder for documents
|
|
368
|
+
: URL.createObjectURL(file);
|
|
369
|
+
|
|
370
|
+
newAttachments.push({
|
|
371
|
+
file,
|
|
372
|
+
type: fileType,
|
|
373
|
+
previewUrl
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
setAttachments(prev => [...prev, ...newAttachments]);
|
|
378
|
+
if (fileInputRef.current) {
|
|
379
|
+
fileInputRef.current.value = '';
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
const removeAttachment = (index: number) => {
|
|
385
|
+
setAttachments(prev => {
|
|
386
|
+
const newAttachments = [...prev];
|
|
387
|
+
URL.revokeObjectURL(newAttachments[index].previewUrl);
|
|
388
|
+
newAttachments.splice(index, 1);
|
|
389
|
+
return newAttachments;
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
|
|
113
393
|
return (
|
|
394
|
+
<div className="message-input-container">
|
|
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)}
|
|
405
|
+
/>
|
|
406
|
+
))}
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
|
|
114
410
|
<form className="chatMessageInputform" onSubmit={handleSubmit}>
|
|
115
411
|
<div className="chatMessageInputdiv">
|
|
412
|
+
{/* Hidden file input */}
|
|
116
413
|
<input
|
|
117
|
-
type="
|
|
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' }}>
|
|
423
|
+
<button
|
|
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
|
+
}}
|
|
433
|
+
>
|
|
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>
|
|
439
|
+
</button>
|
|
440
|
+
|
|
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>
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<textarea
|
|
118
476
|
className="chatMessageInput"
|
|
119
477
|
placeholder="Send a message"
|
|
120
478
|
value={message}
|
|
121
|
-
onChange={(e) =>
|
|
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
|
+
}}
|
|
122
491
|
/>
|
|
123
|
-
<button type="submit" className="chatMessageInputSubmit">
|
|
492
|
+
<button type="submit" className="chatMessageInputSubmit" disabled={isSending}>
|
|
124
493
|
<img width={10} height={10} src={paperplane} alt="send" />
|
|
125
|
-
{/* {loading ? <div className='loading loading-spinner'></div> : <PaperPlaneRight />} Show loading spinner if loading */}
|
|
126
494
|
</button>
|
|
127
495
|
</div>
|
|
128
496
|
|
|
@@ -136,8 +504,8 @@ const MessageInput = () => {
|
|
|
136
504
|
</div>
|
|
137
505
|
</div>
|
|
138
506
|
)}
|
|
139
|
-
|
|
140
507
|
</form>
|
|
508
|
+
</div>
|
|
141
509
|
);
|
|
142
510
|
};
|
|
143
511
|
|
|
@@ -51,7 +51,22 @@ const Messages = () => {
|
|
|
51
51
|
const handleNewMessage = (newMessage: any) => {
|
|
52
52
|
newMessage.shouldShake = true;
|
|
53
53
|
console.log("📩 New message received:", newMessage);
|
|
54
|
-
setMessages((prevMessages) => [...prevMessages, newMessage[1]]);
|
|
54
|
+
// setMessages((prevMessages) => [...prevMessages, newMessage[1]]);
|
|
55
|
+
setMessages(prevMessages => {
|
|
56
|
+
console.log("prevMessages", prevMessages);
|
|
57
|
+
|
|
58
|
+
const isDuplicate = prevMessages.some(msg =>
|
|
59
|
+
msg._id === newMessage[1]._id ||
|
|
60
|
+
(msg.isOptimistic && msg.senderId === userId && msg.message === newMessage[1].message)
|
|
61
|
+
);
|
|
62
|
+
console.log('isDuplicate', isDuplicate);
|
|
63
|
+
|
|
64
|
+
if (isDuplicate) {
|
|
65
|
+
return prevMessages;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return [...prevMessages, newMessage[1]];
|
|
69
|
+
});
|
|
55
70
|
};
|
|
56
71
|
|
|
57
72
|
// const handleNewMessage = (newMessage: any) => {
|
|
@@ -72,7 +87,7 @@ const Messages = () => {
|
|
|
72
87
|
status: string;
|
|
73
88
|
}) => {
|
|
74
89
|
setMessages((prev) =>
|
|
75
|
-
prev.map((msg) => (msg.
|
|
90
|
+
prev.map((msg) => (msg._id === messageId ? { ...msg, status } : msg))
|
|
76
91
|
);
|
|
77
92
|
};
|
|
78
93
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
1
2
|
// import { useSocketContext } from "../../context/SocketContext";
|
|
2
3
|
// import useConversation from "../../zustand/useConversation";
|
|
3
4
|
|
|
@@ -76,7 +77,7 @@ const Conversation = ({ conversation, lastIdx }: ConversationProps) => {
|
|
|
76
77
|
</div>
|
|
77
78
|
</div>
|
|
78
79
|
|
|
79
|
-
{!lastIdx && <div className="divider my-0 py-0 h-1" />}
|
|
80
|
+
{/* {!lastIdx && <div className="divider my-0 py-0 h-1" />} */}
|
|
80
81
|
</>
|
|
81
82
|
);
|
|
82
83
|
};
|
|
@@ -12,7 +12,29 @@ const Conversations = () => {
|
|
|
12
12
|
// const { loading, conversations } = useGetConversations();
|
|
13
13
|
return (
|
|
14
14
|
<div className="chatSidebarConversations">
|
|
15
|
-
<h2 className="text-lg font-semibold text-gray-700">All
|
|
15
|
+
<h2 className="text-lg font-semibold text-gray-700">All Messages</h2>
|
|
16
|
+
{(!conversations || conversations.length === 0) && (
|
|
17
|
+
<div className="flex flex-col items-center justify-center p-8 text-center">
|
|
18
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 mb-4">
|
|
19
|
+
<svg
|
|
20
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
21
|
+
width="32"
|
|
22
|
+
height="32"
|
|
23
|
+
viewBox="0 0 24 24"
|
|
24
|
+
fill="none"
|
|
25
|
+
stroke="currentColor"
|
|
26
|
+
strokeWidth="2"
|
|
27
|
+
strokeLinecap="round"
|
|
28
|
+
strokeLinejoin="round"
|
|
29
|
+
className="text-gray-500"
|
|
30
|
+
>
|
|
31
|
+
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
|
32
|
+
</svg>
|
|
33
|
+
</div>
|
|
34
|
+
<h3 className="text-xl font-semibold mb-1">No Conversations</h3>
|
|
35
|
+
<p className="text-gray-500 text-sm mb-4">There are no conversations under "All messages"</p>
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
16
38
|
{conversations?.map((conversation: any, idx: any) => (
|
|
17
39
|
<Conversation
|
|
18
40
|
key={conversation._id}
|
|
@@ -21,9 +43,11 @@ const Conversations = () => {
|
|
|
21
43
|
/>
|
|
22
44
|
))}
|
|
23
45
|
|
|
46
|
+
|
|
47
|
+
|
|
24
48
|
{/* {loading ? <span className='loading loading-spinner mx-auto'></span> : null} */}
|
|
25
49
|
</div>
|
|
26
50
|
);
|
|
27
51
|
};
|
|
28
52
|
|
|
29
|
-
export default Conversations;
|
|
53
|
+
export default Conversations;
|
|
@@ -22,12 +22,20 @@ const {userId} =useChatContext()
|
|
|
22
22
|
return
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const conversation = data?.find((c: { _id: string; participantDetails: { username: string } }) =>
|
|
25
|
+
const conversation = data?.find((c: { _id: string; participantDetails: { username: string; firstname?: string; idpic?: string } }) =>
|
|
26
26
|
c.participantDetails.username.toLowerCase().includes(search.toLowerCase())
|
|
27
27
|
);
|
|
28
28
|
|
|
29
29
|
if (conversation) {
|
|
30
|
-
|
|
30
|
+
const updatedConversation = {
|
|
31
|
+
...conversation,
|
|
32
|
+
participantDetails: {
|
|
33
|
+
...conversation.participantDetails,
|
|
34
|
+
firstname: conversation.participantDetails.username || "Unknown",
|
|
35
|
+
idpic: conversation.participantDetails.profilePic || "default-idpic.png",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
setSelectedConversation(updatedConversation);
|
|
31
39
|
setSearch("");
|
|
32
40
|
}
|
|
33
41
|
console.error("No such user found!");
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { FileType } from "../components/common/FilePreview";
|
|
2
3
|
import { getApiClient } from "../lib/api/apiClient";
|
|
3
4
|
import { Path } from "../lib/api/endpoint";
|
|
4
5
|
|
|
5
|
-
export const sendMessage = async ({ chatId,senderId, message }: { chatId: any; senderId:any; message: string }) => {
|
|
6
|
+
export const sendMessage = async ({ chatId,senderId, message,attachments }: { chatId: any; senderId:any; message: string,attachments: { type: FileType; url: string; name: string; size: number; }[] }) => {
|
|
6
7
|
const apiClient = getApiClient();
|
|
8
|
+
console.log("sendMessage", chatId, senderId, message, attachments); // Log the parameters
|
|
9
|
+
|
|
7
10
|
const response = await apiClient.post(`${Path.sendmessage}/${chatId}/${senderId}`, {
|
|
8
|
-
message:message
|
|
11
|
+
message:message,
|
|
12
|
+
attachments:attachments
|
|
9
13
|
})
|
|
10
14
|
return response.data;
|
|
11
15
|
};
|
package/src/stores/Zustant.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { FileType } from "../components/common/FilePreview";
|
|
2
3
|
import { create } from "zustand";
|
|
3
4
|
|
|
4
5
|
interface ChatUIState {
|
|
@@ -16,7 +17,23 @@ interface ChatUIState {
|
|
|
16
17
|
setSelectedConversation: (
|
|
17
18
|
selectedConversation: ChatUIState["selectedConversation"]
|
|
18
19
|
) => void;
|
|
19
|
-
messages: {
|
|
20
|
+
messages: {
|
|
21
|
+
_id: string;
|
|
22
|
+
text: string;
|
|
23
|
+
senderId: string;
|
|
24
|
+
status: string ;
|
|
25
|
+
isOptimistic:boolean;
|
|
26
|
+
message:string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
media: {
|
|
29
|
+
type: FileType;
|
|
30
|
+
url: string;
|
|
31
|
+
name: string;
|
|
32
|
+
size: number;
|
|
33
|
+
uploadProgress: number;
|
|
34
|
+
uploadError:string
|
|
35
|
+
}[];
|
|
36
|
+
isUploading: boolean;}[];
|
|
20
37
|
setMessages: (messages: ChatUIState["messages"] | ((prev: ChatUIState["messages"]) => ChatUIState["messages"])) => void;
|
|
21
38
|
updateMessageStatus: (messageId: string, status: string) => void;
|
|
22
39
|
toggleChat: () => void;
|
|
@@ -38,7 +55,7 @@ const useChatUIStore = create<ChatUIState>((set) => ({
|
|
|
38
55
|
updateMessageStatus: (messageId, status) =>
|
|
39
56
|
set((state) => ({
|
|
40
57
|
messages: state.messages.map((msg) =>
|
|
41
|
-
msg.
|
|
58
|
+
msg._id === messageId ? { ...msg, status } : msg
|
|
42
59
|
),
|
|
43
60
|
})),
|
|
44
61
|
setSelectedConversation: (selectedConversation) => set({ selectedConversation }),
|