|
|
import { useState, useEffect, useRef } from 'react'; |
|
|
import io, { Socket } from 'socket.io-client'; |
|
|
import { ClipboardEntryType } from '@/app/[roomCode]/components/ClipboardEntry'; |
|
|
import { FileEntryType } from '@/app/[roomCode]/components/FileEntry'; |
|
|
import { apiUrl, socketUrl } from '../constants'; |
|
|
import { appendTokenToUrl, getAdminToken, withAdminTokenHeader } from '../adminAuth'; |
|
|
|
|
|
interface UseSocketManagerOptions { |
|
|
roomCode: string; |
|
|
clientId: string; |
|
|
} |
|
|
|
|
|
interface UseSocketManagerResult { |
|
|
entries: ClipboardEntryType[]; |
|
|
files: FileEntryType[]; |
|
|
connectedUsers: number; |
|
|
expiresIn: string | null; |
|
|
isLoading: boolean; |
|
|
error: string | null; |
|
|
socketRef: React.MutableRefObject<Socket | null>; |
|
|
addEntry: (content: string) => void; |
|
|
deleteEntry: (entryId: string) => void; |
|
|
clearClipboard: () => void; |
|
|
deleteFile: (fileId: string) => void; |
|
|
} |
|
|
|
|
|
export function useSocketManager({ roomCode, clientId }: UseSocketManagerOptions): UseSocketManagerResult { |
|
|
const [entries, setEntries] = useState<ClipboardEntryType[]>([]); |
|
|
const [files, setFiles] = useState<FileEntryType[]>([]); |
|
|
const [connectedUsers, setConnectedUsers] = useState(1); |
|
|
const [expiresIn, setExpiresIn] = useState<string | null>(null); |
|
|
const [error, setError] = useState<string | null>(null); |
|
|
const [isLoading, setIsLoading] = useState(true); |
|
|
const socketRef = useRef<Socket | null>(null); |
|
|
const dataLoadTimeoutRef = useRef<NodeJS.Timeout | null>(null); |
|
|
|
|
|
|
|
|
const addEntry = (content: string) => { |
|
|
if (socketRef.current) { |
|
|
socketRef.current.emit('addEntry', { |
|
|
roomCode, |
|
|
content, |
|
|
clientId, |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
const deleteEntry = (entryId: string) => { |
|
|
if (socketRef.current) { |
|
|
socketRef.current.emit('deleteEntry', { |
|
|
roomCode, |
|
|
entryId, |
|
|
clientId, |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
const clearClipboard = () => { |
|
|
if (socketRef.current) { |
|
|
socketRef.current.emit('clearClipboard', { |
|
|
roomCode, |
|
|
clientId, |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
const deleteFile = (fileId: string) => { |
|
|
if (socketRef.current) { |
|
|
socketRef.current.emit('deleteFile', { |
|
|
roomCode, |
|
|
fileId, |
|
|
clientId, |
|
|
}); |
|
|
|
|
|
fetch(appendTokenToUrl(`${apiUrl}/clipboard/${roomCode}/files/${fileId}`), { |
|
|
method: 'DELETE', |
|
|
headers: withAdminTokenHeader(), |
|
|
}).catch(err => { |
|
|
console.error('Error deleting file:', err); |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const isValidRoomCode = (code: string): boolean => { |
|
|
const roomCodeRegex = /^[A-Z0-9]{4}$/; |
|
|
return roomCodeRegex.test(code); |
|
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (!isValidRoomCode(roomCode)) { |
|
|
setError(`Invalid clipboard code format: ${roomCode}. Please use a 4-character uppercase alphanumeric code.`); |
|
|
setIsLoading(false); |
|
|
return; |
|
|
} |
|
|
|
|
|
const adminToken = getAdminToken(); |
|
|
|
|
|
if (!adminToken) { |
|
|
setError('Admin token missing. Please refresh and enter your token.'); |
|
|
setIsLoading(false); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
socketRef.current = io(socketUrl, { |
|
|
query: { roomCode, token: adminToken }, |
|
|
extraHeaders: { 'x-admin-token': adminToken }, |
|
|
transports: ['websocket', 'polling'], |
|
|
reconnectionAttempts: 5, |
|
|
reconnectionDelay: 1000, |
|
|
forceNew: true, |
|
|
}); |
|
|
|
|
|
|
|
|
socketRef.current.on('connect', () => { |
|
|
console.log('Connected to WebSocket server'); |
|
|
|
|
|
if (socketRef.current) { |
|
|
socketRef.current.emit('joinRoom', { roomCode, clientId }); |
|
|
} |
|
|
|
|
|
|
|
|
if (dataLoadTimeoutRef.current) clearTimeout(dataLoadTimeoutRef.current); |
|
|
dataLoadTimeoutRef.current = setTimeout(() => { |
|
|
if (isLoading) { |
|
|
console.warn(`Timeout: Did not receive clipboardData for room ${roomCode} in time.`); |
|
|
setError('Failed to load clipboard data in time. Please check your connection or try refreshing.'); |
|
|
setIsLoading(false); |
|
|
} |
|
|
}, 10000); |
|
|
}); |
|
|
|
|
|
socketRef.current.on('connect_error', (error) => { |
|
|
console.error('Connection error:', error); |
|
|
setError('Failed to connect to the server. Please try again later.'); |
|
|
setIsLoading(false); |
|
|
}); |
|
|
|
|
|
socketRef.current.on('disconnect', (reason) => { |
|
|
console.log('Disconnected:', reason); |
|
|
if (reason === 'io server disconnect') { |
|
|
setError('Disconnected from server. The clipboard may not exist.'); |
|
|
setIsLoading(false); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
socketRef.current.on('error', (data: { message: string }) => { |
|
|
console.error('Socket error:', data.message); |
|
|
setError(data.message); |
|
|
setIsLoading(false); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (data.message.toLowerCase().includes('not found') || data.message.toLowerCase().includes('does not exist')) { |
|
|
setTimeout(() => { |
|
|
window.location.href = '/'; |
|
|
}, 3000); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
socketRef.current.on('clipboardData', (data: { entries: ClipboardEntryType[], files?: FileEntryType[], connectedUsers: number, expiresIn?: string }) => { |
|
|
if (dataLoadTimeoutRef.current) clearTimeout(dataLoadTimeoutRef.current); |
|
|
setEntries(data.entries); |
|
|
if (data.files) { |
|
|
setFiles(data.files); |
|
|
} |
|
|
setConnectedUsers(data.connectedUsers); |
|
|
if (data.expiresIn) { |
|
|
setExpiresIn(data.expiresIn); |
|
|
} |
|
|
setIsLoading(false); |
|
|
}); |
|
|
|
|
|
|
|
|
socketRef.current.on('newEntry', (entry: ClipboardEntryType) => { |
|
|
setEntries((prev: ClipboardEntryType[]) => [entry, ...prev]); |
|
|
}); |
|
|
|
|
|
|
|
|
socketRef.current.on('deleteEntry', (entryId: string) => { |
|
|
setEntries((prev: ClipboardEntryType[]) => prev.filter(entry => entry.id !== entryId)); |
|
|
}); |
|
|
|
|
|
|
|
|
socketRef.current.on('userCount', (count: number) => { |
|
|
setConnectedUsers(count); |
|
|
}); |
|
|
|
|
|
|
|
|
socketRef.current.on('expirationUpdate', (time: string) => { |
|
|
setExpiresIn(time); |
|
|
}); |
|
|
|
|
|
|
|
|
socketRef.current.on('fileUploaded', (fileEntry: FileEntryType) => { |
|
|
setFiles((prev: FileEntryType[]) => [fileEntry, ...prev]); |
|
|
}); |
|
|
|
|
|
|
|
|
socketRef.current.on('fileDeleted', (fileId: string) => { |
|
|
setFiles((prev: FileEntryType[]) => prev.filter(file => file.id !== fileId)); |
|
|
}); |
|
|
|
|
|
|
|
|
return () => { |
|
|
console.log(`Cleaning up SocketManager for ${roomCode}`); |
|
|
if (dataLoadTimeoutRef.current) { |
|
|
clearTimeout(dataLoadTimeoutRef.current); |
|
|
} |
|
|
if (socketRef.current) { |
|
|
|
|
|
socketRef.current.off('connect'); |
|
|
socketRef.current.off('connect_error'); |
|
|
socketRef.current.off('disconnect'); |
|
|
socketRef.current.off('error'); |
|
|
socketRef.current.off('clipboardData'); |
|
|
socketRef.current.off('newEntry'); |
|
|
socketRef.current.off('deleteEntry'); |
|
|
socketRef.current.off('userCount'); |
|
|
socketRef.current.off('expirationUpdate'); |
|
|
socketRef.current.off('fileUploaded'); |
|
|
socketRef.current.off('fileDeleted'); |
|
|
|
|
|
|
|
|
if (socketRef.current.connected) { |
|
|
socketRef.current.disconnect(); |
|
|
} |
|
|
socketRef.current = null; |
|
|
} |
|
|
}; |
|
|
}, [roomCode, clientId, isLoading]); |
|
|
|
|
|
return { |
|
|
entries, |
|
|
files, |
|
|
connectedUsers, |
|
|
expiresIn, |
|
|
isLoading, |
|
|
error, |
|
|
socketRef, |
|
|
addEntry, |
|
|
deleteEntry, |
|
|
clearClipboard, |
|
|
deleteFile |
|
|
}; |
|
|
} |
|
|
|