~firefoxreact-appqfsclientsrc
6 itemsDownload ./*

..
assets
components
App.css
App.jsx
index.css
main.jsx


srcApp.jsx
57 KB• 26•  11 hours ago•  DownloadRawClose
11 hours ago•  26

{}
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
©twily.info 2013 - 2025
twily at twily dot info



2 418 765 visits
... ^ v