@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.
@@ -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();
@@ -227,7 +253,7 @@ const MessageInput = () => {
227
253
  text: message1,
228
254
  message: message1,
229
255
  senderId: userId,
230
- status: "sending" as MessageStatus,
256
+ status: "pending" as MessageStatus,
231
257
  createdAt: new Date().toISOString(),
232
258
  media: attachmentsRef.current.map((att) => ({
233
259
  type: att.type,
@@ -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) => {
@@ -321,15 +364,15 @@ const MessageInput = () => {
321
364
 
322
365
  // Send message via WebSocket
323
366
  sendMessage({
324
- type: "sendMessage",
325
- chatId: selectedConversation?._id,
326
- message: message1,
327
- messageId: data[1]._id,
328
- attachments: successfulUploads,
329
- senderId: userId,
330
- receiverId:
331
- !Array.isArray(selectedConversation?.participantDetails) &&
332
- selectedConversation?.participantDetails._id,
367
+ event: "sendMessage",
368
+ data: {
369
+ chatId: selectedConversation?._id,
370
+ message: message1,
371
+ messageId: data[1]._id,
372
+ attachments: successfulUploads,
373
+ senderId: userId,
374
+ receiverId: otherParticipant._id,
375
+ },
333
376
  });
334
377
  },
335
378
  onError: (error) => {
@@ -675,9 +718,7 @@ const MessageInput = () => {
675
718
 
676
719
  {typingUser &&
677
720
  typingUser !== userId &&
678
- typingUser ===
679
- (!Array.isArray(selectedConversation?.participantDetails) &&
680
- selectedConversation?.participantDetails?._id) &&
721
+ typingUser === otherParticipant?._id &&
681
722
  !isSending && (
682
723
  <div className="typingIndicator">
683
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
@@ -42,12 +43,20 @@ const Messages = () => {
42
43
 
43
44
  const handleMessage = (event: MessageEvent) => {
44
45
  try {
45
- const data = JSON.parse(event.data);
46
-
47
- if (data.type === "newMessage") {
48
- const newMessage = data.message;
46
+ const parsed = JSON.parse(event.data);
47
+ console.log("Parsed WebSocket message1:", parsed);
48
+ if (parsed.type === "newMessage" || parsed.event === "newMessage") {
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) => {
52
61
  const isDuplicate = prevMessages.some(
53
62
  (msg) =>
@@ -61,10 +70,63 @@ const Messages = () => {
61
70
  });
62
71
  }
63
72
 
64
- if (data.type === "messageStatusUpdated") {
73
+ const statusOrder = ["sent", "delivered", "read", "edited", "deleted"];
74
+ if (parsed.event === "messageStatusUpdated") {
75
+ const { messageId, status } = parsed.data || {};
76
+ if (!messageId) {
77
+ console.error("Missing messageId in status update", parsed);
78
+ return;
79
+ }
80
+
65
81
  setMessages((prev) =>
66
- prev.map((msg) =>
67
- msg._id === data.messageId ? { ...msg, status: data.status } : msg
82
+ prev.map((msg) => {
83
+ if (msg._id !== messageId) return msg;
84
+
85
+ const currentIdx = statusOrder.indexOf(msg.status);
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
+ }
94
+
95
+ return msg; // No update if new status isn't higher
96
+ })
97
+ );
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
68
130
  )
69
131
  );
70
132
  }
@@ -84,47 +146,37 @@ const Messages = () => {
84
146
  useEffect(() => {
85
147
  if (messages.length > 0) {
86
148
  setTimeout(() => {
87
- lastMessageRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
149
+ lastMessageRef.current?.scrollIntoView({
150
+ behavior: "smooth",
151
+ block: "nearest",
152
+ });
88
153
  }, 100);
89
154
  }
90
155
  }, [messages.length]);
91
156
 
92
157
  // Track message visibility for read receipts
93
- useEffect(() => {
94
- if (!socket || !messages.length) return;
95
-
96
- const observer = new IntersectionObserver(
97
- (entries) => {
98
- entries.forEach((entry) => {
99
- if (entry.isIntersecting) {
100
- const messageId = entry.target.getAttribute("data-message-id");
101
- if (messageId) {
102
- sendDeliveryConfirmation(messageId);
103
- }
104
- }
105
- });
106
- },
107
- { threshold: 0.5 }
108
- );
109
-
110
- const messageElements = document.querySelectorAll("[data-message-id]");
111
- messageElements.forEach((el) => observer.observe(el));
112
-
113
- return () => observer.disconnect();
114
- }, [messages, socket]);
115
-
116
-
117
- const sendDeliveryConfirmation = (messageId: string) => {
118
- if (!socket) return;
119
-
120
- const message = {
121
- type: "confirmDelivery",
122
- messageId,
123
- // timestamp: Date.now()
124
- };
125
-
126
- socket.send(JSON.stringify(message));
127
- };
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]);
128
180
 
129
181
  return (
130
182
  <div
@@ -157,4 +209,4 @@ const Messages = () => {
157
209
  );
158
210
  };
159
211
 
160
- export default Messages;
212
+ export default Messages;
@@ -1,36 +1,71 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { MessageCircle } from "lucide-react";
1
3
  import { useEffect } from "react";
2
4
  import { useChatContext } from "../../providers/ChatProvider";
3
5
  import useChatUIStore from "../../stores/Zustant";
4
6
  import { ConversationProps } from "../../types/type";
5
- import { getChatConfig } from "@pubuduth-aplicy/chat-ui";
6
7
 
7
8
  const Conversation = ({ conversation }: ConversationProps) => {
8
9
  const {
9
10
  setSelectedConversation,
10
11
  setOnlineUsers,
11
- onlineUsers,
12
- selectedConversation,
13
12
  } = useChatUIStore();
14
- const { socket, sendMessage } = useChatContext();
15
- const { role } = getChatConfig();
16
- const handleSelectConversation = async () => {
17
- setSelectedConversation(conversation);
13
+ const { socket, isUserOnline } = useChatContext();
14
+ const selectedConversation = useChatUIStore(
15
+ (state) => state.selectedConversation
16
+ );
17
+ const { userId } = useChatContext();
18
+
19
+ // This should return the other participant
20
+ const participant = conversation.participantDetails?.find(
21
+ (p:any) => p._id !== userId
22
+ );
23
+
18
24
 
19
- const unreadMessages = conversation.unreadMessageIds || [];
20
- if (unreadMessages.length > 0) {
21
- console.log("Marking messages as read:", unreadMessages);
25
+ // const handleSelectConversation = () => {
26
+ // console.log("Selected Conversation Data:", JSON.stringify(conversation, null, 2));
27
+ // setSelectedConversation(conversation);
28
+
29
+ // const unreadMessages = conversation.unreadMessageIds || [];
30
+ // if (unreadMessages.length > 0 && socket?.readyState === WebSocket.OPEN) {
31
+ // console.log('unread meassge',unreadMessages);
32
+
33
+ // const message = {
34
+ // event: "messageRead",
35
+ // data: {
36
+ // messageIds: unreadMessages,
37
+ // senderId: participant?._id,
38
+ // chatId: conversation._id,
39
+ // },
40
+ // };
41
+ // socket.send(JSON.stringify(message));
42
+ // }
43
+ // };
44
+
45
+ const handleSelectConversation = () => {
46
+ console.log(
47
+ "Selected Conversation Data:",
48
+ JSON.stringify(conversation, null, 2)
49
+ );
50
+ setSelectedConversation(conversation);
51
+ const unreadMessageIds = conversation.unreadMessageIds || [];
52
+ if (unreadMessageIds.length > 0 && socket?.readyState === WebSocket.OPEN) {
53
+ console.log("unread messages", unreadMessageIds);
22
54
 
23
- sendMessage({
55
+ const message = {
24
56
  event: "messageRead",
25
57
  data: {
26
- messageIds: unreadMessages,
58
+ messageIds: unreadMessageIds,
59
+ senderId: participant?._id,
60
+ receiverId: userId,
27
61
  chatId: conversation._id,
28
62
  },
29
- });
63
+ };
64
+ socket.send(JSON.stringify(message));
65
+ // updateConversationReadStatus(conversation._id, unreadMessageIds);
30
66
  }
31
67
  };
32
68
 
33
- // Listen for online users updates
34
69
  useEffect(() => {
35
70
  if (!socket) return;
36
71
 
@@ -38,10 +73,10 @@ const Conversation = ({ conversation }: ConversationProps) => {
38
73
  try {
39
74
  const data = JSON.parse(event.data);
40
75
  if (data.event === "getOnlineUsers") {
41
- setOnlineUsers(data.data);
76
+ setOnlineUsers(data.payload);
42
77
  }
43
78
  } catch (error) {
44
- console.error("Error parsing online users update:", error);
79
+ console.error("Failed to parse WebSocket message:", error);
45
80
  }
46
81
  };
47
82
 
@@ -52,100 +87,78 @@ const Conversation = ({ conversation }: ConversationProps) => {
52
87
  };
53
88
  }, [socket, setOnlineUsers]);
54
89
 
55
- const isUserOnline =
56
- conversation?.participantDetails?._id &&
57
- onlineUsers?.includes(conversation.participantDetails._id);
90
+ // Temporary debug in your component
91
+ useEffect(() => {
92
+ console.log("Current conversation state:", conversation);
93
+ }, [conversation]);
58
94
 
95
+ const isOnline = isUserOnline(participant?._id || "");
59
96
  const isSelected = selectedConversation?._id === conversation._id;
97
+ const unreadCount = conversation.unreadMessageCount || 0;
98
+ const conversationName =
99
+ conversation.type === "service" && conversation.bookingId
100
+ ? `Booking #${conversation.bookingId}`
101
+ : participant?.firstname || "Conversation";
102
+
103
+ const lastMessageTimestamp = new Date(
104
+ conversation.lastMessage?.updatedAt || conversation.lastMessage?.createdAt
105
+ ).toLocaleTimeString([], {
106
+ hour: "2-digit",
107
+ minute: "2-digit",
108
+ });
60
109
 
61
110
  return (
62
- <>
63
- <div
64
- className={`conversation-container ${isSelected ? "selected" : ""} `}
65
- onClick={handleSelectConversation}
66
- >
67
- <div className="conversation-avatar">
68
- <img
69
- className="conversation-img"
70
- src={
71
- role === "admin" &&
72
- Array.isArray(selectedConversation?.participantDetails)
73
- ? selectedConversation.participantDetails[1]?.profilePic
74
- : !Array.isArray(selectedConversation?.participantDetails)
75
- ? selectedConversation?.participantDetails?.profilePic
76
- : undefined
77
- }
78
- alt="User Avatar"
79
- />
80
- <span
81
- className={`chatSidebarStatusDot ${isUserOnline && "online"}`}
82
- ></span>
83
- </div>
111
+ <div
112
+ className={`flex items-center p-2 cursor-pointer rounded-md hover:bg-gray-100 ${
113
+ isSelected ? "bg-gray-200" : ""
114
+ }`}
115
+ onClick={handleSelectConversation}
116
+ >
117
+ <div className="relative">
118
+ {conversation.type === "service" ? (
119
+ <div className="gap-2 flex relative">
120
+ <MessageCircle className="text-gray-600 w-4 h-4 mt-.5" />
121
+ </div>
122
+ ) : (
123
+ <>
124
+ <img
125
+ className="w-10 h-10 rounded-full"
126
+ src={participant?.profilePic}
127
+ alt="User Avatar"
128
+ />
129
+ <span
130
+ className={`chatSidebarStatusDot ${isOnline && "online"}`}
131
+ ></span>
132
+ </>
133
+ )}
134
+ </div>
84
135
 
85
- <div className="conversation-info">
86
- <div className="conversation-header">
87
- <p className="conversation-name">
88
- {role === "admin" &&
89
- Array.isArray(selectedConversation?.participantDetails)
90
- ? selectedConversation.participantDetails[1]?.firstname
91
- : !Array.isArray(selectedConversation?.participantDetails)
92
- ? selectedConversation?.participantDetails?.firstname
93
- : undefined}
94
- </p>
95
- <span className="conversation-time">
96
- {conversation.lastMessage.status === 'deleted' ? (
97
- // Show deleted timestamp if message is deleted
98
- new Date(conversation.lastMessage.updatedAt).toLocaleTimeString([], {
99
- hour: "2-digit",
100
- minute: "2-digit",
101
- })
102
- ) : conversation.lastMessage.status === 'edited' ? (
103
- // Show updated timestamp if message was edited
104
- new Date(conversation.lastMessage.updatedAt).toLocaleTimeString([], {
105
- hour: "2-digit",
106
- minute: "2-digit",
107
- })
108
- ) : (
109
- // Default to created timestamp
110
- new Date(conversation.lastMessage.createdAt).toLocaleTimeString([], {
111
- hour: "2-digit",
112
- minute: "2-digit",
113
- })
136
+ <div className="flex-1 ml-3">
137
+ <div className="flex justify-between items-center">
138
+ <p className="text-sm font-semibold text-gray-800">
139
+ {conversationName}
140
+ </p>
141
+ <div className="flex items-center gap-2">
142
+ {unreadCount > 0 && (
143
+ <span className="bg-blue-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
144
+ {unreadCount}
145
+ </span>
114
146
  )}
147
+ <span className="text-xs text-gray-500">
148
+ {lastMessageTimestamp}
115
149
  </span>
116
150
  </div>
117
- <p className="conversation-message">
118
- {conversation.lastMessage.status === "deleted" ? (
119
- "This message was deleted"
120
- ) : conversation.lastMessage.type !== "system" &&
121
- conversation.lastMessage.message.length > 50 ? (
122
- conversation.lastMessage.message.slice(0, 50) + "..."
123
- ) : conversation.lastMessage.media.length > 0 ? (
124
- <div
125
- style={{ display: "flex", alignItems: "center", gap: "5px" }}
126
- >
127
- <svg
128
- xmlns="http://www.w3.org/2000/svg"
129
- width="18"
130
- height="18"
131
- viewBox="0 0 24 24"
132
- fill="none"
133
- stroke="currentColor"
134
- strokeWidth="2"
135
- strokeLinecap="round"
136
- strokeLinejoin="round"
137
- >
138
- <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>
139
- </svg>
140
- attachment
141
- </div>
142
- ) : (
143
- conversation.lastMessage.message
144
- )}
145
- </p>
151
+ {/* <span className="text-xs text-gray-500">{lastMessageTimestamp}</span> */}
146
152
  </div>
153
+ <p className="text-xs text-gray-600 truncate">
154
+ {conversation.lastMessage?.status === "deleted"
155
+ ? "This message was deleted"
156
+ : conversation.lastMessage?.media?.length > 0
157
+ ? "Attachment"
158
+ : conversation.lastMessage?.message}
159
+ </p>
147
160
  </div>
148
- </>
161
+ </div>
149
162
  );
150
163
  };
151
164