/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { MessageStatus } from "../../types/type"; import { useChatContext } from "../../providers/ChatProvider"; import { FileType } from "../common/FilePreview"; import { getChatConfig } from "../../Chat.config"; import { Path } from "../../lib/api/endpoint"; import { MoreHorizontal, Pencil, Trash2 } from "lucide-react"; import { useEditMessageMutation } from "../../hooks/mutations/useEditMessage"; import { useDeleteMessageMutation } from "../../hooks/mutations/useDeleteMessage"; import { useEffect, useRef, useState } from "react"; import BookingCard from "./BookingCard"; interface MessageProps { message: { _id?: string; sender: { senderId: string; role: string; }; message: string; status: MessageStatus; createdAt: any; updatedAt: any; media?: { type: FileType; url: string; name: string; size: number; uploadProgress?: number; uploadError: string | null; }[]; isUploading?: boolean; isEdited?: boolean; isDeleted?: boolean; onEdit?: (messageId: string, newMessage: string) => void; onDelete?: (messageId: string) => void; type?: "user" | "system" | "system-completion"; meta?: { bookingDetails?: { bookingId: string; status: string; // e.g., "confirmed", "pending", "cancelled" serviceId: string; serviceName: string; date: string; time: string; price: number; // Add other booking details as needed }; reviewLink?: string; }; }; } const Message = ({ message }: MessageProps) => { const { userId } = useChatContext(); const { apiUrl, cdnUrl, role: role1 } = getChatConfig(); if (message.type === "system") { const booking = message.meta?.bookingDetails; const status = booking?.status || "Pending"; // Default to Pending const role = role1; // "provider" or "customer" const handleConfirm = () => { console.log("Booking confirmed!"); window.location.href = `/booking/all#${message.meta?.bookingDetails?.bookingId}`; // Update booking status to Confirmed }; const handleInProgress = () => { console.log("Booking started (In Progress)"); window.location.href = `/booking/all#${message.meta?.bookingDetails?.bookingId}`; // Update booking status to InProgress }; const handleComplete = () => { console.log("Booking completed!"); window.location.href = `/booking/all#${message.meta?.bookingDetails?.bookingId}`; // Update booking status to Completed }; const handleReview = () => { if (role === "customer") { console.log("Navigate to review page for customer → provider"); window.location.href = `/customer/customer-booking?bookingId=${message.meta?.bookingDetails?.bookingId}`; } else if (role === "provider") { console.log("Navigate to review page for provider → customer"); window.location.href = `/booking/all#${message.meta?.bookingDetails?.bookingId}`; } }; return (
{role === "provider" && status === "Pending" && ( )} {role === "provider" && status === "Confirmed" && ( )} {role === "provider" && status === "InProgress" && ( )} {status === "Completed" && role !== "provider" && ( )} } />
); } if (message.type === "system-completion") { return (
Leave a Review } />
); } const fromMe = message.sender?.senderId === userId; const timestamp = fromMe ? "timestamp_outgoing" : "timestamp_incomeing"; const alignItems = fromMe ? "outgoing" : "incoming"; const [localStatus, setLocalStatus] = useState(message.status); const [showOptions, setShowOptions] = useState(false); const [showDeleteOption, setShowDeleteOption] = useState(false); const editInputRef = useRef(null); const optionsRef = useRef(null); const { mutate: editMessage } = useEditMessageMutation(); const [editedMessage, setEditedMessage] = useState(""); const [isEditingMode, setIsEditingMode] = useState(false); const { mutate: deleteMessage, isPending: isDeleting } = useDeleteMessageMutation(); useEffect(() => { setLocalStatus(message.status); }, [message.status]); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if ( optionsRef.current && !optionsRef.current.contains(e.target as Node) ) { setShowOptions(false); setShowDeleteOption(false); } }; if (showOptions || showDeleteOption) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [showOptions, showDeleteOption]); // const handleDownload = (url: string, name: string) => { // saveAs(url, name); // }; const [downloadingIndex, setDownloadingIndex] = useState(null); const [downloadProgress, setDownloadProgress] = useState(0); const [downloadController, setDownloadController] = useState(null); const getStatusIcon = () => { if (!fromMe) return null; if (message.isUploading || message.status === "sending") { return 🔄; } if (message.status === "failed") { return ; } switch (localStatus) { case "sending": return 🔄; case "sent": return ( msg-check ); case "delivered": return ( msg-dblcheck ); case "read": return ( msg-dblcheck ); case "edited": return Edited; case "deleted": return Deleted; default: return null; } }; const cancelDownload = () => { if (downloadController) { downloadController.abort(); setDownloadingIndex(null); setDownloadProgress(0); setDownloadController(null); } }; const handleDownload = async (url: string, name: string, index: number) => { setDownloadingIndex(index); setDownloadProgress(0); const controller = new AbortController(); setDownloadController(controller); try { try { const response = await fetch( `${apiUrl}${Path.apiProxy}?url=${encodeURIComponent( url )}&name=${encodeURIComponent(name)}` ); if (!response.ok) throw new Error("Network response was not ok"); const contentLength = response.headers.get("content-length"); const total = contentLength ? Number.parseInt(contentLength, 10) : 0; const reader = response.body?.getReader(); if (!reader) throw new Error("Failed to get response reader"); let receivedLength = 0; const chunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); receivedLength += value.length; if (total) { const progress = Math.round((receivedLength / total) * 100); setDownloadProgress(progress); } } const chunksAll = new Uint8Array(receivedLength); let position = 0; for (const chunk of chunks) { chunksAll.set(chunk, position); position += chunk.length; } const blob = new Blob([chunksAll]); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = name; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); } catch (directDownloadError) { console.log( "Direct download failed, trying alternative method", directDownloadError ); // Fallback: Open in new tab if download fails window.open(url, "_blank"); } } catch (error) { if ((error as Error).name === "AbortError") { console.log("Download was cancelled"); } else { console.error("Download failed:", error); // Final fallback - create a temporary link const link = document.createElement("a"); link.href = url; link.target = "_blank"; link.rel = "noopener noreferrer"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } finally { setDownloadingIndex(null); setDownloadProgress(0); setDownloadController(null); } }; const renderMedia = () => { if (!message.media || message.media.length === 0) return null; return (
1 ? "multi-media" : "single-media" }`} > {message.media.map((media, index) => (
{/* Progress indicator */} {message.isUploading && media.uploadProgress !== undefined && media.uploadProgress < 100 && ( <>
{media.name}
{media.uploadProgress}%
)} {/* Error state */} {media.uploadError && (
⚠️ Upload failed
)} {/* Actual media (shown when upload complete) */} {(!message.isUploading || media.uploadProgress === 100) && !media.uploadError && ( <> {media.type === "image" ? ( {media.name} window.open(`${cdnUrl}${media.url}`, "_blank") } /> ) : media.type === "video" ? ( ) : (
{media.type === "document" && "📄"}
window.open(`${cdnUrl}${media.url}`, "_blank") }> {media.name} {(media.size / 1024).toFixed(1)} KB
)} )}
))}
); }; const handleEditClick = (message: string) => { setEditedMessage(message); setIsEditingMode(true); setShowOptions(false); setTimeout(() => { editInputRef.current?.focus(); }, 0); }; const handleSaveEdit = () => { if (message._id && editedMessage.trim() !== message.message) { editMessage( { messageId: message._id ?? "", userId: userId, // Using userId from useChatContext newMessage: editedMessage.trim(), }, { onSuccess: () => { setIsEditingMode(false); setShowOptions(false); setEditedMessage(""); // Clear the input after saving // Any additional success handling }, onError: (error) => { // Handle error specifically for this edit console.error("Edit failed:", error); }, } ); } else { setIsEditingMode(false); setShowOptions(false); } }; const handleCancelEdit = () => { setEditedMessage(message.message); setIsEditingMode(false); setShowOptions(false); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { handleSaveEdit(); } else if (e.key === "Escape") { handleCancelEdit(); } }; const handleDeleteClick = () => { if (message._id) { deleteMessage( { messageId: message._id, userId: userId, }, { onSuccess: () => { setShowDeleteOption(false); setShowOptions(false); }, onError: (error) => { console.error("Delete failed:", error); }, } ); } }; const isMessageOlderThanOneDay = (createdAt: string | Date) => { const messageDate = new Date(createdAt); const now = new Date(); const oneDayInMs = 24 * 60 * 60 * 1000; return now.getTime() - messageDate.getTime() > oneDayInMs; }; const formatLastMessageTime = (dateString: string) => { // Validate date input if (!dateString) { return ""; } const date = new Date(dateString); // Check if date is valid if (isNaN(date.getTime())) { return ""; } const now = new Date(); const isToday = date.toDateString() === now.toDateString(); const yesterday = new Date(); yesterday.setDate(now.getDate() - 1); const isYesterday = date.toDateString() === yesterday.toDateString(); if (isToday) { return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); } if (isYesterday) { return "Yesterday"; } // MM/DD format return date.toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", }); }; const messageTime = message.status === "deleted" || message.status === "edited" ? message.updatedAt : message.createdAt; const hasMedia = message.media && message.media.length > 0; return (
fromMe && !hasMedia && setShowOptions(true)} onMouseLeave={() => fromMe && !showDeleteOption && setShowOptions(false) } > {message.status === "deleted" ? (
This message was deleted
) : ( (message.message || (message.media && message.media.length > 0)) && (
{renderMedia()} {isEditingMode ? (
setEditedMessage(e.target.value)} onKeyDown={handleKeyDown} className="edit-message-input" />
) : ( message.message && (
{message.message}
) )} {/* Message options for outgoing messages */} {fromMe && showOptions && !isEditingMode && !message.isDeleted && !hasMedia && !isMessageOlderThanOneDay(message.createdAt) && (
{showDeleteOption && (
)}
)}
) )}
{formatLastMessageTime(messageTime)} {getStatusIcon()}
); }; export default Message;