@pubuduth-aplicy/chat-ui 2.1.70 → 2.1.71
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 +287 -184
- package/src/components/sidebar/Conversation.tsx +72 -43
- package/src/style/style.css +78 -0
- package/src/types/type.ts +3 -1
package/package.json
CHANGED
|
@@ -5,32 +5,32 @@ import { useChatContext } from "../../providers/ChatProvider";
|
|
|
5
5
|
import { FileType } from "../common/FilePreview";
|
|
6
6
|
import { getChatConfig } from "../../Chat.config";
|
|
7
7
|
import { Path } from "../../lib/api/endpoint";
|
|
8
|
-
import { MoreHorizontal, Pencil, Trash2 } from "lucide-react"
|
|
8
|
+
import { MoreHorizontal, Pencil, Trash2 } from "lucide-react";
|
|
9
9
|
import { useEditMessageMutation } from "../../hooks/mutations/useEditMessage";
|
|
10
|
-
import { useDeleteMessageMutation } from "../../hooks/mutations/useDeleteMessage"
|
|
10
|
+
import { useDeleteMessageMutation } from "../../hooks/mutations/useDeleteMessage";
|
|
11
11
|
import { useEffect, useRef, useState } from "react";
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
interface MessageProps {
|
|
15
|
-
|
|
16
|
-
_id?: string
|
|
17
|
-
senderId: string
|
|
18
|
-
message: string
|
|
19
|
-
status: MessageStatus
|
|
20
|
-
createdAt: any
|
|
14
|
+
message: {
|
|
15
|
+
_id?: string
|
|
16
|
+
senderId: string
|
|
17
|
+
message: string
|
|
18
|
+
status: MessageStatus
|
|
19
|
+
createdAt: any
|
|
20
|
+
updatedAt: any
|
|
21
21
|
media?: {
|
|
22
|
-
type: FileType
|
|
23
|
-
url: string
|
|
24
|
-
name: string
|
|
25
|
-
size: number
|
|
26
|
-
uploadProgress?: number
|
|
27
|
-
uploadError: string | null
|
|
28
|
-
}[]
|
|
29
|
-
isUploading?: boolean
|
|
22
|
+
type: FileType
|
|
23
|
+
url: string
|
|
24
|
+
name: string
|
|
25
|
+
size: number
|
|
26
|
+
uploadProgress?: number
|
|
27
|
+
uploadError: string | null
|
|
28
|
+
}[]
|
|
29
|
+
isUploading?: boolean
|
|
30
30
|
isEdited?: boolean
|
|
31
31
|
isDeleted?: boolean
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
onEdit?: (messageId: string, newMessage: string) => void
|
|
33
|
+
onDelete?: (messageId: string) => void
|
|
34
34
|
type?: 'user' | 'system' | 'system-completion';
|
|
35
35
|
meta?: {
|
|
36
36
|
bookingDetails?: {
|
|
@@ -41,15 +41,15 @@ interface MessageProps {
|
|
|
41
41
|
// Add other booking details as needed
|
|
42
42
|
};
|
|
43
43
|
reviewLink?: string;
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const Message = ({ message }: MessageProps) => {
|
|
49
49
|
const { userId } = useChatContext();
|
|
50
|
-
|
|
50
|
+
const { apiUrl } = getChatConfig();
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
if (message.type === "system") {
|
|
53
53
|
return (
|
|
54
54
|
<div className="system-message booking-details">
|
|
55
55
|
<h4>Booking Confirmed</h4>
|
|
@@ -63,12 +63,14 @@ const Message = ({ message }: MessageProps) => {
|
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
if (message.type ===
|
|
66
|
+
if (message.type === "system-completion") {
|
|
67
67
|
return (
|
|
68
68
|
<div className="system-message completion-notice">
|
|
69
69
|
<p>Service completed successfully!</p>
|
|
70
|
-
<button
|
|
71
|
-
onClick={() =>
|
|
70
|
+
<button
|
|
71
|
+
onClick={() =>
|
|
72
|
+
(window.location.href = message.meta?.reviewLink || "#")
|
|
73
|
+
}
|
|
72
74
|
className="review-button"
|
|
73
75
|
>
|
|
74
76
|
Leave a Review
|
|
@@ -77,21 +79,19 @@ const Message = ({ message }: MessageProps) => {
|
|
|
77
79
|
);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
82
|
const fromMe = message.senderId === userId;
|
|
83
83
|
const timestamp = fromMe ? "timestamp_outgoing" : "timestamp_incomeing";
|
|
84
84
|
const alignItems = fromMe ? "outgoing" : "incoming";
|
|
85
85
|
const [localStatus, setLocalStatus] = useState(message.status);
|
|
86
86
|
|
|
87
|
-
const [showOptions, setShowOptions] = useState(false)
|
|
88
|
-
const [showDeleteOption, setShowDeleteOption] = useState(false)
|
|
89
|
-
const editInputRef = useRef<HTMLInputElement>(null)
|
|
90
|
-
const optionsRef = useRef<HTMLDivElement>(null)
|
|
91
|
-
const { mutate: editMessage} = useEditMessageMutation();
|
|
92
|
-
const [editedMessage, setEditedMessage] = useState(
|
|
87
|
+
const [showOptions, setShowOptions] = useState(false);
|
|
88
|
+
const [showDeleteOption, setShowDeleteOption] = useState(false);
|
|
89
|
+
const editInputRef = useRef<HTMLInputElement>(null);
|
|
90
|
+
const optionsRef = useRef<HTMLDivElement>(null);
|
|
91
|
+
const { mutate: editMessage } = useEditMessageMutation();
|
|
92
|
+
const [editedMessage, setEditedMessage] = useState("");
|
|
93
93
|
const [isEditingMode, setIsEditingMode] = useState(false);
|
|
94
|
-
|
|
94
|
+
const { mutate: deleteMessage, isPending: isDeleting } = useDeleteMessageMutation();
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
useEffect(() => {
|
|
@@ -102,9 +102,10 @@ const [showOptions, setShowOptions] = useState(false)
|
|
|
102
102
|
// saveAs(url, name);
|
|
103
103
|
// };
|
|
104
104
|
|
|
105
|
-
const [downloadingIndex, setDownloadingIndex] = useState<number | null>(null)
|
|
106
|
-
const [downloadProgress, setDownloadProgress] = useState<number>(0)
|
|
107
|
-
const [downloadController, setDownloadController] =
|
|
105
|
+
const [downloadingIndex, setDownloadingIndex] = useState<number | null>(null);
|
|
106
|
+
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
|
107
|
+
const [downloadController, setDownloadController] =
|
|
108
|
+
useState<AbortController | null>(null);
|
|
108
109
|
|
|
109
110
|
const getStatusIcon = () => {
|
|
110
111
|
if (!fromMe) return null;
|
|
@@ -121,31 +122,76 @@ const [showOptions, setShowOptions] = useState(false)
|
|
|
121
122
|
case "sending":
|
|
122
123
|
return <span className="message-status sending">🔄</span>;
|
|
123
124
|
case "sent":
|
|
124
|
-
return
|
|
125
|
-
<
|
|
126
|
-
|
|
125
|
+
return (
|
|
126
|
+
<span className="message-status sent">
|
|
127
|
+
<svg
|
|
128
|
+
viewBox="0 0 12 11"
|
|
129
|
+
height="11"
|
|
130
|
+
width="16"
|
|
131
|
+
preserveAspectRatio="xMidYMid meet"
|
|
132
|
+
fill="none"
|
|
133
|
+
>
|
|
134
|
+
<title>msg-check</title>
|
|
135
|
+
<path
|
|
136
|
+
d="M11.1549 0.652832C11.0745 0.585124 10.9729 0.55127 10.8502 0.55127C10.7021 0.55127 10.5751 0.610514 10.4693 0.729004L4.28038 8.36523L1.87461 6.09277C1.8323 6.04622 1.78151 6.01025 1.72227 5.98486C1.66303 5.95947 1.60166 5.94678 1.53819 5.94678C1.407 5.94678 1.29275 5.99544 1.19541 6.09277L0.884379 6.40381C0.79128 6.49268 0.744731 6.60482 0.744731 6.74023C0.744731 6.87565 0.79128 6.98991 0.884379 7.08301L3.88047 10.0791C4.02859 10.2145 4.19574 10.2822 4.38194 10.2822C4.48773 10.2822 4.58929 10.259 4.68663 10.2124C4.78396 10.1659 4.86436 10.1003 4.92784 10.0156L11.5738 1.59863C11.6458 1.5013 11.6817 1.40186 11.6817 1.30029C11.6817 1.14372 11.6183 1.01888 11.4913 0.925781L11.1549 0.652832Z"
|
|
137
|
+
fill="currentcolor"
|
|
138
|
+
></path>
|
|
139
|
+
</svg>
|
|
140
|
+
</span>
|
|
141
|
+
);
|
|
127
142
|
case "delivered":
|
|
128
|
-
return
|
|
129
|
-
<
|
|
130
|
-
|
|
143
|
+
return (
|
|
144
|
+
<span className="message-status delivered">
|
|
145
|
+
<svg
|
|
146
|
+
viewBox="0 0 16 11"
|
|
147
|
+
height="11"
|
|
148
|
+
width="16"
|
|
149
|
+
preserveAspectRatio="xMidYMid meet"
|
|
150
|
+
fill="none"
|
|
151
|
+
>
|
|
152
|
+
<title>msg-dblcheck</title>
|
|
153
|
+
<path
|
|
154
|
+
d="M11.0714 0.652832C10.991 0.585124 10.8894 0.55127 10.7667 0.55127C10.6186 0.55127 10.4916 0.610514 10.3858 0.729004L4.19688 8.36523L1.79112 6.09277C1.7488 6.04622 1.69802 6.01025 1.63877 5.98486C1.57953 5.95947 1.51817 5.94678 1.45469 5.94678C1.32351 5.94678 1.20925 5.99544 1.11192 6.09277L0.800883 6.40381C0.707784 6.49268 0.661235 6.60482 0.661235 6.74023C0.661235 6.87565 0.707784 6.98991 0.800883 7.08301L3.79698 10.0791C3.94509 10.2145 4.11224 10.2822 4.29844 10.2822C4.40424 10.2822 4.5058 10.259 4.60313 10.2124C4.70046 10.1659 4.78086 10.1003 4.84434 10.0156L11.4903 1.59863C11.5623 1.5013 11.5982 1.40186 11.5982 1.30029C11.5982 1.14372 11.5348 1.01888 11.4078 0.925781L11.0714 0.652832ZM8.6212 8.32715C8.43077 8.20866 8.2488 8.09017 8.0753 7.97168C7.99489 7.89128 7.8891 7.85107 7.75791 7.85107C7.6098 7.85107 7.4892 7.90397 7.3961 8.00977L7.10411 8.33984C7.01947 8.43717 6.97715 8.54508 6.97715 8.66357C6.97715 8.79476 7.0237 8.90902 7.1168 9.00635L8.1959 10.0791C8.33132 10.2145 8.49636 10.2822 8.69102 10.2822C8.79681 10.2822 8.89838 10.259 8.99571 10.2124C9.09304 10.1659 9.17556 10.1003 9.24327 10.0156L15.8639 1.62402C15.9358 1.53939 15.9718 1.43994 15.9718 1.32568C15.9718 1.1818 15.9125 1.05697 15.794 0.951172L15.4386 0.678223C15.3582 0.610514 15.2587 0.57666 15.1402 0.57666C14.9964 0.57666 14.8715 0.635905 14.7657 0.754395L8.6212 8.32715Z"
|
|
155
|
+
fill="currentColor"
|
|
156
|
+
></path>
|
|
157
|
+
</svg>
|
|
158
|
+
</span>
|
|
159
|
+
);
|
|
131
160
|
case "read":
|
|
132
|
-
return
|
|
133
|
-
<
|
|
134
|
-
|
|
161
|
+
return (
|
|
162
|
+
<span className="message-status read">
|
|
163
|
+
<svg
|
|
164
|
+
viewBox="0 0 16 11"
|
|
165
|
+
height="11"
|
|
166
|
+
width="16"
|
|
167
|
+
preserveAspectRatio="xMidYMid meet"
|
|
168
|
+
fill="none"
|
|
169
|
+
>
|
|
170
|
+
<title>msg-dblcheck</title>
|
|
171
|
+
<path
|
|
172
|
+
d="M11.0714 0.652832C10.991 0.585124 10.8894 0.55127 10.7667 0.55127C10.6186 0.55127 10.4916 0.610514 10.3858 0.729004L4.19688 8.36523L1.79112 6.09277C1.7488 6.04622 1.69802 6.01025 1.63877 5.98486C1.57953 5.95947 1.51817 5.94678 1.45469 5.94678C1.32351 5.94678 1.20925 5.99544 1.11192 6.09277L0.800883 6.40381C0.707784 6.49268 0.661235 6.60482 0.661235 6.74023C0.661235 6.87565 0.707784 6.98991 0.800883 7.08301L3.79698 10.0791C3.94509 10.2145 4.11224 10.2822 4.29844 10.2822C4.40424 10.2822 4.5058 10.259 4.60313 10.2124C4.70046 10.1659 4.78086 10.1003 4.84434 10.0156L11.4903 1.59863C11.5623 1.5013 11.5982 1.40186 11.5982 1.30029C11.5982 1.14372 11.5348 1.01888 11.4078 0.925781L11.0714 0.652832ZM8.6212 8.32715C8.43077 8.20866 8.2488 8.09017 8.0753 7.97168C7.99489 7.89128 7.8891 7.85107 7.75791 7.85107C7.6098 7.85107 7.4892 7.90397 7.3961 8.00977L7.10411 8.33984C7.01947 8.43717 6.97715 8.54508 6.97715 8.66357C6.97715 8.79476 7.0237 8.90902 7.1168 9.00635L8.1959 10.0791C8.33132 10.2145 8.49636 10.2822 8.69102 10.2822C8.79681 10.2822 8.89838 10.259 8.99571 10.2124C9.09304 10.1659 9.17556 10.1003 9.24327 10.0156L15.8639 1.62402C15.9358 1.53939 15.9718 1.43994 15.9718 1.32568C15.9718 1.1818 15.9125 1.05697 15.794 0.951172L15.4386 0.678223C15.3582 0.610514 15.2587 0.57666 15.1402 0.57666C14.9964 0.57666 14.8715 0.635905 14.7657 0.754395L8.6212 8.32715Z"
|
|
173
|
+
fill="currentColor"
|
|
174
|
+
></path>
|
|
175
|
+
</svg>
|
|
176
|
+
</span>
|
|
177
|
+
);
|
|
178
|
+
case "edited":
|
|
179
|
+
return <span className="message-status edited">Edited</span>;
|
|
180
|
+
case "deleted":
|
|
181
|
+
return <span className="message-status deleted">Deleted</span>;
|
|
135
182
|
default:
|
|
136
183
|
return null;
|
|
137
184
|
}
|
|
138
185
|
};
|
|
139
186
|
|
|
140
|
-
|
|
141
187
|
const cancelDownload = () => {
|
|
142
188
|
if (downloadController) {
|
|
143
|
-
downloadController.abort()
|
|
144
|
-
setDownloadingIndex(null)
|
|
145
|
-
setDownloadProgress(0)
|
|
146
|
-
setDownloadController(null)
|
|
189
|
+
downloadController.abort();
|
|
190
|
+
setDownloadingIndex(null);
|
|
191
|
+
setDownloadProgress(0);
|
|
192
|
+
setDownloadController(null);
|
|
147
193
|
}
|
|
148
|
-
}
|
|
194
|
+
};
|
|
149
195
|
|
|
150
196
|
// const handleDownload = async (url: string, name: string, index: number) => {
|
|
151
197
|
// setDownloadingIndex(index);
|
|
@@ -169,51 +215,53 @@ const [showOptions, setShowOptions] = useState(false)
|
|
|
169
215
|
// const renderMedia = () => {
|
|
170
216
|
// if (!message.media || message.media.length === 0) return null;
|
|
171
217
|
|
|
172
|
-
|
|
173
218
|
const handleDownload = async (url: string, name: string, index: number) => {
|
|
174
219
|
setDownloadingIndex(index);
|
|
175
220
|
setDownloadProgress(0);
|
|
176
|
-
|
|
221
|
+
|
|
177
222
|
const controller = new AbortController();
|
|
178
223
|
setDownloadController(controller);
|
|
179
|
-
|
|
224
|
+
|
|
180
225
|
try {
|
|
181
226
|
// First try to download directly
|
|
182
227
|
try {
|
|
183
|
-
const response = await fetch(
|
|
228
|
+
const response = await fetch(
|
|
229
|
+
`${apiUrl}${Path.apiProxy}?url=${encodeURIComponent(
|
|
230
|
+
url
|
|
231
|
+
)}&name=${encodeURIComponent(name)}`
|
|
184
232
|
);
|
|
185
|
-
|
|
186
|
-
if (!response.ok) throw new Error(
|
|
187
|
-
|
|
233
|
+
|
|
234
|
+
if (!response.ok) throw new Error("Network response was not ok");
|
|
235
|
+
|
|
188
236
|
const contentLength = response.headers.get("content-length");
|
|
189
237
|
const total = contentLength ? Number.parseInt(contentLength, 10) : 0;
|
|
190
|
-
|
|
238
|
+
|
|
191
239
|
const reader = response.body?.getReader();
|
|
192
240
|
if (!reader) throw new Error("Failed to get response reader");
|
|
193
|
-
|
|
241
|
+
|
|
194
242
|
let receivedLength = 0;
|
|
195
243
|
const chunks: Uint8Array[] = [];
|
|
196
|
-
|
|
244
|
+
|
|
197
245
|
while (true) {
|
|
198
246
|
const { done, value } = await reader.read();
|
|
199
247
|
if (done) break;
|
|
200
|
-
|
|
248
|
+
|
|
201
249
|
chunks.push(value);
|
|
202
250
|
receivedLength += value.length;
|
|
203
|
-
|
|
251
|
+
|
|
204
252
|
if (total) {
|
|
205
253
|
const progress = Math.round((receivedLength / total) * 100);
|
|
206
254
|
setDownloadProgress(progress);
|
|
207
255
|
}
|
|
208
256
|
}
|
|
209
|
-
|
|
257
|
+
|
|
210
258
|
const chunksAll = new Uint8Array(receivedLength);
|
|
211
259
|
let position = 0;
|
|
212
260
|
for (const chunk of chunks) {
|
|
213
261
|
chunksAll.set(chunk, position);
|
|
214
262
|
position += chunk.length;
|
|
215
263
|
}
|
|
216
|
-
|
|
264
|
+
|
|
217
265
|
const blob = new Blob([chunksAll]);
|
|
218
266
|
const link = document.createElement("a");
|
|
219
267
|
link.href = URL.createObjectURL(blob);
|
|
@@ -223,10 +271,13 @@ const [showOptions, setShowOptions] = useState(false)
|
|
|
223
271
|
document.body.removeChild(link);
|
|
224
272
|
URL.revokeObjectURL(link.href);
|
|
225
273
|
} catch (directDownloadError) {
|
|
226
|
-
console.log(
|
|
227
|
-
|
|
274
|
+
console.log(
|
|
275
|
+
"Direct download failed, trying alternative method",
|
|
276
|
+
directDownloadError
|
|
277
|
+
);
|
|
278
|
+
|
|
228
279
|
// Fallback: Open in new tab if download fails
|
|
229
|
-
window.open(url,
|
|
280
|
+
window.open(url, "_blank");
|
|
230
281
|
}
|
|
231
282
|
} catch (error) {
|
|
232
283
|
if ((error as Error).name === "AbortError") {
|
|
@@ -234,10 +285,10 @@ const [showOptions, setShowOptions] = useState(false)
|
|
|
234
285
|
} else {
|
|
235
286
|
console.error("Download failed:", error);
|
|
236
287
|
// Final fallback - create a temporary link
|
|
237
|
-
const link = document.createElement(
|
|
288
|
+
const link = document.createElement("a");
|
|
238
289
|
link.href = url;
|
|
239
|
-
link.target =
|
|
240
|
-
link.rel =
|
|
290
|
+
link.target = "_blank";
|
|
291
|
+
link.rel = "noopener noreferrer";
|
|
241
292
|
document.body.appendChild(link);
|
|
242
293
|
link.click();
|
|
243
294
|
document.body.removeChild(link);
|
|
@@ -248,11 +299,9 @@ const [showOptions, setShowOptions] = useState(false)
|
|
|
248
299
|
setDownloadController(null);
|
|
249
300
|
}
|
|
250
301
|
};
|
|
251
|
-
|
|
252
302
|
|
|
253
|
-
|
|
254
303
|
const renderMedia = () => {
|
|
255
|
-
if (!message.media || message.media.length === 0) return null
|
|
304
|
+
if (!message.media || message.media.length === 0) return null;
|
|
256
305
|
|
|
257
306
|
return (
|
|
258
307
|
<div
|
|
@@ -341,26 +390,29 @@ const [showOptions, setShowOptions] = useState(false)
|
|
|
341
390
|
</div>
|
|
342
391
|
)}
|
|
343
392
|
<button
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
393
|
+
className="download-btn"
|
|
394
|
+
onClick={(e) => {
|
|
395
|
+
e.stopPropagation();
|
|
396
|
+
if (downloadingIndex === index) {
|
|
397
|
+
cancelDownload();
|
|
398
|
+
} else {
|
|
399
|
+
handleDownload(media.url, media.name, index);
|
|
400
|
+
}
|
|
401
|
+
}}
|
|
402
|
+
title={downloadingIndex === index ? "Cancel" : "Download"}
|
|
403
|
+
>
|
|
404
|
+
{downloadingIndex === index ? (
|
|
405
|
+
<div className="download-progress-container">
|
|
406
|
+
<div
|
|
407
|
+
className="download-progress"
|
|
408
|
+
style={{ width: `${downloadProgress}%` }}
|
|
409
|
+
/>
|
|
410
|
+
<span className="cancel-download">✕</span>
|
|
411
|
+
</div>
|
|
412
|
+
) : (
|
|
413
|
+
"⬇️"
|
|
414
|
+
)}
|
|
415
|
+
</button>
|
|
364
416
|
</>
|
|
365
417
|
)}
|
|
366
418
|
</div>
|
|
@@ -369,129 +421,180 @@ const [showOptions, setShowOptions] = useState(false)
|
|
|
369
421
|
);
|
|
370
422
|
};
|
|
371
423
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
setIsEditingMode(true)
|
|
375
|
-
setShowOptions(false)
|
|
424
|
+
const handleEditClick = (message: string) => {
|
|
425
|
+
setEditedMessage(message);
|
|
426
|
+
setIsEditingMode(true);
|
|
427
|
+
setShowOptions(false);
|
|
376
428
|
setTimeout(() => {
|
|
377
|
-
editInputRef.current?.focus()
|
|
378
|
-
}, 0)
|
|
379
|
-
}
|
|
429
|
+
editInputRef.current?.focus();
|
|
430
|
+
}, 0);
|
|
431
|
+
};
|
|
380
432
|
|
|
381
433
|
const handleSaveEdit = () => {
|
|
382
434
|
if (message._id && editedMessage.trim() !== message.message) {
|
|
383
|
-
editMessage(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
onSuccess: () => {
|
|
389
|
-
setIsEditingMode(false);
|
|
390
|
-
// Any additional success handling
|
|
435
|
+
editMessage(
|
|
436
|
+
{
|
|
437
|
+
messageId: message._id ?? "",
|
|
438
|
+
userId: userId, // Using userId from useChatContext
|
|
439
|
+
newMessage: editedMessage.trim(),
|
|
391
440
|
},
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
441
|
+
{
|
|
442
|
+
onSuccess: () => {
|
|
443
|
+
setIsEditingMode(false);
|
|
444
|
+
setShowOptions(false);
|
|
445
|
+
setEditedMessage(""); // Clear the input after saving
|
|
446
|
+
// Any additional success handling
|
|
447
|
+
},
|
|
448
|
+
onError: (error) => {
|
|
449
|
+
// Handle error specifically for this edit
|
|
450
|
+
console.error("Edit failed:", error);
|
|
451
|
+
},
|
|
395
452
|
}
|
|
396
|
-
|
|
453
|
+
);
|
|
397
454
|
} else {
|
|
398
455
|
setIsEditingMode(false);
|
|
399
456
|
}
|
|
400
|
-
}
|
|
457
|
+
};
|
|
401
458
|
|
|
402
459
|
const handleCancelEdit = () => {
|
|
403
|
-
setEditedMessage(message.message)
|
|
404
|
-
setIsEditingMode(false)
|
|
405
|
-
}
|
|
460
|
+
setEditedMessage(message.message);
|
|
461
|
+
setIsEditingMode(false);
|
|
462
|
+
};
|
|
406
463
|
|
|
407
464
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
408
465
|
if (e.key === "Enter") {
|
|
409
|
-
handleSaveEdit()
|
|
466
|
+
handleSaveEdit();
|
|
410
467
|
} else if (e.key === "Escape") {
|
|
411
|
-
handleCancelEdit()
|
|
468
|
+
handleCancelEdit();
|
|
412
469
|
}
|
|
413
|
-
}
|
|
470
|
+
};
|
|
414
471
|
|
|
415
472
|
const handleDeleteClick = () => {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
473
|
+
if (message._id) {
|
|
474
|
+
deleteMessage(
|
|
475
|
+
{
|
|
476
|
+
messageId: message._id,
|
|
477
|
+
userId: userId,
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
onSuccess: () => {
|
|
481
|
+
setShowDeleteOption(false);
|
|
482
|
+
setShowOptions(false);
|
|
483
|
+
},
|
|
484
|
+
onError: (error) => {
|
|
485
|
+
console.error("Delete failed:", error);
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
);
|
|
489
|
+
}
|
|
420
490
|
};
|
|
421
491
|
|
|
422
|
-
|
|
423
492
|
return (
|
|
424
493
|
<div className="chat-container">
|
|
425
494
|
<div className={`message-row ${alignItems}`}>
|
|
426
495
|
<div
|
|
427
496
|
className="bubble-container"
|
|
428
497
|
onMouseEnter={() => fromMe && setShowOptions(true)}
|
|
429
|
-
onMouseLeave={() =>
|
|
498
|
+
onMouseLeave={() =>
|
|
499
|
+
fromMe && !showDeleteOption && setShowOptions(false)
|
|
500
|
+
}
|
|
430
501
|
>
|
|
431
502
|
{message.isDeleted ? (
|
|
432
503
|
<div className="chat-bubble compact-bubble deleted-message">
|
|
433
504
|
<div className="message-text">This message was deleted</div>
|
|
434
505
|
</div>
|
|
435
|
-
) : (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
506
|
+
) : (
|
|
507
|
+
(message.message ||
|
|
508
|
+
(message.media && message.media.length > 0)) && (
|
|
509
|
+
<div className="chat-bubble compact-bubble">
|
|
510
|
+
{renderMedia()}
|
|
511
|
+
{isEditingMode ? (
|
|
512
|
+
<div className="edit-message-container">
|
|
513
|
+
<input
|
|
514
|
+
ref={editInputRef}
|
|
515
|
+
type="text"
|
|
516
|
+
value={editedMessage}
|
|
517
|
+
onChange={(e) => setEditedMessage(e.target.value)}
|
|
518
|
+
onKeyDown={handleKeyDown}
|
|
519
|
+
className="edit-message-input"
|
|
520
|
+
/>
|
|
521
|
+
<div className="edit-actions">
|
|
522
|
+
<button className="save-edit" onClick={handleSaveEdit}>
|
|
523
|
+
Save
|
|
524
|
+
</button>
|
|
525
|
+
<button
|
|
526
|
+
className="cancel-edit"
|
|
527
|
+
onClick={handleCancelEdit}
|
|
528
|
+
>
|
|
529
|
+
Cancel
|
|
530
|
+
</button>
|
|
531
|
+
</div>
|
|
455
532
|
</div>
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
533
|
+
) : (
|
|
534
|
+
message.message && (
|
|
535
|
+
<div className="message-text">{message.message}</div>
|
|
536
|
+
)
|
|
537
|
+
)}
|
|
538
|
+
|
|
539
|
+
{/* Message options for outgoing messages */}
|
|
540
|
+
{fromMe &&
|
|
541
|
+
showOptions &&
|
|
542
|
+
!isEditingMode &&
|
|
543
|
+
!message.isDeleted && (
|
|
544
|
+
<div className="message-options">
|
|
545
|
+
<button
|
|
546
|
+
className="message-option-btn edit-btn"
|
|
547
|
+
onClick={() => handleEditClick(message.message)}
|
|
548
|
+
title="Edit"
|
|
549
|
+
>
|
|
550
|
+
<Pencil size={16} />
|
|
551
|
+
</button>
|
|
552
|
+
<div className="more-options-container" ref={optionsRef}>
|
|
553
|
+
<button
|
|
554
|
+
className="message-option-btn more-btn"
|
|
555
|
+
onClick={() => setShowDeleteOption(!showDeleteOption)}
|
|
556
|
+
title="More options"
|
|
557
|
+
>
|
|
558
|
+
<MoreHorizontal size={16} />
|
|
481
559
|
</button>
|
|
560
|
+
|
|
561
|
+
{showDeleteOption && (
|
|
562
|
+
<div className="delete-option">
|
|
563
|
+
<button
|
|
564
|
+
className="delete-btn"
|
|
565
|
+
onClick={handleDeleteClick}
|
|
566
|
+
>
|
|
567
|
+
<Trash2 size={16} />
|
|
568
|
+
<span>{isDeleting ? 'Deleting...' : 'Delete'}</span>
|
|
569
|
+
</button>
|
|
570
|
+
</div>
|
|
571
|
+
)}
|
|
482
572
|
</div>
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
</div>
|
|
573
|
+
</div>
|
|
574
|
+
)}
|
|
575
|
+
</div>
|
|
576
|
+
)
|
|
488
577
|
)}
|
|
489
578
|
<div className={`${timestamp}`}>
|
|
490
|
-
{
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
579
|
+
{message.status === 'deleted' ? (
|
|
580
|
+
// Show deleted timestamp if message is deleted
|
|
581
|
+
new Date(message.updatedAt).toLocaleTimeString([], {
|
|
582
|
+
hour: "2-digit",
|
|
583
|
+
minute: "2-digit",
|
|
584
|
+
})
|
|
585
|
+
) : message.status === 'edited' ? (
|
|
586
|
+
// Show updated timestamp if message was edited
|
|
587
|
+
new Date(message.updatedAt).toLocaleTimeString([], {
|
|
588
|
+
hour: "2-digit",
|
|
589
|
+
minute: "2-digit",
|
|
590
|
+
})
|
|
591
|
+
) : (
|
|
592
|
+
// Default to created timestamp
|
|
593
|
+
new Date(message.createdAt).toLocaleTimeString([], {
|
|
594
|
+
hour: "2-digit",
|
|
595
|
+
minute: "2-digit",
|
|
596
|
+
})
|
|
597
|
+
)}
|
|
495
598
|
<span className="status-icon">{getStatusIcon()}</span>
|
|
496
599
|
</div>
|
|
497
600
|
</div>
|
|
@@ -500,4 +603,4 @@ const [showOptions, setShowOptions] = useState(false)
|
|
|
500
603
|
);
|
|
501
604
|
};
|
|
502
605
|
|
|
503
|
-
export default Message;
|
|
606
|
+
export default Message;
|
|
@@ -5,24 +5,27 @@ import { ConversationProps } from "../../types/type";
|
|
|
5
5
|
import { getChatConfig } from "@pubuduth-aplicy/chat-ui";
|
|
6
6
|
|
|
7
7
|
const Conversation = ({ conversation }: ConversationProps) => {
|
|
8
|
-
const {
|
|
9
|
-
|
|
8
|
+
const {
|
|
9
|
+
setSelectedConversation,
|
|
10
|
+
setOnlineUsers,
|
|
11
|
+
onlineUsers,
|
|
12
|
+
selectedConversation,
|
|
13
|
+
} = useChatUIStore();
|
|
10
14
|
const { socket, sendMessage } = useChatContext();
|
|
11
|
-
|
|
15
|
+
const { role } = getChatConfig();
|
|
12
16
|
const handleSelectConversation = async () => {
|
|
13
17
|
setSelectedConversation(conversation);
|
|
14
18
|
|
|
15
19
|
const unreadMessages = conversation.unreadMessageIds || [];
|
|
16
20
|
if (unreadMessages.length > 0) {
|
|
17
21
|
console.log("Marking messages as read:", unreadMessages);
|
|
18
|
-
|
|
22
|
+
|
|
19
23
|
sendMessage({
|
|
20
24
|
event: "messageRead",
|
|
21
|
-
data:{
|
|
25
|
+
data: {
|
|
22
26
|
messageIds: unreadMessages,
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
27
|
+
chatId: conversation._id,
|
|
28
|
+
},
|
|
26
29
|
});
|
|
27
30
|
}
|
|
28
31
|
};
|
|
@@ -53,7 +56,7 @@ const Conversation = ({ conversation }: ConversationProps) => {
|
|
|
53
56
|
conversation?.participantDetails?._id &&
|
|
54
57
|
onlineUsers?.includes(conversation.participantDetails._id);
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
const isSelected = selectedConversation?._id === conversation._id;
|
|
57
60
|
|
|
58
61
|
return (
|
|
59
62
|
<>
|
|
@@ -65,54 +68,80 @@ const Conversation = ({ conversation }: ConversationProps) => {
|
|
|
65
68
|
<img
|
|
66
69
|
className="conversation-img"
|
|
67
70
|
src={
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
role === "admin" &&
|
|
72
|
+
Array.isArray(selectedConversation?.participantDetails)
|
|
73
|
+
? selectedConversation.participantDetails[1]?.profilePic
|
|
74
|
+
: !Array.isArray(selectedConversation?.participantDetails)
|
|
75
|
+
? selectedConversation?.participantDetails?.profilePic
|
|
76
|
+
: undefined
|
|
73
77
|
}
|
|
74
|
-
|
|
75
78
|
alt="User Avatar"
|
|
76
79
|
/>
|
|
77
80
|
<span
|
|
78
|
-
className={`chatSidebarStatusDot ${
|
|
79
|
-
isUserOnline && "online"
|
|
80
|
-
}`}
|
|
81
|
+
className={`chatSidebarStatusDot ${isUserOnline && "online"}`}
|
|
81
82
|
></span>
|
|
82
83
|
</div>
|
|
83
84
|
|
|
84
85
|
<div className="conversation-info">
|
|
85
86
|
<div className="conversation-header">
|
|
86
87
|
<p className="conversation-name">
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
88
|
+
{role === "admin" &&
|
|
89
|
+
Array.isArray(selectedConversation?.participantDetails)
|
|
90
|
+
? selectedConversation.participantDetails[1]?.firstname
|
|
91
|
+
: !Array.isArray(selectedConversation?.participantDetails)
|
|
92
|
+
? selectedConversation?.participantDetails?.firstname
|
|
93
|
+
: undefined}
|
|
94
94
|
</p>
|
|
95
95
|
<span className="conversation-time">
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
{conversation.lastMessage.status === 'deleted' ? (
|
|
97
|
+
// Show deleted timestamp if message is deleted
|
|
98
|
+
new Date(conversation.lastMessage.updatedAt).toLocaleTimeString([], {
|
|
99
|
+
hour: "2-digit",
|
|
100
|
+
minute: "2-digit",
|
|
101
|
+
})
|
|
102
|
+
) : conversation.lastMessage.status === 'edited' ? (
|
|
103
|
+
// Show updated timestamp if message was edited
|
|
104
|
+
new Date(conversation.lastMessage.updatedAt).toLocaleTimeString([], {
|
|
105
|
+
hour: "2-digit",
|
|
106
|
+
minute: "2-digit",
|
|
107
|
+
})
|
|
108
|
+
) : (
|
|
109
|
+
// Default to created timestamp
|
|
110
|
+
new Date(conversation.lastMessage.createdAt).toLocaleTimeString([], {
|
|
111
|
+
hour: "2-digit",
|
|
112
|
+
minute: "2-digit",
|
|
113
|
+
})
|
|
114
|
+
)}
|
|
103
115
|
</span>
|
|
104
116
|
</div>
|
|
105
117
|
<p className="conversation-message">
|
|
106
|
-
{conversation.lastMessage.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
{conversation.lastMessage.status === "deleted" ? (
|
|
119
|
+
"This message was deleted"
|
|
120
|
+
) : conversation.lastMessage.type !== "system" &&
|
|
121
|
+
conversation.lastMessage.message.length > 50 ? (
|
|
122
|
+
conversation.lastMessage.message.slice(0, 50) + "..."
|
|
123
|
+
) : conversation.lastMessage.media.length > 0 ? (
|
|
124
|
+
<div
|
|
125
|
+
style={{ display: "flex", alignItems: "center", gap: "5px" }}
|
|
126
|
+
>
|
|
127
|
+
<svg
|
|
128
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
129
|
+
width="18"
|
|
130
|
+
height="18"
|
|
131
|
+
viewBox="0 0 24 24"
|
|
132
|
+
fill="none"
|
|
133
|
+
stroke="currentColor"
|
|
134
|
+
strokeWidth="2"
|
|
135
|
+
strokeLinecap="round"
|
|
136
|
+
strokeLinejoin="round"
|
|
137
|
+
>
|
|
138
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
|
|
139
|
+
</svg>
|
|
140
|
+
attachment
|
|
141
|
+
</div>
|
|
142
|
+
) : (
|
|
143
|
+
conversation.lastMessage.message
|
|
144
|
+
)}
|
|
116
145
|
</p>
|
|
117
146
|
</div>
|
|
118
147
|
</div>
|
|
@@ -120,4 +149,4 @@ const Conversation = ({ conversation }: ConversationProps) => {
|
|
|
120
149
|
);
|
|
121
150
|
};
|
|
122
151
|
|
|
123
|
-
export default Conversation;
|
|
152
|
+
export default Conversation;
|
package/src/style/style.css
CHANGED
|
@@ -1559,4 +1559,82 @@ font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe U
|
|
|
1559
1559
|
.cancel-edit {
|
|
1560
1560
|
background-color: #f44336;
|
|
1561
1561
|
color: white;
|
|
1562
|
+
}
|
|
1563
|
+
.system-message.booking-details {
|
|
1564
|
+
display: flex;
|
|
1565
|
+
flex-direction: column;
|
|
1566
|
+
align-items: center;
|
|
1567
|
+
justify-content: center;
|
|
1568
|
+
max-width: 400px;
|
|
1569
|
+
margin: 20px auto;
|
|
1570
|
+
padding: 24px;
|
|
1571
|
+
background: #f8f9fa;
|
|
1572
|
+
border: 1px solid #e9ecef;
|
|
1573
|
+
border-radius: 12px;
|
|
1574
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
1575
|
+
text-align: center;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
.system-message.booking-details h4 {
|
|
1579
|
+
margin: 0 0 16px 0;
|
|
1580
|
+
font-size: 18px;
|
|
1581
|
+
font-weight: 600;
|
|
1582
|
+
color: #2c3e50;
|
|
1583
|
+
display: flex;
|
|
1584
|
+
align-items: center;
|
|
1585
|
+
gap: 8px;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
.system-message.booking-details h4::before {
|
|
1589
|
+
content: "✓";
|
|
1590
|
+
display: inline-block;
|
|
1591
|
+
width: 24px;
|
|
1592
|
+
height: 24px;
|
|
1593
|
+
background: #2cb1aa;
|
|
1594
|
+
color: white;
|
|
1595
|
+
border-radius: 50%;
|
|
1596
|
+
font-size: 14px;
|
|
1597
|
+
line-height: 24px;
|
|
1598
|
+
text-align: center;
|
|
1599
|
+
font-weight: bold;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
.system-message.booking-details .details {
|
|
1603
|
+
width: 100%;
|
|
1604
|
+
background: white;
|
|
1605
|
+
border-radius: 8px;
|
|
1606
|
+
padding: 16px;
|
|
1607
|
+
border: 1px solid #dee2e6;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
.system-message.booking-details .details p {
|
|
1611
|
+
margin: 8px 0;
|
|
1612
|
+
padding: 8px 0;
|
|
1613
|
+
border-bottom: 1px solid #f1f3f4;
|
|
1614
|
+
font-size: 14px;
|
|
1615
|
+
color: #495057;
|
|
1616
|
+
display: flex;
|
|
1617
|
+
justify-content: space-between;
|
|
1618
|
+
align-items: center;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
.system-message.booking-details .details p:last-child {
|
|
1622
|
+
border-bottom: none;
|
|
1623
|
+
font-weight: 600;
|
|
1624
|
+
color: #2cb1aa;
|
|
1625
|
+
font-size: 16px;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
.system-message.booking-details .details p strong {
|
|
1629
|
+
color: #2c3e50;
|
|
1630
|
+
font-weight: 500;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/* Responsive design */
|
|
1634
|
+
@media (max-width: 480px) {
|
|
1635
|
+
.system-message.booking-details {
|
|
1636
|
+
max-width: 90%;
|
|
1637
|
+
margin: 16px auto;
|
|
1638
|
+
padding: 20px;
|
|
1639
|
+
}
|
|
1562
1640
|
}
|
package/src/types/type.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface ConversationProps {
|
|
|
52
52
|
senderId: string;
|
|
53
53
|
message: string;
|
|
54
54
|
media:string[];
|
|
55
|
+
type?:'user' | 'system' | 'system-completion';
|
|
56
|
+
status: MessageStatus;
|
|
55
57
|
chatId: string;
|
|
56
58
|
createdAt: string;
|
|
57
59
|
updatedAt: string;
|
|
@@ -70,4 +72,4 @@ export interface ConversationProps {
|
|
|
70
72
|
lastIdx: boolean;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
export type MessageStatus = 'sent' | 'delivered' | 'read' | 'sending' | 'failed';
|
|
75
|
+
export type MessageStatus = 'sent' | 'delivered' | 'read' | 'sending' | 'failed' | 'edited' | 'deleted';
|