@pubuduth-aplicy/chat-ui 2.1.33 → 2.1.35

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubuduth-aplicy/chat-ui",
3
- "version": "2.1.33",
3
+ "version": "2.1.35",
4
4
  "description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  /* eslint-disable @typescript-eslint/no-unused-vars */
3
3
 
4
+ import { MessageStatus } from "../../types/type";
4
5
  import { useChatContext } from "../../providers/ChatProvider";
5
6
  import useChatUIStore from "../../stores/Zustant";
6
7
 
@@ -11,6 +12,7 @@ interface MessageProps {
11
12
  message: {
12
13
  senderId: string;
13
14
  message: string;
15
+ status: MessageStatus;
14
16
  createdAt:any;
15
17
  };
16
18
  }
@@ -29,16 +31,43 @@ const seconds = date.getUTCSeconds();
29
31
 
30
32
  // Format the time as HH:mm:ss (24-hour format)
31
33
  const time = `${hours}.${minutes}`;
34
+
35
+ const getStatusIcon = () => {
36
+ if (!fromMe) return null;
37
+
38
+ switch (message.status) {
39
+ case 'sent':
40
+ return <span className="message-status sent">✓</span>;
41
+ case 'delivered':
42
+ return <span className="message-status delivered">✓✓</span>;
43
+ case 'read':
44
+ return <span className="message-status read">✓✓ (blue)</span>;
45
+ default:
46
+ return null;
47
+ }
48
+ };
49
+
32
50
  return (
33
51
  <>
52
+ {/* <div className="w-max grid">
53
+ <div className="px-3.5 py-2 bg-gray-100 rounded-3xl rounded-tl-none justify-start items-center gap-3 inline-flex">
54
+ <h5 className="text-gray-900 text-sm font-normal leading-snug">{message.message}</h5>
55
+ </div>
56
+ <div className="justify-end items-center inline-flex mb-2.5">
57
+ <h6 className="text-gray-500 text-xs font-normal leading-4 py-1">05:14 PM</h6>
58
+ </div>
59
+ </div> */}
60
+
34
61
  <div className='chatMessage'>
35
62
  <div className="chatMessagesContainer">
36
63
  <div style={{color:"#374151"}}>
37
64
  <div className={`chatMessagesBubble_inner ${alignItems} ${bubbleBgColor} `}>
38
65
  <p style={{fontSize:"14px"}}>{message.message}</p>
39
- <div className='chatMessagesBubble_Time'>{time}</div>
66
+ <div className='chatMessagesBubble_Time'> {new Date(message.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>
67
+ {getStatusIcon()}
40
68
  </div>
41
69
  </div>
70
+
42
71
  {/* <div className="clear-both flex text-gray-700" /> */}
43
72
  </div>
44
73
  </div>
@@ -12,6 +12,13 @@ const MessageContainer = () => {
12
12
  const { selectedConversation, setSelectedConversation ,onlineUsers, setOnlineUsers } = useChatUIStore();
13
13
  const {socket} = useChatContext();
14
14
 
15
+ useEffect(() => {
16
+ if (selectedConversation?._id && socket) {
17
+ socket.emit("joinChat", selectedConversation._id); // Join chat room
18
+ }
19
+ }, [selectedConversation?._id, socket]);
20
+
21
+
15
22
  useEffect(() => {
16
23
  if (!socket) return;
17
24
 
@@ -12,7 +12,8 @@ const MessageInput = () => {
12
12
  const { userId } = useChatContext();
13
13
  const { selectedConversation } = useChatUIStore();
14
14
  const [message, setMessage] = useState(""); // State for storing the message input
15
- const { mutate: sendMessage } = useMessageMutation();
15
+ // const { mutate: sendMessage } = useMessageMutation();
16
+ const mutation = useMessageMutation(); // useMutation hook to send message
16
17
  const [typingUser, setTypingUser] = useState<string | null>(null);
17
18
  const [isSending, setIsSending] = useState(false);
18
19
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -40,20 +41,25 @@ const MessageInput = () => {
40
41
  userId,
41
42
  });
42
43
  }
43
- }, 2000);
44
+ }, 200);
44
45
 
45
46
  return () => clearTimeout(typingTimeout);
46
47
  }, [message, socket, selectedConversation?._id, userId]);
