import React, { useState, useEffect, useRef } from 'react';
import { Card, CardHeader, CardContent,Button,
TextField,Alert,Box,Typography,Paper,
IconButton,InputAdornment,Collapse,Popover, Menu,
MenuItem,Dialog,DialogTitle,DialogContent,
DialogActions,Tooltip
} from '@mui/material';
import {
Chat as ChatIcon,
Send as SendIcon,
Check as CheckIcon,
DoneAll as DoneAllIcon,
Search as SearchIcon,
Close as CloseIcon,
ArrowUpward as ArrowUpwardIcon,
ArrowDownward as ArrowDownwardIcon,
Edit as EditIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import io from '[Link]-client';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; // Import
dropdown icon
const ChatWindow = ({ senderId, receiverId, bidId, role }) => {
const BACKEND_API = [Link].REACT_APP_BACKEND_API;
const [messages, setMessages] = useState([]);
const [message, setMessage] = useState('');
const [socketConnected, setSocketConnected] = useState(false);
const [connectionError, setConnectionError] = useState(null);
const socketRef = useRef(null);
const messageContainerRef = useRef(null);
const scrollRef = useRef(null);
const [showSearch, setShowSearch] = useState(false);
const [searchText, setSearchText] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [currentResultIndex, setCurrentResultIndex] = useState(-1);
const textFieldRef = useRef(null);
// const [anchorEl, setAnchorEl] = useState(null);
const [selectedMessage, setSelectedMessage] = useState(null);
const [isEditing, setIsEditing] = useState(false);
const [editedMessage, setEditedMessage] = useState('');
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const [activeMessageId, setActiveMessageId] = useState(null);
const handleMenuOpen = (event, messageId) => {
setAnchorEl([Link]);
setActiveMessageId(messageId);
};
const handleMenuClose = () => {
setAnchorEl(null);
setActiveMessageId(null);
setSelectedMessage(null);
};
const handleMessageOptionsClick = (event, message) => {
if ([Link] === senderId) {
// Find the container of the specific message
const messageContainer = [Link]('.message-container');
setAnchorEl(messageContainer);
setActiveMessageId(message._id);
setSelectedMessage(message);
}
};
const handleEditClick = () => {
setIsEditing(true);
setEditedMessage([Link]);
setAnchorEl(null);
handleMenuClose();
};
const handleDeleteClick = () => {
setDeleteConfirmOpen(true);
setAnchorEl(null);
handleMenuClose();
};
const handleEditSubmit = async () => {
if (![Link]() || !selectedMessage) return;
try {
const response = await fetch(`${BACKEND_API}api/messages/$
{selectedMessage._id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: [Link]({ text: [Link]() }),
});
if ([Link]) {
const updatedMessage = await [Link]();
setMessages(prevMessages =>
[Link](msg =>
msg._id === selectedMessage._id
? {
...msg,
text: [Link],
isEdited: true,
editedAt: [Link]
}
: msg
)
);
[Link]('messageEdited', updatedMessage);
}
} catch (error) {
[Link]('Error editing message:', error);
}
setIsEditing(false);
setSelectedMessage(null);
};
const handleDeleteConfirm = async () => {
if (!selectedMessage) return;
try {
const response = await fetch(`${BACKEND_API}api/messages/$
{selectedMessage._id}`, {
method: 'DELETE',
});
if ([Link]) {
setMessages(prevMessages =>
[Link](msg => msg._id !== selectedMessage._id)
);
[Link]('messageDeleted', selectedMessage._id);
}
} catch (error) {
[Link]('Error deleting message:', error);
}
setDeleteConfirmOpen(false);
setSelectedMessage(null);
};
const handleSearchIconClick = () => {
setShowSearch(!showSearch);
setTimeout(() => {
[Link]?.focus();
}, 0);
};
const handleSearch = (text) => {
setSearchText(text);
if (![Link]()) {
setSearchResults([]);
setCurrentResultIndex(-1);
return;
}
const results = [Link]((acc, msg, index) => {
if ([Link]().includes([Link]())) {
[Link](index);
}
return acc;
}, []);
setSearchResults(results);
setCurrentResultIndex([Link] > 0 ? 0 : -1);
if ([Link] > 0) {
scrollToMessage(results[0]);
}
};
const scrollToMessage = (messageIndex) => {
const messageElements = [Link]('.message-
content');
if (messageElements[messageIndex]) {
messageElements[messageIndex].scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
};
const navigateSearch = (direction) => {
if ([Link] === 0) return;
let newIndex;
if (direction === 'up') {
newIndex = currentResultIndex > 0 ? currentResultIndex - 1 :
[Link] - 1;
} else {
newIndex = currentResultIndex < [Link] - 1 ? currentResultIndex +
1 : 0;
}
setCurrentResultIndex(newIndex);
scrollToMessage(searchResults[newIndex]);
};
const highlightText = (text, searchTerm) => {
if (![Link]()) return text;
const parts = [Link](new RegExp(`(${searchTerm})`, 'gi'));
return [Link]((part, index) =>
[Link]() === [Link]() ? (
<span
key={index}
style={{
backgroundColor: '#ffeb3b',
padding: '0 2px',
borderRadius: '2px'
}}
>
{part}
</span>
) : part
);
};
const scrollToBottom = (behavior = 'smooth') => {
if ([Link]) {
[Link] =
[Link];
}
};
useEffect(() => {
scrollToBottom('auto');
}, [messages]);
const formatMessageDate = (timestamp) => {
if (!timestamp) return '';
try {
const date = new Date(timestamp);
if (isNaN([Link]())) return '';
return [Link]([], {
hour: '2-digit',
minute: '2-digit',
hour12: true
});
} catch (error) {
[Link]('Error formatting date:', error);
return '';
}
};
// Handle message visibility and read status
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
[Link]((entry) => {
if ([Link]) {
const messageId = [Link]('data-message-id');
const messageData = [Link](msg => msg._id === messageId);
if (messageData &&
[Link] === receiverId &&
[Link] !== 'read') {
handleMessageRead(messageId);
}
}
});
},
{ threshold: 0.5 }
);
const messageElements = [Link]('.message-content');
[Link]((element) => [Link](element));
return () => {
[Link]((element) => [Link](element));
};
}, [messages, receiverId]);
const handleMessageRead = (messageId) => {
if ([Link]?.connected) {
const unreadMessages = [Link](
msg => [Link] === receiverId &&
[Link] !== 'read' &&
(messageId ? msg._id === messageId : true)
);
if ([Link] > 0) {
const messageIds = [Link](msg => msg._id);
[Link]('messageRead', {
messageIds,
sender: receiverId
});
}
}
};
useEffect(() => {
if ([Link]) {
const { scrollHeight, scrollTop, clientHeight } =
[Link];
const isAtBottom = [Link](scrollHeight - scrollTop - clientHeight) < 50;
if (isAtBottom) {
scrollToBottom();
}
}
}, [messages]);
useEffect(() => {
if (senderId && receiverId) {
if ([Link]) {
[Link]();
}
const socketURL = BACKEND_API.endsWith('/') ? BACKEND_API.slice(0, -1) :
BACKEND_API;
[Link] = io(socketURL, {
withCredentials: true,
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
timeout: 20000,
autoConnect: true
});
const handleReceiveMessage = (newMessage) => {
setMessages(prevMessages => {
const messageWithFormattedTime = {
...newMessage,
timestamp: [Link] ? new
Date([Link]).toISOString() : new Date().toISOString(),
isEdited: [Link] || false, // Ensure isEdited is preserved
editedAt: [Link] ? new
Date([Link]).toISOString() : null,
};
const messageExists = [Link](msg =>
msg._id === messageWithFormattedTime._id ||
([Link] === [Link] &&
[Link] === [Link] &&
[Link] === [Link])
);
if (messageExists) return prevMessages;
return [...prevMessages, messageWithFormattedTime];
});
};
const handleMessageDelivered = ({ messageId }) => {
setMessages(prevMessages =>
[Link](msg =>
msg._id === messageId ? { ...msg, status: 'delivered' } : msg
)
);
};
const handleMessagesRead = ({ messageIds, readAt }) => {
setMessages(prevMessages =>
[Link](msg =>
[Link](msg._id)
? { ...msg, status: 'read', readAt }
: msg
)
);
};
const handleMessagesReadConfirmation = ({ messageIds, readAt }) => {
setMessages(prevMessages =>
[Link](msg =>
[Link](msg._id)
? { ...msg, status: 'read', readAt }
: msg
)
);
};
[Link]('connect', () => {
setSocketConnected(true);
setConnectionError(null);
[Link]('register', senderId);
});
[Link]('receiveMessage', handleReceiveMessage);
[Link]('messageDelivered', handleMessageDelivered);
[Link]('messagesRead', handleMessagesRead);
[Link]('messagesReadConfirmation',
handleMessagesReadConfirmation);
[Link]('connect_error', (error) => {
setSocketConnected(false);
setConnectionError(`Connection error: ${[Link]}`);
});
fetch(`${BACKEND_API}api/messages/${bidId}/${senderId}/${receiverId}`)
.then(response => [Link]())
.then(data => {
const formattedData = [Link](msg => ({
...msg,
timestamp: [Link] ? new Date([Link]).toISOString() : new
Date().toISOString(),
readAt: [Link] ? new Date([Link]).toISOString() : null,
editedAt: [Link] ? new Date([Link]).toISOString() : null,
isEdited: [Link] || false, // Ensure isEdited is preserved
}));
setMessages(formattedData);
setTimeout(scrollToBottom, 100);
})
.catch(err => [Link]('Error fetching messages:', err));
[Link]('messageUpdated', (updatedMessage) => {
setMessages(prevMessages =>
[Link](msg =>
msg._id === updatedMessage._id
? {
...msg,
text: [Link],
isEdited: true,
editedAt: [Link]
}
: msg
)
);
});
return () => {
if ([Link]) {
[Link]('receiveMessage', handleReceiveMessage);
[Link]('messageDelivered', handleMessageDelivered);
[Link]('messagesRead', handleMessagesRead);
[Link]('messagesReadConfirmation',
handleMessagesReadConfirmation);
[Link]();
[Link]?.off('messageUpdated');
}
};
}
},
[senderId, receiverId, bidId, BACKEND_API]);
const MessageStatus = ({ status, readAt }) => {
if (status === 'read') {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<DoneAllIcon sx={{ fontSize: 16, color: '[Link]' }} />
<Typography variant="caption" color="[Link]">
{formatMessageDate(readAt)}
</Typography>
</Box>
);
}
if (status === 'delivered') {
return <DoneAllIcon sx={{ fontSize: 16, color: '[Link]' }} />;
}
return <CheckIcon sx={{ fontSize: 16, color: '[Link]' }} />;
};
const MessageContent = ({ message }) => (
<Box
sx={{
position: 'relative',
'&:hover .message-options': {
opacity: 1,
},
}}
>
<Paper
className="message-content"
data-message-id={message._id}
elevation={1}
sx={{
p: 1.5,
bgcolor: [Link] === senderId ? '[Link]' : 'grey.100',
color: [Link] === senderId ? '[Link]' :
'[Link]',
wordBreak: 'break-word',
overflowWrap: 'break-word',
whiteSpace: 'pre-wrap',
maxWidth: '100%',
}}
>
<Typography variant="body2" component="div">
{highlightText([Link], searchText)}
</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mt: 0.5,
opacity: 0.7,
}}
>
<Typography
variant="caption"
sx={{
color: [Link] === senderId ? '[Link]' :
'[Link]',
}}
>
{formatMessageDate([Link])}
</Typography>
{[Link] && (
<Tooltip
title={`Edited ${[Link] ?
formatMessageDate([Link]) : ''}`}
placement="top"
>
<Typography
variant="caption"
sx={{
color: [Link] === senderId ? '[Link]' :
'[Link]',
fontStyle: 'italic',
}}
>
(edited)
</Typography>
</Tooltip>
)}
</Box>
</Paper>
{[Link] === senderId && (
<IconButton
size="small"
className="message-options"
onClick={(e) => handleMessageOptionsClick(e, message)}
sx={{
position: 'absolute',
top: '50%',
right: -22,
transform: 'translateY(-50%)',
opacity: 0,
transition: 'opacity 0.3s ease-in-out',
}}
>
<ArrowDropDownIcon fontSize="small" />
</IconButton>
)}
</Box>
);
const handleSendMessage = () => {
if ([Link]() && [Link]?.connected) {
const newMessage = {
sender: senderId,
receiver: receiverId,
bidId,
text: message,
timestamp: new Date().toISOString(),
status: 'sent'
};
fetch(`${BACKEND_API}api/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: [Link](newMessage),
})
.then(response => [Link]())
.then(savedMessage => {
[Link]('sendMessage', savedMessage);
setMessages(prevMessages => {
const messageExists = [Link](msg =>
[Link] === [Link] &&
[Link] === [Link] &&
[Link] === [Link]
);
if (messageExists) return prevMessages;
return [...prevMessages, savedMessage];
});
setMessage('');
})
.catch(err => {
[Link]('Error sending message:', err);
});
}
};
return (
<Card sx={{ width: '100%', maxWidth: 750, mx: 'auto' }}>
<CardHeader
sx={{
bgcolor: '[Link]',
color: '[Link]',
p: 2,
}}
title={
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems:
'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ChatIcon fontSize="small" />
<Typography variant="h6">Chat with {role}</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton
size="small"
onClick={handleSearchIconClick}
sx={{ color: 'inherit' }}
>
<SearchIcon />
</IconButton>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
{socketConnected ? 'Connected' : 'Disconnected'}
</Typography>
</Box>
</Box>
}
/>
<Collapse in={showSearch}>
<Box sx={{ p: 2, bgcolor: 'grey.100' }}>
<TextField
fullWidth
size="small"
placeholder="Search messages..."
value={searchText}
onChange={(e) => handleSearch([Link])}
inputRef={textFieldRef}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
{[Link] > 0 && (
<>
<Typography variant="caption" sx={{ mr: 1 }}>
{currentResultIndex + 1} of {[Link]}
</Typography>
<IconButton size="small" onClick={() =>
navigateSearch('up')}>
<ArrowUpwardIcon fontSize="small" />
</IconButton>
<IconButton size="small" onClick={() =>
navigateSearch('down')}>
<ArrowDownwardIcon fontSize="small" />
</IconButton>
</>
)}
{searchText && (
<IconButton
size="small"
onClick={() => {
setSearchText('');
setSearchResults([]);
setCurrentResultIndex(-1);
setShowSearch(false);
}}
>
<CloseIcon fontSize="small" />
</IconButton>
)}
</InputAdornment>
),
}}
/>
</Box>
</Collapse>
{connectionError && (
<Alert severity="error" sx={{ m: 1 }}>
{connectionError}
</Alert>
)}
<CardContent sx={{ p: 2 }}>
<Box
ref={messageContainerRef}
sx={{
height: 400,
overflowY: 'auto',
overflowX: 'hidden', // Prevent horizontal scrolling
scrollBehavior: 'smooth',
pr: 2,
'&::-webkit-scrollbar': {
width: 6,
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(0,0,0,.2)',
borderRadius: 3,
},
'&::-webkit-scrollbar-track': {
backgroundColor: 'rgba(0,0,0,.05)',
borderRadius: 3,
}
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{[Link]((msg, index) => (
<Box
key={msg._id || index}
className="message-container"
sx={{
display: 'flex',
justifyContent: [Link] === senderId ? 'flex-end' : 'flex-start',
width: '100%',
position: 'relative', // Add relative positioning
}}
>
<Box sx={{
display: 'flex',
flexDirection: 'column',
maxWidth: '70%',
width: 'auto',
minWidth: 0,
}}>
<MessageContent
message={msg}
onOptionsClick={(e) => handleMessageOptionsClick(e, msg)}
/>
{[Link] === senderId && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 0.5 }}>
<MessageStatus status={[Link]} readAt={[Link]} />
</Box>
)}
</Box>
</Box>
))}
<div ref={scrollRef} />
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
<TextField
fullWidth
size="small"
placeholder="Type a message..."
value={message}
onChange={(e) => setMessage([Link])}
onKeyDown={(e) => {
if ([Link] === 'Enter' && ![Link]) {
[Link]();
handleSendMessage();
}
}}
multiline
sx={{
'& .MuiInputBase-root': {
wordBreak: 'break-word',
overflowWrap: 'break-word'
}
}}
/>
<Button
variant="contained"
disabled={!socketConnected || ![Link]()}
onClick={handleSendMessage}
sx={{ minWidth: 'unset', px: 2 }}
>
<SendIcon fontSize="small" />
</Button>
</Box>
</CardContent>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
anchorOrigin={{
vertical: 'center', // Center vertically with the message
horizontal: 'left', // Position to the left of the message
}}
transformOrigin={{
vertical: 'center',
horizontal: 'right', // Align menu's right edge with message
}}
PaperProps={{
sx: {
// Optional: Add a slight offset from the message
ml: -2, // Move menu slightly away from the message
}
}}
container={[Link]}
>
<MenuItem onClick={handleEditClick}>
<EditIcon fontSize="small" sx={{ mr: 1 }} />
Edit
</MenuItem>
<MenuItem onClick={handleDeleteClick}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
Delete
</MenuItem>
</Menu>
<Dialog open={isEditing} onClose={() => setIsEditing(false)}>
<DialogTitle>Edit Message</DialogTitle>
<DialogContent>
<TextField
fullWidth
multiline
value={editedMessage}
onChange={(e) => setEditedMessage([Link])}
sx={{ mt: 2 }}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setIsEditing(false)}>Cancel</Button>
<Button onClick={handleEditSubmit} variant="contained">
Save
</Button>
</DialogActions>
</Dialog>
<Dialog
open={deleteConfirmOpen}
onClose={() => setDeleteConfirmOpen(false)}
>
<DialogTitle>Delete Message</DialogTitle>
<DialogContent>
Are you sure you want to delete this message?
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteConfirmOpen(false)}>Cancel</Button>
<Button onClick={handleDeleteConfirm} color="error">
Delete
</Button>
</DialogActions>
</Dialog>
</Card>
);
};
export default ChatWindow;