@pubuduth-aplicy/chat-ui 2.2.10 → 2.2.12
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
package/src/components/Chat.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export const Chat = () => {
|
|
|
22
22
|
// Update UI immediately
|
|
23
23
|
setMessages((prev) => [...prev, message]);
|
|
24
24
|
sendMessage({
|
|
25
|
-
event: "
|
|
25
|
+
event: "messageRead",
|
|
26
26
|
data: {
|
|
27
27
|
messageIds: [message._id],
|
|
28
28
|
chatId: message.conversationId, // make sure this is sent from backend
|
|
@@ -34,23 +34,24 @@ export const Chat = () => {
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
// Optional: update UI
|
|
37
|
-
updateMessageStatus(message._id, "delivered");
|
|
37
|
+
// updateMessageStatus(message._id, "delivered");
|
|
38
|
+
updateMessageStatus(message._id, "read");
|
|
38
39
|
|
|
39
|
-
// Send read receipt if user is viewing this conversation
|
|
40
|
-
// This ensures messages are marked as "read" immediately when both users are in the same chat
|
|
41
|
-
if (message.conversationId === selectedConversation?._id) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
40
|
+
// // Send read receipt if user is viewing this conversation
|
|
41
|
+
// // This ensures messages are marked as "read" immediately when both users are in the same chat
|
|
42
|
+
// if (message.conversationId === selectedConversation?._id) {
|
|
43
|
+
// sendMessage({
|
|
44
|
+
// event: "messageRead",
|
|
45
|
+
// data: {
|
|
46
|
+
// messageIds: [message._id],
|
|
47
|
+
// chatId: message.conversationId,
|
|
48
|
+
// senderId: message.senderId,
|
|
49
|
+
// receiverId: message.receiverId,
|
|
50
|
+
// receiverRole: message.receiverRole,
|
|
51
|
+
// senderRole: message.senderRole,
|
|
52
|
+
// },
|
|
53
|
+
// });
|
|
54
|
+
// }
|
|
54
55
|
}
|
|
55
56
|
} catch (error) {
|
|
56
57
|
console.error("WebSocket message parse error:", error);
|
|
@@ -40,21 +40,28 @@ const MessageInput = () => {
|
|
|
40
40
|
const { role } = getChatConfig();
|
|
41
41
|
const { socket, sendMessage, userId } = useChatContext();
|
|
42
42
|
const { selectedConversation, setMessages } = useChatUIStore();
|
|
43
|
+
|
|
43
44
|
const [message, setMessage] = useState("");
|
|
44
45
|
const [message1, setMessage1] = useState("");
|
|
45
46
|
const mutation = useMessageMutation();
|
|
47
|
+
|
|
46
48
|
const [typingUser, setTypingUser] = useState<string | null>(null);
|
|
47
49
|
const [isSending, setIsSending] = useState(false);
|
|
48
50
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
49
51
|
const [isTyping, setIsTyping] = useState(false);
|
|
50
52
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
|
51
53
|
const [showAttachmentOptions, setShowAttachmentOptions] = useState(false);
|
|
54
|
+
|
|
52
55
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
53
56
|
const attachmentsRef = useRef<Attachment[]>([]);
|
|
54
57
|
const [tempMessageId, setTempMessageId] = useState<string | null>(null);
|
|
55
58
|
const [inputError, setInputError] = useState<string | null>(null);
|
|
56
59
|
const attachmentsContainerRef = useRef<HTMLDivElement>(null);
|
|
57
|
-
|
|
60
|
+
|
|
61
|
+
const typingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
62
|
+
const isTypingRef = useRef(false);
|
|
63
|
+
|
|
64
|
+
|
|
58
65
|
const generateTempId = () =>
|
|
59
66
|
`temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
60
67
|
// Join chat room when conversation is selected
|
|
@@ -69,6 +76,49 @@ const MessageInput = () => {
|
|
|
69
76
|
(p: any) => p._id !== userId
|
|
70
77
|
);
|
|
71
78
|
|
|
79
|
+
// --- Emit typing helpers (debounced / stateful) ---
|
|
80
|
+
const emitTyping = useCallback(() => {
|
|
81
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) return;
|
|
82
|
+
if (!selectedConversation?._id || !otherParticipant?._id) return;
|
|
83
|
+
|
|
84
|
+
sendMessage({
|
|
85
|
+
event: "typing",
|
|
86
|
+
data: {
|
|
87
|
+
chatId: selectedConversation._id,
|
|
88
|
+
userId,
|
|
89
|
+
receiverId: otherParticipant._id,
|
|
90
|
+
receiverRole: role === "customer" ? "provider" : "customer",
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}, [socket, selectedConversation?._id, otherParticipant?._id, sendMessage, userId, role]);
|
|
94
|
+
|
|
95
|
+
const emitStopTyping = useCallback(() => {
|
|
96
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) return;
|
|
97
|
+
if (!selectedConversation?._id || !otherParticipant?._id) return;
|
|
98
|
+
|
|
99
|
+
sendMessage({
|
|
100
|
+
event: "stopTyping",
|
|
101
|
+
data: {
|
|
102
|
+
chatId: selectedConversation._id,
|
|
103
|
+
userId,
|
|
104
|
+
receiverId: otherParticipant._id,
|
|
105
|
+
receiverRole: role === "customer" ? "provider" : "customer",
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}, [socket, selectedConversation?._id, otherParticipant?._id, sendMessage, userId, role]);
|
|
109
|
+
|
|
110
|
+
const stopTypingNow = useCallback(() => {
|
|
111
|
+
if (typingTimeoutRef.current) {
|
|
112
|
+
clearTimeout(typingTimeoutRef.current);
|
|
113
|
+
typingTimeoutRef.current = null;
|
|
114
|
+
}
|
|
115
|
+
if (isTypingRef.current) {
|
|
116
|
+
isTypingRef.current = false;
|
|
117
|
+
emitStopTyping();
|
|
118
|
+
}
|
|
119
|
+
}, [emitStopTyping]);
|
|
120
|
+
|
|
121
|
+
|
|
72
122
|
useEffect(() => {
|
|
73
123
|
if (selectedConversation?._id && socket?.readyState === WebSocket.OPEN) {
|
|
74
124
|
sendMessage({
|
|
@@ -88,50 +138,119 @@ const MessageInput = () => {
|
|
|
88
138
|
setInputError(null);
|
|
89
139
|
setShowAttachmentOptions(false);
|
|
90
140
|
|
|
141
|
+
stopTypingNow();
|
|
142
|
+
|
|
91
143
|
// Clean up any existing file input
|
|
92
144
|
if (fileInputRef.current) {
|
|
93
145
|
fileInputRef.current.value = "";
|
|
94
146
|
}
|
|
95
|
-
}, [selectedConversation?._id]);
|
|
147
|
+
}, [selectedConversation?._id, stopTypingNow]);
|
|
96
148
|
|
|
97
149
|
// Typing indicator logic
|
|
150
|
+
// useEffect(() => {
|
|
151
|
+
// if (!socket || !selectedConversation?._id) return;
|
|
152
|
+
|
|
153
|
+
// if (message.trim() !== "") {
|
|
154
|
+
// setIsTyping(true);
|
|
155
|
+
// sendMessage({
|
|
156
|
+
// event: "typing",
|
|
157
|
+
// data: {
|
|
158
|
+
// chatId: selectedConversation._id,
|
|
159
|
+
// userId,
|
|
160
|
+
// receiverId: otherParticipant?._id,
|
|
161
|
+
// receiverRole: role === "customer" ? "provider" : "customer",
|
|
162
|
+
// },
|
|
163
|
+
// });
|
|
164
|
+
// }
|
|
165
|
+
|
|
166
|
+
// typingTimeoutRef.current = setTimeout(() => {
|
|
167
|
+
// if (message.trim() === "") {
|
|
168
|
+
// setIsTyping(false);
|
|
169
|
+
// sendMessage({
|
|
170
|
+
// event: "stopTyping",
|
|
171
|
+
// data: {
|
|
172
|
+
// chatId: selectedConversation._id,
|
|
173
|
+
// userId,
|
|
174
|
+
// receiverId: otherParticipant?._id,
|
|
175
|
+
// receiverRole: role === "customer" ? "provider" : "customer",
|
|
176
|
+
// },
|
|
177
|
+
// });
|
|
178
|
+
// }
|
|
179
|
+
// }, 200);
|
|
180
|
+
|
|
181
|
+
// return () => {
|
|
182
|
+
// if (typingTimeoutRef.current) {
|
|
183
|
+
// clearTimeout(typingTimeoutRef.current);
|
|
184
|
+
// }
|
|
185
|
+
// };
|
|
186
|
+
// }, [message, socket, selectedConversation?._id, userId, sendMessage]);
|
|
187
|
+
|
|
188
|
+
// // Listen for typing indicators from others
|
|
189
|
+
// useEffect(() => {
|
|
190
|
+
// if (!socket || !selectedConversation?._id) return;
|
|
191
|
+
|
|
192
|
+
// const handleMessage = (event: MessageEvent) => {
|
|
193
|
+
// try {
|
|
194
|
+
// const data = JSON.parse(event.data);
|
|
195
|
+
// if (
|
|
196
|
+
// data.event === "typing" &&
|
|
197
|
+
// data.data.chatId === selectedConversation._id
|
|
198
|
+
// ) {
|
|
199
|
+
// setTypingUser(data.data.userId);
|
|
200
|
+
// } else if (
|
|
201
|
+
// data.event === "stopTyping" &&
|
|
202
|
+
// data.data.chatId === selectedConversation._id
|
|
203
|
+
// ) {
|
|
204
|
+
// setTypingUser((prev) => (prev === data.data.userId ? null : prev));
|
|
205
|
+
// }
|
|
206
|
+
// } catch (error) {
|
|
207
|
+
// console.error("Error parsing typing message:", error);
|
|
208
|
+
// }
|
|
209
|
+
// };
|
|
210
|
+
|
|
211
|
+
// socket.addEventListener("message", handleMessage);
|
|
212
|
+
// return () => {
|
|
213
|
+
// socket.removeEventListener("message", handleMessage);
|
|
214
|
+
// };
|
|
215
|
+
// }, [socket, selectedConversation?._id]);
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
// ✅ FIXED Typing indicator logic:
|
|
219
|
+
// - Send "typing" once when user starts typing
|
|
220
|
+
// - Send "stopTyping" after inactivity (even if text remains)
|
|
221
|
+
// - If input becomes empty -> stopTyping once
|
|
98
222
|
useEffect(() => {
|
|
99
|
-
if (
|
|
223
|
+
if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current);
|
|
100
224
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
225
|
+
const hasText = message.trim().length > 0;
|
|
226
|
+
|
|
227
|
+
if (!hasText) {
|
|
228
|
+
// If input empty, ensure stopTyping is emitted once
|
|
229
|
+
if (isTypingRef.current) {
|
|
230
|
+
isTypingRef.current = false;
|
|
231
|
+
emitStopTyping();
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Has text
|
|
237
|
+
if (!isTypingRef.current) {
|
|
238
|
+
isTypingRef.current = true;
|
|
239
|
+
emitTyping();
|
|
112
240
|
}
|
|
113
241
|
|
|
242
|
+
// If user pauses, stop typing after 1s
|
|
114
243
|
typingTimeoutRef.current = setTimeout(() => {
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
event: "stopTyping",
|
|
119
|
-
data: {
|
|
120
|
-
chatId: selectedConversation._id,
|
|
121
|
-
userId,
|
|
122
|
-
receiverId: otherParticipant?._id,
|
|
123
|
-
receiverRole: role === "customer" ? "provider" : "customer",
|
|
124
|
-
},
|
|
125
|
-
});
|
|
244
|
+
if (isTypingRef.current) {
|
|
245
|
+
isTypingRef.current = false;
|
|
246
|
+
emitStopTyping();
|
|
126
247
|
}
|
|
127
|
-
},
|
|
248
|
+
}, 1000);
|
|
128
249
|
|
|
129
250
|
return () => {
|
|
130
|
-
if (typingTimeoutRef.current)
|
|
131
|
-
clearTimeout(typingTimeoutRef.current);
|
|
132
|
-
}
|
|
251
|
+
if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current);
|
|
133
252
|
};
|
|
134
|
-
}, [message,
|
|
253
|
+
}, [message, emitTyping, emitStopTyping]);
|
|
135
254
|
|
|
136
255
|
// Listen for typing indicators from others
|
|
137
256
|
useEffect(() => {
|
|
@@ -140,15 +259,10 @@ const MessageInput = () => {
|
|
|
140
259
|
const handleMessage = (event: MessageEvent) => {
|
|
141
260
|
try {
|
|
142
261
|
const data = JSON.parse(event.data);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
data.data.chatId === selectedConversation._id
|
|
146
|
-
) {
|
|
262
|
+
|
|
263
|
+
if (data.event === "typing" && data.data.chatId === selectedConversation._id) {
|
|
147
264
|
setTypingUser(data.data.userId);
|
|
148
|
-
} else if (
|
|
149
|
-
data.event === "stopTyping" &&
|
|
150
|
-
data.data.chatId === selectedConversation._id
|
|
151
|
-
) {
|
|
265
|
+
} else if (data.event === "stopTyping" && data.data.chatId === selectedConversation._id) {
|
|
152
266
|
setTypingUser((prev) => (prev === data.data.userId ? null : prev));
|
|
153
267
|
}
|
|
154
268
|
} catch (error) {
|
|
@@ -44,13 +44,43 @@ const Messages = () => {
|
|
|
44
44
|
const handleMessage = (event: MessageEvent) => {
|
|
45
45
|
try {
|
|
46
46
|
const parsed = JSON.parse(event.data);
|
|
47
|
-
if (parsed.type === "newMessage" || parsed.event === "newMessage") {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
// if (parsed.type === "newMessage" || parsed.event === "newMessage") {
|
|
48
|
+
// const newMessage = parsed.data;
|
|
49
|
+
// if (!newMessage) {
|
|
50
|
+
// // console.warn(
|
|
51
|
+
// // "Received newMessage event without a message payload",
|
|
52
|
+
// // parsed
|
|
53
|
+
// // );
|
|
54
|
+
// return;
|
|
55
|
+
// }
|
|
56
|
+
|
|
57
|
+
// newMessage.shouldShake = true;
|
|
58
|
+
|
|
59
|
+
// setMessages((prevMessages) => {
|
|
60
|
+
// const isDuplicate = prevMessages.some(
|
|
61
|
+
// (msg) =>
|
|
62
|
+
// msg._id === newMessage._id ||
|
|
63
|
+
// (msg.isOptimistic &&
|
|
64
|
+
// msg.sender.senderId === userId &&
|
|
65
|
+
// msg.message === newMessage.message)
|
|
66
|
+
// );
|
|
67
|
+
|
|
68
|
+
// return isDuplicate ? prevMessages : [...prevMessages, newMessage];
|
|
69
|
+
// });
|
|
70
|
+
// }
|
|
71
|
+
|
|
72
|
+
if (parsed.event === "newMessage" || parsed.type === "newMessage") {
|
|
73
|
+
const newMessage = parsed?.data?.data;
|
|
74
|
+
if (!newMessage) return;
|
|
75
|
+
|
|
76
|
+
const incomingConversationId = newMessage.conversationId;
|
|
77
|
+
const currentConversationId = selectedConversation?._id;
|
|
78
|
+
|
|
79
|
+
// ✅ if message is for another conversation, DO NOT append here
|
|
80
|
+
if (
|
|
81
|
+
!currentConversationId ||
|
|
82
|
+
incomingConversationId !== currentConversationId
|
|
83
|
+
) {
|
|
54
84
|
return;
|
|
55
85
|
}
|
|
56
86
|
|
|
@@ -58,15 +88,17 @@ const Messages = () => {
|
|
|
58
88
|
|
|
59
89
|
setMessages((prevMessages) => {
|
|
60
90
|
const isDuplicate = prevMessages.some(
|
|
61
|
-
(msg) =>
|
|
91
|
+
(msg: any) =>
|
|
62
92
|
msg._id === newMessage._id ||
|
|
63
93
|
(msg.isOptimistic &&
|
|
64
|
-
msg.sender
|
|
65
|
-
msg.message === newMessage.message)
|
|
94
|
+
msg.sender?.senderId === userId &&
|
|
95
|
+
msg.message === newMessage.message),
|
|
66
96
|
);
|
|
67
97
|
|
|
68
98
|
return isDuplicate ? prevMessages : [...prevMessages, newMessage];
|
|
69
99
|
});
|
|
100
|
+
|
|
101
|
+
return;
|
|
70
102
|
}
|
|
71
103
|
|
|
72
104
|
const statusOrder = ["sent", "delivered", "read", "edited", "deleted"];
|
|
@@ -92,7 +124,7 @@ const Messages = () => {
|
|
|
92
124
|
}
|
|
93
125
|
|
|
94
126
|
return msg; // No update if new status isn't higher
|
|
95
|
-
})
|
|
127
|
+
}),
|
|
96
128
|
);
|
|
97
129
|
}
|
|
98
130
|
|
|
@@ -106,8 +138,8 @@ const Messages = () => {
|
|
|
106
138
|
prevMessages.map((msg) =>
|
|
107
139
|
msg._id === updatedMessage.messageId
|
|
108
140
|
? { ...msg, message: updatedMessage.message, status: "edited" }
|
|
109
|
-
: msg
|
|
110
|
-
)
|
|
141
|
+
: msg,
|
|
142
|
+
),
|
|
111
143
|
);
|
|
112
144
|
}
|
|
113
145
|
|
|
@@ -125,8 +157,8 @@ const Messages = () => {
|
|
|
125
157
|
message: "This message was deleted",
|
|
126
158
|
status: "deleted",
|
|
127
159
|
}
|
|
128
|
-
: msg
|
|
129
|
-
)
|
|
160
|
+
: msg,
|
|
161
|
+
),
|
|
130
162
|
);
|
|
131
163
|
}
|
|
132
164
|
} catch (error) {
|
|
@@ -197,7 +229,7 @@ const Messages = () => {
|
|
|
197
229
|
>
|
|
198
230
|
<Message message={message} />
|
|
199
231
|
</div>
|
|
200
|
-
) : null
|
|
232
|
+
) : null,
|
|
201
233
|
)
|
|
202
234
|
) : (
|
|
203
235
|
<p style={{ textAlign: "center" }}>
|
|
@@ -17,10 +17,6 @@ type GroupedServiceChats = {
|
|
|
17
17
|
};
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
type Row =
|
|
21
|
-
| { type: "service-header"; serviceId: string; title: string }
|
|
22
|
-
| { type: "conversation"; conversation: ConversationType };
|
|
23
|
-
|
|
24
20
|
type TabType = "personal" | "service";
|
|
25
21
|
|
|
26
22
|
const Conversations = () => {
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
import { useChatContext } from "../providers/ChatProvider";
|
|
3
3
|
import useChatUIStore from '../stores/Zustant';
|
|
4
|
+
import { getChatConfig } from '../Chat.config';
|
|
4
5
|
|
|
5
6
|
export const useMessageStatus = () => {
|
|
6
|
-
const { socket,userId } = useChatContext();
|
|
7
|
+
const { socket, userId } = useChatContext();
|
|
7
8
|
const { messages, setMessages, selectedConversation } = useChatUIStore();
|
|
8
|
-
|
|
9
|
+
const { role } = getChatConfig();
|
|
9
10
|
useEffect(() => {
|
|
10
11
|
if (!socket) return;
|
|
11
12
|
|
|
12
13
|
const handleStatusUpdate = (event: MessageEvent) => {
|
|
13
14
|
try {
|
|
14
15
|
const parsed = JSON.parse(event.data);
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
if (parsed.event === 'messageStatusUpdated') {
|
|
17
18
|
const { messageId, status, chatId } = parsed.data;
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
setMessages(prev => prev.map(msg => {
|
|
20
21
|
if (msg._id === messageId) {
|
|
21
22
|
const statusOrder = ['sent', 'delivered', 'read'];
|
|
22
23
|
const currentIdx = statusOrder.indexOf(msg.status);
|
|
23
24
|
const newIdx = statusOrder.indexOf(status);
|
|
24
|
-
|
|
25
|
+
|
|
25
26
|
if (newIdx > currentIdx) {
|
|
26
27
|
return { ...msg, status };
|
|
27
28
|
}
|
|
@@ -53,7 +54,9 @@ export const useMessageStatus = () => {
|
|
|
53
54
|
event: 'confirmDelivery',
|
|
54
55
|
data: {
|
|
55
56
|
messageId,
|
|
56
|
-
chatId: selectedConversation._id
|
|
57
|
+
chatId: selectedConversation._id,
|
|
58
|
+
senderRole: message.sender.role,
|
|
59
|
+
senderId: message.sender.senderId,
|
|
57
60
|
}
|
|
58
61
|
}));
|
|
59
62
|
}
|
|
@@ -74,17 +77,23 @@ export const useMessageStatus = () => {
|
|
|
74
77
|
if (!socket || !selectedConversation) return;
|
|
75
78
|
|
|
76
79
|
const unreadMessages = messages.filter(
|
|
77
|
-
msg => msg.status !== 'read' &&
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
msg => msg.status !== 'read' &&
|
|
81
|
+
msg.conversationId === selectedConversation._id &&
|
|
82
|
+
msg.sender.senderId !== userId
|
|
80
83
|
);
|
|
81
84
|
|
|
82
85
|
if (unreadMessages.length > 0) {
|
|
86
|
+
console.log('unread message in usemessagestatus',u);
|
|
87
|
+
|
|
83
88
|
socket.send(JSON.stringify({
|
|
84
89
|
event: 'messageRead',
|
|
85
90
|
data: {
|
|
86
91
|
messageIds: unreadMessages.map(m => m._id),
|
|
87
|
-
chatId: selectedConversation._id
|
|
92
|
+
chatId: selectedConversation._id,
|
|
93
|
+
senderRole: unreadMessages[0].sender.role,
|
|
94
|
+
senderId: unreadMessages[0].sender.senderId,
|
|
95
|
+
receiverId: userId,
|
|
96
|
+
receiverRole: role,
|
|
88
97
|
}
|
|
89
98
|
}));
|
|
90
99
|
}
|