@pubuduth-aplicy/chat-ui 2.1.93 → 2.1.94
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 +1 -1
- package/src/components/sidebar/Conversations.tsx +304 -488
package/package.json
CHANGED
|
@@ -1,541 +1,357 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// import { useGetConversations } from "../../hooks/queries/useChatApi";
|
|
4
|
-
// import { useChatContext } from "../../providers/ChatProvider";
|
|
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
|
-
// };
|
|
19
|
-
|
|
20
|
-
// type Row =
|
|
21
|
-
// | { type: "service-header"; serviceId: string; title: string }
|
|
22
|
-
// | { type: "conversation"; conversation: ConversationType };
|
|
23
|
-
|
|
24
|
-
// type TabType = "personal" | "service";
|
|
25
|
-
|
|
26
|
-
// const Conversations = () => {
|
|
27
|
-
// const { userId, socket } = useChatContext();
|
|
28
|
-
// const { data: participantGroups } = useGetConversations(userId);
|
|
29
|
-
// const { searchTerm } = useChatUIStore();
|
|
30
|
-
// const [activeTab, setActiveTab] = useState<TabType>("personal");
|
|
31
|
-
// const [conversations, setConversations] = useState<{
|
|
32
|
-
// personalChats: ConversationType[];
|
|
33
|
-
// groupedServiceChats: GroupedServiceChats;
|
|
34
|
-
// }>({ personalChats: [], groupedServiceChats: {} });
|
|
35
|
-
|
|
36
|
-
// // Process fetched data
|
|
37
|
-
// useEffect(() => {
|
|
38
|
-
// if (!participantGroups) return;
|
|
39
|
-
// const processConversations = (groups: ParticipantGroup[]) => {
|
|
40
|
-
// const allPersonalChats: ConversationType[] = [];
|
|
41
|
-
// const allServiceChatsMap = new Map<string, GroupedServiceChats[string]>();
|
|
42
|
-
|
|
43
|
-
// groups.forEach((group) => {
|
|
44
|
-
|
|
45
|
-
// if (group.personalConversation) {
|
|
46
|
-
// allPersonalChats.push(group.personalConversation);
|
|
47
|
-
// }
|
|
48
|
-
|
|
49
|
-
// group.serviceConversations.forEach((serviceGroup) => {
|
|
50
|
-
// if (!allServiceChatsMap.has(serviceGroup.serviceId)) {
|
|
51
|
-
// allServiceChatsMap.set(serviceGroup.serviceId, {
|
|
52
|
-
// serviceTitle: serviceGroup.serviceTitle,
|
|
53
|
-
// conversations: [],
|
|
54
|
-
// });
|
|
55
|
-
// }
|
|
56
|
-
|
|
57
|
-
// const existing = allServiceChatsMap.get(serviceGroup.serviceId)!;
|
|
58
|
-
// existing.conversations.push(...serviceGroup.conversations);
|
|
59
|
-
// });
|
|
60
|
-
// });
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// const sortConversations = (convos: ConversationType[]) =>
|
|
64
|
-
// convos.sort(
|
|
65
|
-
// (a, b) =>
|
|
66
|
-
// new Date(b.lastMessage?.createdAt || b.updatedAt).getTime() -
|
|
67
|
-
// new Date(a.lastMessage?.createdAt || a.updatedAt).getTime()
|
|
68
|
-
// );
|
|
69
|
-
|
|
70
|
-
// sortConversations(allPersonalChats);
|
|
71
|
-
// allServiceChatsMap.forEach((value) => {
|
|
72
|
-
// sortConversations(value.conversations);
|
|
73
|
-
// });
|
|
74
|
-
|
|
75
|
-
// const result = {
|
|
76
|
-
// personalChats: allPersonalChats,
|
|
77
|
-
// groupedServiceChats: Object.fromEntries(allServiceChatsMap),
|
|
78
|
-
// };
|
|
79
|
-
|
|
80
|
-
// return result;
|
|
81
|
-
// };
|
|
82
|
-
|
|
83
|
-
// setConversations(processConversations(participantGroups));
|
|
84
|
-
// }, [participantGroups]);
|
|
85
|
-
|
|
86
|
-
// // Real-time update listeners
|
|
87
|
-
// useEffect(() => {
|
|
88
|
-
// if (!socket) return;
|
|
89
|
-
|
|
90
|
-
// const handleMessageReadAck = (data: {
|
|
91
|
-
// messageIds: string[];
|
|
92
|
-
// chatId: string;
|
|
93
|
-
// }) => {
|
|
94
|
-
// const { chatId, messageIds } = data;
|
|
95
|
-
|
|
96
|
-
// setConversations((prev) => {
|
|
97
|
-
// const personalChats = [...prev.personalChats];
|
|
98
|
-
// const groupedServiceChats = { ...prev.groupedServiceChats };
|
|
99
|
-
|
|
100
|
-
// const updateRead = (convo: ConversationType) => {
|
|
101
|
-
// if (convo._id !== chatId) return convo;
|
|
102
|
-
// const updatedUnread = (convo.unreadMessageIds || []).filter(
|
|
103
|
-
// (id) => !messageIds.includes(id)
|
|
104
|
-
// );
|
|
105
|
-
// return {
|
|
106
|
-
// ...convo,
|
|
107
|
-
// unreadMessageIds: updatedUnread,
|
|
108
|
-
// unreadMessageCount: updatedUnread.length,
|
|
109
|
-
// };
|
|
110
|
-
// };
|
|
111
|
-
|
|
112
|
-
// const personalIndex = personalChats.findIndex((c) => c._id === chatId);
|
|
113
|
-
// if (personalIndex >= 0) {
|
|
114
|
-
// personalChats[personalIndex] = updateRead(personalChats[personalIndex]);
|
|
115
|
-
// return { personalChats, groupedServiceChats };
|
|
116
|
-
// }
|
|
117
|
-
|
|
118
|
-
// for (const serviceId in groupedServiceChats) {
|
|
119
|
-
// const convos = groupedServiceChats[serviceId].conversations;
|
|
120
|
-
// const convoIndex = convos.findIndex((c) => c._id === chatId);
|
|
121
|
-
// if (convoIndex >= 0) {
|
|
122
|
-
// const updatedConvos = [...convos];
|
|
123
|
-
// updatedConvos[convoIndex] = updateRead(updatedConvos[convoIndex]);
|
|
124
|
-
// groupedServiceChats[serviceId] = {
|
|
125
|
-
// ...groupedServiceChats[serviceId],
|
|
126
|
-
// conversations: updatedConvos,
|
|
127
|
-
// };
|
|
128
|
-
// return { personalChats, groupedServiceChats };
|
|
129
|
-
// }
|
|
130
|
-
// }
|
|
131
|
-
|
|
132
|
-
// return prev;
|
|
133
|
-
// });
|
|
134
|
-
// };
|
|
135
|
-
|
|
136
|
-
// const handleNewMessage = (newMessage: any) => {
|
|
137
|
-
// if (!newMessage?.conversationId) return;
|
|
138
|
-
|
|
139
|
-
// setConversations((prev) => {
|
|
140
|
-
// const personalChats = [...prev.personalChats];
|
|
141
|
-
// const groupedServiceChats = { ...prev.groupedServiceChats };
|
|
142
|
-
|
|
143
|
-
// const updateConversation = (convo: ConversationType) => ({
|
|
144
|
-
// ...convo,
|
|
145
|
-
// lastMessage: newMessage,
|
|
146
|
-
// updatedAt: new Date().toISOString(),
|
|
147
|
-
// unreadMessageIds:
|
|
148
|
-
// userId !== newMessage.senderId
|
|
149
|
-
// ? [...(convo.unreadMessageIds || []), newMessage._id]
|
|
150
|
-
// : convo.unreadMessageIds || [],
|
|
151
|
-
// unreadMessageCount:
|
|
152
|
-
// userId !== newMessage.senderId
|
|
153
|
-
// ? (convo.unreadMessageCount || 0) + 1
|
|
154
|
-
// : convo.unreadMessageCount || 0,
|
|
155
|
-
// });
|
|
156
|
-
|
|
157
|
-
// const personalIndex = personalChats.findIndex(
|
|
158
|
-
// (c) => c._id === newMessage.conversationId
|
|
159
|
-
// );
|
|
160
|
-
// if (personalIndex >= 0) {
|
|
161
|
-
// personalChats[personalIndex] = updateConversation(
|
|
162
|
-
// personalChats[personalIndex]
|
|
163
|
-
// );
|
|
164
|
-
// return { personalChats, groupedServiceChats };
|
|
165
|
-
// }
|
|
166
|
-
|
|
167
|
-
// for (const serviceId in groupedServiceChats) {
|
|
168
|
-
// const serviceIndex = groupedServiceChats[
|
|
169
|
-
// serviceId
|
|
170
|
-
// ].conversations.findIndex((c) => c._id === newMessage.conversationId);
|
|
171
|
-
// if (serviceIndex >= 0) {
|
|
172
|
-
// const updatedConversations = [
|
|
173
|
-
// ...groupedServiceChats[serviceId].conversations,
|
|
174
|
-
// ];
|
|
175
|
-
// updatedConversations[serviceIndex] = updateConversation(
|
|
176
|
-
// updatedConversations[serviceIndex]
|
|
177
|
-
// );
|
|
178
|
-
// groupedServiceChats[serviceId] = {
|
|
179
|
-
// ...groupedServiceChats[serviceId],
|
|
180
|
-
// conversations: updatedConversations,
|
|
181
|
-
// };
|
|
182
|
-
// return { personalChats, groupedServiceChats };
|
|
183
|
-
// }
|
|
184
|
-
// }
|
|
185
|
-
|
|
186
|
-
// personalChats.push({
|
|
187
|
-
// _id: newMessage.conversationId,
|
|
188
|
-
// participants: [newMessage.senderId, newMessage.receiverId],
|
|
189
|
-
// lastMessage: newMessage,
|
|
190
|
-
// updatedAt: new Date().toISOString(),
|
|
191
|
-
// unreadMessageIds:
|
|
192
|
-
// userId !== newMessage.senderId ? [newMessage._id] : [],
|
|
193
|
-
// unreadMessageCount: userId !== newMessage.senderId ? 1 : 0,
|
|
194
|
-
// type: "personal",
|
|
195
|
-
// readReceipts: [],
|
|
196
|
-
// createdAt: new Date().toISOString(),
|
|
197
|
-
// });
|
|
198
|
-
|
|
199
|
-
// return { personalChats, groupedServiceChats };
|
|
200
|
-
// });
|
|
201
|
-
// };
|
|
202
|
-
|
|
203
|
-
// const messageListener = (event: MessageEvent) => {
|
|
204
|
-
// try {
|
|
205
|
-
// const data = JSON.parse(event.data);
|
|
206
|
-
// if (data.event === "newMessage") {
|
|
207
|
-
// handleNewMessage(data.data);
|
|
208
|
-
// } else if (
|
|
209
|
-
// data.event === "messageStatusUpdated" &&
|
|
210
|
-
// data.data.status === "read"
|
|
211
|
-
// ) {
|
|
212
|
-
// handleMessageReadAck({
|
|
213
|
-
// messageIds: Array.isArray(data.data.messageId)
|
|
214
|
-
// ? data.data.messageId
|
|
215
|
-
// : [data.data.messageId],
|
|
216
|
-
// chatId: data.data.chatId,
|
|
217
|
-
// });
|
|
218
|
-
// }
|
|
219
|
-
// } catch (e) {
|
|
220
|
-
// console.error("Error parsing socket message:", e);
|
|
221
|
-
// }
|
|
222
|
-
// };
|
|
223
|
-
|
|
224
|
-
// socket.addEventListener("message", messageListener);
|
|
225
|
-
// return () => socket.removeEventListener("message", messageListener);
|
|
226
|
-
// }, [socket, userId]);
|
|
227
|
-
|
|
228
|
-
// // const isEmpty =
|
|
229
|
-
// // activeTab === "personal"
|
|
230
|
-
// // ? conversations.personalChats.length === 0
|
|
231
|
-
// // : Object.keys(conversations.groupedServiceChats).length === 0;
|
|
232
|
-
|
|
233
|
-
// const lowerSearch = searchTerm?.toLowerCase().trim();
|
|
234
|
-
|
|
235
|
-
// // Filter personal chats
|
|
236
|
-
// const filteredPersonalChats = !lowerSearch
|
|
237
|
-
// ? conversations.personalChats
|
|
238
|
-
// : conversations.personalChats.filter((convo) =>
|
|
239
|
-
// convo.participantDetails?.some((p: any) =>
|
|
240
|
-
// p?.name?.toLowerCase().includes(lowerSearch)
|
|
241
|
-
// )
|
|
242
|
-
// );
|
|
243
|
-
|
|
244
|
-
// // Filter service chats
|
|
245
|
-
// const filteredGroupedServiceChats: GroupedServiceChats = !lowerSearch
|
|
246
|
-
// ? conversations.groupedServiceChats
|
|
247
|
-
// : Object.fromEntries(
|
|
248
|
-
// Object.entries(conversations.groupedServiceChats)
|
|
249
|
-
// .map(([serviceId, group]) => {
|
|
250
|
-
// const filteredConvos = group.conversations.filter((convo) =>
|
|
251
|
-
// convo.participantDetails?.some((p: any) =>
|
|
252
|
-
// p?.name?.toLowerCase().includes(lowerSearch)
|
|
253
|
-
// ) ||
|
|
254
|
-
// group.serviceTitle?.toLowerCase().includes(lowerSearch) ||
|
|
255
|
-
// convo.serviceTitle?.toLowerCase().includes(lowerSearch)
|
|
256
|
-
// );
|
|
257
|
-
// return [serviceId, { ...group, conversations: filteredConvos }];
|
|
258
|
-
// })
|
|
259
|
-
// .filter(([_, group]) => group.conversations.length > 0)
|
|
260
|
-
// );
|
|
261
|
-
|
|
262
|
-
// // Improved empty state logic
|
|
263
|
-
// const showPersonalTab = activeTab === "personal" && filteredPersonalChats.length > 0;
|
|
264
|
-
// const showServiceTab = activeTab === "service" && Object.keys(filteredGroupedServiceChats).length > 0;
|
|
265
|
-
// const isEmpty = !showPersonalTab && !showServiceTab;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// // Calculate unread counts for tabs
|
|
269
|
-
// const personalUnreadCount = conversations.personalChats.reduce(
|
|
270
|
-
// (total, convo) => total + (convo.unreadMessageCount || 0),
|
|
271
|
-
// 0
|
|
272
|
-
// );
|
|
273
|
-
|
|
274
|
-
// const serviceUnreadCount = Object.values(conversations.groupedServiceChats)
|
|
275
|
-
// .flatMap(group => group.conversations)
|
|
276
|
-
// .reduce((total, convo) => total + (convo.unreadMessageCount || 0), 0);
|
|
277
|
-
|
|
278
|
-
// return (
|
|
279
|
-
// <div className="chatSidebarConversations">
|
|
280
|
-
// {/* Tab Navigation */}
|
|
281
|
-
// <div className="flex border-b border-gray-200">
|
|
282
|
-
// <button
|
|
283
|
-
// className={`flex-1 py-3 px-4 text-sm font-medium text-center border-b-2 transition-colors ${
|
|
284
|
-
// activeTab === "personal"
|
|
285
|
-
// ? "border-primary text-primary "
|
|
286
|
-
// : "border-transparent text-gray-500 hover:text-gray-700"
|
|
287
|
-
// }`}
|
|
288
|
-
// onClick={() => setActiveTab("personal")}
|
|
289
|
-
// >
|
|
290
|
-
// Personal
|
|
291
|
-
// {personalUnreadCount > 0 && (
|
|
292
|
-
// <span className="ml-2 bg-blue-primary text-white text-xs rounded-full px-2 py-1 min-w-5 inline-flex justify-center">
|
|
293
|
-
// {personalUnreadCount}
|
|
294
|
-
// </span>
|
|
295
|
-
// )}
|
|
296
|
-
// </button>
|
|
297
|
-
// <button
|
|
298
|
-
// className={`flex-1 py-3 px-4 text-sm font-medium text-center border-b-2 transition-colors ${
|
|
299
|
-
// activeTab === "service"
|
|
300
|
-
// ? "border-primary text-primary "
|
|
301
|
-
// : "border-transparent text-gray-500 hover:text-gray-700"
|
|
302
|
-
// }`}
|
|
303
|
-
// onClick={() => setActiveTab("service")}
|
|
304
|
-
// >
|
|
305
|
-
// Service
|
|
306
|
-
// {serviceUnreadCount > 0 && (
|
|
307
|
-
// <span className="ml-2 bg-primary text-white text-xs rounded-full px-2 py-1 min-w-5 inline-flex justify-center">
|
|
308
|
-
// {serviceUnreadCount}
|
|
309
|
-
// </span>
|
|
310
|
-
// )}
|
|
311
|
-
// </button>
|
|
312
|
-
// </div>
|
|
313
|
-
|
|
314
|
-
// {/* Tab Content */}
|
|
315
|
-
// <div className="flex-1 overflow-auto">
|
|
316
|
-
// {isEmpty ? (
|
|
317
|
-
// <div className="flex flex-col items-center justify-center p-8 text-center">
|
|
318
|
-
// <h3 className="text-xl font-semibold mb-1">No Conversations</h3>
|
|
319
|
-
// <p className="text-gray-500 text-sm mb-4">
|
|
320
|
-
// {activeTab === "personal"
|
|
321
|
-
// ? "You have no personal messages yet."
|
|
322
|
-
// : "You have no service messages yet."}
|
|
323
|
-
// </p>
|
|
324
|
-
// </div>
|
|
325
|
-
// ) : (
|
|
326
|
-
// <>
|
|
327
|
-
// <div className="h-full overflow-auto">
|
|
328
|
-
// {activeTab === "personal" && filteredPersonalChats.length > 0 && (
|
|
329
|
-
// <VirtualizedChatList conversations={filteredPersonalChats} />
|
|
330
|
-
// )}
|
|
331
|
-
|
|
332
|
-
// {activeTab === "service" &&
|
|
333
|
-
// Object.entries(filteredGroupedServiceChats).length > 0 && (
|
|
334
|
-
// <div className="p-2">
|
|
335
|
-
// {Object.entries(filteredGroupedServiceChats).map(
|
|
336
|
-
// ([serviceId, { serviceTitle, conversations: serviceConvos }]) => (
|
|
337
|
-
// <CollapsibleSection
|
|
338
|
-
// key={serviceId}
|
|
339
|
-
// title={serviceTitle}
|
|
340
|
-
// defaultOpen={false}
|
|
341
|
-
// >
|
|
342
|
-
// <VirtualizedChatList conversations={serviceConvos} />
|
|
343
|
-
// </CollapsibleSection>
|
|
344
|
-
// )
|
|
345
|
-
// )}
|
|
346
|
-
// </div>
|
|
347
|
-
// )}
|
|
348
|
-
// </div>
|
|
349
|
-
|
|
350
|
-
// </>
|
|
351
|
-
// )}
|
|
352
|
-
// </div>
|
|
353
|
-
// </div>
|
|
354
|
-
// );
|
|
355
|
-
// };
|
|
356
|
-
|
|
357
|
-
// export default Conversations;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
/* Optimized single-virtualizer version for 10k+ conversations */
|
|
362
|
-
|
|
363
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
364
|
-
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
365
3
|
import { useGetConversations } from "../../hooks/queries/useChatApi";
|
|
366
4
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
5
|
+
import {
|
|
6
|
+
Conversation as ConversationType,
|
|
7
|
+
ParticipantGroup,
|
|
8
|
+
} from "../../types/type";
|
|
9
|
+
import CollapsibleSection from "../common/CollapsibleSection";
|
|
367
10
|
import useChatUIStore from "../../stores/Zustant";
|
|
368
|
-
import
|
|
369
|
-
|
|
11
|
+
import VirtualizedChatList from "../common/VirtualizedChatList";
|
|
12
|
+
|
|
13
|
+
type GroupedServiceChats = {
|
|
14
|
+
[serviceId: string]: {
|
|
15
|
+
serviceTitle: string;
|
|
16
|
+
conversations: ConversationType[];
|
|
17
|
+
};
|
|
18
|
+
};
|
|
370
19
|
|
|
371
|
-
// ---------- Row model ----------
|
|
372
20
|
type Row =
|
|
373
21
|
| { type: "service-header"; serviceId: string; title: string }
|
|
374
22
|
| { type: "conversation"; conversation: ConversationType };
|
|
375
23
|
|
|
376
24
|
type TabType = "personal" | "service";
|
|
377
25
|
|
|
378
|
-
const
|
|
379
|
-
const ROW_HEIGHT = 60;
|
|
380
|
-
|
|
381
|
-
export default function Conversations() {
|
|
26
|
+
const Conversations = () => {
|
|
382
27
|
const { userId, socket } = useChatContext();
|
|
383
28
|
const { data: participantGroups } = useGetConversations(userId);
|
|
384
29
|
const { searchTerm } = useChatUIStore();
|
|
385
|
-
|
|
386
30
|
const [activeTab, setActiveTab] = useState<TabType>("personal");
|
|
387
|
-
const [
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
>({});
|
|
31
|
+
const [conversations, setConversations] = useState<{
|
|
32
|
+
personalChats: ConversationType[];
|
|
33
|
+
groupedServiceChats: GroupedServiceChats;
|
|
34
|
+
}>({ personalChats: [], groupedServiceChats: {} });
|
|
35
|
+
|
|
36
|
+
// Process fetched data
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!participantGroups) return;
|
|
39
|
+
const processConversations = (groups: ParticipantGroup[]) => {
|
|
40
|
+
const allPersonalChats: ConversationType[] = [];
|
|
41
|
+
const allServiceChatsMap = new Map<string, GroupedServiceChats[string]>();
|
|
42
|
+
|
|
43
|
+
groups.forEach((group) => {
|
|
44
|
+
|
|
45
|
+
if (group.personalConversation) {
|
|
46
|
+
allPersonalChats.push(group.personalConversation);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
group.serviceConversations.forEach((serviceGroup) => {
|
|
50
|
+
if (!allServiceChatsMap.has(serviceGroup.serviceId)) {
|
|
51
|
+
allServiceChatsMap.set(serviceGroup.serviceId, {
|
|
52
|
+
serviceTitle: serviceGroup.serviceTitle,
|
|
53
|
+
conversations: [],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const existing = allServiceChatsMap.get(serviceGroup.serviceId)!;
|
|
58
|
+
existing.conversations.push(...serviceGroup.conversations);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
391
61
|
|
|
392
|
-
// -------- Normalize API data (once) --------
|
|
393
|
-
useEffect(() => {
|
|
394
|
-
if (!participantGroups) return;
|
|
395
62
|
|
|
396
|
-
const
|
|
397
|
-
|
|
63
|
+
const sortConversations = (convos: ConversationType[]) =>
|
|
64
|
+
convos.sort(
|
|
65
|
+
(a, b) =>
|
|
66
|
+
new Date(b.lastMessage?.createdAt || b.updatedAt).getTime() -
|
|
67
|
+
new Date(a.lastMessage?.createdAt || a.updatedAt).getTime()
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
sortConversations(allPersonalChats);
|
|
71
|
+
allServiceChatsMap.forEach((value) => {
|
|
72
|
+
sortConversations(value.conversations);
|
|
73
|
+
});
|
|
398
74
|
|
|
399
|
-
|
|
400
|
-
|
|
75
|
+
const result = {
|
|
76
|
+
personalChats: allPersonalChats,
|
|
77
|
+
groupedServiceChats: Object.fromEntries(allServiceChatsMap),
|
|
78
|
+
};
|
|
401
79
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
setConversations(processConversations(participantGroups));
|
|
84
|
+
}, [participantGroups]);
|
|
85
|
+
|
|
86
|
+
// Real-time update listeners
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!socket) return;
|
|
89
|
+
|
|
90
|
+
const handleMessageReadAck = (data: {
|
|
91
|
+
messageIds: string[];
|
|
92
|
+
chatId: string;
|
|
93
|
+
}) => {
|
|
94
|
+
const { chatId, messageIds } = data;
|
|
95
|
+
|
|
96
|
+
setConversations((prev) => {
|
|
97
|
+
const personalChats = [...prev.personalChats];
|
|
98
|
+
const groupedServiceChats = { ...prev.groupedServiceChats };
|
|
99
|
+
|
|
100
|
+
const updateRead = (convo: ConversationType) => {
|
|
101
|
+
if (convo._id !== chatId) return convo;
|
|
102
|
+
const updatedUnread = (convo.unreadMessageIds || []).filter(
|
|
103
|
+
(id) => !messageIds.includes(id)
|
|
104
|
+
);
|
|
105
|
+
return {
|
|
106
|
+
...convo,
|
|
107
|
+
unreadMessageIds: updatedUnread,
|
|
108
|
+
unreadMessageCount: updatedUnread.length,
|
|
407
109
|
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const personalIndex = personalChats.findIndex((c) => c._id === chatId);
|
|
113
|
+
if (personalIndex >= 0) {
|
|
114
|
+
personalChats[personalIndex] = updateRead(personalChats[personalIndex]);
|
|
115
|
+
return { personalChats, groupedServiceChats };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const serviceId in groupedServiceChats) {
|
|
119
|
+
const convos = groupedServiceChats[serviceId].conversations;
|
|
120
|
+
const convoIndex = convos.findIndex((c) => c._id === chatId);
|
|
121
|
+
if (convoIndex >= 0) {
|
|
122
|
+
const updatedConvos = [...convos];
|
|
123
|
+
updatedConvos[convoIndex] = updateRead(updatedConvos[convoIndex]);
|
|
124
|
+
groupedServiceChats[serviceId] = {
|
|
125
|
+
...groupedServiceChats[serviceId],
|
|
126
|
+
conversations: updatedConvos,
|
|
127
|
+
};
|
|
128
|
+
return { personalChats, groupedServiceChats };
|
|
129
|
+
}
|
|
408
130
|
}
|
|
409
|
-
|
|
131
|
+
|
|
132
|
+
return prev;
|
|
410
133
|
});
|
|
411
|
-
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleNewMessage = (newMessage: any) => {
|
|
137
|
+
if (!newMessage?.conversationId) return;
|
|
138
|
+
|
|
139
|
+
setConversations((prev) => {
|
|
140
|
+
const personalChats = [...prev.personalChats];
|
|
141
|
+
const groupedServiceChats = { ...prev.groupedServiceChats };
|
|
142
|
+
|
|
143
|
+
const updateConversation = (convo: ConversationType) => ({
|
|
144
|
+
...convo,
|
|
145
|
+
lastMessage: newMessage,
|
|
146
|
+
updatedAt: new Date().toISOString(),
|
|
147
|
+
unreadMessageIds:
|
|
148
|
+
userId !== newMessage.senderId
|
|
149
|
+
? [...(convo.unreadMessageIds || []), newMessage._id]
|
|
150
|
+
: convo.unreadMessageIds || [],
|
|
151
|
+
unreadMessageCount:
|
|
152
|
+
userId !== newMessage.senderId
|
|
153
|
+
? (convo.unreadMessageCount || 0) + 1
|
|
154
|
+
: convo.unreadMessageCount || 0,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const personalIndex = personalChats.findIndex(
|
|
158
|
+
(c) => c._id === newMessage.conversationId
|
|
159
|
+
);
|
|
160
|
+
if (personalIndex >= 0) {
|
|
161
|
+
personalChats[personalIndex] = updateConversation(
|
|
162
|
+
personalChats[personalIndex]
|
|
163
|
+
);
|
|
164
|
+
return { personalChats, groupedServiceChats };
|
|
165
|
+
}
|
|
412
166
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
167
|
+
for (const serviceId in groupedServiceChats) {
|
|
168
|
+
const serviceIndex = groupedServiceChats[
|
|
169
|
+
serviceId
|
|
170
|
+
].conversations.findIndex((c) => c._id === newMessage.conversationId);
|
|
171
|
+
if (serviceIndex >= 0) {
|
|
172
|
+
const updatedConversations = [
|
|
173
|
+
...groupedServiceChats[serviceId].conversations,
|
|
174
|
+
];
|
|
175
|
+
updatedConversations[serviceIndex] = updateConversation(
|
|
176
|
+
updatedConversations[serviceIndex]
|
|
177
|
+
);
|
|
178
|
+
groupedServiceChats[serviceId] = {
|
|
179
|
+
...groupedServiceChats[serviceId],
|
|
180
|
+
conversations: updatedConversations,
|
|
181
|
+
};
|
|
182
|
+
return { personalChats, groupedServiceChats };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
416
185
|
|
|
417
|
-
|
|
418
|
-
|
|
186
|
+
personalChats.push({
|
|
187
|
+
_id: newMessage.conversationId,
|
|
188
|
+
participants: [newMessage.senderId, newMessage.receiverId],
|
|
189
|
+
lastMessage: newMessage,
|
|
190
|
+
updatedAt: new Date().toISOString(),
|
|
191
|
+
unreadMessageIds:
|
|
192
|
+
userId !== newMessage.senderId ? [newMessage._id] : [],
|
|
193
|
+
unreadMessageCount: userId !== newMessage.senderId ? 1 : 0,
|
|
194
|
+
type: "personal",
|
|
195
|
+
readReceipts: [],
|
|
196
|
+
createdAt: new Date().toISOString(),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return { personalChats, groupedServiceChats };
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const messageListener = (event: MessageEvent) => {
|
|
204
|
+
try {
|
|
205
|
+
const data = JSON.parse(event.data);
|
|
206
|
+
if (data.event === "newMessage") {
|
|
207
|
+
handleNewMessage(data.data);
|
|
208
|
+
} else if (
|
|
209
|
+
data.event === "messageStatusUpdated" &&
|
|
210
|
+
data.data.status === "read"
|
|
211
|
+
) {
|
|
212
|
+
handleMessageReadAck({
|
|
213
|
+
messageIds: Array.isArray(data.data.messageId)
|
|
214
|
+
? data.data.messageId
|
|
215
|
+
: [data.data.messageId],
|
|
216
|
+
chatId: data.data.chatId,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.error("Error parsing socket message:", e);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
419
223
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}, [
|
|
224
|
+
socket.addEventListener("message", messageListener);
|
|
225
|
+
return () => socket.removeEventListener("message", messageListener);
|
|
226
|
+
}, [socket, userId]);
|
|
227
|
+
|
|
228
|
+
// const isEmpty =
|
|
229
|
+
// activeTab === "personal"
|
|
230
|
+
// ? conversations.personalChats.length === 0
|
|
231
|
+
// : Object.keys(conversations.groupedServiceChats).length === 0;
|
|
423
232
|
|
|
424
|
-
// -------- Search filtering --------
|
|
425
233
|
const lowerSearch = searchTerm?.toLowerCase().trim();
|
|
426
234
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
235
|
+
// Filter personal chats
|
|
236
|
+
const filteredPersonalChats = !lowerSearch
|
|
237
|
+
? conversations.personalChats
|
|
238
|
+
: conversations.personalChats.filter((convo) =>
|
|
239
|
+
convo.participantDetails?.some((p: any) =>
|
|
431
240
|
p?.name?.toLowerCase().includes(lowerSearch)
|
|
432
241
|
)
|
|
433
242
|
);
|
|
434
|
-
}, [personalChats, lowerSearch]);
|
|
435
|
-
|
|
436
|
-
const filteredService = useMemo(() => {
|
|
437
|
-
if (!lowerSearch) return serviceChats;
|
|
438
|
-
|
|
439
|
-
const result: typeof serviceChats = {};
|
|
440
|
-
Object.entries(serviceChats).forEach(([sid, group]) => {
|
|
441
|
-
const convs = group.conversations.filter((c) =>
|
|
442
|
-
c.participantDetails?.some((p: any) =>
|
|
443
|
-
p?.name?.toLowerCase().includes(lowerSearch)
|
|
444
|
-
) ||
|
|
445
|
-
group.serviceTitle.toLowerCase().includes(lowerSearch)
|
|
446
|
-
);
|
|
447
|
-
if (convs.length) result[sid] = { ...group, conversations: convs };
|
|
448
|
-
});
|
|
449
|
-
return result;
|
|
450
|
-
}, [serviceChats, lowerSearch]);
|
|
451
|
-
|
|
452
|
-
// -------- Flatten into ONE list --------
|
|
453
|
-
const rows: Row[] = useMemo(() => {
|
|
454
|
-
const out: Row[] = [];
|
|
455
|
-
|
|
456
|
-
if (activeTab === "personal") {
|
|
457
|
-
filteredPersonal.forEach((c) =>
|
|
458
|
-
out.push({ type: "conversation", conversation: c })
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
243
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
|
|
244
|
+
// Filter service chats
|
|
245
|
+
const filteredGroupedServiceChats: GroupedServiceChats = !lowerSearch
|
|
246
|
+
? conversations.groupedServiceChats
|
|
247
|
+
: Object.fromEntries(
|
|
248
|
+
Object.entries(conversations.groupedServiceChats)
|
|
249
|
+
.map(([serviceId, group]) => {
|
|
250
|
+
const filteredConvos = group.conversations.filter((convo) =>
|
|
251
|
+
convo.participantDetails?.some((p: any) =>
|
|
252
|
+
p?.name?.toLowerCase().includes(lowerSearch)
|
|
253
|
+
) ||
|
|
254
|
+
group.serviceTitle?.toLowerCase().includes(lowerSearch) ||
|
|
255
|
+
convo.serviceTitle?.toLowerCase().includes(lowerSearch)
|
|
256
|
+
);
|
|
257
|
+
return [serviceId, { ...group, conversations: filteredConvos }];
|
|
258
|
+
})
|
|
259
|
+
.filter(([_, group]) => group.conversations.length > 0)
|
|
260
|
+
);
|
|
470
261
|
|
|
471
|
-
|
|
472
|
-
|
|
262
|
+
// Improved empty state logic
|
|
263
|
+
const showPersonalTab = activeTab === "personal" && filteredPersonalChats.length > 0;
|
|
264
|
+
const showServiceTab = activeTab === "service" && Object.keys(filteredGroupedServiceChats).length > 0;
|
|
265
|
+
const isEmpty = !showPersonalTab && !showServiceTab;
|
|
266
|
+
|
|
473
267
|
|
|
474
|
-
//
|
|
475
|
-
const
|
|
268
|
+
// Calculate unread counts for tabs
|
|
269
|
+
const personalUnreadCount = conversations.personalChats.reduce(
|
|
270
|
+
(total, convo) => total + (convo.unreadMessageCount || 0),
|
|
271
|
+
0
|
|
272
|
+
);
|
|
476
273
|
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
estimateSize: (index) =>
|
|
481
|
-
rows[index]?.type === "service-header" ? HEADER_HEIGHT : ROW_HEIGHT,
|
|
482
|
-
overscan: 10,
|
|
483
|
-
});
|
|
274
|
+
const serviceUnreadCount = Object.values(conversations.groupedServiceChats)
|
|
275
|
+
.flatMap(group => group.conversations)
|
|
276
|
+
.reduce((total, convo) => total + (convo.unreadMessageCount || 0), 0);
|
|
484
277
|
|
|
485
|
-
// -------- UI --------
|
|
486
278
|
return (
|
|
487
|
-
<div className="
|
|
488
|
-
{/*
|
|
489
|
-
<div className="flex border-b">
|
|
279
|
+
<div className="chatSidebarConversations">
|
|
280
|
+
{/* Tab Navigation */}
|
|
281
|
+
<div className="flex border-b border-gray-200">
|
|
490
282
|
<button
|
|
491
|
-
className={`flex-1 py-3
|
|
283
|
+
className={`flex-1 py-3 px-4 text-sm font-medium text-center border-b-2 transition-colors ${
|
|
284
|
+
activeTab === "personal"
|
|
285
|
+
? "border-primary text-primary "
|
|
286
|
+
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
287
|
+
}`}
|
|
492
288
|
onClick={() => setActiveTab("personal")}
|
|
493
289
|
>
|
|
494
290
|
Personal
|
|
291
|
+
{personalUnreadCount > 0 && (
|
|
292
|
+
<span className="ml-2 bg-blue-primary text-white text-xs rounded-full px-2 py-1 min-w-5 inline-flex justify-center">
|
|
293
|
+
{personalUnreadCount}
|
|
294
|
+
</span>
|
|
295
|
+
)}
|
|
495
296
|
</button>
|
|
496
297
|
<button
|
|
497
|
-
className={`flex-1 py-3
|
|
298
|
+
className={`flex-1 py-3 px-4 text-sm font-medium text-center border-b-2 transition-colors ${
|
|
299
|
+
activeTab === "service"
|
|
300
|
+
? "border-primary text-primary "
|
|
301
|
+
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
302
|
+
}`}
|
|
498
303
|
onClick={() => setActiveTab("service")}
|
|
499
304
|
>
|
|
500
305
|
Service
|
|
306
|
+
{serviceUnreadCount > 0 && (
|
|
307
|
+
<span className="ml-2 bg-primary text-white text-xs rounded-full px-2 py-1 min-w-5 inline-flex justify-center">
|
|
308
|
+
{serviceUnreadCount}
|
|
309
|
+
</span>
|
|
310
|
+
)}
|
|
501
311
|
</button>
|
|
502
312
|
</div>
|
|
503
313
|
|
|
504
|
-
{/*
|
|
505
|
-
<div
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
314
|
+
{/* Tab Content */}
|
|
315
|
+
<div className="flex-1 overflow-auto">
|
|
316
|
+
{isEmpty ? (
|
|
317
|
+
<div className="flex flex-col items-center justify-center p-8 text-center">
|
|
318
|
+
<h3 className="text-xl font-semibold mb-1">No Conversations</h3>
|
|
319
|
+
<p className="text-gray-500 text-sm mb-4">
|
|
320
|
+
{activeTab === "personal"
|
|
321
|
+
? "You have no personal messages yet."
|
|
322
|
+
: "You have no service messages yet."}
|
|
323
|
+
</p>
|
|
324
|
+
</div>
|
|
325
|
+
) : (
|
|
326
|
+
<>
|
|
327
|
+
<div className="h-full overflow-auto">
|
|
328
|
+
{activeTab === "personal" && filteredPersonalChats.length > 0 && (
|
|
329
|
+
<VirtualizedChatList conversations={filteredPersonalChats} />
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
{activeTab === "service" &&
|
|
333
|
+
Object.entries(filteredGroupedServiceChats).length > 0 && (
|
|
334
|
+
<div className="p-2">
|
|
335
|
+
{Object.entries(filteredGroupedServiceChats).map(
|
|
336
|
+
([serviceId, { serviceTitle, conversations: serviceConvos }]) => (
|
|
337
|
+
<CollapsibleSection
|
|
338
|
+
key={serviceId}
|
|
339
|
+
title={serviceTitle}
|
|
340
|
+
defaultOpen={false}
|
|
341
|
+
>
|
|
342
|
+
<VirtualizedChatList conversations={serviceConvos} />
|
|
343
|
+
</CollapsibleSection>
|
|
344
|
+
)
|
|
533
345
|
)}
|
|
534
346
|
</div>
|
|
535
|
-
)
|
|
536
|
-
|
|
537
|
-
|
|
347
|
+
)}
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
</>
|
|
351
|
+
)}
|
|
538
352
|
</div>
|
|
539
353
|
</div>
|
|
540
354
|
);
|
|
541
|
-
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
export default Conversations;
|