can add image to message
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from 'react';
|
||||
import { messageAPI } from '../services/api';
|
||||
import { messageAPI, getMessageImageUrl } from '../services/api';
|
||||
import { User, Message } from '../types';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useSocket } from '../contexts/SocketContext';
|
||||
@@ -23,11 +23,14 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
const [initialUnreadMessageIds, setInitialUnreadMessageIds] = useState<Set<string>>(new Set());
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const [hasScrolledToUnread, setHasScrolledToUnread] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<File | null>(null);
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||
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);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
@@ -258,9 +261,47 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
handleTyping();
|
||||
};
|
||||
|
||||
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('Please select an image file');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert('Image size must be less than 5MB');
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedImage(file);
|
||||
|
||||
// Create preview URL
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveImage = () => {
|
||||
setSelectedImage(null);
|
||||
setImagePreview(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageButtonClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleSend = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newMessage.trim()) return;
|
||||
if (!newMessage.trim() && !selectedImage) return;
|
||||
|
||||
// Stop typing indicator
|
||||
if (isConnected) {
|
||||
@@ -269,14 +310,25 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
|
||||
setSending(true);
|
||||
const messageContent = newMessage;
|
||||
const imageToSend = selectedImage;
|
||||
setNewMessage(''); // Clear input immediately for better UX
|
||||
setSelectedImage(null);
|
||||
setImagePreview(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await messageAPI.sendMessage({
|
||||
receiverId: recipient.id,
|
||||
subject: `Message from ${currentUser?.firstName}`,
|
||||
content: messageContent
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
|
||||
const response = await messageAPI.sendMessage(formData);
|
||||
|
||||
// Add message to sender's chat immediately for instant feedback
|
||||
// Socket will handle updating the receiver's chat
|
||||
@@ -290,6 +342,14 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
setNewMessage(messageContent); // Restore message on error
|
||||
setSelectedImage(imageToSend);
|
||||
if (imageToSend) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(imageToSend);
|
||||
}
|
||||
} finally {
|
||||
setSending(false);
|
||||
// Defer focus until after all DOM updates and scroll operations complete
|
||||
@@ -413,21 +473,39 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
</div>
|
||||
)}
|
||||
<div className={`d-flex mb-2 ${isCurrentUser ? 'justify-content-end' : ''}`}>
|
||||
<div
|
||||
<div
|
||||
className={`px-3 py-2 rounded-3 ${
|
||||
isCurrentUser
|
||||
? 'bg-primary text-white'
|
||||
isCurrentUser
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-white border'
|
||||
}`}
|
||||
style={{
|
||||
style={{
|
||||
maxWidth: '75%',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
<p className="mb-1" style={{ fontSize: '0.95rem' }}>
|
||||
{message.content}
|
||||
</p>
|
||||
<small
|
||||
{message.imagePath && (
|
||||
<div className="mb-2">
|
||||
<img
|
||||
src={getMessageImageUrl(message.imagePath)}
|
||||
alt="Shared image"
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
maxHeight: '300px',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
onClick={() => window.open(getMessageImageUrl(message.imagePath!), '_blank')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{message.content.trim() && (
|
||||
<p className="mb-1" style={{ fontSize: '0.95rem' }}>
|
||||
{message.content}
|
||||
</p>
|
||||
)}
|
||||
<small
|
||||
className={isCurrentUser ? 'opacity-75' : 'text-muted'}
|
||||
style={{ fontSize: '0.75rem' }}
|
||||
>
|
||||
@@ -452,7 +530,46 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
|
||||
{/* Input Area */}
|
||||
<form onSubmit={handleSend} className="border-top p-3 flex-shrink-0">
|
||||
{/* Image Preview */}
|
||||
{imagePreview && (
|
||||
<div className="mb-2 position-relative d-inline-block">
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '150px',
|
||||
maxHeight: '150px',
|
||||
borderRadius: '8px',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1 rounded-circle"
|
||||
onClick={handleRemoveImage}
|
||||
style={{ width: '24px', height: '24px', padding: '0', fontSize: '0.7rem' }}
|
||||
>
|
||||
<i className="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="input-group">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageSelect}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={handleImageButtonClick}
|
||||
disabled={sending}
|
||||
title="Attach image"
|
||||
>
|
||||
<i className="bi bi-image"></i>
|
||||
</button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
@@ -462,10 +579,10 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
||||
onChange={handleInputChange}
|
||||
disabled={sending}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={sending || !newMessage.trim()}
|
||||
disabled={sending || (!newMessage.trim() && !selectedImage)}
|
||||
>
|
||||
{sending ? (
|
||||
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
|
||||
@@ -21,11 +21,12 @@ const MessageModal: React.FC<MessageModalProps> = ({ show, onClose, recipient, o
|
||||
setSending(true);
|
||||
|
||||
try {
|
||||
await messageAPI.sendMessage({
|
||||
receiverId: recipient.id,
|
||||
subject,
|
||||
content
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append('receiverId', recipient.id);
|
||||
formData.append('subject', subject);
|
||||
formData.append('content', content);
|
||||
|
||||
await messageAPI.sendMessage(formData);
|
||||
|
||||
setSubject('');
|
||||
setContent('');
|
||||
|
||||
@@ -68,12 +68,14 @@ const MessageDetail: React.FC = () => {
|
||||
|
||||
try {
|
||||
const recipientId = message.senderId === user?.id ? message.receiverId : message.senderId;
|
||||
const response = await messageAPI.sendMessage({
|
||||
receiverId: recipientId,
|
||||
subject: `Re: ${message.subject}`,
|
||||
content: replyContent,
|
||||
parentMessageId: message.id
|
||||
});
|
||||
|
||||
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('');
|
||||
|
||||
|
||||
@@ -164,9 +164,12 @@ export const authAPI = {
|
||||
getStatus: () => api.get("/auth/status"),
|
||||
verifyEmail: (token: string) => api.post("/auth/verify-email", { token }),
|
||||
resendVerification: () => api.post("/auth/resend-verification"),
|
||||
forgotPassword: (email: string) => api.post("/auth/forgot-password", { email }),
|
||||
verifyResetToken: (token: string) => api.post("/auth/verify-reset-token", { token }),
|
||||
resetPassword: (token: string, newPassword: string) => api.post("/auth/reset-password", { token, newPassword }),
|
||||
forgotPassword: (email: string) =>
|
||||
api.post("/auth/forgot-password", { email }),
|
||||
verifyResetToken: (token: string) =>
|
||||
api.post("/auth/verify-reset-token", { token }),
|
||||
resetPassword: (token: string, newPassword: string) =>
|
||||
api.post("/auth/reset-password", { token, newPassword }),
|
||||
};
|
||||
|
||||
export const userAPI = {
|
||||
@@ -241,7 +244,12 @@ export const messageAPI = {
|
||||
getSentMessages: () => api.get("/messages/sent"),
|
||||
getConversations: () => api.get("/messages/conversations"),
|
||||
getMessage: (id: string) => api.get(`/messages/${id}`),
|
||||
sendMessage: (data: any) => api.post("/messages", data),
|
||||
sendMessage: (formData: FormData) =>
|
||||
api.post("/messages", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}),
|
||||
markAsRead: (id: string) => api.put(`/messages/${id}/read`),
|
||||
getUnreadCount: () => api.get("/messages/unread/count"),
|
||||
};
|
||||
@@ -304,4 +312,8 @@ export const feedbackAPI = {
|
||||
api.post("/feedback", data),
|
||||
};
|
||||
|
||||
// Helper to construct message image URLs
|
||||
export const getMessageImageUrl = (imagePath: string) =>
|
||||
`${API_BASE_URL}/messages/images/${imagePath}`;
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface Message {
|
||||
content: string;
|
||||
isRead: boolean;
|
||||
parentMessageId?: string;
|
||||
imagePath?: string;
|
||||
sender?: User;
|
||||
receiver?: User;
|
||||
replies?: Message[];
|
||||
|
||||
Reference in New Issue
Block a user