@pubuduth-aplicy/chat-ui 2.2.9 → 2.2.11
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
|
@@ -17,12 +17,12 @@ export const Chat = () => {
|
|
|
17
17
|
const parsed = JSON.parse(event.data);
|
|
18
18
|
|
|
19
19
|
if (parsed.event === "newMessage") {
|
|
20
|
-
const message = parsed.data;
|
|
20
|
+
const message = parsed.data.data;
|
|
21
21
|
// Send delivery confirmation
|
|
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) {
|
|
@@ -3,7 +3,7 @@ import { useChatContext } from "../providers/ChatProvider";
|
|
|
3
3
|
import useChatUIStore from '../stores/Zustant';
|
|
4
4
|
|
|
5
5
|
export const useMessageStatus = () => {
|
|
6
|
-
const { socket,userId } = useChatContext();
|
|
6
|
+
const { socket, userId } = useChatContext();
|
|
7
7
|
const { messages, setMessages, selectedConversation } = useChatUIStore();
|
|
8
8
|
|
|
9
9
|
useEffect(() => {
|
|
@@ -12,16 +12,16 @@ export const useMessageStatus = () => {
|
|
|
12
12
|
const handleStatusUpdate = (event: MessageEvent) => {
|
|
13
13
|
try {
|
|
14
14
|
const parsed = JSON.parse(event.data);
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
if (parsed.event === 'messageStatusUpdated') {
|
|
17
17
|
const { messageId, status, chatId } = parsed.data;
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
setMessages(prev => prev.map(msg => {
|
|
20
20
|
if (msg._id === messageId) {
|
|
21
21
|
const statusOrder = ['sent', 'delivered', 'read'];
|
|
22
22
|
const currentIdx = statusOrder.indexOf(msg.status);
|
|
23
23
|
const newIdx = statusOrder.indexOf(status);
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
if (newIdx > currentIdx) {
|
|
26
26
|
return { ...msg, status };
|
|
27
27
|
}
|
|
@@ -53,7 +53,9 @@ export const useMessageStatus = () => {
|
|
|
53
53
|
event: 'confirmDelivery',
|
|
54
54
|
data: {
|
|
55
55
|
messageId,
|
|
56
|
-
chatId: selectedConversation._id
|
|
56
|
+
chatId: selectedConversation._id,
|
|
57
|
+
senderRole: message.sender.role,
|
|
58
|
+
senderId: message.sender.senderId,
|
|
57
59
|
}
|
|
58
60
|
}));
|
|
59
61
|
}
|
|
@@ -74,9 +76,9 @@ export const useMessageStatus = () => {
|
|
|
74
76
|
if (!socket || !selectedConversation) return;
|
|
75
77
|
|
|
76
78
|
const unreadMessages = messages.filter(
|
|
77
|
-
msg => msg.status !== 'read' &&
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
msg => msg.status !== 'read' &&
|
|
80
|
+
msg.conversationId === selectedConversation._id &&
|
|
81
|
+
msg.sender.senderId !== userId
|
|
80
82
|
);
|
|
81
83
|
|
|
82
84
|
if (unreadMessages.length > 0) {
|
|
@@ -84,7 +86,9 @@ export const useMessageStatus = () => {
|
|
|
84
86
|
event: 'messageRead',
|
|
85
87
|
data: {
|
|
86
88
|
messageIds: unreadMessages.map(m => m._id),
|
|
87
|
-
chatId: selectedConversation._id
|
|
89
|
+
chatId: selectedConversation._id,
|
|
90
|
+
senderRole: unreadMessages[0].sender.role,
|
|
91
|
+
senderId: unreadMessages[0].sender.senderId,
|
|
88
92
|
}
|
|
89
93
|
}));
|
|
90
94
|
}
|