@pubuduth-aplicy/chat-ui 2.1.53 → 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.53",
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
- // import { saveAs } from 'file-saver';
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 === 'sending') {
46
+
47
+ if (message.isUploading || message.status === "sending") {
47
48
  return <span className="message-status uploading">🔄</span>;
48
49
  }
49
-
50
- if (message.status === 'failed') {
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 'sending':
56
+ case "sending":
56
57
  return <span className="message-status sending">🔄</span>;
57
- case 'sent':
58
+ case "sent":
58
59
  return <span className="message-status sent">✓</span>;
59
- case 'delivered':
60
+ case "delivered":
60
61
  return <span className="message-status delivered">✓✓</span>;
61
- case 'read':
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 = async (url: string, name: string, index: number) => {
69
+ const handleDownload = (url: string, name: string,index:number) => {
69
70
  setDownloadingIndex(index);
70
71
  try {
71
- const response = await fetch(url);
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 className={`media-grid ${message.media.length > 1 ? 'multi-media' : 'single-media'}`}>
90
- {message.media.map((media, index) => (
91
- <div key={index} className="media-item">
92
-
93
- {/* Progress indicator */}
94
- {(media.uploadProgress !== undefined && media.uploadProgress < 100) && (
95
- <div className="circular-progress-container">
96
- <div className="media-preview-background">
97
- <img
98
- src={media.url}
99
- alt={media.name}
100
- className="blurred-preview"
101
- />
102
- </div>
103
- <div className="circular-progress">
104
- <svg className="circular-progress-svg" viewBox="0 0 36 36">
105
- <path
106
- className="circular-progress-track"
107
- d="M18 2.0845
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
- <path
112
- className="circular-progress-bar"
113
- strokeDasharray={`${media.uploadProgress}, 100`}
114
- d="M18 2.0845
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
- </svg>
119
- <span className="circular-progress-text">
120
- {media.uploadProgress}%
121
- </span>
122
- </div>
123
- </div>
124
- )}
140
+ />
141
+ </svg>
142
+ <span className="circular-progress-text">
143
+ {media.uploadProgress}%
144
+ </span>
145
+ </div>
146
+ </div>
147
+ </>
148
+ )}
125
149
 
126
- {/* Error state */}
127
- {media.uploadError && (
128
- <div className="upload-error">
129
- <span>⚠️ Upload failed</span>
130
- </div>
131
- )}
150
+ {/* Error state */}
151
+ {media.uploadError && (
152
+ <div className="upload-error">
153
+ <span>⚠️ Upload failed</span>
154
+ </div>
155
+ )}
132
156
 
133
- {/* Actual media (shown when upload complete) */}
134
- {(!media.uploadProgress || media.uploadProgress >= 100) && !media.uploadError && (
135
- <>
136
- {media.type === 'image' ? (
137
- <img
138
- src={media.url}
139
- alt={media.name}
140
- className="media-content"
141
- onClick={() => window.open(media.url, '_blank')}
142
- />
143
- ) : media.type === 'video' ? (
144
- <video controls className="media-content">
145
- <source src={media.url} type={`video/${media.url.split('.').pop()}`} />
146
- </video>
147
- ) : (
148
- <div className="document-preview">
149
- <div className="file-icon">
150
- {media.type === 'document' && '📄'}
151
- </div>
152
- <div className="file-info">
153
- <span className="file-name">{media.name}</span>
154
- <span className="file-size">{(media.size / 1024).toFixed(1)} KB</span>
155
- </div>
156
-
157
- </div>
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
- <button
160
- className="download-btn"
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
- <div className="chat-container">
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 && <div className="message-text">{message.message}</div>}
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([], { hour: '2-digit', minute: '2-digit' })}
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
- element.style.height = "auto"
47
- element.style.height = Math.min(150, element.scrollHeight) + "px"
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
- setMessage(e.target.value)
481
- autoResizeTextarea(e.target)
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,14 +527,16 @@ const MessageInput = () => {
489
527
  }
490
528
  }}
491
529
  />
492
- <button type="submit" className="chatMessageInputSubmit" disabled={isSending}>
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>
496
536
 
497
537
  {typingUser && typingUser !== userId && typingUser === selectedConversation?.participantDetails?._id && !isSending && (
498
538
  <div className="typingIndicator">
499
- <div className="loader">
539
+ <div className="typing-loader">
500
540
  <div className="ball" />
501
541
  <div className="ball" />
502
542
  <div className="ball" />
@@ -33,12 +33,12 @@
33
33
 
34
34
  .chatSidebarSearchbar {
35
35
  display: inline-flex;
36
- padding-top: 0.5rem;
37
- padding-bottom: 0.5rem;
38
- padding-left: 1rem;
36
+ padding-top: 2.3rem;
37
+ padding-bottom: 2.4rem;
38
+ /* padding-left: 1rem; */
39
39
  padding-right: 1rem;
40
40
  gap: 1rem;
41
- border-bottom: 1px;
41
+ border-bottom: 0.5px solid lightgray;
42
42
  width: 100%;
43
43
  justify-content: flex-start;
44
44
  align-items: center;
@@ -512,7 +512,7 @@ font-size: .875rem;
512
512
  }
513
513
 
514
514
  /* From Uiverse.io by ashish-yadv */
515
- .loader {
515
+ .typing-loader {
516
516
  width: 60px;
517
517
  display: flex;
518
518
  align-items: center;
@@ -1198,25 +1198,63 @@ background-color: #ccc;
1198
1198
 
1199
1199
 
1200
1200
 
1201
- /* Blurred background preview */
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
- overflow: hidden;
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
- /* Circular progress container (updated to be on top of blurred background) */
1213
- .circular-progress-container {
1224
+ .circular-progress {
1214
1225
  position: relative;
1215
- display: flex;
1216
- justify-content: center;
1217
- align-items: center;
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) */