@pubuduth-aplicy/chat-ui 2.1.71 → 2.1.74

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubuduth-aplicy/chat-ui",
3
- "version": "2.1.71",
3
+ "version": "2.1.74",
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",
@@ -5,35 +5,48 @@ import { Sidebar } from "./sidebar/Sidebar";
5
5
  import { useChatContext } from "../providers/ChatProvider";
6
6
 
7
7
  export const Chat = () => {
8
- const { messages, setMessages, updateMessageStatus } =
9
- useChatUIStore();
10
- const { socket } = useChatContext();
8
+ const { setMessages, updateMessageStatus } = useChatUIStore();
9
+
10
+ const { socket, sendMessage } = useChatContext();
11
11
 
12
12
  useEffect(() => {
13
13
  if (!socket) return;
14
14
 
15
15
  const handleMessage = (event: MessageEvent) => {
16
16
  try {
17
- const data = JSON.parse(event.data);
18
-
19
- if (data.type === "receiveMessage") {
20
- setMessages([...messages, { ...data.message, status: "sent" }]);
21
- }
17
+ const parsed = JSON.parse(event.data);
18
+ console.log("Parsed WebSocket message:", parsed);
19
+
20
+ if (parsed.event === "newMessage") {
21
+ const message = parsed.data;
22
+ console.log(
23
+ "📨 Message received at:",
24
+ message.createdAt,
25
+ "from:",
26
+ message
27
+ );
28
+ // Send delivery confirmation
29
+ // Update UI immediately
30
+ setMessages((prev) => [...prev, message]);
31
+ sendMessage({
32
+ event: "confirmDelivery",
33
+ data: {
34
+ messageIds: [message._id],
35
+ chatId: message.conversationId, // make sure this is sent from backend
36
+ },
37
+ });
22
38
 
23
- if (data.type === "messageDelivered") {
24
- updateMessageStatus(data.messageId, "delivered");
39
+ // Optional: update UI
40
+ updateMessageStatus(message._id, "delivered");
25
41
  }
26
42
  } catch (error) {
27
- console.error("Error parsing WebSocket message:", error);
43
+ console.error("WebSocket message parse error:", error);
28
44
  }
29
45
  };
30
46
 
31
47
  socket.addEventListener("message", handleMessage);
32
-
33
- return () => {
34
- socket.removeEventListener("message", handleMessage);
35
- };
36
- }, [socket, messages, setMessages, updateMessageStatus]);
48
+ return () => socket.removeEventListener("message", handleMessage);
49
+ }, [socket, setMessages, sendMessage, updateMessageStatus]);
37
50
 
38
51
  return (
39
52
  <div className="container mx-auto mb-5">
@@ -47,4 +60,4 @@ export const Chat = () => {
47
60
  </div>
48
61
  </div>
49
62
  );
50
- };
63
+ };
@@ -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">{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">{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;
@@ -11,27 +11,27 @@ import { useDeleteMessageMutation } from "../../hooks/mutations/useDeleteMessage
11
11
  import { useEffect, useRef, useState } from "react";
12
12
 
13
13
  interface MessageProps {
14
- message: {
15
- _id?: string
16
- senderId: string
17
- message: string
18
- status: MessageStatus
19
- createdAt: any
20
- updatedAt: any
14
+ message: {
15
+ _id?: string;
16
+ senderId: string;
17
+ message: string;
18
+ status: MessageStatus;
19
+ createdAt: any;
20
+ updatedAt: any;
21
21
  media?: {
22
- type: FileType
23
- url: string
24
- name: string
25
- size: number
26
- uploadProgress?: number
27
- uploadError: string | null
28
- }[]
29
- isUploading?: boolean
30
- isEdited?: boolean
31
- isDeleted?: boolean
32
- onEdit?: (messageId: string, newMessage: string) => void
33
- onDelete?: (messageId: string) => void
34
- type?: 'user' | 'system' | 'system-completion';
22
+ type: FileType;
23
+ url: string;
24
+ name: string;
25
+ size: number;
26
+ uploadProgress?: number;
27
+ uploadError: string | null;
28
+ }[];
29
+ isUploading?: boolean;
30
+ isEdited?: boolean;
31
+ isDeleted?: boolean;
32
+ onEdit?: (messageId: string, newMessage: string) => void;
33
+ onDelete?: (messageId: string) => void;
34
+ type?: "user" | "system" | "system-completion";
35
35
  meta?: {
36
36
  bookingDetails?: {
37
37
  serviceId: string;
@@ -41,8 +41,8 @@ interface MessageProps {
41
41
  // Add other booking details as needed
42
42
  };
43
43
  reviewLink?: string;
44
- }
45
- }
44
+ };
45
+ };
46
46
  }