47
48
 
49
+
48
50
  useEffect(() => {
49
- if (!socket) return;
51
+ if (!socket || !selectedConversation?._id) return;
50
52
 
51
- const handleTyping = ({ userId }: { userId: string }) => {
52
- setTypingUser(userId);
53
+ const handleTyping = ({ userId, chatId }: { userId: string; chatId: string }) => {
54
+ if (chatId === selectedConversation._id) {
55
+ setTypingUser(userId);
56
+ }
53
57
  };
54
-
55
- const handleStopTyping = ({ userId }: { userId: string }) => {
56
- setTypingUser((prev) => (prev === userId ? null : prev));
58
+
59
+ const handleStopTyping = ({ userId, chatId }: { userId: string; chatId: string }) => {
60
+ if (chatId === selectedConversation._id) {
61
+ setTypingUser((prev) => (prev === userId ? null : prev));
62
+ }
57
63
  };
58
64
 
59
65
  socket.on("typing", handleTyping);
@@ -63,27 +69,7 @@ const MessageInput = () => {
63
69
  socket.off("typing", handleTyping);
64
70
  socket.off("stopTyping", handleStopTyping);
65
71
  };
66
- }, [socket]);
67
-
68
- // useEffect(() => {
69
- // if (message) {
70
- // setIsTyping(true);
71
- // socket.emit("typing", {
72
- // chatId: selectedConversation?._id,
73
- // userId,
74
- // });
75
- // }
76
-
77
- // // Clear typing indicator when user stops typing
78
- // const typingTimeout = setTimeout(() => {
79
- // if (message === "") {
80
- // setIsTyping(false);
81
- // socket.emit("stopTyping", { chatId: selectedConversation?._id, userId });
82
- // }
83
- // }, 2000);
84
-
85
- // return () => clearTimeout(typingTimeout);
86
- // }, [message, socket, selectedConversation?._id, userId]);
72
+ }, [socket, selectedConversation?._id]);
87
73
 
