@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 +2 -1
- package/src/components/common/CollapsibleSection.tsx +40 -0
- package/src/components/common/VirtualizedChatList.tsx +57 -0
- package/src/components/messages/Message.tsx +2 -1
- package/src/components/messages/MessageContainer.tsx +82 -62
- package/src/components/messages/MessageInput.tsx +50 -11
- package/src/components/messages/Messages.tsx +76 -46
- package/src/components/sidebar/Conversation.tsx +122 -163
- package/src/components/sidebar/Conversations.tsx +270 -40
- package/src/components/sidebar/SearchInput.tsx +16 -54
- package/src/hooks/useMessageStatus.ts +97 -0
- package/src/service/messageService.ts +86 -61
- package/src/stores/Zustant.ts +9 -4
- package/src/style/style.css +168 -0
- package/src/types/type.ts +24 -27
|
@@ -1,205 +1,164 @@
|
|
|
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
|
-
|
|
19
|
+
// This should return the other participant
|
|
20
|
+
const participant = conversation.participantDetails?.find(
|
|
21
|
+
(p:any) => p._id !== userId
|
|
22
|
+
);
|
|
39
23
|
|
|
40
|
-
const handleSelectConversation = async () => {
|
|
41
|
-
// Set as selected conversation
|
|
42
|
-
setSelectedConversation(conversation);
|
|
43
24
|
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
// };
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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);
|
|
54
|
+
|
|
55
|
+
const message = {
|
|
56
|
+
event: "messageRead",
|
|
51
57
|
data: {
|
|
58
|
+
messageIds: unreadMessageIds,
|
|
59
|
+
senderId: participant?._id,
|
|
60
|
+
receiverId: userId,
|
|
52
61
|
chatId: conversation._id,
|
|
53
|
-
// Send any existing unread messages to mark as read
|
|
54
|
-
messageIds: conversation.unreadMessageIds || [],
|
|
55
62
|
},
|
|
56
|
-
}
|
|
63
|
+
};
|
|
64
|
+
socket.send(JSON.stringify(message));
|
|
65
|
+
// updateConversationReadStatus(conversation._id, unreadMessageIds);
|
|
57
66
|
}
|
|
58
67
|
};
|
|
59
68
|
|
|
60
|
-
// // Enhanced message handler
|
|
61
69
|
useEffect(() => {
|
|
62
70
|
if (!socket) return;
|
|
63
71
|
|
|
64
72
|
const handleMessage = (event: MessageEvent) => {
|
|
65
73
|
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
|
-
}
|
|
74
|
+
const data = JSON.parse(event.data);
|
|
75
|
+
if (data.event === "getOnlineUsers") {
|
|
76
|
+
setOnlineUsers(data.payload);
|
|
97
77
|
}
|
|
98
78
|
} catch (error) {
|
|
99
|
-
console.error("
|
|
79
|
+
console.error("Failed to parse WebSocket message:", error);
|
|
100
80
|
}
|
|
101
81
|
};
|
|
102
82
|
|
|
103
83
|
socket.addEventListener("message", handleMessage);
|
|
104
|
-
return () => socket.removeEventListener("message", handleMessage);
|
|
105
|
-
}, [socket, activeChatId, setMessages, updateMessageStatus]);
|
|
106
84
|
|
|
107
|
-
|
|
108
|
-
|
|
85
|
+
return () => {
|
|
86
|
+
socket.removeEventListener("message", handleMessage);
|
|
87
|
+
};
|
|
88
|
+
}, [socket, setOnlineUsers]);
|
|
89
|
+
|
|
90
|
+
// Temporary debug in your component
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
console.log("Current conversation state:", conversation);
|
|
93
|
+
}, [conversation]);
|
|
94
|
+
|
|
95
|
+
const isOnline = isUserOnline(participant?._id || "");
|
|
109
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
|
+
});
|
|
110
109
|
|
|
111
110
|
return (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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>
|
|
134
135
|
|
|
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
|
-
})}
|
|
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>
|
|
146
|
+
)}
|
|
147
|
+
<span className="text-xs text-gray-500">
|
|
148
|
+
{lastMessageTimestamp}
|
|
169
149
|
</span>
|
|
170
150
|
</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>
|
|
151
|
+
{/* <span className="text-xs text-gray-500">{lastMessageTimestamp}</span> */}
|
|
200
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>
|
|
201
160
|
</div>
|
|
202
|
-
|
|
161
|
+
</div>
|
|
203
162
|
);
|
|
204
163
|
};
|
|
205
164
|
|
|
@@ -1,56 +1,286 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
3
|
import { useGetConversations } from "../../hooks/queries/useChatApi";
|
|
4
4
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
Conversation as ConversationType,
|
|
7
|
+
ParticipantGroup,
|
|
8
|
+
} from "../../types/type";
|
|
9
|
+
import CollapsibleSection from "../common/CollapsibleSection";
|
|
10
|
+
import useChatUIStore from "../../stores/Zustant";
|
|
11
|
+
import VirtualizedChatList from "../common/VirtualizedChatList";
|
|
12
|
+
|
|
13
|
+
type GroupedServiceChats = {
|
|
14
|
+
[serviceId: string]: {
|
|
15
|
+
serviceTitle: string;
|
|
16
|
+
conversations: ConversationType[];
|
|
17
|
+
};
|
|
18
|
+
};
|
|
6
19
|
|
|
7
20
|
const Conversations = () => {
|
|
8
|
-
const { userId } = useChatContext();
|
|
9
|
-
const { data:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
const { userId, socket } = useChatContext();
|
|
22
|
+
const { data: participantGroups } = useGetConversations(userId);
|
|
23
|
+
const { searchTerm } = useChatUIStore();
|
|
24
|
+
const [conversations, setConversations] = useState<{
|
|
25
|
+
generalChats: ConversationType[];
|
|
26
|
+
groupedServiceChats: GroupedServiceChats;
|
|
27
|
+
}>({ generalChats: [], groupedServiceChats: {} });
|
|
28
|
+
|
|
29
|
+
// Process fetched data
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!participantGroups) return;
|
|
32
|
+
|
|
33
|
+
const processConversations = (groups: ParticipantGroup[]) => {
|
|
34
|
+
const allGeneralChats: ConversationType[] = [];
|
|
35
|
+
const allServiceChatsMap = new Map<string, GroupedServiceChats[string]>();
|
|
36
|
+
|
|
37
|
+
groups.forEach((group) => {
|
|
38
|
+
if (group.personalConversation) {
|
|
39
|
+
allGeneralChats.push(group.personalConversation);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
group.serviceConversations.forEach((serviceGroup) => {
|
|
43
|
+
if (!allServiceChatsMap.has(serviceGroup.serviceId)) {
|
|
44
|
+
allServiceChatsMap.set(serviceGroup.serviceId, {
|
|
45
|
+
serviceTitle: serviceGroup.serviceTitle,
|
|
46
|
+
conversations: [],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
allServiceChatsMap
|
|
50
|
+
.get(serviceGroup.serviceId)!
|
|
51
|
+
.conversations.push(...serviceGroup.conversations);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const sortConversations = (convos: ConversationType[]) =>
|
|
56
|
+
convos.sort(
|
|
57
|
+
(a, b) =>
|
|
58
|
+
new Date(b.lastMessage?.createdAt || b.updatedAt).getTime() -
|
|
59
|
+
new Date(a.lastMessage?.createdAt || a.updatedAt).getTime()
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
sortConversations(allGeneralChats);
|
|
63
|
+
allServiceChatsMap.forEach((value) =>
|
|
64
|
+
sortConversations(value.conversations)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
generalChats: allGeneralChats,
|
|
69
|
+
groupedServiceChats: Object.fromEntries(allServiceChatsMap),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
setConversations(processConversations(participantGroups));
|
|
74
|
+
}, [participantGroups]);
|
|
75
|
+
|
|
76
|
+
// Real-time update listeners
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!socket) return;
|
|
79
|
+
|
|
80
|
+
const handleMessageReadAck = (data: {
|
|
81
|
+
messageIds: string[];
|
|
82
|
+
chatId: string;
|
|
83
|
+
}) => {
|
|
84
|
+
const { chatId, messageIds } = data;
|
|
85
|
+
|
|
86
|
+
setConversations((prev) => {
|
|
87
|
+
const generalChats = [...prev.generalChats];
|
|
88
|
+
const groupedServiceChats = { ...prev.groupedServiceChats };
|
|
89
|
+
|
|
90
|
+
const updateRead = (convo: ConversationType) => {
|
|
91
|
+
if (convo._id !== chatId) return convo;
|
|
92
|
+
const updatedUnread = (convo.unreadMessageIds || []).filter(
|
|
93
|
+
(id) => !messageIds.includes(id)
|
|
94
|
+
);
|
|
95
|
+
return {
|
|
96
|
+
...convo,
|
|
97
|
+
unreadMessageIds: updatedUnread,
|
|
98
|
+
unreadMessageCount: updatedUnread.length,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const generalIndex = generalChats.findIndex((c) => c._id === chatId);
|
|
103
|
+
if (generalIndex >= 0) {
|
|
104
|
+
generalChats[generalIndex] = updateRead(generalChats[generalIndex]);
|
|
105
|
+
return { generalChats, groupedServiceChats };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const serviceId in groupedServiceChats) {
|
|
109
|
+
const convos = groupedServiceChats[serviceId].conversations;
|
|
110
|
+
const convoIndex = convos.findIndex((c) => c._id === chatId);
|
|
111
|
+
if (convoIndex >= 0) {
|
|
112
|
+
const updatedConvos = [...convos];
|
|
113
|
+
updatedConvos[convoIndex] = updateRead(updatedConvos[convoIndex]);
|
|
114
|
+
groupedServiceChats[serviceId] = {
|
|
115
|
+
...groupedServiceChats[serviceId],
|
|
116
|
+
conversations: updatedConvos,
|
|
117
|
+
};
|
|
118
|
+
return { generalChats, groupedServiceChats };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return prev;
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleNewMessage = (newMessage: any) => {
|
|
127
|
+
if (!newMessage?.conversationId) return;
|
|
128
|
+
|
|
129
|
+
setConversations((prev) => {
|
|
130
|
+
const generalChats = [...prev.generalChats];
|
|
131
|
+
const groupedServiceChats = { ...prev.groupedServiceChats };
|
|
132
|
+
|
|
133
|
+
const updateConversation = (convo: ConversationType) => ({
|
|
134
|
+
...convo,
|
|
135
|
+
lastMessage: newMessage,
|
|
136
|
+
updatedAt: new Date().toISOString(),
|
|
137
|
+
unreadMessageIds:
|
|
138
|
+
userId !== newMessage.senderId
|
|
139
|
+
? [...(convo.unreadMessageIds || []), newMessage._id]
|
|
140
|
+
: convo.unreadMessageIds || [],
|
|
141
|
+
unreadMessageCount:
|
|
142
|
+
userId !== newMessage.senderId
|
|
143
|
+
? (convo.unreadMessageCount || 0) + 1
|
|
144
|
+
: convo.unreadMessageCount || 0,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const generalIndex = generalChats.findIndex(
|
|
148
|
+
(c) => c._id === newMessage.conversationId
|
|
149
|
+
);
|
|
150
|
+
if (generalIndex >= 0) {
|
|
151
|
+
generalChats[generalIndex] = updateConversation(
|
|
152
|
+
generalChats[generalIndex]
|
|
153
|
+
);
|
|
154
|
+
return { generalChats, groupedServiceChats };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const serviceId in groupedServiceChats) {
|
|
158
|
+
const serviceIndex = groupedServiceChats[
|
|
159
|
+
serviceId
|
|
160
|
+
].conversations.findIndex((c) => c._id === newMessage.conversationId);
|
|
161
|
+
if (serviceIndex >= 0) {
|
|
162
|
+
const updatedConversations = [
|
|
163
|
+
...groupedServiceChats[serviceId].conversations,
|
|
164
|
+
];
|
|
165
|
+
updatedConversations[serviceIndex] = updateConversation(
|
|
166
|
+
updatedConversations[serviceIndex]
|
|
167
|
+
);
|
|
168
|
+
groupedServiceChats[serviceId] = {
|
|
169
|
+
...groupedServiceChats[serviceId],
|
|
170
|
+
conversations: updatedConversations,
|
|
171
|
+
};
|
|
172
|
+
return { generalChats, groupedServiceChats };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
generalChats.push({
|
|
177
|
+
_id: newMessage.conversationId,
|
|
178
|
+
participants: [newMessage.senderId, newMessage.receiverId],
|
|
179
|
+
lastMessage: newMessage,
|
|
180
|
+
updatedAt: new Date().toISOString(),
|
|
181
|
+
unreadMessageIds:
|
|
182
|
+
userId !== newMessage.senderId ? [newMessage._id] : [],
|
|
183
|
+
unreadMessageCount: userId !== newMessage.senderId ? 1 : 0,
|
|
184
|
+
type: "personal",
|
|
185
|
+
readReceipts: [],
|
|
186
|
+
createdAt: new Date().toISOString(),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return { generalChats, groupedServiceChats };
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const messageListener = (event: MessageEvent) => {
|
|
194
|
+
try {
|
|
195
|
+
const data = JSON.parse(event.data);
|
|
196
|
+
if (data.event === "newMessage") {
|
|
197
|
+
handleNewMessage(data.data);
|
|
198
|
+
} else if (
|
|
199
|
+
data.event === "messageStatusUpdated" &&
|
|
200
|
+
data.data.status === "read"
|
|
201
|
+
) {
|
|
202
|
+
handleMessageReadAck({
|
|
203
|
+
messageIds: Array.isArray(data.data.messageId)
|
|
204
|
+
? data.data.messageId
|
|
205
|
+
: [data.data.messageId],
|
|
206
|
+
chatId: data.data.chatId,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.error("Error parsing socket message:", e);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
socket.addEventListener("message", messageListener);
|
|
215
|
+
return () => socket.removeEventListener("message", messageListener);
|
|
216
|
+
}, [socket, userId]);
|
|
217
|
+
|
|
218
|
+
const isEmpty =
|
|
219
|
+
conversations.generalChats.length === 0 &&
|
|
220
|
+
Object.keys(conversations.groupedServiceChats).length === 0;
|
|
221
|
+
|
|
222
|
+
const lowerSearch = searchTerm?.toLowerCase().trim();
|
|
223
|
+
|
|
224
|
+
const filteredGeneralChats = conversations.generalChats.filter((convo) =>
|
|
225
|
+
convo.participantDetails?.some((p:any) =>
|
|
226
|
+
typeof p === "string"
|
|
227
|
+
? p.toLowerCase().includes(lowerSearch)
|
|
228
|
+
: p.firstname?.toLowerCase().includes(lowerSearch)
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const filteredGroupedServiceChats: GroupedServiceChats = Object.fromEntries(
|
|
233
|
+
Object.entries(conversations.groupedServiceChats)
|
|
234
|
+
.map(([serviceId, group]) => {
|
|
235
|
+
const filteredConvos = group.conversations.filter((convo) =>
|
|
236
|
+
convo.participants?.some((p:any) =>
|
|
237
|
+
typeof p === "string"
|
|
238
|
+
? p.toLowerCase().includes(lowerSearch)
|
|
239
|
+
: p.name?.toLowerCase().includes(lowerSearch)
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
return [serviceId, { ...group, conversations: filteredConvos }];
|
|
243
|
+
})
|
|
244
|
+
.filter(([_, group]) => group.conversations.length > 0) // Remove empty groups
|
|
245
|
+
);
|
|
246
|
+
|
|
13
247
|
return (
|
|
14
248
|
<div className="chatSidebarConversations">
|
|
15
|
-
|
|
16
|
-
className="text-lg font-semibold text-gray-700"
|
|
17
|
-
style={{ paddingLeft: "1rem" }}
|
|
18
|
-
>
|
|
19
|
-
All Messages
|
|
20
|
-
</h2>
|
|
21
|
-
{(!conversations || conversations.length === 0) && (
|
|
249
|
+
{isEmpty ? (
|
|
22
250
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
|
23
|
-
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 mb-4">
|
|
24
|
-
<svg
|
|
25
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
26
|
-
width="32"
|
|
27
|
-
height="32"
|
|
28
|
-
viewBox="0 0 24 24"
|
|
29
|
-
fill="none"
|
|
30
|
-
stroke="currentColor"
|
|
31
|
-
strokeWidth="2"
|
|
32
|
-
strokeLinecap="round"
|
|
33
|
-
strokeLinejoin="round"
|
|
34
|
-
className="text-gray-500"
|
|
35
|
-
>
|
|
36
|
-
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
|
37
|
-
</svg>
|
|
38
|
-
</div>
|
|
39
251
|
<h3 className="text-xl font-semibold mb-1">No Conversations</h3>
|
|
40
252
|
<p className="text-gray-500 text-sm mb-4">
|
|
41
|
-
|
|
253
|
+
You have no messages yet.
|
|
42
254
|
</p>
|
|
43
255
|
</div>
|
|
256
|
+
) : (
|
|
257
|
+
<>
|
|
258
|
+
{filteredGeneralChats.length > 0 && (
|
|
259
|
+
<CollapsibleSection title="General Chats">
|
|
260
|
+
<VirtualizedChatList conversations={filteredGeneralChats} />
|
|
261
|
+
</CollapsibleSection>
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
{Object.entries(filteredGroupedServiceChats).length > 0 && (
|
|
265
|
+
<CollapsibleSection title="Service Chats">
|
|
266
|
+
{Object.entries(filteredGroupedServiceChats).map(
|
|
267
|
+
([
|
|
268
|
+
serviceId,
|
|
269
|
+
{ serviceTitle, conversations: serviceConvos },
|
|
270
|
+
]) => (
|
|
271
|
+
<CollapsibleSection
|
|
272
|
+
key={serviceId}
|
|
273
|
+
title={serviceTitle}
|
|
274
|
+
defaultOpen={false}
|
|
275
|
+
>
|
|
276
|
+
<VirtualizedChatList conversations={serviceConvos} />
|
|
277
|
+
</CollapsibleSection>
|
|
278
|
+
)
|
|
279
|
+
)}
|
|
280
|
+
</CollapsibleSection>
|
|
281
|
+
)}
|
|
282
|
+
</>
|
|
44
283
|
)}
|
|
45
|
-
{conversations?.map((conversation: any, idx: any) => (
|
|
46
|
-
<Conversation
|
|
47
|
-
key={conversation._id}
|
|
48
|
-
conversation={conversation}
|
|
49
|
-
lastIdx={idx === conversations.length - 1}
|
|
50
|
-
/>
|
|
51
|
-
))}
|
|
52
|
-
|
|
53
|
-
{/* {loading ? <span className='loading loading-spinner mx-auto'></span> : null} */}
|
|
54
284
|
</div>
|
|
55
285
|
);
|
|
56
286
|
};
|