@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 +1 -1
- package/src/components/messages/Message.tsx +175 -54
- package/src/components/messages/MessageContainer.tsx +81 -11
- package/src/components/messages/MessageInput.tsx +328 -269
- package/src/components/messages/Messages.tsx +17 -2
- package/src/components/sidebar/Conversation.tsx +1 -0
- package/src/components/sidebar/Conversations.tsx +26 -2
- package/src/components/sidebar/SearchInput.tsx +14 -2
- package/src/components/sidebar/Sidebar.tsx +4 -5
- package/src/service/messageService.ts +6 -2
- package/src/stores/Zustant.ts +19 -2
- package/src/style/style.css +265 -6
- package/src/types/type.ts +1 -1
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const alignItems = fromMe ? "outgoing" : "incoming";
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setLocalStatus(message.status);
|
|
37
|
+
}, [message.status]);
|
|
26
38
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const seconds = date.getUTCSeconds();
|
|
39
|
+
// const handleDownload = (url: string, name: string) => {
|
|
40
|
+
// saveAs(url, name);
|
|
41
|
+
// };
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
};
|