88
74
  const handleSubmit = useCallback(
89
75
  async (e: any) => {
@@ -94,18 +80,48 @@ const MessageInput = () => {
94
80
  try {
95
81
  console.log("📤 Sending message:", message);
96
82
 
97
- if (selectedConversation?._id) {
98
- sendMessage({
99
- chatId: selectedConversation.participantDetails._id,
100
- senderId: userId,
101
- message,
102
- });
103
-
104
- socket.emit("sendMessage", {
105
- chatId: selectedConversation._id,
106
- message,
107
- });
108
- }
83
+ // if (selectedConversation?._id) {
84
+ // const response = await sendMessage({
85
+ // chatId: selectedConversation.participantDetails._id,
86
+ // senderId: userId,
87
+ // message,
88
+ // });
89
+
90
+ // // You can log or handle the response here
91
+ // console.log('Response from sendMessage:', response);
92
+
93
+
94
+ // socket.emit("sendMessage", {
95
+ // chatId: selectedConversation._id,
96
+ // message,
97
+ // senderId: userId,
98
+ // receiverId: selectedConversation.participantDetails._id,
99
+ // });
100
+ // }
101
+
102
+
103
+
104
+ mutation.mutate({
105
+ chatId: selectedConversation?.participantDetails._id,
106
+ senderId: userId,
107
+ message,
108
+ }, {
109
+ onSuccess: (data) => {
110
+ console.log('Response from sendMessage:', data);
111
+ // After successfully sending the message, emit the socket event
112
+ socket.emit("sendMessage", {
113
+ chatId: selectedConversation?._id,
114
+ message,
115
+ messageId:data[1]._id,
116
+ senderId: userId,
117
+ receiverId: selectedConversation?.participantDetails._id,
118
+ });
119
+ },
120
+ onError: (error) => {
121
+ console.error("❌ Error in sending message:", error);
122
+ },
123
+ });
124
+
109
125
  } catch (error) {
110
126
  console.error("❌ Error sending message:", error);
111
127
  } finally {
@@ -116,7 +132,6 @@ const MessageInput = () => {
116
132
  [message, selectedConversation, userId, isSending]
117
133
  );
118
134
 
119
-
120
135
  return (
121
136
  <form className="chatMessageInputform" onSubmit={handleSubmit}>
122
137
  <div className="chatMessageInputdiv">
@@ -133,18 +148,17 @@ const MessageInput = () => {
133
148
  </button>
134
149
  </div>
135
150
 
136
- {typingUser && typingUser !== userId && !isSending && (
137
- <div className="typingIndicator">
138
- <section className="dots-container">
139
- <div className="dot" />
140
- <div className="dot" />
141
- <div className="dot" />
142
- <div className="dot" />
143
- <div className="dot" />
144
- </section>
145
- typing...
146
- </div>
147
- )}
151
+ {typingUser && typingUser !== userId && typingUser === selectedConversation?.participantDetails?._id && !isSending && (
152
+ <div className="typingIndicator">
153
+ <div className="loader">
154
+ <div className="ball" />
155
+ <div className="ball" />
156
+ <div className="ball" />
157
+ typing
158
+ </div>
159
+ </div>
160
+ )}
161
+
148
162
  </form>
149
163
  );
150
164
  };
@@ -29,13 +29,22 @@ const Messages = () => {
29
29
  const handleNewMessage = (newMessage:any) => {
30
30
  newMessage.shouldShake = true;
31
31
  console.log("📩 New message received:", newMessage);
32
- setMessages([...messages, newMessage[1]]);
32
+ // setMessages([...messages, newMessage[1]]);
33
+ setMessages((prevMessages) => [...prevMessages, newMessage]);
33
34
  };
35
+
36
+ const handleStatusUpdate = ({ messageId, status }) => {
37
+ setMessages(prev => prev.map(msg =>
38
+ msg._id === messageId ? { ...msg, status } : msg
39
+ ));
40
+ };
34
41
 
35
- socket.on("newMessage", handleNewMessage);
42
+ socket.on("newMessage", handleNewMessage);
43
+ socket.on("messageStatusUpdated", handleStatusUpdate);
36
44
 
37
45
  return () => {
38
- socket.off("newMessage", handleNewMessage);
46
+ socket.off("newMessage", handleNewMessage);
47
+ socket.off("messageStatusUpdated", handleStatusUpdate);
39
48
  };
40
49
  }, [socket,setMessages, messages]);
41
50
 
@@ -43,7 +52,27 @@ const Messages = () => {
43
52
  setTimeout(() => {
44
53
  lastMessageRef.current?.scrollIntoView({ behavior: "smooth" });
45
54
  }, 100);
46
- }, [messages]);
55
+ }, [ messages]);
56
+
57
+ useEffect(() => {
58
+ if (!socket || !messages.length) return;
59
+
60
+ const observer = new IntersectionObserver((entries) => {
61
+ entries.forEach(entry => {
62
+ if (entry.isIntersecting) {
63
+ const messageId = entry.target.getAttribute('data-message-id');
64
+ if (messageId) {
65
+ socket.emit('confirmDelivery', { messageId });
66
+ }
67
+ }
68
+ });
69
+ }, { threshold: 0.5 });
70
+
71
+ const messageElements = document.querySelectorAll('[data-message-id]');
72
+ messageElements.forEach(el => observer.observe(el));
73
+
74
+ return () => observer.disconnect();
75
+ }, [messages, socket]);
47
76
 
