@pubuduth-aplicy/chat-ui 2.1.73 → 2.1.75

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.
@@ -1,56 +1,295 @@
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 Conversation from "./Conversation";
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: conversations } = useGetConversations(userId);
10
- console.log("conversations", conversations);
11
- console.log("userId", userId);
12
- // const { loading, conversations } = useGetConversations();
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
- <div className="chatSidebarConversations">
15
- <h2
16
- className="text-lg font-semibold text-gray-700"
17
- style={{ paddingLeft: "1rem" }}
18
- >
19
- All Messages
20
- </h2>
21
- {(!conversations || conversations.length === 0) && (
248
+ <div className="chatSidebarConversations" style={{height:'100%'}}>
249
+ {isEmpty ? (
22
250
  <div className="flex flex-col items-center justify-center p-8 text-center">
23
- <div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 mb-4">
24
- <svg
25
- xmlns="http://www.w3.org/2000/svg"
26
- width="32"
27
- height="32"
28
- viewBox="0 0 24 24"
29
- fill="none"
30
- stroke="currentColor"
31
- strokeWidth="2"
32
- strokeLinecap="round"
33
- strokeLinejoin="round"
34
- className="text-gray-500"
35
- >
36
- <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" />
37
- </svg>
38
- </div>
39
251
  <h3 className="text-xl font-semibold mb-1">No Conversations</h3>
40
252
  <p className="text-gray-500 text-sm mb-4">
41
- There are no conversations under &quot;All messages&quot;
253
+ You have no messages yet.
42
254
  </p>
43
255
  </div>
256
+ ) : (
257
+ <>
258
+ <div
259
+ className="overflow-y-auto"
260
+ style={{
261
+ maxHeight: 'calc(5 * 5rem)', // Adjust 3.5rem to match your chat item height
262
+ scrollbarWidth: 'thin'
263
+ }}
264
+ >
265
+ {filteredGeneralChats.length > 0 && (
266
+ <CollapsibleSection title="General Chats">
267
+ <VirtualizedChatList conversations={filteredGeneralChats} />
268
+ </CollapsibleSection>
269
+ )}
270
+
271
+ {Object.entries(filteredGroupedServiceChats).length > 0 && (
272
+ <CollapsibleSection title="Service Chats">
273
+ {Object.entries(filteredGroupedServiceChats).map(
274
+ ([
275
+ serviceId,
276
+ { serviceTitle, conversations: serviceConvos },
277
+ ]) => (
278
+ <CollapsibleSection
279
+ key={serviceId}
280
+ title={serviceTitle}
281
+ defaultOpen={false}
282
+ >
283
+ <VirtualizedChatList conversations={serviceConvos} />
284
+ </CollapsibleSection>
285
+ )
286
+ )}
287
+ </CollapsibleSection>
288
+ )}
289
+ </div>
290
+
291
+ </>
44
292
  )}
45
- {conversations?.map((conversation: any, idx: any) => (
46
- <Conversation
47
- key={conversation._id}
48
- conversation={conversation}
49
- lastIdx={idx === conversations.length - 1}
50
- />
51
- ))}
52
-
53
- {/* {loading ? <span className='loading loading-spinner mx-auto'></span> : null} */}
54
293
  </div>
55
294
  );
56
295
  };
@@ -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 { userId } = useChatContext();
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 handleSubmit = (e: any) => {
19
- e.preventDefault();
20
- if (!search) return;
21
- if (search.length < 3) {
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
- <form onSubmit={handleSubmit}>
58
- <div className="chatSidebarSearchbar">
59
- <div className="chatSidebarSearchbarContainer">
60
- <img src={searchicon} className="chatSidebarSearchbarImg" />
61
- <input
62
- className="chatSidebarInput"
63
- placeholder="Search…"
64
- value={search}
65
- onChange={(e) => setSearch(e.target.value)}
66
- />
67
- </div>
21
+ <div className="chatSidebarSearchbar bg-{FFFFFF} dark:bg-gray-800 text-gray-900 dark:text-gray-300">
22
+ <div className="chatSidebarSearchbarContainer bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-300">
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
- </form>
31
+ </div>
70
32
  </>
71
33
  );
72
34
  };
