@pubuduth-aplicy/chat-ui 2.1.51 → 2.1.53

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.51",
3
+ "version": "2.1.53",
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": "",
@@ -1,77 +1,198 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- /* eslint-disable @typescript-eslint/no-unused-vars */
3
-
4
2
  import { MessageStatus } from "../../types/type";
5
3
  import { useChatContext } from "../../providers/ChatProvider";
6
- import useChatUIStore from "../../stores/Zustant";
4
+ // import { saveAs } from 'file-saver';
5
+ import { useEffect, useState } from "react";
6
+ import { FileType } from "../common/FilePreview";
7
7
 
8
- // import { useAuthContext } from "../../context/AuthContext";
9
- // import { extractTime } from "../../utils/extractTime";
10
- // import useConversation from "../../zustand/useConversation";
11
8
  interface MessageProps {
12
9
  message: {
10
+ _id?: string;
13
11
  senderId: string;
14
12
  message: string;
15
13
  status: MessageStatus;
16
- createdAt:any;
14
+ createdAt: any;
15
+ media?: {
16
+ type: FileType;
17
+ url: string;
18
+ name: string;
19
+ size: number;
20
+ uploadProgress?: number;
21
+ uploadError: string | null;
22
+ }[];
23
+ isUploading?: boolean;
17
24
  };
18
25
  }
19
26
 
20
27
  const Message = ({ message }: MessageProps) => {
28
+ const { userId } = useChatContext();
29
+ const fromMe = message.senderId === userId;
30
+ const timestamp = fromMe ? "timestamp_outgoing" : "timestamp_incomeing";
31
+ const alignItems = fromMe ? "outgoing" : "incoming";
32
+ const [localStatus, setLocalStatus] = useState(message.status);
33
+ const [downloadingIndex, setDownloadingIndex] = useState<number | null>(null);
21
34
 
22
- const { userId } = useChatContext();
23
- const fromMe = message.senderId === userId;
24
- const timestamp = fromMe ? "timestamp_outgoing" : "timestamp_incomeing";
25
- const alignItems = fromMe ? "outgoing" : "incoming";
35
+ useEffect(() => {
36
+ setLocalStatus(message.status);
37
+ }, [message.status]);
26
38
 
27
- const date = new Date(message.createdAt);
28
- const hours = date.getUTCHours();
29
- const minutes = date.getUTCMinutes();
30
- const seconds = date.getUTCSeconds();
39
+ // const handleDownload = (url: string, name: string) => {
40
+ // saveAs(url, name);
41
+ // };
31
42
 
32
- // Format the time as HH:mm:ss (24-hour format)
33
- const time = `${hours}.${minutes}`;
43
+ const getStatusIcon = () => {
44
+ if (!fromMe) return null;
45
+
46
+ if (message.isUploading || message.status === 'sending') {
47
+ return <span className="message-status uploading">🔄</span>;
48
+ }
49
+
50
+ if (message.status === 'failed') {
51
+ return <span className="message-status failed">❌</span>;
52
+ }
53
+
54
+ switch (localStatus) {
55
+ case 'sending':
56
+ return <span className="message-status sending">🔄</span>;
57
+ case 'sent':
58
+ return <span className="message-status sent">✓</span>;
59
+ case 'delivered':
60
+ return <span className="message-status delivered">✓✓</span>;
61
+ case 'read':
62
+ return <span className="message-status read">✓✓</span>;
63
+ default:
64
+ return null;
65
+ }
66
+ };
34
67
 
35
- const getStatusIcon = () => {
36
- if (!fromMe) return null;
37
-
38
- switch (message.status) {
39
- case 'sent':
40
- return <span className="message-status sent">✓</span>;
41
- case 'delivered':
42
- return <span className="message-status delivered">✓✓</span>;
43
- case 'read':
44
- return <span className="message-status read">✓✓ (blue)</span>;
45
- default:
46
- return null;
47
- }
48
- };
68
+ const handleDownload = async (url: string, name: string, index: number) => {
69
+ setDownloadingIndex(index);
70
+ 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);
78
+ } catch (error) {
79
+ console.error("Download failed:", error);
80
+ } finally {
81
+ setDownloadingIndex(null);
82
+ }
83
+ };
49
84
 
50
- return (
51
- <>
52
- {/* <div className="w-max grid">
53
- <div className="px-3.5 py-2 bg-gray-100 rounded-3xl rounded-tl-none justify-start items-center gap-3 inline-flex">
54
- <h5 className="text-gray-900 text-sm font-normal leading-snug">{message.message}</h5>
55
- </div>
56
- <div className="justify-end items-center inline-flex mb-2.5">
57
- <h6 className="text-gray-500 text-xs font-normal leading-4 py-1">05:14 PM</h6>
58
- </div>
59
- </div> */}
85
+ const renderMedia = () => {
86
+ if (!message.media || message.media.length === 0) return null;
87
+
88
+ 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
108
+ a 15.9155 15.9155 0 0 1 0 31.831
109
+ 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
115
+ a 15.9155 15.9155 0 0 1 0 31.831
116
+ 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
+ )}
125
+
126
+ {/* Error state */}
127
+ {media.uploadError && (
128
+ <div className="upload-error">
129
+ <span>⚠️ Upload failed</span>
130
+ </div>
131
+ )}
60
132
 
