@pubuduth-aplicy/chat-ui 2.1.50 → 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/MessageInput.tsx +358 -49
- package/src/components/sidebar/Conversation.tsx +1 -1
- package/src/components/sidebar/SearchInput.tsx +0 -4
- package/src/components/sidebar/Sidebar.tsx +5 -5
- package/src/style/style.css +168 -13
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
|
+
};
|
|
@@ -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
89
|
|
|
59
|
-
const handleStopTyping = ({
|
|
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
|
-
});
|
|
181
|
+
mutation.mutate(
|
|
182
|
+
{
|
|
183
|
+
chatId: selectedConversation?.participantDetails._id,
|
|
184
|
+
senderId: userId,
|
|
185
|
+
message,
|
|
97
186
|
},
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
</div>
|
|
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>
|
|
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
|
+
))}
|
|
137
314
|
</div>
|
|
138
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
|
|
|
@@ -2,12 +2,12 @@ import Conversations from "./Conversations";
|
|
|
2
2
|
import SearchInput from "./SearchInput";
|
|
3
3
|
|
|
4
4
|
export const Sidebar = () => {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
return (
|
|
6
|
+
<div className='chatSidebarMain'>
|
|
7
7
|
<SearchInput />
|
|
8
8
|
<div className='divider px-3'></div>
|
|
9
9
|
<Conversations />
|
|
10
|
-
|
|
11
|
-
</div>
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
13
|
}
|
package/src/style/style.css
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
.grid-container {
|
|
2
2
|
display: grid;
|
|
3
3
|
overflow: hidden;
|
|
4
|
-
grid-template-columns: repeat(
|
|
4
|
+
/* grid-template-columns: repeat(12, minmax(0, 1fr)); */
|
|
5
5
|
border-radius: 0.5rem;
|
|
6
|
+
width: '100%';
|
|
6
7
|
background-clip: padding-box;
|
|
7
8
|
/* background-color: #9CA3AF; */
|
|
8
9
|
--bg-opacity: 0;
|
|
@@ -10,14 +11,14 @@
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
.sidebarContainer {
|
|
13
|
-
grid-column: span 3;
|
|
14
|
-
overflow-y: auto;
|
|
14
|
+
/* grid-column: span 3; */
|
|
15
|
+
/* overflow-y: auto; */
|
|
15
16
|
border: 1px solid #ddd;
|
|
16
17
|
max-height: 100vh;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
.messageContainer {
|
|
20
|
-
grid-column: span 4;
|
|
21
|
+
/* grid-column: span 4; */
|
|
21
22
|
/* border: 1px solid; */
|
|
22
23
|
max-height: 100vh;
|
|
23
24
|
overflow-y: auto;
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
padding-bottom: 0.5rem;
|
|
52
53
|
padding-left: 1rem;
|
|
53
54
|
padding-right: 1rem;
|
|
54
|
-
flex-basis: 0px;
|
|
55
|
+
/* flex-basis: 0px; */
|
|
55
56
|
shrink: 1;
|
|
56
57
|
gap: 1rem;
|
|
57
58
|
justify-content: flex-start;
|
|
@@ -80,8 +81,8 @@ line-height: 1.5rem;
|
|
|
80
81
|
font-weight: 400;
|
|
81
82
|
border: 1px solid #ccc;
|
|
82
83
|
width: 100%; /* Ensure full width */
|
|
83
|
-
min-width: 400px;
|
|
84
|
-
max-width: 600px;
|
|
84
|
+
/* min-width: 400px; */
|
|
85
|
+
max-width: 600px;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
.chatSidebarInput:focus {
|
|
@@ -452,7 +453,7 @@ font-size: 12px;
|
|
|
452
453
|
color: blue;
|
|
453
454
|
}
|
|
454
455
|
|
|
455
|
-
@media (min-width: 640px) {
|
|
456
|
+
/* @media (min-width: 640px) {
|
|
456
457
|
.grid-container {
|
|
457
458
|
height: 450px;
|
|
458
459
|
}
|
|
@@ -471,22 +472,22 @@ color: blue;
|
|
|
471
472
|
padding-right: 1.5rem;
|
|
472
473
|
}
|
|
473
474
|
|
|
474
|
-
}
|
|
475
|
+
} */
|
|
475
476
|
|
|
476
477
|
@media (min-width: 768px) {
|
|
477
478
|
.grid-container {
|
|
478
|
-
height: 550px;
|
|
479
|
+
/* height: 550px; */
|
|
479
480
|
grid-template-columns: repeat(9, minmax(0, 1fr));
|
|
480
481
|
}
|
|
481
482
|
|
|
482
483
|
.sidebarContainer {
|
|
483
|
-
display: grid;
|
|
484
|
-
grid-column: span
|
|
484
|
+
/* display: grid; */
|
|
485
|
+
grid-column: span 4 ;
|
|
485
486
|
}
|
|
486
487
|
|
|
487
488
|
.messageContainer {
|
|
488
489
|
display: grid;
|
|
489
|
-
grid-column: span
|
|
490
|
+
grid-column: span 5;
|
|
490
491
|
}
|
|
491
492
|
|
|
492
493
|
.chatMessageContainerInnerDiv_button {
|
|
@@ -819,3 +820,157 @@ background-color: #ccc;
|
|
|
819
820
|
opacity: 1;
|
|
820
821
|
}
|
|
821
822
|
}
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
/* chat input medial */
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
.message-input-container {
|
|
830
|
+
width: 100%;
|
|
831
|
+
padding: 10px;
|
|
832
|
+
position: sticky;
|
|
833
|
+
bottom: 0;
|
|
834
|
+
background: #fff;
|
|
835
|
+
border-top: 1px solid #eaeaea;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.attachments-preview {
|
|
839
|
+
display: flex;
|
|
840
|
+
flex-wrap: wrap;
|
|
841
|
+
gap: 10px;
|
|
842
|
+
margin-bottom: 10px;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.file-preview {
|
|
846
|
+
position: relative;
|
|
847
|
+
width: 120px;
|
|
848
|
+
height: 120px;
|
|
849
|
+
border-radius: 8px;
|
|
850
|
+
overflow: hidden;
|
|
851
|
+
background: #f5f5f5;
|
|
852
|
+
border: 1px solid #eaeaea;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.file-preview-content {
|
|
856
|
+
width: 100%;
|
|
857
|
+
height: 100%;
|
|
858
|
+
display: flex;
|
|
859
|
+
flex-direction: column;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.file-preview-image {
|
|
863
|
+
width: 100%;
|
|
864
|
+
height: 80px;
|
|
865
|
+
object-fit: cover;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.file-preview-video {
|
|
869
|
+
width: 100%;
|
|
870
|
+
height: 80px;
|
|
871
|
+
background: #000;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
.file-preview-document {
|
|
875
|
+
display: flex;
|
|
876
|
+
flex-direction: column;
|
|
877
|
+
align-items: center;
|
|
878
|
+
justify-content: center;
|
|
879
|
+
height: 80px;
|
|
880
|
+
padding: 10px;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.file-preview-document svg {
|
|
884
|
+
width: 40px;
|
|
885
|
+
height: 40px;
|
|
886
|
+
color: #555;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.file-info {
|
|
890
|
+
padding: 5px;
|
|
891
|
+
font-size: 12px;
|
|
892
|
+
overflow: hidden;
|
|
893
|
+
text-overflow: ellipsis;
|
|
894
|
+
white-space: nowrap;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
.file-name {
|
|
898
|
+
display: block;
|
|
899
|
+
overflow: hidden;
|
|
900
|
+
text-overflow: ellipsis;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.file-size {
|
|
904
|
+
color: #777;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.remove-file {
|
|
908
|
+
position: absolute;
|
|
909
|
+
top: 5px;
|
|
910
|
+
right: 5px;
|
|
911
|
+
width: 20px;
|
|
912
|
+
height: 20px;
|
|
913
|
+
border-radius: 50%;
|
|
914
|
+
background: rgba(0, 0, 0, 0.5);
|
|
915
|
+
color: white;
|
|
916
|
+
border: none;
|
|
917
|
+
cursor: pointer;
|
|
918
|
+
display: flex;
|
|
919
|
+
align-items: center;
|
|
920
|
+
justify-content: center;
|
|
921
|
+
font-size: 12px;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.remove-file:hover {
|
|
925
|
+
background: rgba(0, 0, 0, 0.7);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.upload-progress {
|
|
929
|
+
width: 100%;
|
|
930
|
+
height: 4px;
|
|
931
|
+
background: #eaeaea;
|
|
932
|
+
position: absolute;
|
|
933
|
+
bottom: 0;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.progress-bar {
|
|
937
|
+
height: 100%;
|
|
938
|
+
background: #4CAF50;
|
|
939
|
+
transition: width 0.3s;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.upload-error {
|
|
943
|
+
color: #f44336;
|
|
944
|
+
font-size: 12px;
|
|
945
|
+
padding: 5px;
|
|
946
|
+
text-align: center;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.attachment-options {
|
|
950
|
+
position: absolute;
|
|
951
|
+
bottom: 100%;
|
|
952
|
+
left: 0;
|
|
953
|
+
background-color: white;
|
|
954
|
+
border-radius: 8px;
|
|
955
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
|
956
|
+
padding: 8px;
|
|
957
|
+
z-index: 1000;
|
|
958
|
+
display: flex;
|
|
959
|
+
flex-direction: column;
|
|
960
|
+
gap: 4px;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.attachment-options button {
|
|
964
|
+
display: flex;
|
|
965
|
+
align-items: center;
|
|
966
|
+
gap: 8px;
|
|
967
|
+
padding: 8px;
|
|
968
|
+
background: none;
|
|
969
|
+
border: none;
|
|
970
|
+
cursor: pointer;
|
|
971
|
+
border-radius: 4px;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.attachment-options button:hover {
|
|
975
|
+
background: #f5f5f5;
|
|
976
|
+
}
|