import React, { useState, useEffect, useRef, useMemo } 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,
MoreVert as MoreVertIcon,
} 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 handleMessageOptionsClick = (event, message) => {
if ([Link] === senderId) {
setAnchorEl([Link]);
setSelectedMessage(message);
}
};
const handleEditClick = () => {
setIsEditing(true);
setEditedMessage([Link]);
setAnchorEl(null);
};
const handleDeleteClick = () => {
setDeleteConfirmOpen(true);
setAnchorEl(null);
};
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, // Initially hidden
transition: 'opacity 0.3s ease-in-out', // Smooth appearance
}}
>
<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}
sx={{
display: 'flex',
justifyContent: [Link] === senderId ? 'flex-end' : 'flex-
start',
width: '100%', // Ensure the container takes full width
}}
>
<Box sx={{ display: 'flex',
flexDirection: 'column',
maxWidth: '70%',
width: 'auto', // Allow the box to size based on content
minWidth: 0, // Allow the box to shrink below its content size
}}>
<MessageContent message={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={() => setAnchorEl(null)}
>
<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;