@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
|
@@ -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.
|
|
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: "
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
@@ -1,205 +1,140 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { MessageCircle } from "lucide-react";
|
|
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
|
-
setMessages,
|
|
13
|
-
selectedConversation,
|
|
14
|
-
updateMessageStatus,
|
|
15
12
|
} = useChatUIStore();
|
|
16
|
-
const { socket,
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// // Mark unread messages as read
|
|
22
|
-
// const unreadMessages = conversation.unreadMessageIds || [];
|
|
23
|
-
// if (selectedConversation?._id && socket?.readyState === WebSocket.OPEN) {
|
|
24
|
-
// sendMessage({
|
|
25
|
-
// event: "joinChat",
|
|
26
|
-
// data: {
|
|
27
|
-
// chatId: conversation._id,
|
|
28
|
-
// },
|
|
29
|
-
// // event: "messageRead",
|
|
30
|
-
// // data: {
|
|
31
|
-
// // messageIds: unreadMessages,
|
|
32
|
-
// // chatId: conversation._id,
|
|
33
|
-
// // },
|
|
34
|
-
// });
|
|
35
|
-
// }
|
|
36
|
-
// };
|
|
13
|
+
const { socket, isUserOnline } = useChatContext();
|
|
14
|
+
const selectedConversation = useChatUIStore(
|
|
15
|
+
(state) => state.selectedConversation
|
|
16
|
+
);
|
|
17
|
+
const { userId } = useChatContext();
|
|
37
18
|
|
|
38
|
-
const
|
|
19
|
+
const participant = conversation.participantDetails?.find(
|
|
20
|
+
(p:any) => p._id !== userId
|
|
21
|
+
);
|
|
39
22
|
|
|
40
|
-
const handleSelectConversation =
|
|
41
|
-
|
|
23
|
+
const handleSelectConversation = () => {
|
|
24
|
+
console.log(
|
|
25
|
+
"Selected Conversation Data:",
|
|
26
|
+
JSON.stringify(conversation, null, 2)
|
|
27
|
+
);
|
|
42
28
|
setSelectedConversation(conversation);
|
|
29
|
+
const unreadMessageIds = conversation.unreadMessageIds || [];
|
|
30
|
+
if (unreadMessageIds.length > 0 && socket?.readyState === WebSocket.OPEN) {
|
|
31
|
+
console.log("unread messages", unreadMessageIds);
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// Join chat via WebSocket
|
|
48
|
-
if (socket?.readyState === WebSocket.OPEN) {
|
|
49
|
-
sendMessage({
|
|
50
|
-
event: "joinChat",
|
|
33
|
+
const message = {
|
|
34
|
+
event: "messageRead",
|
|
51
35
|
data: {
|
|
36
|
+
messageIds: unreadMessageIds,
|
|
37
|
+
senderId: participant?._id,
|
|
38
|
+
receiverId: userId,
|
|
52
39
|
chatId: conversation._id,
|
|
53
|
-
// Send any existing unread messages to mark as read
|
|
54
|
-
messageIds: conversation.unreadMessageIds || [],
|
|
55
40
|
},
|
|
56
|
-
}
|
|
41
|
+
};
|
|
42
|
+
socket.send(JSON.stringify(message));
|
|
57
43
|
}
|
|
58
44
|
};
|
|
59
45
|
|
|
60
|
-
// // Enhanced message handler
|
|
61
46
|
useEffect(() => {
|
|
62
47
|
if (!socket) return;
|
|
63
48
|
|
|
64
49
|
const handleMessage = (event: MessageEvent) => {
|
|
65
50
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (message.event === "newMessage") {
|
|
70
|
-
const newMessage = message.data;
|
|
71
|
-
|
|
72
|
-
// If this is the active chat, mark as read immediately
|
|
73
|
-
if (activeChatId === newMessage.conversationId) {
|
|
74
|
-
console.log("rtrtr");
|
|
75
|
-
|
|
76
|
-
sendMessage({
|
|
77
|
-
event: "messageRead",
|
|
78
|
-
data: {
|
|
79
|
-
messageIds: [newMessage._id],
|
|
80
|
-
chatId: newMessage.conversationId,
|
|
81
|
-
senderId: newMessage.senderId,
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Optimistic UI update
|
|
86
|
-
// updateMessageStatus(newMessage._id, "read");
|
|
87
|
-
} else {
|
|
88
|
-
// Otherwise mark as delivered
|
|
89
|
-
sendMessage({
|
|
90
|
-
event: "confirmDelivery",
|
|
91
|
-
data: {
|
|
92
|
-
messageIds: [newMessage._id],
|
|
93
|
-
chatId: newMessage.conversationId,
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
}
|
|
51
|
+
const data = JSON.parse(event.data);
|
|
52
|
+
if (data.event === "getOnlineUsers") {
|
|
53
|
+
setOnlineUsers(data.payload);
|
|
97
54
|
}
|
|
98
55
|
} catch (error) {
|
|
99
|
-
console.error("
|
|
56
|
+
console.error("Failed to parse WebSocket message:", error);
|
|
100
57
|
}
|
|
101
58
|
};
|
|
102
59
|
|
|
103
60
|
socket.addEventListener("message", handleMessage);
|
|
104
|
-
return () => socket.removeEventListener("message", handleMessage);
|
|
105
|
-
}, [socket, activeChatId, setMessages, updateMessageStatus]);
|
|
106
61
|
|
|
107
|
-
|
|
108
|
-
|
|
62
|
+
return () => {
|
|
63
|
+
socket.removeEventListener("message", handleMessage);
|
|
64
|
+
};
|
|
65
|
+
}, [socket, setOnlineUsers]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
console.log("Current conversation state:", conversation);
|
|
69
|
+
}, [conversation]);
|
|
70
|
+
|
|
71
|
+
const isOnline = isUserOnline(participant?._id || "");
|
|
109
72
|
const isSelected = selectedConversation?._id === conversation._id;
|
|
73
|
+
const unreadCount = conversation.unreadMessageCount || 0;
|
|
74
|
+
const conversationName =
|
|
75
|
+
conversation.type === "service" && conversation.bookingId
|
|
76
|
+
? `Booking #${conversation.bookingId}`
|
|
77
|
+
: participant?.name || "Conversation";
|
|
78
|
+
|
|
79
|
+
const lastMessageTimestamp = new Date(
|
|
80
|
+
conversation.lastMessage?.updatedAt || conversation.lastMessage?.createdAt
|
|
81
|
+
).toLocaleTimeString([], {
|
|
82
|
+
hour: "2-digit",
|
|
83
|
+
minute: "2-digit",
|
|
84
|
+
});
|
|
110
85
|
|
|
111
86
|
return (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
87
|
+
<div
|
|
88
|
+
className={`flex items-center p-2 cursor-pointer rounded-md hover:bg-gray-100 ${
|
|
89
|
+
isSelected ? "bg-gray-200" : ""
|
|
90
|
+
}`}
|
|
91
|
+
onClick={handleSelectConversation}
|
|
92
|
+
>
|
|
93
|
+
<div className="relative">
|
|
94
|
+
{conversation.type === "service" ? (
|
|
95
|
+
<div className="gap-2 flex relative">
|
|
96
|
+
<MessageCircle className="text-gray-600 w-4 h-4 mt-.5" />
|
|
97
|
+
</div>
|
|
98
|
+
) : (
|
|
99
|
+
<>
|
|
100
|
+
<img
|
|
101
|
+
className="w-10 h-10 rounded-full"
|
|
102
|
+
src={participant?.profilePicture}
|
|
103
|
+
alt="User Avatar"
|
|
104
|
+
/>
|
|
105
|
+
<span
|
|
106
|
+
className={`chatSidebarStatusDot ${isOnline && "online"}`}
|
|
107
|
+
></span>
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
134
111
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
new Date(
|
|
149
|
-
conversation.lastMessage.updatedAt
|
|
150
|
-
).toLocaleTimeString([], {
|
|
151
|
-
hour: "2-digit",
|
|
152
|
-
minute: "2-digit",
|
|
153
|
-
})
|
|
154
|
-
: conversation.lastMessage.status === "edited"
|
|
155
|
-
? // Show updated timestamp if message was edited
|
|
156
|
-
new Date(
|
|
157
|
-
conversation.lastMessage.updatedAt
|
|
158
|
-
).toLocaleTimeString([], {
|
|
159
|
-
hour: "2-digit",
|
|
160
|
-
minute: "2-digit",
|
|
161
|
-
})
|
|
162
|
-
: // Default to created timestamp
|
|
163
|
-
new Date(
|
|
164
|
-
conversation.lastMessage.createdAt
|
|
165
|
-
).toLocaleTimeString([], {
|
|
166
|
-
hour: "2-digit",
|
|
167
|
-
minute: "2-digit",
|
|
168
|
-
})}
|
|
112
|
+
<div className="flex-1 ml-3">
|
|
113
|
+
<div className="flex justify-between items-center">
|
|
114
|
+
<p className="text-sm font-semibold text-gray-800">
|
|
115
|
+
{conversationName}
|
|
116
|
+
</p>
|
|
117
|
+
<div className="flex items-center gap-2">
|
|
118
|
+
{unreadCount > 0 && (
|
|
119
|
+
<span className="bg-blue-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
|
|
120
|
+
{unreadCount}
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
<span className="text-xs text-gray-500">
|
|
124
|
+
{lastMessageTimestamp}
|
|
169
125
|
</span>
|
|
170
126
|
</div>
|
|
171
|
-
<
|
|
172
|
-
{conversation.lastMessage.status === "deleted" ? (
|
|
173
|
-
"This message was deleted"
|
|
174
|
-
) : conversation.lastMessage.type !== "system" &&
|
|
175
|
-
conversation.lastMessage.message.length > 50 ? (
|
|
176
|
-
conversation.lastMessage.message.slice(0, 50) + "..."
|
|
177
|
-
) : conversation.lastMessage.media.length > 0 ? (
|
|
178
|
-
<div
|
|
179
|
-
style={{ display: "flex", alignItems: "center", gap: "5px" }}
|
|
180
|
-
>
|
|
181
|
-
<svg
|
|
182
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
183
|
-
width="18"
|
|
184
|
-
height="18"
|
|
185
|
-
viewBox="0 0 24 24"
|
|
186
|
-
fill="none"
|
|
187
|
-
stroke="currentColor"
|
|
188
|
-
strokeWidth="2"
|
|
189
|
-
strokeLinecap="round"
|
|
190
|
-
strokeLinejoin="round"
|
|
191
|
-
>
|
|
192
|
-
<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>
|
|
193
|
-
</svg>
|
|
194
|
-
attachment
|
|
195
|
-
</div>
|
|
196
|
-
) : (
|
|
197
|
-
conversation.lastMessage.message
|
|
198
|
-
)}
|
|
199
|
-
</p>
|
|
127
|
+
{/* <span className="text-xs text-gray-500">{lastMessageTimestamp}</span> */}
|
|
200
128
|
</div>
|
|
129
|
+
<p className="text-xs text-gray-600 truncate dark:text-gray-500">
|
|
130
|
+
{conversation.lastMessage?.status === "deleted"
|
|
131
|
+
? "This message was deleted"
|
|
132
|
+
: conversation.lastMessage?.media?.length > 0
|
|
133
|
+
? "Attachment"
|
|
134
|
+
: conversation.lastMessage?.message}
|
|
135
|
+
</p>
|
|
201
136
|
</div>
|
|
202
|
-
|
|
137
|
+
</div>
|
|
203
138
|
);
|
|
204
139
|
};
|
|
205
140
|
|