/* 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 (
);
case "delivered":
return (
);
case "read":
return (
);
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 && (
<>
>
)}
{/* Error state */}
{media.uploadError && (
⚠️ Upload failed
)}
{/* Actual media (shown when upload complete) */}
{(!message.isUploading || media.uploadProgress === 100) &&
!media.uploadError && (
<>
{media.type === "image" ? (

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" ? (
) : (
(message.message ||
(message.media && message.media.length > 0)) && (
{renderMedia()}
{isEditingMode ? (
) : (
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;