@pubuduth-aplicy/chat-ui 2.1.54 → 2.1.56
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,10 +1,10 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { MessageStatus } from "../../types/type";
|
|
3
3
|
import { useChatContext } from "../../providers/ChatProvider";
|
|
4
|
-
// import { saveAs } from 'file-saver';
|
|
5
4
|
import { useEffect, useState } from "react";
|
|
6
5
|
import { FileType } from "../common/FilePreview";
|
|
7
6
|
|
|
7
|
+
|
|
8
8
|
interface MessageProps {
|
|
9
9
|
message: {
|
|
10
10
|
_id?: string;
|
|
@@ -42,23 +42,23 @@ const Message = ({ message }: MessageProps) => {
|
|
|
42
42
|
|
|
43
43
|
const getStatusIcon = () => {
|
|
44
44
|
if (!fromMe) return null;
|
|
45
|
-
|
|
46
|
-
if (message.isUploading || message.status ===
|
|
45
|
+
|
|
46
|
+
if (message.isUploading || message.status === "sending") {
|
|
47
47
|
return <span className="message-status uploading">🔄</span>;
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
if (message.status ===
|
|
49
|
+
|
|
50
|
+
if (message.status === "failed") {
|
|
51
51
|
return <span className="message-status failed">❌</span>;
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
switch (localStatus) {
|
|
55
|
-
case
|
|
55
|
+
case "sending":
|
|
56
56
|
return <span className="message-status sending">🔄</span>;
|
|
57
|
-
case
|
|
57
|
+
case "sent":
|
|
58
58
|
return <span className="message-status sent">✓</span>;
|
|
59
|
-
case
|
|
59
|
+
case "delivered":
|
|
60
60
|
return <span className="message-status delivered">✓✓</span>;
|
|
61
|
-
case
|
|
61
|
+
case "read":
|
|
62
62
|
return <span className="message-status read">✓✓</span>;
|
|
63
63
|
default:
|
|
64
64
|
return null;
|
|
@@ -70,7 +70,7 @@ const Message = ({ message }: MessageProps) => {
|
|
|
70
70
|
try {
|
|
71
71
|
const response = await fetch(url);
|
|
72
72
|
const blob = await response.blob();
|
|
73
|
-
const link = document.createElement(
|
|
73
|
+
const link = document.createElement("a");
|
|
74
74
|
link.href = URL.createObjectURL(blob);
|
|
75
75
|
link.download = name;
|
|
76
76
|
link.click();
|
|
@@ -86,107 +86,132 @@ const Message = ({ message }: MessageProps) => {
|
|
|
86
86
|
if (!message.media || message.media.length === 0) return null;
|
|
87
87
|
|
|
88
88
|
return (
|
|
89
|
-
<div
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
89
|
+
<div
|
|
90
|
+
className={`media-grid ${
|
|
91
|
+
message.media.length > 1 ? "multi-media" : "single-media"
|
|
92
|
+
}`}
|
|
93
|
+
>
|
|
94
|
+
{message.media.map((media, index) => (
|
|
95
|
+
<div key={index} className="media-item">
|
|
96
|
+
{/* Progress indicator */}
|
|
97
|
+
{message.isUploading &&
|
|
98
|
+
media.uploadProgress !== undefined &&
|
|
99
|
+
media.uploadProgress < 100 && (
|
|
100
|
+
<>
|
|
101
|
+
{console.log("media.uploadProgress", media.uploadProgress)};
|
|
102
|
+
<div className="circular-progress-container">
|
|
103
|
+
<div className="media-preview-background">
|
|
104
|
+
<img
|
|
105
|
+
src={media.url}
|
|
106
|
+
alt={media.name}
|
|
107
|
+
className="blurred-preview"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="circular-progress">
|
|
111
|
+
<svg
|
|
112
|
+
className="circular-progress-svg"
|
|
113
|
+
viewBox="0 0 36 36"
|
|
114
|
+
>
|
|
115
|
+
<path
|
|
116
|
+
className="circular-progress-track"
|
|
117
|
+
d="M18 2.0845
|
|
108
118
|
a 15.9155 15.9155 0 0 1 0 31.831
|
|
109
119
|
a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
/>
|
|
121
|
+
<path
|
|
122
|
+
className="circular-progress-bar"
|
|
123
|
+
strokeDasharray={`${media.uploadProgress}, 100`}
|
|
124
|
+
d="M18 2.0845
|
|
115
125
|
a 15.9155 15.9155 0 0 1 0 31.831
|
|
116
126
|
a 15.9155 15.9155 0 0 1 0 -31.831"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
/>
|
|
128
|
+
</svg>
|
|
129
|
+
<span className="circular-progress-text">
|
|
130
|
+
{media.uploadProgress}%
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</>
|
|
135
|
+
)}
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
{/* Error state */}
|
|
138
|
+
{media.uploadError && (
|
|
139
|
+
<div className="upload-error">
|
|
140
|
+
<span>⚠️ Upload failed</span>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
132
143
|
|
|
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
|
-
|
|
144
|
+
{/* Actual media (shown when upload complete) */}
|
|
145
|
+
{(!message.isUploading || media.uploadProgress === 100) &&
|
|
146
|
+
!media.uploadError && (
|
|
147
|
+
<>
|
|
148
|
+
{media.type === "image" ? (
|
|
149
|
+
<img
|
|
150
|
+
src={media.url}
|
|
151
|
+
alt={media.name}
|
|
152
|
+
className="media-content"
|
|
153
|
+
onClick={() => window.open(media.url, "_blank")}
|
|
154
|
+
/>
|
|
155
|
+
) : media.type === "video" ? (
|
|
156
|
+
<video controls className="media-content">
|
|
157
|
+
<source
|
|
158
|
+
src={media.url}
|
|
159
|
+
type={`video/${media.url.split(".").pop()}`}
|
|
160
|
+
/>
|
|
161
|
+
</video>
|
|
162
|
+
) : (
|
|
163
|
+
<div className="document-preview">
|
|
164
|
+
<div className="file-icon">
|
|
165
|
+
{media.type === "document" && "📄"}
|
|
166
|
+
</div>
|
|
167
|
+
<div className="file-info">
|
|
168
|
+
<span className="file-name">{media.name}</span>
|
|
169
|
+
<span className="file-size">
|
|
170
|
+
{(media.size / 1024).toFixed(1)} KB
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
<button
|
|
176
|
+
className="download-btn"
|
|
177
|
+
onClick={(e) => {
|
|
178
|
+
e.stopPropagation();
|
|
179
|
+
handleDownload(media.url, media.name, index);
|
|
180
|
+
}}
|
|
181
|
+
title="Download"
|
|
182
|
+
disabled={downloadingIndex === index}
|
|
183
|
+
>
|
|
184
|
+
{downloadingIndex === index ? (
|
|
185
|
+
<span className="loader" />
|
|
186
|
+
) : (
|
|
187
|
+
"⬇️"
|
|
188
|
+
)}
|
|
189
|
+
</button>
|
|
190
|
+
</>
|
|
158
191
|
)}
|
|
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
|
-
))}
|
|
192
|
+
</div>
|
|
193
|
+
))}
|
|
174
194
|
</div>
|
|
175
195
|
);
|
|
176
196
|
};
|
|
177
197
|
|
|
178
198
|
return (
|
|
179
|
-
|
|
199
|
+
<div className="chat-container">
|
|
180
200
|
<div className={`message-row ${alignItems}`}>
|
|
181
201
|
<div className="bubble-container">
|
|
182
202
|
{(message.message || (message.media && message.media.length > 0)) && (
|
|
183
203
|
<div className="chat-bubble compact-bubble">
|
|
184
204
|
{renderMedia()}
|
|
185
|
-
{message.message &&
|
|
205
|
+
{message.message && (
|
|
206
|
+
<div className="message-text">{message.message}</div>
|
|
207
|
+
)}
|
|
186
208
|
</div>
|
|
187
209
|
)}
|
|
188
210
|
<div className={`${timestamp}`}>
|
|
189
|
-
{new Date(message.createdAt).toLocaleTimeString([], {
|
|
211
|
+
{new Date(message.createdAt).toLocaleTimeString([], {
|
|
212
|
+
hour: "2-digit",
|
|
213
|
+
minute: "2-digit",
|
|
214
|
+
})}
|
|
190
215
|
<span className="status-icon">{getStatusIcon()}</span>
|
|
191
216
|
</div>
|
|
192
217
|
</div>
|
|
@@ -195,4 +220,4 @@ const Message = ({ message }: MessageProps) => {
|
|
|
195
220
|
);
|
|
196
221
|
};
|
|
197
222
|
|
|
198
|
-
export default Message;
|
|
223
|
+
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) */
|