removed console logs from frontend and a logs from locationService

This commit is contained in:
jackiettran
2025-11-26 15:01:00 -05:00
parent 8b10103ae4
commit fab79e64ee
6 changed files with 344 additions and 275 deletions

View File

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

View File

@@ -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<ChatWindowProps> = ({ show, onClose, recipient, onMessagesRead }) => {
const ChatWindow: React.FC<ChatWindowProps> = ({
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<Message[]>([]);
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<Set<string>>(new Set());
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);
@@ -52,59 +73,57 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ 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<ChatWindowProps> = ({ 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<ChatWindowProps> = ({ 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<ChatWindowProps> = ({ 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<ChatWindowProps> = ({ 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<ChatWindowProps> = ({ 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<ChatWindowProps> = ({ 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<ChatWindowProps> = ({ 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,10 +391,10 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ 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,
});
};
@@ -374,19 +403,19 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
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,
});
};
@@ -396,13 +425,13 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
<div
className="position-fixed bottom-0 end-0 m-3 shadow-lg d-flex flex-column"
style={{
width: '350px',
height: '500px',
maxHeight: 'calc(100vh - 100px)',
width: "350px",
height: "500px",
maxHeight: "calc(100vh - 100px)",
zIndex: 1050,
borderRadius: '12px',
overflow: 'hidden',
backgroundColor: 'white'
borderRadius: "12px",
overflow: "hidden",
backgroundColor: "white",
}}
>
{/* Header */}
@@ -413,18 +442,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ 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" }}
/>
) : (
<div
className="rounded-circle bg-white bg-opacity-25 d-flex align-items-center justify-content-center me-2"
style={{ width: '35px', height: '35px' }}
style={{ width: "35px", height: "35px" }}
>
<i className="bi bi-person-fill text-white"></i>
</div>
)}
<div>
<h6 className="mb-0">{recipient.firstName} {recipient.lastName}</h6>
<h6 className="mb-0">
{recipient.firstName} {recipient.lastName}
</h6>
</div>
</div>
<button
@@ -440,8 +471,8 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ 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<ChatWindowProps> = ({ show, onClose, recipient, onMes
</div>
) : messages.length === 0 ? (
<div className="text-center py-5">
<i className="bi bi-chat-dots" style={{ fontSize: '3rem', color: '#dee2e6' }}></i>
<p className="text-muted mt-2">Start a conversation with {recipient.firstName}</p>
<i
className="bi bi-chat-dots"
style={{ fontSize: "3rem", color: "#dee2e6" }}
></i>
<p className="text-muted mt-2">
Start a conversation with {recipient.firstName}
</p>
</div>
) : (
<>
{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 (
<div key={message.id} ref={setMessageRef(message.id)}>
@@ -471,16 +509,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
</small>
</div>
)}
<div className={`d-flex mb-2 ${isCurrentUser ? 'justify-content-end' : ''}`}>
<div
className={`d-flex mb-2 ${
isCurrentUser ? "justify-content-end" : ""
}`}
>
<div
className={`px-3 py-2 rounded-3 ${
isCurrentUser
? 'bg-primary text-white'
: 'bg-white border'
? "bg-primary text-white"
: "bg-white border"
}`}
style={{
maxWidth: '75%',
wordBreak: 'break-word'
maxWidth: "75%",
wordBreak: "break-word",
}}
>
{message.imagePath && (
@@ -489,24 +531,29 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ 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"
)
}
/>
</div>
)}
{message.content.trim() && (
<p className="mb-1" style={{ fontSize: '0.95rem' }}>
<p className="mb-1" style={{ fontSize: "0.95rem" }}>
{message.content}
</p>
)}
<small
className={isCurrentUser ? 'opacity-75' : 'text-muted'}
style={{ fontSize: '0.75rem' }}
className={isCurrentUser ? "opacity-75" : "text-muted"}
style={{ fontSize: "0.75rem" }}
>
{formatTime(message.createdAt)}
</small>
@@ -536,17 +583,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ 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",
}}
/>
<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' }}
style={{
width: "24px",
height: "24px",
padding: "0",
fontSize: "0.7rem",
}}
>
<i className="bi bi-x"></i>
</button>
@@ -558,7 +610,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
type="file"
accept="image/*"
onChange={handleImageSelect}
style={{ display: 'none' }}
style={{ display: "none" }}
/>
<button
type="button"
@@ -584,7 +636,11 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
disabled={sending || (!newMessage.trim() && !selectedImage)}
>
{sending ? (
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span
className="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
) : (
<i className="bi bi-send-fill"></i>
)}

View File

@@ -1,6 +1,13 @@
import React, { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react';
import { Socket } from 'socket.io-client';
import socketService from '../services/socket';
import React, {
createContext,
useContext,
useEffect,
useState,
useCallback,
ReactNode,
} from "react";
import { Socket } from "socket.io-client";
import socketService from "../services/socket";
/**
* Socket Context Type
@@ -14,8 +21,20 @@ interface SocketContextType {
emitTypingStop: (receiverId: string) => void;
emitMarkMessageRead: (messageId: string, senderId: string) => void;
onNewMessage: (callback: (message: any) => void) => () => void;
onMessageRead: (callback: (data: { messageId: string; readAt: string; readBy: string }) => void) => () => void;
onUserTyping: (callback: (data: { userId: string; firstName: string; isTyping: boolean }) => void) => () => void;
onMessageRead: (
callback: (data: {
messageId: string;
readAt: string;
readBy: string;
}) => void
) => () => void;
onUserTyping: (
callback: (data: {
userId: string;
firstName: string;
isTyping: boolean;
}) => void
) => () => void;
}
/**
@@ -37,7 +56,7 @@ interface SocketProviderProps {
*/
export const SocketProvider: React.FC<SocketProviderProps> = ({
children,
isAuthenticated = false
isAuthenticated = false,
}) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
@@ -46,27 +65,20 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
* Initialize socket connection when user is authenticated
*/
useEffect(() => {
console.log('[SocketProvider] useEffect running', { isAuthenticated });
if (!isAuthenticated) {
console.log('[SocketProvider] Not authenticated, skipping socket setup');
return;
}
console.log('[SocketProvider] Initializing socket connection');
const newSocket = socketService.connect();
setSocket(newSocket);
// Listen for connection status changes
console.log('[SocketProvider] Setting up connection listener');
const removeListener = socketService.addConnectionListener((connected) => {
console.log('[SocketProvider] Connection status changed:', connected);
setIsConnected(connected);
});
// Cleanup on unmount
return () => {
console.log('[SocketProvider] Cleaning up connection listener');
removeListener();
};
}, [isAuthenticated]);
@@ -76,7 +88,6 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
*/
useEffect(() => {
if (!isAuthenticated && socket) {
console.log('[SocketProvider] User logged out, disconnecting socket');
socketService.disconnect();
setSocket(null);
setIsConnected(false);
@@ -114,9 +125,12 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
/**
* Emit mark message as read event
*/
const emitMarkMessageRead = useCallback((messageId: string, senderId: string) => {
socketService.emitMarkMessageRead(messageId, senderId);
}, []);
const emitMarkMessageRead = useCallback(
(messageId: string, senderId: string) => {
socketService.emitMarkMessageRead(messageId, senderId);
},
[]
);
/**
* Listen for new messages
@@ -128,16 +142,34 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
/**
* Listen for message read events
*/
const onMessageRead = useCallback((callback: (data: { messageId: string; readAt: string; readBy: string }) => void) => {
return socketService.onMessageRead(callback);
}, []);
const onMessageRead = useCallback(
(
callback: (data: {
messageId: string;
readAt: string;
readBy: string;
}) => void
) => {
return socketService.onMessageRead(callback);
},
[]
);
/**
* Listen for typing indicators
*/
const onUserTyping = useCallback((callback: (data: { userId: string; firstName: string; isTyping: boolean }) => void) => {
return socketService.onUserTyping(callback);
}, []);
const onUserTyping = useCallback(
(
callback: (data: {
userId: string;
firstName: string;
isTyping: boolean;
}) => void
) => {
return socketService.onUserTyping(callback);
},
[]
);
const value: SocketContextType = {
socket,
@@ -153,9 +185,7 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
};
return (
<SocketContext.Provider value={value}>
{children}
</SocketContext.Provider>
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
);
};
@@ -167,7 +197,7 @@ export const useSocket = (): SocketContextType => {
const context = useContext(SocketContext);
if (context === undefined) {
throw new Error('useSocket must be used within a SocketProvider');
throw new Error("useSocket must be used within a SocketProvider");
}
return context;

View File

@@ -208,19 +208,11 @@ const ItemDetail: React.FC = () => {
const dayTimes = item.weeklyTimes[dayName];
availableAfter = dayTimes.availableAfter;
availableBefore = dayTimes.availableBefore;
console.log("Using day-specific times:", {
availableAfter,
availableBefore,
});
}
// Otherwise use global times
else if (item.availableAfter && item.availableBefore) {
availableAfter = item.availableAfter;
availableBefore = item.availableBefore;
} else {
console.log(
"No time constraints found, using default 24-hour availability"
);
}
}
@@ -249,7 +241,6 @@ const ItemDetail: React.FC = () => {
// If no options are available, return at least one option to prevent empty dropdown
if (options.length === 0) {
console.log("No valid time options found, showing Not Available");
options.push({ value: "00:00", label: "Not Available" });
}

View File

@@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react';
import { Conversation, Message, User } from '../types';
import { messageAPI } from '../services/api';
import { useAuth } from '../contexts/AuthContext';
import { useSocket } from '../contexts/SocketContext';
import ChatWindow from '../components/ChatWindow';
import React, { useState, useEffect } from "react";
import { Conversation, Message, User } from "../types";
import { messageAPI } from "../services/api";
import { useAuth } from "../contexts/AuthContext";
import { useSocket } from "../contexts/SocketContext";
import ChatWindow from "../components/ChatWindow";
const Messages: React.FC = () => {
const { user } = useAuth();
@@ -23,17 +23,16 @@ const Messages: React.FC = () => {
if (!isConnected) return;
const cleanup = onNewMessage((newMessage: Message) => {
console.log('[Messages] Received new message:', newMessage);
setConversations((prevConversations) => {
// Determine conversation partner
const partnerId = newMessage.senderId === user?.id
? newMessage.receiverId
: newMessage.senderId;
const partnerId =
newMessage.senderId === user?.id
? newMessage.receiverId
: newMessage.senderId;
// Find existing conversation
const existingIndex = prevConversations.findIndex(
c => c.partnerId === partnerId
(c) => c.partnerId === partnerId
);
if (existingIndex !== -1) {
@@ -46,7 +45,7 @@ const Messages: React.FC = () => {
content: newMessage.content,
senderId: newMessage.senderId,
createdAt: newMessage.createdAt,
isRead: newMessage.isRead
isRead: newMessage.isRead,
};
conv.lastMessageAt = newMessage.createdAt;
@@ -58,20 +57,21 @@ const Messages: React.FC = () => {
updated[existingIndex] = conv;
// Re-sort by most recent
updated.sort((a, b) =>
new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime()
updated.sort(
(a, b) =>
new Date(b.lastMessageAt).getTime() -
new Date(a.lastMessageAt).getTime()
);
console.log('[Messages] Updated existing conversation');
return updated;
} else {
// New conversation - add to top
const partner = newMessage.senderId === user?.id
? newMessage.receiver!
: newMessage.sender!;
const partner =
newMessage.senderId === user?.id
? newMessage.receiver!
: newMessage.sender!;
if (!partner) {
console.warn('[Messages] Partner data missing from new message');
return prevConversations;
}
@@ -83,13 +83,13 @@ const Messages: React.FC = () => {
content: newMessage.content,
senderId: newMessage.senderId,
createdAt: newMessage.createdAt,
isRead: newMessage.isRead
isRead: newMessage.isRead,
},
unreadCount: newMessage.receiverId === user?.id && !newMessage.isRead ? 1 : 0,
lastMessageAt: newMessage.createdAt
unreadCount:
newMessage.receiverId === user?.id && !newMessage.isRead ? 1 : 0,
lastMessageAt: newMessage.createdAt,
};
console.log('[Messages] Created new conversation');
return [newConv, ...prevConversations];
}
});
@@ -103,16 +103,17 @@ const Messages: React.FC = () => {
if (!isConnected) return;
const cleanup = onMessageRead((data: any) => {
console.log('[Messages] Message read:', data);
setConversations((prevConversations) => {
return prevConversations.map(conv => {
return prevConversations.map((conv) => {
// If this is the conversation and the last message was marked as read
if (conv.lastMessage.id === data.messageId && !conv.lastMessage.isRead) {
if (
conv.lastMessage.id === data.messageId &&
!conv.lastMessage.isRead
) {
return {
...conv,
lastMessage: { ...conv.lastMessage, isRead: true },
unreadCount: Math.max(0, conv.unreadCount - 1)
unreadCount: Math.max(0, conv.unreadCount - 1),
};
}
return conv;
@@ -127,10 +128,8 @@ const Messages: React.FC = () => {
try {
const response = await messageAPI.getConversations();
setConversations(response.data);
console.log('[Messages] Fetched conversations:', response.data.length);
} catch (err: any) {
console.error('[Messages] Failed to fetch conversations:', err);
setError(err.response?.data?.error || 'Failed to fetch conversations');
setError(err.response?.data?.error || "Failed to fetch conversations");
} finally {
setLoading(false);
}
@@ -142,11 +141,17 @@ const Messages: React.FC = () => {
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
if (diffInHours < 24) {
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
return date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
});
} else if (diffInHours < 48) {
return 'Yesterday';
return "Yesterday";
} else {
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}
};
@@ -156,14 +161,10 @@ const Messages: React.FC = () => {
};
const handleMessagesRead = (partnerId: string, count: number) => {
console.log(`[Messages] ${count} messages marked as read for partner ${partnerId}`);
// Update the conversation's unread count
setConversations(prevConversations =>
prevConversations.map(conv =>
conv.partnerId === partnerId
? { ...conv, unreadCount: 0 }
: conv
setConversations((prevConversations) =>
prevConversations.map((conv) =>
conv.partnerId === partnerId ? { ...conv, unreadCount: 0 } : conv
)
);
};
@@ -201,23 +202,29 @@ const Messages: React.FC = () => {
{conversations.length === 0 ? (
<div className="text-center py-5">
<i className="bi bi-envelope" style={{ fontSize: '3rem', color: '#ccc' }}></i>
<i
className="bi bi-envelope"
style={{ fontSize: "3rem", color: "#ccc" }}
></i>
<p className="text-muted mt-2">No conversations yet</p>
</div>
) : (
<div className="list-group">
{conversations.map((conversation) => {
const isUnread = conversation.unreadCount > 0;
const isLastMessageFromPartner = conversation.lastMessage.senderId === conversation.partnerId;
const isLastMessageFromPartner =
conversation.lastMessage.senderId === conversation.partnerId;
return (
<div
key={conversation.partnerId}
className={`list-group-item list-group-item-action ${isUnread ? 'border-start border-primary border-4' : ''}`}
className={`list-group-item list-group-item-action ${
isUnread ? "border-start border-primary border-4" : ""
}`}
onClick={() => handleConversationClick(conversation)}
style={{
cursor: 'pointer',
backgroundColor: isUnread ? '#f0f7ff' : 'white'
cursor: "pointer",
backgroundColor: isUnread ? "#f0f7ff" : "white",
}}
>
<div className="d-flex w-100 justify-content-between align-items-start">
@@ -228,12 +235,16 @@ const Messages: React.FC = () => {
src={conversation.partner.profileImage}
alt={`${conversation.partner.firstName} ${conversation.partner.lastName}`}
className="rounded-circle me-3"
style={{ width: '50px', height: '50px', objectFit: 'cover' }}
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' }}
style={{ width: "50px", height: "50px" }}
>
<i className="bi bi-person-fill text-white"></i>
</div>
@@ -242,18 +253,25 @@ const Messages: React.FC = () => {
<div className="flex-grow-1" style={{ minWidth: 0 }}>
{/* User Name and Unread Badge */}
<div className="d-flex align-items-center mb-1">
<h6 className={`mb-0 ${isUnread ? 'fw-bold' : ''}`}>
{conversation.partner.firstName} {conversation.partner.lastName}
<h6 className={`mb-0 ${isUnread ? "fw-bold" : ""}`}>
{conversation.partner.firstName}{" "}
{conversation.partner.lastName}
</h6>
{isUnread && (
<span className="badge bg-primary ms-2">{conversation.unreadCount}</span>
<span className="badge bg-primary ms-2">
{conversation.unreadCount}
</span>
)}
</div>
{/* Last Message Preview */}
<p
className={`mb-0 text-truncate ${isUnread && isLastMessageFromPartner ? 'fw-semibold' : 'text-muted'}`}
style={{ maxWidth: '100%' }}
className={`mb-0 text-truncate ${
isUnread && isLastMessageFromPartner
? "fw-semibold"
: "text-muted"
}`}
style={{ maxWidth: "100%" }}
>
{conversation.lastMessage.senderId === user?.id && (
<span className="me-1">You: </span>
@@ -264,7 +282,10 @@ const Messages: React.FC = () => {
</div>
{/* Timestamp */}
<div className="text-end ms-3" style={{ minWidth: 'fit-content' }}>
<div
className="text-end ms-3"
style={{ minWidth: "fit-content" }}
>
<small className="text-muted d-block">
{formatDate(conversation.lastMessageAt)}
</small>

View File

@@ -52,12 +52,9 @@ class SocketService {
*/
connect(): Socket {
if (this.socket?.connected) {
console.log("[Socket] Already connected");
return this.socket;
}
console.log("[Socket] Connecting to server...");
this.socket = io(this.getSocketUrl(), {
withCredentials: true, // Send cookies for authentication
transports: ["websocket", "polling"], // Try WebSocket first, fallback to polling
@@ -71,23 +68,15 @@ class SocketService {
// Connection event handlers
this.socket.on("connect", () => {
console.log("[Socket] Connected successfully", {
socketId: this.socket?.id,
});
this.reconnectAttempts = 0;
this.notifyConnectionListeners(true);
});
this.socket.on("disconnect", (reason) => {
console.log("[Socket] Disconnected", { reason });
this.notifyConnectionListeners(false);
});
this.socket.on("connect_error", (error) => {
console.error("[Socket] Connection error", {
error: error.message,
attempt: this.reconnectAttempts + 1,
});
this.reconnectAttempts++;
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
@@ -96,10 +85,6 @@ class SocketService {
}
});
this.socket.on("error", (error) => {
console.error("[Socket] Socket error", error);
});
return this.socket;
}
@@ -108,7 +93,6 @@ class SocketService {
*/
disconnect(): void {
if (this.socket) {
console.log("[Socket] Disconnecting...");
this.socket.disconnect();
this.socket = null;
this.notifyConnectionListeners(false);
@@ -138,7 +122,6 @@ class SocketService {
return;
}
console.log("[Socket] Joining conversation", { otherUserId });
this.socket.emit("join_conversation", { otherUserId });
}
@@ -150,7 +133,6 @@ class SocketService {
return;
}
console.log("[Socket] Leaving conversation", { otherUserId });
this.socket.emit("leave_conversation", { otherUserId });
}