Add rooms, per-network file sharing, and file transfer UX
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>
This commit is contained in:
51
web/src/components/Transfers.tsx
Normal file
51
web/src/components/Transfers.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user