@pubuduth-aplicy/chat-ui 2.1.67 → 2.1.69

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.67",
3
+ "version": "2.1.69",
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": "",
@@ -14,6 +14,7 @@
14
14
  "dependencies": {
15
15
  "@tanstack/react-query": "^5.67.2",
16
16
  "axios": "^1.8.2",
17
+ "lucide-react": "^0.514.0",
17
18
  "react": "^19.0.0",
18
19
  "react-dom": "^19.0.0",
19
20
  "react-intersection-observer": "^9.16.0",
@@ -4,74 +4,47 @@ import MessageContainer from "./messages/MessageContainer";
4
4
  import { Sidebar } from "./sidebar/Sidebar";
5
5
  import { useChatContext } from "../providers/ChatProvider";
6
6
 
7
- // import MessageContainer from './components/messages/MessageContainer'
8
- // import useConversation from '../../zustand/useConversation';
9
-
10
7
  export const Chat = () => {
11
- const { selectedConversation, messages, setMessages, updateMessageStatus } =
8
+ const { messages, setMessages, updateMessageStatus } =
12
9
  useChatUIStore();
13
10
  const { socket } = useChatContext();
11
+
14
12
  useEffect(() => {
15
- socket.on("receiveMessage", (messageData) => {
16
- setMessages([...messages, { ...messageData, status: "sent" }]);
17
- });
13
+ if (!socket) return;
14
+
15
+ const handleMessage = (event: MessageEvent) => {
16
+ try {
17
+ const data = JSON.parse(event.data);
18
+
19
+ if (data.type === "receiveMessage") {
20
+ setMessages([...messages, { ...data.message, status: "sent" }]);
21
+ }
22
+
23
+ if (data.type === "messageDelivered") {
24
+ updateMessageStatus(data.messageId, "delivered");
25
+ }
26
+ } catch (error) {
27
+ console.error("Error parsing WebSocket message:", error);
28
+ }
29
+ };
18
30
 
19
- socket.on("messageDelivered", ({ messageId }) => {
20
- updateMessageStatus(messageId, "delivered");
21
- });
31
+ socket.addEventListener("message", handleMessage);
22
32
 
23
33
  return () => {
24
- socket.off("receiveMessage");
25
- socket.off("messageDelivered");
34
+ socket.removeEventListener("message", handleMessage);
26
35
  };
27
- }, [messages, setMessages, updateMessageStatus]);
36
+ }, [socket, messages, setMessages, updateMessageStatus]);
37
+
28
38
  return (
29
- <>
30
- <div className="container mx-auto mb-5">
31
- <div className="grid-container">
32
- {selectedConversation ? (
33
- <>
34
- <div className={`sidebarContainer`}>
35
- <Sidebar />
36
- </div>
37
- <div className="messageContainer">
38
- <MessageContainer />
39
- </div>
40
- </>
41
- ) : (
42
- <>
43
- <div className="sidebarContainer">
44
- <Sidebar />
45
- </div>
46
- <div className="messageContainer">
47
- <MessageContainer />
48
- </div>
49
- </>
50
- )}
39
+ <div className="container mx-auto mb-5">
40
+ <div className="grid-container">
41
+ <div className={`sidebarContainer`}>
42
+ <Sidebar />
43
+ </div>
44
+ <div className="messageContainer">
45
+ <MessageContainer />
51
46
  </div>
52
47
  </div>
53
- </>
48
+ </div>
54
49
  );
