@pubuduth-aplicy/chat-ui 2.1.92 → 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 +1 -1
- 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
|
};
|
|
@@ -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
|
+
}
|