47
47
 
48
48
  const Message = ({ message }: MessageProps) => {
@@ -91,8 +91,8 @@ const Message = ({ message }: MessageProps) => {
91
91
  const { mutate: editMessage } = useEditMessageMutation();
92
92
  const [editedMessage, setEditedMessage] = useState("");
93
93
  const [isEditingMode, setIsEditingMode] = useState(false);
94
- const { mutate: deleteMessage, isPending: isDeleting } = useDeleteMessageMutation();
95
-
94
+ const { mutate: deleteMessage, isPending: isDeleting } =
95
+ useDeleteMessageMutation();
96
96
 
97
97
  useEffect(() => {
98
98
  setLocalStatus(message.status);
@@ -101,6 +101,7 @@ const { mutate: deleteMessage, isPending: isDeleting } = useDeleteMessageMutatio
101
101
  // const handleDownload = (url: string, name: string) => {
102
102
  // saveAs(url, name);
103
103
  // };
104
+ console.log("check message status", message.status);
104
105
 
105
106
  const [downloadingIndex, setDownloadingIndex] = useState<number | null>(null);
106
107
  const [downloadProgress, setDownloadProgress] = useState<number>(0);
@@ -499,7 +500,7 @@ const { mutate: deleteMessage, isPending: isDeleting } = useDeleteMessageMutatio
499
500
  fromMe && !showDeleteOption && setShowOptions(false)
500
501
  }
501
502
  >
502
- {message.isDeleted ? (
503
+ {message.status === "deleted" ? (
503
504
  <div className="chat-bubble compact-bubble deleted-message">
504
505
  <div className="message-text">This message was deleted</div>
505
506
  </div>
@@ -565,7 +566,9 @@ const { mutate: deleteMessage, isPending: isDeleting } = useDeleteMessageMutatio
565
566
  onClick={handleDeleteClick}
566
567
  >
567
568
  <Trash2 size={16} />
568
- <span>{isDeleting ? 'Deleting...' : 'Delete'}</span>
569
+ <span>
570
+ {isDeleting ? "Deleting..." : "Delete"}
571
+ </span>
569
572
  </button>
570
573
  </div>
571
574
  )}
@@ -576,25 +579,23 @@ const { mutate: deleteMessage, isPending: isDeleting } = useDeleteMessageMutatio
576
579
  )
577
580
  )}
578
581
  <div className={`${timestamp}`}>
579
- {message.status === 'deleted' ? (
580
- // Show deleted timestamp if message is deleted
581
- new Date(message.updatedAt).toLocaleTimeString([], {
582
- hour: "2-digit",
583
- minute: "2-digit",
584
- })
585
- ) : message.status === 'edited' ? (
586
- // Show updated timestamp if message was edited
587
- new Date(message.updatedAt).toLocaleTimeString([], {
588
- hour: "2-digit",
589
- minute: "2-digit",
590
- })
591
- ) : (
592
- // Default to created timestamp
593
- new Date(message.createdAt).toLocaleTimeString([], {
594
- hour: "2-digit",
595
- minute: "2-digit",
596
- })
597
- )}
582
+ {message.status === "deleted"
583
+ ? // Show deleted timestamp if message is deleted
584
+ new Date(message.updatedAt).toLocaleTimeString([], {
585
+ hour: "2-digit",
586
+ minute: "2-digit",
587
+ })
588
+ : message.status === "edited"
589
+ ? // Show updated timestamp if message was edited
590
+ new Date(message.updatedAt).toLocaleTimeString([], {
591
+ hour: "2-digit",
592
+ minute: "2-digit",
593
+ })
594
+ : // Default to created timestamp
595
+ new Date(message.createdAt).toLocaleTimeString([], {
596
+ hour: "2-digit",
597
+ minute: "2-digit",
598
+ })}
598
599
  <span className="status-icon">{getStatusIcon()}</span>
599
600
  </div>
600
601
  </div>
@@ -1,47 +1,91 @@
1
- import { useEffect, useState } from "react";
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 "@pubuduth-aplicy/chat-ui";
8
+ import { getChatConfig } from "../../Chat.config";
7
9
 
8
10
  const MessageContainer = () => {
9
- const {
10
- selectedConversation,
11
- setSelectedConversation,
12
- onlineUsers,
13
- setOnlineUsers,
14
- } = useChatUIStore();
15
- const { socket, sendMessage } = useChatContext();
16
- const {role}= getChatConfig()
17
- const [joinedChats, setJoinedChats] = useState<Set<string>>(new Set());
11
+ const { selectedConversation, setSelectedConversation, setOnlineUsers } =
12
+ useChatUIStore();
13
+ const { socket, isUserOnline } = useChatContext();
14
+ const { role } = getChatConfig();
15
+ // useEffect(() => {
16
+ // if (!socket) return;
17
+
18
+ // const handleMessage = (event) => {
19
+ // try {
20
+ // const parsed = JSON.parse(event.data);
21
+
22
+ // if (parsed.event === 'newMessage') {
23
+ // const message = parsed.data;
24
+ // console.log('Received message:', message);
25
+
26
+ // if (selectedConversation?._id !== message.chatId) return;
27
+
28
+ // const messageId = message._id || message.messageId;
29
+ // console.log('Message ID for unread:', messageId);
30
+
31
+ // if (!messageId) {
32
+ // console.warn('Message has no _id or messageId, skipping unread tracking');
33
+ // return;
34
+ // }
35
+
36
+ // const updatedUnread = [
37
+ // ...(selectedConversation?.unreadMessageIds || []),
38
+ // messageId,
39
+ // ];
40
+
41
+ // console.log('Updated unreadMessageIds:', updatedUnread);
42
+
43
+ // setSelectedConversation({
44
+ // ...selectedConversation,
45
+ // unreadMessageIds: updatedUnread,
46
+ // });
47
+ // }
48
+
49
+ // // Handle other events...
50
+
51
+ // } catch (error) {
52
+ // console.error("WebSocket message parse error:", error);
53
+ // }
54
+ // };
55
+
56
+ // socket.addEventListener("message", handleMessage);
57
+ // return () => socket.removeEventListener("message", handleMessage);
58
+ // }, [socket, setMessages, selectedConversation, setSelectedConversation]);
18
59
 
19
60
  // Join chat room when conversation is selected
61
+
20
62
  useEffect(() => {
21
63
  if (selectedConversation?._id && socket?.readyState === WebSocket.OPEN) {
22
- const chatId = selectedConversation._id;
23
- if (!joinedChats.has(chatId)) {
24
- sendMessage({
25
- type: "joinChat",
26
- chatId: chatId,
27
- });
28
- setJoinedChats(new Set(joinedChats).add(chatId));
29
- }
64
+ // Send joinChat command to server via WebSocket
65
+ socket.send(
66
+ JSON.stringify({
67
+ event: "joinChat",
68
+ data: {
69
+ chatId: selectedConversation._id,
70
+ },
71
+ })
72
+ );
30
73
  }
31
- }, [selectedConversation?._id, socket, sendMessage, joinedChats]);
74
+ }, [selectedConversation?._id, socket]);
32
75
 
33
- // Listen for online users updates
34
76
  useEffect(() => {
35
77
  if (!socket) return;
36
78
 
37
79
  const handleMessage = (event: MessageEvent) => {
38
80
  try {
39
81
  const data = JSON.parse(event.data);
40
- if (data.type === "getOnlineUsers") {
41
- setOnlineUsers(data.users);
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
42
86
  }
43
- } catch (error) {
44
- console.error("Error parsing message:", error);
87
+ } catch (err) {
88
+ console.error("Failed to parse WebSocket message", err);
45
89
  }
46
90
  };
47
91
 
@@ -52,17 +96,23 @@ const MessageContainer = () => {
52
96
  };
53
97
  }, [socket, setOnlineUsers]);
54
98
 
55
- role === 'admin' && Array.isArray(selectedConversation?.participantDetails)
56
- ? selectedConversation.participantDetails[1]?.profilePic
57
- : !Array.isArray(selectedConversation?.participantDetails)
58
- ? selectedConversation?.participantDetails?.profilePic
59
- : undefined;
99
+ // Listen for online users updates
100
+
101
+ const { userId } = useChatContext();
60
102
 
61
- const isUserOnline =
62
- !Array.isArray(selectedConversation?.participantDetails) &&
63
- !!selectedConversation?.participantDetails?._id &&
64
- onlineUsers?.includes(selectedConversation.participantDetails._id);
103
+ // const participantDetails = Array.isArray(selectedConversation?.participantDetails)
104
+ // ? selectedConversation?.participantDetails
105
+ // : [selectedConversation?.participantDetails].filter(Boolean);
65
106
 
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 || "");
66
116
 