55
- };
56
-
57
- // const MessageBody = () => {
58
-
59
- // return (
60
- // <><div className='flex-grow px-8 pt-8 text-left text-gray-700 overflow-y-auto'>
61
- // <div className="relative mb-6 text-left">
62
- // <div className="text-gray-700">
63
- // <div className="absolute inset-x-0 top-0">
64
- // <img src="/images/fR71TFZIDTv2jhvKsOMhC.png" alt className="float-right inline-block h-6 w-6 sm:h-12 sm:w-12 rounded-full" />
65
- // </div>
66
- // <div className="relative float-right mr-8 sm:mr-16 inline-block rounded-md bg-blue-700 py-3 px-4 text-white">
67
- // <p className="text-sm">Hi, John</p>
68
- // </div>
69
- // </div>
70
- // <div className="clear-both flex text-gray-700" />
71
- // </div>
72
- // </div><div className="relative mt-4 flex items-start border-t border-gray-300 sm:p-8 py-4 text-left text-gray-700">
73
- // <input placeholder="Your Message" className="mr-4 overflow-hidden w-full flex-1 cursor-text resize-none whitespace-pre-wrap rounded-md bg-white text-sm py-2 sm:py-0 font-normal text-gray-600 opacity-70 shadow-none outline-none focus:text-gray-600 focus:opacity-100" defaultValue={""} />
74
- // <button className="relative inline-flex h-10 w-auto flex-initial cursor-pointer items-center justify-center self-center rounded-md bg-blue-700 px-6 text-center align-middle text-sm font-medium text-white outline-none focus:ring-2">Send</button>
75
- // </div></>
76
- // );
77
- // };
50
+ };
@@ -1,10 +1,15 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
1
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
3
  import { MessageStatus } from "../../types/type";
3
4
  import { useChatContext } from "../../providers/ChatProvider";
4
- import { useEffect, useState } from "react";
5
5
  import { FileType } from "../common/FilePreview";
6
6
  import { getChatConfig } from "../../Chat.config";
7
7
  import { Path } from "../../lib/api/endpoint";
8
+ import { MoreHorizontal, Pencil, Trash2 } from "lucide-react"
9
+ import { useEditMessageMutation } from "../../hooks/mutations/useEditMessage";
10
+ import { useDeleteMessageMutation } from "../../hooks/mutations/useDeleteMessage"
11
+ import { useEffect, useRef, useState } from "react";
12
+
8
13
 
9
14
  interface MessageProps {
10
15
  message: {
@@ -22,17 +27,72 @@ interface MessageProps {
22
27
  uploadError: string | null;
23
28
  }[];
24
29
  isUploading?: boolean;
30
+ isEdited?: boolean
31
+ isDeleted?: boolean
32
+ onEdit?: (messageId: string, newMessage: string) => void
33
+ onDelete?: (messageId: string) => void
34
+ type?: 'user' | 'system' | 'system-completion';
35
+ meta?: {
36
+ bookingDetails?: {
37
+ serviceId: string;
38
+ date: string;
39
+ time: string;
40
+ price: number;
41
+ // Add other booking details as needed
42
+ };
43
+ reviewLink?: string;
44
+ };
25
45
  };
26
46
  }
27
47
 
