@pubuduth-aplicy/chat-ui 2.1.71 → 2.1.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/components/Chat.tsx +30 -17
- package/src/components/common/CollapsibleSection.tsx +40 -0
- package/src/components/common/VirtualizedChatList.tsx +57 -0
- package/src/components/messages/Message.tsx +46 -45
- package/src/components/messages/MessageContainer.tsx +116 -59
- package/src/components/messages/MessageInput.tsx +59 -18
- package/src/components/messages/Messages.tsx +97 -45
- package/src/components/sidebar/Conversation.tsx +113 -100
- package/src/components/sidebar/Conversations.tsx +270 -35
- package/src/components/sidebar/SearchInput.tsx +16 -54
- package/src/hooks/useMessageStatus.ts +97 -0
- package/src/providers/ChatProvider.tsx +46 -26
- package/src/service/messageService.ts +86 -61
- package/src/stores/Zustant.ts +28 -21
- package/src/style/style.css +200 -24
- package/src/types/type.ts +25 -28
|
@@ -1,51 +1,286 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
3
|
import { useGetConversations } from "../../hooks/queries/useChatApi";
|
|
4
4
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
Conversation as ConversationType,
|
|
7
|
+
ParticipantGroup,
|
|
8
|
+
} from "../../types/type";
|
|
9
|
+
import CollapsibleSection from "../common/CollapsibleSection";
|
|
10
|
+
import useChatUIStore from "../../stores/Zustant";
|
|
11
|
+
import VirtualizedChatList from "../common/VirtualizedChatList";
|
|
12
|
+
|
|
13
|
+
type GroupedServiceChats = {
|
|
14
|
+
[serviceId: string]: {
|
|
15
|
+
serviceTitle: string;
|
|
16
|
+
conversations: ConversationType[];
|
|
17
|
+
};
|
|
18
|
+
};
|
|
6
19
|
|
|
7
20
|
const Conversations = () => {
|
|
8
|
-
const { userId } = useChatContext();
|
|
9
|
-
const { data:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
const { userId, socket } = useChatContext();
|
|
22
|
+
const { data: participantGroups } = useGetConversations(userId);
|
|
23
|
+
const { searchTerm } = useChatUIStore();
|
|
24
|
+
const [conversations, setConversations] = useState<{
|
|
25
|
+
generalChats: ConversationType[];
|
|
26
|
+
groupedServiceChats: GroupedServiceChats;
|
|
27
|
+
}>({ generalChats: [], groupedServiceChats: {} });
|
|
28
|
+
|
|
29
|
+
// Process fetched data
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!participantGroups) return;
|
|
32
|
+
|
|
33
|
+
const processConversations = (groups: ParticipantGroup[]) => {
|
|
34
|
+
const allGeneralChats: ConversationType[] = [];
|
|
35
|
+
const allServiceChatsMap = new Map<string, GroupedServiceChats[string]>();
|
|
36
|
+
|
|
37
|
+
groups.forEach((group) => {
|
|
38
|
+
if (group.personalConversation) {
|
|
39
|
+
allGeneralChats.push(group.personalConversation);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
group.serviceConversations.forEach((serviceGroup) => {
|
|
43
|
+
if (!allServiceChatsMap.has(serviceGroup.serviceId)) {
|
|
44
|
+
allServiceChatsMap.set(serviceGroup.serviceId, {
|
|
45
|
+
serviceTitle: serviceGroup.serviceTitle,
|
|
46
|
+
conversations: [],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
allServiceChatsMap
|
|
50
|
+
.get(serviceGroup.serviceId)!
|
|
51
|
+
.conversations.push(...serviceGroup.conversations);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const sortConversations = (convos: ConversationType[]) =>
|
|
56
|
+
convos.sort(
|
|
57
|
+
(a, b) =>
|
|
58
|
+
new Date(b.lastMessage?.createdAt || b.updatedAt).getTime() -
|
|
59
|
+
new Date(a.lastMessage?.createdAt || a.updatedAt).getTime()
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
sortConversations(allGeneralChats);
|
|
63
|
+
allServiceChatsMap.forEach((value) =>
|
|
64
|
+
sortConversations(value.conversations)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
generalChats: allGeneralChats,
|
|
69
|
+
groupedServiceChats: Object.fromEntries(allServiceChatsMap),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
setConversations(processConversations(participantGroups));
|
|
74
|
+
}, [participantGroups]);
|
|
75
|
+
|
|
76
|
+
// Real-time update listeners
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!socket) return;
|
|
79
|
+
|
|
80
|
+
const handleMessageReadAck = (data: {
|
|
81
|
+
messageIds: string[];
|
|
82
|
+
chatId: string;
|
|
83
|
+
}) => {
|
|
84
|
+
const { chatId, messageIds } = data;
|
|
85
|
+
|
|
86
|
+
setConversations((prev) => {
|
|
87
|
+
const generalChats = [...prev.generalChats];
|
|
88
|
+
const groupedServiceChats = { ...prev.groupedServiceChats };
|
|
89
|
+
|
|
90
|
+
const updateRead = (convo: ConversationType) => {
|
|
91
|
+
if (convo._id !== chatId) return convo;
|
|
92
|
+
const updatedUnread = (convo.unreadMessageIds || []).filter(
|
|
93
|
+
(id) => !messageIds.includes(id)
|
|
94
|
+
);
|
|
95
|
+
return {
|
|
96
|
+
...convo,
|
|
97
|
+
unreadMessageIds: updatedUnread,
|
|
98
|
+
unreadMessageCount: updatedUnread.length,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const generalIndex = generalChats.findIndex((c) => c._id === chatId);
|
|
103
|
+
if (generalIndex >= 0) {
|
|
104
|
+
generalChats[generalIndex] = updateRead(generalChats[generalIndex]);
|
|
105
|
+
return { generalChats, groupedServiceChats };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const serviceId in groupedServiceChats) {
|
|
109
|
+
const convos = groupedServiceChats[serviceId].conversations;
|
|
110
|
+
const convoIndex = convos.findIndex((c) => c._id === chatId);
|
|
111
|
+
if (convoIndex >= 0) {
|
|
112
|
+
const updatedConvos = [...convos];
|
|
113
|
+
updatedConvos[convoIndex] = updateRead(updatedConvos[convoIndex]);
|
|
114
|
+
groupedServiceChats[serviceId] = {
|
|
115
|
+
...groupedServiceChats[serviceId],
|
|
116
|
+
conversations: updatedConvos,
|
|
117
|
+
};
|
|
118
|
+
return { generalChats, groupedServiceChats };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return prev;
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleNewMessage = (newMessage: any) => {
|
|
127
|
+
if (!newMessage?.conversationId) return;
|
|
128
|
+
|
|
129
|
+
setConversations((prev) => {
|
|
130
|
+
const generalChats = [...prev.generalChats];
|
|
131
|
+
const groupedServiceChats = { ...prev.groupedServiceChats };
|
|
132
|
+
|
|
133
|
+
const updateConversation = (convo: ConversationType) => ({
|
|
134
|
+
...convo,
|
|
135
|
+
lastMessage: newMessage,
|
|
136
|
+
updatedAt: new Date().toISOString(),
|
|
137
|
+
unreadMessageIds:
|
|
138
|
+
userId !== newMessage.senderId
|
|
139
|
+
? [...(convo.unreadMessageIds || []), newMessage._id]
|
|
140
|
+
: convo.unreadMessageIds || [],
|
|
141
|
+
unreadMessageCount:
|
|
142
|
+
userId !== newMessage.senderId
|
|
143
|
+
? (convo.unreadMessageCount || 0) + 1
|
|
144
|
+
: convo.unreadMessageCount || 0,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const generalIndex = generalChats.findIndex(
|
|
148
|
+
(c) => c._id === newMessage.conversationId
|
|
149
|
+
);
|
|
150
|
+
if (generalIndex >= 0) {
|
|
151
|
+
generalChats[generalIndex] = updateConversation(
|
|
152
|
+
generalChats[generalIndex]
|
|
153
|
+
);
|
|
154
|
+
return { generalChats, groupedServiceChats };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const serviceId in groupedServiceChats) {
|
|
158
|
+
const serviceIndex = groupedServiceChats[
|
|
159
|
+
serviceId
|
|
160
|
+
].conversations.findIndex((c) => c._id === newMessage.conversationId);
|
|
161
|
+
if (serviceIndex >= 0) {
|
|
162
|
+
const updatedConversations = [
|
|
163
|
+
...groupedServiceChats[serviceId].conversations,
|
|
164
|
+
];
|
|
165
|
+
updatedConversations[serviceIndex] = updateConversation(
|
|
166
|
+
updatedConversations[serviceIndex]
|
|
167
|
+
);
|
|
168
|
+
groupedServiceChats[serviceId] = {
|
|
169
|
+
...groupedServiceChats[serviceId],
|
|
170
|
+
conversations: updatedConversations,
|
|
171
|
+
};
|
|
172
|
+
return { generalChats, groupedServiceChats };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
generalChats.push({
|
|
177
|
+
_id: newMessage.conversationId,
|
|
178
|
+
participants: [newMessage.senderId, newMessage.receiverId],
|
|
179
|
+
lastMessage: newMessage,
|
|
180
|
+
updatedAt: new Date().toISOString(),
|
|
181
|
+
unreadMessageIds:
|
|
182
|
+
userId !== newMessage.senderId ? [newMessage._id] : [],
|
|
183
|
+
unreadMessageCount: userId !== newMessage.senderId ? 1 : 0,
|
|
184
|
+
type: "personal",
|
|
185
|
+
readReceipts: [],
|
|
186
|
+
createdAt: new Date().toISOString(),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return { generalChats, groupedServiceChats };
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const messageListener = (event: MessageEvent) => {
|
|
194
|
+
try {
|
|
195
|
+
const data = JSON.parse(event.data);
|
|
196
|
+
if (data.event === "newMessage") {
|
|
197
|
+
handleNewMessage(data.data);
|
|
198
|
+
} else if (
|
|
199
|
+
data.event === "messageStatusUpdated" &&
|
|
200
|
+
data.data.status === "read"
|
|
201
|
+
) {
|
|
202
|
+
handleMessageReadAck({
|
|
203
|
+
messageIds: Array.isArray(data.data.messageId)
|
|
204
|
+
? data.data.messageId
|
|
205
|
+
: [data.data.messageId],
|
|
206
|
+
chatId: data.data.chatId,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.error("Error parsing socket message:", e);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
socket.addEventListener("message", messageListener);
|
|
215
|
+
return () => socket.removeEventListener("message", messageListener);
|
|
216
|
+
}, [socket, userId]);
|
|
217
|
+
|
|
218
|
+
const isEmpty =
|
|
219
|
+
conversations.generalChats.length === 0 &&
|
|
220
|
+
Object.keys(conversations.groupedServiceChats).length === 0;
|
|
221
|
+
|
|
222
|
+
const lowerSearch = searchTerm?.toLowerCase().trim();
|
|
223
|
+
|
|
224
|
+
const filteredGeneralChats = conversations.generalChats.filter((convo) =>
|
|
225
|
+
convo.participantDetails?.some((p:any) =>
|
|
226
|
+
typeof p === "string"
|
|
227
|
+
? p.toLowerCase().includes(lowerSearch)
|
|
228
|
+
: p.firstname?.toLowerCase().includes(lowerSearch)
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const filteredGroupedServiceChats: GroupedServiceChats = Object.fromEntries(
|
|
233
|
+
Object.entries(conversations.groupedServiceChats)
|
|
234
|
+
.map(([serviceId, group]) => {
|
|
235
|
+
const filteredConvos = group.conversations.filter((convo) =>
|
|
236
|
+
convo.participants?.some((p:any) =>
|
|
237
|
+
typeof p === "string"
|
|
238
|
+
? p.toLowerCase().includes(lowerSearch)
|
|
239
|
+
: p.name?.toLowerCase().includes(lowerSearch)
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
return [serviceId, { ...group, conversations: filteredConvos }];
|
|
243
|
+
})
|
|
244
|
+
.filter(([_, group]) => group.conversations.length > 0) // Remove empty groups
|
|
245
|
+
);
|
|
246
|
+
|
|
13
247
|
return (
|
|
14
248
|
<div className="chatSidebarConversations">
|
|
15
|
-
|
|
16
|
-
{(!conversations || conversations.length === 0) && (
|
|
249
|
+
{isEmpty ? (
|
|
17
250
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
|
18
|
-
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 mb-4">
|
|
19
|
-
<svg
|
|
20
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
21
|
-
width="32"
|
|
22
|
-
height="32"
|
|
23
|
-
viewBox="0 0 24 24"
|
|
24
|
-
fill="none"
|
|
25
|
-
stroke="currentColor"
|
|
26
|
-
strokeWidth="2"
|
|
27
|
-
strokeLinecap="round"
|
|
28
|
-
strokeLinejoin="round"
|
|
29
|
-
className="text-gray-500"
|
|
30
|
-
>
|
|
31
|
-
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
|
32
|
-
</svg>
|
|
33
|
-
</div>
|
|
34
251
|
<h3 className="text-xl font-semibold mb-1">No Conversations</h3>
|
|
35
252
|
<p className="text-gray-500 text-sm mb-4">
|
|
36
|
-
|
|
253
|
+
You have no messages yet.
|
|
37
254
|
</p>
|
|
38
255
|
</div>
|
|
256
|
+
) : (
|
|
257
|
+
<>
|
|
258
|
+
{filteredGeneralChats.length > 0 && (
|
|
259
|
+
<CollapsibleSection title="General Chats">
|
|
260
|
+
<VirtualizedChatList conversations={filteredGeneralChats} />
|
|
261
|
+
</CollapsibleSection>
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
{Object.entries(filteredGroupedServiceChats).length > 0 && (
|
|
265
|
+
<CollapsibleSection title="Service Chats">
|
|
266
|
+
{Object.entries(filteredGroupedServiceChats).map(
|
|
267
|
+
([
|
|
268
|
+
serviceId,
|
|
269
|
+
{ serviceTitle, conversations: serviceConvos },
|
|
270
|
+
]) => (
|
|
271
|
+
<CollapsibleSection
|
|
272
|
+
key={serviceId}
|
|
273
|
+
title={serviceTitle}
|
|
274
|
+
defaultOpen={false}
|
|
275
|
+
>
|
|
276
|
+
<VirtualizedChatList conversations={serviceConvos} />
|
|
277
|
+
</CollapsibleSection>
|
|
278
|
+
)
|
|
279
|
+
)}
|
|
280
|
+
</CollapsibleSection>
|
|
281
|
+
)}
|
|
282
|
+
</>
|
|
39
283
|
)}
|
|
40
|
-
{conversations?.map((conversation: any, idx: any) => (
|
|
41
|
-
<Conversation
|
|
42
|
-
key={conversation._id}
|
|
43
|
-
conversation={conversation}
|
|
44
|
-
lastIdx={idx === conversations.length - 1}
|
|
45
|
-
/>
|
|
46
|
-
))}
|
|
47
|
-
|
|
48
|
-
{/* {loading ? <span className='loading loading-spinner mx-auto'></span> : null} */}
|
|
49
284
|
</div>
|
|
50
285
|
);
|
|
51
286
|
};
|
|
@@ -2,71 +2,33 @@
|
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import searchicon from "../../assets/icons8-search.svg";
|
|
4
4
|
import useChatUIStore from "../../stores/Zustant";
|
|
5
|
-
import { useGetConversations } from "../../hooks/queries/useChatApi";
|
|
6
|
-
import { useChatContext } from "../../providers/ChatProvider";
|
|
7
5
|
// import { MagnifyingGlass } from "@phosphor-icons/react"
|
|
8
6
|
// import useGetConversations from "../../hooks/useGetConversations";
|
|
9
7
|
// import useConversation from '../../zustand/useConversation'
|
|
10
8
|
// import toast from 'react-hot-toast';
|
|
11
9
|
|
|
12
10
|
const SearchInput = () => {
|
|
13
|
-
const {
|
|
14
|
-
const [search, setSearch] = useState(
|
|
15
|
-
const { setSelectedConversation } = useChatUIStore();
|
|
16
|
-
const { data } = useGetConversations(userId);
|
|
11
|
+
const { searchTerm, setSearchTerm } = useChatUIStore();
|
|
12
|
+
const [search, setSearch] = useState(searchTerm);
|
|
17
13
|
|
|
18
|
-
const
|
|
19
|
-
e.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const conversation = data?.find(
|
|
26
|
-
(c: {
|
|
27
|
-
_id: string;
|
|
28
|
-
participantDetails: {
|
|
29
|
-
username: string;
|
|
30
|
-
firstname?: string;
|
|
31
|
-
idpic?: string;
|
|
32
|
-
};
|
|
33
|
-
}) =>
|
|
34
|
-
c.participantDetails.username
|
|
35
|
-
.toLowerCase()
|
|
36
|
-
.includes(search.toLowerCase())
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
if (conversation) {
|
|
40
|
-
const updatedConversation = {
|
|
41
|
-
...conversation,
|
|
42
|
-
participantDetails: {
|
|
43
|
-
...conversation.participantDetails,
|
|
44
|
-
firstname: conversation.participantDetails.username || "Unknown",
|
|
45
|
-
idpic:
|
|
46
|
-
conversation.participantDetails.profilePic || "default-idpic.png",
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
setSelectedConversation(updatedConversation);
|
|
50
|
-
setSearch("");
|
|
51
|
-
}
|
|
52
|
-
console.error("No such user found!");
|
|
14
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
15
|
+
const term = e.target.value;
|
|
16
|
+
setSearch(term);
|
|
17
|
+
setSearchTerm(term);
|
|
53
18
|
};
|
|
54
|
-
|
|
55
19
|
return (
|
|
56
20
|
<>
|
|
57
|
-
<
|
|
58
|
-
<div className="
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/>
|
|
67
|
-
</div>
|
|
21
|
+
<div className="chatSidebarSearchbar">
|
|
22
|
+
<div className="chatSidebarSearchbarContainer">
|
|
23
|
+
<img src={searchicon} className="chatSidebarSearchbarImg" />
|
|
24
|
+
<input
|
|
25
|
+
className="chatSidebarInput"
|
|
26
|
+
placeholder="Search…"
|
|
27
|
+
value={search}
|
|
28
|
+
onChange={handleChange}
|
|
29
|
+
/>
|
|
68
30
|
</div>
|
|
69
|
-
</
|
|
31
|
+
</div>
|
|
70
32
|
</>
|
|
71
33
|
);
|
|
72
34
|
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useChatContext } from "../providers/ChatProvider";
|
|
3
|
+
import useChatUIStore from '../stores/Zustant';
|
|
4
|
+
|
|
5
|
+
export const useMessageStatus = () => {
|
|
6
|
+
const { socket,userId } = useChatContext();
|
|
7
|
+
const { messages, setMessages, selectedConversation } = useChatUIStore();
|
|
8
|
+
|
|
9
|
+
// Handle status updates from server
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!socket) return;
|
|
12
|
+
|
|
13
|
+
const handleStatusUpdate = (event: MessageEvent) => {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(event.data);
|
|
16
|
+
|
|
17
|
+
if (parsed.event === 'messageStatusUpdated') {
|
|
18
|
+
const { messageId, status, chatId } = parsed.data;
|
|
19
|
+
|
|
20
|
+
setMessages(prev => prev.map(msg => {
|
|
21
|
+
if (msg._id === messageId) {
|
|
22
|
+
// Only update if new status is higher priority
|
|
23
|
+
const statusOrder = ['sent', 'delivered', 'read'];
|
|
24
|
+
const currentIdx = statusOrder.indexOf(msg.status);
|
|
25
|
+
const newIdx = statusOrder.indexOf(status);
|
|
26
|
+
|
|
27
|
+
if (newIdx > currentIdx) {
|
|
28
|
+
return { ...msg, status };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return msg;
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Error handling status update:', error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
socket.addEventListener('message', handleStatusUpdate);
|
|
40
|
+
return () => socket.removeEventListener('message', handleStatusUpdate);
|
|
41
|
+
}, [socket, setMessages]);
|
|
42
|
+
|
|
43
|
+
// Send delivery confirmations for visible messages
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!socket || !selectedConversation) return;
|
|
46
|
+
|
|
47
|
+
const observer = new IntersectionObserver(
|
|
48
|
+
(entries) => {
|
|
49
|
+
entries.forEach(entry => {
|
|
50
|
+
if (entry.isIntersecting) {
|
|
51
|
+
const messageId = entry.target.getAttribute('data-message-id');
|
|
52
|
+
if (messageId) {
|
|
53
|
+
const message = messages.find(m => m._id === messageId);
|
|
54
|
+
if (message && message.status === 'sent') {
|
|
55
|
+
socket.send(JSON.stringify({
|
|
56
|
+
event: 'confirmDelivery',
|
|
57
|
+
data: {
|
|
58
|
+
messageId,
|
|
59
|
+
chatId: selectedConversation._id
|
|
60
|
+
}
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
{ threshold: 0.7 }
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Observe all messages in current chat
|
|
71
|
+
const messageElements = document.querySelectorAll('[data-message-id]');
|
|
72
|
+
messageElements.forEach(el => observer.observe(el));
|
|
73
|
+
|
|
74
|
+
return () => observer.disconnect();
|
|
75
|
+
}, [messages, socket, selectedConversation]);
|
|
76
|
+
|
|
77
|
+
// Mark messages as read when chat is active
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!socket || !selectedConversation) return;
|
|
80
|
+
|
|
81
|
+
const unreadMessages = messages.filter(
|
|
82
|
+
msg => msg.status !== 'read' &&
|
|
83
|
+
msg.conversationId === selectedConversation._id &&
|
|
84
|
+
msg.senderId !== userId
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (unreadMessages.length > 0) {
|
|
88
|
+
socket.send(JSON.stringify({
|
|
89
|
+
event: 'messageRead',
|
|
90
|
+
data: {
|
|
91
|
+
messageIds: unreadMessages.map(m => m._id),
|
|
92
|
+
chatId: selectedConversation._id
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
}, [messages, socket, selectedConversation, userId]);
|
|
97
|
+
};
|
|
@@ -18,9 +18,11 @@ interface ChatProviderProps {
|
|
|
18
18
|
interface ChatContextType {
|
|
19
19
|
socket: WebSocket | null;
|
|
20
20
|
userId: string;
|
|
21
|
-
onlineUsers: any[];
|
|
21
|
+
// onlineUsers: any[];
|
|
22
22
|
sendMessage: (data: any) => void;
|
|
23
23
|
isConnected: boolean;
|
|
24
|
+
onlineUsers: Set<string>; // Change to Set for better performance
|
|
25
|
+
isUserOnline: (userId: string) => boolean; // Add helper function
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const ChatContext = createContext<ChatContextType | null>(null);
|
|
@@ -31,7 +33,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
31
33
|
}) => {
|
|
32
34
|
const socketRef = useRef<WebSocket | null>(null);
|
|
33
35
|
const [socket, setSocket] = useState<WebSocket | null>(null);
|
|
34
|
-
const [onlineUsers, setOnlineUsers] = useState<any[]>([]);
|
|
36
|
+
// const [onlineUsers, setOnlineUsers] = useState<any[]>([]);
|
|
37
|
+
const [onlineUsers, setOnlineUsers] = useState<Set<string>>(new Set());
|
|
35
38
|
const [isConnected, setIsConnected] = useState(false);
|
|
36
39
|
const reconnectAttempts = useRef(0);
|
|
37
40
|
const maxReconnectAttempts = 5;
|
|
@@ -40,35 +43,40 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
40
43
|
|
|
41
44
|
const connectWebSocket = useCallback(() => {
|
|
42
45
|
console.log("🔌 Creating new WebSocket connection...");
|
|
43
|
-
|
|
46
|
+
|
|
44
47
|
// Convert HTTP URL to WebSocket URL
|
|
45
|
-
const wsUrl = apiUrl.replace(/^http:/,
|
|
46
|
-
const socketInstance = new WebSocket(
|
|
48
|
+
const wsUrl = apiUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
|
|
49
|
+
const socketInstance = new WebSocket(
|
|
50
|
+
`${wsUrl}?userId=${encodeURIComponent(userId)}`
|
|
51
|
+
);
|
|
47
52
|
|
|
48
53
|
socketInstance.onopen = () => {
|
|
49
54
|
console.log("✅ WebSocket connected");
|
|
50
55
|
setIsConnected(true);
|
|
51
56
|
reconnectAttempts.current = 0;
|
|
52
|
-
|
|
57
|
+
|
|
53
58
|
// Send initial handshake if needed
|
|
54
|
-
socketInstance.send(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
socketInstance.send(
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
event: "handshake",
|
|
62
|
+
userId: userId,
|
|
63
|
+
})
|
|
64
|
+
);
|
|
58
65
|
};
|
|
59
66
|
|
|
60
67
|
socketInstance.onmessage = (event) => {
|
|
61
68
|
try {
|
|
62
69
|
const data = JSON.parse(event.data);
|
|
63
|
-
|
|
64
|
-
if (data.
|
|
65
|
-
|
|
70
|
+
|
|
71
|
+
if (data.event === "getOnlineUsers") {
|
|
72
|
+
console.log("Online users update:", data.data);
|
|
73
|
+
setOnlineUsers(new Set(data.data));
|
|
66
74
|
}
|
|
67
|
-
|
|
75
|
+
|
|
68
76
|
// Handle other message types here
|
|
69
|
-
console.log(
|
|
77
|
+
console.log("Received message:", data);
|
|
70
78
|
} catch (error) {
|
|
71
|
-
console.error(
|
|
79
|
+
console.error("Error parsing message:", error);
|
|
72
80
|
}
|
|
73
81
|
};
|
|
74
82
|
|
|
@@ -80,11 +88,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
80
88
|
console.log("🔌 WebSocket connection closed", event);
|
|
81
89
|
console.log("❌ WebSocket disconnected:", event.code, event.reason);
|
|
82
90
|
setIsConnected(false);
|
|
83
|
-
|
|
91
|
+
|
|
84
92
|
// Attempt reconnection
|
|
85
93
|
if (reconnectAttempts.current < maxReconnectAttempts) {
|
|
86
94
|
reconnectAttempts.current += 1;
|
|
87
|
-
console.log(
|
|
95
|
+
console.log(
|
|
96
|
+
`Attempting to reconnect (${reconnectAttempts.current}/${maxReconnectAttempts})...`
|
|
97
|
+
);
|
|
88
98
|
setTimeout(connectWebSocket, reconnectInterval);
|
|
89
99
|
}
|
|
90
100
|
};
|
|
@@ -113,14 +123,24 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
113
123
|
};
|
|
114
124
|
}, [connectWebSocket]);
|
|
115
125
|
|
|
126
|
+
const isUserOnline = useCallback(
|
|
127
|
+
(userId: string) => {
|
|
128
|
+
return onlineUsers.has(userId);
|
|
129
|
+
},
|
|
130
|
+
[onlineUsers]
|
|
131
|
+
);
|
|
132
|
+
|
|
116
133
|
return (
|
|
117
|
-
<ChatContext.Provider
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
<ChatContext.Provider
|
|
135
|
+
value={{
|
|
136
|
+
socket,
|
|
137
|
+
userId,
|
|
138
|
+
onlineUsers,
|
|
139
|
+
sendMessage,
|
|
140
|
+
isConnected,
|
|
141
|
+
isUserOnline,
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
124
144
|
{children}
|
|
125
145
|
</ChatContext.Provider>
|
|
126
146
|
);
|
|
@@ -132,4 +152,4 @@ export const useChatContext = () => {
|
|
|
132
152
|
throw new Error("useChatContext must be used within a ChatProvider");
|
|
133
153
|
}
|
|
134
154
|
return context;
|
|
135
|
-
};
|
|
155
|
+
};
|