@pubuduth-aplicy/chat-ui 2.2.14 → 2.2.15

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.2.14",
3
+ "version": "2.2.15",
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": "",
@@ -39,7 +39,7 @@ const MessageInput = () => {
39
39
  const apiClient = getApiClient();
40
40
  const { role } = getChatConfig();
41
41
  const { socket, sendMessage, userId } = useChatContext();
42
- const { selectedConversation, setMessages } = useChatUIStore();
42
+ const { selectedConversation, setMessages, setLastSentMessage } = useChatUIStore();
43
43
 
44
44
  const [message, setMessage] = useState("");
45
45
  const [message1, setMessage1] = useState("");
@@ -47,8 +47,6 @@ const MessageInput = () => {
47
47
 
48
48
  const [typingUser, setTypingUser] = useState<string | null>(null);
49
49
  const [isSending, setIsSending] = useState(false);
50
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
51
- const [isTyping, setIsTyping] = useState(false);
52
50
  const [attachments, setAttachments] = useState<Attachment[]>([]);
53
51
  const [showAttachmentOptions, setShowAttachmentOptions] = useState(false);
54
52
 
@@ -474,6 +472,8 @@ const MessageInput = () => {
474
472
  },
475
473
  {
476
474
  onSuccess: (data) => {
475
+ const confirmedMessage = data[1];
476
+
477
477
  setMessages((prev) => {
478
478
  const filtered = prev.filter(
479
479
  (msg) => msg._id !== tempMessageId
@@ -481,20 +481,32 @@ const MessageInput = () => {
481
481
  return [
482
482
  ...filtered,
483
483
  {
484
- ...data[1],
484
+ ...confirmedMessage,
485
485
  isUploading: false,
486
486
  isOptimistic: false,
487
487
  },
488
488
  ];
489
489
  });
490
490
 
491
+ // Update the sidebar last message for the sender immediately
492
+ setLastSentMessage({
493
+ _id: confirmedMessage._id,
494
+ conversationId: selectedConversation?._id ?? "",
495
+ message: message1,
496
+ senderId: userId,
497
+ media: successfulUploads,
498
+ status: confirmedMessage.status ?? "sent",
499
+ createdAt: confirmedMessage.createdAt ?? new Date().toISOString(),
500
+ updatedAt: confirmedMessage.updatedAt ?? new Date().toISOString(),
501
+ });
502
+
491
503
  // Send message via WebSocket
492
504
  sendMessage({
493
505
  event: "sendMessage",
494
506
  data: {
495
507
  chatId: selectedConversation?._id,
496
508
  message: message1,
497
- messageId: data[1]._id,
509
+ messageId: confirmedMessage._id,
498
510
  attachments: successfulUploads,
499
511
  senderId: userId,
500
512
  receiverId: otherParticipant._id,
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { useEffect, useState } from "react";
2
+ import { useCallback, useEffect, useState } from "react";
3
3
  import { useGetConversations } from "../../hooks/queries/useChatApi";
4
4
  import { useChatContext } from "../../providers/ChatProvider";
5
5
  import {
@@ -22,7 +22,7 @@ type TabType = "personal" | "service";
22
22
  const Conversations = () => {
23
23
  const { userId, socket } = useChatContext();
24
24
  const { data: participantGroups } = useGetConversations(userId);
25
- const { searchTerm } = useChatUIStore();
25
+ const { searchTerm, lastSentMessage, setLastSentMessage } = useChatUIStore();
26
26
  const [activeTab, setActiveTab] = useState<TabType>("personal");
27
27
  const [conversations, setConversations] = useState<{
28
28
  personalChats: ConversationType[];
@@ -77,6 +77,68 @@ const Conversations = () => {
77
77
  setConversations(processConversations(participantGroups));
78
78
  }, [participantGroups]);
79
79
 
80
+ // ── Hoisted to component scope so both the socket listener AND the
81
+ // lastSentMessage effect can call it ─────────────────────────────
82
+ const handleNewMessage = useCallback((newMessage: any) => {
83
+ if (!newMessage?.conversationId) return;
84
+
85
+ setConversations((prev) => {
86
+ const personalChats = [...prev.personalChats];
87
+ const groupedServiceChats = { ...prev.groupedServiceChats };
88
+
89
+ const updateConversation = (convo: ConversationType) => ({
90
+ ...convo,
91
+ lastMessage: newMessage,
92
+ updatedAt: new Date().toISOString(),
93
+ unreadMessageIds:
94
+ userId !== newMessage.senderId
95
+ ? [...(convo.unreadMessageIds || []), newMessage._id]
96
+ : convo.unreadMessageIds || [],
97
+ unreadMessageCount:
98
+ userId !== newMessage.senderId
99
+ ? (convo.unreadMessageCount || 0) + 1
100
+ : convo.unreadMessageCount || 0,
101
+ });
102
+
103
+ const personalIndex = personalChats.findIndex(
104
+ (c) => c._id === newMessage.conversationId
105
+ );
106
+ if (personalIndex >= 0) {
107
+ personalChats[personalIndex] = updateConversation(personalChats[personalIndex]);
108
+ return { personalChats, groupedServiceChats };
109
+ }
110
+
111
+ for (const serviceId in groupedServiceChats) {
112
+ const serviceIndex = groupedServiceChats[serviceId].conversations.findIndex(
113
+ (c) => c._id === newMessage.conversationId
114
+ );
115
+ if (serviceIndex >= 0) {
116
+ const updatedConversations = [...groupedServiceChats[serviceId].conversations];
117
+ updatedConversations[serviceIndex] = updateConversation(updatedConversations[serviceIndex]);
118
+ groupedServiceChats[serviceId] = {
119
+ ...groupedServiceChats[serviceId],
120
+ conversations: updatedConversations,
121
+ };
122
+ return { personalChats, groupedServiceChats };
123
+ }
124
+ }
125
+
126
+ // Fallback: brand-new conversation not yet in local state
127
+ personalChats.push({
128
+ _id: newMessage.conversationId,
129
+ participants: [newMessage.senderId, newMessage.receiverId],
130
+ lastMessage: newMessage,
131
+ updatedAt: new Date().toISOString(),
132
+ unreadMessageIds: userId !== newMessage.senderId ? [newMessage._id] : [],
133
+ unreadMessageCount: userId !== newMessage.senderId ? 1 : 0,
134
+ type: "personal",
135
+ createdAt: new Date().toISOString(),
136
+ } as any);
137
+
138
+ return { personalChats, groupedServiceChats };
139
+ });
140
+ }, [userId]);
141
+
80
142
  // Real-time update listeners
81
143
  useEffect(() => {
82
144
  if (!socket) return;
@@ -129,86 +191,14 @@ const Conversations = () => {
129
191
  });
130
192
  };
131
193
 
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
- }
181
-
182
- personalChats.push({
183
- _id: newMessage.conversationId,
184
- participants: [newMessage.senderId, newMessage.receiverId],
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
194
  const messageListener = (event: MessageEvent) => {
200
195
  try {
201
196
  const parsed = JSON.parse(event.data);
202
197
 
203
198
  if (parsed.event === "newMessage") {
204
199
  // Actual message is nested at parsed.data.data — same structure as Messages.tsx
205
- const newMessage = parsed?.data?.data;
206
- handleNewMessage(newMessage);
207
- } else if (
208
- parsed.event === "messageStatusUpdated" &&
209
- parsed.data?.status === "read"
210
- ) {
211
- // messageId can come as a single id or an array depending on backend
200
+ handleNewMessage(parsed?.data?.data);
201
+ } else if (parsed.event === "messageStatusUpdated" && parsed.data?.status === "read") {
212
202
  const rawId = parsed.data.messageId ?? parsed.data.messageIds;
213
203
  handleMessageReadAck({
214
204
  messageIds: Array.isArray(rawId) ? rawId : [rawId].filter(Boolean),
@@ -222,7 +212,14 @@ const Conversations = () => {
222
212
 
223
213
  socket.addEventListener("message", messageListener);
224
214
  return () => socket.removeEventListener("message", messageListener);
225
- }, [socket, userId]);
215
+ }, [socket, handleNewMessage]);
216
+
217
+ // React to messages sent by the current user (no server echo needed)
218
+ useEffect(() => {
219
+ if (!lastSentMessage) return;
220
+ handleNewMessage(lastSentMessage);
221
+ setLastSentMessage(null);
222
+ }, [lastSentMessage, handleNewMessage, setLastSentMessage]);
226
223
 
227
224
  // const isEmpty =
228
225
  // activeTab === "personal"
@@ -234,11 +231,12 @@ const Conversations = () => {
234
231
  // Filter personal chats
235
232
  const filteredPersonalChats = !lowerSearch
236
233
  ? conversations.personalChats
237
- : conversations.personalChats.filter((convo) =>
238
- convo.participantDetails?.some((p: any) =>
239
- p?.name?.toLowerCase().includes(lowerSearch)
240
- )
241
- );
234
+ : conversations.personalChats.filter((convo) => {
235
+ const details = Array.isArray(convo.participantDetails)
236
+ ? convo.participantDetails
237
+ : [convo.participantDetails].filter(Boolean);
238
+ return details.some((p: any) => p?.name?.toLowerCase().includes(lowerSearch));
239
+ });
242
240
 
243
241
  // Filter service chats
244
242
  const filteredGroupedServiceChats: GroupedServiceChats = !lowerSearch
@@ -246,17 +244,21 @@ const Conversations = () => {
246
244
  : Object.fromEntries(
247
245
  Object.entries(conversations.groupedServiceChats)
248
246
  .map(([serviceId, group]) => {
247
+ const details = (convo: ConversationType) => {
248
+ const d = Array.isArray(convo.participantDetails)
249
+ ? convo.participantDetails
250
+ : [convo.participantDetails].filter(Boolean);
251
+ return d.some((p: any) => p?.name?.toLowerCase().includes(lowerSearch));
252
+ };
249
253
  const filteredConvos = group.conversations.filter(
250
254
  (convo) =>
251
- convo.participantDetails?.some((p: any) =>
252
- p?.name?.toLowerCase().includes(lowerSearch)
253
- ) ||
255
+ details(convo) ||
254
256
  group.serviceTitle?.toLowerCase().includes(lowerSearch) ||
255
257
  convo.serviceTitle?.toLowerCase().includes(lowerSearch)
256
258
  );
257
- return [serviceId, { ...group, conversations: filteredConvos }];
259
+ return [serviceId, { ...group, conversations: filteredConvos }] as [string, GroupedServiceChats[string]];
258
260
  })
259
- .filter(([_, group]) => group.conversations.length > 0)
261
+ .filter(([, group]) => group.conversations.length > 0)
260
262
  );
261
263
 
262
264
  // Improved empty state logic
@@ -2,6 +2,18 @@
2
2
  import { FileType } from "../components/common/FilePreview";
3
3
  import { create } from "zustand";
4
4
 
5
+ /** Minimal shape needed to update the sidebar after a send */
6
+ export interface SentMessageSignal {
7
+ _id: string;
8
+ conversationId: string;
9
+ message: string;
10
+ senderId: string;
11
+ media?: any[];
12
+ status: string;
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ }
16
+
5
17
  interface ChatUIState {
6
18
  isChatOpen: boolean;
7
19
  unreadCount: number;
@@ -51,6 +63,9 @@ interface ChatUIState {
51
63
  setOnlineUsers: (users: string[]) => void;
52
64
  searchTerm: string;
53
65
  setSearchTerm: (searchTerm: string) => void;
66
+ /** Signal published by MessageInput after a successful send so the sidebar can update */
67
+ lastSentMessage: SentMessageSignal | null;
68
+ setLastSentMessage: (msg: SentMessageSignal | null) => void;
54
69
  }
55
70
 
56
71
  const useChatUIStore = create<ChatUIState>((set) => ({
@@ -74,6 +89,8 @@ const useChatUIStore = create<ChatUIState>((set) => ({
74
89
  setOnlineUsers: (users) => set({ onlineUsers: users }),
75
90
  searchTerm: "",
76
91
  setSearchTerm: (searchTerm) => set({ searchTerm }),
92
+ lastSentMessage: null,
93
+ setLastSentMessage: (msg) => set({ lastSentMessage: msg }),
77
94
  }));
78
95
 
79
96
  export default useChatUIStore;