67
117
  // Cleanup on unmount
68
118
  useEffect(() => {
@@ -80,38 +130,45 @@ const MessageContainer = () => {
80
130
  <button className="chatMessageContainerInnerDiv_button">
81
131
  {/* <CaretLeft size={25} /> */}
82
132
  </button>
83
- <img
84
- className="chatMessageContainerInnerImg"
85
- alt="Profile"
86
- src={
87
- role === 'admin' && Array.isArray(selectedConversation?.participantDetails)
88
- ? selectedConversation.participantDetails[1]?.profilePic
89
- : !Array.isArray(selectedConversation?.participantDetails)
90
- ? selectedConversation?.participantDetails?.profilePic
91
- : undefined
92
- }
93
- />
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?.profilePic}
142
+ />
143
+ )}
94
144
  <div className="chatMessageContainerOutter">
95
145
  <div className="chatMessageContainerOutterDiv">
96
- <p className="chatMessageContainerOutterDiv_name">
97
- {role === 'admin' && Array.isArray(selectedConversation?.participantDetails)
98
- ? selectedConversation.participantDetails[1]?.firstname
99
- : !Array.isArray(selectedConversation?.participantDetails)
100
- ? selectedConversation?.participantDetails?.firstname
101
- : undefined
102
- }
103
- </p>
104
- <p className="text-sm">
105
- {isUserOnline ? "Online" : "Offline"}
106
- </p>
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?.firstname || ""}
159
+ </p>
160
+ <p className="text-sm">
161
+ {isOnline ? "Online" : "Offline"}
162
+ </p>
163
+ </>
164
+ )}
107
165
  </div>
108
166
  </div>
109
167
  </div>
110
168
  </div>
111
169
 
112
170
  <Messages />
113
- {role !== 'admin'&& (<MessageInput />)}
114
-
171
+ {role !== "admin" && <MessageInput />}
115
172
  </>
116
173
  )}
117
174
  </div>
@@ -213,4 +270,4 @@ const EmptyInbox: React.FC<EmptyInboxProps> = ({
213
270
  <p className="text-gray-500 max-w-sm">{description}</p>
214
271
  </div>
215
272
  );
216
- };
273
+ };