28
48
  const Message = ({ message }: MessageProps) => {
29
49
  const { userId } = useChatContext();
30
50
  const { apiUrl } = getChatConfig();
51
+
52
+ if (message.type === 'system') {
53
+ return (
54
+ <div className="system-message booking-details">
55
+ <h4>Booking Confirmed</h4>
56
+ <div className="details">
57
+ <p>Service: {message.meta?.bookingDetails?.serviceId}</p>
58
+ <p>Date: {message.meta?.bookingDetails?.date}</p>
59
+ <p>Time: {message.meta?.bookingDetails?.time}</p>
60
+ <p>Price: ${message.meta?.bookingDetails?.price}</p>
61
+ </div>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ if (message.type === 'system-completion') {
67
+ return (
68
+ <div className="system-message completion-notice">
69
+ <p>Service completed successfully!</p>
70
+ <button
71
+ onClick={() => window.location.href = message.meta?.reviewLink || '#'}
72
+ className="review-button"
73
+ >
74
+ Leave a Review
75
+ </button>
76
+ </div>
77
+ );
78
+ }
79
+
80
+
81
+
31
82
  const fromMe = message.senderId === userId;
32
83
  const timestamp = fromMe ? "timestamp_outgoing" : "timestamp_incomeing";
33
84
  const alignItems = fromMe ? "outgoing" : "incoming";
34
85
  const [localStatus, setLocalStatus] = useState(message.status);
35
- const [downloadingIndex, setDownloadingIndex] = useState<number | null>(null);
86
+
87
+ const [showOptions, setShowOptions] = useState(false)
88
+ const [showDeleteOption, setShowDeleteOption] = useState(false)
89
+ const editInputRef = useRef<HTMLInputElement>(null)
90
+ const optionsRef = useRef<HTMLDivElement>(null)
91
+ const { mutate: editMessage} = useEditMessageMutation();
92
+ const [editedMessage, setEditedMessage] = useState('');
93
+ const [isEditingMode, setIsEditingMode] = useState(false);
94
+ const { mutate: deleteMessage} = useDeleteMessageMutation();
95
+
36
96
 
37
97
  useEffect(() => {
38
98
  setLocalStatus(message.status);
@@ -42,7 +102,7 @@ const Message = ({ message }: MessageProps) => {
42
102
  // saveAs(url, name);
43
103
  // };
44
104
 
45
- // const [downloadingIndex, setDownloadingIndex] = useState<number | null>(null)
105
+ const [downloadingIndex, setDownloadingIndex] = useState<number | null>(null)
46
106
  const [downloadProgress, setDownloadProgress] = useState<number>(0)
47
107
  const [downloadController, setDownloadController] = useState<AbortController | null>(null)
48
108
 
@@ -309,15 +369,120 @@ const Message = ({ message }: MessageProps) => {
309
369
  );
310
370
  };
311
371
 
372
+
373
+ const handleEditClick = () => {
374
+ setIsEditingMode(true)
375
+ setShowOptions(false)
376
+ setTimeout(() => {
377
+ editInputRef.current?.focus()
378
+ }, 0)
379
+ }
380
+
381
+ const handleSaveEdit = () => {
382
+ if (message._id && editedMessage.trim() !== message.message) {
383
+ editMessage({
384
+ messageId: message._id ?? '',
385
+ userId: userId, // Using userId from useChatContext
386
+ newMessage: editedMessage.trim()
387
+ }, {
388
+ onSuccess: () => {
389
+ setIsEditingMode(false);
390
+ // Any additional success handling
391
+ },
392
+ onError: (error:any) => {
393
+ // Handle error specifically for this edit
394
+ console.error("Edit failed:", error);
395
+ }
396
+ });
397
+ } else {
398
+ setIsEditingMode(false);
399
+ }
400
+ }
401
+
402
+ const handleCancelEdit = () => {
403
+ setEditedMessage(message.message)
404
+ setIsEditingMode(false)
405
+ }
406
+
407
+ const handleKeyDown = (e: React.KeyboardEvent) => {
408
+ if (e.key === "Enter") {
409
+ handleSaveEdit()
410
+ } else if (e.key === "Escape") {
411
+ handleCancelEdit()
412
+ }
413
+ }
414
+
415
+ const handleDeleteClick = () => {
416
+ deleteMessage({
417
+ messageId: message._id ?? '',
418
+ userId: userId // Get this from your auth context
419
+ });
420
+ };
421
+
422
+
312
423
  return (
313
424
  <div className="chat-container">
314
425
  <div className={`message-row ${alignItems}`}>
315
- <div className="bubble-container">
316
- {(message.message || (message.media && message.media.length > 0)) && (
426
+ <div
427
+ className="bubble-container"
428
+ onMouseEnter={() => fromMe && setShowOptions(true)}
429
+ onMouseLeave={() => fromMe && !showDeleteOption && setShowOptions(false)}
430
+ >
431
+ {message.isDeleted ? (
432
+ <div className="chat-bubble compact-bubble deleted-message">
433
+ <div className="message-text">This message was deleted</div>
434
+ </div>
435
+ ) : (message.message || (message.media && message.media.length > 0)) && (
317
436
  <div className="chat-bubble compact-bubble">
318
437
  {renderMedia()}
319
- {message.message && (
320
- <div className="message-text">{message.message}</div>
438
+ {isEditingMode ? (
439
+ <div className="edit-message-container">
440
+ <input
441
+ ref={editInputRef}
442
+ type="text"
443
+ value={editedMessage}
444
+ onChange={(e) => setEditedMessage(e.target.value)}
445
+ onKeyDown={handleKeyDown}
446
+ className="edit-message-input"
447
+ />
448
+ <div className="edit-actions">
449
+ <button className="save-edit" onClick={handleSaveEdit}>
450
+ Save
451
+ </button>
452
+ <button className="cancel-edit" onClick={handleCancelEdit}>
453
+ Cancel
454
+ </button>
455
+ </div>
456
+ </div>
457
+ ) : (
458
+ message.message && <div className="message-text">{message.message}</div>
459
+ )}
460
+
461
+ {/* Message options for outgoing messages */}
462
+ {fromMe && showOptions && !isEditingMode && !message.isDeleted && (
463
+ <div className="message-options">
464
+ <button className="message-option-btn edit-btn" onClick={handleEditClick} title="Edit">
465
+ <Pencil size={16} />
466
+ </button>
467
+ <div className="more-options-container" ref={optionsRef}>
468
+ <button
469
+ className="message-option-btn more-btn"
470
+ onClick={() => setShowDeleteOption(!showDeleteOption)}
471
+ title="More options"
472
+ >
473
+ <MoreHorizontal size={16} />
474
+ </button>
475
+
476
+ {showDeleteOption && (
477
+ <div className="delete-option">
478
+ <button className="delete-btn" onClick={handleDeleteClick}>
479
+ <Trash2 size={16} />
480
+ <span>Delete</span>
481
+ </button>
482
+ </div>
483
+ )}
484
+ </div>
485
+ </div>
321
486
  )}
322
487
  </div>
323
488
  )}
@@ -326,6 +491,7 @@ const Message = ({ message }: MessageProps) => {
326
491
  hour: "2-digit",
327
492
  minute: "2-digit",
328
493
  })}
494
+ {message.isEdited && !message.isDeleted && <span className="edited-label">edited</span>}
329
495
  <span className="status-icon">{getStatusIcon()}</span>
330
496
  </div>
331
497
  </div>
@@ -1,11 +1,9 @@
1
- import { useEffect } from "react";
2
- // import useConversation from "../../zustand/useConversation";
1
+ import { useEffect, useState } from "react";
3
2
  import MessageInput from "./MessageInput";
4
3
  import Messages from "./Messages";
5
4
  import useChatUIStore from "../../stores/Zustant";
6
5
  import { useChatContext } from "../../providers/ChatProvider";
7
- // import { Chat, CaretLeft } from "@phosphor-icons/react";
8
- // import { useAuthContext } from "../../context/AuthContext";
6
+ import { getChatConfig } from "@pubuduth-aplicy/chat-ui";
9
7
 
10
8
  const MessageContainer = () => {
11
9
  const {
@@ -14,34 +12,60 @@ const MessageContainer = () => {
14
12
  onlineUsers,
15
13
  setOnlineUsers,
16
14
  } = useChatUIStore();
17
- const { socket } = useChatContext();
15
+ const { socket, sendMessage } = useChatContext();
16
+ const {role}= getChatConfig()
17
+ const [joinedChats, setJoinedChats] = useState<Set<string>>(new Set());
18
18
 
19
+ // Join chat room when conversation is selected
19
20
  useEffect(() => {
20
- if (selectedConversation?._id && socket) {
21
- socket.emit("joinChat", selectedConversation._id); // Join chat room
21
+ if (selectedConversation?._id && socket?.readyState === WebSocket.OPEN) {
22
+ const chatId = selectedConversation._id;
23
+ if (!joinedChats.has(chatId)) {
24
+ sendMessage({
25
+ type: "joinChat",
26
+ chatId: chatId,
27
+ });
28
+ setJoinedChats(new Set(joinedChats).add(chatId));
29
+ }
22
30
  }
23
- }, [selectedConversation?._id, socket]);
31
+ }, [selectedConversation?._id, socket, sendMessage, joinedChats]);
24
32
 
33
+ // Listen for online users updates
25
34
  useEffect(() => {
26
35
  if (!socket) return;
27
36
 
28
- const handleOnlineUsers = (users: string[]) => {
29
- setOnlineUsers(users);
37
+ const handleMessage = (event: MessageEvent) => {
38
+ try {
39
+ const data = JSON.parse(event.data);
40
+ if (data.type === "getOnlineUsers") {
41
+ setOnlineUsers(data.users);
42
+ }
43
+ } catch (error) {
44
+ console.error("Error parsing message:", error);
45
+ }
30
46
  };
31
47
 
32
- socket.on("getOnlineUsers", handleOnlineUsers);
48
+ socket.addEventListener("message", handleMessage);
33
49
 
34
50
  return () => {
35
- socket.off("getOnlineUsers", handleOnlineUsers);
51
+ socket.removeEventListener("message", handleMessage);
36
52
  };
37
53
  }, [socket, setOnlineUsers]);
38
54
 
55
+ role === 'admin' && Array.isArray(selectedConversation?.participantDetails)
56
+ ? selectedConversation.participantDetails[1]?.profilePic
57
+ : !Array.isArray(selectedConversation?.participantDetails)
58
+ ? selectedConversation?.participantDetails?.profilePic
59
+ : undefined;
60
+
39
61
  const isUserOnline =
40
- selectedConversation?.participantDetails?._id &&
41
- onlineUsers?.includes(selectedConversation.participantDetails._id);
62
+ !Array.isArray(selectedConversation?.participantDetails) &&
63
+ !!selectedConversation?.participantDetails?._id &&
64
+ onlineUsers?.includes(selectedConversation.participantDetails._id);
65
+
42
66
 
67
+ // Cleanup on unmount
43
68
  useEffect(() => {
44
- // cleanup function (unmounts)
45
69
  return () => setSelectedConversation(null);
46
70
  }, [setSelectedConversation]);
47
71
 
@@ -60,44 +84,34 @@ const MessageContainer = () => {
60
84
  className="chatMessageContainerInnerImg"
61
85
  alt="Profile"
62
86
  src={
63
- selectedConversation.participantDetails?.profilePic ||
64
- selectedConversation.participantDetails?.idpic
87
+ role === 'admin' && Array.isArray(selectedConversation?.participantDetails)
88
+ ? selectedConversation.participantDetails[1]?.profilePic
89
+ : !Array.isArray(selectedConversation?.participantDetails)
90
+ ? selectedConversation?.participantDetails?.profilePic
91
+ : undefined
65
92
  }
66
93
  />
67
94
  <div className="chatMessageContainerOutter">
68
95
  <div className="chatMessageContainerOutterDiv">
69
96
  <p className="chatMessageContainerOutterDiv_name">
70
- {selectedConversation.participantDetails.firstname}
97
+ {role === 'admin' && Array.isArray(selectedConversation?.participantDetails)
98
+ ? selectedConversation.participantDetails[1]?.firstname
99
+ : !Array.isArray(selectedConversation?.participantDetails)
100
+ ? selectedConversation?.participantDetails?.firstname
101
+ : undefined
102
+ }
71
103
  </p>
72
- <p className="text-sm ">
104
+ <p className="text-sm">
73
105
  {isUserOnline ? "Online" : "Offline"}
74
106
  </p>
75
107
  </div>
76
108
  </div>
77
- {/* <h4 className=" inline-block py-2 text-left font-sans font-semibold normal-case">Lara Abegnale</h4> */}
78
109
  </div>
79
110
  </div>
80
111
 
81
- {/* <div className="h-14 overflow-x-hidden">
82
- <div className="top-0 h-14 px-4 py-4 w-full border-b border-gray-300 justify-start items-start gap-2 inline-flex sticky z-10">
83
- <div className="grow shrink basis-0 self-stretch py-2 justify-start items-center gap-4 flex">
84
- <button onClick={() => setSelectedConversation(null)} className="text-blue-500 md:hidden">
85
- <CaretLeft size={25} />
86
- </button>
87
- <img className="w-10 h-10 rounded-circle" src={selectedConversation.profile} alt="Profile" />
88
- <div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
89
- <div className="self-stretch justify-start items-center inline-flex">
90
- <div className="grow shrink basis-0 text-slate-900 text-base font-semibold font-inter leading-tight">
91
- {selectedConversation.username}
92
- </div>
93
- </div>
94
- </div>
95
- </div>
96
- </div>
97
- </div> */}
98
-
99
112
  <Messages />
100
- <MessageInput />
113
+ {role !== 'admin'&& (<MessageInput />)}
114
+
101
115
  </>
102
116
  )}
103
117
  </div>
@@ -106,6 +120,7 @@ const MessageContainer = () => {
106
120
 
107
121
  export default MessageContainer;
108
122
 
123
+ // EmptyInbox component remains the same
109
124
  interface EmptyInboxProps {
110
125
  title?: string;
111
126
  description?: string;
@@ -198,4 +213,4 @@ const EmptyInbox: React.FC<EmptyInboxProps> = ({
198
213
  <p className="text-gray-500 max-w-sm">{description}</p>
199
214
  </div>
200
215
  );
201
- };
216
+ };