Files
rentall-app/frontend/src/pages/Messages.tsx

315 lines
10 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { Conversation, Message, User } from "../types";
import { messageAPI } from "../services/api";
import { useAuth } from "../contexts/AuthContext";
import { useSocket } from "../contexts/SocketContext";
import ChatWindow from "../components/ChatWindow";
const Messages: React.FC = () => {
const { user } = useAuth();
const { isConnected, onNewMessage, onMessageRead } = useSocket();
const [conversations, setConversations] = useState<Conversation[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedRecipient, setSelectedRecipient] = useState<User | null>(null);
const [showChat, setShowChat] = useState(false);
useEffect(() => {
fetchConversations();
}, []);
// Listen for new messages and update conversations in real-time
useEffect(() => {
if (!isConnected) return;
const cleanup = onNewMessage((newMessage: Message) => {
setConversations((prevConversations) => {
// Determine conversation partner
const partnerId =
newMessage.senderId === user?.id
? newMessage.receiverId
: newMessage.senderId;
// Find existing conversation
const existingIndex = prevConversations.findIndex(
(c) => c.partnerId === partnerId
);
if (existingIndex !== -1) {
// Update existing conversation
const updated = [...prevConversations];
const conv = { ...updated[existingIndex] };
conv.lastMessage = {
id: newMessage.id,
content: newMessage.content,
senderId: newMessage.senderId,
createdAt: newMessage.createdAt,
isRead: newMessage.isRead,
};
conv.lastMessageAt = newMessage.createdAt;
// Increment unread count if user received the message
if (newMessage.receiverId === user?.id && !newMessage.isRead) {
conv.unreadCount++;
}
updated[existingIndex] = conv;
// Re-sort by most recent
updated.sort(
(a, b) =>
new Date(b.lastMessageAt).getTime() -
new Date(a.lastMessageAt).getTime()
);
return updated;
} else {
// New conversation - add to top
const partner =
newMessage.senderId === user?.id
? newMessage.receiver!
: newMessage.sender!;
if (!partner) {
return prevConversations;
}
const newConv: Conversation = {
partnerId,
partner,
lastMessage: {
id: newMessage.id,
content: newMessage.content,
senderId: newMessage.senderId,
createdAt: newMessage.createdAt,
isRead: newMessage.isRead,
},
unreadCount:
newMessage.receiverId === user?.id && !newMessage.isRead ? 1 : 0,
lastMessageAt: newMessage.createdAt,
};
return [newConv, ...prevConversations];
}
});
});
return cleanup;
}, [isConnected, user?.id, onNewMessage]);
// Listen for read receipts and update unread counts
useEffect(() => {
if (!isConnected) return;
const cleanup = onMessageRead((data: any) => {
setConversations((prevConversations) => {
return prevConversations.map((conv) => {
// If this is the conversation and the last message was marked as read
if (
conv.lastMessage.id === data.messageId &&
!conv.lastMessage.isRead
) {
return {
...conv,
lastMessage: { ...conv.lastMessage, isRead: true },
unreadCount: Math.max(0, conv.unreadCount - 1),
};
}
return conv;
});
});
});
return cleanup;
}, [isConnected, onMessageRead]);
const fetchConversations = async () => {
try {
const response = await messageAPI.getConversations();
setConversations(response.data);
} catch (err: any) {
setError(err.response?.data?.error || "Failed to fetch conversations");
} finally {
setLoading(false);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
if (diffInHours < 24) {
return date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
});
} else if (diffInHours < 48) {
return "Yesterday";
} else {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}
};
const handleConversationClick = (conversation: Conversation) => {
setSelectedRecipient(conversation.partner);
setShowChat(true);
};
const handleMessagesRead = (partnerId: string, count: number) => {
// Update the conversation's unread count
setConversations((prevConversations) =>
prevConversations.map((conv) =>
conv.partnerId === partnerId ? { ...conv, unreadCount: 0 } : conv
)
);
};
const handleChatClose = () => {
setShowChat(false);
setSelectedRecipient(null);
// Refresh conversations to get updated unread counts
fetchConversations();
};
if (loading) {
return (
<div className="container mt-5">
<div className="text-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
</div>
);
}
return (
<div className="container mt-4">
<div className="row justify-content-center">
<div className="col-md-8">
<h1 className="mb-4">Messages</h1>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
{conversations.length === 0 ? (
<div className="text-center py-5">
<i
className="bi bi-envelope"
style={{ fontSize: "3rem", color: "#ccc" }}
></i>
<p className="text-muted mt-2">No conversations yet</p>
</div>
) : (
<div className="list-group">
{conversations.map((conversation) => {
const isUnread = conversation.unreadCount > 0;
const isLastMessageFromPartner =
conversation.lastMessage.senderId === conversation.partnerId;
return (
<div
key={conversation.partnerId}
className={`list-group-item list-group-item-action ${
isUnread ? "border-start border-primary border-4" : ""
}`}
onClick={() => handleConversationClick(conversation)}
style={{
cursor: "pointer",
backgroundColor: isUnread ? "#f0f7ff" : "white",
}}
>
<div className="d-flex w-100 justify-content-between align-items-start">
<div className="d-flex align-items-center flex-grow-1">
{/* Profile Picture */}
{conversation.partner.profileImage ? (
<img
src={conversation.partner.profileImage}
alt={`${conversation.partner.firstName} ${conversation.partner.lastName}`}
className="rounded-circle me-3"
style={{
width: "50px",
height: "50px",
objectFit: "cover",
}}
/>
) : (
<div
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-3"
style={{ width: "50px", height: "50px" }}
>
<i className="bi bi-person-fill text-white"></i>
</div>
)}
<div className="flex-grow-1" style={{ minWidth: 0 }}>
{/* User Name and Unread Badge */}
<div className="d-flex align-items-center mb-1">
<h6 className={`mb-0 ${isUnread ? "fw-bold" : ""}`}>
{conversation.partner.firstName}{" "}
{conversation.partner.lastName}
</h6>
{isUnread && (
<span className="badge bg-primary ms-2">
{conversation.unreadCount}
</span>
)}
</div>
{/* Last Message Preview */}
<p
className={`mb-0 text-truncate ${
isUnread && isLastMessageFromPartner
? "fw-semibold"
: "text-muted"
}`}
style={{ maxWidth: "100%" }}
>
{conversation.lastMessage.senderId === user?.id && (
<span className="me-1">You: </span>
)}
{conversation.lastMessage.content}
</p>
</div>
</div>
{/* Timestamp */}
<div
className="text-end ms-3"
style={{ minWidth: "fit-content" }}
>
<small className="text-muted d-block">
{formatDate(conversation.lastMessageAt)}
</small>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
{selectedRecipient && (
<ChatWindow
show={showChat}
onClose={handleChatClose}
recipient={selectedRecipient}
onMessagesRead={handleMessagesRead}
/>
)}
</div>
);
};
export default Messages;