removed console logs from frontend and a logs from locationService
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
const { sequelize } = require('../models');
|
const { sequelize } = require("../models");
|
||||||
const { QueryTypes } = require('sequelize');
|
const { QueryTypes } = require("sequelize");
|
||||||
|
|
||||||
class LocationService {
|
class LocationService {
|
||||||
/**
|
/**
|
||||||
@@ -13,19 +13,13 @@ class LocationService {
|
|||||||
*/
|
*/
|
||||||
async findUsersInRadius(latitude, longitude, radiusMiles = 10) {
|
async findUsersInRadius(latitude, longitude, radiusMiles = 10) {
|
||||||
if (!latitude || !longitude) {
|
if (!latitude || !longitude) {
|
||||||
throw new Error('Latitude and longitude are required');
|
throw new Error("Latitude and longitude are required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (radiusMiles <= 0 || radiusMiles > 100) {
|
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 {
|
try {
|
||||||
// Haversine formula:
|
// Haversine formula:
|
||||||
// distance = 3959 * acos(cos(radians(lat1)) * cos(radians(lat2))
|
// distance = 3959 * acos(cos(radians(lat1)) * cos(radians(lat2))
|
||||||
@@ -62,29 +56,22 @@ class LocationService {
|
|||||||
replacements: {
|
replacements: {
|
||||||
lat: parseFloat(latitude),
|
lat: parseFloat(latitude),
|
||||||
lng: parseFloat(longitude),
|
lng: parseFloat(longitude),
|
||||||
radiusMiles: parseFloat(radiusMiles)
|
radiusMiles: parseFloat(radiusMiles),
|
||||||
},
|
},
|
||||||
type: QueryTypes.SELECT
|
type: QueryTypes.SELECT,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Users found in radius:', users.map(u => ({
|
return users.map((user) => ({
|
||||||
id: u.id,
|
|
||||||
userLat: u.latitude,
|
|
||||||
userLng: u.longitude,
|
|
||||||
distance: parseFloat(u.distance).toFixed(2)
|
|
||||||
})));
|
|
||||||
|
|
||||||
return users.map(user => ({
|
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
latitude: parseFloat(user.latitude),
|
latitude: parseFloat(user.latitude),
|
||||||
longitude: parseFloat(user.longitude),
|
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) {
|
} 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}`);
|
throw new Error(`Failed to find users in radius: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,8 +92,10 @@ class LocationService {
|
|||||||
|
|
||||||
const a =
|
const a =
|
||||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
|
Math.cos(this.toRadians(lat1)) *
|
||||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
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 c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
const distance = R * c;
|
const distance = R * c;
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import React, { useState, useEffect, useLayoutEffect, useRef, useCallback } from 'react';
|
import React, {
|
||||||
import { messageAPI, getMessageImageUrl } from '../services/api';
|
useState,
|
||||||
import { User, Message } from '../types';
|
useEffect,
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
useLayoutEffect,
|
||||||
import { useSocket } from '../contexts/SocketContext';
|
useRef,
|
||||||
import TypingIndicator from './TypingIndicator';
|
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 {
|
interface ChatWindowProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -12,15 +18,30 @@ interface ChatWindowProps {
|
|||||||
onMessagesRead?: (partnerId: string, count: number) => void;
|
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 { 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 [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState("");
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isRecipientTyping, setIsRecipientTyping] = useState(false);
|
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 [isAtBottom, setIsAtBottom] = useState(true);
|
||||||
const [hasScrolledToUnread, setHasScrolledToUnread] = useState(false);
|
const [hasScrolledToUnread, setHasScrolledToUnread] = useState(false);
|
||||||
const [selectedImage, setSelectedImage] = useState<File | null>(null);
|
const [selectedImage, setSelectedImage] = useState<File | null>(null);
|
||||||
@@ -52,59 +73,57 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
}, [show, recipient.id, isConnected]);
|
}, [show, recipient.id, isConnected]);
|
||||||
|
|
||||||
// Create a stable callback for handling new messages
|
// Create a stable callback for handling new messages
|
||||||
const handleNewMessage = useCallback(async (message: Message) => {
|
const handleNewMessage = useCallback(
|
||||||
console.log('[ChatWindow] Received new_message event:', message);
|
async (message: Message) => {
|
||||||
|
// Only add messages that are part of this conversation
|
||||||
// Only add messages that are part of this conversation
|
if (
|
||||||
if (
|
(message.senderId === recipient.id &&
|
||||||
(message.senderId === recipient.id && message.receiverId === currentUser?.id) ||
|
message.receiverId === currentUser?.id) ||
|
||||||
(message.senderId === currentUser?.id && message.receiverId === recipient.id)
|
(message.senderId === currentUser?.id &&
|
||||||
) {
|
message.receiverId === recipient.id)
|
||||||
console.log('[ChatWindow] Message is for this conversation, adding to chat');
|
) {
|
||||||
setMessages((prevMessages) => {
|
setMessages((prevMessages) => {
|
||||||
// Check if message already exists (avoid duplicates)
|
// Check if message already exists (avoid duplicates)
|
||||||
if (prevMessages.some(m => m.id === message.id)) {
|
if (prevMessages.some((m) => m.id === message.id)) {
|
||||||
console.log('[ChatWindow] Message already exists, skipping');
|
return prevMessages;
|
||||||
return prevMessages;
|
}
|
||||||
}
|
return [...prevMessages, message];
|
||||||
console.log('[ChatWindow] Adding new message to chat');
|
});
|
||||||
return [...prevMessages, message];
|
|
||||||
});
|
// Mark incoming messages from recipient as read
|
||||||
|
if (
|
||||||
// Mark incoming messages from recipient as read
|
message.senderId === recipient.id &&
|
||||||
if (message.senderId === recipient.id && message.receiverId === currentUser?.id && !message.isRead) {
|
message.receiverId === currentUser?.id &&
|
||||||
console.log('[ChatWindow] Marking new incoming message as read');
|
!message.isRead
|
||||||
try {
|
) {
|
||||||
await messageAPI.markAsRead(message.id);
|
try {
|
||||||
|
await messageAPI.markAsRead(message.id);
|
||||||
// Notify parent component that message was marked read
|
|
||||||
if (onMessagesRead) {
|
// Notify parent component that message was marked read
|
||||||
onMessagesRead(recipient.id, 1);
|
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
|
// Listen for new messages in real-time
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[ChatWindow] Message listener useEffect running', { isConnected, show, recipientId: recipient.id });
|
|
||||||
|
|
||||||
if (!isConnected || !show) {
|
if (!isConnected || !show) {
|
||||||
console.log('[ChatWindow] Skipping listener setup:', { isConnected, show });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[ChatWindow] Setting up message listener for recipient:', recipient.id);
|
|
||||||
|
|
||||||
const cleanup = onNewMessage(handleNewMessage);
|
const cleanup = onNewMessage(handleNewMessage);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log('[ChatWindow] Cleaning up message listener for recipient:', recipient.id);
|
|
||||||
cleanup();
|
cleanup();
|
||||||
};
|
};
|
||||||
}, [isConnected, show, onNewMessage, handleNewMessage]);
|
}, [isConnected, show, onNewMessage, handleNewMessage]);
|
||||||
@@ -143,21 +162,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
if (!loading && !hasScrolledToUnread && messages.length > 0) {
|
if (!loading && !hasScrolledToUnread && messages.length > 0) {
|
||||||
if (initialUnreadMessageIds.size > 0) {
|
if (initialUnreadMessageIds.size > 0) {
|
||||||
// Find the oldest unread message
|
// 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)) {
|
if (oldestUnread && messageRefs.current.has(oldestUnread.id)) {
|
||||||
console.log(`[ChatWindow] Scrolling to oldest unread message: ${oldestUnread.id}`);
|
|
||||||
messageRefs.current.get(oldestUnread.id)?.scrollIntoView({
|
messageRefs.current.get(oldestUnread.id)?.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: "smooth",
|
||||||
block: 'start'
|
block: "start",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('[ChatWindow] Unread message ref not found, scrolling to bottom');
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No unread messages, scroll to bottom
|
// No unread messages, scroll to bottom
|
||||||
console.log('[ChatWindow] No unread messages, scrolling to bottom');
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
setHasScrolledToUnread(true);
|
setHasScrolledToUnread(true);
|
||||||
@@ -176,26 +194,33 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
// Fetch all messages between current user and recipient
|
// Fetch all messages between current user and recipient
|
||||||
const [sentRes, receivedRes] = await Promise.all([
|
const [sentRes, receivedRes] = await Promise.all([
|
||||||
messageAPI.getSentMessages(),
|
messageAPI.getSentMessages(),
|
||||||
messageAPI.getMessages()
|
messageAPI.getMessages(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const sentToRecipient = sentRes.data.filter((msg: Message) => msg.receiverId === recipient.id);
|
const sentToRecipient = sentRes.data.filter(
|
||||||
const receivedFromRecipient = receivedRes.data.filter((msg: Message) => msg.senderId === recipient.id);
|
(msg: Message) => msg.receiverId === recipient.id
|
||||||
|
);
|
||||||
|
const receivedFromRecipient = receivedRes.data.filter(
|
||||||
|
(msg: Message) => msg.senderId === recipient.id
|
||||||
|
);
|
||||||
|
|
||||||
// Combine and sort by date
|
// Combine and sort by date
|
||||||
const allMessages = [...sentToRecipient, ...receivedFromRecipient].sort(
|
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);
|
setMessages(allMessages);
|
||||||
|
|
||||||
// Mark all unread messages from recipient as read
|
// 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) {
|
if (unreadMessages.length > 0) {
|
||||||
console.log(`[ChatWindow] Marking ${unreadMessages.length} messages as read`);
|
|
||||||
|
|
||||||
// Save unread message IDs for scrolling purposes
|
// 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
|
// Mark each message as read
|
||||||
const markReadPromises = unreadMessages.map((message: Message) =>
|
const markReadPromises = unreadMessages.map((message: Message) =>
|
||||||
@@ -212,28 +237,32 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch messages:', error);
|
console.error("Failed to fetch messages:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const setMessageRef = useCallback((id: string) => (el: HTMLDivElement | null) => {
|
const setMessageRef = useCallback(
|
||||||
if (el) {
|
(id: string) => (el: HTMLDivElement | null) => {
|
||||||
messageRefs.current.set(id, el);
|
if (el) {
|
||||||
} else {
|
messageRefs.current.set(id, el);
|
||||||
messageRefs.current.delete(id);
|
} else {
|
||||||
}
|
messageRefs.current.delete(id);
|
||||||
}, []);
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!messagesContainerRef.current) return;
|
if (!messagesContainerRef.current) return;
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = messagesContainerRef.current;
|
const { scrollTop, scrollHeight, clientHeight } =
|
||||||
|
messagesContainerRef.current;
|
||||||
const isBottom = scrollHeight - scrollTop - clientHeight < 50; // 50px threshold
|
const isBottom = scrollHeight - scrollTop - clientHeight < 50; // 50px threshold
|
||||||
setIsAtBottom(isBottom);
|
setIsAtBottom(isBottom);
|
||||||
};
|
};
|
||||||
@@ -265,14 +294,14 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
// Validate file type
|
// Validate file type
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith("image/")) {
|
||||||
alert('Please select an image file');
|
alert("Please select an image file");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate file size (5MB)
|
// Validate file size (5MB)
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
alert('Image size must be less than 5MB');
|
alert("Image size must be less than 5MB");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,7 +320,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
setSelectedImage(null);
|
setSelectedImage(null);
|
||||||
setImagePreview(null);
|
setImagePreview(null);
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = '';
|
fileInputRef.current.value = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -311,20 +340,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
setSending(true);
|
setSending(true);
|
||||||
const messageContent = newMessage;
|
const messageContent = newMessage;
|
||||||
const imageToSend = selectedImage;
|
const imageToSend = selectedImage;
|
||||||
setNewMessage(''); // Clear input immediately for better UX
|
setNewMessage(""); // Clear input immediately for better UX
|
||||||
setSelectedImage(null);
|
setSelectedImage(null);
|
||||||
setImagePreview(null);
|
setImagePreview(null);
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = '';
|
fileInputRef.current.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build FormData for message (with or without image)
|
// Build FormData for message (with or without image)
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('receiverId', recipient.id);
|
formData.append("receiverId", recipient.id);
|
||||||
formData.append('content', messageContent || ' '); // Send space if only image
|
formData.append("content", messageContent || " "); // Send space if only image
|
||||||
if (imageToSend) {
|
if (imageToSend) {
|
||||||
formData.append('image', imageToSend);
|
formData.append("image", imageToSend);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await messageAPI.sendMessage(formData);
|
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
|
// Socket will handle updating the receiver's chat
|
||||||
setMessages((prevMessages) => {
|
setMessages((prevMessages) => {
|
||||||
// Avoid duplicates
|
// Avoid duplicates
|
||||||
if (prevMessages.some(m => m.id === response.data.id)) {
|
if (prevMessages.some((m) => m.id === response.data.id)) {
|
||||||
return prevMessages;
|
return prevMessages;
|
||||||
}
|
}
|
||||||
return [...prevMessages, response.data];
|
return [...prevMessages, response.data];
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send message:', error);
|
console.error("Failed to send message:", error);
|
||||||
setNewMessage(messageContent); // Restore message on error
|
setNewMessage(messageContent); // Restore message on error
|
||||||
setSelectedImage(imageToSend);
|
setSelectedImage(imageToSend);
|
||||||
if (imageToSend) {
|
if (imageToSend) {
|
||||||
@@ -362,47 +391,47 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
|
|
||||||
const formatTime = (dateString: string) => {
|
const formatTime = (dateString: string) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleTimeString('en-US', {
|
return date.toLocaleTimeString("en-US", {
|
||||||
hour: 'numeric',
|
hour: "numeric",
|
||||||
minute: '2-digit',
|
minute: "2-digit",
|
||||||
hour12: true
|
hour12: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
||||||
if (date.toDateString() === today.toDateString()) {
|
if (date.toDateString() === today.toDateString()) {
|
||||||
return 'Today';
|
return "Today";
|
||||||
}
|
}
|
||||||
|
|
||||||
const yesterday = new Date(today);
|
const yesterday = new Date(today);
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
if (date.toDateString() === yesterday.toDateString()) {
|
if (date.toDateString() === yesterday.toDateString()) {
|
||||||
return 'Yesterday';
|
return "Yesterday";
|
||||||
}
|
}
|
||||||
|
|
||||||
return date.toLocaleDateString('en-US', {
|
return date.toLocaleDateString("en-US", {
|
||||||
month: 'short',
|
month: "short",
|
||||||
day: 'numeric',
|
day: "numeric",
|
||||||
year: date.getFullYear() !== today.getFullYear() ? 'numeric' : undefined
|
year: date.getFullYear() !== today.getFullYear() ? "numeric" : undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="position-fixed bottom-0 end-0 m-3 shadow-lg d-flex flex-column"
|
className="position-fixed bottom-0 end-0 m-3 shadow-lg d-flex flex-column"
|
||||||
style={{
|
style={{
|
||||||
width: '350px',
|
width: "350px",
|
||||||
height: '500px',
|
height: "500px",
|
||||||
maxHeight: 'calc(100vh - 100px)',
|
maxHeight: "calc(100vh - 100px)",
|
||||||
zIndex: 1050,
|
zIndex: 1050,
|
||||||
borderRadius: '12px',
|
borderRadius: "12px",
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
backgroundColor: 'white'
|
backgroundColor: "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -413,23 +442,25 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
src={recipient.profileImage}
|
src={recipient.profileImage}
|
||||||
alt={`${recipient.firstName} ${recipient.lastName}`}
|
alt={`${recipient.firstName} ${recipient.lastName}`}
|
||||||
className="rounded-circle me-2"
|
className="rounded-circle me-2"
|
||||||
style={{ width: '35px', height: '35px', objectFit: 'cover' }}
|
style={{ width: "35px", height: "35px", objectFit: "cover" }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="rounded-circle bg-white bg-opacity-25 d-flex align-items-center justify-content-center me-2"
|
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>
|
<i className="bi bi-person-fill text-white"></i>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h6 className="mb-0">{recipient.firstName} {recipient.lastName}</h6>
|
<h6 className="mb-0">
|
||||||
|
{recipient.firstName} {recipient.lastName}
|
||||||
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn-close btn-close-white"
|
className="btn-close btn-close-white"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -440,8 +471,8 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="p-3 overflow-auto flex-grow-1"
|
className="p-3 overflow-auto flex-grow-1"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f8f9fa',
|
backgroundColor: "#f8f9fa",
|
||||||
minHeight: 0
|
minHeight: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -452,15 +483,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
</div>
|
</div>
|
||||||
) : messages.length === 0 ? (
|
) : messages.length === 0 ? (
|
||||||
<div className="text-center py-5">
|
<div className="text-center py-5">
|
||||||
<i className="bi bi-chat-dots" style={{ fontSize: '3rem', color: '#dee2e6' }}></i>
|
<i
|
||||||
<p className="text-muted mt-2">Start a conversation with {recipient.firstName}</p>
|
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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{messages.map((message, index) => {
|
{messages.map((message, index) => {
|
||||||
const isCurrentUser = message.senderId === currentUser?.id;
|
const isCurrentUser = message.senderId === currentUser?.id;
|
||||||
const showDate = index === 0 ||
|
const showDate =
|
||||||
formatDate(message.createdAt) !== formatDate(messages[index - 1].createdAt);
|
index === 0 ||
|
||||||
|
formatDate(message.createdAt) !==
|
||||||
|
formatDate(messages[index - 1].createdAt);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={message.id} ref={setMessageRef(message.id)}>
|
<div key={message.id} ref={setMessageRef(message.id)}>
|
||||||
@@ -471,16 +509,20 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={`d-flex mb-2 ${isCurrentUser ? 'justify-content-end' : ''}`}>
|
<div
|
||||||
|
className={`d-flex mb-2 ${
|
||||||
|
isCurrentUser ? "justify-content-end" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`px-3 py-2 rounded-3 ${
|
className={`px-3 py-2 rounded-3 ${
|
||||||
isCurrentUser
|
isCurrentUser
|
||||||
? 'bg-primary text-white'
|
? "bg-primary text-white"
|
||||||
: 'bg-white border'
|
: "bg-white border"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '75%',
|
maxWidth: "75%",
|
||||||
wordBreak: 'break-word'
|
wordBreak: "break-word",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{message.imagePath && (
|
{message.imagePath && (
|
||||||
@@ -489,24 +531,29 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
src={getMessageImageUrl(message.imagePath)}
|
src={getMessageImageUrl(message.imagePath)}
|
||||||
alt="Shared image"
|
alt="Shared image"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
borderRadius: '8px',
|
borderRadius: "8px",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
maxHeight: '300px',
|
maxHeight: "300px",
|
||||||
objectFit: 'cover'
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
onClick={() => window.open(getMessageImageUrl(message.imagePath!), '_blank')}
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
getMessageImageUrl(message.imagePath!),
|
||||||
|
"_blank"
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{message.content.trim() && (
|
{message.content.trim() && (
|
||||||
<p className="mb-1" style={{ fontSize: '0.95rem' }}>
|
<p className="mb-1" style={{ fontSize: "0.95rem" }}>
|
||||||
{message.content}
|
{message.content}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<small
|
<small
|
||||||
className={isCurrentUser ? 'opacity-75' : 'text-muted'}
|
className={isCurrentUser ? "opacity-75" : "text-muted"}
|
||||||
style={{ fontSize: '0.75rem' }}
|
style={{ fontSize: "0.75rem" }}
|
||||||
>
|
>
|
||||||
{formatTime(message.createdAt)}
|
{formatTime(message.createdAt)}
|
||||||
</small>
|
</small>
|
||||||
@@ -536,17 +583,22 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
src={imagePreview}
|
src={imagePreview}
|
||||||
alt="Preview"
|
alt="Preview"
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '150px',
|
maxWidth: "150px",
|
||||||
maxHeight: '150px',
|
maxHeight: "150px",
|
||||||
borderRadius: '8px',
|
borderRadius: "8px",
|
||||||
objectFit: 'cover'
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1 rounded-circle"
|
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1 rounded-circle"
|
||||||
onClick={handleRemoveImage}
|
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>
|
<i className="bi bi-x"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -558,7 +610,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
onChange={handleImageSelect}
|
onChange={handleImageSelect}
|
||||||
style={{ display: 'none' }}
|
style={{ display: "none" }}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -584,7 +636,11 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
disabled={sending || (!newMessage.trim() && !selectedImage)}
|
disabled={sending || (!newMessage.trim() && !selectedImage)}
|
||||||
>
|
>
|
||||||
{sending ? (
|
{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>
|
<i className="bi bi-send-fill"></i>
|
||||||
)}
|
)}
|
||||||
@@ -595,4 +651,4 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ show, onClose, recipient, onMes
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChatWindow;
|
export default ChatWindow;
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react';
|
import React, {
|
||||||
import { Socket } from 'socket.io-client';
|
createContext,
|
||||||
import socketService from '../services/socket';
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import { Socket } from "socket.io-client";
|
||||||
|
import socketService from "../services/socket";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Socket Context Type
|
* Socket Context Type
|
||||||
@@ -14,8 +21,20 @@ interface SocketContextType {
|
|||||||
emitTypingStop: (receiverId: string) => void;
|
emitTypingStop: (receiverId: string) => void;
|
||||||
emitMarkMessageRead: (messageId: string, senderId: string) => void;
|
emitMarkMessageRead: (messageId: string, senderId: string) => void;
|
||||||
onNewMessage: (callback: (message: any) => void) => () => void;
|
onNewMessage: (callback: (message: any) => void) => () => void;
|
||||||
onMessageRead: (callback: (data: { messageId: string; readAt: string; readBy: string }) => void) => () => void;
|
onMessageRead: (
|
||||||
onUserTyping: (callback: (data: { userId: string; firstName: string; isTyping: boolean }) => void) => () => void;
|
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> = ({
|
export const SocketProvider: React.FC<SocketProviderProps> = ({
|
||||||
children,
|
children,
|
||||||
isAuthenticated = false
|
isAuthenticated = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [socket, setSocket] = useState<Socket | null>(null);
|
const [socket, setSocket] = useState<Socket | null>(null);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
@@ -46,27 +65,20 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
|
|||||||
* Initialize socket connection when user is authenticated
|
* Initialize socket connection when user is authenticated
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[SocketProvider] useEffect running', { isAuthenticated });
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
console.log('[SocketProvider] Not authenticated, skipping socket setup');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[SocketProvider] Initializing socket connection');
|
|
||||||
const newSocket = socketService.connect();
|
const newSocket = socketService.connect();
|
||||||
setSocket(newSocket);
|
setSocket(newSocket);
|
||||||
|
|
||||||
// Listen for connection status changes
|
// Listen for connection status changes
|
||||||
console.log('[SocketProvider] Setting up connection listener');
|
|
||||||
const removeListener = socketService.addConnectionListener((connected) => {
|
const removeListener = socketService.addConnectionListener((connected) => {
|
||||||
console.log('[SocketProvider] Connection status changed:', connected);
|
|
||||||
setIsConnected(connected);
|
setIsConnected(connected);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
return () => {
|
return () => {
|
||||||
console.log('[SocketProvider] Cleaning up connection listener');
|
|
||||||
removeListener();
|
removeListener();
|
||||||
};
|
};
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
@@ -76,7 +88,6 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthenticated && socket) {
|
if (!isAuthenticated && socket) {
|
||||||
console.log('[SocketProvider] User logged out, disconnecting socket');
|
|
||||||
socketService.disconnect();
|
socketService.disconnect();
|
||||||
setSocket(null);
|
setSocket(null);
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
@@ -114,9 +125,12 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
|
|||||||
/**
|
/**
|
||||||
* Emit mark message as read event
|
* Emit mark message as read event
|
||||||
*/
|
*/
|
||||||
const emitMarkMessageRead = useCallback((messageId: string, senderId: string) => {
|
const emitMarkMessageRead = useCallback(
|
||||||
socketService.emitMarkMessageRead(messageId, senderId);
|
(messageId: string, senderId: string) => {
|
||||||
}, []);
|
socketService.emitMarkMessageRead(messageId, senderId);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen for new messages
|
* Listen for new messages
|
||||||
@@ -128,16 +142,34 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
|
|||||||
/**
|
/**
|
||||||
* Listen for message read events
|
* Listen for message read events
|
||||||
*/
|
*/
|
||||||
const onMessageRead = useCallback((callback: (data: { messageId: string; readAt: string; readBy: string }) => void) => {
|
const onMessageRead = useCallback(
|
||||||
return socketService.onMessageRead(callback);
|
(
|
||||||
}, []);
|
callback: (data: {
|
||||||
|
messageId: string;
|
||||||
|
readAt: string;
|
||||||
|
readBy: string;
|
||||||
|
}) => void
|
||||||
|
) => {
|
||||||
|
return socketService.onMessageRead(callback);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen for typing indicators
|
* Listen for typing indicators
|
||||||
*/
|
*/
|
||||||
const onUserTyping = useCallback((callback: (data: { userId: string; firstName: string; isTyping: boolean }) => void) => {
|
const onUserTyping = useCallback(
|
||||||
return socketService.onUserTyping(callback);
|
(
|
||||||
}, []);
|
callback: (data: {
|
||||||
|
userId: string;
|
||||||
|
firstName: string;
|
||||||
|
isTyping: boolean;
|
||||||
|
}) => void
|
||||||
|
) => {
|
||||||
|
return socketService.onUserTyping(callback);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const value: SocketContextType = {
|
const value: SocketContextType = {
|
||||||
socket,
|
socket,
|
||||||
@@ -153,9 +185,7 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Provider value={value}>
|
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||||
{children}
|
|
||||||
</SocketContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,7 +197,7 @@ export const useSocket = (): SocketContextType => {
|
|||||||
const context = useContext(SocketContext);
|
const context = useContext(SocketContext);
|
||||||
|
|
||||||
if (context === undefined) {
|
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;
|
return context;
|
||||||
|
|||||||
@@ -208,19 +208,11 @@ const ItemDetail: React.FC = () => {
|
|||||||
const dayTimes = item.weeklyTimes[dayName];
|
const dayTimes = item.weeklyTimes[dayName];
|
||||||
availableAfter = dayTimes.availableAfter;
|
availableAfter = dayTimes.availableAfter;
|
||||||
availableBefore = dayTimes.availableBefore;
|
availableBefore = dayTimes.availableBefore;
|
||||||
console.log("Using day-specific times:", {
|
|
||||||
availableAfter,
|
|
||||||
availableBefore,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// Otherwise use global times
|
// Otherwise use global times
|
||||||
else if (item.availableAfter && item.availableBefore) {
|
else if (item.availableAfter && item.availableBefore) {
|
||||||
availableAfter = item.availableAfter;
|
availableAfter = item.availableAfter;
|
||||||
availableBefore = item.availableBefore;
|
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 no options are available, return at least one option to prevent empty dropdown
|
||||||
if (options.length === 0) {
|
if (options.length === 0) {
|
||||||
console.log("No valid time options found, showing Not Available");
|
|
||||||
options.push({ value: "00:00", label: "Not Available" });
|
options.push({ value: "00:00", label: "Not Available" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { Conversation, Message, User } from '../types';
|
import { Conversation, Message, User } from "../types";
|
||||||
import { messageAPI } from '../services/api';
|
import { messageAPI } from "../services/api";
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { useSocket } from '../contexts/SocketContext';
|
import { useSocket } from "../contexts/SocketContext";
|
||||||
import ChatWindow from '../components/ChatWindow';
|
import ChatWindow from "../components/ChatWindow";
|
||||||
|
|
||||||
const Messages: React.FC = () => {
|
const Messages: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -23,17 +23,16 @@ const Messages: React.FC = () => {
|
|||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
const cleanup = onNewMessage((newMessage: Message) => {
|
const cleanup = onNewMessage((newMessage: Message) => {
|
||||||
console.log('[Messages] Received new message:', newMessage);
|
|
||||||
|
|
||||||
setConversations((prevConversations) => {
|
setConversations((prevConversations) => {
|
||||||
// Determine conversation partner
|
// Determine conversation partner
|
||||||
const partnerId = newMessage.senderId === user?.id
|
const partnerId =
|
||||||
? newMessage.receiverId
|
newMessage.senderId === user?.id
|
||||||
: newMessage.senderId;
|
? newMessage.receiverId
|
||||||
|
: newMessage.senderId;
|
||||||
|
|
||||||
// Find existing conversation
|
// Find existing conversation
|
||||||
const existingIndex = prevConversations.findIndex(
|
const existingIndex = prevConversations.findIndex(
|
||||||
c => c.partnerId === partnerId
|
(c) => c.partnerId === partnerId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
@@ -46,7 +45,7 @@ const Messages: React.FC = () => {
|
|||||||
content: newMessage.content,
|
content: newMessage.content,
|
||||||
senderId: newMessage.senderId,
|
senderId: newMessage.senderId,
|
||||||
createdAt: newMessage.createdAt,
|
createdAt: newMessage.createdAt,
|
||||||
isRead: newMessage.isRead
|
isRead: newMessage.isRead,
|
||||||
};
|
};
|
||||||
conv.lastMessageAt = newMessage.createdAt;
|
conv.lastMessageAt = newMessage.createdAt;
|
||||||
|
|
||||||
@@ -58,20 +57,21 @@ const Messages: React.FC = () => {
|
|||||||
updated[existingIndex] = conv;
|
updated[existingIndex] = conv;
|
||||||
|
|
||||||
// Re-sort by most recent
|
// Re-sort by most recent
|
||||||
updated.sort((a, b) =>
|
updated.sort(
|
||||||
new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime()
|
(a, b) =>
|
||||||
|
new Date(b.lastMessageAt).getTime() -
|
||||||
|
new Date(a.lastMessageAt).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('[Messages] Updated existing conversation');
|
|
||||||
return updated;
|
return updated;
|
||||||
} else {
|
} else {
|
||||||
// New conversation - add to top
|
// New conversation - add to top
|
||||||
const partner = newMessage.senderId === user?.id
|
const partner =
|
||||||
? newMessage.receiver!
|
newMessage.senderId === user?.id
|
||||||
: newMessage.sender!;
|
? newMessage.receiver!
|
||||||
|
: newMessage.sender!;
|
||||||
|
|
||||||
if (!partner) {
|
if (!partner) {
|
||||||
console.warn('[Messages] Partner data missing from new message');
|
|
||||||
return prevConversations;
|
return prevConversations;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,13 +83,13 @@ const Messages: React.FC = () => {
|
|||||||
content: newMessage.content,
|
content: newMessage.content,
|
||||||
senderId: newMessage.senderId,
|
senderId: newMessage.senderId,
|
||||||
createdAt: newMessage.createdAt,
|
createdAt: newMessage.createdAt,
|
||||||
isRead: newMessage.isRead
|
isRead: newMessage.isRead,
|
||||||
},
|
},
|
||||||
unreadCount: newMessage.receiverId === user?.id && !newMessage.isRead ? 1 : 0,
|
unreadCount:
|
||||||
lastMessageAt: newMessage.createdAt
|
newMessage.receiverId === user?.id && !newMessage.isRead ? 1 : 0,
|
||||||
|
lastMessageAt: newMessage.createdAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[Messages] Created new conversation');
|
|
||||||
return [newConv, ...prevConversations];
|
return [newConv, ...prevConversations];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -103,16 +103,17 @@ const Messages: React.FC = () => {
|
|||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
const cleanup = onMessageRead((data: any) => {
|
const cleanup = onMessageRead((data: any) => {
|
||||||
console.log('[Messages] Message read:', data);
|
|
||||||
|
|
||||||
setConversations((prevConversations) => {
|
setConversations((prevConversations) => {
|
||||||
return prevConversations.map(conv => {
|
return prevConversations.map((conv) => {
|
||||||
// If this is the conversation and the last message was marked as read
|
// 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 {
|
return {
|
||||||
...conv,
|
...conv,
|
||||||
lastMessage: { ...conv.lastMessage, isRead: true },
|
lastMessage: { ...conv.lastMessage, isRead: true },
|
||||||
unreadCount: Math.max(0, conv.unreadCount - 1)
|
unreadCount: Math.max(0, conv.unreadCount - 1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return conv;
|
return conv;
|
||||||
@@ -127,10 +128,8 @@ const Messages: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const response = await messageAPI.getConversations();
|
const response = await messageAPI.getConversations();
|
||||||
setConversations(response.data);
|
setConversations(response.data);
|
||||||
console.log('[Messages] Fetched conversations:', response.data.length);
|
|
||||||
} catch (err: any) {
|
} 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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -142,11 +141,17 @@ const Messages: React.FC = () => {
|
|||||||
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
||||||
|
|
||||||
if (diffInHours < 24) {
|
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) {
|
} else if (diffInHours < 48) {
|
||||||
return 'Yesterday';
|
return "Yesterday";
|
||||||
} else {
|
} 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) => {
|
const handleMessagesRead = (partnerId: string, count: number) => {
|
||||||
console.log(`[Messages] ${count} messages marked as read for partner ${partnerId}`);
|
|
||||||
|
|
||||||
// Update the conversation's unread count
|
// Update the conversation's unread count
|
||||||
setConversations(prevConversations =>
|
setConversations((prevConversations) =>
|
||||||
prevConversations.map(conv =>
|
prevConversations.map((conv) =>
|
||||||
conv.partnerId === partnerId
|
conv.partnerId === partnerId ? { ...conv, unreadCount: 0 } : conv
|
||||||
? { ...conv, unreadCount: 0 }
|
|
||||||
: conv
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -201,23 +202,29 @@ const Messages: React.FC = () => {
|
|||||||
|
|
||||||
{conversations.length === 0 ? (
|
{conversations.length === 0 ? (
|
||||||
<div className="text-center py-5">
|
<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>
|
<p className="text-muted mt-2">No conversations yet</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="list-group">
|
<div className="list-group">
|
||||||
{conversations.map((conversation) => {
|
{conversations.map((conversation) => {
|
||||||
const isUnread = conversation.unreadCount > 0;
|
const isUnread = conversation.unreadCount > 0;
|
||||||
const isLastMessageFromPartner = conversation.lastMessage.senderId === conversation.partnerId;
|
const isLastMessageFromPartner =
|
||||||
|
conversation.lastMessage.senderId === conversation.partnerId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={conversation.partnerId}
|
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)}
|
onClick={() => handleConversationClick(conversation)}
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
backgroundColor: isUnread ? '#f0f7ff' : 'white'
|
backgroundColor: isUnread ? "#f0f7ff" : "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="d-flex w-100 justify-content-between align-items-start">
|
<div className="d-flex w-100 justify-content-between align-items-start">
|
||||||
@@ -228,12 +235,16 @@ const Messages: React.FC = () => {
|
|||||||
src={conversation.partner.profileImage}
|
src={conversation.partner.profileImage}
|
||||||
alt={`${conversation.partner.firstName} ${conversation.partner.lastName}`}
|
alt={`${conversation.partner.firstName} ${conversation.partner.lastName}`}
|
||||||
className="rounded-circle me-3"
|
className="rounded-circle me-3"
|
||||||
style={{ width: '50px', height: '50px', objectFit: 'cover' }}
|
style={{
|
||||||
|
width: "50px",
|
||||||
|
height: "50px",
|
||||||
|
objectFit: "cover",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-3"
|
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>
|
<i className="bi bi-person-fill text-white"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,18 +253,25 @@ const Messages: React.FC = () => {
|
|||||||
<div className="flex-grow-1" style={{ minWidth: 0 }}>
|
<div className="flex-grow-1" style={{ minWidth: 0 }}>
|
||||||
{/* User Name and Unread Badge */}
|
{/* User Name and Unread Badge */}
|
||||||
<div className="d-flex align-items-center mb-1">
|
<div className="d-flex align-items-center mb-1">
|
||||||
<h6 className={`mb-0 ${isUnread ? 'fw-bold' : ''}`}>
|
<h6 className={`mb-0 ${isUnread ? "fw-bold" : ""}`}>
|
||||||
{conversation.partner.firstName} {conversation.partner.lastName}
|
{conversation.partner.firstName}{" "}
|
||||||
|
{conversation.partner.lastName}
|
||||||
</h6>
|
</h6>
|
||||||
{isUnread && (
|
{isUnread && (
|
||||||
<span className="badge bg-primary ms-2">{conversation.unreadCount}</span>
|
<span className="badge bg-primary ms-2">
|
||||||
|
{conversation.unreadCount}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Last Message Preview */}
|
{/* Last Message Preview */}
|
||||||
<p
|
<p
|
||||||
className={`mb-0 text-truncate ${isUnread && isLastMessageFromPartner ? 'fw-semibold' : 'text-muted'}`}
|
className={`mb-0 text-truncate ${
|
||||||
style={{ maxWidth: '100%' }}
|
isUnread && isLastMessageFromPartner
|
||||||
|
? "fw-semibold"
|
||||||
|
: "text-muted"
|
||||||
|
}`}
|
||||||
|
style={{ maxWidth: "100%" }}
|
||||||
>
|
>
|
||||||
{conversation.lastMessage.senderId === user?.id && (
|
{conversation.lastMessage.senderId === user?.id && (
|
||||||
<span className="me-1">You: </span>
|
<span className="me-1">You: </span>
|
||||||
@@ -264,7 +282,10 @@ const Messages: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Timestamp */}
|
{/* 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">
|
<small className="text-muted d-block">
|
||||||
{formatDate(conversation.lastMessageAt)}
|
{formatDate(conversation.lastMessageAt)}
|
||||||
</small>
|
</small>
|
||||||
|
|||||||
@@ -52,12 +52,9 @@ class SocketService {
|
|||||||
*/
|
*/
|
||||||
connect(): Socket {
|
connect(): Socket {
|
||||||
if (this.socket?.connected) {
|
if (this.socket?.connected) {
|
||||||
console.log("[Socket] Already connected");
|
|
||||||
return this.socket;
|
return this.socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Socket] Connecting to server...");
|
|
||||||
|
|
||||||
this.socket = io(this.getSocketUrl(), {
|
this.socket = io(this.getSocketUrl(), {
|
||||||
withCredentials: true, // Send cookies for authentication
|
withCredentials: true, // Send cookies for authentication
|
||||||
transports: ["websocket", "polling"], // Try WebSocket first, fallback to polling
|
transports: ["websocket", "polling"], // Try WebSocket first, fallback to polling
|
||||||
@@ -71,23 +68,15 @@ class SocketService {
|
|||||||
|
|
||||||
// Connection event handlers
|
// Connection event handlers
|
||||||
this.socket.on("connect", () => {
|
this.socket.on("connect", () => {
|
||||||
console.log("[Socket] Connected successfully", {
|
|
||||||
socketId: this.socket?.id,
|
|
||||||
});
|
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
this.notifyConnectionListeners(true);
|
this.notifyConnectionListeners(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on("disconnect", (reason) => {
|
this.socket.on("disconnect", (reason) => {
|
||||||
console.log("[Socket] Disconnected", { reason });
|
|
||||||
this.notifyConnectionListeners(false);
|
this.notifyConnectionListeners(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on("connect_error", (error) => {
|
this.socket.on("connect_error", (error) => {
|
||||||
console.error("[Socket] Connection error", {
|
|
||||||
error: error.message,
|
|
||||||
attempt: this.reconnectAttempts + 1,
|
|
||||||
});
|
|
||||||
this.reconnectAttempts++;
|
this.reconnectAttempts++;
|
||||||
|
|
||||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
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;
|
return this.socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +93,6 @@ class SocketService {
|
|||||||
*/
|
*/
|
||||||
disconnect(): void {
|
disconnect(): void {
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
console.log("[Socket] Disconnecting...");
|
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
this.socket = null;
|
this.socket = null;
|
||||||
this.notifyConnectionListeners(false);
|
this.notifyConnectionListeners(false);
|
||||||
@@ -138,7 +122,6 @@ class SocketService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Socket] Joining conversation", { otherUserId });
|
|
||||||
this.socket.emit("join_conversation", { otherUserId });
|
this.socket.emit("join_conversation", { otherUserId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +133,6 @@ class SocketService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Socket] Leaving conversation", { otherUserId });
|
|
||||||
this.socket.emit("leave_conversation", { otherUserId });
|
this.socket.emit("leave_conversation", { otherUserId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user