2025-11-12 17:50:44 +01:00
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
|
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
|
|
|
import Layout from '../components/layout/Layout';
|
|
|
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
|
|
|
import { mockUsers } from '../mocks/users';
|
|
|
|
|
import { Send, Video, Upload, X, Check, Link as LinkIcon } from 'lucide-react';
|
2025-11-12 22:42:15 +01:00
|
|
|
import { connectSocket, getSocket } from '../services/socket';
|
2025-11-12 17:50:44 +01:00
|
|
|
|
|
|
|
|
const MatchChatPage = () => {
|
|
|
|
|
const { matchId } = useParams();
|
|
|
|
|
const { user } = useAuth();
|
|
|
|
|
const navigate = useNavigate();
|
2025-11-12 22:42:15 +01:00
|
|
|
const [messages, setMessages] = useState([]);
|
2025-11-12 17:50:44 +01:00
|
|
|
const [newMessage, setNewMessage] = useState('');
|
|
|
|
|
const [selectedFile, setSelectedFile] = useState(null);
|
|
|
|
|
const [isTransferring, setIsTransferring] = useState(false);
|
|
|
|
|
const [transferProgress, setTransferProgress] = useState(0);
|
|
|
|
|
const [webrtcStatus, setWebrtcStatus] = useState('disconnected'); // disconnected, connecting, connected, failed
|
|
|
|
|
const [showLinkInput, setShowLinkInput] = useState(false);
|
|
|
|
|
const [videoLink, setVideoLink] = useState('');
|
2025-11-12 22:42:15 +01:00
|
|
|
const [isConnected, setIsConnected] = useState(false);
|
2025-11-12 17:50:44 +01:00
|
|
|
const messagesEndRef = useRef(null);
|
|
|
|
|
const fileInputRef = useRef(null);
|
|
|
|
|
|
2025-11-12 22:42:15 +01:00
|
|
|
// Partner user (mockup - TODO: fetch from backend in Phase 2)
|
2025-11-12 17:50:44 +01:00
|
|
|
const partner = mockUsers[1]; // sarah_swing
|
|
|
|
|
|
|
|
|
|
const scrollToBottom = () => {
|
|
|
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
}, [messages]);
|
|
|
|
|
|
2025-11-12 22:42:15 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
// 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 match room
|
|
|
|
|
socket.emit('join_match_room', { matchId: parseInt(matchId) });
|
|
|
|
|
console.log(`Joined match room ${matchId}`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on('disconnect', () => {
|
|
|
|
|
setIsConnected(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Receive messages
|
|
|
|
|
socket.on('match_message', (message) => {
|
|
|
|
|
setMessages((prev) => [...prev, message]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Cleanup
|
|
|
|
|
return () => {
|
|
|
|
|
socket.off('connect');
|
|
|
|
|
socket.off('disconnect');
|
|
|
|
|
socket.off('match_message');
|
|
|
|
|
};
|
|
|
|
|
}, [matchId, user.id]);
|
|
|
|
|
|
2025-11-12 17:50:44 +01:00
|
|
|
const handleSendMessage = (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (!newMessage.trim()) return;
|
|
|
|
|
|
2025-11-12 22:42:15 +01:00
|
|
|
const socket = getSocket();
|
|
|
|
|
if (!socket || !socket.connected) {
|
|
|
|
|
alert('Not connected to chat server');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send message via Socket.IO
|
|
|
|
|
socket.emit('send_match_message', {
|
|
|
|
|
matchId: parseInt(matchId),
|
2025-11-12 17:50:44 +01:00
|
|
|
content: newMessage,
|
2025-11-12 22:42:15 +01:00
|
|
|
});
|
2025-11-12 17:50:44 +01:00
|
|
|
|
|
|
|
|
setNewMessage('');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFileSelect = (e) => {
|
|
|
|
|
const file = e.target.files[0];
|
|
|
|
|
if (file && file.type.startsWith('video/')) {
|
|
|
|
|
setSelectedFile(file);
|
|
|
|
|
} else {
|
|
|
|
|
alert('Proszę wybrać plik wideo');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const simulateWebRTCConnection = () => {
|
|
|
|
|
setWebrtcStatus('connecting');
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setWebrtcStatus('connected');
|
|
|
|
|
}, 1500);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleStartTransfer = () => {
|
|
|
|
|
if (!selectedFile) return;
|
|
|
|
|
|
|
|
|
|
// Simulate WebRTC connection
|
|
|
|
|
simulateWebRTCConnection();
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setIsTransferring(true);
|
|
|
|
|
setTransferProgress(0);
|
|
|
|
|
|
|
|
|
|
// Simulate transfer progress
|
|
|
|
|
const interval = setInterval(() => {
|
|
|
|
|
setTransferProgress((prev) => {
|
|
|
|
|
if (prev >= 100) {
|
|
|
|
|
clearInterval(interval);
|
|
|
|
|
setIsTransferring(false);
|
|
|
|
|
setSelectedFile(null);
|
|
|
|
|
setWebrtcStatus('disconnected');
|
|
|
|
|
|
|
|
|
|
// Add message about completed transfer
|
|
|
|
|
const message = {
|
|
|
|
|
id: messages.length + 1,
|
|
|
|
|
room_id: 10,
|
|
|
|
|
user_id: user.id,
|
|
|
|
|
username: user.username,
|
|
|
|
|
content: `📹 Video sent: ${selectedFile.name} (${(selectedFile.size / 1024 / 1024).toFixed(2)} MB)`,
|
|
|
|
|
type: 'video',
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
setMessages((prev) => [...prev, message]);
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return prev + 5;
|
|
|
|
|
});
|
|
|
|
|
}, 200);
|
|
|
|
|
}, 2000);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCancelTransfer = () => {
|
|
|
|
|
setIsTransferring(false);
|
|
|
|
|
setTransferProgress(0);
|
|
|
|
|
setSelectedFile(null);
|
|
|
|
|
setWebrtcStatus('disconnected');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSendLink = (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (!videoLink.trim()) return;
|
|
|
|
|
|
|
|
|
|
const message = {
|
|
|
|
|
id: messages.length + 1,
|
|
|
|
|
room_id: 10,
|
|
|
|
|
user_id: user.id,
|
|
|
|
|
username: user.username,
|
|
|
|
|
content: `🔗 Video link: ${videoLink}`,
|
|
|
|
|
type: 'link',
|
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setMessages([...messages, message]);
|
|
|
|
|
setVideoLink('');
|
|
|
|
|
setShowLinkInput(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleEndMatch = () => {
|
|
|
|
|
navigate(`/matches/${matchId}/rate`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getWebRTCStatusColor = () => {
|
|
|
|
|
switch (webrtcStatus) {
|
|
|
|
|
case 'connected':
|
|
|
|
|
return 'text-green-600';
|
|
|
|
|
case 'connecting':
|
|
|
|
|
return 'text-yellow-600';
|
|
|
|
|
case 'failed':
|
|
|
|
|
return 'text-red-600';
|
|
|
|
|
default:
|
|
|
|
|
return 'text-gray-400';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getWebRTCStatusText = () => {
|
|
|
|
|
switch (webrtcStatus) {
|
|
|
|
|
case 'connected':
|
|
|
|
|
return 'Connected (P2P)';
|
|
|
|
|
case 'connecting':
|
|
|
|
|
return 'Connecting...';
|
|
|
|
|
case 'failed':
|
|
|
|
|
return 'Connection failed';
|
|
|
|
|
default:
|
|
|
|
|
return 'Disconnected';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Layout>
|
|
|
|
|
<div className="max-w-4xl mx-auto">
|
|
|
|
|
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
|
|
|
|
{/* Header with Partner Info */}
|
|
|
|
|
<div className="bg-primary-600 text-white p-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center space-x-4">
|
|
|
|
|
<img
|
|
|
|
|
src={partner.avatar}
|
|
|
|
|
alt={partner.username}
|
|
|
|
|
className="w-12 h-12 rounded-full border-2 border-white"
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 className="text-xl font-bold">{partner.username}</h2>
|
|
|
|
|
<p className="text-sm text-primary-100">⭐ {partner.rating} • {partner.matches_count} collaborations</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleEndMatch}
|
|
|
|
|
className="px-4 py-2 bg-white text-primary-600 rounded-md hover:bg-primary-50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
End & rate
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* WebRTC Status Bar */}
|
|
|
|
|
<div className="bg-gray-50 border-b px-4 py-2 flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<div className={`w-2 h-2 rounded-full ${webrtcStatus === 'connected' ? 'bg-green-500' : 'bg-gray-300'}`} />
|
|
|
|
|
<span className={`text-sm font-medium ${getWebRTCStatusColor()}`}>
|
|
|
|
|
{getWebRTCStatusText()}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-xs text-gray-500">
|
|
|
|
|
{webrtcStatus === 'connected' ? '🔒 E2E Encrypted (DTLS/SRTP)' : 'WebRTC ready to connect'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col h-[calc(100vh-320px)]">
|
|
|
|
|
{/* Messages */}
|
|
|
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
2025-11-12 22:42:15 +01:00
|
|
|
{messages.length === 0 && (
|
|
|
|
|
<div className="text-center text-gray-500 py-8">
|
|
|
|
|
No messages yet. Start the conversation!
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-11-12 17:50:44 +01:00
|
|
|
{messages.map((message) => {
|
2025-11-12 22:42:15 +01:00
|
|
|
const isOwnMessage = message.userId === user.id;
|
2025-11-12 17:50:44 +01:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={message.id}
|
|
|
|
|
className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`}
|
|
|
|
|
>
|
|
|
|
|
<div className={`flex items-start space-x-2 max-w-md ${isOwnMessage ? 'flex-row-reverse space-x-reverse' : ''}`}>
|
|
|
|
|
<img
|
2025-11-12 22:42:15 +01:00
|
|
|
src={message.avatar}
|
2025-11-12 17:50:44 +01:00
|
|
|
alt={message.username}
|
|
|
|
|
className="w-8 h-8 rounded-full"
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-baseline space-x-2 mb-1">
|
|
|
|
|
<span className="text-sm font-medium text-gray-900">
|
|
|
|
|
{message.username}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-xs text-gray-500">
|
2025-11-12 22:42:15 +01:00
|
|
|
{new Date(message.createdAt).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
2025-11-12 17:50:44 +01:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className={`rounded-lg px-4 py-2 ${
|
|
|
|
|
isOwnMessage
|
|
|
|
|
? 'bg-primary-600 text-white'
|
|
|
|
|
: 'bg-gray-100 text-gray-900'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{message.content}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
<div ref={messagesEndRef} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Video Transfer Section */}
|
|
|
|
|
{(selectedFile || isTransferring) && (
|
|
|
|
|
<div className="border-t bg-blue-50 p-4">
|
|
|
|
|
<div className="bg-white rounded-lg p-4 shadow-sm">
|
|
|
|
|
<div className="flex items-center justify-between mb-3">
|
|
|
|
|
<div className="flex items-center space-x-3">
|
|
|
|
|
<Video className="w-6 h-6 text-primary-600" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium text-gray-900">{selectedFile?.name}</p>
|
|
|
|
|
<p className="text-sm text-gray-500">
|
|
|
|
|
{selectedFile && `${(selectedFile.size / 1024 / 1024).toFixed(2)} MB`}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{!isTransferring && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setSelectedFile(null)}
|
|
|
|
|
className="text-gray-400 hover:text-gray-600"
|
|
|
|
|
>
|
|
|
|
|
<X className="w-5 h-5" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{isTransferring ? (
|
|
|
|
|
<>
|
|
|
|
|
<div className="mb-2">
|
|
|
|
|
<div className="flex justify-between text-sm text-gray-600 mb-1">
|
|
|
|
|
<span>Transferring via WebRTC...</span>
|
|
|
|
|
<span>{transferProgress}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
|
|
|
<div
|
|
|
|
|
className="bg-primary-600 h-2 rounded-full transition-all duration-200"
|
|
|
|
|
style={{ width: `${transferProgress}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleCancelTransfer}
|
|
|
|
|
className="w-full px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleStartTransfer}
|
|
|
|
|
className="w-full px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors flex items-center justify-center space-x-2"
|
|
|
|
|
>
|
|
|
|
|
<Upload className="w-4 h-4" />
|
|
|
|
|
<span>Send video (P2P)</span>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Link Input Section */}
|
|
|
|
|
{showLinkInput && (
|
|
|
|
|
<div className="border-t bg-yellow-50 p-4">
|
|
|
|
|
<div className="bg-white rounded-lg p-4 shadow-sm">
|
|
|
|
|
<form onSubmit={handleSendLink} className="space-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
Video link (Google Drive, Dropbox, etc.)
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="url"
|
|
|
|
|
value={videoLink}
|
|
|
|
|
onChange={(e) => setVideoLink(e.target.value)}
|
|
|
|
|
placeholder="https://drive.google.com/..."
|
|
|
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex space-x-2">
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Send link
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowLinkInput(false);
|
|
|
|
|
setVideoLink('');
|
|
|
|
|
}}
|
|
|
|
|
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Message Input & Actions */}
|
|
|
|
|
<div className="border-t p-4 bg-gray-50">
|
|
|
|
|
<div className="flex space-x-2 mb-3">
|
|
|
|
|
<input
|
|
|
|
|
type="file"
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
onChange={handleFileSelect}
|
|
|
|
|
accept="video/*"
|
|
|
|
|
className="hidden"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => fileInputRef.current?.click()}
|
|
|
|
|
disabled={isTransferring || selectedFile}
|
|
|
|
|
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
|
|
|
|
|
>
|
|
|
|
|
<Video className="w-4 h-4" />
|
|
|
|
|
<span>Send video (WebRTC)</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowLinkInput(!showLinkInput)}
|
|
|
|
|
disabled={isTransferring}
|
|
|
|
|
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
|
|
|
|
|
>
|
|
|
|
|
<LinkIcon className="w-4 h-4" />
|
|
|
|
|
<span>Link</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSendMessage} className="flex space-x-2">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={newMessage}
|
|
|
|
|
onChange={(e) => setNewMessage(e.target.value)}
|
|
|
|
|
placeholder="Write a message..."
|
2025-11-12 22:42:15 +01:00
|
|
|
disabled={!isConnected}
|
|
|
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50"
|
2025-11-12 17:50:44 +01:00
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
2025-11-12 22:42:15 +01:00
|
|
|
disabled={!isConnected}
|
|
|
|
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50"
|
2025-11-12 17:50:44 +01:00
|
|
|
>
|
|
|
|
|
<Send className="w-5 h-5" />
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Info Box */}
|
|
|
|
|
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
|
|
|
<p className="text-sm text-blue-800">
|
|
|
|
|
<strong>🚀 WebRTC P2P Functionality Mockup:</strong> In the full version, videos will be transferred directly
|
|
|
|
|
between users via RTCDataChannel, with chunking and progress monitoring.
|
|
|
|
|
The server is only used for SDP/ICE exchange (signaling).
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Layout>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default MatchChatPage;
|