import { useState, useEffect, useRef } from 'react';
import io from 'socket.io-client';
import CodeDisplay from './components/CodeDisplay';
import FileList from './components/FileList';
import TextShare from './components/TextShare';
import './App.css';
//const SERVER_URL = import.meta.env.REACT_APP_SERVER_URL || 'http://localhost:3000';
//const SERVER_URL = ''; // prod
const SERVER_URL = 'http://10.0.0.64:3000'; // dev
const socket = io((SERVER_URL=="")?"/":SERVER_URL, { withCredentials: true });
//const socket = io('/', { path: '/socket.io', autoConnect: true, });
function App() {
const [showApkLink, setShowApkLink] = useState(true);
const [code, setCode] = useState('');
const [joinCode, setJoinCode] = useState('');
const [copied, setCopied] = useState(false);
const [isRoomCreator, setIsRoomCreator] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const [useFallback, setUseFallback] = useState(false);
const [countdown, setCountdown] = useState(600);
const [textHistory, setTextHistory] = useState([]); // Accumulative text
const [error, setError] = useState(''); // For UI error messages
const [dataChannelsState, setDataChannelsState] = useState({}); // Force re-render
const [files, setFiles] = useState([]);
const [downloadStates, setDownloadStates] = useState({}); // { fileName: { status: 'idle'|'downloading'|'completed', progress: number } }
const [users, setUsers] = useState([]);
const [downloadCounts, setDownloadCounts] = useState({});
const [reconTrig, setReconTrig] = useState(0);
const localFilesRef = useRef({}); // Store local file data
const filesRef = useRef([]); // Store latest files synchronously
const retryCountsRef = useRef({});
const cancelRequestsRef = useRef(new Set()); // Track canceled downloads
const thumbnailRetriesRef = useRef({}); // { [fileName]: number }
const thumbnailRequestsRef = useRef(new Set()); // Track ongoing thumbnail requests: Set(`${fileName}:${peerId}`)
const peerConnectionsRef = useRef({});
const dataChannelsRef = useRef({});
const binaryChannelsRef = useRef({});
const timeoutRefs = useRef({}); // Store timeouts per peer
const transferIdCounterRef = useRef(0);
const binaryStateRef = useRef({}); // { [transferId]: {kind, fileName, chunks: [], expectedSize, received: 0, peerId} }
const downloadersRef = useRef({}); // { fileName: Set(peerIds) }
const textHistoryRef = useRef(textHistory); // New ref to avoid stale closure in onopen
// windoow expose to support blob download in app
window.binaryStateRef = binaryStateRef; // Expose to window
window.localFilesRef = localFilesRef; // Expose ref to window
window.downloadStates = downloadStates; // Expose state to window
// Update window.downloadStates on state changes
useEffect(() => {
window.downloadStates = downloadStates;
}, [downloadStates]);
// New: Sync ref with state changes
useEffect(() => {
textHistoryRef.current = textHistory;
}, [textHistory]);
const protocol = window.location.protocol;
const hostname = window.location.hostname || 'localhost';
const roomCodeOnly = (code || '').replace(/ /g, '_');
const roomLink = `${protocol}//${hostname}/room/${(roomCodeOnly || '')}`;
//const roomLink = `${protocol}://${hostname}/room/${(code || '').replace(/ /g, '_')}`;
const handleCopyLink = async (type) => {
if (!navigator.clipboard) {
console.error('Clipboard API not available. Ensure the app is running in a secure context (https or localhost).');
return;
}
try {
const textToCopy = type === 'link' ? roomLink : roomCodeOnly;
await navigator.clipboard.writeText(textToCopy);
setCopied(type);
setTimeout(() => setCopied(null), 2000); // Reset after 2 seconds
} catch (err) {
console.error(`Failed to copy ${type}:`, err);
}
};
const handleCreateRoom = async () => {
try {
const response = await fetch(`${SERVER_URL}/api/generate-code`, {
method: 'GET',
credentials: 'include',
});
const { code, error } = await response.json();
if (error) {
setError(error);
return;
}
setCode(code);
setIsRoomCreator(true);
setError('');
setUseFallback(false);
socket.emit('create', code, (response) => {
if (response?.error) {
console.error(`Create room failed: ${response.error}`);
setError(`Failed to create room: ${response.error}`);
} else {
console.log(`Successfully created room ${code}`);
}
});
//window.history.pushState({}, '', `/room/${code.replace(/ /g, '_')}`);
} catch (err) {
console.error('Error generating code:', err);
setError('Failed to generate room code');
}
};
const handleJoinRoom = (joinCode) => {
if (!joinCode) {
setError('Please enter a room code');
return;
}
const fixCode = joinCode.replace(/_/g, ' ');
setCode(fixCode);
setError('');
setUseFallback(false);
socket.emit('join', fixCode, (response) => {
if (!response || response.error) {
const errorMsg = response?.error || 'Failed to join room';
if(errorMsg.includes('Already in room')) {
// custom handle
setFiles([]);
setError('');
setReconTrig(prevCount => prevCount + 1);
console.error(`Re-enter room: ${errorMsg}`);
} else {
console.error(`Join room failed: ${errorMsg}`);
if (retryCount < 3) {
console.log(`Retrying join room ${fixCode}, attempt ${retryCount + 1}/3`);
setTimeout(() => handleJoinRoom(joinCode, retryCount + 1), 2000);
} else {
clearWebRTCState();
setError(`Failed to join room after retries: ${errorMsg}`);
window.history.pushState({}, '', '/');
}
}
} else {
console.log(`Successfully joined room ${fixCode}`);
setError('');
}
});
//window.history.pushState({}, '', `/room/${joinCode.replace(/ /g, '_')}`);
};
const handleLeaveRoom = () => {
// Notify server and peers of leaving
socket.emit('leave', code, (response) => {
if (response?.error) {
console.error(`Leave room failed: ${response.error}`);
setError(`Failed to leave room: ${response.error}`);
clearWebRTCState();
setFiles([]);
setCode('');
setJoinCode('');
localFilesRef.current = {};
filesRef.current = [];
window.history.pushState({}, '', '/');
} else {
console.log(`Successfully left room ${code}: ${response.message}`);
// Clear local state
clearWebRTCState();
// Reset file list for this client only (don't broadcast remove-file)
setFiles([]);
setCode('');
setJoinCode('');
localFilesRef.current = {};
filesRef.current = [];
window.history.pushState({}, '', '/');
}
});
};
const clearWebRTCState = () => {
setCode('');
//setJoinCode(''); // only clear on error and leave
setIsRoomCreator(false);
setIsConnected(false);
setUseFallback(false);
setCountdown(600);
setFiles([]);
setTextHistory([]);
setError('');
setDataChannelsState({});
setUsers([]);
setDownloadCounts({});
localFilesRef.current = {};
Object.values(peerConnectionsRef.current).forEach((pc) => {
pc.onicecandidate = null;
pc.oniceconnectionstatechange = null;
pc.onicecandidateerror = null;
pc.ondatachannel = null;
pc.onnegotiationneeded = null;
pc.close();
});
Object.values(dataChannelsRef.current).forEach((ch) => {
if (ch.readyState === 'open') ch.close();
});
Object.values(binaryChannelsRef.current).forEach((ch) => {
if (ch.readyState === 'open') ch.close();
});
peerConnectionsRef.current = {};
dataChannelsRef.current = {};
binaryChannelsRef.current = {};
Object.keys(timeoutRefs.current).forEach((peerId) => clearTimeout(timeoutRefs.current[peerId]));
timeoutRefs.current = {};
retryCountsRef.current = {};
cancelRequestsRef.current = new Set();
thumbnailRetriesRef.current = {};
thumbnailRequestsRef.current = new Set();
binaryStateRef.current = {};
downloadersRef.current = {};
};
// WebRTC setup
const setupWebRTC = (peerId, roomCode, isOfferer = false) => {
if (!peerId || peerId === socket.id || peerId === 'true') {
console.log(`Skipping WebRTC setup for invalid peerId: ${peerId}`);
return;
}
if (useFallback) {
console.log(`Skipping WebRTC setup for ${peerId} due to fallback mode`);
return;
}
if (peerConnectionsRef.current[peerId]) {
console.log(`WebRTC connection already exists for ${peerId}, skipping setup`);
return;
}
console.log(`Setting up WebRTC with peer ${peerId} (${isOfferer ? 'offerer' : 'answerer'}, attempt ${retryCountsRef.current[peerId] || 1}/3)`);
const pc = new RTCPeerConnection({
iceServers: [ // more than 5=issues, more than 2=slows discovery
//{ urls: 'turn:openrelay.metered.ca:80', username: 'openrelay.project', credential: 'openrelayproject' },
//{ urls: 'turn:openrelay.metered.ca:443', username: 'openrelay.project', credential: 'openrelayproject' },
{ urls: 'turn:twily.info:5349', username:'ana', credential:'butt' },
{ urls: 'stun:stun.l.google.com:19302' },
//{ urls: 'stun:stun1.l.google.com:19302' },
//{ urls: 'stun:stun2.l.google.com:19302' },
//{ urls: 'stun:stun3.l.google.com:19302' },
//{ urls: 'stun:stun4.l.google.com:19302' },
],
});
retryCountsRef.current[peerId] = (retryCountsRef.current[peerId] || 0) + 1;
peerConnectionsRef.current[peerId] = pc;
pc.ondatachannel = (event) => { // answerer
const channel = event.channel;
if (channel.label === 'file-share') {
dataChannelsRef.current[peerId] = channel;
channel.onopen = () => {
console.log(`Main DataChannel opened with peer ${peerId}`);
setIsConnected(true);
setCountdown(0);
Object.keys(timeoutRefs.current).forEach((pId) => {
console.log(`Clearing timeout for ${pId}`);
clearTimeout(timeoutRefs.current[pId]);
});
timeoutRefs.current = {};
retryCountsRef.current[peerId] = 0;
if (dataChannelsRef.current['true']) {
delete dataChannelsRef.current['true'];
}
setDataChannelsState({ ...dataChannelsRef.current });
// Share full file list with new peer
const fileList = filesRef.current.map((file) => ({
name: file.name,
size: file.size,
peerId: file.peerId,
thumbnail: !!localFilesRef.current[`${file.name}_thumbnail`],
}));
channel.send(JSON.stringify({
type: 'file-list',
files: fileList,
}));
// Request thumbnails for all files not owned by this peer
fileList.forEach((file) => {
const requestKey = `${file.name}:${file.peerId}`;
if (file.thumbnail && file.peerId !== socket.id && !localFilesRef.current[`${file.name}_thumbnail`] && !thumbnailRequestsRef.current.has(requestKey)) {
requestThumbnail(file);
}
});
if (isRoomCreator) {
setTimeout(() => {
if (channel.readyState === 'open') {
channel.send(JSON.stringify({ type: 'text-history', history: textHistoryRef.current })); // Use ref for latest value
console.log(`Sent text-history to ${peerId}:`, textHistoryRef.current);
} else {
console.warn(`Main channel not open for ${peerId}, skipping text-history send`);
}
}, 1000);
}
};
channel.onmessage = (event) => handleMessage(peerId, event);
channel.onclose = () => {
console.log(`Main DataChannel closed with peer ${peerId}`);
delete peerConnectionsRef.current[peerId];
setDataChannelsState({ ...dataChannelsRef.current });
};
channel.onerror = (err) => {
console.error(`Main DataChannel error with ${peerId}:`, err);
};
} else if (channel.label === 'binary-transfer') {
channel.binaryType = 'arraybuffer';
binaryChannelsRef.current[peerId] = channel;
channel.onmessage = (event) => handleBinaryMessage(peerId, event);
channel.onclose = () => {
console.log(`Binary channel closed with peer ${peerId}`);
};
channel.onerror = (err) => {
console.error(`Binary channel error with ${peerId}:`, err);
};
}
};
let mainChannel = null;
let binaryChannel = null;
if (isOfferer) {
mainChannel = pc.createDataChannel('file-share');
dataChannelsRef.current[peerId] = mainChannel;
mainChannel.onopen = () => {
console.log(`Main DataChannel opened with peer ${peerId} (joiner)`);
setIsConnected(true);
setCountdown(0);
Object.keys(timeoutRefs.current).forEach((pId) => {
console.log(`Clearing timeout for ${pId}`);
clearTimeout(timeoutRefs.current[pId]);
});
timeoutRefs.current = {};
retryCountsRef.current[peerId] = 0;
if (dataChannelsRef.current['true']) {
delete dataChannelsRef.current['true'];
}
setDataChannelsState({ ...dataChannelsRef.current });
// Share full file list with new peer
const fileList = filesRef.current.map((file) => ({
name: file.name,
size: file.size,
peerId: file.peerId,
thumbnail: !!localFilesRef.current[`${file.name}_thumbnail`],
}));
mainChannel.send(JSON.stringify({
type: 'file-list',
files: fileList,
}));
// Request thumbnails for all files not owned by this peer
fileList.forEach((file) => {
const requestKey = `${file.name}:${file.peerId}`;
if (file.thumbnail && file.peerId !== socket.id && !localFilesRef.current[`${file.name}_thumbnail`] && !thumbnailRequestsRef.current.has(requestKey)) {
requestThumbnail(file);
}
});
if (isRoomCreator) {
setTimeout(() => {
if (mainChannel.readyState === 'open') {
mainChannel.send(JSON.stringify({ type: 'text-history', history: textHistoryRef.current })); // Use ref for latest value
console.log(`Sent text-history to ${peerId}:`, textHistoryRef.current);
} else {
console.warn(`Main channel not open for ${peerId}, skipping text-history send`);
}
}, 1000);
}
};
mainChannel.onmessage = (event) => handleMessage(peerId, event);
mainChannel.onclose = () => {
console.log(`Main DataChannel closed with peer ${peerId}`);
delete peerConnectionsRef.current[peerId];
setDataChannelsState({ ...dataChannelsRef.current });
};
mainChannel.onerror = (err) => {
console.error(`Main DataChannel error with ${peerId}:`, err);
};
binaryChannel = pc.createDataChannel('binary-transfer');
binaryChannel.binaryType = 'arraybuffer';
binaryChannelsRef.current[peerId] = binaryChannel;
binaryChannel.onmessage = (event) => handleBinaryMessage(peerId, event);
binaryChannel.onclose = () => {
console.log(`Binary channel closed with peer ${peerId}`);
};
binaryChannel.onerror = (err) => {
console.error(`Binary channel error with ${peerId}:`, err);
};
}
pc.onicecandidate = (event) => {
if (event.candidate && roomCode) {
socket.emit('ice-candidate', { code: roomCode, candidate: event.candidate, to: peerId, from: socket.id });
console.log(`Sent ICE candidate to ${peerId} from ${socket.id}: ${event.candidate.candidate}`);
}
};
pc.onicecandidateerror = (err) => {
console.error(`ICE candidate error for ${peerId}:`, err);
};
pc.oniceconnectionstatechange = () => {
console.log(`ICE state with ${peerId} (${isRoomCreator ? 'creator' : 'joiner'}): ${pc.iceConnectionState}`);
if(typeof retryCountsRef.current[peerId]!=='undefined') {
if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'disconnected') {
console.log(`WebRTC ${pc.iceConnectionState} with ${peerId}, retrying in 5s (attempt ${retryCountsRef.current[peerId]}/3)`);
setTimeout(() => {
if (pc.iceConnectionState !== 'connected' && !useFallback && retryCountsRef.current[peerId] < 3) {
setupWebRTC(peerId, roomCode, isOfferer);
} else if (pc.iceConnectionState !== 'connected' && !useFallback) {
console.log(`Max retries reached for ${peerId}, switching to fallback`);
setUseFallback(true);
setError('WebRTC connection failed after retries, using server fallback');
setCountdown(0);
setFiles([]);
setTextHistory([]);
if (isRoomCreator) {
socket.emit('fallback', { code: roomCode });
}
}
}, 5000);
} else if (pc.iceConnectionState === 'connected') {
setIsConnected(true);
setUseFallback(false);
setError('');
//setFiles([]);
clearTimeout(timeoutRefs.current[peerId]);
retryCountsRef.current[peerId] = 0;
}
} else {
console.log(`err: peerId ${peerId} not found in retryCountsRef`);
setUseFallback(false);
}
};
// Re-negotiate for data channels change m= tracks in sdp
pc.onnegotiationneeded = async () => {
try {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.emit('offer', { code: roomCode, offer: pc.localDescription, to: peerId, from: socket.id });
console.log(`Sent renegotiation offer to ${peerId} for room ${roomCode}`);
} catch (err) {
console.error(`Negotiation needed error for ${peerId}:`, err);
}
};
if (isOfferer) {
pc.createOffer()
.then((offer) => {
pc.setLocalDescription(offer);
socket.emit('offer', { code: roomCode, offer, to: peerId, from: socket.id });
console.log(`Sent offer to ${peerId} for room ${roomCode}`);
})
.catch((err) => {
console.error(`Error creating offer for ${peerId}:`, err);
setError('Failed to create WebRTC offer');
setUseFallback(true);
setCountdown(0);
setFiles([]);
setTextHistory([]);
if (isRoomCreator) {
socket.emit('fallback', { code: roomCode });
}
});
}
return () => clearTimeout(timeoutRefs.current[peerId]);
};
// Socket event handlers
useEffect(() => {
socket.on('peer-joined', (data) => {
if (!data || !data.peerId || data.peerId === 'true') {
console.error('Invalid peer-joined data:', data);
return;
}
const { peerId } = data;
console.log(`Peer ${peerId} joined room ${code}`);
setupWebRTC(peerId, code, true); // "Other" is always the offerer
});
socket.on('peer-left', (data) => {
const { peerId } = data;
if (peerConnectionsRef.current[peerId]) {
peerConnectionsRef.current[peerId].close();
if (dataChannelsRef.current[peerId]?.readyState === 'open') {
dataChannelsRef.current[peerId].close();
}
if (binaryChannelsRef.current[peerId]?.readyState === 'open') {
binaryChannelsRef.current[peerId].close();
}
delete peerConnectionsRef.current[peerId];
delete dataChannelsRef.current[peerId];
delete binaryChannelsRef.current[peerId];
delete timeoutRefs.current[peerId];
// Clean binary state for this peer
Object.keys(binaryStateRef.current).forEach((tid) => {
if (binaryStateRef.current[tid].peerId === peerId) {
delete binaryStateRef.current[tid];
thumbnailRequestsRef.current.delete(`${binaryStateRef.current[tid]?.fileName}:${peerId}`);
}
});
setDataChannelsState({ ...dataChannelsRef.current });
console.log(`Peer ${peerId} left room`);
setFiles((prev) => {
const remainingFiles = prev.filter((file) => file.peerId !== peerId);
// Broadcast updated file list to remaining peers
Object.values(dataChannelsRef.current).forEach((ch) => {
if (ch.readyState === 'open') {
ch.send(JSON.stringify({
type: 'file-list',
files: remainingFiles.map((file) => ({
name: file.name,
size: file.size,
peerId: file.peerId,
thumbnail: !!localFilesRef.current[`${file.name}_thumbnail`],
})),
}));
}
});
return remainingFiles;
});
// Clean download counts for left peer
let updated = false;
Object.keys(downloadersRef.current).forEach((fileName) => {
if (downloadersRef.current[fileName].delete(peerId)) {
updated = true;
broadcastUpdateCount(fileName, downloadersRef.current[fileName].size);
}
});
if (updated) {
setDownloadCounts((prev) => ({ ...prev })); // Trigger re-render if needed
}
}
if (isRoomCreator && Object.keys(peerConnectionsRef.current).length === 0) {
setIsConnected(false);
// Do not clear files or text here, as creator remains
if (useFallback) {
setUseFallback(false);
setError('');
} else {
setCountdown(600);
}
}
});
socket.on('offer', async ({ code: receivedCode, offer, to, from }) => { // other but first creator only
if (to !== socket.id || useFallback || receivedCode !== code) {
console.log(`Ignoring offer from ${from} (to=${to}, socket.id=${socket.id}, useFallback=${useFallback}, code mismatch: ${receivedCode} vs ${code})`);
return;
}
console.log(`Received offer from ${from} for room ${receivedCode}`);
if (!peerConnectionsRef.current[from]) {
console.log(`No existing connection for ${from}, setting up as answerer`);
setupWebRTC(from, receivedCode, false); // Joiner is answerer
}
try {
const pc = peerConnectionsRef.current[from];
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('answer', { code: receivedCode, answer, to: from, from: socket.id });
console.log(`Sent answer to ${from} from ${socket.id} for room ${receivedCode}`);
} catch (err) {
console.error(`Error handling offer from ${from}:`, err);
setError('Failed to process WebRTC offer');
setUseFallback(true);
setCountdown(0);
setFiles([]);
setTextHistory([]);
if (isRoomCreator) {
socket.emit('fallback', { code: receivedCode });
}
}
});
socket.on('answer', async ({ code: receivedCode, answer, to, from }) => { // peer-joined always answers
if (to !== socket.id || useFallback || receivedCode !== code) {
console.log(`Ignoring answer from ${from} (to=${to}, socket.id=${socket.id}, useFallback=${useFallback}, code mismatch: ${receivedCode} vs ${code})`);
return;
}
console.log(`Received answer from ${from} for room ${receivedCode}`);
if (peerConnectionsRef.current[from]) {
try {
await peerConnectionsRef.current[from].setRemoteDescription(new RTCSessionDescription(answer));
console.log(`Successfully set remote description for answer from ${from}`);
} catch (err) {
console.error(`Error handling answer from ${from}:`, err);
setError('Failed to process WebRTC answer');
setUseFallback(true);
setCountdown(0);
setFiles([]);
setTextHistory([]);
if (isRoomCreator) {
socket.emit('fallback', { code: receivedCode });
}
}
} else {
console.warn(`No peer connection found for ${from} when receiving answer`);
}
});
// textHistory in fallback through room-status-
socket.on('room-status', ({ useFallback, textHistory, files }) => {
if (useFallback) {
console.log(`Received room-status: useFallback=${useFallback}, textHistory=`, textHistory);
setUseFallback(useFallback);
setTextHistory(textHistory || []);
setFiles(files || []);
setIsConnected(true);
setCountdown(0);
setError('Room is in fallback mode');
}
});
socket.on('fallback', () => {
console.log(`Received fallback signal for room ${roomCode}`);
setUseFallback(true);
setError('Room switched to fallback mode');
setCountdown(0);
setFiles([]);
setTextHistory([]);
setDownloadCounts({}); // Clear download counts in fallback mode
Object.values(peerConnectionsRef.current).forEach((pc) => pc.close());
peerConnectionsRef.current = {};
dataChannelsRef.current = {};
binaryChannelsRef.current = {};
Object.keys(timeoutRefs.current).forEach((pId) => clearTimeout(timeoutRefs.current[pId]));
timeoutRefs.current = {};
retryCountsRef.current = {};
thumbnailRequestsRef.current = new Set();
});
socket.on('ice-candidate', async ({ code: receivedCode, candidate, from, to }) => {
if (to !== socket.id || receivedCode !== code) {
console.log(`Ignoring ICE candidate from ${from} (to=${to}, code mismatch: ${receivedCode} vs ${code})`);
return;
}
console.log(`Received ICE candidate from ${from}`);
if (peerConnectionsRef.current[from]) {
try {
await peerConnectionsRef.current[from].addIceCandidate(new RTCIceCandidate(candidate));
console.log(`Added ICE candidate from ${from}`);
} catch (err) {
console.error(`Error adding ICE candidate from ${from}:`, err);
}
} else {
console.warn(`No peer connection found for ${from} when receiving ICE candidate`);
}
});
socket.on('error', (message) => {
clearWebRTCState();
console.log('Server error:', message);
setError(message);
if (message.includes('Invalid code') || message.includes('room not found')) {
window.history.pushState({}, '', '/');
} else if (message.includes('Room closed by')) {
setJoinCode('');
}
});
socket.on('files', (files) => {
console.log('Received files:', files);
setFiles(files);
});
// fallback files are uploaded? direct download?
//socket.on('file', ({ name, path, peerId, size }) => {
// console.log(`Received file via socket: ${name}, uploader: ${peerId}`);
// setFiles((prev) => [...prev, { name, path, peerId, size }]);
//});
// fallback text-history is handled in room-status
//socket.on('text-history', (history) => {
// console.log(`Received text history for room ${code}:`, history);
// setTextHistory(history || []);
//});
socket.on('text', ({ text }) => {
console.log(`Received text via socket for room ${code}:`, text);
setTextHistory((prev) => [...prev, text]);
});
socket.on('file-list', ({ files }) => {
setFiles((prev) => {
const newFiles = files.filter(
(newFile) => !prev.some((file) => file.name === newFile.name && file.peerId === newFile.peerId)
).map((file) => ({ ...file, peerId: file.peerId || 'unknown' }));
newFiles.forEach((file) => {
const requestKey = `${file.name}:${file.peerId}`;
if (file.thumbnail && !localFilesRef.current[`${file.name}_thumbnail`] && !thumbnailRequestsRef.current.has(requestKey)) {
console.log(`Requesting thumbnail for ${file.name} from ${file.peerId}`);
requestThumbnail(file);
} else if (file.thumbnail) {
console.log(`Thumbnail for ${file.name} already cached or requested, skipping request`);
}
});
console.log(`Received file-list from server:`, files);
return [...prev, ...newFiles];
});
});
// New: Handle fallback download count updates from server
socket.on('update-count-fallback', ({ fileName, count }) => {
setDownloadCounts((prev) => ({ ...prev, [fileName]: count }));
});
// New: Handle room status check response
socket.on('room-status-check', ({ exists, isInRoom }) => {
if (!exists) {
clearWebRTCState();
setError('Room closed');
setJoinCode('');
console.log(`Room ${code} closed`);
} else if (!isInRoom) {
console.log(`Rejoining room ${code}`);
handleJoinRoom(code); // Auto-rejoin if room exists but user not in it
}
});
return () => {
socket.off('peer-joined');
socket.off('offer');
socket.off('answer');
socket.off('room-status');
socket.off('fallback');
socket.off('peer-left');
socket.off('ice-candidate');
socket.off('error');
socket.off('files');
//socket.off('file');
socket.off('text');
//socket.off('text-history');
socket.off('file-list');
socket.off('update-count-fallback');
socket.off('room-status-check');
};
}, [code, isRoomCreator, useFallback]); // Depend on code to ensure handlers update with room
// New: Check room status on socket reconnect
useEffect(() => {
const handleReconnect = () => {
if (code) {
socket.emit('check-room-status', code);
console.log(`Reconnected, checking room status for ${code}`);
}
};
socket.io.on('reconnect', handleReconnect);
return () => {
socket.io.off('reconnect', handleReconnect);
};
}, [code]);
// New: Reconnect and check on visible if disconnected (no force disconnect on hidden)
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && code) {
if (socket.disconnected) {
socket.connect();
console.log('Tab visible, reconnecting socket');
}
socket.emit('check-room-status', code);
console.log(`Tab visible, checking room status for ${code}`);
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [code]);
// Countdown for room creator
useEffect(() => {
if (isRoomCreator && countdown > 0 && !isConnected && !useFallback) {
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearWebRTCState();
setError('No one joined the room');
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}
}, [isRoomCreator, countdown, isConnected, useFallback, code]);
// Sync filesRef with files state
useEffect(() => {
filesRef.current = files;
console.log('Updated filesRef:', filesRef.current);
}, [files]);
// Update users list
useEffect(() => {
const peerIds = Object.keys(peerConnectionsRef.current);
setUsers([socket.id, ...peerIds].sort());
}, [dataChannelsState, reconTrig]);
// Auto join from URL
useEffect(() => {
if (!code) {
const path = window.location.pathname;
if (path.startsWith('/room/')) {
const urlCode = path.slice(6).replace(/_/g, ' ');
handleJoinRoom(urlCode);
}
}
}, []);
// disabled for now
//useEffect(() => {
// const path = window.location.pathname;
// setShowApkLink(!path.startsWith('/app/'));
//}, []);
const requestThumbnail = (file) => {
const requestKey = `${file.name}:${file.peerId}`;
if (file.thumbnail && file.peerId !== socket.id && !useFallback) {
const retries = (thumbnailRetriesRef.current[file.name] || 0) + 1;
if (retries > 3) {
console.error(`Max retries reached for thumbnail ${file.name} from ${file.peerId}`);
return;
}
if (thumbnailRequestsRef.current.has(requestKey)) {
console.log(`Thumbnail request for ${file.name} from ${file.peerId} already in progress, skipping`);
return;
}
const channel = dataChannelsRef.current[file.peerId];
if (channel?.readyState === 'open') {
thumbnailRetriesRef.current[file.name] = retries;
thumbnailRequestsRef.current.add(requestKey);
channel.send(JSON.stringify({ type: 'request-thumbnail', fileName: file.name }));
console.log(`Requested thumbnail for ${file.name} from ${file.peerId} via WebRTC (retry ${retries})`);
} else {
console.warn(`Data channel not ready for ${file.peerId}, retrying in 1s`);
setTimeout(() => {
if (!thumbnailRequestsRef.current.has(requestKey)) {
requestThumbnail(file);
}
}, 1000);
}
} else {
console.error(`Cannot request thumbnail for ${file.name}:`, {
isThumbnail: file.thumbnail,
isLocal: file.peerId === socket.id,
useFallback,
channelOpen: dataChannelsRef.current[file.peerId]?.readyState,
});
}
};
const sendBinaryData = async (peerId, kind, fileName, data) => {
const binaryChannel = binaryChannelsRef.current[peerId];
if (!binaryChannel || binaryChannel.readyState !== 'open') {
console.error(`Binary channel not open for ${peerId}`);
return;
}
if (kind === 'thumbnail' && data.size > 100 * 1024) {
console.warn(`Thumbnail ${fileName} size ${data.size} exceeds 100KB limit, skipping`);
return;
}
const transferId = transferIdCounterRef.current++;
console.log(`Sending ${kind} ${fileName} to ${peerId}, size: ${data.size}`);
binaryChannel.send(JSON.stringify({ type: 'start', transferId, kind, fileName, size: data.size }));
const reader = new FileReader();
reader.onload = () => {
const buffer = reader.result;
const fileNameBytes = new TextEncoder().encode(fileName);
const prefixLength = 4 + 4 + fileNameBytes.length; // 4 bytes for transferId, 4 for fileName length
const prefixed = new ArrayBuffer(prefixLength + buffer.byteLength);
const view = new DataView(prefixed);
view.setUint32(0, transferId);
view.setUint32(4, fileNameBytes.length);
new Uint8Array(prefixed).set(fileNameBytes, 8);
new Uint8Array(prefixed).set(new Uint8Array(buffer), prefixLength);
binaryChannel.send(prefixed);
binaryChannel.send(JSON.stringify({ type: 'end', transferId }));
};
reader.onerror = (err) => {
console.error(`Error reading ${kind} ${fileName} for ${peerId}:`, err);
};
reader.readAsArrayBuffer(data);
};
const sendFileChunked = async (peerId, fileName, file) => {
if (!fileName) {
console.error(`No fileName provided for sendFileChunked to ${peerId}`);
return;
}
const binaryChannel = binaryChannelsRef.current[peerId];
if (!binaryChannel || binaryChannel.readyState !== 'open') {
console.error(`Binary channel not open for ${peerId}`);
return;
}
if (cancelRequestsRef.current.has(fileName)) {
cancelRequestsRef.current.delete(fileName);
console.log(`Cleared previous cancel for ${fileName}, starting new send to ${peerId}`);
}
const transferId = transferIdCounterRef.current++;
console.log(`Sending start message for file ${fileName} to ${peerId}, transferId: ${transferId}, size: ${file.size}`);
binaryChannel.send(JSON.stringify({ type: 'start', transferId, kind: 'file', fileName, size: file.size }));
// drain to avoid 18MB transfer limit, large file support
const HIGH_WATER_MARK = 8 * 1024 * 1024; // 8MB
const LOW_WATER_MARK = 4 * 1024 * 1024; // 4MB
binaryChannel.bufferedAmountLowThreshold = LOW_WATER_MARK;
let drainResolver;
binaryChannel.onbufferedamountlow = () => {
if (drainResolver) {
drainResolver();
drainResolver = null;
}
};
const waitForDrain = () => {
if (binaryChannel.bufferedAmount <= HIGH_WATER_MARK) {
return Promise.resolve();
}
return new Promise((resolve) => {
drainResolver = resolve;
});
};
await waitForDrain();
const chunkSize = 16384;
let offset = 0;
console.log(`--- offset : ${offset} --- file.size : ${file.size}`);
while (offset < file.size) {
if (cancelRequestsRef.current.has(fileName)) {
console.log(`Canceled sending ${fileName} to ${peerId}`);
binaryChannel.send(JSON.stringify({ type: 'cancel', transferId }));
return;
}
const slice = file.slice(offset, offset + chunkSize);
console.log(`--- slice.size : ${slice.size}`);
if (slice.size === 0) {
break;
}
try {
const chunk = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = reject;
reader.readAsArrayBuffer(slice);
});
if (cancelRequestsRef.current.has(fileName)) {
console.log(`Canceled sending ${fileName} to ${peerId} during read`);
binaryChannel.send(JSON.stringify({ type: 'cancel', transferId }));
return;
}
// Prefix chunk with transferId and fileName
const fileNameBytes = new TextEncoder().encode(fileName);
const prefixLength = 4 + 4 + fileNameBytes.length; // 4 bytes for transferId, 4 for fileName length
const prefixed = new ArrayBuffer(prefixLength + chunk.byteLength);
const view = new DataView(prefixed);
view.setUint32(0, transferId);
view.setUint32(4, fileNameBytes.length);
new Uint8Array(prefixed).set(fileNameBytes, 8);
new Uint8Array(prefixed).set(new Uint8Array(chunk), prefixLength);
binaryChannel.send(prefixed);
await waitForDrain();
offset += chunk.byteLength;
} catch (err) {
console.error(`Error reading ${fileName}:`, err);
binaryChannel.send(JSON.stringify({ type: 'cancel', transferId }));
return;
}
}
binaryChannel.send(JSON.stringify({ type: 'end', transferId }));
cancelRequestsRef.current.delete(fileName);
console.log(`Finished sending ${fileName} to ${peerId}`);
};
const cancelDownload = (fileName, peerId) => {
if (!cancelRequestsRef.current.has(fileName)) {
cancelRequestsRef.current.add(fileName);
const mainCh = dataChannelsRef.current[peerId];
if (mainCh && mainCh.readyState === 'open') {
mainCh.send(JSON.stringify({ type: 'end-download', fileName }));
mainCh.send(JSON.stringify({ type: 'cancel-download', fileName }));
console.log(`Sent end-download and cancel-download for ${fileName} to ${peerId}`);
}
setDownloadStates((prev) => {
const wasSaved = prev[fileName]?.status === 'saved';
const { [fileName]: _, ...rest } = prev;
return wasSaved ? { ...rest, [fileName]: { status: 'saved', progress: 100, peerId } } : rest;
});
// Cancel any ongoing binary transfers for this file
Object.keys(binaryStateRef.current).forEach((tid) => {
const state = binaryStateRef.current[tid];
if (state.fileName === fileName && state.peerId === peerId) {
delete binaryStateRef.current[tid];
thumbnailRequestsRef.current.delete(`${fileName}:${peerId}`);
}
});
console.log(`Canceled download for ${fileName} locally`);
}
};
const handleDeleteFile = (fileName) => {
setFiles((prev) => prev.filter((file) => file.name !== fileName));
delete localFilesRef.current[fileName];
delete localFilesRef.current[`${fileName}_thumbnail`];
Object.values(dataChannelsRef.current).forEach((ch) => {
if (ch.readyState === 'open') {
ch.send(JSON.stringify({ type: 'remove-file', fileName }));
}
});
// Clear download counts for deleted file
if (downloadersRef.current[fileName]) {
delete downloadersRef.current[fileName];
setDownloadCounts((prev) => {
const { [fileName]: _, ...rest } = prev;
return rest;
});
broadcastUpdateCount(fileName, 0);
}
console.log(`Deleted file ${fileName} and notified peers`);
};
const broadcastUpdateCount = (fileName, count) => {
setDownloadCounts((prev) => ({ ...prev, [fileName]: count }));
Object.values(dataChannelsRef.current).forEach((ch) => {
if (ch.readyState === 'open') {
ch.send(JSON.stringify({ type: 'update-count', fileName, count }));
}
});
};
const handleMessage = (peerId, event) => {
const message = event.data;
try {
if (typeof message === 'string') {
const data = JSON.parse(message);
if (data.type === 'text') {
setTextHistory((prev) => [...prev, data.text]);
console.log(`Received text via WebRTC from ${peerId}: ${data.text}`);
} else if (data.type === 'text-history') {
console.log(`Received text-history from ${peerId}:`, data.history, `Current textHistory:`, textHistory);
if (Array.isArray(data.history)) {
setTextHistory(data.history);
} else {
console.warn(`Invalid text-history format from ${peerId}:`, data.history);
}
} else if (data.type === 'request-file') {
const file = localFilesRef.current[data.fileName];
if (file) {
console.log(`Handling request-file for ${data.fileName} from ${peerId}`);
// Increment download count when a download starts
handleDownloadCountUpdate(peerId, data.fileName, 'start');
sendFileChunked(peerId, data.fileName, file);
} else {
console.error(`File not found: ${data.fileName}`);
}
} else if (data.type === 'request-thumbnail') {
const thumbnail = localFilesRef.current[`${data.fileName}_thumbnail`];
if (thumbnail) {
console.log(`Handling request-thumbnail for ${data.fileName} from ${peerId}`);
sendBinaryData(peerId, 'thumbnail', data.fileName, thumbnail);
} else {
console.error(`Thumbnail not found for ${data.fileName}`);
}
} else if (data.type === 'end-download') {
handleDownloadCountUpdate(peerId, data.fileName, 'end');
} else if (data.type === 'remove-file') {
setFiles((prev) => prev.filter((file) => file.name !== data.fileName));
console.log(`Removed file ${data.fileName} from list`);
} else if (data.type === 'update-count') {
setDownloadCounts((prev) => ({ ...prev, [data.fileName]: data.count }));
} else if (data.type === 'file-list') {
setFiles((prev) => {
const newFiles = data.files.filter(
(newFile) => !prev.some((file) => file.name === newFile.name && file.peerId === newFile.peerId)
).map((file) => ({ ...file, peerId: file.peerId || 'unknown' })); // Ensure peerId is set
newFiles.forEach((file) => {
const requestKey = `${file.name}:${file.peerId}`;
if (file.thumbnail && !localFilesRef.current[`${file.name}_thumbnail`] && !thumbnailRequestsRef.current.has(requestKey)) {
console.log(`Requesting thumbnail for ${file.name} from ${file.peerId}`);
requestThumbnail(file);
} else if (file.thumbnail) {
console.log(`Thumbnail for ${file.name} already cached or requested, skipping request`);
}
});
console.log(`Received file-list from server:`, data.files);
return [...prev, ...newFiles];
});
}
} else {
console.warn(`Unexpected non-string message on main channel from ${peerId}`);
console.log(message);
}
} catch (err) {
console.error(`Error processing message from ${peerId}:`, err);
}
};
const handleBinaryMessage = (peerId, event) => {
const message = event.data;
//console.log(`Received message from ${peerId}, type: ${typeof message}, size: ${message instanceof Blob ? message.size : message.byteLength || 'N/A'}`);
if (typeof message === 'string') {
try {
const data = JSON.parse(message);
if (data.type === 'start') {
if (data.transferId === undefined || data.transferId === null || !data.fileName || !data.kind || !data.size) {
console.error(`Invalid start message from ${peerId}:`, data);
return;
}
if (binaryStateRef.current[data.transferId]) {
console.warn(`TransferId ${data.transferId} already exists for ${peerId}, ignoring new start for ${data.fileName}`);
return;
}
binaryStateRef.current[data.transferId] = {
kind: data.kind,
fileName: data.fileName,
chunks: [],
expectedSize: data.size,
received: 0,
peerId,
};
console.log(`Started ${data.kind} transfer ${data.transferId} for ${data.fileName} from ${peerId}, expected size: ${data.size}`);
if (data.kind === 'file') {
setDownloadStates((prev) => ({
...prev,
[data.fileName]: { status: 'downloading', progress: 0, total: data.size, received: 0, peerId },
}));
}
} else if (data.type === 'end') {
const state = binaryStateRef.current[data.transferId];
if (!state) {
console.warn(`No state for transferId ${data.transferId} from ${peerId}`);
return;
}
if (state.received >= state.expectedSize) {
const blob = new Blob(state.chunks, { type: state.kind === 'thumbnail' ? 'image/jpeg' : 'application/octet-stream' });
console.log(`Assembled ${state.kind} ${state.fileName}, size: ${blob.size}`);
if (state.kind === 'thumbnail') {
localFilesRef.current[`${state.fileName}_thumbnail`] = blob;
console.log(`Received thumbnail for ${state.fileName} from ${peerId}, size: ${blob.size}`);
setFiles((prev) =>
prev.map((file) =>
file.name === state.fileName && file.peerId !== socket.id
? { ...file, thumbnail: true }
: file
)
);
thumbnailRequestsRef.current.delete(`${state.fileName}:${peerId}`);
} else if (state.kind === 'file') {
localFilesRef.current[state.fileName] = blob;
const url = URL.createObjectURL(blob);
// Store blobUrl in binaryStateRef
binaryStateRef.current[data.transferId].blobUrl = url;
console.log(`Stored blobUrl ${url} for transferId ${data.transferId}, fileName: ${state.fileName}`);
console.log(`binaryStateRef.current keys: `, JSON.stringify(Object.keys(binaryStateRef.current)));
console.log(`binaryStateRef.current[${data.transferId}]: `, JSON.stringify(binaryStateRef.current[data.transferId]));
const link = document.createElement('a');
link.href = url;
link.download = state.fileName;
link.click();
//URL.revokeObjectURL(url);
setTimeout(() => {
URL.revokeObjectURL(url);
console.log(`Revoked blobUrl ${url} for transferId ${data.transferId}`);
// Optionally keep binaryStateRef.current for debugging
// delete binaryStateRef.current[data.transferId]; // Comment out if present
}, 5000); // Delay revoke by 5s
setDownloadStates((prev) => ({
...prev,
[state.fileName]: { status: 'saved', progress: 100, peerId },
}));
const mainCh = dataChannelsRef.current[peerId];
if (mainCh && mainCh.readyState === 'open') {
mainCh.send(JSON.stringify({ type: 'end-download', fileName: state.fileName }));
}
}
} else {
console.warn(`Size mismatch for ${state.kind} ${state.fileName}: received ${state.received}, expected ${state.expectedSize}`);
if (state.kind === 'file') {
setDownloadStates((prev) => ({
...prev,
[state.fileName]: { status: 'error', progress: (state.received / state.expectedSize) * 100 },
}));
}
thumbnailRequestsRef.current.delete(`${state.fileName}:${peerId}`);
}
//--delete binaryStateRef.current[data.transferId];
} else if (data.type === 'cancel') {
const state = binaryStateRef.current[data.transferId];
if (state && state.kind === 'file') {
setDownloadStates((prev) => ({
...prev,
[state.fileName]: { status: 'canceled', progress: 0 },
}));
}
thumbnailRequestsRef.current.delete(`${state.fileName}:${peerId}`);
delete binaryStateRef.current[data.transferId];
}
} catch (err) {
console.error(`Error processing binary control message from ${peerId}:`, err);
}
} else if (message instanceof ArrayBuffer) {
if (message.byteLength < 8) {
console.warn(`Invalid binary message size from ${peerId}: ${message.byteLength} bytes`);
return;
}
const view = new DataView(message);
const transferId = view.getUint32(0);
const fileNameLength = view.getUint32(4);
if (message.byteLength < 8 + fileNameLength) {
console.warn(`Invalid binary message: insufficient length for fileName from ${peerId}`);
return;
}
const fileNameBytes = new Uint8Array(message, 8, fileNameLength);
const fileName = new TextDecoder().decode(fileNameBytes);
const chunk = message.slice(8 + fileNameLength);
const state = binaryStateRef.current[transferId];
if (state && state.fileName === fileName) {
if (cancelRequestsRef.current.has(state.fileName)) {
console.log(`Ignoring chunk for canceled ${state.kind} ${state.fileName}`);
return;
}
state.chunks.push(chunk);
state.received += chunk.byteLength;
//console.log(`Received chunk for ${state.kind} ${state.fileName}, transferId ${transferId}: ${state.received}/${state.expectedSize}`);
if (state.kind === 'file') {
const progress = Math.min(100, (state.received / state.expectedSize) * 100);
setDownloadStates((prev) => ({
...prev,
[state.fileName]: { ...prev[state.fileName], progress, received: state.received },
}));
}
} else {
console.warn(`No state or fileName mismatch for transferId ${transferId} from ${peerId}: expected ${state?.fileName}, got ${fileName}`);
}
} else if (message instanceof Blob) {
console.log(`Received Blob message from ${peerId}, size: ${message.size}`);
console.dir(message);
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
if (arrayBuffer.byteLength < 4) {
console.warn(`Invalid Blob-converted ArrayBuffer size from ${peerId}: ${arrayBuffer.byteLength} bytes`);
return;
}
const view = new DataView(arrayBuffer);
const transferId = view.getUint32(0);
const fileNameLength = view.getUint32(4);
if (arrayBuffer.byteLength < 8 + fileNameLength) {
console.warn(`Invalid Blob-converted ArrayBuffer: insufficient length for fileName from ${peerId}`);
return;
}
const fileNameBytes = new Uint8Array(arrayBuffer, 8, fileNameLength);
const fileName = new TextDecoder().decode(fileNameBytes);
const chunk = arrayBuffer.slice(8 + fileNameLength);
const state = binaryStateRef.current[transferId];
if (state && state.fileName === fileName) {
if (cancelRequestsRef.current.has(state.fileName)) {
console.log(`Ignoring chunk for canceled ${state.kind} ${state.fileName}`);
return;
}
state.chunks.push(chunk);
state.received += chunk.byteLength;
console.log(`Received Blob chunk for ${state.kind} ${state.fileName}, transferId ${transferId}: ${state.received}/${state.expectedSize}`);
if (state.kind === 'file') {
const progress = Math.min(100, (state.received / state.expectedSize) * 100);
setDownloadStates((prev) => ({
...prev,
[state.fileName]: { ...prev[state.fileName], progress, received: state.received },
}));
}
} else {
console.warn(`No state or fileName mismatch for transferId ${transferId} from ${peerId}: expected ${state?.fileName}, got ${fileName}`);
}
};
reader.onerror = (err) => {
console.error(`Error reading Blob from ${peerId}:`, err);
};
reader.readAsArrayBuffer(message);
} else {
console.warn(`Unexpected message type from ${peerId}:`, typeof message);
console.log(message);
}
};
const handleDownloadCountUpdate = (peerId, fileName, action) => {
if (localFilesRef.current[fileName]) { // Only uploader handles
downloadersRef.current[fileName] = downloadersRef.current[fileName] || new Set();
if (action === 'start') {
downloadersRef.current[fileName].add(peerId);
console.log(`Started download of ${fileName} by ${peerId}, count: ${downloadersRef.current[fileName].size}`);
} else if (action === 'end') {
downloadersRef.current[fileName].delete(peerId);
console.log(`Ended download of ${fileName} by ${peerId}, count: ${downloadersRef.current[fileName].size}`);
}
const count = downloadersRef.current[fileName].size;
broadcastUpdateCount(fileName, count);
}
};
return (
<div className="container mx-auto left">
<>
{code && (
<>
{isRoomCreator && countdown > 0 && !isConnected && (
<p>Waiting for peer(s) to connect [WebRTC]... {countdown}s</p>
)}
</>
)}
<h1 className="text-2xl font-bold mb-2 cursor-default">Quick File Share</h1>
{error && <p className="text-red-500 mb-2">{error}</p>}
<CodeDisplay
code={code}
isConnected={isConnected}
onCreateRoom={handleCreateRoom}
onJoinRoom={handleJoinRoom}
joinCode={joinCode}
setJoinCode={setJoinCode}
/>
{/*<div id="webrtc"></div>*/}
</>
{code && (
<>
<p className="text-lg">
{/*{protocol}//{hostname}/room/<strong>{code || 'N/A'}</strong>*/}
{protocol}//{hostname}/room/<strong>{roomCodeOnly || 'N/A'}</strong>
</p>
<button
className="bg-stone-600 text-darkPurple px-4 py-2 rounded hover:bg-stone-500 "
onClick={handleLeaveRoom}
>
{isRoomCreator ? 'Close Room' : 'Leave Room'}
</button>
●
<button
onClick={() => handleCopyLink('link')}
className="bg-stone-600 text-darkPurple px-4 py-2 rounded hover:bg-stone-500"
disabled={!navigator.clipboard}
>
{copied === 'link' ? 'Copied!' : 'Copy Link'}
</button>
<button
onClick={() => handleCopyLink('code')}
className="bg-stone-600 text-darkPurple px-4 py-2 rounded hover:bg-stone-500"
disabled={!navigator.clipboard}
>
{copied === 'code' ? 'Copied!' : 'Copy Code'}
</button>
<div className="mb-4 fixed top-0 right-10 text-right">
<h2 className="text-xl font-bold text-right">Connected Users ({users.length}):</h2>
<ul>
{users
.filter((userId) => userId != null)
.map((userId, index) => (
<li key={userId || `user-${index}`}>
{socket.id && userId === socket.id ? '(You)' : ''} {userId}
</li>
))}
</ul>
</div>
<TextShare
textHistory={textHistory}
setTextHistory={setTextHistory}
dataChannels={dataChannelsState}
useFallback={useFallback}
socket={socket}
code={code}
/>
<FileList
files={files}
setFiles={setFiles}
dataChannels={dataChannelsState}
dataChannelsRef={dataChannelsRef}
useFallback={useFallback}
socket={socket}
code={code}
socketId={socket.id}
localFilesRef={localFilesRef}
downloadStates={downloadStates}
setDownloadStates={setDownloadStates}
cancelDownload={cancelDownload}
cancelRequestsRef={cancelRequestsRef}
downloadCounts={downloadCounts}
handleDeleteFile={handleDeleteFile}
SERVER_URL={SERVER_URL}
/>
{useFallback && <p className="text-red-500">Using server fallback</p>}
</>
)}
{!code && showApkLink && (
/*<a href="/apk/" download id="apk">*/
<a href="/apk/" id="apk">
<button>APK</button>
</a>
)}
</div>
);
}
export default App;
Top