@pubuduth-aplicy/chat-ui 2.1.49 → 2.1.51
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 +12 -14
- package/src/components/messages/MessageInput.tsx +361 -52
- package/src/components/messages/Messages.tsx +52 -53
- package/src/components/sidebar/Conversation.tsx +27 -30
- package/src/components/sidebar/SearchInput.tsx +0 -4
- package/src/components/sidebar/Sidebar.tsx +5 -5
- package/src/lib/api/endpoint.ts +3 -3
- package/src/style/style.css +841 -416
- package/src/types/type.ts +11 -1
package/package.json
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export type FileType = 'image' | 'video' | 'document' | null;
|
|
4
|
+
|
|
5
|
+
interface FilePreviewProps {
|
|
6
|
+
file: File;
|
|
7
|
+
type: FileType;
|
|
8
|
+
previewUrl: string;
|
|
9
|
+
onRemove: () => void;
|
|
10
|
+
uploadProgress?: number;
|
|
11
|
+
uploadError?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const FilePreview: React.FC<FilePreviewProps> = ({
|
|
15
|
+
file,
|
|
16
|
+
type,
|
|
17
|
+
previewUrl,
|
|
18
|
+
onRemove,
|
|
19
|
+
uploadProgress,
|
|
20
|
+
uploadError
|
|
21
|
+
}) => {
|
|
22
|
+
const getFileIcon = () => {
|
|
23
|
+
switch (type) {
|
|
24
|
+
case 'document':
|
|
25
|
+
return (
|
|
26
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
27
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
28
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
29
|
+
</svg>
|
|
30
|
+
);
|
|
31
|
+
default:
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const formatFileSize = (bytes: number) => {
|
|
37
|
+
if (bytes === 0) return '0 Bytes';
|
|
38
|
+
const k = 1024;
|
|
39
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
40
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
41
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="file-preview">
|
|
46
|
+
<div className="file-preview-content">
|
|
47
|
+
{type === 'image' && (
|
|
48
|
+
<img src={previewUrl} alt={file.name} className="file-preview-image" />
|
|
49
|
+
)}
|
|
50
|
+
{type === 'video' && (
|
|
51
|
+
<video controls className="file-preview-video">
|
|
52
|
+
<source src={previewUrl} type={file.type} />
|
|
53
|
+
</video>
|
|
54
|
+
)}
|
|
55
|
+
{type === 'document' && (
|
|
56
|
+
<div className="file-preview-document">
|
|
57
|
+
{getFileIcon()}
|
|
58
|
+
<span>{file.name}</span>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
<div className="file-info">
|
|
63
|
+
<span className="file-name">{file.name}</span>
|
|
64
|
+
<span className="file-size">{formatFileSize(file.size)}</span>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{uploadProgress !== undefined && uploadProgress < 100 && (
|
|
68
|
+
<div className="upload-progress">
|
|
69
|
+
<div
|
|
70
|
+
className="progress-bar"
|
|
71
|
+
style={{ width: `${uploadProgress}%` }}
|
|
72
|
+
></div>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{uploadError && (
|
|
77
|
+
<div className="upload-error">{uploadError}</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
className="remove-file"
|
|
84
|
+
onClick={onRemove}
|
|
85
|
+
aria-label="Remove file"
|
|
86
|
+
>
|
|
87
|
+
×
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
@@ -21,8 +21,8 @@ const Message = ({ message }: MessageProps) => {
|
|
|
21
21
|
|
|
22
22
|
const { userId } = useChatContext();
|
|
23
23
|
const fromMe = message.senderId === userId;
|
|
24
|
-
const
|
|
25
|
-
const alignItems = fromMe ? "
|
|
24
|
+
const timestamp = fromMe ? "timestamp_outgoing" : "timestamp_incomeing";
|
|
25
|
+
const alignItems = fromMe ? "outgoing" : "incoming";
|
|
26
26
|
|
|
27
27
|
const date = new Date(message.createdAt);
|
|
28
28
|
const hours = date.getUTCHours();
|
|
@@ -58,19 +58,17 @@ const getStatusIcon = () => {
|
|
|
58
58
|
</div>
|
|
59
59
|
</div> */}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{getStatusIcon()}
|
|
68
|
-
</div>
|
|
61
|
+
<div className="chat-container">
|
|
62
|
+
<div className={`message-row ${alignItems}`}>
|
|
63
|
+
<div className="bubble-container">
|
|
64
|
+
<div className="chat-bubble">{message.message}</div>
|
|
65
|
+
<div className={`${timestamp}`}>{new Date(message.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
66
|
+
<span className="status-icon">{getStatusIcon()}</span>
|
|
69
67
|
</div>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
74
72
|
|
|
75
73
|
</>
|
|
76
74
|
)
|
|
@@ -1,13 +1,38 @@
|
|
|
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
|
+
const MAX_FILE_SIZE_MB = 10; // 10MB max file size
|
|
12
|
+
const ACCEPTED_IMAGE_TYPES = [
|
|
13
|
+
"image/jpeg",
|
|
14
|
+
"image/png",
|
|
15
|
+
"image/gif",
|
|
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
|
+
];
|
|
25
|
+
|
|
26
|
+
interface Attachment {
|
|
27
|
+
file: File;
|
|
28
|
+
type: FileType;
|
|
29
|
+
previewUrl: string;
|
|
30
|
+
uploadProgress?: number;
|
|
31
|
+
uploadError?: string;
|
|
32
|
+
}
|
|
9
33
|
|
|
10
34
|
const MessageInput = () => {
|
|
35
|
+
const apiClient = getApiClient();
|
|
11
36
|
const { socket } = useChatContext();
|
|
12
37
|
const { userId } = useChatContext();
|
|
13
38
|
const { selectedConversation } = useChatUIStore();
|
|
@@ -18,6 +43,7 @@ const MessageInput = () => {
|
|
|
18
43
|
const [isSending, setIsSending] = useState(false);
|
|
19
44
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
20
45
|
const [isTyping, setIsTyping] = useState(false); // Track if the user is typing
|
|
46
|
+
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
|
21
47
|
|
|
22
48
|
useEffect(() => {
|
|
23
49
|
if (selectedConversation?._id) {
|
|
@@ -46,17 +72,28 @@ const MessageInput = () => {
|
|
|
46
72
|
return () => clearTimeout(typingTimeout);
|
|
47
73
|
}, [message, socket, selectedConversation?._id, userId]);
|
|
48
74
|
|
|
49
|
-
|
|
50
75
|
useEffect(() => {
|
|
51
76
|
if (!socket || !selectedConversation?._id) return;
|
|
52
77
|
|
|
53
|
-
const handleTyping = ({
|
|
78
|
+
const handleTyping = ({
|
|
79
|
+
userId,
|
|
80
|
+
chatId,
|
|
81
|
+
}: {
|
|
82
|
+
userId: string;
|
|
83
|
+
chatId: string;
|
|
84
|
+
}) => {
|
|
54
85
|
if (chatId === selectedConversation._id) {
|
|
55
86
|
setTypingUser(userId);
|
|
56
87
|
}
|
|
57
88
|
};
|
|
58
|
-
|
|
59
|
-
const handleStopTyping = ({
|
|
89
|
+
|
|
90
|
+
const handleStopTyping = ({
|
|
91
|
+
userId,
|
|
92
|
+
chatId,
|
|
93
|
+
}: {
|
|
94
|
+
userId: string;
|
|
95
|
+
chatId: string;
|
|
96
|
+
}) => {
|
|
60
97
|
if (chatId === selectedConversation._id) {
|
|
61
98
|
setTypingUser((prev) => (prev === userId ? null : prev));
|
|
62
99
|
}
|
|
@@ -71,35 +108,99 @@ const MessageInput = () => {
|
|
|
71
108
|
};
|
|
72
109
|
}, [socket, selectedConversation?._id]);
|
|
73
110
|
|
|
111
|
+
const getFileType = (mimeType: string): FileType | null => {
|
|
112
|
+
if (ACCEPTED_IMAGE_TYPES.includes(mimeType)) return "image";
|
|
113
|
+
if (ACCEPTED_VIDEO_TYPES.includes(mimeType)) return "video";
|
|
114
|
+
if (ACCEPTED_DOCUMENT_TYPES.includes(mimeType)) return "document";
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const uploadToS3 = async (
|
|
119
|
+
file: File
|
|
120
|
+
): Promise<{ url: string; name: string; size: number; type: FileType }> => {
|
|
121
|
+
// Get pre-signed URL from your backend
|
|
122
|
+
const response = await apiClient.post("api/chat/generatePresignedUrl", {
|
|
123
|
+
fileName: file.name,
|
|
124
|
+
fileType: file.type,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const { signedUrl, fileUrl } = await response.data();
|
|
128
|
+
|
|
129
|
+
// Upload file directly to S3 using the pre-signed URL
|
|
130
|
+
const uploadResponse = await fetch(signedUrl, {
|
|
131
|
+
method: "PUT",
|
|
132
|
+
body: file,
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": file.type,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!uploadResponse.ok) {
|
|
139
|
+
throw new Error("Upload failed");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
url: fileUrl,
|
|
144
|
+
name: file.name,
|
|
145
|
+
size: file.size,
|
|
146
|
+
type: getFileType(file.type),
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
74
150
|
const handleSubmit = useCallback(
|
|
75
151
|
async (e: any) => {
|
|
76
152
|
e.preventDefault();
|
|
77
|
-
if (!message || isSending) return;
|
|
153
|
+
// if (!message || isSending) return;
|
|
78
154
|
|
|
79
155
|
setIsSending(true);
|
|
80
156
|
try {
|
|
157
|
+
const uploadedFiles = await Promise.all(
|
|
158
|
+
attachments.map(async (attachment) => {
|
|
159
|
+
try {
|
|
160
|
+
const result = await uploadToS3(attachment.file);
|
|
161
|
+
return {
|
|
162
|
+
type: attachment.type,
|
|
163
|
+
url: result.url,
|
|
164
|
+
name: result.name,
|
|
165
|
+
size: result.size,
|
|
166
|
+
};
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(
|
|
169
|
+
`Error uploading file ${attachment.file.name}:`,
|
|
170
|
+
error
|
|
171
|
+
);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Filter out any failed uploads
|
|
178
|
+
const successfulUploads = uploadedFiles.filter((file) => file !== null);
|
|
179
|
+
|
|
81
180
|
console.log("📤 Sending message:", message);
|
|
82
|
-
mutation.mutate(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
onSuccess: (data) => {
|
|
88
|
-
console.log('Response from sendMessage:', data);
|
|
89
|
-
// After successfully sending the message, emit the socket event
|
|
90
|
-
socket.emit("sendMessage", {
|
|
91
|
-
chatId: selectedConversation?._id,
|
|
92
|
-
message,
|
|
93
|
-
messageId:data[1]._id,
|
|
94
|
-
senderId: userId,
|
|
95
|
-
receiverId: selectedConversation?.participantDetails._id,
|
|
96
|
-
});
|
|
97
|
-
},
|
|
98
|
-
onError: (error) => {
|
|
99
|
-
console.error("❌ Error in sending message:", error);
|
|
181
|
+
mutation.mutate(
|
|
182
|
+
{
|
|
183
|
+
chatId: selectedConversation?.participantDetails._id,
|
|
184
|
+
senderId: userId,
|
|
185
|
+
message,
|
|
100
186
|
},
|
|
101
|
-
|
|
102
|
-
|
|
187
|
+
{
|
|
188
|
+
onSuccess: (data) => {
|
|
189
|
+
console.log("Response from sendMessage:", data);
|
|
190
|
+
// After successfully sending the message, emit the socket event
|
|
191
|
+
socket.emit("sendMessage", {
|
|
192
|
+
chatId: selectedConversation?._id,
|
|
193
|
+
message,
|
|
194
|
+
messageId: data[1]._id,
|
|
195
|
+
senderId: userId,
|
|
196
|
+
receiverId: selectedConversation?.participantDetails._id,
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
onError: (error) => {
|
|
200
|
+
console.error("❌ Error in sending message:", error);
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
);
|
|
103
204
|
} catch (error) {
|
|
104
205
|
console.error("❌ Error sending message:", error);
|
|
105
206
|
} finally {
|
|
@@ -110,34 +211,242 @@ const MessageInput = () => {
|
|
|
110
211
|
[message, selectedConversation, userId, isSending]
|
|
111
212
|
);
|
|
112
213
|
|
|
214
|
+
const [showAttachmentOptions, setShowAttachmentOptions] = useState(false);
|
|
215
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
return () => {
|
|
219
|
+
attachments.forEach((attachment) => {
|
|
220
|
+
URL.revokeObjectURL(attachment.previewUrl);
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
}, [attachments]);
|
|
224
|
+
|
|
225
|
+
const handleAttachmentClick = () => {
|
|
226
|
+
setShowAttachmentOptions(!showAttachmentOptions);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const handleFileSelect = (type: FileType) => {
|
|
230
|
+
if (fileInputRef.current) {
|
|
231
|
+
fileInputRef.current.accept = getAcceptString(type);
|
|
232
|
+
fileInputRef.current.click();
|
|
233
|
+
}
|
|
234
|
+
setShowAttachmentOptions(false);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const getAcceptString = (type: FileType): string => {
|
|
238
|
+
switch (type) {
|
|
239
|
+
case "image":
|
|
240
|
+
return ACCEPTED_IMAGE_TYPES.join(",");
|
|
241
|
+
case "video":
|
|
242
|
+
return ACCEPTED_VIDEO_TYPES.join(",");
|
|
243
|
+
case "document":
|
|
244
|
+
return ACCEPTED_DOCUMENT_TYPES.join(",");
|
|
245
|
+
default:
|
|
246
|
+
return "*";
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
251
|
+
const files = e.target.files;
|
|
252
|
+
if (!files || files.length === 0) return;
|
|
253
|
+
|
|
254
|
+
const newAttachments: Attachment[] = [];
|
|
255
|
+
|
|
256
|
+
for (let i = 0; i < files.length; i++) {
|
|
257
|
+
const file = files[i];
|
|
258
|
+
const fileType = getFileType(file.type);
|
|
259
|
+
|
|
260
|
+
// Validate file type
|
|
261
|
+
if (!fileType) {
|
|
262
|
+
console.error(`Unsupported file type: ${file.type}`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Validate file size
|
|
267
|
+
if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) {
|
|
268
|
+
console.error(`File too large: ${file.name}`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Create preview URL
|
|
273
|
+
const previewUrl =
|
|
274
|
+
fileType === "document"
|
|
275
|
+
? URL.createObjectURL(new Blob([""], { type: "application/pdf" })) // Placeholder for documents
|
|
276
|
+
: URL.createObjectURL(file);
|
|
277
|
+
|
|
278
|
+
newAttachments.push({
|
|
279
|
+
file,
|
|
280
|
+
type: fileType,
|
|
281
|
+
previewUrl,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
setAttachments((prev) => [...prev, ...newAttachments]);
|
|
286
|
+
if (fileInputRef.current) {
|
|
287
|
+
fileInputRef.current.value = "";
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const removeAttachment = (index: number) => {
|
|
292
|
+
setAttachments((prev) => {
|
|
293
|
+
const newAttachments = [...prev];
|
|
294
|
+
URL.revokeObjectURL(newAttachments[index].previewUrl);
|
|
295
|
+
newAttachments.splice(index, 1);
|
|
296
|
+
return newAttachments;
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
|
|
113
300
|
return (
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
{typingUser && typingUser !== userId && typingUser === selectedConversation?.participantDetails?._id && !isSending && (
|
|
130
|
-
<div className="typingIndicator">
|
|
131
|
-
<div className="loader">
|
|
132
|
-
<div className="ball" />
|
|
133
|
-
<div className="ball" />
|
|
134
|
-
<div className="ball" />
|
|
135
|
-
typing
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
)}
|
|
301
|
+
<div className="message-input-container">
|
|
302
|
+
{/* Preview area for attachments */}
|
|
303
|
+
{attachments.length > 0 && (
|
|
304
|
+
<div className="attachments-preview">
|
|
305
|
+
{attachments.map((attachment, index) => (
|
|
306
|
+
<FilePreview
|
|
307
|
+
key={index}
|
|
308
|
+
file={attachment.file}
|
|
309
|
+
type={attachment.type}
|
|
310
|
+
previewUrl={attachment.previewUrl}
|
|
311
|
+
onRemove={() => removeAttachment(index)}
|
|
312
|
+
/>
|
|
313
|
+
))}
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
139
316
|
|
|
140
|
-
|
|
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)}
|
|
425
|
+
/>
|
|
426
|
+
<button
|
|
427
|
+
type="submit"
|
|
428
|
+
className="chatMessageInputSubmit"
|
|
429
|
+
disabled={isSending}
|
|
430
|
+
>
|
|
431
|
+
<img width={10} height={10} src={paperplane} alt="send" />
|
|
432
|
+
</button>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
{typingUser &&
|
|
436
|
+
typingUser !== userId &&
|
|
437
|
+
typingUser === selectedConversation?.participantDetails?._id &&
|
|
438
|
+
!isSending && (
|
|
439
|
+
<div className="typingIndicator">
|
|
440
|
+
<div className="loader">
|
|
441
|
+
<div className="ball" />
|
|
442
|
+
<div className="ball" />
|
|
443
|
+
<div className="ball" />
|
|
444
|
+
typing
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
</form>
|
|
449
|
+
</div>
|
|
141
450
|
);
|
|
142
451
|
};
|
|
143
452
|
|