61
- <div className="chat-container">
62
- <div className={`message-row ${alignItems}`}>
63
- <div className="bubble-container">
64
- <div className="chat-bubble">{message.message}</div>
65
- <div className={`${timestamp}`}>{new Date(message.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
66
- <span className="status-icon">{getStatusIcon()}</span>
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>
158
+ )}
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
+ ))}
67
174
  </div>
175
+ );
176
+ };
177
+
178
+ return (
179
+ <div className="chat-container">
180
+ <div className={`message-row ${alignItems}`}>
181
+ <div className="bubble-container">
182
+ {(message.message || (message.media && message.media.length > 0)) && (
183
+ <div className="chat-bubble compact-bubble">
184
+ {renderMedia()}
185
+ {message.message && <div className="message-text">{message.message}</div>}
186
+ </div>
187
+ )}
188
+ <div className={`${timestamp}`}>
189
+ {new Date(message.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
190
+ <span className="status-icon">{getStatusIcon()}</span>
191
+ </div>
192
+ </div>
68
193
  </div>
69
194
  </div>
70
- </div>
71
-
72
-
73
- </>
74
- )
75
- }
195
+ );
196
+ };
76
197
 
77
- export default Message
198
+ export default Message;
@@ -46,7 +46,7 @@ const MessageContainer = () => {
46
46
  <div className='chatMessageContainer'>
47
47
 
48
48
  {!selectedConversation ? (
49
- <NoChatSelected />
49
+ <EmptyInbox />
50
50
  ) : (
51
51
  <>
52
52
  <div className="chatMessageContainerInner">
@@ -97,16 +97,86 @@ const MessageContainer = () => {
97
97
 
98
98
  export default MessageContainer;
99
99
 
100
- const NoChatSelected = () => {
101
- // const { authUser } = useAuthContext();
102
-
100
+ interface EmptyInboxProps {
101
+ title?: string;
102
+ description?: string;
103
+ }
104
+
105
+ const EmptyInbox: React.FC<EmptyInboxProps> = ({
106
+ title = "Ah, a fresh new inbox",
107
+ description = "You haven't started any conversations yet, but when you do, you'll find them here.",
108
+ }) => {
103
109
  return (
104
- <div className='chatMessageContainerNoChat'>
105
- <div className='chatMessageContainerNoChat_div'>
106
- <p>Welcome </p> {/* Safely access username */}
107
- <p>Select a chat to start messaging</p>
108
- {/* <Chat className='text-3xl md:text-6xl text-center' /> */}
109
- </div>
110
+ <div className="flex flex-col items-center justify-center h-full p-6 text-center">
111
+ <div className="w-48 h-48 mb-6 relative">
112
+ <svg
113
+ viewBox="0 0 200 200"
114
+ fill="none"
115
+ xmlns="http://www.w3.org/2000/svg"
116
+ className="w-full h-full"
117
+ >
118
+ <line x1="100" y1="100" x2="100" y2="160" stroke="black" strokeWidth="2" />
119
+ <line x1="40" y1="160" x2="160" y2="160" stroke="black" strokeWidth="1" />
120
+ <path
121
+ d="M70 160C75 150 80 155 85 160"
122
+ stroke="black"
123
+ strokeWidth="1"
124
+ fill="none"
125
+ />
126
+ <path
127
+ d="M115 160C120 150 125 155 130 160"
128
+ stroke="black"
129
+ strokeWidth="1"
130
+ fill="none"
131
+ />
132
+ <rect
133
+ x="70"
134
+ y="80"
135
+ width="60"
136
+ height="30"
137
+ stroke="black"
138
+ strokeWidth="1"
139
+ fill="white"
140
+ />
141
+ <path
142
+ d="M70 80C70 65 130 65 130 80"
143
+ stroke="black"
144
+ strokeWidth="1"
145
+ fill="none"
146
+ />
147
+ <rect
148
+ x="70"
149
+ y="80"
150
+ width="60"
151
+ height="20"
152
+ stroke="black"
153
+ strokeWidth="1"
154
+ fill="white"
155
+ />
156
+ <path d="M120 90H125V95H120V90Z" fill="#10B981" />
157
+ <path
158
+ d="M120 90H125V95H120V90Z"
159
+ stroke="black"
160
+ strokeWidth="0.5"
161
+ />
162
+ <path d="M125 92L130 87" stroke="#10B981" strokeWidth="1" />
163
+ <path d="M125 92L130 97" stroke="#10B981" strokeWidth="1" />
164
+ <path
165
+ d="M130 60C140 55 150 65 140 70"
166
+ stroke="black"
167
+ strokeWidth="1"
168
+ strokeDasharray="2"
169
+ fill="none"
170
+ />
171
+ <text x="140" y="60" fontSize="12" fill="black">
172
+
173
+ </text>
174
+ <circle cx="85" cy="175" r="5" fill="#10B981" />
175
+ <circle cx="115" cy="175" r="5" fill="#10B981" />
176
+ </svg>
110
177
  </div>
178
+ <h3 className="text-xl font-medium text-gray-800 mb-2">{title}</h3>
179
+ <p className="text-gray-500 max-w-sm">{description}</p>
180
+ </div>
111
181
  );
112
- };
182
+ };