@pubuduth-aplicy/chat-ui 2.1.91 → 2.1.93
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/common/VirtualizedChatList.tsx +39 -22
- package/src/components/messages/Message.tsx +53 -23
- package/src/components/messages/MessageInput.tsx +1 -1
- package/src/components/sidebar/Conversation.tsx +2 -2
- package/src/components/sidebar/Conversations.tsx +491 -300
package/package.json
CHANGED
|
@@ -23,33 +23,50 @@ const VirtualizedChatList = ({ conversations }: Props) => {
|
|
|
23
23
|
<div
|
|
24
24
|
ref={parentRef}
|
|
25
25
|
style={{
|
|
26
|
-
height:
|
|
26
|
+
height: `400px`,
|
|
27
27
|
width: "100%",
|
|
28
|
-
position: "relative",
|
|
28
|
+
// position: "relative",
|
|
29
29
|
overflow: "auto",
|
|
30
30
|
}}
|
|
31
31
|
>
|
|
32
|
-
{virtualizer.getVirtualItems().map((item) => {
|
|
32
|
+
{/* {virtualizer.getVirtualItems().map((item) => {
|
|
33
33
|
const conversation = conversations[item.index];
|
|
34
|
-
return (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
34
|
+
return ( */}
|
|
35
|
+
<div
|
|
36
|
+
// key={item.key}
|
|
37
|
+
style={{
|
|
38
|
+
position: "relative",
|
|
39
|
+
top: 0,
|
|
40
|
+
left: 0,
|
|
41
|
+
width: "100%",
|
|
42
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
43
|
+
// transform: `translateY(${item.start}px)`,
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{virtualizer.getVirtualItems().map((item) => {
|
|
47
|
+
const conversation = conversations[item.index];
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
key={item.key}
|
|
51
|
+
style={{
|
|
52
|
+
position: "absolute",
|
|
53
|
+
top: 0,
|
|
54
|
+
left: 0,
|
|
55
|
+
width: "100%",
|
|
56
|
+
height: `${item.size}px`,
|
|
57
|
+
transform: `translateY(${item.start}px)`,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<Conversation
|
|
61
|
+
conversation={conversation}
|
|
62
|
+
lastIdx={item.index === conversations.length - 1}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</div>
|
|
68
|
+
{/* ); */}
|
|
69
|
+
{/* })} */}
|
|
53
70
|
</div>
|
|
54
71
|
);
|
|
55
72
|
};
|
|
@@ -51,31 +51,31 @@ interface MessageProps {
|
|
|
51
51
|
|
|
52
52
|
const Message = ({ message }: MessageProps) => {
|
|
53
53
|
const { userId } = useChatContext();
|
|
54
|
-
const { apiUrl,cdnUrl
|
|
54
|
+
const { apiUrl, cdnUrl, role: role1 } = getChatConfig();
|
|
55
55
|
|
|
56
56
|
if (message.type === "system") {
|
|
57
57
|
const booking = message.meta?.bookingDetails;
|
|
58
58
|
const status = booking?.status || "Pending"; // Default to Pending
|
|
59
59
|
const role = role1; // "provider" or "customer"
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
const handleConfirm = () => {
|
|
62
62
|
console.log("Booking confirmed!");
|
|
63
63
|
window.location.href = `/booking/all`;
|
|
64
64
|
// Update booking status to Confirmed
|
|
65
65
|
};
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
const handleInProgress = () => {
|
|
68
68
|
console.log("Booking started (In Progress)");
|
|
69
69
|
window.location.href = `/booking/all`;
|
|
70
70
|
// Update booking status to InProgress
|
|
71
71
|
};
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
const handleComplete = () => {
|
|
74
74
|
console.log("Booking completed!");
|
|
75
75
|
window.location.href = `/booking/all`;
|
|
76
76
|
// Update booking status to Completed
|
|
77
77
|
};
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
const handleReview = () => {
|
|
80
80
|
if (role === "customer") {
|
|
81
81
|
console.log("Navigate to review page for customer → provider");
|
|
@@ -85,35 +85,35 @@ const Message = ({ message }: MessageProps) => {
|
|
|
85
85
|
window.location.href = `/booking/all`;
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
return (
|
|
90
90
|
<div className="system-message booking-details">
|
|
91
91
|
<h4>{status}</h4>
|
|
92
92
|
<div className="details">
|
|
93
93
|
<p>Service: {booking?.serviceId}</p>
|
|
94
|
-
<p>Date: {booking?.date.split(
|
|
94
|
+
<p>Date: {booking?.date.split("T")[0]}</p>
|
|
95
95
|
<p>Time: {booking?.time}</p>
|
|
96
96
|
</div>
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
{/* Actions based on role + status */}
|
|
99
99
|
{role === "provider" && status === "Pending" && (
|
|
100
100
|
<button className="confirm-button" onClick={handleConfirm}>
|
|
101
101
|
Confirm Booking ✅
|
|
102
102
|
</button>
|
|
103
103
|
)}
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
{role === "provider" && status === "Confirmed" && (
|
|
106
106
|
<button className="confirm-button" onClick={handleInProgress}>
|
|
107
107
|
Start Service ▶️
|
|
108
108
|
</button>
|
|
109
109
|
)}
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
{role === "provider" && status === "InProgress" && (
|
|
112
112
|
<button className="confirm-button" onClick={handleComplete}>
|
|
113
113
|
Mark as Completed ✔️
|
|
114
114
|
</button>
|
|
115
115
|
)}
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
{status === "Completed" && (
|
|
118
118
|
<button className="confirm-button" onClick={handleReview}>
|
|
119
119
|
Leave a Review ✍️
|
|
@@ -253,7 +253,6 @@ const Message = ({ message }: MessageProps) => {
|
|
|
253
253
|
}
|
|
254
254
|
};
|
|
255
255
|
|
|
256
|
-
|
|
257
256
|
const handleDownload = async (url: string, name: string, index: number) => {
|
|
258
257
|
setDownloadingIndex(index);
|
|
259
258
|
setDownloadProgress(0);
|
|
@@ -405,16 +404,37 @@ const Message = ({ message }: MessageProps) => {
|
|
|
405
404
|
src={`${cdnUrl}${media.url}`}
|
|
406
405
|
alt={media.name}
|
|
407
406
|
className="media-content"
|
|
408
|
-
onClick={() =>
|
|
407
|
+
onClick={() =>
|
|
408
|
+
window.open(`${cdnUrl}${media.url}`, "_blank")
|
|
409
|
+
}
|
|
409
410
|
/>
|
|
410
411
|
) : media.type === "video" ? (
|
|
411
|
-
|
|
412
|
-
<
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
412
|
+
<>
|
|
413
|
+
<div
|
|
414
|
+
className="video-file-preview"
|
|
415
|
+
onClick={() =>
|
|
416
|
+
window.open(`${cdnUrl}${media.url}`, "_blank")
|
|
417
|
+
}
|
|
418
|
+
>
|
|
419
|
+
<div className="file-icon video-icon">🎥</div>
|
|
420
|
+
|
|
421
|
+
<div className="file-info">
|
|
422
|
+
<span className="file-name">{media.name}</span>
|
|
423
|
+
<span className="file-size">
|
|
424
|
+
{(media.size / 1024 / 1024).toFixed(1)} MB
|
|
425
|
+
</span>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<div className="play-overlay">▶</div>
|
|
429
|
+
</div>
|
|
430
|
+
</>
|
|
417
431
|
) : (
|
|
432
|
+
// <video controls className="media-content" preload="metadata">
|
|
433
|
+
// <source
|
|
434
|
+
// src={`${cdnUrl}${media.url}`}
|
|
435
|
+
// type={`video/${`${cdnUrl}${media.url}`.split(".").pop()?.toLocaleLowerCase()}`}
|
|
436
|
+
// />
|
|
437
|
+
// </video>
|
|
418
438
|
<div className="document-preview">
|
|
419
439
|
<div className="file-icon">
|
|
420
440
|
{media.type === "document" && "📄"}
|
|
@@ -434,7 +454,11 @@ const Message = ({ message }: MessageProps) => {
|
|
|
434
454
|
if (downloadingIndex === index) {
|
|
435
455
|
cancelDownload();
|
|
436
456
|
} else {
|
|
437
|
-
handleDownload(
|
|
457
|
+
handleDownload(
|
|
458
|
+
`${cdnUrl}/${media.url}`,
|
|
459
|
+
media.name,
|
|
460
|
+
index
|
|
461
|
+
);
|
|
438
462
|
}
|
|
439
463
|
}}
|
|
440
464
|
title={downloadingIndex === index ? "Cancel" : "Download"}
|
|
@@ -491,12 +515,14 @@ const Message = ({ message }: MessageProps) => {
|
|
|
491
515
|
);
|
|
492
516
|
} else {
|
|
493
517
|
setIsEditingMode(false);
|
|
518
|
+
setShowOptions(false);
|
|
494
519
|
}
|
|
495
520
|
};
|
|
496
521
|
|
|
497
522
|
const handleCancelEdit = () => {
|
|
498
523
|
setEditedMessage(message.message);
|
|
499
524
|
setIsEditingMode(false);
|
|
525
|
+
setShowOptions(false);
|
|
500
526
|
};
|
|
501
527
|
|
|
502
528
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
@@ -534,7 +560,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
534
560
|
return now.getTime() - messageDate.getTime() > oneDayInMs;
|
|
535
561
|
};
|
|
536
562
|
|
|
537
|
-
|
|
563
|
+
const hasMedia = message.media && message.media.length > 0;
|
|
538
564
|
|
|
539
565
|
return (
|
|
540
566
|
<div className="chat-container">
|
|
@@ -548,7 +574,9 @@ const Message = ({ message }: MessageProps) => {
|
|
|
548
574
|
>
|
|
549
575
|
{message.status === "deleted" ? (
|
|
550
576
|
<div className="chat-bubble compact-bubble deleted-message">
|
|
551
|
-
<div className="message-text text-gray-900 dark:text-gray-100">
|
|
577
|
+
<div className="message-text text-gray-900 dark:text-gray-100">
|
|
578
|
+
This message was deleted
|
|
579
|
+
</div>
|
|
552
580
|
</div>
|
|
553
581
|
) : (
|
|
554
582
|
(message.message ||
|
|
@@ -579,7 +607,9 @@ const Message = ({ message }: MessageProps) => {
|
|
|
579
607
|
</div>
|
|
580
608
|
) : (
|
|
581
609
|
message.message && (
|
|
582
|
-
<div className="message-text text-gray-900 dark:text-gray-100">
|
|
610
|
+
<div className="message-text text-gray-900 dark:text-gray-100">
|
|
611
|
+
{message.message}
|
|
612
|
+
</div>
|
|
583
613
|
)
|
|
584
614
|
)}
|
|
585
615
|
|
|
@@ -194,8 +194,8 @@ const MessageInput = () => {
|
|
|
194
194
|
};
|
|
195
195
|
|
|
196
196
|
const getFileType = (mimeType: string): FileType | null => {
|
|
197
|
-
if (ACCEPTED_IMAGE_TYPES.includes(mimeType)) return "image";
|
|
198
197
|
if (ACCEPTED_VIDEO_TYPES.includes(mimeType)) return "video";
|
|
198
|
+
if (ACCEPTED_IMAGE_TYPES.includes(mimeType)) return "image";
|
|
199
199
|
if (ACCEPTED_DOCUMENT_TYPES.includes(mimeType)) return "document";
|
|
200
200
|
return null;
|
|
201
201
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { MessageCircle } from "lucide-react";
|
|
3
|
-
import { useEffect } from "react";
|
|
3
|
+
import { memo, useEffect } from "react";
|
|
4
4
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
5
5
|
import useChatUIStore from "../../stores/Zustant";
|
|
6
6
|
import { ConversationProps } from "../../types/type";
|
|
@@ -143,4 +143,4 @@ const Conversation = ({ conversation }: ConversationProps) => {
|
|
|
143
143
|
);
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
-
export default Conversation;
|
|
146
|
+
export default memo(Conversation);
|
|
@@ -1,350 +1,541 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
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";
|
|
3
365
|
import { useGetConversations } from "../../hooks/queries/useChatApi";
|
|
4
366
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
5
|
-
import {
|
|
6
|
-
Conversation as ConversationType,
|
|
7
|
-
ParticipantGroup,
|
|
8
|
-
} from "../../types/type";
|
|
9
|
-
import CollapsibleSection from "../common/CollapsibleSection";
|
|
10
367
|
import useChatUIStore from "../../stores/Zustant";
|
|
11
|
-
import
|
|
368
|
+
import Conversation from "../sidebar/Conversation";
|
|
369
|
+
import { Conversation as ConversationType, ParticipantGroup } from "../../types/type";
|
|
12
370
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
18
|
-
};
|
|
371
|
+
// ---------- Row model ----------
|
|
372
|
+
type Row =
|
|
373
|
+
| { type: "service-header"; serviceId: string; title: string }
|
|
374
|
+
| { type: "conversation"; conversation: ConversationType };
|
|
19
375
|
|
|
20
376
|
type TabType = "personal" | "service";
|
|
21
377
|
|
|
22
|
-
const
|
|
378
|
+
const HEADER_HEIGHT = 36;
|
|
379
|
+
const ROW_HEIGHT = 60;
|
|
380
|
+
|
|
381
|
+
export default function Conversations() {
|
|
23
382
|
const { userId, socket } = useChatContext();
|
|
24
383
|
const { data: participantGroups } = useGetConversations(userId);
|
|
25
384
|
const { searchTerm } = useChatUIStore();
|
|
26
|
-
const [activeTab, setActiveTab] = useState<TabType>("personal");
|
|
27
|
-
const [conversations, setConversations] = useState<{
|
|
28
|
-
personalChats: ConversationType[];
|
|
29
|
-
groupedServiceChats: GroupedServiceChats;
|
|
30
|
-
}>({ personalChats: [], groupedServiceChats: {} });
|
|
31
|
-
|
|
32
|
-
// Process fetched data
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (!participantGroups) return;
|
|
35
|
-
const processConversations = (groups: ParticipantGroup[]) => {
|
|
36
|
-
const allPersonalChats: ConversationType[] = [];
|
|
37
|
-
const allServiceChatsMap = new Map<string, GroupedServiceChats[string]>();
|
|
38
|
-
|
|
39
|
-
groups.forEach((group, groupIndex) => {
|
|
40
|
-
|
|
41
|
-
if (group.personalConversation) {
|
|
42
|
-
allPersonalChats.push(group.personalConversation);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
group.serviceConversations.forEach((serviceGroup, serviceIndex) => {
|
|
46
|
-
if (!allServiceChatsMap.has(serviceGroup.serviceId)) {
|
|
47
|
-
allServiceChatsMap.set(serviceGroup.serviceId, {
|
|
48
|
-
serviceTitle: serviceGroup.serviceTitle,
|
|
49
|
-
conversations: [],
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const existing = allServiceChatsMap.get(serviceGroup.serviceId)!;
|
|
54
|
-
existing.conversations.push(...serviceGroup.conversations);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
385
|
|
|
386
|
+
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
|
+
>({});
|
|
58
391
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
new Date(b.lastMessage?.createdAt || b.updatedAt).getTime() -
|
|
63
|
-
new Date(a.lastMessage?.createdAt || a.updatedAt).getTime()
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
sortConversations(allPersonalChats);
|
|
67
|
-
allServiceChatsMap.forEach((value, key) => {
|
|
68
|
-
sortConversations(value.conversations);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const result = {
|
|
72
|
-
personalChats: allPersonalChats,
|
|
73
|
-
groupedServiceChats: Object.fromEntries(allServiceChatsMap),
|
|
74
|
-
};
|
|
392
|
+
// -------- Normalize API data (once) --------
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
if (!participantGroups) return;
|
|
75
395
|
|
|
76
|
-
|
|
77
|
-
|
|
396
|
+
const personals: ConversationType[] = [];
|
|
397
|
+
const services: Record<string, { serviceTitle: string; conversations: ConversationType[] }> = {};
|
|
78
398
|
|
|
79
|
-
|
|
80
|
-
|
|
399
|
+
participantGroups.forEach((group: ParticipantGroup) => {
|
|
400
|
+
if (group.personalConversation) personals.push(group.personalConversation);
|
|
81
401
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
messageIds: string[];
|
|
88
|
-
chatId: string;
|
|
89
|
-
}) => {
|
|
90
|
-
const { chatId, messageIds } = data;
|
|
91
|
-
|
|
92
|
-
setConversations((prev) => {
|
|
93
|
-
const personalChats = [...prev.personalChats];
|
|
94
|
-
const groupedServiceChats = { ...prev.groupedServiceChats };
|
|
95
|
-
|
|
96
|
-
const updateRead = (convo: ConversationType) => {
|
|
97
|
-
if (convo._id !== chatId) return convo;
|
|
98
|
-
const updatedUnread = (convo.unreadMessageIds || []).filter(
|
|
99
|
-
(id) => !messageIds.includes(id)
|
|
100
|
-
);
|
|
101
|
-
return {
|
|
102
|
-
...convo,
|
|
103
|
-
unreadMessageIds: updatedUnread,
|
|
104
|
-
unreadMessageCount: updatedUnread.length,
|
|
402
|
+
group.serviceConversations.forEach((sg) => {
|
|
403
|
+
if (!services[sg.serviceId]) {
|
|
404
|
+
services[sg.serviceId] = {
|
|
405
|
+
serviceTitle: sg.serviceTitle,
|
|
406
|
+
conversations: [],
|
|
105
407
|
};
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const personalIndex = personalChats.findIndex((c) => c._id === chatId);
|
|
109
|
-
if (personalIndex >= 0) {
|
|
110
|
-
personalChats[personalIndex] = updateRead(personalChats[personalIndex]);
|
|
111
|
-
return { personalChats, groupedServiceChats };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
for (const serviceId in groupedServiceChats) {
|
|
115
|
-
const convos = groupedServiceChats[serviceId].conversations;
|
|
116
|
-
const convoIndex = convos.findIndex((c) => c._id === chatId);
|
|
117
|
-
if (convoIndex >= 0) {
|
|
118
|
-
const updatedConvos = [...convos];
|
|
119
|
-
updatedConvos[convoIndex] = updateRead(updatedConvos[convoIndex]);
|
|
120
|
-
groupedServiceChats[serviceId] = {
|
|
121
|
-
...groupedServiceChats[serviceId],
|
|
122
|
-
conversations: updatedConvos,
|
|
123
|
-
};
|
|
124
|
-
return { personalChats, groupedServiceChats };
|
|
125
|
-
}
|
|
126
408
|
}
|
|
127
|
-
|
|
128
|
-
return prev;
|
|
409
|
+
services[sg.serviceId].conversations.push(...sg.conversations);
|
|
129
410
|
});
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const handleNewMessage = (newMessage: any) => {
|
|
133
|
-
if (!newMessage?.conversationId) return;
|
|
134
|
-
|
|
135
|
-
setConversations((prev) => {
|
|
136
|
-
const personalChats = [...prev.personalChats];
|
|
137
|
-
const groupedServiceChats = { ...prev.groupedServiceChats };
|
|
138
|
-
|
|
139
|
-
const updateConversation = (convo: ConversationType) => ({
|
|
140
|
-
...convo,
|
|
141
|
-
lastMessage: newMessage,
|
|
142
|
-
updatedAt: new Date().toISOString(),
|
|
143
|
-
unreadMessageIds:
|
|
144
|
-
userId !== newMessage.senderId
|
|
145
|
-
? [...(convo.unreadMessageIds || []), newMessage._id]
|
|
146
|
-
: convo.unreadMessageIds || [],
|
|
147
|
-
unreadMessageCount:
|
|
148
|
-
userId !== newMessage.senderId
|
|
149
|
-
? (convo.unreadMessageCount || 0) + 1
|
|
150
|
-
: convo.unreadMessageCount || 0,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const personalIndex = personalChats.findIndex(
|
|
154
|
-
(c) => c._id === newMessage.conversationId
|
|
155
|
-
);
|
|
156
|
-
if (personalIndex >= 0) {
|
|
157
|
-
personalChats[personalIndex] = updateConversation(
|
|
158
|
-
personalChats[personalIndex]
|
|
159
|
-
);
|
|
160
|
-
return { personalChats, groupedServiceChats };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
for (const serviceId in groupedServiceChats) {
|
|
164
|
-
const serviceIndex = groupedServiceChats[
|
|
165
|
-
serviceId
|
|
166
|
-
].conversations.findIndex((c) => c._id === newMessage.conversationId);
|
|
167
|
-
if (serviceIndex >= 0) {
|
|
168
|
-
const updatedConversations = [
|
|
169
|
-
...groupedServiceChats[serviceId].conversations,
|
|
170
|
-
];
|
|
171
|
-
updatedConversations[serviceIndex] = updateConversation(
|
|
172
|
-
updatedConversations[serviceIndex]
|
|
173
|
-
);
|
|
174
|
-
groupedServiceChats[serviceId] = {
|
|
175
|
-
...groupedServiceChats[serviceId],
|
|
176
|
-
conversations: updatedConversations,
|
|
177
|
-
};
|
|
178
|
-
return { personalChats, groupedServiceChats };
|
|
179
|
-
}
|
|
180
|
-
}
|
|
411
|
+
});
|
|
181
412
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
lastMessage: newMessage,
|
|
186
|
-
updatedAt: new Date().toISOString(),
|
|
187
|
-
unreadMessageIds:
|
|
188
|
-
userId !== newMessage.senderId ? [newMessage._id] : [],
|
|
189
|
-
unreadMessageCount: userId !== newMessage.senderId ? 1 : 0,
|
|
190
|
-
type: "personal",
|
|
191
|
-
readReceipts: [],
|
|
192
|
-
createdAt: new Date().toISOString(),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
return { personalChats, groupedServiceChats };
|
|
196
|
-
});
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const messageListener = (event: MessageEvent) => {
|
|
200
|
-
try {
|
|
201
|
-
const data = JSON.parse(event.data);
|
|
202
|
-
if (data.event === "newMessage") {
|
|
203
|
-
handleNewMessage(data.data);
|
|
204
|
-
} else if (
|
|
205
|
-
data.event === "messageStatusUpdated" &&
|
|
206
|
-
data.data.status === "read"
|
|
207
|
-
) {
|
|
208
|
-
handleMessageReadAck({
|
|
209
|
-
messageIds: Array.isArray(data.data.messageId)
|
|
210
|
-
? data.data.messageId
|
|
211
|
-
: [data.data.messageId],
|
|
212
|
-
chatId: data.data.chatId,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
} catch (e) {
|
|
216
|
-
console.error("Error parsing socket message:", e);
|
|
217
|
-
}
|
|
218
|
-
};
|
|
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();
|
|
219
416
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}, [socket, userId]);
|
|
417
|
+
personals.sort(sortFn);
|
|
418
|
+
Object.values(services).forEach((g) => g.conversations.sort(sortFn));
|
|
223
419
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// : Object.keys(conversations.groupedServiceChats).length === 0;
|
|
420
|
+
setPersonalChats(personals);
|
|
421
|
+
setServiceChats(services);
|
|
422
|
+
}, [participantGroups]);
|
|
228
423
|
|
|
424
|
+
// -------- Search filtering --------
|
|
229
425
|
const lowerSearch = searchTerm?.toLowerCase().trim();
|
|
230
426
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
convo.participantDetails?.some((p: any) =>
|
|
427
|
+
const filteredPersonal = useMemo(() => {
|
|
428
|
+
if (!lowerSearch) return personalChats;
|
|
429
|
+
return personalChats.filter((c) =>
|
|
430
|
+
c.participantDetails?.some((p: any) =>
|
|
236
431
|
p?.name?.toLowerCase().includes(lowerSearch)
|
|
237
432
|
)
|
|
238
433
|
);
|
|
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]);
|
|
239
451
|
|
|
240
|
-
//
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
: Object.fromEntries(
|
|
244
|
-
Object.entries(conversations.groupedServiceChats)
|
|
245
|
-
.map(([serviceId, group]) => {
|
|
246
|
-
const filteredConvos = group.conversations.filter((convo) =>
|
|
247
|
-
convo.participantDetails?.some((p: any) =>
|
|
248
|
-
p?.name?.toLowerCase().includes(lowerSearch)
|
|
249
|
-
) ||
|
|
250
|
-
group.serviceTitle?.toLowerCase().includes(lowerSearch) ||
|
|
251
|
-
convo.serviceTitle?.toLowerCase().includes(lowerSearch)
|
|
252
|
-
);
|
|
253
|
-
return [serviceId, { ...group, conversations: filteredConvos }];
|
|
254
|
-
})
|
|
255
|
-
.filter(([_, group]) => group.conversations.length > 0)
|
|
256
|
-
);
|
|
452
|
+
// -------- Flatten into ONE list --------
|
|
453
|
+
const rows: Row[] = useMemo(() => {
|
|
454
|
+
const out: Row[] = [];
|
|
257
455
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
456
|
+
if (activeTab === "personal") {
|
|
457
|
+
filteredPersonal.forEach((c) =>
|
|
458
|
+
out.push({ type: "conversation", conversation: c })
|
|
459
|
+
);
|
|
460
|
+
}
|
|
263
461
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
}
|
|
269
470
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
.reduce((total, convo) => total + (convo.unreadMessageCount || 0), 0);
|
|
471
|
+
return out;
|
|
472
|
+
}, [activeTab, filteredPersonal, filteredService]);
|
|
273
473
|
|
|
474
|
+
// -------- Virtualizer --------
|
|
475
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
476
|
+
|
|
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
|
+
});
|
|
484
|
+
|
|
485
|
+
// -------- UI --------
|
|
274
486
|
return (
|
|
275
|
-
<div className="
|
|
276
|
-
{/*
|
|
277
|
-
<div className="flex border-b
|
|
487
|
+
<div className="flex flex-col h-full">
|
|
488
|
+
{/* Tabs */}
|
|
489
|
+
<div className="flex border-b">
|
|
278
490
|
<button
|
|
279
|
-
className={`flex-1 py-3
|
|
280
|
-
activeTab === "personal"
|
|
281
|
-
? "border-primary text-primary "
|
|
282
|
-
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
283
|
-
}`}
|
|
491
|
+
className={`flex-1 py-3 ${activeTab === "personal" ? "border-b-2 border-primary" : ""}`}
|
|
284
492
|
onClick={() => setActiveTab("personal")}
|
|
285
493
|
>
|
|
286
494
|
Personal
|
|
287
|
-
{personalUnreadCount > 0 && (
|
|
288
|
-
<span className="ml-2 bg-blue-primary text-white text-xs rounded-full px-2 py-1 min-w-5 inline-flex justify-center">
|
|
289
|
-
{personalUnreadCount}
|
|
290
|
-
</span>
|
|
291
|
-
)}
|
|
292
495
|
</button>
|
|
293
496
|
<button
|
|
294
|
-
className={`flex-1 py-3
|
|
295
|
-
activeTab === "service"
|
|
296
|
-
? "border-primary text-primary "
|
|
297
|
-
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
298
|
-
}`}
|
|
497
|
+
className={`flex-1 py-3 ${activeTab === "service" ? "border-b-2 border-primary" : ""}`}
|
|
299
498
|
onClick={() => setActiveTab("service")}
|
|
300
499
|
>
|
|
301
500
|
Service
|
|
302
|
-
{serviceUnreadCount > 0 && (
|
|
303
|
-
<span className="ml-2 bg-primary text-white text-xs rounded-full px-2 py-1 min-w-5 inline-flex justify-center">
|
|
304
|
-
{serviceUnreadCount}
|
|
305
|
-
</span>
|
|
306
|
-
)}
|
|
307
501
|
</button>
|
|
308
502
|
</div>
|
|
309
503
|
|
|
310
|
-
{/*
|
|
311
|
-
<div className="flex-1 overflow-auto">
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
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} />
|
|
340
533
|
)}
|
|
341
534
|
</div>
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
|
|
535
|
+
);
|
|
536
|
+
})}
|
|
537
|
+
</div>
|
|
345
538
|
</div>
|
|
346
539
|
</div>
|
|
347
540
|
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
export default Conversations;
|
|
541
|
+
}
|