@pubuduth-aplicy/chat-ui 2.1.73 → 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.73",
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",
@@ -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;
@@ -101,6 +101,7 @@ const Message = ({ message }: MessageProps) => {
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 Message = ({ message }: MessageProps) => {
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>
@@ -1,22 +1,17 @@
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
- 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
- const chatId = selectedConversation._id;
70
- const unreadMessages = selectedConversation.unreadMessageIds || [];
71
-
72
- console.log("Unread messages:", unreadMessages);
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
- messageIds: unreadMessages,
79
- chatId: chatId,
69
+ chatId: selectedConversation._id,
80
70
  },
81
- });
82
- }
83
- // }
71
+ })
72
+ );
84
73
  }
85
- }, [
86
- selectedConversation?._id,
87
- socket,
88
- selectedConversation?.unreadMessageIds,
89
- sendMessage,
90
- joinedChats,
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
- role === "admin" && Array.isArray(selectedConversation?.participantDetails)
96
- ? selectedConversation.participantDetails[1]?.profilePic
97
- : !Array.isArray(selectedConversation?.participantDetails)
98
- ? selectedConversation?.participantDetails?.profilePic
99
- : undefined;
101
+ const { userId } = useChatContext();
102
+
103
+ // const participantDetails = Array.isArray(selectedConversation?.participantDetails)
104
+ // ? selectedConversation?.participantDetails
105
+ // : [selectedConversation?.participantDetails].filter(Boolean);
100
106
 
101
- const isOnline =
102
- !Array.isArray(selectedConversation?.participantDetails) &&
103
- !!selectedConversation?.participantDetails?._id &&
104
- isUserOnline(selectedConversation.participantDetails._id);
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(() => {
@@ -119,29 +130,38 @@ const MessageContainer = () => {
119
130
  <button className="chatMessageContainerInnerDiv_button">
120
131
  {/* <CaretLeft size={25} /> */}
121
132
  </button>
122
- <img
123
- className="chatMessageContainerInnerImg"
124
- alt="Profile"
125
- src={
126
- role === "admin" &&
127
- Array.isArray(selectedConversation?.participantDetails)
128
- ? selectedConversation.participantDetails[1]?.profilePic
129
- : !Array.isArray(selectedConversation?.participantDetails)
130
- ? selectedConversation?.participantDetails?.profilePic
131
- : undefined
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?.profilePic}
142
+ />
143
+ )}
134
144
  <div className="chatMessageContainerOutter">
135
145
  <div className="chatMessageContainerOutterDiv">
136
- <p className="chatMessageContainerOutterDiv_name">
137
- {role === "admin" &&
138
- Array.isArray(selectedConversation?.participantDetails)
139
- ? selectedConversation.participantDetails[1]?.firstname
140
- : !Array.isArray(selectedConversation?.participantDetails)
141
- ? selectedConversation?.participantDetails?.firstname
142
- : undefined}
143
- </p>
144
- <p className="text-sm">{isOnline ? "Online" : "Offline"}</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
+ )}
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) {
@@ -68,6 +68,20 @@ const MessageInput = () => {
68
68
  }
69
69
  }, [selectedConversation?._id, socket, sendMessage]);
70
70
 
71
+ useEffect(() => {
72
+ // Clear all input state when conversation changes
73
+ setMessage("");
74
+ setMessage1("");
75
+ setAttachments([]);
76
+ setInputError(null);
77
+ setShowAttachmentOptions(false);
78
+
79
+ // Clean up any existing file input
80
+ if (fileInputRef.current) {
81
+ fileInputRef.current.value = "";
82
+ }
83
+ }, [selectedConversation?._id]);
84
+
71
85
  // Typing indicator logic
72
86
  useEffect(() => {
73
87
  if (!socket || !selectedConversation?._id) return;
@@ -211,6 +225,18 @@ const MessageInput = () => {
211
225
  });
212
226
  };
213
227
 
