simplified message model

This commit is contained in:
jackiettran
2025-11-25 17:22:57 -05:00
parent 2983f67ce8
commit 31d94b1b3f
12 changed files with 74 additions and 473 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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 ? (
<>

View File

@@ -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;

View File

@@ -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;
}