diff --git a/backend/services/locationService.js b/backend/services/locationService.js index 1c5dc72..63f37cc 100644 --- a/backend/services/locationService.js +++ b/backend/services/locationService.js @@ -1,5 +1,5 @@ -const { sequelize } = require('../models'); -const { QueryTypes } = require('sequelize'); +const { sequelize } = require("../models"); +const { QueryTypes } = require("sequelize"); class LocationService { /** @@ -13,19 +13,13 @@ class LocationService { */ async findUsersInRadius(latitude, longitude, radiusMiles = 10) { if (!latitude || !longitude) { - throw new Error('Latitude and longitude are required'); + throw new Error("Latitude and longitude are required"); } if (radiusMiles <= 0 || radiusMiles > 100) { - throw new Error('Radius must be between 1 and 100 miles'); + throw new Error("Radius must be between 1 and 100 miles"); } - console.log('Finding users in radius:', { - centerLatitude: latitude, - centerLongitude: longitude, - radiusMiles - }); - try { // Haversine formula: // distance = 3959 * acos(cos(radians(lat1)) * cos(radians(lat2)) @@ -62,29 +56,22 @@ class LocationService { replacements: { lat: parseFloat(latitude), lng: parseFloat(longitude), - radiusMiles: parseFloat(radiusMiles) + radiusMiles: parseFloat(radiusMiles), }, - type: QueryTypes.SELECT + type: QueryTypes.SELECT, }); - console.log('Users found in radius:', users.map(u => ({ - id: u.id, - userLat: u.latitude, - userLng: u.longitude, - distance: parseFloat(u.distance).toFixed(2) - }))); - - return users.map(user => ({ + return users.map((user) => ({ id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, latitude: parseFloat(user.latitude), longitude: parseFloat(user.longitude), - distance: parseFloat(user.distance).toFixed(2) // Round to 2 decimal places + distance: parseFloat(user.distance).toFixed(2), // Round to 2 decimal places })); } catch (error) { - console.error('Error finding users in radius:', error); + console.error("Error finding users in radius:", error); throw new Error(`Failed to find users in radius: ${error.message}`); } } @@ -105,8 +92,10 @@ class LocationService { const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * - Math.sin(dLon / 2) * Math.sin(dLon / 2); + Math.cos(this.toRadians(lat1)) * + Math.cos(this.toRadians(lat2)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const distance = R * c; diff --git a/frontend/src/components/ChatWindow.tsx b/frontend/src/components/ChatWindow.tsx index ffde089..50e676e 100644 --- a/frontend/src/components/ChatWindow.tsx +++ b/frontend/src/components/ChatWindow.tsx @@ -1,9 +1,15 @@ -import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from 'react'; -import { messageAPI, getMessageImageUrl } from '../services/api'; -import { User, Message } from '../types'; -import { useAuth } from '../contexts/AuthContext'; -import { useSocket } from '../contexts/SocketContext'; -import TypingIndicator from './TypingIndicator'; +import React, { + useState, + useEffect, + useLayoutEffect, + useRef, + useCallback, +} from "react"; +import { messageAPI, getMessageImageUrl } from "../services/api"; +import { User, Message } from "../types"; +import { useAuth } from "../contexts/AuthContext"; +import { useSocket } from "../contexts/SocketContext"; +import TypingIndicator from "./TypingIndicator"; interface ChatWindowProps { show: boolean; @@ -12,15 +18,30 @@ interface ChatWindowProps { onMessagesRead?: (partnerId: string, count: number) => void; } -const ChatWindow: React.FC = ({ show, onClose, recipient, onMessagesRead }) => { +const ChatWindow: React.FC = ({ + show, + onClose, + recipient, + onMessagesRead, +}) => { const { user: currentUser } = useAuth(); - const { isConnected, joinConversation, leaveConversation, onNewMessage, onUserTyping, emitTypingStart, emitTypingStop } = useSocket(); + const { + isConnected, + joinConversation, + leaveConversation, + onNewMessage, + onUserTyping, + emitTypingStart, + emitTypingStop, + } = useSocket(); const [messages, setMessages] = useState([]); - const [newMessage, setNewMessage] = useState(''); + const [newMessage, setNewMessage] = useState(""); const [sending, setSending] = useState(false); const [loading, setLoading] = useState(true); const [isRecipientTyping, setIsRecipientTyping] = useState(false); - const [initialUnreadMessageIds, setInitialUnreadMessageIds] = useState>(new Set()); + const [initialUnreadMessageIds, setInitialUnreadMessageIds] = useState< + Set + >(new Set()); const [isAtBottom, setIsAtBottom] = useState(true); const [hasScrolledToUnread, setHasScrolledToUnread] = useState(false); const [selectedImage, setSelectedImage] = useState(null); @@ -52,59 +73,57 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes }, [show, recipient.id, isConnected]); // Create a stable callback for handling new messages - const handleNewMessage = useCallback(async (message: Message) => { - console.log('[ChatWindow] Received new_message event:', message); - - // Only add messages that are part of this conversation - if ( - (message.senderId === recipient.id && message.receiverId === currentUser?.id) || - (message.senderId === currentUser?.id && message.receiverId === recipient.id) - ) { - console.log('[ChatWindow] Message is for this conversation, adding to chat'); - setMessages((prevMessages) => { - // Check if message already exists (avoid duplicates) - if (prevMessages.some(m => m.id === message.id)) { - console.log('[ChatWindow] Message already exists, skipping'); - return prevMessages; - } - 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); + const handleNewMessage = useCallback( + async (message: Message) => { + // Only add messages that are part of this conversation + if ( + (message.senderId === recipient.id && + message.receiverId === currentUser?.id) || + (message.senderId === currentUser?.id && + message.receiverId === recipient.id) + ) { + setMessages((prevMessages) => { + // Check if message already exists (avoid duplicates) + if (prevMessages.some((m) => m.id === message.id)) { + return prevMessages; + } + return [...prevMessages, message]; + }); + + // Mark incoming messages from recipient as read + if ( + message.senderId === recipient.id && + message.receiverId === currentUser?.id && + !message.isRead + ) { + 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 + ); } - } 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, onMessagesRead]); + }, + [recipient.id, currentUser?.id, onMessagesRead] + ); // Listen for new messages in real-time useEffect(() => { - console.log('[ChatWindow] Message listener useEffect running', { isConnected, show, recipientId: recipient.id }); - if (!isConnected || !show) { - console.log('[ChatWindow] Skipping listener setup:', { isConnected, show }); return; } - console.log('[ChatWindow] Setting up message listener for recipient:', recipient.id); - const cleanup = onNewMessage(handleNewMessage); return () => { - console.log('[ChatWindow] Cleaning up message listener for recipient:', recipient.id); cleanup(); }; }, [isConnected, show, onNewMessage, handleNewMessage]); @@ -143,21 +162,20 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes if (!loading && !hasScrolledToUnread && messages.length > 0) { if (initialUnreadMessageIds.size > 0) { // Find the oldest unread message - const oldestUnread = messages.find(m => initialUnreadMessageIds.has(m.id)); + 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' + 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); @@ -176,26 +194,33 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes // Fetch all messages between current user and recipient const [sentRes, receivedRes] = await Promise.all([ messageAPI.getSentMessages(), - messageAPI.getMessages() + messageAPI.getMessages(), ]); - const sentToRecipient = sentRes.data.filter((msg: Message) => msg.receiverId === recipient.id); - const receivedFromRecipient = receivedRes.data.filter((msg: Message) => msg.senderId === recipient.id); + const sentToRecipient = sentRes.data.filter( + (msg: Message) => msg.receiverId === recipient.id + ); + const receivedFromRecipient = receivedRes.data.filter( + (msg: Message) => msg.senderId === recipient.id + ); // Combine and sort by date const allMessages = [...sentToRecipient, ...receivedFromRecipient].sort( - (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + (a, b) => + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); setMessages(allMessages); // Mark all unread messages from recipient as read - const unreadMessages = receivedFromRecipient.filter((msg: Message) => !msg.isRead); + 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))); + setInitialUnreadMessageIds( + new Set(unreadMessages.map((m: Message) => m.id)) + ); // Mark each message as read const markReadPromises = unreadMessages.map((message: Message) => @@ -212,28 +237,32 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes } } } catch (error) { - console.error('Failed to fetch messages:', error); + console.error("Failed to fetch messages:", error); } finally { setLoading(false); } }; const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + 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 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 { scrollTop, scrollHeight, clientHeight } = + messagesContainerRef.current; const isBottom = scrollHeight - scrollTop - clientHeight < 50; // 50px threshold setIsAtBottom(isBottom); }; @@ -265,14 +294,14 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes const file = e.target.files?.[0]; if (file) { // Validate file type - if (!file.type.startsWith('image/')) { - alert('Please select an image file'); + 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'); + alert("Image size must be less than 5MB"); return; } @@ -291,7 +320,7 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes setSelectedImage(null); setImagePreview(null); if (fileInputRef.current) { - fileInputRef.current.value = ''; + fileInputRef.current.value = ""; } }; @@ -311,20 +340,20 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes setSending(true); const messageContent = newMessage; const imageToSend = selectedImage; - setNewMessage(''); // Clear input immediately for better UX + setNewMessage(""); // Clear input immediately for better UX setSelectedImage(null); setImagePreview(null); if (fileInputRef.current) { - fileInputRef.current.value = ''; + fileInputRef.current.value = ""; } try { // Build FormData for message (with or without image) const formData = new FormData(); - formData.append('receiverId', recipient.id); - formData.append('content', messageContent || ' '); // Send space if only image + formData.append("receiverId", recipient.id); + formData.append("content", messageContent || " "); // Send space if only image if (imageToSend) { - formData.append('image', imageToSend); + formData.append("image", imageToSend); } const response = await messageAPI.sendMessage(formData); @@ -333,13 +362,13 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes // Socket will handle updating the receiver's chat setMessages((prevMessages) => { // Avoid duplicates - if (prevMessages.some(m => m.id === response.data.id)) { + if (prevMessages.some((m) => m.id === response.data.id)) { return prevMessages; } return [...prevMessages, response.data]; }); } catch (error) { - console.error('Failed to send message:', error); + console.error("Failed to send message:", error); setNewMessage(messageContent); // Restore message on error setSelectedImage(imageToSend); if (imageToSend) { @@ -362,47 +391,47 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes const formatTime = (dateString: string) => { const date = new Date(dateString); - return date.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: '2-digit', - hour12: true + return date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, }); }; const formatDate = (dateString: string) => { const date = new Date(dateString); const today = new Date(); - + if (date.toDateString() === today.toDateString()) { - return 'Today'; + return "Today"; } - + const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); if (date.toDateString() === yesterday.toDateString()) { - return 'Yesterday'; + return "Yesterday"; } - - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: date.getFullYear() !== today.getFullYear() ? 'numeric' : undefined + + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: date.getFullYear() !== today.getFullYear() ? "numeric" : undefined, }); }; if (!show) return null; return ( -
{/* Header */} @@ -413,23 +442,25 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes src={recipient.profileImage} alt={`${recipient.firstName} ${recipient.lastName}`} className="rounded-circle me-2" - style={{ width: '35px', height: '35px', objectFit: 'cover' }} + style={{ width: "35px", height: "35px", objectFit: "cover" }} /> ) : ( -
)}
-
{recipient.firstName} {recipient.lastName}
+
+ {recipient.firstName} {recipient.lastName} +
- @@ -440,8 +471,8 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes onScroll={handleScroll} className="p-3 overflow-auto flex-grow-1" style={{ - backgroundColor: '#f8f9fa', - minHeight: 0 + backgroundColor: "#f8f9fa", + minHeight: 0, }} > {loading ? ( @@ -452,15 +483,22 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes ) : messages.length === 0 ? (
- -

Start a conversation with {recipient.firstName}

+ +

+ Start a conversation with {recipient.firstName} +

) : ( <> {messages.map((message, index) => { const isCurrentUser = message.senderId === currentUser?.id; - const showDate = index === 0 || - formatDate(message.createdAt) !== formatDate(messages[index - 1].createdAt); + const showDate = + index === 0 || + formatDate(message.createdAt) !== + formatDate(messages[index - 1].createdAt); return (
@@ -471,16 +509,20 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes
)} -
+
{message.imagePath && ( @@ -489,24 +531,29 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes src={getMessageImageUrl(message.imagePath)} alt="Shared image" style={{ - width: '100%', - borderRadius: '8px', - cursor: 'pointer', - maxHeight: '300px', - objectFit: 'cover' + width: "100%", + borderRadius: "8px", + cursor: "pointer", + maxHeight: "300px", + objectFit: "cover", }} - onClick={() => window.open(getMessageImageUrl(message.imagePath!), '_blank')} + onClick={() => + window.open( + getMessageImageUrl(message.imagePath!), + "_blank" + ) + } />
)} {message.content.trim() && ( -

+

{message.content}

)} {formatTime(message.createdAt)} @@ -536,17 +583,22 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes src={imagePreview} alt="Preview" style={{ - maxWidth: '150px', - maxHeight: '150px', - borderRadius: '8px', - objectFit: 'cover' + maxWidth: "150px", + maxHeight: "150px", + borderRadius: "8px", + objectFit: "cover", }} /> @@ -558,7 +610,7 @@ const ChatWindow: React.FC = ({ show, onClose, recipient, onMes type="file" accept="image/*" onChange={handleImageSelect} - style={{ display: 'none' }} + style={{ display: "none" }} />