48
77
  if (isLoading) {
49
78
  return <p>Loading messages...</p>;
@@ -57,18 +86,33 @@ const Messages = () => {
57
86
  console.log("📩 Messages Length:", messages?.length);
58
87
 
59
88
  return (
60
- <div className="chatMessages">
61
- {messages?.length > 0 ? (
62
- messages?.map((message: any) => (
63
- <div key={message._id} ref={lastMessageRef}>
64
- <Message message={message} />
65
- </div>
66
- ))
67
- ) : (
68
- <p style={{ textAlign: "center" }}>Send a message to start the conversation</p>
69
- )}
70
- </div>
71
-
89
+ // <div className="chatMessages">
90
+ // {messages?.length > 0 ? (
91
+ // messages?.map((message: any) => (
92
+ // <div key={message._id} ref={lastMessageRef}>
93
+ // <Message message={message} />
94
+ // </div>
95
+ // ))
96
+ // ) : (
97
+ // <p style={{ textAlign: "center" }}>Send a message to start the conversation</p>
98
+ // )}
99
+ // </div>
100
+ <div className="chatMessages">
101
+ {messages?.length > 0 ? (
102
+ messages?.map((message: any) =>
103
+ // Check if the message object is valid and has an _id before rendering
104
+ message ? (
105
+ <div key={message._id} ref={lastMessageRef}>
106
+ <Message message={message} />
107
+ </div>
108
+ ) : null
109
+ )
110
+ ) : (
111
+ <p style={{ textAlign: "center" }}>
112
+ Send a message to start the conversation
113
+ </p>
114
+ )}
115
+ </div>
72
116
  );
73
117
  };
74
118
  export default Messages;
@@ -1,13 +1,31 @@
1
1
  // import { useSocketContext } from "../../context/SocketContext";
2
2
  // import useConversation from "../../zustand/useConversation";
3
3
 
4
+ import { useChatContext } from "../../providers/ChatProvider";
4
5
  import useChatUIStore from "../../stores/Zustant";
5
6
  import { ConversationProps } from "../../types/type";
6
7
 
7
8
  const Conversation = ({ conversation, lastIdx }: ConversationProps) => {
8
9
  const { setSelectedConversation } = useChatUIStore();
10
+ const { socket } = useChatContext();
9
11
  console.log(conversation);
10
12
 
13
+ const handleSelectConversation = async () => {
14
+ setSelectedConversation(conversation);
15
+
16
+ const lastMessageId = conversation.lastMessageId; // You should have this in conversation data
17
+
18
+ if (lastMessageId) {
19
+ // ✅ Notify server via socket
20
+ socket.emit("messageRead", {
21
+ messageId: lastMessageId,
22
+ receiverId: conversation.participantDetails._id, // or receiverId
23
+ });
24
+
25
+
26
+ }
27
+ };
28
+
11
29
  // const isSelected = selectedConversation?._id === conversation._id;
12
30
  // const { onlineUsers } = useSocketContext();
13
31
  // const isOnline = onlineUsers.includes(conversation._id);
@@ -7,18 +7,23 @@ import { ApiResponse } from "../types/type";
7
7
  export const getAllConversationData = async (userid: string) => {
8
8
  try {
9
9
  const res = await apiClient.get<ApiResponse>(`${Path.getconversation}/${userid}`);
10
-
10
+ if (res.data) {
11
+ console.log("API Response: ", res.data);
12
+
13
+ }
11
14
  // Access conversations with participant details from the API response
12
- const { conversationsWithParticipantDetails } = res.data.serviceInfo;
13
-
15
+ const conversationsWithParticipantDetails = res.data.serviceInfo;
16
+ console.log("conversationsWithParticipantDetails", res.data.serviceInfo);
17
+
14
18
  // If needed, you can map the conversations in the specific structure
15
- const formattedConversations = conversationsWithParticipantDetails.map((conversation) => ({
16
- _id: conversation._id,
17
- participantDetails: conversation.participantDetails,
18
- }));
19
+ // const formattedConversations = conversationsWithParticipantDetails?.map((conversation) => ({
20
+ // _id: conversation._id,
21
+ // participantDetails: conversation.participantDetails,
22
+ // }));
23
+ // console.log("formattedConversations", formattedConversations);
19
24
 
20
25
  // Return the formatted conversations
21
- return formattedConversations;
26
+ return conversationsWithParticipantDetails;
22
27
  } catch (error: any) {
23
28
  console.error("ERROR: ", error);
24
29
  // Optionally log the error to an external logger
@@ -1,21 +1,24 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import {create} from 'zustand';
2
+ import { create } from "zustand";
3
3
 
4
4
  interface ChatUIState {
5
5
  isChatOpen: boolean;
6
6
  unreadCount: number;
7
7
  selectedConversation: {
8
- participantDetails:{
8
+ participantDetails: {
9
9
  _id: string;
10
- profilePic: string;
11
- firstname: string;
12
- idpic:string;
13
- }
14
- _id:string;
10
+ profilePic: string;
11
+ firstname: string;
12
+ idpic: string;
13
+ };
14
+ _id: string;
15
15
  } | null;
16
- setSelectedConversation: (selectedConversation: { participantDetails: { _id: string; profilePic: string; firstname: string; idpic: string; }; _id: string; } | null) => void;
17
- messages: any[];
18
- setMessages: (messages: any[]) => void;
16
+ setSelectedConversation: (
17
+ selectedConversation: ChatUIState["selectedConversation"]
18
+ ) => void;
19
+ messages: { id: string; text: string; sender: string; status: string }[];
20
+ setMessages: (messages: ChatUIState["messages"] | ((prev: ChatUIState["messages"]) => ChatUIState["messages"])) => void;
21
+ updateMessageStatus: (messageId: string, status: string) => void;
19
22
  toggleChat: () => void;
20
23
  incrementUnreadCount: () => void;
21
24
  onlineUsers: string[];
@@ -28,7 +31,16 @@ const useChatUIStore = create<ChatUIState>((set) => ({
28
31
  unreadCount: 0,
29
32
  selectedConversation: null,
30
33
  messages: [],
31
- setMessages: (messages: any) => set({ messages }),
34
+ setMessages: (updater: any) =>
35
+ set((state) => ({
36
+ messages: typeof updater === "function" ? updater(state.messages) : updater,
37
+ })),
38
+ updateMessageStatus: (messageId, status) =>
39
+ set((state) => ({
40
+ messages: state.messages.map((msg) =>
41
+ msg.id === messageId ? { ...msg, status } : msg
42
+ ),
43
+ })),
32
44
  setSelectedConversation: (selectedConversation) => set({ selectedConversation }),
33
45
  toggleChat: () => set((state) => ({ isChatOpen: !state.isChatOpen })),
34
46
  onlineUsers: [],
@@ -37,4 +49,4 @@ const useChatUIStore = create<ChatUIState>((set) => ({
37
49
  resetUnreadCount: () => set({ unreadCount: 0 }),
38
50
  }));
39
51
 
40
- export default useChatUIStore;
52
+ export default useChatUIStore;
@@ -299,7 +299,7 @@
299
299
  .chatMessageInputform {
300
300
  position: sticky;
301
301
  bottom: 0;
302
- background: #dbdbdb;
302
+ background: #eee7e7;
303
303
  padding-left: 1rem;
304
304
  padding-right: 1rem;
305
305
  padding-top: 0.25rem;
@@ -389,57 +389,57 @@
389
389
 
390
390
  }
391
391
 
392
+ .typingIndicator{
393
+ display: flex;
394
+ align-items: center;
395
+ gap: 0.5rem;
396
+ }
392
397
 
393
- /* From Uiverse.io by adamgiebl */
394
- .dots-container {
398
+ /* From Uiverse.io by ashish-yadv */
399
+ .loader {
400
+ width: 60px;
395
401
  display: flex;
396
402
  align-items: center;
397
- justify-content: center;
398
- height: 100%;
399
- width: 100%;
403
+ height: 100%;
404
+ margin-right: 10px;
405
+ width: 100%;
406
+ font-size: smaller;
407
+ gap: 0.5rem;
400
408
  }
401
409
 
402
- .dot {
403
- height: 20px;
404
- width: 20px;
405
- margin-right: 10px;
406
- border-radius: 10px;
407
- background-color: #b3d4fc;
408
- animation: pulse 1.5s infinite ease-in-out;
410
+ .ball {
411
+ width: 6px;
412
+ height: 6px;
413
+ border-radius: 50%;
414
+ background-color: #3b2dfd;
409
415
  }
410
416
 
411
- .dot:last-child {
412
- margin-right: 0;
417
+ .ball:nth-child(1) {
418
+ animation: bounce-1 2.1s ease-in-out infinite;
413
419
  }
414
420
 
415
- .dot:nth-child(1) {
416
- animation-delay: -0.3s;
421
+ @keyframes bounce-1 {
422
+ 50% {
423
+ transform: translateY(-3px);
424
+ }
417
425
  }
418
426
 
419
- .dot:nth-child(2) {
420
- animation-delay: -0.1s;
427
+ .ball:nth-child(2) {
428
+ animation: bounce-3 2.1s ease-in-out 0.3s infinite;
421
429
  }
422
430
 
423
- .dot:nth-child(3) {
424
- animation-delay: 0.1s;
431
+ @keyframes bounce-2 {
432
+ 50% {
433
+ transform: translateY(-3px);
434
+ }
425
435
  }
426
436
 
427
- @keyframes pulse {
428
- 0% {
429
- transform: scale(0.8);
430
- background-color: #b3d4fc;
431
- box-shadow: 0 0 0 0 rgba(178, 212, 252, 0.7);
432
- }
437
+ .ball:nth-child(3) {
438
+ animation: bounce-3 2.1s ease-in-out 0.6s infinite;
439
+ }
433
440
 
441
+ @keyframes bounce-3 {
434
442
  50% {
435
- transform: scale(1.2);
436
- background-color: #6793fb;
437
- box-shadow: 0 0 0 10px rgba(178, 212, 252, 0);
438
- }
439
-
440
- 100% {
441
- transform: scale(0.8);
442
- background-color: #b3d4fc;
443
- box-shadow: 0 0 0 0 rgba(178, 212, 252, 0.7);
443
+ transform: translateY(-3px);
444
444
  }
445
445
  }
package/src/types/type.ts CHANGED
@@ -21,6 +21,15 @@ export interface ParticipantDetails {
21
21
  export interface Conversation {
22
22
  _id: string;
23
23
  createdAt: string;
24
+ lastMessage:{
25
+ _id: string;
26
+ senderId: string;
27
+ message: string;
28
+ chatId: string;
29
+ createdAt: string;
30
+ updatedAt: string;
31
+ __v: number;
32
+ },
24
33
  updatedAt: string;
25
34
  __v: number;
26
35
  participantDetails: ParticipantDetails;
@@ -33,7 +42,7 @@ export interface ServiceInfo {
33
42
  export interface ApiResponse {
34
43
  success: boolean;
35
44
  message: string;
36
- serviceInfo: ServiceInfo;
45
+ serviceInfo: Conversation[];
37
46
  }
38
47
 
39
48
  export interface ConversationProps {
@@ -48,4 +57,6 @@ export interface ConversationProps {
48
57
  _id: string;
49
58
  };
50
59
  lastIdx: boolean;
51
- }
60
+ }
61
+
62
+ export type MessageStatus = 'sent' | 'delivered' | 'read';