@pubuduth-aplicy/chat-ui 2.1.73 → 2.1.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/Chat.config.ts +1 -0
- package/src/components/Chat.tsx +1 -1
- package/src/components/common/CollapsibleSection.tsx +40 -0
- package/src/components/common/VirtualizedChatList.tsx +57 -0
- package/src/components/messages/Message.tsx +38 -35
- package/src/components/messages/MessageContainer.tsx +85 -65
- package/src/components/messages/MessageInput.tsx +56 -15
- package/src/components/messages/Messages.tsx +76 -46
- package/src/components/sidebar/Conversation.tsx +98 -163
- package/src/components/sidebar/Conversations.tsx +280 -41
- package/src/components/sidebar/SearchInput.tsx +16 -54
- package/src/hooks/useMessageStatus.ts +92 -0
- package/src/providers/ChatProvider.tsx +16 -12
- package/src/service/messageService.ts +86 -61
- package/src/service/sidebarApi.ts +2 -5
- package/src/stores/Zustant.ts +9 -4
- package/src/style/style.css +260 -0
- package/src/types/type.ts +24 -27
- package/src/ChatWindow.tsx +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pubuduth-aplicy/chat-ui",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.75",
|
|
4
4
|
"description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@tanstack/react-query": "^5.67.2",
|
|
16
|
+
"@tanstack/react-virtual": "^3.13.12",
|
|
16
17
|
"axios": "^1.8.2",
|
|
17
18
|
"lucide-react": "^0.514.0",
|
|
18
19
|
"react": "^19.0.0",
|
package/src/Chat.config.ts
CHANGED
package/src/components/Chat.tsx
CHANGED
|
@@ -51,7 +51,7 @@ export const Chat = () => {
|
|
|
51
51
|
return (
|
|
52
52
|
<div className="container mx-auto mb-5">
|
|
53
53
|
<div className="grid-container">
|
|
54
|
-
<div className={`sidebarContainer`}>
|
|
54
|
+
<div className={`sidebarContainer dark:bg-gray-800`}>
|
|
55
55
|
<Sidebar />
|
|
56
56
|
</div>
|
|
57
57
|
<div className="messageContainer">
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface CollapsibleSectionProps {
|
|
5
|
+
title: string;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
defaultOpen?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CollapsibleSection = ({
|
|
11
|
+
title,
|
|
12
|
+
children,
|
|
13
|
+
defaultOpen = true,
|
|
14
|
+
}: CollapsibleSectionProps) => {
|
|
15
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
16
|
+
|
|
17
|
+
const toggleOpen = () => {
|
|
18
|
+
setIsOpen(!isOpen);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="border-b border-gray-200">
|
|
23
|
+
<div
|
|
24
|
+
className="flex justify-between items-center p-2 cursor-pointer hover:bg-gray-50"
|
|
25
|
+
onClick={toggleOpen}
|
|
26
|
+
>
|
|
27
|
+
<p className="text-xs uppercase text-gray-900 dark:text-gray-200">{title}</p>
|
|
28
|
+
<ChevronDown
|
|
29
|
+
size={20}
|
|
30
|
+
className={`transform transition-transform duration-200 ${
|
|
31
|
+
isOpen ? "rotate-180" : ""
|
|
32
|
+
}`}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
{isOpen && <div className="p-2 text-gray-900 dark:text-gray-200">{children}</div>}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default CollapsibleSection;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
3
|
+
import Conversation from "../sidebar/Conversation";
|
|
4
|
+
import { Conversation as ConversationType } from "../../types/type";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
conversations: ConversationType[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ITEM_HEIGHT = 60;
|
|
11
|
+
|
|
12
|
+
const VirtualizedChatList = ({ conversations }: Props) => {
|
|
13
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
const virtualizer = useVirtualizer({
|
|
16
|
+
count: conversations.length,
|
|
17
|
+
getScrollElement: () => parentRef.current,
|
|
18
|
+
estimateSize: () => ITEM_HEIGHT,
|
|
19
|
+
overscan: 5,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
ref={parentRef}
|
|
25
|
+
style={{
|
|
26
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
27
|
+
width: "100%",
|
|
28
|
+
position: "relative",
|
|
29
|
+
overflow: "auto",
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{virtualizer.getVirtualItems().map((item) => {
|
|
33
|
+
const conversation = conversations[item.index];
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
key={item.key}
|
|
37
|
+
style={{
|
|
38
|
+
position: "absolute",
|
|
39
|
+
top: 0,
|
|
40
|
+
left: 0,
|
|
41
|
+
width: "100%",
|
|
42
|
+
height: `${item.size}px`,
|
|
43
|
+
transform: `translateY(${item.start}px)`,
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<Conversation
|
|
47
|
+
conversation={conversation}
|
|
48
|
+
lastIdx={item.index === conversations.length - 1}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default VirtualizedChatList;
|
|
@@ -34,6 +34,7 @@ interface MessageProps {
|
|
|
34
34
|
type?: "user" | "system" | "system-completion";
|
|
35
35
|
meta?: {
|
|
36
36
|
bookingDetails?: {
|
|
37
|
+
status: string; // e.g., "confirmed", "pending", "cancelled"
|
|
37
38
|
serviceId: string;
|
|
38
39
|
date: string;
|
|
39
40
|
time: string;
|
|
@@ -47,18 +48,30 @@ interface MessageProps {
|
|
|
47
48
|
|
|
48
49
|
const Message = ({ message }: MessageProps) => {
|
|
49
50
|
const { userId } = useChatContext();
|
|
50
|
-
const { apiUrl } = getChatConfig();
|
|
51
|
+
const { apiUrl,cdnUrl } = getChatConfig();
|
|
51
52
|
|
|
52
53
|
if (message.type === "system") {
|
|
53
54
|
return (
|
|
54
55
|
<div className="system-message booking-details">
|
|
55
|
-
<h4>
|
|
56
|
+
<h4>{message.meta?.bookingDetails?.status || 'Unknown'}</h4>
|
|
56
57
|
<div className="details">
|
|
57
58
|
<p>Service: {message.meta?.bookingDetails?.serviceId}</p>
|
|
58
59
|
<p>Date: {message.meta?.bookingDetails?.date}</p>
|
|
59
60
|
<p>Time: {message.meta?.bookingDetails?.time}</p>
|
|
60
61
|
<p>Price: ${message.meta?.bookingDetails?.price}</p>
|
|
61
62
|
</div>
|
|
63
|
+
<button
|
|
64
|
+
className="confirm-button"
|
|
65
|
+
onClick={() => {
|
|
66
|
+
// Add your confirmation logic here
|
|
67
|
+
console.log('Booking confirmed!');
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
Confirm Booking
|
|
71
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
72
|
+
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z" />
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
62
75
|
</div>
|
|
63
76
|
);
|
|
64
77
|
}
|
|
@@ -101,6 +114,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
101
114
|
// const handleDownload = (url: string, name: string) => {
|
|
102
115
|
// saveAs(url, name);
|
|
103
116
|
// };
|
|
117
|
+
console.log("check message status", message.status);
|
|
104
118
|
|
|
105
119
|
const [downloadingIndex, setDownloadingIndex] = useState<number | null>(null);
|
|
106
120
|
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
|
@@ -193,27 +207,6 @@ const Message = ({ message }: MessageProps) => {
|
|
|
193
207
|
}
|
|
194
208
|
};
|
|
195
209
|
|
|
196
|
-
// const handleDownload = async (url: string, name: string, index: number) => {
|
|
197
|
-
// setDownloadingIndex(index);
|
|
198
|
-
// try {
|
|
199
|
-
// var xhr = new XMLHttpRequest()
|
|
200
|
-
// xhr.open('HEAD', url, false)
|
|
201
|
-
// xhr.send()
|
|
202
|
-
// const blob = await response.blob();
|
|
203
|
-
// const link = document.createElement("a");
|
|
204
|
-
// link.href = URL.createObjectURL(blob);
|
|
205
|
-
// link.download = name;
|
|
206
|
-
// link.click();
|
|
207
|
-
// URL.revokeObjectURL(link.href);
|
|
208
|
-
// } catch (error) {
|
|
209
|
-
// console.error("Download failed:", error);
|
|
210
|
-
// } finally {
|
|
211
|
-
// setDownloadingIndex(null);
|
|
212
|
-
// }
|
|
213
|
-
// };
|
|
214
|
-
|
|
215
|
-
// const renderMedia = () => {
|
|
216
|
-
// if (!message.media || message.media.length === 0) return null;
|
|
217
210
|
|
|
218
211
|
const handleDownload = async (url: string, name: string, index: number) => {
|
|
219
212
|
setDownloadingIndex(index);
|
|
@@ -223,7 +216,6 @@ const Message = ({ message }: MessageProps) => {
|
|
|
223
216
|
setDownloadController(controller);
|
|
224
217
|
|
|
225
218
|
try {
|
|
226
|
-
// First try to download directly
|
|
227
219
|
try {
|
|
228
220
|
const response = await fetch(
|
|
229
221
|
`${apiUrl}${Path.apiProxy}?url=${encodeURIComponent(
|
|
@@ -319,7 +311,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
319
311
|
<div className="circular-progress-container">
|
|
320
312
|
<div className="media-preview-background">
|
|
321
313
|
<img
|
|
322
|
-
src={media.url}
|
|
314
|
+
src={`${cdnUrl}${`${cdnUrl}${media.url}`}`}
|
|
323
315
|
alt={media.name}
|
|
324
316
|
className="blurred-preview"
|
|
325
317
|
/>
|
|
@@ -364,16 +356,16 @@ const Message = ({ message }: MessageProps) => {
|
|
|
364
356
|
<>
|
|
365
357
|
{media.type === "image" ? (
|
|
366
358
|
<img
|
|
367
|
-
src={media.url}
|
|
359
|
+
src={`${cdnUrl}${media.url}`}
|
|
368
360
|
alt={media.name}
|
|
369
361
|
className="media-content"
|
|
370
|
-
onClick={() => window.open(media.url
|
|
362
|
+
onClick={() => window.open(`${cdnUrl}${media.url}`, "_blank")}
|
|
371
363
|
/>
|
|
372
364
|
) : media.type === "video" ? (
|
|
373
365
|
<video controls className="media-content">
|
|
374
366
|
<source
|
|
375
|
-
src={media.url}
|
|
376
|
-
type={`video/${media.url
|
|
367
|
+
src={`${cdnUrl}${media.url}`}
|
|
368
|
+
type={`video/${`${cdnUrl}${media.url}`.split(".").pop()}`}
|
|
377
369
|
/>
|
|
378
370
|
</video>
|
|
379
371
|
) : (
|
|
@@ -396,7 +388,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
396
388
|
if (downloadingIndex === index) {
|
|
397
389
|
cancelDownload();
|
|
398
390
|
} else {
|
|
399
|
-
handleDownload(media.url
|
|
391
|
+
handleDownload(`${cdnUrl}${media.url}`, media.name, index);
|
|
400
392
|
}
|
|
401
393
|
}}
|
|
402
394
|
title={downloadingIndex === index ? "Cancel" : "Download"}
|
|
@@ -489,19 +481,28 @@ const Message = ({ message }: MessageProps) => {
|
|
|
489
481
|
}
|
|
490
482
|
};
|
|
491
483
|
|
|
484
|
+
const isMessageOlderThanOneDay = (createdAt: string | Date) => {
|
|
485
|
+
const messageDate = new Date(createdAt);
|
|
486
|
+
const now = new Date();
|
|
487
|
+
const oneDayInMs = 24 * 60 * 60 * 1000;
|
|
488
|
+
return now.getTime() - messageDate.getTime() > oneDayInMs;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const hasMedia = message.media && message.media.length > 0;
|
|
492
|
+
|
|
492
493
|
return (
|
|
493
494
|
<div className="chat-container">
|
|
494
495
|
<div className={`message-row ${alignItems}`}>
|
|
495
496
|
<div
|
|
496
497
|
className="bubble-container"
|
|
497
|
-
onMouseEnter={() => fromMe && setShowOptions(true)}
|
|
498
|
+
onMouseEnter={() => fromMe && !hasMedia && setShowOptions(true)}
|
|
498
499
|
onMouseLeave={() =>
|
|
499
500
|
fromMe && !showDeleteOption && setShowOptions(false)
|
|
500
501
|
}
|
|
501
502
|
>
|
|
502
|
-
{message.
|
|
503
|
+
{message.status === "deleted" ? (
|
|
503
504
|
<div className="chat-bubble compact-bubble deleted-message">
|
|
504
|
-
<div className="message-text">This message was deleted</div>
|
|
505
|
+
<div className="message-text text-gray-900 dark:text-gray-100">This message was deleted</div>
|
|
505
506
|
</div>
|
|
506
507
|
) : (
|
|
507
508
|
(message.message ||
|
|
@@ -532,7 +533,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
532
533
|
</div>
|
|
533
534
|
) : (
|
|
534
535
|
message.message && (
|
|
535
|
-
<div className="message-text">{message.message}</div>
|
|
536
|
+
<div className="message-text text-gray-900 dark:text-gray-100">{message.message}</div>
|
|
536
537
|
)
|
|
537
538
|
)}
|
|
538
539
|
|
|
@@ -540,7 +541,9 @@ const Message = ({ message }: MessageProps) => {
|
|
|
540
541
|
{fromMe &&
|
|
541
542
|
showOptions &&
|
|
542
543
|
!isEditingMode &&
|
|
543
|
-
!message.isDeleted &&
|
|
544
|
+
!message.isDeleted &&
|
|
545
|
+
!hasMedia &&
|
|
546
|
+
!isMessageOlderThanOneDay(message.createdAt) && (
|
|
544
547
|
<div className="message-options">
|
|
545
548
|
<button
|
|
546
549
|
className="message-option-btn edit-btn"
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { MessageSquare } from "lucide-react";
|
|
3
|
+
import { useEffect } from "react";
|
|
2
4
|
import MessageInput from "./MessageInput";
|
|
3
5
|
import Messages from "./Messages";
|
|
4
6
|
import useChatUIStore from "../../stores/Zustant";
|
|
5
7
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
6
|
-
import { getChatConfig } from "
|
|
8
|
+
import { getChatConfig } from "../../Chat.config";
|
|
7
9
|
|
|
8
10
|
const MessageContainer = () => {
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
onlineUsers,
|
|
13
|
-
setOnlineUsers,
|
|
14
|
-
setMessages,
|
|
15
|
-
} = useChatUIStore();
|
|
16
|
-
const { socket, sendMessage, isUserOnline } = useChatContext();
|
|
11
|
+
const { selectedConversation, setSelectedConversation, setOnlineUsers } =
|
|
12
|
+
useChatUIStore();
|
|
13
|
+
const { socket, isUserOnline } = useChatContext();
|
|
17
14
|
const { role } = getChatConfig();
|
|
18
|
-
const [joinedChats, setJoinedChats] = useState<Set<string>>(new Set());
|
|
19
|
-
|
|
20
15
|
// useEffect(() => {
|
|
21
16
|
// if (!socket) return;
|
|
22
17
|
|
|
@@ -66,42 +61,58 @@ const MessageContainer = () => {
|
|
|
66
61
|
|
|
67
62
|
useEffect(() => {
|
|
68
63
|
if (selectedConversation?._id && socket?.readyState === WebSocket.OPEN) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (unreadMessages.length > 0) {
|
|
75
|
-
sendMessage({
|
|
76
|
-
event: "messageRead",
|
|
64
|
+
// Send joinChat command to server via WebSocket
|
|
65
|
+
socket.send(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
event: "joinChat",
|
|
77
68
|
data: {
|
|
78
|
-
|
|
79
|
-
chatId: chatId,
|
|
69
|
+
chatId: selectedConversation._id,
|
|
80
70
|
},
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
// }
|
|
71
|
+
})
|
|
72
|
+
);
|
|
84
73
|
}
|
|
85
|
-
}, [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
74
|
+
}, [selectedConversation?._id, socket]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!socket) return;
|
|
78
|
+
|
|
79
|
+
const handleMessage = (event: MessageEvent) => {
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(event.data);
|
|
82
|
+
console.log("Parsed WebSocket message in mc:", data);
|
|
83
|
+
|
|
84
|
+
if (data.event === "getOnlineUsers") {
|
|
85
|
+
setOnlineUsers(data.payload); // payload should be an array of user IDs
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error("Failed to parse WebSocket message", err);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
socket.addEventListener("message", handleMessage);
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
socket.removeEventListener("message", handleMessage);
|
|
96
|
+
};
|
|
97
|
+
}, [socket, setOnlineUsers]);
|
|
92
98
|
|
|
93
99
|
// Listen for online users updates
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
const { userId } = useChatContext();
|
|
102
|
+
|
|
103
|
+
// const participantDetails = Array.isArray(selectedConversation?.participantDetails)
|
|
104
|
+
// ? selectedConversation?.participantDetails
|
|
105
|
+
// : [selectedConversation?.participantDetails].filter(Boolean);
|
|
100
106
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
// const participant = participantDetails.find(
|
|
108
|
+
// (p: any) => p._id !== userId
|
|
109
|
+
// );
|
|
110
|
+
|
|
111
|
+
const participant = selectedConversation?.participantDetails?.find(
|
|
112
|
+
(p: any) => p._id !== userId
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const isOnline = isUserOnline(participant?._id || "");
|
|
105
116
|
|
|
106
117
|
// Cleanup on unmount
|
|
107
118
|
useEffect(() => {
|
|
@@ -109,39 +120,48 @@ const MessageContainer = () => {
|
|
|
109
120
|
}, [setSelectedConversation]);
|
|
110
121
|
|
|
111
122
|
return (
|
|
112
|
-
<div className="chatMessageContainer">
|
|
123
|
+
<div className="chatMessageContainer bg-[#FFFFFF] dark:bg-gray-800 text-gray-900 dark:text-gray-100">
|
|
113
124
|
{!selectedConversation ? (
|
|
114
125
|
<EmptyInbox />
|
|
115
126
|
) : (
|
|
116
127
|
<>
|
|
117
|
-
<div className="chatMessageContainerInner">
|
|
118
|
-
<div className="chatMessageContainerInnerDiv">
|
|
128
|
+
<div className="chatMessageContainerInner bg-[FFFFFF] dark:bg-gray-800">
|
|
129
|
+
<div className="chatMessageContainerInnerDiv text-gray-900 dark:text-gray-200">
|
|
119
130
|
<button className="chatMessageContainerInnerDiv_button">
|
|
120
131
|
{/* <CaretLeft size={25} /> */}
|
|
121
132
|
</button>
|
|
122
|
-
|
|
123
|
-
className="
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
/>
|
|
133
|
+
{selectedConversation.type === "service" ? (
|
|
134
|
+
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center">
|
|
135
|
+
<MessageSquare size={24} className="text-gray-600" />
|
|
136
|
+
</div>
|
|
137
|
+
) : (
|
|
138
|
+
<img
|
|
139
|
+
className="chatMessageContainerInnerImg"
|
|
140
|
+
alt="Profile"
|
|
141
|
+
src={participant?.profilePicture}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
134
144
|
<div className="chatMessageContainerOutter">
|
|
135
145
|
<div className="chatMessageContainerOutterDiv">
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
{selectedConversation.type === "service" ? (
|
|
147
|
+
<>
|
|
148
|
+
<p className="chatMessageContainerOutterDiv_name">
|
|
149
|
+
{selectedConversation.serviceTitle}
|
|
150
|
+
</p>
|
|
151
|
+
<p className="text-sm">
|
|
152
|
+
Booking ID: #{selectedConversation.bookingId}
|
|
153
|
+
</p>
|
|
154
|
+
</>
|
|
155
|
+
) : (
|
|
156
|
+
<>
|
|
157
|
+
<p className="chatMessageContainerOutterDiv_name">
|
|
158
|
+
{participant?.name || ""}
|
|
159
|
+
</p>
|
|
160
|
+
<p className="text-sm">
|
|
161
|
+
{isOnline ? "Online" : "Offline"}
|
|
162
|
+
</p>
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
145
165
|
</div>
|
|
146
166
|
</div>
|
|
147
167
|
</div>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
3
|
import { useMessageMutation } from "../../hooks/mutations/useSendMessage";
|
|
3
4
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
4
5
|
import useChatUIStore from "../../stores/Zustant";
|
|
5
|
-
import paperplane from "../../assets/icons8-send-50.png";
|
|
6
6
|
import { FilePreview, FileType } from "../common/FilePreview";
|
|
7
7
|
import { getApiClient } from "../../lib/api/apiClient";
|
|
8
8
|
import { MessageStatus } from "../../types/type";
|
|
@@ -44,6 +44,7 @@ const MessageInput = () => {
|
|
|
44
44
|
const mutation = useMessageMutation();
|
|
45
45
|
const [typingUser, setTypingUser] = useState<string | null>(null);
|
|
46
46
|
const [isSending, setIsSending] = useState(false);
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
47
48
|
const [isTyping, setIsTyping] = useState(false);
|
|
48
49
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
|
49
50
|
const [showAttachmentOptions, setShowAttachmentOptions] = useState(false);
|
|
@@ -55,7 +56,6 @@ const MessageInput = () => {
|
|
|
55
56
|
const typingTimeoutRef = useRef<number | null>(null);
|
|
56
57
|
const generateTempId = () =>
|
|
57
58
|
`temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
58
|
-
|
|
59
59
|
// Join chat room when conversation is selected
|
|
60
60
|
useEffect(() => {
|
|
61
61
|
if (selectedConversation?._id && socket?.readyState === WebSocket.OPEN) {
|
|
@@ -67,6 +67,21 @@ const MessageInput = () => {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
}, [selectedConversation?._id, socket, sendMessage]);
|
|
70
|
+
console.log('selected', selectedConversation);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
// Clear all input state when conversation changes
|
|
74
|
+
setMessage("");
|
|
75
|
+
setMessage1("");
|
|
76
|
+
setAttachments([]);
|
|
77
|
+
setInputError(null);
|
|
78
|
+
setShowAttachmentOptions(false);
|
|
79
|
+
|
|
80
|
+
// Clean up any existing file input
|
|
81
|
+
if (fileInputRef.current) {
|
|
82
|
+
fileInputRef.current.value = "";
|
|
83
|
+
}
|
|
84
|
+
}, [selectedConversation?._id]);
|
|
70
85
|
|
|
71
86
|
// Typing indicator logic
|
|
72
87
|
useEffect(() => {
|
|
@@ -178,6 +193,7 @@ const MessageInput = () => {
|
|
|
178
193
|
const response = await apiClient.post(`${Path.preSignUrl}`, {
|
|
179
194
|
fileName: file.name,
|
|
180
195
|
fileType: file.type,
|
|
196
|
+
conversationId: selectedConversation?._id
|
|
181
197
|
});
|
|
182
198
|
|
|
183
199
|
const { signedUrl, fileUrl } = await response.data;
|
|
@@ -211,6 +227,18 @@ const MessageInput = () => {
|
|
|
211
227
|
});
|
|
212
228
|
};
|
|
213
229
|
|
|
230
|
+
const participantDetails = Array.isArray(selectedConversation?.participantDetails)
|
|
231
|
+
? selectedConversation?.participantDetails
|
|
232
|
+
: [selectedConversation?.participantDetails].filter(Boolean);
|
|
233
|
+
|
|
234
|
+
const otherParticipant = participantDetails.find(
|
|
235
|
+
(p: any) => p._id !== userId
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// const otherParticipant = selectedConversation?.participantDetails?.find(
|
|
239
|
+
// (p:any) => p._id !== userId
|
|
240
|
+
// );
|
|
241
|
+
|
|
214
242
|
const handleSubmit = useCallback(
|
|
215
243
|
async (e: React.FormEvent) => {
|
|
216
244
|
e.preventDefault();
|
|
@@ -294,14 +322,31 @@ const MessageInput = () => {
|
|
|
294
322
|
|
|
295
323
|
const successfulUploads = uploadedFiles.filter((file) => file !== null);
|
|
296
324
|
|
|
325
|
+
if (!otherParticipant?._id) {
|
|
326
|
+
console.error("Cannot send message: receiver ID is missing.");
|
|
327
|
+
setIsSending(false);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
297
331
|
mutation.mutate(
|
|
298
332
|
{
|
|
299
|
-
|
|
300
|
-
!Array.isArray(selectedConversation?.participantDetails) &&
|
|
301
|
-
selectedConversation?.participantDetails._id,
|
|
333
|
+
receiverId: otherParticipant._id,
|
|
302
334
|
senderId: userId,
|
|
303
335
|
message: message1,
|
|
304
336
|
attachments: successfulUploads,
|
|
337
|
+
bookingId:
|
|
338
|
+
selectedConversation?.type === "service"
|
|
339
|
+
? selectedConversation?.bookingId
|
|
340
|
+
: undefined,
|
|
341
|
+
serviceTitle:
|
|
342
|
+
selectedConversation?.type === "service"
|
|
343
|
+
? selectedConversation?.title
|
|
344
|
+
: undefined,
|
|
345
|
+
type: selectedConversation?.type,
|
|
346
|
+
serviceId:
|
|
347
|
+
selectedConversation?.type === "service"
|
|
348
|
+
? selectedConversation?.serviceId
|
|
349
|
+
: undefined,
|
|
305
350
|
},
|
|
306
351
|
{
|
|
307
352
|
onSuccess: (data) => {
|
|
@@ -328,9 +373,7 @@ const MessageInput = () => {
|
|
|
328
373
|
messageId: data[1]._id,
|
|
329
374
|
attachments: successfulUploads,
|
|
330
375
|
senderId: userId,
|
|
331
|
-
receiverId:
|
|
332
|
-
!Array.isArray(selectedConversation?.participantDetails) &&
|
|
333
|
-
selectedConversation?.participantDetails._id,
|
|
376
|
+
receiverId: otherParticipant._id,
|
|
334
377
|
},
|
|
335
378
|
});
|
|
336
379
|
},
|
|
@@ -490,7 +533,7 @@ const MessageInput = () => {
|
|
|
490
533
|
};
|
|
491
534
|
|
|
492
535
|
return (
|
|
493
|
-
<div className="message-input-container">
|
|
536
|
+
<div className="message-input-container bg-[#FFFFFF] dark:bg-gray-800 text-gray-900 dark:text-gray-200">
|
|
494
537
|
{attachments.length > 0 && (
|
|
495
538
|
<div className="attachments-preview-container">
|
|
496
539
|
<button
|
|
@@ -533,12 +576,12 @@ const MessageInput = () => {
|
|
|
533
576
|
</div>
|
|
534
577
|
)}
|
|
535
578
|
|
|
536
|
-
<form className="chatMessageInputform" onSubmit={handleSubmit}>
|
|
579
|
+
<form className="chatMessageInputform bg-[#FFFFFF] dark:bg-gray-800 text-gray-900 dark:text-gray-200" onSubmit={handleSubmit}>
|
|
537
580
|
{inputError && (
|
|
538
581
|
<p style={{ color: "red", fontSize: "12px" }}>{inputError}</p>
|
|
539
582
|
)}
|
|
540
583
|
|
|
541
|
-
<div className="chatMessageInputdiv">
|
|
584
|
+
<div className="chatMessageInputdiv bg-[#FFFFFF] dark:bg-gray-800 text-gray-900 dark:text-gray-200">
|
|
542
585
|
<input
|
|
543
586
|
type="file"
|
|
544
587
|
ref={fileInputRef}
|
|
@@ -638,7 +681,7 @@ const MessageInput = () => {
|
|
|
638
681
|
</div>
|
|
639
682
|
|
|
640
683
|
<textarea
|
|
641
|
-
className="chatMessageInput"
|
|
684
|
+
className="chatMessageInput bg-[#FFFFFF] dark:bg-gray-800 text-gray-900 dark:text-gray-300"
|
|
642
685
|
placeholder="Send a message"
|
|
643
686
|
value={message}
|
|
644
687
|
onChange={handleChange}
|
|
@@ -677,9 +720,7 @@ const MessageInput = () => {
|
|
|
677
720
|
|
|
678
721
|
{typingUser &&
|
|
679
722
|
typingUser !== userId &&
|
|
680
|
-
typingUser ===
|
|
681
|
-
(!Array.isArray(selectedConversation?.participantDetails) &&
|
|
682
|
-
selectedConversation?.participantDetails?._id) &&
|
|
723
|
+
typingUser === otherParticipant?._id &&
|
|
683
724
|
!isSending && (
|
|
684
725
|
<div className="typingIndicator">
|
|
685
726
|
<div className="typing-loader">
|