refactor(frontend): Phase 2 - extract business logic into custom hooks
Separate concerns - move Socket.IO and form logic from components to reusable hooks New Hooks: - useForm: Generic form state management with handleChange/handleSubmit/reset - useEventChat: Extract Socket.IO logic from EventChatPage (156 lines) * Manages messages, active users, connection state * Handles send message, load older messages with scroll preservation * Real-time updates via Socket.IO event listeners - useMatchChat: Extract Socket.IO logic from MatchChatPage (115 lines) * Manages 1:1 chat messages and connection * Loads message history from API * Real-time message sync via Socket.IO Pages Refactored: - EventChatPage: 661 → 564 lines (-97 lines, -15%) - MatchChatPage: 517 → 446 lines (-71 lines, -14%) Benefits: - Cleaner component code - UI separated from business logic - Reusable hooks can be used in other components - Easier to test - hooks can be unit tested independently - Better code organization - single responsibility principle - 168 lines eliminated from pages, moved to 271 lines of reusable hooks Phase 2 Total: -168 lines Grand Total (Phase 1+2): -389 lines (-12%)
This commit is contained in:
180
frontend/src/hooks/useEventChat.js
Normal file
180
frontend/src/hooks/useEventChat.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { connectSocket, getSocket } from '../services/socket';
|
||||||
|
import { eventsAPI } from '../services/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for Event Chat functionality
|
||||||
|
* Extracts Socket.IO logic and chat state management from EventChatPage
|
||||||
|
*
|
||||||
|
* @param {string} slug - Event slug
|
||||||
|
* @param {number} userId - Current user ID
|
||||||
|
* @param {object} event - Event object (needed to check if event exists)
|
||||||
|
* @param {object} messagesContainerRef - Ref to messages container for scroll management
|
||||||
|
* @returns {object} Chat state and handlers
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const {
|
||||||
|
* messages,
|
||||||
|
* isConnected,
|
||||||
|
* activeUsers,
|
||||||
|
* sendMessage,
|
||||||
|
* newMessage,
|
||||||
|
* setNewMessage,
|
||||||
|
* loadOlderMessages,
|
||||||
|
* loadingOlder,
|
||||||
|
* hasMore
|
||||||
|
* } = useEventChat(slug, user.id, event, messagesContainerRef);
|
||||||
|
*/
|
||||||
|
const useEventChat = (slug, userId, event, messagesContainerRef) => {
|
||||||
|
// Chat state
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [newMessage, setNewMessage] = useState('');
|
||||||
|
const [activeUsers, setActiveUsers] = useState([]);
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||||
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
|
||||||
|
// Socket.IO connection and event listeners
|
||||||
|
useEffect(() => {
|
||||||
|
if (!event) return;
|
||||||
|
|
||||||
|
// Connect to Socket.IO
|
||||||
|
const socket = connectSocket();
|
||||||
|
|
||||||
|
if (!socket) {
|
||||||
|
console.error('Failed to connect to socket');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socket event listeners
|
||||||
|
socket.on('connect', () => {
|
||||||
|
setIsConnected(true);
|
||||||
|
// Join event room
|
||||||
|
socket.emit('join_event_room', { slug });
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
setIsConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive message history (initial 20 messages)
|
||||||
|
socket.on('message_history', (history) => {
|
||||||
|
setMessages(history);
|
||||||
|
setHasMore(history.length === 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive new messages
|
||||||
|
socket.on('event_message', (message) => {
|
||||||
|
setMessages((prev) => [...prev, message]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive active users list
|
||||||
|
socket.on('active_users', (users) => {
|
||||||
|
// Filter out duplicates and current user
|
||||||
|
const uniqueUsers = users
|
||||||
|
.filter((u, index, self) =>
|
||||||
|
index === self.findIndex((t) => t.userId === u.userId)
|
||||||
|
)
|
||||||
|
.filter((u) => u.userId !== userId);
|
||||||
|
setActiveUsers(uniqueUsers);
|
||||||
|
});
|
||||||
|
|
||||||
|
// User joined notification
|
||||||
|
socket.on('user_joined', (userData) => {
|
||||||
|
console.log(`${userData.username} joined the room`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// User left notification
|
||||||
|
socket.on('user_left', (userData) => {
|
||||||
|
console.log(`${userData.username} left the room`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
socket.emit('leave_event_room');
|
||||||
|
socket.off('connect');
|
||||||
|
socket.off('disconnect');
|
||||||
|
socket.off('message_history');
|
||||||
|
socket.off('event_message');
|
||||||
|
socket.off('active_users');
|
||||||
|
socket.off('user_joined');
|
||||||
|
socket.off('user_left');
|
||||||
|
};
|
||||||
|
}, [event, slug, userId]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to the event chat
|
||||||
|
*/
|
||||||
|
const sendMessage = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newMessage.trim()) return;
|
||||||
|
|
||||||
|
const socket = getSocket();
|
||||||
|
if (!socket || !socket.connected) {
|
||||||
|
alert('Not connected to chat server');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message via Socket.IO
|
||||||
|
socket.emit('send_event_message', {
|
||||||
|
content: newMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
setNewMessage('');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load older messages (pagination)
|
||||||
|
* Preserves scroll position when loading older messages
|
||||||
|
*/
|
||||||
|
const loadOlderMessages = async () => {
|
||||||
|
if (loadingOlder || !hasMore || messages.length === 0) return;
|
||||||
|
|
||||||
|
setLoadingOlder(true);
|
||||||
|
try {
|
||||||
|
const oldestMessageId = messages[0].id;
|
||||||
|
const response = await eventsAPI.getMessages(slug, oldestMessageId, 20);
|
||||||
|
|
||||||
|
if (response.data.length > 0) {
|
||||||
|
// Save current scroll position
|
||||||
|
const container = messagesContainerRef.current;
|
||||||
|
const oldScrollHeight = container?.scrollHeight || 0;
|
||||||
|
const oldScrollTop = container?.scrollTop || 0;
|
||||||
|
|
||||||
|
// Prepend older messages
|
||||||
|
setMessages((prev) => [...response.data, ...prev]);
|
||||||
|
setHasMore(response.hasMore);
|
||||||
|
|
||||||
|
// Restore scroll position (adjust for new content)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (container) {
|
||||||
|
const newScrollHeight = container.scrollHeight;
|
||||||
|
container.scrollTop = oldScrollTop + (newScrollHeight - oldScrollHeight);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
setHasMore(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load older messages:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingOlder(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
messages,
|
||||||
|
newMessage,
|
||||||
|
setNewMessage,
|
||||||
|
activeUsers,
|
||||||
|
isConnected,
|
||||||
|
loadingOlder,
|
||||||
|
hasMore,
|
||||||
|
// Actions
|
||||||
|
sendMessage,
|
||||||
|
loadOlderMessages
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useEventChat;
|
||||||
89
frontend/src/hooks/useForm.js
Normal file
89
frontend/src/hooks/useForm.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for form state management
|
||||||
|
*
|
||||||
|
* Eliminates repetitive useState and onChange handlers for each form field
|
||||||
|
*
|
||||||
|
* @param {Object} initialValues - Initial form values { fieldName: value, ... }
|
||||||
|
* @param {Function} onSubmit - Optional submit handler
|
||||||
|
* @returns {Object} Form state and handlers
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { values, handleChange, handleSubmit, reset } = useForm(
|
||||||
|
* { email: '', password: '' },
|
||||||
|
* async (values) => { await login(values.email, values.password); }
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* <input name="email" value={values.email} onChange={handleChange} />
|
||||||
|
* <form onSubmit={handleSubmit}>...</form>
|
||||||
|
*/
|
||||||
|
const useForm = (initialValues = {}, onSubmit) => {
|
||||||
|
const [values, setValues] = useState(initialValues);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic change handler for all form fields
|
||||||
|
* Works with any input that has name and value attributes
|
||||||
|
*/
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value, type, checked } = e.target;
|
||||||
|
|
||||||
|
// Handle checkboxes differently
|
||||||
|
const fieldValue = type === 'checkbox' ? checked : value;
|
||||||
|
|
||||||
|
setValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: fieldValue
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle form submission
|
||||||
|
* Prevents default, calls optional onSubmit callback
|
||||||
|
*/
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (onSubmit) {
|
||||||
|
await onSubmit(values);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset form to initial values
|
||||||
|
*/
|
||||||
|
const reset = () => {
|
||||||
|
setValues(initialValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set multiple values at once
|
||||||
|
* Useful for pre-filling form from API data
|
||||||
|
*/
|
||||||
|
const setFormValues = (newValues) => {
|
||||||
|
setValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
...newValues
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a single field value programmatically
|
||||||
|
*/
|
||||||
|
const setValue = (name, value) => {
|
||||||
|
setValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
handleChange,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
setFormValues,
|
||||||
|
setValue
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useForm;
|
||||||
122
frontend/src/hooks/useMatchChat.js
Normal file
122
frontend/src/hooks/useMatchChat.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { connectSocket, getSocket } from '../services/socket';
|
||||||
|
import { matchesAPI } from '../services/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for Match Chat functionality (1:1 private chat)
|
||||||
|
* Extracts Socket.IO logic and chat state management from MatchChatPage
|
||||||
|
*
|
||||||
|
* @param {object} match - Match object with id and partner info
|
||||||
|
* @param {number} userId - Current user ID
|
||||||
|
* @param {string} slug - Match slug for API calls
|
||||||
|
* @returns {object} Chat state and handlers
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const {
|
||||||
|
* messages,
|
||||||
|
* isConnected,
|
||||||
|
* sendMessage,
|
||||||
|
* newMessage,
|
||||||
|
* setNewMessage
|
||||||
|
* } = useMatchChat(match, user.id, slug);
|
||||||
|
*/
|
||||||
|
const useMatchChat = (match, userId, slug) => {
|
||||||
|
// Chat state
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [newMessage, setNewMessage] = useState('');
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
|
||||||
|
// Load message history from API
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMessages = async () => {
|
||||||
|
if (!match || !slug) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await matchesAPI.getMatchMessages(slug);
|
||||||
|
setMessages(result.data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load messages:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadMessages();
|
||||||
|
}, [match, slug]);
|
||||||
|
|
||||||
|
// Socket.IO connection and event listeners
|
||||||
|
useEffect(() => {
|
||||||
|
// Wait for match to be loaded
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
// Connect to Socket.IO
|
||||||
|
const socket = connectSocket();
|
||||||
|
|
||||||
|
if (!socket) {
|
||||||
|
console.error('Failed to connect to socket');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to join match room
|
||||||
|
const joinMatchRoom = () => {
|
||||||
|
setIsConnected(true);
|
||||||
|
socket.emit('join_match_room', { matchId: match.id });
|
||||||
|
console.log(`Joined match room ${match.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Socket event listeners
|
||||||
|
socket.on('connect', joinMatchRoom);
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
setIsConnected(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive messages
|
||||||
|
socket.on('match_message', (message) => {
|
||||||
|
setMessages((prev) => [...prev, message]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Join immediately if already connected
|
||||||
|
if (socket.connected) {
|
||||||
|
joinMatchRoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
socket.off('connect', joinMatchRoom);
|
||||||
|
socket.off('disconnect');
|
||||||
|
socket.off('match_message');
|
||||||
|
};
|
||||||
|
}, [match, userId]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to the match chat
|
||||||
|
*/
|
||||||
|
const sendMessage = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newMessage.trim() || !match) return;
|
||||||
|
|
||||||
|
const socket = getSocket();
|
||||||
|
if (!socket || !socket.connected) {
|
||||||
|
alert('Not connected to chat server');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message via Socket.IO using numeric match ID
|
||||||
|
socket.emit('send_match_message', {
|
||||||
|
matchId: match.id,
|
||||||
|
content: newMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
setNewMessage('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
messages,
|
||||||
|
newMessage,
|
||||||
|
setNewMessage,
|
||||||
|
isConnected,
|
||||||
|
// Actions
|
||||||
|
sendMessage
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useMatchChat;
|
||||||
@@ -11,6 +11,7 @@ import ChatMessageList from '../components/chat/ChatMessageList';
|
|||||||
import ChatInput from '../components/chat/ChatInput';
|
import ChatInput from '../components/chat/ChatInput';
|
||||||
import ConfirmationModal from '../components/modals/ConfirmationModal';
|
import ConfirmationModal from '../components/modals/ConfirmationModal';
|
||||||
import Modal from '../components/modals/Modal';
|
import Modal from '../components/modals/Modal';
|
||||||
|
import useEventChat from '../hooks/useEventChat';
|
||||||
|
|
||||||
const EventChatPage = () => {
|
const EventChatPage = () => {
|
||||||
const { slug } = useParams();
|
const { slug } = useParams();
|
||||||
@@ -19,18 +20,25 @@ const EventChatPage = () => {
|
|||||||
const [event, setEvent] = useState(null);
|
const [event, setEvent] = useState(null);
|
||||||
const [isParticipant, setIsParticipant] = useState(false);
|
const [isParticipant, setIsParticipant] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [messages, setMessages] = useState([]);
|
|
||||||
const [newMessage, setNewMessage] = useState('');
|
|
||||||
const [activeUsers, setActiveUsers] = useState([]);
|
|
||||||
const [checkedInUsers, setCheckedInUsers] = useState([]);
|
const [checkedInUsers, setCheckedInUsers] = useState([]);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
|
||||||
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
||||||
const [hasMore, setHasMore] = useState(true);
|
|
||||||
const [showLeaveModal, setShowLeaveModal] = useState(false);
|
const [showLeaveModal, setShowLeaveModal] = useState(false);
|
||||||
const [isLeaving, setIsLeaving] = useState(false);
|
const [isLeaving, setIsLeaving] = useState(false);
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const messagesContainerRef = useRef(null);
|
const messagesContainerRef = useRef(null);
|
||||||
|
|
||||||
|
// Event Chat hook - manages messages, Socket.IO, and active users
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
newMessage,
|
||||||
|
setNewMessage,
|
||||||
|
activeUsers,
|
||||||
|
isConnected,
|
||||||
|
loadingOlder,
|
||||||
|
hasMore,
|
||||||
|
sendMessage: handleSendMessage,
|
||||||
|
loadOlderMessages
|
||||||
|
} = useEventChat(slug, user?.id, event, messagesContainerRef);
|
||||||
|
|
||||||
// Heats state
|
// Heats state
|
||||||
const [myHeats, setMyHeats] = useState([]);
|
const [myHeats, setMyHeats] = useState([]);
|
||||||
const [userHeats, setUserHeats] = useState(new Map());
|
const [userHeats, setUserHeats] = useState(new Map());
|
||||||
@@ -116,62 +124,15 @@ const EventChatPage = () => {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
// Heats updates listener (specific to EventChatPage)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
|
|
||||||
// Connect to Socket.IO
|
const socket = getSocket();
|
||||||
const socket = connectSocket();
|
if (!socket) return;
|
||||||
|
|
||||||
if (!socket) {
|
|
||||||
console.error('Failed to connect to socket');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Socket event listeners
|
|
||||||
socket.on('connect', () => {
|
|
||||||
setIsConnected(true);
|
|
||||||
// Join event room
|
|
||||||
socket.emit('join_event_room', { slug });
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
setIsConnected(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Receive message history (initial 20 messages)
|
|
||||||
socket.on('message_history', (history) => {
|
|
||||||
setMessages(history);
|
|
||||||
setHasMore(history.length === 20);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Receive new messages
|
|
||||||
socket.on('event_message', (message) => {
|
|
||||||
setMessages((prev) => [...prev, message]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Receive active users list
|
|
||||||
socket.on('active_users', (users) => {
|
|
||||||
// Filter out duplicates and current user
|
|
||||||
const uniqueUsers = users
|
|
||||||
.filter((u, index, self) =>
|
|
||||||
index === self.findIndex((t) => t.userId === u.userId)
|
|
||||||
)
|
|
||||||
.filter((u) => u.userId !== user.id);
|
|
||||||
setActiveUsers(uniqueUsers);
|
|
||||||
});
|
|
||||||
|
|
||||||
// User joined notification
|
|
||||||
socket.on('user_joined', (userData) => {
|
|
||||||
console.log(`${userData.username} joined the room`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// User left notification
|
|
||||||
socket.on('user_left', (userData) => {
|
|
||||||
console.log(`${userData.username} left the room`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Heats updated notification
|
// Heats updated notification
|
||||||
socket.on('heats_updated', (data) => {
|
const handleHeatsUpdated = (data) => {
|
||||||
const { userId, heats } = data;
|
const { userId, heats } = data;
|
||||||
|
|
||||||
// Update userHeats map
|
// Update userHeats map
|
||||||
@@ -190,72 +151,14 @@ const EventChatPage = () => {
|
|||||||
setMyHeats(heats || []);
|
setMyHeats(heats || []);
|
||||||
setShowHeatsBanner(heats.length === 0);
|
setShowHeatsBanner(heats.length === 0);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
|
||||||
socket.emit('leave_event_room');
|
|
||||||
socket.off('connect');
|
|
||||||
socket.off('disconnect');
|
|
||||||
socket.off('message_history');
|
|
||||||
socket.off('event_message');
|
|
||||||
socket.off('active_users');
|
|
||||||
socket.off('user_joined');
|
|
||||||
socket.off('user_left');
|
|
||||||
socket.off('heats_updated');
|
|
||||||
};
|
};
|
||||||
}, [event, slug, user.id]);
|
|
||||||
|
|
||||||
const handleSendMessage = (e) => {
|
socket.on('heats_updated', handleHeatsUpdated);
|
||||||
e.preventDefault();
|
|
||||||
if (!newMessage.trim()) return;
|
|
||||||
|
|
||||||
const socket = getSocket();
|
return () => {
|
||||||
if (!socket || !socket.connected) {
|
socket.off('heats_updated', handleHeatsUpdated);
|
||||||
alert('Not connected to chat server');
|
};
|
||||||
return;
|
}, [event, user.id]);
|
||||||
}
|
|
||||||
|
|
||||||
// Send message via Socket.IO
|
|
||||||
socket.emit('send_event_message', {
|
|
||||||
content: newMessage,
|
|
||||||
});
|
|
||||||
|
|
||||||
setNewMessage('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadOlderMessages = async () => {
|
|
||||||
if (loadingOlder || !hasMore || messages.length === 0) return;
|
|
||||||
|
|
||||||
setLoadingOlder(true);
|
|
||||||
try {
|
|
||||||
const oldestMessageId = messages[0].id;
|
|
||||||
const response = await eventsAPI.getMessages(slug, oldestMessageId, 20);
|
|
||||||
|
|
||||||
if (response.data.length > 0) {
|
|
||||||
// Save current scroll position
|
|
||||||
const container = messagesContainerRef.current;
|
|
||||||
const oldScrollHeight = container.scrollHeight;
|
|
||||||
const oldScrollTop = container.scrollTop;
|
|
||||||
|
|
||||||
// Prepend older messages
|
|
||||||
setMessages((prev) => [...response.data, ...prev]);
|
|
||||||
setHasMore(response.hasMore);
|
|
||||||
|
|
||||||
// Restore scroll position (adjust for new content)
|
|
||||||
setTimeout(() => {
|
|
||||||
const newScrollHeight = container.scrollHeight;
|
|
||||||
container.scrollTop = oldScrollTop + (newScrollHeight - oldScrollHeight);
|
|
||||||
}, 0);
|
|
||||||
} else {
|
|
||||||
setHasMore(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load older messages:', error);
|
|
||||||
} finally {
|
|
||||||
setLoadingOlder(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMatchWith = async (userId) => {
|
const handleMatchWith = async (userId) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import WebRTCWarning from '../components/WebRTCWarning';
|
|||||||
import Avatar from '../components/common/Avatar';
|
import Avatar from '../components/common/Avatar';
|
||||||
import ChatMessageList from '../components/chat/ChatMessageList';
|
import ChatMessageList from '../components/chat/ChatMessageList';
|
||||||
import ChatInput from '../components/chat/ChatInput';
|
import ChatInput from '../components/chat/ChatInput';
|
||||||
|
import useMatchChat from '../hooks/useMatchChat';
|
||||||
|
|
||||||
const MatchChatPage = () => {
|
const MatchChatPage = () => {
|
||||||
const { slug } = useParams();
|
const { slug } = useParams();
|
||||||
@@ -18,16 +19,22 @@ const MatchChatPage = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [match, setMatch] = useState(null);
|
const [match, setMatch] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [messages, setMessages] = useState([]);
|
|
||||||
const [newMessage, setNewMessage] = useState('');
|
|
||||||
const [selectedFile, setSelectedFile] = useState(null);
|
const [selectedFile, setSelectedFile] = useState(null);
|
||||||
const [showLinkInput, setShowLinkInput] = useState(false);
|
const [showLinkInput, setShowLinkInput] = useState(false);
|
||||||
const [videoLink, setVideoLink] = useState('');
|
const [videoLink, setVideoLink] = useState('');
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
|
||||||
const [webrtcDetection, setWebrtcDetection] = useState(null);
|
const [webrtcDetection, setWebrtcDetection] = useState(null);
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
// Match Chat hook - manages messages and Socket.IO
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
newMessage,
|
||||||
|
setNewMessage,
|
||||||
|
isConnected,
|
||||||
|
sendMessage: handleSendMessage
|
||||||
|
} = useMatchChat(match, user?.id, slug);
|
||||||
|
|
||||||
// WebRTC hook
|
// WebRTC hook
|
||||||
const {
|
const {
|
||||||
connectionState,
|
connectionState,
|
||||||
@@ -72,21 +79,6 @@ const MatchChatPage = () => {
|
|||||||
loadMatch();
|
loadMatch();
|
||||||
}, [slug, navigate]);
|
}, [slug, navigate]);
|
||||||
|
|
||||||
// Load message history
|
|
||||||
useEffect(() => {
|
|
||||||
const loadMessages = async () => {
|
|
||||||
if (!match) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await matchesAPI.getMatchMessages(slug);
|
|
||||||
setMessages(result.data || []);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load messages:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadMessages();
|
|
||||||
}, [match, slug]);
|
|
||||||
|
|
||||||
const partner = match?.partner;
|
const partner = match?.partner;
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
@@ -97,69 +89,6 @@ const MatchChatPage = () => {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Wait for match to be loaded
|
|
||||||
if (!match) return;
|
|
||||||
|
|
||||||
// Connect to Socket.IO
|
|
||||||
const socket = connectSocket();
|
|
||||||
|
|
||||||
if (!socket) {
|
|
||||||
console.error('Failed to connect to socket');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to join match room
|
|
||||||
const joinMatchRoom = () => {
|
|
||||||
setIsConnected(true);
|
|
||||||
socket.emit('join_match_room', { matchId: match.id });
|
|
||||||
console.log(`Joined match room ${match.id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Socket event listeners
|
|
||||||
socket.on('connect', joinMatchRoom);
|
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
setIsConnected(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Receive messages
|
|
||||||
socket.on('match_message', (message) => {
|
|
||||||
setMessages((prev) => [...prev, message]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Join immediately if already connected
|
|
||||||
if (socket.connected) {
|
|
||||||
joinMatchRoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
|
||||||
socket.off('connect', joinMatchRoom);
|
|
||||||
socket.off('disconnect');
|
|
||||||
socket.off('match_message');
|
|
||||||
};
|
|
||||||
}, [match, user.id]);
|
|
||||||
|
|
||||||
const handleSendMessage = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!newMessage.trim() || !match) return;
|
|
||||||
|
|
||||||
const socket = getSocket();
|
|
||||||
if (!socket || !socket.connected) {
|
|
||||||
alert('Not connected to chat server');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send message via Socket.IO using numeric match ID
|
|
||||||
socket.emit('send_match_message', {
|
|
||||||
matchId: match.id,
|
|
||||||
content: newMessage,
|
|
||||||
});
|
|
||||||
|
|
||||||
setNewMessage('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileSelect = (e) => {
|
const handleFileSelect = (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (file && file.type.startsWith('video/')) {
|
if (file && file.type.startsWith('video/')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user