Additional rooms: - Store tracks customRooms per networkId; createRoom action slugifies and deduplicates, switches active room on creation - Sidebar: + button next to Rooms label opens an inline input; Escape dismisses; Enter creates the room Per-network share directories: - Store tracks sharedFilesByNetwork keyed by networkId instead of a single global map; setSharedFiles(files, networkId?) defaults to activeNetworkId - FolderPicker reads per-network map to display accurate file count label File transfer UX: - Incoming file-offer now emits pending_offer instead of auto-accepting; user sees Accept / Reject buttons in the sidebar Transfers section - Progress events emitted per chunk during receive; Transfers panel shows a live progress bar with received/total sizes - file-cancel protocol message: sender or receiver can cancel an in-flight transfer; DataChannel is closed and buffers discarded - PeerConn: acceptOffer(), rejectOffer(), cancelRecv(), cancelSend() - BrowserAdapter: acceptOffer(), rejectOffer(), cancelTransfer() - Store: pendingOffers, fileProgress, acceptOffer, rejectOffer, cancelTransfer - Transfers component renders in sidebar; auto-hides when no activity Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
52 lines
2.1 KiB
TypeScript
52 lines
2.1 KiB
TypeScript
import { useWaste } from '../store'
|
|
|
|
function fmt(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
}
|
|
|
|
export function Transfers() {
|
|
const { pendingOffers, fileProgress, acceptOffer, rejectOffer, cancelTransfer, connectedPeers } = useWaste()
|
|
|
|
const hasPending = Object.keys(pendingOffers).length > 0
|
|
const hasActive = Object.keys(fileProgress).length > 0
|
|
|
|
if (!hasPending && !hasActive) return null
|
|
|
|
function alias(peerId: string) {
|
|
return connectedPeers.find(p => p.id === peerId)?.alias ?? peerId.slice(0, 8)
|
|
}
|
|
|
|
return (
|
|
<div className="sidebar-section">
|
|
<span className="sidebar-label">Transfers</span>
|
|
|
|
{Object.entries(pendingOffers).map(([xid, offer]) => (
|
|
<div key={xid} className="transfer-row">
|
|
<span className="transfer-name" title={offer.name}>{offer.name}</span>
|
|
<span className="transfer-meta">{fmt(offer.size)} from {alias(offer.peerId)}</span>
|
|
<div className="transfer-actions">
|
|
<button className="transfer-btn accept" onClick={() => acceptOffer(offer.peerId, xid, offer.name, offer.size)}>Accept</button>
|
|
<button className="transfer-btn reject" onClick={() => rejectOffer(offer.peerId, xid)}>Reject</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{Object.entries(fileProgress).map(([xid, p]) => {
|
|
const pct = p.total > 0 ? Math.round((p.received / p.total) * 100) : 0
|
|
return (
|
|
<div key={xid} className="transfer-row">
|
|
<span className="transfer-name" title={p.name}>{p.name}</span>
|
|
<span className="transfer-meta">{fmt(p.received)} / {fmt(p.total)} · {alias(p.peerId)}</span>
|
|
<div className="transfer-progress">
|
|
<div className="transfer-progress-bar" style={{ width: `${pct}%` }} />
|
|
</div>
|
|
<button className="transfer-btn reject" onClick={() => cancelTransfer(p.peerId, xid, 'recv')}>Cancel</button>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|