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