Edit Del Not Working Codee)

Download as txt, pdf, or txt
Download as txt, pdf, or txt
You are on page 1of 14

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 'socket.io-client';

import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; // Import


dropdown icon
const ChatWindow = ({ senderId, receiverId, bidId, role }) => {
const BACKEND_API = process.env.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(event.currentTarget);
setActiveMessageId(messageId);
};

const handleMenuClose = () => {


setAnchorEl(null);
setActiveMessageId(null);
setSelectedMessage(null);
};

const handleMessageOptionsClick = (event, message) => {


if (message.sender === senderId) {
// Find the container of the specific message
const messageContainer = event.currentTarget.closest('.message-container');
setAnchorEl(messageContainer);
setActiveMessageId(message._id);
setSelectedMessage(message);
}
};

const handleEditClick = () => {


setIsEditing(true);
setEditedMessage(selectedMessage.text);
setAnchorEl(null);
handleMenuClose();
};

const handleDeleteClick = () => {


setDeleteConfirmOpen(true);
setAnchorEl(null);
handleMenuClose();
};

const handleEditSubmit = async () => {


if (!editedMessage.trim() || !selectedMessage) return;

try {
const response = await fetch(`${BACKEND_API}api/messages/$
{selectedMessage._id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: editedMessage.trim() }),
});

if (response.ok) {
const updatedMessage = await response.json();
setMessages(prevMessages =>
prevMessages.map(msg =>
msg._id === selectedMessage._id
? {
...msg,
text: updatedMessage.text,
isEdited: true,
editedAt: updatedMessage.editedAt
}
: msg
)
);
socketRef.current.emit('messageEdited', updatedMessage);
}
} catch (error) {
console.error('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 (response.ok) {
setMessages(prevMessages =>
prevMessages.filter(msg => msg._id !== selectedMessage._id)
);
socketRef.current.emit('messageDeleted', selectedMessage._id);
}
} catch (error) {
console.error('Error deleting message:', error);
}

setDeleteConfirmOpen(false);
setSelectedMessage(null);
};

const handleSearchIconClick = () => {


setShowSearch(!showSearch);
setTimeout(() => {
textFieldRef.current?.focus();
}, 0);
};

const handleSearch = (text) => {


setSearchText(text);
if (!text.trim()) {
setSearchResults([]);
setCurrentResultIndex(-1);
return;
}

const results = messages.reduce((acc, msg, index) => {


if (msg.text.toLowerCase().includes(text.toLowerCase())) {
acc.push(index);
}
return acc;
}, []);

setSearchResults(results);
setCurrentResultIndex(results.length > 0 ? 0 : -1);

if (results.length > 0) {
scrollToMessage(results[0]);
}
};

const scrollToMessage = (messageIndex) => {


const messageElements = messageContainerRef.current.querySelectorAll('.message-
content');
if (messageElements[messageIndex]) {
messageElements[messageIndex].scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
};

const navigateSearch = (direction) => {


if (searchResults.length === 0) return;

let newIndex;
if (direction === 'up') {
newIndex = currentResultIndex > 0 ? currentResultIndex - 1 :
searchResults.length - 1;
} else {
newIndex = currentResultIndex < searchResults.length - 1 ? currentResultIndex +
1 : 0;
}

setCurrentResultIndex(newIndex);
scrollToMessage(searchResults[newIndex]);
};

const highlightText = (text, searchTerm) => {


if (!searchTerm.trim()) return text;

const parts = text.split(new RegExp(`(${searchTerm})`, 'gi'));


return parts.map((part, index) =>
part.toLowerCase() === searchTerm.toLowerCase() ? (
<span
key={index}
style={{
backgroundColor: '#ffeb3b',
padding: '0 2px',
borderRadius: '2px'
}}
>
{part}
</span>
) : part
);
};

const scrollToBottom = (behavior = 'smooth') => {


if (messageContainerRef.current) {
messageContainerRef.current.scrollTop =
messageContainerRef.current.scrollHeight;
}
};

useEffect(() => {
scrollToBottom('auto');
}, [messages]);

const formatMessageDate = (timestamp) => {


if (!timestamp) return '';
try {
const date = new Date(timestamp);
if (isNaN(date.getTime())) return '';

return date.toLocaleString([], {
hour: '2-digit',
minute: '2-digit',
hour12: true
});
} catch (error) {
console.error('Error formatting date:', error);
return '';
}
};

// Handle message visibility and read status


useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const messageId = entry.target.getAttribute('data-message-id');
const messageData = messages.find(msg => msg._id === messageId);

if (messageData &&
messageData.sender === receiverId &&
messageData.status !== 'read') {
handleMessageRead(messageId);
}
}
});
},
{ threshold: 0.5 }
);

const messageElements = document.querySelectorAll('.message-content');


messageElements.forEach((element) => observer.observe(element));

return () => {
messageElements.forEach((element) => observer.unobserve(element));
};
}, [messages, receiverId]);

const handleMessageRead = (messageId) => {


if (socketRef.current?.connected) {
const unreadMessages = messages.filter(
msg => msg.sender === receiverId &&
msg.status !== 'read' &&
(messageId ? msg._id === messageId : true)
);

if (unreadMessages.length > 0) {
const messageIds = unreadMessages.map(msg => msg._id);
socketRef.current.emit('messageRead', {
messageIds,
sender: receiverId
});
}
}
};

useEffect(() => {
if (messageContainerRef.current) {
const { scrollHeight, scrollTop, clientHeight } =
messageContainerRef.current;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 50;
if (isAtBottom) {
scrollToBottom();
}
}
}, [messages]);

useEffect(() => {
if (senderId && receiverId) {
if (socketRef.current) {
socketRef.current.disconnect();
}

const socketURL = BACKEND_API.endsWith('/') ? BACKEND_API.slice(0, -1) :


BACKEND_API;

socketRef.current = 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: newMessage.timestamp ? new
Date(newMessage.timestamp).toISOString() : new Date().toISOString(),
isEdited: newMessage.isEdited || false, // Ensure isEdited is preserved
editedAt: newMessage.editedAt ? new
Date(newMessage.editedAt).toISOString() : null,
};

const messageExists = prevMessages.some(msg =>


msg._id === messageWithFormattedTime._id ||
(msg.timestamp === messageWithFormattedTime.timestamp &&
msg.sender === messageWithFormattedTime.sender &&
msg.text === messageWithFormattedTime.text)
);

if (messageExists) return prevMessages;


return [...prevMessages, messageWithFormattedTime];
});
};

const handleMessageDelivered = ({ messageId }) => {


setMessages(prevMessages =>
prevMessages.map(msg =>
msg._id === messageId ? { ...msg, status: 'delivered' } : msg
)
);
};

const handleMessagesRead = ({ messageIds, readAt }) => {


setMessages(prevMessages =>
prevMessages.map(msg =>
messageIds.includes(msg._id)
? { ...msg, status: 'read', readAt }
: msg
)
);
};

const handleMessagesReadConfirmation = ({ messageIds, readAt }) => {


setMessages(prevMessages =>
prevMessages.map(msg =>
messageIds.includes(msg._id)
? { ...msg, status: 'read', readAt }
: msg
)
);
};

socketRef.current.on('connect', () => {
setSocketConnected(true);
setConnectionError(null);
socketRef.current.emit('register', senderId);
});

socketRef.current.on('receiveMessage', handleReceiveMessage);
socketRef.current.on('messageDelivered', handleMessageDelivered);
socketRef.current.on('messagesRead', handleMessagesRead);
socketRef.current.on('messagesReadConfirmation',
handleMessagesReadConfirmation);

socketRef.current.on('connect_error', (error) => {


setSocketConnected(false);
setConnectionError(`Connection error: ${error.message}`);
});

fetch(`${BACKEND_API}api/messages/${bidId}/${senderId}/${receiverId}`)
.then(response => response.json())
.then(data => {
const formattedData = data.map(msg => ({
...msg,
timestamp: msg.timestamp ? new Date(msg.timestamp).toISOString() : new
Date().toISOString(),
readAt: msg.readAt ? new Date(msg.readAt).toISOString() : null,
editedAt: msg.editedAt ? new Date(msg.editedAt).toISOString() : null,
isEdited: msg.isEdited || false, // Ensure isEdited is preserved
}));
setMessages(formattedData);
setTimeout(scrollToBottom, 100);
})
.catch(err => console.error('Error fetching messages:', err));

socketRef.current.on('messageUpdated', (updatedMessage) => {


setMessages(prevMessages =>
prevMessages.map(msg =>
msg._id === updatedMessage._id
? {
...msg,
text: updatedMessage.text,
isEdited: true,
editedAt: updatedMessage.editedAt
}
: msg
)
);
});

return () => {
if (socketRef.current) {
socketRef.current.off('receiveMessage', handleReceiveMessage);
socketRef.current.off('messageDelivered', handleMessageDelivered);
socketRef.current.off('messagesRead', handleMessagesRead);
socketRef.current.off('messagesReadConfirmation',
handleMessagesReadConfirmation);
socketRef.current.disconnect();
socketRef.current?.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: 'primary.main' }} />
<Typography variant="caption" color="text.secondary">
{formatMessageDate(readAt)}
</Typography>
</Box>
);
}

if (status === 'delivered') {


return <DoneAllIcon sx={{ fontSize: 16, color: 'text.secondary' }} />;
}

return <CheckIcon sx={{ fontSize: 16, color: 'text.secondary' }} />;


};

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: message.sender === senderId ? 'primary.main' : 'grey.100',
color: message.sender === senderId ? 'primary.contrastText' :
'text.primary',
wordBreak: 'break-word',
overflowWrap: 'break-word',
whiteSpace: 'pre-wrap',
maxWidth: '100%',
}}
>
<Typography variant="body2" component="div">
{highlightText(message.text, searchText)}
</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mt: 0.5,
opacity: 0.7,
}}
>
<Typography
variant="caption"
sx={{
color: message.sender === senderId ? 'primary.contrastText' :
'text.secondary',
}}
>
{formatMessageDate(message.timestamp)}
</Typography>
{message.isEdited && (
<Tooltip
title={`Edited ${message.editedAt ?
formatMessageDate(message.editedAt) : ''}`}
placement="top"
>
<Typography
variant="caption"
sx={{
color: message.sender === senderId ? 'primary.contrastText' :
'text.secondary',
fontStyle: 'italic',
}}
>
(edited)
</Typography>
</Tooltip>
)}
</Box>
</Paper>
{message.sender === 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 (message.trim() && socketRef.current?.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: JSON.stringify(newMessage),
})
.then(response => response.json())
.then(savedMessage => {
socketRef.current.emit('sendMessage', savedMessage);
setMessages(prevMessages => {
const messageExists = prevMessages.some(msg =>
msg.timestamp === savedMessage.timestamp &&
msg.sender === savedMessage.sender &&
msg.text === savedMessage.text
);

if (messageExists) return prevMessages;


return [...prevMessages, savedMessage];
});
setMessage('');
})
.catch(err => {
console.error('Error sending message:', err);
});
}
};

return (
<Card sx={{ width: '100%', maxWidth: 750, mx: 'auto' }}>
<CardHeader
sx={{
bgcolor: 'primary.main',
color: 'primary.contrastText',
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(e.target.value)}
inputRef={textFieldRef}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
{searchResults.length > 0 && (
<>
<Typography variant="caption" sx={{ mr: 1 }}>
{currentResultIndex + 1} of {searchResults.length}
</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 }}>
{messages.map((msg, index) => (
<Box
key={msg._id || index}
className="message-container"
sx={{
display: 'flex',
justifyContent: msg.sender === 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)}
/>
{msg.sender === senderId && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 0.5 }}>
<MessageStatus status={msg.status} readAt={msg.readAt} />
</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(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}}
multiline
sx={{
'& .MuiInputBase-root': {
wordBreak: 'break-word',
overflowWrap: 'break-word'
}
}}
/>
<Button
variant="contained"
disabled={!socketConnected || !message.trim()}
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={messageContainerRef.current}
>
<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(e.target.value)}
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;

You might also like