@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubuduth-aplicy/chat-ui",
3
- "version": "2.1.93",
3
+ "version": "2.1.94",
4
4
  "description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -1,541 +1,357 @@
1
- // /* eslint-disable @typescript-eslint/no-explicit-any */
2
- // import { useEffect, useState } from "react";
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 Conversation from "../sidebar/Conversation";
369
- import { Conversation as ConversationType, ParticipantGroup } from "../../types/type";
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 HEADER_HEIGHT = 36;
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 [personalChats, setPersonalChats] = useState<ConversationType[]>([]);
388
- const [serviceChats, setServiceChats] = useState<
389
- Record<string, { serviceTitle: string; conversations: ConversationType[] }>
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 personals: ConversationType[] = [];
397
- const services: Record<string, { serviceTitle: string; conversations: ConversationType[] }> = {};
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
- participantGroups.forEach((group: ParticipantGroup) => {
400
- if (group.personalConversation) personals.push(group.personalConversation);
75
+ const result = {
76
+ personalChats: allPersonalChats,
77
+ groupedServiceChats: Object.fromEntries(allServiceChatsMap),
78
+ };
401
79
 
402
- group.serviceConversations.forEach((sg) => {
403
- if (!services[sg.serviceId]) {
404
- services[sg.serviceId] = {
405
- serviceTitle: sg.serviceTitle,
406
- conversations: [],
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
- services[sg.serviceId].conversations.push(...sg.conversations);
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
- const sortFn = (a: ConversationType, b: ConversationType) =>
414
- new Date(b.lastMessage?.createdAt || b.updatedAt).getTime() -
415
- new Date(a.lastMessage?.createdAt || a.updatedAt).getTime();
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
- personals.sort(sortFn);
418
- Object.values(services).forEach((g) => g.conversations.sort(sortFn));
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
- setPersonalChats(personals);
421
- setServiceChats(services);
422
- }, [participantGroups]);
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
- const filteredPersonal = useMemo(() => {
428
- if (!lowerSearch) return personalChats;
429
- return personalChats.filter((c) =>
430
- c.participantDetails?.some((p: any) =>
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
- if (activeTab === "service") {
463
- Object.entries(filteredService).forEach(([sid, group]) => {
464
- out.push({ type: "service-header", serviceId: sid, title: group.serviceTitle });
465
- group.conversations.forEach((c) =>
466
- out.push({ type: "conversation", conversation: c })
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
- return out;
472
- }, [activeTab, filteredPersonal, filteredService]);
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
- // -------- Virtualizer --------
475
- const parentRef = useRef<HTMLDivElement>(null);
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 virtualizer = useVirtualizer({
478
- count: rows.length,
479
- getScrollElement: () => parentRef.current,
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="flex flex-col h-full">
488
- {/* Tabs */}
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 ${activeTab === "personal" ? "border-b-2 border-primary" : ""}`}
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 ${activeTab === "service" ? "border-b-2 border-primary" : ""}`}
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
- {/* Single scroll container */}
505
- <div ref={parentRef} className="flex-1 overflow-auto">
506
- <div
507
- style={{
508
- height: virtualizer.getTotalSize(),
509
- position: "relative",
510
- }}
511
- >
512
- {virtualizer.getVirtualItems().map((item) => {
513
- const row = rows[item.index];
514
- if (!row) return null;
515
-
516
- return (
517
- <div
518
- key={item.key}
519
- style={{
520
- position: "absolute",
521
- top: 0,
522
- left: 0,
523
- width: "100%",
524
- transform: `translateY(${item.start}px)`,
525
- }}
526
- >
527
- {row.type === "service-header" ? (
528
- <div className="px-3 py-2 text-xs font-semibold uppercase bg-gray-100 dark:bg-gray-800">
529
- {row.title}
530
- </div>
531
- ) : (
532
- <Conversation conversation={row.conversation} />
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
- </div>
347
+ )}
348
+ </div>
349
+
350
+ </>
351
+ )}
538
352
  </div>
539
353
  </div>
540
354
  );
541
- }
355
+ };
356
+
357
+ export default Conversations;