@pubuduth-aplicy/chat-ui 2.1.54 → 2.1.55
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.
|
|
3
|
+
"version": "2.1.55",
|
|
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
|
+
"file-saver": "^2.0.5",
|
|
17
18
|
"react": "^19.0.0",
|
|
18
19
|
"react-dom": "^19.0.0",
|
|
19
20
|
"react-intersection-observer": "^9.16.0",
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@eslint/js": "^9.21.0",
|
|
26
|
+
"@types/file-saver": "^2.0.7",
|
|
25
27
|
"@types/react": "^19.0.10",
|
|
26
28
|
"@types/react-dom": "^19.0.4",
|
|
27
29
|
"@vitejs/plugin-react": "^4.3.4",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { MessageStatus } from "../../types/type";
|
|
3
3
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
4
|
-
|
|
4
|
+
import { saveAs } from 'file-saver';
|
|
5
5
|
import { useEffect, useState } from "react";
|
|
6
6
|
import { FileType } from "../common/FilePreview";
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
interface MessageProps {
|
|
9
10
|
message: {
|
|
10
11
|
_id?: string;
|
|
@@ -42,151 +43,188 @@ const Message = ({ message }: MessageProps) => {
|
|
|
42
43
|
|
|
43
44
|
const getStatusIcon = () => {
|
|
44
45
|
if (!fromMe) return null;
|
|
45
|
-
|
|
46
|
-
if (message.isUploading || message.status ===
|
|
46
|
+
|
|
47
|
+
if (message.isUploading || message.status === "sending") {
|
|
47
48
|
return <span className="message-status uploading">🔄</span>;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
if (message.status ===
|
|
50
|
+
|
|
51
|
+
if (message.status === "failed") {
|
|
51
52
|
return <span className="message-status failed">❌</span>;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
|
|
54
55
|
switch (localStatus) {
|
|
55
|
-
case
|
|
56
|
+
case "sending":
|
|
56
57
|
return <span className="message-status sending">🔄</span>;
|
|
57
|
-
case
|
|
58
|
+
case "sent":
|
|
58
59
|
return <span className="message-status sent">✓</span>;
|
|
59
|
-
case
|
|
60
|
+
case "delivered":
|
|
60
61
|
return <span className="message-status delivered">✓✓</span>;
|
|
61
|
-
case
|
|
62
|
+
case "read":
|
|
62
63
|
return <span className="message-status read">✓✓</span>;
|
|
63
64
|
default:
|
|
64
65
|
return null;
|
|
65
66
|
}
|
|
66
67
|
};
|
|
67
68
|
|
|
68
|
-
const handleDownload =
|
|
69
|
+
const handleDownload = (url: string, name: string,index:number) => {
|
|
69
70
|
setDownloadingIndex(index);
|
|
70
71
|
try {
|
|
71
|
-
|
|
72
|
-
const blob = await response.blob();
|
|
73
|
-
const link = document.createElement('a');
|
|
74
|
-
link.href = URL.createObjectURL(blob);
|
|
75
|
-
link.download = name;
|
|
76
|
-
link.click();
|
|
77
|
-
URL.revokeObjectURL(link.href);
|
|
72
|
+
saveAs(url, name);
|
|
78
73
|
} catch (error) {
|
|
79
74
|
console.error("Download failed:", error);
|
|
80
75
|
} finally {
|
|
81
76
|
setDownloadingIndex(null);
|
|
82
77
|
}
|
|
78
|
+
|
|
83
79
|
};
|
|
84
80
|
|
|
81
|
+
// const handleDownload = async (url: string, name: string, index: number) => {
|
|
82
|
+
// setDownloadingIndex(index);
|
|
83
|
+
// try {
|
|
84
|
+
// const response = await fetch(url);
|
|
85
|
+
// const blob = await response.blob();
|
|
86
|
+
// const link = document.createElement("a");
|
|
87
|
+
// link.href = URL.createObjectURL(blob);
|
|
88
|
+
// link.download = name;
|
|
89
|
+
// link.click();
|
|
90
|
+
// URL.revokeObjectURL(link.href);
|
|
91
|
+
// } catch (error) {
|
|
92
|
+
// console.error("Download failed:", error);
|
|
93
|
+
// } finally {
|
|
94
|
+
// setDownloadingIndex(null);
|
|
95
|
+
// }
|
|
96
|
+
// };
|
|
97
|
+
|
|
85
98
|
const renderMedia = () => {
|
|
86
99
|
if (!message.media || message.media.length === 0) return null;
|
|
87
100
|
|
|
88
101
|
return (
|
|
89
|
-
<div
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
<div
|
|
103
|
+
className={`media-grid ${
|
|
104
|
+
message.media.length > 1 ? "multi-media" : "single-media"
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
{message.media.map((media, index) => (
|
|
108
|
+
<div key={index} className="media-item">
|
|
109
|
+
{/* Progress indicator */}
|
|
110
|
+
{message.isUploading &&
|
|
111
|
+
media.uploadProgress !== undefined &&
|
|
112
|
+
media.uploadProgress < 100 && (
|
|
113
|
+
<>
|
|
114
|
+
{console.log("media.uploadProgress", media.uploadProgress)};
|
|
115
|
+
<div className="circular-progress-container">
|
|
116
|
+
<div className="media-preview-background">
|
|
117
|
+
<img
|
|
118
|
+
src={media.url}
|
|
119
|
+
alt={media.name}
|
|
120
|
+
className="blurred-preview"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="circular-progress">
|
|
124
|
+
<svg
|
|
125
|
+
className="circular-progress-svg"
|
|
126
|
+
viewBox="0 0 36 36"
|
|
127
|
+
>
|
|
128
|
+
<path
|
|
129
|
+
className="circular-progress-track"
|
|
130
|
+
d="M18 2.0845
|
|
108
131
|
a 15.9155 15.9155 0 0 1 0 31.831
|
|
109
132
|
a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
/>
|
|
134
|
+
<path
|
|
135
|
+
className="circular-progress-bar"
|
|
136
|
+
strokeDasharray={`${media.uploadProgress}, 100`}
|
|
137
|
+
d="M18 2.0845
|
|
115
138
|
a 15.9155 15.9155 0 0 1 0 31.831
|
|
116
139
|
a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
140
|
+
/>
|
|
141
|
+
</svg>
|
|
142
|
+
<span className="circular-progress-text">
|
|
143
|
+
{media.uploadProgress}%
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
125
149
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
{/* Error state */}
|
|
151
|
+
{media.uploadError && (
|
|
152
|
+
<div className="upload-error">
|
|
153
|
+
<span>⚠️ Upload failed</span>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
132
156
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
{/* Actual media (shown when upload complete) */}
|
|
158
|
+
{(!message.isUploading || media.uploadProgress === 100) &&
|
|
159
|
+
!media.uploadError && (
|
|
160
|
+
<>
|
|
161
|
+
{media.type === "image" ? (
|
|
162
|
+
<img
|
|
163
|
+
src={media.url}
|
|
164
|
+
alt={media.name}
|
|
165
|
+
className="media-content"
|
|
166
|
+
onClick={() => window.open(media.url, "_blank")}
|
|
167
|
+
/>
|
|
168
|
+
) : media.type === "video" ? (
|
|
169
|
+
<video controls className="media-content">
|
|
170
|
+
<source
|
|
171
|
+
src={media.url}
|
|
172
|
+
type={`video/${media.url.split(".").pop()}`}
|
|
173
|
+
/>
|
|
174
|
+
</video>
|
|
175
|
+
) : (
|
|
176
|
+
<div className="document-preview">
|
|
177
|
+
<div className="file-icon">
|
|
178
|
+
{media.type === "document" && "📄"}
|
|
179
|
+
</div>
|
|
180
|
+
<div className="file-info">
|
|
181
|
+
<span className="file-name">{media.name}</span>
|
|
182
|
+
<span className="file-size">
|
|
183
|
+
{(media.size / 1024).toFixed(1)} KB
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
<button
|
|
189
|
+
className="download-btn"
|
|
190
|
+
onClick={(e) => {
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
handleDownload(media.url, media.name, index);
|
|
193
|
+
}}
|
|
194
|
+
title="Download"
|
|
195
|
+
disabled={downloadingIndex === index}
|
|
196
|
+
>
|
|
197
|
+
{downloadingIndex === index ? (
|
|
198
|
+
<span className="loader" />
|
|
199
|
+
) : (
|
|
200
|
+
"⬇️"
|
|
201
|
+
)}
|
|
202
|
+
</button>
|
|
203
|
+
</>
|
|
158
204
|
)}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
onClick={(e) => {
|
|
162
|
-
e.stopPropagation();
|
|
163
|
-
handleDownload(media.url, media.name, index);
|
|
164
|
-
}}
|
|
165
|
-
title="Download"
|
|
166
|
-
disabled={downloadingIndex === index}
|
|
167
|
-
>
|
|
168
|
-
{downloadingIndex === index ? <span className="loader" /> : '⬇️'}
|
|
169
|
-
</button>
|
|
170
|
-
</>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
))}
|
|
205
|
+
</div>
|
|
206
|
+
))}
|
|
174
207
|
</div>
|
|
175
208
|
);
|
|
176
209
|
};
|
|
177
210
|
|
|
178
211
|
return (
|
|
179
|
-
|
|
212
|
+
<div className="chat-container">
|
|
180
213
|
<div className={`message-row ${alignItems}`}>
|
|
181
214
|
<div className="bubble-container">
|
|
182
215
|
{(message.message || (message.media && message.media.length > 0)) && (
|
|
183
216
|
<div className="chat-bubble compact-bubble">
|
|
184
217
|
{renderMedia()}
|
|
185
|
-
{message.message &&
|
|
218
|
+
{message.message && (
|
|
219
|
+
<div className="message-text">{message.message}</div>
|
|
220
|
+
)}
|
|
186
221
|
</div>
|
|
187
222
|
)}
|
|
188
223
|
<div className={`${timestamp}`}>
|
|
189
|
-
{new Date(message.createdAt).toLocaleTimeString([], {
|
|
224
|
+
{new Date(message.createdAt).toLocaleTimeString([], {
|
|
225
|
+
hour: "2-digit",
|
|
226
|
+
minute: "2-digit",
|
|
227
|
+
})}
|
|
190
228
|
<span className="status-icon">{getStatusIcon()}</span>
|
|
191
229
|
</div>
|
|
192
230
|
</div>
|
|
@@ -195,4 +233,4 @@ const Message = ({ message }: MessageProps) => {
|
|
|
195
233
|
);
|
|
196
234
|
};
|
|
197
235
|
|
|
198
|
-
export default Message;
|
|
236
|
+
export default Message;
|
|
@@ -38,14 +38,15 @@ const MessageInput = () => {
|
|
|
38
38
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
39
39
|
const attachmentsRef = useRef<Attachment[]>([]);
|
|
40
40
|
const [tempMessageId, setTempMessageId] = useState<string | null>(null);
|
|
41
|
+
const [inputError, setInputError] = useState<string | null>(null);
|
|
41
42
|
|
|
42
43
|
const generateTempId = () => `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
43
44
|
|
|
44
45
|
// Function to auto-resize the textarea
|
|
45
|
-
const autoResizeTextarea = (element: HTMLTextAreaElement) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
46
|
+
// const autoResizeTextarea = (element: HTMLTextAreaElement) => {
|
|
47
|
+
// element.style.height = "auto"
|
|
48
|
+
// element.style.height = Math.min(150, element.scrollHeight) + "px"
|
|
49
|
+
// }
|
|
49
50
|
|
|
50
51
|
useEffect(() => {
|
|
51
52
|
if (selectedConversation?._id) {
|
|
@@ -75,6 +76,42 @@ const MessageInput = () => {
|
|
|
75
76
|
}, [message, socket, selectedConversation?._id, userId]);
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
const validateInput = (text: string) => {
|
|
80
|
+
// Check for email
|
|
81
|
+
const emailRegex = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
|
|
82
|
+
if (emailRegex.test(text)) {
|
|
83
|
+
return "Email addresses are not allowed.";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for phone number (very basic)
|
|
87
|
+
const phoneRegex = /(\+?\d{1,4}[\s-]?)?(\(?\d{3}\)?[\s-]?)?\d{3}[\s-]?\d{4}/;
|
|
88
|
+
if (phoneRegex.test(text)) {
|
|
89
|
+
return "Phone numbers are not allowed.";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// // Check for bad words
|
|
93
|
+
// for (const word of badWords) {
|
|
94
|
+
// if (text.toLowerCase().includes(word)) {
|
|
95
|
+
// return "Inappropriate language is not allowed.";
|
|
96
|
+
// }
|
|
97
|
+
// }
|
|
98
|
+
|
|
99
|
+
return null; // No errors
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
103
|
+
const newValue = e.target.value;
|
|
104
|
+
const validationError = validateInput(newValue);
|
|
105
|
+
|
|
106
|
+
if (validationError) {
|
|
107
|
+
setInputError(validationError);
|
|
108
|
+
} else {
|
|
109
|
+
setInputError(null);
|
|
110
|
+
setMessage(newValue);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
|
|
78
115
|
useEffect(() => {
|
|
79
116
|
if (!socket || !selectedConversation?._id) return;
|
|
80
117
|
|
|
@@ -476,10 +513,11 @@ const MessageInput = () => {
|
|
|
476
513
|
className="chatMessageInput"
|
|
477
514
|
placeholder="Send a message"
|
|
478
515
|
value={message}
|
|
479
|
-
onChange={(e) => {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}}
|
|
516
|
+
// onChange={(e) => {
|
|
517
|
+
// setMessage(e.target.value)
|
|
518
|
+
// autoResizeTextarea(e.target)
|
|
519
|
+
// }}
|
|
520
|
+
onChange={handleChange}
|
|
483
521
|
rows={1}
|
|
484
522
|
style={{ resize: "none" }}
|
|
485
523
|
onKeyDown={(e) => {
|
|
@@ -489,7 +527,9 @@ const MessageInput = () => {
|
|
|
489
527
|
}
|
|
490
528
|
}}
|
|
491
529
|
/>
|
|
492
|
-
|
|
530
|
+
|
|
531
|
+
{inputError && <p style={{ color: 'red', fontSize: '12px' }}>{inputError}</p>}
|
|
532
|
+
<button type="submit" className="chatMessageInputSubmit" disabled={!!inputError || isSending}>
|
|
493
533
|
<img width={10} height={10} src={paperplane} alt="send" />
|
|
494
534
|
</button>
|
|
495
535
|
</div>
|
package/src/style/style.css
CHANGED
|
@@ -1198,25 +1198,63 @@ background-color: #ccc;
|
|
|
1198
1198
|
|
|
1199
1199
|
|
|
1200
1200
|
|
|
1201
|
-
|
|
1201
|
+
.circular-progress-container {
|
|
1202
|
+
position: relative;
|
|
1203
|
+
width: 100%;
|
|
1204
|
+
height: 200px;
|
|
1205
|
+
display: flex;
|
|
1206
|
+
justify-content: center;
|
|
1207
|
+
align-items: center;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1202
1210
|
.media-preview-background {
|
|
1203
1211
|
position: absolute;
|
|
1204
|
-
top: 0;
|
|
1205
|
-
left: 0;
|
|
1206
1212
|
width: 100%;
|
|
1207
1213
|
height: 100%;
|
|
1208
|
-
|
|
1214
|
+
filter: blur(5px);
|
|
1215
|
+
opacity: 0.7;
|
|
1209
1216
|
}
|
|
1210
1217
|
|
|
1218
|
+
.blurred-preview {
|
|
1219
|
+
width: 100%;
|
|
1220
|
+
height: 100%;
|
|
1221
|
+
object-fit: cover;
|
|
1222
|
+
}
|
|
1211
1223
|
|
|
1212
|
-
|
|
1213
|
-
.circular-progress-container {
|
|
1224
|
+
.circular-progress {
|
|
1214
1225
|
position: relative;
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1226
|
+
width: 80px;
|
|
1227
|
+
height: 80px;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.circular-progress-svg {
|
|
1218
1231
|
width: 100%;
|
|
1219
1232
|
height: 100%;
|
|
1233
|
+
transform: rotate(-90deg);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
.circular-progress-track {
|
|
1237
|
+
fill: none;
|
|
1238
|
+
stroke: #eee;
|
|
1239
|
+
stroke-width: 4;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
.circular-progress-bar {
|
|
1243
|
+
fill: none;
|
|
1244
|
+
stroke: #4CAF50;
|
|
1245
|
+
stroke-width: 4;
|
|
1246
|
+
stroke-linecap: round;
|
|
1247
|
+
transition: stroke-dasharray 0.3s ease;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
.circular-progress-text {
|
|
1251
|
+
position: absolute;
|
|
1252
|
+
top: 50%;
|
|
1253
|
+
left: 50%;
|
|
1254
|
+
transform: translate(-50%, -50%);
|
|
1255
|
+
font-size: 16px;
|
|
1256
|
+
font-weight: bold;
|
|
1257
|
+
color: #333;
|
|
1220
1258
|
}
|
|
1221
1259
|
|
|
1222
1260
|
/* Error message (on top of blurred background) */
|