@pubuduth-aplicy/chat-ui 2.1.73 → 2.1.74
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 +2 -1
- package/src/components/common/CollapsibleSection.tsx +40 -0
- package/src/components/common/VirtualizedChatList.tsx +57 -0
- package/src/components/messages/Message.tsx +2 -1
- package/src/components/messages/MessageContainer.tsx +82 -62
- package/src/components/messages/MessageInput.tsx +50 -11
- package/src/components/messages/Messages.tsx +76 -46
- package/src/components/sidebar/Conversation.tsx +122 -163
- package/src/components/sidebar/Conversations.tsx +270 -40
- package/src/components/sidebar/SearchInput.tsx +16 -54
- package/src/hooks/useMessageStatus.ts +97 -0
- package/src/service/messageService.ts +86 -61
- package/src/stores/Zustant.ts +9 -4
- package/src/style/style.css +168 -0
- package/src/types/type.ts +24 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pubuduth-aplicy/chat-ui",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.74",
|
|
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": "",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@tanstack/react-query": "^5.67.2",
|
|
16
|
+
"@tanstack/react-virtual": "^3.13.12",
|
|
16
17
|
"axios": "^1.8.2",
|
|
17
18
|
"lucide-react": "^0.514.0",
|
|
18
19
|
"react": "^19.0.0",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface CollapsibleSectionProps {
|
|
5
|
+
title: string;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
defaultOpen?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CollapsibleSection = ({
|
|
11
|
+
title,
|
|
12
|
+
children,
|
|
13
|
+
defaultOpen = true,
|
|
14
|
+
}: CollapsibleSectionProps) => {
|
|
15
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
16
|
+
|
|
17
|
+
const toggleOpen = () => {
|
|
18
|
+
setIsOpen(!isOpen);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="border-b border-gray-200">
|
|
23
|
+
<div
|
|
24
|
+
className="flex justify-between items-center p-2 cursor-pointer hover:bg-gray-50"
|
|
25
|
+
onClick={toggleOpen}
|
|
26
|
+
>
|
|
27
|
+
<p className="text-xs uppercase">{title}</p>
|
|
28
|
+
<ChevronDown
|
|
29
|
+
size={20}
|
|
30
|
+
className={`transform transition-transform duration-200 ${
|
|
31
|
+
isOpen ? "rotate-180" : ""
|
|
32
|
+
}`}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
{isOpen && <div className="p-2">{children}</div>}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default CollapsibleSection;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
3
|
+
import Conversation from "../sidebar/Conversation";
|
|
4
|
+
import { Conversation as ConversationType } from "../../types/type";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
conversations: ConversationType[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ITEM_HEIGHT = 60;
|
|
11
|
+
|
|
12
|
+
const VirtualizedChatList = ({ conversations }: Props) => {
|
|
13
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
const virtualizer = useVirtualizer({
|
|
16
|
+
count: conversations.length,
|
|
17
|
+
getScrollElement: () => parentRef.current,
|
|
18
|
+
estimateSize: () => ITEM_HEIGHT,
|
|
19
|
+
overscan: 5,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
ref={parentRef}
|
|
25
|
+
style={{
|
|
26
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
27
|
+
width: "100%",
|
|
28
|
+
position: "relative",
|
|
29
|
+
overflow: "auto",
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{virtualizer.getVirtualItems().map((item) => {
|
|
33
|
+
const conversation = conversations[item.index];
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
key={item.key}
|
|
37
|
+
style={{
|
|
38
|
+
position: "absolute",
|
|
39
|
+
top: 0,
|
|
40
|
+
left: 0,
|
|
41
|
+
width: "100%",
|
|
42
|
+
height: `${item.size}px`,
|
|
43
|
+
transform: `translateY(${item.start}px)`,
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<Conversation
|
|
47
|
+
conversation={conversation}
|
|
48
|
+
lastIdx={item.index === conversations.length - 1}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default VirtualizedChatList;
|
|
@@ -101,6 +101,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
101
101
|
// const handleDownload = (url: string, name: string) => {
|
|
102
102
|
// saveAs(url, name);
|
|
103
103
|
// };
|
|
104
|
+
console.log("check message status", message.status);
|
|
104
105
|
|
|
105
106
|
const [downloadingIndex, setDownloadingIndex] = useState<number | null>(null);
|
|
106
107
|
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
|
@@ -499,7 +500,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
499
500
|
fromMe && !showDeleteOption && setShowOptions(false)
|
|
500
501
|
}
|
|
501
502
|
>
|
|
502
|
-
{message.
|
|
503
|
+
{message.status === "deleted" ? (
|
|
503
504
|
<div className="chat-bubble compact-bubble deleted-message">
|
|
504
505
|
<div className="message-text">This message was deleted</div>
|
|
505
506
|
</div>
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { MessageSquare } from "lucide-react";
|
|
3
|
+
import { useEffect } from "react";
|
|
2
4
|
import MessageInput from "./MessageInput";
|
|
3
5
|
import Messages from "./Messages";
|
|
4
6
|
import useChatUIStore from "../../stores/Zustant";
|
|
5
7
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
6
|
-
import { getChatConfig } from "
|
|
8
|
+
import { getChatConfig } from "../../Chat.config";
|
|
7
9
|
|
|
8
10
|
const MessageContainer = () => {
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
onlineUsers,
|
|
13
|
-
setOnlineUsers,
|
|
14
|
-
setMessages,
|
|
15
|
-
} = useChatUIStore();
|
|
16
|
-
const { socket, sendMessage, isUserOnline } = useChatContext();
|
|
11
|
+
const { selectedConversation, setSelectedConversation, setOnlineUsers } =
|
|
12
|
+
useChatUIStore();
|
|
13
|
+
const { socket, isUserOnline } = useChatContext();
|
|
17
14
|
const { role } = getChatConfig();
|
|
18
|
-
const [joinedChats, setJoinedChats] = useState<Set<string>>(new Set());
|
|
19
|
-
|
|
20
15
|
// useEffect(() => {
|
|
21
16
|
// if (!socket) return;
|
|
22
17
|
|
|
@@ -66,42 +61,58 @@ const MessageContainer = () => {
|
|
|
66
61
|
|
|
67
62
|
useEffect(() => {
|
|
68
63
|
if (selectedConversation?._id && socket?.readyState === WebSocket.OPEN) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (unreadMessages.length > 0) {
|
|
75
|
-
sendMessage({
|
|
76
|
-
event: "messageRead",
|
|
64
|
+
// Send joinChat command to server via WebSocket
|
|
65
|
+
socket.send(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
event: "joinChat",
|
|
77
68
|
data: {
|
|
78
|
-
|
|
79
|
-
chatId: chatId,
|
|
69
|
+
chatId: selectedConversation._id,
|
|
80
70
|
},
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
// }
|
|
71
|
+
})
|
|
72
|
+
);
|
|
84
73
|
}
|
|
85
|
-
}, [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
74
|
+
}, [selectedConversation?._id, socket]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!socket) return;
|
|
78
|
+
|
|
79
|
+
const handleMessage = (event: MessageEvent) => {
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(event.data);
|
|
82
|
+
console.log("Parsed WebSocket message in mc:", data);
|
|
83
|
+
|
|
84
|
+
if (data.event === "getOnlineUsers") {
|
|
85
|
+
setOnlineUsers(data.payload); // payload should be an array of user IDs
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error("Failed to parse WebSocket message", err);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
socket.addEventListener("message", handleMessage);
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
socket.removeEventListener("message", handleMessage);
|
|
96
|
+
};
|
|
97
|
+
}, [socket, setOnlineUsers]);
|
|
92
98
|
|
|
93
99
|
// Listen for online users updates
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
const { userId } = useChatContext();
|
|
102
|
+
|
|
103
|
+
// const participantDetails = Array.isArray(selectedConversation?.participantDetails)
|
|
104
|
+
// ? selectedConversation?.participantDetails
|
|
105
|
+
// : [selectedConversation?.participantDetails].filter(Boolean);
|
|
100
106
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
// const participant = participantDetails.find(
|
|
108
|
+
// (p: any) => p._id !== userId
|
|
109
|
+
// );
|
|
110
|
+
|
|
111
|
+
const participant = selectedConversation?.participantDetails?.find(
|
|
112
|
+
(p: any) => p._id !== userId
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const isOnline = isUserOnline(participant?._id || "");
|
|
105
116
|
|
|
106
117
|
// Cleanup on unmount
|
|
107
118
|
useEffect(() => {
|
|
@@ -119,29 +130,38 @@ const MessageContainer = () => {
|
|
|
119
130
|
<button className="chatMessageContainerInnerDiv_button">
|
|
120
131
|
{/* <CaretLeft size={25} /> */}
|
|
121
132
|
</button>
|
|
122
|
-
|
|
123
|
-
className="
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
/>
|
|
133
|
+
{selectedConversation.type === "service" ? (
|
|
134
|
+
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center">
|
|
135
|
+
<MessageSquare size={24} className="text-gray-600" />
|
|
136
|
+
</div>
|
|
137
|
+
) : (
|
|
138
|
+
<img
|
|
139
|
+
className="chatMessageContainerInnerImg"
|
|
140
|
+
alt="Profile"
|
|
141
|
+
src={participant?.profilePic}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
134
144
|
<div className="chatMessageContainerOutter">
|
|
135
145
|
<div className="chatMessageContainerOutterDiv">
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
{selectedConversation.type === "service" ? (
|
|
147
|
+
<>
|
|
148
|
+
<p className="chatMessageContainerOutterDiv_name">
|
|
149
|
+
{selectedConversation.serviceTitle}
|
|
150
|
+
</p>
|
|
151
|
+
<p className="text-sm">
|
|
152
|
+
Booking ID: #{selectedConversation.bookingId}
|
|
153
|
+
</p>
|
|
154
|
+
</>
|
|
155
|
+
) : (
|
|
156
|
+
<>
|
|
157
|
+
<p className="chatMessageContainerOutterDiv_name">
|
|
158
|
+
{participant?.firstname || ""}
|
|
159
|
+
</p>
|
|
160
|
+
<p className="text-sm">
|
|
161
|
+
{isOnline ? "Online" : "Offline"}
|
|
162
|
+
</p>
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
145
165
|
</div>
|
|
146
166
|
</div>
|
|
147
167
|
</div>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
3
|
import { useMessageMutation } from "../../hooks/mutations/useSendMessage";
|
|
3
4
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
4
5
|
import useChatUIStore from "../../stores/Zustant";
|
|
5
|
-
import paperplane from "../../assets/icons8-send-50.png";
|
|
6
6
|
import { FilePreview, FileType } from "../common/FilePreview";
|
|
7
7
|
import { getApiClient } from "../../lib/api/apiClient";
|
|
8
8
|
import { MessageStatus } from "../../types/type";
|
|
@@ -44,6 +44,7 @@ const MessageInput = () => {
|
|
|
44
44
|
const mutation = useMessageMutation();
|
|
45
45
|
const [typingUser, setTypingUser] = useState<string | null>(null);
|
|
46
46
|
const [isSending, setIsSending] = useState(false);
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
47
48
|
const [isTyping, setIsTyping] = useState(false);
|
|
48
49
|
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
|
49
50
|
const [showAttachmentOptions, setShowAttachmentOptions] = useState(false);
|
|
@@ -55,7 +56,6 @@ const MessageInput = () => {
|
|
|
55
56
|
const typingTimeoutRef = useRef<number | null>(null);
|
|
56
57
|
const generateTempId = () =>
|
|
57
58
|
`temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
58
|
-
|
|
59
59
|
// Join chat room when conversation is selected
|
|
60
60
|
useEffect(() => {
|
|
61
61
|
if (selectedConversation?._id && socket?.readyState === WebSocket.OPEN) {
|
|
@@ -68,6 +68,20 @@ const MessageInput = () => {
|
|
|
68
68
|
}
|
|
69
69
|
}, [selectedConversation?._id, socket, sendMessage]);
|
|
70
70
|
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
// Clear all input state when conversation changes
|
|
73
|
+
setMessage("");
|
|
74
|
+
setMessage1("");
|
|
75
|
+
setAttachments([]);
|
|
76
|
+
setInputError(null);
|
|
77
|
+
setShowAttachmentOptions(false);
|
|
78
|
+
|
|
79
|
+
// Clean up any existing file input
|
|
80
|
+
if (fileInputRef.current) {
|
|
81
|
+
fileInputRef.current.value = "";
|
|
82
|
+
}
|
|
83
|
+
}, [selectedConversation?._id]);
|
|
84
|
+
|
|
71
85
|
// Typing indicator logic
|
|
72
86
|
useEffect(() => {
|
|
73
87
|
if (!socket || !selectedConversation?._id) return;
|
|
@@ -211,6 +225,18 @@ const MessageInput = () => {
|
|
|
211
225
|
});
|
|
212
226
|
};
|
|
213
227
|
|
|
228
|
+
// const participantDetails = Array.isArray(selectedConversation?.participantDetails)
|
|
229
|
+
// ? selectedConversation?.participantDetails
|
|
230
|
+
// : [selectedConversation?.participantDetails].filter(Boolean);
|
|
231
|
+
|
|
232
|
+
// const otherParticipant = participantDetails.find(
|
|
233
|
+
// (p: any) => p._id !== userId
|
|
234
|
+
// );
|
|
235
|
+
|
|
236
|
+
const otherParticipant = selectedConversation?.participantDetails?.find(
|
|
237
|
+
(p:any) => p._id !== userId
|
|
238
|
+
);
|
|
239
|
+
|
|
214
240
|
const handleSubmit = useCallback(
|
|
215
241
|
async (e: React.FormEvent) => {
|
|
216
242
|
e.preventDefault();
|
|
@@ -294,14 +320,31 @@ const MessageInput = () => {
|
|
|
294
320
|
|
|
295
321
|
const successfulUploads = uploadedFiles.filter((file) => file !== null);
|
|
296
322
|
|
|
323
|
+
if (!otherParticipant?._id) {
|
|
324
|
+
console.error("Cannot send message: receiver ID is missing.");
|
|
325
|
+
setIsSending(false);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
297
329
|
mutation.mutate(
|
|
298
330
|
{
|
|
299
|
-
|
|
300
|
-
!Array.isArray(selectedConversation?.participantDetails) &&
|
|
301
|
-
selectedConversation?.participantDetails._id,
|
|
331
|
+
receiverId: otherParticipant._id,
|
|
302
332
|
senderId: userId,
|
|
303
333
|
message: message1,
|
|
304
334
|
attachments: successfulUploads,
|
|
335
|
+
bookingId:
|
|
336
|
+
selectedConversation?.type === "service"
|
|
337
|
+
? selectedConversation?.bookingId
|
|
338
|
+
: undefined,
|
|
339
|
+
serviceTitle:
|
|
340
|
+
selectedConversation?.type === "service"
|
|
341
|
+
? selectedConversation?.title
|
|
342
|
+
: undefined,
|
|
343
|
+
type: selectedConversation?.type,
|
|
344
|
+
serviceId:
|
|
345
|
+
selectedConversation?.type === "service"
|
|
346
|
+
? selectedConversation?.serviceId
|
|
347
|
+
: undefined,
|
|
305
348
|
},
|
|
306
349
|
{
|
|
307
350
|
onSuccess: (data) => {
|
|
@@ -328,9 +371,7 @@ const MessageInput = () => {
|
|
|
328
371
|
messageId: data[1]._id,
|
|
329
372
|
attachments: successfulUploads,
|
|
330
373
|
senderId: userId,
|
|
331
|
-
receiverId:
|
|
332
|
-
!Array.isArray(selectedConversation?.participantDetails) &&
|
|
333
|
-
selectedConversation?.participantDetails._id,
|
|
374
|
+
receiverId: otherParticipant._id,
|
|
334
375
|
},
|
|
335
376
|
});
|
|
336
377
|
},
|
|
@@ -677,9 +718,7 @@ const MessageInput = () => {
|
|
|
677
718
|
|
|
678
719
|
{typingUser &&
|
|
679
720
|
typingUser !== userId &&
|
|
680
|
-
typingUser ===
|
|
681
|
-
(!Array.isArray(selectedConversation?.participantDetails) &&
|
|
682
|
-
selectedConversation?.participantDetails?._id) &&
|
|
721
|
+
typingUser === otherParticipant?._id &&
|
|
683
722
|
!isSending && (
|
|
684
723
|
<div className="typingIndicator">
|
|
685
724
|
<div className="typing-loader">
|
|
@@ -21,6 +21,7 @@ const Messages = () => {
|
|
|
21
21
|
fetchMessages(selectedConversation?._id, userId, pageParam),
|
|
22
22
|
getNextPageParam: (lastPage) => lastPage.nextPage,
|
|
23
23
|
initialPageParam: 1,
|
|
24
|
+
enabled: !!selectedConversation?._id, // Prevent fetching if no conversation is selected
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
// Handle infinite scroll
|
|
@@ -45,7 +46,15 @@ const Messages = () => {
|
|
|
45
46
|
const parsed = JSON.parse(event.data);
|
|
46
47
|
console.log("Parsed WebSocket message1:", parsed);
|
|
47
48
|
if (parsed.type === "newMessage" || parsed.event === "newMessage") {
|
|
48
|
-
const newMessage = parsed.
|
|
49
|
+
const newMessage = parsed.data;
|
|
50
|
+
if (!newMessage) {
|
|
51
|
+
console.warn(
|
|
52
|
+
"Received newMessage event without a message payload",
|
|
53
|
+
parsed
|
|
54
|
+
);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
newMessage.shouldShake = true;
|
|
50
59
|
|
|
51
60
|
setMessages((prevMessages) => {
|
|
@@ -61,7 +70,7 @@ const Messages = () => {
|
|
|
61
70
|
});
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
const statusOrder = ["sent", "delivered", "read"];
|
|
73
|
+
const statusOrder = ["sent", "delivered", "read", "edited", "deleted"];
|
|
65
74
|
if (parsed.event === "messageStatusUpdated") {
|
|
66
75
|
const { messageId, status } = parsed.data || {};
|
|
67
76
|
if (!messageId) {
|
|
@@ -69,22 +78,58 @@ const Messages = () => {
|
|
|
69
78
|
return;
|
|
70
79
|
}
|
|
71
80
|
|
|
72
|
-
console.log(`Updating status for ${messageId} to ${status}`);
|
|
73
81
|
setMessages((prev) =>
|
|
74
82
|
prev.map((msg) => {
|
|
75
83
|
if (msg._id !== messageId) return msg;
|
|
76
84
|
|
|
77
|
-
// Only update if new status is higher than current status
|
|
78
85
|
const currentIdx = statusOrder.indexOf(msg.status);
|
|
79
86
|
const newIdx = statusOrder.indexOf(status);
|
|
87
|
+
if (newIdx === -1 || currentIdx === -1 || newIdx <= currentIdx)
|
|
88
|
+
return msg;
|
|
89
|
+
|
|
90
|
+
if (newIdx > currentIdx) {
|
|
91
|
+
console.log(`Updating status for ${messageId} to ${status}`);
|
|
92
|
+
return { ...msg, status };
|
|
93
|
+
}
|
|
80
94
|
|
|
81
|
-
return
|
|
82
|
-
...msg,
|
|
83
|
-
status: newIdx > currentIdx ? status : msg.status,
|
|
84
|
-
};
|
|
95
|
+
return msg; // No update if new status isn't higher
|
|
85
96
|
})
|
|
86
97
|
);
|
|
87
98
|
}
|
|
99
|
+
|
|
100
|
+
if (parsed.event === "messageEdited") {
|
|
101
|
+
console.log("Received messageEdited event:", parsed);
|
|
102
|
+
|
|
103
|
+
const updatedMessage = parsed.data;
|
|
104
|
+
if (!updatedMessage || !updatedMessage.messageId) return;
|
|
105
|
+
|
|
106
|
+
setMessages((prevMessages) =>
|
|
107
|
+
prevMessages.map((msg) =>
|
|
108
|
+
msg._id === updatedMessage.messageId
|
|
109
|
+
? { ...msg, message: updatedMessage.message, status: "edited" }
|
|
110
|
+
: msg
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (parsed.event === "messageDeleted") {
|
|
116
|
+
console.log("Received messageDeleted event:", parsed);
|
|
117
|
+
|
|
118
|
+
const { messageId } = parsed.data || {};
|
|
119
|
+
if (!messageId) return;
|
|
120
|
+
|
|
121
|
+
setMessages((prevMessages) =>
|
|
122
|
+
prevMessages.map((msg) =>
|
|
123
|
+
msg._id === messageId
|
|
124
|
+
? {
|
|
125
|
+
...msg,
|
|
126
|
+
message: "This message was deleted",
|
|
127
|
+
status: "deleted",
|
|
128
|
+
}
|
|
129
|
+
: msg
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
88
133
|
} catch (error) {
|
|
89
134
|
console.error("Error parsing WebSocket message:", error);
|
|
90
135
|
}
|
|
@@ -97,56 +142,41 @@ const Messages = () => {
|
|
|
97
142
|
};
|
|
98
143
|
}, [socket, selectedConversation?._id, setMessages, userId]);
|
|
99
144
|
|
|
100
|
-
const sendDeliveryConfirmation = (messageId: string) => {
|
|
101
|
-
if (!socket) return;
|
|
102
|
-
|
|
103
|
-
const message = {
|
|
104
|
-
event: "confirmDelivery",
|
|
105
|
-
data: {
|
|
106
|
-
messageId,
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
// timestamp: Date.now()
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
socket.send(JSON.stringify(message));
|
|
113
|
-
};
|
|
114
|
-
|
|
115
145
|
// Scroll to bottom when messages change
|
|
116
146
|
useEffect(() => {
|
|
117
147
|
if (messages.length > 0) {
|
|
118
148
|
setTimeout(() => {
|
|
119
149
|
lastMessageRef.current?.scrollIntoView({
|
|
120
150
|
behavior: "smooth",
|
|
121
|
-
block: "
|
|
151
|
+
block: "nearest",
|
|
122
152
|
});
|
|
123
153
|
}, 100);
|
|
124
154
|
}
|
|
125
155
|
}, [messages.length]);
|
|
126
156
|
|
|
127
157
|
// Track message visibility for read receipts
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}, [messages, socket]);
|
|
158
|
+
// useEffect(() => {
|
|
159
|
+
// if (!socket || !messages.length) return;
|
|
160
|
+
|
|
161
|
+
// const observer = new IntersectionObserver(
|
|
162
|
+
// (entries) => {
|
|
163
|
+
// entries.forEach((entry) => {
|
|
164
|
+
// if (entry.isIntersecting) {
|
|
165
|
+
// const messageId = entry.target.getAttribute("data-message-id");
|
|
166
|
+
// if (messageId) {
|
|
167
|
+
// sendDeliveryConfirmation(messageId);
|
|
168
|
+
// }
|
|
169
|
+
// }
|
|
170
|
+
// });
|
|
171
|
+
// },
|
|
172
|
+
// { threshold: 0.5 }
|
|
173
|
+
// );
|
|
174
|
+
|
|
175
|
+
// const messageElements = document.querySelectorAll("[data-message-id]");
|
|
176
|
+
// messageElements.forEach((el) => observer.observe(el));
|
|
177
|
+
|
|
178
|
+
// return () => observer.disconnect();
|
|
179
|
+
// }, [messages, socket]);
|
|
150
180
|
|
|
151
181
|
return (
|
|
152
182
|
<div
|