@@ -0,0 +1,92 @@
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
+ useEffect(() => {
10
+ if (!socket) return;
11
+
12
+ const handleStatusUpdate = (event: MessageEvent) => {
13
+ try {
14
+ const parsed = JSON.parse(event.data);
15
+
16
+ if (parsed.event === 'messageStatusUpdated') {
17
+ const { messageId, status, chatId } = parsed.data;
18
+
19
+ setMessages(prev => prev.map(msg => {
20
+ if (msg._id === messageId) {
21
+ const statusOrder = ['sent', 'delivered', 'read'];
22
+ const currentIdx = statusOrder.indexOf(msg.status);
23
+ const newIdx = statusOrder.indexOf(status);
24
+
25
+ if (newIdx > currentIdx) {
26
+ return { ...msg, status };
27
+ }
28
+ }
29
+ return msg;
30
+ }));
31
+ }
32
+ } catch (error) {
33
+ console.error('Error handling status update:', error);
34
+ }
35
+ };
36
+
37
+ socket.addEventListener('message', handleStatusUpdate);
38
+ return () => socket.removeEventListener('message', handleStatusUpdate);
39
+ }, [socket, setMessages]);
40
+
41
+ useEffect(() => {
42
+ if (!socket || !selectedConversation) return;
43
+
44
+ const observer = new IntersectionObserver(
45
+ (entries) => {
46
+ entries.forEach(entry => {
47
+ if (entry.isIntersecting) {
48
+ const messageId = entry.target.getAttribute('data-message-id');
49
+ if (messageId) {
50
+ const message = messages.find(m => m._id === messageId);
51
+ if (message && message.status === 'sent') {
52
+ socket.send(JSON.stringify({
53
+ event: 'confirmDelivery',
54
+ data: {
55
+ messageId,
56
+ chatId: selectedConversation._id
57
+ }
58
+ }));
59
+ }
60
+ }
61
+ }
62
+ });
63
+ },
64
+ { threshold: 0.7 }
65
+ );
66
+
67
+ const messageElements = document.querySelectorAll('[data-message-id]');
68
+ messageElements.forEach(el => observer.observe(el));
69
+
70
+ return () => observer.disconnect();
71
+ }, [messages, socket, selectedConversation]);
72
+
73
+ useEffect(() => {
74
+ if (!socket || !selectedConversation) return;
75
+
76
+ const unreadMessages = messages.filter(
77
+ msg => msg.status !== 'read' &&
78
+ msg.conversationId === selectedConversation._id &&
79
+ msg.senderId !== userId
80
+ );
81
+
82
+ if (unreadMessages.length > 0) {
83
+ socket.send(JSON.stringify({
84
+ event: 'messageRead',
85
+ data: {
86
+ messageIds: unreadMessages.map(m => m._id),
87
+ chatId: selectedConversation._id
88
+ }
89
+ }));
90
+ }
91
+ }, [messages, socket, selectedConversation, userId]);
92
+ };
@@ -9,6 +9,7 @@ import React, {
9
9
  useCallback,
10
10
  } from "react";
11
11
  import { getChatConfig } from "../Chat.config";
12
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
12
13
 
13
14
  interface ChatProviderProps {
14
15
  userId: string;
@@ -26,6 +27,7 @@ interface ChatContextType {
26
27
  }
27
28
 
28
29
  const ChatContext = createContext<ChatContextType | null>(null);
30
+ const queryClient = new QueryClient();
29
31
 
30
32
  export const ChatProvider: React.FC<ChatProviderProps> = ({
31
33
  userId,
@@ -131,18 +133,20 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
131
133
  );
132
134
 
133
135
  return (
134
- <ChatContext.Provider
135
- value={{
136
- socket,
137
- userId,
138
- onlineUsers,
139
- sendMessage,
140
- isConnected,
141
- isUserOnline,
142
- }}
143
- >
144
- {children}
145
- </ChatContext.Provider>
136
+ <QueryClientProvider client={queryClient}>
137
+ <ChatContext.Provider
138
+ value={{
139
+ socket,
140
+ userId,
141
+ onlineUsers,
142
+ sendMessage,
143
+ isConnected,
144
+ isUserOnline,
145
+ }}
146
+ >
147
+ {children}
148
+ </ChatContext.Provider>
149
+ </QueryClientProvider>
146
150
  );
147
151
  };
148
152