- Added real-time messaging with Socket.io - Created comprehensive database schema (8 tables, functions, triggers) - Implemented messaging service with full CRUD operations - Built Socket.io service for real-time communication - Created React messaging components (Chat, ConversationList, MessageList, MessageInput) - Added end-to-end encryption utilities (RSA + AES-256-GCM) - Implemented 16 RESTful API endpoints - Added typing indicators, presence tracking, reactions - Created modern, responsive UI with animations - Updated server with Socket.io integration - Fixed auth middleware imports - Added comprehensive documentation Features: - Direct and group conversations - Real-time message delivery - Message editing and deletion - Emoji reactions - Typing indicators - Online/offline presence - Read receipts - User search - File attachment support (endpoint ready) - Client-side encryption utilities Dependencies: - socket.io ^4.7.5 - socket.io-client ^4.7.5
110 lines
3.6 KiB
JavaScript
110 lines
3.6 KiB
JavaScript
/**
|
|
* ConversationList Component
|
|
* Displays list of conversations in sidebar
|
|
*/
|
|
|
|
import React from 'react';
|
|
import './ConversationList.css';
|
|
|
|
export default function ConversationList({ conversations, activeConversation, onSelectConversation }) {
|
|
|
|
const formatTime = (timestamp) => {
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
|
|
if (diffMins < 1) return 'Just now';
|
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
return date.toLocaleDateString();
|
|
};
|
|
|
|
const getConversationTitle = (conv) => {
|
|
if (conv.title) return conv.title;
|
|
|
|
// For direct conversations, show other participant's domain
|
|
if (conv.otherParticipants && conv.otherParticipants.length > 0) {
|
|
return conv.otherParticipants[0].verified_domain || conv.otherParticipants[0].username;
|
|
}
|
|
|
|
return 'Unknown';
|
|
};
|
|
|
|
const getConversationAvatar = (conv) => {
|
|
if (conv.avatarUrl) return conv.avatarUrl;
|
|
|
|
// For direct conversations, show other participant's avatar
|
|
if (conv.otherParticipants && conv.otherParticipants.length > 0) {
|
|
return conv.otherParticipants[0].avatar_url;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
return (
|
|
<div className="conversation-list">
|
|
<div className="conversation-list-header">
|
|
<h2>Messages</h2>
|
|
<button className="btn-new-conversation" title="New Conversation">
|
|
+
|
|
</button>
|
|
</div>
|
|
|
|
<div className="conversation-list-items">
|
|
{conversations.length === 0 ? (
|
|
<div className="no-conversations">
|
|
<p>No conversations yet</p>
|
|
<p className="hint">Start a new conversation to get started</p>
|
|
</div>
|
|
) : (
|
|
conversations.map(conv => (
|
|
<div
|
|
key={conv.id}
|
|
className={`conversation-item ${activeConversation?.id === conv.id ? 'active' : ''}`}
|
|
onClick={() => onSelectConversation(conv)}
|
|
>
|
|
<div className="conversation-avatar-container">
|
|
{getConversationAvatar(conv) ? (
|
|
<img
|
|
src={getConversationAvatar(conv)}
|
|
alt="Avatar"
|
|
className="conversation-avatar-img"
|
|
/>
|
|
) : (
|
|
<div className="conversation-avatar-placeholder">
|
|
{getConversationTitle(conv)[0]?.toUpperCase()}
|
|
</div>
|
|
)}
|
|
{conv.otherParticipants?.[0]?.status === 'online' && (
|
|
<span className="online-indicator"></span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="conversation-details">
|
|
<div className="conversation-header-row">
|
|
<h3 className="conversation-title">{getConversationTitle(conv)}</h3>
|
|
<span className="conversation-time">
|
|
{formatTime(conv.updatedAt)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="conversation-last-message">
|
|
<p className="last-message-text">
|
|
{conv.lastMessage?.content || 'No messages yet'}
|
|
</p>
|
|
{conv.unreadCount > 0 && (
|
|
<span className="unread-badge">{conv.unreadCount}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|