@pubuduth-aplicy/chat-ui 2.1.74 → 2.1.76
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/README.md +143 -40
- package/package.json +1 -1
- package/src/Chat.config.ts +1 -0
- package/src/components/Chat.tsx +1 -1
- package/src/components/common/CollapsibleSection.tsx +2 -2
- package/src/components/messages/Message.tsx +36 -34
- package/src/components/messages/MessageContainer.tsx +13 -11
- package/src/components/messages/MessageInput.tsx +17 -10
- package/src/components/sidebar/Conversation.tsx +3 -27
- package/src/components/sidebar/Conversations.tsx +6 -1
- package/src/components/sidebar/SearchInput.tsx +2 -2
- package/src/hooks/useMessageStatus.ts +0 -5
- package/src/providers/ChatProvider.tsx +16 -12
- package/src/service/messageService.ts +5 -4
- package/src/service/sidebarApi.ts +2 -5
- package/src/style/style.css +92 -0
- package/src/ChatWindow.tsx +0 -15
package/README.md
CHANGED
|
@@ -1,50 +1,153 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pubuduth-aplicy/chat-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A flexible and easy-to-use React chat UI component.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Description
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
7
|
+
`@pubuduth-aplicy/chat-ui` provides a complete chat interface that can be easily integrated into any React application. It includes features like real-time messaging, user presence, and a customizable interface. The component is built with TypeScript, React, and Zustand for state management.
|
|
9
8
|
|
|
10
|
-
##
|
|
9
|
+
## Installation
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
To install the package, use npm or yarn:
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
```bash
|
|
14
|
+
npm install @pubuduth-aplicy/chat-ui
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
or
|
|
15
18
|
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
languageOptions: {
|
|
19
|
-
// other options...
|
|
20
|
-
parserOptions: {
|
|
21
|
-
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
22
|
-
tsconfigRootDir: import.meta.dirname,
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
})
|
|
19
|
+
```bash
|
|
20
|
+
yarn add @pubuduth-aplicy/chat-ui
|
|
26
21
|
```
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
To use the chat component, you need to initialize the configuration, wrap your application with the `ChatProvider`, and then render the `Chat` component.
|
|
26
|
+
|
|
27
|
+
### 1. Initialize the Configuration
|
|
28
|
+
|
|
29
|
+
First, you need to initialize the chat configuration at the entry point of your application (e.g., `index.tsx` or `main.tsx`).
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// src/main.tsx
|
|
33
|
+
import React from 'react';
|
|
34
|
+
import ReactDOM from 'react-dom/client';
|
|
35
|
+
import App from './App';
|
|
36
|
+
import { initChatConfig } from '@pubuduth-aplicy/chat-ui';
|
|
37
|
+
|
|
38
|
+
// Initialize the chat configuration
|
|
39
|
+
initChatConfig({
|
|
40
|
+
apiUrl: 'YOUR_API_URL', // Your backend API URL
|
|
41
|
+
role: 'user', // Optional: 'user' or 'admin'
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
45
|
+
<React.StrictMode>
|
|
46
|
+
<App />
|
|
47
|
+
</React.StrictMode>,
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Wrap with ChatProvider
|
|
52
|
+
|
|
53
|
+
Next, wrap your component tree with `ChatProvider`. This provider manages the chat state and WebSocket connection.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// src/App.tsx
|
|
57
|
+
import { ChatProvider, Chat } from '@pubuduth-aplicy/chat-ui';
|
|
58
|
+
|
|
59
|
+
function App() {
|
|
60
|
+
const userId = 'CURRENT_USER_ID'; // The ID of the currently logged-in user
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<ChatProvider userId={userId}>
|
|
64
|
+
<div className="App">
|
|
65
|
+
{/* Your other components */}
|
|
66
|
+
<Chat />
|
|
67
|
+
</div>
|
|
68
|
+
</ChatProvider>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default App;
|
|
50
73
|
```
|
|
74
|
+
|
|
75
|
+
### 3. Render the Chat Component
|
|
76
|
+
|
|
77
|
+
Finally, render the `<Chat />` component wherever you want the chat interface to appear.
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
### `initChatConfig(config)`
|
|
82
|
+
|
|
83
|
+
This function initializes the chat component's configuration. It must be called once before any chat components are rendered.
|
|
84
|
+
|
|
85
|
+
**Parameters:**
|
|
86
|
+
|
|
87
|
+
* `config` (object):
|
|
88
|
+
* `apiUrl` (string, required): The base URL of your chat backend. The WebSocket connection will be derived from this URL.
|
|
89
|
+
* `role` (string, optional): The role of the user, e.g., 'user' or 'admin'.
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
### `<ChatProvider />`
|
|
94
|
+
|
|
95
|
+
This component provides the chat context to its children.
|
|
96
|
+
|
|
97
|
+
**Props:**
|
|
98
|
+
|
|
99
|
+
* `userId` (string, required): The unique identifier for the current user.
|
|
100
|
+
* `children` (ReactNode, required): The child components. The `<Chat />` component must be a descendant of `ChatProvider`.
|
|
101
|
+
|
|
102
|
+
### `<Chat />`
|
|
103
|
+
|
|
104
|
+
This component renders the main chat interface. It takes no props.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
To set up the project for local development:
|
|
109
|
+
|
|
110
|
+
1. **Clone the repository:**
|
|
111
|
+
```bash
|
|
112
|
+
git clone https://github.com/pubuduth-aplicy/chat-ui.git
|
|
113
|
+
cd chat-ui
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
2. **Install dependencies:**
|
|
117
|
+
```bash
|
|
118
|
+
npm install
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
3. **Run the development server:**
|
|
122
|
+
This project uses Vite. To run the development server, you can add a `dev` script to your `package.json`:
|
|
123
|
+
```json
|
|
124
|
+
"scripts": {
|
|
125
|
+
"dev": "vite",
|
|
126
|
+
"build": "tsc",
|
|
127
|
+
"prepare": "npm run build"
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
Then run:
|
|
131
|
+
```bash
|
|
132
|
+
npm run dev
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Building
|
|
136
|
+
|
|
137
|
+
To build the component for production, run the following command. This will transpile the TypeScript code.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm run build
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
For a full production build, you should use `vite build`. You can add this to your `package.json`:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
"scripts": {
|
|
147
|
+
"build:vite": "vite build"
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
This project is licensed under the [ISC License](LICENSE).
|
package/package.json
CHANGED
package/src/Chat.config.ts
CHANGED
package/src/components/Chat.tsx
CHANGED
|
@@ -51,7 +51,7 @@ export const Chat = () => {
|
|
|
51
51
|
return (
|
|
52
52
|
<div className="container mx-auto mb-5">
|
|
53
53
|
<div className="grid-container">
|
|
54
|
-
<div className={`sidebarContainer`}>
|
|
54
|
+
<div className={`sidebarContainer dark:bg-gray-800`}>
|
|
55
55
|
<Sidebar />
|
|
56
56
|
</div>
|
|
57
57
|
<div className="messageContainer">
|
|
@@ -24,7 +24,7 @@ const CollapsibleSection = ({
|
|
|
24
24
|
className="flex justify-between items-center p-2 cursor-pointer hover:bg-gray-50"
|
|
25
25
|
onClick={toggleOpen}
|
|
26
26
|
>
|
|
27
|
-
<p className="text-xs uppercase">{title}</p>
|
|
27
|
+
<p className="text-xs uppercase text-gray-900 dark:text-gray-200">{title}</p>
|
|
28
28
|
<ChevronDown
|
|
29
29
|
size={20}
|
|
30
30
|
className={`transform transition-transform duration-200 ${
|
|
@@ -32,7 +32,7 @@ const CollapsibleSection = ({
|
|
|
32
32
|
}`}
|
|
33
33
|
/>
|
|
34
34
|
</div>
|
|
35
|
-
{isOpen && <div className="p-2">{children}</div>}
|
|
35
|
+
{isOpen && <div className="p-2 text-gray-900 dark:text-gray-200">{children}</div>}
|
|
36
36
|
</div>
|
|
37
37
|
);
|
|
38
38
|
};
|
|
@@ -34,6 +34,7 @@ interface MessageProps {
|
|
|
34
34
|
type?: "user" | "system" | "system-completion";
|
|
35
35
|
meta?: {
|
|
36
36
|
bookingDetails?: {
|
|
37
|
+
status: string; // e.g., "confirmed", "pending", "cancelled"
|
|
37
38
|
serviceId: string;
|
|
38
39
|
date: string;
|
|
39
40
|
time: string;
|
|
@@ -47,18 +48,30 @@ interface MessageProps {
|
|
|
47
48
|
|
|
48
49
|
const Message = ({ message }: MessageProps) => {
|
|
49
50
|
const { userId } = useChatContext();
|
|
50
|
-
const { apiUrl } = getChatConfig();
|
|
51
|
+
const { apiUrl,cdnUrl } = getChatConfig();
|
|
51
52
|
|
|
52
53
|
if (message.type === "system") {
|
|
53
54
|
return (
|
|
54
55
|
<div className="system-message booking-details">
|
|
55
|
-
<h4>
|
|
56
|
+
<h4>{message.meta?.bookingDetails?.status || 'Unknown'}</h4>
|
|
56
57
|
<div className="details">
|
|
57
58
|
<p>Service: {message.meta?.bookingDetails?.serviceId}</p>
|
|
58
59
|
<p>Date: {message.meta?.bookingDetails?.date}</p>
|
|
59
60
|
<p>Time: {message.meta?.bookingDetails?.time}</p>
|
|
60
61
|
<p>Price: ${message.meta?.bookingDetails?.price}</p>
|
|
61
62
|
</div>
|
|
63
|
+
<button
|
|
64
|
+
className="confirm-button"
|
|
65
|
+
onClick={() => {
|
|
66
|
+
// Add your confirmation logic here
|
|
67
|
+
console.log('Booking confirmed!');
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
Confirm Booking
|
|
71
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
72
|
+
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z" />
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
62
75
|
</div>
|
|
63
76
|
);
|
|
64
77
|
}
|
|
@@ -194,27 +207,6 @@ const Message = ({ message }: MessageProps) => {
|
|
|
194
207
|
}
|
|
195
208
|
};
|
|
196
209
|
|
|
197
|
-
// const handleDownload = async (url: string, name: string, index: number) => {
|
|
198
|
-
// setDownloadingIndex(index);
|
|
199
|
-
// try {
|
|
200
|
-
// var xhr = new XMLHttpRequest()
|
|
201
|
-
// xhr.open('HEAD', url, false)
|
|
202
|
-
// xhr.send()
|
|
203
|
-
// const blob = await response.blob();
|
|
204
|
-
// const link = document.createElement("a");
|
|
205
|
-
// link.href = URL.createObjectURL(blob);
|
|
206
|
-
// link.download = name;
|
|
207
|
-
// link.click();
|
|
208
|
-
// URL.revokeObjectURL(link.href);
|
|
209
|
-
// } catch (error) {
|
|
210
|
-
// console.error("Download failed:", error);
|
|
211
|
-
// } finally {
|
|
212
|
-
// setDownloadingIndex(null);
|
|
213
|
-
// }
|
|
214
|
-
// };
|
|
215
|
-
|
|
216
|
-
// const renderMedia = () => {
|
|
217
|
-
// if (!message.media || message.media.length === 0) return null;
|
|
218
210
|
|
|
219
211
|
const handleDownload = async (url: string, name: string, index: number) => {
|
|
220
212
|
setDownloadingIndex(index);
|
|
@@ -224,7 +216,6 @@ const Message = ({ message }: MessageProps) => {
|
|
|
224
216
|
setDownloadController(controller);
|
|
225
217
|
|
|
226
218
|
try {
|
|
227
|
-
// First try to download directly
|
|
228
219
|
try {
|
|
229
220
|
const response = await fetch(
|
|
230
221
|
`${apiUrl}${Path.apiProxy}?url=${encodeURIComponent(
|
|
@@ -320,7 +311,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
320
311
|
<div className="circular-progress-container">
|
|
321
312
|
<div className="media-preview-background">
|
|
322
313
|
<img
|
|
323
|
-
src={media.url}
|
|
314
|
+
src={`${cdnUrl}${`${cdnUrl}${media.url}`}`}
|
|
324
315
|
alt={media.name}
|
|
325
316
|
className="blurred-preview"
|
|
326
317
|
/>
|
|
@@ -365,16 +356,16 @@ const Message = ({ message }: MessageProps) => {
|
|
|
365
356
|
<>
|
|
366
357
|
{media.type === "image" ? (
|
|
367
358
|
<img
|
|
368
|
-
src={media.url}
|
|
359
|
+
src={`${cdnUrl}${media.url}`}
|
|
369
360
|
alt={media.name}
|
|
370
361
|
className="media-content"
|
|
371
|
-
onClick={() => window.open(media.url
|
|
362
|
+
onClick={() => window.open(`${cdnUrl}${media.url}`, "_blank")}
|
|
372
363
|
/>
|
|
373
364
|
) : media.type === "video" ? (
|
|
374
365
|
<video controls className="media-content">
|
|
375
366
|
<source
|
|
376
|
-
src={media.url}
|
|
377
|
-
type={`video/${media.url
|
|
367
|
+
src={`${cdnUrl}${media.url}`}
|
|
368
|
+
type={`video/${`${cdnUrl}${media.url}`.split(".").pop()}`}
|
|
378
369
|
/>
|
|
379
370
|
</video>
|
|
380
371
|
) : (
|
|
@@ -397,7 +388,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
397
388
|
if (downloadingIndex === index) {
|
|
398
389
|
cancelDownload();
|
|
399
390
|
} else {
|
|
400
|
-
handleDownload(media.url
|
|
391
|
+
handleDownload(`${cdnUrl}${media.url}`, media.name, index);
|
|
401
392
|
}
|
|
402
393
|
}}
|
|
403
394
|
title={downloadingIndex === index ? "Cancel" : "Download"}
|
|
@@ -490,19 +481,28 @@ const Message = ({ message }: MessageProps) => {
|
|
|
490
481
|
}
|
|
491
482
|
};
|
|
492
483
|
|
|
484
|
+
const isMessageOlderThanOneDay = (createdAt: string | Date) => {
|
|
485
|
+
const messageDate = new Date(createdAt);
|
|
486
|
+
const now = new Date();
|
|
487
|
+
const oneDayInMs = 24 * 60 * 60 * 1000;
|
|
488
|
+
return now.getTime() - messageDate.getTime() > oneDayInMs;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const hasMedia = message.media && message.media.length > 0;
|
|
492
|
+
|
|
493
493
|
return (
|
|
494
494
|
<div className="chat-container">
|
|
495
495
|
<div className={`message-row ${alignItems}`}>
|
|
496
496
|
<div
|
|
497
497
|
className="bubble-container"
|
|
498
|
-
onMouseEnter={() => fromMe && setShowOptions(true)}
|
|
498
|
+
onMouseEnter={() => fromMe && !hasMedia && setShowOptions(true)}
|
|
499
499
|
onMouseLeave={() =>
|
|
500
500
|
fromMe && !showDeleteOption && setShowOptions(false)
|
|
501
501
|
}
|
|
502
502
|
>
|
|
503
503
|
{message.status === "deleted" ? (
|
|
504
504
|
<div className="chat-bubble compact-bubble deleted-message">
|
|
505
|
-
<div className="message-text">This message was deleted</div>
|
|
505
|
+
<div className="message-text text-gray-900 dark:text-gray-100">This message was deleted</div>
|
|
506
506
|
</div>
|
|
507
507
|
) : (
|
|
508
508
|
(message.message ||
|
|
@@ -533,7 +533,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
533
533
|
</div>
|
|
534
534
|
) : (
|
|
535
535
|
message.message && (
|
|
536
|
-
<div className="message-text">{message.message}</div>
|
|
536
|
+
<div className="message-text text-gray-900 dark:text-gray-100">{message.message}</div>
|
|
537
537
|
)
|
|
538
538
|
)}
|
|
539
539
|
|
|
@@ -541,7 +541,9 @@ const Message = ({ message }: MessageProps) => {
|
|
|
541
541
|
{fromMe &&
|
|
542
542
|
showOptions &&
|
|
543
543
|
!isEditingMode &&
|
|
544
|
-
!message.isDeleted &&
|
|
544
|
+
!message.isDeleted &&
|
|
545
|
+
!hasMedia &&
|
|
546
|
+
!isMessageOlderThanOneDay(message.createdAt) && (
|
|
545
547
|
<div className="message-options">
|
|
546
548
|
<button
|
|
547
549
|
className="message-option-btn edit-btn"
|
|
@@ -8,6 +8,7 @@ import { useChatContext } from "../../providers/ChatProvider";
|
|
|
8
8
|
import { getChatConfig } from "../../Chat.config";
|
|
9
9
|
|
|
10
10
|
const MessageContainer = () => {
|
|
11
|
+
const { userId } = useChatContext();
|
|
11
12
|
const { selectedConversation, setSelectedConversation, setOnlineUsers } =
|
|
12
13
|
useChatUIStore();
|
|
13
14
|
const { socket, isUserOnline } = useChatContext();
|
|
@@ -67,6 +68,7 @@ const MessageContainer = () => {
|
|
|
67
68
|
event: "joinChat",
|
|
68
69
|
data: {
|
|
69
70
|
chatId: selectedConversation._id,
|
|
71
|
+
userId: userId
|
|
70
72
|
},
|
|
71
73
|
})
|
|
72
74
|
);
|
|
@@ -98,19 +100,19 @@ const MessageContainer = () => {
|
|
|
98
100
|
|
|
99
101
|
// Listen for online users updates
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
const participantDetails = Array.isArray(selectedConversation?.participantDetails)
|
|
106
|
+
? selectedConversation?.participantDetails
|
|
107
|
+
: [selectedConversation?.participantDetails].filter(Boolean);
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
// (p: any) => p._id !== userId
|
|
109
|
-
// );
|
|
110
|
-
|
|
111
|
-
const participant = selectedConversation?.participantDetails?.find(
|
|
109
|
+
const participant = participantDetails.find(
|
|
112
110
|
(p: any) => p._id !== userId
|
|
113
111
|
);
|
|
112
|
+
|
|
113
|
+
// const participant = selectedConversation?.participantDetails?.find(
|
|
114
|
+
// (p: any) => p._id !== userId
|
|
115
|
+
// );
|
|
114
116
|
|
|
115
117
|
const isOnline = isUserOnline(participant?._id || "");
|
|
116
118
|
|
|
@@ -138,7 +140,7 @@ const MessageContainer = () => {
|
|
|
138
140
|
<img
|
|
139
141
|
className="chatMessageContainerInnerImg"
|
|
140
142
|
alt="Profile"
|
|
141
|
-
src={participant?.
|
|
143
|
+
src={participant?.profilePicture}
|
|
142
144
|
/>
|
|
143
145
|
)}
|
|
144
146
|
<div className="chatMessageContainerOutter">
|
|
@@ -155,7 +157,7 @@ const MessageContainer = () => {
|
|
|
155
157
|
) : (
|
|
156
158
|
<>
|
|
157
159
|
<p className="chatMessageContainerOutterDiv_name">
|
|
158
|
-
{participant?.
|
|
160
|
+
{participant?.name || ""}
|
|
159
161
|
</p>
|
|
160
162
|
<p className="text-sm">
|
|
161
163
|
{isOnline ? "Online" : "Offline"}
|
|
@@ -57,6 +57,15 @@ const MessageInput = () => {
|
|
|
57
57
|
const generateTempId = () =>
|
|
58
58
|
`temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
59
59
|
// Join chat room when conversation is selected
|
|
60
|
+
|
|
61
|
+
const participantDetails = Array.isArray(selectedConversation?.participantDetails)
|
|
62
|
+
? selectedConversation?.participantDetails
|
|
63
|
+
: [selectedConversation?.participantDetails].filter(Boolean);
|
|
64
|
+
|
|
65
|
+
const otherParticipant = participantDetails.find(
|
|
66
|
+
(p: any) => p._id !== userId
|
|
67
|
+
);
|
|
68
|
+
|
|
60
69
|
useEffect(() => {
|
|
61
70
|
if (selectedConversation?._id && socket?.readyState === WebSocket.OPEN) {
|
|
62
71
|
sendMessage({
|
|
@@ -67,7 +76,8 @@ const MessageInput = () => {
|
|
|
67
76
|
});
|
|
68
77
|
}
|
|
69
78
|
}, [selectedConversation?._id, socket, sendMessage]);
|
|
70
|
-
|
|
79
|
+
console.log('selected', selectedConversation);
|
|
80
|
+
|
|
71
81
|
useEffect(() => {
|
|
72
82
|
// Clear all input state when conversation changes
|
|
73
83
|
setMessage("");
|
|
@@ -93,6 +103,7 @@ const MessageInput = () => {
|
|
|
93
103
|
data: {
|
|
94
104
|
chatId: selectedConversation._id,
|
|
95
105
|
userId,
|
|
106
|
+
reciverId: otherParticipant?._id,
|
|
96
107
|
},
|
|
97
108
|
});
|
|
98
109
|
}
|
|
@@ -105,6 +116,7 @@ const MessageInput = () => {
|
|
|
105
116
|
data: {
|
|
106
117
|
chatId: selectedConversation._id,
|
|
107
118
|
userId,
|
|
119
|
+
reciverId: otherParticipant?._id,
|
|
108
120
|
},
|
|
109
121
|
});
|
|
110
122
|
}
|
|
@@ -192,6 +204,7 @@ const MessageInput = () => {
|
|
|
192
204
|
const response = await apiClient.post(`${Path.preSignUrl}`, {
|
|
193
205
|
fileName: file.name,
|
|
194
206
|
fileType: file.type,
|
|
207
|
+
conversationId: selectedConversation?._id
|
|
195
208
|
});
|
|
196
209
|
|
|
197
210
|
const { signedUrl, fileUrl } = await response.data;
|
|
@@ -225,18 +238,12 @@ const MessageInput = () => {
|
|
|
225
238
|
});
|
|
226
239
|
};
|
|
227
240
|
|
|
228
|
-
|
|
229
|
-
// ? selectedConversation?.participantDetails
|
|
230
|
-
// : [selectedConversation?.participantDetails].filter(Boolean);
|
|
241
|
+
|
|
231
242
|
|
|
232
|
-
//
|
|
233
|
-
// (p:
|
|
243
|
+
// const otherParticipant = selectedConversation?.participantDetails?.find(
|
|
244
|
+
// (p:any) => p._id !== userId
|
|
234
245
|
// );
|
|
235
246
|
|
|
236
|
-
const otherParticipant = selectedConversation?.participantDetails?.find(
|
|
237
|
-
(p:any) => p._id !== userId
|
|
238
|
-
);
|
|
239
|
-
|
|
240
247
|
const handleSubmit = useCallback(
|
|
241
248
|
async (e: React.FormEvent) => {
|
|
242
249
|
e.preventDefault();
|
|
@@ -16,32 +16,10 @@ const Conversation = ({ conversation }: ConversationProps) => {
|
|
|
16
16
|
);
|
|
17
17
|
const { userId } = useChatContext();
|
|
18
18
|
|
|
19
|
-
// This should return the other participant
|
|
20
19
|
const participant = conversation.participantDetails?.find(
|
|
21
20
|
(p:any) => p._id !== userId
|
|
22
21
|
);
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
// const handleSelectConversation = () => {
|
|
26
|
-
// console.log("Selected Conversation Data:", JSON.stringify(conversation, null, 2));
|
|
27
|
-
// setSelectedConversation(conversation);
|
|
28
|
-
|
|
29
|
-
// const unreadMessages = conversation.unreadMessageIds || [];
|
|
30
|
-
// if (unreadMessages.length > 0 && socket?.readyState === WebSocket.OPEN) {
|
|
31
|
-
// console.log('unread meassge',unreadMessages);
|
|
32
|
-
|
|
33
|
-
// const message = {
|
|
34
|
-
// event: "messageRead",
|
|
35
|
-
// data: {
|
|
36
|
-
// messageIds: unreadMessages,
|
|
37
|
-
// senderId: participant?._id,
|
|
38
|
-
// chatId: conversation._id,
|
|
39
|
-
// },
|
|
40
|
-
// };
|
|
41
|
-
// socket.send(JSON.stringify(message));
|
|
42
|
-
// }
|
|
43
|
-
// };
|
|
44
|
-
|
|
45
23
|
const handleSelectConversation = () => {
|
|
46
24
|
console.log(
|
|
47
25
|
"Selected Conversation Data:",
|
|
@@ -62,7 +40,6 @@ const Conversation = ({ conversation }: ConversationProps) => {
|
|
|
62
40
|
},
|
|
63
41
|
};
|
|
64
42
|
socket.send(JSON.stringify(message));
|
|
65
|
-
// updateConversationReadStatus(conversation._id, unreadMessageIds);
|
|
66
43
|
}
|
|
67
44
|
};
|
|
68
45
|
|
|
@@ -87,7 +64,6 @@ const Conversation = ({ conversation }: ConversationProps) => {
|
|
|
87
64
|
};
|
|
88
65
|
}, [socket, setOnlineUsers]);
|
|
89
66
|
|
|
90
|
-
// Temporary debug in your component
|
|
91
67
|
useEffect(() => {
|
|
92
68
|
console.log("Current conversation state:", conversation);
|
|
93
69
|
}, [conversation]);
|
|
@@ -98,7 +74,7 @@ const Conversation = ({ conversation }: ConversationProps) => {
|
|
|
98
74
|
const conversationName =
|
|
99
75
|
conversation.type === "service" && conversation.bookingId
|
|
100
76
|
? `Booking #${conversation.bookingId}`
|
|
101
|
-
: participant?.
|
|
77
|
+
: participant?.name || "Conversation";
|
|
102
78
|
|
|
103
79
|
const lastMessageTimestamp = new Date(
|
|
104
80
|
conversation.lastMessage?.updatedAt || conversation.lastMessage?.createdAt
|
|
@@ -123,7 +99,7 @@ const Conversation = ({ conversation }: ConversationProps) => {
|
|
|
123
99
|
<>
|
|
124
100
|
<img
|
|
125
101
|
className="w-10 h-10 rounded-full"
|
|
126
|
-
src={participant?.
|
|
102
|
+
src={participant?.profilePicture}
|
|
127
103
|
alt="User Avatar"
|
|
128
104
|
/>
|
|
129
105
|
<span
|
|
@@ -150,7 +126,7 @@ const Conversation = ({ conversation }: ConversationProps) => {
|
|
|
150
126
|
</div>
|
|
151
127
|
{/* <span className="text-xs text-gray-500">{lastMessageTimestamp}</span> */}
|
|
152
128
|
</div>
|
|
153
|
-
<p className="text-xs text-gray-600 truncate">
|
|
129
|
+
<p className="text-xs text-gray-600 truncate dark:text-gray-500">
|
|
154
130
|
{conversation.lastMessage?.status === "deleted"
|
|
155
131
|
? "This message was deleted"
|
|
156
132
|
: conversation.lastMessage?.media?.length > 0
|
|
@@ -35,6 +35,8 @@ const Conversations = () => {
|
|
|
35
35
|
const allServiceChatsMap = new Map<string, GroupedServiceChats[string]>();
|
|
36
36
|
|
|
37
37
|
groups.forEach((group) => {
|
|
38
|
+
console.log("group", group.personalConversation);
|
|
39
|
+
|
|
38
40
|
if (group.personalConversation) {
|
|
39
41
|
allGeneralChats.push(group.personalConversation);
|
|
40
42
|
}
|
|
@@ -225,7 +227,7 @@ const Conversations = () => {
|
|
|
225
227
|
convo.participantDetails?.some((p:any) =>
|
|
226
228
|
typeof p === "string"
|
|
227
229
|
? p.toLowerCase().includes(lowerSearch)
|
|
228
|
-
: p.
|
|
230
|
+
: p.name?.toLowerCase().includes(lowerSearch)
|
|
229
231
|
)
|
|
230
232
|
);
|
|
231
233
|
|
|
@@ -244,6 +246,9 @@ const Conversations = () => {
|
|
|
244
246
|
.filter(([_, group]) => group.conversations.length > 0) // Remove empty groups
|
|
245
247
|
);
|
|
246
248
|
|
|
249
|
+
console.log("filteredGeneralChats",filteredGeneralChats);
|
|
250
|
+
console.log("filteredGroupedServiceChats");
|
|
251
|
+
|
|
247
252
|
return (
|
|
248
253
|
<div className="chatSidebarConversations">
|
|
249
254
|
{isEmpty ? (
|
|
@@ -18,8 +18,8 @@ const SearchInput = () => {
|
|
|
18
18
|
};
|
|
19
19
|
return (
|
|
20
20
|
<>
|
|
21
|
-
<div className="chatSidebarSearchbar">
|
|
22
|
-
<div className="chatSidebarSearchbarContainer">
|
|
21
|
+
<div className="chatSidebarSearchbar bg-{FFFFFF} dark:bg-gray-800 text-gray-900 dark:text-gray-300">
|
|
22
|
+
<div className="chatSidebarSearchbarContainer bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-300">
|
|
23
23
|
<img src={searchicon} className="chatSidebarSearchbarImg" />
|
|
24
24
|
<input
|
|
25
25
|
className="chatSidebarInput"
|
|
@@ -6,7 +6,6 @@ export const useMessageStatus = () => {
|
|
|
6
6
|
const { socket,userId } = useChatContext();
|
|
7
7
|
const { messages, setMessages, selectedConversation } = useChatUIStore();
|
|
8
8
|
|
|
9
|
-
// Handle status updates from server
|
|
10
9
|
useEffect(() => {
|
|
11
10
|
if (!socket) return;
|
|
12
11
|
|
|
@@ -19,7 +18,6 @@ export const useMessageStatus = () => {
|
|
|
19
18
|
|
|
20
19
|
setMessages(prev => prev.map(msg => {
|
|
21
20
|
if (msg._id === messageId) {
|
|
22
|
-
// Only update if new status is higher priority
|
|
23
21
|
const statusOrder = ['sent', 'delivered', 'read'];
|
|
24
22
|
const currentIdx = statusOrder.indexOf(msg.status);
|
|
25
23
|
const newIdx = statusOrder.indexOf(status);
|
|
@@ -40,7 +38,6 @@ export const useMessageStatus = () => {
|
|
|
40
38
|
return () => socket.removeEventListener('message', handleStatusUpdate);
|
|
41
39
|
}, [socket, setMessages]);
|
|
42
40
|
|
|
43
|
-
// Send delivery confirmations for visible messages
|
|
44
41
|
useEffect(() => {
|
|
45
42
|
if (!socket || !selectedConversation) return;
|
|
46
43
|
|
|
@@ -67,14 +64,12 @@ export const useMessageStatus = () => {
|
|
|
67
64
|
{ threshold: 0.7 }
|
|
68
65
|
);
|
|
69
66
|
|
|
70
|
-
// Observe all messages in current chat
|
|
71
67
|
const messageElements = document.querySelectorAll('[data-message-id]');
|
|
72
68
|
messageElements.forEach(el => observer.observe(el));
|
|
73
69
|
|
|
74
70
|
return () => observer.disconnect();
|
|
75
71
|
}, [messages, socket, selectedConversation]);
|
|
76
72
|
|
|
77
|
-
// Mark messages as read when chat is active
|
|
78
73
|
useEffect(() => {
|
|
79
74
|
if (!socket || !selectedConversation) return;
|
|
80
75
|
|
|
@@ -9,6 +9,7 @@ import React, {
|
|
|
9
9
|
useCallback,
|
|
10
10
|
} from "react";
|
|
11
11
|
import { getChatConfig } from "../Chat.config";
|
|
12
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
12
13
|
|
|
13
14
|
interface ChatProviderProps {
|
|
14
15
|
userId: string;
|
|
@@ -26,6 +27,7 @@ interface ChatContextType {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const ChatContext = createContext<ChatContextType | null>(null);
|
|
30
|
+
const queryClient = new QueryClient();
|
|
29
31
|
|
|
30
32
|
export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
31
33
|
userId,
|
|
@@ -131,18 +133,20 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
131
133
|
);
|
|
132
134
|
|
|
133
135
|
return (
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
136
|
+
<QueryClientProvider client={queryClient}>
|
|
137
|
+
<ChatContext.Provider
|
|
138
|
+
value={{
|
|
139
|
+
socket,
|
|
140
|
+
userId,
|
|
141
|
+
onlineUsers,
|
|
142
|
+
sendMessage,
|
|
143
|
+
isConnected,
|
|
144
|
+
isUserOnline,
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</ChatContext.Provider>
|
|
149
|
+
</QueryClientProvider>
|
|
146
150
|
);
|
|
147
151
|
};
|
|
148
152
|
|
|
@@ -34,6 +34,7 @@ export const sendMessage = async (params: {
|
|
|
34
34
|
serviceTitle,
|
|
35
35
|
type,
|
|
36
36
|
serviceId,
|
|
37
|
+
messageType: "user"
|
|
37
38
|
}
|
|
38
39
|
);
|
|
39
40
|
return response.data;
|
|
@@ -46,11 +47,11 @@ export const fetchMessages = async (chatId: string | undefined, userid: string,
|
|
|
46
47
|
const response = await apiClient.get(`${Path.getmessage}/${chatId}/${userid}`, {
|
|
47
48
|
params: { pagenum, limit: 20 },
|
|
48
49
|
});
|
|
49
|
-
console.log(response);
|
|
50
|
-
return response.data;
|
|
50
|
+
console.log(response);
|
|
51
|
+
return response.data;
|
|
51
52
|
} catch (error) {
|
|
52
53
|
console.error("Error fetching messages:", error);
|
|
53
|
-
return [];
|
|
54
|
+
return [];
|
|
54
55
|
}
|
|
55
56
|
};
|
|
56
57
|
|
|
@@ -89,7 +90,7 @@ export const deleteMessage = async ({
|
|
|
89
90
|
const response = await apiClient.delete(
|
|
90
91
|
`${Path.deleteMessage}/${messageId}`,
|
|
91
92
|
{
|
|
92
|
-
data: { userId }
|
|
93
|
+
data: { userId }
|
|
93
94
|
}
|
|
94
95
|
);
|
|
95
96
|
return response.data;
|
|
@@ -9,19 +9,16 @@ export const getAllConversationData = async (userid: string) => {
|
|
|
9
9
|
try {
|
|
10
10
|
const {role} = getChatConfig();
|
|
11
11
|
const apiClient = getApiClient();
|
|
12
|
-
|
|
13
|
-
// check if the user is an admin
|
|
14
|
-
//create url for admon and user
|
|
12
|
+
|
|
15
13
|
const endpoint = role === 'admin'
|
|
16
14
|
? `${Path.getConversationListByAdmin}`
|
|
17
15
|
: `${Path.getconversation}/${userid}`;
|
|
18
|
-
|
|
16
|
+
|
|
19
17
|
const res = await apiClient.get<ApiResponse>(endpoint);
|
|
20
18
|
if (res.data) {
|
|
21
19
|
console.log("API Response: ", res.data);
|
|
22
20
|
|
|
23
21
|
}
|
|
24
|
-
// Access conversations with participant details from the API response
|
|
25
22
|
const conversationsWithParticipantDetails = res.data.serviceInfo;
|
|
26
23
|
console.log("conversationsWithParticipantDetails", res.data.serviceInfo);
|
|
27
24
|
|
package/src/style/style.css
CHANGED
|
@@ -1813,4 +1813,96 @@
|
|
|
1813
1813
|
height: 100%;
|
|
1814
1814
|
background: rgba(0, 0, 0, 0.2);
|
|
1815
1815
|
/* z-index: 1; */
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
.confirm-button {
|
|
1819
|
+
background: linear-gradient(135deg, #2AA8A6, #1E9C9A);
|
|
1820
|
+
color: white;
|
|
1821
|
+
border: none;
|
|
1822
|
+
padding: 12px 24px;
|
|
1823
|
+
border-radius: 8px;
|
|
1824
|
+
font-weight: 600;
|
|
1825
|
+
font-size: 16px;
|
|
1826
|
+
cursor: pointer;
|
|
1827
|
+
display: flex;
|
|
1828
|
+
align-items: center;
|
|
1829
|
+
justify-content: center;
|
|
1830
|
+
gap: 8px;
|
|
1831
|
+
margin-top: 16px;
|
|
1832
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
1833
|
+
transition: all 0.3s ease;
|
|
1834
|
+
position: relative;
|
|
1835
|
+
overflow: hidden;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
.confirm-button:hover {
|
|
1839
|
+
background: linear-gradient(135deg, #1E9C9A, #168A88);
|
|
1840
|
+
transform: translateY(-2px);
|
|
1841
|
+
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
.confirm-button:active {
|
|
1845
|
+
transform: translateY(0);
|
|
1846
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
.confirm-button::before {
|
|
1850
|
+
content: '';
|
|
1851
|
+
position: absolute;
|
|
1852
|
+
top: 0;
|
|
1853
|
+
left: -100%;
|
|
1854
|
+
width: 100%;
|
|
1855
|
+
height: 100%;
|
|
1856
|
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
|
1857
|
+
transition: 0.5s;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
.confirm-button:hover::before {
|
|
1861
|
+
left: 100%;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
.confirm-button svg {
|
|
1865
|
+
transition: transform 0.3s ease;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
.confirm-button:hover svg {
|
|
1869
|
+
transform: scale(1.1);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
|
|
1873
|
+
.message-option-btn.disabled {
|
|
1874
|
+
opacity: 0.5;
|
|
1875
|
+
cursor: not-allowed;
|
|
1876
|
+
pointer-events: none;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
.message-option-btn[disabled] {
|
|
1880
|
+
opacity: 0.5;
|
|
1881
|
+
cursor: not-allowed;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
/* Tooltip styles */
|
|
1885
|
+
.message-option-btn {
|
|
1886
|
+
position: relative;
|
|
1887
|
+
overflow: hidden;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
.message-option-btn:hover::before {
|
|
1891
|
+
content: attr(title);
|
|
1892
|
+
position: absolute;
|
|
1893
|
+
bottom: 100%;
|
|
1894
|
+
left: 50%;
|
|
1895
|
+
transform: translateX(-50%);
|
|
1896
|
+
background: #333;
|
|
1897
|
+
color: white;
|
|
1898
|
+
padding: 4px 8px;
|
|
1899
|
+
border-radius: 4px;
|
|
1900
|
+
font-size: 12px;
|
|
1901
|
+
white-space: nowrap;
|
|
1902
|
+
z-index: 10;
|
|
1903
|
+
margin-bottom: 5px;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
.message-option-btn.disabled:hover::before {
|
|
1907
|
+
background: #666;
|
|
1816
1908
|
}
|
package/src/ChatWindow.tsx
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// import { ChatWindowProps } from "./types/type";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// export const ChatWindow: React.FC<ChatWindowProps> = ({ userId}) => {
|
|
5
|
-
// // const { messages, sendMessage, sendFile } = useChat(userId);
|
|
6
|
-
// // const [file, setFile] = useState<File | null>(null);
|
|
7
|
-
|
|
8
|
-
// return (
|
|
9
|
-
// <div className="w-full max-w-lg border p-4 rounded-lg shadow-lg bg-white">
|
|
10
|
-
// <MessageList messages={messages} />
|
|
11
|
-
// <FileUploader file={file} setFile={setFile} onUpload={sendFile} />
|
|
12
|
-
// <MessageInput onSend={sendMessage} />
|
|
13
|
-
// </div>
|
|
14
|
-
// );
|
|
15
|
-
// };
|