@pubuduth-aplicy/chat-ui 2.1.67 → 2.1.69
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 +32 -59
- package/src/components/messages/Message.tsx +173 -7
- package/src/components/messages/MessageContainer.tsx +55 -40
- package/src/components/messages/MessageInput.tsx +333 -248
- package/src/components/messages/Messages.tsx +58 -91
- package/src/components/sidebar/Conversation.tsx +55 -38
- package/src/hooks/mutations/useDeleteMessage.ts +26 -0
- package/src/hooks/mutations/useEditMessage.ts +25 -0
- package/src/lib/api/apiClient.ts +1 -1
- package/src/lib/api/endpoint.ts +3 -1
- package/src/providers/ChatProvider.tsx +90 -63
- package/src/service/messageService.ts +46 -1
- package/src/style/style.css +36 -13
|
@@ -13,101 +13,83 @@ const Messages = () => {
|
|
|
13
13
|
const { selectedConversation, setMessages, messages } = useChatUIStore();
|
|
14
14
|
const { userId, socket } = useChatContext();
|
|
15
15
|
const { ref, inView } = useInView();
|
|
16
|
-
// const { data, isLoading, isError, error } = useMessages(selectedConversation?._id, userId);
|
|
17
|
-
|
|
18
16
|
const lastMessageRef = useRef<HTMLDivElement>(null);
|
|
19
17
|
|
|
20
18
|
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
|
|
21
19
|
queryKey: ["messages", selectedConversation?._id, userId],
|
|
22
20
|
queryFn: ({ pageParam = 1 }) =>
|
|
23
21
|
fetchMessages(selectedConversation?._id, userId, pageParam),
|
|
24
|
-
getNextPageParam: (lastPage) =>
|
|
25
|
-
return lastPage.nextPage; // Use the nextPage from API response
|
|
26
|
-
},
|
|
22
|
+
getNextPageParam: (lastPage) => lastPage.nextPage,
|
|
27
23
|
initialPageParam: 1,
|
|
28
24
|
});
|
|
29
25
|
|
|
26
|
+
// Handle infinite scroll
|
|
30
27
|
useEffect(() => {
|
|
31
|
-
if (inView)
|
|
32
|
-
fetchNextPage();
|
|
33
|
-
}
|
|
28
|
+
if (inView) fetchNextPage();
|
|
34
29
|
}, [fetchNextPage, inView]);
|
|
35
30
|
|
|
31
|
+
// Update messages when data changes
|
|
36
32
|
useEffect(() => {
|
|
37
33
|
if (data) {
|
|
38
|
-
console.log("message fetching data", data);
|
|
39
|
-
|
|
40
34
|
const allMessages = data.pages.flatMap((page) => page.messages).reverse();
|
|
41
35
|
setMessages(allMessages);
|
|
42
36
|
}
|
|
43
|
-
}, [data]);
|
|
37
|
+
}, [data, setMessages]);
|
|
44
38
|
|
|
45
|
-
// Listen for
|
|
39
|
+
// Listen for WebSocket messages
|
|
46
40
|
useEffect(() => {
|
|
47
41
|
if (!socket || !selectedConversation?._id) return;
|
|
48
42
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
43
|
+
const handleMessage = (event: MessageEvent) => {
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(event.data);
|
|
46
|
+
|
|
47
|
+
if (data.type === "newMessage") {
|
|
48
|
+
const newMessage = data.message;
|
|
49
|
+
newMessage.shouldShake = true;
|
|
50
|
+
|
|
51
|
+
setMessages((prevMessages) => {
|
|
52
|
+
const isDuplicate = prevMessages.some(
|
|
53
|
+
(msg) =>
|
|
54
|
+
msg._id === newMessage._id ||
|
|
55
|
+
(msg.isOptimistic &&
|
|
56
|
+
msg.senderId === userId &&
|
|
57
|
+
msg.message === newMessage.message)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return isDuplicate ? prevMessages : [...prevMessages, newMessage];
|
|
61
|
+
});
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// });
|
|
80
|
-
// };
|
|
81
|
-
|
|
82
|
-
const handleStatusUpdate = ({
|
|
83
|
-
messageId,
|
|
84
|
-
status,
|
|
85
|
-
}: {
|
|
86
|
-
messageId: string;
|
|
87
|
-
status: string;
|
|
88
|
-
}) => {
|
|
89
|
-
setMessages((prev) =>
|
|
90
|
-
prev.map((msg) => (msg._id === messageId ? { ...msg, status } : msg))
|
|
91
|
-
);
|
|
64
|
+
if (data.type === "messageStatusUpdated") {
|
|
65
|
+
setMessages((prev) =>
|
|
66
|
+
prev.map((msg) =>
|
|
67
|
+
msg._id === data.messageId ? { ...msg, status: data.status } : msg
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Error parsing WebSocket message:", error);
|
|
73
|
+
}
|
|
92
74
|
};
|
|
93
75
|
|
|
94
|
-
socket.
|
|
95
|
-
socket.on("messageStatusUpdated", handleStatusUpdate);
|
|
76
|
+
socket.addEventListener("message", handleMessage);
|
|
96
77
|
|
|
97
78
|
return () => {
|
|
98
|
-
socket.
|
|
99
|
-
socket.off("messageStatusUpdated", handleStatusUpdate);
|
|
79
|
+
socket.removeEventListener("message", handleMessage);
|
|
100
80
|
};
|
|
101
|
-
}, [socket, setMessages,
|
|
81
|
+
}, [socket, selectedConversation?._id, setMessages, userId]);
|
|
102
82
|
|
|
83
|
+
// Scroll to bottom when messages change
|
|
103
84
|
useEffect(() => {
|
|
104
85
|
if (messages.length > 0) {
|
|
105
86
|
setTimeout(() => {
|
|
106
|
-
lastMessageRef.current?.scrollIntoView({ behavior: "smooth",block:
|
|
87
|
+
lastMessageRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
107
88
|
}, 100);
|
|
108
89
|
}
|
|
109
90
|
}, [messages.length]);
|
|
110
91
|
|
|
92
|
+
// Track message visibility for read receipts
|
|
111
93
|
useEffect(() => {
|
|
112
94
|
if (!socket || !messages.length) return;
|
|
113
95
|
|
|
@@ -117,7 +99,7 @@ const Messages = () => {
|
|
|
117
99
|
if (entry.isIntersecting) {
|
|
118
100
|
const messageId = entry.target.getAttribute("data-message-id");
|
|
119
101
|
if (messageId) {
|
|
120
|
-
|
|
102
|
+
sendDeliveryConfirmation(messageId);
|
|
121
103
|
}
|
|
122
104
|
}
|
|
123
105
|
});
|
|
@@ -131,51 +113,35 @@ const Messages = () => {
|
|
|
131
113
|
return () => observer.disconnect();
|
|
132
114
|
}, [messages, socket]);
|
|
133
115
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// if (container) {
|
|
147
|
-
// container.addEventListener('scroll', handleScroll);
|
|
148
|
-
// return () => container.removeEventListener('scroll', handleScroll);
|
|
149
|
-
// }
|
|
150
|
-
// }, [handleScroll]);
|
|
151
|
-
|
|
152
|
-
// useEffect(() => {
|
|
153
|
-
// const scrollContainer = scrollContainerRef.current;
|
|
154
|
-
// if (scrollContainer) {
|
|
155
|
-
// scrollContainer.addEventListener("scroll", handleScroll);
|
|
156
|
-
// return () => scrollContainer.removeEventListener("scroll", handleScroll);
|
|
157
|
-
// }
|
|
158
|
-
// }, [handleScroll]);
|
|
159
|
-
|
|
160
|
-
console.log("📩 Messages:", messages);
|
|
161
|
-
console.log("📩 Messages Length:", messages?.length);
|
|
116
|
+
|
|
117
|
+
const sendDeliveryConfirmation = (messageId: string) => {
|
|
118
|
+
if (!socket) return;
|
|
119
|
+
|
|
120
|
+
const message = {
|
|
121
|
+
type: "confirmDelivery",
|
|
122
|
+
messageId,
|
|
123
|
+
// timestamp: Date.now()
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
socket.send(JSON.stringify(message));
|
|
127
|
+
};
|
|
162
128
|
|
|
163
129
|
return (
|
|
164
130
|
<div
|
|
165
131
|
className="chatMessages"
|
|
166
132
|
style={{ overflowY: "auto", height: "100%", position: "relative" }}
|
|
167
133
|
>
|
|
168
|
-
<SystemNotice/>
|
|
134
|
+
<SystemNotice />
|
|
169
135
|
<div ref={ref} className="my-8">
|
|
170
136
|
{isFetchingNextPage ? <Loader /> : null}
|
|
171
137
|
</div>
|
|
172
138
|
{messages?.length > 0 ? (
|
|
173
139
|
messages?.map((message: any) =>
|
|
174
|
-
// Check if the message object is valid and has an _id before rendering
|
|
175
140
|
message ? (
|
|
176
141
|
<div
|
|
177
142
|
key={message._id}
|
|
178
143
|
ref={lastMessageRef}
|
|
144
|
+
data-message-id={message._id}
|
|
179
145
|
style={{ flex: 1, minHeight: 0, overflowY: "auto" }}
|
|
180
146
|
>
|
|
181
147
|
<Message message={message} />
|
|
@@ -190,4 +156,5 @@ const Messages = () => {
|
|
|
190
156
|
</div>
|
|
191
157
|
);
|
|
192
158
|
};
|
|
193
|
-
|
|
159
|
+
|
|
160
|
+
export default Messages;
|
|
@@ -1,68 +1,82 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
-
// import { useSocketContext } from "../../context/SocketContext";
|
|
3
|
-
// import useConversation from "../../zustand/useConversation";
|
|
4
|
-
|
|
5
1
|
import { useEffect } from "react";
|
|
6
2
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
7
3
|
import useChatUIStore from "../../stores/Zustant";
|
|
8
4
|
import { ConversationProps } from "../../types/type";
|
|
5
|
+
import { getChatConfig } from "@pubuduth-aplicy/chat-ui";
|
|
9
6
|
|
|
10
|
-
const Conversation = ({ conversation
|
|
11
|
-
const { setSelectedConversation, setOnlineUsers, onlineUsers } =
|
|
7
|
+
const Conversation = ({ conversation }: ConversationProps) => {
|
|
8
|
+
const { setSelectedConversation, setOnlineUsers, onlineUsers,selectedConversation } =
|
|
12
9
|
useChatUIStore();
|
|
13
|
-
const { socket } = useChatContext();
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const { socket, sendMessage } = useChatContext();
|
|
11
|
+
const {role} =getChatConfig()
|
|
16
12
|
const handleSelectConversation = async () => {
|
|
17
13
|
setSelectedConversation(conversation);
|
|
18
14
|
|
|
19
|
-
const unreadMessages = conversation.unreadMessageIds || [];
|
|
20
|
-
|
|
15
|
+
const unreadMessages = conversation.unreadMessageIds || [];
|
|
21
16
|
if (unreadMessages.length > 0) {
|
|
22
|
-
console.log("
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
console.log("Marking messages as read:", unreadMessages);
|
|
18
|
+
|
|
19
|
+
sendMessage({
|
|
20
|
+
event: "messageRead",
|
|
21
|
+
data:{
|
|
22
|
+
messageIds: unreadMessages,
|
|
23
|
+
chatId: conversation._id
|
|
24
|
+
}
|
|
25
|
+
|
|
27
26
|
});
|
|
28
27
|
}
|
|
29
28
|
};
|
|
30
29
|
|
|
30
|
+
// Listen for online users updates
|
|
31
31
|
useEffect(() => {
|
|
32
32
|
if (!socket) return;
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const handleMessage = (event: MessageEvent) => {
|
|
35
|
+
try {
|
|
36
|
+
const data = JSON.parse(event.data);
|
|
37
|
+
if (data.event === "getOnlineUsers") {
|
|
38
|
+
setOnlineUsers(data.data);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("Error parsing online users update:", error);
|
|
42
|
+
}
|
|
36
43
|
};
|
|
37
44
|
|
|
38
|
-
socket.
|
|
45
|
+
socket.addEventListener("message", handleMessage);
|
|
39
46
|
|
|
40
47
|
return () => {
|
|
41
|
-
socket.
|
|
48
|
+
socket.removeEventListener("message", handleMessage);
|
|
42
49
|
};
|
|
43
50
|
}, [socket, setOnlineUsers]);
|
|
44
51
|
|
|
45
52
|
const isUserOnline =
|
|
46
53
|
conversation?.participantDetails?._id &&
|
|
47
54
|
onlineUsers?.includes(conversation.participantDetails._id);
|
|
55
|
+
|
|
56
|
+
const isSelected = selectedConversation?._id === conversation._id;
|
|
57
|
+
|
|
48
58
|
return (
|
|
49
59
|
<>
|
|
50
60
|
<div
|
|
51
|
-
className=
|
|
61
|
+
className={`conversation-container ${isSelected ? "selected" : ""} `}
|
|
52
62
|
onClick={handleSelectConversation}
|
|
53
63
|
>
|
|
54
64
|
<div className="conversation-avatar">
|
|
55
65
|
<img
|
|
56
66
|
className="conversation-img"
|
|
57
67
|
src={
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
role === 'admin' && Array.isArray(selectedConversation?.participantDetails)
|
|
69
|
+
? selectedConversation.participantDetails[1]?.profilePic
|
|
70
|
+
: !Array.isArray(selectedConversation?.participantDetails)
|
|
71
|
+
? selectedConversation?.participantDetails?.profilePic
|
|
72
|
+
: undefined
|
|
60
73
|
}
|
|
74
|
+
|
|
61
75
|
alt="User Avatar"
|
|
62
76
|
/>
|
|
63
77
|
<span
|
|
64
78
|
className={`chatSidebarStatusDot ${
|
|
65
|
-
isUserOnline
|
|
79
|
+
isUserOnline && "online"
|
|
66
80
|
}`}
|
|
67
81
|
></span>
|
|
68
82
|
</div>
|
|
@@ -70,7 +84,13 @@ const Conversation = ({ conversation, lastIdx }: ConversationProps) => {
|
|
|
70
84
|
<div className="conversation-info">
|
|
71
85
|
<div className="conversation-header">
|
|
72
86
|
<p className="conversation-name">
|
|
73
|
-
|
|
87
|
+
{
|
|
88
|
+
role === 'admin' && Array.isArray(selectedConversation?.participantDetails)
|
|
89
|
+
? selectedConversation.participantDetails[1]?.firstname
|
|
90
|
+
: !Array.isArray(selectedConversation?.participantDetails)
|
|
91
|
+
? selectedConversation?.participantDetails?.firstname
|
|
92
|
+
: undefined
|
|
93
|
+
}
|
|
74
94
|
</p>
|
|
75
95
|
<span className="conversation-time">
|
|
76
96
|
{new Date(conversation.lastMessage.createdAt).toLocaleTimeString(
|
|
@@ -83,24 +103,21 @@ const Conversation = ({ conversation, lastIdx }: ConversationProps) => {
|
|
|
83
103
|
</span>
|
|
84
104
|
</div>
|
|
85
105
|
<p className="conversation-message">
|
|
86
|
-
|
|
106
|
+
{conversation.lastMessage.message.length > 50
|
|
87
107
|
? conversation.lastMessage.message.slice(0, 50) + "..."
|
|
88
|
-
:conversation.lastMessage.media.length > 0 ?
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
:conversation.lastMessage.message}
|
|
108
|
+
: conversation.lastMessage.media.length > 0 ? (
|
|
109
|
+
<div style={{display:"flex",alignItems:"center", gap:"5px"}}>
|
|
110
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
111
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
|
|
112
|
+
</svg>
|
|
113
|
+
attachment
|
|
114
|
+
</div>
|
|
115
|
+
) : conversation.lastMessage.message}
|
|
97
116
|
</p>
|
|
98
117
|
</div>
|
|
99
118
|
</div>
|
|
100
|
-
|
|
101
|
-
{/* {!lastIdx && <div className="divider my-0 py-0 h-1" />} */}
|
|
102
119
|
</>
|
|
103
120
|
);
|
|
104
121
|
};
|
|
105
122
|
|
|
106
|
-
export default Conversation;
|
|
123
|
+
export default Conversation;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { deleteMessage } from "../../service/messageService";
|
|
4
|
+
|
|
5
|
+
export const useDeleteMessageMutation = () => {
|
|
6
|
+
const queryClient = useQueryClient();
|
|
7
|
+
|
|
8
|
+
return useMutation({
|
|
9
|
+
mutationFn: deleteMessage,
|
|
10
|
+
onSuccess: () => {
|
|
11
|
+
console.log("Message deleted successfully!", "success");
|
|
12
|
+
// Invalidate both messages and conversations queries
|
|
13
|
+
queryClient.invalidateQueries({ queryKey: ['messages'] });
|
|
14
|
+
queryClient.invalidateQueries({ queryKey: ['conversations'] });
|
|
15
|
+
},
|
|
16
|
+
onError: (error: any) => {
|
|
17
|
+
console.error("Failed to delete message:", error);
|
|
18
|
+
|
|
19
|
+
const errorMessage =
|
|
20
|
+
error?.response?.data?.errors[0]?.msg ||
|
|
21
|
+
"An error occurred while deleting the message.";
|
|
22
|
+
|
|
23
|
+
console.log("useDeleteMessageMutation error:", errorMessage);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { setEditMessage } from "../../service/messageService";
|
|
4
|
+
|
|
5
|
+
export const useEditMessageMutation = () => {
|
|
6
|
+
const queryClient = useQueryClient();
|
|
7
|
+
|
|
8
|
+
return useMutation({
|
|
9
|
+
mutationFn: setEditMessage,
|
|
10
|
+
onSuccess: () => {
|
|
11
|
+
console.log("Message edited successfully!", "success");
|
|
12
|
+
queryClient.invalidateQueries({ queryKey: ['messages'] });
|
|
13
|
+
},
|
|
14
|
+
onError: (error: any) => {
|
|
15
|
+
console.error("Failed to edit message:", error);
|
|
16
|
+
const errorMessage =
|
|
17
|
+
error?.response?.data?.errors[0]?.msg ||
|
|
18
|
+
"An error occurred while editing the message.";
|
|
19
|
+
console.log("useMessageMutation edit error:", errorMessage);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
};
|
package/src/lib/api/apiClient.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { getChatConfig } from "../../Chat.config";
|
|
|
5
5
|
export const getApiClient = () => {
|
|
6
6
|
const { apiUrl } = getChatConfig(); // ✅ safe: runs after init
|
|
7
7
|
return axios.create({
|
|
8
|
-
baseURL: `${apiUrl}
|
|
8
|
+
baseURL: `${apiUrl}`,
|
|
9
9
|
timeout: 5000,
|
|
10
10
|
withCredentials: true,
|
|
11
11
|
headers: {
|
package/src/lib/api/endpoint.ts
CHANGED
|
@@ -4,5 +4,7 @@ export const Path = {
|
|
|
4
4
|
sendmessage:"/api/chat",
|
|
5
5
|
apiProxy:'/api/proxy-download',
|
|
6
6
|
preSignUrl:"/api/chat/generatePresignedUrl",
|
|
7
|
-
getConversationListByAdmin:"/api/chat/getConversationListByAdmin"
|
|
7
|
+
getConversationListByAdmin:"/api/chat/getConversationListByAdmin",
|
|
8
|
+
editMessage: "api/chat/message",
|
|
9
|
+
deleteMessage: "api/chat/deleteMessage"
|
|
8
10
|
};
|
|
@@ -6,31 +6,21 @@ import React, {
|
|
|
6
6
|
useEffect,
|
|
7
7
|
useState,
|
|
8
8
|
useRef,
|
|
9
|
+
useCallback,
|
|
9
10
|
} from "react";
|
|
10
|
-
import { Socket, io } from "socket.io-client";
|
|
11
11
|
import { getChatConfig } from "../Chat.config";
|
|
12
|
-
// import { apiClient } from '../lib/api/apiClient';
|
|
13
|
-
// import { S3Client } from '../lib/storage/s3Client';
|
|
14
|
-
// import { CryptoUtils } from '../lib/encryption/cryptoUtils';
|
|
15
12
|
|
|
16
13
|
interface ChatProviderProps {
|
|
17
|
-
|
|
18
|
-
// s3Config: {
|
|
19
|
-
// bucket: string;
|
|
20
|
-
// region: string;
|
|
21
|
-
// accessKeyId: string;
|
|
22
|
-
// secretAccessKey: string;
|
|
23
|
-
// };
|
|
24
|
-
userId: string;
|
|
14
|
+
userId: string;
|
|
25
15
|
children: ReactNode;
|
|
26
16
|
}
|
|
27
17
|
|
|
28
18
|
interface ChatContextType {
|
|
29
|
-
|
|
30
|
-
socket: Socket;
|
|
31
|
-
// cryptoUtils: CryptoUtils;
|
|
19
|
+
socket: WebSocket | null;
|
|
32
20
|
userId: string;
|
|
33
|
-
onlineUsers: any;
|
|
21
|
+
onlineUsers: any[];
|
|
22
|
+
sendMessage: (data: any) => void;
|
|
23
|
+
isConnected: boolean;
|
|
34
24
|
}
|
|
35
25
|
|
|
36
26
|
const ChatContext = createContext<ChatContextType | null>(null);
|
|
@@ -39,61 +29,98 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
39
29
|
userId,
|
|
40
30
|
children,
|
|
41
31
|
}) => {
|
|
42
|
-
const socketRef = useRef<
|
|
43
|
-
const [socket, setSocket] = useState<
|
|
44
|
-
const [onlineUsers, setOnlineUsers] = useState([]);
|
|
32
|
+
const socketRef = useRef<WebSocket | null>(null);
|
|
33
|
+
const [socket, setSocket] = useState<WebSocket | null>(null);
|
|
34
|
+
const [onlineUsers, setOnlineUsers] = useState<any[]>([]);
|
|
35
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
36
|
+
const reconnectAttempts = useRef(0);
|
|
37
|
+
const maxReconnectAttempts = 5;
|
|
38
|
+
const reconnectInterval = 5000; // 5 seconds
|
|
45
39
|
const { apiUrl } = getChatConfig();
|
|
46
|
-
// const apiUrl = import.meta.env.VITE_APP_BACKEND_PORT;
|
|
47
|
-
console.log("API URL:", apiUrl);
|
|
48
40
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
41
|
+
const connectWebSocket = useCallback(() => {
|
|
42
|
+
console.log("🔌 Creating new WebSocket connection...");
|
|
43
|
+
|
|
44
|
+
// Convert HTTP URL to WebSocket URL
|
|
45
|
+
const wsUrl = apiUrl.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:');
|
|
46
|
+
const socketInstance = new WebSocket(`${wsUrl}?userId=${userId}`);
|
|
47
|
+
|
|
48
|
+
socketInstance.onopen = () => {
|
|
49
|
+
console.log("✅ WebSocket connected");
|
|
50
|
+
setIsConnected(true);
|
|
51
|
+
reconnectAttempts.current = 0;
|
|
52
|
+
|
|
53
|
+
// Send initial handshake if needed
|
|
54
|
+
socketInstance.send(JSON.stringify({
|
|
55
|
+
type: 'handshake',
|
|
56
|
+
userId: userId
|
|
57
|
+
}));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
socketInstance.onmessage = (event) => {
|
|
61
|
+
try {
|
|
62
|
+
const data = JSON.parse(event.data);
|
|
63
|
+
|
|
64
|
+
if (data.type === 'getOnlineUsers') {
|
|
65
|
+
setOnlineUsers(data.users);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle other message types here
|
|
69
|
+
console.log('Received message:', data);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Error parsing message:', error);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
socketInstance.onerror = (error) => {
|
|
76
|
+
console.error("❌ WebSocket error:", error);
|
|
85
77
|
};
|
|
78
|
+
|
|
79
|
+
socketInstance.onclose = (event) => {
|
|
80
|
+
console.log("🔌 WebSocket connection closed", event);
|
|
81
|
+
console.log("❌ WebSocket disconnected:", event.code, event.reason);
|
|
82
|
+
setIsConnected(false);
|
|
83
|
+
|
|
84
|
+
// Attempt reconnection
|
|
85
|
+
if (reconnectAttempts.current < maxReconnectAttempts) {
|
|
86
|
+
reconnectAttempts.current += 1;
|
|
87
|
+
console.log(`Attempting to reconnect (${reconnectAttempts.current}/${maxReconnectAttempts})...`);
|
|
88
|
+
setTimeout(connectWebSocket, reconnectInterval);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
socketRef.current = socketInstance;
|
|
93
|
+
setSocket(socketInstance);
|
|
86
94
|
}, [apiUrl, userId]);
|
|
87
95
|
|
|
88
|
-
|
|
96
|
+
const sendMessage = useCallback((data: any) => {
|
|
97
|
+
if (socketRef.current?.readyState === WebSocket.OPEN) {
|
|
98
|
+
socketRef.current.send(JSON.stringify(data));
|
|
99
|
+
} else {
|
|
100
|
+
console.error("Cannot send message - WebSocket not connected");
|
|
101
|
+
}
|
|
102
|
+
}, []);
|
|
89
103
|
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
connectWebSocket();
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
return () => {
|
|
108
|
+
console.log("❌ Closing WebSocket connection...");
|
|
109
|
+
if (socketRef.current) {
|
|
110
|
+
socketRef.current.close(1000, "Component unmounted");
|
|
111
|
+
socketRef.current = null;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}, [connectWebSocket]);
|
|
94
115
|
|
|
95
116
|
return (
|
|
96
|
-
<ChatContext.Provider value={{
|
|
117
|
+
<ChatContext.Provider value={{
|
|
118
|
+
socket,
|
|
119
|
+
userId,
|
|
120
|
+
onlineUsers,
|
|
121
|
+
sendMessage,
|
|
122
|
+
isConnected
|
|
123
|
+
}}>
|
|
97
124
|
{children}
|
|
98
125
|
</ChatContext.Provider>
|
|
99
126
|
);
|
|
@@ -105,4 +132,4 @@ export const useChatContext = () => {
|
|
|
105
132
|
throw new Error("useChatContext must be used within a ChatProvider");
|
|
106
133
|
}
|
|
107
134
|
return context;
|
|
108
|
-
};
|
|
135
|
+
};
|