@pubuduth-aplicy/chat-ui 2.1.67 → 2.1.69
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 +2 -1
- package/src/components/Chat.tsx +32 -59
- package/src/components/messages/Message.tsx +173 -7
- package/src/components/messages/MessageContainer.tsx +55 -40
- package/src/components/messages/MessageInput.tsx +333 -248
- package/src/components/messages/Messages.tsx +58 -91
- package/src/components/sidebar/Conversation.tsx +55 -38
- package/src/hooks/mutations/useDeleteMessage.ts +26 -0
- package/src/hooks/mutations/useEditMessage.ts +25 -0
- package/src/lib/api/apiClient.ts +1 -1
- package/src/lib/api/endpoint.ts +3 -1
- package/src/providers/ChatProvider.tsx +90 -63
- package/src/service/messageService.ts +46 -1
- package/src/style/style.css +36 -13
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
1
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
2
|
import { useMessageMutation } from "../../hooks/mutations/useSendMessage";
|
|
4
3
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
5
4
|
import useChatUIStore from "../../stores/Zustant";
|
|
6
5
|
import paperplane from "../../assets/icons8-send-50.png";
|
|
7
|
-
// import { PaperPlaneRight } from '@phosphor-icons/react'; // Assuming you're using icons from Phosphor Icons library
|
|
8
|
-
// import useSendMessage from '../../hooks/useSendMessage'; // Importing the useSendMessage hook
|
|
9
6
|
import { FilePreview, FileType } from "../common/FilePreview";
|
|
10
7
|
import { getApiClient } from "../../lib/api/apiClient";
|
|
11
8
|
import { MessageStatus } from "../../types/type";
|
|
12
9
|
import { Path } from "../../lib/api/endpoint";
|
|
13
|
-
|
|
10
|
+
|
|
11
|
+
const MAX_FILE_SIZE_MB = 5;
|
|
14
12
|
const MAX_FILE_COUNT = 5;
|
|
15
|
-
const ACCEPTED_IMAGE_TYPES = [
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const ACCEPTED_IMAGE_TYPES = [
|
|
14
|
+
"image/jpeg",
|
|
15
|
+
"image/png",
|
|
16
|
+
"image/gif",
|
|
17
|
+
"image/webp",
|
|
18
|
+
"video/mp4",
|
|
19
|
+
"video/webm",
|
|
20
|
+
"video/ogg",
|
|
21
|
+
];
|
|
22
|
+
const ACCEPTED_VIDEO_TYPES = ["video/mp4", "video/webm", "video/ogg"];
|
|
23
|
+
const ACCEPTED_DOCUMENT_TYPES = [
|
|
24
|
+
"application/pdf",
|
|
25
|
+
"application/msword",
|
|
26
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
27
|
+
"text/plain",
|
|
28
|
+
];
|
|
18
29
|
|
|
19
30
|
interface Attachment {
|
|
20
31
|
file: File;
|
|
@@ -26,15 +37,13 @@ interface Attachment {
|
|
|
26
37
|
|
|
27
38
|
const MessageInput = () => {
|
|
28
39
|
const apiClient = getApiClient();
|
|
29
|
-
const { socket } = useChatContext();
|
|
30
|
-
const { userId } = useChatContext();
|
|
40
|
+
const { socket, sendMessage, userId } = useChatContext();
|
|
31
41
|
const { selectedConversation, setMessages } = useChatUIStore();
|
|
32
42
|
const [message, setMessage] = useState("");
|
|
33
43
|
const [message1, setMessage1] = useState("");
|
|
34
44
|
const mutation = useMessageMutation();
|
|
35
45
|
const [typingUser, setTypingUser] = useState<string | null>(null);
|
|
36
46
|
const [isSending, setIsSending] = useState(false);
|
|
37
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
38
47
|
const [isTyping, setIsTyping] = useState(false);
|
|
39
48
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
|
40
49
|
const [showAttachmentOptions, setShowAttachmentOptions] = useState(false);
|
|
@@ -43,63 +52,101 @@ const MessageInput = () => {
|
|
|
43
52
|
const [tempMessageId, setTempMessageId] = useState<string | null>(null);
|
|
44
53
|
const [inputError, setInputError] = useState<string | null>(null);
|
|
45
54
|
const attachmentsContainerRef = useRef<HTMLDivElement>(null);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// const autoResizeTextarea = (element: HTMLTextAreaElement) => {
|
|
50
|
-
// element.style.height = "auto"
|
|
51
|
-
// element.style.height = Math.min(150, element.scrollHeight) + "px"
|
|
52
|
-
// }
|
|
55
|
+
const typingTimeoutRef = useRef<number | null>(null);
|
|
56
|
+
const generateTempId = () =>
|
|
57
|
+
`temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
53
58
|
|
|
59
|
+
// Join chat room when conversation is selected
|
|
54
60
|
useEffect(() => {
|
|
55
|
-
if (selectedConversation?._id) {
|
|
56
|
-
|
|
61
|
+
if (selectedConversation?._id && socket?.readyState === WebSocket.OPEN) {
|
|
62
|
+
sendMessage({
|
|
63
|
+
event: "joinChat",
|
|
64
|
+
data: {
|
|
65
|
+
chatId: selectedConversation._id,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
57
68
|
}
|
|
58
|
-
}, [selectedConversation, socket]);
|
|
69
|
+
}, [selectedConversation?._id, socket, sendMessage]);
|
|
59
70
|
|
|
71
|
+
// Typing indicator logic
|
|
60
72
|
useEffect(() => {
|
|
61
|
-
if (!socket) return;
|
|
73
|
+
if (!socket || !selectedConversation?._id) return;
|
|
62
74
|
|
|
63
75
|
if (message.trim() !== "") {
|
|
64
76
|
setIsTyping(true);
|
|
65
|
-
|
|
77
|
+
sendMessage({
|
|
78
|
+
event: "typing",
|
|
79
|
+
data: {
|
|
80
|
+
chatId: selectedConversation._id,
|
|
81
|
+
userId,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
66
84
|
}
|
|
67
85
|
|
|
68
|
-
|
|
86
|
+
typingTimeoutRef.current = setTimeout(() => {
|
|
69
87
|
if (message.trim() === "") {
|
|
70
88
|
setIsTyping(false);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
sendMessage({
|
|
90
|
+
event: "stopTyping",
|
|
91
|
+
data: {
|
|
92
|
+
chatId: selectedConversation._id,
|
|
93
|
+
userId,
|
|
94
|
+
},
|
|
74
95
|
});
|
|
75
96
|
}
|
|
76
97
|
}, 200);
|
|
77
98
|
|
|
78
|
-
return () =>
|
|
79
|
-
|
|
99
|
+
return () => {
|
|
100
|
+
if (typingTimeoutRef.current) {
|
|
101
|
+
clearTimeout(typingTimeoutRef.current);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}, [message, socket, selectedConversation?._id, userId, sendMessage]);
|
|
105
|
+
|
|
106
|
+
// Listen for typing indicators from others
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!socket || !selectedConversation?._id) return;
|
|
80
109
|
|
|
110
|
+
const handleMessage = (event: MessageEvent) => {
|
|
111
|
+
try {
|
|
112
|
+
const data = JSON.parse(event.data);
|
|
113
|
+
console.log("Received WebSocket message:", data);
|
|
114
|
+
if (
|
|
115
|
+
data.event === "typing" &&
|
|
116
|
+
data.data.chatId === selectedConversation._id
|
|
117
|
+
) {
|
|
118
|
+
console.log("Setting typing user:", data.data.userId);
|
|
119
|
+
setTypingUser(data.data.userId);
|
|
120
|
+
} else if (
|
|
121
|
+
data.event === "stopTyping" &&
|
|
122
|
+
data.data.chatId === selectedConversation._id
|
|
123
|
+
) {
|
|
124
|
+
setTypingUser((prev) => (prev === data.data.userId ? null : prev));
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error("Error parsing typing message:", error);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
socket.addEventListener("message", handleMessage);
|
|
132
|
+
return () => {
|
|
133
|
+
socket.removeEventListener("message", handleMessage);
|
|
134
|
+
};
|
|
135
|
+
}, [socket, selectedConversation?._id]);
|
|
81
136
|
|
|
82
137
|
const validateInput = (text: string) => {
|
|
83
|
-
// Check for email
|
|
84
138
|
const emailRegex = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
|
|
85
139
|
if (emailRegex.test(text)) {
|
|
86
140
|
return "To protect your account, make sure not to share any personal or sensitive information.";
|
|
87
141
|
}
|
|
88
142
|
|
|
89
|
-
|
|
90
|
-
|
|
143
|
+
const phoneRegex =
|
|
144
|
+
/(\+?\d{1,4}[\s-]?)?(\(?\d{3}\)?[\s-]?)?\d{3}[\s-]?\d{4}/;
|
|
91
145
|
if (phoneRegex.test(text)) {
|
|
92
146
|
return "To protect your account, make sure not to share any personal or sensitive information.";
|
|
93
147
|
}
|
|
94
148
|
|
|
95
|
-
|
|
96
|
-
// for (const word of badWords) {
|
|
97
|
-
// if (text.toLowerCase().includes(word)) {
|
|
98
|
-
// return "Inappropriate language is not allowed.";
|
|
99
|
-
// }
|
|
100
|
-
// }
|
|
101
|
-
|
|
102
|
-
return null; // No errors
|
|
149
|
+
return null;
|
|
103
150
|
};
|
|
104
151
|
|
|
105
152
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
@@ -114,63 +161,29 @@ const MessageInput = () => {
|
|
|
114
161
|
setInputError(null);
|
|
115
162
|
setMessage(newValue);
|
|
116
163
|
setMessage1(newValue);
|
|
117
|
-
|
|
118
164
|
}
|
|
119
165
|
};
|
|
120
166
|
|
|
121
|
-
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
if (!socket || !selectedConversation?._id) return;
|
|
124
|
-
|
|
125
|
-
const handleTyping = ({ userId, chatId }: { userId: string; chatId: string }) => {
|
|
126
|
-
if (chatId === selectedConversation._id) {
|
|
127
|
-
setTypingUser(userId);
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const handleStopTyping = ({ userId, chatId }: { userId: string; chatId: string }) => {
|
|
132
|
-
if (chatId === selectedConversation._id) {
|
|
133
|
-
setTypingUser((prev) => (prev === userId ? null : prev));
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
socket.on("typing", handleTyping);
|
|
138
|
-
socket.on("stopTyping", handleStopTyping);
|
|
139
|
-
|
|
140
|
-
return () => {
|
|
141
|
-
socket.off("typing", handleTyping);
|
|
142
|
-
socket.off("stopTyping", handleStopTyping);
|
|
143
|
-
};
|
|
144
|
-
}, [socket, selectedConversation?._id]);
|
|
145
|
-
|
|
146
167
|
const getFileType = (mimeType: string): FileType | null => {
|
|
147
|
-
if (ACCEPTED_IMAGE_TYPES.includes(mimeType)) return
|
|
148
|
-
if (ACCEPTED_VIDEO_TYPES.includes(mimeType)) return
|
|
149
|
-
if (ACCEPTED_DOCUMENT_TYPES.includes(mimeType)) return
|
|
168
|
+
if (ACCEPTED_IMAGE_TYPES.includes(mimeType)) return "image";
|
|
169
|
+
if (ACCEPTED_VIDEO_TYPES.includes(mimeType)) return "video";
|
|
170
|
+
if (ACCEPTED_DOCUMENT_TYPES.includes(mimeType)) return "document";
|
|
150
171
|
return null;
|
|
151
172
|
};
|
|
152
173
|
|
|
153
|
-
const uploadToS3 = async (
|
|
174
|
+
const uploadToS3 = async (
|
|
175
|
+
file: File,
|
|
176
|
+
onProgress?: (progress: number) => void
|
|
177
|
+
): Promise<{ url: string; name: string; size: number; type: FileType }> => {
|
|
154
178
|
const response = await apiClient.post(`${Path.preSignUrl}`, {
|
|
155
179
|
fileName: file.name,
|
|
156
180
|
fileType: file.type,
|
|
157
|
-
}
|
|
158
|
-
);
|
|
181
|
+
});
|
|
159
182
|
|
|
160
183
|
const { signedUrl, fileUrl } = await response.data;
|
|
161
|
-
|
|
162
|
-
// const uploadResponse = await fetch(signedUrl, {
|
|
163
|
-
// method: 'PUT',
|
|
164
|
-
// body: file,
|
|
165
|
-
// headers: {
|
|
166
|
-
// 'Content-Type': file.type,
|
|
167
|
-
// // 'x-amz-acl': 'public-read'
|
|
168
|
-
// },
|
|
169
|
-
// });
|
|
170
|
-
|
|
171
184
|
const xhr = new XMLHttpRequest();
|
|
172
|
-
xhr.open(
|
|
173
|
-
xhr.setRequestHeader(
|
|
185
|
+
xhr.open("PUT", signedUrl, true);
|
|
186
|
+
xhr.setRequestHeader("Content-Type", file.type);
|
|
174
187
|
|
|
175
188
|
return new Promise((resolve, reject) => {
|
|
176
189
|
xhr.upload.onprogress = (event) => {
|
|
@@ -189,30 +202,20 @@ const MessageInput = () => {
|
|
|
189
202
|
type: getFileType(file.type),
|
|
190
203
|
});
|
|
191
204
|
} else {
|
|
192
|
-
reject(new Error(
|
|
205
|
+
reject(new Error("Upload failed"));
|
|
193
206
|
}
|
|
194
207
|
};
|
|
195
208
|
|
|
196
|
-
xhr.onerror = () => reject(new Error(
|
|
209
|
+
xhr.onerror = () => reject(new Error("Upload failed"));
|
|
197
210
|
xhr.send(file);
|
|
198
211
|
});
|
|
199
|
-
|
|
200
|
-
// if (!uploadResponse.ok) {
|
|
201
|
-
// throw new Error('Upload failed');
|
|
202
|
-
// }
|
|
203
|
-
|
|
204
|
-
// return {
|
|
205
|
-
// url: fileUrl,
|
|
206
|
-
// name: file.name,
|
|
207
|
-
// size: file.size,
|
|
208
|
-
// type: getFileType(file.type),
|
|
209
|
-
// };
|
|
210
212
|
};
|
|
211
213
|
|
|
212
214
|
const handleSubmit = useCallback(
|
|
213
|
-
async (e:
|
|
215
|
+
async (e: React.FormEvent) => {
|
|
214
216
|
e.preventDefault();
|
|
215
|
-
if (!message && attachmentsRef.current.length === 0 || isSending)
|
|
217
|
+
if ((!message && attachmentsRef.current.length === 0) || isSending)
|
|
218
|
+
return;
|
|
216
219
|
setIsSending(true);
|
|
217
220
|
setAttachments([]);
|
|
218
221
|
setMessage("");
|
|
@@ -221,12 +224,12 @@ const MessageInput = () => {
|
|
|
221
224
|
|
|
222
225
|
const optimisticMessage = {
|
|
223
226
|
_id: tempId,
|
|
224
|
-
text: message1,
|
|
227
|
+
text: message1,
|
|
225
228
|
message: message1,
|
|
226
229
|
senderId: userId,
|
|
227
|
-
status:
|
|
230
|
+
status: "sending" as MessageStatus,
|
|
228
231
|
createdAt: new Date().toISOString(),
|
|
229
|
-
media: attachmentsRef.current.map(att => ({
|
|
232
|
+
media: attachmentsRef.current.map((att) => ({
|
|
230
233
|
type: att.type,
|
|
231
234
|
url: att.previewUrl,
|
|
232
235
|
name: att.file.name,
|
|
@@ -235,111 +238,117 @@ const MessageInput = () => {
|
|
|
235
238
|
uploadError: "",
|
|
236
239
|
})),
|
|
237
240
|
isUploading: true,
|
|
238
|
-
isOptimistic: true
|
|
241
|
+
isOptimistic: true,
|
|
239
242
|
};
|
|
240
243
|
|
|
241
|
-
setMessages(prev => [...prev, optimisticMessage]);
|
|
244
|
+
setMessages((prev) => [...prev, optimisticMessage]);
|
|
242
245
|
|
|
243
246
|
try {
|
|
244
|
-
|
|
245
247
|
const uploadedFiles = await Promise.all(
|
|
246
248
|
attachmentsRef.current.map(async (attachment, index) => {
|
|
247
249
|
try {
|
|
248
250
|
const result = await uploadToS3(attachment.file, (progress) => {
|
|
249
|
-
setMessages(prev =>
|
|
251
|
+
setMessages((prev) =>
|
|
252
|
+
prev.map((msg) => {
|
|
253
|
+
if (msg._id === tempId) {
|
|
254
|
+
const updatedMedia = [...msg.media!];
|
|
255
|
+
updatedMedia[index] = {
|
|
256
|
+
...updatedMedia[index],
|
|
257
|
+
uploadProgress: progress,
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
...msg,
|
|
261
|
+
media: updatedMedia,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return msg;
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
return result;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error(
|
|
271
|
+
`Error uploading file ${attachment.file.name}:`,
|
|
272
|
+
error
|
|
273
|
+
);
|
|
274
|
+
setMessages((prev) =>
|
|
275
|
+
prev.map((msg) => {
|
|
250
276
|
if (msg._id === tempId) {
|
|
251
277
|
const updatedMedia = [...msg.media!];
|
|
252
278
|
updatedMedia[index] = {
|
|
253
279
|
...updatedMedia[index],
|
|
254
|
-
|
|
280
|
+
uploadError: "Upload failed",
|
|
255
281
|
};
|
|
256
282
|
return {
|
|
257
283
|
...msg,
|
|
258
|
-
media: updatedMedia
|
|
284
|
+
media: updatedMedia,
|
|
259
285
|
};
|
|
260
286
|
}
|
|
261
287
|
return msg;
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
console.log("Uploaded file:", result);
|
|
265
|
-
return result;
|
|
266
|
-
// return {
|
|
267
|
-
// type: attachment.type,
|
|
268
|
-
// url: result.url,
|
|
269
|
-
// name: result.name,
|
|
270
|
-
// size: result.size
|
|
271
|
-
// };
|
|
272
|
-
} catch (error) {
|
|
273
|
-
console.error(`Error uploading file ${attachment.file.name}:`, error);
|
|
274
|
-
setMessages(prev => prev.map(msg => {
|
|
275
|
-
if (msg._id === tempId) {
|
|
276
|
-
const updatedMedia = [...msg.media!];
|
|
277
|
-
updatedMedia[index] = {
|
|
278
|
-
...updatedMedia[index],
|
|
279
|
-
uploadError: "Upload failed"
|
|
280
|
-
};
|
|
281
|
-
return {
|
|
282
|
-
...msg,
|
|
283
|
-
media: updatedMedia
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
return msg;
|
|
287
|
-
}))
|
|
288
|
+
})
|
|
289
|
+
);
|
|
288
290
|
return null;
|
|
289
291
|
}
|
|
290
292
|
})
|
|
291
293
|
);
|
|
292
294
|
|
|
295
|
+
const successfulUploads = uploadedFiles.filter((file) => file !== null);
|
|
293
296
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}, {
|
|
303
|
-
onSuccess: (data) => {
|
|
304
|
-
console.log('Response from sendMessage:', data);
|
|
305
|
-
setMessages(prev => {
|
|
306
|
-
console.log("Removing optimistic message:", prev);
|
|
307
|
-
|
|
308
|
-
// Definitely remove the optimistic message
|
|
309
|
-
const filtered = prev.filter(msg => msg._id !== tempMessageId);
|
|
310
|
-
// Add the real message from server
|
|
311
|
-
console.log("Adding real message:", filtered);
|
|
312
|
-
|
|
313
|
-
return [...filtered, {
|
|
314
|
-
...data[1],
|
|
315
|
-
isUploading: false,
|
|
316
|
-
isOptimistic: false
|
|
317
|
-
}
|
|
318
|
-
];
|
|
319
|
-
});
|
|
320
|
-
socket.emit("sendMessage", {
|
|
321
|
-
chatId: selectedConversation?._id,
|
|
322
|
-
message: message1,
|
|
323
|
-
messageId: data[1]._id,
|
|
324
|
-
attachments: successfulUploads,
|
|
325
|
-
senderId: userId,
|
|
326
|
-
receiverId: selectedConversation?.participantDetails._id,
|
|
327
|
-
});
|
|
328
|
-
|
|
297
|
+
mutation.mutate(
|
|
298
|
+
{
|
|
299
|
+
chatId:
|
|
300
|
+
!Array.isArray(selectedConversation?.participantDetails) &&
|
|
301
|
+
selectedConversation?.participantDetails._id,
|
|
302
|
+
senderId: userId,
|
|
303
|
+
message: message1,
|
|
304
|
+
attachments: successfulUploads,
|
|
329
305
|
},
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
306
|
+
{
|
|
307
|
+
onSuccess: (data) => {
|
|
308
|
+
setMessages((prev) => {
|
|
309
|
+
const filtered = prev.filter(
|
|
310
|
+
(msg) => msg._id !== tempMessageId
|
|
311
|
+
);
|
|
312
|
+
return [
|
|
313
|
+
...filtered,
|
|
314
|
+
{
|
|
315
|
+
...data[1],
|
|
316
|
+
isUploading: false,
|
|
317
|
+
isOptimistic: false,
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
});
|
|
337
321
|
|
|
322
|
+
// Send message via WebSocket
|
|
323
|
+
sendMessage({
|
|
324
|
+
type: "sendMessage",
|
|
325
|
+
chatId: selectedConversation?._id,
|
|
326
|
+
message: message1,
|
|
327
|
+
messageId: data[1]._id,
|
|
328
|
+
attachments: successfulUploads,
|
|
329
|
+
senderId: userId,
|
|
330
|
+
receiverId:
|
|
331
|
+
!Array.isArray(selectedConversation?.participantDetails) &&
|
|
332
|
+
selectedConversation?.participantDetails._id,
|
|
333
|
+
});
|
|
334
|
+
},
|
|
335
|
+
onError: (error) => {
|
|
336
|
+
console.error("Error in sending message:", error);
|
|
337
|
+
setMessages((prev) =>
|
|
338
|
+
prev.map((msg) =>
|
|
339
|
+
msg._id === tempId ? { ...msg, status: "failed" } : msg
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
);
|
|
338
345
|
} catch (error) {
|
|
339
|
-
console.error("
|
|
340
|
-
setMessages(prev =>
|
|
341
|
-
|
|
342
|
-
|
|
346
|
+
console.error("Error sending message:", error);
|
|
347
|
+
setMessages((prev) =>
|
|
348
|
+
prev.map((msg) =>
|
|
349
|
+
msg._id === tempId ? { ...msg, status: "failed" } : msg
|
|
350
|
+
)
|
|
351
|
+
);
|
|
343
352
|
} finally {
|
|
344
353
|
setIsSending(false);
|
|
345
354
|
setMessage1("");
|
|
@@ -347,17 +356,33 @@ const MessageInput = () => {
|
|
|
347
356
|
setTempMessageId(null);
|
|
348
357
|
}
|
|
349
358
|
},
|
|
350
|
-
[
|
|
359
|
+
[
|
|
360
|
+
message,
|
|
361
|
+
message1,
|
|
362
|
+
selectedConversation,
|
|
363
|
+
userId,
|
|
364
|
+
isSending,
|
|
365
|
+
mutation,
|
|
366
|
+
setMessages,
|
|
367
|
+
sendMessage,
|
|
368
|
+
]
|
|
351
369
|
);
|
|
352
370
|
|
|
371
|
+
// Clean up object URLs when component unmounts
|
|
353
372
|
useEffect(() => {
|
|
354
373
|
return () => {
|
|
355
|
-
attachments.forEach(attachment => {
|
|
374
|
+
attachments.forEach((attachment) => {
|
|
356
375
|
URL.revokeObjectURL(attachment.previewUrl);
|
|
357
376
|
});
|
|
358
377
|
};
|
|
359
378
|
}, [attachments]);
|
|
360
379
|
|
|
380
|
+
// Update attachments ref
|
|
381
|
+
useEffect(() => {
|
|
382
|
+
attachmentsRef.current = attachments;
|
|
383
|
+
}, [attachments]);
|
|
384
|
+
|
|
385
|
+
// File attachment handlers
|
|
361
386
|
const handleAttachmentClick = () => {
|
|
362
387
|
setShowAttachmentOptions(!showAttachmentOptions);
|
|
363
388
|
};
|
|
@@ -372,38 +397,39 @@ const MessageInput = () => {
|
|
|
372
397
|
|
|
373
398
|
const getAcceptString = (type: FileType): string => {
|
|
374
399
|
switch (type) {
|
|
375
|
-
case
|
|
376
|
-
return ACCEPTED_IMAGE_TYPES.join(
|
|
377
|
-
case
|
|
378
|
-
return ACCEPTED_VIDEO_TYPES.join(
|
|
379
|
-
case
|
|
380
|
-
return ACCEPTED_DOCUMENT_TYPES.join(
|
|
400
|
+
case "image":
|
|
401
|
+
return ACCEPTED_IMAGE_TYPES.join(",");
|
|
402
|
+
case "video":
|
|
403
|
+
return ACCEPTED_VIDEO_TYPES.join(",");
|
|
404
|
+
case "document":
|
|
405
|
+
return ACCEPTED_DOCUMENT_TYPES.join(",");
|
|
381
406
|
default:
|
|
382
|
-
return
|
|
407
|
+
return "*";
|
|
383
408
|
}
|
|
384
409
|
};
|
|
385
410
|
|
|
386
|
-
|
|
387
|
-
useEffect(() => {
|
|
388
|
-
attachmentsRef.current = attachments;
|
|
389
|
-
}, [attachments]);
|
|
390
|
-
|
|
391
411
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
392
412
|
const files = e.target.files;
|
|
393
413
|
if (!files || files.length === 0) return;
|
|
394
414
|
|
|
395
|
-
// Check if adding these files would exceed the maximum count
|
|
396
415
|
if (attachments.length + files.length > MAX_FILE_COUNT) {
|
|
397
416
|
alert(`You can only attach up to ${MAX_FILE_COUNT} files`);
|
|
398
417
|
return;
|
|
399
418
|
}
|
|
400
419
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
420
|
+
const newFilesSize = Array.from(files).reduce(
|
|
421
|
+
(total, file) => total + file.size,
|
|
422
|
+
0
|
|
423
|
+
);
|
|
424
|
+
const currentAttachmentsSize = attachments.reduce(
|
|
425
|
+
(total, att) => total + att.file.size,
|
|
426
|
+
0
|
|
427
|
+
);
|
|
404
428
|
|
|
405
|
-
|
|
406
|
-
|
|
429
|
+
if (
|
|
430
|
+
currentAttachmentsSize + newFilesSize >
|
|
431
|
+
MAX_FILE_SIZE_MB * 1024 * 1024
|
|
432
|
+
) {
|
|
407
433
|
alert(`Total file size cannot exceed ${MAX_FILE_SIZE_MB}MB`);
|
|
408
434
|
return;
|
|
409
435
|
}
|
|
@@ -424,35 +450,36 @@ const MessageInput = () => {
|
|
|
424
450
|
continue;
|
|
425
451
|
}
|
|
426
452
|
|
|
427
|
-
const previewUrl =
|
|
428
|
-
|
|
429
|
-
|
|
453
|
+
const previewUrl =
|
|
454
|
+
fileType === "document"
|
|
455
|
+
? URL.createObjectURL(new Blob([""], { type: "application/pdf" }))
|
|
456
|
+
: URL.createObjectURL(file);
|
|
430
457
|
|
|
431
458
|
newAttachments.push({
|
|
432
459
|
file,
|
|
433
460
|
type: fileType,
|
|
434
|
-
previewUrl
|
|
461
|
+
previewUrl,
|
|
435
462
|
});
|
|
436
463
|
}
|
|
437
464
|
|
|
438
|
-
setAttachments(prev => [...prev, ...newAttachments]);
|
|
465
|
+
setAttachments((prev) => [...prev, ...newAttachments]);
|
|
439
466
|
if (fileInputRef.current) {
|
|
440
|
-
fileInputRef.current.value =
|
|
467
|
+
fileInputRef.current.value = "";
|
|
441
468
|
}
|
|
442
469
|
};
|
|
443
470
|
|
|
444
|
-
const scrollAttachments = (direction:
|
|
471
|
+
const scrollAttachments = (direction: "left" | "right") => {
|
|
445
472
|
if (attachmentsContainerRef.current) {
|
|
446
|
-
const scrollAmount = direction ===
|
|
473
|
+
const scrollAmount = direction === "right" ? 200 : -200;
|
|
447
474
|
attachmentsContainerRef.current.scrollBy({
|
|
448
475
|
left: scrollAmount,
|
|
449
|
-
behavior:
|
|
476
|
+
behavior: "smooth",
|
|
450
477
|
});
|
|
451
478
|
}
|
|
452
479
|
};
|
|
453
480
|
|
|
454
481
|
const removeAttachment = (index: number) => {
|
|
455
|
-
setAttachments(prev => {
|
|
482
|
+
setAttachments((prev) => {
|
|
456
483
|
const newAttachments = [...prev];
|
|
457
484
|
URL.revokeObjectURL(newAttachments[index].previewUrl);
|
|
458
485
|
newAttachments.splice(index, 1);
|
|
@@ -462,12 +489,11 @@ const MessageInput = () => {
|
|
|
462
489
|
|
|
463
490
|
return (
|
|
464
491
|
<div className="message-input-container">
|
|
465
|
-
{/* Preview area for attachments */}
|
|
466
492
|
{attachments.length > 0 && (
|
|
467
493
|
<div className="attachments-preview-container">
|
|
468
494
|
<button
|
|
469
495
|
className="scroll-button left"
|
|
470
|
-
onClick={() => scrollAttachments(
|
|
496
|
+
onClick={() => scrollAttachments("left")}
|
|
471
497
|
disabled={attachments.length <= 3}
|
|
472
498
|
>
|
|
473
499
|
<
|
|
@@ -485,7 +511,10 @@ const MessageInput = () => {
|
|
|
485
511
|
))}
|
|
486
512
|
|
|
487
513
|
{attachments.length < MAX_FILE_COUNT && (
|
|
488
|
-
<div
|
|
514
|
+
<div
|
|
515
|
+
className="add-more-files"
|
|
516
|
+
onClick={() => fileInputRef.current?.click()}
|
|
517
|
+
>
|
|
489
518
|
<div className="plus-icon">+</div>
|
|
490
519
|
<div className="add-more-text">Add more</div>
|
|
491
520
|
</div>
|
|
@@ -494,7 +523,7 @@ const MessageInput = () => {
|
|
|
494
523
|
|
|
495
524
|
<button
|
|
496
525
|
className="scroll-button right"
|
|
497
|
-
onClick={() => scrollAttachments(
|
|
526
|
+
onClick={() => scrollAttachments("right")}
|
|
498
527
|
disabled={attachments.length <= 3}
|
|
499
528
|
>
|
|
500
529
|
>
|
|
@@ -503,33 +532,46 @@ const MessageInput = () => {
|
|
|
503
532
|
)}
|
|
504
533
|
|
|
505
534
|
<form className="chatMessageInputform" onSubmit={handleSubmit}>
|
|
506
|
-
{inputError &&
|
|
535
|
+
{inputError && (
|
|
536
|
+
<p style={{ color: "red", fontSize: "12px" }}>{inputError}</p>
|
|
537
|
+
)}
|
|
507
538
|
|
|
508
539
|
<div className="chatMessageInputdiv">
|
|
509
|
-
{/* Hidden file input */}
|
|
510
540
|
<input
|
|
511
541
|
type="file"
|
|
512
542
|
ref={fileInputRef}
|
|
513
|
-
style={{ display:
|
|
543
|
+
style={{ display: "none" }}
|
|
514
544
|
onChange={handleFileChange}
|
|
515
545
|
multiple
|
|
516
546
|
/>
|
|
517
547
|
|
|
518
|
-
|
|
519
|
-
|
|
548
|
+
<div
|
|
549
|
+
className="attachment-container"
|
|
550
|
+
style={{ position: "relative" }}
|
|
551
|
+
>
|
|
520
552
|
<button
|
|
521
553
|
type="button"
|
|
522
554
|
className="attachment-button"
|
|
523
555
|
onClick={handleAttachmentClick}
|
|
524
556
|
style={{
|
|
525
|
-
background:
|
|
526
|
-
border:
|
|
527
|
-
cursor:
|
|
528
|
-
padding:
|
|
557
|
+
background: "none",
|
|
558
|
+
border: "none",
|
|
559
|
+
cursor: "pointer",
|
|
560
|
+
padding: "8px",
|
|
529
561
|
}}
|
|
530
562
|
>
|
|
531
563
|
<div className="attachment-icon">
|
|
532
|
-
<svg
|
|
564
|
+
<svg
|
|
565
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
566
|
+
width="18"
|
|
567
|
+
height="18"
|
|
568
|
+
viewBox="0 0 24 24"
|
|
569
|
+
fill="none"
|
|
570
|
+
stroke="currentColor"
|
|
571
|
+
strokeWidth="2"
|
|
572
|
+
strokeLinecap="round"
|
|
573
|
+
strokeLinejoin="round"
|
|
574
|
+
>
|
|
533
575
|
<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>
|
|
534
576
|
</svg>
|
|
535
577
|
</div>
|
|
@@ -537,13 +579,27 @@ const MessageInput = () => {
|
|
|
537
579
|
|
|
538
580
|
{showAttachmentOptions && (
|
|
539
581
|
<div className="attachment-options">
|
|
540
|
-
<button
|
|
541
|
-
type="button"
|
|
542
|
-
onClick={() => handleFileSelect('image')}
|
|
543
|
-
>
|
|
582
|
+
<button type="button" onClick={() => handleFileSelect("image")}>
|
|
544
583
|
<div className="icon">
|
|
545
|
-
<svg
|
|
546
|
-
|
|
584
|
+
<svg
|
|
585
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
586
|
+
width="18"
|
|
587
|
+
height="18"
|
|
588
|
+
viewBox="0 0 24 24"
|
|
589
|
+
fill="none"
|
|
590
|
+
stroke="currentColor"
|
|
591
|
+
strokeWidth="2"
|
|
592
|
+
strokeLinecap="round"
|
|
593
|
+
strokeLinejoin="round"
|
|
594
|
+
>
|
|
595
|
+
<rect
|
|
596
|
+
x="3"
|
|
597
|
+
y="3"
|
|
598
|
+
width="18"
|
|
599
|
+
height="18"
|
|
600
|
+
rx="2"
|
|
601
|
+
ry="2"
|
|
602
|
+
></rect>
|
|
547
603
|
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
|
548
604
|
<polyline points="21 15 16 10 5 21"></polyline>
|
|
549
605
|
</svg>
|
|
@@ -552,10 +608,20 @@ const MessageInput = () => {
|
|
|
552
608
|
</button>
|
|
553
609
|
<button
|
|
554
610
|
type="button"
|
|
555
|
-
onClick={() => handleFileSelect(
|
|
611
|
+
onClick={() => handleFileSelect("document")}
|
|
556
612
|
>
|
|
557
613
|
<div className="icon">
|
|
558
|
-
<svg
|
|
614
|
+
<svg
|
|
615
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
616
|
+
width="18"
|
|
617
|
+
height="18"
|
|
618
|
+
viewBox="0 0 24 24"
|
|
619
|
+
fill="none"
|
|
620
|
+
stroke="currentColor"
|
|
621
|
+
strokeWidth="2"
|
|
622
|
+
strokeLinecap="round"
|
|
623
|
+
strokeLinejoin="round"
|
|
624
|
+
>
|
|
559
625
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
560
626
|
<polyline points="14 2 14 8 20 8"></polyline>
|
|
561
627
|
<line x1="16" y1="13" x2="8" y2="13"></line>
|
|
@@ -573,36 +639,55 @@ const MessageInput = () => {
|
|
|
573
639
|
className="chatMessageInput"
|
|
574
640
|
placeholder="Send a message"
|
|
575
641
|
value={message}
|
|
576
|
-
// onChange={(e) => {
|
|
577
|
-
// setMessage(e.target.value)
|
|
578
|
-
// autoResizeTextarea(e.target)
|
|
579
|
-
// }}
|
|
580
642
|
onChange={handleChange}
|
|
581
643
|
rows={1}
|
|
582
644
|
style={{ resize: "none" }}
|
|
583
645
|
onKeyDown={(e) => {
|
|
584
646
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
585
|
-
e.preventDefault()
|
|
586
|
-
handleSubmit(e)
|
|
647
|
+
e.preventDefault();
|
|
648
|
+
handleSubmit(e);
|
|
587
649
|
}
|
|
588
650
|
}}
|
|
589
651
|
/>
|
|
590
652
|
|
|
591
|
-
<button
|
|
592
|
-
|
|
653
|
+
<button
|
|
654
|
+
type="submit"
|
|
655
|
+
className="chatMessageInputSubmit"
|
|
656
|
+
disabled={!!inputError || isSending}
|
|
657
|
+
>
|
|
658
|
+
<svg
|
|
659
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
660
|
+
className="icon"
|
|
661
|
+
width="14"
|
|
662
|
+
height="14"
|
|
663
|
+
viewBox="0 0 24 24"
|
|
664
|
+
fill="none"
|
|
665
|
+
stroke="#ffffff"
|
|
666
|
+
stroke-width="2"
|
|
667
|
+
stroke-linecap="round"
|
|
668
|
+
stroke-linejoin="round"
|
|
669
|
+
>
|
|
670
|
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
|
671
|
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
|
672
|
+
</svg>
|
|
593
673
|
</button>
|
|
594
674
|
</div>
|
|
595
675
|
|
|
596
|
-
{typingUser &&
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
676
|
+
{typingUser &&
|
|
677
|
+
typingUser !== userId &&
|
|
678
|
+
typingUser ===
|
|
679
|
+
(!Array.isArray(selectedConversation?.participantDetails) &&
|
|
680
|
+
selectedConversation?.participantDetails?._id) &&
|
|
681
|
+
!isSending && (
|
|
682
|
+
<div className="typingIndicator">
|
|
683
|
+
<div className="typing-loader">
|
|
684
|
+
<div className="ball" />
|
|
685
|
+
<div className="ball" />
|
|
686
|
+
<div className="ball" />
|
|
687
|
+
typing
|
|
688
|
+
</div>
|
|
603
689
|
</div>
|
|
604
|
-
|
|
605
|
-
)}
|
|
690
|
+
)}
|
|
606
691
|
</form>
|
|
607
692
|
</div>
|
|
608
693
|
);
|