228
+ // const participantDetails = Array.isArray(selectedConversation?.participantDetails)
229
+ // ? selectedConversation?.participantDetails
230
+ // : [selectedConversation?.participantDetails].filter(Boolean);
231
+
232
+ // const otherParticipant = participantDetails.find(
233
+ // (p: any) => p._id !== userId
234
+ // );
235
+
236
+ const otherParticipant = selectedConversation?.participantDetails?.find(
237
+ (p:any) => p._id !== userId
238
+ );
239
+
214
240
  const handleSubmit = useCallback(
215
241
  async (e: React.FormEvent) => {
216
242
  e.preventDefault();
@@ -294,14 +320,31 @@ const MessageInput = () => {
294
320
 
295
321
  const successfulUploads = uploadedFiles.filter((file) => file !== null);
296
322
 
323
+ if (!otherParticipant?._id) {
324
+ console.error("Cannot send message: receiver ID is missing.");
325
+ setIsSending(false);
326
+ return;
327
+ }
328
+
297
329
  mutation.mutate(
298
330
  {
299
- chatId:
300
- !Array.isArray(selectedConversation?.participantDetails) &&
301
- selectedConversation?.participantDetails._id,
331
+ receiverId: otherParticipant._id,
302
332
  senderId: userId,
303
333
  message: message1,
304
334
  attachments: successfulUploads,
335
+ bookingId:
336
+ selectedConversation?.type === "service"
337
+ ? selectedConversation?.bookingId
338
+ : undefined,
339
+ serviceTitle:
340
+ selectedConversation?.type === "service"
341
+ ? selectedConversation?.title
342
+ : undefined,
343
+ type: selectedConversation?.type,
344
+ serviceId:
345
+ selectedConversation?.type === "service"
346
+ ? selectedConversation?.serviceId
347
+ : undefined,
305
348
  },
306
349
  {
307
350
  onSuccess: (data) => {
@@ -328,9 +371,7 @@ const MessageInput = () => {
328
371
  messageId: data[1]._id,
329
372
  attachments: successfulUploads,
330
373
  senderId: userId,
331
- receiverId:
332
- !Array.isArray(selectedConversation?.participantDetails) &&
333
- selectedConversation?.participantDetails._id,
374
+ receiverId: otherParticipant._id,
334
375
  },
335
376
  });
336
377
  },
@@ -677,9 +718,7 @@ const MessageInput = () => {
677
718
 
678
719
  {typingUser &&
679
720
  typingUser !== userId &&
680
- typingUser ===
681
- (!Array.isArray(selectedConversation?.participantDetails) &&
682
- selectedConversation?.participantDetails?._id) &&
721
+ typingUser === otherParticipant?._id &&
683
722
  !isSending && (
684
723
  <div className="typingIndicator">
685
724
  <div className="typing-loader">
@@ -21,6 +21,7 @@ const Messages = () => {
21
21
  fetchMessages(selectedConversation?._id, userId, pageParam),
22
22
  getNextPageParam: (lastPage) => lastPage.nextPage,
23
23
  initialPageParam: 1,
24
+ enabled: !!selectedConversation?._id, // Prevent fetching if no conversation is selected
24
25
  });
25
26
 
26
27
  // Handle infinite scroll
@@ -45,7 +46,15 @@ const Messages = () => {
45
46
  const parsed = JSON.parse(event.data);
46
47
  console.log("Parsed WebSocket message1:", parsed);
47
48
  if (parsed.type === "newMessage" || parsed.event === "newMessage") {
48
- const newMessage = parsed.message;
49
+ const newMessage = parsed.data;
50
+ if (!newMessage) {
51
+ console.warn(
52
+ "Received newMessage event without a message payload",
53
+ parsed
54
+ );
55
+ return;
56
+ }
57
+
49
58
  newMessage.shouldShake = true;
50
59
 
51
60
  setMessages((prevMessages) => {
@@ -61,7 +70,7 @@ const Messages = () => {
61
70
  });
62
71
  }
63
72
 
64
- const statusOrder = ["sent", "delivered", "read"];
73
+ const statusOrder = ["sent", "delivered", "read", "edited", "deleted"];
65
74
  if (parsed.event === "messageStatusUpdated") {
66
75
  const { messageId, status } = parsed.data || {};
67
76
  if (!messageId) {
@@ -69,22 +78,58 @@ const Messages = () => {
69
78
  return;
70
79
  }
71
80
 
72
- console.log(`Updating status for ${messageId} to ${status}`);
73
81
  setMessages((prev) =>
74
82
  prev.map((msg) => {
75
83
  if (msg._id !== messageId) return msg;
76
84
 
77
- // Only update if new status is higher than current status
78
85
  const currentIdx = statusOrder.indexOf(msg.status);
79
86
  const newIdx = statusOrder.indexOf(status);
87
+ if (newIdx === -1 || currentIdx === -1 || newIdx <= currentIdx)
88
+ return msg;
89
+
90
+ if (newIdx > currentIdx) {
91
+ console.log(`Updating status for ${messageId} to ${status}`);
92
+ return { ...msg, status };
93
+ }
80
94
 
81
- return {
82
- ...msg,
83
- status: newIdx > currentIdx ? status : msg.status,
84
- };
95
+ return msg; // No update if new status isn't higher
85
96
  })
86
97
  );
87
98
  }
99
+
100
+ if (parsed.event === "messageEdited") {
101
+ console.log("Received messageEdited event:", parsed);
102
+
103
+ const updatedMessage = parsed.data;
104
+ if (!updatedMessage || !updatedMessage.messageId) return;
105
+
106
+ setMessages((prevMessages) =>
107
+ prevMessages.map((msg) =>
108
+ msg._id === updatedMessage.messageId
109
+ ? { ...msg, message: updatedMessage.message, status: "edited" }
110
+ : msg
111
+ )
112
+ );
113
+ }
114
+
115
+ if (parsed.event === "messageDeleted") {
116
+ console.log("Received messageDeleted event:", parsed);
117
+
118
+ const { messageId } = parsed.data || {};
119
+ if (!messageId) return;
120
+
121
+ setMessages((prevMessages) =>
122
+ prevMessages.map((msg) =>
123
+ msg._id === messageId
124
+ ? {
125
+ ...msg,
126
+ message: "This message was deleted",
127
+ status: "deleted",
128
+ }
129
+ : msg
130
+ )
131
+ );
132
+ }
88
133
  } catch (error) {
89
134
  console.error("Error parsing WebSocket message:", error);
90
135
  }
@@ -97,56 +142,41 @@ const Messages = () => {
97
142
  };
98
143
  }, [socket, selectedConversation?._id, setMessages, userId]);
99
144
 
100
- const sendDeliveryConfirmation = (messageId: string) => {
101
- if (!socket) return;
102
-
103
- const message = {
104
- event: "confirmDelivery",
105
- data: {
106
- messageId,
107
- },
108
-
109
- // timestamp: Date.now()
110
- };
111
-
112
- socket.send(JSON.stringify(message));
113
- };
114
-
115
145
  // Scroll to bottom when messages change
116
146
  useEffect(() => {
117
147
  if (messages.length > 0) {
118
148
  setTimeout(() => {
119
149
  lastMessageRef.current?.scrollIntoView({
120
150
  behavior: "smooth",
121
- block: "end",
151
+ block: "nearest",
122
152
  });
123
153
  }, 100);
124
154
  }
125
155
  }, [messages.length]);
126
156
 
127
157
  // Track message visibility for read receipts
128
- useEffect(() => {
129
- if (!socket || !messages.length) return;
130
-
131
- const observer = new IntersectionObserver(
132
- (entries) => {
133
- entries.forEach((entry) => {
134
- if (entry.isIntersecting) {
135
- const messageId = entry.target.getAttribute("data-message-id");
136
- if (messageId) {
137
- sendDeliveryConfirmation(messageId);
138
- }
139
- }
140
- });
141
- },
142
- { threshold: 0.5 }
143
- );
144
-
145
- const messageElements = document.querySelectorAll("[data-message-id]");
146
- messageElements.forEach((el) => observer.observe(el));
147
-
148
- return () => observer.disconnect();
149
- }, [messages, socket]);
158
+ // useEffect(() => {
159
+ // if (!socket || !messages.length) return;
160
+
161
+ // const observer = new IntersectionObserver(
162
+ // (entries) => {
163
+ // entries.forEach((entry) => {
164
+ // if (entry.isIntersecting) {
165
+ // const messageId = entry.target.getAttribute("data-message-id");
166
+ // if (messageId) {
167
+ // sendDeliveryConfirmation(messageId);
168
+ // }
169
+ // }
170
+ // });
171
+ // },
172
+ // { threshold: 0.5 }
173
+ // );
174
+
175
+ // const messageElements = document.querySelectorAll("[data-message-id]");
176
+ // messageElements.forEach((el) => observer.observe(el));
177
+
178
+ // return () => observer.disconnect();
179
+ // }, [messages, socket]);
150
180
 
151
181
  return (
152
182
  <div