simplified message model
This commit is contained in:
@@ -21,7 +21,6 @@ import Owning from './pages/Owning';
|
||||
import Profile from './pages/Profile';
|
||||
import PublicProfile from './pages/PublicProfile';
|
||||
import Messages from './pages/Messages';
|
||||
import MessageDetail from './pages/MessageDetail';
|
||||
import ForumPosts from './pages/ForumPosts';
|
||||
import ForumPostDetail from './pages/ForumPostDetail';
|
||||
import CreateForumPost from './pages/CreateForumPost';
|
||||
@@ -150,14 +149,6 @@ const AppContent: React.FC = () => {
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/messages/:id"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<MessageDetail />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/forum" element={<ForumPosts />} />
|
||||
<Route path="/forum/:id" element={<ForumPostDetail />} />
|
||||
<Route
|
||||
|
||||
@@ -322,7 +322,6 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
// Build FormData for message (with or without image)
|
||||
const formData = new FormData();
|
||||
formData.append('receiverId', recipient.id);
|
||||
formData.append('subject', `Message from ${currentUser?.firstName}`);
|
||||
formData.append('content', messageContent || ' '); // Send space if only image
|
||||
if (imageToSend) {
|
||||
formData.append('image', imageToSend);
|
||||
|
||||
@@ -10,7 +10,6 @@ interface MessageModalProps {
|
||||
}
|
||||
|
||||
const MessageModal: React.FC<MessageModalProps> = ({ show, onClose, recipient, onSuccess }) => {
|
||||
const [subject, setSubject] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -23,12 +22,10 @@ const MessageModal: React.FC<MessageModalProps> = ({ show, onClose, recipient, o
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('receiverId', recipient.id);
|
||||
formData.append('subject', subject);
|
||||
formData.append('content', content);
|
||||
|
||||
await messageAPI.sendMessage(formData);
|
||||
|
||||
setSubject('');
|
||||
setContent('');
|
||||
onClose();
|
||||
if (onSuccess) {
|
||||
@@ -58,19 +55,6 @@ const MessageModal: React.FC<MessageModalProps> = ({ show, onClose, recipient, o
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="subject" className="form-label">Subject</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="subject"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
required
|
||||
disabled={sending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="content" className="form-label">Message</label>
|
||||
@@ -95,10 +79,10 @@ const MessageModal: React.FC<MessageModalProps> = ({ show, onClose, recipient, o
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={sending || !subject || !content}
|
||||
disabled={sending || !content}
|
||||
>
|
||||
{sending ? (
|
||||
<>
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Message } from '../types';
|
||||
import { messageAPI } from '../services/api';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useSocket } from '../contexts/SocketContext';
|
||||
|
||||
const MessageDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const { isConnected, onNewMessage } = useSocket();
|
||||
const [message, setMessage] = useState<Message | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [replyContent, setReplyContent] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessage();
|
||||
}, [id]);
|
||||
|
||||
// Listen for new replies in real-time
|
||||
useEffect(() => {
|
||||
if (!isConnected || !message) return;
|
||||
|
||||
const cleanup = onNewMessage((newMessage: Message) => {
|
||||
// Check if this is a reply to the current thread
|
||||
if (newMessage.parentMessageId === message.id) {
|
||||
setMessage((prevMessage) => {
|
||||
if (!prevMessage) return prevMessage;
|
||||
|
||||
// Check if reply already exists (avoid duplicates)
|
||||
const replies = prevMessage.replies || [];
|
||||
if (replies.some(r => r.id === newMessage.id)) {
|
||||
return prevMessage;
|
||||
}
|
||||
|
||||
// Add new reply to the thread
|
||||
return {
|
||||
...prevMessage,
|
||||
replies: [...replies, newMessage]
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [isConnected, message?.id, onNewMessage]);
|
||||
|
||||
const fetchMessage = async () => {
|
||||
try {
|
||||
const response = await messageAPI.getMessage(id!);
|
||||
setMessage(response.data);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to fetch message');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReply = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!message) return;
|
||||
|
||||
setSending(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const recipientId = message.senderId === user?.id ? message.receiverId : message.senderId;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('receiverId', recipientId);
|
||||
formData.append('subject', `Re: ${message.subject}`);
|
||||
formData.append('content', replyContent);
|
||||
formData.append('parentMessageId', message.id);
|
||||
|
||||
const response = await messageAPI.sendMessage(formData);
|
||||
|
||||
setReplyContent('');
|
||||
|
||||
// Note: Socket will automatically add the reply to the thread
|
||||
// But we add it manually for immediate feedback if socket is disconnected
|
||||
if (!isConnected) {
|
||||
setMessage((prevMessage) => {
|
||||
if (!prevMessage) return prevMessage;
|
||||
const replies = prevMessage.replies || [];
|
||||
return {
|
||||
...prevMessage,
|
||||
replies: [...replies, response.data]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
alert('Reply sent successfully!');
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.error || 'Failed to send reply');
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
Message not found
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isReceiver = message.receiverId === user?.id;
|
||||
const otherUser = isReceiver ? message.sender : message.receiver;
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-8">
|
||||
<button
|
||||
className="btn btn-link text-decoration-none mb-3"
|
||||
onClick={() => navigate('/messages')}
|
||||
>
|
||||
<i className="bi bi-arrow-left"></i> Back to Messages
|
||||
</button>
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="d-flex align-items-center">
|
||||
{otherUser?.profileImage ? (
|
||||
<img
|
||||
src={otherUser.profileImage}
|
||||
alt={`${otherUser.firstName} ${otherUser.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>
|
||||
<h5 className="mb-0">{message.subject}</h5>
|
||||
<small className="text-muted">
|
||||
{isReceiver ? 'From' : 'To'}: {otherUser?.firstName} {otherUser?.lastName} • {formatDateTime(message.createdAt)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p style={{ whiteSpace: 'pre-wrap' }}>{message.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message.replies && message.replies.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h6>Replies</h6>
|
||||
{message.replies.map((reply) => (
|
||||
<div key={reply.id} className="card mb-2">
|
||||
<div className="card-body">
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
{reply.sender?.profileImage ? (
|
||||
<img
|
||||
src={reply.sender.profileImage}
|
||||
alt={`${reply.sender.firstName} ${reply.sender.lastName}`}
|
||||
className="rounded-circle me-2"
|
||||
style={{ width: '30px', height: '30px', objectFit: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-2"
|
||||
style={{ width: '30px', height: '30px' }}
|
||||
>
|
||||
<i className="bi bi-person-fill text-white" style={{ fontSize: '0.8rem' }}></i>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<strong>{reply.sender?.firstName} {reply.sender?.lastName}</strong>
|
||||
<small className="text-muted ms-2">{formatDateTime(reply.createdAt)}</small>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-0" style={{ whiteSpace: 'pre-wrap' }}>{reply.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card mt-4">
|
||||
<div className="card-body">
|
||||
<h6>Send Reply</h6>
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleReply}>
|
||||
<div className="mb-3">
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows={4}
|
||||
value={replyContent}
|
||||
onChange={(e) => setReplyContent(e.target.value)}
|
||||
placeholder="Type your reply..."
|
||||
required
|
||||
disabled={sending}
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={sending || !replyContent.trim()}
|
||||
>
|
||||
{sending ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bi bi-send-fill me-2"></i>Send Reply
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageDetail;
|
||||
@@ -39,14 +39,11 @@ export interface Message {
|
||||
id: string;
|
||||
senderId: string;
|
||||
receiverId: string;
|
||||
subject: string;
|
||||
content: string;
|
||||
isRead: boolean;
|
||||
parentMessageId?: string;
|
||||
imagePath?: string;
|
||||
sender?: User;
|
||||
receiver?: User;
|
||||
replies?: Message[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user