conversations, unread count, autoscrolling to recent messages, cursor in text bar
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from 'react';
|
||||
import { messageAPI } from '../services/api';
|
||||
import { User, Message } from '../types';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
@@ -9,9 +9,10 @@ interface ChatWindowProps {
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
recipient: User;
|
||||
onMessagesRead?: (partnerId: string, count: number) => void;
|
||||
}
|
||||
|
||||
const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) => {
|
||||
const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMessagesRead }) => {
|
||||
const { user: currentUser } = useAuth();
|
||||
const { isConnected, joinConversation, leaveConversation, onNewMessage, onUserTyping, emitTypingStart, emitTypingStop } = useSocket();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
@@ -19,11 +20,18 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) =>
|
||||
const [sending, setSending] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isRecipientTyping, setIsRecipientTyping] = useState(false);
|
||||
const [initialUnreadMessageIds, setInitialUnreadMessageIds] = useState<Set<string>>(new Set());
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const [hasScrolledToUnread, setHasScrolledToUnread] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const messageRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setHasScrolledToUnread(false); // Reset flag when opening chat
|
||||
fetchMessages();
|
||||
|
||||
// Join conversation room when chat opens
|
||||
@@ -41,7 +49,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) =>
|
||||
}, [show, recipient.id, isConnected]);
|
||||
|
||||
// Create a stable callback for handling new messages
|
||||
const handleNewMessage = useCallback((message: Message) => {
|
||||
const handleNewMessage = useCallback(async (message: Message) => {
|
||||
console.log('[ChatWindow] Received new_message event:', message);
|
||||
|
||||
// Only add messages that are part of this conversation
|
||||
@@ -59,10 +67,25 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) =>
|
||||
console.log('[ChatWindow] Adding new message to chat');
|
||||
return [...prevMessages, message];
|
||||
});
|
||||
|
||||
// Mark incoming messages from recipient as read
|
||||
if (message.senderId === recipient.id && message.receiverId === currentUser?.id && !message.isRead) {
|
||||
console.log('[ChatWindow] Marking new incoming message as read');
|
||||
try {
|
||||
await messageAPI.markAsRead(message.id);
|
||||
|
||||
// Notify parent component that message was marked read
|
||||
if (onMessagesRead) {
|
||||
onMessagesRead(recipient.id, 1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to mark message ${message.id} as read:`, error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('[ChatWindow] Message not for this conversation, ignoring');
|
||||
}
|
||||
}, [recipient.id, currentUser?.id]);
|
||||
}, [recipient.id, currentUser?.id, onMessagesRead]);
|
||||
|
||||
// Listen for new messages in real-time
|
||||
useEffect(() => {
|
||||
@@ -112,9 +135,38 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) =>
|
||||
};
|
||||
}, [isConnected, show, recipient.id, onUserTyping]);
|
||||
|
||||
// Initial scroll to unread messages (runs synchronously after DOM updates, refs are ready)
|
||||
useLayoutEffect(() => {
|
||||
if (!loading && !hasScrolledToUnread && messages.length > 0) {
|
||||
if (initialUnreadMessageIds.size > 0) {
|
||||
// Find the oldest unread message
|
||||
const oldestUnread = messages.find(m => initialUnreadMessageIds.has(m.id));
|
||||
|
||||
if (oldestUnread && messageRefs.current.has(oldestUnread.id)) {
|
||||
console.log(`[ChatWindow] Scrolling to oldest unread message: ${oldestUnread.id}`);
|
||||
messageRefs.current.get(oldestUnread.id)?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
} else {
|
||||
console.log('[ChatWindow] Unread message ref not found, scrolling to bottom');
|
||||
scrollToBottom();
|
||||
}
|
||||
} else {
|
||||
// No unread messages, scroll to bottom
|
||||
console.log('[ChatWindow] No unread messages, scrolling to bottom');
|
||||
scrollToBottom();
|
||||
}
|
||||
setHasScrolledToUnread(true);
|
||||
}
|
||||
}, [loading, hasScrolledToUnread, messages, initialUnreadMessageIds]);
|
||||
|
||||
// Auto-scroll for new messages (only if user is at bottom)
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages, isRecipientTyping]);
|
||||
if (isAtBottom && hasScrolledToUnread) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [messages, isRecipientTyping, isAtBottom, hasScrolledToUnread]);
|
||||
|
||||
const fetchMessages = async () => {
|
||||
try {
|
||||
@@ -133,6 +185,29 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) =>
|
||||
);
|
||||
|
||||
setMessages(allMessages);
|
||||
|
||||
// Mark all unread messages from recipient as read
|
||||
const unreadMessages = receivedFromRecipient.filter((msg: Message) => !msg.isRead);
|
||||
if (unreadMessages.length > 0) {
|
||||
console.log(`[ChatWindow] Marking ${unreadMessages.length} messages as read`);
|
||||
|
||||
// Save unread message IDs for scrolling purposes
|
||||
setInitialUnreadMessageIds(new Set(unreadMessages.map((m: Message) => m.id)));
|
||||
|
||||
// Mark each message as read
|
||||
const markReadPromises = unreadMessages.map((message: Message) =>
|
||||
messageAPI.markAsRead(message.id).catch((err: any) => {
|
||||
console.error(`Failed to mark message ${message.id} as read:`, err);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(markReadPromises);
|
||||
|
||||
// Notify parent component that messages were marked read
|
||||
if (onMessagesRead) {
|
||||
onMessagesRead(recipient.id, unreadMessages.length);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch messages:', error);
|
||||
} finally {
|
||||
@@ -144,6 +219,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) =>
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const setMessageRef = useCallback((id: string) => (el: HTMLDivElement | null) => {
|
||||
if (el) {
|
||||
messageRefs.current.set(id, el);
|
||||
} else {
|
||||
messageRefs.current.delete(id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!messagesContainerRef.current) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = messagesContainerRef.current;
|
||||
const isBottom = scrollHeight - scrollTop - clientHeight < 50; // 50px threshold
|
||||
setIsAtBottom(isBottom);
|
||||
};
|
||||
|
||||
// Handle typing indicators with debouncing
|
||||
const handleTyping = useCallback(() => {
|
||||
if (!isConnected) return;
|
||||
@@ -201,6 +292,12 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) =>
|
||||
setNewMessage(messageContent); // Restore message on error
|
||||
} finally {
|
||||
setSending(false);
|
||||
// Defer focus until after all DOM updates and scroll operations complete
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -279,9 +376,11 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) =>
|
||||
</div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<div
|
||||
className="p-3 overflow-auto flex-grow-1"
|
||||
style={{
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
onScroll={handleScroll}
|
||||
className="p-3 overflow-auto flex-grow-1"
|
||||
style={{
|
||||
backgroundColor: '#f8f9fa',
|
||||
minHeight: 0
|
||||
}}
|
||||
@@ -305,7 +404,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) =>
|
||||
formatDate(message.createdAt) !== formatDate(messages[index - 1].createdAt);
|
||||
|
||||
return (
|
||||
<div key={message.id}>
|
||||
<div key={message.id} ref={setMessageRef(message.id)}>
|
||||
{showDate && (
|
||||
<div className="text-center my-3">
|
||||
<small className="text-muted bg-white px-2 py-1 rounded">
|
||||
@@ -355,6 +454,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient }) =>
|
||||
<form onSubmit={handleSend} className="border-top p-3 flex-shrink-0">
|
||||
<div className="input-group">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Type a message..."
|
||||
|
||||
@@ -1,52 +1,136 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Message, User } from '../types';
|
||||
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 navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { isConnected, onNewMessage } = useSocket();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
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(() => {
|
||||
fetchMessages();
|
||||
fetchConversations();
|
||||
}, []);
|
||||
|
||||
// Listen for new messages in real-time
|
||||
// Listen for new messages and update conversations in real-time
|
||||
useEffect(() => {
|
||||
if (!isConnected) return;
|
||||
|
||||
const cleanup = onNewMessage((newMessage: Message) => {
|
||||
// Only add if this is a received message (user is the receiver)
|
||||
if (newMessage.receiverId === user?.id) {
|
||||
setMessages((prevMessages) => {
|
||||
// Check if message already exists (avoid duplicates)
|
||||
if (prevMessages.some(m => m.id === newMessage.id)) {
|
||||
return prevMessages;
|
||||
console.log('[Messages] Received new message:', newMessage);
|
||||
|
||||
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++;
|
||||
}
|
||||
// Add new message to the top of the inbox
|
||||
return [newMessage, ...prevMessages];
|
||||
});
|
||||
}
|
||||
|
||||
updated[existingIndex] = conv;
|
||||
|
||||
// Re-sort by most recent
|
||||
updated.sort((a, b) =>
|
||||
new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime()
|
||||
);
|
||||
|
||||
console.log('[Messages] Updated existing conversation');
|
||||
return updated;
|
||||
} else {
|
||||
// New conversation - add to top
|
||||
const partner = newMessage.senderId === user?.id
|
||||
? newMessage.receiver!
|
||||
: newMessage.sender!;
|
||||
|
||||
if (!partner) {
|
||||
console.warn('[Messages] Partner data missing from new message');
|
||||
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
|
||||
};
|
||||
|
||||
console.log('[Messages] Created new conversation');
|
||||
return [newConv, ...prevConversations];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [isConnected, user?.id, onNewMessage]);
|
||||
|
||||
const fetchMessages = async () => {
|
||||
// Listen for read receipts and update unread counts
|
||||
useEffect(() => {
|
||||
if (!isConnected) return;
|
||||
|
||||
const cleanup = onMessageRead((data: any) => {
|
||||
console.log('[Messages] Message read:', data);
|
||||
|
||||
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.getMessages();
|
||||
setMessages(response.data);
|
||||
const response = await messageAPI.getConversations();
|
||||
setConversations(response.data);
|
||||
console.log('[Messages] Fetched conversations:', response.data.length);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to fetch messages');
|
||||
console.error('[Messages] Failed to fetch conversations:', err);
|
||||
setError(err.response?.data?.error || 'Failed to fetch conversations');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -56,7 +140,7 @@ const Messages: React.FC = () => {
|
||||
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) {
|
||||
@@ -66,25 +150,29 @@ const Messages: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMessageClick = async (message: Message) => {
|
||||
// Mark as read if unread
|
||||
if (!message.isRead) {
|
||||
try {
|
||||
await messageAPI.markAsRead(message.id);
|
||||
// Update local state
|
||||
setMessages(messages.map(m =>
|
||||
m.id === message.id ? { ...m, isRead: true } : m
|
||||
));
|
||||
} catch (err) {
|
||||
console.error('Failed to mark message as read:', err);
|
||||
}
|
||||
}
|
||||
const handleConversationClick = (conversation: Conversation) => {
|
||||
setSelectedRecipient(conversation.partner);
|
||||
setShowChat(true);
|
||||
};
|
||||
|
||||
// Open chat with sender
|
||||
if (message.sender) {
|
||||
setSelectedRecipient(message.sender);
|
||||
setShowChat(true);
|
||||
}
|
||||
const handleMessagesRead = (partnerId: string, count: number) => {
|
||||
console.log(`[Messages] ${count} messages marked as read for partner ${partnerId}`);
|
||||
|
||||
// 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) {
|
||||
@@ -99,7 +187,6 @@ const Messages: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="row justify-content-center">
|
||||
@@ -112,61 +199,80 @@ const Messages: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.length === 0 ? (
|
||||
{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 messages in your inbox</p>
|
||||
<p className="text-muted mt-2">No conversations yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="list-group">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`list-group-item list-group-item-action ${!message.isRead ? 'border-start border-primary border-4' : ''}`}
|
||||
onClick={() => handleMessageClick(message)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: !message.isRead ? '#f0f7ff' : 'white'
|
||||
}}
|
||||
>
|
||||
<div className="d-flex w-100 justify-content-between">
|
||||
<div className="d-flex align-items-center">
|
||||
{message.sender?.profileImage ? (
|
||||
<img
|
||||
src={message.sender.profileImage}
|
||||
alt={`${message.sender.firstName} ${message.sender.lastName}`}
|
||||
className="rounded-circle me-3"
|
||||
style={{ width: '40px', height: '40px', objectFit: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-3"
|
||||
style={{ width: '40px', height: '40px' }}
|
||||
>
|
||||
<i className="bi bi-person-fill text-white"></i>
|
||||
{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>
|
||||
<div className="d-flex align-items-center">
|
||||
<h6 className={`mb-1 ${!message.isRead ? 'fw-bold' : ''}`}>
|
||||
{message.sender?.firstName} {message.sender?.lastName}
|
||||
</h6>
|
||||
{!message.isRead && (
|
||||
<span className="badge bg-primary ms-2">New</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={`mb-1 text-truncate ${!message.isRead ? 'fw-semibold' : ''}`} style={{ maxWidth: '400px' }}>
|
||||
{message.subject}
|
||||
</p>
|
||||
<small className="text-muted text-truncate d-block" style={{ maxWidth: '400px' }}>
|
||||
{message.content}
|
||||
</div>
|
||||
|
||||
{/* Timestamp */}
|
||||
<div className="text-end ms-3" style={{ minWidth: 'fit-content' }}>
|
||||
<small className="text-muted d-block">
|
||||
{formatDate(conversation.lastMessageAt)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<small className="text-muted">{formatDate(message.createdAt)}</small>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -175,15 +281,13 @@ const Messages: React.FC = () => {
|
||||
{selectedRecipient && (
|
||||
<ChatWindow
|
||||
show={showChat}
|
||||
onClose={() => {
|
||||
setShowChat(false);
|
||||
setSelectedRecipient(null);
|
||||
}}
|
||||
onClose={handleChatClose}
|
||||
recipient={selectedRecipient}
|
||||
onMessagesRead={handleMessagesRead}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Messages;
|
||||
export default Messages;
|
||||
|
||||
@@ -239,6 +239,7 @@ export const rentalAPI = {
|
||||
export const messageAPI = {
|
||||
getMessages: () => api.get("/messages"),
|
||||
getSentMessages: () => api.get("/messages/sent"),
|
||||
getConversations: () => api.get("/messages/conversations"),
|
||||
getMessage: (id: string) => api.get(`/messages/${id}`),
|
||||
sendMessage: (data: any) => api.post("/messages", data),
|
||||
markAsRead: (id: string) => api.put(`/messages/${id}/read`),
|
||||
|
||||
@@ -48,6 +48,20 @@ export interface Message {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
partnerId: string;
|
||||
partner: User;
|
||||
lastMessage: {
|
||||
id: string;
|
||||
content: string;
|
||||
senderId: string;
|
||||
createdAt: string;
|
||||
isRead: boolean;
|
||||
};
|
||||
unreadCount: number;
|
||||
lastMessageAt: string;
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user