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:
@@ -33,6 +33,8 @@ interface WasteState {
|
||||
// chat — keyed by room
|
||||
messages: Record<string, ChatMessage[]>
|
||||
activeRoom: string
|
||||
// user-created rooms, keyed by networkId
|
||||
customRooms: Record<string, string[]>
|
||||
|
||||
// file listings — keyed by peer id
|
||||
fileLists: Record<string, FileEntry[]>
|
||||
@@ -45,6 +47,12 @@ interface WasteState {
|
||||
|
||||
// file browser state
|
||||
activeFilePeer: string | null
|
||||
// shared files keyed by networkId
|
||||
sharedFilesByNetwork: Record<string, Map<string, File>>
|
||||
// incoming offers awaiting accept/reject: xid → offer info
|
||||
pendingOffers: Record<string, { peerId: string; name: string; size: number }>
|
||||
// active in-progress transfers: xid → progress
|
||||
fileProgress: Record<string, { peerId: string; name: string; received: number; total: number }>
|
||||
|
||||
// actions
|
||||
connect: (url: string) => void
|
||||
@@ -54,9 +62,13 @@ interface WasteState {
|
||||
setActiveNetwork: (id: string) => void
|
||||
setActiveRoom: (room: string) => void
|
||||
setActiveFilePeer: (peerId: string | null) => void
|
||||
setSharedFiles: (files: Map<string, File>) => void
|
||||
setSharedFiles: (files: Map<string, File>, networkId?: string) => void
|
||||
browseFiles: (peerId: string) => void
|
||||
sendFileTo: (peerId: string, file: File) => void
|
||||
acceptOffer: (peerId: string, xid: string, name: string, size: number) => void
|
||||
rejectOffer: (peerId: string, xid: string) => void
|
||||
cancelTransfer: (peerId: string, xid: string, direction: 'recv' | 'send') => void
|
||||
createRoom: (name: string) => void
|
||||
handleEvent: (msg: IpcMessage) => void
|
||||
}
|
||||
|
||||
@@ -72,10 +84,14 @@ export const useWaste = create<WasteState>((set, get) => ({
|
||||
connectedPeers: [],
|
||||
messages: {},
|
||||
activeRoom: 'general',
|
||||
customRooms: {},
|
||||
fileLists: {},
|
||||
exportedBackup: null,
|
||||
peerStatus: {},
|
||||
activeFilePeer: null,
|
||||
sharedFilesByNetwork: {},
|
||||
pendingOffers: {},
|
||||
fileProgress: {},
|
||||
|
||||
connect(url: string) {
|
||||
const adapter = new DaemonAdapter(url)
|
||||
@@ -116,7 +132,10 @@ export const useWaste = create<WasteState>((set, get) => ({
|
||||
set({ activeFilePeer: peerId })
|
||||
},
|
||||
|
||||
setSharedFiles(files) {
|
||||
setSharedFiles(files, networkId) {
|
||||
const netId = networkId ?? get().activeNetworkId ?? ''
|
||||
if (!netId) return
|
||||
set(s => ({ sharedFilesByNetwork: { ...s.sharedFilesByNetwork, [netId]: files } }))
|
||||
const a = get().adapter
|
||||
if (a instanceof BrowserAdapter) a.setSharedFiles(files)
|
||||
},
|
||||
@@ -126,6 +145,44 @@ export const useWaste = create<WasteState>((set, get) => ({
|
||||
if (a instanceof BrowserAdapter) a.sendFileTo(peerId, file)
|
||||
},
|
||||
|
||||
acceptOffer(peerId, xid, name, size) {
|
||||
set(s => {
|
||||
const p = { ...s.pendingOffers }; delete p[xid]
|
||||
return { pendingOffers: p, fileProgress: { ...s.fileProgress, [xid]: { peerId, name, received: 0, total: size } } }
|
||||
})
|
||||
const a = get().adapter
|
||||
if (a instanceof BrowserAdapter) a.acceptOffer(peerId, xid, name, size)
|
||||
},
|
||||
|
||||
rejectOffer(peerId, xid) {
|
||||
set(s => { const p = { ...s.pendingOffers }; delete p[xid]; return { pendingOffers: p } })
|
||||
const a = get().adapter
|
||||
if (a instanceof BrowserAdapter) a.rejectOffer(peerId, xid)
|
||||
},
|
||||
|
||||
cancelTransfer(peerId, xid, direction) {
|
||||
set(s => {
|
||||
const fp = { ...s.fileProgress }; delete fp[xid]
|
||||
return { fileProgress: fp }
|
||||
})
|
||||
const a = get().adapter
|
||||
if (a instanceof BrowserAdapter) a.cancelTransfer(peerId, xid, direction)
|
||||
},
|
||||
|
||||
createRoom(name) {
|
||||
const netId = get().activeNetworkId
|
||||
if (!netId || !name.trim()) return
|
||||
const key = name.trim().toLowerCase().replace(/\s+/g, '-')
|
||||
set(s => {
|
||||
const existing = s.customRooms[netId] ?? []
|
||||
if (existing.includes(key)) return s
|
||||
return {
|
||||
customRooms: { ...s.customRooms, [netId]: [...existing, key] },
|
||||
activeRoom: key,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
browseFiles(peerId) {
|
||||
const a = get().adapter
|
||||
if (a instanceof BrowserAdapter) {
|
||||
@@ -239,6 +296,30 @@ export const useWaste = create<WasteState>((set, get) => ({
|
||||
if (msg.backup) set({ exportedBackup: msg.backup })
|
||||
break
|
||||
}
|
||||
case 'incoming_file': {
|
||||
if (msg.peer_id && msg.offer) {
|
||||
const { xid, name, size } = msg.offer
|
||||
set(s => ({ pendingOffers: { ...s.pendingOffers, [xid]: { peerId: String(msg.peer_id), name, size } } }))
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'file_progress': {
|
||||
if (msg.transfer_id && msg.peer_id) {
|
||||
const xid = msg.transfer_id
|
||||
set(s => ({
|
||||
fileProgress: {
|
||||
...s.fileProgress,
|
||||
[xid]: {
|
||||
peerId: String(msg.peer_id),
|
||||
name: msg.offer?.name ?? xid,
|
||||
received: msg.bytes_received ?? 0,
|
||||
total: msg.total_bytes ?? 0,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'file_list': {
|
||||
if (msg.peer_id && msg.files) {
|
||||
set(s => ({
|
||||
@@ -248,8 +329,10 @@ export const useWaste = create<WasteState>((set, get) => ({
|
||||
break
|
||||
}
|
||||
case 'file_complete': {
|
||||
// path holds the blob URL, offer.name holds the filename
|
||||
if (msg.path && msg.offer?.name) {
|
||||
// clear progress entry
|
||||
const xid = msg.offer.xid
|
||||
set(s => { const fp = { ...s.fileProgress }; delete fp[xid]; return { fileProgress: fp } })
|
||||
const a = document.createElement('a')
|
||||
a.href = msg.path
|
||||
a.download = msg.offer.name
|
||||
|
||||
Reference in New Issue
Block a user